mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Both found running the brand-loop guide end-to-end against the Simple Design System: - nodeToHtml subtracted the ROOT origin from every node's absolute bounds, but CSS absolute positioning resolves against the nearest positioned ancestor — every nesting level re-added its ancestors' offsets, drifting nested content down-right and pushing deep children off-frame (hero buttons invisible, pricing grid collapsed to one card). Children now subtract their PARENT's box; regression test with a two-level tree. - trackCommandFailures asserted unknown flags against the command group's own (flagless) arg table even when the group was delegating to a subcommand, so `figma component <ref> --name x` imported and THEN threw "Unknown flag: --name". The assertion is now skipped when the first positional names a subcommand; leaf and non-delegating behavior is unchanged and covered by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
5.0 KiB
TypeScript
113 lines
5.0 KiB
TypeScript
import type { CommandDef } from "citty";
|
|
import { assertKnownFlags } from "./reject-unknown-flags.js";
|
|
|
|
// citty types subcommands as `CommandDef<any>` (SubCommandsDef); mirror that so
|
|
// each command's specific args type is accepted without per-command generics.
|
|
type AnyCommandDef = CommandDef<any>;
|
|
|
|
/**
|
|
* Wrap a lazy command loader so a thrown failure is reported via `onFailure`
|
|
* before it propagates. citty's `runMain` catches command errors and exits 1
|
|
* without re-throwing, so this is the only place to capture the reason. The
|
|
* error is re-thrown unchanged, preserving citty's print + exit-1 behavior.
|
|
*
|
|
* `onFailure` is awaited so it can resolve the (lazily-loaded) telemetry module
|
|
* before the error propagates — otherwise a command that throws before the
|
|
* telemetry import settles would lose its event. A throw from `onFailure` is
|
|
* swallowed so telemetry can never mask the real command failure.
|
|
*
|
|
* Commands that call `process.exit()` themselves bypass this (the process is
|
|
* already gone) and must report their failure inline.
|
|
*/
|
|
export function trackCommandFailures(
|
|
load: () => Promise<AnyCommandDef>,
|
|
onFailure: (err: unknown) => void | Promise<void>,
|
|
): () => Promise<AnyCommandDef> {
|
|
return () => load().then((cmd) => wrapCommand(cmd, onFailure));
|
|
}
|
|
|
|
/**
|
|
* Wrap a resolved command's `run` (assert-flags + report-failure) AND
|
|
* recursively wrap every entry in its `subCommands`. Two HF#2033 fixes live
|
|
* here:
|
|
* 1. `assertKnownFlags` runs INSIDE the try, so an unknown-flag throw is
|
|
* routed through `onFailure` (telemetry) like any other failure — it
|
|
* used to throw before the try and lose the event entirely.
|
|
* 2. Recursion covers nested command groups (`cloud/*`, `auth/*`, `figma/*`,
|
|
* `lambda/*`, `capture/*`, `skills`). cli.ts only wraps the top-level
|
|
* loaders, so before this a `hyperframes cloud render --badflag` silently
|
|
* ignored the flag and reported nothing — citty dispatches to the leaf,
|
|
* whose `run` was never wrapped.
|
|
*/
|
|
function wrapCommand(
|
|
cmd: AnyCommandDef,
|
|
onFailure: (err: unknown) => void | Promise<void>,
|
|
): AnyCommandDef {
|
|
const run = cmd.run;
|
|
// Nothing to wrap (no run, no nested subcommands) — preserve identity.
|
|
if (typeof run !== "function" && !cmd.subCommands) return cmd;
|
|
|
|
const wrapped: AnyCommandDef = { ...cmd };
|
|
if (typeof run === "function") {
|
|
wrapped.run = async (ctx: Parameters<typeof run>[0]) => {
|
|
try {
|
|
// Reject unknown flags before the command body: citty silently ignores
|
|
// them otherwise, dropping the value (e.g. `render --out x` fell back to
|
|
// the default output path). Inside the try so the rejection is reported.
|
|
//
|
|
// A command group (subCommands + fallback-help run) delegating to a
|
|
// subcommand must NOT assert here: the flags belong to the subcommand's
|
|
// table, not the group's, and would be falsely rejected (e.g.
|
|
// `figma component <ref> --name x`). The wrapped subcommand loaders
|
|
// below assert the leaf's own table, so typo protection is preserved.
|
|
// Heuristic caveat: "first non-dash token names a subcommand" is sound
|
|
// only while command groups declare no flags of their own — if a group
|
|
// grows a flag whose value could match a subcommand name, replace this
|
|
// with a real argv parse.
|
|
const rawArgs = ctx?.rawArgs ?? [];
|
|
const firstPositional = rawArgs.find((tok) => tok && !tok.startsWith("-"));
|
|
const delegatesToSub =
|
|
cmd.subCommands != null &&
|
|
firstPositional != null &&
|
|
Object.prototype.hasOwnProperty.call(cmd.subCommands, firstPositional);
|
|
if (!delegatesToSub) assertKnownFlags(cmd, rawArgs);
|
|
return await run(ctx);
|
|
} catch (err) {
|
|
try {
|
|
await onFailure(err);
|
|
} catch {
|
|
// Telemetry must never mask the real command failure.
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
}
|
|
if (cmd.subCommands) {
|
|
const wrappedSubs: Record<string, () => Promise<AnyCommandDef>> = {};
|
|
for (const [name, sub] of Object.entries(cmd.subCommands)) {
|
|
// citty subCommands are Resolvable<CommandDef>: a def, a promise, or a
|
|
// (possibly async) loader. Normalize to a loader that resolves then wraps.
|
|
wrappedSubs[name] = () =>
|
|
Promise.resolve(typeof sub === "function" ? (sub as () => unknown)() : sub).then((c) =>
|
|
wrapCommand(c as AnyCommandDef, onFailure),
|
|
);
|
|
}
|
|
wrapped.subCommands = wrappedSubs;
|
|
}
|
|
return wrapped;
|
|
}
|
|
|
|
/**
|
|
* Report a command failure to telemetry, loading the telemetry module on demand
|
|
* (keeps it off the CLI cold-start path) and awaiting it so the event is
|
|
* enqueued before the caller re-throws / exits. Best-effort — never throws.
|
|
*/
|
|
export async function reportCommandFailure(command: string, err: unknown): Promise<void> {
|
|
try {
|
|
const { trackCommandFailure } = await import("../telemetry/events.js");
|
|
trackCommandFailure(command, err);
|
|
} catch {
|
|
// ignore: a telemetry failure must not affect the command's exit path
|
|
}
|
|
}
|