mockpit 2.0.0 → 3.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.
Files changed (90) hide show
  1. package/README.md +59 -223
  2. package/bin/demoData.js +2 -2
  3. package/bin/initDesign.js +17 -22
  4. package/bin/mockpit.js +626 -216
  5. package/bin/poster.js +434 -0
  6. package/dist/mcp/server.js +127 -29
  7. package/dist/server/answerAdditions.d.ts +7 -0
  8. package/dist/server/answerAdditions.js +76 -0
  9. package/dist/server/apiViews.d.ts +627 -385
  10. package/dist/server/apiViews.js +198 -81
  11. package/dist/server/app.d.ts +7 -2
  12. package/dist/server/app.js +3514 -984
  13. package/dist/server/appIcon.js +15 -5
  14. package/dist/server/appIconPng.js +3 -3
  15. package/dist/server/askNudges.js +1 -1
  16. package/dist/server/cdnInline.d.ts +8 -0
  17. package/dist/server/cdnInline.js +640 -0
  18. package/dist/server/codeWorker.js +2 -2
  19. package/dist/server/contrast.d.ts +4 -0
  20. package/dist/server/contrast.js +157 -0
  21. package/dist/server/decisionList.d.ts +48 -0
  22. package/dist/server/decisionList.js +340 -0
  23. package/dist/server/decisions.d.ts +4 -4
  24. package/dist/server/decisions.js +9 -9
  25. package/dist/server/designGuide.d.ts +14 -7
  26. package/dist/server/designGuide.js +314 -88
  27. package/dist/server/embeds.d.ts +3 -1
  28. package/dist/server/embeds.js +31 -6
  29. package/dist/server/eventLog.d.ts +2 -2
  30. package/dist/server/eventLog.js +11 -11
  31. package/dist/server/events.d.ts +15 -6
  32. package/dist/server/feedbackBatch.d.ts +39 -13
  33. package/dist/server/feedbackBatch.js +129 -47
  34. package/dist/server/fills.d.ts +4 -0
  35. package/dist/server/fills.js +55 -1
  36. package/dist/server/guidance.d.ts +1 -1
  37. package/dist/server/guidance.js +2 -2
  38. package/dist/server/handoff.d.ts +4 -1
  39. package/dist/server/handoff.js +8 -2
  40. package/dist/server/icons.js +1 -1
  41. package/dist/server/index.js +39 -10
  42. package/dist/server/jump.d.ts +21 -0
  43. package/dist/server/jump.js +175 -0
  44. package/dist/server/kits.d.ts +2 -2
  45. package/dist/server/kits.js +54 -1
  46. package/dist/server/knobs.d.ts +3 -3
  47. package/dist/server/knobs.js +6 -6
  48. package/dist/server/mcpHttp.d.ts +7 -1
  49. package/dist/server/mcpHttp.js +29 -12
  50. package/dist/server/mcpSpec.d.ts +27 -27
  51. package/dist/server/mcpSpec.js +196 -56
  52. package/dist/server/parts.js +9 -1
  53. package/dist/server/run.d.ts +1 -1
  54. package/dist/server/run.js +47 -17
  55. package/dist/server/runApi.d.ts +3 -3
  56. package/dist/server/runApi.js +34 -21
  57. package/dist/server/sqlStore.d.ts +148 -103
  58. package/dist/server/sqlStore.js +1052 -181
  59. package/dist/server/standings.d.ts +25 -0
  60. package/dist/server/standings.js +113 -0
  61. package/dist/server/surfacePage.d.ts +8 -9
  62. package/dist/server/surfacePage.js +423 -100
  63. package/dist/server/system.d.ts +234 -0
  64. package/dist/server/system.js +1508 -0
  65. package/dist/server/systemChecks.d.ts +15 -0
  66. package/dist/server/systemChecks.js +224 -0
  67. package/dist/server/systemSpecimen.d.ts +2 -0
  68. package/dist/server/systemSpecimen.js +49 -0
  69. package/dist/server/themes.d.ts +9 -7
  70. package/dist/server/themes.js +43 -90
  71. package/dist/server/types.d.ts +266 -93
  72. package/dist/server/types.js +76 -79
  73. package/extensions/mockpit.js +89 -72
  74. package/guide/AGENT_SETUP.md +15 -1
  75. package/guide/packs/design-craft.md +2 -3
  76. package/guide/packs/foundations.md +35 -0
  77. package/guide/topics/asks.md +108 -63
  78. package/guide/topics/feedback.md +129 -120
  79. package/guide/topics/html.md +18 -25
  80. package/guide/topics/http.md +43 -41
  81. package/guide/topics/knobs.md +7 -10
  82. package/guide/topics/scripts.md +22 -33
  83. package/guide/topics/surfaces.md +39 -48
  84. package/guide/topics/system.md +157 -0
  85. package/opencode/mockpit.js +120 -0
  86. package/opencode/package.json +8 -0
  87. package/package.json +15 -14
  88. package/skills/launch-video/SKILL.md +11 -11
  89. package/skills/mockpit/SKILL.md +33 -26
  90. package/viewer/dist/index.html +10 -9
package/bin/mockpit.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  mkdirSync,
7
7
  readdirSync,
8
8
  readFileSync,
9
+ statSync,
9
10
  writeFileSync,
10
11
  writeSync,
11
12
  } from "node:fs";
@@ -13,6 +14,7 @@ import { homedir, tmpdir, userInfo } from "node:os";
13
14
  import { dirname, join } from "node:path";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import { parseArgs } from "node:util";
17
+ import { findBrowser, queuePoster, queueRetakes } from "./poster.js";
16
18
  import { serveUrl } from "./serveUrl.js";
17
19
 
18
20
  const BASE = (process.env.MOCKPIT_URL ?? "http://localhost:8228").replace(/\/$/, "");
@@ -28,9 +30,9 @@ const HEADERS = {
28
30
 
29
31
  const HELP = `mockpit — a live visual surface for terminal coding agents
30
32
 
31
- vocabulary: project › mock › state › variant › version. A project is a repo; a
32
- mock is a sketch, page or component, by slug; a state is one thing the user sees in it ("Lab
33
- open"); a variant is a parallel design of a state; a version is its history.
33
+ vocabulary: project › design › state › variant › version. A project is a repo; a
34
+ design is an idea, screen or component, by slug; a state is one thing the user sees in it ("Lab
35
+ open"); a variant is a parallel take on a state; a version is its history.
34
36
  Declare the parts you want feedback on with data-part="name" in the html.
35
37
 
36
38
  the loop (never blocks on the user):
@@ -41,22 +43,22 @@ the loop (never blocks on the user):
41
43
  on; don't re-ask it. Visual options carry a variant or knob set. One
42
44
  render plus a control: --knobs.
43
45
  4. Tell the user in one line where to look, then end your turn. The question
44
- itself lives in the mock, never in chat. The user answers in the
46
+ itself lives in the design, never in chat. The user answers in the
45
47
  browser at their own pace. Never poll.
46
48
  5. When the user says they answered (or a mockpit watch line wakes you), run
47
49
  mockpit feedback. Every write also returns feedback; read it.
48
50
  6. mockpit publish the revision; mockpit export once a variant is in use.
49
51
  In Claude Code, arm mockpit watch under Monitor after asking, then end the
50
- turn; elsewhere the user's next message is the wake-up.
52
+ turn, unless you have the mockpit plugin, which wakes you itself; elsewhere
53
+ the user's next message is the wake-up.
51
54
 
52
55
  verbs:
53
56
  mockpit init [--project name] [--kit <id>] [--kit-url <url> --kit-doc <file|text>]
54
- detect the repo's design system, store
55
- palette/kit/icon sets, write .mockpit/starter.html
56
- mockpit publish --mock <slug> --html <file> [options]
57
+ read the repo's tokens, kit and icon sets
58
+ into the project's system, write .mockpit/starter.html
59
+ mockpit publish --design <slug> --html <file> [options]
57
60
  create or version one variant of one state
58
- --state <label> state, in the user's words (omit for a single-state mock)
59
- --situation <s> another render of that state ("Couldn't save", "Loading")
61
+ --state <label> state, in the user's words (omit for a single-state design)
60
62
  --variant <name> variant label (default "default")
61
63
  --html <file|-> the html body (or a positional file)
62
64
  --surfaces <json|file> instead: the full ordered surface list; {"id"}
@@ -65,14 +67,18 @@ verbs:
65
67
  the latest version (repeatable; name#key targets one instance)
66
68
  --fills <part#fill=file|-> instead: replace or add one fill of a part in
67
69
  the latest version (repeatable)
68
- --title <t> mock title
69
- --kind <k> page|component, only for an app's screens (default sketch)
70
+ --title <t> design title
71
+ --kind <k> screen|component, only for an app's screens (default idea);
72
+ with no html it plans one, drawn by a later publish
70
73
  --area <name> where in the product it belongs ("Shell"); Home groups by it
71
74
  --source <path> the code path that implements it
72
75
  --knobs <json|file> knobs in tunekit usePane shape, keyed by path
73
76
  --from <N> branch from version N
77
+ --amend replace the latest version in place: a fix before the
78
+ user has acted on it (one request, one version)
79
+ --withdraw take the latest version back, under the same rule
74
80
  --prompt <text> what prompted this version
75
- --note <text> one line on what this variant is, shown beside it
81
+ --description <text> one line on what this variant is, shown beside it
76
82
  --widths <list> device widths to show it at, e.g. 390,1280
77
83
  --flow <name> [--step <N>] make this state step N of a flow (0 takes
78
84
  it out); with no html it only places the state
@@ -84,33 +90,42 @@ verbs:
84
90
  --md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
85
91
  add a surface of that kind after the html (repeatable;
86
92
  --data is a JSON tree)
87
- mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
93
+ mockpit ask --design <slug> "<question>" --option <label[=variant]> ...
88
94
  ask the user; bind options to variants
89
95
  --option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
90
- "=variant@3" to its version 3, "=knob:path=value" sets one knob
96
+ "=variant@3" to its version 3, "=knob:path=value" sets one knob,
97
+ "=palette:id" chooses a palette of the project's system
91
98
  --recommend <l> the option you would pick, by label; a quiet mark on it
92
- --scope <s> mock|state|part (default mock)
99
+ --scope <s> design|state|part (default design)
93
100
  --state <label> the state a state-scoped question is about
101
+ --states <label> a design-wide question: a state its answers can be seen
102
+ on (repeatable, best first)
94
103
  --part <name> the part a part-scoped question is about
95
104
  --multi allow several answers
96
105
  --id <id> stable id; reusing one replaces that question
97
106
  --asks <json|file> several questions at once, full shape
98
- mockpit read [slug] [--body] [--history]
99
- no slug: every mock plus what the user
100
- is doing now; a slug: that mock
107
+ mockpit read [slug] [--body] [--history] [--decisions]
108
+ what the user already decided, then
109
+ no slug: every design plus what the user
110
+ is doing now; a slug: that design
101
111
  mockpit feedback what the user sent since you last heard,
102
112
  plus pending; returns at once
103
- mockpit say "<text>" --mock <slug> [--state s] [--variant v]
104
- a plain-text message in a mock's thread
105
- mockpit edit --mock <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
106
- change a mock without a new version ("" clears)
113
+ mockpit say "<text>" --design <slug> [--state s] [--variant v]
114
+ a plain-text message in a design's thread
115
+ mockpit move --design <slug> --variant <v>... (--to <design> [--to-state s] | --archive | --restore)
116
+ move variants elsewhere, archive or restore them
117
+ mockpit edit --design <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
118
+ change a design without a new version ("" clears)
107
119
  mockpit edit --project <name> --rename <new>
108
120
  rename a project
109
121
  mockpit edit --project <name> --area-order "Shell,Food"
110
122
  the order Home lists areas in
111
- mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
123
+ mockpit system [--sheet f.css] [--classes j] [--tokens j] [--scales j] [--palettes j] ...
124
+ the project's system: read it, or change a part
125
+ (applies to every design at once)
126
+ mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
112
127
  write the html in use + history per state
113
- to .mockpit/accepted/<mock>/<state>/
128
+ to .mockpit/accepted/<design>/<state>/
114
129
  mockpit upload <file> [--kind image|file]
115
130
  upload an asset, print its id and URL
116
131
  mockpit guide [--topic <id>] the project-aware brief, or one reference topic
@@ -124,7 +139,7 @@ verbs:
124
139
  other commands:
125
140
  mockpit serve [--port N] [--host H] [--open]
126
141
  start the server (API + viewer)
127
- mockpit demo seed the Writer mock to explore the viewer
142
+ mockpit demo seed the Writer design to explore the viewer
128
143
  mockpit icons [add|remove <set>...] list the icon sets html surfaces can name
129
144
  (icon="prefix:name"); add or remove an
130
145
  Iconify set for this project
@@ -147,6 +162,7 @@ environment:
147
162
  MOCKPIT_PROJECT project name; overrides the git-remote/directory default
148
163
  MOCKPIT_URL server base URL (default http://localhost:8228)
149
164
  MOCKPIT_TOKEN bearer token for a deployed instance
165
+ CHROME_PATH the browser for automatic posters; pointing nowhere turns them off
150
166
  MOCKPIT_HOST address serve binds to (default: every interface)
151
167
  MOCKPIT_LOG serve's event log file, or "off" (default ~/.mockpit/events.jsonl)
152
168
  MOCKPIT_SESSION fixed session id (overrides auto-detection)
@@ -158,7 +174,7 @@ environment:
158
174
  const COMMAND_HELP = {
159
175
  init: `mockpit init [--project <name>] [--kit <id>]
160
176
  [--kit-url <https url> [--kit-script <url>] --kit-doc <file.md|text>]
161
- Detect the repo's design system, store palette + kit for the project, add the
177
+ Read the repo's tokens and kit into the project's system, add the
162
178
  Iconify sets matching its icon packages, and write .mockpit/starter.html
163
179
  (gitignored). --kit makes a bundled or project kit the default for every html
164
180
  surface. --kit-url defines the project's own kit (id from --kit, else
@@ -167,7 +183,8 @@ const COMMAND_HELP = {
167
183
  mockpit rules --mine
168
184
  Guidance on top of the core brief, highest first: the project's
169
185
  .mockpit/rules.md (committed with the repo), your ~/.mockpit/rules.md, then
170
- the packs either names (\`packs: [design-craft]\` in its frontmatter). A file
186
+ the packs either names (\`packs: [design-craft, foundations]\` in its frontmatter:
187
+ design-craft for how to design, foundations for starting a project's system). A file
171
188
  has "## Rules" (always followed) and "## Suggestions" (aimed for).
172
189
  This sends the file as it is on disk; a missing file clears the stored copy.
173
190
  init sends the project's too. A local server reads yours live, so --mine is
@@ -183,14 +200,21 @@ mockpit icons remove <set> [<set>...]
183
200
  html surfaces write <i icon="lucide:check"></i>; the server inlines the svg.
184
201
  lucide and mage are bundled. add finds @iconify-json/<set> in node_modules,
185
202
  the global npm root or bun's cache, else fetches it from jsDelivr.`,
186
- publish: `mockpit publish --mock <slug> --html <file> [options]
187
- mockpit publish --mock <slug> --surfaces <json|file> [options]
188
- mockpit publish --mock <slug> --parts <name=file|-> ... [options]
189
- mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
190
- Creates the mock, state or variant, or the next version of an existing one.
191
- --state <label> state in the user's words (omit for a single-state mock)
192
- --situation <s> another render of that state ("Couldn't save", "Loading"),
193
- after the state itself is published
203
+ publish: `mockpit publish --design <slug> --html <file> [options]
204
+ mockpit publish --design <slug> --surfaces <json|file> [options]
205
+ mockpit publish --design <slug> --parts <name=file|-> ... [options]
206
+ mockpit publish --design <slug> --fills <part#fill=file|-> ... [options]
207
+ mockpit publish --design <slug> [--state s] [--variant v] --withdraw
208
+ Creates the design, state or variant, or the next version of an existing one.
209
+ One request gets one version: fixes to it before the user acts use --amend.
210
+ --amend replace the variant's latest version in place (same number)
211
+ instead of adding one. Only from the session that published
212
+ it, and only while the user hasn't acted on it (no Send,
213
+ comment, draft answer, not in use)
214
+ --withdraw take the variant's latest version back under the same
215
+ rule: the version before it is current again, and a
216
+ variant with no other version goes
217
+ --state <label> state in the user's words (omit for a single-state design)
194
218
  --variant <name> variant label (default "default")
195
219
  --surfaces <json|file> the full ordered list: {"id"} alone keeps a surface,
196
220
  a missing id removes it, order is the list order
@@ -199,10 +223,10 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
199
223
  --fills <part#fill=file|-> replace the element carrying data-part="part"
200
224
  data-fill="fill" in the latest version, or add it after
201
225
  that part's other fills; the marks are added if missing
202
- --title <t> mock title
203
- --kind <k> page|component|sketch: page for a screen of the product,
204
- component for a piece pages embed (embedding makes one);
205
- omit for sketches, diagrams, reviews (default sketch)
226
+ --title <t> design title
227
+ --kind <k> screen|component|idea: screen for a screen of the product,
228
+ component for a reusable design screens embed (embedding makes one);
229
+ omit for ideas, diagrams, reviews (default idea)
206
230
  --area <name> where in the product it belongs, in the project's words
207
231
  ("Shell", "Food"); Home groups each tab by it. "" clears
208
232
  --source <path> the code path that implements it (apps/web/src/shell). "" clears
@@ -212,17 +236,18 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
212
236
  its latest); alone it copies v, with --parts/--fills it
213
237
  edits the copy
214
238
  --prompt <text> what prompted this version
215
- --note <text> one line on what this variant is, shown beside it (never
239
+ --description <text> one line on what this variant is, shown beside it (never
216
240
  inside the design)
217
- --widths <list> device widths the mock is judged at, side by side, e.g.
241
+ --widths <list> device widths the design is judged at, side by side, e.g.
218
242
  390,1280 (the ones the user ships to)
219
243
  --flow <name> a flow this state is a step of: the states the user moves
220
- between, in order, across mocks; without html it only
244
+ between, in order, across designs; without html it only
221
245
  places the existing state
222
246
  --step <N> its 1-based place in the flow (moves it if it is there);
223
247
  0 takes it out; omitted, it joins at the end
224
248
  --project <name> project (default: git remote, else directory name)
225
- --poster <file> a still of the page (png, jpg) that Studio and Home show
249
+ --poster <file> a still of the page (png, jpg) that Studio and Home show; without
250
+ one, a local Chrome or Chromium takes it just after the publish
226
251
  instead of running it; with --parts or --fills it is the
227
252
  still of the spliced page. A version that changes the page
228
253
  without one drops the old still
@@ -230,59 +255,94 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
230
255
  --json / --quiet
231
256
  Prints the parts found per state, parts that vanished or were renamed,
232
257
  nudges, a ready-to-run "mockpit ask" when variants have no question, and feedback.`,
233
- ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
234
- [--state s] [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
235
- mockpit ask --mock <slug> --asks <json|file>
258
+ ask: `mockpit ask --design <slug> "<question>" --option <label[=variant]> ... [--scope design|state|part]
259
+ [--state s] [--states s]... [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
260
+ mockpit ask --design <slug> --asks <json|file>
236
261
  Ask the user. A choice is several variants plus one question that binds them.
237
262
  Then tell the user where to look and end your turn.`,
238
- read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
239
- No slug: one line per mock (states, variants, open questions), one per flow, plus pending, what
240
- the user is doing now. A slug: the mock's states, variants, questions with
241
- answers, parts, knobs and settled values, plus its pending, as JSON.
263
+ read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree] [--decisions]
264
+ Read this first. Both forms carry the project's decision list, what the user
265
+ already settled, from their replies: treat each line as settled unless they say
266
+ otherwise. No slug: one line per design (states, variants, open questions), one per
267
+ flow, plus pending, what the user is doing now, then the decisions. A slug: the
268
+ design's states, variants, questions with answers, parts, knobs and settled values,
269
+ plus its pending and decisionList (its own lines marked here), as JSON.
242
270
  --body includes surfaces, --history version rows. --tree (no slug): the
243
- project as a tree, each mock's variants with status and forkedFrom, and the
244
- mocks it embeds and is embedded in, as JSON. Never consumes feedback.`,
271
+ project as a tree, each design's variants with status and forkedFrom, and the
272
+ designs it embeds and is embedded in, as JSON. --decisions: the whole list, when
273
+ a read says lines were left out. Never consumes feedback.`,
245
274
  feedback: `mockpit feedback [--project <name>] [--session <id>]
246
- Returns at once with {feedback, pending} as JSON: one batch per mock the user
247
- sent since you last heard ({mock, reply: {asks, borrowed, tuned, comments,
248
- verdict}, messages, inUse, archived}, or {mock, closed: true, dismissed} when the
249
- user closed it unanswered), delivered once across feedback, watch and
250
- every write; and pending ({mock, viewerOpen, draft}) so you can tell the
275
+ Returns at once with {feedback, pending} as JSON: one batch per design the user
276
+ sent since you last heard ({design, reply: {asks, borrowed, tuned, comments,
277
+ verdict}, messages, inUse, archived}), delivered once across feedback, watch and
278
+ every write; and pending ({design, viewerOpen, draft}) so you can tell the
251
279
  user "take your time" or "2 of 3 answered". Run it when the user says they
252
280
  answered.`,
253
- say: `mockpit say "<text>" --mock <slug> [--state s] [--variant v]
254
- A plain-text message in the mock's thread, as your session's agent.`,
255
- edit: `mockpit edit --mock <slug> [--slug <new>] [--title <t>] [--kind page|component|sketch]
281
+ say: `mockpit say "<text>" --design <slug> [--state s] [--variant v]
282
+ A plain-text message in the design's thread, as your session's agent.`,
283
+ move: `mockpit move --design <slug> [--state s] --variant <v> [--variant <v>...]
284
+ --to <design> [--to-state <s>] | --to-state <s> | --archive | --restore
285
+ mockpit move --design <slug> [--state s] [--variant <v>...] --hide-older | --delete-older
286
+ Tidy up. --to or --to-state moves them to another design or tab (a new state is
287
+ created) and takes them out of here; --archive takes them out of sight; --restore
288
+ brings them back. Their group goes along; one in use stays and is reported. The
289
+ thread says what happened, so the user can undo it.
290
+ Only when the user asks: --hide-older hides all but each variant's newest visible
291
+ version (no --variant: every variant), and --delete-older deletes them for good,
292
+ skipping and listing any that a reply, comment, question, embed, copy or fork still
293
+ points at, or that is in use. It can't be undone: confirm first.`,
294
+ edit: `mockpit edit --design <slug> [--slug <new>] [--title <t>] [--kind screen|component|idea]
295
+ [--first-state <name>] [--notes <text>|""]
256
296
  [--area <name>|""] [--source <path>|""] [--project <name>]
257
297
  mockpit edit --project <name> --rename <new name>
258
298
  mockpit edit --project <name> --area-order "Shell,Food,Chat"
259
- Change a mock without publishing a new version; "" clears area or source. A new
299
+ Change a design without publishing a new version; "" clears area or source. A new
260
300
  slug rewrites every embed, data-goto and flow step that names the old one, in
261
- every version. A mock still embedded somewhere stays a component, not a sketch.
262
- --rename moves the project's mocks, sessions, flows and design settings to the
263
- new name (refused if that project has mocks); pass --project <new> or set
301
+ every version. A design still embedded somewhere stays a component, not an idea.
302
+ --notes drafts the design's short plain-text spec, which read returns; the user's own
303
+ edit of it stands, so the draft is then not applied and the result says so.
304
+ --first-state names a design's one unnamed state, and every variant it holds goes
305
+ there, so the design can then have more states.
306
+ --rename moves the project's designs, sessions, flows, system and settings to the
307
+ new name (refused if that project has designs); pass --project <new> or set
264
308
  MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
265
309
  --area-order lists the project's areas in the order Home shows them; areas it
266
310
  leaves out follow alphabetically, "" goes back to alphabetical.`,
311
+ system: `mockpit system [--project <name>] [--json]
312
+ mockpit system [--sheet <file.css>|""] [--classes <json>] [--fonts <json>] [--tokens <json>]
313
+ [--scales <json>] [--roles <json>] [--palettes <json>] [--pairs <json>] [--as <json>]
314
+ [--knobs <json>] [--remove <json>]
315
+ mockpit system --restore <rev>
316
+ The project's system: a shared stylesheet, typed tokens, type and space scales, colour
317
+ palettes and project knobs that every render of the project wears. With no flags it
318
+ prints the system, its checks and its latest revisions (--json: all of it). Each flag
319
+ replaces that part and leaves the rest; --tokens, --scales and --palettes upsert by
320
+ name or id, --remove '{"tokens":["x"]}' takes them out. A JSON flag may name a file.
321
+ Every change is a new revision the user can restore. The repo's own tokens (from
322
+ mockpit init) can't be written here: change the code and rerun init.
323
+ mockpit guide --topic system has the contract.`,
267
324
  run: `mockpit run <file|-> [--session <id>] [--project <name>] [--json]
268
325
  Run a script (the body of an async function, plain JavaScript) in the server's
269
326
  sandbox against the mockpit API; see mockpit guide --topic scripts. Prints each
270
327
  host call in order, the script's prints, its value or error, and any user
271
328
  feedback the run received. --json prints the whole result. Exits 2 when the
272
329
  script failed.`,
273
- watch: `mockpit watch [--session <id>] [--after <seq>]
274
- Stream user feedback, one line per Send, for a background monitor (in
275
- Claude Code, arm it under Monitor after asking, then end the turn).
276
- Delivery is shared with feedback and every write: each Send arrives once.`,
277
- export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
278
- Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
330
+ watch: `mockpit watch [--session <id>] [--after <seq>] [--once] [--exit-code <n>]
331
+ Print one line per Send, for a background monitor (in Claude Code, arm it
332
+ under Monitor after asking, then end the turn; with the mockpit plugin, don't,
333
+ since it wakes you itself). A line says a reply landed;
334
+ feedback or the next write delivers it, once.
335
+ --once stops after the first Send, for a harness whose wake is a process
336
+ exiting; --exit-code sets the code it stops with (default 0).`,
337
+ export: `mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
338
+ Write index.html + history.json per state to .mockpit/accepted/<design>/<state>/;
279
339
  with fills, index.html is the chosen state, plus all-fills.html and decisions.json.`,
280
340
  upload: `mockpit upload <file> [--kind image|file] [--session <id>]
281
341
  Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
282
342
  guide: `mockpit guide [--topic <id>] [--project <name>]
283
343
  No flag prints the brief: the loop, parts, questions and knobs, feedback, and
284
- this project's palette, kit and icons. --topic prints one reference topic
285
- (knobs, asks, surfaces, html, feedback, http, scripts).`,
344
+ this project's system, kit and icons. --topic prints one reference topic
345
+ (system, knobs, asks, surfaces, html, feedback, http, scripts).`,
286
346
  };
287
347
 
288
348
  // `console.log(...)` on a pipe is asynchronous, so exiting on the next line
@@ -314,7 +374,7 @@ function die(what, fix) {
314
374
  }
315
375
 
316
376
  // `report` decides how a failure reads: plumbing verbs keep the one-line
317
- // `mockpit: …` (exit 1); the mock verbs pass `die` so the error / fix / exit 2
377
+ // `mockpit: …` (exit 1); the design verbs pass `die` so the error / fix / exit 2
318
378
  // format is what an agent parses.
319
379
  async function api(path, init = {}, { report, fix } = {}) {
320
380
  const bail = (what, hint) => (report === "die" ? die(what, hint ?? fix) : fail(what));
@@ -343,7 +403,11 @@ async function api(path, init = {}, { report, fix } = {}) {
343
403
  (body.error ?? `${res.status} ${res.statusText}`) +
344
404
  (Array.isArray(choices) ? ` (${choices.join(", ")})` : "");
345
405
  const extra = issue?.message ?? issue?.code;
346
- bail(extra && !main.includes(extra) ? `${main}: ${extra}` : main);
406
+ // The server's own fix, when it names one, beats the caller's generic hint.
407
+ bail(
408
+ extra && !main.includes(extra) ? `${main}: ${extra}` : main,
409
+ typeof body.fix === "string" ? body.fix : undefined,
410
+ );
347
411
  }
348
412
  return body;
349
413
  }
@@ -437,8 +501,11 @@ function agentName(flags) {
437
501
  // doesn't keep; Claude Code's session id does, so the server can hand back the
438
502
  // same mockpit session (and its feedback cursor).
439
503
  function harnessKey() {
440
- const id = process.env.CLAUDE_CODE_SESSION_ID;
441
- return id ? `claude-code:${id}` : undefined;
504
+ if (process.env.MOCKPIT_SESSION_KEY) return process.env.MOCKPIT_SESSION_KEY;
505
+ if (process.env.CLAUDE_CODE_SESSION_ID)
506
+ return `claude-code:${process.env.CLAUDE_CODE_SESSION_ID}`;
507
+ if (process.env.CODEX_THREAD_ID) return `codex:${process.env.CODEX_THREAD_ID}`;
508
+ return undefined;
442
509
  }
443
510
 
444
511
  async function resolveSession(flags, { create = false } = {}) {
@@ -680,20 +747,55 @@ async function installIconSets(project, prefixes) {
680
747
  errors.push({ prefix, message: err instanceof Error ? err.message : String(err) });
681
748
  }
682
749
  }
683
- if (added.length === 0) return { added: [], errors, design: null };
684
- const stored = (await api(`${projectPath(project)}/design`)) ?? {};
750
+ if (added.length === 0) return { added: [], errors, system: null };
751
+ const stored = (await api(`${projectPath(project)}/system`)) ?? {};
685
752
  const keep = (stored.iconSets ?? []).filter((s) => !added.some((a) => a.prefix === s.prefix));
686
- const design = await api(`${projectPath(project)}/design`, {
753
+ await api(`${projectPath(project)}/system`, {
687
754
  method: "PUT",
688
- body: JSON.stringify({ ...stored, iconSets: [...keep, ...added] }),
755
+ body: JSON.stringify({ iconSets: [...keep, ...added] }),
689
756
  });
757
+ const system = await api(`${projectPath(project)}/system`);
690
758
  return {
691
- added: design.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
759
+ added: system.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
692
760
  errors,
693
- design,
761
+ system,
694
762
  };
695
763
  }
696
764
 
765
+ // Every page of the project looks different after a system change, so its stills are
766
+ // taken again in the background; until then the overviews run the pages live.
767
+ function retakeStills(project, session) {
768
+ queueRetakes({ base: BASE, token: TOKEN, client: HEADERS["x-mockpit-client"], session, project });
769
+ }
770
+
771
+ // The system at a glance; `--json` prints all of it.
772
+ function printSystem(s) {
773
+ const palettes = s.color?.palettes ?? [];
774
+ const lines = [
775
+ `${s.project} · r${s.rev}${s.kit && s.kit !== "none" ? ` · kit ${s.kit}` : ""}`,
776
+ s.sheet
777
+ ? `sheet ${(s.sheet.css.length / 1000).toFixed(1)} KB · ${s.sheet.classes.length} classes`
778
+ : null,
779
+ s.fonts?.length ? `fonts ${s.fonts.map((f) => f.family).join(", ")}` : null,
780
+ s.scales?.length
781
+ ? `scales ${s.scales.map((x) => `${x.id} (${x.steps.join(" ")})`).join(", ")}`
782
+ : null,
783
+ s.tokens?.length
784
+ ? `tokens ${s.tokens.filter((t) => !t.scale).length} (+${s.tokens.filter((t) => t.scale).length} from scales)`
785
+ : null,
786
+ s.color?.roles?.length ? `roles ${s.color.roles.map((r) => r.name).join(", ")}` : null,
787
+ palettes.length
788
+ ? `palettes ${palettes.map((p) => `${p.label} (${p.status})`).join(", ")}`
789
+ : null,
790
+ Object.keys(s.knobs ?? {}).length ? `knobs ${Object.keys(s.knobs).join(", ")}` : null,
791
+ ].filter(Boolean);
792
+ for (const l of lines) console.log(l);
793
+ for (const c of s.checks ?? []) console.log(`check: ${c}`);
794
+ for (const r of (s.revisions ?? []).slice(0, 5))
795
+ console.log(`r${r.rev} ${r.at.slice(0, 16)} ${r.summary}`);
796
+ if (s.url) console.log(s.url);
797
+ }
798
+
697
799
  // `--doc` is a file when one exists at that path, else the cheat sheet itself.
698
800
  function readKitDoc(doc) {
699
801
  if (doc === undefined) return undefined;
@@ -706,9 +808,9 @@ function readKitDoc(doc) {
706
808
  // The server checks the URLs against the CDN allowlist and the doc's length;
707
809
  // its one-line error is the message.
708
810
  function putProjectKit(project, id, { url, script, doc }) {
709
- return api(`${projectPath(project)}/kits/${encodeURIComponent(id)}`, {
811
+ return api(`${projectPath(project)}/system`, {
710
812
  method: "PUT",
711
- body: JSON.stringify({ href: url, script, doc: readKitDoc(doc) }),
813
+ body: JSON.stringify({ projectKits: [{ id, href: url, script, doc: readKitDoc(doc) }] }),
712
814
  });
713
815
  }
714
816
 
@@ -834,7 +936,7 @@ function slugify(text) {
834
936
 
835
937
  const projectPath = (project) => `/api/projects/${encodeURIComponent(project)}`;
836
938
 
837
- const mockPath = (slug) => `/api/mocks/${encodeURIComponent(slug)}`;
939
+ const designPath = (slug) => `/api/designs/${encodeURIComponent(slug)}`;
838
940
 
839
941
  const query = (params) => {
840
942
  const q = new URLSearchParams();
@@ -845,18 +947,42 @@ const query = (params) => {
845
947
  return s ? `?${s}` : "";
846
948
  };
847
949
 
848
- // The flags every mock verb shares: which mock, which state, which variant.
849
- const MOCK_FLAGS = {
850
- mock: { type: "string" },
950
+ // The flags every design verb shares: which design, which state, which variant.
951
+ const DESIGN_FLAGS = {
952
+ design: { type: "string" },
851
953
  state: { type: "string" },
852
954
  variant: { type: "string" },
853
955
  project: { type: "string" },
854
956
  session: { type: "string" },
855
957
  };
856
958
 
857
- function requireMock(flags, example) {
858
- const slug = flags.mock ? slugify(flags.mock) : "";
859
- if (!slug) die("--mock needs a slug", example);
959
+ // `mockpit export` with no design: the project's system as files code can import.
960
+ async function exportSystem(flags) {
961
+ const project = resolveProject(flags).name;
962
+ const data = await api(
963
+ `/api/projects/${encodeURIComponent(project)}/system/export`,
964
+ {},
965
+ { report: "die", fix: "mockpit system" },
966
+ );
967
+ if (flags.json) return out(data);
968
+ const dir = flags.out ? flags.out : join(process.cwd(), ".mockpit", "system");
969
+ mkdirSync(dir, { recursive: true });
970
+ const files = [];
971
+ const write = (name, text) => {
972
+ if (!text) return;
973
+ writeFileSync(join(dir, name), text);
974
+ files.push(name);
975
+ };
976
+ write("sheet.css", data.sheet);
977
+ write("tokens.css", data.css);
978
+ const { sheet: _s, css: _c, ...rest } = data;
979
+ write("system.json", JSON.stringify(rest, null, 2) + "\n");
980
+ if (!flags.quiet) console.log(`${project} system r${data.rev} → ${dir} (${files.join(", ")})`);
981
+ }
982
+
983
+ function requireDesign(flags, example) {
984
+ const slug = flags.design ? slugify(flags.design) : "";
985
+ if (!slug) die("--design needs a slug", example);
860
986
  return slug;
861
987
  }
862
988
 
@@ -878,7 +1004,7 @@ const shq = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${String(s).replace(/'/g,
878
1004
  // The server's suggested ask as the command that sends it: an agent that was
879
1005
  // nudged can run it as is. --asks carries ids and scope, which flags can't.
880
1006
  function suggestedAskLine(slug, ask) {
881
- return `mockpit ask --mock ${shq(slug)} --asks ${shq(JSON.stringify([ask]))}`;
1007
+ return `mockpit ask --design ${shq(slug)} --asks ${shq(JSON.stringify([ask]))}`;
882
1008
  }
883
1009
 
884
1010
  // One line per write: what was published, its URL, the parts found per state,
@@ -886,10 +1012,22 @@ function suggestedAskLine(slug, ask) {
886
1012
  function printPublished(result, flags) {
887
1013
  if (flags.json) return out(result);
888
1014
  if (flags.quiet) return;
889
- const { mock, post } = result;
1015
+ const { design, post } = result;
1016
+ if (result.planned) {
1017
+ console.log(`${design.slug}: planned, nothing drawn yet · ${result.url}`);
1018
+ if (result.flow) console.log(flowLine(result.flow));
1019
+ for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1020
+ printFeedback(result.feedback);
1021
+ return;
1022
+ }
890
1023
  const branch = post.from !== undefined && post.from !== post.version - 1;
1024
+ const how = result.withdrawn
1025
+ ? ` (v${result.withdrawn.version} withdrawn)`
1026
+ : flags.amend
1027
+ ? " (amended)"
1028
+ : "";
891
1029
  console.log(
892
- `${mock.slug}/${stateLabel(post.state)}${post.variant} v${post.version}${branch && post.from ? ` (from v${post.from})` : ""} · ${result.url}`,
1030
+ `${design.slug}/${stateLabel(post.state)}${post.variant} v${post.version}${branch && post.from ? ` (from v${post.from})` : ""}${how} · ${result.url}`,
893
1031
  );
894
1032
  for (const { state, parts } of result.parts ?? []) {
895
1033
  if (parts.length === 0) continue;
@@ -904,13 +1042,41 @@ function printPublished(result, flags) {
904
1042
  if (changes?.vanished?.length) console.log(`vanished: ${changes.vanished.join(", ")}`);
905
1043
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
906
1044
  if (result.suggestedAsk)
907
- console.log(`suggested: ${suggestedAskLine(mock.slug, result.suggestedAsk)}`);
1045
+ console.log(`suggested: ${suggestedAskLine(design.slug, result.suggestedAsk)}`);
908
1046
  for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
909
1047
  printFeedback(result.feedback);
910
1048
  }
911
1049
 
1050
+ async function withdrawVersion(flags, slug, project) {
1051
+ const session = await resolveSession(flags);
1052
+ if (!session) die(`no session has published ${slug} from here`, `mockpit read ${slug}`);
1053
+ const result = await api(
1054
+ "/api/designs",
1055
+ {
1056
+ method: "POST",
1057
+ body: JSON.stringify({
1058
+ session,
1059
+ project,
1060
+ design: slug,
1061
+ state: flags.state,
1062
+ variant: flags.variant,
1063
+ withdraw: true,
1064
+ }),
1065
+ },
1066
+ { report: "die", fix: `mockpit read ${slug} --history` },
1067
+ );
1068
+ if (!result.removed) return printPublished(result, flags);
1069
+ if (flags.json) return out(result);
1070
+ if (flags.quiet) return;
1071
+ const w = result.withdrawn;
1072
+ console.log(
1073
+ `${slug}/${stateLabel(w.state)}${w.variant} v${w.version} withdrawn, variant removed · ${result.url}`,
1074
+ );
1075
+ printFeedback(result.feedback);
1076
+ }
1077
+
912
1078
  function flowLine(flow) {
913
- const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
1079
+ const steps = flow.steps.map((x) => (x.state === null ? x.design : `${x.design}/${x.state}`));
914
1080
  return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
915
1081
  }
916
1082
 
@@ -920,15 +1086,15 @@ function printFeedback(feedback) {
920
1086
  if (feedback?.length) console.log(`feedback: ${JSON.stringify(feedback)}`);
921
1087
  }
922
1088
 
923
- // What the user is doing right now, per mock.
1089
+ // What the user is doing right now, per design.
924
1090
  function pendingLines(pending) {
925
1091
  return (pending ?? []).flatMap((p) =>
926
1092
  p.draft
927
1093
  ? [
928
- `pending: ${p.mock} — the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comment${p.draft.comments === 1 ? "" : "s"})`,
1094
+ `pending: ${p.design} — the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comment${p.draft.comments === 1 ? "" : "s"})`,
929
1095
  ]
930
1096
  : p.viewerOpen
931
- ? [`pending: ${p.mock} — open in the viewer`]
1097
+ ? [`pending: ${p.design} — open in the viewer`]
932
1098
  : [],
933
1099
  );
934
1100
  }
@@ -938,8 +1104,7 @@ function parsePublishFlags() {
938
1104
  tokens: true,
939
1105
  allowPositionals: true,
940
1106
  options: {
941
- ...MOCK_FLAGS,
942
- situation: { type: "string" },
1107
+ ...DESIGN_FLAGS,
943
1108
  title: { type: "string" },
944
1109
  kind: { type: "string" },
945
1110
  area: { type: "string" },
@@ -953,7 +1118,7 @@ function parsePublishFlags() {
953
1118
  from: { type: "string" },
954
1119
  fork: { type: "string" },
955
1120
  prompt: { type: "string" },
956
- note: { type: "string" },
1121
+ description: { type: "string" },
957
1122
  widths: { type: "string" },
958
1123
  flow: { type: "string" },
959
1124
  step: { type: "string" },
@@ -969,6 +1134,8 @@ function parsePublishFlags() {
969
1134
  "session-title": { type: "string" },
970
1135
  agent: { type: "string" },
971
1136
  "new-session": { type: "boolean" },
1137
+ amend: { type: "boolean" },
1138
+ withdraw: { type: "boolean" },
972
1139
  },
973
1140
  });
974
1141
  }
@@ -980,8 +1147,8 @@ function readPartFlags(values, slug, flag = "parts") {
980
1147
  if (!values?.length) return undefined;
981
1148
  const example =
982
1149
  flag === "parts"
983
- ? `mockpit publish --mock ${slug} --parts body=body.html`
984
- : `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
1150
+ ? `mockpit publish --design ${slug} --parts body=body.html`
1151
+ : `mockpit publish --design ${slug} --fills entry#skeleton=skeleton.html`;
985
1152
  const parts = {};
986
1153
  let stdin = false;
987
1154
  for (const raw of values) {
@@ -1003,14 +1170,15 @@ function readPartFlags(values, slug, flag = "parts") {
1003
1170
  return parts;
1004
1171
  }
1005
1172
 
1006
- async function publishMock() {
1173
+ async function publishDesign() {
1007
1174
  const { values: flags, positionals, tokens } = parsePublishFlags();
1008
- const slug = requireMock(flags, "mockpit publish --mock writer --html writer.html");
1175
+ const slug = requireDesign(flags, "mockpit publish --design writer --html writer.html");
1009
1176
  const project = resolveProject(flags).name;
1010
1177
  const from = flags.from === undefined ? undefined : Number(flags.from);
1011
1178
  if (from !== undefined && !Number.isInteger(from)) {
1012
1179
  die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
1013
1180
  }
1181
+ if (flags.withdraw) return withdrawVersion(flags, slug, project);
1014
1182
  const step = flags.step === undefined ? undefined : Number(flags.step);
1015
1183
  if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
1016
1184
  die(
@@ -1059,16 +1227,16 @@ async function publishMock() {
1059
1227
  }
1060
1228
  surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
1061
1229
  }
1062
- if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
1063
- die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
1230
+ const declaring = flags.kind === "screen" || flags.kind === "component";
1231
+ if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined && !declaring)
1232
+ die(`no html for ${slug}`, `mockpit publish --design ${slug} --html <file>`);
1064
1233
  const poster =
1065
1234
  flags.poster === undefined ? undefined : (await uploadFile(flags.poster, { session })).id;
1066
1235
  const body = {
1067
1236
  session,
1068
1237
  project,
1069
- mock: slug,
1238
+ design: slug,
1070
1239
  state: flags.state,
1071
- situation: flags.situation,
1072
1240
  variant: flags.variant,
1073
1241
  title: flags.title,
1074
1242
  kind: flags.kind,
@@ -1078,7 +1246,7 @@ async function publishMock() {
1078
1246
  ...(from !== undefined && { from }),
1079
1247
  ...(flags.fork !== undefined && { fork: flags.fork }),
1080
1248
  prompt: flags.prompt,
1081
- note: flags.note,
1249
+ description: flags.description,
1082
1250
  ...(flags.widths !== undefined && {
1083
1251
  widths: flags.widths
1084
1252
  .split(",")
@@ -1091,13 +1259,26 @@ async function publishMock() {
1091
1259
  ...(parts && { parts }),
1092
1260
  ...(fills && { fills }),
1093
1261
  ...(poster && { poster }),
1262
+ ...(!poster && findBrowser() && { posterComing: true }),
1263
+ ...(flags.amend && { amend: true }),
1094
1264
  };
1095
1265
  const result = await api(
1096
- "/api/mocks",
1266
+ "/api/designs",
1097
1267
  { method: "POST", body: JSON.stringify(body) },
1098
1268
  { report: "die", fix: `mockpit read ${slug}` },
1099
1269
  );
1100
1270
  printPublished(result, flags);
1271
+ if (!poster && result.post?.surfaces?.some((s) => s.kind === "html") && !result.withdrawn) {
1272
+ queuePoster({
1273
+ base: BASE,
1274
+ token: TOKEN,
1275
+ client: HEADERS["x-mockpit-client"],
1276
+ session,
1277
+ design: result.design.id,
1278
+ postId: result.post.id,
1279
+ version: result.post.version,
1280
+ });
1281
+ }
1101
1282
  return result;
1102
1283
  }
1103
1284
 
@@ -1105,7 +1286,14 @@ async function publishMock() {
1105
1286
  // "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
1106
1287
  // "Above=knob:trim.position=top" sets one knob. The value is JSON when it
1107
1288
  // parses (17, true), else a string, so slider and toggle values keep their type.
1289
+ // "Grape=palette:grape" chooses a palette of the project's system.
1108
1290
  function parseOption(raw) {
1291
+ const palette = raw.indexOf("=palette:");
1292
+ if (palette > 0) {
1293
+ const id = raw.slice(palette + "=palette:".length);
1294
+ if (!id) die(`option "${raw}" needs palette:<id>`, "--option Grape=palette:grape");
1295
+ return { label: raw.slice(0, palette), palette: id };
1296
+ }
1109
1297
  const knob = raw.indexOf("=knob:");
1110
1298
  if (knob > 0) {
1111
1299
  const pair = raw.slice(knob + "=knob:".length);
@@ -1148,21 +1336,22 @@ function printRun(r) {
1148
1336
 
1149
1337
  function watchLines(batch) {
1150
1338
  const lines = [];
1151
- const name = batch.mock ?? "a mock";
1339
+ const name = batch.design ?? "a design";
1152
1340
  if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
1153
- if (batch.closed) {
1154
- const n = batch.dismissed?.length ?? 0;
1155
- lines.push(
1156
- `mockpit closed ${name}: ${n ? `${n} question${n === 1 ? "" : "s"} dismissed unanswered (${batch.dismissed.map((d) => d.text || d.ask).join("; ")}); don't ask again` : "closed by the user"}`,
1157
- );
1158
- }
1159
1341
  if (batch.edited) {
1160
1342
  const e = batch.edited;
1161
1343
  const bits = [
1162
1344
  e.kind ? `now a ${e.kind}` : "",
1163
1345
  e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
1164
1346
  ].filter(Boolean);
1165
- lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
1347
+ if (bits.length) lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
1348
+ if (e.notes !== undefined) {
1349
+ lines.push(
1350
+ e.notes === null
1351
+ ? `mockpit edited ${name}: the user cleared its notes`
1352
+ : `mockpit edited ${name}: the user edited its notes: ${e.notes}`,
1353
+ );
1354
+ }
1166
1355
  }
1167
1356
  if (batch.reply) {
1168
1357
  const r = batch.reply;
@@ -1171,7 +1360,7 @@ function watchLines(batch) {
1171
1360
  ? r.asks
1172
1361
  .map(
1173
1362
  (a) =>
1174
- `${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.note ? ` (“${a.note}”)` : ""}`,
1363
+ `${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.comment ? ` (“${a.comment}”)` : ""}`,
1175
1364
  )
1176
1365
  .join("; ")
1177
1366
  : "",
@@ -1207,7 +1396,7 @@ function watchLines(batch) {
1207
1396
  return lines;
1208
1397
  }
1209
1398
 
1210
- // GET where "not there yet" is an answer, not an exit — a project or mock that
1399
+ // GET where "not there yet" is an answer, not an exit — a project or design that
1211
1400
  // doesn't exist is the normal state before the first publish.
1212
1401
  async function apiSoft(path) {
1213
1402
  let res;
@@ -1426,7 +1615,7 @@ const commands = {
1426
1615
  child.on("exit", (code) => process.exit(code ?? 0));
1427
1616
  },
1428
1617
 
1429
- // Detect the repo's design system once, store it on the project, and leave a
1618
+ // Read the repo's tokens and kit once into the project's system, and leave a
1430
1619
  // starter file behind — so every later publish is markup only, with no CSS or
1431
1620
  // icon paths pasted into the agent's context.
1432
1621
  async init() {
@@ -1443,39 +1632,47 @@ const commands = {
1443
1632
  const say = (label, text) => {
1444
1633
  if (!flags.quiet && !flags.json) console.log(`${label.padEnd(8)} ${text}`);
1445
1634
  };
1446
- const { detectDesign, detectIconSets, renderStarter } = await import("./initDesign.js");
1635
+ const { detectDesignSystem, detectIconSets, renderStarter } = await import("./initDesign.js");
1447
1636
  say("project:", `${project} (from ${source})`);
1448
1637
 
1449
- const design = await detectDesign(process.cwd());
1450
- const d = design.detected;
1638
+ const designSystem = await detectDesignSystem(process.cwd());
1639
+ const d = designSystem.detected;
1451
1640
  // `detected` is always an object, so emptiness has to be read off the
1452
- // fields — otherwise a repo with no design system printed a blank line.
1453
- const stripped = design.strippedImports?.length
1454
- ? ` (stripped: ${design.strippedImports.join(", ")})`
1641
+ // fields — otherwise a repo with nothing to read printed a blank line.
1642
+ const stripped = designSystem.strippedImports?.length
1643
+ ? ` (stripped: ${designSystem.strippedImports.join(", ")})`
1455
1644
  : "";
1456
1645
  const found = [
1457
1646
  d?.tailwind &&
1458
- (design.tailwindSource ? `tailwind: ${design.tailwindSource}${stripped}` : "tailwind"),
1647
+ (designSystem.tailwindSource
1648
+ ? `tailwind: ${designSystem.tailwindSource}${stripped}`
1649
+ : "tailwind"),
1459
1650
  d?.shadcn && "shadcn",
1460
- d?.cssVars ? `${d.cssVars} css vars${design.source ? ` from ${design.source}` : ""}` : null,
1651
+ d?.cssVars
1652
+ ? `${d.cssVars} css vars${designSystem.source ? ` from ${designSystem.source}` : ""}`
1653
+ : null,
1461
1654
  d?.fonts?.length ? `fonts ${d.fonts.join(", ")}` : null,
1462
1655
  ].filter(Boolean);
1463
1656
  say(
1464
1657
  "design:",
1465
1658
  found.length ? found.join(" · ") : "nothing detected — using the built-in palette",
1466
1659
  );
1467
- let kit = flags.kit ?? design.kit ?? "builtin";
1468
- // The project kit is stored first so the design PUT can name it as default.
1660
+ let kit = flags.kit ?? designSystem.kit ?? "builtin";
1661
+ // A project kit goes in the same write, so the system can name it as default.
1662
+ let projectKits;
1469
1663
  if (flags["kit-url"] !== undefined) {
1470
1664
  kit = flags.kit ?? "project";
1471
- await putProjectKit(project, kit, {
1472
- url: flags["kit-url"],
1473
- script: flags["kit-script"],
1474
- doc: flags["kit-doc"],
1475
- });
1665
+ projectKits = [
1666
+ {
1667
+ id: kit,
1668
+ href: flags["kit-url"],
1669
+ script: flags["kit-script"],
1670
+ doc: readKitDoc(flags["kit-doc"]),
1671
+ },
1672
+ ];
1476
1673
  }
1477
1674
  say("kit:", kit);
1478
- const files = design.designFiles;
1675
+ const files = designSystem.designFiles;
1479
1676
  if (files) {
1480
1677
  const sh = files.shadcn;
1481
1678
  say(
@@ -1490,18 +1687,45 @@ const commands = {
1490
1687
  );
1491
1688
  }
1492
1689
 
1493
- let stored = await api(`${projectPath(project)}/design`, {
1690
+ const wrote = await api(`${projectPath(project)}/system`, {
1494
1691
  method: "PUT",
1495
1692
  body: JSON.stringify({
1496
- detected: design.detected ?? null,
1497
- palette: design.palette ?? null,
1498
- cssVars: design.cssVars ?? "",
1499
- tailwindCss: design.tailwindCss ?? "",
1500
- strippedImports: design.strippedImports ?? [],
1693
+ repo: {
1694
+ detected: designSystem.detected ?? null,
1695
+ cssVars: designSystem.cssVars ?? "",
1696
+ tailwindCss: designSystem.tailwindCss ?? "",
1697
+ strippedImports: designSystem.strippedImports ?? [],
1698
+ designFiles: designSystem.designFiles ?? null,
1699
+ },
1501
1700
  kit,
1502
- designFiles: design.designFiles ?? null,
1701
+ ...(projectKits ? { projectKits } : {}),
1503
1702
  }),
1504
1703
  });
1704
+ if (wrote?.changed?.length) {
1705
+ const session = await resolveSession(flags);
1706
+ if (session) retakeStills(project, session);
1707
+ }
1708
+ const derived = wrote?.derived;
1709
+ if (derived) {
1710
+ say(
1711
+ "system:",
1712
+ [
1713
+ `r${wrote.rev}`,
1714
+ derived.roles
1715
+ ? `${derived.roles} colour role${derived.roles === 1 ? "" : "s"} (palette repo)`
1716
+ : null,
1717
+ derived.tokens
1718
+ ? `${derived.tokens} token${derived.tokens === 1 ? "" : "s"}, repo-owned`
1719
+ : null,
1720
+ derived.untyped?.length
1721
+ ? `${derived.untyped.length} untyped, from the raw block only`
1722
+ : null,
1723
+ ]
1724
+ .filter(Boolean)
1725
+ .join(" · "),
1726
+ );
1727
+ }
1728
+ let stored = await api(`${projectPath(project)}/system`);
1505
1729
 
1506
1730
  // Sets the repo already uses are added the same way `icons add` does; one
1507
1731
  // that cannot be fetched (offline) is reported, never fatal to init.
@@ -1522,7 +1746,7 @@ const commands = {
1522
1746
  if (added) say("icons:", `${f.prefix} (${added.count} icons, from ${f.from})`);
1523
1747
  else say("icons:", `${f.prefix} not added: ${error?.message ?? "unknown error"}`);
1524
1748
  }
1525
- if (result.design) stored = result.design;
1749
+ if (result.system) stored = result.system;
1526
1750
  }
1527
1751
  const iconSets = [
1528
1752
  ...new Set([...(stored?.iconSets ?? []).map((s) => s.prefix), ...BUNDLED_ICON_SETS]),
@@ -1531,7 +1755,7 @@ const commands = {
1531
1755
 
1532
1756
  const starter = join(process.cwd(), ".mockpit", "starter.html");
1533
1757
  mkdirSync(dirname(starter), { recursive: true });
1534
- writeFileSync(starter, renderStarter({ ...design, kit }, iconSets));
1758
+ writeFileSync(starter, renderStarter({ ...designSystem, kit }, iconSets));
1535
1759
  say("wrote:", ".mockpit/starter.html");
1536
1760
  if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/, keeping .mockpit/rules.md)");
1537
1761
  const rulesFile = join(repoRoot(), ".mockpit", "rules.md");
@@ -1539,24 +1763,25 @@ const commands = {
1539
1763
  say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
1540
1764
  }
1541
1765
  say("next:", "mockpit guide");
1542
- if (flags.json) out({ project, design: stored, starter });
1766
+ if (flags.json) out({ project, system: stored, derived: derived ?? null, starter });
1543
1767
  },
1544
1768
 
1545
1769
  async publish() {
1546
- await publishMock();
1770
+ await publishDesign();
1547
1771
  },
1548
1772
 
1549
1773
  async ask() {
1550
1774
  const { values: flags, positionals } = parse({
1551
1775
  allowPositionals: true,
1552
1776
  options: {
1553
- mock: { type: "string" },
1777
+ design: { type: "string" },
1554
1778
  project: { type: "string" },
1555
1779
  session: { type: "string" },
1556
1780
  option: { type: "string", multiple: true },
1557
1781
  recommend: { type: "string" },
1558
1782
  scope: { type: "string" },
1559
1783
  state: { type: "string" },
1784
+ states: { type: "string", multiple: true },
1560
1785
  part: { type: "string" },
1561
1786
  multi: { type: "boolean" },
1562
1787
  id: { type: "string" },
@@ -1564,16 +1789,16 @@ const commands = {
1564
1789
  asks: { type: "string" },
1565
1790
  },
1566
1791
  });
1567
- const slug = requireMock(
1792
+ const slug = requireDesign(
1568
1793
  flags,
1569
- 'mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet',
1794
+ 'mockpit ask --design writer "Quiet or dark?" --option Quiet=quiet',
1570
1795
  );
1571
1796
  let asks;
1572
1797
  if (flags.asks !== undefined) {
1573
1798
  if (positionals.length > 0) {
1574
1799
  die(
1575
1800
  "use the question or --asks, not both",
1576
- `mockpit ask --mock ${slug} --asks <json|file>`,
1801
+ `mockpit ask --design ${slug} --asks <json|file>`,
1577
1802
  );
1578
1803
  }
1579
1804
  asks = readJsonFlag("asks", flags.asks);
@@ -1583,13 +1808,13 @@ const commands = {
1583
1808
  if (!text)
1584
1809
  die(
1585
1810
  "ask needs a question",
1586
- `mockpit ask --mock ${slug} "Quiet or dark?" --option Quiet=quiet`,
1811
+ `mockpit ask --design ${slug} "Quiet or dark?" --option Quiet=quiet`,
1587
1812
  );
1588
1813
  const options = (flags.option ?? []).map(parseOption);
1589
1814
  if (options.length === 0) {
1590
1815
  die(
1591
1816
  "ask needs options",
1592
- `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
1817
+ `mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
1593
1818
  );
1594
1819
  }
1595
1820
  if (flags.recommend !== undefined) {
@@ -1599,7 +1824,7 @@ const commands = {
1599
1824
  if (!pick)
1600
1825
  die(
1601
1826
  `--recommend "${flags.recommend}" names no option`,
1602
- `mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
1827
+ `mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
1603
1828
  );
1604
1829
  pick.recommended = true;
1605
1830
  }
@@ -1610,6 +1835,7 @@ const commands = {
1610
1835
  ...(flags.id && { id: flags.id }),
1611
1836
  ...(flags.scope && { scope: flags.scope }),
1612
1837
  ...(flags.state && { state: flags.state }),
1838
+ ...(flags.states && { states: flags.states }),
1613
1839
  ...(flags.part && { part: flags.part }),
1614
1840
  ...(flags.multi && { multi: true }),
1615
1841
  ...(flags.after && { after: flags.after }),
@@ -1618,7 +1844,7 @@ const commands = {
1618
1844
  }
1619
1845
  const session = await resolveSession(flags);
1620
1846
  const result = await api(
1621
- `${mockPath(slug)}/asks`,
1847
+ `${designPath(slug)}/asks`,
1622
1848
  {
1623
1849
  method: "POST",
1624
1850
  body: JSON.stringify({
@@ -1633,7 +1859,7 @@ const commands = {
1633
1859
  if (flags.quiet) return;
1634
1860
  for (const ask of result.asks) {
1635
1861
  console.log(
1636
- `asked on ${result.mock}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
1862
+ `asked on ${result.design}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
1637
1863
  );
1638
1864
  }
1639
1865
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
@@ -1651,10 +1877,20 @@ const commands = {
1651
1877
  body: { type: "boolean" },
1652
1878
  history: { type: "boolean" },
1653
1879
  tree: { type: "boolean" },
1880
+ decisions: { type: "boolean" },
1654
1881
  },
1655
1882
  });
1656
1883
  const project = resolveProject(flags).name;
1657
- if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
1884
+ if (positionals.length > 1) die("read takes one design slug", "mockpit read writer");
1885
+ if (flags.decisions) {
1886
+ return out(
1887
+ await api(
1888
+ `/api/projects/${encodeURIComponent(project)}/decisions`,
1889
+ {},
1890
+ { report: "die", fix: `mockpit read --project ${project}` },
1891
+ ),
1892
+ );
1893
+ }
1658
1894
  if (flags.tree) {
1659
1895
  return out(
1660
1896
  await api(
@@ -1668,49 +1904,59 @@ const commands = {
1668
1904
  const slug = slugify(positionals[0]);
1669
1905
  return out(
1670
1906
  await api(
1671
- `${mockPath(slug)}${query({ project, body: flags.body, history: flags.history })}`,
1907
+ `${designPath(slug)}${query({ project, body: flags.body, history: flags.history })}`,
1672
1908
  {},
1673
1909
  { report: "die", fix: `mockpit read --project ${project}` },
1674
1910
  ),
1675
1911
  );
1676
1912
  }
1677
- const list = (await apiSoft(`/api/mocks${query({ project })}`)) ?? {
1678
- mocks: [],
1913
+ const list = (await apiSoft(`/api/designs${query({ project })}`)) ?? {
1914
+ designs: [],
1679
1915
  open: 0,
1680
1916
  pending: [],
1681
1917
  };
1682
1918
  if (flags.json) return out(list);
1683
1919
  if (flags.quiet) return;
1684
- const mocks = list.mocks ?? [];
1920
+ const designs = list.designs ?? [];
1685
1921
  console.log(
1686
- `${project} · ${mocks.length} mock${mocks.length === 1 ? "" : "s"}` +
1922
+ `${project} · ${designs.length} design${designs.length === 1 ? "" : "s"}` +
1687
1923
  (list.open
1688
1924
  ? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
1689
1925
  : " · nothing waiting on the user"),
1690
1926
  );
1691
- for (const m of mocks) {
1927
+ if (list.system) console.log(` system ${list.system}`);
1928
+ for (const m of designs) {
1692
1929
  const states = m.states.length ? m.states.join(" / ") : "single state";
1693
1930
  console.log(
1694
- ` ${m.slug} · ${m.kind}${m.area ? ` · ${m.area}` : ""} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.closedAt ? " · closed" : m.open ? ` · ${m.open} open` : ""}`,
1931
+ ` ${m.slug} · ${m.kind}${m.area ? ` · ${m.area}` : ""} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.planned ? " · planned" : ""}${m.archivedAt ? " · archived" : m.open ? ` · ${m.open} open` : ""}`,
1695
1932
  );
1696
1933
  }
1697
1934
  for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
1698
1935
  for (const line of pendingLines(list.pending)) console.log(line);
1936
+ if (list.decisionList) {
1937
+ console.log(
1938
+ `Decided, ${list.decisionList.total}. Treat as settled unless the user says otherwise:`,
1939
+ );
1940
+ for (const d of list.decisionList.lines)
1941
+ console.log(` ${d.system ? "project" : d.design}: ${d.text}`);
1942
+ if (list.decisionList.more) console.log(` ${list.decisionList.more} (--decisions)`);
1943
+ }
1699
1944
  },
1700
1945
  async export() {
1701
1946
  const { values: flags } = parse({
1702
1947
  options: {
1703
- mock: { type: "string" },
1948
+ design: { type: "string" },
1704
1949
  state: { type: "string" },
1705
1950
  variant: { type: "string" },
1706
1951
  project: { type: "string" },
1707
1952
  out: { type: "string" },
1708
1953
  },
1709
1954
  });
1710
- const slug = requireMock(flags, "mockpit export --mock writer");
1955
+ if (!flags.design) return exportSystem(flags);
1956
+ const slug = requireDesign(flags, "mockpit export --design writer");
1711
1957
  const project = resolveProject(flags).name;
1712
1958
  const data = await api(
1713
- `${mockPath(slug)}/export${query({ project, state: flags.state, variant: flags.variant })}`,
1959
+ `${designPath(slug)}/export${query({ project, state: flags.state, variant: flags.variant })}`,
1714
1960
  {},
1715
1961
  { report: "die", fix: `mockpit read ${slug}` },
1716
1962
  );
@@ -1734,7 +1980,7 @@ const commands = {
1734
1980
  JSON.stringify(
1735
1981
  {
1736
1982
  project,
1737
- mock: slug,
1983
+ design: slug,
1738
1984
  state: entry.state,
1739
1985
  variant: entry.variant,
1740
1986
  version: entry.version,
@@ -1802,24 +2048,44 @@ const commands = {
1802
2048
  },
1803
2049
  async watch() {
1804
2050
  const { values: flags } = parse({
1805
- options: { session: { type: "string" }, after: { type: "string" } },
2051
+ options: {
2052
+ session: { type: "string" },
2053
+ after: { type: "string" },
2054
+ once: { type: "boolean" },
2055
+ "exit-code": { type: "string" },
2056
+ },
1806
2057
  });
1807
2058
  if (flags.after !== undefined && !/^\d+$/.test(flags.after)) {
1808
2059
  fail(`--after must be a number (got "${flags.after}")`);
1809
2060
  }
2061
+ const exitCode = flags["exit-code"] ?? "0";
2062
+ if (!/^\d+$/.test(exitCode)) fail(`--exit-code must be a number (got "${exitCode}")`);
2063
+ // A waiter outliving its agent would take a Send and tell no one, and each
2064
+ // Send is delivered only once.
2065
+ if (flags.once) {
2066
+ const parent = process.ppid;
2067
+ setInterval(() => {
2068
+ if (process.ppid !== parent || process.ppid === 1) process.exit(0);
2069
+ }, 5000).unref();
2070
+ }
1810
2071
  // A continuous long-poll that streams each piece of feedback as one line —
1811
2072
  // one line is one monitor notification. It re-arms forever; a transient
1812
2073
  // network error backs off and retries rather than exiting.
1813
2074
  //
1814
- // After the first poll it carries no client cursor: an author=user read
1815
- // resumes from the session's server-side cursor and advances it, so
1816
- // feedback is delivered exactly once across watch, feedback, and piggyback.
2075
+ // It only says a reply landed (peek): the reply stays on the session's cursor
2076
+ // until the agent's feedback or next write takes it, so a harness dropping or
2077
+ // cutting this output can't lose it, and each Send is still delivered once.
1817
2078
  let firstAfter = flags.after;
1818
2079
  for (;;) {
1819
2080
  const key = harnessKey();
2081
+ // The state file is per process and cwd, which several threads of one harness
2082
+ // can share, so a conversation's own key decides whenever there is one.
1820
2083
  const session =
1821
- (await resolveSession(flags)) ??
1822
- (key ? await resolveSessionByKey(key) : await resolveSessionByCwd());
2084
+ flags.session ??
2085
+ process.env.MOCKPIT_SESSION ??
2086
+ (key
2087
+ ? await resolveSessionByKey(key)
2088
+ : ((await resolveSession(flags)) ?? (await resolveSessionByCwd())));
1823
2089
  if (!session) {
1824
2090
  // A conversation that hasn't used mockpit yet may never; ask rarely.
1825
2091
  await sleep(key ? 10_000 : 2000);
@@ -1828,7 +2094,7 @@ const commands = {
1828
2094
  let result;
1829
2095
  try {
1830
2096
  const res = await fetch(
1831
- `${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60 })}`,
2097
+ `${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60, peek: 1 })}`,
1832
2098
  { headers: HEADERS },
1833
2099
  );
1834
2100
  if (!res.ok) {
@@ -1841,20 +2107,26 @@ const commands = {
1841
2107
  continue;
1842
2108
  }
1843
2109
  firstAfter = undefined;
1844
- for (const batch of result.feedback ?? []) {
1845
- for (const line of watchLines(batch)) console.log(line);
2110
+ const lines = (result.feedback ?? []).flatMap(watchLines);
2111
+ for (const line of lines) console.log(line);
2112
+ if (flags.once && lines.length) {
2113
+ // A harness may frame this output as a hook failure, so it says what it is.
2114
+ console.log(
2115
+ "This is the user's reply from the mockpit viewer, not an error. Call mockpit's feedback for all of it.",
2116
+ );
2117
+ process.exit(Number(exitCode));
1846
2118
  }
1847
2119
  }
1848
2120
  },
1849
2121
 
1850
2122
  async say() {
1851
- const { values: flags, positionals } = parse({ allowPositionals: true, options: MOCK_FLAGS });
2123
+ const { values: flags, positionals } = parse({ allowPositionals: true, options: DESIGN_FLAGS });
1852
2124
  const text = positionals.join(" ").trim();
1853
- if (!text) die("say needs text", 'mockpit say "…" --mock writer');
1854
- const slug = requireMock(flags, `mockpit say "${text}" --mock writer`);
2125
+ if (!text) die("say needs text", 'mockpit say "…" --design writer');
2126
+ const slug = requireDesign(flags, `mockpit say "${text}" --design writer`);
1855
2127
  const session = await resolveSession(flags);
1856
2128
  const result = await api(
1857
- `${mockPath(slug)}/say`,
2129
+ `${designPath(slug)}/say`,
1858
2130
  {
1859
2131
  method: "POST",
1860
2132
  body: JSON.stringify({
@@ -1872,10 +2144,69 @@ const commands = {
1872
2144
  console.log(`said on ${slug}`);
1873
2145
  printFeedback(result.feedback);
1874
2146
  },
2147
+ async move() {
2148
+ const { values: flags } = parse({
2149
+ options: {
2150
+ ...DESIGN_FLAGS,
2151
+ variant: { type: "string", multiple: true },
2152
+ to: { type: "string" },
2153
+ "to-state": { type: "string" },
2154
+ archive: { type: "boolean" },
2155
+ restore: { type: "boolean" },
2156
+ "hide-older": { type: "boolean" },
2157
+ "delete-older": { type: "boolean" },
2158
+ },
2159
+ });
2160
+ const example = "mockpit move --design writer --variant quiet --archive";
2161
+ const slug = requireDesign(flags, example);
2162
+ const tidying = flags["hide-older"] || flags["delete-older"];
2163
+ if (!flags.variant?.length && !tidying) die("move needs --variant", example);
2164
+ const session = await resolveSession(flags);
2165
+ const result = await api(
2166
+ `${designPath(slug)}/move`,
2167
+ {
2168
+ method: "POST",
2169
+ body: JSON.stringify({
2170
+ state: flags.state,
2171
+ variants: flags.variant,
2172
+ to: flags.to,
2173
+ toState: flags["to-state"],
2174
+ archive: flags.archive,
2175
+ restore: flags.restore,
2176
+ hideOlder: flags["hide-older"],
2177
+ deleteOlder: flags["delete-older"],
2178
+ project: resolveProject(flags).name,
2179
+ session: session ?? undefined,
2180
+ }),
2181
+ },
2182
+ { report: "die", fix: `mockpit read ${slug}` },
2183
+ );
2184
+ if (flags.json) return out(result);
2185
+ if (flags.quiet) return;
2186
+ if (tidying) {
2187
+ const verb = flags["delete-older"] ? "deleted" : "hid";
2188
+ console.log(`${verb} ${result.versions.length} older versions`);
2189
+ for (const s of result.skipped) {
2190
+ const which = s.version ? ` v${s.version}` : "";
2191
+ console.log(`stayed: ${s.state ? `${s.state}/` : ""}${s.variant}${which} (${s.why})`);
2192
+ }
2193
+ printFeedback(result.feedback);
2194
+ return;
2195
+ }
2196
+ const names = result.variants.map((v) => v.variant).join(", ") || "nothing";
2197
+ const done = result.to
2198
+ ? `moved to ${result.to.design}`
2199
+ : flags.restore
2200
+ ? "restored"
2201
+ : "archived";
2202
+ console.log(`${done}: ${names}`);
2203
+ for (const s of result.skipped) console.log(`stayed: ${s.variant} (${s.why})`);
2204
+ printFeedback(result.feedback);
2205
+ },
1875
2206
  async edit() {
1876
2207
  const { values: flags } = parse({
1877
2208
  options: {
1878
- mock: { type: "string" },
2209
+ design: { type: "string" },
1879
2210
  project: { type: "string" },
1880
2211
  session: { type: "string" },
1881
2212
  slug: { type: "string" },
@@ -1883,6 +2214,8 @@ const commands = {
1883
2214
  kind: { type: "string" },
1884
2215
  area: { type: "string" },
1885
2216
  source: { type: "string" },
2217
+ "first-state": { type: "string" },
2218
+ notes: { type: "string" },
1886
2219
  rename: { type: "string" },
1887
2220
  "area-order": { type: "string" },
1888
2221
  },
@@ -1891,9 +2224,9 @@ const commands = {
1891
2224
  const session = (await resolveSession(flags)) ?? undefined;
1892
2225
  const areaOrder = flags["area-order"];
1893
2226
  if (flags.rename !== undefined || areaOrder !== undefined) {
1894
- if (flags.mock) {
2227
+ if (flags.design) {
1895
2228
  die(
1896
- "--rename and --area-order edit the project; edit the mock without them",
2229
+ "--rename and --area-order edit the project; edit the design without them",
1897
2230
  "mockpit edit --help",
1898
2231
  );
1899
2232
  }
@@ -1922,7 +2255,7 @@ const commands = {
1922
2255
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
1923
2256
  if (!result.renamed) return printFeedback(result.feedback);
1924
2257
  console.log(
1925
- `renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.mocks} mock${result.mocks === 1 ? "" : "s"} · ${result.url}`,
2258
+ `renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.designs} design${result.designs === 1 ? "" : "s"} · ${result.url}`,
1926
2259
  );
1927
2260
  if (!flags.project && !process.env.MOCKPIT_PROJECT) {
1928
2261
  console.log(
@@ -1932,40 +2265,46 @@ const commands = {
1932
2265
  printFeedback(result.feedback);
1933
2266
  return;
1934
2267
  }
1935
- const slug = requireMock(flags, "mockpit edit --mock writer --area Shell");
1936
- const fields = ["slug", "title", "kind", "area", "source"].filter(
2268
+ const slug = requireDesign(flags, "mockpit edit --design writer --area Shell");
2269
+ const fields = ["slug", "title", "kind", "area", "source", "notes"].filter(
1937
2270
  (k) => flags[k] !== undefined,
1938
2271
  );
1939
- if (!fields.length) {
1940
- die("edit needs --slug, --title, --kind, --area or --source", "mockpit edit --help");
2272
+ if (!fields.length && flags["first-state"] === undefined) {
2273
+ die(
2274
+ "edit needs --slug, --title, --kind, --area, --source, --notes or --first-state",
2275
+ "mockpit edit --help",
2276
+ );
1941
2277
  }
1942
2278
  const result = await api(
1943
- mockPath(slug),
2279
+ designPath(slug),
1944
2280
  {
1945
2281
  method: "PATCH",
1946
2282
  body: JSON.stringify({
1947
2283
  project,
1948
2284
  session,
1949
2285
  ...Object.fromEntries(fields.map((k) => [k, flags[k]])),
2286
+ firstState: flags["first-state"],
1950
2287
  }),
1951
2288
  },
1952
2289
  { report: "die", fix: `mockpit read ${slug}` },
1953
2290
  );
1954
2291
  if (flags.json) return out(result);
1955
2292
  if (flags.quiet) return;
1956
- const m = result.mock;
2293
+ const m = result.design;
1957
2294
  console.log(
1958
2295
  [
1959
2296
  result.renamed ? `${result.renamed.from} → ${m.slug}` : m.slug,
1960
2297
  m.kind,
1961
2298
  m.area && `area ${m.area}`,
1962
2299
  m.source && `source ${m.source}`,
2300
+ result.notes && (result.notes.applied ? "notes saved" : "notes NOT applied"),
1963
2301
  result.url,
1964
2302
  ]
1965
2303
  .filter(Boolean)
1966
2304
  .join(" · "),
1967
2305
  );
1968
2306
  for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
2307
+ if (result.notes && !result.notes.applied) console.log(`notes: ${result.notes.text}`);
1969
2308
  printFeedback(result.feedback);
1970
2309
  },
1971
2310
  async demo() {
@@ -1979,13 +2318,13 @@ const commands = {
1979
2318
  });
1980
2319
  for (const state of DEMO.states) {
1981
2320
  for (const variant of DEMO.variants) {
1982
- await api("/api/mocks", {
2321
+ await api("/api/designs", {
1983
2322
  method: "POST",
1984
2323
  body: JSON.stringify({
1985
2324
  session: session.id,
1986
2325
  project: DEMO.project,
1987
- mock: DEMO.slug,
1988
- kind: "page",
2326
+ design: DEMO.slug,
2327
+ kind: "screen",
1989
2328
  title: DEMO.title,
1990
2329
  state: state.label,
1991
2330
  variant: variant.name,
@@ -1995,7 +2334,7 @@ const commands = {
1995
2334
  });
1996
2335
  }
1997
2336
  }
1998
- await api(`${mockPath(DEMO.slug)}/asks`, {
2337
+ await api(`${designPath(DEMO.slug)}/asks`, {
1999
2338
  method: "POST",
2000
2339
  body: JSON.stringify({ project: DEMO.project, session: session.id, asks: DEMO.asks }),
2001
2340
  });
@@ -2048,14 +2387,14 @@ const commands = {
2048
2387
  if (sub === "add") {
2049
2388
  const result = await installIconSets(project, sets);
2050
2389
  if (result.errors.length) fail(result.errors.map((e) => e.message).join("; "));
2051
- if (flags.json) return out(result.design);
2390
+ if (flags.json) return out(result.system);
2052
2391
  for (const a of result.added) {
2053
2392
  if (!flags.quiet) console.log(`added ${a.prefix} (${a.count} icons)`);
2054
2393
  }
2055
2394
  return;
2056
2395
  }
2057
2396
  if (sub === "remove") {
2058
- const stored = await api(`${projectPath(project)}/design`);
2397
+ const stored = await api(`${projectPath(project)}/system`);
2059
2398
  const installed = stored?.iconSets ?? [];
2060
2399
  for (const set of sets) {
2061
2400
  if (installed.some((s) => s.prefix === set)) continue;
@@ -2065,14 +2404,11 @@ const commands = {
2065
2404
  : `${set} is not installed for ${project}`,
2066
2405
  );
2067
2406
  }
2068
- const design = await api(`${projectPath(project)}/design`, {
2407
+ const result = await api(`${projectPath(project)}/system`, {
2069
2408
  method: "PUT",
2070
- body: JSON.stringify({
2071
- ...stored,
2072
- iconSets: installed.filter((s) => !sets.includes(s.prefix)),
2073
- }),
2409
+ body: JSON.stringify({ iconSets: installed.filter((s) => !sets.includes(s.prefix)) }),
2074
2410
  });
2075
- if (flags.json) return out(design);
2411
+ if (flags.json) return out(result);
2076
2412
  if (!flags.quiet) console.log(`removed ${sets.join(", ")}`);
2077
2413
  return;
2078
2414
  }
@@ -2099,21 +2435,93 @@ const commands = {
2099
2435
  const { name: project } = resolveProject(flags);
2100
2436
  if ((sub !== "add" && sub !== "remove") || !id)
2101
2437
  fail("usage: mockpit kit add <id> --url <url> --doc <file|text> | mockpit kit remove <id>");
2102
- const design =
2438
+ const result =
2103
2439
  sub === "add"
2104
2440
  ? await putProjectKit(project, id, flags)
2105
- : await api(`${projectPath(project)}/kits/${encodeURIComponent(id)}`, {
2106
- method: "DELETE",
2441
+ : await api(`${projectPath(project)}/system`, {
2442
+ method: "PUT",
2443
+ body: JSON.stringify({ removeKits: [id] }),
2107
2444
  });
2108
- if (flags.json) return out(design);
2445
+ if (flags.json) return out(result);
2109
2446
  if (!flags.quiet) console.log(`${sub === "add" ? "added" : "removed"} kit ${id} (${project})`);
2110
2447
  },
2111
2448
 
2449
+ // The project's system: no flags reads it, any flag writes that part.
2450
+ async system() {
2451
+ const JSON_FLAGS = [
2452
+ "classes",
2453
+ "fonts",
2454
+ "tokens",
2455
+ "scales",
2456
+ "roles",
2457
+ "palettes",
2458
+ "pairs",
2459
+ "as",
2460
+ "knobs",
2461
+ "remove",
2462
+ ];
2463
+ const { values: flags } = parse({
2464
+ options: {
2465
+ project: { type: "string" },
2466
+ session: { type: "string" },
2467
+ sheet: { type: "string" },
2468
+ restore: { type: "string" },
2469
+ ...Object.fromEntries(JSON_FLAGS.map((f) => [f, { type: "string" }])),
2470
+ },
2471
+ });
2472
+ const project = resolveProject(flags).name;
2473
+ const body = {};
2474
+ if (flags.sheet !== undefined) {
2475
+ // CSS text has a block; anything else is a path, which must be a file.
2476
+ const sheet = flags.sheet;
2477
+ if (sheet === "") body.sheet = null;
2478
+ else if (sheet.includes("{")) body.sheet = sheet;
2479
+ else if (!existsSync(sheet))
2480
+ die(`--sheet: no file at ${sheet}`, "mockpit system --sheet sheet.css");
2481
+ else if (!statSync(sheet).isFile())
2482
+ die(`--sheet: ${sheet} is not a file`, "mockpit system --sheet sheet.css");
2483
+ else body.sheet = readFileSync(sheet, "utf8");
2484
+ }
2485
+ for (const f of JSON_FLAGS) {
2486
+ if (flags[f] === undefined) continue;
2487
+ const raw = existsSync(flags[f]) ? readFileSync(flags[f], "utf8") : flags[f];
2488
+ try {
2489
+ body[f] = JSON.parse(raw);
2490
+ } catch {
2491
+ die(`--${f} must be JSON (or a JSON file)`, `mockpit guide --topic system`);
2492
+ }
2493
+ }
2494
+ if (flags.restore !== undefined) body.restore = Number(flags.restore);
2495
+ if (Object.keys(body).length === 0) {
2496
+ const system = await api(`${projectPath(project)}/system`, {}, { report: "die" });
2497
+ if (flags.json || !process.stdout.isTTY) return out(system);
2498
+ printSystem(system);
2499
+ return;
2500
+ }
2501
+ const session = (await resolveSession(flags, { create: true })) ?? undefined;
2502
+ const result = await api(
2503
+ `${projectPath(project)}/system`,
2504
+ { method: "PUT", body: JSON.stringify({ ...body, session }) },
2505
+ { report: "die", fix: "mockpit guide --topic system" },
2506
+ );
2507
+ if (result.changed?.length) retakeStills(project, session);
2508
+ if (flags.json) return out(result);
2509
+ if (flags.quiet) return;
2510
+ console.log(
2511
+ result.changed?.length
2512
+ ? `r${result.rev}: ${result.changed.join("; ")}`
2513
+ : `r${result.rev}: nothing changed`,
2514
+ );
2515
+ for (const c of result.checks ?? []) console.log(`check: ${c}`);
2516
+ console.log(result.url);
2517
+ printFeedback(result.feedback);
2518
+ },
2519
+
2112
2520
  async guide() {
2113
2521
  const { values: flags } = parse({
2114
2522
  options: { topic: { type: "string" }, project: { type: "string" } },
2115
2523
  });
2116
- if (flags.topic !== undefined) return printTopic(flags.topic);
2524
+ if (flags.topic !== undefined) return printTopic(flags.topic, flags);
2117
2525
  return printBrief(flags);
2118
2526
  },
2119
2527
 
@@ -2180,7 +2588,7 @@ const commands = {
2180
2588
  // no server running.
2181
2589
  const TOPICS_DIR = join(ROOT, "guide", "topics");
2182
2590
 
2183
- async function printTopic(topic) {
2591
+ async function printTopic(topic, flags = {}) {
2184
2592
  const topics = readdirSync(TOPICS_DIR)
2185
2593
  .filter((f) => f.endsWith(".md"))
2186
2594
  .map((f) => f.slice(0, -3))
@@ -2191,11 +2599,13 @@ async function printTopic(topic) {
2191
2599
  `mockpit guide --topic ${topics[0]}`,
2192
2600
  );
2193
2601
  }
2194
- const path = `/agent-howto?${new URLSearchParams({ topic })}`;
2602
+ const path = `/agent-howto?${new URLSearchParams(
2603
+ topic === "system" ? { topic, project: resolveProject(flags).name } : { topic },
2604
+ )}`;
2195
2605
  console.log(await fetchTextWithFallback(path, join(TOPICS_DIR, `${topic}.md`)));
2196
2606
  }
2197
2607
 
2198
- // The brief is rendered from the project's stored design settings, so the
2608
+ // The brief is rendered from the project's stored system, so the
2199
2609
  // server's copy is the real one. With no server it still has to print: an
2200
2610
  // agent reads it before anything else, so render the generic brief locally
2201
2611
  // (the same code the server runs) and say where the project-aware one comes from.
@@ -2208,7 +2618,7 @@ async function printBrief(flags) {
2208
2618
  const { renderBriefGuide } = await import(entrypoint("server", "designGuide.ts"));
2209
2619
  console.log(renderBriefGuide(null));
2210
2620
  console.error(
2211
- `note: no mockpit at ${BASE}; this is the generic brief. Run mockpit serve, then mockpit init for this project's palette, kit and icons.`,
2621
+ `note: no mockpit at ${BASE}; this is the generic brief. Run mockpit serve, then mockpit init for this project's system, kit and icons.`,
2212
2622
  );
2213
2623
  }
2214
2624