diff --git a/packages/cli/src/auth/browser.ts b/packages/cli/src/auth/browser.ts index fab036307..12bc79985 100644 --- a/packages/cli/src/auth/browser.ts +++ b/packages/cli/src/auth/browser.ts @@ -5,6 +5,7 @@ */ import { c } from "../ui/colors.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; export interface OpenBrowserResult { /** True when we successfully invoked the platform "open" command. */ @@ -21,7 +22,7 @@ export async function openBrowser(url: string): Promise { await open(url); return { opened: true }; } catch (err) { - printManualInstructions(url, err instanceof Error ? err.message : String(err)); + printManualInstructions(url, normalizeErrorMessage(err)); return { opened: false }; } } diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index 279bce673..d2615b53b 100644 --- a/packages/cli/src/browser/manager.ts +++ b/packages/cli/src/browser/manager.ts @@ -4,6 +4,7 @@ import { existsSync, readdirSync, rmSync } from "node:fs"; import { basename } from "node:path"; import { homedir } from "node:os"; import { join } from "node:path"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; type PuppeteerBrowsers = typeof import("@puppeteer/browsers"); @@ -11,7 +12,7 @@ async function loadPuppeteerBrowsers(): Promise { try { return await import("@puppeteer/browsers"); } catch (err) { - const cause = err instanceof Error ? err.message : String(err); + const cause = normalizeErrorMessage(err); throw new Error( `Failed to load @puppeteer/browsers: ${cause}\n` + `Fix: run \`npm install\` or \`bun install\` to restore missing packages, then retry.`, @@ -268,7 +269,7 @@ export async function findBrowser(): Promise { try { return await downloadBrowser(); } catch (err) { - const cause = err instanceof Error ? err.message : String(err); + const cause = normalizeErrorMessage(err); throw new Error( `Cached Chrome binary was missing at ${fromCache.staleHyperframesCachePath}, and re-download failed: ${cause}\n` + `Run \`hyperframes browser ensure --force\` to re-download.`, diff --git a/packages/cli/src/capture/index.ts b/packages/cli/src/capture/index.ts index c2124b8ab..a1c083d4e 100644 --- a/packages/cli/src/capture/index.ts +++ b/packages/cli/src/capture/index.ts @@ -18,6 +18,7 @@ import { extractTokens } from "./tokenExtractor.js"; import { extractDesignStyles } from "./designStyleExtractor.js"; import { downloadAssets, downloadAndRewriteFonts } from "./assetDownloader.js"; import { extractFontMetadata } from "./fontMetadataExtractor.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; // briefGenerator.ts, visual-style, capture-summary removed — DESIGN.md replaces them import { setupAnimationCapture, @@ -353,7 +354,8 @@ export async function captureWebsite( `${designStyles.typography.length} typography roles, ${designStyles.buttons.length} button styles, ${designStyles.shadows.length} shadow values extracted`, ); } catch (err) { - const errMsg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err); + const errMsg = + err instanceof Error ? `${err.message}\n${err.stack}` : normalizeErrorMessage(err); console.error(` ⚠ Design style extraction failed: ${errMsg}`); warnings.push(`Design style extraction failed: ${errMsg}`); } @@ -480,10 +482,7 @@ export async function captureWebsite( } } } catch (err) { - console.warn( - "Font metadata extraction failed (non-fatal):", - err instanceof Error ? err.message : err, - ); + console.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/cloud/detectAspectRatio.ts b/packages/cli/src/cloud/detectAspectRatio.ts index 1de9fe445..5e60fa0bf 100644 --- a/packages/cli/src/cloud/detectAspectRatio.ts +++ b/packages/cli/src/cloud/detectAspectRatio.ts @@ -22,6 +22,7 @@ */ import { readFileSync } from "node:fs"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; export type SupportedAspectRatio = "16:9" | "9:16" | "1:1"; @@ -87,7 +88,7 @@ export function detectAspectRatioFromHtml(entryHtmlPath: string): AspectRatioDet try { html = readFileSync(entryHtmlPath, "utf-8"); } catch (err) { - return { kind: "read-error", error: err instanceof Error ? err.message : String(err) }; + return { kind: "read-error", error: normalizeErrorMessage(err) }; } return detectAspectRatioFromHtmlString(html); } diff --git a/packages/cli/src/commands/batchRender.ts b/packages/cli/src/commands/batchRender.ts index c62c11f9d..d1e9d1846 100644 --- a/packages/cli/src/commands/batchRender.ts +++ b/packages/cli/src/commands/batchRender.ts @@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve, sep } from "node:path"; import { c } from "../ui/colors.js"; import { errorBox } from "../ui/format.js"; +import { normalizeErrorMessage as errorMessage } from "../utils/errorMessage.js"; import { loadProjectVariableSchema, reportVariableIssues, @@ -90,10 +91,7 @@ function parseJson(raw: string, source: string): unknown { try { return JSON.parse(raw); } catch (error: unknown) { - throw new BatchRenderInputError( - "Invalid JSON in --batch", - `${source}: ${error instanceof Error ? error.message : String(error)}`, - ); + throw new BatchRenderInputError("Invalid JSON in --batch", `${source}: ${errorMessage(error)}`); } } @@ -239,7 +237,7 @@ export function prepareBatchRender(options: PrepareBatchRenderOptions): Prepared } catch (error: unknown) { throw new BatchRenderInputError( "Could not read --batch", - `${batchPath}: ${error instanceof Error ? error.message : String(error)}`, + `${batchPath}: ${errorMessage(error)}`, ); } @@ -310,10 +308,6 @@ function emitJsonEvent(event: Record, json: boolean): void { if (json) console.log(JSON.stringify(event)); } -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - async function renderBatchRow( row: PreparedBatchRow, manifest: BatchManifest, diff --git a/packages/cli/src/commands/capture.ts b/packages/cli/src/commands/capture.ts index b10d76f94..8a3c92627 100644 --- a/packages/cli/src/commands/capture.ts +++ b/packages/cli/src/commands/capture.ts @@ -1,6 +1,7 @@ import { defineCommand } from "citty"; import { resolve } from "node:path"; import type { Example } from "./_examples.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; export const examples: Example[] = [ ["Capture a website into ./capture/", "hyperframes capture https://stripe.com"], @@ -213,7 +214,7 @@ export default defineCommand({ console.log(); } } catch (err) { - const errMsg = err instanceof Error ? err.message : String(err); + const errMsg = normalizeErrorMessage(err); // Write BLOCKED.md so the user/agent knows the capture failed try { const { mkdirSync, writeFileSync } = await import("node:fs"); diff --git a/packages/cli/src/commands/cloud/render.ts b/packages/cli/src/commands/cloud/render.ts index 85c52cd0b..c483eef9e 100644 --- a/packages/cli/src/commands/cloud/render.ts +++ b/packages/cli/src/commands/cloud/render.ts @@ -33,6 +33,7 @@ import { import { c } from "../../ui/colors.js"; import { errorBox, formatBytes, formatDuration } from "../../ui/format.js"; import { resolveProject } from "../../utils/project.js"; +import { normalizeErrorMessage } from "../../utils/errorMessage.js"; import { createPublishArchive } from "../../utils/publishProject.js"; import { reportVariableIssues, @@ -541,7 +542,7 @@ async function maybeUploadProject( try { archive = createPublishArchive(project.dir); } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = normalizeErrorMessage(err); errorBox("Zip failed", msg, "Check the project for missing files or unreadable permissions."); process.exit(1); } @@ -733,7 +734,7 @@ async function streamVideo( } return { bytes: result.bytes }; } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = normalizeErrorMessage(err); errorBox( "Download failed", message, diff --git a/packages/cli/src/commands/cloudrun.ts b/packages/cli/src/commands/cloudrun.ts index 5953733f3..326806225 100644 --- a/packages/cli/src/commands/cloudrun.ts +++ b/packages/cli/src/commands/cloudrun.ts @@ -30,6 +30,7 @@ import { resolveVariablesArg, validateVariablesAgainstProject, } from "../utils/variables.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; export const examples: Example[] = [ ["Deploy the Cloud Run render stack", "hyperframes cloudrun deploy --project my-gcp-project"], @@ -670,7 +671,7 @@ async function runRenderBatch(args: Record): Promise { } catch (err) { return { outputKey: entry.outputKey, - error: err instanceof Error ? err.message : String(err), + error: normalizeErrorMessage(err), }; } }), diff --git a/packages/cli/src/commands/lambda/policies.ts b/packages/cli/src/commands/lambda/policies.ts index e22c44d4d..d2bbddb79 100644 --- a/packages/cli/src/commands/lambda/policies.ts +++ b/packages/cli/src/commands/lambda/policies.ts @@ -22,6 +22,7 @@ import { readFileSync } from "node:fs"; import { c } from "../../ui/colors.js"; +import { normalizeErrorMessage } from "../../utils/errorMessage.js"; export type PoliciesVerb = "role" | "user" | "validate"; @@ -247,7 +248,7 @@ export async function runPolicies(args: PoliciesArgs): Promise { try { result = validatePolicy(args.inputPath); } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = normalizeErrorMessage(err); if (args.json) { console.log(JSON.stringify({ ok: false, error: msg }, null, 2)); process.exitCode = 1; diff --git a/packages/cli/src/commands/lambda/render-batch.ts b/packages/cli/src/commands/lambda/render-batch.ts index 96c8322fe..2c5ad177c 100644 --- a/packages/cli/src/commands/lambda/render-batch.ts +++ b/packages/cli/src/commands/lambda/render-batch.ts @@ -36,6 +36,7 @@ import { reportVariableIssues, validateVariablesAgainstSchema, } from "../../utils/variables.js"; +import { normalizeErrorMessage } from "../../utils/errorMessage.js"; import { warnOnDimensionMismatch } from "./_dimensions.js"; import { requireStack } from "./state.js"; @@ -294,7 +295,7 @@ export async function runRenderBatch(args: RenderBatchArgs): Promise { outputKey: entry.outputKey, executionArn: null, status: "failed-to-start", - error: err instanceof Error ? err.message : String(err), + error: normalizeErrorMessage(err), }; } }; @@ -371,10 +372,7 @@ export function parseBatchFile(path: string): Array<{ entry: BatchEntry; lineNum try { parsed = JSON.parse(line); } catch (err) { - errorBox( - `Invalid JSON in batch file on line ${i + 1}`, - err instanceof Error ? err.message : String(err), - ); + errorBox(`Invalid JSON in batch file on line ${i + 1}`, normalizeErrorMessage(err)); process.exit(1); } if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { diff --git a/packages/cli/src/commands/layout.ts b/packages/cli/src/commands/layout.ts index 18d1fe811..34a70eb5f 100644 --- a/packages/cli/src/commands/layout.ts +++ b/packages/cli/src/commands/layout.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url"; import type { Example } from "./_examples.js"; import { c } from "../ui/colors.js"; import { resolveProject } from "../utils/project.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { serveStaticProjectHtml } from "../utils/staticProjectServer.js"; import { withMeta } from "../utils/updateCheck.js"; import { @@ -634,7 +635,7 @@ export function createInspectCommand(commandName: "inspect" | "layout") { process.exit(ok ? 0 : 1); } catch (err) { - const message = err instanceof Error ? err.message : String(err); + const message = normalizeErrorMessage(err); if (args.json) { console.log( JSON.stringify( diff --git a/packages/cli/src/commands/preview.ts b/packages/cli/src/commands/preview.ts index ce19734be..598d6e92e 100644 --- a/packages/cli/src/commands/preview.ts +++ b/packages/cli/src/commands/preview.ts @@ -26,6 +26,7 @@ import { createRequire } from "node:module"; import * as clack from "@clack/prompts"; import { c } from "../ui/colors.js"; import { isDevMode } from "../utils/env.js"; +import { normalizeErrorMessage as errorMessage } from "../utils/errorMessage.js"; import { buildNpxCommand } from "../utils/npxCommand.js"; import type { StudioSelectionSnapshot } from "@hyperframes/studio-server"; import { @@ -307,10 +308,6 @@ function printSelectionFailure(code: string, message: string, json: boolean): vo process.exitCode = 1; } -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function previewServerPayload(server: { port: number; host?: string; @@ -471,11 +468,7 @@ async function printCurrentContext( try { fields = parseContextFields(options.fields); } catch (err) { - printSelectionFailure( - "invalid-context-fields", - err instanceof Error ? err.message : String(err), - options.json, - ); + printSelectionFailure("invalid-context-fields", errorMessage(err), options.json); return; } const fullDetail = options.detail === "full"; diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 2c2ed2724..238d6fd3d 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -753,7 +753,7 @@ export default defineCommand({ browserSpinner?.stop(c.error("Browser not available")); errorBox( "Chrome not found", - err instanceof Error ? err.message : String(err), + normalizeErrorMessage(err), "Run: npx hyperframes browser ensure", ); process.exit(1); @@ -1076,7 +1076,7 @@ function ensureDockerImage(version: string, platform: string, quiet: boolean): s { stdio: quiet ? "pipe" : "inherit", timeout: 600_000 }, ); } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); + const message = normalizeErrorMessage(error); throw new Error(`Failed to build Docker image: ${message}`); } finally { rmSync(tmpDir, { recursive: true, force: true }); @@ -1149,7 +1149,7 @@ async function renderDocker( try { imageTag = ensureDockerImage(dockerVersion, platform, options.quiet); } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); + const message = normalizeErrorMessage(error); const isDockerMissing = /connect|not found|ENOENT/i.test(message); errorBox( isDockerMissing ? "Docker not available" : "Docker image build failed", diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index 3b9606bdb..93e813924 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -5,6 +5,7 @@ import { existsSync, mkdtempSync, readFileSync, mkdirSync, rmSync, writeFileSync import { tmpdir } from "node:os"; import { resolve, join, relative, isAbsolute, basename } from "node:path"; import { resolveProject } from "../utils/project.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js"; import { serveStaticProjectHtml } from "../utils/staticProjectServer.js"; import { c } from "../ui/colors.js"; @@ -594,8 +595,7 @@ export default defineCommand({ descriptions.push(`## ${result.value.filename}`, `${result.value.desc}`, ``); } else { // Log first failure so Gemini issues are visible rather than silent - const errMsg = - result.reason instanceof Error ? result.reason.message : String(result.reason); + const errMsg = normalizeErrorMessage(result.reason); descriptions.push(`## (error)`, `Gemini call failed: ${errMsg.slice(0, 120)}`, ``); } } @@ -605,12 +605,12 @@ export default defineCommand({ console.log(` ${c.dim("descriptions.md")} (Gemini frame analysis)`); } } catch (descErr) { - const msg = descErr instanceof Error ? descErr.message : String(descErr); + const msg = normalizeErrorMessage(descErr); console.log(` ${c.dim(`--describe failed: ${msg.slice(0, 80)}`)}`); } } } catch (err) { - const msg = err instanceof Error ? err.message : String(err); + const msg = normalizeErrorMessage(err); console.error(`\n${c.error("✗")} Snapshot failed: ${msg}`); process.exit(1); } diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 1aca7ada8..c08ba6e71 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { resolveProject } from "../utils/project.js"; +import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js"; import { c } from "../ui/colors.js"; import { withMeta } from "../utils/updateCheck.js"; @@ -226,7 +227,7 @@ async function validateInBrowser( }); page.on("pageerror", (err) => { - const text = err instanceof Error ? err.message : String(err); + const text = normalizeErrorMessage(err); // CDN scripts (e.g. GSAP from jsdelivr) returning HTML error pages // instead of JS produce "Unexpected token '<'" SyntaxErrors. These // are network failures, not composition authoring errors. @@ -394,7 +395,7 @@ Examples: const exitCode = printValidationResult(result, asJson); process.exit(exitCode); } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); + const message = normalizeErrorMessage(err); emitFailureReport(message, asJson); process.exit(1); } diff --git a/packages/cli/src/utils/errorMessage.test.ts b/packages/cli/src/utils/errorMessage.test.ts index 19fb7fcc8..e8476d479 100644 --- a/packages/cli/src/utils/errorMessage.test.ts +++ b/packages/cli/src/utils/errorMessage.test.ts @@ -46,6 +46,26 @@ describe("normalizeErrorMessage", () => { expect(normalizeErrorMessage(hostile)).toBe("[object Object]"); }); + it("never yields '[object Object]' for a no-message object (the reported validate/inspect bug)", () => { + const out = normalizeErrorMessage({ code: 42 }); + expect(out).not.toBe("[object Object]"); + expect(out).toContain("42"); + }); + + it("surfaces a Puppeteer-style protocol error object via its message", () => { + expect(normalizeErrorMessage({ name: "ProtocolError", message: "Target closed" })).toBe( + "Target closed", + ); + }); + + it("surfaces a structured CDP error object (no message) instead of '[object Object]'", () => { + // The shape snapshot/render can receive when a CDP/protocol rejection is a + // plain object rather than an Error: no string `message`, only code + data. + const out = normalizeErrorMessage({ code: -32000, data: { reason: "navigation timeout" } }); + expect(out).not.toBe("[object Object]"); + expect(out).toContain("navigation timeout"); + }); + it("returns 'unknown error' for null and undefined", () => { expect(normalizeErrorMessage(null)).toBe("unknown error"); expect(normalizeErrorMessage(undefined)).toBe("unknown error");