From 0098d73cb22cc3a2b97a74214c5179d769a34684 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:50:15 +0200 Subject: [PATCH] Fix CLI end-of-options operands for subcommands agent-session-id: 6b78e583-eb90-415e-8bcb-c4fc342cb179 agent-tool: Codex CLI agent-tool-version: 0.145.0 agent-model: unknown agent-runtime-profile: /nix/store/ph8rlhdj25mg71v81jsfzy6dq4xpcs9m-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/lsykz8x5481xrpbgk280xh3pypk1c5jy-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@3649b53 --- .changeset/quiet-clis-parse.md | 5 + .../src/unstable/cli/internal/parser.ts | 4 +- .../effect/test/unstable/cli/Command.test.ts | 94 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-clis-parse.md diff --git a/.changeset/quiet-clis-parse.md b/.changeset/quiet-clis-parse.md new file mode 100644 index 00000000000..2fc507ec822 --- /dev/null +++ b/.changeset/quiet-clis-parse.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix unstable CLI subcommands dropping operands after the `--` end-of-options terminator. diff --git a/packages/effect/src/unstable/cli/internal/parser.ts b/packages/effect/src/unstable/cli/internal/parser.ts index 80431e568e9..1d388cc00c4 100644 --- a/packages/effect/src/unstable/cli/internal/parser.ts +++ b/packages/effect/src/unstable/cli/internal/parser.ts @@ -84,7 +84,7 @@ export const parseArgs = ( } } - const subLex: LexResult = { tokens: result.childTokens, trailingOperands: [] } + const subLex: LexResult = { tokens: result.childTokens, trailingOperands: afterEndOfOptions } const subParsed = yield* parseArgs( subLex, result.sub, @@ -94,7 +94,7 @@ export const parseArgs = ( const allErrors = [...result.errors, ...(subParsed.errors ?? [])] return { flags: result.flags, - arguments: afterEndOfOptions, + arguments: [], subcommand: Option.some({ name: result.sub.name, parsedInput: subParsed }), ...(allErrors.length > 0 && { errors: allErrors }) } diff --git a/packages/effect/test/unstable/cli/Command.test.ts b/packages/effect/test/unstable/cli/Command.test.ts index 5dac07ac127..ca3fadf0671 100644 --- a/packages/effect/test/unstable/cli/Command.test.ts +++ b/packages/effect/test/unstable/cli/Command.test.ts @@ -1289,6 +1289,100 @@ describe("Command", () => { assert.deepStrictEqual(captured, [["child", "--value", "x"]]) }).pipe(Effect.provide(TestLayer))) + it.effect("should pass trailing operands to the selected subcommand", () => + Effect.gen(function*() { + const captured: Array> = [] + + const child = Command.make("child", { + values: Argument.string("value").pipe(Argument.variadic()) + }, ({ values }) => Effect.sync(() => captured.push(values))) + + const cli = Command.make("tool").pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })([ + "child", + "--", + "value", + "--literal", + "-x" + ]) + + assert.deepStrictEqual(captured, [["value", "--literal", "-x"]]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should pass trailing operands through nested subcommands", () => + Effect.gen(function*() { + const captured: Array = [] + + const child = Command.make("child", { + value: Argument.string("value") + }, ({ value }) => Effect.sync(() => captured.push(value))) + const group = Command.make("group").pipe(Command.withSubcommands([child])) + const cli = Command.make("tool").pipe(Command.withSubcommands([group])) + + yield* Command.runWith(cli, { version: "1.0.0" })(["group", "child", "--", "-literal"]) + + assert.deepStrictEqual(captured, ["-literal"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should allow no trailing operands after -- for a subcommand", () => + Effect.gen(function*() { + let invoked = false + + const child = Command.make("child", {}, () => + Effect.sync(() => { + invoked = true + })) + const cli = Command.make("tool").pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })(["child", "--"]) + + assert.isTrue(invoked) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should preserve trailing operands for a leaf command", () => + Effect.gen(function*() { + const captured: Array> = [] + + const command = Command.make("tool", { + values: Argument.string("value").pipe(Argument.variadic()) + }, ({ values }) => Effect.sync(() => captured.push(values))) + + yield* Command.runWith(command, { version: "1.0.0" })(["--", "--literal", "-x"]) + + assert.deepStrictEqual(captured, [["--literal", "-x"]]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should preserve inherited flags around a subcommand with trailing operands", () => + Effect.gen(function*() { + const captured: Array<{ before: boolean; after: boolean; value: string }> = [] + + const root = Command.make("tool").pipe( + Command.withSharedFlags({ + before: Flag.boolean("before"), + after: Flag.boolean("after") + }) + ) + const child = Command.make("child", { + value: Argument.string("value") + }, ({ value }) => + Effect.gen(function*() { + const parent = yield* root + captured.push({ before: parent.before, after: parent.after, value }) + })) + const cli = root.pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })([ + "--before", + "child", + "--after", + "--", + "-literal" + ]) + + assert.deepStrictEqual(captured, [{ before: true, after: true, value: "-literal" }]) + }).pipe(Effect.provide(TestLayer))) + it.effect("should coerce boolean flags to false when given falsey literals", () => Effect.gen(function*() { const captured: Array = []