figura-cli 0.25.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figura-cli",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Command-line client for the Figura visualization SaaS",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  name: fig
3
3
  description: "Mock up a screen as an on-brand, shareable HTML fig via your Figura team — render a UI state in seconds before writing the real code. Use when asked to visualize a screen, mock a UI, or 'make a fig'. iPhone-framed by default; browser frame for web/desktop."
4
4
  argument-hint: '<what to visualize>'
5
- allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_fig_scaffold, mcp__figura__figura_fig_devices, mcp__figura__figura_list_figs, mcp__figura__figura_generate_fig, mcp__figura__figura_compose_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_upload_asset, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, mcp__figura__figura_request_feedback, Bash(figura *), Bash(bunx figura-cli *)'
5
+ allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_fig_scaffold, mcp__figura__figura_fig_devices, mcp__figura__figura_list_figs, mcp__figura__figura_generate_fig, mcp__figura__figura_compose_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_upload_asset, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, mcp__figura__figura_merge_figs, mcp__figura__figura_request_feedback, Bash(figura *), Bash(bunx figura-cli *)'
6
6
  ---
7
7
 
8
8
  # /fig — figure it out before you build it
@@ -62,6 +62,20 @@ something in a vacuum — and end up rebuilding what already ships. So every tim
62
62
  `figura_list_figs`). If a fig of this screen already exists, **iterate that
63
63
  fig** (step 5), never a fresh duplicate.
64
64
 
65
+ ★ **The chain is the unit — and Figura now enforces it.** A create that looks
66
+ like the next round of an open question is **refused** (409 `similar_chain`) and
67
+ the response names the chain to revise. That is not an obstacle to route around:
68
+ each round you land as its own fig is a card in the library nobody can walk, and
69
+ a reviewer who opens round 4 sees neither rounds 1–3 nor the discussion held on
70
+ them. Revise the named fig. `new: true` (CLI `--new`) exists for a genuinely
71
+ different question — not for "revising felt like more work".
72
+
73
+ **Already forked it?** `figura_merge_figs` (CLI `figura fig merge <id> --into
74
+ <id>`) folds the sibling back in: its versions append after the destination's
75
+ head, share links keep resolving, verdicts and decisions follow. Run it with
76
+ `dry_run` first to see the plan. Merging your own fork mid-session is normal
77
+ housekeeping, not an admission of anything.
78
+
65
79
  If any prior design exists → build in **`mode: compare`** and render it as
66
80
  **existing (as-is) → proposed**, grounded in the real current state. Even when the
67
81
  proposal is better, it must be argued against what's there — that's the whole
@@ -178,7 +192,9 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
178
192
  - **`sharedCss`** — ONE stylesheet every screen shares: put your brand tokens
179
193
  (from the scaffold) and shared content classes here. Per-screen one-offs go
180
194
  in that screen's `css`.
181
- - Plus `area`, `title`, `orientation`, and a real `description`.
195
+ - Plus `area`, `title`, `orientation`, and a real `description` — the fig's
196
+ BRIEF, and the reviewer's only view of the thread. Contract: "The brief"
197
+ below.
182
198
 
183
199
  **Small screens are the SERVER's job — with one exception that is yours.**
184
200
  Below a catalog-derived breakpoint every multi-frame layout (`compare`,
@@ -280,7 +296,8 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
280
296
  `html` for a composed fig is refused (422) — it would leave the references
281
297
  behind. `figura_get_fig` returns the fig's `children` when you need to see the
282
298
  current composition. Never hand-author the framed document to feed `revise`
283
- (hard rule 1). Publishing is
299
+ (hard rule 1). A revision takes its own `description` — restate the arc
300
+ there; whoever reads v4 did not read v1. Publishing is
284
301
  NOT inherited automatically: a revision of an already-published fig still
285
302
  lands unpublished by default (`viewUrl` is a team-scoped view, and the
286
303
  existing public link keeps serving the OLD version) — pass `publish: true`
@@ -317,6 +334,8 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
317
334
  **`figura_request_feedback`** (CLI: `figura fig request-feedback <id> --from
318
335
  <name|email> --note <text>`). See "Assign a reviewer" below — this step is
319
336
  what stops a fig from being landed, committed, and never actually read.
337
+ The note names the decision; the fig's `description` carries the context —
338
+ check it against "The brief" below *before* you send the ask.
320
339
 
321
340
  ## Presenting a redesign — FOCUS first, overview second
322
341
 
@@ -339,6 +358,27 @@ using it for one shipped a 4-state fig as 134px phones with ~3px annotation
339
358
  chips. A Focus-only fig is **complete on its own**; add Overview only when there
340
359
  are enough states that scanning them at once genuinely helps.
341
360
 
361
+ **★ THE LAST PILL IS YOUR READ — a fig that shows options must pick one.**
362
+ Whenever a fig carries more than one *direction* (A / B / …, or any "which way
363
+ do we go" set), the FINAL state is a **verdict state**, not one more variation:
364
+ the options weighed against each other, and one of them named as the pick.
365
+ Without it a reviewer gets a gallery and no argument, and the decision lands back
366
+ on whoever opened the link — which is the exact thing a fig exists to prevent.
367
+
368
+ The verdict state is one framed screen like any other, labelled `My read`, and
369
+ it carries, tersely:
370
+
371
+ - **the pick, named in the first line** — "Go with A" — never "both have merit";
372
+ - **one line per option** of what it wins and what it costs (`+` / `–`) — the
373
+ same pairs the Overview chips carry, so the two views can't disagree;
374
+ - **the deciding reason**: the one constraint that broke the tie (a component
375
+ that already ships, a cap, a rule the others violate);
376
+ - **what would change your mind**, when the pick is close.
377
+
378
+ Two states of the SAME direction (a before/after, one layout at two
379
+ orientations) need no verdict — there is nothing to weigh. Three directions and
380
+ no verdict pill is an unfinished fig.
381
+
342
382
  - **Focus — full frame (default).** One state at a time behind state pills
343
383
  (Today / A / B / …), each state framed identically so flipping states in place
344
384
  reads as spot-the-difference. This is where the context lives: what's broken
@@ -374,6 +414,21 @@ are enough states that scanning them at once genuinely helps.
374
414
  same rule holds by hand: the rail is a **sibling of the frame**, never a
375
415
  descendant of the clipped `.fig-screen`, and nothing but a pin sits on the
376
416
  design.
417
+ - **Every direction gets the SAME treatment.** If one direction is shown in a
418
+ second orientation, a second state, or a second breakpoint, they ALL are.
419
+ A subset argues from unequal evidence and quietly favours whichever direction
420
+ got the extra look: one fig shipped `Today ↻` and `A ↻` pills while B and
421
+ Fresh had no landscape frame at all, so two directions were judged turned and
422
+ two were judged only upright. **Cover the orientations the product actually
423
+ ships** — on an app that is landscape-native, that means portrait AND
424
+ landscape for every direction, by default, not as an extra. Narrow to one
425
+ orientation only when the work itself is scoped to one: a portrait-only bug,
426
+ or a conversation already about the turn, doesn't need its opposite.
427
+ - **A pill label is plain TEXT.** `label` is a text field, not markup — write
428
+ the character (`·`, `↻`, `—`), never an HTML entity. The same call carries
429
+ each screen's `html`, where `&middot;` is exactly right, so the habit crosses
430
+ over easily; the composer decodes an entity it finds in a label rather than
431
+ shipping it literally, but the label still means text.
377
432
  - **Overview — side by side (alternative).** Every state at once: the honest
378
433
  current state first, then each variation, as scaled-down frames in a grid.
379
434
  Under each: a status chip and ONE condensed context line (`+ / –`). This view
@@ -387,8 +442,8 @@ are enough states that scanning them at once genuinely helps.
387
442
  (`broken` / `improved` / `tradeoff` / `detail`) rather
388
443
  than hand-picking colors — the rail and its pins take the team's status colors
389
444
  from the design system, and a `sharedCss` override of `--fig-note-improved` and
390
- friends re-points all of them at once; mark the recommended variation and
391
- close with a short "my read" arguing the pick against the others. Keep the
445
+ friends re-points all of them at once; and mark the recommended direction in
446
+ both the pick the verdict state argues for wears its chip here too. Keep the
392
447
  chrome around the frames minimal — the frames are the content; no legends
393
448
  (notes label themselves), no repeated context, fluid `max-width` throughout.
394
449
  - **Verify the SERVED fig, not your local draft.** The component kit (device
@@ -479,6 +534,54 @@ Example — a GSAP-driven entrance for a mobile screen, pinned + allowlisted:
479
534
  </script>
480
535
  ```
481
536
 
537
+ ## The brief — what the reviewer reads before your fig
538
+
539
+ **`description` is the fig's BRIEF, not a caption.** Figura renders it as the
540
+ **Brief** panel beside the fig, above the discussion: it is the first thing the
541
+ person you assign in step 8 reads, and across a multi-round exercise it is the
542
+ only place the *thread* survives — each round lands as its own fig, and nothing
543
+ in the product links them. The fig argues THIS round. The brief carries what
544
+ happened before it.
545
+
546
+ ★ **Four beats, in this order.**
547
+
548
+ 1. **The objective, in the user's own words.** Quote the sentence that started
549
+ the exercise, verbatim — and re-quote it in every later round. It is the
550
+ standard the fig gets judged against, and the first thing to evaporate when a
551
+ round is written from memory.
552
+ 2. **The arc — one line per prior round: what it tried, and what it settled or
553
+ killed.** Name each round so the reviewer can go and find it.
554
+ 3. **What is still open** — the question this round does *not* close.
555
+ 4. **The pick, and the decision you want back.**
556
+
557
+ ★ **An abandoned direction stays in the brief, with the reason it was
558
+ abandoned.** "Withdrawn", "superseded", "we tried that" are deletions, not
559
+ summaries. A dead route is the most useful thing a reviewer can be handed: it
560
+ stops them re-proposing it, and it is usually where the unresolved tension is
561
+ still sitting. And when a round was killed by *evidence* — a real user rejected
562
+ it, a constraint bit, a measurement came back — that evidence outranks every
563
+ design argument in the brief, and has to survive into the next round's.
564
+
565
+ ★ **Never write a later round's brief from session memory.** Before composing a
566
+ fig that continues, supersedes, or replaces an earlier one, read the earlier
567
+ briefs — `figura_list_revisions` for the chain you're revising, or
568
+ `figura_list_figs` (by `area`/`tag`) then `figura_get_fig` — and carry the
569
+ objective and the arc forward. A long session compresses; the brief must
570
+ not. The prior fig is on the server, so this costs one call and never depends on
571
+ what is still in your context.
572
+
573
+ **Which field is which.** `description` is the brief, on every write path
574
+ (`figura_compose_fig` / `figura_create_fig` / `figura_revise_fig` /
575
+ `figura_update_fig`), and it is carried per version — so **a revision restates
576
+ the arc** instead of assuming the reader saw v1. Cap: 2000 characters. `prompt`
577
+ is a different field: the raw brief/instruction you were handed, kept for
578
+ provenance. `intent` (the `figura:context` meta) is a one-line design intent for
579
+ search and is **truncated at 400 characters** — never park the objective there.
580
+
581
+ **The brief is not the fig.** Don't narrate what the frames already show. If the
582
+ brief and the fig disagree about the recommendation, one of them is stale — fix
583
+ it before you send the ask.
584
+
482
585
  ## Assign a reviewer — a proposal fig isn't done until someone's on it
483
586
 
484
587
  **A link nobody was pointed at is a fig nobody saw.** Landing, publishing, and
@@ -71,10 +71,47 @@ Hunt across whatever stack this repo uses — do not assume one framework:
71
71
  `src/routes/*`, `src/screens/*`). These become each platform's `surfaces[]` —
72
72
  THIS product's real areas ("Feed", "Checkout"), never a generic placeholder
73
73
  list and never another product's.
74
+ - **Locales** — which languages the product ships, their script and direction.
75
+ Run `figura brand detect-locales [path]`: an offline scan returning
76
+ `locales` (paste-ready `identity.locales`), `evidence` (why each locale got its
77
+ script/direction), `needsConfirmation`, and the `signals` it fired on. Put the
78
+ DEFAULT language first.
79
+
80
+ Three rules, each from a real miss on a real repo (#998):
81
+
82
+ - **NEVER list a vendor directory's languages.** `node_modules`, `Pods`,
83
+ `dist`, `build`, `.next`, `out`, `vendor`, `DerivedData`, `.git`, `.claude`
84
+ are not this product. A React Native monorepo's `node_modules` alone carries
85
+ 40+ `*.lproj` languages; listing them makes the brand claim languages the
86
+ product does not ship. `detect-locales` drops them itself — if you scan by
87
+ hand instead, exclude every one of those path segments.
88
+ - **Script and direction come from the message TEXT, not the tag.** A repo can
89
+ tag Sorani Kurdish `ku`, which any tag table calls Latin/LTR — SHA7N's
90
+ `locale/ku/general.json` is 4848 Arabic codepoints. Read the locale's message
91
+ files and judge from the characters. Arabic script plus any of ڕ ڵ ێ ۆ is
92
+ Sorani → emit the tag **`ckb`** (that is the tag the per-script font
93
+ resolver's Sorani coverage check keys on; `ku` would let a font missing those
94
+ letters through), not `ar` and not `ku`.
95
+ - **Ambiguous tags are a question, not a guess.** `ku ks pa sd az uz tk ha ber
96
+ mn` are each written in more than one script depending on region or
97
+ orthography. With message content, classify from it. Without, they arrive in
98
+ `needsConfirmation` — ASK the user which script that language uses in this
99
+ product, and do not paste the locale until they answer.
100
+
101
+ Show the user the `evidence` line for each locale alongside the JSON, so a
102
+ wrong call is visible rather than silent.
103
+
104
+ Manual signals if the CLI is unavailable: i18n deps in `package.json`
105
+ (`next-intl`, `react-i18next`, `i18next`, `@lingui/*`, `vue-i18n`), an i18n
106
+ config's `locales: [...]` array, `locale/*` / `locales/*.json` /
107
+ `messages/*.json` dirs, `dir="rtl"` in markup, iOS `*.lproj/` dirs +
108
+ `Localizable.strings`, Android `values-ar/strings.xml`.
74
109
  - **Which platform(s)** — detect the target platform(s) so you only emit the
75
110
  surfaces that apply:
76
111
  - `mobile` — an iOS/Android/React-Native/Expo app (`*.xcodeproj`, `Info.plist`,
77
112
  `android/`, `app.json`/Expo, SF Symbols usage, safe-area/tab-bar tokens).
113
+ Note WHICH OS it targets — an `android/` + Gradle + Material repo is
114
+ `os: "android"`, an `*.xcodeproj`/SwiftUI repo is `os: "ios"`.
78
115
  - `web` — a web app / site (`index.html`, a web router, CSS media queries,
79
116
  hover states, a `tailwind.config`).
80
117
  - `desktop` — an Electron/Tauri/native-desktop shell.
@@ -93,14 +130,22 @@ EXACTLY (strict keys, hex 3/4/6/8 digits, no blank array entries, only
93
130
 
94
131
  - **name** — the product/brand name (still required on the write).
95
132
  - **identity** — the SHARED brand: `colors` (real palette, each
96
- `{ name, hex, role?, meaning? }`), `gradient` (if any), `type`
133
+ `{ name, hex, role?, meaning? }`), `gradient` (if any), `signature`
134
+ (`{ kind: solid|gradient|iridescent, stops?: hex[] }` — solid unless the repo's
135
+ tokens define a gradient identity; see `reference/brand-format.md`), `type`
97
136
  (`{ families, ramp, rules }`), `spacing`, `radius`, `voice`
98
- (`{ nouns, promise, lines, tone }`), `forbidden` (hard guardrails as
99
- `[{ id?, rule }]`).
137
+ (`{ nouns, promise, lines, tone }`), `locales` (each
138
+ `{ tag, script, direction, fonts? }` — BCP-47 tag, ISO 15924 script,
139
+ `ltr`/`rtl`; FIRST entry is the default; per-locale `fonts` override the
140
+ shared families for that script, e.g. an Arabic display face), `forbidden`
141
+ (hard guardrails as `[{ id?, rule }]`).
100
142
  - **platforms** — a map with a key ONLY for each platform you detected in step 1.
101
143
  Each surface: `components` (signature components as `[{ name, spec }]`), `icons`
102
144
  (the platform's icon language), `layout` (its frame/shell law), `surfaces` (this
103
- product's real areas), `bans` (platform-specific don'ts). Use the per-platform
145
+ product's real areas), `bans` (platform-specific don'ts). `mobile` ALSO takes
146
+ `os` (`"ios"` | `"android"`) — set it when the repo names its OS; it drives the
147
+ fig's device frame, system-bar heights, back affordance, and corner radii.
148
+ Omit it for an iOS/cross-platform repo (iOS is the default). Use the per-platform
104
149
  **`tokensBlock`** escape-hatch ONLY for guidance that genuinely doesn't fit a
105
150
  structured field — prefer a structured field.
106
151
 
@@ -8,21 +8,41 @@ so it can't drift). This page is only orientation + one worked example.
8
8
 
9
9
  Two halves:
10
10
  - **`identity`** — the SHARED brand (same on every platform): `colors`
11
- (`[{ name, hex, role?, meaning? }]`), `gradient?`, `type?`
12
- (`{ families?: { slot: fontName }, ramp?: number[], rules? }`), `spacing?`,
13
- `radius?`, `voice?` (`{ nouns?, promise?, lines?, tone? }`), `forbidden?`
14
- (`[{ id?, rule }]`).
11
+ (`[{ name, hex, role?, meaning? }]`), `gradient?`,
12
+ `signature?` (`{ kind: "solid" | "gradient" | "iridescent", stops?: hex[] }`),
13
+ `type?` (`{ families?: { slot: fontName }, ramp?: number[], rules? }`), `spacing?`,
14
+ `radius?`, `voice?` (`{ nouns?, promise?, lines?, tone? }`), `locales?`
15
+ (`[{ tag, script, direction, fonts?: { display?, body?, mono? } }]` — the
16
+ languages the product ships: BCP-47 `tag` ("en", "ar", "ckb"), ISO 15924
17
+ `script` ("Latn", "Arab"), `direction` `ltr`|`rtl`; the FIRST entry is the
18
+ default; per-locale `fonts` override the shared type families for that
19
+ script), `forbidden?` (`[{ id?, rule }]`).
20
+
21
+ `signature` is the brand page's hero treatment. The rule: **solid unless the
22
+ repo's tokens define a gradient identity** — a signature gradient named in the
23
+ design system, not a stray mention of the word (a "avoid gradients" guardrail is
24
+ a solid brand). `gradient`/`iridescent` require 2–5 `stops` (the gradient's own
25
+ hexes, in order). Omit the field entirely for a plain solid-accent brand.
15
26
  - **`platforms`** — a map keyed by `mobile` / `web` / `desktop`. Include ONLY the
16
27
  platforms the product targets. Each surface: `components?` (`[{ name, spec }]`),
17
28
  `icons?`, `layout?`, `surfaces?` (this product's real areas), `bans?`,
18
29
  `tokensBlock?` (the prose ESCAPE HATCH for guidance that genuinely doesn't fit a
19
- structured field — use sparingly).
30
+ structured field — use sparingly). `mobile` also takes `os?` (`"ios"` |
31
+ `"android"`), which drives the fig's device frame and OS chrome geometry;
32
+ unset means iOS. `os` on `web`/`desktop` is rejected.
20
33
 
21
34
  Validation the schema enforces (a violation is a 400 at `PUT /brand`): **strict
22
35
  keys** (an unknown/misspelled key is rejected); **hex** = `#` + exactly 3/4/6/8
23
36
  digits; **no blank** array entries; platform keys exactly `mobile`/`web`/`desktop`;
24
- caps on array lengths + string lengths (the fetched schema carries the exact
25
- numbers). Author against the fetched schema, not these prose notes.
37
+ `locales` when present needs >= 1 entry with unique tags; caps on array lengths +
38
+ string lengths (the fetched schema carries the exact numbers). Author against the
39
+ fetched schema, not these prose notes.
40
+
41
+ Locales drive fig generation: the scaffold states them, `fig create --locale ar`
42
+ (or the `locale` param on `figura_create_fig`/`figura_revise_fig`) stamps
43
+ `<html lang dir>` and loads a per-script font — a family that does not cover the
44
+ script (Inter has no Arabic) resolves to a recorded fallback instead of a silent
45
+ browser substitute.
26
46
 
27
47
  ## Worked example — a web/desktop brand ("Ledgerline")
28
48
 
@@ -36,6 +56,7 @@ numbers). Author against the fetched schema, not these prose notes.
36
56
  { "name": "Ink", "hex": "#101312", "role": "ground", "meaning": "near-black base background" }
37
57
  ],
38
58
  "gradient": "linear-gradient(90deg, #1F6F54, #C9A227)",
59
+ "signature": { "kind": "gradient", "stops": ["#1F6F54", "#C9A227"] },
39
60
  "type": {
40
61
  "families": { "display": "Fraunces", "body": "Inter", "mono": "JetBrains Mono" },
41
62
  "ramp": [12, 14, 16, 18, 20, 24, 30, 36, 48],
@@ -43,6 +64,10 @@ numbers). Author against the fetched schema, not these prose notes.
43
64
  },
44
65
  "spacing": [4, 8, 12, 16, 20, 24, 32, 40, 48, 64],
45
66
  "radius": [4, 8, 12, 999],
67
+ "locales": [
68
+ { "tag": "en", "script": "Latn", "direction": "ltr" },
69
+ { "tag": "ar", "script": "Arab", "direction": "rtl", "fonts": { "body": "IBM Plex Sans Arabic" } }
70
+ ],
46
71
  "voice": {
47
72
  "nouns": ["ledger", "balance", "month-end"],
48
73
  "promise": "Books, balanced.",
@@ -1,13 +1,13 @@
1
1
  {
2
- "$comment": "Skill version manifest (#752) the source of truth for each Figura skill's OWN version line, DECOUPLED from the figura-cli package version. A release bumps the version here when a skill's content changes, then regenerates the engine seed (bun packages/core/scripts/build-skill-seed.mjs) and re-seeds. Version is a date tag (YYYY.MM.DD[.n]); releasedAt is ISO-8601. The CLI/MCP stamp this version into .figura-version at install; the engine seeds the registry from it.",
2
+ "$comment": "Skill version manifest (#752) \u2014 the source of truth for each Figura skill's OWN version line, DECOUPLED from the figura-cli package version. A release bumps the version here when a skill's content changes, then regenerates the engine seed (bun packages/core/scripts/build-skill-seed.mjs) and re-seeds. Version is a date tag (YYYY.MM.DD[.n]); releasedAt is ISO-8601. The CLI/MCP stamp this version into .figura-version at install; the engine seeds the registry from it.",
3
3
  "skills": {
4
4
  "fig": {
5
- "version": "2026.08.11.1",
6
- "releasedAt": "2026-08-11T17:35:23.364Z"
5
+ "version": "2026.08.27",
6
+ "releasedAt": "2026-08-27T00:00:00.000Z"
7
7
  },
8
8
  "figura-brand": {
9
- "version": "2026.07.26",
10
- "releasedAt": "2026-07-26T00:00:00.000Z"
9
+ "version": "2026.08.25.2",
10
+ "releasedAt": "2026-08-26T00:00:00.000Z"
11
11
  }
12
12
  }
13
13
  }