refactor(cli): add stderr diagnostics logger + oxlint guard (#2551)

This commit is contained in:
James Russo
2026-07-16 09:35:27 -04:00
committed by GitHub
parent d92d1d4f51
commit 1b4cf12cd3
5 changed files with 65 additions and 24 deletions
+6 -5
View File
@@ -19,6 +19,7 @@ import { extractDesignStyles } from "./designStyleExtractor.js";
import { downloadAssets, downloadAndRewriteFonts } from "./assetDownloader.js";
import { extractFontMetadata } from "./fontMetadataExtractor.js";
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { diag } from "../ui/diagnostics.js";
// briefGenerator.ts, visual-style, capture-summary removed — DESIGN.md replaces them
import {
setupAnimationCapture,
@@ -474,17 +475,17 @@ export async function captureWebsite(
const summary = fontsManifest.families
.map((f) => `${f.family}${f.variable ? " (variable)" : ""} × ${f.fileCount}`)
.join(", ");
// 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}`);
// stderr (via diag): `capture --json` writes its envelope to stdout, so
// these progress/advisory lines must not land there.
diag.notice(`Font metadata extracted: ${summary}`);
if (fontsManifest.unidentified.length > 0) {
console.warn(
diag.warn(
` ${fontsManifest.unidentified.length} font file(s) could not be identified — DESIGN.md should flag these explicitly.`,
);
}
}
} catch (err) {
console.warn("Font metadata extraction failed (non-fatal):", normalizeErrorMessage(err));
diag.warn("Font metadata extraction failed (non-fatal):", normalizeErrorMessage(err));
}
// Save animation catalog — lean version for the agent (not 745 raw CSS declarations)
+11 -9
View File
@@ -2,6 +2,7 @@ import { defineCommand } from "citty";
import { execFileSync, spawn } from "node:child_process";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
import { diag } from "../ui/diagnostics.js";
import { buildNpxCommand } from "../utils/npxCommand.js";
import { withMeta } from "../utils/updateCheck.js";
import {
@@ -176,9 +177,9 @@ function mirrorToInstalledAgents(): void {
const { mirrored } = mirrorGlobalSkills({ skills: names });
const n = mirrored.length;
if (n > 0) {
// stderr: reachable from `skills update --json` (via installSkills) before
// the JSON envelope is written to stdout.
console.error(
// stderr (via diag): reachable from `skills update --json` (via installSkills)
// before the JSON envelope is written to stdout.
diag.notice(
c.dim(`Linked skills into ${n} other agent ${n === 1 ? "directory" : "directories"}.`),
);
}
@@ -251,17 +252,18 @@ 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.
// stderr (via diag): installSkills runs on the `skills update --json` path before
// its JSON envelope is written to stdout — progress here must not corrupt it.
for (const source of SOURCES) {
console.error();
console.error(c.bold(`Installing ${source.name} skills...`));
console.error();
diag.notice();
diag.notice(c.bold(`Installing ${source.name} skills...`));
diag.notice();
try {
await runSkillsAdd(source.url, safeSelection, opts);
} catch (err) {
if (opts.strict) throw err instanceof Error ? err : new Error(String(err));
console.error(c.dim(`${source.name} skills skipped`));
// warn, not notice: this is a non-fatal skip after a caught install error.
diag.warn(c.dim(`${source.name} skills skipped`));
}
}
+11 -10
View File
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
import { readConfig, writeConfig } from "./config.js";
import { VERSION } from "../version.js";
import { c } from "../ui/colors.js";
import { diag } from "../ui/diagnostics.js";
import { isDevMode } from "../utils/env.js";
import { getSystemMeta } from "./system.js";
@@ -219,18 +220,18 @@ export function showTelemetryNotice(): boolean {
config.telemetryNoticeShown = true;
writeConfig(config);
// 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(
// stderr (via diag), 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` etc.
diag.notice();
diag.notice(` ${c.dim("Hyperframes collects anonymous usage data to improve the tool.")}`);
diag.notice(` ${c.dim("File paths and composition content are never collected.")}`);
diag.notice(
` ${c.dim("If you sign in to HeyGen, your account (email, or username) is linked to your usage.")}`,
);
console.error();
console.error(` ${c.dim("Disable anytime:")} ${c.accent("hyperframes telemetry disable")}`);
console.error();
diag.notice();
diag.notice(` ${c.dim("Disable anytime:")} ${c.accent("hyperframes telemetry disable")}`);
diag.notice();
return true;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* CLI diagnostics logger.
*
* Diagnostics (notices, progress, warnings, errors) go to **stderr**. stdout is
* reserved for a command's machine-readable payload — most importantly `--json`
* output — and its primary human result. A diagnostic written to stdout corrupts
* that payload for any consumer that parses it (see #2520 / #2522).
*
* This mirrors the producer's `ProducerLogger` contract for the CLI, but without
* `[LEVEL]` prefixes so user-facing notices keep their own formatting. Route all
* diagnostic output through `diag` (or `console.error` / `console.warn` directly)
* — never `console.log` / `console.info` for diagnostics.
*/
export const diag = {
/** A user-facing notice or progress line (e.g. "Installing … skills…"). */
notice(...args: unknown[]): void {
console.error(...args);
},
/** A warning the user should see but that isn't fatal. */
warn(...args: unknown[]): void {
console.warn(...args);
},
};