fix(cli): report unknown root commands (#2433)

This commit is contained in:
Vyctor H. Brzezowski
2026-05-29 11:03:53 -05:00
committed by GitHub
parent 23932ec7de
commit 309723f7a8
2 changed files with 87 additions and 0 deletions
+51
View File
@@ -89,6 +89,55 @@ function registerCommandGroup(parent: Command, path: readonly string[]) {
return parent.command(path.at(-1) ?? "");
}
function validateTopLevelCommand(args: string[]) {
if (hasTerminalGlobalFlag(args)) return;
const commandName = findFirstTopLevelOperand(args);
if (!commandName) return;
const knownCommands = new Set([
"help",
...program.commands.flatMap((command) => [command.name(), ...command.aliases()]),
]);
if (knownCommands.has(commandName)) return;
program.error(`error: unknown command '${commandName}'`, { code: "commander.unknownCommand" });
}
function hasTerminalGlobalFlag(args: string[]) {
return args.some(
(arg) => arg === "--help" || arg === "-h" || arg === "--cli-version" || arg === "-V",
);
}
function findFirstTopLevelOperand(args: string[]) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) continue;
if (arg === "--") return args[index + 1];
if (arg.startsWith("--")) {
if (arg === "--workdir" || arg === "--dir" || arg === "--site" || arg === "--registry") {
index += 1;
continue;
}
if (
arg.startsWith("--workdir=") ||
arg.startsWith("--dir=") ||
arg.startsWith("--site=") ||
arg.startsWith("--registry=")
) {
continue;
}
if (arg === "--help" || arg === "--cli-version") return undefined;
if (arg === "--no-input") continue;
return undefined;
}
if (arg.startsWith("-")) {
if (arg === "-h" || arg === "-V") return undefined;
return undefined;
}
return arg;
}
return undefined;
}
async function resolveGlobalOpts(): Promise<GlobalOpts> {
const raw = program.opts<{ workdir?: string; dir?: string; site?: string; registry?: string }>();
const workdir = await resolveWorkdir(raw.workdir);
@@ -735,6 +784,8 @@ program.action(async () => {
process.exitCode = 0;
});
validateTopLevelCommand(process.argv.slice(2));
void program.parseAsync(process.argv).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
fail(message);
@@ -54,6 +54,42 @@ describe("built CLI artifact", () => {
expect(result.stdout).toContain("ClawHub CLI");
});
it("reports unknown top-level commands clearly", async () => {
const result = runNode([binPath, "nope"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("error: unknown command 'nope'");
expect(result.stderr).not.toContain("too many arguments");
});
it("reports unknown top-level commands after global options", async () => {
const result = runNode([binPath, "--registry", "https://clawhub.ai", "nope"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("error: unknown command 'nope'");
expect(result.stderr).not.toContain("too many arguments");
});
it("does not mask unknown global options", async () => {
const result = runNode([binPath, "--bad", "nope"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("error: unknown option '--bad'");
expect(result.stderr).not.toContain("unknown command 'nope'");
});
it("keeps help and version flags terminal", async () => {
const helpResult = runNode([binPath, "nope", "--help"]);
const versionResult = runNode([binPath, "--cli-version", "nope"]);
expect(helpResult.status).toBe(0);
expect(helpResult.stderr).toBe("");
expect(helpResult.stdout).toContain("ClawHub CLI");
expect(versionResult.status).toBe(0);
expect(versionResult.stderr).toBe("");
expect(versionResult.stdout).toMatch(/^\d+\.\d+\.\d+/);
});
it("publishes a local code plugin in dry-run json mode from built output", async () => {
const root = await makeTmpDir("clawhub-artifact-");
const pluginDir = join(root, "demo-plugin");