From 0f223450b81cf9fc7b3fbc8b79f84da39ba0a5e7 Mon Sep 17 00:00:00 2001 From: Travis Lyons Date: Thu, 24 Apr 2025 11:39:23 -0400 Subject: [PATCH 1/4] feat: introduce prompt library management The following subcommands are included: - `list`: lists prompts - `get`: gets a prompt by ID - `create`: creates a prompt - `update`: updates a prompt - `delete`: deletes a prompt - `tags`: manages prompt tags (with subcommands: list, create, update, delete) --- cmd/src/prompts.go | 100 +++++++++ cmd/src/prompts_create.go | 130 +++++++++++ cmd/src/prompts_delete.go | 74 +++++++ cmd/src/prompts_export.go | 270 +++++++++++++++++++++++ cmd/src/prompts_get.go | 100 +++++++++ cmd/src/prompts_import.go | 387 +++++++++++++++++++++++++++++++++ cmd/src/prompts_list.go | 227 +++++++++++++++++++ cmd/src/prompts_tags.go | 42 ++++ cmd/src/prompts_tags_create.go | 126 +++++++++++ cmd/src/prompts_tags_delete.go | 74 +++++++ cmd/src/prompts_tags_get.go | 86 ++++++++ cmd/src/prompts_tags_list.go | 129 +++++++++++ cmd/src/prompts_tags_update.go | 79 +++++++ cmd/src/prompts_update.go | 123 +++++++++++ 14 files changed, 1947 insertions(+) create mode 100644 cmd/src/prompts.go create mode 100644 cmd/src/prompts_create.go create mode 100644 cmd/src/prompts_delete.go create mode 100644 cmd/src/prompts_export.go create mode 100644 cmd/src/prompts_get.go create mode 100644 cmd/src/prompts_import.go create mode 100644 cmd/src/prompts_list.go create mode 100644 cmd/src/prompts_tags.go create mode 100644 cmd/src/prompts_tags_create.go create mode 100644 cmd/src/prompts_tags_delete.go create mode 100644 cmd/src/prompts_tags_get.go create mode 100644 cmd/src/prompts_tags_list.go create mode 100644 cmd/src/prompts_tags_update.go create mode 100644 cmd/src/prompts_update.go diff --git a/cmd/src/prompts.go b/cmd/src/prompts.go new file mode 100644 index 0000000000..bb8a37d1f6 --- /dev/null +++ b/cmd/src/prompts.go @@ -0,0 +1,100 @@ +package main + +import ( + "flag" + "fmt" +) + +var promptsCommands commander + +func init() { + usage := `'src prompts' is a tool that manages prompt library prompts and tags in a Sourcegraph instance. + +Usage: + + src prompts command [command options] + +The commands are: + + list lists prompts + get get a prompt by ID + create create a prompt + update update a prompt + delete delete a prompt + export export prompts to a JSON file + import import prompts from a JSON file + tags manage prompt tags (use "src prompts tags [command] -h" for more info) + +Use "src prompts [command] -h" for more information about a command. +` + + flagSet := flag.NewFlagSet("prompts", flag.ExitOnError) + handler := func(args []string) error { + promptsCommands.run(flagSet, "src prompts", usage, args) + return nil + } + + // Register the command. + commands = append(commands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: func() { + fmt.Println(usage) + }, + }) +} + +const promptFragment = ` +fragment PromptFields on Prompt { + id + name + description + definition { + text + } + draft + visibility + autoSubmit + mode + recommended + tags(first: 100) { + nodes { + id + name + } + } +} +` + +const promptTagFragment = ` +fragment PromptTagFields on PromptTag { + id + name +} +` + +type Prompt struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Definition Definition `json:"definition"` + Draft bool `json:"draft"` + Visibility string `json:"visibility"` + AutoSubmit bool `json:"autoSubmit"` + Mode string `json:"mode"` + Recommended bool `json:"recommended"` + Tags PromptTags `json:"tags"` +} + +type Definition struct { + Text string `json:"text"` +} + +type PromptTags struct { + Nodes []PromptTag `json:"nodes"` +} + +type PromptTag struct { + ID string `json:"id"` + Name string `json:"name"` +} diff --git a/cmd/src/prompts_create.go b/cmd/src/prompts_create.go new file mode 100644 index 0000000000..694836ffa1 --- /dev/null +++ b/cmd/src/prompts_create.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Create a prompt "Go Error Handling": + + $ src prompts create -name="Go Error Handling" \ + -description="Best practices for Go error handling" \ + -content="When handling errors in Go..." \ + -owner= +` + + flagSet := flag.NewFlagSet("create", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + nameFlag = flagSet.String("name", "", "The prompt name") + descriptionFlag = flagSet.String("description", "", "Description of the prompt") + contentFlag = flagSet.String("content", "", "The prompt template text content") + ownerFlag = flagSet.String("owner", "", "The ID of the owner (user or organization)") + tagsFlag = flagSet.String("tags", "", "Comma-separated list of tag IDs") + draftFlag = flagSet.Bool("draft", false, "Whether the prompt is a draft") + visibilityFlag = flagSet.String("visibility", "PUBLIC", "Visibility of the prompt (PUBLIC or SECRET)") + autoSubmitFlag = flagSet.Bool("auto-submit", false, "Whether the prompt should be automatically executed in one click") + modeFlag = flagSet.String("mode", "CHAT", "Mode to execute prompt (CHAT, EDIT, or INSERT)") + recommendedFlag = flagSet.Bool("recommended", false, "Whether the prompt is recommended") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *nameFlag == "" { + return errors.New("provide a name for the prompt") + } + if *descriptionFlag == "" { + return errors.New("provide a description for the prompt") + } + if *contentFlag == "" { + return errors.New("provide content for the prompt") + } + if *ownerFlag == "" { + return errors.New("provide an owner ID for the prompt") + } + + // Validate mode + validModes := map[string]bool{"CHAT": true, "EDIT": true, "INSERT": true} + mode := strings.ToUpper(*modeFlag) + if !validModes[mode] { + return errors.New("mode must be one of: CHAT, EDIT, or INSERT") + } + + // Validate visibility + validVisibility := map[string]bool{"PUBLIC": true, "SECRET": true} + visibility := strings.ToUpper(*visibilityFlag) + if !validVisibility[visibility] { + return errors.New("visibility must be either PUBLIC or SECRET") + } + + // Parse tags into array + var tagIDs []string + if *tagsFlag != "" { + tagIDs = strings.Split(*tagsFlag, ",") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation CreatePrompt( + $input: PromptInput! +) { + createPrompt(input: $input) { + ...PromptFields + } +} +` + promptFragment + + input := map[string]interface{}{ + "name": *nameFlag, + "description": *descriptionFlag, + "definitionText": *contentFlag, + "owner": *ownerFlag, + "draft": *draftFlag, + "visibility": visibility, + "autoSubmit": *autoSubmitFlag, + "mode": mode, + "recommended": *recommendedFlag, + } + + if len(tagIDs) > 0 { + input["tags"] = tagIDs + } + + var result struct { + CreatePrompt Prompt + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "input": input, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Prompt created: %s\n", result.CreatePrompt.ID) + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_delete.go b/cmd/src/prompts_delete.go new file mode 100644 index 0000000000..82082f5aa8 --- /dev/null +++ b/cmd/src/prompts_delete.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Delete a prompt by ID: + + $ src prompts delete -id= + +` + + flagSet := flag.NewFlagSet("delete", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + idFlag = flagSet.String("id", "", "The ID of the prompt to delete") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *idFlag == "" { + return errors.New("provide the ID of the prompt to delete") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation DeletePrompt($id: ID!) { + deletePrompt(id: $id) { + alwaysNil + } +} +` + + var result struct { + DeletePrompt struct { + AlwaysNil interface{} `json:"alwaysNil"` + } + } + + if ok, err := client.NewRequest(query, map[string]interface{}{ + "id": *idFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Println("Prompt deleted successfully.") + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_export.go b/cmd/src/prompts_export.go new file mode 100644 index 0000000000..5ca2081540 --- /dev/null +++ b/cmd/src/prompts_export.go @@ -0,0 +1,270 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/sourcegraph/src-cli/internal/api" +) + +type PromptsExport struct { + Version string `json:"version"` + Prompts []Prompt `json:"prompts"` + ExportDate string `json:"exportDate"` +} + +func init() { + usage := ` +Examples: + + Export all prompts to a file: + + $ src prompts export -o prompts.json + + Export prompts with specific tags: + + $ src prompts export -o prompts.json -tags=go,python + + Export with pretty JSON formatting: + + $ src prompts export -o prompts.json -format=pretty + + Export to stdout: + + $ src prompts export +` + + flagSet := flag.NewFlagSet("export", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + outputFlag = flagSet.String("o", "", "Output file path (defaults to stdout if not specified)") + tagsFlag = flagSet.String("tags", "", "Comma-separated list of tag names to filter by") + formatFlag = flagSet.String("format", "compact", "JSON format: 'pretty' or 'compact'") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + // Validate format flag + format := *formatFlag + if format != "pretty" && format != "compact" { + return fmt.Errorf("format must be either 'pretty' or 'compact'") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + // Parse tags into array + var tagNames []string + if *tagsFlag != "" { + tagNames = parseIDsFromString(*tagsFlag) + } + + // If tags are specified, first resolve tag names to IDs + var tagIDs []string + if len(tagNames) > 0 { + ids, err := resolveTagNamesToIDs(client, tagNames) + if err != nil { + return err + } + tagIDs = ids + } + + // Fetch all prompts using pagination + allPrompts, err := fetchAllPrompts(client, tagIDs) + if err != nil { + return err + } + + // Create export data structure + export := PromptsExport{ + Version: "1.0", + Prompts: allPrompts, + ExportDate: time.Now().UTC().Format(time.RFC3339), + } + + // Marshal to JSON + var jsonData []byte + var jsonErr error + if format == "pretty" { + jsonData, jsonErr = json.MarshalIndent(export, "", " ") + } else { + jsonData, jsonErr = json.Marshal(export) + } + + if jsonErr != nil { + return fmt.Errorf("error marshaling JSON: %w", jsonErr) + } + + // Determine output destination + var out io.Writer + if *outputFlag == "" { + out = flagSet.Output() + } else { + file, err := os.Create(*outputFlag) + if err != nil { + return fmt.Errorf("error creating output file: %w", err) + } + defer file.Close() + out = file + } + + // Write output + _, err = out.Write(jsonData) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + // Print summary if output is to a file + if *outputFlag != "" { + fmt.Printf("Exported %d prompts to %s\n", len(allPrompts), *outputFlag) + } + + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} + +// fetchAllPrompts fetches all prompts from the API using pagination +func fetchAllPrompts(client api.Client, tagIDs []string) ([]Prompt, error) { + var allPrompts []Prompt + after := "" + hasNextPage := true + + for hasNextPage { + // Build the query dynamically based on which parameters we have + queryStr := "query Prompts($first: Int!, $includeDrafts: Boolean" + promptsParams := "first: $first, includeDrafts: $includeDrafts" + + // Add optional parameters + if after != "" { + queryStr += ", $after: String" + promptsParams += ", after: $after" + } + if len(tagIDs) > 0 { + queryStr += ", $tags: [ID!]" + promptsParams += ", tags: $tags" + } + + // Close the query definition + queryStr += ") {" + + query := queryStr + ` + prompts( + ` + promptsParams + ` + ) { + totalCount + nodes { + ...PromptFields + } + pageInfo { + hasNextPage + endCursor + } + } +}` + promptFragment + + // Initialize variables with the required parameters + vars := map[string]interface{}{ + "first": 100, // Get max prompts per page + "includeDrafts": true, + } + + // Add optional parameters + if after != "" { + vars["after"] = after + } + if len(tagIDs) > 0 { + vars["tags"] = tagIDs + } + + var result struct { + Prompts struct { + TotalCount int `json:"totalCount"` + Nodes []Prompt + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + EndCursor string `json:"endCursor"` + } `json:"pageInfo"` + } + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } + + // Add current page prompts to the result + allPrompts = append(allPrompts, result.Prompts.Nodes...) + + // Update pagination info + hasNextPage = result.Prompts.PageInfo.HasNextPage + if hasNextPage { + after = result.Prompts.PageInfo.EndCursor + } + } + + return allPrompts, nil +} + +// resolveTagNamesToIDs resolves tag names to their IDs +func resolveTagNamesToIDs(client api.Client, tagNames []string) ([]string, error) { + // Query to get all tags + query := `query PromptTags { + promptTags { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + if ok, err := client.NewRequest(query, nil).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } + + // Create a map of tag names to IDs + tagNameToID := make(map[string]string) + for _, tag := range result.PromptTags.Nodes { + tagNameToID[tag.Name] = tag.ID + } + + // Resolve IDs for requested tag names + var tagIDs []string + var missingTags []string + for _, name := range tagNames { + if id, ok := tagNameToID[name]; ok { + tagIDs = append(tagIDs, id) + } else { + missingTags = append(missingTags, name) + } + } + + // If we have missing tags, return an error + if len(missingTags) > 0 { + return nil, fmt.Errorf("the following tags were not found: %v", missingTags) + } + + return tagIDs, nil +} diff --git a/cmd/src/prompts_get.go b/cmd/src/prompts_get.go new file mode 100644 index 0000000000..e0a832d276 --- /dev/null +++ b/cmd/src/prompts_get.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Get prompt details by ID: + + $ src prompts get + +` + + flagSet := flag.NewFlagSet("get", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if len(flagSet.Args()) != 1 { + return errors.New("provide exactly one prompt ID") + } + + promptID := flagSet.Arg(0) + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `query GetPrompt($id: ID!) { + node(id: $id) { + ... on Prompt { + ...PromptFields + } + } +} +` + promptFragment + + vars := map[string]interface{}{ + "id": promptID, + } + + var result struct { + Node *Prompt `json:"node"` + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + if result.Node == nil { + return errors.Newf("prompt not found: %s", promptID) + } + + p := result.Node + tagNames := []string{} + for _, tag := range p.Tags.Nodes { + tagNames = append(tagNames, tag.Name) + } + + fmt.Printf("ID: %s\n", p.ID) + fmt.Printf("Name: %s\n", p.Name) + fmt.Printf("Description: %s\n", p.Description) + fmt.Printf("Content: %s\n", p.Definition.Text) + fmt.Printf("Draft: %t\n", p.Draft) + fmt.Printf("Visibility: %s\n", p.Visibility) + fmt.Printf("Mode: %s\n", p.Mode) + fmt.Printf("Auto-submit: %t\n", p.AutoSubmit) + fmt.Printf("Recommended: %t\n", p.Recommended) + + if len(tagNames) > 0 { + fmt.Printf("Tags: %s\n", joinStrings(tagNames, ", ")) + } + + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_import.go b/cmd/src/prompts_import.go new file mode 100644 index 0000000000..c933033564 --- /dev/null +++ b/cmd/src/prompts_import.go @@ -0,0 +1,387 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +const importTagName = "src_cli_import" + +func init() { + usage := ` +Examples: + + Import prompts from a file (uses current user as owner): + + $ src prompts import -i prompts.json + + Import prompts with a specific owner: + + $ src prompts import -i prompts.json -owner= + + Perform a dry run without creating any prompts: + + $ src prompts import -i prompts.json -dry-run + + Skip existing prompts with the same name: + + $ src prompts import -i prompts.json -skip-existing + + Note: Prompts that already exist for the owner will be automatically skipped. +` + + flagSet := flag.NewFlagSet("import", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + inputFlag = flagSet.String("i", "", "Input file path (required)") + dryRunFlag = flagSet.Bool("dry-run", false, "Validate without importing") + skipExistingFlag = flagSet.Bool("skip-existing", false, "Skip prompts that already exist (based on name)") + ownerFlag = flagSet.String("owner", "", "The ID of the owner for all imported prompts (defaults to current user)") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *inputFlag == "" { + return errors.New("provide an input file path with -i") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + // If owner not specified, use the current user + owner := *ownerFlag + if owner == "" { + // Get the current user ID + currentUserID, err := getViewerUserID(context.Background(), client) + if err != nil { + return errors.New("unable to determine current user ID, please provide -owner explicitly") + } + owner = currentUserID + fmt.Printf("Using current user as owner (ID: %s)\n", owner) + } + + // Read the input file + file, err := os.Open(*inputFlag) + if err != nil { + return fmt.Errorf("error opening input file: %w", err) + } + defer file.Close() + + // Parse the JSON + data, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("error reading input file: %w", err) + } + + var export PromptsExport + if err := json.Unmarshal(data, &export); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + // Validate the export data + if export.Version == "" { + return errors.New("invalid export file: missing version") + } + + if len(export.Prompts) == 0 { + return errors.New("no prompts found in the export file") + } + + // Get or create the import tag + importTagID, err := getOrCreateTag(client, importTagName) + if err != nil { + return fmt.Errorf("error getting/creating import tag: %w", err) + } + + // Fetch all existing tags to build a mapping + tagNameToID, err := getTagMapping(client) + if err != nil { + return fmt.Errorf("error fetching existing tags: %w", err) + } + + // In case of skip-existing, get all existing prompt names + existingPromptNames := make(map[string]bool) + if *skipExistingFlag { + names, err := getAllPromptNames(client) + if err != nil { + return fmt.Errorf("error fetching existing prompts: %w", err) + } + existingPromptNames = names + } + + // Dry run message + if *dryRunFlag { + fmt.Printf("Dry run: would import %d prompts\n", len(export.Prompts)) + } + + // Process each prompt + var importedCount, skippedCount int + for _, prompt := range export.Prompts { + // Skip if prompt with same name exists and skip-existing is enabled + if *skipExistingFlag && existingPromptNames[prompt.Name] { + fmt.Printf("Skipping prompt '%s' as it already exists\n", prompt.Name) + skippedCount++ + continue + } + + // Process tags for this prompt + tagIDs := []string{importTagID} // Always add the import tag + for _, tag := range prompt.Tags.Nodes { + tagID, created, err := resolveTagID(client, tag.Name, tagNameToID) + if err != nil { + return fmt.Errorf("error resolving tag '%s': %w", tag.Name, err) + } + + if created { + // Update our mapping with the new tag + tagNameToID[tag.Name] = tagID + if !*dryRunFlag { + fmt.Printf("Created new tag: %s\n", tag.Name) + } + } + + tagIDs = append(tagIDs, tagID) + } + + // Skip actual creation in dry run mode + if *dryRunFlag { + fmt.Printf("Would import prompt: %s\n", prompt.Name) + importedCount++ + continue + } + + // Create the prompt + created, err := createPrompt(client, prompt, owner, tagIDs) + if err != nil { + // Check if this is a duplicate prompt error + if strings.Contains(err.Error(), "already exists") { + fmt.Printf("Skipping prompt '%s' as it already exists\n", prompt.Name) + skippedCount++ + continue + } + return fmt.Errorf("error creating prompt '%s': %w", prompt.Name, err) + } + + fmt.Printf("Imported prompt: %s (ID: %s)\n", prompt.Name, created.ID) + importedCount++ + } + + // Print summary + action := "Imported" + if *dryRunFlag { + action = "Would import" + } + fmt.Printf("%s %d prompts", action, importedCount) + if skippedCount > 0 { + fmt.Printf(" (skipped %d existing)", skippedCount) + } + fmt.Println() + + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} + +// getOrCreateTag gets a tag by name or creates it if it doesn't exist +func getOrCreateTag(client api.Client, name string) (string, error) { + // First try to get the tag by name + query := `query PromptTags($query: String!) { + promptTags(query: $query, first: 1) { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + vars := map[string]interface{}{ + "query": name, + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return "", err + } + + // If the tag exists, return its ID + if len(result.PromptTags.Nodes) > 0 { + return result.PromptTags.Nodes[0].ID, nil + } + + // Tag doesn't exist, create it + mutation := `mutation CreatePromptTag($input: PromptTagCreateInput!) { + createPromptTag(input: $input) { + ...PromptTagFields + } +}` + promptTagFragment + + var createResult struct { + CreatePromptTag PromptTag + } + + if ok, err := client.NewRequest(mutation, map[string]interface{}{ + "input": map[string]interface{}{ + "name": name, + }, + }).Do(context.Background(), &createResult); err != nil || !ok { + return "", err + } + + return createResult.CreatePromptTag.ID, nil +} + +// getTagMapping fetches all tags and returns a map of tag names to IDs +func getTagMapping(client api.Client) (map[string]string, error) { + query := `query PromptTags { + promptTags(first: 1000) { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + if ok, err := client.NewRequest(query, nil).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } + + tagMap := make(map[string]string) + for _, tag := range result.PromptTags.Nodes { + tagMap[tag.Name] = tag.ID + } + + return tagMap, nil +} + +// getAllPromptNames fetches all prompt names and returns them as a map for quick lookup +func getAllPromptNames(client api.Client) (map[string]bool, error) { + query := `query AllPromptNames { + prompts(first: 1000) { + nodes { + name + } + } +}` + + var result struct { + Prompts struct { + Nodes []struct { + Name string + } + } + } + + if ok, err := client.NewRequest(query, nil).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } + + nameMap := make(map[string]bool) + for _, prompt := range result.Prompts.Nodes { + nameMap[prompt.Name] = true + } + + return nameMap, nil +} + +// resolveTagID resolves a tag name to an ID, creating the tag if it doesn't exist +// Returns the tag ID, a boolean indicating whether a new tag was created, and an error +func resolveTagID(client api.Client, name string, tagMap map[string]string) (string, bool, error) { + // Check if we already have this tag + if id, ok := tagMap[name]; ok { + return id, false, nil + } + + // Create the tag + mutation := `mutation CreatePromptTag($input: PromptTagCreateInput!) { + createPromptTag(input: $input) { + ...PromptTagFields + } +}` + promptTagFragment + + var result struct { + CreatePromptTag PromptTag + } + + if ok, err := client.NewRequest(mutation, map[string]interface{}{ + "input": map[string]interface{}{ + "name": name, + }, + }).Do(context.Background(), &result); err != nil || !ok { + return "", false, err + } + + return result.CreatePromptTag.ID, true, nil +} + +// createPrompt creates a new prompt with the given properties +func createPrompt(client api.Client, prompt Prompt, ownerID string, tagIDs []string) (*Prompt, error) { + mutation := `mutation CreatePrompt( + $input: PromptInput! +) { + createPrompt(input: $input) { + ...PromptFields + } +}` + promptFragment + + // Build input from the prompt + input := map[string]interface{}{ + "name": prompt.Name, + "description": prompt.Description, + "definitionText": prompt.Definition.Text, + "owner": ownerID, + "draft": prompt.Draft, + "visibility": prompt.Visibility, + "autoSubmit": prompt.AutoSubmit, + "mode": prompt.Mode, + "recommended": prompt.Recommended, + "tags": tagIDs, + } + + var result struct { + CreatePrompt Prompt + } + + if ok, err := client.NewRequest(mutation, map[string]interface{}{ + "input": input, + }).Do(context.Background(), &result); err != nil || !ok { + // Check if this is a duplicate prompt error + if err != nil && (strings.Contains(err.Error(), "duplicate key value") || + strings.Contains(err.Error(), "prompts_name_is_unique_in_owner_user")) { + return nil, fmt.Errorf("a prompt with the name '%s' already exists for this owner", prompt.Name) + } + return nil, err + } + + return &result.CreatePrompt, nil +} diff --git a/cmd/src/prompts_list.go b/cmd/src/prompts_list.go new file mode 100644 index 0000000000..0deff2866f --- /dev/null +++ b/cmd/src/prompts_list.go @@ -0,0 +1,227 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + List all prompts: + + $ src prompts list + + Search prompts by name or contents: + + $ src prompts list -query="error handling" + + Filter prompts by tag: + + $ src prompts list -tags=id1,id2 + + List prompts for a specific owner: + + $ src prompts list -owner= + + Paginate through results: + + $ src prompts list -after= + +` + + flagSet := flag.NewFlagSet("list", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + queryFlag = flagSet.String("query", "", "Search prompts by name, description, or content") + ownerFlag = flagSet.String("owner", "", "Filter by prompt owner (a namespace, either a user or organization)") + tagsFlag = flagSet.String("tags", "", "Comma-separated list of tag IDs to filter by") + affilatedFlag = flagSet.Bool("affiliated", false, "Filter to only prompts owned by the viewer or viewer's organizations") + includeDraftsFlag = flagSet.Bool("include-drafts", true, "Whether to include draft prompts") + recommendedOnlyFlag = flagSet.Bool("recommended-only", false, "Whether to include only recommended prompts") + builtinOnlyFlag = flagSet.Bool("builtin-only", false, "Whether to include only builtin prompts") + includeBuiltinFlag = flagSet.Bool("include-builtin", false, "Whether to include builtin prompts") + limitFlag = flagSet.Int("limit", 100, "Maximum number of prompts to list") + afterFlag = flagSet.String("after", "", "Cursor for pagination (from previous page's endCursor)") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + // Parse tags into array + var tagIDs []string + if *tagsFlag != "" { + tagIDs = append(tagIDs, parseIDsFromString(*tagsFlag)...) + } + + // Build the query dynamically based on which parameters we have + queryStr := "query Prompts($first: Int!, $includeDrafts: Boolean" + promptsParams := "first: $first, includeDrafts: $includeDrafts" + + // Add optional parameters to the query + if *queryFlag != "" { + queryStr += ", $query: String" + promptsParams += ", query: $query" + } + if *ownerFlag != "" { + queryStr += ", $owner: ID" + promptsParams += ", owner: $owner" + } + if *affilatedFlag { + queryStr += ", $viewerIsAffiliated: Boolean" + promptsParams += ", viewerIsAffiliated: $viewerIsAffiliated" + } + if *recommendedOnlyFlag { + queryStr += ", $recommendedOnly: Boolean" + promptsParams += ", recommendedOnly: $recommendedOnly" + } + if *builtinOnlyFlag { + queryStr += ", $builtinOnly: Boolean" + promptsParams += ", builtinOnly: $builtinOnly" + } + if *includeBuiltinFlag { + queryStr += ", $includeBuiltin: Boolean" + promptsParams += ", includeBuiltin: $includeBuiltin" + } + if *afterFlag != "" { + queryStr += ", $after: String" + promptsParams += ", after: $after" + } + if len(tagIDs) > 0 { + queryStr += ", $tags: [ID!]" + promptsParams += ", tags: $tags" + } + + // Close the query definition + queryStr += ") {" + + query := queryStr + ` + prompts( + ` + promptsParams + ` + ) { + totalCount + nodes { + ...PromptFields + } + pageInfo { + hasNextPage + endCursor + } + } +}` + promptFragment + + // Initialize variables with the required parameters + vars := map[string]interface{}{ + "first": *limitFlag, + "includeDrafts": *includeDraftsFlag, + } + + // Only add optional parameters if they're provided + if *queryFlag != "" { + vars["query"] = *queryFlag + } + if *ownerFlag != "" { + vars["owner"] = *ownerFlag + } + if *affilatedFlag { + vars["viewerIsAffiliated"] = true + } + if *recommendedOnlyFlag { + vars["recommendedOnly"] = true + } + if *builtinOnlyFlag { + vars["builtinOnly"] = true + } + if *includeBuiltinFlag { + vars["includeBuiltin"] = true + } + if *afterFlag != "" { + vars["after"] = *afterFlag + } + if len(tagIDs) > 0 { + vars["tags"] = tagIDs + } + + var result struct { + Prompts struct { + TotalCount int `json:"totalCount"` + Nodes []Prompt + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + EndCursor string `json:"endCursor"` + } `json:"pageInfo"` + } + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Showing %d of %d prompts\n\n", len(result.Prompts.Nodes), result.Prompts.TotalCount) + + for _, p := range result.Prompts.Nodes { + tagNames := []string{} + for _, tag := range p.Tags.Nodes { + tagNames = append(tagNames, tag.Name) + } + + fmt.Printf("ID: %s\nName: %s\nDescription: %s\n", p.ID, p.Name, p.Description) + fmt.Printf("Draft: %t | Visibility: %s | Mode: %s\n", p.Draft, p.Visibility, p.Mode) + if len(tagNames) > 0 { + fmt.Printf("Tags: %s\n", joinStrings(tagNames, ", ")) + } + fmt.Println() + } + + if result.Prompts.PageInfo.HasNextPage { + fmt.Printf("More results available. Use -after=%s to fetch the next page.\n", result.Prompts.PageInfo.EndCursor) + } + + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} + +// Helper function to parse comma-separated IDs +func parseIDsFromString(s string) []string { + if s == "" { + return nil + } + + split := strings.Split(s, ",") + result := make([]string, 0, len(split)) + + for _, id := range split { + trimmed := strings.TrimSpace(id) + if trimmed != "" { + result = append(result, trimmed) + } + } + + return result +} + +// Helper function to join string slices +func joinStrings(s []string, sep string) string { + return strings.Join(s, sep) +} diff --git a/cmd/src/prompts_tags.go b/cmd/src/prompts_tags.go new file mode 100644 index 0000000000..3ae063de91 --- /dev/null +++ b/cmd/src/prompts_tags.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "fmt" +) + +var promptsTagsCommands commander + +func init() { + usage := `'src prompts tags' is a tool that manages prompt tags in a Sourcegraph instance. + +Usage: + + src prompts tags command [command options] + +The commands are: + + list lists prompt tags + get get a prompt tag by name + create create a prompt tag + update update a prompt tag + delete delete a prompt tag + +Use "src prompts tags [command] -h" for more information about a command. +` + + flagSet := flag.NewFlagSet("tags", flag.ExitOnError) + handler := func(args []string) error { + promptsTagsCommands.run(flagSet, "src prompts tags", usage, args) + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: func() { + fmt.Println(usage) + }, + }) +} diff --git a/cmd/src/prompts_tags_create.go b/cmd/src/prompts_tags_create.go new file mode 100644 index 0000000000..a869a185bc --- /dev/null +++ b/cmd/src/prompts_tags_create.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Create a new prompt tag: + + $ src prompts tags create go + + Note: If a tag with this name already exists, the command will return the existing tag's ID. + +` + + flagSet := flag.NewFlagSet("create", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts tags %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + // Check for tag name as positional argument + if len(args) == 0 { + return errors.New("provide a tag name as an argument") + } + + tagName := args[0] + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation CreatePromptTag($input: PromptTagCreateInput!) { + createPromptTag(input: $input) { + ...PromptTagFields + } +} +` + promptTagFragment + + var result struct { + CreatePromptTag PromptTag + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "input": map[string]interface{}{ + "name": tagName, + }, + }).Do(context.Background(), &result); err != nil || !ok { + // Check if this is a duplicate key error + if err != nil && (strings.Contains(err.Error(), "duplicate key value") || + strings.Contains(err.Error(), "unique constraint")) { + // Try to fetch the existing tag to provide more useful information + existingTag, fetchErr := getExistingTag(client, tagName) + if fetchErr == nil && existingTag != nil { + return fmt.Errorf("a tag with the name '%s' already exists (ID: %s)", tagName, existingTag.ID) + } + return fmt.Errorf("a tag with the name '%s' already exists", tagName) + } + return err + } + + fmt.Printf("Prompt tag created: %s\n", result.CreatePromptTag.ID) + return nil + } + + // Register the command. + promptsTagsCommands = append(promptsTagsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} + +// getExistingTag tries to fetch an existing tag by name +func getExistingTag(client api.Client, name string) (*PromptTag, error) { + query := `query PromptTags($query: String!) { + promptTags(query: $query) { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + vars := map[string]interface{}{ + "query": name, + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } + + if len(result.PromptTags.Nodes) == 0 { + return nil, nil + } + + for _, tag := range result.PromptTags.Nodes { + // Look for exact name match + if tag.Name == name { + return &tag, nil + } + } + + return nil, nil +} diff --git a/cmd/src/prompts_tags_delete.go b/cmd/src/prompts_tags_delete.go new file mode 100644 index 0000000000..aaa33c4a02 --- /dev/null +++ b/cmd/src/prompts_tags_delete.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Delete a prompt tag by ID: + + $ src prompts tags delete -id= + +` + + flagSet := flag.NewFlagSet("delete", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts tags %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + idFlag = flagSet.String("id", "", "The ID of the tag to delete") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *idFlag == "" { + return errors.New("provide the ID of the tag to delete") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation DeletePromptTag($id: ID!) { + deletePromptTag(id: $id) { + alwaysNil + } +} +` + + var result struct { + DeletePromptTag struct { + AlwaysNil interface{} `json:"alwaysNil"` + } + } + + if ok, err := client.NewRequest(query, map[string]interface{}{ + "id": *idFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Println("Prompt tag deleted successfully.") + return nil + } + + // Register the command. + promptsTagsCommands = append(promptsTagsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_tags_get.go b/cmd/src/prompts_tags_get.go new file mode 100644 index 0000000000..667172e2f6 --- /dev/null +++ b/cmd/src/prompts_tags_get.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Get a prompt tag by name: + + $ src prompts tags get go + +` + + flagSet := flag.NewFlagSet("get", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts tags %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + // Check for tag name as positional argument + if len(args) == 0 { + return errors.New("provide a tag name as an argument") + } + + tagName := args[0] + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `query PromptTags($query: String!) { + promptTags(query: $query, first: 1) { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + vars := map[string]interface{}{ + "query": tagName, + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + if len(result.PromptTags.Nodes) == 0 { + return fmt.Errorf("no tag found with name '%s'", tagName) + } + + // Display the tag information + tag := result.PromptTags.Nodes[0] + fmt.Printf("ID: %s\nName: %s\n", tag.ID, tag.Name) + + return nil + } + + // Register the command. + promptsTagsCommands = append(promptsTagsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_tags_list.go b/cmd/src/prompts_tags_list.go new file mode 100644 index 0000000000..9e5f33dc47 --- /dev/null +++ b/cmd/src/prompts_tags_list.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + List all prompt tags: + + $ src prompts tags list + + Search for prompt tags by name: + + $ src prompts tags list -query="go" + + Paginate through results: + + $ src prompts tags list -after= + +` + + flagSet := flag.NewFlagSet("list", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts tags %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + queryFlag = flagSet.String("query", "", "Search prompt tags by name") + limitFlag = flagSet.Int("limit", 100, "Maximum number of tags to list") + afterFlag = flagSet.String("after", "", "Cursor for pagination (from previous page's endCursor)") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + // Build query dynamically based on provided parameters + queryStr := "query PromptTags($first: Int!" + tagsParams := "first: $first" + + if *queryFlag != "" { + queryStr += ", $query: String" + tagsParams += ", query: $query" + } + + if *afterFlag != "" { + queryStr += ", $after: String" + tagsParams += ", after: $after" + } + + // Close the query definition + queryStr += ") {" + + query := queryStr + ` + promptTags( + ` + tagsParams + ` + ) { + totalCount + nodes { + ...PromptTagFields + } + pageInfo { + hasNextPage + endCursor + } + } +} +` + promptTagFragment + + // Initialize with required parameters + vars := map[string]interface{}{ + "first": *limitFlag, + } + + // Only add optional parameters when provided + if *queryFlag != "" { + vars["query"] = *queryFlag + } + if *afterFlag != "" { + vars["after"] = *afterFlag + } + + var result struct { + PromptTags struct { + TotalCount int `json:"totalCount"` + Nodes []PromptTag + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + EndCursor string `json:"endCursor"` + } `json:"pageInfo"` + } + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Showing %d of %d prompt tags\n\n", len(result.PromptTags.Nodes), result.PromptTags.TotalCount) + + for _, tag := range result.PromptTags.Nodes { + fmt.Printf("ID: %s\nName: %s\n\n", tag.ID, tag.Name) + } + + if result.PromptTags.PageInfo.HasNextPage { + fmt.Printf("More results available. Use -after=%s to fetch the next page.\n", result.PromptTags.PageInfo.EndCursor) + } + + return nil + } + + // Register the command. + promptsTagsCommands = append(promptsTagsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_tags_update.go b/cmd/src/prompts_tags_update.go new file mode 100644 index 0000000000..2fcf24f05b --- /dev/null +++ b/cmd/src/prompts_tags_update.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Update a prompt tag: + + $ src prompts tags update -id= -name="updated-tag-name" + +` + + flagSet := flag.NewFlagSet("update", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts tags %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + idFlag = flagSet.String("id", "", "The ID of the tag to update") + nameFlag = flagSet.String("name", "", "The new name for the tag") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *idFlag == "" { + return errors.New("provide the ID of the tag to update") + } + + if *nameFlag == "" { + return errors.New("provide a new name for the tag") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation UpdatePromptTag($id: ID!, $input: PromptTagUpdateInput!) { + updatePromptTag(id: $id, input: $input) { + ...PromptTagFields + } +} +` + promptTagFragment + + var result struct { + UpdatePromptTag PromptTag + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "id": *idFlag, + "input": map[string]interface{}{ + "name": *nameFlag, + }, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Prompt tag updated: %s\n", result.UpdatePromptTag.ID) + return nil + } + + // Register the command. + promptsTagsCommands = append(promptsTagsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} diff --git a/cmd/src/prompts_update.go b/cmd/src/prompts_update.go new file mode 100644 index 0000000000..a80a2ac626 --- /dev/null +++ b/cmd/src/prompts_update.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + "github.com/sourcegraph/src-cli/internal/api" +) + +func init() { + usage := ` +Examples: + + Update a prompt's description: + + $ src prompts update -id= -name="Updated Name" -description="Updated description" [-content="Updated content"] [-tags=id1,id2] [-draft=false] [-auto-submit=false] [-mode=CHAT] [-recommended=false] + +` + + flagSet := flag.NewFlagSet("update", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src prompts %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + idFlag = flagSet.String("id", "", "The ID of the prompt to update") + nameFlag = flagSet.String("name", "", "The updated prompt name") + descriptionFlag = flagSet.String("description", "", "Updated description of the prompt") + contentFlag = flagSet.String("content", "", "Updated prompt template text content") + tagsFlag = flagSet.String("tags", "", "Comma-separated list of tag IDs (replaces existing tags)") + draftFlag = flagSet.Bool("draft", false, "Whether the prompt is a draft") + autoSubmitFlag = flagSet.Bool("auto-submit", false, "Whether the prompt should be automatically executed in one click") + modeFlag = flagSet.String("mode", "CHAT", "Mode to execute prompt (CHAT, EDIT, or INSERT)") + recommendedFlag = flagSet.Bool("recommended", false, "Whether the prompt is recommended") + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + if *idFlag == "" { + return errors.New("provide the ID of the prompt to update") + } + + if *nameFlag == "" { + return errors.New("provide a name for the prompt") + } + + if *descriptionFlag == "" { + return errors.New("provide a description for the prompt") + } + + if *contentFlag == "" { + return errors.New("provide content for the prompt") + } + + // Validate mode + validModes := map[string]bool{"CHAT": true, "EDIT": true, "INSERT": true} + mode := strings.ToUpper(*modeFlag) + if !validModes[mode] { + return errors.New("mode must be one of: CHAT, EDIT, or INSERT") + } + + // Parse tags into array + var tagIDs []string + if *tagsFlag != "" { + tagIDs = strings.Split(*tagsFlag, ",") + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + query := `mutation UpdatePrompt( + $id: ID!, + $input: PromptUpdateInput! +) { + updatePrompt(id: $id, input: $input) { + ...PromptFields + } +} +` + promptFragment + + input := map[string]interface{}{ + "name": *nameFlag, + "description": *descriptionFlag, + "definitionText": *contentFlag, + "draft": *draftFlag, + "autoSubmit": *autoSubmitFlag, + "mode": mode, + "recommended": *recommendedFlag, + } + + if len(tagIDs) > 0 { + input["tags"] = tagIDs + } + + var result struct { + UpdatePrompt Prompt + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "id": *idFlag, + "input": input, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Prompt updated: %s\n", result.UpdatePrompt.ID) + return nil + } + + // Register the command. + promptsCommands = append(promptsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} From 0ba4140f3befe2ef3522b81c50c25d28dcbee806 Mon Sep 17 00:00:00 2001 From: Travis Lyons Date: Thu, 22 May 2025 15:03:58 -0400 Subject: [PATCH 2/4] feat: enhance prompt management with column selection and JSON output This commit introduces enhancements to the prompt management commands, allowing users to select specific columns for display and output results in JSON format. The following changes were made: - Added column selection functionality to `src prompts list` and `src prompts tags list` commands using the `-c` flag. Users can now specify a comma-separated list of columns to display. - Implemented JSON output functionality for `src prompts list` and `src prompts tags list` commands using the `-json` flag. - Updated `src prompts update`, `src prompts tags update`, `src prompts delete`, and `src prompts tags delete` commands to accept the ID as a positional argument instead of a flag. - Updated `src prompts export` to fetch all tags with pagination. --- cmd/src/prompts.go | 16 ++-- cmd/src/prompts_delete.go | 11 +-- cmd/src/prompts_export.go | 12 ++- cmd/src/prompts_list.go | 150 ++++++++++++++++++++++++++++++--- cmd/src/prompts_tags_delete.go | 11 +-- cmd/src/prompts_tags_list.go | 127 ++++++++++++++++++++++++++-- cmd/src/prompts_tags_update.go | 16 ++-- cmd/src/prompts_update.go | 11 +-- 8 files changed, 301 insertions(+), 53 deletions(-) diff --git a/cmd/src/prompts.go b/cmd/src/prompts.go index bb8a37d1f6..74a0c15952 100644 --- a/cmd/src/prompts.go +++ b/cmd/src/prompts.go @@ -16,14 +16,14 @@ Usage: The commands are: - list lists prompts - get get a prompt by ID - create create a prompt - update update a prompt - delete delete a prompt - export export prompts to a JSON file - import import prompts from a JSON file - tags manage prompt tags (use "src prompts tags [command] -h" for more info) + list lists prompts + get get a prompt by ID + create create a prompt + update update a prompt + delete delete a prompt + export export prompts to a JSON file + import import prompts from a JSON file + tags manage prompt tags (use "src prompts tags [command] -h" for more info) Use "src prompts [command] -h" for more information about a command. ` diff --git a/cmd/src/prompts_delete.go b/cmd/src/prompts_delete.go index 82082f5aa8..ee44cf38c1 100644 --- a/cmd/src/prompts_delete.go +++ b/cmd/src/prompts_delete.go @@ -16,7 +16,7 @@ Examples: Delete a prompt by ID: - $ src prompts delete -id= + $ src prompts delete ` @@ -27,7 +27,6 @@ Examples: fmt.Println(usage) } var ( - idFlag = flagSet.String("id", "", "The ID of the prompt to delete") apiFlags = api.NewFlags(flagSet) ) @@ -36,9 +35,11 @@ Examples: return err } - if *idFlag == "" { - return errors.New("provide the ID of the prompt to delete") + // Check for prompt ID as positional argument + if len(flagSet.Args()) != 1 { + return errors.New("provide exactly one prompt ID as an argument") } + promptID := flagSet.Arg(0) client := cfg.apiClient(apiFlags, flagSet.Output()) @@ -56,7 +57,7 @@ Examples: } if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": *idFlag, + "id": promptID, }).Do(context.Background(), &result); err != nil || !ok { return err } diff --git a/cmd/src/prompts_export.go b/cmd/src/prompts_export.go index 5ca2081540..44b76e5383 100644 --- a/cmd/src/prompts_export.go +++ b/cmd/src/prompts_export.go @@ -225,9 +225,9 @@ func fetchAllPrompts(client api.Client, tagIDs []string) ([]Prompt, error) { // resolveTagNamesToIDs resolves tag names to their IDs func resolveTagNamesToIDs(client api.Client, tagNames []string) ([]string, error) { - // Query to get all tags - query := `query PromptTags { - promptTags { + // Query to get all tags with required pagination parameter + query := `query PromptTags($first: Int!) { + promptTags(first: $first) { nodes { ...PromptTagFields } @@ -240,7 +240,11 @@ func resolveTagNamesToIDs(client api.Client, tagNames []string) ([]string, error } } - if ok, err := client.NewRequest(query, nil).Do(context.Background(), &result); err != nil || !ok { + vars := map[string]interface{}{ + "first": 1000, // Get enough tags to cover all possible names + } + + if ok, err := client.NewRequest(query, vars).Do(context.Background(), &result); err != nil || !ok { return nil, err } diff --git a/cmd/src/prompts_list.go b/cmd/src/prompts_list.go index 0deff2866f..842048a8aa 100644 --- a/cmd/src/prompts_list.go +++ b/cmd/src/prompts_list.go @@ -9,6 +9,126 @@ import ( "github.com/sourcegraph/src-cli/internal/api" ) +// availablePromptColumns defines the available column names for output +var availablePromptColumns = map[string]bool{ + "id": true, + "name": true, + "description": true, + "draft": true, + "visibility": true, + "mode": true, + "tags": true, +} + +// defaultPromptColumns defines the default columns to display +var defaultPromptColumns = []string{"id", "name", "visibility", "tags"} + +// displayPrompts formats and outputs multiple prompts +func displayPrompts(prompts []Prompt, columns []string, asJSON bool) error { + if asJSON { + return outputAsJSON(prompts) + } + + // Collect all data first to calculate column widths + allRows := make([][]string, 0, len(prompts)+1) + + // Add header row + headers := make([]string, 0, len(columns)) + for _, col := range columns { + headers = append(headers, strings.ToUpper(col)) + } + allRows = append(allRows, headers) + + // Collect all data rows + for _, p := range prompts { + row := make([]string, 0, len(columns)) + + // Prepare tag names for display + tagNames := []string{} + for _, tag := range p.Tags.Nodes { + tagNames = append(tagNames, tag.Name) + } + tagsStr := joinStrings(tagNames, ", ") + + for _, col := range columns { + switch col { + case "id": + row = append(row, p.ID) + case "name": + row = append(row, p.Name) + case "description": + row = append(row, p.Description) + case "draft": + row = append(row, fmt.Sprintf("%t", p.Draft)) + case "visibility": + row = append(row, p.Visibility) + case "mode": + row = append(row, p.Mode) + case "tags": + row = append(row, tagsStr) + } + } + allRows = append(allRows, row) + } + + // Calculate max width for each column + colWidths := make([]int, len(columns)) + for _, row := range allRows { + for i, cell := range row { + if len(cell) > colWidths[i] { + colWidths[i] = len(cell) + } + } + } + + // Print all rows with proper padding + for i, row := range allRows { + for j, cell := range row { + fmt.Print(cell) + // Add padding (at least 2 spaces between columns) + padding := colWidths[j] - len(cell) + 2 + fmt.Print(strings.Repeat(" ", padding)) + } + fmt.Println() + + // Add separator line after headers + if i == 0 { + for j, width := range colWidths { + fmt.Print(strings.Repeat("-", width)) + if j < len(colWidths)-1 { + fmt.Print(" ") + } + } + fmt.Println() + } + } + + return nil +} + +// parsePromptColumns parses and validates the columns flag +func parsePromptColumns(columnsFlag string) []string { + if columnsFlag == "" { + return defaultPromptColumns + } + + columns := strings.Split(columnsFlag, ",") + var validColumns []string + + for _, col := range columns { + col = strings.ToLower(strings.TrimSpace(col)) + if availablePromptColumns[col] { + validColumns = append(validColumns, col) + } + } + + if len(validColumns) == 0 { + return defaultPromptColumns + } + + return validColumns +} + func init() { usage := ` Examples: @@ -32,6 +152,14 @@ Examples: Paginate through results: $ src prompts list -after= + + Select specific columns to display: + + $ src prompts list -c id,name,visibility,tags + + Output results as JSON: + + $ src prompts list -json ` @@ -52,6 +180,8 @@ Examples: includeBuiltinFlag = flagSet.Bool("include-builtin", false, "Whether to include builtin prompts") limitFlag = flagSet.Int("limit", 100, "Maximum number of prompts to list") afterFlag = flagSet.String("after", "", "Cursor for pagination (from previous page's endCursor)") + columnsFlag = flagSet.String("c", strings.Join(defaultPromptColumns, ","), "Comma-separated list of columns to display. Available: id,name,description,draft,visibility,mode,tags") + jsonFlag = flagSet.Bool("json", false, "Output results as JSON for programmatic access") apiFlags = api.NewFlags(flagSet) ) @@ -171,24 +301,18 @@ Examples: return err } - fmt.Printf("Showing %d of %d prompts\n\n", len(result.Prompts.Nodes), result.Prompts.TotalCount) + // Parse columns + columns := parsePromptColumns(*columnsFlag) - for _, p := range result.Prompts.Nodes { - tagNames := []string{} - for _, tag := range p.Tags.Nodes { - tagNames = append(tagNames, tag.Name) - } + fmt.Printf("Showing %d of %d prompts\n\n", len(result.Prompts.Nodes), result.Prompts.TotalCount) - fmt.Printf("ID: %s\nName: %s\nDescription: %s\n", p.ID, p.Name, p.Description) - fmt.Printf("Draft: %t | Visibility: %s | Mode: %s\n", p.Draft, p.Visibility, p.Mode) - if len(tagNames) > 0 { - fmt.Printf("Tags: %s\n", joinStrings(tagNames, ", ")) - } - fmt.Println() + // Display prompts in tabular format + if err := displayPrompts(result.Prompts.Nodes, columns, *jsonFlag); err != nil { + return err } if result.Prompts.PageInfo.HasNextPage { - fmt.Printf("More results available. Use -after=%s to fetch the next page.\n", result.Prompts.PageInfo.EndCursor) + fmt.Printf("\nMore results available. Use -after=%s to fetch the next page.\n", result.Prompts.PageInfo.EndCursor) } return nil diff --git a/cmd/src/prompts_tags_delete.go b/cmd/src/prompts_tags_delete.go index aaa33c4a02..5954dbf2db 100644 --- a/cmd/src/prompts_tags_delete.go +++ b/cmd/src/prompts_tags_delete.go @@ -16,7 +16,7 @@ Examples: Delete a prompt tag by ID: - $ src prompts tags delete -id= + $ src prompts tags delete ` @@ -27,7 +27,6 @@ Examples: fmt.Println(usage) } var ( - idFlag = flagSet.String("id", "", "The ID of the tag to delete") apiFlags = api.NewFlags(flagSet) ) @@ -36,9 +35,11 @@ Examples: return err } - if *idFlag == "" { - return errors.New("provide the ID of the tag to delete") + // Check for tag ID as positional argument + if len(flagSet.Args()) != 1 { + return errors.New("provide exactly one tag ID as an argument") } + tagID := flagSet.Arg(0) client := cfg.apiClient(apiFlags, flagSet.Output()) @@ -56,7 +57,7 @@ Examples: } if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": *idFlag, + "id": tagID, }).Do(context.Background(), &result); err != nil || !ok { return err } diff --git a/cmd/src/prompts_tags_list.go b/cmd/src/prompts_tags_list.go index 9e5f33dc47..cbdfcdfc3d 100644 --- a/cmd/src/prompts_tags_list.go +++ b/cmd/src/prompts_tags_list.go @@ -4,10 +4,109 @@ import ( "context" "flag" "fmt" + "strings" "github.com/sourcegraph/src-cli/internal/api" ) +// availableTagColumns defines the available column names for output +var availableTagColumns = map[string]bool{ + "id": true, + "name": true, +} + +// defaultTagColumns defines the default columns to display +var defaultTagColumns = []string{"id", "name"} + +// displayPromptTags formats and outputs multiple prompt tags +func displayPromptTags(tags []PromptTag, columns []string, asJSON bool) error { + if asJSON { + return outputAsJSON(tags) + } + + // Collect all data first to calculate column widths + allRows := make([][]string, 0, len(tags)+1) + + // Add header row + headers := make([]string, 0, len(columns)) + for _, col := range columns { + headers = append(headers, strings.ToUpper(col)) + } + allRows = append(allRows, headers) + + // Collect all data rows + for _, tag := range tags { + row := make([]string, 0, len(columns)) + + for _, col := range columns { + switch col { + case "id": + row = append(row, tag.ID) + case "name": + row = append(row, tag.Name) + } + } + allRows = append(allRows, row) + } + + // Calculate max width for each column + colWidths := make([]int, len(columns)) + for _, row := range allRows { + for i, cell := range row { + if len(cell) > colWidths[i] { + colWidths[i] = len(cell) + } + } + } + + // Print all rows with proper padding + for i, row := range allRows { + for j, cell := range row { + fmt.Print(cell) + // Add padding (at least 2 spaces between columns) + padding := colWidths[j] - len(cell) + 2 + fmt.Print(strings.Repeat(" ", padding)) + } + fmt.Println() + + // Add separator line after headers + if i == 0 { + for j, width := range colWidths { + fmt.Print(strings.Repeat("-", width)) + if j < len(colWidths)-1 { + fmt.Print(" ") + } + } + fmt.Println() + } + } + + return nil +} + +// parseTagColumns parses and validates the columns flag +func parseTagColumns(columnsFlag string) []string { + if columnsFlag == "" { + return defaultTagColumns + } + + columns := strings.Split(columnsFlag, ",") + var validColumns []string + + for _, col := range columns { + col = strings.ToLower(strings.TrimSpace(col)) + if availableTagColumns[col] { + validColumns = append(validColumns, col) + } + } + + if len(validColumns) == 0 { + return defaultTagColumns + } + + return validColumns +} + func init() { usage := ` Examples: @@ -23,6 +122,14 @@ Examples: Paginate through results: $ src prompts tags list -after= + + Select specific columns to display: + + $ src prompts tags list -c id,name + + Output results as JSON: + + $ src prompts tags list -json ` @@ -33,10 +140,12 @@ Examples: fmt.Println(usage) } var ( - queryFlag = flagSet.String("query", "", "Search prompt tags by name") - limitFlag = flagSet.Int("limit", 100, "Maximum number of tags to list") - afterFlag = flagSet.String("after", "", "Cursor for pagination (from previous page's endCursor)") - apiFlags = api.NewFlags(flagSet) + queryFlag = flagSet.String("query", "", "Search prompt tags by name") + limitFlag = flagSet.Int("limit", 100, "Maximum number of tags to list") + afterFlag = flagSet.String("after", "", "Cursor for pagination (from previous page's endCursor)") + columnsFlag = flagSet.String("c", strings.Join(defaultTagColumns, ","), "Comma-separated list of columns to display. Available: id,name") + jsonFlag = flagSet.Bool("json", false, "Output results as JSON for programmatic access") + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -107,14 +216,18 @@ Examples: return err } + // Parse columns + columns := parseTagColumns(*columnsFlag) + fmt.Printf("Showing %d of %d prompt tags\n\n", len(result.PromptTags.Nodes), result.PromptTags.TotalCount) - for _, tag := range result.PromptTags.Nodes { - fmt.Printf("ID: %s\nName: %s\n\n", tag.ID, tag.Name) + // Display tags in tabular format + if err := displayPromptTags(result.PromptTags.Nodes, columns, *jsonFlag); err != nil { + return err } if result.PromptTags.PageInfo.HasNextPage { - fmt.Printf("More results available. Use -after=%s to fetch the next page.\n", result.PromptTags.PageInfo.EndCursor) + fmt.Printf("\nMore results available. Use -after=%s to fetch the next page.\n", result.PromptTags.PageInfo.EndCursor) } return nil diff --git a/cmd/src/prompts_tags_update.go b/cmd/src/prompts_tags_update.go index 2fcf24f05b..7245e32a37 100644 --- a/cmd/src/prompts_tags_update.go +++ b/cmd/src/prompts_tags_update.go @@ -16,7 +16,7 @@ Examples: Update a prompt tag: - $ src prompts tags update -id= -name="updated-tag-name" + $ src prompts tags update -name="updated-tag-name" ` @@ -27,7 +27,6 @@ Examples: fmt.Println(usage) } var ( - idFlag = flagSet.String("id", "", "The ID of the tag to update") nameFlag = flagSet.String("name", "", "The new name for the tag") apiFlags = api.NewFlags(flagSet) ) @@ -37,12 +36,17 @@ Examples: return err } - if *idFlag == "" { - return errors.New("provide the ID of the tag to update") + // Check for tag ID as positional argument + if len(flagSet.Args()) != 1 { + if len(flagSet.Args()) == 0 { + return errors.New("provide exactly one tag ID as an argument") + } + return errors.New("provide exactly one tag ID as an argument (flags must come before positional arguments)") } + tagID := flagSet.Arg(0) if *nameFlag == "" { - return errors.New("provide a new name for the tag") + return errors.New("provide a new name for the tag using -name flag (flags must come before positional arguments)") } client := cfg.apiClient(apiFlags, flagSet.Output()) @@ -58,7 +62,7 @@ Examples: UpdatePromptTag PromptTag } if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": *idFlag, + "id": tagID, "input": map[string]interface{}{ "name": *nameFlag, }, diff --git a/cmd/src/prompts_update.go b/cmd/src/prompts_update.go index a80a2ac626..22d1dfbf6a 100644 --- a/cmd/src/prompts_update.go +++ b/cmd/src/prompts_update.go @@ -17,7 +17,7 @@ Examples: Update a prompt's description: - $ src prompts update -id= -name="Updated Name" -description="Updated description" [-content="Updated content"] [-tags=id1,id2] [-draft=false] [-auto-submit=false] [-mode=CHAT] [-recommended=false] + $ src prompts update -name="Updated Name" -description="Updated description" [-content="Updated content"] [-tags=id1,id2] [-draft=false] [-auto-submit=false] [-mode=CHAT] [-recommended=false] ` @@ -28,7 +28,6 @@ Examples: fmt.Println(usage) } var ( - idFlag = flagSet.String("id", "", "The ID of the prompt to update") nameFlag = flagSet.String("name", "", "The updated prompt name") descriptionFlag = flagSet.String("description", "", "Updated description of the prompt") contentFlag = flagSet.String("content", "", "Updated prompt template text content") @@ -45,9 +44,11 @@ Examples: return err } - if *idFlag == "" { - return errors.New("provide the ID of the prompt to update") + // Check for prompt ID as positional argument + if len(flagSet.Args()) != 1 { + return errors.New("provide exactly one prompt ID as an argument") } + promptID := flagSet.Arg(0) if *nameFlag == "" { return errors.New("provide a name for the prompt") @@ -104,7 +105,7 @@ Examples: UpdatePrompt Prompt } if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": *idFlag, + "id": promptID, "input": input, }).Do(context.Background(), &result); err != nil || !ok { return err From 0e8aa2e00c0f366010433b80717c6b039b82f455 Mon Sep 17 00:00:00 2001 From: Travis Lyons Date: Thu, 22 May 2025 16:51:40 -0400 Subject: [PATCH 3/4] feat(prompts): set current user as default owner for prompt creation This commit modifies the `src prompts create` command to default the prompt owner to the current user if no owner is explicitly specified via the `-owner` flag. --- cmd/src/prompts_create.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/src/prompts_create.go b/cmd/src/prompts_create.go index 694836ffa1..85e75c010b 100644 --- a/cmd/src/prompts_create.go +++ b/cmd/src/prompts_create.go @@ -33,7 +33,7 @@ Examples: nameFlag = flagSet.String("name", "", "The prompt name") descriptionFlag = flagSet.String("description", "", "Description of the prompt") contentFlag = flagSet.String("content", "", "The prompt template text content") - ownerFlag = flagSet.String("owner", "", "The ID of the owner (user or organization)") + ownerFlag = flagSet.String("owner", "", "The ID of the owner (user or organization). Defaults to current user if not specified.") tagsFlag = flagSet.String("tags", "", "Comma-separated list of tag IDs") draftFlag = flagSet.Bool("draft", false, "Whether the prompt is a draft") visibilityFlag = flagSet.String("visibility", "PUBLIC", "Visibility of the prompt (PUBLIC or SECRET)") @@ -57,8 +57,16 @@ Examples: if *contentFlag == "" { return errors.New("provide content for the prompt") } - if *ownerFlag == "" { - return errors.New("provide an owner ID for the prompt") + client := cfg.apiClient(apiFlags, flagSet.Output()) + + // Use current user as default owner if not specified + ownerID := *ownerFlag + if ownerID == "" { + var err error + ownerID, err = getViewerUserID(context.Background(), client) + if err != nil { + return errors.Wrap(err, "failed to get current user ID") + } } // Validate mode @@ -81,8 +89,6 @@ Examples: tagIDs = strings.Split(*tagsFlag, ",") } - client := cfg.apiClient(apiFlags, flagSet.Output()) - query := `mutation CreatePrompt( $input: PromptInput! ) { @@ -96,7 +102,7 @@ Examples: "name": *nameFlag, "description": *descriptionFlag, "definitionText": *contentFlag, - "owner": *ownerFlag, + "owner": ownerID, "draft": *draftFlag, "visibility": visibility, "autoSubmit": *autoSubmitFlag, From 26a34ccb799f47fdda9cf4c8ce24e0624e03afd3 Mon Sep 17 00:00:00 2001 From: Travis Lyons Date: Tue, 27 May 2025 09:26:06 -0400 Subject: [PATCH 4/4] fix(prompts): align prompt tags command descriptions --- cmd/src/prompts_tags.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/src/prompts_tags.go b/cmd/src/prompts_tags.go index 3ae063de91..57c3db616a 100644 --- a/cmd/src/prompts_tags.go +++ b/cmd/src/prompts_tags.go @@ -16,11 +16,11 @@ Usage: The commands are: - list lists prompt tags - get get a prompt tag by name - create create a prompt tag - update update a prompt tag - delete delete a prompt tag + list lists prompt tags + get get a prompt tag by name + create create a prompt tag + update update a prompt tag + delete delete a prompt tag Use "src prompts tags [command] -h" for more information about a command. `