mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
26405e68c7
Follows #952 (merged); rebased onto main. ## Why `argent-cli` had **seven** copies of the same argv loop — `server start`, `link`/`unlink`, `flow run`, `lens`, `config`, `enable`/`disable`, and (from #952) `telemetry` — each hand-rolling `--x value` / `--x=value` / `-x` / unknown-flag / missing-value handling with slightly different messages and gaps (e.g. `lens` silently ignored unknown flags). Adding an option meant another `if (tok === "--x") … else if (tok.startsWith("--x="))` pair. ## What `packages/argent-cli/src/command-args.ts` (introduced in #952) now covers the whole subcommand surface, and every command declares its options as a spec: ```ts const START_OPTIONS = { help: { kind: "boolean", alias: "h" }, port: { kind: "value", alias: "p" }, … } as const satisfies OptionSpecs; ``` Parser features: `--name value`, `--name=value`, single-letter aliases, boolean switches, `--` end-of-options, `choices` validation, "don't swallow a following flag as a value", and the guard against a stray `true`/`false` word after a switch (previously duplicated in `flow` and `lens`). Value *semantics* (port ranges, host wildcards, flag-name charset, link targets) stay in each command — the parser only shapes argv. `flag-parser.ts` is untouched: it is the schema-driven `argent run <tool>` payload builder (JSON Schema → tool args, `--args`/stdin hatches), a different job. ## Behavior changes (bad-input paths only) | Command | Before | After | |---|---|---| | `lens --bogus` / `lens extra` | silently ignored | `lens: Unknown flag: --bogus`, exit 2 | | `lens --terminal x` | `--terminal expects "iterm" or "terminal", got "x"` | `--terminal must be "iterm" or "terminal", got "x"` | | `flow run --platfrom=ios` | `unknown flag --platfrom=ios` | `Unknown flag: --platfrom=ios` | | `unlink foo` | `Unknown flag: foo` | `Unexpected argument "foo"` | | `config … --scope x` | `--scope must be "global" or "project", got "x"` | unchanged wording | | `enable/disable`, `server start`, `link` | — | unchanged wording (tests pinned) | Happy paths are byte-identical; `server`/`link` still throw `StartFlagError`, `flow` still throws `FlagParseException` (wrapped from `UsageError`), so callers and tests keep their contracts. ## Tests `argent-cli` 489/489 (26 files): rewritten `command-args.test.ts` (aliases, `-` positional, empty-token value, true/false guard, choices message), pinned-string updates only where the table above says the wording changed (`flow.test.ts` ×4, `parse-link-flags.test.ts` ×1). `tsc --build`, `typecheck:tests`, eslint, prettier, knip green. Smoke-tested every command's error path through the built `packages/argent/dist/cli.js` (see the "Behavior changes" table — all exit 2 with usage where the command prints one). ## Docs No docs update needed: the pages document flags and their meaning, not error strings; no flag was added, removed or renamed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BkkAihA8Kp26hHS4uhkNGu
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parseCommandArgs, UsageError, type OptionSpecs } from "../src/command-args.js";
|
|
|
|
const SPECS = {
|
|
scope: { kind: "value", choices: ["global", "project"] },
|
|
out: { kind: "value", alias: "o" },
|
|
json: { kind: "boolean" },
|
|
yes: { kind: "boolean", alias: "y" },
|
|
} as const satisfies OptionSpecs;
|
|
|
|
describe("parseCommandArgs", () => {
|
|
it("parses value options in both spellings, boolean flags and positionals", () => {
|
|
expect(
|
|
parseCommandArgs(["a", "--scope", "project", "--json", "b", "--out=x.txt"], SPECS)
|
|
).toEqual({
|
|
positionals: ["a", "b"],
|
|
options: { scope: "project", json: true, out: "x.txt" },
|
|
});
|
|
});
|
|
|
|
it("accepts single-letter aliases for value and boolean options", () => {
|
|
expect(parseCommandArgs(["-o", "x.txt", "-y"], SPECS)).toEqual({
|
|
positionals: [],
|
|
options: { out: "x.txt", yes: true },
|
|
});
|
|
});
|
|
|
|
it("returns nothing set for an empty argv", () => {
|
|
expect(parseCommandArgs([], SPECS)).toEqual({ positionals: [], options: {} });
|
|
});
|
|
|
|
it("last occurrence of a repeated option wins", () => {
|
|
expect(parseCommandArgs(["--scope=global", "--scope", "project"], SPECS).options.scope).toBe(
|
|
"project"
|
|
);
|
|
});
|
|
|
|
it("treats everything after -- as positionals, and a bare - as a positional", () => {
|
|
expect(parseCommandArgs(["--json", "--", "--scope", "-x"], SPECS)).toEqual({
|
|
positionals: ["--scope", "-x"],
|
|
options: { json: true },
|
|
});
|
|
expect(parseCommandArgs(["-", "--out", "-"], SPECS)).toEqual({
|
|
positionals: ["-"],
|
|
options: { out: "-" },
|
|
});
|
|
});
|
|
|
|
it("rejects an unknown long or short flag, reporting the token as typed", () => {
|
|
expect(() => parseCommandArgs(["--force"], SPECS)).toThrow(UsageError);
|
|
expect(() => parseCommandArgs(["--force"], SPECS)).toThrow("Unknown flag: --force");
|
|
expect(() => parseCommandArgs(["-z"], SPECS)).toThrow("Unknown flag: -z");
|
|
expect(() => parseCommandArgs(["--platfrom=ios"], SPECS)).toThrow(
|
|
"Unknown flag: --platfrom=ios"
|
|
);
|
|
});
|
|
|
|
it("rejects a value outside the declared choices, listing them", () => {
|
|
expect(() => parseCommandArgs(["--scope", "team"], SPECS)).toThrow(
|
|
'--scope must be "global" or "project", got "team"'
|
|
);
|
|
});
|
|
|
|
it("rejects a missing value, naming the long form even for an alias", () => {
|
|
expect(() => parseCommandArgs(["--scope"], SPECS)).toThrow("--scope requires a value");
|
|
expect(() => parseCommandArgs(["-o"], SPECS)).toThrow("--out requires a value");
|
|
expect(() => parseCommandArgs(["--out="], SPECS)).toThrow("--out requires a value");
|
|
});
|
|
|
|
it("does not swallow a following flag as the value", () => {
|
|
expect(() => parseCommandArgs(["--scope", "--json"], SPECS)).toThrow(
|
|
"--scope requires a value"
|
|
);
|
|
expect(() => parseCommandArgs(["--out", "-y"], SPECS)).toThrow("--out requires a value");
|
|
});
|
|
|
|
it("passes an explicitly supplied empty token through as the value", () => {
|
|
// The command validates it (e.g. link rejects "" as a bind address).
|
|
expect(parseCommandArgs(["--out", ""], SPECS).options.out).toBe("");
|
|
});
|
|
|
|
it("rejects a value given to a boolean flag, including a trailing true/false word", () => {
|
|
expect(() => parseCommandArgs(["--json=1"], SPECS)).toThrow("--json does not take a value");
|
|
expect(() => parseCommandArgs(["--json", "false"], SPECS)).toThrow(
|
|
"--json does not take a value — it is a switch"
|
|
);
|
|
});
|
|
});
|