From 1183be978b6285b3e9c01c21fd8767c504cb7f22 Mon Sep 17 00:00:00 2001 From: Cliff Tawiah Date: Thu, 22 Oct 2026 22:05:57 +0000 Subject: [PATCH] feat: add the create flags of sync prompts add sync prompts add takes --content, --model-config-key, and --name, and --yes, which skips the confirmation of a create. --content turns the add into a create. The help says how the content becomes the prompt of an agent or a completion config, and has an example of a create without prompting. The project-key/config-key/variation-key form still adds variations that exist, and a create with that form is an error. The error for a variation that LaunchDarkly does not have now names the add that creates it, with --model-config-key and --content. --- cmd/sync/attachments.go | 3 ++- cmd/sync/prompt.go | 24 +++++++++++++++----- cmd/sync/scope_test.go | 20 ++++++++++++----- internal/sync/bootstrap/bootstrap.go | 3 ++- internal/sync/bootstrap/bootstrap_test.go | 13 +++++++++++ internal/sync/prompt/create_test.go | 27 +++++++++++++++++++++++ 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/cmd/sync/attachments.go b/cmd/sync/attachments.go index 9e8b706a..827c9cf0 100644 --- a/cmd/sync/attachments.go +++ b/cmd/sync/attachments.go @@ -151,7 +151,8 @@ func createInput(cmd *cobra.Command) *synccreate.Input { return &text } input := synccreate.Input{ - Content: value(contentFlag), JSONSchema: value(jsonSchemaFlag), Name: value(nameFlag), Description: value(descriptionFlag), + Content: value(contentFlag), JSONSchema: value(jsonSchemaFlag), Name: value(nameFlag), + Description: value(descriptionFlag), ModelConfigKey: value(modelConfigKeyFlag), } if !set { return nil diff --git a/cmd/sync/prompt.go b/cmd/sync/prompt.go index a7d6b037..9b69806b 100644 --- a/cmd/sync/prompt.go +++ b/cmd/sync/prompt.go @@ -153,15 +153,23 @@ func newWatchCmd(client resources.Client, scope syncdomain.Scope, help targetFla func newAddCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ Use: "add [project-key/config-key/variation-key...]", - Short: "Add prompt variations from LaunchDarkly", + Short: "Add prompt variations from LaunchDarkly, or create a variation", + Long: "Add prompt variations from LaunchDarkly, or create a variation. " + + "Without --content, add starts syncing variations that exist in LaunchDarkly. " + + "With --content, add creates the variation in an existing config. For an agent config, the content is the instructions. " + + "For a completion config, the content is role blocks such as ..., or plain text for one system message. " + + "The project-key/config-key/variation-key form still works.", Example: ` # Choose variations interactively ldcli sync prompts add - # Choose variations in one config - ldcli sync prompts add --project production --config-key support - # Add variations by key without prompting - ldcli sync prompts add --project production --config-key support --key default --key concise --no-input`, + ldcli sync prompts add --project production --config-key support --key default --key concise --no-input + + # Create an agent variation without prompting + ldcli sync prompts add --project production --config-key support --key escalation \ + --model-config-key claude \ + --content "Hand the conversation to a person when the customer asks for one." \ + --yes --no-input`, Args: validatedArgs(cobra.MinimumNArgs(0)), RunE: runCommand(client, syncdomain.ScopePrompts, func(cmd *cobra.Command, args []string) (syncprompt.CommandAction, error) { variations, err := parseVariationSelectors(args) @@ -169,12 +177,16 @@ func newAddCmd(client resources.Client) *cobra.Command { return nil, err } dryRun, _ := cmd.Flags().GetBool(dryRunFlag) - return syncprompt.AddAction{Resources: variations, Filter: targetFilter(cmd), DryRun: dryRun}, nil + return syncprompt.AddAction{Resources: variations, Filter: targetFilter(cmd), DryRun: dryRun, Create: createInput(cmd)}, nil }), } addTargetFlags(cmd, questionFlagHelp) + cmd.Flags().String(contentFlag, "", "Prompt of a variation that add creates") + cmd.Flags().String(modelConfigKeyFlag, "", "Model config of a variation that add creates") + cmd.Flags().String(nameFlag, "", "Name of a variation that add creates. The default comes from the key.") cmd.Flags().Bool(dryRunFlag, false, "Preview local files without creating them") cmd.Flags().Bool(diffFlag, false, "Show full diffs and file content in the plan") + cmd.Flags().Bool(yesFlag, false, "Create without confirmation") cmd.Flags().Bool(noInputFlag, false, "Fail instead of prompting for missing input") return cmd } diff --git a/cmd/sync/scope_test.go b/cmd/sync/scope_test.go index c2b036c2..a75d894c 100644 --- a/cmd/sync/scope_test.go +++ b/cmd/sync/scope_test.go @@ -15,11 +15,13 @@ import ( // syncCommandFlags is the exact flag set of every sync command that runs. A // new command, or a new question without a flag, must change this list. var syncCommandFlags = map[string][]string{ - "sync all": {"conflict", "diff", "dry-run", "no-input", "project", "yes"}, - "sync all watch": {"conflict", "diff", "no-input", "project", "yes"}, - "sync prompts": {"config-key", "conflict", "diff", "dry-run", "key", "no-input", "project", "resolve", "yes"}, - "sync prompts watch": {"config-key", "conflict", "diff", "key", "no-input", "project", "resolve", "yes"}, - "sync prompts add": {"config-key", "diff", "dry-run", "key", "no-input", "project"}, + "sync all": {"conflict", "diff", "dry-run", "no-input", "project", "yes"}, + "sync all watch": {"conflict", "diff", "no-input", "project", "yes"}, + "sync prompts": {"config-key", "conflict", "diff", "dry-run", "key", "no-input", "project", "resolve", "yes"}, + "sync prompts watch": {"config-key", "conflict", "diff", "key", "no-input", "project", "resolve", "yes"}, + "sync prompts add": { + "config-key", "content", "diff", "dry-run", "key", "model-config-key", "name", "no-input", "project", "yes", + }, "sync prompts attach tool": {"conflict", "diff", "no-input", "project", "resolve", "to", "yes"}, "sync prompts attach skill": {"conflict", "diff", "no-input", "project", "resolve", "to", "yes"}, "sync prompts detach": {"archive", "config-key", "key", "no-input", "project", "yes"}, @@ -102,6 +104,14 @@ func TestAddReadsTheCreateInputFromTheFlagsThatAreSet(t *testing.T) { assert.Equal(t, "", *input.Description) assert.Nil(t, input.Name) assert.Nil(t, input.JSONSchema) + + prompts, _, err := root.Find([]string{"prompts", "add"}) + require.NoError(t, err) + require.NoError(t, prompts.ParseFlags([]string{"--content", "Escalate.", "--model-config-key", "claude"})) + input = createInput(prompts) + require.NotNil(t, input) + assert.Equal(t, "claude", *input.ModelConfigKey) + assert.Nil(t, input.Name) } func TestSyncCommandScopeNamesTheKindOfTheCommand(t *testing.T) { diff --git a/internal/sync/bootstrap/bootstrap.go b/internal/sync/bootstrap/bootstrap.go index 6c0063cf..7eb1ab94 100644 --- a/internal/sync/bootstrap/bootstrap.go +++ b/internal/sync/bootstrap/bootstrap.go @@ -115,7 +115,8 @@ func selectedVariations(options Options) ([]synclocal.VariationFile, error) { }) if index < 0 { return nil, fmt.Errorf( - "variation %q does not exist in config %q in project %q", variationKey, configKey, selection.ProjectKey, + "variation %q does not exist in config %q in project %q. To create it, run %s with --model-config-key and --content.", + variationKey, configKey, selection.ProjectKey, AddCommand(selection), ) } diff --git a/internal/sync/bootstrap/bootstrap_test.go b/internal/sync/bootstrap/bootstrap_test.go index 8851b517..8c451146 100644 --- a/internal/sync/bootstrap/bootstrap_test.go +++ b/internal/sync/bootstrap/bootstrap_test.go @@ -177,6 +177,19 @@ func TestRunUsesExplicitVariationSelectorsWithoutTerminal(t *testing.T) { assert.Contains(t, output.String(), " .launchdarkly/project/configs/config/variation.prompt.md +9") } +func TestRunNamesTheCreateFlagsOfAMissingVariation(t *testing.T) { + catalog := &fakeCatalog{config: syncapi.Config{Key: "support", Mode: syncdomain.VariationModeAgent}} + + err := Run(Options{ + Catalog: catalog, Store: synclocal.NewStore(t.TempDir()), Output: &bytes.Buffer{}, + Selections: []syncdomain.ResourceID{syncdomain.VariationID("demo", "support", "escalation")}, + }) + + require.EqualError(t, err, `variation "escalation" does not exist in config "support" in project "demo". `+ + "To create it, run ldcli sync prompts add --project demo --config-key support --key escalation "+ + "with --model-config-key and --content.") +} + func TestFinishSelectionDryRunDoesNotCreateFiles(t *testing.T) { root := t.TempDir() var output bytes.Buffer diff --git a/internal/sync/prompt/create_test.go b/internal/sync/prompt/create_test.go index 20fc0aff..810ede1e 100644 --- a/internal/sync/prompt/create_test.go +++ b/internal/sync/prompt/create_test.go @@ -786,6 +786,33 @@ func TestCreateVariationRemovesTheFileWhenLaunchDarklyRejectsTheCreate(t *testin assert.NoDirExists(t, filepath.Join(root, syncdomain.RootDir)) } +func TestPromptsAddCreatesAVariationFromItsFlags(t *testing.T) { + root := initRepository(t) + api := &directAPI{modelConfigs: []syncapi.ModelConfig{claude}} + + _, stderr, err := runPrompt(t, root, api, "add", "--project", "production", "--config-key", "support", "--key", "escalation", + "--model-config-key", "claude", "--name", "Escalate to a person", "--content", "Hand the conversation to a person.", + "--yes", "--no-input") + + require.NoError(t, err) + assert.Contains(t, stderr, "Note: Created .launchdarkly/production/"+escalationFile+".\n") + require.NotNil(t, api.variation) + assert.Equal(t, "Escalate to a person", api.variation.Name) + assert.Equal(t, "Hand the conversation to a person.", api.variation.Instructions) +} + +func TestPromptsAddRefusesASelectorWithCreateFlags(t *testing.T) { + root := initRepository(t) + api := &directAPI{} + + _, _, err := runPrompt(t, root, api, "add", "production/support/escalation", + "--model-config-key", "claude", "--content", "Escalate.", "--yes", "--no-input") + + require.ErrorContains(t, err, "a variation that add creates comes from --project, --config-key, and --key, "+ + "not from project-key/config-key/variation-key arguments") + assert.Empty(t, api.requests) +} + // createVariationAction is the add that creates the variation key in the // config "support" of the project "production". func createVariationAction(key string, input synccreate.Input) syncprompt.AddAction {