diff --git a/cmd/src/prompts.go b/cmd/src/prompts.go new file mode 100644 index 0000000000..74a0c15952 --- /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..85e75c010b --- /dev/null +++ b/cmd/src/prompts_create.go @@ -0,0 +1,136 @@ +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). 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)") + 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") + } + 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 + 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, ",") + } + + query := `mutation CreatePrompt( + $input: PromptInput! +) { + createPrompt(input: $input) { + ...PromptFields + } +} +` + promptFragment + + input := map[string]interface{}{ + "name": *nameFlag, + "description": *descriptionFlag, + "definitionText": *contentFlag, + "owner": ownerID, + "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..ee44cf38c1 --- /dev/null +++ b/cmd/src/prompts_delete.go @@ -0,0 +1,75 @@ +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 + +` + + 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 ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + // 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()) + + 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": promptID, + }).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..44b76e5383 --- /dev/null +++ b/cmd/src/prompts_export.go @@ -0,0 +1,274 @@ +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 with required pagination parameter + query := `query PromptTags($first: Int!) { + promptTags(first: $first) { + nodes { + ...PromptTagFields + } + } +}` + promptTagFragment + + var result struct { + PromptTags struct { + Nodes []PromptTag + } + } + + 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 + } + + // 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..842048a8aa --- /dev/null +++ b/cmd/src/prompts_list.go @@ -0,0 +1,351 @@ +package main + +import ( + "context" + "flag" + "fmt" + "strings" + + "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: + + 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= + + Select specific columns to display: + + $ src prompts list -c id,name,visibility,tags + + Output results as JSON: + + $ src prompts list -json + +` + + 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)") + 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) + ) + + 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 + } + + // Parse columns + columns := parsePromptColumns(*columnsFlag) + + fmt.Printf("Showing %d of %d prompts\n\n", len(result.Prompts.Nodes), result.Prompts.TotalCount) + + // Display prompts in tabular format + if err := displayPrompts(result.Prompts.Nodes, columns, *jsonFlag); err != nil { + return err + } + + if result.Prompts.PageInfo.HasNextPage { + fmt.Printf("\nMore 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..57c3db616a --- /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..5954dbf2db --- /dev/null +++ b/cmd/src/prompts_tags_delete.go @@ -0,0 +1,75 @@ +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 + +` + + 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 ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + // 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()) + + 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": tagID, + }).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..cbdfcdfc3d --- /dev/null +++ b/cmd/src/prompts_tags_list.go @@ -0,0 +1,242 @@ +package main + +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: + + 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= + + Select specific columns to display: + + $ src prompts tags list -c id,name + + Output results as JSON: + + $ src prompts tags list -json + +` + + 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)") + 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 { + 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 + } + + // Parse columns + columns := parseTagColumns(*columnsFlag) + + fmt.Printf("Showing %d of %d prompt tags\n\n", len(result.PromptTags.Nodes), result.PromptTags.TotalCount) + + // Display tags in tabular format + if err := displayPromptTags(result.PromptTags.Nodes, columns, *jsonFlag); err != nil { + return err + } + + if result.PromptTags.PageInfo.HasNextPage { + fmt.Printf("\nMore 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..7245e32a37 --- /dev/null +++ b/cmd/src/prompts_tags_update.go @@ -0,0 +1,83 @@ +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 -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 ( + 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 + } + + // 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 using -name flag (flags must come before positional arguments)") + } + + 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": tagID, + "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..22d1dfbf6a --- /dev/null +++ b/cmd/src/prompts_update.go @@ -0,0 +1,124 @@ +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 -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 ( + 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 + } + + // 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") + } + + 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": promptID, + "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, + }) +}