diff --git a/.oxlintrc.json b/.oxlintrc.json index 2c2be728a9..fa95efa69d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,5 +12,19 @@ "packages/producer/tests/**/src/vendor/**", "registry/blocks/**/lib/*.iife.js", "chrome/" + ], + "overrides": [ + { + "files": [ + "packages/cli/src/capture/**/*.ts", + "packages/cli/src/registry/**/*.ts", + "packages/cli/src/templates/**/*.ts", + "packages/cli/src/telemetry/**/*.ts" + ], + "excludeFiles": ["**/*.test.ts", "**/*.test.tsx", "packages/cli/src/telemetry/feedback.ts"], + "rules": { + "no-console": ["error", { "allow": ["error", "warn"] }] + } + } ] } diff --git a/packages/cli/src/capture/index.ts b/packages/cli/src/capture/index.ts index 055c0d38e6..fe58a972f4 100644 --- a/packages/cli/src/capture/index.ts +++ b/packages/cli/src/capture/index.ts @@ -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) diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index f589eaf757..3948370610 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -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`)); } } diff --git a/packages/cli/src/telemetry/client.ts b/packages/cli/src/telemetry/client.ts index b8417eba76..db26e1f194 100644 --- a/packages/cli/src/telemetry/client.ts +++ b/packages/cli/src/telemetry/client.ts @@ -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; } diff --git a/packages/cli/src/ui/diagnostics.ts b/packages/cli/src/ui/diagnostics.ts new file mode 100644 index 0000000000..bc24d6e986 --- /dev/null +++ b/packages/cli/src/ui/diagnostics.ts @@ -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); + }, +};