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.
- package/README.md +59 -223
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +17 -22
- package/bin/mockpit.js +626 -216
- package/bin/poster.js +434 -0
- package/dist/mcp/server.js +127 -29
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +627 -385
- package/dist/server/apiViews.js +198 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +3514 -984
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +640 -0
- package/dist/server/codeWorker.js +2 -2
- package/dist/server/contrast.d.ts +4 -0
- package/dist/server/contrast.js +157 -0
- package/dist/server/decisionList.d.ts +48 -0
- package/dist/server/decisionList.js +340 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +14 -7
- package/dist/server/designGuide.js +314 -88
- package/dist/server/embeds.d.ts +3 -1
- package/dist/server/embeds.js +31 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +15 -6
- package/dist/server/feedbackBatch.d.ts +39 -13
- package/dist/server/feedbackBatch.js +129 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +1 -1
- package/dist/server/guidance.js +2 -2
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +39 -10
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/kits.d.ts +2 -2
- package/dist/server/kits.js +54 -1
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +7 -1
- package/dist/server/mcpHttp.js +29 -12
- package/dist/server/mcpSpec.d.ts +27 -27
- package/dist/server/mcpSpec.js +196 -56
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +47 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -21
- package/dist/server/sqlStore.d.ts +148 -103
- package/dist/server/sqlStore.js +1052 -181
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +8 -9
- package/dist/server/surfacePage.js +423 -100
- package/dist/server/system.d.ts +234 -0
- package/dist/server/system.js +1508 -0
- package/dist/server/systemChecks.d.ts +15 -0
- package/dist/server/systemChecks.js +224 -0
- package/dist/server/systemSpecimen.d.ts +2 -0
- package/dist/server/systemSpecimen.js +49 -0
- package/dist/server/themes.d.ts +9 -7
- package/dist/server/themes.js +43 -90
- package/dist/server/types.d.ts +266 -93
- package/dist/server/types.js +76 -79
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +15 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/packs/foundations.md +35 -0
- package/guide/topics/asks.md +108 -63
- package/guide/topics/feedback.md +129 -120
- package/guide/topics/html.md +18 -25
- package/guide/topics/http.md +43 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/guide/topics/system.md +157 -0
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +15 -14
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +33 -26
- 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 ›
|
|
32
|
-
|
|
33
|
-
open"); a variant is a parallel
|
|
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
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
mockpit publish --
|
|
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
|
|
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>
|
|
69
|
-
--kind <k>
|
|
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
|
-
--
|
|
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 --
|
|
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>
|
|
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
|
-
|
|
100
|
-
|
|
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>" --
|
|
104
|
-
a plain-text message in a
|
|
105
|
-
mockpit
|
|
106
|
-
|
|
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
|
|
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/<
|
|
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
|
|
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
|
-
|
|
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
|
|
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 --
|
|
187
|
-
mockpit publish --
|
|
188
|
-
mockpit publish --
|
|
189
|
-
mockpit publish --
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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>
|
|
203
|
-
--kind <k>
|
|
204
|
-
component for a
|
|
205
|
-
omit for
|
|
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
|
-
--
|
|
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
|
|
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
|
|
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 --
|
|
234
|
-
[--state s] [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
|
|
235
|
-
mockpit ask --
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
244
|
-
|
|
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
|
|
247
|
-
sent since you last heard ({
|
|
248
|
-
verdict}, messages, inUse, archived},
|
|
249
|
-
|
|
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>" --
|
|
254
|
-
A plain-text message in the
|
|
255
|
-
|
|
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
|
|
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
|
|
262
|
-
--
|
|
263
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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,
|
|
684
|
-
const stored = (await api(`${projectPath(project)}/
|
|
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
|
-
|
|
753
|
+
await api(`${projectPath(project)}/system`, {
|
|
687
754
|
method: "PUT",
|
|
688
|
-
body: JSON.stringify({
|
|
755
|
+
body: JSON.stringify({ iconSets: [...keep, ...added] }),
|
|
689
756
|
});
|
|
757
|
+
const system = await api(`${projectPath(project)}/system`);
|
|
690
758
|
return {
|
|
691
|
-
added:
|
|
759
|
+
added: system.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
|
|
692
760
|
errors,
|
|
693
|
-
|
|
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)}/
|
|
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
|
|
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
|
|
849
|
-
const
|
|
850
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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 --
|
|
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 {
|
|
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
|
-
`${
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
...
|
|
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
|
-
|
|
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 --
|
|
984
|
-
: `mockpit publish --
|
|
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
|
|
1173
|
+
async function publishDesign() {
|
|
1007
1174
|
const { values: flags, positionals, tokens } = parsePublishFlags();
|
|
1008
|
-
const slug =
|
|
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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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 {
|
|
1635
|
+
const { detectDesignSystem, detectIconSets, renderStarter } = await import("./initDesign.js");
|
|
1447
1636
|
say("project:", `${project} (from ${source})`);
|
|
1448
1637
|
|
|
1449
|
-
const
|
|
1450
|
-
const d =
|
|
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
|
|
1453
|
-
const stripped =
|
|
1454
|
-
? ` (stripped: ${
|
|
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
|
-
(
|
|
1647
|
+
(designSystem.tailwindSource
|
|
1648
|
+
? `tailwind: ${designSystem.tailwindSource}${stripped}`
|
|
1649
|
+
: "tailwind"),
|
|
1459
1650
|
d?.shadcn && "shadcn",
|
|
1460
|
-
d?.cssVars
|
|
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 ??
|
|
1468
|
-
//
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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 =
|
|
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
|
-
|
|
1690
|
+
const wrote = await api(`${projectPath(project)}/system`, {
|
|
1494
1691
|
method: "PUT",
|
|
1495
1692
|
body: JSON.stringify({
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
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.
|
|
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({ ...
|
|
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,
|
|
1766
|
+
if (flags.json) out({ project, system: stored, derived: derived ?? null, starter });
|
|
1543
1767
|
},
|
|
1544
1768
|
|
|
1545
1769
|
async publish() {
|
|
1546
|
-
await
|
|
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
|
-
|
|
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 =
|
|
1792
|
+
const slug = requireDesign(
|
|
1568
1793
|
flags,
|
|
1569
|
-
'mockpit ask --
|
|
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 --
|
|
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 --
|
|
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 --
|
|
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 --
|
|
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
|
-
`${
|
|
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.
|
|
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
|
|
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
|
-
`${
|
|
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/
|
|
1678
|
-
|
|
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
|
|
1920
|
+
const designs = list.designs ?? [];
|
|
1685
1921
|
console.log(
|
|
1686
|
-
`${project} · ${
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`${
|
|
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
|
-
|
|
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: {
|
|
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
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
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
|
-
|
|
1822
|
-
|
|
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
|
-
|
|
1845
|
-
|
|
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:
|
|
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 "…" --
|
|
1854
|
-
const slug =
|
|
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
|
-
`${
|
|
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
|
-
|
|
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.
|
|
2227
|
+
if (flags.design) {
|
|
1895
2228
|
die(
|
|
1896
|
-
"--rename and --area-order edit the project; edit the
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
1988
|
-
kind: "
|
|
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(`${
|
|
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.
|
|
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)}/
|
|
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
|
|
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(
|
|
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
|
|
2438
|
+
const result =
|
|
2103
2439
|
sub === "add"
|
|
2104
2440
|
? await putProjectKit(project, id, flags)
|
|
2105
|
-
: await api(`${projectPath(project)}/
|
|
2106
|
-
method: "
|
|
2441
|
+
: await api(`${projectPath(project)}/system`, {
|
|
2442
|
+
method: "PUT",
|
|
2443
|
+
body: JSON.stringify({ removeKits: [id] }),
|
|
2107
2444
|
});
|
|
2108
|
-
if (flags.json) return out(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|