refactor(cli): share registered command traversal

This commit is contained in:
morluto
2026-10-22 07:37:21 +08:00
parent 1828cf87a0
commit b2dc45006d
5 changed files with 106 additions and 44 deletions
+9 -27
View File
@@ -1,7 +1,7 @@
import { constants } from "node:buffer";
import type { Writable } from "node:stream";
import { Cli, Filter, Formatter } from "incur";
import { Filter, Formatter } from "incur";
import { estimateTokenCount, sliceByTokens } from "tokenx";
import {
@@ -23,6 +23,10 @@ import {
writeOutputText,
} from "./jsonOutput.js";
import type { CliInstance } from "./types.js";
import {
visitRegisteredCommands,
type RegisteredCommand,
} from "./visitRegisteredCommands.js";
/** Result metadata supplied by the command that actually ran. */
export interface CliResultOutputMetadata {
@@ -41,40 +45,18 @@ export interface CliResultOutput {
recoverFormattingFailure(cause: unknown): Promise<boolean>;
}
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
type CommandDefinition = Extract<
CommandMap extends Map<string, infer Entry> ? Entry : never,
{ run: unknown }
>;
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
/** Install formatting once, after registration, for every ordinary CLI result. */
export const streamCliCommandResults = (
cli: CliInstance,
output: CliResultOutput,
): void => {
const commands = registeredCommands.get(cli);
if (commands !== undefined) wrapCommands(commands, [], output);
};
const wrapCommands = (
commands: CommandMap,
parents: readonly string[],
output: CliResultOutput,
): void => {
for (const [name, entry] of commands) {
const path = [...parents, name];
if ("_group" in entry) {
wrapCommands(entry.commands, path, output);
if (entry.root !== undefined)
wrapCommand(entry.root, path.join(" "), output);
} else if (!("_alias" in entry) && !("_fetch" in entry))
wrapCommand(entry, path.join(" "), output);
}
visitRegisteredCommands(cli, (command, path) =>
wrapCommand(command, path, output),
);
};
const wrapCommand = (
command: Pick<CommandDefinition, "run">,
command: Pick<RegisteredCommand, "run">,
name: string,
output: CliResultOutput,
): void => {
+3 -17
View File
@@ -1,15 +1,12 @@
import { Cli, z } from "incur";
import { z } from "incur";
import type { CliInstance } from "./types.js";
import { visitRegisteredCommands } from "./visitRegisteredCommands.js";
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
type CommandInputs = Partial<Record<(typeof INPUT_GROUPS)[number], unknown>>;
const INPUT_GROUPS = ["args", "env", "options"] as const;
// Incur types its router key without vars, env, or globals; accept any CLI.
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
/**
* Advertise parser-filled defaults as omittable CLI inputs.
*
@@ -20,18 +17,7 @@ const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
* value stay unchanged.
*/
export const presentOmittableDefaults = (cli: CliInstance): void => {
const commands = registeredCommands.get(cli);
if (commands !== undefined) presentCommands(commands);
};
const presentCommands = (commands: CommandMap): void => {
for (const entry of commands.values()) {
if ("_group" in entry) {
presentCommands(entry.commands);
if (entry.root !== undefined) presentInputs(entry.root);
} else if (!("_alias" in entry) && !("_fetch" in entry))
presentInputs(entry);
}
visitRegisteredCommands(cli, (command) => presentInputs(command));
};
const presentInputs = (command: CommandInputs): void => {
+35
View File
@@ -0,0 +1,35 @@
import { Cli } from "incur";
import type { CliInstance } from "./types.js";
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
type CommandEntry = CommandMap extends Map<string, infer Entry> ? Entry : never;
export type RegisteredCommand = Extract<CommandEntry, { run: unknown }>;
// Incur types its router key without vars, env, or globals; accept any CLI.
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
/** Visit ordinary commands in map order, children before each group's root. */
export const visitRegisteredCommands = (
cli: CliInstance,
visit: (command: RegisteredCommand, path: string) => void,
): void => {
const commands = registeredCommands.get(cli);
if (commands !== undefined) visitCommands(commands, [], visit);
};
const visitCommands = (
commands: CommandMap,
parents: readonly string[],
visit: (command: RegisteredCommand, path: string) => void,
): void => {
for (const [name, entry] of commands) {
const path = [...parents, name];
if ("_group" in entry) {
visitCommands(entry.commands, path, visit);
if (entry.root !== undefined) visit(entry.root, path.join(" "));
} else if (!("_alias" in entry) && !("_fetch" in entry)) {
visit(entry, path.join(" "));
}
}
};
@@ -8,6 +8,7 @@ import {
apktoolInputSchemas,
MAX_SELECTED_LOCALES,
} from "../../../src/domain/apktool/apktoolResourceAnalysis.js";
import { presentOmittableDefaults } from "../../../src/cli/omittableDefaults.js";
type CliInstance = ReturnType<typeof createCli>;
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
@@ -165,6 +166,22 @@ describe("CLI advertised input schemas", () => {
"archive Relative archive path when the target is a bundle",
);
});
it("advertises defaults as omittable on a mounted group root", async () => {
const group = Cli.create("group", {
args: z.object({ scope: z.enum(["all", "selected"]).default("all") }),
run: () => ({ ok: true }),
}).command("child", { run: () => ({ ok: true }) });
const cli = Cli.create("group-root-test", { sync: false }).command(group);
presentOmittableDefaults(cli as unknown as CliInstance);
const advertised = await advertisedSchema(
cli as unknown as CliInstance,
"group",
);
expect(advertised.args?.required ?? []).not.toContain("scope");
});
});
describe("firmware CLI advertised input schema", () => {
+42
View File
@@ -161,6 +161,48 @@ describe("incremental CLI JSON", () => {
);
});
describe("CLI command output wrapping", () => {
it("wraps a mounted group root reached through its alias", async () => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const output = createCliResultOutput(
["--format", "json", "--full-output"],
destination,
);
if (output === undefined)
throw new Error("Missing structured output surface");
const group = Cli.create("nested", {
aliases: ["nested-alias"],
run: () => ({ kind: "group-root" }),
}).command("child", { run: () => ({ kind: "child" }) });
const cli = Cli.create("rea-output-group-test", { sync: false }).command(
group,
);
streamCliCommandResults(cli, output);
let fallback = "";
await cli.serve(["nested-alias", "--format", "json"], {
stdout: (text) => {
if (!output.handled) fallback += text;
},
});
expect(output.handled).toBe(true);
expect(fallback).toBe("");
expect(JSON.parse(Buffer.concat(chunks).toString("utf8"))).toMatchObject({
ok: true,
data: { kind: "group-root" },
meta: { command: "nested" },
});
});
});
describe("CLI result output surface", () => {
it.each(["json", "yaml", "toon", "md"] as const)(
"places typed analysis failures in the full-output error envelope (%s)",