fix(cli): keep diagnostics on stderr for --json commands (#2522)

This commit is contained in:
James Russo
2026-07-16 09:14:39 -04:00
committed by GitHub
parent 5e78a29af3
commit d92d1d4f51
5 changed files with 37 additions and 19 deletions
+3 -1
View File
@@ -474,7 +474,9 @@ export async function captureWebsite(
const summary = fontsManifest.families
.map((f) => `${f.family}${f.variable ? " (variable)" : ""} × ${f.fileCount}`)
.join(", ");
console.log(`Font metadata extracted: ${summary}`);
// stderr: `capture --json` writes its envelope to stdout; this progress
// note must not corrupt it (matches the sibling console.warn below).
console.warn(`Font metadata extracted: ${summary}`);
if (fontsManifest.unidentified.length > 0) {
console.warn(
` ${fontsManifest.unidentified.length} font file(s) could not be identified — DESIGN.md should flag these explicitly.`,
+6 -4
View File
@@ -249,13 +249,15 @@ export default defineCommand({
ensureDOMParser();
const compositions = parseCompositions(html, dirname(project.indexPath));
if (compositions.length === 0) {
console.log(`${c.success("◇")} ${c.accent(project.name)} — no compositions found`);
// --json must always emit a machine-readable envelope, including the empty
// case — check it before the human "no compositions" message on stdout.
if (args.json) {
console.log(JSON.stringify(withMeta({ compositions }), null, 2));
return;
}
if (args.json) {
console.log(JSON.stringify(withMeta({ compositions }), null, 2));
if (compositions.length === 0) {
console.log(`${c.success("◇")} ${c.accent(project.name)} — no compositions found`);
return;
}
+15 -6
View File
@@ -56,7 +56,12 @@ function spawnNpx(args: string[], opts: { cwd?: string } = {}): Promise<void> {
const npx = buildNpxCommand(args);
return new Promise((resolve, reject) => {
const child = spawn(npx.command, npx.args, {
stdio: "inherit",
// Route the child's stdout to the parent's stderr (fd 2), keeping stdin
// inherited. `skills update --json` runs this installer before printing its
// JSON envelope on stdout; child progress chatter on stdout would corrupt it.
// Diagnostics belong on stderr regardless of mode, so this is safe for the
// interactive path too (the user still sees the output).
stdio: ["inherit", 2, 2],
// We install with --full-depth (a full `git clone` of the repo, the only
// path that bypasses the laggy skills.sh blob — see GLOBAL_INSTALL_ARGS_TAIL),
// which is heavier than the blob fetch, so allow more headroom.
@@ -171,7 +176,9 @@ function mirrorToInstalledAgents(): void {
const { mirrored } = mirrorGlobalSkills({ skills: names });
const n = mirrored.length;
if (n > 0) {
console.log(
// stderr: reachable from `skills update --json` (via installSkills) before
// the JSON envelope is written to stdout.
console.error(
c.dim(`Linked skills into ${n} other agent ${n === 1 ? "directory" : "directories"}.`),
);
}
@@ -244,15 +251,17 @@ async function installSkills(
if (!skillsToolingReady(opts.strict ?? false)) return;
// stderr: installSkills runs on the `skills update --json` path before its JSON
// envelope is written to stdout — progress here must not corrupt that output.
for (const source of SOURCES) {
console.log();
console.log(c.bold(`Installing ${source.name} skills...`));
console.log();
console.error();
console.error(c.bold(`Installing ${source.name} skills...`));
console.error();
try {
await runSkillsAdd(source.url, safeSelection, opts);
} catch (err) {
if (opts.strict) throw err instanceof Error ? err : new Error(String(err));
console.log(c.dim(`${source.name} skills skipped`));
console.error(c.dim(`${source.name} skills skipped`));
}
}
+10 -7
View File
@@ -219,15 +219,18 @@ export function showTelemetryNotice(): boolean {
config.telemetryNoticeShown = true;
writeConfig(config);
console.log();
console.log(` ${c.dim("Hyperframes collects anonymous usage data to improve the tool.")}`);
console.log(` ${c.dim("File paths and composition content are never collected.")}`);
console.log(
// stderr, not stdout: this first-run disclosure is not gated by --json (the
// guard in cli.ts filters by command only), so a stdout banner would corrupt
// the JSON envelope of the very first `check --json` / `info --json` etc.
console.error();
console.error(` ${c.dim("Hyperframes collects anonymous usage data to improve the tool.")}`);
console.error(` ${c.dim("File paths and composition content are never collected.")}`);
console.error(
` ${c.dim("If you sign in to HeyGen, your account (email, or username) is linked to your usage.")}`,
);
console.log();
console.log(` ${c.dim("Disable anytime:")} ${c.accent("hyperframes telemetry disable")}`);
console.log();
console.error();
console.error(` ${c.dim("Disable anytime:")} ${c.accent("hyperframes telemetry disable")}`);
console.error();
return true;
}
@@ -1908,7 +1908,9 @@ export async function compileForRender(
);
}
if (metadata.isVFR) {
console.info(
// defaultLogger (stderr), not console.info (stdout) — matches the sibling
// warning above; a stdout line here corrupts `check --json` / `validate --json`.
defaultLogger.warn(
`[Compiler] Video "${video.id}" is variable frame rate (VFR); ` +
`the engine will normalize it to CFR before frame extraction. ` +
`If rendering feels slow on this video, pre-encode once with: ${reencode}`,