diff --git a/src/resources/extensions/gsd/commands/handlers/workflow.ts b/src/resources/extensions/gsd/commands/handlers/workflow.ts index 85f6276e2..a755bf9ab 100644 --- a/src/resources/extensions/gsd/commands/handlers/workflow.ts +++ b/src/resources/extensions/gsd/commands/handlers/workflow.ts @@ -38,6 +38,67 @@ const WORKFLOW_USAGE = [ " resume — Resume paused custom workflow auto-mode", ].join("\n"); +function splitWorkflowRunArgs(input: string): string[] { + const tokens: string[] = []; + let current = ""; + let quote: '"' | "'" | null = null; + let escapeNext = false; + + for (const ch of input) { + if (escapeNext) { + current += ch; + escapeNext = false; + continue; + } + + if (ch === "\\") { + escapeNext = true; + continue; + } + + if (quote) { + if (ch === quote) { + quote = null; + } else { + current += ch; + } + continue; + } + + if (ch === '"' || ch === "'") { + quote = ch; + continue; + } + + if (/\s/.test(ch)) { + if (current) { + tokens.push(current); + current = ""; + } + continue; + } + + current += ch; + } + + if (escapeNext) current += "\\"; + if (current) tokens.push(current); + return tokens; +} + +export function parseWorkflowRunArgs(args: string): { defName: string; overrides: Record } { + const parts = splitWorkflowRunArgs(args); + const defName = parts[0] ?? ""; + const overrides: Record = {}; + for (let i = 1; i < parts.length; i++) { + const eqIdx = parts[i].indexOf("="); + if (eqIdx > 0) { + overrides[parts[i].slice(0, eqIdx)] = parts[i].slice(eqIdx + 1); + } + } + return { defName, overrides }; +} + async function handleCustomWorkflow( sub: string, ctx: ExtensionCommandContext, @@ -62,15 +123,7 @@ async function handleCustomWorkflow( ctx.ui.notify("Usage: /gsd workflow run [param=value ...]", "warning"); return true; } - const parts = args.split(/\s+/); - const defName = parts[0]; - const overrides: Record = {}; - for (let i = 1; i < parts.length; i++) { - const eqIdx = parts[i].indexOf("="); - if (eqIdx > 0) { - overrides[parts[i].slice(0, eqIdx)] = parts[i].slice(eqIdx + 1); - } - } + const { defName, overrides } = parseWorkflowRunArgs(args); try { const base = projectRoot(); const runDir = createRun(base, defName, Object.keys(overrides).length > 0 ? overrides : undefined); diff --git a/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts b/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts index 537bcab4d..21b2f07fa 100644 --- a/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +++ b/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts @@ -217,6 +217,20 @@ describe("workflow command handler", () => { ); }); + it("preserves quoted workflow run overrides (#4130)", async () => { + const { parseWorkflowRunArgs } = await import("../commands/handlers/workflow.ts"); + assert.deepStrictEqual( + parseWorkflowRunArgs('demo-workflow target="multi word target" region=\'us east\''), + { + defName: "demo-workflow", + overrides: { + target: "multi word target", + region: "us east", + }, + }, + ); + }); + it("'/gsd workflow run nonexistent' shows error for missing definition", async () => { const { handled, notifications } = await callHandler("workflow run nonexistent-def-12345"); assert.ok(handled, "should be handled");