Merge pull request #7411 from earendil-works/feat/experimental-cli-parser

feat(coding-agent): add experimental CLI option parser
This commit is contained in:
Christian Klotz 2026-08-01 23:42:18 +03:00 • committed by GitHub
commit aa0ec808b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 395 additions and 0 deletions

View file

@ -0,0 +1,21 @@
export type AuthInput =
| { readonly type: "token"; readonly token: string }
| { readonly type: "file"; readonly path: string };
export interface RawAuthOptions {
readonly authToken?: string;
readonly authTokenFile?: string;
}
export function parseAuthInput(options: RawAuthOptions): { auth?: AuthInput; errors: string[] } {
if (options.authToken !== undefined && options.authTokenFile !== undefined) {
return { errors: ["--auth-token and --auth-token-file are mutually exclusive"] };
}
if (options.authToken !== undefined) {
return { auth: { type: "token", token: options.authToken }, errors: [] };
}
if (options.authTokenFile !== undefined) {
return { auth: { type: "file", path: options.authTokenFile }, errors: [] };
}
return { errors: [] };
}

View file

@ -0,0 +1,142 @@
import { type AuthInput, parseAuthInput } from "./auth.ts";
import { parseTransportAddress, type TransportAddress } from "./transport-address.ts";
interface CommandBase {
readonly command: "combined" | "server" | "client";
readonly auth?: AuthInput;
readonly remainingArgs: readonly string[];
}
export interface CombinedCommand extends CommandBase {
readonly command: "combined";
readonly listen?: readonly TransportAddress[];
}
export interface ServerCommand extends CommandBase {
readonly command: "server";
readonly listen?: readonly TransportAddress[];
}
export interface ClientCommand extends CommandBase {
readonly command: "client";
readonly connect?: TransportAddress;
}
export type Command = CombinedCommand | ServerCommand | ClientCommand;
export type CommandParseResult =
| { readonly ok: true; readonly command: Command }
| { readonly ok: false; readonly errors: readonly string[] };
const VALUE_OPTIONS = new Set(["--listen", "--connect", "--auth-token", "--auth-token-file"]);
interface RawCommandOptions {
authToken?: string;
authTokenFile?: string;
listenValues: string[];
connectValue?: string;
remainingArgs: string[];
}
function splitOption(argument: string): { option: string; inlineValue?: string } {
const equals = argument.indexOf("=");
return equals === -1
? { option: argument }
: { option: argument.slice(0, equals), inlineValue: argument.slice(equals + 1) };
}
function parseRawOptions(argv: readonly string[]): { raw: RawCommandOptions; errors: string[] } {
const raw: RawCommandOptions = { listenValues: [], remainingArgs: [] };
const errors: string[] = [];
for (let index = 0; index < argv.length; index++) {
const argument = argv[index]!;
if (argument === "--") {
raw.remainingArgs.push(...argv.slice(index));
break;
}
const { option, inlineValue } = splitOption(argument);
if (!VALUE_OPTIONS.has(option)) {
raw.remainingArgs.push(...argv.slice(index));
break;
}
let value = inlineValue;
if (value === undefined) {
const next = argv[index + 1];
if (next !== undefined && !next.startsWith("-")) {
value = next;
index++;
}
}
if (value === undefined || value === "") {
errors.push(`${option} requires a value`);
continue;
}
switch (option) {
case "--listen":
raw.listenValues.push(value);
break;
case "--connect":
if (raw.connectValue !== undefined) errors.push("--connect may only be specified once");
raw.connectValue = value;
break;
case "--auth-token":
if (raw.authToken !== undefined) errors.push("--auth-token may only be specified once");
else raw.authToken = value;
break;
case "--auth-token-file":
if (raw.authTokenFile !== undefined) errors.push("--auth-token-file may only be specified once");
else raw.authTokenFile = value;
break;
}
}
return { raw, errors };
}
export function parseCommand(argv: readonly string[]): CommandParseResult {
const [candidate, ...rest] = argv;
const commandName = candidate === "server" || candidate === "client" ? candidate : "combined";
const { raw, errors } = parseRawOptions(commandName === "combined" ? argv : rest);
const listen = raw.listenValues.flatMap((value) => {
const result = parseTransportAddress(value, "--listen");
if (result.error) errors.push(result.error);
return result.address ? [result.address] : [];
});
const connectResult = raw.connectValue ? parseTransportAddress(raw.connectValue, "--connect") : undefined;
if (connectResult?.error) errors.push(connectResult.error);
const { auth, errors: authErrors } = parseAuthInput(raw);
errors.push(...authErrors);
if (commandName === "client" && raw.listenValues.length > 0) {
errors.push("--listen is only valid for combined or server mode");
}
if (commandName !== "client" && raw.connectValue !== undefined) {
errors.push("--connect is only valid for client mode");
}
if (errors.length > 0) return { ok: false, errors };
const common = {
remainingArgs: raw.remainingArgs,
...(auth === undefined ? {} : { auth }),
};
if (commandName === "client") {
return {
ok: true,
command: {
...common,
command: commandName,
...(connectResult?.address === undefined ? {} : { connect: connectResult.address }),
},
};
}
return {
ok: true,
command: {
...common,
command: commandName,
...(listen.length === 0 ? {} : { listen }),
},
};
}

View file

@ -0,0 +1,48 @@
import { posix } from "node:path";
export interface UnixTransportAddress {
readonly transport: "unix";
readonly path: string;
}
export type TransportAddress = UnixTransportAddress;
export function parseTransportAddress(
value: string,
option: "--listen" | "--connect",
): { address?: TransportAddress; error?: string } {
let url: URL;
try {
url = new URL(value);
} catch {
return { error: `Invalid ${option} address "${value}"` };
}
if (url.protocol !== "unix:") {
return { error: `Unsupported ${option} transport "${url.protocol}"` };
}
if (url.hostname || url.port || url.username || url.password) {
return { error: "Unix transport address must not include an authority" };
}
if (
!value.startsWith("unix:///") ||
value.startsWith("unix:////") ||
value.includes("?") ||
value.includes("#") ||
url.href !== value
) {
return { error: `Invalid ${option} address "${value}"` };
}
let path: string;
try {
path = decodeURIComponent(url.pathname);
} catch {
return { error: `Invalid ${option} address "${value}"` };
}
if (path.includes("\0")) {
return { error: `Invalid ${option} address "${value}"` };
}
if (!posix.isAbsolute(path)) {
return { error: "Unix transport address requires an absolute path" };
}
return { address: { transport: "unix", path } };
}

View file

@ -0,0 +1,184 @@
import { describe, expect, test } from "vitest";
import { parseCommand } from "../src/cli/experimental/command.ts";
describe("parseCommand", () => {
test("selects combined mode and preserves existing CLI arguments", () => {
expect(
parseCommand([
"--cwd",
"/workspace",
"--provider=anthropic",
"--model",
"claude-sonnet",
"--thinking=high",
"inspect",
"the project",
]),
).toEqual({
ok: true,
command: {
command: "combined",
remainingArgs: [
"--cwd",
"/workspace",
"--provider=anthropic",
"--model",
"claude-sonnet",
"--thinking=high",
"inspect",
"the project",
],
},
});
});
test("parses repeatable server listeners and preserves other arguments", () => {
expect(
parseCommand([
"server",
"--listen",
"unix:///tmp/pi.sock",
"--listen=unix:///tmp/pi-admin.sock",
"--model",
"claude-sonnet",
]),
).toEqual({
ok: true,
command: {
command: "server",
listen: [
{ transport: "unix", path: "/tmp/pi.sock" },
{ transport: "unix", path: "/tmp/pi-admin.sock" },
],
remainingArgs: ["--model", "claude-sonnet"],
},
});
});
test("preserves existing option values that look like experimental options", () => {
expect(parseCommand(["--system-prompt", "--listen", "unix:///tmp/pi.sock"])).toEqual({
ok: true,
command: {
command: "combined",
remainingArgs: ["--system-prompt", "--listen", "unix:///tmp/pi.sock"],
},
});
});
test("preserves experimental-looking arguments after the existing option prefix begins", () => {
expect(
parseCommand([
"server",
"--listen",
"unix:///tmp/pi.sock",
"--model",
"claude-sonnet",
"--listen=unix:///tmp/second.sock",
]),
).toEqual({
ok: true,
command: {
command: "server",
listen: [{ transport: "unix", path: "/tmp/pi.sock" }],
remainingArgs: ["--model", "claude-sonnet", "--listen=unix:///tmp/second.sock"],
},
});
});
test("parses a client transport address", () => {
expect(parseCommand(["client", "--connect", "unix:///tmp/pi.sock", "hello"])).toEqual({
ok: true,
command: {
command: "client",
connect: { transport: "unix", path: "/tmp/pi.sock" },
remainingArgs: ["hello"],
},
});
});
test.each([
[["--auth-token", "secret"], { type: "token", token: "secret" }],
[["--auth-token-file", "/tmp/token"], { type: "file", path: "/tmp/token" }],
] as const)("parses authentication source %j", (argv, auth) => {
expect(parseCommand(argv)).toEqual({
ok: true,
command: { command: "combined", auth, remainingArgs: [] },
});
});
test.each([[[]], [["server"]], [["client"]]] as const)(
"permits omitted authentication for later environment/default resolution",
(argv) => {
expect(parseCommand(argv)).toEqual({
ok: true,
command: { command: argv[0] ?? "combined", remainingArgs: [] },
});
},
);
test("preserves unknown options, @file arguments, and the positional separator", () => {
expect(parseCommand(["--unknown", "@prompt.md", "--", "--listen", "unix:///tmp/pi.sock"])).toEqual({
ok: true,
command: {
command: "combined",
remainingArgs: ["--unknown", "@prompt.md", "--", "--listen", "unix:///tmp/pi.sock"],
},
});
});
test.each([
[
["--auth-token", "secret", "--auth-token-file", "/tmp/token"],
"--auth-token and --auth-token-file are mutually exclusive",
],
[["--auth-token", "first", "--auth-token", "second"], "--auth-token may only be specified once"],
[
["--auth-token-file", "/tmp/first", "--auth-token-file=/tmp/second"],
"--auth-token-file may only be specified once",
],
[["--listen", "/tmp/pi.sock"], 'Invalid --listen address "/tmp/pi.sock"'],
[["--listen", "ws://localhost:8080"], 'Unsupported --listen transport "ws:"'],
[["--listen", "unix://relative.sock"], "Unix transport address must not include an authority"],
[["--listen", "unix:///tmp/pi.sock?wrong=value"], 'Invalid --listen address "unix:///tmp/pi.sock?wrong=value"'],
[["--listen", "unix:///tmp/pi.sock#fragment"], 'Invalid --listen address "unix:///tmp/pi.sock#fragment"'],
[["--listen", "unix:/tmp/pi.sock"], 'Invalid --listen address "unix:/tmp/pi.sock"'],
[["--listen", "unix:///tmp/%00pi.sock"], 'Invalid --listen address "unix:///tmp/%00pi.sock"'],
[["client", "--listen", "unix:///tmp/pi.sock"], "--listen is only valid for combined or server mode"],
[["server", "--connect", "unix:///tmp/pi.sock"], "--connect is only valid for client mode"],
[["client", "--connect", "ws://localhost:8080"], 'Unsupported --connect transport "ws:"'],
[["--listen"], "--listen requires a value"],
[["--connect="], "--connect requires a value"],
] as const)("rejects invalid experimental input %j", (argv, error) => {
const result = parseCommand(argv);
expect(result).toMatchObject({ ok: false });
if (!result.ok) expect(result.errors).toContainEqual(expect.stringContaining(error));
});
test("reports independent experimental errors together", () => {
expect(
parseCommand([
"client",
"--listen",
"ws://localhost:8080",
"--auth-token",
"secret",
"--auth-token-file",
"/tmp/token",
]),
).toEqual({
ok: false,
errors: [
'Unsupported --listen transport "ws:"',
"--auth-token and --auth-token-file are mutually exclusive",
"--listen is only valid for combined or server mode",
],
});
});
test("treats command names after the first argument as existing CLI arguments", () => {
expect(parseCommand(["--cwd", "/workspace", "server"])).toEqual({
ok: true,
command: { command: "combined", remainingArgs: ["--cwd", "/workspace", "server"] },
});
});
});