blob: 8be71c75e33c3f7eb281e58e058dcbbef4400de4 [file]
#!/usr/bin/env node
/**
* Workspace `console.*` audit (@kenji PR101 follow-up).
*
* Walks all .ts / .tsx source under apps/ and packages/, excludes
* __tests__/, dist/, node_modules/. Flags any `console.<method>(...)` call
* site that isn't on the allow-list. Exits 1 with a printed diff if a new
* console site appears.
*
* Why a script and not an ESLint plugin: keeps tooling surface minimal and
* the policy auditable from a single file. ESLint plugins drag in shared
* config + parser deps for a 50-line check.
*
* To add a legitimate new console site: append to ALLOW with a one-line
* reason. The audit grants every match in the allowed file path; specific
* line-level allow-listing isn't worth the maintenance churn.
*/
import { readdir, readFile, stat } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url));
const ROOTS = ['apps', 'packages'];
const EXTS = new Set(['.ts', '.tsx']);
const SKIP_DIRS = new Set(['node_modules', 'dist', '__tests__']);
/**
* Allow-list — file path (relative to repo root) → reason.
* Allowing the whole file means every `console.*` site inside is OK.
*/
const ALLOW = new Map([
[
'apps/desktop/src/renderer/error-boundary.tsx',
'React error boundary; DevTools-only, surfaces uncaught render errors.',
],
[
'apps/desktop/src/main/main.ts',
'thin ESM entry; pre-ready config, [startup] ready/fatal diagnostics (moved from boot, PR1880).',
],
[
'apps/desktop/src/main/boot.ts',
'startup chain diagnostics (e2e-fixture fatal/scenario, window create failure, repair/cleanup paths); no secrets (moved from main.ts, PR1880).',
],
[
'apps/desktop/src/main/runtime-host-boot.ts',
'opt-in Runtime Host startup, reconnect projection, and bounded shutdown diagnostics; no credentials or provider payloads.',
],
[
'apps/desktop/src/main/runtime-host-desktop-owner.ts',
'Runtime Host reconnect exhaustion is fatal before the Desktop can present an in-app error; no credentials or provider payloads.',
],
[
'apps/desktop/src/main/startup-step.ts',
'names a startup step that has not come back, before any window exists to show it in; a step name and no secrets.',
],
[
'apps/desktop/src/main/app-lifecycle.ts',
'startup/shutdown diagnostics (dock icon, credential migration, e2e-fixture marker, cleanup failures); no secrets (moved from main.ts, arch R6).',
],
[
'apps/desktop/src/main/settings-runtime-effects.ts',
'external-settings apply failure is a main-process diagnostic, no secrets (moved from main.ts, arch R5).',
],
[
'apps/desktop/src/main/daily-review-main.ts',
'scheduler failures are main-process diagnostics and do not expose secrets.',
],
[
'apps/desktop/src/main/main-window.ts',
'real-window smoke diagnostics are dev/test gated and stdout-parsed by capture tooling.',
],
[
'apps/desktop/src/main/permission-overlay/permission-overlay-main.ts',
'#1515 drag-grant diagnostics (locale fallback, missing .app bundle, controller log sink); main-process only, paths not secrets.',
],
[
'apps/desktop/src/main/oauth-model-connections-main.ts',
'OAuth model sync logs provider-level failure reason only; no tokens or raw provider bodies.',
],
[
'apps/desktop/src/main/subscription-ipc-main.ts',
'best-effort post-OAuth model discovery diagnostic; main-process only and never logs credentials.',
],
[
'apps/desktop/src/main/onboarding-service.ts',
'PR110b: credential lookup failure logs error class only (no message / secret bytes); never reaches renderer.',
],
[
'apps/desktop/src/main/permission-overlay/permission-overlay-main.ts',
'macOS permission onboarding lifecycle diagnostics stay in the main process and contain no credential or file contents.',
],
[
'packages/runtime/src/bots/bot-registry.ts',
'message routed through generalizedErrorMessage (xuan raw-error sweep).',
],
[
'packages/runtime/src/telemetry/record-llm-call.ts',
'message routed through generalizedErrorMessage.',
],
[
'packages/runtime/src/telemetry/record-tool-invocation.ts',
'message routed through generalizedErrorMessage.',
],
[
'packages/headless/src/cli.ts',
'CLI entrypoint prints command progress, usage, and failures to stdout/stderr by design.',
],
[
'packages/headless/src/harbor-cli.ts',
'Harbor CLI subcommand prints usage and command failures to stderr by design.',
],
[
'apps/desktop/src/main/config-file-watcher.ts',
'Watcher startup failure and runtime error diagnostics; non-fatal, no secrets.',
],
[
'apps/desktop/src/main/shell-env.ts',
'login-shell PATH resolution diagnostics at startup (PATH-entry count and sanitized failure reason); non-fatal, no shell-controlled output.',
],
['scripts/check-console.mjs', 'this script — explicit allow.'],
[
'apps/desktop/src/main/computer-use/pip-electron.ts',
'picture-in-picture load failure: the one state that is otherwise silent and invisible; logs the overlay asset path and the Chromium error code only.',
],
[
'apps/desktop/src/main/automation-wiring.ts',
'best-effort sync warning when durable automation persistence fails.',
],
[
'packages/storage/src/automation-store.ts',
'best-effort warning when automation store read/write fails.',
],
[
'packages/storage/src/session-store.ts',
'one-time legacy JSONL session import diagnostics (imported/failed counts + per-file reasons); no credentials or provider payloads.',
],
[
'packages/cli/src/runtime-bootstrap.ts',
'best-effort warning when CLI durable automation persistence fails.',
],
[
'packages/core/src/shell-run-result.ts',
'ShellRun reconciliation invariant diagnostics contain only runtime refs and revisions, never command or output data.',
],
[
'packages/runtime-host/src/server/host-kernel.ts',
'Host shutdown failure diagnostics print the full nested AggregateError chain (Node truncates it to [errors]: [Array] by default); error metadata only, no secrets (PR1760).',
],
]);
async function walk(root) {
const out = [];
async function visit(dir) {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
const full = join(dir, entry.name);
if (entry.isDirectory()) {
await visit(full);
} else if (entry.isFile()) {
const dot = entry.name.lastIndexOf('.');
const ext = dot >= 0 ? entry.name.slice(dot) : '';
if (EXTS.has(ext)) out.push(full);
}
}
}
await visit(root);
return out;
}
const CONSOLE_RE = /\bconsole\.(log|info|warn|error|debug|trace)\s*\(/g;
async function main() {
const offenders = [];
for (const root of ROOTS) {
const files = await walk(join(REPO_ROOT, root));
for (const file of files) {
const rel = relative(REPO_ROOT, file).split('\\').join('/');
if (ALLOW.has(rel)) continue;
const src = await readFile(file, 'utf8');
const lines = src.split('\n');
lines.forEach((line, i) => {
CONSOLE_RE.lastIndex = 0;
const match = CONSOLE_RE.exec(line);
if (match) {
offenders.push({ path: rel, line: i + 1, method: match[1], code: line.trim() });
}
});
}
}
if (offenders.length === 0) {
console.log(
`[check-console] OK — ${ALLOW.size} files explicitly allow-listed, no other console sites found.`,
);
return;
}
console.error('[check-console] FAILED — new console.* call sites detected:');
for (const o of offenders) {
console.error(` ${o.path}:${o.line} console.${o.method}(...)`);
console.error(` ${o.code}`);
}
console.error('');
console.error('If the new site is dev-gated or routed through generalizedErrorMessage,');
console.error('add the file path to the ALLOW map in scripts/check-console.mjs with a');
console.error('one-line reason. Otherwise, replace with a dev-only / generalized log.');
process.exit(1);
}
main().catch((err) => {
console.error('[check-console] unexpected error:', err);
process.exit(2);
});