From 9a77e5584244675183b64fdf93e6ff50815162cd Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 00:45:00 +0300
Subject: [PATCH 1/6] feat: add Enable Chrome setting for Claude provider
Claude Code supports a --chrome flag to launch with Chrome browser
integration, but the SDK query path used by T3 Code never passed it.
Add an `enableChrome` boolean to Claude provider settings (default off)
and forward it via the SDK's `extraArgs` option when enabled.
---
.../src/provider/Layers/ClaudeAdapter.ts | 2 ++
.../components/settings/SettingsPanels.tsx | 33 ++++++++++++++++++-
packages/contracts/src/settings.ts | 2 ++
3 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
index 1a0657c499d..b7e09f1b595 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -2680,6 +2680,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
),
);
const claudeBinaryPath = claudeSettings.binaryPath;
+ const enableChrome = claudeSettings.enableChrome;
const modelSelection =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
const caps = getClaudeModelCapabilities(modelSelection?.model);
@@ -2719,6 +2720,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
canUseTool,
env: process.env,
...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
+ ...(enableChrome ? { extraArgs: { chrome: null } } : {}),
};
const queryRuntime = yield* Effect.try({
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index c8766f26436..ec5f3b34e57 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -440,7 +440,8 @@ export function GeneralSettingsPanel() {
claudeAgent: Boolean(
settings.providers.claudeAgent.binaryPath !==
DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath ||
- settings.providers.claudeAgent.customModels.length > 0,
+ settings.providers.claudeAgent.customModels.length > 0 ||
+ settings.providers.claudeAgent.enableChrome,
),
});
const [customModelInputByProvider, setCustomModelInputByProvider] = useState<
@@ -1179,6 +1180,36 @@ export function GeneralSettingsPanel() {
) : null}
+ {providerCard.provider === "claudeAgent" ? (
+
+
+
+
+ Enable Chrome
+
+
+ Launch Claude with Chrome browser integration (--chrome).
+
+
+
+ updateSettings({
+ providers: {
+ ...settings.providers,
+ claudeAgent: {
+ ...settings.providers.claudeAgent,
+ enableChrome: Boolean(checked),
+ },
+ },
+ })
+ }
+ aria-label="Enable Chrome browser integration"
+ />
+
+
+ ) : null}
+
Models
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 426f8bee568..499175fbd57 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -70,6 +70,7 @@ export const ClaudeSettings = Schema.Struct({
enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
binaryPath: makeBinaryPathSetting("claude"),
customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
+ enableChrome: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
});
export type ClaudeSettings = typeof ClaudeSettings.Type;
@@ -163,6 +164,7 @@ const ClaudeSettingsPatch = Schema.Struct({
enabled: Schema.optionalKey(Schema.Boolean),
binaryPath: Schema.optionalKey(Schema.String),
customModels: Schema.optionalKey(Schema.Array(Schema.String)),
+ enableChrome: Schema.optionalKey(Schema.Boolean),
});
export const ServerSettingsPatch = Schema.Struct({
From 9873c016fb513b8004c3d4e6951a949637a6ec7e Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 01:09:34 +0300
Subject: [PATCH 2/6] feat: launch args instead of specific chrome flag - fix
test - add tests for parser
---
.../src/provider/Layers/ClaudeAdapter.test.ts | 70 ++++++++++++++++++-
.../src/provider/Layers/ClaudeAdapter.ts | 35 +++++++++-
apps/server/src/serverSettings.test.ts | 2 +
.../components/settings/SettingsPanels.tsx | 36 +++++-----
packages/contracts/src/settings.ts | 4 +-
5 files changed, 126 insertions(+), 21 deletions(-)
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
index 8b1f1389c41..2a93aace199 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
@@ -25,7 +25,7 @@ import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderAdapterValidationError } from "../Errors.ts";
import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
-import { makeClaudeAdapterLive, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
+import { makeClaudeAdapterLive, parseLaunchArgs, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
class FakeClaudeQuery implements AsyncIterable
{
private readonly queue: Array = [];
@@ -3219,3 +3219,71 @@ describe("ClaudeAdapterLive", () => {
);
});
});
+
+describe("parseLaunchArgs", () => {
+ it("returns empty object for empty string", () => {
+ assert.deepEqual(parseLaunchArgs(""), {});
+ });
+
+ it("returns empty object for whitespace-only string", () => {
+ assert.deepEqual(parseLaunchArgs(" "), {});
+ });
+
+ it("parses --chrome boolean flag", () => {
+ assert.deepEqual(parseLaunchArgs("--chrome"), { chrome: null });
+ });
+
+ it("parses --chrome with --verbose", () => {
+ assert.deepEqual(parseLaunchArgs("--chrome --verbose"), {
+ chrome: null,
+ verbose: null,
+ });
+ });
+
+ it("parses --effort with a value", () => {
+ assert.deepEqual(parseLaunchArgs("--effort high"), { effort: "high" });
+ });
+
+ it("parses --chrome --effort high --debug", () => {
+ assert.deepEqual(parseLaunchArgs("--chrome --effort high --debug"), {
+ chrome: null,
+ effort: "high",
+ debug: null,
+ });
+ });
+
+ it("parses --model with full model name", () => {
+ assert.deepEqual(parseLaunchArgs("--model claude-sonnet-4-6"), {
+ model: "claude-sonnet-4-6",
+ });
+ });
+
+ it("parses --append-system-prompt with value and --chrome", () => {
+ assert.deepEqual(parseLaunchArgs("--append-system-prompt always-think-step-by-step --chrome"), {
+ "append-system-prompt": "always-think-step-by-step",
+ chrome: null,
+ });
+ });
+
+ it("parses --max-budget-usd with numeric value", () => {
+ assert.deepEqual(parseLaunchArgs("--chrome --max-budget-usd 5.00"), {
+ chrome: null,
+ "max-budget-usd": "5.00",
+ });
+ });
+
+ it("handles extra whitespace between tokens", () => {
+ assert.deepEqual(parseLaunchArgs(" --chrome --verbose "), {
+ chrome: null,
+ verbose: null,
+ });
+ });
+
+ it("ignores bare -- with no flag name", () => {
+ assert.deepEqual(parseLaunchArgs("--"), {});
+ });
+
+ it("ignores tokens that don't start with --", () => {
+ assert.deepEqual(parseLaunchArgs("chrome"), {});
+ });
+});
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
index b7e09f1b595..d286801ed88 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -506,6 +506,35 @@ const CLAUDE_SETTING_SOURCES = [
"local",
] as const satisfies ReadonlyArray;
+/**
+ * Parse a CLI-style launch args string into the SDK's `extraArgs` format.
+ *
+ * Boolean flags (no value) become `null`, flags with a value become strings:
+ * "" → {}
+ * "--chrome" → { chrome: null }
+ * "--chrome --debug" → { chrome: null, debug: null }
+ * "--chrome --max-turns 5" → { chrome: null, "max-turns": "5" }
+ */
+export function parseLaunchArgs(args: string): Record {
+ const result: Record = {};
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i]!;
+ if (token.startsWith("--")) {
+ const key = token.slice(2);
+ if (!key) continue;
+ const next = tokens[i + 1];
+ if (next !== undefined && !next.startsWith("--")) {
+ result[key] = next;
+ i++;
+ } else {
+ result[key] = null;
+ }
+ }
+ }
+ return result;
+}
+
function buildPromptText(input: ProviderSendTurnInput): string {
const rawEffort =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.options?.effort : null;
@@ -2680,7 +2709,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
),
);
const claudeBinaryPath = claudeSettings.binaryPath;
- const enableChrome = claudeSettings.enableChrome;
+ const launchArgs = claudeSettings.launchArgs;
const modelSelection =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
const caps = getClaudeModelCapabilities(modelSelection?.model);
@@ -2720,7 +2749,9 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
canUseTool,
env: process.env,
...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
- ...(enableChrome ? { extraArgs: { chrome: null } } : {}),
+ ...(Object.keys(parseLaunchArgs(launchArgs)).length > 0
+ ? { extraArgs: parseLaunchArgs(launchArgs) }
+ : {}),
};
const queryRuntime = yield* Effect.try({
diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts
index 289bc689617..d3d92e42126 100644
--- a/apps/server/src/serverSettings.test.ts
+++ b/apps/server/src/serverSettings.test.ts
@@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
enabled: true,
binaryPath: "/usr/local/bin/claude",
customModels: ["claude-custom"],
+ launchArgs: "",
});
assert.deepEqual(next.textGenerationModelSelection, {
provider: "codex",
@@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
enabled: true,
binaryPath: "/opt/homebrew/bin/claude",
customModels: [],
+ launchArgs: "",
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index ec5f3b34e57..fb234805c3a 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -441,7 +441,7 @@ export function GeneralSettingsPanel() {
settings.providers.claudeAgent.binaryPath !==
DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath ||
settings.providers.claudeAgent.customModels.length > 0 ||
- settings.providers.claudeAgent.enableChrome,
+ settings.providers.claudeAgent.launchArgs !== "",
),
});
const [customModelInputByProvider, setCustomModelInputByProvider] = useState<
@@ -1182,31 +1182,35 @@ export function GeneralSettingsPanel() {
{providerCard.provider === "claudeAgent" ? (
-
-
-
- Enable Chrome
-
-
- Launch Claude with Chrome browser integration (--chrome).
-
-
-
+
+
+ Additional CLI arguments passed to Claude Code on session start.
+
+
) : null}
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 499175fbd57..a334553006c 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -70,7 +70,7 @@ export const ClaudeSettings = Schema.Struct({
enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
binaryPath: makeBinaryPathSetting("claude"),
customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
- enableChrome: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
+ launchArgs: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
});
export type ClaudeSettings = typeof ClaudeSettings.Type;
@@ -164,7 +164,7 @@ const ClaudeSettingsPatch = Schema.Struct({
enabled: Schema.optionalKey(Schema.Boolean),
binaryPath: Schema.optionalKey(Schema.String),
customModels: Schema.optionalKey(Schema.Array(Schema.String)),
- enableChrome: Schema.optionalKey(Schema.Boolean),
+ launchArgs: Schema.optionalKey(Schema.String),
});
export const ServerSettingsPatch = Schema.Struct({
From cfefd477ca6e8f84fd455028555c8a0548978942 Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 01:30:13 +0300
Subject: [PATCH 3/6] feat: use normal string with spaces
---
packages/contracts/src/settings.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index a334553006c..dcb13127b1b 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -70,7 +70,7 @@ export const ClaudeSettings = Schema.Struct({
enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
binaryPath: makeBinaryPathSetting("claude"),
customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
- launchArgs: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
+ launchArgs: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
});
export type ClaudeSettings = typeof ClaudeSettings.Type;
From 173029192f2da8c19f27ea40505fd4911452f8d8 Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 01:40:25 +0300
Subject: [PATCH 4/6] feat: fix parse args twice
---
apps/server/src/provider/Layers/ClaudeAdapter.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
index d286801ed88..d65c7f8d8c0 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -2709,7 +2709,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
),
);
const claudeBinaryPath = claudeSettings.binaryPath;
- const launchArgs = claudeSettings.launchArgs;
+ const extraArgs = parseLaunchArgs(claudeSettings.launchArgs);
const modelSelection =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
const caps = getClaudeModelCapabilities(modelSelection?.model);
@@ -2749,9 +2749,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
canUseTool,
env: process.env,
...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
- ...(Object.keys(parseLaunchArgs(launchArgs)).length > 0
- ? { extraArgs: parseLaunchArgs(launchArgs) }
- : {}),
+ ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),
};
const queryRuntime = yield* Effect.try({
From 55333640826d3e831581082742d18800210baeab Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 10:35:23 +0300
Subject: [PATCH 5/6] feat: common parser of cli args, unit tests
---
.../src/provider/Layers/ClaudeAdapter.test.ts | 69 +-----------
.../src/provider/Layers/ClaudeAdapter.ts | 32 +-----
packages/shared/package.json | 4 +
packages/shared/src/cliArgs.test.ts | 105 ++++++++++++++++++
packages/shared/src/cliArgs.ts | 62 +++++++++++
.../update-release-package-versions.test.ts | 63 +++++++++++
scripts/update-release-package-versions.ts | 51 ++++-----
7 files changed, 256 insertions(+), 130 deletions(-)
create mode 100644 packages/shared/src/cliArgs.test.ts
create mode 100644 packages/shared/src/cliArgs.ts
create mode 100644 scripts/update-release-package-versions.test.ts
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
index 2a93aace199..44b7d090d4e 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.test.ts
@@ -25,7 +25,7 @@ import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderAdapterValidationError } from "../Errors.ts";
import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
-import { makeClaudeAdapterLive, parseLaunchArgs, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
+import { makeClaudeAdapterLive, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
class FakeClaudeQuery implements AsyncIterable {
private readonly queue: Array = [];
@@ -3220,70 +3220,3 @@ describe("ClaudeAdapterLive", () => {
});
});
-describe("parseLaunchArgs", () => {
- it("returns empty object for empty string", () => {
- assert.deepEqual(parseLaunchArgs(""), {});
- });
-
- it("returns empty object for whitespace-only string", () => {
- assert.deepEqual(parseLaunchArgs(" "), {});
- });
-
- it("parses --chrome boolean flag", () => {
- assert.deepEqual(parseLaunchArgs("--chrome"), { chrome: null });
- });
-
- it("parses --chrome with --verbose", () => {
- assert.deepEqual(parseLaunchArgs("--chrome --verbose"), {
- chrome: null,
- verbose: null,
- });
- });
-
- it("parses --effort with a value", () => {
- assert.deepEqual(parseLaunchArgs("--effort high"), { effort: "high" });
- });
-
- it("parses --chrome --effort high --debug", () => {
- assert.deepEqual(parseLaunchArgs("--chrome --effort high --debug"), {
- chrome: null,
- effort: "high",
- debug: null,
- });
- });
-
- it("parses --model with full model name", () => {
- assert.deepEqual(parseLaunchArgs("--model claude-sonnet-4-6"), {
- model: "claude-sonnet-4-6",
- });
- });
-
- it("parses --append-system-prompt with value and --chrome", () => {
- assert.deepEqual(parseLaunchArgs("--append-system-prompt always-think-step-by-step --chrome"), {
- "append-system-prompt": "always-think-step-by-step",
- chrome: null,
- });
- });
-
- it("parses --max-budget-usd with numeric value", () => {
- assert.deepEqual(parseLaunchArgs("--chrome --max-budget-usd 5.00"), {
- chrome: null,
- "max-budget-usd": "5.00",
- });
- });
-
- it("handles extra whitespace between tokens", () => {
- assert.deepEqual(parseLaunchArgs(" --chrome --verbose "), {
- chrome: null,
- verbose: null,
- });
- });
-
- it("ignores bare -- with no flag name", () => {
- assert.deepEqual(parseLaunchArgs("--"), {});
- });
-
- it("ignores tokens that don't start with --", () => {
- assert.deepEqual(parseLaunchArgs("chrome"), {});
- });
-});
diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts
index d65c7f8d8c0..5b98eac3432 100644
--- a/apps/server/src/provider/Layers/ClaudeAdapter.ts
+++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts
@@ -20,6 +20,7 @@ import {
ModelUsage,
NonNullableUsage,
} from "@anthropic-ai/claude-agent-sdk";
+import { parseCliArgs } from "@t3tools/shared/cliArgs";
import {
ApprovalRequestId,
type CanonicalItemType,
@@ -506,35 +507,6 @@ const CLAUDE_SETTING_SOURCES = [
"local",
] as const satisfies ReadonlyArray;
-/**
- * Parse a CLI-style launch args string into the SDK's `extraArgs` format.
- *
- * Boolean flags (no value) become `null`, flags with a value become strings:
- * "" → {}
- * "--chrome" → { chrome: null }
- * "--chrome --debug" → { chrome: null, debug: null }
- * "--chrome --max-turns 5" → { chrome: null, "max-turns": "5" }
- */
-export function parseLaunchArgs(args: string): Record {
- const result: Record = {};
- const tokens = args.trim().split(/\s+/).filter(Boolean);
- for (let i = 0; i < tokens.length; i++) {
- const token = tokens[i]!;
- if (token.startsWith("--")) {
- const key = token.slice(2);
- if (!key) continue;
- const next = tokens[i + 1];
- if (next !== undefined && !next.startsWith("--")) {
- result[key] = next;
- i++;
- } else {
- result[key] = null;
- }
- }
- }
- return result;
-}
-
function buildPromptText(input: ProviderSendTurnInput): string {
const rawEffort =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.options?.effort : null;
@@ -2709,7 +2681,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
),
);
const claudeBinaryPath = claudeSettings.binaryPath;
- const extraArgs = parseLaunchArgs(claudeSettings.launchArgs);
+ const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags;
const modelSelection =
input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined;
const caps = getClaudeModelCapabilities(modelSelection?.model);
diff --git a/packages/shared/package.json b/packages/shared/package.json
index ed65cbeaf3c..2fcc327e6b8 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -59,6 +59,10 @@
"./qrCode": {
"types": "./src/qrCode.ts",
"import": "./src/qrCode.ts"
+ },
+ "./cliArgs": {
+ "types": "./src/cliArgs.ts",
+ "import": "./src/cliArgs.ts"
}
},
"scripts": {
diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts
new file mode 100644
index 00000000000..2afc5856a95
--- /dev/null
+++ b/packages/shared/src/cliArgs.test.ts
@@ -0,0 +1,105 @@
+import { describe, expect, it } from "vitest";
+
+import { parseCliArgs } from "./cliArgs.ts";
+
+describe("parseCliArgs", () => {
+ it("returns empty result for empty string", () => {
+ expect(parseCliArgs("")).toEqual({ flags: {}, positionals: [] });
+ });
+
+ it("returns empty result for whitespace-only string", () => {
+ expect(parseCliArgs(" ")).toEqual({ flags: {}, positionals: [] });
+ });
+
+ it("returns empty result for empty array", () => {
+ expect(parseCliArgs([])).toEqual({ flags: {}, positionals: [] });
+ });
+
+ it("parses --chrome boolean flag", () => {
+ expect(parseCliArgs("--chrome")).toEqual({
+ flags: { chrome: null },
+ positionals: [],
+ });
+ });
+
+ it("parses --chrome with --verbose", () => {
+ expect(parseCliArgs("--chrome --verbose")).toEqual({
+ flags: { chrome: null, verbose: null },
+ positionals: [],
+ });
+ });
+
+ it("parses --effort with a value", () => {
+ expect(parseCliArgs("--effort high")).toEqual({
+ flags: { effort: "high" },
+ positionals: [],
+ });
+ });
+
+ it("parses --chrome --effort high --debug", () => {
+ expect(parseCliArgs("--chrome --effort high --debug")).toEqual({
+ flags: { chrome: null, effort: "high", debug: null },
+ positionals: [],
+ });
+ });
+
+ it("parses --model with full model name", () => {
+ expect(parseCliArgs("--model claude-sonnet-4-6")).toEqual({
+ flags: { model: "claude-sonnet-4-6" },
+ positionals: [],
+ });
+ });
+
+ it("parses --append-system-prompt with value and --chrome", () => {
+ expect(parseCliArgs("--append-system-prompt always-think-step-by-step --chrome")).toEqual({
+ flags: { "append-system-prompt": "always-think-step-by-step", chrome: null },
+ positionals: [],
+ });
+ });
+
+ it("parses --max-budget-usd with numeric value", () => {
+ expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({
+ flags: { chrome: null, "max-budget-usd": "5.00" },
+ positionals: [],
+ });
+ });
+
+ it("parses --effort=high syntax", () => {
+ expect(parseCliArgs("--effort=high")).toEqual({
+ flags: { effort: "high" },
+ positionals: [],
+ });
+ });
+
+ it("parses --key=value mixed with boolean flags", () => {
+ expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({
+ flags: { chrome: null, model: "claude-sonnet-4-6", debug: null },
+ positionals: [],
+ });
+ });
+
+ it("collects positional arguments", () => {
+ expect(parseCliArgs("1.2.3")).toEqual({
+ flags: {},
+ positionals: ["1.2.3"],
+ });
+ });
+
+ it("collects positionals mixed with flags (argv array)", () => {
+ expect(parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
+ flags: { root: "/path", "github-output": null },
+ positionals: ["1.2.3"],
+ });
+ });
+
+ it("handles extra whitespace between tokens", () => {
+ expect(parseCliArgs(" --chrome --verbose ")).toEqual({
+ flags: { chrome: null, verbose: null },
+ positionals: [],
+ });
+ });
+
+ it("ignores bare -- with no flag name", () => {
+ expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] });
+ });
+});
diff --git a/packages/shared/src/cliArgs.ts b/packages/shared/src/cliArgs.ts
new file mode 100644
index 00000000000..6f75387363d
--- /dev/null
+++ b/packages/shared/src/cliArgs.ts
@@ -0,0 +1,62 @@
+export interface ParsedCliArgs {
+ readonly flags: Record;
+ readonly positionals: string[];
+}
+
+/**
+ * Parse CLI-style arguments into flags and positionals.
+ *
+ * Accepts a string (split by whitespace) or a pre-split argv array.
+ * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax.
+ *
+ * parseCliArgs("")
+ * → { flags: {}, positionals: [] }
+ *
+ * parseCliArgs("--chrome")
+ * → { flags: { chrome: null }, positionals: [] }
+ *
+ * parseCliArgs("--chrome --effort high")
+ * → { flags: { chrome: null, effort: "high" }, positionals: [] }
+ *
+ * parseCliArgs("--effort=high")
+ * → { flags: { effort: "high" }, positionals: [] }
+ *
+ * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])
+ * → { flags: { root: "/path", "github-output": null }, positionals: ["1.2.3"] }
+ */
+export function parseCliArgs(args: string | readonly string[]): ParsedCliArgs {
+ const tokens =
+ typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args);
+
+ const flags: Record = {};
+ const positionals: string[] = [];
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i]!;
+
+ if (token.startsWith("--")) {
+ const rest = token.slice(2);
+ if (!rest) continue;
+
+ // Handle --key=value syntax
+ const eqIndex = rest.indexOf("=");
+ if (eqIndex !== -1) {
+ flags[rest.slice(0, eqIndex)] = rest.slice(eqIndex + 1);
+ continue;
+ }
+
+ // Handle --key value or --flag (boolean)
+ const next = tokens[i + 1];
+ if (next !== undefined && !next.startsWith("--")) {
+ flags[rest] = next;
+ i++;
+ } else {
+ flags[rest] = null;
+ }
+ } else {
+ positionals.push(token);
+ }
+ }
+
+ return { flags, positionals };
+}
diff --git a/scripts/update-release-package-versions.test.ts b/scripts/update-release-package-versions.test.ts
new file mode 100644
index 00000000000..41056438c85
--- /dev/null
+++ b/scripts/update-release-package-versions.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it } from "vitest";
+
+import { parseArgs } from "./update-release-package-versions.ts";
+
+describe("parseArgs", () => {
+ it("parses version only", () => {
+ expect(parseArgs(["1.2.3"])).toEqual({
+ version: "1.2.3",
+ rootDir: undefined,
+ writeGithubOutput: false,
+ });
+ });
+
+ it("parses version with --root", () => {
+ expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({
+ version: "1.2.3",
+ rootDir: "/path",
+ writeGithubOutput: false,
+ });
+ });
+
+ it("parses version with --github-output", () => {
+ expect(parseArgs(["1.2.3", "--github-output"])).toEqual({
+ version: "1.2.3",
+ rootDir: undefined,
+ writeGithubOutput: true,
+ });
+ });
+
+ it("parses version with --root and --github-output", () => {
+ expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({
+ version: "1.2.3",
+ rootDir: "/path",
+ writeGithubOutput: true,
+ });
+ });
+
+ it("accepts flags before the version positional", () => {
+ expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({
+ version: "1.2.3",
+ rootDir: "/path",
+ writeGithubOutput: true,
+ });
+ });
+
+ it("throws on missing version", () => {
+ expect(() => parseArgs([])).toThrow("Usage:");
+ });
+
+ it("throws on duplicate version", () => {
+ expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow(
+ "Only one release version can be provided.",
+ );
+ });
+
+ it("throws on unknown flag", () => {
+ expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown");
+ });
+
+ it("throws on --root without value", () => {
+ expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root.");
+ });
+});
diff --git a/scripts/update-release-package-versions.ts b/scripts/update-release-package-versions.ts
index b860b85e8e8..e56babef4e0 100644
--- a/scripts/update-release-package-versions.ts
+++ b/scripts/update-release-package-versions.ts
@@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { parseCliArgs } from "@t3tools/shared/cliArgs";
+
export const releasePackageFiles = [
"apps/server/package.json",
"apps/desktop/package.json",
@@ -40,52 +42,37 @@ export function updateReleasePackageVersions(
return { changed };
}
-function parseArgs(argv: ReadonlyArray): {
+export function parseArgs(argv: ReadonlyArray): {
version: string;
rootDir: string | undefined;
writeGithubOutput: boolean;
} {
- let version: string | undefined;
- let rootDir: string | undefined;
- let writeGithubOutput = false;
-
- for (let index = 0; index < argv.length; index += 1) {
- const argument = argv[index];
- if (argument === undefined) {
- continue;
- }
-
- if (argument === "--github-output") {
- writeGithubOutput = true;
- continue;
- }
+ const { flags, positionals } = parseCliArgs(argv);
- if (argument === "--root") {
- rootDir = argv[index + 1];
- if (!rootDir) {
- throw new Error("Missing value for --root.");
- }
- index += 1;
- continue;
- }
+ const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root");
+ if (unknownFlags.length > 0) {
+ throw new Error(`Unknown argument: --${unknownFlags[0]}`);
+ }
- if (argument.startsWith("--")) {
- throw new Error(`Unknown argument: ${argument}`);
- }
+ if ("root" in flags && flags.root === null) {
+ throw new Error("Missing value for --root.");
+ }
- if (version !== undefined) {
- throw new Error("Only one release version can be provided.");
- }
- version = argument;
+ if (positionals.length > 1) {
+ throw new Error("Only one release version can be provided.");
}
- if (!version) {
+ if (positionals.length !== 1 || !positionals[0]) {
throw new Error(
"Usage: node scripts/update-release-package-versions.ts [--root ] [--github-output]",
);
}
- return { version, rootDir, writeGithubOutput };
+ return {
+ version: positionals[0],
+ rootDir: flags.root ?? undefined,
+ writeGithubOutput: "github-output" in flags,
+ };
}
const isMain =
From 459c1308a990bc3ec085a1891c05b9cc6f39615c Mon Sep 17 00:00:00 2001
From: Karabach <12261380+akarabach@users.noreply.github.com>
Date: Mon, 13 Apr 2026 12:38:09 +0300
Subject: [PATCH 6/6] feat: fixes cursor comment add boolean flags parameter
---
packages/shared/src/cliArgs.test.ts | 31 ++++++++++++++++++-
packages/shared/src/cliArgs.ts | 15 +++++++--
.../update-release-package-versions.test.ts | 8 +++++
scripts/update-release-package-versions.ts | 2 +-
4 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts
index 2afc5856a95..02c0b48805b 100644
--- a/packages/shared/src/cliArgs.test.ts
+++ b/packages/shared/src/cliArgs.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { parseCliArgs } from "./cliArgs.ts";
+import { parseCliArgs } from "./cliArgs";
describe("parseCliArgs", () => {
it("returns empty result for empty string", () => {
@@ -102,4 +102,33 @@ describe("parseCliArgs", () => {
it("ignores bare -- with no flag name", () => {
expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] });
});
+
+ it("boolean flag does not consume next token as value", () => {
+ expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual(
+ {
+ flags: { "github-output": null },
+ positionals: ["1.2.3"],
+ },
+ );
+ });
+
+ it("non-boolean flag still consumes next token", () => {
+ expect(parseCliArgs(["--root", "/path", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual(
+ {
+ flags: { root: "/path" },
+ positionals: ["1.2.3"],
+ },
+ );
+ });
+
+ it("mixes boolean and value flags with positionals", () => {
+ expect(
+ parseCliArgs(["--github-output", "--root", "/path", "1.2.3"], {
+ booleanFlags: ["github-output"],
+ }),
+ ).toEqual({
+ flags: { "github-output": null, root: "/path" },
+ positionals: ["1.2.3"],
+ });
+ });
});
diff --git a/packages/shared/src/cliArgs.ts b/packages/shared/src/cliArgs.ts
index 6f75387363d..0cb0583ce51 100644
--- a/packages/shared/src/cliArgs.ts
+++ b/packages/shared/src/cliArgs.ts
@@ -3,6 +3,10 @@ export interface ParsedCliArgs {
readonly positionals: string[];
}
+export interface ParseCliArgsOptions {
+ readonly booleanFlags?: readonly string[];
+}
+
/**
* Parse CLI-style arguments into flags and positionals.
*
@@ -21,12 +25,13 @@ export interface ParsedCliArgs {
* parseCliArgs("--effort=high")
* → { flags: { effort: "high" }, positionals: [] }
*
- * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])
+ * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"], { booleanFlags: ["github-output"] })
* → { flags: { root: "/path", "github-output": null }, positionals: ["1.2.3"] }
*/
-export function parseCliArgs(args: string | readonly string[]): ParsedCliArgs {
+export function parseCliArgs(args: string | readonly string[], options?: ParseCliArgsOptions): ParsedCliArgs {
const tokens =
typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args);
+ const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined;
const flags: Record = {};
const positionals: string[] = [];
@@ -45,6 +50,12 @@ export function parseCliArgs(args: string | readonly string[]): ParsedCliArgs {
continue;
}
+ // Known boolean flag — never consumes next token
+ if (booleanSet?.has(rest)) {
+ flags[rest] = null;
+ continue;
+ }
+
// Handle --key value or --flag (boolean)
const next = tokens[i + 1];
if (next !== undefined && !next.startsWith("--")) {
diff --git a/scripts/update-release-package-versions.test.ts b/scripts/update-release-package-versions.test.ts
index 41056438c85..9e31c7675b3 100644
--- a/scripts/update-release-package-versions.test.ts
+++ b/scripts/update-release-package-versions.test.ts
@@ -60,4 +60,12 @@ describe("parseArgs", () => {
it("throws on --root without value", () => {
expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root.");
});
+
+ it("does not consume version as --github-output value", () => {
+ expect(parseArgs(["--github-output", "1.2.3"])).toEqual({
+ version: "1.2.3",
+ rootDir: undefined,
+ writeGithubOutput: true,
+ });
+ });
});
diff --git a/scripts/update-release-package-versions.ts b/scripts/update-release-package-versions.ts
index e56babef4e0..cefeef33ea2 100644
--- a/scripts/update-release-package-versions.ts
+++ b/scripts/update-release-package-versions.ts
@@ -47,7 +47,7 @@ export function parseArgs(argv: ReadonlyArray): {
rootDir: string | undefined;
writeGithubOutput: boolean;
} {
- const { flags, positionals } = parseCliArgs(argv);
+ const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] });
const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root");
if (unknownFlags.length > 0) {