diff --git a/cli/src/commands/variable/variable.ts b/cli/src/commands/variable/variable.ts index 8d95db5b2b..dec35fc4f0 100644 --- a/cli/src/commands/variable/variable.ts +++ b/cli/src/commands/variable/variable.ts @@ -185,7 +185,13 @@ async function push( } async function add( - opts: GlobalOptions & { public?: boolean; plainSecrets?: boolean }, + opts: GlobalOptions & { + public?: boolean; + plainSecrets?: boolean; + yes?: boolean; + secret?: boolean; + description?: string; + }, value: string, remotePath: string ) { @@ -196,6 +202,10 @@ async function add( return; } + // --secret/--no-secret take precedence over the legacy --public flag; + // undefined means "secret on create, preserve current setting on update" + const isSecret = opts.secret ?? (opts.public ? false : undefined); + if ( await wmill.existsVariable({ workspace: workspace.workspaceId, @@ -203,6 +213,7 @@ async function add( }) ) { if ( + !opts.yes && !(await Confirm.prompt({ message: `Variable already exist, do you want to update its value?`, default: true, @@ -210,22 +221,46 @@ async function add( ) { return; } + if (isSecret === false) { + const existing = await wmill.getVariable({ + workspace: workspace.workspaceId, + path: remotePath, + decryptSecret: false, + }); + if (existing.is_secret) { + log.warn( + colors.yellow( + `Variable ${remotePath} is currently secret and will be downgraded to non-secret: its value will be stored in plaintext` + ) + ); + } + } log.info(colors.bold.yellow("Updating variable...")); + await wmill.updateVariable({ + workspace: workspace.workspaceId, + path: remotePath, + alreadyEncrypted: false, // value from CLI is always plaintext + requestBody: { + value, + ...(isSecret !== undefined ? { is_secret: isSecret } : {}), + ...(opts.description !== undefined + ? { description: opts.description } + : {}), + }, + }); + } else { + log.info(colors.bold.yellow("Creating variable...")); + await wmill.createVariable({ + workspace: workspace.workspaceId, + alreadyEncrypted: false, // value from CLI is always plaintext + requestBody: { + path: remotePath, + value, + is_secret: isSecret ?? true, + description: opts.description ?? "", + }, + }); } - - log.info(colors.bold.yellow("Pushing variable...")); - - await pushVariable( - workspace.workspaceId, - remotePath + ".variable.yaml", - undefined, - { - value, - is_secret: !opts.public, - description: "", - }, - true // value from CLI is always plaintext — tell API not to treat it as pre-encrypted - ); log.info(colors.bold.underline.green(`Variable ${remotePath} pushed`)); } @@ -255,8 +290,24 @@ const command = new Command() "Create a new variable on the remote. This will update the variable if it already exists." ) .arguments(" ") + .option( + "--yes", + "Skip confirmation prompt when updating an existing variable" + ) + .option( + "--secret", + "Mark the variable as secret (default when creating a new variable)" + ) + .option( + "--no-secret", + "Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)" + ) + .option( + "--description ", + "Set the variable description (when updating, the existing description is preserved if not passed)" + ) .option("--plain-secrets", "Push secrets as plain text") - .option("--public", "Legacy option, use --plain-secrets instead") + .option("--public", "Legacy option, use --no-secret instead") .action(add as any); diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index c1c882993e..1071331822 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -6705,8 +6705,12 @@ variable related commands - \`variable push \` - Push a local variable spec. This overrides any remote versions. - \`--plain-secrets\` - Push secrets as plain text - \`variable add \` - Create a new variable on the remote. This will update the variable if it already exists. + - \`--yes\` - Skip confirmation prompt when updating an existing variable + - \`--secret\` - Mark the variable as secret (default when creating a new variable) + - \`--no-secret\` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed) + - \`--description \` - Set the variable description (when updating, the existing description is preserved if not passed) - \`--plain-secrets\` - Push secrets as plain text - - \`--public\` - Legacy option, use --plain-secrets instead + - \`--public\` - Legacy option, use --no-secret instead ### version diff --git a/cli/test/variable_resource_push.test.ts b/cli/test/variable_resource_push.test.ts index c348a31e66..e8b0dc9e26 100644 --- a/cli/test/variable_resource_push.test.ts +++ b/cli/test/variable_resource_push.test.ts @@ -133,6 +133,92 @@ describe("variable", () => { }); }); + test("add creates secret by default and preserves fields on update", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspaceProfile(backend); + + const uniqueId = Date.now(); + const varPath = `f/test/add_var_${uniqueId}`; + + // Create: secret by default, --description sets the description + const createResult = await backend.runCLICommand( + ["variable", "add", "v1", varPath, "--description", "first desc"], + tempDir + ); + expect(createResult.code).toEqual(0); + + let apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/${varPath}` + ); + expect(apiResp.status).toEqual(200); + let varData = await apiResp.json(); + expect(varData.is_secret).toBe(true); + expect(varData.description).toBe("first desc"); + + // Update with --yes only: no prompt, is_secret and description preserved + const updateResult = await backend.runCLICommand( + ["variable", "add", "v2", varPath, "--yes"], + tempDir + ); + expect(updateResult.code).toEqual(0); + + apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/${varPath}` + ); + expect(apiResp.status).toEqual(200); + varData = await apiResp.json(); + expect(varData.is_secret).toBe(true); + expect(varData.description).toBe("first desc"); + expect(varData.value).toBe("v2"); + + // Update with --no-secret: flips to non-secret + const noSecretResult = await backend.runCLICommand( + ["variable", "add", "v3", varPath, "--yes", "--no-secret"], + tempDir + ); + expect(noSecretResult.code).toEqual(0); + + apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/${varPath}` + ); + expect(apiResp.status).toEqual(200); + varData = await apiResp.json(); + expect(varData.is_secret).toBe(false); + expect(varData.value).toBe("v3"); + + // Update the non-secret variable with no secret flags: is_secret must + // stay false (preserved, not re-defaulted to secret) + const preserveResult = await backend.runCLICommand( + ["variable", "add", "v4", varPath, "--yes"], + tempDir + ); + expect(preserveResult.code).toEqual(0); + + apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/${varPath}` + ); + expect(apiResp.status).toEqual(200); + varData = await apiResp.json(); + expect(varData.is_secret).toBe(false); + expect(varData.value).toBe("v4"); + + // Explicit --secret flips it back + const secretResult = await backend.runCLICommand( + ["variable", "add", "v5", varPath, "--yes", "--secret"], + tempDir + ); + expect(secretResult.code).toEqual(0); + + apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/variables/get/${varPath}` + ); + expect(apiResp.status).toEqual(200); + varData = await apiResp.json(); + expect(varData.is_secret).toBe(true); + expect(varData.value).toBe("v5"); + }); + }); + test("pull retrieves variables into local files", async () => { await withTestBackend(async (backend, tempDir) => { await setupWorkspaceProfile(backend); diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index 86c43bedac..4712aae8c4 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -668,8 +668,12 @@ variable related commands - `variable push ` - Push a local variable spec. This overrides any remote versions. - `--plain-secrets` - Push secrets as plain text - `variable add ` - Create a new variable on the remote. This will update the variable if it already exists. + - `--yes` - Skip confirmation prompt when updating an existing variable + - `--secret` - Mark the variable as secret (default when creating a new variable) + - `--no-secret` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed) + - `--description ` - Set the variable description (when updating, the existing description is preserved if not passed) - `--plain-secrets` - Push secrets as plain text - - `--public` - Legacy option, use --plain-secrets instead + - `--public` - Legacy option, use --no-secret instead ### version diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index f22caeaa4c..ac2b77719b 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -3219,8 +3219,12 @@ variable related commands - \`variable push \` - Push a local variable spec. This overrides any remote versions. - \`--plain-secrets\` - Push secrets as plain text - \`variable add \` - Create a new variable on the remote. This will update the variable if it already exists. + - \`--yes\` - Skip confirmation prompt when updating an existing variable + - \`--secret\` - Mark the variable as secret (default when creating a new variable) + - \`--no-secret\` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed) + - \`--description \` - Set the variable description (when updating, the existing description is preserved if not passed) - \`--plain-secrets\` - Push secrets as plain text - - \`--public\` - Legacy option, use --plain-secrets instead + - \`--public\` - Legacy option, use --no-secret instead ### version diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index 13d423dad7..8f4c15786d 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -673,8 +673,12 @@ variable related commands - `variable push ` - Push a local variable spec. This overrides any remote versions. - `--plain-secrets` - Push secrets as plain text - `variable add ` - Create a new variable on the remote. This will update the variable if it already exists. + - `--yes` - Skip confirmation prompt when updating an existing variable + - `--secret` - Mark the variable as secret (default when creating a new variable) + - `--no-secret` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed) + - `--description ` - Set the variable description (when updating, the existing description is preserved if not passed) - `--plain-secrets` - Push secrets as plain text - - `--public` - Legacy option, use --plain-secrets instead + - `--public` - Legacy option, use --no-secret instead ### version