From d92d1d4f51e2737d19db8a67073da8ae04a16789 Mon Sep 17 00:00:00 2001 From: James Russo Date: Thu, 16 Jul 2026 09:14:39 -0400 Subject: [PATCH] fix(cli): keep diagnostics on stderr for --json commands (#2522) --- packages/cli/src/capture/index.ts | 4 +++- packages/cli/src/commands/compositions.ts | 10 +++++---- packages/cli/src/commands/skills.ts | 21 +++++++++++++------ packages/cli/src/telemetry/client.ts | 17 ++++++++------- .../producer/src/services/htmlCompiler.ts | 4 +++- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/capture/index.ts b/packages/cli/src/capture/index.ts index a1c083d4e..055c0d38e 100644 --- a/packages/cli/src/capture/index.ts +++ b/packages/cli/src/capture/index.ts @@ -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.`, diff --git a/packages/cli/src/commands/compositions.ts b/packages/cli/src/commands/compositions.ts index f962640dc..2376cb28a 100644 --- a/packages/cli/src/commands/compositions.ts +++ b/packages/cli/src/commands/compositions.ts @@ -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; } diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index 483c1d5d2..f589eaf75 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -56,7 +56,12 @@ function spawnNpx(args: string[], opts: { cwd?: string } = {}): Promise { 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`)); } } diff --git a/packages/cli/src/telemetry/client.ts b/packages/cli/src/telemetry/client.ts index afd059ffc..b8417eba7 100644 --- a/packages/cli/src/telemetry/client.ts +++ b/packages/cli/src/telemetry/client.ts @@ -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; } diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 4ad17000d..d6be2d6f3 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -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}`,