diff --git a/AGENTS.md b/AGENTS.md index 3de16c1..958b11a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ The `pc config` command manages these settings via a key-based interface. Valid ### `--json` flag -Available on most commands. Forces structured, machine-readable output. Also activated automatically when stdout is not a TTY. +Available on most commands. Must be set explicitly to force structured, machine-readable output — it is **not** inferred from whether stdout is a TTY. (Color is suppressed automatically on a non-TTY, but the data output format is not.) The interactive auth flows — `login`, `target`, and `auth` — are the exception: they additionally infer JSON output when stdout is not a TTY, since an agent on a non-TTY can't drive their prompts. --- diff --git a/internal/pkg/cli/command/apiKey/delete.go b/internal/pkg/cli/command/apiKey/delete.go index 2866996..a42d752 100644 --- a/internal/pkg/cli/command/apiKey/delete.go +++ b/internal/pkg/cli/command/apiKey/delete.go @@ -1,13 +1,11 @@ package apiKey import ( - "bufio" "fmt" - "os" - "strings" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/secrets" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -61,7 +59,11 @@ func NewDeleteKeyCmd() *cobra.Command { } if !options.skipConfirmation && !options.json { - confirmDeleteApiKey(keyToDelete.Name) + confirm.Deletion( + fmt.Sprintf("This operation will delete API key %s from project %s.", style.Emphasis(keyToDelete.Name), style.Emphasis(state.TargetProj.Get().Name)), + "Any integrations that authenticate with this API key will immediately stop working.", + "This action cannot be undone.", + ) } err = ac.APIKey.Delete(cmd.Context(), keyToDelete.Id) @@ -96,31 +98,3 @@ func NewDeleteKeyCmd() *cobra.Command { cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } - -func confirmDeleteApiKey(apiKeyName string) { - msg.WarnMsg("This operation will delete API key %s from project %s.", style.Emphasis(apiKeyName), style.Emphasis(state.TargetProj.Get().Name)) - msg.WarnMsg("Any integrations that authenticate with this API key will immediately stop working.") - msg.WarnMsg("This action cannot be undone.") - - // Prompt the user - fmt.Fprint(os.Stderr, "Do you want to continue? (y/N): ") - - // Read the user's input - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - msg.FailMsg("Error reading input: %+v", err) - exit.Error(err, "Error reading input") - } - - // Trim any whitespace from the input and convert to lowercase - input = strings.TrimSpace(strings.ToLower(input)) - - // Check if the user entered "y" or "yes" - if input == "y" || input == "yes" { - msg.InfoMsg("You chose to continue delete.") - } else { - msg.InfoMsg("Operation canceled.") - exit.Success() - } -} diff --git a/internal/pkg/cli/command/apiKey/list.go b/internal/pkg/cli/command/apiKey/list.go index 91cdf65..e400ebe 100644 --- a/internal/pkg/cli/command/apiKey/list.go +++ b/internal/pkg/cli/command/apiKey/list.go @@ -47,7 +47,7 @@ func NewListKeysCmd() *cobra.Command { if projId == "" { projId, err = state.GetTargetProjectId() if err != nil { - msg.FailJSON(options.json, "No target project set, and no project ID provided. Use %s to set the target project. Use %s to create the key in a specific project.", style.Code("pc target -o -p "), style.Code("pc api-key create -i -n ")) + msg.FailJSON(options.json, "No target project set, and no project ID provided. Use %s to set the target project. Use %s to list keys for a specific project.", style.Code("pc target -o -p "), style.Code("pc api-key list -i ")) exit.ErrorMsg("No project ID provided, and no target project set") } } diff --git a/internal/pkg/cli/command/auth/configure.go b/internal/pkg/cli/command/auth/configure.go index dfae2c2..525876a 100644 --- a/internal/pkg/cli/command/auth/configure.go +++ b/internal/pkg/cli/command/auth/configure.go @@ -184,6 +184,10 @@ func Run(ctx context.Context, opts configureCmdOptions) { break } } + if targetProject == nil { + msg.FailJSON(opts.json, "No project found with ID %s for the service account", opts.projectID) + exit.ErrorMsg(fmt.Sprintf("No project found with ID %s for the service account", opts.projectID)) + } } else { targetProject = uiProjectSelector(projects, opts.json) } diff --git a/internal/pkg/cli/command/auth/local_keys_prune.go b/internal/pkg/cli/command/auth/local_keys_prune.go index fbdd26d..6ae1f7d 100644 --- a/internal/pkg/cli/command/auth/local_keys_prune.go +++ b/internal/pkg/cli/command/auth/local_keys_prune.go @@ -123,7 +123,7 @@ func runPruneLocalKeys(ctx context.Context, options pruneLocalKeysCmdOptions) { // Dry run preview if options.dryRun { printDryRunPlan(plan, options) - msg.InfoMsg("Dry run complete. Re-run with %s and %s to apply changes", style.Emphasis("--yes"), style.Emphasis("--dry-run=false")) + msg.InfoMsg("Dry run complete. Re-run without %s to apply changes (add %s to skip the prompt)", style.Emphasis("--dry-run"), style.Emphasis("--skip-confirmation")) return } diff --git a/internal/pkg/cli/command/index/backup/delete.go b/internal/pkg/cli/command/index/backup/delete.go index 1e4ff98..cd86e0c 100644 --- a/internal/pkg/cli/command/index/backup/delete.go +++ b/internal/pkg/cli/command/index/backup/delete.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -15,8 +16,9 @@ import ( ) type deleteBackupCmdOptions struct { - backupId string - json bool + backupId string + skipConfirmation bool + json bool } func NewDeleteBackupCmd() *cobra.Command { @@ -32,6 +34,13 @@ func NewDeleteBackupCmd() *cobra.Command { ctx := cmd.Context() pc := sdk.NewPineconeClient(ctx) + if !options.skipConfirmation && !options.json { + confirm.Deletion( + fmt.Sprintf("This will delete backup %s.", style.Emphasis(options.backupId)), + "This action cannot be undone.", + ) + } + err := runDeleteBackupCmd(ctx, pc, options) if err != nil { msg.FailJSON(options.json, "Failed to delete backup: %s\n", err) @@ -42,7 +51,8 @@ func NewDeleteBackupCmd() *cobra.Command { cmd.Flags().StringVarP(&options.backupId, "id", "i", "", "ID of the backup to delete") _ = cmd.MarkFlagRequired("id") - cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip the deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } diff --git a/internal/pkg/cli/command/index/collection/delete.go b/internal/pkg/cli/command/index/collection/delete.go index 182a66f..2309292 100644 --- a/internal/pkg/cli/command/index/collection/delete.go +++ b/internal/pkg/cli/command/index/collection/delete.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -14,8 +15,9 @@ import ( ) type deleteCollectionCmdOptions struct { - name string - json bool + name string + skipConfirmation bool + json bool } // DeleteCollectionService abstracts the Pinecone Go SDK for unit testing (runDeleteCollectionCmd) @@ -36,6 +38,13 @@ func NewDeleteCollectionCmd() *cobra.Command { ctx := cmd.Context() pc := sdk.NewPineconeClient(ctx) + if !options.skipConfirmation && !options.json { + confirm.Deletion( + fmt.Sprintf("This will delete collection %s.", style.Emphasis(options.name)), + "This action cannot be undone.", + ) + } + err := runDeleteCollectionCmd(ctx, pc, options) if err != nil { msg.FailJSON(options.json, "Failed to delete collection %s: %s\n", style.Emphasis(options.name), err) @@ -49,7 +58,8 @@ func NewDeleteCollectionCmd() *cobra.Command { _ = cmd.MarkFlagRequired("name") // Optional flags - cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip the deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } diff --git a/internal/pkg/cli/command/index/collection/list.go b/internal/pkg/cli/command/index/collection/list.go index 59d5185..f8e85a4 100644 --- a/internal/pkg/cli/command/index/collection/list.go +++ b/internal/pkg/cli/command/index/collection/list.go @@ -69,7 +69,7 @@ func printTable(collections []*pinecone.Collection) { fmt.Fprint(writer, header) for _, coll := range collections { - values := []string{coll.Name, string(coll.Dimension), strconv.FormatInt(coll.Size, 10), string(coll.Status), string(coll.VectorCount), coll.Environment} + values := []string{coll.Name, strconv.Itoa(int(coll.Dimension)), strconv.FormatInt(coll.Size, 10), string(coll.Status), strconv.Itoa(int(coll.VectorCount)), coll.Environment} fmt.Fprint(writer, strings.Join(values, "\t")+"\n") } writer.Flush() diff --git a/internal/pkg/cli/command/index/configure.go b/internal/pkg/cli/command/index/configure.go index 6a89373..58c0eef 100644 --- a/internal/pkg/cli/command/index/configure.go +++ b/internal/pkg/cli/command/index/configure.go @@ -85,8 +85,9 @@ func NewConfigureIndexCmd() *cobra.Command { cmd.Flags().Int32Var(&options.readReplicas, "read-replicas", 0, "The number of replicas to use. Replicas duplicate the compute resources and data of an index, allowing higher query throughput and availability") // optional for all index types - cmd.Flags().StringVarP(&options.deletionProtection, "deletion-protection", "p", "", "Enable or disable deletion protection for the index. One of: enabled, disabled") + cmd.Flags().StringVar(&options.deletionProtection, "deletion-protection", "", "Enable or disable deletion protection for the index. One of: enabled, disabled") cmd.Flags().StringToStringVar(&options.tags, "tags", map[string]string{}, "Custom user tags to add to an index") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON") return cmd } diff --git a/internal/pkg/cli/command/index/delete.go b/internal/pkg/cli/command/index/delete.go index 8f96716..4714333 100644 --- a/internal/pkg/cli/command/index/delete.go +++ b/internal/pkg/cli/command/index/delete.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -15,8 +16,9 @@ import ( ) type deleteCmdOptions struct { - indexName string - json bool + indexName string + skipConfirmation bool + json bool } // DeleteIndexService abstracts the Pinecone Go SDK for unit testing (runDeleteIndexCmd) @@ -43,6 +45,13 @@ func NewDeleteCmd() *cobra.Command { ctx := cmd.Context() pc := sdk.NewPineconeClient(ctx) + if !options.skipConfirmation && !options.json { + confirm.Deletion( + fmt.Sprintf("This will delete index %s and all of its data.", style.Emphasis(options.indexName)), + "This action cannot be undone.", + ) + } + err := runDeleteIndexCmd(ctx, pc, options) if err != nil { if strings.Contains(err.Error(), "not found") { @@ -60,7 +69,8 @@ func NewDeleteCmd() *cobra.Command { cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of index to delete") cmd.Flags().StringVarP(&options.indexName, "name", "n", "", "name of index to delete") _ = cmd.Flags().MarkDeprecated("name", "use --index-name instead") - cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip the deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } diff --git a/internal/pkg/cli/command/index/namespace/delete.go b/internal/pkg/cli/command/index/namespace/delete.go index 20b12fe..c930ab4 100644 --- a/internal/pkg/cli/command/index/namespace/delete.go +++ b/internal/pkg/cli/command/index/namespace/delete.go @@ -5,18 +5,21 @@ import ( "fmt" "strings" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteNamespaceCmdOptions struct { - indexName string - name string - json bool + indexName string + name string + skipConfirmation bool + json bool } func NewDeleteNamespaceCmd() *cobra.Command { @@ -34,6 +37,9 @@ func NewDeleteNamespaceCmd() *cobra.Command { Example: help.Examples(` # delete a namespace from an index pc index namespace delete --index-name "my-index" --name "tenant-a" + + # delete the default namespace + pc index namespace delete --index-name "my-index" --name "__default__" `), Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() @@ -50,6 +56,13 @@ func NewDeleteNamespaceCmd() *cobra.Command { exit.Error(err, "Failed to delete namespace") } + if !options.skipConfirmation && !options.json { + confirm.Deletion( + fmt.Sprintf("This will delete namespace %s from index %s and remove all of its data.", style.Emphasis(options.name), style.Emphasis(options.indexName)), + "This action cannot be undone.", + ) + } + err = runDeleteNamespaceCmd(ctx, ic, options) if err != nil { msg.FailJSON(options.json, "Failed to delete namespace: %s", err) @@ -59,10 +72,11 @@ func NewDeleteNamespaceCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of the index to delete the namespace from") - cmd.Flags().StringVar(&options.name, "name", "", "name of the namespace to delete") + cmd.Flags().StringVar(&options.name, "name", "", "name of the namespace to delete (use \"__default__\" for the default namespace)") _ = cmd.MarkFlagRequired("index-name") _ = cmd.MarkFlagRequired("name") - cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip the deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } diff --git a/internal/pkg/cli/command/index/namespace/describe.go b/internal/pkg/cli/command/index/namespace/describe.go index b434c38..70e77e1 100644 --- a/internal/pkg/cli/command/index/namespace/describe.go +++ b/internal/pkg/cli/command/index/namespace/describe.go @@ -34,6 +34,9 @@ func NewDescribeNamespaceCmd() *cobra.Command { # describe a namespace pc index namespace describe --index-name "my-index" --name "tenant-a" + # describe the default namespace + pc index namespace describe --index-name "my-index" --name "__default__" + # describe a namespace and return JSON pc index namespace describe --index-name "my-index" --name "tenant-a" --json `), @@ -61,7 +64,7 @@ func NewDescribeNamespaceCmd() *cobra.Command { } cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of the index to describe the namespace from") - cmd.Flags().StringVar(&options.name, "name", "", "name of the namespace to describe") + cmd.Flags().StringVar(&options.name, "name", "", "name of the namespace to describe (use \"__default__\" for the default namespace)") cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") _ = cmd.MarkFlagRequired("index-name") _ = cmd.MarkFlagRequired("name") diff --git a/internal/pkg/cli/command/index/restore/cmd.go b/internal/pkg/cli/command/index/restore/cmd.go index 6c3fb8e..f45a901 100644 --- a/internal/pkg/cli/command/index/restore/cmd.go +++ b/internal/pkg/cli/command/index/restore/cmd.go @@ -77,7 +77,7 @@ func NewRestoreCmd() *cobra.Command { cmd.Flags().StringVarP(&options.backupId, "id", "i", "", "ID of the backup to restore from") cmd.Flags().StringVarP(&options.name, "name", "n", "", "Name of the index to create from the backup") - cmd.Flags().StringVarP(&options.deletionProtection, "deletion-protection", "d", "", "Whether to enable deletion protection on the new index (enabled|disabled)") + cmd.Flags().StringVar(&options.deletionProtection, "deletion-protection", "", "Whether to enable deletion protection on the new index (enabled|disabled)") cmd.Flags().StringToStringVarP(&options.tags, "tags", "t", map[string]string{}, "Tags to apply to the new index") cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON") _ = cmd.MarkFlagRequired("id") diff --git a/internal/pkg/cli/command/index/vector/delete.go b/internal/pkg/cli/command/index/vector/delete.go index 27d5c97..61ebda5 100644 --- a/internal/pkg/cli/command/index/vector/delete.go +++ b/internal/pkg/cli/command/index/vector/delete.go @@ -2,12 +2,15 @@ package vector import ( "context" + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/flags" "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/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/style" "github.com/pinecone-io/go-pinecone/v5/pinecone" "github.com/spf13/cobra" ) @@ -18,6 +21,7 @@ type deleteVectorsCmdOptions struct { ids flags.StringList filter flags.JSONObject deleteAllVectors bool + skipConfirmation bool json bool } @@ -48,7 +52,8 @@ func NewDeleteVectorsCmd() *cobra.Command { cmd.Flags().Var(&options.ids, "ids", "IDs of the vectors to delete (inline JSON string array, ./path.json, or '-' for stdin)") cmd.Flags().Var(&options.filter, "filter", "filter to delete the vectors with (inline JSON, ./path.json, or '-' for stdin)") cmd.Flags().BoolVar(&options.deleteAllVectors, "all-vectors", false, "delete all vectors from the namespace") - cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON") + cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip the deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON (also skips confirmation prompt)") _ = cmd.MarkFlagRequired("index-name") @@ -70,6 +75,16 @@ func runDeleteVectorsCmd(ctx context.Context, options deleteVectorsCmdOptions) { // Delete all vectors in namespace if options.deleteAllVectors { + if !options.skipConfirmation && !options.json { + target := "the default namespace" + if options.namespace != "" { + target = fmt.Sprintf("namespace %s", style.Emphasis(options.namespace)) + } + confirm.Deletion( + fmt.Sprintf("This will delete ALL vectors in %s of index %s.", target, style.Emphasis(options.indexName)), + "This action cannot be undone.", + ) + } err = ic.DeleteAllVectorsInNamespace(ctx) if err != nil { msg.FailJSON(options.json, "Failed to delete all vectors in namespace: %s", err) diff --git a/internal/pkg/cli/command/index/vector/query.go b/internal/pkg/cli/command/index/vector/query.go index a742768..e7fbe47 100644 --- a/internal/pkg/cli/command/index/vector/query.go +++ b/internal/pkg/cli/command/index/vector/query.go @@ -135,8 +135,8 @@ func runQueryCmd(ctx context.Context, options queryCmdOptions) { } if options.id == "" && options.vector == nil && options.sparseIndices == nil && options.sparseValues == nil { - msg.FailJSON(options.json, "Either --id, --vector, --sparse-indices & --sparse-values") - exit.ErrorMsg("Either --id, --vector, --sparse-indices & --sparse-values") + msg.FailJSON(options.json, "One of --id, --vector, or --sparse-indices & --sparse-values must be provided") + exit.ErrorMsg("One of --id, --vector, or --sparse-indices & --sparse-values must be provided") } // Get IndexConnection diff --git a/internal/pkg/cli/command/organization/delete.go b/internal/pkg/cli/command/organization/delete.go index 809241b..27791af 100644 --- a/internal/pkg/cli/command/organization/delete.go +++ b/internal/pkg/cli/command/organization/delete.go @@ -1,13 +1,12 @@ package organization import ( - "bufio" "context" "fmt" "os" - "strings" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -35,7 +34,7 @@ func NewDeleteOrganizationCmd() *cobra.Command { Use: "delete", Short: "Delete an organization by ID", Example: help.Examples(` - pc organization delete --id "organization-id"" + pc organization delete --id "organization-id" pc organization delete --id "organization-id" --skip-confirmation `), GroupID: help.GROUP_ORGANIZATIONS.ID, @@ -51,7 +50,10 @@ func NewDeleteOrganizationCmd() *cobra.Command { } if !options.skipConfirmation && !options.json { - confirmDelete(org.Name, org.Id) + confirm.Deletion( + fmt.Sprintf("This will delete the organization %s (ID: %s).", style.Emphasis(org.Name), style.Emphasis(org.Id)), + "This action cannot be undone.", + ) } err = runDeleteOrganizationCmd(ctx, ac.Organization, options, org.Name, org.Id) @@ -98,28 +100,3 @@ func runDeleteOrganizationCmd(ctx context.Context, svc DeleteOrganizationService msg.SuccessMsg("Organization %s (ID: %s) deleted.\n", style.Emphasis(name), style.Emphasis(id)) return nil } - -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.") - - // Prompt the user - fmt.Fprint(os.Stderr, "Do you want to continue? (y/N): ") - - // Read the user's input - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - msg.FailMsg("Error reading input: %v", err) - exit.Error(err, "Error reading input") - } - - input = strings.TrimSpace(strings.ToLower(input)) - - if input == "y" || input == "yes" { - msg.InfoMsg("You chose to continue delete.") - } else { - msg.InfoMsg("Operation canceled.") - exit.Success() - } -} diff --git a/internal/pkg/cli/command/organization/update.go b/internal/pkg/cli/command/organization/update.go index eabaa1b..e383937 100644 --- a/internal/pkg/cli/command/organization/update.go +++ b/internal/pkg/cli/command/organization/update.go @@ -41,7 +41,7 @@ func NewUpdateOrganizationCmd() *cobra.Command { if orgId == "" { orgId, err = state.GetTargetOrgId() if err != nil { - msg.FailJSON(options.json, "No target organization set and no organization ID provided. Use %s to set the target organization. Use %s to describe an organization by ID.", style.Code("pc target -o "), style.Code("pc organization describe -i ")) + msg.FailJSON(options.json, "No target organization set and no organization ID provided. Use %s to set the target organization. Use %s to update an organization by ID.", style.Code("pc target -o "), style.Code("pc organization update -i ")) exit.ErrorMsg("No organization ID provided, and no target organization set") } } diff --git a/internal/pkg/cli/command/project/delete.go b/internal/pkg/cli/command/project/delete.go index 22d7c05..4e6b19d 100644 --- a/internal/pkg/cli/command/project/delete.go +++ b/internal/pkg/cli/command/project/delete.go @@ -1,13 +1,12 @@ package project import ( - "bufio" "context" "fmt" "os" - "strings" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" + "github.com/pinecone-io/cli/internal/pkg/utils/confirm" "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" @@ -67,7 +66,10 @@ func NewDeleteProjectCmd() *cobra.Command { verifyNoCollections(ctx, projToDelete.Id, projToDelete.Name, options.json) if !options.skipConfirmation && !options.json { - confirmDelete(projToDelete.Name) + confirm.Deletion( + fmt.Sprintf("This will delete the project %s in organization %s.", style.Emphasis(projToDelete.Name), style.Emphasis(state.TargetOrg.Get().Name)), + "This action cannot be undone.", + ) } err = runDeleteProjectCmd(ctx, ac.Project, options, projToDelete.Name, projToDelete.Id) @@ -112,33 +114,6 @@ func runDeleteProjectCmd(ctx context.Context, svc DeleteProjectService, opts del return nil } -func confirmDelete(projectName string) { - msg.WarnMsg("This will delete the project %s in organization %s.", style.Emphasis(projectName), style.Emphasis(state.TargetOrg.Get().Name)) - msg.WarnMsg("This action cannot be undone.") - - // Prompt the user - fmt.Fprint(os.Stderr, "Do you want to continue? (y/N): ") - - // Read the user's input - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - msg.FailMsg("Error reading input: %v", err) - exit.Error(err, "Error reading input") - } - - // Trim any whitespace from the input and convert to lowercase - input = strings.TrimSpace(strings.ToLower(input)) - - // Check if the user entered "y" or "yes" - if input == "y" || input == "yes" { - msg.InfoMsg("You chose to continue delete.") - } else { - msg.InfoMsg("Operation canceled.") - exit.Success() - } -} - func verifyNoIndexes(ctx context.Context, projectId, projectName string, jsonOutput bool) { // Check if project contains indexes pc := sdk.NewPineconeClientForProjectById(ctx, projectId) diff --git a/internal/pkg/cli/command/project/describe.go b/internal/pkg/cli/command/project/describe.go index ce81c08..31c80b6 100644 --- a/internal/pkg/cli/command/project/describe.go +++ b/internal/pkg/cli/command/project/describe.go @@ -39,7 +39,7 @@ func NewDescribeProjectCmd() *cobra.Command { if projId == "" { projId, err = state.GetTargetProjectId() if err != nil { - msg.FailJSON(options.json, "No target project set and no project ID provided. Use %s to set the target project. Use %s to delete a specific project.", style.Code("pc target -p "), style.Code("pc project delete -i ")) + msg.FailJSON(options.json, "No target project set and no project ID provided. Use %s to set the target project. Use %s to describe a specific project.", style.Code("pc target -p "), style.Code("pc project describe -i ")) exit.ErrorMsg("No project ID provided, and no target project set") } } diff --git a/internal/pkg/cli/command/project/update.go b/internal/pkg/cli/command/project/update.go index 4ae9f13..cdf0763 100644 --- a/internal/pkg/cli/command/project/update.go +++ b/internal/pkg/cli/command/project/update.go @@ -43,7 +43,7 @@ func NewUpdateProjectCmd() *cobra.Command { if projId == "" { projId, err = state.GetTargetProjectId() if err != nil { - msg.FailJSON(options.json, "No target project set and no project ID provided. Use %s to set the target project. Use %s to delete a specific project.", style.Code("pc target -p "), style.Code("pc project delete -i ")) + msg.FailJSON(options.json, "No target project set and no project ID provided. Use %s to set the target project. Use %s to update a specific project.", style.Code("pc target -p "), style.Code("pc project update -i ")) exit.ErrorMsg("No project ID provided, and no target project set") } } diff --git a/internal/pkg/utils/confirm/confirm.go b/internal/pkg/utils/confirm/confirm.go new file mode 100644 index 0000000..8204a9e --- /dev/null +++ b/internal/pkg/utils/confirm/confirm.go @@ -0,0 +1,44 @@ +// Package confirm provides interactive confirmation prompts for destructive +// operations such as deletes. +package confirm + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/pinecone-io/cli/internal/pkg/utils/exit" + "github.com/pinecone-io/cli/internal/pkg/utils/msg" +) + +// Deletion emits the provided warning lines to stderr, prompts the user to +// confirm a destructive action, and exits successfully (canceling the +// operation) if they decline. Each warning is printed verbatim, so callers may +// pre-format them (e.g. with style.Emphasis) without worrying about format +// directives. +// +// Callers are responsible for skipping this prompt when --skip-confirmation or +// --json is set. +func Deletion(warnings ...string) { + for _, w := range warnings { + msg.WarnMsg("%s", w) + } + + fmt.Fprint(os.Stderr, "Do you want to continue? (y/N): ") + + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + msg.FailMsg("Error reading input: %v", err) + exit.Error(err, "Error reading input") + } + + switch strings.TrimSpace(strings.ToLower(input)) { + case "y", "yes": + return + default: + msg.InfoMsg("Operation canceled.") + exit.Success() + } +} diff --git a/test/e2e/namespace_test.go b/test/e2e/namespace_test.go index c515cc2..089053f 100644 --- a/test/e2e/namespace_test.go +++ b/test/e2e/namespace_test.go @@ -30,7 +30,7 @@ func (s *ServiceAccountSuite) TestNamespaceLifecycle() { s.Require().Equal(1, len(list.Namespaces), "expected one namespace in list") s.Require().Equal(name, list.Namespaces[0].Name, "namespace name mismatch") - _, _, err = s.cli.RunCtx(s.ctx, "index", "namespace", "delete", "--index-name", s.indexName, "--name", name) + _, _, err = s.cli.RunCtx(s.ctx, "index", "namespace", "delete", "--index-name", s.indexName, "--name", name, "--skip-confirmation") s.Require().NoError(err, "namespace delete failed") } @@ -57,6 +57,6 @@ func (a *APIKeySuite) TestNamespaceLifecycle() { a.Require().Equal(1, len(list.Namespaces), "expected one namespace in list") a.Require().Equal(name, list.Namespaces[0].Name, "namespace name mismatch") - _, _, err = a.cli.RunCtx(a.ctx, "index", "namespace", "delete", "--index-name", a.indexName, "--name", name) + _, _, err = a.cli.RunCtx(a.ctx, "index", "namespace", "delete", "--index-name", a.indexName, "--name", name, "--skip-confirmation") a.Require().NoError(err, "namespace delete failed") } diff --git a/test/e2e/suite_api_test.go b/test/e2e/suite_api_test.go index f7f529c..2636742 100644 --- a/test/e2e/suite_api_test.go +++ b/test/e2e/suite_api_test.go @@ -72,7 +72,7 @@ func (a *APIKeySuite) SetupSuite() { func (a *APIKeySuite) TearDownSuite() { if a.createdIndex && a.indexName != "" && a.cli != nil { - _, _, _ = a.cli.RunCtx(a.ctx, "index", "delete", "--index-name", a.indexName) + _, _, _ = a.cli.RunCtx(a.ctx, "index", "delete", "--index-name", a.indexName, "--skip-confirmation") } if a.tempHome != "" { _ = os.RemoveAll(a.tempHome) diff --git a/test/e2e/suite_sa_test.go b/test/e2e/suite_sa_test.go index 57fd7c0..8000374 100644 --- a/test/e2e/suite_sa_test.go +++ b/test/e2e/suite_sa_test.go @@ -57,6 +57,6 @@ func (s *ServiceAccountSuite) TearDownSuite() { if !s.createdIdx || s.indexName == "" || s.cli == nil { return } - _, _, err := s.cli.RunCtx(s.ctx, "index", "delete", "--index-name", s.indexName) + _, _, err := s.cli.RunCtx(s.ctx, "index", "delete", "--index-name", s.indexName, "--skip-confirmation") s.Require().NoError(err, "index delete failed (sa)") }