diff --git a/packages/cli/script/generate-sdk.ts b/packages/cli/script/generate-sdk.ts index 19f953cc6..743ec6f3f 100644 --- a/packages/cli/script/generate-sdk.ts +++ b/packages/cli/script/generate-sdk.ts @@ -633,11 +633,14 @@ for (const { path, command } of allCommands) { return `${camelCase(name)}: ${flag?.tsType ?? "string"}`; }); const streamingConstraint = streamingFlagTypes.join("; "); + const streamingRest = hasVariadicPositional + ? ", ...positional: string[]" + : ""; typeDecl = [ `${indent} /** ${brief} */`, `${indent} ${methodName}: {`, - `${indent} (params: ${params?.name ?? "Record"} & { ${streamingConstraint} }): AsyncIterable;`, + `${indent} (params: ${params?.name ?? "Record"} & { ${streamingConstraint} }${streamingRest}): AsyncIterable;`, `${indent} ${sig}: Promise<${returnType}>;`, `${indent} };`, ].join("\n"); diff --git a/packages/cli/src/lib/sdk-invoke.ts b/packages/cli/src/lib/sdk-invoke.ts index 1cb859571..bd44c4356 100644 --- a/packages/cli/src/lib/sdk-invoke.ts +++ b/packages/cli/src/lib/sdk-invoke.ts @@ -74,7 +74,8 @@ export type FlagDef = { default?: unknown; optional?: boolean; parse?: (value: string) => unknown; - variadic?: boolean; + /** `true` or a separator string — Stricli collects either form into an array. */ + variadic?: boolean | string; }; /** Resolved command: handler function + flag definitions for default application. */ @@ -161,6 +162,9 @@ function resolveFlagDefault(def: FlagDef): unknown { * (same as Stricli's `parseInput` path in `parseInputsForFlag`). * - For `kind: "boolean"` / `kind: "enum"` flags with a `default`, uses the raw value. * - Skips flags already set (non-`undefined`) by the caller. + * - Wraps a scalar caller value for a variadic flag in an array. Stricli always + * hands variadic flags to the handler as arrays, so an untyped caller passing + * `{ field: "title" }` would otherwise crash on array methods (CLI-28C). * * @param flags - The flags object from the SDK caller (may have `undefined` values). * @param flagDefs - The command's Stricli flag definitions. @@ -174,9 +178,11 @@ export function applyFlagDefaults( // Copy caller-provided flags, skipping undefined values so they // don't shadow the defaults we're about to apply. for (const [key, value] of Object.entries(flags)) { - if (value !== undefined) { - result[key] = value; + if (value === undefined) { + continue; } + result[key] = + flagDefs[key]?.variadic && !Array.isArray(value) ? [value] : value; } // Apply defaults for any flag not already set by the caller for (const [name, def] of Object.entries(flagDefs)) { diff --git a/packages/cli/test/lib/sdk-flag-types.test.ts b/packages/cli/test/lib/sdk-flag-types.test.ts index 279618931..abbd65d2d 100644 --- a/packages/cli/test/lib/sdk-flag-types.test.ts +++ b/packages/cli/test/lib/sdk-flag-types.test.ts @@ -30,4 +30,15 @@ describe("generated SDK flag types", () => { expect(login).toContain("scope?: Array;"); expect(login).toContain("token?: string;"); }); + + test("streaming overloads accept the target positional", () => { + // Streaming calls still take their target as trailing positionals, e.g. + // `sdk.log.list({ follow: "5" }, "org/project")` must type as an AsyncIterable. + expect(declarations).toContain( + "(params: LogListParams & { follow: string }, ...positional: string[]): AsyncIterable;", + ); + expect(declarations).toContain( + "(params: DashboardViewParams & { refresh: string }, ...positional: string[]): AsyncIterable;", + ); + }); }); diff --git a/packages/cli/test/lib/sdk-invoke.test.ts b/packages/cli/test/lib/sdk-invoke.test.ts index 7d2c8b764..c477d6230 100644 --- a/packages/cli/test/lib/sdk-invoke.test.ts +++ b/packages/cli/test/lib/sdk-invoke.test.ts @@ -104,6 +104,40 @@ describe("applyFlagDefaults: boolean flags", () => { }); }); +// --------------------------------------------------------------------------- +// applyFlagDefaults — variadic flags +// --------------------------------------------------------------------------- + +describe("applyFlagDefaults: variadic flags", () => { + const flagDefs: Record = { + field: { kind: "parsed", parse: String, variadic: true, optional: true }, + environment: { + kind: "parsed", + parse: String, + variadic: ",", + optional: true, + }, + dataset: { kind: "parsed", parse: String, optional: true }, + }; + + test("wraps a scalar caller value in an array", () => { + // Stricli always hands variadic flags to the handler as arrays (CLI-28C). + const result = applyFlagDefaults( + { field: "title", environment: "prod", dataset: "spans" }, + flagDefs, + ); + expect(result.field).toEqual(["title"]); + expect(result.environment).toEqual(["prod"]); + expect(result.dataset).toBe("spans"); + }); + + test("keeps caller arrays unchanged", () => { + const fields = ["title", "count()"]; + const result = applyFlagDefaults({ field: fields }, flagDefs); + expect(result.field).toBe(fields); + }); +}); + // --------------------------------------------------------------------------- // applyFlagDefaults — enum flags // ---------------------------------------------------------------------------