From 9607776ed784d7f4bb0989845af35f858b138935 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 13:36:39 +0100 Subject: [PATCH] refactor: share cli root option scanning --- src/cli/container-target.ts | 41 +++++++------------------- src/cli/profile.ts | 52 ++++++++++----------------------- src/cli/root-option-scan.ts | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 68 deletions(-) create mode 100644 src/cli/root-option-scan.ts diff --git a/src/cli/container-target.ts b/src/cli/container-target.ts index b72b3db8593..9ec2ac82f6f 100644 --- a/src/cli/container-target.ts +++ b/src/cli/container-target.ts @@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process"; import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; -import { forwardConsumedCliRootOption } from "./root-option-forward.js"; +import { scanCliRootOptions } from "./root-option-scan.js"; import { takeCliRootOptionValue } from "./root-option-value.js"; type CliContainerParseResult = @@ -27,47 +27,26 @@ type ContainerRuntimeExec = { }; export function parseCliContainerArgs(argv: string[]): CliContainerParseResult { - if (argv.length < 2) { - return { ok: true, container: null, argv }; - } - - const out: string[] = argv.slice(0, 2); let container: string | null = null; - const args = argv.slice(2); - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - if (arg === undefined) { - continue; - } - if (arg === FLAG_TERMINATOR) { - out.push(arg, ...args.slice(i + 1)); - break; - } - + const scanned = scanCliRootOptions(argv, ({ arg, args, index }) => { if (arg === "--container" || arg.startsWith("--container=")) { - const next = args[i + 1]; + const next = args[index + 1]; const { value, consumedNext } = takeCliRootOptionValue(arg, next); - if (consumedNext) { - i += 1; - } if (!value) { - return { ok: false, error: "--container requires a value" }; + return { kind: "error", error: "--container requires a value" }; } container = value; - continue; + return { kind: "handled", consumedNext }; } + return { kind: "pass" }; + }); - const consumedRootOption = forwardConsumedCliRootOption(args, i, out); - if (consumedRootOption > 0) { - i += consumedRootOption - 1; - continue; - } - - out.push(arg); + if (!scanned.ok) { + return scanned; } - return { ok: true, container, argv: out }; + return { ok: true, container, argv: scanned.argv }; } export function resolveCliContainerTarget( diff --git a/src/cli/profile.ts b/src/cli/profile.ts index b443a3cc50e..894c2dcb6c0 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -1,6 +1,5 @@ import os from "node:os"; import path from "node:path"; -import { FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; import { normalizeLowercaseStringOrEmpty, @@ -8,7 +7,7 @@ import { } from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { isValidProfileName } from "./profile-utils.js"; -import { forwardConsumedCliRootOption } from "./root-option-forward.js"; +import { scanCliRootOptions } from "./root-option-scan.js"; import { takeCliRootOptionValue } from "./root-option-value.js"; export type CliProfileParseResult = @@ -16,70 +15,49 @@ export type CliProfileParseResult = | { ok: false; error: string }; export function parseCliProfileArgs(argv: string[]): CliProfileParseResult { - if (argv.length < 2) { - return { ok: true, profile: null, argv }; - } - - const out: string[] = argv.slice(0, 2); let profile: string | null = null; let sawDev = false; - const args = argv.slice(2); - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - if (arg === undefined) { - continue; - } - if (arg === FLAG_TERMINATOR) { - out.push(arg, ...args.slice(i + 1)); - break; - } - + const scanned = scanCliRootOptions(argv, ({ arg, args, index, out }) => { if (arg === "--dev") { if (resolveCliArgvInvocation(out).primary === "gateway") { out.push(arg); - continue; + return { kind: "handled" }; } if (profile && profile !== "dev") { - return { ok: false, error: "Cannot combine --dev with --profile" }; + return { kind: "error", error: "Cannot combine --dev with --profile" }; } sawDev = true; profile = "dev"; - continue; + return { kind: "handled" }; } if (arg === "--profile" || arg.startsWith("--profile=")) { if (sawDev) { - return { ok: false, error: "Cannot combine --dev with --profile" }; + return { kind: "error", error: "Cannot combine --dev with --profile" }; } - const next = args[i + 1]; + const next = args[index + 1]; const { value, consumedNext } = takeCliRootOptionValue(arg, next); - if (consumedNext) { - i += 1; - } if (!value) { - return { ok: false, error: "--profile requires a value" }; + return { kind: "error", error: "--profile requires a value" }; } if (!isValidProfileName(value)) { return { - ok: false, + kind: "error", error: 'Invalid --profile (use letters, numbers, "_", "-" only)', }; } profile = value; - continue; + return { kind: "handled", consumedNext }; } + return { kind: "pass" }; + }); - const consumedRootOption = forwardConsumedCliRootOption(args, i, out); - if (consumedRootOption > 0) { - i += consumedRootOption - 1; - continue; - } - - out.push(arg); + if (!scanned.ok) { + return scanned; } - return { ok: true, profile, argv: out }; + return { ok: true, profile, argv: scanned.argv }; } function resolveProfileStateDir( diff --git a/src/cli/root-option-scan.ts b/src/cli/root-option-scan.ts new file mode 100644 index 00000000000..509d2962552 --- /dev/null +++ b/src/cli/root-option-scan.ts @@ -0,0 +1,57 @@ +import { FLAG_TERMINATOR } from "../infra/cli-root-options.js"; +import { forwardConsumedCliRootOption } from "./root-option-forward.js"; + +export type CliRootOptionScanResult = { ok: true; argv: string[] } | { ok: false; error: string }; + +type CliRootOptionVisitResult = + | { kind: "pass" } + | { kind: "handled"; consumedNext?: boolean } + | { kind: "error"; error: string }; + +export function scanCliRootOptions( + argv: string[], + visit: (params: { + arg: string; + args: string[]; + index: number; + out: string[]; + }) => CliRootOptionVisitResult, +): CliRootOptionScanResult { + if (argv.length < 2) { + return { ok: true, argv }; + } + + const out: string[] = argv.slice(0, 2); + const args = argv.slice(2); + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === undefined) { + continue; + } + if (arg === FLAG_TERMINATOR) { + out.push(arg, ...args.slice(i + 1)); + break; + } + + const visited = visit({ arg, args, index: i, out }); + if (visited.kind === "error") { + return { ok: false, error: visited.error }; + } + if (visited.kind === "handled") { + if (visited.consumedNext) { + i += 1; + } + continue; + } + + const consumedRootOption = forwardConsumedCliRootOption(args, i, out); + if (consumedRootOption > 0) { + i += consumedRootOption - 1; + continue; + } + + out.push(arg); + } + + return { ok: true, argv: out }; +}