figura-cli 0.15.0 → 0.16.1

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.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Command-line client for the Figura visualization SaaS",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,15 +2,16 @@
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_generate_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_publish_fig, Bash(figura *), Bash(bunx figura-cli *)'
5
+ allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_generate_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, Bash(figura *), Bash(bunx figura-cli *)'
6
6
  ---
7
7
 
8
8
  # /fig — figure it out before you build it
9
9
 
10
10
  A **fig** is a self-contained HTML mock of one UI state, rendered against your
11
- Figura team's design system and saved (optionally published with a shareable
12
- link) to your team's timeline. Build a fig to pressure-test a layout or screen in
13
- seconds, before writing the real code.
11
+ Figura team's design system and saved to Figura as a workspace draft —
12
+ committed to the team library when it's the final pick, and optionally
13
+ published with a shareable link. Build a fig to pressure-test a layout or
14
+ screen in seconds, before writing the real code.
14
15
 
15
16
  This skill is a **thin wrapper over the Figura MCP**. It deliberately carries **no
16
17
  brand tokens, colors, fonts, or design rules** — the brand lives in exactly one
@@ -46,6 +47,44 @@ point. Only build a standalone `single` fig when the screen is genuinely net-new
46
47
  Unsure whether a version already exists? **Ask the user** rather than assume
47
48
  greenfield.
48
49
 
50
+ ### Reconstruct the baseline from SOURCE — required, no screenshot
51
+
52
+ The existing/current/broken frame of a `compare` fig must be a **faithful
53
+ recreation of what actually ships**, not a hand-approximation — an approximated
54
+ "before" quietly rebuilds a strawman and the diptych argues against a screen that
55
+ doesn't exist. So when you build the baseline state and **no screenshot is
56
+ provided**, studying the source and rebuilding its layout is a **REQUIRED step**,
57
+ not optional reading:
58
+
59
+ 1. **Locate the screen** — Grep/Glob for the route dir, the `page`/`route`
60
+ file, and the component(s) it renders (e.g. `apps/*/src/app/<route>/page.tsx`
61
+ plus imported views).
62
+ 2. **Read the component file(s)** and extract the real structure:
63
+ - the **JSX/TSX element hierarchy and order** (containers → sections →
64
+ controls, top to bottom);
65
+ - the **class names / style modules / design tokens** on each element
66
+ (`flex`/`grid`, spacing, radii, colors) — these are the layout;
67
+ - the **imported UI primitives** (`Button`, `Input`, `Card`, …) and *their*
68
+ rendered shape (a `variant="cta"` white pill vs an `outline` button is a
69
+ visible difference — open the primitive if unsure);
70
+ - **conditional branches** — auth-provider variants, stages, empty/error/
71
+ loading states — and decide which branch the baseline depicts (state it);
72
+ - the **real copy and labels** (headings, placeholders, button text, error
73
+ banners) verbatim, not paraphrased.
74
+ 3. **Rebuild that layout as the baseline** — real element hierarchy, real
75
+ controls, real copy, real spacing — so the "before" frame matches the shipped
76
+ screen line-for-line, then drift only the *proposed* frame.
77
+ 4. **Fallback (only if the source genuinely can't be located/read):**
78
+ approximate, and **caption the baseline as an approximation** (a chip / note on
79
+ the frame: "approximated — source not found") so no one mistakes a guess for
80
+ the shipped UI.
81
+
82
+ This is what "grounded in the real current state" above means: the baseline is
83
+ **source-faithful by reconstruction**, or it is explicitly labelled a guess —
84
+ never a silent approximation. It applies to **both** the hand-build path and the
85
+ server render below — on a server render you can't read the repo *for* the engine,
86
+ so you hand it the studied structure in the brief (see step 3, "Server render").
87
+
49
88
  ## How to fig
50
89
 
51
90
  1. **Verify the destination team** — call **`figura_whoami`** (or
@@ -74,12 +113,26 @@ greenfield.
74
113
  fetched design system. **Follow that design system verbatim; never invent brand
75
114
  values.**
76
115
 
77
- **Landing IS hosting — leave `publish` unset.** Creating a fig always persists
78
- it to Figura (there is no separate "save it for real" step); the response's
79
- **`viewUrl`** is a link you can open right now, whether or not anyone has
80
- decided to share it wider. Don't pass `publish: true` just to get something
81
- viewable that makes a first-draft iteration PUBLIC. Publishing is a
82
- deliberate, later step (see step 5).
116
+ **Landing IS hosting — leave `publish` and `commit` unset.** Creating a fig
117
+ always persists it to Figura (there is no separate "save it for real" step);
118
+ the response's **`viewUrl`** is a link you can open right now, whether or not
119
+ anyone has decided to share it wider. It lands as a **workspace draft**
120
+ hosted and shareable by URL, visible to its author, but NOT in the team
121
+ library and silent for teammates — so iterate freely without claiming the
122
+ team's attention. Don't pass `publish: true` just to get something viewable
123
+ (that makes a first-draft iteration PUBLIC), and don't pass `commit: true`
124
+ on an exploration — committing is reserved for the session's final
125
+ recommendation (step 7).
126
+
127
+ **Pass the fig markup INLINE as `html` — never a shell reference.** The `html`
128
+ argument to `figura_create_fig` / `figura_revise_fig` is the *full HTML
129
+ document itself*, passed directly as the value. An MCP tool argument is **not**
130
+ a shell: `$(cat "…/fig.html")`, a backtick `` `cat file` ``, any command
131
+ substitution, or a bare file path will **not** expand — the literal string is
132
+ stored as the fig, producing a tiny black/empty canvas (the engine now rejects
133
+ this with a 400). If you authored the HTML in a scratch file to render/verify
134
+ it (step "Verify before publishing"), **read that file's contents** and pass
135
+ the contents as `html` — do not reference the path.
83
136
 
84
137
  3. **Server render — the user's call, never yours.** Figura can also render the
85
138
  fig server-side against the team brand: **`figura_generate_fig`** with a
@@ -95,6 +148,19 @@ greenfield.
95
148
  on brand). Never pick this path on your own to save effort: it costs credits
96
149
  and hand-built figs track the brand better.
97
150
 
151
+ **The engine CANNOT read your repo — carry the studied baseline in the brief.**
152
+ `figura_generate_fig` runs server-side against the team brand; it has no access
153
+ to the caller's filesystem, so for a `compare`/existing state it would otherwise
154
+ *guess* the "before" too. Close the seam yourself: do the source study from
155
+ "Reconstruct the baseline from SOURCE" above **first**, then pass the
156
+ reconstructed structure into the `prompt` brief as an explicit "before" spec —
157
+ the file you grounded it in, the element order top-to-bottom, the real controls
158
+ and primitive shapes, the branch depicted, and the verbatim copy/labels. The
159
+ `prompt` is free NL text, so this needs no extra parameter — the studied layout
160
+ just becomes the baseline half of the brief (the proposed half describes the
161
+ change). If you truly couldn't locate the source, say so in the brief and label
162
+ the baseline an approximation, same as the hand-build fallback.
163
+
98
164
  4. **Open the HOSTED fig for approval — never a local file.** Run
99
165
  `figura fig open <id>` (the id `figura_create_fig`/`figura_generate_fig` just
100
166
  returned). It opens **`viewUrl`** in the reviewer's browser — the actual
@@ -121,13 +187,30 @@ greenfield.
121
187
  **`figura_publish_fig`** (or `figura fig publish <id>`) — Figura's
122
188
  `figura.so/f/<id>` share link. Team-only review never needed this; treat
123
189
  publishing as a deliberate call the human makes, not something create/revise
124
- does on its own.
190
+ does on its own. Publishing is the share link ONLY — it does not put the
191
+ fig in the team library (that's committing, step 7); a shared draft's URL
192
+ resolves fine.
193
+
194
+ 7. **Commit exactly ONE fig — the session's recommendation.** Every fig this
195
+ session landed is a workspace draft: teammates never saw it. **Committing**
196
+ (`draft → published`) is what puts a fig in the team library and notifies
197
+ the team (digest-batched) — so at the end of the session, commit exactly
198
+ the fig the user asked for / the final agreed version, and nothing else.
199
+ Explorations, rejected variants, and intermediate iterations STAY drafts.
200
+ If the deliverable is the fig you're about to create/revise anyway, prefer
201
+ `commit: true` on that final `figura_create_fig` / `figura_revise_fig`
202
+ call (CLI: `--commit`) over a separate call; otherwise commit it afterwards
203
+ with **`figura_commit_fig`** (CLI: `figura fig commit <id>`). Committed the
204
+ wrong one? `figura_commit_fig` with `uncommit: true` (CLI:
205
+ `figura fig uncommit <id>`) returns it to a draft — the share link is
206
+ untouched either way.
125
207
 
126
208
  ## Presenting a redesign — overview ⇄ focus
127
209
 
128
210
  When a fig argues a change against a current state (a shipped baseline, or
129
211
  several candidate directions), build one fig with **two switchable views** —
130
- CSS-only radio tabs, since figs carry no `<script>`:
212
+ plain CSS-only radio tabs for THIS specific toggle (it's binary, must survive
213
+ truncation, and doesn't need a library):
131
214
 
132
215
  - **Overview — side by side.** Every state at once: the honest current state
133
216
  first, then each variation, as scaled-down frames in a grid. Under each: a
@@ -141,7 +224,9 @@ CSS-only radio tabs, since figs carry no `<script>`:
141
224
  as annotation chips: what's broken today, what each variation improves,
142
225
  what it trades away.
143
226
  - **In both views:** render the current state honestly from the shipped
144
- implementation; color-code chips by function (broken / improved /
227
+ implementation **reconstructed from source** per "Reconstruct the baseline
228
+ from SOURCE" (no screenshot ⇒ study the component file(s) and rebuild the real
229
+ layout, don't approximate); color-code chips by function (broken / improved /
145
230
  trade-off / detail) using the team's status colors from the fetched design
146
231
  system; mark the recommended variation and close with a short "my read"
147
232
  arguing the pick against the others. Keep the chrome around the frames
@@ -167,6 +252,45 @@ layout with pixel-level tweaks. Never ship a squeezed side-by-side as the
167
252
  *only* view of desktop frames, and never a full-frame toggle as the only
168
253
  view of many variations — each is one half of the pair above.
169
254
 
255
+ ## JavaScript is ALLOWED — use it for high-fidelity animation
256
+
257
+ Figs are **not** CSS-only. The served fig's CSP (`script-src 'unsafe-inline'
258
+ https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com`) runs
259
+ inline `<script>` **and** external libraries loaded from those three CDNs — so
260
+ reach for real JS whenever it raises fidelity, especially for a mobile-app fig
261
+ where the point is to sell a screen's motion (entrance transitions, spring/
262
+ easing on interactive controls, scroll-driven or gesture-driven animation),
263
+ not just its static layout. Don't self-censor to CSS keyframes when a library
264
+ like GSAP does the job better.
265
+
266
+ Rules, so the script actually runs on the published fig instead of getting
267
+ CSP-blocked or failing validation:
268
+
269
+ - **Only the 3 allowlisted CDNs** — `cdn.jsdelivr.net`, `cdnjs.cloudflare.com`,
270
+ `unpkg.com`. Any other host is CSP-blocked at serve time; the script silently
271
+ never runs.
272
+ - **Pin an exact version** — `gsap@3.13.0`, never `gsap@latest` or an
273
+ unversioned path. The engine's `depsPinned` check fails an unpinned CDN
274
+ script.
275
+ - Inline `<script>` (no `src`) has no CDN restriction — it's your own code, not
276
+ a third-party load.
277
+ - **No network calls** — `connect-src 'none'` blocks fetch/XHR/WebSocket/
278
+ sendBeacon outright; a script that tries to phone home just fails silently.
279
+ Animate/interact with what's already in the DOM.
280
+ - The fig still runs in a sandboxed, opaque-origin iframe (no cookies, no
281
+ parent-document access) — the security tradeoff Figura accepts to allow this
282
+ is documented in `packages/core/src/fig/fig-csp.ts` and `cdn-hosts.ts`.
283
+
284
+ Example — a GSAP-driven entrance for a mobile screen, pinned + allowlisted:
285
+
286
+ ```html
287
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
288
+ <script>
289
+ gsap.from('.card', { y: 24, opacity: 0, duration: 0.5, stagger: 0.08, ease: 'power2.out' });
290
+ gsap.from('.cta', { scale: 0.9, opacity: 0, duration: 0.4, delay: 0.3, ease: 'back.out(1.7)' });
291
+ </script>
292
+ ```
293
+
170
294
  ## Metadata & geometry — stamped for you
171
295
 
172
296
  Every landed fig is **auto-stamped server-side** (on `create` and `generate`)
@@ -184,11 +308,20 @@ with two things you do **not** hand-write:
184
308
 
185
309
  So when you **hand-build** a fig (the default path), do your part of the contract:
186
310
 
187
- - **Mobile:** size the body to the exact logical screen and emit
188
- `<meta name="figura:artboard" content="WxH">` matching it (iPhone portrait
189
- `393x852` / landscape `852x393`). For OS-chrome insets prefer
190
- `var(--fig-safe-top, 59px)` / `.fig-safe-top` over hardcoded pixels — the stamp
191
- fills the value, the fallback keeps you correct if the kit is absent.
311
+ - **Bare mobile screen and ONLY then:** when the fig's HTML *is* one raw app
312
+ screen (no masthead, no state pills, no hand-drawn `.fig-device` frames — the
313
+ Figura viewer draws the iPhone shell around it), size the body to the exact
314
+ logical screen and emit `<meta name="figura:artboard" content="WxH">` matching
315
+ it (iPhone portrait `393x852` / landscape `852x393`). For OS-chrome insets
316
+ prefer `var(--fig-safe-top, 59px)` / `.fig-safe-top` over hardcoded pixels —
317
+ the stamp fills the value, the fallback keeps you correct if the kit is absent.
318
+ - **A document-style fig NEVER gets an artboard.** The artboard meta is the
319
+ viewer's opt-in signal for "size me to exactly WxH and wrap me in a device
320
+ shell" — stamped on an overview ⇄ focus redesign page (or any compare /
321
+ variations document with its own masthead, pills, or hand-drawn device
322
+ frames), it crams the whole document into a phone frame. A document fig
323
+ renders full-page precisely because it carries NO artboard meta, even when
324
+ every screen it depicts is a mobile app.
192
325
  - **Always:** emit `<meta name="figura:context" content="...">` (design intent).
193
326
  - **Optional but valued:** if the fig grounds a specific feature or flag, emit
194
327
  `<meta name="figura:feature" content="<flag-or-slug>">` so it links back to the
@@ -198,7 +331,9 @@ So when you **hand-build** a fig (the default path), do your part of the contrac
198
331
 
199
332
  - **MCP unavailable** (headless / cron, or no interactive auth): fall back to the
200
333
  CLI — `bunx figura-cli fig create` / `fig publish`, with `fig revise` /
201
- `fig revisions` for versions, and `bunx figura-cli brand --json` for the brand.
334
+ `fig revisions` for versions, `fig commit` / `fig uncommit` (or `--commit` on
335
+ the final create/revise) for the team library, and
336
+ `bunx figura-cli brand --json` for the brand.
202
337
  - **`figura_create_fig`/`figura_revise_fig` itself fails** (no network, API down,
203
338
  auth expired): the fig never entered Figura, so there is no `id`/`viewUrl` to
204
339
  open. Only THEN write the HTML to a local scratch file (project-scoped —