diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2439f96..32f2d27d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,20 @@ permissions: contents: read jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.12.2 + unit-tests: runs-on: ubuntu-latest steps: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..c47ee3b8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,71 @@ +version: "2" + +run: + timeout: 5m + +linters: + # Enables the standard set: errcheck, gosimple, govet, ineffassign, staticcheck, unused + default: standard + enable: + - bodyclose # HTTP response bodies are closed + - gocritic # broad style and correctness checks + - misspell # catches common spelling mistakes in comments and strings + - nolintlint # ensures //nolint directives are valid and specific + - unconvert # removes unnecessary type conversions + - unparam # detects function params that always receive the same value + disable: + # errcheck: 312 existing violations — deferred to a dedicated cleanup PR + - errcheck + # revive: 578 existing violations, mostly missing GoDoc on exported symbols — deferred to a dedicated cleanup PR + - revive + + settings: + gocritic: + enabled-tags: + - diagnostic + - style + - performance + disabled-checks: + # CLI performance is not sensitive enough to warrant changing all option structs to pointers + - hugeParam + # Same rationale: range-copy cost is negligible in a CLI + - rangeValCopy + + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + + exclusions: + rules: + # Test files get more latitude — errcheck and unparam noise is expected there + - path: _test\.go + linters: + - gocritic + - unparam + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/cmd/gen-manpages/main.go b/cmd/gen-manpages/main.go index 0ba998b0..96c14d84 100644 --- a/cmd/gen-manpages/main.go +++ b/cmd/gen-manpages/main.go @@ -39,7 +39,7 @@ func main() { } // Ensure output directory exists - if err := os.MkdirAll(*output, 0755); err != nil { + if err := os.MkdirAll(*output, 0o755); err != nil { fmt.Fprintf(os.Stderr, "Error: Could not create output directory: %s\n", err) os.Exit(1) } @@ -70,11 +70,12 @@ func main() { // List generated files for verbose if *verbose { files, err := filepath.Glob(filepath.Join(*output, "*.1")) - if err != nil { + switch { + case err != nil: fmt.Fprintf(os.Stderr, "Warning: Failed to list generated files: %s\n", err) - } else if len(files) == 0 { + case len(files) == 0: fmt.Printf("No man pages found in output directory: %s\n", *output) - } else { + default: fmt.Printf("Generated %d man pages:\n", len(files)) for _, file := range files { fmt.Printf(" - %s\n", filepath.Base(file)) diff --git a/internal/pkg/cli/command/auth/clear.go b/internal/pkg/cli/command/auth/clear.go index 2461a843..99b9b2b4 100644 --- a/internal/pkg/cli/command/auth/clear.go +++ b/internal/pkg/cli/command/auth/clear.go @@ -49,19 +49,20 @@ func NewClearCmd() *cobra.Command { } // After clearing things, we need to resolve whether the user is still authenticated - if secrets.DefaultAPIKey.Get() != "" { + switch { + case secrets.DefaultAPIKey.Get() != "": state.AuthedUser.Update(func(u *state.TargetUser) { u.AuthContext = state.AuthDefaultAPIKey }) - } else if secrets.ClientId.Get() != "" && secrets.ClientSecret.Get() != "" { + case secrets.ClientId.Get() != "" && secrets.ClientSecret.Get() != "": state.AuthedUser.Update(func(u *state.TargetUser) { u.AuthContext = state.AuthServiceAccount }) - } else if secrets.GetOAuth2Token().AccessToken != "" { + case secrets.GetOAuth2Token().AccessToken != "": state.AuthedUser.Update(func(u *state.TargetUser) { u.AuthContext = state.AuthUserToken }) - } else { + default: state.AuthedUser.Update(func(u *state.TargetUser) { u.AuthContext = state.AuthNone }) diff --git a/internal/pkg/cli/command/config/config_test.go b/internal/pkg/cli/command/config/config_test.go index 3d581c3e..f3e3da19 100644 --- a/internal/pkg/cli/command/config/config_test.go +++ b/internal/pkg/cli/command/config/config_test.go @@ -41,12 +41,12 @@ type mockConfigService struct { lastDescribeKey string } -func (m *mockConfigService) Get(key string) (string, bool, string, bool, error) { +func (m *mockConfigService) Get(key string) (value string, sensitive bool, envVarName string, envVarOverride bool, err error) { m.lastGetKey = key return m.getValue, m.getSensitive, m.getEnvVarName, m.getEnvVarOverride, m.getErr } -func (m *mockConfigService) GetStored(key string) (string, bool, error) { +func (m *mockConfigService) GetStored(key string) (value string, sensitive bool, err error) { m.lastGetStoredKey = key if m.getStoredOverride { return m.getStoredValue, m.getStoredSensitive, m.getStoredErr diff --git a/internal/pkg/cli/command/config/list.go b/internal/pkg/cli/command/config/list.go index b8196254..55c1c7bd 100644 --- a/internal/pkg/cli/command/config/list.go +++ b/internal/pkg/cli/command/config/list.go @@ -4,9 +4,7 @@ import ( "fmt" "os" - "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" - "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" @@ -33,10 +31,7 @@ func NewListCmd() *cobra.Command { Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { svc := newDefaultConfigService() - if err := runListCmd(svc, options); err != nil { - msg.FailJSON(options.json, "%s", err) - exit.ErrorMsg(err.Error()) - } + runListCmd(svc, options) }, } @@ -47,7 +42,7 @@ func NewListCmd() *cobra.Command { return cmd } -func runListCmd(svc ConfigService, opts ListCmdOptions) error { +func runListCmd(svc ConfigService, opts ListCmdOptions) { // --json output for the list command type listOutput struct { Key string `json:"key"` @@ -74,7 +69,7 @@ func runListCmd(svc ConfigService, opts ListCmdOptions) error { jsonEntries = append(jsonEntries, entry) } fmt.Fprintln(os.Stdout, text.IndentJSON(jsonEntries)) - return nil + return } w := presenters.NewTabWriter() @@ -102,5 +97,4 @@ func runListCmd(svc ConfigService, opts ListCmdOptions) error { e.Description) } w.Flush() - return nil } diff --git a/internal/pkg/cli/command/config/list_test.go b/internal/pkg/cli/command/config/list_test.go index 4475be47..e9f937fa 100644 --- a/internal/pkg/cli/command/config/list_test.go +++ b/internal/pkg/cli/command/config/list_test.go @@ -11,8 +11,7 @@ func Test_runListCmd_TabularOutputIncludesHeader(t *testing.T) { svc := &mockConfigService{listResult: []ConfigEntry{}} out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{}) }) assert.Contains(t, out, "KEY") @@ -30,8 +29,7 @@ func Test_runListCmd_TabularOutputMasksSensitiveKey(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{}) }) assert.Contains(t, out, "api-key") @@ -46,8 +44,7 @@ func Test_runListCmd_TabularOutputRevealsSensitiveKey(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{reveal: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{reveal: true}) }) assert.Contains(t, out, "sk-supersecret") @@ -62,8 +59,7 @@ func Test_runListCmd_JSONOutput(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{json: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{json: true}) }) // Sensitive key should be masked in JSON output @@ -83,8 +79,7 @@ func Test_runListCmd_AllFlagIncludesHiddenKeys(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{all: true}) }) assert.Contains(t, out, "environment") @@ -99,8 +94,7 @@ func Test_runListCmd_JSONAllFlagIncludesHiddenField(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{json: true, all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{json: true, all: true}) }) assert.Contains(t, out, "environment") @@ -117,8 +111,7 @@ func Test_runListCmd_JSONOutputRevealsSensitiveKey(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{json: true, reveal: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{json: true, reveal: true}) }) assert.Contains(t, out, "sk-supersecret") @@ -132,8 +125,7 @@ func Test_runListCmd_TabularOutputAnnotatesActiveEnvVarOverride(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{all: true}) }) assert.Contains(t, out, "staging") @@ -148,8 +140,7 @@ func Test_runListCmd_TabularOutputNoAnnotationWithoutOverride(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{all: true}) }) assert.NotContains(t, out, "[$PINECONE_ENVIRONMENT]") @@ -164,8 +155,7 @@ func Test_runListCmd_JSONOutputIncludesEnvVarFieldsWhenBound(t *testing.T) { } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{json: true, all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{json: true, all: true}) }) assert.Contains(t, out, `"PINECONE_ENVIRONMENT"`) @@ -182,8 +172,7 @@ func Test_runListCmd_JSONOutputEnvVarOverrideIsFalseWhenNotActive(t *testing.T) } out := testutils.CaptureStdout(t, func() { - err := runListCmd(svc, ListCmdOptions{json: true, all: true}) - assert.NoError(t, err) + runListCmd(svc, ListCmdOptions{json: true, all: true}) }) assert.Contains(t, out, `"PINECONE_ENVIRONMENT"`) diff --git a/internal/pkg/cli/command/config/registry.go b/internal/pkg/cli/command/config/registry.go index ce6c23e0..6746cd92 100644 --- a/internal/pkg/cli/command/config/registry.go +++ b/internal/pkg/cli/command/config/registry.go @@ -296,7 +296,7 @@ func (s *defaultConfigService) Get(key string) (value string, sensitive bool, en return desc.getStr(), desc.Sensitive, "", false, nil } -func (s *defaultConfigService) GetStored(key string) (string, bool, error) { +func (s *defaultConfigService) GetStored(key string) (value string, sensitive bool, err error) { desc, err := lookupKey(key) if err != nil { return "", false, err diff --git a/internal/pkg/cli/command/index/create.go b/internal/pkg/cli/command/index/create.go index d39ca05d..5d8d5e69 100644 --- a/internal/pkg/cli/command/index/create.go +++ b/internal/pkg/cli/command/index/create.go @@ -221,7 +221,7 @@ func runCreateIndexCmd(ctx context.Context, cmd *cobra.Command, service CreateIn idx, err = service.CreateServerlessIndex(ctx, &args) if err != nil { - wrapped := fmt.Errorf("Failed to create serverless index %s: %w", style.Emphasis(options.name), err) + wrapped := fmt.Errorf("failed to create serverless index %s: %w", style.Emphasis(options.name), err) return nil, wrapped } case indexTypePod: @@ -249,7 +249,7 @@ func runCreateIndexCmd(ctx context.Context, cmd *cobra.Command, service CreateIn idx, err = service.CreatePodIndex(ctx, &args) if err != nil { - wrapped := fmt.Errorf("Failed to create pod index %s: %w", style.Emphasis(options.name), err) + wrapped := fmt.Errorf("failed to create pod index %s: %w", style.Emphasis(options.name), err) return nil, wrapped } case indexTypeIntegrated: @@ -290,7 +290,7 @@ func runCreateIndexCmd(ctx context.Context, cmd *cobra.Command, service CreateIn idx, err = service.CreateIndexForModel(ctx, &args) if err != nil { - wrapped := fmt.Errorf("Failed to create integrated index %s: %w", style.Emphasis(options.name), err) + wrapped := fmt.Errorf("failed to create integrated index %s: %w", style.Emphasis(options.name), err) return nil, wrapped } case indexTypeBYOC: @@ -307,11 +307,11 @@ func runCreateIndexCmd(ctx context.Context, cmd *cobra.Command, service CreateIn idx, err = service.CreateBYOCIndex(ctx, &args) if err != nil { - wrapped := fmt.Errorf("Failed to create BYOC index %s: %w", style.Emphasis(options.name), err) + wrapped := fmt.Errorf("failed to create BYOC index %s: %w", style.Emphasis(options.name), err) return nil, wrapped } default: - err := fmt.Errorf("Error creating index: invalid index type") + err := fmt.Errorf("error creating index: invalid index type") return nil, err } @@ -411,10 +411,8 @@ func buildReadCapacityFromFlags(cmd *cobra.Command, mode, nodeType string, shard default: return nil, fmt.Errorf("invalid read-mode") } - } else { // read-mode not provided, return nil if no specific configuration values are passed - if !nodeSet && !shardsSet && !replSet { - return nil, nil - } + } else if !nodeSet && !shardsSet && !replSet { // read-mode not provided, return nil if no specific configuration values are passed + return nil, nil } // dedicated mode if ondemand mode was not requested diff --git a/internal/pkg/cli/command/index/create_test.go b/internal/pkg/cli/command/index/create_test.go index ce4ca194..4642b048 100644 --- a/internal/pkg/cli/command/index/create_test.go +++ b/internal/pkg/cli/command/index/create_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/pinecone-io/go-pinecone/v5/pinecone" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -334,7 +333,7 @@ func Test_createIndexOptions_validate(t *testing.T) { } func Test_buildReadCapacityFromFlags(t *testing.T) { - newCmd := func() *cobra.Command { return NewCreateIndexCmd() } + newCmd := NewCreateIndexCmd t.Run("no flags set returns nil", func(t *testing.T) { cmd := newCmd() diff --git a/internal/pkg/cli/command/index/import/import_test.go b/internal/pkg/cli/command/index/import/import_test.go index 45081a6c..76277998 100644 --- a/internal/pkg/cli/command/index/import/import_test.go +++ b/internal/pkg/cli/command/index/import/import_test.go @@ -25,7 +25,7 @@ type mockImportService struct { cancelImportErr error } -func (m *mockImportService) StartImport(ctx context.Context, uri string, integrationId *string, errorMode *string) (*pinecone.StartImportResponse, error) { +func (m *mockImportService) StartImport(ctx context.Context, uri string, integrationId, errorMode *string) (*pinecone.StartImportResponse, error) { m.lastStartImportUri = uri m.lastStartImportIntegrationId = integrationId m.lastStartImportErrorMode = errorMode diff --git a/internal/pkg/cli/command/index/vector/upsert.go b/internal/pkg/cli/command/index/vector/upsert.go index 8b95c34b..eb0fb045 100644 --- a/internal/pkg/cli/command/index/vector/upsert.go +++ b/internal/pkg/cli/command/index/vector/upsert.go @@ -136,8 +136,7 @@ func runUpsertCmd(ctx context.Context, options upsertCmdOptions) { exit.Errorf(err, "Failed to upsert %d vectors in batch %d", len(batch), i+1) } else { if options.json { - json := text.IndentJSON(resp) - fmt.Fprintln(os.Stdout, json) + fmt.Fprintln(os.Stdout, text.IndentJSON(resp)) } else { msg.SuccessMsg("Upserted %d vectors into namespace %s (batch %d of %d)", len(batch), options.namespace, i+1, len(batches)) } diff --git a/internal/pkg/cli/command/organization/delete.go b/internal/pkg/cli/command/organization/delete.go index d4a3b8c4..809241b4 100644 --- a/internal/pkg/cli/command/organization/delete.go +++ b/internal/pkg/cli/command/organization/delete.go @@ -99,7 +99,7 @@ func runDeleteOrganizationCmd(ctx context.Context, svc DeleteOrganizationService return nil } -func confirmDelete(organizationName string, organizationID string) { +func confirmDelete(organizationName, organizationID string) { msg.WarnMsg("This will delete the organization %s (ID: %s).", style.Emphasis(organizationName), style.Emphasis(organizationID)) msg.WarnMsg("This action cannot be undone.") diff --git a/internal/pkg/cli/command/project/delete.go b/internal/pkg/cli/command/project/delete.go index ed41c143..22d7c055 100644 --- a/internal/pkg/cli/command/project/delete.go +++ b/internal/pkg/cli/command/project/delete.go @@ -139,7 +139,7 @@ func confirmDelete(projectName string) { } } -func verifyNoIndexes(ctx context.Context, projectId string, projectName string, jsonOutput bool) { +func verifyNoIndexes(ctx context.Context, projectId, projectName string, jsonOutput bool) { // Check if project contains indexes pc := sdk.NewPineconeClientForProjectById(ctx, projectId) @@ -155,7 +155,7 @@ func verifyNoIndexes(ctx context.Context, projectId string, projectName string, } } -func verifyNoCollections(ctx context.Context, projectId string, projectName string, jsonOutput bool) { +func verifyNoCollections(ctx context.Context, projectId, projectName string, jsonOutput bool) { // Check if project contains collections pc := sdk.NewPineconeClientForProjectById(ctx, projectId) diff --git a/internal/pkg/cli/command/root/root.go b/internal/pkg/cli/command/root/root.go index 481bb647..ecce3ca6 100644 --- a/internal/pkg/cli/command/root/root.go +++ b/internal/pkg/cli/command/root/root.go @@ -66,11 +66,11 @@ type GlobalOptions struct { } func Execute() { - //Base context: cancel on SIGINT / SIGTERM + // Base context: cancel on SIGINT / SIGTERM ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer cancel() err := rootCmd.ExecuteContext(ctx) + cancel() if err != nil { os.Exit(1) } diff --git a/internal/pkg/cli/command/target/target.go b/internal/pkg/cli/command/target/target.go index bdb02044..6012b116 100644 --- a/internal/pkg/cli/command/target/target.go +++ b/internal/pkg/cli/command/target/target.go @@ -470,18 +470,19 @@ func postLoginInteractiveTargetOrg(orgsList []*pinecone.Organization, jsonOutput func postLoginInteractiveTargetProject(projectList []*pinecone.Project, jsonOutput bool) *pinecone.Project { var project *pinecone.Project - if len(projectList) < 1 { + switch { + case len(projectList) < 1: log.Debug().Msg("No projects available for organization. Please create a project before proceeding.") exit.ErrorMsg("No projects found. Please create a project before proceeding.") return nil - } else if len(projectList) == 1 { + case len(projectList) == 1: project = projectList[0] state.TargetProj.Set(state.TargetProject{ Name: project.Name, Id: project.Id, }) return project - } else { + default: projectItems := []string{} for _, proj := range projectList { projectItems = append(projectItems, proj.Name) @@ -504,7 +505,7 @@ func postLoginInteractiveTargetProject(projectList []*pinecone.Project, jsonOutp } func uiProjectSelector(projectItems []string, jsonOutput bool) string { - var targetProject string = "" + var targetProject = "" m2 := prompt.NewList(projectItems, len(projectItems)+6, "Choose a project to target", func() { msg.InfoMsg("Exiting without targeting a project.") msg.HintMsg("You can always run %s to set or change a project context later.", style.Code("pc target")) diff --git a/internal/pkg/utils/argio/argio_test.go b/internal/pkg/utils/argio/argio_test.go index 29b257ed..85901f06 100644 --- a/internal/pkg/utils/argio/argio_test.go +++ b/internal/pkg/utils/argio/argio_test.go @@ -58,7 +58,7 @@ func Test_OpenReader_StdinDash(t *testing.T) { go func() { defer pw.Close() - if _, err := pw.Write([]byte(`{"stdin":true}`)); err != nil { + if _, err := pw.WriteString(`{"stdin":true}`); err != nil { panic(err) } }() @@ -113,7 +113,7 @@ func Test_OpenReader_StdinOnlyOnce(t *testing.T) { go func() { defer pw.Close() - if _, err := pw.Write([]byte(`{"stdin":true}`)); err != nil { + if _, err := pw.WriteString(`{"stdin":true}`); err != nil { panic(err) } }() diff --git a/internal/pkg/utils/configuration/config/config.go b/internal/pkg/utils/configuration/config/config.go index 43d7db60..d20cb559 100644 --- a/internal/pkg/utils/configuration/config/config.go +++ b/internal/pkg/utils/configuration/config/config.go @@ -59,7 +59,7 @@ func validateEnvironment(env string) error { } quotedEnvs := make([]string, len(validEnvs)) for i, validEnv := range validEnvs { - quotedEnvs[i] = fmt.Sprintf("\"%s\"", validEnv) + quotedEnvs[i] = fmt.Sprintf("%q", validEnv) } return fmt.Errorf("invalid environment: \"%s\", must be one of %s", env, strings.Join(quotedEnvs, ", ")) } diff --git a/internal/pkg/utils/configuration/files.go b/internal/pkg/utils/configuration/files.go index ef947a48..3ff19675 100644 --- a/internal/pkg/utils/configuration/files.go +++ b/internal/pkg/utils/configuration/files.go @@ -35,7 +35,7 @@ func (c ConfigFile) Init() { // Set permissions on config file if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { path := filepath.Join(locations.ConfigPath, fmt.Sprintf("%s.%s", c.FileName, c.FileFormat)) - os.Chmod(path, 0600) + os.Chmod(path, 0o600) } c.LoadConfig() @@ -45,7 +45,7 @@ func (c ConfigFile) Clear() { log.Debug().Str("file_name", c.FileName).Msg("Clearing config file") for _, property := range c.Properties { log.Debug(). - Str("property", fmt.Sprintf("%s", reflect.TypeOf(property))). + Str("property", reflect.TypeOf(property).String()). Msg("Clearing property") property.Clear() } diff --git a/internal/pkg/utils/configuration/locations.go b/internal/pkg/utils/configuration/locations.go index c5c700d8..4d500ff7 100644 --- a/internal/pkg/utils/configuration/locations.go +++ b/internal/pkg/utils/configuration/locations.go @@ -34,7 +34,7 @@ func ensureConfigDir() string { } if !doesFileExist(configPath) { - err = os.MkdirAll(configPath, 0755) + err = os.MkdirAll(configPath, 0o755) if err != nil { exit.Error(err, "Error creating config directory") } diff --git a/internal/pkg/utils/help/content_test.go b/internal/pkg/utils/help/content_test.go index 2369707b..8d6e8e08 100644 --- a/internal/pkg/utils/help/content_test.go +++ b/internal/pkg/utils/help/content_test.go @@ -1,19 +1,12 @@ package help import ( - "regexp" "strings" "testing" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/config" ) -var ( - ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*m") -) - -func stripANSI(s string) string { return ansiRegex.ReplaceAllString(s, "") } - // setColor allows disabling color output for tests // it handles restoring configuration to the previous value func setColor(t *testing.T, on bool) { @@ -65,10 +58,7 @@ func TestExamples_TrimsRightSpacesTabsCRLF(t *testing.T) { in := "pc one \t\r\npc two\t \r\n" got := Examples(in) - want := strings.Join([]string{ - " $ pc one", - " $ pc two", - }, "\n") + want := " $ pc one" + "\n" + " $ pc two" if got != want { t.Fatalf("Mismatch: want %q, got %q", want, got) diff --git a/internal/pkg/utils/help/render.go b/internal/pkg/utils/help/render.go index a53d18c8..6b64eda5 100644 --- a/internal/pkg/utils/help/render.go +++ b/internal/pkg/utils/help/render.go @@ -137,7 +137,7 @@ func isListLine(s string) bool { } func isListBlock(lines []string) bool { - any := false + hasAny := false for _, ln := range lines { if ln == "" { continue @@ -145,9 +145,9 @@ func isListBlock(lines []string) bool { if !isListLine(ln) { return false } - any = true + hasAny = true } - return any + return hasAny } func isHeadingLine(s string) bool { @@ -213,12 +213,12 @@ func resolveWrapWidth() int { return defaultWidth } -func clamp(v, min, max int) int { - if v < min { - return min +func clamp(v, lo, hi int) int { + if v < lo { + return lo } - if v > max { - return max + if v > hi { + return hi } return v } diff --git a/internal/pkg/utils/login/login.go b/internal/pkg/utils/login/login.go index 2869d4e8..9d93669f 100644 --- a/internal/pkg/utils/login/login.go +++ b/internal/pkg/utils/login/login.go @@ -396,8 +396,8 @@ func pollForResult(sessionId string, createdAt time.Time, wait bool, ssoConnecti if result == nil { continue // daemon not done yet } - defer CleanupSession(sessionId) if result.Status == "error" { + CleanupSession(sessionId) return fmt.Errorf("authentication failed: %s", result.Error) } // The daemon wrote the token to secrets.yaml from a separate process. @@ -405,9 +405,12 @@ func pollForResult(sessionId string, createdAt time.Time, wait bool, ssoConnecti _ = secrets.SecretsViper.ReadInConfig() if wait { // Caller handles post-auth state and output. + CleanupSession(sessionId) return nil } - return finishAuthWithSSO(context.Background(), sessionId, ssoConnection) + err = finishAuthWithSSO(context.Background(), sessionId, ssoConnection) + CleanupSession(sessionId) + return err } } } @@ -428,7 +431,7 @@ func printPendingJSON(authURL, sessionId string) { // getAndSetAccessTokenInteractive is the original interactive path: inline callback server, // optional [Enter]-to-open-browser prompt when stdin is a TTY. -func getAndSetAccessTokenInteractive(ctx context.Context, orgId *string, ssoConnection *string) error { +func getAndSetAccessTokenInteractive(ctx context.Context, orgId, ssoConnection *string) error { // If a daemon from a prior JSON-mode login exists, check whether it has // already finished before deciding whether to block interactive login. sess, result, err := findResumableSession() @@ -665,9 +668,9 @@ func ServeAuthCodeListener(ctx context.Context, csrfState string) (string, error mux := http.NewServeMux() mux.HandleFunc("/auth-callback", func(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") + stateParam := r.URL.Query().Get("state") - if state != csrfState { + if stateParam != csrfState { errCh <- fmt.Errorf("state mismatch on authentication") return } @@ -779,18 +782,18 @@ func EnsureAuthenticated(ctx context.Context) error { // Token was expired and there's no pending session to fall back to. return err } - return fmt.Errorf("not authenticated. Run %s to log in.", style.Code("pc login")) + return fmt.Errorf("not authenticated. Run %s to log in", style.Code("pc login")) } if result == nil { // Daemon still running — auth not yet complete. - return fmt.Errorf("authentication in progress. Visit the following URL to complete login, then retry:\n\n %s\n\nOr run %s to check status.", sess.AuthURL, style.Code("pc login -j")) + return fmt.Errorf("authentication in progress. Visit the following URL to complete login, then retry:\n\n %s\n\nOr run %s to check status", sess.AuthURL, style.Code("pc login -j")) } // Daemon finished. if result.Status == "error" { defer CleanupSession(sess.SessionId) - return fmt.Errorf("authentication failed: %s. Run %s to try again.", result.Error, style.Code("pc login")) + return fmt.Errorf("authentication failed: %s. Run %s to try again", result.Error, style.Code("pc login")) } // Reload credentials so we can check SSO enforcement before finalising. @@ -807,7 +810,7 @@ func EnsureAuthenticated(ctx context.Context) error { if token != nil && token.AccessToken != "" { if claims, claimsErr := oauth.ParseClaimsUnverified(token); claimsErr == nil { if ResolveSSOConnection(ctx, claims.OrgId) != nil { - return fmt.Errorf("SSO authentication is required for this organization. Run %s to complete authentication.", style.Code("pc login")) + return fmt.Errorf("SSO authentication is required for this organization. Run %s to complete authentication", style.Code("pc login")) } } } diff --git a/internal/pkg/utils/login/session.go b/internal/pkg/utils/login/session.go index a0486867..292bad2b 100644 --- a/internal/pkg/utils/login/session.go +++ b/internal/pkg/utils/login/session.go @@ -35,7 +35,7 @@ const sessionMaxAge = 5 * time.Minute func sessionsDir() (string, error) { dir := filepath.Join(configuration.ConfigDirPath(), "sessions") - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0o700); err != nil { return "", fmt.Errorf("error creating sessions directory: %w", err) } return dir, nil @@ -66,7 +66,7 @@ func writeSessionState(s SessionState) error { if err != nil { return fmt.Errorf("error marshaling session state: %w", err) } - return os.WriteFile(path, data, 0600) + return os.WriteFile(path, data, 0o600) } func ReadSessionState(sessionId string) (*SessionState, error) { @@ -106,7 +106,7 @@ func WriteSessionResult(r SessionResult) error { os.Remove(tmpPath) return fmt.Errorf("error writing temp result file: %w", err) } - if err := tmp.Chmod(0600); err != nil { + if err := tmp.Chmod(0o600); err != nil { tmp.Close() os.Remove(tmpPath) return fmt.Errorf("error setting permissions on temp result file: %w", err) diff --git a/internal/pkg/utils/login/sso.go b/internal/pkg/utils/login/sso.go index 80ce16cb..bc324e94 100644 --- a/internal/pkg/utils/login/sso.go +++ b/internal/pkg/utils/login/sso.go @@ -58,9 +58,9 @@ func FetchSSOConnection(ctx context.Context, orgId string) (string, error) { // fetchSSOConnectionFromURL is the testable core: it takes an explicit HTTP // client and dashboard base URL so tests can inject a local httptest.Server. -func fetchSSOConnectionFromURL(ctx context.Context, orgId string, accessToken string, client *http.Client, dashboardURL string) (string, error) { +func fetchSSOConnectionFromURL(ctx context.Context, orgId, accessToken string, client *http.Client, dashboardURL string) (string, error) { url := dashboardURL + "/v2/dashboard/organizations" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return "", nil } diff --git a/internal/pkg/utils/manpage/manpage.go b/internal/pkg/utils/manpage/manpage.go index f235de62..c7dd45a6 100644 --- a/internal/pkg/utils/manpage/manpage.go +++ b/internal/pkg/utils/manpage/manpage.go @@ -10,7 +10,7 @@ import ( func GenerateManPages(rootCmd *cobra.Command, outputDir string) error { // Ensure output directory exists - if err := os.MkdirAll(outputDir, 0755); err != nil { + if err := os.MkdirAll(outputDir, 0o755); err != nil { return err } diff --git a/internal/pkg/utils/oauth/auth.go b/internal/pkg/utils/oauth/auth.go index fea55a65..713d6d05 100644 --- a/internal/pkg/utils/oauth/auth.go +++ b/internal/pkg/utils/oauth/auth.go @@ -15,7 +15,7 @@ const ( SourceTag = "pinecone_cli" ) -func (a *Auth) GetAuthURL(ctx context.Context, csrfState string, codeChallenge string, orgId *string, ssoConnection *string) (string, error) { +func (a *Auth) GetAuthURL(ctx context.Context, csrfState, codeChallenge string, orgId, ssoConnection *string) (string, error) { conf, err := newOauth2Config() if err != nil { return "", err @@ -27,10 +27,12 @@ func (a *Auth) GetAuthURL(ctx context.Context, csrfState string, codeChallenge s } opts := []oauth2.AuthCodeOption{} - opts = append(opts, oauth2.SetAuthURLParam("audience", audience)) - opts = append(opts, oauth2.SetAuthURLParam("code_challenge", codeChallenge)) - opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", "S256")) - opts = append(opts, oauth2.SetAuthURLParam("sourceTag", SourceTag)) + opts = append(opts, + oauth2.SetAuthURLParam("audience", audience), + oauth2.SetAuthURLParam("code_challenge", codeChallenge), + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("sourceTag", SourceTag), + ) if orgId != nil && *orgId != "" { opts = append(opts, oauth2.SetAuthURLParam("orgId", *orgId)) } @@ -41,7 +43,7 @@ func (a *Auth) GetAuthURL(ctx context.Context, csrfState string, codeChallenge s return conf.AuthCodeURL(csrfState, opts...), nil } -func (a *Auth) ExchangeAuthCode(ctx context.Context, codeVerifier string, authCode string) (*oauth2.Token, error) { +func (a *Auth) ExchangeAuthCode(ctx context.Context, codeVerifier, authCode string) (*oauth2.Token, error) { conf, err := newOauth2Config() if err != nil { return nil, err @@ -58,13 +60,13 @@ func (a *Auth) ExchangeAuthCode(ctx context.Context, codeVerifier string, authCo return token, nil } -func (a *Auth) CreateNewVerifierAndChallenge() (string, string, error) { +func (a *Auth) CreateNewVerifierAndChallenge() (verifier, challenge string, err error) { bytes := make([]byte, 32) - if _, err := rand.Read(bytes); err != nil { + if _, err = rand.Read(bytes); err != nil { return "", "", err } - verifier := base64.RawURLEncoding.EncodeToString(bytes) + verifier = base64.RawURLEncoding.EncodeToString(bytes) hash := sha256.Sum256([]byte(verifier)) - challenge := base64.RawURLEncoding.EncodeToString(hash[:]) + challenge = base64.RawURLEncoding.EncodeToString(hash[:]) return verifier, challenge, nil } diff --git a/internal/pkg/utils/oauth/error.go b/internal/pkg/utils/oauth/error.go index 112557ce..752f5658 100644 --- a/internal/pkg/utils/oauth/error.go +++ b/internal/pkg/utils/oauth/error.go @@ -74,13 +74,14 @@ func classifyTokenErrorKind(err *TokenError) TokenErrorKind { return TokenErrInvalidScope default: // Unknown oauth error code, fall back to HTTP semantics - if err.HTTPStatus == 429 { + switch { + case err.HTTPStatus == 429: return TokenErrRateLimited - } else if err.HTTPStatus >= 500 { + case err.HTTPStatus >= 500: return TokenErrAuthServerIssue - } else if err.HTTPStatus == 400 { + case err.HTTPStatus == 400: return TokenErrInvalidRequest - } else { + default: return TokenErrUnknown } } diff --git a/internal/pkg/utils/oauth/error_test.go b/internal/pkg/utils/oauth/error_test.go index b8427bcb..b3446fb7 100644 --- a/internal/pkg/utils/oauth/error_test.go +++ b/internal/pkg/utils/oauth/error_test.go @@ -415,8 +415,11 @@ func TestNewTokenErrorFromResponse(t *testing.T) { defer server.Close() client := &http.Client{} - req, _ := http.NewRequest("GET", server.URL, nil) + req, _ := http.NewRequest("GET", server.URL, http.NoBody) resp, _ = client.Do(req) + if resp != nil { + defer resp.Body.Close() + } } got := NewTokenErrorFromResponse(test.operation, resp) diff --git a/internal/pkg/utils/oauth/token.go b/internal/pkg/utils/oauth/token.go index f9518140..12ace3b1 100644 --- a/internal/pkg/utils/oauth/token.go +++ b/internal/pkg/utils/oauth/token.go @@ -209,12 +209,12 @@ func isEmptyToken(token *oauth2.Token) bool { return token == nil || (token.AccessToken == "" && token.RefreshToken == "") } -func shouldPersistToken(old, new *oauth2.Token) bool { - if old == nil || new == nil { - return old != new +func shouldPersistToken(old, next *oauth2.Token) bool { + if old == nil || next == nil { + return old != next } - if old.AccessToken != new.AccessToken || old.RefreshToken != new.RefreshToken { + if old.AccessToken != next.AccessToken || old.RefreshToken != next.RefreshToken { return true } return false diff --git a/internal/pkg/utils/presenters/fetch_vectors.go b/internal/pkg/utils/presenters/fetch_vectors.go index c37feadb..898a959f 100644 --- a/internal/pkg/utils/presenters/fetch_vectors.go +++ b/internal/pkg/utils/presenters/fetch_vectors.go @@ -90,9 +90,9 @@ func previewSliceFloat32(values *[]float32, limit int) string { vals = vals[:limit] truncated = true } - text := text.InlineJSON(vals) - if truncated && strings.HasSuffix(text, "]") { - text = text[:len(text)-1] + ", ...]" + jsonStr := text.InlineJSON(vals) + if truncated && strings.HasSuffix(jsonStr, "]") { + jsonStr = jsonStr[:len(jsonStr)-1] + ", ...]" } - return text + return jsonStr } diff --git a/internal/pkg/utils/presenters/mask.go b/internal/pkg/utils/presenters/mask.go index 468b5812..a4723a07 100644 --- a/internal/pkg/utils/presenters/mask.go +++ b/internal/pkg/utils/presenters/mask.go @@ -23,9 +23,3 @@ func MaskHeadTail(s string, head, tail int) string { return start + "***" + end } -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/internal/pkg/utils/presenters/query_vectors.go b/internal/pkg/utils/presenters/query_vectors.go index 8270589b..c1e888c8 100644 --- a/internal/pkg/utils/presenters/query_vectors.go +++ b/internal/pkg/utils/presenters/query_vectors.go @@ -103,10 +103,10 @@ func previewSliceUint32(values []uint32, limit int) string { truncated = true } - text := text.InlineJSON(vals) - if truncated && strings.HasSuffix(text, "]") { - text = text[:len(text)-1] + ", ...]" + jsonStr := text.InlineJSON(vals) + if truncated && strings.HasSuffix(jsonStr, "]") { + jsonStr = jsonStr[:len(jsonStr)-1] + ", ...]" } - return text + return jsonStr } diff --git a/internal/pkg/utils/presenters/target_context.go b/internal/pkg/utils/presenters/target_context.go index 48955c84..ec7d67e6 100644 --- a/internal/pkg/utils/presenters/target_context.go +++ b/internal/pkg/utils/presenters/target_context.go @@ -25,8 +25,8 @@ func PrintTargetContext(context *state.TargetContext) { } log.Info(). - Str("org", string(context.Organization.Name)). - Str("project", string(context.Project.Name)). + Str("org", context.Organization.Name). + Str("project", context.Project.Name). Msg("Printing target context") columns := []string{"ATTRIBUTE", "VALUE"} @@ -36,10 +36,10 @@ func PrintTargetContext(context *state.TargetContext) { // Get API key for presentational layer defaultAPIKeyMasked := MaskHeadTail(secrets.DefaultAPIKey.Get(), 4, 4) - fmt.Fprintf(writer, "Organization\t%s\n", labelUnsetIfEmpty(string(context.Organization.Name))) - fmt.Fprintf(writer, "Organization ID\t%s\n", labelUnsetIfEmpty(string(context.Organization.Id))) - fmt.Fprintf(writer, "Project\t%s\n", labelUnsetIfEmpty(string(context.Project.Name))) - fmt.Fprintf(writer, "Project ID\t%s\n", labelUnsetIfEmpty(string(context.Project.Id))) + fmt.Fprintf(writer, "Organization\t%s\n", labelUnsetIfEmpty(context.Organization.Name)) + fmt.Fprintf(writer, "Organization ID\t%s\n", labelUnsetIfEmpty(context.Organization.Id)) + fmt.Fprintf(writer, "Project\t%s\n", labelUnsetIfEmpty(context.Project.Name)) + fmt.Fprintf(writer, "Project ID\t%s\n", labelUnsetIfEmpty(context.Project.Id)) fmt.Fprintf(writer, "Default API Key\t%s\n", labelUnsetIfEmpty(defaultAPIKeyMasked)) writer.Flush() diff --git a/internal/pkg/utils/prompt/list.go b/internal/pkg/utils/prompt/list.go index 50de7500..b0259111 100644 --- a/internal/pkg/utils/prompt/list.go +++ b/internal/pkg/utils/prompt/list.go @@ -60,9 +60,9 @@ func (m ListModel) View() string { return "\n" + m.list.View() } -func mapStringsToItems(strings []string) []list.Item { - items := make([]list.Item, len(strings)) - for i, s := range strings { +func mapStringsToItems(ss []string) []list.Item { + items := make([]list.Item, len(ss)) + for i, s := range ss { items[i] = item(s) } return items @@ -86,8 +86,6 @@ func NewList(items []string, listHeight int, title string, onQuit func(), onChoi } } -const listHeight = 14 - var ( titleStyle = lipgloss.NewStyle().MarginLeft(0) itemStyle = lipgloss.NewStyle().PaddingLeft(3) diff --git a/internal/pkg/utils/sdk/client.go b/internal/pkg/utils/sdk/client.go index 1fccd921..2cf1e4f5 100644 --- a/internal/pkg/utils/sdk/client.go +++ b/internal/pkg/utils/sdk/client.go @@ -151,7 +151,7 @@ func NewPineconeAdminClient(ctx context.Context) *pinecone.AdminClient { return ac } -func NewIndexConnection(ctx context.Context, pc *pinecone.Client, indexName string, namespace string) (*pinecone.IndexConnection, error) { +func NewIndexConnection(ctx context.Context, pc *pinecone.Client, indexName, namespace string) (*pinecone.IndexConnection, error) { index, err := pc.DescribeIndex(ctx, indexName) if err != nil { return nil, fmt.Errorf("failed to describe index: %w", err) diff --git a/internal/pkg/utils/style/spinner.go b/internal/pkg/utils/style/spinner.go index cd5e8673..94882215 100644 --- a/internal/pkg/utils/style/spinner.go +++ b/internal/pkg/utils/style/spinner.go @@ -10,9 +10,8 @@ import ( ) var ( - spinnerTextEllipsis = "..." - spinnerTextDone = StatusGreen("done") - spinnerTextFailed = StatusRed("failed") + spinnerTextDone = StatusGreen("done") + spinnerTextFailed = StatusRed("failed") spinnerColor = "blue" ) diff --git a/internal/pkg/utils/style/typography.go b/internal/pkg/utils/style/typography.go index 32732339..6b8add92 100644 --- a/internal/pkg/utils/style/typography.go +++ b/internal/pkg/utils/style/typography.go @@ -26,7 +26,7 @@ func Hint(s string) string { return applyStyle("Hint: ", color.Faint) + s } -func CodeHint(templateString string, codeString string) string { +func CodeHint(templateString, codeString string) string { return applyStyle("Hint: ", color.Faint) + fmt.Sprintf(templateString, Code(codeString)) } diff --git a/justfile b/justfile index 6b09a2d3..c781fe12 100644 --- a/justfile +++ b/justfile @@ -24,6 +24,10 @@ ensure-goreleaser: exit 127; \ fi +# Run golangci-lint (requires golangci-lint to be installed: https://golangci-lint.run/welcome/install/) +lint: ensure-go + golangci-lint run ./... + # Run all unit tests for the CLI (e2e and unit tests) test-unit *ARGS: ensure-go go test -v ./... {{ARGS}}