Files
hyperframes/packages/cli/src/ui/format.ts
T
Vance IngallsandClaude Fable 5 4fc699fee6 fix(core,cli): parse figma 403 body, batch asset fetch, fix NO_TOKEN box
Extends the scope+retry work from the figma bug-bash (valid report:
9-bugs-with-repros; the skill-not-used report was discarded).

- 403-body parse (bug 4): figma returns 403 {"err":"Invalid token"} for bad
  PATs (NOT 401), and 403 {"err":"Invalid scope(s)… requires X"} for missing
  scopes. get() now reads the body: "Invalid token" reclassifies to BAD_TOKEN
  with re-mint advice; a scope body surfaces figma's own diagnosis verbatim;
  else falls back to the endpoint's scope hint. Reads both err and message
  (variables endpoint uses message). One fix, honest messages for bugs 1/4/9.

- Batch asset fetch (requested): figma asset accepts multiple refs
  (space-separated or comma-joined) of one file and renders them in a SINGLE
  /v1/images call via new client.renderNodes — figma's documented per-minute
  rate-limit workaround. runAssetImport delegates to runAssetImportMany;
  cache-checks per node, batches only the misses, one index.md regen.

- NO_TOKEN box (bug 8): errorBox indented only the first hint line, mangling
  the numbered setup list. Indent every line; single-line hints unchanged.

Verified live: 3 refs -> 3 imports -> 1 request; bad token -> BAD_TOKEN not
scope advice. Client suite 22, cli figma 33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:14:57 -07:00

62 lines
2.3 KiB
TypeScript

import { c } from "./colors.js";
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export function formatDuration(ms: number): string {
const seconds = ms / 1000;
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const minutes = Math.floor(seconds / 60);
const remaining = seconds - minutes * 60;
return `${minutes}m ${remaining.toFixed(1)}s`;
}
/**
* Build the detail portion of the render-complete summary (everything after the
* file size). The output video length is shown as the primary figure, with the
* wall-clock render time explicitly labeled "rendered in" so the two are never
* confused (users were comparing the render time to ffprobe's media duration).
* Directory (png-sequence) output has no single muxed video, so it shows a frame
* count instead, or just the render time when neither is known.
*/
export function formatRenderSummaryDetail(input: {
elapsedMs: number;
outputDurationSeconds?: number;
isDirectory: boolean;
frameCount?: number;
}): string {
const middle = input.isDirectory
? input.frameCount != null
? `${input.frameCount} frames`
: undefined
: input.outputDurationSeconds != null && input.outputDurationSeconds > 0
? `${formatDuration(input.outputDurationSeconds * 1000)} video`
: undefined;
const renderTime = `rendered in ${formatDuration(input.elapsedMs)}`;
return [middle, renderTime].filter(Boolean).join(" · ");
}
export function label(name: string, value: string): string {
const pad = 14 - name.length;
return ` ${c.dim(name)}${" ".repeat(Math.max(1, pad))}${c.bold(value)}`;
}
export function errorBox(title: string, hint?: string, suggestion?: string): void {
console.error(`\n${c.error("\u2717")} ${c.bold(title)}`);
if (hint) {
// Indent EVERY hint line, not just the first \u2014 a multi-line hint (e.g. the
// NO_TOKEN numbered setup list) otherwise had line 1 indented and the rest
// flush-left, mangling the list. Single-line hints are unchanged.
const indented = hint
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
console.error(`\n${c.dim(indented)}`);
}
if (suggestion) console.error(` ${c.accent(suggestion)}`);
console.error();
}