font-lab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # cli — the Font Lab loop (M1 walking skeleton)
2
+
3
+ The first real (non-throwaway) slice: the whole loop runs end to end. The human flips
4
+ between curated directions on their own running site and picks one; the pick is written to
5
+ `.font-lab/selection.json` — the seam the agent reads to ship the real code (M4).
6
+
7
+ ## Install (one command)
8
+
9
+ Inside your Next.js + Tailwind project — or just ask your agent *"install Font Lab"*:
10
+
11
+ ```bash
12
+ npx font-lab install
13
+ ```
14
+
15
+ This does two things, idempotently and reversibly (mirroring the `npx impeccable install`
16
+ pattern):
17
+
18
+ 1. **Skill** → copies the `font-lab` skill into `~/.claude/skills/font-lab`, so the agent
19
+ *discovers* it every session. You just say "pick new fonts" and it reaches for Font Lab.
20
+ 2. **MCP server** → registers `font-lab` in the project's `.mcp.json` so the agent has the
21
+ `font_lab_*` tools to drive the loop. (A newly registered MCP server is picked up on the
22
+ next session/MCP reload.)
23
+
24
+ ```bash
25
+ npx font-lab uninstall # remove the skill + the .mcp.json entry
26
+ ```
27
+
28
+ Useful flags: `--project <dir>` (target a project other than the cwd), `--no-mcp` /
29
+ `--no-skill` (do only one half), `--local` (register the MCP server as `node <checkout>/mcp.mjs`
30
+ for testing an unpublished clone), `--dry-run` (print the plan, write nothing).
31
+
32
+ ## The choosing moment: Headless vs Live
33
+
34
+ This is the heart of Font Lab. **The human always makes the taste decision** — Font Lab's job
35
+ is to hand you a *curated, controlled* menu (never a 1,500-font dump), rendered on your own
36
+ site, and ship exactly what you pick. There are two ways to present that menu. They are the
37
+ same loop and the same catalog — only the surface differs.
38
+
39
+ ### 1. Headless — the default, works everywhere
40
+
41
+ The agent screenshots your real site in each direction and shows you the images right in the
42
+ chat. You pick one by name (on a phone: just tap it). The agent ships it.
43
+
44
+ - Works in **every** surface: Claude Code on the web, the iPhone/desktop apps, any MCP agent.
45
+ - No dev server for *you* to babysit, no browser window to manage — you compare finished
46
+ pictures and choose.
47
+ - The screenshots are driven through the real preview engine, so **what you see is what ships**
48
+ (same `next/font` + Tailwind, same metric-matched fallback).
49
+
50
+ Tools: `font_lab_screenshot_directions({ projectDir, baseUrl })` → show the images → the human
51
+ picks → `font_lab_select({ projectDir, directionId })` → `font_lab_apply`.
52
+
53
+ ### 2. Live — the full UI, when you want to drive
54
+
55
+ A real dev-panel on your running site: flip with `← →`, mix a heading from one direction with a
56
+ body from another (`[ ]`), toggle before/after (`B`), pin two to compare, "more like this," and
57
+ see each face across multiple routes. This is the richest way to choose — and it needs a browser
58
+ *you* can click in, so it runs **locally**: a Mac/Linux terminal, or the integrated terminal in
59
+ **VS Code / Cursor / the Claude Code IDE extension**.
60
+
61
+ Get the exact commands anytime with `font_lab_live_instructions({ projectDir })`.
62
+
63
+ ### How an agent should offer this
64
+
65
+ > **Default to Headless.** Present the screenshots first — it works no matter where the user is.
66
+ > Then tell them the full live UI exists: *"Want to flip, mix, and compare these yourself? I can
67
+ > give you a one-time command to open the full editor locally."* Give them the option; never
68
+ > auto-pick. Curated and controlled, with a clear path to expand — that's how Font Lab rolls.
69
+
70
+ | | Headless (default) | Live (full UI) |
71
+ |---|---|---|
72
+ | Where it runs | anywhere (web, phone, any agent) | local terminal / IDE / Cursor |
73
+ | You interact with | screenshots in chat | a live panel on your site |
74
+ | Mix roles, before/after, multi-route | pick a whole direction | yes — the complete UX |
75
+ | Fidelity | preview == ship | preview == ship |
76
+ | Who decides | **the human** | **the human** |
77
+
78
+
79
+ > **Status: M1 + M2 + M3 PASS.**
80
+ > - **M1** (`cli/run-m1.sh`, 16/16): arrow-flip, live display+body+mono swap, Pick →
81
+ > `selection.json`, re-pick appends to `picks.log.jsonl`.
82
+ > - **M2** (`cli/run-m2.sh`, 19/19): `selection.json` → real `next/font` + Tailwind edits
83
+ > that **build** and **render** the picked fonts, applied **idempotently** and
84
+ > **reversibly** (backup-first undo, byte-identical restore). The link nobody else closes.
85
+ > - **M6** (`cli/run-m6.sh`, M1 16/16 + M6 17/17): the **choosing moment polished**, driven in
86
+ > a real browser. **Mixed picks** (heading from one direction, body from another), **before/
87
+ > after**, **pin-two-to-compare**, **more-like-this**, refined keyboard UX, and **multi-route
88
+ > flipping** — the working pairing persists across routes (`/`, `/dense`, `/form`) via
89
+ > sessionStorage, because a face reads differently on a hero vs. a dense page vs. a form. A
90
+ > mixed pick ships end to end (verified: Fraunces/Figtree/JetBrains Mono → real `next/font`).
91
+ > - **M5** (`cli/run-m5.sh`, 26/26): the **MCP server + skill** so an agent drives the whole
92
+ > loop (analyze → curate *or* compose → preview → read the pick → apply). The agent gets the
93
+ > curated default for free **and can take the wheel** — composing its own directions from the
94
+ > catalog (option 3) — but only from catalog fonts, so preview == ship still holds. The human
95
+ > always makes the final pick. Verified over real stdio (initialize / tools-list / tools-call).
96
+ > - **M4** (`cli/run-m4.sh`, 96/96): the **parity catalog + curator**. A 41-font catalog of
97
+ > variable Google fonts, each gated on *verified* capsize coverage (checked by importing
98
+ > the metrics) and single-woff2 variable parity. A deterministic, **LLM-free** curator
99
+ > turns the analysis into ~5 directions — moving off the current fonts, rankable by vibe,
100
+ > reproducible. Evidence in `cli/out/m4-report.json`.
101
+ > - **M3** (`cli/run-m3.sh`, 60/60): the **real analyzer** reads framework, App vs Pages
102
+ > Router, Tailwind v3 vs v4, current fonts per role, and font wiring — and feeds both the
103
+ > codegen branch selection and the panel's before/after. Verified on the in-repo fixtures
104
+ > **and the real jack-mcgovern.com site**, where it correctly reads `Bricolage Grotesque /
105
+ > Hanken Grotesk` on `<body>` and codegen **adopts** those project variables to ship a
106
+ > building, reversible swap. Out-of-branch projects (v3 / Pages / hardcoded) are refused
107
+ > with a clear reason.
108
+ >
109
+ > Evidence in `cli/out/{m1,m2,m3}-report.json` (+ `cli/out/jack-applied.*` — the actual code
110
+ > Font Lab generated for the real site). Runs on Next 16 + Tailwind v4 + Turbopack.
111
+
112
+ ### Dogfood note — what jack-mcgovern.com taught us (M3)
113
+
114
+ Applied into the real site, Font Lab **builds and renders**: body fonts swap
115
+ Hanken Grotesk → Libre Franklin everywhere they show. The headings keep rendering the body
116
+ font *both before and after* — because the site's own `@layer base { h1,h2,h3 { font-family:
117
+ var(--font-display) } }` never resolves (Tailwind v4's `@theme inline` doesn't emit
118
+ `--font-display` as a `:root` variable; only the `font-display` *utility* derefs it). Font
119
+ Lab **preserved that behavior faithfully** rather than silently "fixing" it — exactly the
120
+ honesty the WYSIWYG promise requires: we swap the families through the project's own wiring
121
+ and change nothing else.
122
+
123
+ That dogfood turned into a feature. The analyzer now ships **coverage diagnostics** so the
124
+ tool *detects* this class of problem instead of being surprised by it:
125
+
126
+ - **Dead roles.** Under `@theme inline`, Tailwind v4 doesn't publish `--font-*` as a `:root`
127
+ variable — only the generated `font-*` utilities deref it. A site that hand-writes
128
+ `font-family: var(--font-display)` (a very common pattern) therefore has *silently broken*
129
+ display wiring. `analyze` flags it: `⚠ dead display — swap invisible until rewired`.
130
+ - **Other subsystems.** Fonts declared with their own next/font in another route/component
131
+ (jack's `/gus` uses `--font-fraunces`/`--font-dm-sans`) are reported, so the agent/user
132
+ knows a global swap's true scope (full per-route flipping is M6).
133
+
134
+ The principle this protects: **preview and ship must operate on the same leaf next/font
135
+ variable, applied at the same element next/font uses** (the analyzer reports both
136
+ `classNameTarget` and each role's `nextFontVar`). When they match, preview == ship *by
137
+ construction* on any site — and when a swap genuinely can't be seen, the tool says so rather
138
+ than letting the user pick blind.
139
+
140
+ ## Run it
141
+
142
+ ```bash
143
+ cd cli && pnpm install # @capsizecss/metrics + playwright
144
+ bash cli/run-m1.sh # from the repo root
145
+ ```
146
+
147
+ `run-m1.sh` builds the parity catalog, starts the fixture dev server, and drives the loop
148
+ with a headless browser, asserting the pick lands on disk. Verdict + assertions in
149
+ `cli/out/m1-report.json`.
150
+
151
+ ## Use it by hand
152
+
153
+ ```bash
154
+ node cli/analyze.mjs --project <your-project> # what Font Lab sees (read-only)
155
+ node cli/curate.mjs --project <your-project> # the ~5 directions it would offer
156
+ node cli/curate.mjs --project <your-project> --vibe editorial
157
+ node cli/gen-catalog.mjs # self-host fonts + build catalog
158
+ cd examples/sample-next-site && pnpm dev # your dev server
159
+ node cli/font-lab.mjs --project examples/sample-next-site # the pick endpoint (:7777)
160
+ ```
161
+
162
+ Then open the dev site: a panel (bottom-right) shows the current state + the curated
163
+ directions, swapping live on your real content. Keys (M6):
164
+
165
+ - `←` `→` — flip direction · `↑` `↓` — focus a role · `[` `]` — swap just that role (**mixed
166
+ picks**: heading from one direction, body from another)
167
+ - `B` — before/after · `P` then `Space` — **pin two and compare** · `M` — more like this
168
+ - `Enter` or **Pick** — write the selection
169
+
170
+ The working pairing follows you across routes (`/`, `/dense`, `/form`) so you can judge a face
171
+ on a hero, a dense page, and a form — "your real site" is more than one screen.
172
+
173
+ ### Run it on your own project (`font-lab init`)
174
+
175
+ One command makes any supported project (App Router + Tailwind v4 + CSS-variable fonts)
176
+ previewable — it self-hosts the parity bundles, drops in the dev panel, and mounts it
177
+ dev-only in your layout:
178
+
179
+ ```bash
180
+ node cli/init.mjs --project <your-project> # scaffold panel + parity bundles (reversible)
181
+ cd <your-project> && <your dev command> # e.g. next dev / npm run dev
182
+ node cli/font-lab.mjs --project <your-project> # the pick endpoint (:7777)
183
+ # → flip in the panel, Pick, then `node cli/apply.mjs --project <your-project>`
184
+ node cli/init.mjs --project <your-project> --undo # remove the panel scaffolding
185
+ ```
186
+
187
+ The panel swaps through your project's **own** leaf font variables (the analyzer's `wiring`),
188
+ so the live preview is byte-for-byte what `apply` ships. A role the site doesn't route through
189
+ a variable is shown as *not wired* rather than faked. Proven end to end on the real
190
+ jack-mcgovern.com (body swapped site-wide live → shipped Playfair Display / Source Serif 4 /
191
+ Roboto Mono → reverted clean).
192
+
193
+ ### Fix a dead role (`font-lab rewire`)
194
+
195
+ If `analyze` flags a role as **dead** (declared but not actually rendered — a heading rule that
196
+ reads `var(--font-display)` under `@theme inline`), `rewire` points those raw usages at the
197
+ published leaf var so the font renders. Reversible.
198
+
199
+ ```bash
200
+ node cli/rewire.mjs --project <your-project> # var(--font-display) → var(--font-bricolage)
201
+ node cli/undo.mjs --project <your-project> # revert
202
+ ```
203
+
204
+ Proven on the real jack-mcgovern.com: headings rendered `Hanken Grotesk` (body font) before →
205
+ `Bricolage Grotesque` after, build-verified, then reverted.
206
+
207
+ ### Let an agent drive it (the easy way)
208
+
209
+ Register the server once with Claude Code (user scope = available in every project):
210
+
211
+ ```bash
212
+ cd Font-Lab/cli && pnpm install # one-time
213
+ claude mcp add font-lab -s user -- node "$(pwd)/mcp.mjs"
214
+ ```
215
+
216
+ Then, in any supported project, just tell Claude:
217
+
218
+ > "Use Font Lab to pick fonts for this site."
219
+
220
+ Claude runs the whole setup — `analyze` → `init` (installs the panel + self-hosts the fonts) →
221
+ `rewire` if headings are dead → starts your dev server + the pick endpoint. **Your only job:**
222
+ open the site, flip/mix/compare in the panel, and hit **Pick**. Claude reads your pick and
223
+ ships it (`apply`), reversibly.
224
+
225
+ Prefer not to use the CLI? A ready-to-use [`.mcp.json`](../.mcp.json) is committed at the repo
226
+ root (loads automatically when you open this repo), or copy the entry into another project's
227
+ `.mcp.json` with an absolute path:
228
+
229
+ ```jsonc
230
+ { "mcpServers": { "font-lab": { "command": "node", "args": ["/abs/path/to/cli/mcp.mjs"] } } }
231
+ ```
232
+
233
+ The 11 tools (`analyze`, `list_catalog`, `curate`, `compose_directions`, `init`, `uninit`,
234
+ `prepare_preview`, `read_pick`, `apply`, `rewire_dead_roles`, `undo` — all `font_lab_*`) and the
235
+ loop are described in [`../skill/font-lab/SKILL.md`](../skill/font-lab/SKILL.md). The agent gets
236
+ the curated default for free and can compose its own directions from the catalog — but the
237
+ **human always makes the pick**, and composed fonts must be catalog members so preview still
238
+ equals ship. Proven end to end over MCP on the real jack-mcgovern.com.
239
+
240
+ ### Ship the pick (M2)
241
+
242
+ ```bash
243
+ node cli/apply.mjs --project <your-project> # selection.json -> next/font + Tailwind edits
244
+ node cli/undo.mjs --project <your-project> # restore the files Font Lab last edited
245
+ ```
246
+
247
+ `apply` edits `app/layout.tsx` (ts-morph: merges the next/font import, rewrites the
248
+ font consts in a fenced block, merges the `<html>` className) and `app/globals.css`
249
+ (a fenced `@theme` block), backing up every touched file first. Re-running is idempotent;
250
+ `undo` restores byte-for-byte. Run `font-lab --apply` to ship a pick the moment it's made.
251
+
252
+ ## Pieces
253
+
254
+ | file | role |
255
+ |---|---|
256
+ | `analyzer.mjs` | **the analyzer (M3):** pure, read-only audit of a project — framework, router, Tailwind version, current fonts per role, and wiring. Traces the CSS custom-property graph from `--font-*` back to the next/font const that feeds it. **Coverage diagnostics**: flags dead roles (a swap that won't be visible) and other font subsystems (routes a global swap won't reach) |
257
+ | `analyze.mjs` | thin CLI around the analyzer (`--project`, `--json`) |
258
+ | `catalog.mjs` | **the catalog (M4):** ~41 variable Google fonts as parity bundles — each with a verified capsize slug, a discovered css2 latin query, role suitability, and vibe tags. Pure data; the parity asset |
259
+ | `curator.mjs` | **the curator (M4):** ~12 hand-authored directions + a deterministic `curate(analysis, {vibe, count})` that returns ~5, moving off the current fonts. No runtime LLM |
260
+ | `curate.mjs` | thin CLI to preview the directions for a project (`--project`, `--vibe`, `--count`, `--json`) |
261
+ | `catalog-build.mjs` | reusable `generateCatalog(projectDir, directions, meta)` — self-hosts fonts + computes parity fallbacks + writes the generated module. Built from curated OR agent-composed directions |
262
+ | `gen-catalog.mjs` | CLI: analyzer → curator → `generateCatalog`, bakes the real `current`/`target`/`directions` into `app/_fontlab/catalog.generated.ts` |
263
+ | `engine.mjs` | **the engine facade (M5):** the stable API the MCP wraps — `analyze`, `listCatalog`, `curate`, `composeDirections` (option 3, catalog-gated), `preparePreview`, `readSelection`, `apply`, `undo` |
264
+ | `mcp.mjs` | **the MCP server (M5):** dependency-free JSON-RPC/stdio server exposing the engine as 8 agent tools, descriptions tuned for discoverability |
265
+ | `init.mjs` | **the installer:** scaffolds the panel + parity bundles into a real project and mounts it dev-only in the layout; `--undo` restores byte-for-byte. The last mile to "your own running site" |
266
+ | `.mcp.json` (repo root) | ready-to-use MCP registration — open the repo in an MCP client and the `font-lab` server loads |
267
+ | `templates/font-lab-panel.tsx` | the portable dev panel `init` installs — same UX as the fixture's, but swaps through the analyzer's `wiring` so it's honest on any site |
268
+ | `../skill/font-lab/SKILL.md` | the skill manifest — how an agent drives the loop and the rules (human picks; catalog-only; be honest about coverage) |
269
+ | `font-lab.mjs` | the CLI: the localhost write-back endpoint (`POST /select` → `.font-lab/selection.json` + `picks.log.jsonl`); `--apply` ships on pick |
270
+ | `codegen.mjs` | the ship engine (M2+M3): `applySelection` / `undo` — analyzer-gated branch selection, ts-morph + fenced markers, backup-first. Handles both the role-var path and the **adopt-existing-variable** path (real sites) |
271
+ | `apply.mjs` / `undo.mjs` / `rewire.mjs` | thin CLI wrappers around the ship engine (`rewire` fixes dead roles) |
272
+ | `loop-test.mjs` / `apply-test.mjs` / `m3-test.mjs` / `m4-test.mjs` / `m5-test.mjs` / `m6-test.mjs` | headless e2e of the loop (M1), ship engine (M2), analyzer + branch selection (M3), catalog + curator (M4), engine + MCP over stdio (M5), and the polished panel — mixed picks / pin / multi-route — in a real browser (M6) |
273
+ | `run-m1.sh` … `run-m6.sh` | loop test; apply+build+render+idempotency/reversibility; analyzer+codegen; catalog+curator; engine+MCP; mixed-picks/pin/multi-route in a browser |
274
+
275
+ ## The contract it writes
276
+
277
+ `.font-lab/selection.json` follows the schema in
278
+ [`../ARCHITECTURE.md`](../ARCHITECTURE.md): `direction`, `roles` (display/body/mono with
279
+ family/source/weights), `replaces` (current fonts), and `target` (framework/router/Tailwind
280
+ version/wiring). `picks.log.jsonl` appends every pick — the taste-memory stream the roadmap
281
+ starts capturing at M1.
package/analyze.mjs ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // `font-lab analyze` — print what Font Lab sees in a project (M3). Read-only.
3
+ // node cli/analyze.mjs [--project <dir>] [--json]
4
+ import path from "node:path";
5
+ import { analyzeProject, summarize } from "./analyzer.mjs";
6
+
7
+ const arg = (f, d) => {
8
+ const i = process.argv.indexOf(f);
9
+ return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : d;
10
+ };
11
+ const project = path.resolve(arg("--project", process.cwd()));
12
+ const a = analyzeProject(project);
13
+
14
+ if (process.argv.includes("--json")) {
15
+ console.log(JSON.stringify(a, null, 2));
16
+ } else {
17
+ console.log(`Font Lab — analysis of ${path.relative(process.cwd(), project) || "."}`);
18
+ console.log(summarize(a));
19
+ }