mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
3f2680788d
## The bug `argent <cmd> --help` for the installer subcommands `init` / `install` / `update` / `uninstall` / `remove` did not print help — it ran the real, side-effecting command. `argent uninstall --help` even opened the destructive "Remove argent configuration from this workspace?" prompt (with Yes pre-selected). Root cause: `main()` in `packages/argent/src/cli.ts` dispatched these subcommands and forwarded `rest` (including `--help`) to the installer functions, which have no help handling. Only the top-level `--help` case called `printHelp()`. ## The fix Intercept `--help`/`-h` for exactly the installer command set before dispatching, and print a short per-subcommand usage block without loading or invoking any installer code (no network, no wizard, no prompt). - New side-effect-free module `packages/argent/src/installer-help.ts` exports the pure decision function `installerHelpRequested(command, rest)` plus `printInstallerHelp(command)`. - `cli.ts` calls the guard at the top of `main()` and returns early when help is requested. - The other subcommands (`run`, `tools`, `server`, `link`, `flags`, `enable`, `disable`, `telemetry`) already handle `--help` themselves, so they are deliberately excluded — `argent run <tool> --help` still prints that tool's flags. ## Verification - `npm run build` (root tsc) + `npm run build -w @swmansion/argent` — green. - Ran the built CLI end-to-end (stdin from /dev/null): `init --help`, `update --help`, `uninstall --help`, `install --help`, `remove --help`, `uninstall -h` all print usage and exit 0 with no wizard/network/prompt. - Regression: `argent --help` still prints top-level help; `argent run --help` still prints the `run` usage (not intercepted). - Added unit tests in `packages/argent/test/installer-help.test.ts` for `installerHelpRequested` / `isInstallerCommand` (8 tests, all pass). - `prettier` and `eslint` clean on the touched files; test typecheck passes. Fixes #451
112 lines
4.8 KiB
TypeScript
112 lines
4.8 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { INSTALLER_COMMAND_META, VALUE_TAKING_FLAGS } from "../src/installer-help.js";
|
|
|
|
// Drift tripwire between the help text in INSTALLER_COMMAND_META and the flags
|
|
// the installers actually parse. installer-help.ts is deliberately
|
|
// side-effect-free, so it cannot import the parsers; instead this test reads
|
|
// the parser sources and extracts every flag they consult through the parsing
|
|
// idioms the installer uses today (`args.includes("--x")`,
|
|
// `extractFlag(args, "--x")`, and the `arg === "--x"` / `arg?.startsWith("--x=")`
|
|
// lookahead loops). If a flag is added, removed, or renamed there — or the
|
|
// installer moves to a parsing idiom this extraction doesn't know — the set
|
|
// comparison goes red and whoever made the change updates the help (or this
|
|
// test) instead of silently under- or over-reporting flags in `--help`.
|
|
|
|
const INSTALLER_SRC = path.resolve(import.meta.dirname, "../../argent-installer/src");
|
|
|
|
// Where each command's flags are parsed. init parses everything in
|
|
// init-args.ts; update/uninstall read --global/--local through
|
|
// install-targets.ts (parseTargetFlags) and the rest inline.
|
|
const PARSER_SOURCES = {
|
|
init: ["init-args.ts"],
|
|
update: ["update.ts", "install-targets.ts"],
|
|
uninstall: ["uninstall.ts", "install-targets.ts"],
|
|
} as const;
|
|
|
|
// Flags the installers parse on purpose but keep out of the help text.
|
|
// --project-root is an internal pin passed by the agent-triggered
|
|
// update-argent tool (see getProjectRootOverride in update.ts), not a flag
|
|
// users should reach for.
|
|
const INTERNAL_FLAGS: Record<keyof typeof PARSER_SOURCES, readonly string[]> = {
|
|
init: [],
|
|
update: ["--project-root"],
|
|
uninstall: [],
|
|
};
|
|
|
|
const FLAG_PARSE_PATTERNS = [
|
|
/(?<![\w.$])args\.includes\("([^"]+)"\)/g,
|
|
/extractFlag\(args,\s*"([^"]+)"\)/g,
|
|
/arg\??\s*===\s*"(-[^"]+)"/g,
|
|
/arg\??\.startsWith\("(-[^"]+)="\)/g,
|
|
];
|
|
|
|
// The idioms that consume the NEXT argv token: `extractFlag(args, "--x")` and
|
|
// the `arg === "--x"` lookahead branch (which reads `args[i + 1]`). The
|
|
// `--x=value` startsWith form deliberately does NOT count — it is inline and
|
|
// consumes nothing, so its presence must not certify a flag as value-taking.
|
|
const VALUE_CONSUMING_PATTERNS = [
|
|
/extractFlag\(args,\s*"(-[^"]+)"\)/g,
|
|
/arg\??\s*===\s*"(-[^"]+)"/g,
|
|
];
|
|
|
|
function readSources(command: keyof typeof PARSER_SOURCES): string {
|
|
return PARSER_SOURCES[command]
|
|
.map((file) => fs.readFileSync(path.join(INSTALLER_SRC, file), "utf8"))
|
|
.join("\n");
|
|
}
|
|
|
|
function parsedFlags(command: keyof typeof PARSER_SOURCES): string[] {
|
|
const source = readSources(command);
|
|
const flags = new Set<string>();
|
|
for (const pattern of FLAG_PARSE_PATTERNS) {
|
|
for (const match of source.matchAll(pattern)) {
|
|
flags.add(match[1]!);
|
|
}
|
|
}
|
|
return [...flags].sort();
|
|
}
|
|
|
|
/** The machine-readable spellings behind a display flag: `--yes, -y` → both. */
|
|
function documentedFlags(command: keyof typeof PARSER_SOURCES): string[] {
|
|
return INSTALLER_COMMAND_META[command].options.flatMap((option) =>
|
|
option.flag.split(",").map((spelling) => spelling.trim().split(" ")[0]!)
|
|
);
|
|
}
|
|
|
|
describe("installer help options stay in sync with the real parsers", () => {
|
|
for (const command of ["init", "update", "uninstall"] as const) {
|
|
it(`\`${command}\` documents exactly the flags its parser consults`, () => {
|
|
const documented = [...documentedFlags(command), ...INTERNAL_FLAGS[command]].sort();
|
|
expect(parsedFlags(command)).toEqual(documented);
|
|
});
|
|
}
|
|
|
|
for (const command of ["init", "update", "uninstall"] as const) {
|
|
it(`\`${command}\`'s VALUE_TAKING_FLAGS mirror the parser's token-consuming sites exactly`, () => {
|
|
// installerHelpRequested treats a bareword `help` after one of these as
|
|
// the flag's value rather than a help request, so the list must match
|
|
// the parser in BOTH directions: an entry the parser doesn't consume a
|
|
// token for would wrongly forward `--x help` to the installer, and a
|
|
// token-consuming parser flag missing here would wrongly swallow its
|
|
// `help` value as a help request.
|
|
const source = readSources(command);
|
|
const consuming = new Set<string>();
|
|
for (const pattern of VALUE_CONSUMING_PATTERNS) {
|
|
for (const match of source.matchAll(pattern)) {
|
|
consuming.add(match[1]!);
|
|
}
|
|
}
|
|
expect([...consuming].sort()).toEqual([...VALUE_TAKING_FLAGS[command]].sort());
|
|
});
|
|
}
|
|
|
|
it("aliases defer to a target whose value-taking flags they share", () => {
|
|
// The dispatcher forwards `install` to init and `remove` to uninstall, so
|
|
// their bareword-help semantics must match the target's.
|
|
expect(VALUE_TAKING_FLAGS.install).toEqual(VALUE_TAKING_FLAGS.init);
|
|
expect(VALUE_TAKING_FLAGS.remove).toEqual(VALUE_TAKING_FLAGS.uninstall);
|
|
});
|
|
});
|