diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index e22a70a..a44dbb9 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -2,20 +2,34 @@ name: Go Tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 'stable' + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" - - name: Test - run: go test -count=1 -v ./... \ No newline at end of file + - name: Test + run: go test -count=1 -v ./... + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + + - name: golangci-lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: v2.1 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b89afc4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,12 @@ +version: "2" +linters: + enable: + - exhaustive + settings: + exhaustive: + check: + - "switch" + - "map" + staticcheck: + checks: + - "-QF1001" # disable "could apply De Morgan's law" diff --git a/cmd/attach/attach.go b/cmd/attach/attach.go index 6f092ec..8bbb409 100644 --- a/cmd/attach/attach.go +++ b/cmd/attach/attach.go @@ -80,7 +80,9 @@ func ReadScreenOverHTTP(ctx context.Context, url string, ch chan<- httpapi.Scree if err != nil { return xerrors.Errorf("failed to do request: %w", err) } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() for ev, err := range sse.Read(res.Body, &sse.ReadConfig{ // 256KB: screen can be big. The default terminal size is 80x1000, @@ -115,7 +117,9 @@ func WriteRawInputOverHTTP(ctx context.Context, url string, msg string) error { if err != nil { return xerrors.Errorf("failed to do request: %w", err) } - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() if res.StatusCode != http.StatusOK { return xerrors.Errorf("failed to write raw input: %w", errors.New(res.Status)) } @@ -132,7 +136,9 @@ func runAttach(remoteUrl string) error { if err != nil { return xerrors.Errorf("failed to make raw: %w", err) } - defer term.Restore(stdin, oldState) + defer func() { + _ = term.Restore(stdin, oldState) + }() stdinWriter := &ChannelWriter{ ch: make(chan []byte, 4096), diff --git a/cmd/server/server.go b/cmd/server/server.go index 5cb479a..84ad594 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "os" + "sort" "strings" "github.com/spf13/cobra" @@ -38,45 +39,31 @@ const ( AgentTypeCustom AgentType = msgfmt.AgentTypeCustom ) +// exhaustiveness of this map is checked by the exhaustive linter +var agentTypeMap = map[AgentType]bool{ + AgentTypeClaude: true, + AgentTypeGoose: true, + AgentTypeAider: true, + AgentTypeCodex: true, + AgentTypeGemini: true, + AgentTypeCustom: true, +} + func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) { - var agentType AgentType - switch agentTypeVar { - case string(AgentTypeClaude): - agentType = AgentTypeClaude - case string(AgentTypeGoose): - agentType = AgentTypeGoose - case string(AgentTypeAider): - agentType = AgentTypeAider - case string(AgentTypeGemini): - agentType = AgentTypeGemini - case string(AgentTypeCustom): - agentType = AgentTypeCustom - case string(AgentTypeCodex): - agentType = AgentTypeCodex - case "": - // do nothing - default: - return "", fmt.Errorf("invalid agent type: %s", agentTypeVar) + // if the agent type is provided, use it + castedAgentType := AgentType(agentTypeVar) + if _, ok := agentTypeMap[castedAgentType]; ok { + return castedAgentType, nil } - if agentType != "" { - return agentType, nil + if agentTypeVar != "" { + return AgentTypeCustom, fmt.Errorf("invalid agent type: %s", agentTypeVar) } - - switch firstArg { - case string(AgentTypeClaude): - agentType = AgentTypeClaude - case string(AgentTypeGoose): - agentType = AgentTypeGoose - case string(AgentTypeAider): - agentType = AgentTypeAider - case string(AgentTypeCodex): - agentType = AgentTypeCodex - case string(AgentTypeGemini): - agentType = AgentTypeGemini - default: - agentType = AgentTypeCustom + // if the agent type is not provided, guess it from the first argument + castedFirstArg := AgentType(firstArg) + if _, ok := agentTypeMap[castedFirstArg]; ok { + return castedFirstArg, nil } - return agentType, nil + return AgentTypeCustom, nil } func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) error { @@ -139,10 +126,19 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er return nil } +var agentNames = (func() []string { + names := make([]string, 0, len(agentTypeMap)) + for agentType := range agentTypeMap { + names = append(names, string(agentType)) + } + sort.Strings(names) + return names +})() + var ServerCmd = &cobra.Command{ Use: "server [agent]", Short: "Run the server", - Long: `Run the server with the specified agent (claude, goose, aider, gemini, codex)`, + Long: fmt.Sprintf("Run the server with the specified agent (one of: %s)", strings.Join(agentNames, ", ")), Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) @@ -155,7 +151,7 @@ var ServerCmd = &cobra.Command{ } func init() { - ServerCmd.Flags().StringVarP(&agentTypeVar, "type", "t", "", "Override the agent type (one of: claude, goose, aider, custom)") + ServerCmd.Flags().StringVarP(&agentTypeVar, "type", "t", "", fmt.Sprintf("Override the agent type (one of: %s, custom)", strings.Join(agentNames, ", "))) ServerCmd.Flags().IntVarP(&port, "port", "p", 3284, "Port to run the server on") ServerCmd.Flags().BoolVarP(&printOpenAPI, "print-openapi", "P", false, "Print the OpenAPI schema to stdout and exit") ServerCmd.Flags().StringVarP(&chatBasePath, "chat-base-path", "c", "/chat", "Base path for assets and routes used in the static files of the chat interface") diff --git a/lib/httpapi/embed.go b/lib/httpapi/embed.go index f21eeda..25cb91b 100644 --- a/lib/httpapi/embed.go +++ b/lib/httpapi/embed.go @@ -87,7 +87,9 @@ func FileServerWithIndexFallback(chatBasePath string) http.Handler { // Try to serve the file directly f, err := chatFS.Open(trimmedPath) if err == nil { - defer f.Close() + defer func() { + _ = f.Close() + }() fileServer.ServeHTTP(w, r) return } diff --git a/lib/httpapi/server_test.go b/lib/httpapi/server_test.go index 083e33b..456235a 100644 --- a/lib/httpapi/server_test.go +++ b/lib/httpapi/server_test.go @@ -55,7 +55,9 @@ func TestOpenAPISchema(t *testing.T) { if err != nil { t.Fatalf("failed to open disk schema: %s", err) } - defer diskSchemaFile.Close() + defer func() { + _ = diskSchemaFile.Close() + }() diskSchemaBytes, err := io.ReadAll(diskSchemaFile) if err != nil { diff --git a/lib/screentracker/conversation_test.go b/lib/screentracker/conversation_test.go index 1b797aa..61b28df 100644 --- a/lib/screentracker/conversation_test.go +++ b/lib/screentracker/conversation_test.go @@ -186,7 +186,7 @@ func TestMessages(t *testing.T) { assert.Equal(t, []st.ConversationMessage{ agentMsg(0, "1"), }, msgs) - nowWrapper.Time = nowWrapper.Time.Add(1 * time.Second) + nowWrapper.Time = nowWrapper.Add(1 * time.Second) c.AddSnapshot("1") assert.Equal(t, msgs, c.Messages()) })