From 3909486c9a73f5b070df4cfba97e3dc5a915454b Mon Sep 17 00:00:00 2001 From: Jack Singer Date: Tue, 28 Jul 2026 09:21:56 -0500 Subject: [PATCH 1/2] bool CLI: replace the figlet logo with a real trace of the wordmark Rasterizes the actual bool-logo artwork into brand-colored quadrant-block art instead of a generic font, so the b/o/o/l match the web logomark's blue/purple/teal/coral exactly. On a TTY the tri-color spark bursts open over three redrawn frames (mirroring the web logo's hover animation); everywhere else (piped output, NO_COLOR, tests) it's one static print. Co-Authored-By: Claude Sonnet 5 --- src/cli.test.ts | 31 +++++++++- src/cli.ts | 150 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 168 insertions(+), 13 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index b3e7d0e..2a03d85 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -21,12 +21,17 @@ function makeDeps(cwd: string, routes: Record Re const calls: Call[] = []; const logs: string[] = []; const errors: string[] = []; + const writes: string[] = []; + const sleeps: number[] = []; const deps = { cwd, env: { BOOL_TOKEN: "bool_live_test" } as Record, log: (m: string) => logs.push(m), error: (m: string) => errors.push(m), - sleep: async () => {}, + write: (m: string) => writes.push(m), + sleep: async (ms: number) => { + sleeps.push(ms); + }, fetch: async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); calls.push({ url, init }); @@ -36,7 +41,7 @@ function makeDeps(cwd: string, routes: Record Re return handler(init); }, }; - return { deps, calls, logs, errors }; + return { deps, calls, logs, errors, writes, sleeps }; } function json(body: unknown, status = 200): Response { @@ -454,7 +459,7 @@ describe("help / unknown", () => { const { deps, logs } = makeDeps(cwd, {}); expect(await runCli([], deps)).toBe(1); const output = logs.join("\n"); - expect(output).toContain("/ /_ ____ ____ / /"); + expect(output).toContain("████▌"); // the wordmark's "l" — traced from the logo, no color codes expect(output).toContain("Build locally. Ship to Bool."); expect(output).toContain("Usage:"); expect(output).not.toContain("\u001b["); @@ -464,6 +469,26 @@ describe("help / unknown", () => { expect(await runCli(["help"], { ...deps, color: true })).toBe(0); expect(logs.join("\n")).toContain("\u001b[36m"); }); + test("color without a TTY prints the wordmark once via log, not the animated writer", async () => { + const { deps, logs, writes } = makeDeps(cwd, {}); + expect(await runCli(["help"], { ...deps, color: true, isTTY: false })).toBe(0); + expect(logs.join("\n")).toContain("\u2588\u2588\u2588\u2588\u258c"); + expect(writes).toEqual([]); + }); + test("TTY + color bursts the spark over three redrawn frames", async () => { + const { deps, writes, sleeps } = makeDeps(cwd, {}); + expect(await runCli(["help"], { ...deps, color: true, isTTY: true })).toBe(0); + // 3 frames written (bare letters, partial spark, full spark); the first + // has no cursor-up (nothing to redraw over yet), the other two do. + expect(writes).toHaveLength(3); + expect(writes[0]).not.toContain("\u001b[10A"); + expect(writes[1]).toContain("\u001b[10A"); + expect(writes[2]).toContain("\u001b[10A"); + // Letters are identical across every frame — only the spark cells change. + const strip = (w: string) => w.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, ""); + expect(strip(writes[2]!)).toContain("\u2588\u2588\u2588\u2588\u258c"); + expect(sleeps).toEqual([110, 110]); + }); test("unknown command errors", async () => { const { deps, errors } = makeDeps(cwd, {}); expect(await runCli(["frobnicate"], deps)).toBe(1); diff --git a/src/cli.ts b/src/cli.ts index 5170228..a3af2a8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -68,6 +68,10 @@ export type CliDeps = { sleep: (ms: number) => Promise; isTTY: boolean; color: boolean; + /** Unterminated stdout write, for the wordmark's in-place frame redraws. + * Separate from `log` so hermetic tests can capture whole lines without + * carriage-return/cursor-movement noise. */ + write: (chunk: string) => void; }; function defaults(): CliDeps { @@ -83,6 +87,7 @@ function defaults(): CliDeps { color: process.env.NO_COLOR === undefined && (isTTY || (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "0")), + write: (chunk) => process.stdout.write(chunk), }; } @@ -99,6 +104,137 @@ const green = (deps: CliDeps, text: string) => paint(deps, 32, text); const yellow = (deps: CliDeps, text: string) => paint(deps, 33, text); const cyan = (deps: CliDeps, text: string) => paint(deps, 36, text); +/** The four logo accents (b / o / o / l + spark), lifted from the bool + * logomark. Reserved for the wordmark below — per Bool's design language these + * are illustration, not routine UI, so nothing else in the CLI uses them. */ +const BRAND = { + blue: [1, 102, 253], + purple: [124, 54, 239], + teal: [0, 194, 187], + coral: [253, 102, 67], +} as const satisfies Record; +type BrandColor = keyof typeof BRAND; + +function brand(deps: CliDeps, color: BrandColor, text: string): string { + if (!deps.color) return text; + const [r, g, b] = BRAND[color]; + return `${ESC}38;2;${r};${g};${b}m${text}${ESC}0m`; +} + +// The wordmark below is a direct trace of the logo artwork (rasterized, then +// downsampled to quadrant block characters for 2x the horizontal resolution +// half-blocks give you), not a generic font — same "b" bowl, same two "o" +// rings, same "l", at 10 lines tall. WORDMARK_GLYPHS/COLORS are the letters +// alone; SPARK_CELLS are the tri-color flairs traced separately (rendering the +// logo with vs. without its spark elements and diffing the two), so +// they can be dropped in progressively — same trick as the web logo's hover +// burst (see components/logo.tsx's sparkOnHover), without touching a single +// letter pixel. +const WORDMARK_GLYPHS = [ + " ▗▄▄▄ ", + " ▗▄▟███ ████▌", + " ▝██████ ████▌", + " ▘████ ▗▄████▄▖ ████▌", + " ████▗██▄▄▖ ▗▄▄▄▄ ▟███▛▜███▙ ████▌", + " ███▛▜█████▙ ▄███████▙ ▐██▛ ███▖████▌", + " ███▌ ▝███▌▐██▛▘ ▝▜██▙▝███▄ ▄███ ████▌", + " ▝███▙▖ ▗███▌▐██▌ ▗███ ▝███████▛▘ ████▌", + " ▜█████████▛ ▝████▄▟███▘ ▝▀▀▀▀ ▜███▘", + " ▝▀█████▛▘ ▀▜███▛▀▘ ▝▀ ", +]; +// One code per glyph cell above: b/p/t/c = the four brand accents, . = blank. +const WORDMARK_COLORS = [ + "..........................................cccc.", + ".....bbbbbb...............................ccccc", + "....bbbbbbb...............................ccccc", + "......bbbbb.....................tttttttt..ccccc", + ".......bbbbbbbbbb.....ppppp....tttttttttt.ccccc", + ".......bbbbbbbbbbb..ppppppppp.tttt....ttttccccc", + ".......bbbb...bbbbbppppp.pppppttttt..tttt.ccccc", + "......bbbbbb..bbbbbpppp...pppp.tttttttttt.ccccc", + ".......bbbbbbbbbbb.ppppppppppp...ttttt....ccccc", + "........bbbbbbbbb....pppppppp..............cc..", +]; +const WORDMARK_COLOR_KEY: Record = { + b: "blue", + p: "purple", + t: "teal", + c: "coral", +}; + +type SparkCell = { row: number; col: number; ch: string; color: BrandColor }; +// `stage` 1 = drawn from the first burst frame (closest to the b, where each +// flair actually starts); 2 = only appears once the flair is fully extended. +const SPARK_CELLS: Array = [ + { row: 0, col: 2, ch: "▝", color: "teal", stage: 2 }, + { row: 0, col: 3, ch: "▙", color: "teal", stage: 2 }, + { row: 0, col: 4, ch: "▖", color: "teal", stage: 1 }, + { row: 1, col: 4, ch: "▀", color: "teal", stage: 1 }, + { row: 1, col: 0, ch: "▄", color: "purple", stage: 2 }, + { row: 1, col: 1, ch: "▖", color: "purple", stage: 1 }, + { row: 2, col: 0, ch: "▝", color: "purple", stage: 2 }, + { row: 2, col: 1, ch: "▀", color: "purple", stage: 2 }, + { row: 2, col: 2, ch: "▀", color: "purple", stage: 2 }, + { row: 2, col: 3, ch: "▘", color: "purple", stage: 1 }, + { row: 3, col: 1, ch: "▄", color: "coral", stage: 2 }, + { row: 3, col: 2, ch: "▄", color: "coral", stage: 2 }, + { row: 3, col: 3, ch: "▟", color: "coral", stage: 1 }, + { row: 4, col: 1, ch: "▘", color: "coral", stage: 1 }, +]; + +/** Overlay spark cells onto the base letters, then paint into colored lines + * (run-length grouped so a same-color stretch is one escape, not one per + * char). Cells up to `stage` are drawn; stage 0 renders bare letters. */ +function renderWordmark(deps: CliDeps, stage: 0 | 1 | 2): string { + const glyphs = WORDMARK_GLYPHS.map((row) => [...row]); + const colors = WORDMARK_COLORS.map((row) => [...row]); + for (const cell of SPARK_CELLS) { + if (cell.stage > stage) continue; + glyphs[cell.row]![cell.col] = cell.ch; + colors[cell.row]![cell.col] = cell.color[0]!; + } + return glyphs + .map((row, r) => { + const colorRow = colors[r]!; + let out = ""; + for (let i = 0; i < row.length; ) { + let j = i + 1; + while (j < row.length && colorRow[j] === colorRow[i]) j++; + const run = row.slice(i, j).join(""); + const code = colorRow[i]!; + out += code === "." ? run : brand(deps, WORDMARK_COLOR_KEY[code]!, run); + i = j; + } + return out.replace(/\s+$/, ""); + }) + .join("\n"); +} + +const SPARK_FRAME_MS = 110; + +/** Print the wordmark. Interactively (TTY + color) the spark bursts open over + * three frames, redrawn in place — a nod to the web logo's hover animation. + * Anywhere else (piped output, NO_COLOR, tests) it's one static print of the + * finished mark, so scripts and assertions see plain, stable text. */ +async function printWordmark(deps: CliDeps): Promise { + if (!deps.isTTY || !deps.color) { + deps.log(renderWordmark(deps, 2)); + return; + } + const height = WORDMARK_GLYPHS.length; + for (const stage of [0, 1, 2] as const) { + const frame = renderWordmark(deps, stage) + .split("\n") + .map((line) => `${ESC}2K${line}`) + .join("\n"); + // One write per frame: the cursor-up (skipped on the first frame — nothing + // to redraw over yet) plus the repainted lines, batched so a redraw can't + // tear across two writes. + deps.write(`${stage > 0 ? `${ESC}${height}A` : ""}${frame}\n`); + if (stage < 2) await deps.sleep(SPARK_FRAME_MS); + } +} + function success(deps: CliDeps, message: string): void { deps.log(`${green(deps, "✓")} ${message}`); } @@ -113,7 +249,7 @@ function warning(deps: CliDeps, message: string): void { function commandHeader(deps: CliDeps, command: string): void { if (!deps.isTTY) return; - deps.log(`${bold(deps, cyan(deps, "bool"))} ${dim(deps, "/")} ${bold(deps, command)}\n`); + deps.log(`${bold(deps, brand(deps, "blue", "bool"))} ${dim(deps, "/")} ${bold(deps, command)}\n`); } /** Tiny arg parser: `--key value` / `--key=value` / bare `--flag`, plus bare @@ -774,17 +910,9 @@ async function cmdDeploy( throw new CliError("Timed out waiting for the deploy — check the project on Bool."); } -const LOGO = ` __ __ - / /_ ____ ____ / / - / __ \u005c/ __ \u005c/ __ \u005c/ / - / /_/ / /_/ / /_/ / / -/_.___/\u005c____/\u005c____/_/`; - function usage(deps: CliDeps): string { const command = (value: string) => cyan(deps, value.padEnd(34)); - return `${cyan(deps, LOGO)} - -${bold(deps, "Build locally. Ship to Bool.")} + return `${bold(deps, "Build locally. Ship to Bool.")} ${bold(deps, "Usage:")} ${command("bool create [name]")} Scaffold a new app and project @@ -834,6 +962,8 @@ export async function runCli(argv: string[], overrides?: Partial): Prom case undefined: case "help": case "--help": + await printWordmark(deps); + deps.log(""); deps.log(usage(deps)); return command ? 0 : 1; default: From c638a6ec7a7423369d3550d5efeae9cfb75af125 Mon Sep 17 00:00:00 2001 From: Jack Singer Date: Tue, 28 Jul 2026 11:31:38 -0500 Subject: [PATCH 2/2] Release 0.2.0-next.27: real-logo wordmark for the CLI Cuts the release backing this PR's traced, brand-colored wordmark and spark animation for bool help/--help/bare bool. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e95e1..63bf916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.2.0-next.27 + +- **`bool help` / `bool --help` / bare `bool` now print a real trace of the + bool logomark instead of a generic figlet font.** The wordmark is rasterized + from the actual logo artwork and downsampled to quadrant block characters — + same b bowl, same two o rings, same l — in the exact brand colors + (blue/purple/teal/coral) from the web logo. On a TTY, the tri-color spark + bursts open over three redrawn frames, the same flourish as the web logo's + hover animation, without moving a single letter. Piped output, `NO_COLOR`, + and non-interactive runs get one static print of the finished mark — no + animation, no escape codes beyond color. + + Zero new dependencies: the art is precomputed offline from the SVG and + baked into `cli.ts` as data, consistent with the CLI staying + zero-dependency at runtime. + ## 0.2.0-next.26 - **Removes the public-channel fallback entirely. Every doorbell topic is now diff --git a/package.json b/package.json index b33053c..7327a8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bool-sdk", - "version": "0.2.0-next.26", + "version": "0.2.0-next.27", "description": "Client SDK for apps built on Bool — gateway data access, end-user auth, the AI battery, the React auth layer, and the local-dev CLI (link, types, deploy).", "type": "module", "main": "./dist/index.js",