| author | |
| committer | |
| log | 5e1dd56d2c534d90ba3363f97b1e1e23412507c0 |
| tree | 1724a1a96434558cc5bd400332faa583d2fe2396 |
| parent | 68061af0420ba6acd9c1a4c2f77eeddfff0e8bd2 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>10 files changed, 345 insertions(+), 34 deletions(-)
apps/server/src/provider/Layers/ClaudeAdapter.ts+3| ... | @@ -20,6 +20,7 @@ import { | ... | @@ -20,6 +20,7 @@ import { |
| 20 | ModelUsage, | 20 | ModelUsage, |
| 21 | NonNullableUsage, | 21 | NonNullableUsage, |
| 22 | } from "@anthropic-ai/claude-agent-sdk"; | 22 | } from "@anthropic-ai/claude-agent-sdk"; |
| 23 | import { parseCliArgs } from "@t3tools/shared/cliArgs"; | ||
| 23 | import { | 24 | import { |
| 24 | ApprovalRequestId, | 25 | ApprovalRequestId, |
| 25 | type CanonicalItemType, | 26 | type CanonicalItemType, |
| ... | @@ -2742,6 +2743,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2742,6 +2743,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2742 | ), | 2743 | ), |
| 2743 | ); | 2744 | ); |
| 2744 | const claudeBinaryPath = claudeSettings.binaryPath; | 2745 | const claudeBinaryPath = claudeSettings.binaryPath; |
| 2746 | const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags; | ||
| 2745 | const modelSelection = | 2747 | const modelSelection = |
| 2746 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined; | 2748 | input.modelSelection?.provider === "claudeAgent" ? input.modelSelection : undefined; |
| 2747 | const caps = getClaudeModelCapabilities(modelSelection?.model); | 2749 | const caps = getClaudeModelCapabilities(modelSelection?.model); |
| ... | @@ -2781,6 +2783,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2781,6 +2783,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2781 | canUseTool, | 2783 | canUseTool, |
| 2782 | env: process.env, | 2784 | env: process.env, |
| 2783 | ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), | 2785 | ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), |
| 2786 | ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}), | ||
| 2784 | }; | 2787 | }; |
| 2785 | 2788 | ||
| 2786 | const queryRuntime = yield* Effect.try({ | 2789 | const queryRuntime = yield* Effect.try({ |
apps/server/src/serverSettings.test.ts+2| ... | @@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { | ... | @@ -92,6 +92,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 92 | enabled: true, | 92 | enabled: true, |
| 93 | binaryPath: "/usr/local/bin/claude", | 93 | binaryPath: "/usr/local/bin/claude", |
| 94 | customModels: ["claude-custom"], | 94 | customModels: ["claude-custom"], |
| 95 | launchArgs: "", | ||
| 95 | }); | 96 | }); |
| 96 | assert.deepEqual(next.textGenerationModelSelection, { | 97 | assert.deepEqual(next.textGenerationModelSelection, { |
| 97 | provider: "codex", | 98 | provider: "codex", |
| ... | @@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { | ... | @@ -167,6 +168,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { |
| 167 | enabled: true, | 168 | enabled: true, |
| 168 | binaryPath: "/opt/homebrew/bin/claude", | 169 | binaryPath: "/opt/homebrew/bin/claude", |
| 169 | customModels: [], | 170 | customModels: [], |
| 171 | launchArgs: "", | ||
| 170 | }); | 172 | }); |
| 171 | }).pipe(Effect.provide(makeServerSettingsLayer())), | 173 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 172 | ); | 174 | ); |
apps/web/src/components/KeybindingsToast.browser.tsx+1-1| ... | @@ -98,7 +98,7 @@ function createBaseServerConfig(): ServerConfig { | ... | @@ -98,7 +98,7 @@ function createBaseServerConfig(): ServerConfig { |
| 98 | textGenerationModelSelection: { provider: "codex" as const, model: "gpt-5.4-mini" }, | 98 | textGenerationModelSelection: { provider: "codex" as const, model: "gpt-5.4-mini" }, |
| 99 | providers: { | 99 | providers: { |
| 100 | codex: { enabled: true, binaryPath: "", homePath: "", customModels: [] }, | 100 | codex: { enabled: true, binaryPath: "", homePath: "", customModels: [] }, |
| 101 | claudeAgent: { enabled: true, binaryPath: "", customModels: [] }, | 101 | claudeAgent: { enabled: true, binaryPath: "", customModels: [], launchArgs: "" }, |
| 102 | }, | 102 | }, |
| 103 | }, | 103 | }, |
| 104 | }; | 104 | }; |
apps/web/src/components/settings/SettingsPanels.tsx+33-1| ... | @@ -511,7 +511,8 @@ export function GeneralSettingsPanel() { | ... | @@ -511,7 +511,8 @@ export function GeneralSettingsPanel() { |
| 511 | claudeAgent: Boolean( | 511 | claudeAgent: Boolean( |
| 512 | settings.providers.claudeAgent.binaryPath !== | 512 | settings.providers.claudeAgent.binaryPath !== |
| 513 | DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath || | 513 | DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath || |
| 514 | settings.providers.claudeAgent.customModels.length > 0, | 514 | settings.providers.claudeAgent.customModels.length > 0 || |
| 515 | settings.providers.claudeAgent.launchArgs !== "", | ||
| 515 | ), | 516 | ), |
| 516 | }); | 517 | }); |
| 517 | const [customModelInputByProvider, setCustomModelInputByProvider] = useState< | 518 | const [customModelInputByProvider, setCustomModelInputByProvider] = useState< |
| ... | @@ -1278,6 +1279,37 @@ export function GeneralSettingsPanel() { | ... | @@ -1278,6 +1279,37 @@ export function GeneralSettingsPanel() { |
| 1278 | </div> | 1279 | </div> |
| 1279 | ) : null} | 1280 | ) : null} |
| 1280 | 1281 | ||
| 1282 | {providerCard.provider === "claudeAgent" ? ( | ||
| 1283 | <div className="border-t border-border/60 px-4 py-3 sm:px-5"> | ||
| 1284 | <label htmlFor="provider-install-claudeAgent-launch-args" className="block"> | ||
| 1285 | <span className="text-xs font-medium text-foreground"> | ||
| 1286 | Launch arguments | ||
| 1287 | </span> | ||
| 1288 | <Input | ||
| 1289 | id="provider-install-claudeAgent-launch-args" | ||
| 1290 | className="mt-1.5" | ||
| 1291 | value={settings.providers.claudeAgent.launchArgs} | ||
| 1292 | onChange={(event) => | ||
| 1293 | updateSettings({ | ||
| 1294 | providers: { | ||
| 1295 | ...settings.providers, | ||
| 1296 | claudeAgent: { | ||
| 1297 | ...settings.providers.claudeAgent, | ||
| 1298 | launchArgs: event.target.value, | ||
| 1299 | }, | ||
| 1300 | }, | ||
| 1301 | }) | ||
| 1302 | } | ||
| 1303 | placeholder="e.g. --chrome" | ||
| 1304 | spellCheck={false} | ||
| 1305 | /> | ||
| 1306 | <span className="mt-1 block text-xs text-muted-foreground"> | ||
| 1307 | Additional CLI arguments passed to Claude Code on session start. | ||
| 1308 | </span> | ||
| 1309 | </label> | ||
| 1310 | </div> | ||
| 1311 | ) : null} | ||
| 1312 | |||
| 1281 | <div className="border-t border-border/60 px-4 py-3 sm:px-5"> | 1313 | <div className="border-t border-border/60 px-4 py-3 sm:px-5"> |
| 1282 | <div className="text-xs font-medium text-foreground">Models</div> | 1314 | <div className="text-xs font-medium text-foreground">Models</div> |
| 1283 | <div className="mt-1 text-xs text-muted-foreground"> | 1315 | <div className="mt-1 text-xs text-muted-foreground"> |
packages/contracts/src/settings.ts+2| ... | @@ -70,6 +70,7 @@ export const ClaudeSettings = Schema.Struct({ | ... | @@ -70,6 +70,7 @@ export const ClaudeSettings = Schema.Struct({ |
| 70 | enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | 70 | enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 71 | binaryPath: makeBinaryPathSetting("claude"), | 71 | binaryPath: makeBinaryPathSetting("claude"), |
| 72 | customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))), | 72 | customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))), |
| 73 | launchArgs: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))), | ||
| 73 | }); | 74 | }); |
| 74 | export type ClaudeSettings = typeof ClaudeSettings.Type; | 75 | export type ClaudeSettings = typeof ClaudeSettings.Type; |
| 75 | 76 | ||
| ... | @@ -164,6 +165,7 @@ const ClaudeSettingsPatch = Schema.Struct({ | ... | @@ -164,6 +165,7 @@ const ClaudeSettingsPatch = Schema.Struct({ |
| 164 | enabled: Schema.optionalKey(Schema.Boolean), | 165 | enabled: Schema.optionalKey(Schema.Boolean), |
| 165 | binaryPath: Schema.optionalKey(Schema.String), | 166 | binaryPath: Schema.optionalKey(Schema.String), |
| 166 | customModels: Schema.optionalKey(Schema.Array(Schema.String)), | 167 | customModels: Schema.optionalKey(Schema.Array(Schema.String)), |
| 168 | launchArgs: Schema.optionalKey(Schema.String), | ||
| 167 | }); | 169 | }); |
| 168 | 170 | ||
| 169 | export const ServerSettingsPatch = Schema.Struct({ | 171 | export const ServerSettingsPatch = Schema.Struct({ |
packages/shared/package.json+4| ... | @@ -60,6 +60,10 @@ | ... | @@ -60,6 +60,10 @@ |
| 60 | "types": "./src/qrCode.ts", | 60 | "types": "./src/qrCode.ts", |
| 61 | "import": "./src/qrCode.ts" | 61 | "import": "./src/qrCode.ts" |
| 62 | }, | 62 | }, |
| 63 | "./cliArgs": { | ||
| 64 | "types": "./src/cliArgs.ts", | ||
| 65 | "import": "./src/cliArgs.ts" | ||
| 66 | }, | ||
| 63 | "./path": { | 67 | "./path": { |
| 64 | "types": "./src/path.ts", | 68 | "types": "./src/path.ts", |
| 65 | "import": "./src/path.ts" | 69 | "import": "./src/path.ts" |
packages/shared/src/cliArgs.test.ts created+134| ... | @@ -0,0 +1,134 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { parseCliArgs } from "./cliArgs"; | ||
| 4 | |||
| 5 | describe("parseCliArgs", () => { | ||
| 6 | it("returns empty result for empty string", () => { | ||
| 7 | expect(parseCliArgs("")).toEqual({ flags: {}, positionals: [] }); | ||
| 8 | }); | ||
| 9 | |||
| 10 | it("returns empty result for whitespace-only string", () => { | ||
| 11 | expect(parseCliArgs(" ")).toEqual({ flags: {}, positionals: [] }); | ||
| 12 | }); | ||
| 13 | |||
| 14 | it("returns empty result for empty array", () => { | ||
| 15 | expect(parseCliArgs([])).toEqual({ flags: {}, positionals: [] }); | ||
| 16 | }); | ||
| 17 | |||
| 18 | it("parses --chrome boolean flag", () => { | ||
| 19 | expect(parseCliArgs("--chrome")).toEqual({ | ||
| 20 | flags: { chrome: null }, | ||
| 21 | positionals: [], | ||
| 22 | }); | ||
| 23 | }); | ||
| 24 | |||
| 25 | it("parses --chrome with --verbose", () => { | ||
| 26 | expect(parseCliArgs("--chrome --verbose")).toEqual({ | ||
| 27 | flags: { chrome: null, verbose: null }, | ||
| 28 | positionals: [], | ||
| 29 | }); | ||
| 30 | }); | ||
| 31 | |||
| 32 | it("parses --effort with a value", () => { | ||
| 33 | expect(parseCliArgs("--effort high")).toEqual({ | ||
| 34 | flags: { effort: "high" }, | ||
| 35 | positionals: [], | ||
| 36 | }); | ||
| 37 | }); | ||
| 38 | |||
| 39 | it("parses --chrome --effort high --debug", () => { | ||
| 40 | expect(parseCliArgs("--chrome --effort high --debug")).toEqual({ | ||
| 41 | flags: { chrome: null, effort: "high", debug: null }, | ||
| 42 | positionals: [], | ||
| 43 | }); | ||
| 44 | }); | ||
| 45 | |||
| 46 | it("parses --model with full model name", () => { | ||
| 47 | expect(parseCliArgs("--model claude-sonnet-4-6")).toEqual({ | ||
| 48 | flags: { model: "claude-sonnet-4-6" }, | ||
| 49 | positionals: [], | ||
| 50 | }); | ||
| 51 | }); | ||
| 52 | |||
| 53 | it("parses --append-system-prompt with value and --chrome", () => { | ||
| 54 | expect(parseCliArgs("--append-system-prompt always-think-step-by-step --chrome")).toEqual({ | ||
| 55 | flags: { "append-system-prompt": "always-think-step-by-step", chrome: null }, | ||
| 56 | positionals: [], | ||
| 57 | }); | ||
| 58 | }); | ||
| 59 | |||
| 60 | it("parses --max-budget-usd with numeric value", () => { | ||
| 61 | expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({ | ||
| 62 | flags: { chrome: null, "max-budget-usd": "5.00" }, | ||
| 63 | positionals: [], | ||
| 64 | }); | ||
| 65 | }); | ||
| 66 | |||
| 67 | it("parses --effort=high syntax", () => { | ||
| 68 | expect(parseCliArgs("--effort=high")).toEqual({ | ||
| 69 | flags: { effort: "high" }, | ||
| 70 | positionals: [], | ||
| 71 | }); | ||
| 72 | }); | ||
| 73 | |||
| 74 | it("parses --key=value mixed with boolean flags", () => { | ||
| 75 | expect(parseCliArgs("--chrome --model=claude-sonnet-4-6 --debug")).toEqual({ | ||
| 76 | flags: { chrome: null, model: "claude-sonnet-4-6", debug: null }, | ||
| 77 | positionals: [], | ||
| 78 | }); | ||
| 79 | }); | ||
| 80 | |||
| 81 | it("collects positional arguments", () => { | ||
| 82 | expect(parseCliArgs("1.2.3")).toEqual({ | ||
| 83 | flags: {}, | ||
| 84 | positionals: ["1.2.3"], | ||
| 85 | }); | ||
| 86 | }); | ||
| 87 | |||
| 88 | it("collects positionals mixed with flags (argv array)", () => { | ||
| 89 | expect(parseCliArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({ | ||
| 90 | flags: { root: "/path", "github-output": null }, | ||
| 91 | positionals: ["1.2.3"], | ||
| 92 | }); | ||
| 93 | }); | ||
| 94 | |||
| 95 | it("handles extra whitespace between tokens", () => { | ||
| 96 | expect(parseCliArgs(" --chrome --verbose ")).toEqual({ | ||
| 97 | flags: { chrome: null, verbose: null }, | ||
| 98 | positionals: [], | ||
| 99 | }); | ||
| 100 | }); | ||
| 101 | |||
| 102 | it("ignores bare -- with no flag name", () => { | ||
| 103 | expect(parseCliArgs("--")).toEqual({ flags: {}, positionals: [] }); | ||
| 104 | }); | ||
| 105 | |||
| 106 | it("boolean flag does not consume next token as value", () => { | ||
| 107 | expect(parseCliArgs(["--github-output", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual( | ||
| 108 | { | ||
| 109 | flags: { "github-output": null }, | ||
| 110 | positionals: ["1.2.3"], | ||
| 111 | }, | ||
| 112 | ); | ||
| 113 | }); | ||
| 114 | |||
| 115 | it("non-boolean flag still consumes next token", () => { | ||
| 116 | expect(parseCliArgs(["--root", "/path", "1.2.3"], { booleanFlags: ["github-output"] })).toEqual( | ||
| 117 | { | ||
| 118 | flags: { root: "/path" }, | ||
| 119 | positionals: ["1.2.3"], | ||
| 120 | }, | ||
| 121 | ); | ||
| 122 | }); | ||
| 123 | |||
| 124 | it("mixes boolean and value flags with positionals", () => { | ||
| 125 | expect( | ||
| 126 | parseCliArgs(["--github-output", "--root", "/path", "1.2.3"], { | ||
| 127 | booleanFlags: ["github-output"], | ||
| 128 | }), | ||
| 129 | ).toEqual({ | ||
| 130 | flags: { "github-output": null, root: "/path" }, | ||
| 131 | positionals: ["1.2.3"], | ||
| 132 | }); | ||
| 133 | }); | ||
| 134 | }); | ||
packages/shared/src/cliArgs.ts created+76| ... | @@ -0,0 +1,76 @@ | ||
| 1 | export interface ParsedCliArgs { | ||
| 2 | readonly flags: Record<string, string | null>; | ||
| 3 | readonly positionals: string[]; | ||
| 4 | } | ||
| 5 | |||
| 6 | export interface ParseCliArgsOptions { | ||
| 7 | readonly booleanFlags?: readonly string[]; | ||
| 8 | } | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Parse CLI-style arguments into flags and positionals. | ||
| 12 | * | ||
| 13 | * Accepts a string (split by whitespace) or a pre-split argv array. | ||
| 14 | * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax. | ||
| 15 | * | ||
| 16 | * parseCliArgs("") | ||
| 17 | * → { flags: {}, positionals: [] } | ||
| 18 | * | ||
| 19 | * parseCliArgs("--chrome") | ||
| 20 | * → { flags: { chrome: null }, positionals: [] } | ||
| 21 | * | ||
| 22 | * parseCliArgs("--chrome --effort high") | ||
| 23 | * → { flags: { chrome: null, effort: "high" }, positionals: [] } | ||
| 24 | * | ||
| 25 | * parseCliArgs("--effort=high") | ||
| 26 | * → { flags: { effort: "high" }, positionals: [] } | ||
| 27 | * | ||
| 28 | * parseCliArgs(["1.2.3", "--root", "/path", "--github-output"], { booleanFlags: ["github-output"] }) | ||
| 29 | * → { flags: { root: "/path", "github-output": null }, positionals: ["1.2.3"] } | ||
| 30 | */ | ||
| 31 | export function parseCliArgs( | ||
| 32 | args: string | readonly string[], | ||
| 33 | options?: ParseCliArgsOptions, | ||
| 34 | ): ParsedCliArgs { | ||
| 35 | const tokens = | ||
| 36 | typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args); | ||
| 37 | const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined; | ||
| 38 | |||
| 39 | const flags: Record<string, string | null> = {}; | ||
| 40 | const positionals: string[] = []; | ||
| 41 | |||
| 42 | for (let i = 0; i < tokens.length; i++) { | ||
| 43 | const token = tokens[i]!; | ||
| 44 | |||
| 45 | if (token.startsWith("--")) { | ||
| 46 | const rest = token.slice(2); | ||
| 47 | if (!rest) continue; | ||
| 48 | |||
| 49 | // Handle --key=value syntax | ||
| 50 | const eqIndex = rest.indexOf("="); | ||
| 51 | if (eqIndex !== -1) { | ||
| 52 | flags[rest.slice(0, eqIndex)] = rest.slice(eqIndex + 1); | ||
| 53 | continue; | ||
| 54 | } | ||
| 55 | |||
| 56 | // Known boolean flag — never consumes next token | ||
| 57 | if (booleanSet?.has(rest)) { | ||
| 58 | flags[rest] = null; | ||
| 59 | continue; | ||
| 60 | } | ||
| 61 | |||
| 62 | // Handle --key value or --flag (boolean) | ||
| 63 | const next = tokens[i + 1]; | ||
| 64 | if (next !== undefined && !next.startsWith("--")) { | ||
| 65 | flags[rest] = next; | ||
| 66 | i++; | ||
| 67 | } else { | ||
| 68 | flags[rest] = null; | ||
| 69 | } | ||
| 70 | } else { | ||
| 71 | positionals.push(token); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | return { flags, positionals }; | ||
| 76 | } | ||
scripts/update-release-package-versions.test.ts created+71| ... | @@ -0,0 +1,71 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { parseArgs } from "./update-release-package-versions.ts"; | ||
| 4 | |||
| 5 | describe("parseArgs", () => { | ||
| 6 | it("parses version only", () => { | ||
| 7 | expect(parseArgs(["1.2.3"])).toEqual({ | ||
| 8 | version: "1.2.3", | ||
| 9 | rootDir: undefined, | ||
| 10 | writeGithubOutput: false, | ||
| 11 | }); | ||
| 12 | }); | ||
| 13 | |||
| 14 | it("parses version with --root", () => { | ||
| 15 | expect(parseArgs(["1.2.3", "--root", "/path"])).toEqual({ | ||
| 16 | version: "1.2.3", | ||
| 17 | rootDir: "/path", | ||
| 18 | writeGithubOutput: false, | ||
| 19 | }); | ||
| 20 | }); | ||
| 21 | |||
| 22 | it("parses version with --github-output", () => { | ||
| 23 | expect(parseArgs(["1.2.3", "--github-output"])).toEqual({ | ||
| 24 | version: "1.2.3", | ||
| 25 | rootDir: undefined, | ||
| 26 | writeGithubOutput: true, | ||
| 27 | }); | ||
| 28 | }); | ||
| 29 | |||
| 30 | it("parses version with --root and --github-output", () => { | ||
| 31 | expect(parseArgs(["1.2.3", "--root", "/path", "--github-output"])).toEqual({ | ||
| 32 | version: "1.2.3", | ||
| 33 | rootDir: "/path", | ||
| 34 | writeGithubOutput: true, | ||
| 35 | }); | ||
| 36 | }); | ||
| 37 | |||
| 38 | it("accepts flags before the version positional", () => { | ||
| 39 | expect(parseArgs(["--github-output", "--root", "/path", "1.2.3"])).toEqual({ | ||
| 40 | version: "1.2.3", | ||
| 41 | rootDir: "/path", | ||
| 42 | writeGithubOutput: true, | ||
| 43 | }); | ||
| 44 | }); | ||
| 45 | |||
| 46 | it("throws on missing version", () => { | ||
| 47 | expect(() => parseArgs([])).toThrow("Usage:"); | ||
| 48 | }); | ||
| 49 | |||
| 50 | it("throws on duplicate version", () => { | ||
| 51 | expect(() => parseArgs(["1.2.3", "2.0.0"])).toThrow( | ||
| 52 | "Only one release version can be provided.", | ||
| 53 | ); | ||
| 54 | }); | ||
| 55 | |||
| 56 | it("throws on unknown flag", () => { | ||
| 57 | expect(() => parseArgs(["1.2.3", "--unknown"])).toThrow("Unknown argument: --unknown"); | ||
| 58 | }); | ||
| 59 | |||
| 60 | it("throws on --root without value", () => { | ||
| 61 | expect(() => parseArgs(["1.2.3", "--root"])).toThrow("Missing value for --root."); | ||
| 62 | }); | ||
| 63 | |||
| 64 | it("does not consume version as --github-output value", () => { | ||
| 65 | expect(parseArgs(["--github-output", "1.2.3"])).toEqual({ | ||
| 66 | version: "1.2.3", | ||
| 67 | rootDir: undefined, | ||
| 68 | writeGithubOutput: true, | ||
| 69 | }); | ||
| 70 | }); | ||
| 71 | }); | ||
scripts/update-release-package-versions.ts+19-32| ... | @@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; | ... | @@ -2,6 +2,8 @@ import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; |
| 2 | import { resolve } from "node:path"; | 2 | import { resolve } from "node:path"; |
| 3 | import { fileURLToPath } from "node:url"; | 3 | import { fileURLToPath } from "node:url"; |
| 4 | 4 | ||
| 5 | import { parseCliArgs } from "@t3tools/shared/cliArgs"; | ||
| 6 | |||
| 5 | export const releasePackageFiles = [ | 7 | export const releasePackageFiles = [ |
| 6 | "apps/server/package.json", | 8 | "apps/server/package.json", |
| 7 | "apps/desktop/package.json", | 9 | "apps/desktop/package.json", |
| ... | @@ -40,52 +42,37 @@ export function updateReleasePackageVersions( | ... | @@ -40,52 +42,37 @@ export function updateReleasePackageVersions( |
| 40 | return { changed }; | 42 | return { changed }; |
| 41 | } | 43 | } |
| 42 | 44 | ||
| 43 | function parseArgs(argv: ReadonlyArray<string>): { | 45 | export function parseArgs(argv: ReadonlyArray<string>): { |
| 44 | version: string; | 46 | version: string; |
| 45 | rootDir: string | undefined; | 47 | rootDir: string | undefined; |
| 46 | writeGithubOutput: boolean; | 48 | writeGithubOutput: boolean; |
| 47 | } { | 49 | } { |
| 48 | let version: string | undefined; | 50 | const { flags, positionals } = parseCliArgs(argv, { booleanFlags: ["github-output"] }); |
| 49 | let rootDir: string | undefined; | ||
| 50 | let writeGithubOutput = false; | ||
| 51 | |||
| 52 | for (let index = 0; index < argv.length; index += 1) { | ||
| 53 | const argument = argv[index]; | ||
| 54 | if (argument === undefined) { | ||
| 55 | continue; | ||
| 56 | } | ||
| 57 | |||
| 58 | if (argument === "--github-output") { | ||
| 59 | writeGithubOutput = true; | ||
| 60 | continue; | ||
| 61 | } | ||
| 62 | 51 | ||
| 63 | if (argument === "--root") { | 52 | const unknownFlags = Object.keys(flags).filter((k) => k !== "github-output" && k !== "root"); |
| 64 | rootDir = argv[index + 1]; | 53 | if (unknownFlags.length > 0) { |
| 65 | if (!rootDir) { | 54 | throw new Error(`Unknown argument: --${unknownFlags[0]}`); |
| 66 | throw new Error("Missing value for --root."); | 55 | } |
| 67 | } | ||
| 68 | index += 1; | ||
| 69 | continue; | ||
| 70 | } | ||
| 71 | 56 | ||
| 72 | if (argument.startsWith("--")) { | 57 | if ("root" in flags && flags.root === null) { |
| 73 | throw new Error(`Unknown argument: ${argument}`); | 58 | throw new Error("Missing value for --root."); |
| 74 | } | 59 | } |
| 75 | 60 | ||
| 76 | if (version !== undefined) { | 61 | if (positionals.length > 1) { |
| 77 | throw new Error("Only one release version can be provided."); | 62 | throw new Error("Only one release version can be provided."); |
| 78 | } | ||
| 79 | version = argument; | ||
| 80 | } | 63 | } |
| 81 | 64 | ||
| 82 | if (!version) { | 65 | if (positionals.length !== 1 || !positionals[0]) { |
| 83 | throw new Error( | 66 | throw new Error( |
| 84 | "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]", | 67 | "Usage: node scripts/update-release-package-versions.ts <version> [--root <path>] [--github-output]", |
| 85 | ); | 68 | ); |
| 86 | } | 69 | } |
| 87 | 70 | ||
| 88 | return { version, rootDir, writeGithubOutput }; | 71 | return { |
| 72 | version: positionals[0], | ||
| 73 | rootDir: flags.root ?? undefined, | ||
| 74 | writeGithubOutput: "github-output" in flags, | ||
| 75 | }; | ||
| 89 | } | 76 | } |
| 90 | 77 | ||
| 91 | const isMain = | 78 | const isMain = |