mockpit 1.3.0 → 3.0.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 +66 -202
- package/bin/demoData.js +5 -5
- package/bin/initDesign.js +6 -6
- package/bin/mockpit.js +687 -192
- package/bin/poster.js +377 -0
- package/dist/mcp/server.js +110 -19
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +563 -41
- package/dist/server/apiViews.js +223 -74
- package/dist/server/app.d.ts +10 -2
- package/dist/server/app.js +3284 -782
- 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 +638 -0
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisionList.d.ts +45 -0
- package/dist/server/decisionList.js +302 -0
- package/dist/server/decisions.d.ts +8 -8
- package/dist/server/decisions.js +18 -17
- package/dist/server/designGuide.d.ts +10 -6
- package/dist/server/designGuide.js +173 -78
- package/dist/server/embeds.d.ts +4 -1
- package/dist/server/embeds.js +57 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +9 -6
- package/dist/server/feedbackBatch.d.ts +38 -16
- package/dist/server/feedbackBatch.js +121 -50
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- 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 +49 -11
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +8 -1
- package/dist/server/mcpHttp.js +44 -7
- package/dist/server/mcpSpec.d.ts +2 -1
- package/dist/server/mcpSpec.js +172 -50
- package/dist/server/parts.js +9 -1
- package/dist/server/postSurfaces.js +3 -0
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +31 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -24
- package/dist/server/sqlStore.d.ts +117 -94
- package/dist/server/sqlStore.js +1089 -141
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +6 -9
- package/dist/server/surfacePage.js +361 -86
- package/dist/server/types.d.ts +252 -69
- package/dist/server/types.js +87 -69
- package/extensions/mockpit.js +245 -65
- package/guide/AGENT_SETUP.md +13 -1
- package/guide/packs/design-craft.md +34 -0
- package/guide/topics/asks.md +117 -69
- package/guide/topics/feedback.md +156 -119
- package/guide/topics/html.md +19 -32
- package/guide/topics/http.md +43 -40
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +24 -35
- package/guide/topics/surfaces.md +50 -41
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +4 -6
- package/skills/launch-video/SKILL.md +12 -12
- package/skills/mockpit/SKILL.md +35 -23
- package/viewer/dist/index.html +9 -8
package/bin/mockpit.js
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
writeSync,
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import { tmpdir, userInfo } from "node:os";
|
|
12
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
|
+
import { findBrowser, queuePoster } from "./poster.js";
|
|
16
17
|
import { serveUrl } from "./serveUrl.js";
|
|
17
18
|
|
|
18
19
|
const BASE = (process.env.MOCKPIT_URL ?? "http://localhost:8228").replace(/\/$/, "");
|
|
@@ -28,79 +29,103 @@ const HEADERS = {
|
|
|
28
29
|
|
|
29
30
|
const HELP = `mockpit — a live visual surface for terminal coding agents
|
|
30
31
|
|
|
31
|
-
vocabulary: project ›
|
|
32
|
-
|
|
33
|
-
open"); a variant is a parallel
|
|
34
|
-
|
|
32
|
+
vocabulary: project › design › state › variant › version. A project is a repo; a
|
|
33
|
+
design is an idea, screen or component, by slug; a state is one thing the user sees in it ("Lab
|
|
34
|
+
open"); a variant is a parallel take on a state; a version is its history.
|
|
35
|
+
Declare the parts you want feedback on with data-part="name" in the html.
|
|
35
36
|
|
|
36
37
|
the loop (never blocks on the user):
|
|
37
38
|
1. mockpit init once per repo; read the brief (mockpit guide).
|
|
38
|
-
2. mockpit publish once per variant;
|
|
39
|
-
3. A choice is several variants plus one
|
|
39
|
+
2. mockpit publish once per variant; declare parts with data-part.
|
|
40
|
+
3. A choice is several variants plus one question that binds them: mockpit ask
|
|
40
41
|
with options bound to variants. A variant decides everything it differs
|
|
41
42
|
on; don't re-ask it. Visual options carry a variant or knob set. One
|
|
42
43
|
render plus a control: --knobs.
|
|
43
44
|
4. Tell the user in one line where to look, then end your turn. The question
|
|
44
|
-
itself lives in the
|
|
45
|
+
itself lives in the design, never in chat. The user answers in the
|
|
45
46
|
browser at their own pace. Never poll.
|
|
46
47
|
5. When the user says they answered (or a mockpit watch line wakes you), run
|
|
47
48
|
mockpit feedback. Every write also returns feedback; read it.
|
|
48
|
-
6. mockpit publish the revision; mockpit export once a variant is
|
|
49
|
+
6. mockpit publish the revision; mockpit export once a variant is in use.
|
|
49
50
|
In Claude Code, arm mockpit watch under Monitor after asking, then end the
|
|
50
|
-
turn
|
|
51
|
+
turn, unless you have the mockpit plugin, which wakes you itself; elsewhere
|
|
52
|
+
the user's next message is the wake-up.
|
|
51
53
|
|
|
52
54
|
verbs:
|
|
53
55
|
mockpit init [--project name] [--kit <id>] [--kit-url <url> --kit-doc <file|text>]
|
|
54
56
|
detect the repo's design system, store
|
|
55
57
|
palette/kit/icon sets, write .mockpit/starter.html
|
|
56
|
-
mockpit publish --
|
|
58
|
+
mockpit publish --design <slug> --html <file> [options]
|
|
57
59
|
create or version one variant of one state
|
|
58
|
-
--state <label> state, in the user's words (omit for a single-state
|
|
60
|
+
--state <label> state, in the user's words (omit for a single-state design)
|
|
59
61
|
--variant <name> variant label (default "default")
|
|
60
62
|
--html <file|-> the html body (or a positional file)
|
|
61
63
|
--surfaces <json|file> instead: the full ordered surface list; {"id"}
|
|
62
64
|
alone keeps a surface, a missing id removes it
|
|
63
65
|
--parts <name=file|-> instead: replace just that data-part's element in
|
|
64
66
|
the latest version (repeatable; name#key targets one instance)
|
|
65
|
-
--fills <part#fill=file|-> instead: replace or add one fill of a
|
|
67
|
+
--fills <part#fill=file|-> instead: replace or add one fill of a part in
|
|
66
68
|
the latest version (repeatable)
|
|
67
|
-
--title <t>
|
|
68
|
-
--kind <k> component
|
|
69
|
+
--title <t> design title
|
|
70
|
+
--kind <k> screen|component, only for an app's screens (default idea);
|
|
71
|
+
with no html it plans one, drawn by a later publish
|
|
72
|
+
--area <name> where in the product it belongs ("Shell"); Home groups by it
|
|
73
|
+
--source <path> the code path that implements it
|
|
69
74
|
--knobs <json|file> knobs in tunekit usePane shape, keyed by path
|
|
70
75
|
--from <N> branch from version N
|
|
76
|
+
--amend replace the latest version in place: a fix before the
|
|
77
|
+
user has acted on it (one request, one version)
|
|
78
|
+
--withdraw take the latest version back, under the same rule
|
|
71
79
|
--prompt <text> what prompted this version
|
|
72
|
-
--
|
|
80
|
+
--description <text> one line on what this variant is, shown beside it
|
|
73
81
|
--widths <list> device widths to show it at, e.g. 390,1280
|
|
74
|
-
--flow <name> [--step <N>] make this
|
|
75
|
-
it out); with no html it only places the
|
|
82
|
+
--flow <name> [--step <N>] make this state step N of a flow (0 takes
|
|
83
|
+
it out); with no html it only places the state
|
|
76
84
|
--project <name> project (default: git remote, else directory name)
|
|
77
85
|
--kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
|
|
86
|
+
--poster <file> a still of the page (png, jpg); Studio and Home show it
|
|
87
|
+
instead of running the page. Send one with every version
|
|
88
|
+
that changes the page, or it goes
|
|
78
89
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
|
|
79
90
|
add a surface of that kind after the html (repeatable;
|
|
80
91
|
--data is a JSON tree)
|
|
81
|
-
mockpit ask --
|
|
92
|
+
mockpit ask --design <slug> "<question>" --option <label[=variant]> ...
|
|
82
93
|
ask the user; bind options to variants
|
|
83
94
|
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
|
|
84
95
|
"=variant@3" to its version 3, "=knob:path=value" sets one knob
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
96
|
+
--recommend <l> the option you would pick, by label; a quiet mark on it
|
|
97
|
+
--scope <s> design|state|part (default design)
|
|
98
|
+
--state <label> the state a state-scoped question is about
|
|
99
|
+
--states <label> a design-wide question: a state its answers can be seen
|
|
100
|
+
on (repeatable, best first)
|
|
101
|
+
--part <name> the part a part-scoped question is about
|
|
88
102
|
--multi allow several answers
|
|
89
|
-
--id <id> stable id; reusing one replaces that
|
|
90
|
-
--asks <json|file> several
|
|
91
|
-
mockpit read [slug] [--body] [--history]
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
--id <id> stable id; reusing one replaces that question
|
|
104
|
+
--asks <json|file> several questions at once, full shape
|
|
105
|
+
mockpit read [slug] [--body] [--history] [--decisions]
|
|
106
|
+
what the user already decided, then
|
|
107
|
+
no slug: every design plus what the user
|
|
108
|
+
is doing now; a slug: that design
|
|
94
109
|
mockpit feedback what the user sent since you last heard,
|
|
95
110
|
plus pending; returns at once
|
|
96
|
-
mockpit say "<text>" --
|
|
97
|
-
a plain-text message in a
|
|
98
|
-
mockpit
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
mockpit say "<text>" --design <slug> [--state s] [--variant v]
|
|
112
|
+
a plain-text message in a design's thread
|
|
113
|
+
mockpit move --design <slug> --variant <v>... (--to <design> [--to-state s] | --archive | --restore)
|
|
114
|
+
move variants elsewhere, archive or restore them
|
|
115
|
+
mockpit edit --design <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
|
|
116
|
+
change a design without a new version ("" clears)
|
|
117
|
+
mockpit edit --project <name> --rename <new>
|
|
118
|
+
rename a project
|
|
119
|
+
mockpit edit --project <name> --area-order "Shell,Food"
|
|
120
|
+
the order Home lists areas in
|
|
121
|
+
mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
|
|
122
|
+
write the html in use + history per state
|
|
123
|
+
to .mockpit/accepted/<design>/<state>/
|
|
101
124
|
mockpit upload <file> [--kind image|file]
|
|
102
125
|
upload an asset, print its id and URL
|
|
103
126
|
mockpit guide [--topic <id>] the project-aware brief, or one reference topic
|
|
127
|
+
mockpit rules [--mine] send .mockpit/rules.md (or, with --mine, your
|
|
128
|
+
~/.mockpit/rules.md) to the server for the brief
|
|
104
129
|
mockpit run <file|-> run a script against the mockpit API on the
|
|
105
130
|
server (mockpit guide --topic scripts)
|
|
106
131
|
mockpit watch [--session <id>] stream user feedback, one line per Send
|
|
@@ -109,7 +134,7 @@ verbs:
|
|
|
109
134
|
other commands:
|
|
110
135
|
mockpit serve [--port N] [--host H] [--open]
|
|
111
136
|
start the server (API + viewer)
|
|
112
|
-
mockpit demo seed the Writer
|
|
137
|
+
mockpit demo seed the Writer design to explore the viewer
|
|
113
138
|
mockpit icons [add|remove <set>...] list the icon sets html surfaces can name
|
|
114
139
|
(icon="prefix:name"); add or remove an
|
|
115
140
|
Iconify set for this project
|
|
@@ -132,6 +157,7 @@ environment:
|
|
|
132
157
|
MOCKPIT_PROJECT project name; overrides the git-remote/directory default
|
|
133
158
|
MOCKPIT_URL server base URL (default http://localhost:8228)
|
|
134
159
|
MOCKPIT_TOKEN bearer token for a deployed instance
|
|
160
|
+
CHROME_PATH the browser for automatic posters; pointing nowhere turns them off
|
|
135
161
|
MOCKPIT_HOST address serve binds to (default: every interface)
|
|
136
162
|
MOCKPIT_LOG serve's event log file, or "off" (default ~/.mockpit/events.jsonl)
|
|
137
163
|
MOCKPIT_SESSION fixed session id (overrides auto-detection)
|
|
@@ -148,6 +174,15 @@ const COMMAND_HELP = {
|
|
|
148
174
|
(gitignored). --kit makes a bundled or project kit the default for every html
|
|
149
175
|
surface. --kit-url defines the project's own kit (id from --kit, else
|
|
150
176
|
"project") and makes it the default; the doc is the cheat sheet the brief prints.`,
|
|
177
|
+
rules: `mockpit rules [--project <name>]
|
|
178
|
+
mockpit rules --mine
|
|
179
|
+
Guidance on top of the core brief, highest first: the project's
|
|
180
|
+
.mockpit/rules.md (committed with the repo), your ~/.mockpit/rules.md, then
|
|
181
|
+
the packs either names (\`packs: [design-craft]\` in its frontmatter). A file
|
|
182
|
+
has "## Rules" (always followed) and "## Suggestions" (aimed for).
|
|
183
|
+
This sends the file as it is on disk; a missing file clears the stored copy.
|
|
184
|
+
init sends the project's too. A local server reads yours live, so --mine is
|
|
185
|
+
for a deployed one.`,
|
|
151
186
|
kit: `mockpit kit add <id> --url <https url> --doc <file.md|text> [--script <url>]
|
|
152
187
|
mockpit kit remove <id>
|
|
153
188
|
A project kit is a stylesheet (and optional script) on the CDN allowlist plus a
|
|
@@ -159,12 +194,21 @@ mockpit icons remove <set> [<set>...]
|
|
|
159
194
|
html surfaces write <i icon="lucide:check"></i>; the server inlines the svg.
|
|
160
195
|
lucide and mage are bundled. add finds @iconify-json/<set> in node_modules,
|
|
161
196
|
the global npm root or bun's cache, else fetches it from jsDelivr.`,
|
|
162
|
-
publish: `mockpit publish --
|
|
163
|
-
mockpit publish --
|
|
164
|
-
mockpit publish --
|
|
165
|
-
mockpit publish --
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
publish: `mockpit publish --design <slug> --html <file> [options]
|
|
198
|
+
mockpit publish --design <slug> --surfaces <json|file> [options]
|
|
199
|
+
mockpit publish --design <slug> --parts <name=file|-> ... [options]
|
|
200
|
+
mockpit publish --design <slug> --fills <part#fill=file|-> ... [options]
|
|
201
|
+
mockpit publish --design <slug> [--state s] [--variant v] --withdraw
|
|
202
|
+
Creates the design, state or variant, or the next version of an existing one.
|
|
203
|
+
One request gets one version: fixes to it before the user acts use --amend.
|
|
204
|
+
--amend replace the variant's latest version in place (same number)
|
|
205
|
+
instead of adding one. Only from the session that published
|
|
206
|
+
it, and only while the user hasn't acted on it (no Send,
|
|
207
|
+
comment, draft answer, not in use)
|
|
208
|
+
--withdraw take the variant's latest version back under the same
|
|
209
|
+
rule: the version before it is current again, and a
|
|
210
|
+
variant with no other version goes
|
|
211
|
+
--state <label> state in the user's words (omit for a single-state design)
|
|
168
212
|
--variant <name> variant label (default "default")
|
|
169
213
|
--surfaces <json|file> the full ordered list: {"id"} alone keeps a surface,
|
|
170
214
|
a missing id removes it, order is the list order
|
|
@@ -172,68 +216,112 @@ mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
|
|
|
172
216
|
(name#key for one instance) in the latest version; repeatable
|
|
173
217
|
--fills <part#fill=file|-> replace the element carrying data-part="part"
|
|
174
218
|
data-fill="fill" in the latest version, or add it after
|
|
175
|
-
that
|
|
176
|
-
--title <t>
|
|
177
|
-
--kind <k> component|
|
|
219
|
+
that part's other fills; the marks are added if missing
|
|
220
|
+
--title <t> design title
|
|
221
|
+
--kind <k> screen|component|idea: screen for a screen of the product,
|
|
222
|
+
component for a reusable design screens embed (embedding makes one);
|
|
223
|
+
omit for ideas, diagrams, reviews (default idea)
|
|
224
|
+
--area <name> where in the product it belongs, in the project's words
|
|
225
|
+
("Shell", "Food"); Home groups each tab by it. "" clears
|
|
226
|
+
--source <path> the code path that implements it (apps/web/src/shell). "" clears
|
|
178
227
|
--knobs <json|file> knobs in tunekit usePane shape
|
|
179
228
|
--from <N> branch from version N
|
|
180
229
|
--fork <v[@N]> start this new variant from sibling v (its version N, else
|
|
181
230
|
its latest); alone it copies v, with --parts/--fills it
|
|
182
231
|
edits the copy
|
|
183
232
|
--prompt <text> what prompted this version
|
|
184
|
-
--
|
|
233
|
+
--description <text> one line on what this variant is, shown beside it (never
|
|
185
234
|
inside the design)
|
|
186
|
-
--widths <list> device widths the
|
|
235
|
+
--widths <list> device widths the design is judged at, side by side, e.g.
|
|
187
236
|
390,1280 (the ones the user ships to)
|
|
188
|
-
--flow <name> a flow this
|
|
189
|
-
between, in order, across
|
|
190
|
-
places the existing
|
|
237
|
+
--flow <name> a flow this state is a step of: the states the user moves
|
|
238
|
+
between, in order, across designs; without html it only
|
|
239
|
+
places the existing state
|
|
191
240
|
--step <N> its 1-based place in the flow (moves it if it is there);
|
|
192
241
|
0 takes it out; omitted, it joins at the end
|
|
193
242
|
--project <name> project (default: git remote, else directory name)
|
|
243
|
+
--poster <file> a still of the page (png, jpg) that Studio and Home show; without
|
|
244
|
+
one, a local Chrome or Chromium takes it just after the publish
|
|
245
|
+
instead of running it; with --parts or --fills it is the
|
|
246
|
+
still of the spliced page. A version that changes the page
|
|
247
|
+
without one drops the old still
|
|
194
248
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
|
|
195
249
|
--json / --quiet
|
|
196
250
|
Prints the parts found per state, parts that vanished or were renamed,
|
|
197
|
-
nudges, a ready-to-run "mockpit ask" when variants have no
|
|
198
|
-
ask: `mockpit ask --
|
|
199
|
-
[--state s] [--part p] [--multi] [--id id] [--after <ask id>]
|
|
200
|
-
mockpit ask --
|
|
201
|
-
Ask the user. A choice is several variants plus one
|
|
251
|
+
nudges, a ready-to-run "mockpit ask" when variants have no question, and feedback.`,
|
|
252
|
+
ask: `mockpit ask --design <slug> "<question>" --option <label[=variant]> ... [--scope design|state|part]
|
|
253
|
+
[--state s] [--states s]... [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
|
|
254
|
+
mockpit ask --design <slug> --asks <json|file>
|
|
255
|
+
Ask the user. A choice is several variants plus one question that binds them.
|
|
202
256
|
Then tell the user where to look and end your turn.`,
|
|
203
|
-
read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
257
|
+
read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree] [--decisions]
|
|
258
|
+
Read this first. Both forms carry the project's decision list, what the user
|
|
259
|
+
already settled, from their replies: treat each line as settled unless they say
|
|
260
|
+
otherwise. No slug: one line per design (states, variants, open questions), one per
|
|
261
|
+
flow, plus pending, what the user is doing now, then the decisions. A slug: the
|
|
262
|
+
design's states, variants, questions with answers, parts, knobs and settled values,
|
|
263
|
+
plus its pending and decisionList (its own lines marked here), as JSON.
|
|
207
264
|
--body includes surfaces, --history version rows. --tree (no slug): the
|
|
208
|
-
project as a tree, each
|
|
209
|
-
|
|
265
|
+
project as a tree, each design's variants with status and forkedFrom, and the
|
|
266
|
+
designs it embeds and is embedded in, as JSON. --decisions: the whole list, when
|
|
267
|
+
a read says lines were left out. Never consumes feedback.`,
|
|
210
268
|
feedback: `mockpit feedback [--project <name>] [--session <id>]
|
|
211
|
-
Returns at once with {feedback, pending} as JSON: one batch per
|
|
212
|
-
sent since you last heard ({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
every write; and pending ({mock, viewerOpen, draft}) so you can tell the
|
|
269
|
+
Returns at once with {feedback, pending} as JSON: one batch per design the user
|
|
270
|
+
sent since you last heard ({design, reply: {asks, borrowed, tuned, comments,
|
|
271
|
+
verdict}, messages, inUse, archived}), delivered once across feedback, watch and
|
|
272
|
+
every write; and pending ({design, viewerOpen, draft}) so you can tell the
|
|
216
273
|
user "take your time" or "2 of 3 answered". Run it when the user says they
|
|
217
274
|
answered.`,
|
|
218
|
-
say: `mockpit say "<text>" --
|
|
219
|
-
A plain-text message in the
|
|
275
|
+
say: `mockpit say "<text>" --design <slug> [--state s] [--variant v]
|
|
276
|
+
A plain-text message in the design's thread, as your session's agent.`,
|
|
277
|
+
move: `mockpit move --design <slug> [--state s] --variant <v> [--variant <v>...]
|
|
278
|
+
--to <design> [--to-state <s>] | --to-state <s> | --archive | --restore
|
|
279
|
+
mockpit move --design <slug> [--state s] [--variant <v>...] --hide-older | --delete-older
|
|
280
|
+
Tidy up. --to or --to-state moves them to another design or tab (a new state is
|
|
281
|
+
created) and takes them out of here; --archive takes them out of sight; --restore
|
|
282
|
+
brings them back. Their group goes along; one in use stays and is reported. The
|
|
283
|
+
thread says what happened, so the user can undo it.
|
|
284
|
+
Only when the user asks: --hide-older hides all but each variant's newest visible
|
|
285
|
+
version (no --variant: every variant), and --delete-older deletes them for good,
|
|
286
|
+
skipping and listing any that a reply, comment, question, embed, copy or fork still
|
|
287
|
+
points at, or that is in use. It can't be undone: confirm first.`,
|
|
288
|
+
edit: `mockpit edit --design <slug> [--slug <new>] [--title <t>] [--kind screen|component|idea]
|
|
289
|
+
[--first-state <name>] [--notes <text>|""]
|
|
290
|
+
[--area <name>|""] [--source <path>|""] [--project <name>]
|
|
291
|
+
mockpit edit --project <name> --rename <new name>
|
|
292
|
+
mockpit edit --project <name> --area-order "Shell,Food,Chat"
|
|
293
|
+
Change a design without publishing a new version; "" clears area or source. A new
|
|
294
|
+
slug rewrites every embed, data-goto and flow step that names the old one, in
|
|
295
|
+
every version. A design still embedded somewhere stays a component, not an idea.
|
|
296
|
+
--notes drafts the design's short plain-text spec, which read returns; the user's own
|
|
297
|
+
edit of it stands, so the draft is then not applied and the result says so.
|
|
298
|
+
--first-state names a design's one unnamed state, and every variant it holds goes
|
|
299
|
+
there, so the design can then have more states.
|
|
300
|
+
--rename moves the project's designs, sessions, flows and design settings to the
|
|
301
|
+
new name (refused if that project has designs); pass --project <new> or set
|
|
302
|
+
MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
|
|
303
|
+
--area-order lists the project's areas in the order Home shows them; areas it
|
|
304
|
+
leaves out follow alphabetically, "" goes back to alphabetical.`,
|
|
220
305
|
run: `mockpit run <file|-> [--session <id>] [--project <name>] [--json]
|
|
221
306
|
Run a script (the body of an async function, plain JavaScript) in the server's
|
|
222
307
|
sandbox against the mockpit API; see mockpit guide --topic scripts. Prints each
|
|
223
308
|
host call in order, the script's prints, its value or error, and any user
|
|
224
309
|
feedback the run received. --json prints the whole result. Exits 2 when the
|
|
225
310
|
script failed.`,
|
|
226
|
-
watch: `mockpit watch [--session <id>] [--after <seq>]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
311
|
+
watch: `mockpit watch [--session <id>] [--after <seq>] [--once] [--exit-code <n>]
|
|
312
|
+
Print one line per Send, for a background monitor (in Claude Code, arm it
|
|
313
|
+
under Monitor after asking, then end the turn; with the mockpit plugin, don't,
|
|
314
|
+
since it wakes you itself). A line says a reply landed;
|
|
315
|
+
feedback or the next write delivers it, once.
|
|
316
|
+
--once stops after the first Send, for a harness whose wake is a process
|
|
317
|
+
exiting; --exit-code sets the code it stops with (default 0).`,
|
|
318
|
+
export: `mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
|
|
319
|
+
Write index.html + history.json per state to .mockpit/accepted/<design>/<state>/;
|
|
320
|
+
with fills, index.html is the chosen state, plus all-fills.html and decisions.json.`,
|
|
233
321
|
upload: `mockpit upload <file> [--kind image|file] [--session <id>]
|
|
234
322
|
Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
|
|
235
323
|
guide: `mockpit guide [--topic <id>] [--project <name>]
|
|
236
|
-
No flag prints the brief: the loop, parts,
|
|
324
|
+
No flag prints the brief: the loop, parts, questions and knobs, feedback, and
|
|
237
325
|
this project's palette, kit and icons. --topic prints one reference topic
|
|
238
326
|
(knobs, asks, surfaces, html, feedback, http, scripts).`,
|
|
239
327
|
};
|
|
@@ -267,7 +355,7 @@ function die(what, fix) {
|
|
|
267
355
|
}
|
|
268
356
|
|
|
269
357
|
// `report` decides how a failure reads: plumbing verbs keep the one-line
|
|
270
|
-
// `mockpit: …` (exit 1); the
|
|
358
|
+
// `mockpit: …` (exit 1); the design verbs pass `die` so the error / fix / exit 2
|
|
271
359
|
// format is what an agent parses.
|
|
272
360
|
async function api(path, init = {}, { report, fix } = {}) {
|
|
273
361
|
const bail = (what, hint) => (report === "die" ? die(what, hint ?? fix) : fail(what));
|
|
@@ -296,7 +384,11 @@ async function api(path, init = {}, { report, fix } = {}) {
|
|
|
296
384
|
(body.error ?? `${res.status} ${res.statusText}`) +
|
|
297
385
|
(Array.isArray(choices) ? ` (${choices.join(", ")})` : "");
|
|
298
386
|
const extra = issue?.message ?? issue?.code;
|
|
299
|
-
|
|
387
|
+
// The server's own fix, when it names one, beats the caller's generic hint.
|
|
388
|
+
bail(
|
|
389
|
+
extra && !main.includes(extra) ? `${main}: ${extra}` : main,
|
|
390
|
+
typeof body.fix === "string" ? body.fix : undefined,
|
|
391
|
+
);
|
|
300
392
|
}
|
|
301
393
|
return body;
|
|
302
394
|
}
|
|
@@ -390,8 +482,11 @@ function agentName(flags) {
|
|
|
390
482
|
// doesn't keep; Claude Code's session id does, so the server can hand back the
|
|
391
483
|
// same mockpit session (and its feedback cursor).
|
|
392
484
|
function harnessKey() {
|
|
393
|
-
|
|
394
|
-
|
|
485
|
+
if (process.env.MOCKPIT_SESSION_KEY) return process.env.MOCKPIT_SESSION_KEY;
|
|
486
|
+
if (process.env.CLAUDE_CODE_SESSION_ID)
|
|
487
|
+
return `claude-code:${process.env.CLAUDE_CODE_SESSION_ID}`;
|
|
488
|
+
if (process.env.CODEX_THREAD_ID) return `codex:${process.env.CODEX_THREAD_ID}`;
|
|
489
|
+
return undefined;
|
|
395
490
|
}
|
|
396
491
|
|
|
397
492
|
async function resolveSession(flags, { create = false } = {}) {
|
|
@@ -422,6 +517,19 @@ async function resolveSession(flags, { create = false } = {}) {
|
|
|
422
517
|
return session.id;
|
|
423
518
|
}
|
|
424
519
|
|
|
520
|
+
// The harness's conversation id names exactly one session, where the cwd can
|
|
521
|
+
// match several agents working in one repo; the watcher would then take a
|
|
522
|
+
// sibling's replies. Never creates one: most conversations never use mockpit.
|
|
523
|
+
async function resolveSessionByKey(key) {
|
|
524
|
+
try {
|
|
525
|
+
const res = await fetch(`${BASE}/api/sessions${query({ key })}`, { headers: HEADERS });
|
|
526
|
+
if (!res.ok) return null;
|
|
527
|
+
return (await res.json())[0]?.id ?? null;
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
425
533
|
// A monitor process (e.g. the Claude Code plugin) may not share the local
|
|
426
534
|
// state file written by the agent's CLI calls — different spawn tree, so
|
|
427
535
|
// `agentPid()` can hash to a different key. Fall back to asking the server for
|
|
@@ -620,17 +728,17 @@ async function installIconSets(project, prefixes) {
|
|
|
620
728
|
errors.push({ prefix, message: err instanceof Error ? err.message : String(err) });
|
|
621
729
|
}
|
|
622
730
|
}
|
|
623
|
-
if (added.length === 0) return { added: [], errors,
|
|
624
|
-
const stored = (await api(`${projectPath(project)}/design`)) ?? {};
|
|
731
|
+
if (added.length === 0) return { added: [], errors, designSystem: null };
|
|
732
|
+
const stored = (await api(`${projectPath(project)}/design-system`)) ?? {};
|
|
625
733
|
const keep = (stored.iconSets ?? []).filter((s) => !added.some((a) => a.prefix === s.prefix));
|
|
626
|
-
const
|
|
734
|
+
const designSystem = await api(`${projectPath(project)}/design-system`, {
|
|
627
735
|
method: "PUT",
|
|
628
736
|
body: JSON.stringify({ ...stored, iconSets: [...keep, ...added] }),
|
|
629
737
|
});
|
|
630
738
|
return {
|
|
631
|
-
added:
|
|
739
|
+
added: designSystem.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
|
|
632
740
|
errors,
|
|
633
|
-
|
|
741
|
+
designSystem,
|
|
634
742
|
};
|
|
635
743
|
}
|
|
636
744
|
|
|
@@ -774,7 +882,7 @@ function slugify(text) {
|
|
|
774
882
|
|
|
775
883
|
const projectPath = (project) => `/api/projects/${encodeURIComponent(project)}`;
|
|
776
884
|
|
|
777
|
-
const
|
|
885
|
+
const designPath = (slug) => `/api/designs/${encodeURIComponent(slug)}`;
|
|
778
886
|
|
|
779
887
|
const query = (params) => {
|
|
780
888
|
const q = new URLSearchParams();
|
|
@@ -785,18 +893,18 @@ const query = (params) => {
|
|
|
785
893
|
return s ? `?${s}` : "";
|
|
786
894
|
};
|
|
787
895
|
|
|
788
|
-
// The flags every
|
|
789
|
-
const
|
|
790
|
-
|
|
896
|
+
// The flags every design verb shares: which design, which state, which variant.
|
|
897
|
+
const DESIGN_FLAGS = {
|
|
898
|
+
design: { type: "string" },
|
|
791
899
|
state: { type: "string" },
|
|
792
900
|
variant: { type: "string" },
|
|
793
901
|
project: { type: "string" },
|
|
794
902
|
session: { type: "string" },
|
|
795
903
|
};
|
|
796
904
|
|
|
797
|
-
function
|
|
798
|
-
const slug = flags.
|
|
799
|
-
if (!slug) die("--
|
|
905
|
+
function requireDesign(flags, example) {
|
|
906
|
+
const slug = flags.design ? slugify(flags.design) : "";
|
|
907
|
+
if (!slug) die("--design needs a slug", example);
|
|
800
908
|
return slug;
|
|
801
909
|
}
|
|
802
910
|
|
|
@@ -818,7 +926,7 @@ const shq = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${String(s).replace(/'/g,
|
|
|
818
926
|
// The server's suggested ask as the command that sends it: an agent that was
|
|
819
927
|
// nudged can run it as is. --asks carries ids and scope, which flags can't.
|
|
820
928
|
function suggestedAskLine(slug, ask) {
|
|
821
|
-
return `mockpit ask --
|
|
929
|
+
return `mockpit ask --design ${shq(slug)} --asks ${shq(JSON.stringify([ask]))}`;
|
|
822
930
|
}
|
|
823
931
|
|
|
824
932
|
// One line per write: what was published, its URL, the parts found per state,
|
|
@@ -826,10 +934,22 @@ function suggestedAskLine(slug, ask) {
|
|
|
826
934
|
function printPublished(result, flags) {
|
|
827
935
|
if (flags.json) return out(result);
|
|
828
936
|
if (flags.quiet) return;
|
|
829
|
-
const {
|
|
937
|
+
const { design, post } = result;
|
|
938
|
+
if (result.planned) {
|
|
939
|
+
console.log(`${design.slug}: planned, nothing drawn yet · ${result.url}`);
|
|
940
|
+
if (result.flow) console.log(flowLine(result.flow));
|
|
941
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
942
|
+
printFeedback(result.feedback);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
830
945
|
const branch = post.from !== undefined && post.from !== post.version - 1;
|
|
946
|
+
const how = result.withdrawn
|
|
947
|
+
? ` (v${result.withdrawn.version} withdrawn)`
|
|
948
|
+
: flags.amend
|
|
949
|
+
? " (amended)"
|
|
950
|
+
: "";
|
|
831
951
|
console.log(
|
|
832
|
-
`${
|
|
952
|
+
`${design.slug}/${stateLabel(post.state)}${post.variant} v${post.version}${branch && post.from ? ` (from v${post.from})` : ""}${how} · ${result.url}`,
|
|
833
953
|
);
|
|
834
954
|
for (const { state, parts } of result.parts ?? []) {
|
|
835
955
|
if (parts.length === 0) continue;
|
|
@@ -844,13 +964,41 @@ function printPublished(result, flags) {
|
|
|
844
964
|
if (changes?.vanished?.length) console.log(`vanished: ${changes.vanished.join(", ")}`);
|
|
845
965
|
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
846
966
|
if (result.suggestedAsk)
|
|
847
|
-
console.log(`suggested: ${suggestedAskLine(
|
|
967
|
+
console.log(`suggested: ${suggestedAskLine(design.slug, result.suggestedAsk)}`);
|
|
848
968
|
for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
|
|
849
969
|
printFeedback(result.feedback);
|
|
850
970
|
}
|
|
851
971
|
|
|
972
|
+
async function withdrawVersion(flags, slug, project) {
|
|
973
|
+
const session = await resolveSession(flags);
|
|
974
|
+
if (!session) die(`no session has published ${slug} from here`, `mockpit read ${slug}`);
|
|
975
|
+
const result = await api(
|
|
976
|
+
"/api/designs",
|
|
977
|
+
{
|
|
978
|
+
method: "POST",
|
|
979
|
+
body: JSON.stringify({
|
|
980
|
+
session,
|
|
981
|
+
project,
|
|
982
|
+
design: slug,
|
|
983
|
+
state: flags.state,
|
|
984
|
+
variant: flags.variant,
|
|
985
|
+
withdraw: true,
|
|
986
|
+
}),
|
|
987
|
+
},
|
|
988
|
+
{ report: "die", fix: `mockpit read ${slug} --history` },
|
|
989
|
+
);
|
|
990
|
+
if (!result.removed) return printPublished(result, flags);
|
|
991
|
+
if (flags.json) return out(result);
|
|
992
|
+
if (flags.quiet) return;
|
|
993
|
+
const w = result.withdrawn;
|
|
994
|
+
console.log(
|
|
995
|
+
`${slug}/${stateLabel(w.state)}${w.variant} v${w.version} withdrawn, variant removed · ${result.url}`,
|
|
996
|
+
);
|
|
997
|
+
printFeedback(result.feedback);
|
|
998
|
+
}
|
|
999
|
+
|
|
852
1000
|
function flowLine(flow) {
|
|
853
|
-
const steps = flow.steps.map((x) => (x.state === null ? x.
|
|
1001
|
+
const steps = flow.steps.map((x) => (x.state === null ? x.design : `${x.design}/${x.state}`));
|
|
854
1002
|
return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
|
|
855
1003
|
}
|
|
856
1004
|
|
|
@@ -860,15 +1008,15 @@ function printFeedback(feedback) {
|
|
|
860
1008
|
if (feedback?.length) console.log(`feedback: ${JSON.stringify(feedback)}`);
|
|
861
1009
|
}
|
|
862
1010
|
|
|
863
|
-
// What the user is doing right now, per
|
|
1011
|
+
// What the user is doing right now, per design.
|
|
864
1012
|
function pendingLines(pending) {
|
|
865
1013
|
return (pending ?? []).flatMap((p) =>
|
|
866
1014
|
p.draft
|
|
867
1015
|
? [
|
|
868
|
-
`pending: ${p.
|
|
1016
|
+
`pending: ${p.design} — the user is answering (${p.draft.answered} of ${p.draft.of} answered, ${p.draft.comments} comment${p.draft.comments === 1 ? "" : "s"})`,
|
|
869
1017
|
]
|
|
870
1018
|
: p.viewerOpen
|
|
871
|
-
? [`pending: ${p.
|
|
1019
|
+
? [`pending: ${p.design} — open in the viewer`]
|
|
872
1020
|
: [],
|
|
873
1021
|
);
|
|
874
1022
|
}
|
|
@@ -878,10 +1026,13 @@ function parsePublishFlags() {
|
|
|
878
1026
|
tokens: true,
|
|
879
1027
|
allowPositionals: true,
|
|
880
1028
|
options: {
|
|
881
|
-
...
|
|
1029
|
+
...DESIGN_FLAGS,
|
|
882
1030
|
title: { type: "string" },
|
|
883
1031
|
kind: { type: "string" },
|
|
1032
|
+
area: { type: "string" },
|
|
1033
|
+
source: { type: "string" },
|
|
884
1034
|
html: { type: "string" },
|
|
1035
|
+
poster: { type: "string" },
|
|
885
1036
|
surfaces: { type: "string" },
|
|
886
1037
|
parts: { type: "string", multiple: true },
|
|
887
1038
|
fills: { type: "string", multiple: true },
|
|
@@ -889,7 +1040,7 @@ function parsePublishFlags() {
|
|
|
889
1040
|
from: { type: "string" },
|
|
890
1041
|
fork: { type: "string" },
|
|
891
1042
|
prompt: { type: "string" },
|
|
892
|
-
|
|
1043
|
+
description: { type: "string" },
|
|
893
1044
|
widths: { type: "string" },
|
|
894
1045
|
flow: { type: "string" },
|
|
895
1046
|
step: { type: "string" },
|
|
@@ -905,19 +1056,21 @@ function parsePublishFlags() {
|
|
|
905
1056
|
"session-title": { type: "string" },
|
|
906
1057
|
agent: { type: "string" },
|
|
907
1058
|
"new-session": { type: "boolean" },
|
|
1059
|
+
amend: { type: "boolean" },
|
|
1060
|
+
withdraw: { type: "boolean" },
|
|
908
1061
|
},
|
|
909
1062
|
});
|
|
910
1063
|
}
|
|
911
1064
|
|
|
912
1065
|
// `--parts name=file` (repeatable; `name#key` targets one instance): the new
|
|
913
1066
|
// outer html of one part, spliced server-side into the latest version.
|
|
914
|
-
// `--fills part#fill=file` is the same for one
|
|
1067
|
+
// `--fills part#fill=file` is the same for one fill of a part.
|
|
915
1068
|
function readPartFlags(values, slug, flag = "parts") {
|
|
916
1069
|
if (!values?.length) return undefined;
|
|
917
1070
|
const example =
|
|
918
1071
|
flag === "parts"
|
|
919
|
-
? `mockpit publish --
|
|
920
|
-
: `mockpit publish --
|
|
1072
|
+
? `mockpit publish --design ${slug} --parts body=body.html`
|
|
1073
|
+
: `mockpit publish --design ${slug} --fills entry#skeleton=skeleton.html`;
|
|
921
1074
|
const parts = {};
|
|
922
1075
|
let stdin = false;
|
|
923
1076
|
for (const raw of values) {
|
|
@@ -939,14 +1092,15 @@ function readPartFlags(values, slug, flag = "parts") {
|
|
|
939
1092
|
return parts;
|
|
940
1093
|
}
|
|
941
1094
|
|
|
942
|
-
async function
|
|
1095
|
+
async function publishDesign() {
|
|
943
1096
|
const { values: flags, positionals, tokens } = parsePublishFlags();
|
|
944
|
-
const slug =
|
|
1097
|
+
const slug = requireDesign(flags, "mockpit publish --design writer --html writer.html");
|
|
945
1098
|
const project = resolveProject(flags).name;
|
|
946
1099
|
const from = flags.from === undefined ? undefined : Number(flags.from);
|
|
947
1100
|
if (from !== undefined && !Number.isInteger(from)) {
|
|
948
1101
|
die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
|
|
949
1102
|
}
|
|
1103
|
+
if (flags.withdraw) return withdrawVersion(flags, slug, project);
|
|
950
1104
|
const step = flags.step === undefined ? undefined : Number(flags.step);
|
|
951
1105
|
if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
|
|
952
1106
|
die(
|
|
@@ -971,6 +1125,9 @@ async function publishMock() {
|
|
|
971
1125
|
}
|
|
972
1126
|
// Checked before the session exists so a typo'd path leaves nothing behind on the server.
|
|
973
1127
|
if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
|
|
1128
|
+
if (flags.poster !== undefined && !existsSync(flags.poster)) {
|
|
1129
|
+
die(`cannot read ${flags.poster}`, `ls ${flags.poster}`);
|
|
1130
|
+
}
|
|
974
1131
|
const parts = readPartFlags(flags.parts, slug);
|
|
975
1132
|
const fills = readPartFlags(flags.fills, slug, "fills");
|
|
976
1133
|
let surfaces =
|
|
@@ -992,21 +1149,26 @@ async function publishMock() {
|
|
|
992
1149
|
}
|
|
993
1150
|
surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
|
|
994
1151
|
}
|
|
995
|
-
|
|
996
|
-
|
|
1152
|
+
const declaring = flags.kind === "screen" || flags.kind === "component";
|
|
1153
|
+
if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined && !declaring)
|
|
1154
|
+
die(`no html for ${slug}`, `mockpit publish --design ${slug} --html <file>`);
|
|
1155
|
+
const poster =
|
|
1156
|
+
flags.poster === undefined ? undefined : (await uploadFile(flags.poster, { session })).id;
|
|
997
1157
|
const body = {
|
|
998
1158
|
session,
|
|
999
1159
|
project,
|
|
1000
|
-
|
|
1160
|
+
design: slug,
|
|
1001
1161
|
state: flags.state,
|
|
1002
1162
|
variant: flags.variant,
|
|
1003
1163
|
title: flags.title,
|
|
1004
1164
|
kind: flags.kind,
|
|
1165
|
+
area: flags.area,
|
|
1166
|
+
source: flags.source,
|
|
1005
1167
|
...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
|
|
1006
1168
|
...(from !== undefined && { from }),
|
|
1007
1169
|
...(flags.fork !== undefined && { fork: flags.fork }),
|
|
1008
1170
|
prompt: flags.prompt,
|
|
1009
|
-
|
|
1171
|
+
description: flags.description,
|
|
1010
1172
|
...(flags.widths !== undefined && {
|
|
1011
1173
|
widths: flags.widths
|
|
1012
1174
|
.split(",")
|
|
@@ -1018,13 +1180,27 @@ async function publishMock() {
|
|
|
1018
1180
|
...(surfaces && { surfaces }),
|
|
1019
1181
|
...(parts && { parts }),
|
|
1020
1182
|
...(fills && { fills }),
|
|
1183
|
+
...(poster && { poster }),
|
|
1184
|
+
...(!poster && findBrowser() && { posterComing: true }),
|
|
1185
|
+
...(flags.amend && { amend: true }),
|
|
1021
1186
|
};
|
|
1022
1187
|
const result = await api(
|
|
1023
|
-
"/api/
|
|
1188
|
+
"/api/designs",
|
|
1024
1189
|
{ method: "POST", body: JSON.stringify(body) },
|
|
1025
1190
|
{ report: "die", fix: `mockpit read ${slug}` },
|
|
1026
1191
|
);
|
|
1027
1192
|
printPublished(result, flags);
|
|
1193
|
+
if (!poster && result.post?.surfaces?.some((s) => s.kind === "html") && !result.withdrawn) {
|
|
1194
|
+
queuePoster({
|
|
1195
|
+
base: BASE,
|
|
1196
|
+
token: TOKEN,
|
|
1197
|
+
client: HEADERS["x-mockpit-client"],
|
|
1198
|
+
session,
|
|
1199
|
+
design: result.design.id,
|
|
1200
|
+
postId: result.post.id,
|
|
1201
|
+
version: result.post.version,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1028
1204
|
return result;
|
|
1029
1205
|
}
|
|
1030
1206
|
|
|
@@ -1075,13 +1251,22 @@ function printRun(r) {
|
|
|
1075
1251
|
|
|
1076
1252
|
function watchLines(batch) {
|
|
1077
1253
|
const lines = [];
|
|
1078
|
-
const name = batch.
|
|
1254
|
+
const name = batch.design ?? "a design";
|
|
1079
1255
|
if (batch.handoff) lines.push(`mockpit handoff on ${name}: ${batch.handoff}`);
|
|
1080
|
-
if (batch.
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1256
|
+
if (batch.edited) {
|
|
1257
|
+
const e = batch.edited;
|
|
1258
|
+
const bits = [
|
|
1259
|
+
e.kind ? `now a ${e.kind}` : "",
|
|
1260
|
+
e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
|
|
1261
|
+
].filter(Boolean);
|
|
1262
|
+
if (bits.length) lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
|
|
1263
|
+
if (e.notes !== undefined) {
|
|
1264
|
+
lines.push(
|
|
1265
|
+
e.notes === null
|
|
1266
|
+
? `mockpit edited ${name}: the user cleared its notes`
|
|
1267
|
+
: `mockpit edited ${name}: the user edited its notes: ${e.notes}`,
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1085
1270
|
}
|
|
1086
1271
|
if (batch.reply) {
|
|
1087
1272
|
const r = batch.reply;
|
|
@@ -1090,26 +1275,35 @@ function watchLines(batch) {
|
|
|
1090
1275
|
? r.asks
|
|
1091
1276
|
.map(
|
|
1092
1277
|
(a) =>
|
|
1093
|
-
`${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.
|
|
1278
|
+
`${a.text || a.ask}: ${a.chosen.map((o) => (o.other ? `“${o.label}”` : o.label)).join("+")}${a.comment ? ` (“${a.comment}”)` : ""}`,
|
|
1094
1279
|
)
|
|
1095
1280
|
.join("; ")
|
|
1096
1281
|
: "",
|
|
1097
1282
|
Object.keys(r.tuned ?? {}).length ? `${Object.keys(r.tuned).length} tuned` : "",
|
|
1098
|
-
Object.keys(r.
|
|
1099
|
-
? `
|
|
1283
|
+
Object.keys(r.borrowed ?? {}).length
|
|
1284
|
+
? `borrowed ${Object.entries(r.borrowed)
|
|
1100
1285
|
.map(([p, v]) => `${p}←${v}`)
|
|
1101
1286
|
.join(", ")}`
|
|
1102
1287
|
: "",
|
|
1103
|
-
r.comments
|
|
1288
|
+
...(r.comments ?? [])
|
|
1289
|
+
.filter((c) => c.borrow)
|
|
1290
|
+
.map(
|
|
1291
|
+
(c) =>
|
|
1292
|
+
`borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? ` (“${c.text}”)` : ""}`,
|
|
1293
|
+
),
|
|
1294
|
+
(() => {
|
|
1295
|
+
const n = (r.comments ?? []).filter((c) => !c.borrow).length;
|
|
1296
|
+
return n ? `${n} comment${n === 1 ? "" : "s"}` : "";
|
|
1297
|
+
})(),
|
|
1104
1298
|
r.reactions?.length ? r.reactions.join(", ") : "",
|
|
1105
|
-
r.
|
|
1299
|
+
r.verdict ? `${r.verdict.kind} ${r.verdict.variant}` : "",
|
|
1106
1300
|
r.text ?? "",
|
|
1107
1301
|
].filter(Boolean);
|
|
1108
1302
|
lines.push(`mockpit reply on ${name}: ${bits.join(" · ").replace(/\s+/g, " ")}`);
|
|
1109
1303
|
}
|
|
1110
|
-
for (const c of batch.
|
|
1304
|
+
for (const c of batch.messages ?? []) {
|
|
1111
1305
|
lines.push(
|
|
1112
|
-
`mockpit
|
|
1306
|
+
`mockpit message on ${name}: “${String(c.text ?? "")
|
|
1113
1307
|
.replace(/\s+/g, " ")
|
|
1114
1308
|
.trim()}”`,
|
|
1115
1309
|
);
|
|
@@ -1117,7 +1311,7 @@ function watchLines(batch) {
|
|
|
1117
1311
|
return lines;
|
|
1118
1312
|
}
|
|
1119
1313
|
|
|
1120
|
-
// GET where "not there yet" is an answer, not an exit — a project or
|
|
1314
|
+
// GET where "not there yet" is an answer, not an exit — a project or design that
|
|
1121
1315
|
// doesn't exist is the normal state before the first publish.
|
|
1122
1316
|
async function apiSoft(path) {
|
|
1123
1317
|
let res;
|
|
@@ -1132,8 +1326,13 @@ async function apiSoft(path) {
|
|
|
1132
1326
|
return res.json().catch(() => null);
|
|
1133
1327
|
}
|
|
1134
1328
|
|
|
1135
|
-
// Keep the scratch directory out of the repo
|
|
1136
|
-
//
|
|
1329
|
+
// Keep the scratch directory out of the repo, all but the project's rules
|
|
1330
|
+
// file, which is meant to be committed. Git can't re-include a file under an
|
|
1331
|
+
// ignored directory, so the ignore has to name the directory's contents, and
|
|
1332
|
+
// the `.mockpit/` line older versions wrote is upgraded in place. Returns true
|
|
1333
|
+
// when .gitignore was touched, so init can report it.
|
|
1334
|
+
const MOCKPIT_IGNORE = ".mockpit/*\n!.mockpit/rules.md";
|
|
1335
|
+
|
|
1137
1336
|
function ignoreMockpitDir() {
|
|
1138
1337
|
const file = join(process.cwd(), ".gitignore");
|
|
1139
1338
|
let current = "";
|
|
@@ -1142,14 +1341,56 @@ function ignoreMockpitDir() {
|
|
|
1142
1341
|
} catch {
|
|
1143
1342
|
// no .gitignore yet — create one
|
|
1144
1343
|
}
|
|
1145
|
-
if (/^\.mockpit
|
|
1344
|
+
if (/^\.mockpit\/\*$/m.test(current) && /^!\.mockpit\/rules\.md$/m.test(current)) return false;
|
|
1345
|
+
if (/^\.mockpit\/?$/m.test(current)) {
|
|
1346
|
+
writeFileSync(file, current.replace(/^\.mockpit\/?$/m, MOCKPIT_IGNORE));
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
const lines = /^\.mockpit\/\*$/m.test(current) ? "!.mockpit/rules.md" : MOCKPIT_IGNORE;
|
|
1146
1350
|
writeFileSync(
|
|
1147
1351
|
file,
|
|
1148
|
-
current && !current.endsWith("\n") ? `${current}\n
|
|
1352
|
+
current && !current.endsWith("\n") ? `${current}\n${lines}\n` : `${current}${lines}\n`,
|
|
1149
1353
|
);
|
|
1150
1354
|
return true;
|
|
1151
1355
|
}
|
|
1152
1356
|
|
|
1357
|
+
function repoRoot() {
|
|
1358
|
+
try {
|
|
1359
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1360
|
+
encoding: "utf8",
|
|
1361
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1362
|
+
}).trim();
|
|
1363
|
+
if (top) return top;
|
|
1364
|
+
} catch {}
|
|
1365
|
+
return process.cwd();
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Where `mockpit serve` reads the user's own rules: next to its database.
|
|
1369
|
+
const myRulesFile = () =>
|
|
1370
|
+
join(dirname(process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db")), "rules.md");
|
|
1371
|
+
|
|
1372
|
+
// Sends a rules file as it is on disk: a missing file clears the stored copy,
|
|
1373
|
+
// so deleting the file and re-running is how rules are switched off.
|
|
1374
|
+
async function sendRules(file, path) {
|
|
1375
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
1376
|
+
const result = await api(
|
|
1377
|
+
path,
|
|
1378
|
+
{ method: "PUT", body: JSON.stringify({ text }) },
|
|
1379
|
+
{ report: "die", fix: "mockpit rules --help" },
|
|
1380
|
+
);
|
|
1381
|
+
return { ...result, missing: !text };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function rulesSummary(r) {
|
|
1385
|
+
if (r.cleared) return "none (cleared)";
|
|
1386
|
+
const bits = [
|
|
1387
|
+
r.packs?.length ? `packs ${r.packs.join(", ")}` : null,
|
|
1388
|
+
r.rules ? "own rules" : null,
|
|
1389
|
+
].filter(Boolean);
|
|
1390
|
+
const unknown = r.unknown?.length ? ` · unknown pack ${r.unknown.join(", ")}` : "";
|
|
1391
|
+
return `${bits.join(" · ") || "empty"}${unknown}`;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1153
1394
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1154
1395
|
|
|
1155
1396
|
// Semver greater-than for plain x.y.z (mirrors server/app.ts versionGt).
|
|
@@ -1306,28 +1547,32 @@ const commands = {
|
|
|
1306
1547
|
const say = (label, text) => {
|
|
1307
1548
|
if (!flags.quiet && !flags.json) console.log(`${label.padEnd(8)} ${text}`);
|
|
1308
1549
|
};
|
|
1309
|
-
const {
|
|
1550
|
+
const { detectDesignSystem, detectIconSets, renderStarter } = await import("./initDesign.js");
|
|
1310
1551
|
say("project:", `${project} (from ${source})`);
|
|
1311
1552
|
|
|
1312
|
-
const
|
|
1313
|
-
const d =
|
|
1553
|
+
const designSystem = await detectDesignSystem(process.cwd());
|
|
1554
|
+
const d = designSystem.detected;
|
|
1314
1555
|
// `detected` is always an object, so emptiness has to be read off the
|
|
1315
1556
|
// fields — otherwise a repo with no design system printed a blank line.
|
|
1316
|
-
const stripped =
|
|
1317
|
-
? ` (stripped: ${
|
|
1557
|
+
const stripped = designSystem.strippedImports?.length
|
|
1558
|
+
? ` (stripped: ${designSystem.strippedImports.join(", ")})`
|
|
1318
1559
|
: "";
|
|
1319
1560
|
const found = [
|
|
1320
1561
|
d?.tailwind &&
|
|
1321
|
-
(
|
|
1562
|
+
(designSystem.tailwindSource
|
|
1563
|
+
? `tailwind: ${designSystem.tailwindSource}${stripped}`
|
|
1564
|
+
: "tailwind"),
|
|
1322
1565
|
d?.shadcn && "shadcn",
|
|
1323
|
-
d?.cssVars
|
|
1566
|
+
d?.cssVars
|
|
1567
|
+
? `${d.cssVars} css vars${designSystem.source ? ` from ${designSystem.source}` : ""}`
|
|
1568
|
+
: null,
|
|
1324
1569
|
d?.fonts?.length ? `fonts ${d.fonts.join(", ")}` : null,
|
|
1325
1570
|
].filter(Boolean);
|
|
1326
1571
|
say(
|
|
1327
1572
|
"design:",
|
|
1328
1573
|
found.length ? found.join(" · ") : "nothing detected — using the built-in palette",
|
|
1329
1574
|
);
|
|
1330
|
-
let kit = flags.kit ??
|
|
1575
|
+
let kit = flags.kit ?? designSystem.kit ?? "builtin";
|
|
1331
1576
|
// The project kit is stored first so the design PUT can name it as default.
|
|
1332
1577
|
if (flags["kit-url"] !== undefined) {
|
|
1333
1578
|
kit = flags.kit ?? "project";
|
|
@@ -1338,7 +1583,7 @@ const commands = {
|
|
|
1338
1583
|
});
|
|
1339
1584
|
}
|
|
1340
1585
|
say("kit:", kit);
|
|
1341
|
-
const files =
|
|
1586
|
+
const files = designSystem.designFiles;
|
|
1342
1587
|
if (files) {
|
|
1343
1588
|
const sh = files.shadcn;
|
|
1344
1589
|
say(
|
|
@@ -1353,16 +1598,16 @@ const commands = {
|
|
|
1353
1598
|
);
|
|
1354
1599
|
}
|
|
1355
1600
|
|
|
1356
|
-
let stored = await api(`${projectPath(project)}/design`, {
|
|
1601
|
+
let stored = await api(`${projectPath(project)}/design-system`, {
|
|
1357
1602
|
method: "PUT",
|
|
1358
1603
|
body: JSON.stringify({
|
|
1359
|
-
detected:
|
|
1360
|
-
palette:
|
|
1361
|
-
cssVars:
|
|
1362
|
-
tailwindCss:
|
|
1363
|
-
strippedImports:
|
|
1604
|
+
detected: designSystem.detected ?? null,
|
|
1605
|
+
palette: designSystem.palette ?? null,
|
|
1606
|
+
cssVars: designSystem.cssVars ?? "",
|
|
1607
|
+
tailwindCss: designSystem.tailwindCss ?? "",
|
|
1608
|
+
strippedImports: designSystem.strippedImports ?? [],
|
|
1364
1609
|
kit,
|
|
1365
|
-
designFiles:
|
|
1610
|
+
designFiles: designSystem.designFiles ?? null,
|
|
1366
1611
|
}),
|
|
1367
1612
|
});
|
|
1368
1613
|
|
|
@@ -1385,7 +1630,7 @@ const commands = {
|
|
|
1385
1630
|
if (added) say("icons:", `${f.prefix} (${added.count} icons, from ${f.from})`);
|
|
1386
1631
|
else say("icons:", `${f.prefix} not added: ${error?.message ?? "unknown error"}`);
|
|
1387
1632
|
}
|
|
1388
|
-
if (result.
|
|
1633
|
+
if (result.designSystem) stored = result.designSystem;
|
|
1389
1634
|
}
|
|
1390
1635
|
const iconSets = [
|
|
1391
1636
|
...new Set([...(stored?.iconSets ?? []).map((s) => s.prefix), ...BUNDLED_ICON_SETS]),
|
|
@@ -1394,27 +1639,33 @@ const commands = {
|
|
|
1394
1639
|
|
|
1395
1640
|
const starter = join(process.cwd(), ".mockpit", "starter.html");
|
|
1396
1641
|
mkdirSync(dirname(starter), { recursive: true });
|
|
1397
|
-
writeFileSync(starter, renderStarter({ ...
|
|
1642
|
+
writeFileSync(starter, renderStarter({ ...designSystem, kit }, iconSets));
|
|
1398
1643
|
say("wrote:", ".mockpit/starter.html");
|
|
1399
|
-
if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/)");
|
|
1644
|
+
if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/, keeping .mockpit/rules.md)");
|
|
1645
|
+
const rulesFile = join(repoRoot(), ".mockpit", "rules.md");
|
|
1646
|
+
if (existsSync(rulesFile)) {
|
|
1647
|
+
say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
|
|
1648
|
+
}
|
|
1400
1649
|
say("next:", "mockpit guide");
|
|
1401
|
-
if (flags.json) out({ project,
|
|
1650
|
+
if (flags.json) out({ project, designSystem: stored, starter });
|
|
1402
1651
|
},
|
|
1403
1652
|
|
|
1404
1653
|
async publish() {
|
|
1405
|
-
await
|
|
1654
|
+
await publishDesign();
|
|
1406
1655
|
},
|
|
1407
1656
|
|
|
1408
1657
|
async ask() {
|
|
1409
1658
|
const { values: flags, positionals } = parse({
|
|
1410
1659
|
allowPositionals: true,
|
|
1411
1660
|
options: {
|
|
1412
|
-
|
|
1661
|
+
design: { type: "string" },
|
|
1413
1662
|
project: { type: "string" },
|
|
1414
1663
|
session: { type: "string" },
|
|
1415
1664
|
option: { type: "string", multiple: true },
|
|
1665
|
+
recommend: { type: "string" },
|
|
1416
1666
|
scope: { type: "string" },
|
|
1417
1667
|
state: { type: "string" },
|
|
1668
|
+
states: { type: "string", multiple: true },
|
|
1418
1669
|
part: { type: "string" },
|
|
1419
1670
|
multi: { type: "boolean" },
|
|
1420
1671
|
id: { type: "string" },
|
|
@@ -1422,13 +1673,16 @@ const commands = {
|
|
|
1422
1673
|
asks: { type: "string" },
|
|
1423
1674
|
},
|
|
1424
1675
|
});
|
|
1425
|
-
const slug =
|
|
1676
|
+
const slug = requireDesign(
|
|
1677
|
+
flags,
|
|
1678
|
+
'mockpit ask --design writer "Quiet or dark?" --option Quiet=quiet',
|
|
1679
|
+
);
|
|
1426
1680
|
let asks;
|
|
1427
1681
|
if (flags.asks !== undefined) {
|
|
1428
1682
|
if (positionals.length > 0) {
|
|
1429
1683
|
die(
|
|
1430
1684
|
"use the question or --asks, not both",
|
|
1431
|
-
`mockpit ask --
|
|
1685
|
+
`mockpit ask --design ${slug} --asks <json|file>`,
|
|
1432
1686
|
);
|
|
1433
1687
|
}
|
|
1434
1688
|
asks = readJsonFlag("asks", flags.asks);
|
|
@@ -1438,14 +1692,25 @@ const commands = {
|
|
|
1438
1692
|
if (!text)
|
|
1439
1693
|
die(
|
|
1440
1694
|
"ask needs a question",
|
|
1441
|
-
`mockpit ask --
|
|
1695
|
+
`mockpit ask --design ${slug} "Quiet or dark?" --option Quiet=quiet`,
|
|
1442
1696
|
);
|
|
1443
1697
|
const options = (flags.option ?? []).map(parseOption);
|
|
1444
1698
|
if (options.length === 0) {
|
|
1445
1699
|
die(
|
|
1446
1700
|
"ask needs options",
|
|
1447
|
-
`mockpit ask --
|
|
1701
|
+
`mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
if (flags.recommend !== undefined) {
|
|
1705
|
+
const pick = options.find(
|
|
1706
|
+
(o) => o.label.toLowerCase() === flags.recommend.trim().toLowerCase(),
|
|
1448
1707
|
);
|
|
1708
|
+
if (!pick)
|
|
1709
|
+
die(
|
|
1710
|
+
`--recommend "${flags.recommend}" names no option`,
|
|
1711
|
+
`mockpit ask --design ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
|
|
1712
|
+
);
|
|
1713
|
+
pick.recommended = true;
|
|
1449
1714
|
}
|
|
1450
1715
|
asks = [
|
|
1451
1716
|
{
|
|
@@ -1454,6 +1719,7 @@ const commands = {
|
|
|
1454
1719
|
...(flags.id && { id: flags.id }),
|
|
1455
1720
|
...(flags.scope && { scope: flags.scope }),
|
|
1456
1721
|
...(flags.state && { state: flags.state }),
|
|
1722
|
+
...(flags.states && { states: flags.states }),
|
|
1457
1723
|
...(flags.part && { part: flags.part }),
|
|
1458
1724
|
...(flags.multi && { multi: true }),
|
|
1459
1725
|
...(flags.after && { after: flags.after }),
|
|
@@ -1462,7 +1728,7 @@ const commands = {
|
|
|
1462
1728
|
}
|
|
1463
1729
|
const session = await resolveSession(flags);
|
|
1464
1730
|
const result = await api(
|
|
1465
|
-
`${
|
|
1731
|
+
`${designPath(slug)}/asks`,
|
|
1466
1732
|
{
|
|
1467
1733
|
method: "POST",
|
|
1468
1734
|
body: JSON.stringify({
|
|
@@ -1477,7 +1743,7 @@ const commands = {
|
|
|
1477
1743
|
if (flags.quiet) return;
|
|
1478
1744
|
for (const ask of result.asks) {
|
|
1479
1745
|
console.log(
|
|
1480
|
-
`asked on ${result.
|
|
1746
|
+
`asked on ${result.design}: ${ask.text} [${ask.options.map((o) => o.label).join(" | ")}]`,
|
|
1481
1747
|
);
|
|
1482
1748
|
}
|
|
1483
1749
|
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
@@ -1495,10 +1761,20 @@ const commands = {
|
|
|
1495
1761
|
body: { type: "boolean" },
|
|
1496
1762
|
history: { type: "boolean" },
|
|
1497
1763
|
tree: { type: "boolean" },
|
|
1764
|
+
decisions: { type: "boolean" },
|
|
1498
1765
|
},
|
|
1499
1766
|
});
|
|
1500
1767
|
const project = resolveProject(flags).name;
|
|
1501
|
-
if (positionals.length > 1) die("read takes one
|
|
1768
|
+
if (positionals.length > 1) die("read takes one design slug", "mockpit read writer");
|
|
1769
|
+
if (flags.decisions) {
|
|
1770
|
+
return out(
|
|
1771
|
+
await api(
|
|
1772
|
+
`/api/projects/${encodeURIComponent(project)}/decisions`,
|
|
1773
|
+
{},
|
|
1774
|
+
{ report: "die", fix: `mockpit read --project ${project}` },
|
|
1775
|
+
),
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1502
1778
|
if (flags.tree) {
|
|
1503
1779
|
return out(
|
|
1504
1780
|
await api(
|
|
@@ -1512,49 +1788,56 @@ const commands = {
|
|
|
1512
1788
|
const slug = slugify(positionals[0]);
|
|
1513
1789
|
return out(
|
|
1514
1790
|
await api(
|
|
1515
|
-
`${
|
|
1791
|
+
`${designPath(slug)}${query({ project, body: flags.body, history: flags.history })}`,
|
|
1516
1792
|
{},
|
|
1517
1793
|
{ report: "die", fix: `mockpit read --project ${project}` },
|
|
1518
1794
|
),
|
|
1519
1795
|
);
|
|
1520
1796
|
}
|
|
1521
|
-
const list = (await apiSoft(`/api/
|
|
1522
|
-
|
|
1797
|
+
const list = (await apiSoft(`/api/designs${query({ project })}`)) ?? {
|
|
1798
|
+
designs: [],
|
|
1523
1799
|
open: 0,
|
|
1524
1800
|
pending: [],
|
|
1525
1801
|
};
|
|
1526
1802
|
if (flags.json) return out(list);
|
|
1527
1803
|
if (flags.quiet) return;
|
|
1528
|
-
const
|
|
1804
|
+
const designs = list.designs ?? [];
|
|
1529
1805
|
console.log(
|
|
1530
|
-
`${project} · ${
|
|
1806
|
+
`${project} · ${designs.length} design${designs.length === 1 ? "" : "s"}` +
|
|
1531
1807
|
(list.open
|
|
1532
|
-
? ` · ${list.open} open
|
|
1808
|
+
? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
|
|
1533
1809
|
: " · nothing waiting on the user"),
|
|
1534
1810
|
);
|
|
1535
|
-
for (const m of
|
|
1811
|
+
for (const m of designs) {
|
|
1536
1812
|
const states = m.states.length ? m.states.join(" / ") : "single state";
|
|
1537
1813
|
console.log(
|
|
1538
|
-
` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.
|
|
1814
|
+
` ${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` : ""}`,
|
|
1539
1815
|
);
|
|
1540
1816
|
}
|
|
1541
1817
|
for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
|
|
1542
1818
|
for (const line of pendingLines(list.pending)) console.log(line);
|
|
1819
|
+
if (list.decisionList) {
|
|
1820
|
+
console.log(
|
|
1821
|
+
`Decided, ${list.decisionList.total}. Treat as settled unless the user says otherwise:`,
|
|
1822
|
+
);
|
|
1823
|
+
for (const d of list.decisionList.lines) console.log(` ${d.design}: ${d.text}`);
|
|
1824
|
+
if (list.decisionList.more) console.log(` ${list.decisionList.more} (--decisions)`);
|
|
1825
|
+
}
|
|
1543
1826
|
},
|
|
1544
1827
|
async export() {
|
|
1545
1828
|
const { values: flags } = parse({
|
|
1546
1829
|
options: {
|
|
1547
|
-
|
|
1830
|
+
design: { type: "string" },
|
|
1548
1831
|
state: { type: "string" },
|
|
1549
1832
|
variant: { type: "string" },
|
|
1550
1833
|
project: { type: "string" },
|
|
1551
1834
|
out: { type: "string" },
|
|
1552
1835
|
},
|
|
1553
1836
|
});
|
|
1554
|
-
const slug =
|
|
1837
|
+
const slug = requireDesign(flags, "mockpit export --design writer");
|
|
1555
1838
|
const project = resolveProject(flags).name;
|
|
1556
1839
|
const data = await api(
|
|
1557
|
-
`${
|
|
1840
|
+
`${designPath(slug)}/export${query({ project, state: flags.state, variant: flags.variant })}`,
|
|
1558
1841
|
{},
|
|
1559
1842
|
{ report: "die", fix: `mockpit read ${slug}` },
|
|
1560
1843
|
);
|
|
@@ -1564,8 +1847,8 @@ const commands = {
|
|
|
1564
1847
|
const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
|
|
1565
1848
|
mkdirSync(dir, { recursive: true });
|
|
1566
1849
|
writeFileSync(join(dir, "index.html"), entry.html ?? "");
|
|
1567
|
-
// The
|
|
1568
|
-
if (entry.
|
|
1850
|
+
// The state with every fill, when index.html is the composed choice.
|
|
1851
|
+
if (entry.allFills) writeFileSync(join(dir, "all-fills.html"), entry.allFills);
|
|
1569
1852
|
const decided = data.decisions?.states?.[entry.state ?? ""];
|
|
1570
1853
|
if (decided) {
|
|
1571
1854
|
writeFileSync(
|
|
@@ -1578,7 +1861,7 @@ const commands = {
|
|
|
1578
1861
|
JSON.stringify(
|
|
1579
1862
|
{
|
|
1580
1863
|
project,
|
|
1581
|
-
|
|
1864
|
+
design: slug,
|
|
1582
1865
|
state: entry.state,
|
|
1583
1866
|
variant: entry.variant,
|
|
1584
1867
|
version: entry.version,
|
|
@@ -1646,29 +1929,53 @@ const commands = {
|
|
|
1646
1929
|
},
|
|
1647
1930
|
async watch() {
|
|
1648
1931
|
const { values: flags } = parse({
|
|
1649
|
-
options: {
|
|
1932
|
+
options: {
|
|
1933
|
+
session: { type: "string" },
|
|
1934
|
+
after: { type: "string" },
|
|
1935
|
+
once: { type: "boolean" },
|
|
1936
|
+
"exit-code": { type: "string" },
|
|
1937
|
+
},
|
|
1650
1938
|
});
|
|
1651
1939
|
if (flags.after !== undefined && !/^\d+$/.test(flags.after)) {
|
|
1652
1940
|
fail(`--after must be a number (got "${flags.after}")`);
|
|
1653
1941
|
}
|
|
1942
|
+
const exitCode = flags["exit-code"] ?? "0";
|
|
1943
|
+
if (!/^\d+$/.test(exitCode)) fail(`--exit-code must be a number (got "${exitCode}")`);
|
|
1944
|
+
// A waiter outliving its agent would take a Send and tell no one, and each
|
|
1945
|
+
// Send is delivered only once.
|
|
1946
|
+
if (flags.once) {
|
|
1947
|
+
const parent = process.ppid;
|
|
1948
|
+
setInterval(() => {
|
|
1949
|
+
if (process.ppid !== parent || process.ppid === 1) process.exit(0);
|
|
1950
|
+
}, 5000).unref();
|
|
1951
|
+
}
|
|
1654
1952
|
// A continuous long-poll that streams each piece of feedback as one line —
|
|
1655
1953
|
// one line is one monitor notification. It re-arms forever; a transient
|
|
1656
1954
|
// network error backs off and retries rather than exiting.
|
|
1657
1955
|
//
|
|
1658
|
-
//
|
|
1659
|
-
//
|
|
1660
|
-
//
|
|
1956
|
+
// It only says a reply landed (peek): the reply stays on the session's cursor
|
|
1957
|
+
// until the agent's feedback or next write takes it, so a harness dropping or
|
|
1958
|
+
// cutting this output can't lose it, and each Send is still delivered once.
|
|
1661
1959
|
let firstAfter = flags.after;
|
|
1662
1960
|
for (;;) {
|
|
1663
|
-
const
|
|
1961
|
+
const key = harnessKey();
|
|
1962
|
+
// The state file is per process and cwd, which several threads of one harness
|
|
1963
|
+
// can share, so a conversation's own key decides whenever there is one.
|
|
1964
|
+
const session =
|
|
1965
|
+
flags.session ??
|
|
1966
|
+
process.env.MOCKPIT_SESSION ??
|
|
1967
|
+
(key
|
|
1968
|
+
? await resolveSessionByKey(key)
|
|
1969
|
+
: ((await resolveSession(flags)) ?? (await resolveSessionByCwd())));
|
|
1664
1970
|
if (!session) {
|
|
1665
|
-
|
|
1971
|
+
// A conversation that hasn't used mockpit yet may never; ask rarely.
|
|
1972
|
+
await sleep(key ? 10_000 : 2000);
|
|
1666
1973
|
continue;
|
|
1667
1974
|
}
|
|
1668
1975
|
let result;
|
|
1669
1976
|
try {
|
|
1670
1977
|
const res = await fetch(
|
|
1671
|
-
`${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60 })}`,
|
|
1978
|
+
`${BASE}/api/comments${query({ session, author: "user", after: firstAfter, wait: 60, peek: 1 })}`,
|
|
1672
1979
|
{ headers: HEADERS },
|
|
1673
1980
|
);
|
|
1674
1981
|
if (!res.ok) {
|
|
@@ -1681,20 +1988,26 @@ const commands = {
|
|
|
1681
1988
|
continue;
|
|
1682
1989
|
}
|
|
1683
1990
|
firstAfter = undefined;
|
|
1684
|
-
|
|
1685
|
-
|
|
1991
|
+
const lines = (result.feedback ?? []).flatMap(watchLines);
|
|
1992
|
+
for (const line of lines) console.log(line);
|
|
1993
|
+
if (flags.once && lines.length) {
|
|
1994
|
+
// A harness may frame this output as a hook failure, so it says what it is.
|
|
1995
|
+
console.log(
|
|
1996
|
+
"This is the user's reply from the mockpit viewer, not an error. Call mockpit's feedback for all of it.",
|
|
1997
|
+
);
|
|
1998
|
+
process.exit(Number(exitCode));
|
|
1686
1999
|
}
|
|
1687
2000
|
}
|
|
1688
2001
|
},
|
|
1689
2002
|
|
|
1690
2003
|
async say() {
|
|
1691
|
-
const { values: flags, positionals } = parse({ allowPositionals: true, options:
|
|
2004
|
+
const { values: flags, positionals } = parse({ allowPositionals: true, options: DESIGN_FLAGS });
|
|
1692
2005
|
const text = positionals.join(" ").trim();
|
|
1693
|
-
if (!text) die("say needs text", 'mockpit say "…" --
|
|
1694
|
-
const slug =
|
|
2006
|
+
if (!text) die("say needs text", 'mockpit say "…" --design writer');
|
|
2007
|
+
const slug = requireDesign(flags, `mockpit say "${text}" --design writer`);
|
|
1695
2008
|
const session = await resolveSession(flags);
|
|
1696
2009
|
const result = await api(
|
|
1697
|
-
`${
|
|
2010
|
+
`${designPath(slug)}/say`,
|
|
1698
2011
|
{
|
|
1699
2012
|
method: "POST",
|
|
1700
2013
|
body: JSON.stringify({
|
|
@@ -1712,6 +2025,169 @@ const commands = {
|
|
|
1712
2025
|
console.log(`said on ${slug}`);
|
|
1713
2026
|
printFeedback(result.feedback);
|
|
1714
2027
|
},
|
|
2028
|
+
async move() {
|
|
2029
|
+
const { values: flags } = parse({
|
|
2030
|
+
options: {
|
|
2031
|
+
...DESIGN_FLAGS,
|
|
2032
|
+
variant: { type: "string", multiple: true },
|
|
2033
|
+
to: { type: "string" },
|
|
2034
|
+
"to-state": { type: "string" },
|
|
2035
|
+
archive: { type: "boolean" },
|
|
2036
|
+
restore: { type: "boolean" },
|
|
2037
|
+
"hide-older": { type: "boolean" },
|
|
2038
|
+
"delete-older": { type: "boolean" },
|
|
2039
|
+
},
|
|
2040
|
+
});
|
|
2041
|
+
const example = "mockpit move --design writer --variant quiet --archive";
|
|
2042
|
+
const slug = requireDesign(flags, example);
|
|
2043
|
+
const tidying = flags["hide-older"] || flags["delete-older"];
|
|
2044
|
+
if (!flags.variant?.length && !tidying) die("move needs --variant", example);
|
|
2045
|
+
const session = await resolveSession(flags);
|
|
2046
|
+
const result = await api(
|
|
2047
|
+
`${designPath(slug)}/move`,
|
|
2048
|
+
{
|
|
2049
|
+
method: "POST",
|
|
2050
|
+
body: JSON.stringify({
|
|
2051
|
+
state: flags.state,
|
|
2052
|
+
variants: flags.variant,
|
|
2053
|
+
to: flags.to,
|
|
2054
|
+
toState: flags["to-state"],
|
|
2055
|
+
archive: flags.archive,
|
|
2056
|
+
restore: flags.restore,
|
|
2057
|
+
hideOlder: flags["hide-older"],
|
|
2058
|
+
deleteOlder: flags["delete-older"],
|
|
2059
|
+
project: resolveProject(flags).name,
|
|
2060
|
+
session: session ?? undefined,
|
|
2061
|
+
}),
|
|
2062
|
+
},
|
|
2063
|
+
{ report: "die", fix: `mockpit read ${slug}` },
|
|
2064
|
+
);
|
|
2065
|
+
if (flags.json) return out(result);
|
|
2066
|
+
if (flags.quiet) return;
|
|
2067
|
+
if (tidying) {
|
|
2068
|
+
const verb = flags["delete-older"] ? "deleted" : "hid";
|
|
2069
|
+
console.log(`${verb} ${result.versions.length} older versions`);
|
|
2070
|
+
for (const s of result.skipped) {
|
|
2071
|
+
const which = s.version ? ` v${s.version}` : "";
|
|
2072
|
+
console.log(`stayed: ${s.state ? `${s.state}/` : ""}${s.variant}${which} (${s.why})`);
|
|
2073
|
+
}
|
|
2074
|
+
printFeedback(result.feedback);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const names = result.variants.map((v) => v.variant).join(", ") || "nothing";
|
|
2078
|
+
const done = result.to
|
|
2079
|
+
? `moved to ${result.to.design}`
|
|
2080
|
+
: flags.restore
|
|
2081
|
+
? "restored"
|
|
2082
|
+
: "archived";
|
|
2083
|
+
console.log(`${done}: ${names}`);
|
|
2084
|
+
for (const s of result.skipped) console.log(`stayed: ${s.variant} (${s.why})`);
|
|
2085
|
+
printFeedback(result.feedback);
|
|
2086
|
+
},
|
|
2087
|
+
async edit() {
|
|
2088
|
+
const { values: flags } = parse({
|
|
2089
|
+
options: {
|
|
2090
|
+
design: { type: "string" },
|
|
2091
|
+
project: { type: "string" },
|
|
2092
|
+
session: { type: "string" },
|
|
2093
|
+
slug: { type: "string" },
|
|
2094
|
+
title: { type: "string" },
|
|
2095
|
+
kind: { type: "string" },
|
|
2096
|
+
area: { type: "string" },
|
|
2097
|
+
source: { type: "string" },
|
|
2098
|
+
"first-state": { type: "string" },
|
|
2099
|
+
notes: { type: "string" },
|
|
2100
|
+
rename: { type: "string" },
|
|
2101
|
+
"area-order": { type: "string" },
|
|
2102
|
+
},
|
|
2103
|
+
});
|
|
2104
|
+
const project = resolveProject(flags).name;
|
|
2105
|
+
const session = (await resolveSession(flags)) ?? undefined;
|
|
2106
|
+
const areaOrder = flags["area-order"];
|
|
2107
|
+
if (flags.rename !== undefined || areaOrder !== undefined) {
|
|
2108
|
+
if (flags.design) {
|
|
2109
|
+
die(
|
|
2110
|
+
"--rename and --area-order edit the project; edit the design without them",
|
|
2111
|
+
"mockpit edit --help",
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
if (flags.rename !== undefined && !flags.rename.trim()) {
|
|
2115
|
+
die("--rename needs the new name", `mockpit edit --rename <name>`);
|
|
2116
|
+
}
|
|
2117
|
+
const result = await api(
|
|
2118
|
+
projectPath(project),
|
|
2119
|
+
{
|
|
2120
|
+
method: "PATCH",
|
|
2121
|
+
body: JSON.stringify({
|
|
2122
|
+
name: flags.rename,
|
|
2123
|
+
...(areaOrder === undefined ? {} : { areaOrder: areaOrder.split(",") }),
|
|
2124
|
+
session,
|
|
2125
|
+
}),
|
|
2126
|
+
},
|
|
2127
|
+
{ report: "die", fix: "mockpit read" },
|
|
2128
|
+
);
|
|
2129
|
+
if (flags.json) return out(result);
|
|
2130
|
+
if (flags.quiet) return;
|
|
2131
|
+
if (result.areaOrder) {
|
|
2132
|
+
console.log(
|
|
2133
|
+
`area order ${result.areaOrder.length ? result.areaOrder.join(", ") : "alphabetical"} · ${result.url}`,
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
2137
|
+
if (!result.renamed) return printFeedback(result.feedback);
|
|
2138
|
+
console.log(
|
|
2139
|
+
`renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.designs} design${result.designs === 1 ? "" : "s"} · ${result.url}`,
|
|
2140
|
+
);
|
|
2141
|
+
if (!flags.project && !process.env.MOCKPIT_PROJECT) {
|
|
2142
|
+
console.log(
|
|
2143
|
+
`note: this repo still resolves to "${project}"; pass --project ${shq(result.project)} or set MOCKPIT_PROJECT`,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
printFeedback(result.feedback);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const slug = requireDesign(flags, "mockpit edit --design writer --area Shell");
|
|
2150
|
+
const fields = ["slug", "title", "kind", "area", "source", "notes"].filter(
|
|
2151
|
+
(k) => flags[k] !== undefined,
|
|
2152
|
+
);
|
|
2153
|
+
if (!fields.length && flags["first-state"] === undefined) {
|
|
2154
|
+
die(
|
|
2155
|
+
"edit needs --slug, --title, --kind, --area, --source, --notes or --first-state",
|
|
2156
|
+
"mockpit edit --help",
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
const result = await api(
|
|
2160
|
+
designPath(slug),
|
|
2161
|
+
{
|
|
2162
|
+
method: "PATCH",
|
|
2163
|
+
body: JSON.stringify({
|
|
2164
|
+
project,
|
|
2165
|
+
session,
|
|
2166
|
+
...Object.fromEntries(fields.map((k) => [k, flags[k]])),
|
|
2167
|
+
firstState: flags["first-state"],
|
|
2168
|
+
}),
|
|
2169
|
+
},
|
|
2170
|
+
{ report: "die", fix: `mockpit read ${slug}` },
|
|
2171
|
+
);
|
|
2172
|
+
if (flags.json) return out(result);
|
|
2173
|
+
if (flags.quiet) return;
|
|
2174
|
+
const m = result.design;
|
|
2175
|
+
console.log(
|
|
2176
|
+
[
|
|
2177
|
+
result.renamed ? `${result.renamed.from} → ${m.slug}` : m.slug,
|
|
2178
|
+
m.kind,
|
|
2179
|
+
m.area && `area ${m.area}`,
|
|
2180
|
+
m.source && `source ${m.source}`,
|
|
2181
|
+
result.notes && (result.notes.applied ? "notes saved" : "notes NOT applied"),
|
|
2182
|
+
result.url,
|
|
2183
|
+
]
|
|
2184
|
+
.filter(Boolean)
|
|
2185
|
+
.join(" · "),
|
|
2186
|
+
);
|
|
2187
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
2188
|
+
if (result.notes && !result.notes.applied) console.log(`notes: ${result.notes.text}`);
|
|
2189
|
+
printFeedback(result.feedback);
|
|
2190
|
+
},
|
|
1715
2191
|
async demo() {
|
|
1716
2192
|
parse();
|
|
1717
2193
|
const { DEMO } = await import("./demoData.js");
|
|
@@ -1723,12 +2199,13 @@ const commands = {
|
|
|
1723
2199
|
});
|
|
1724
2200
|
for (const state of DEMO.states) {
|
|
1725
2201
|
for (const variant of DEMO.variants) {
|
|
1726
|
-
await api("/api/
|
|
2202
|
+
await api("/api/designs", {
|
|
1727
2203
|
method: "POST",
|
|
1728
2204
|
body: JSON.stringify({
|
|
1729
2205
|
session: session.id,
|
|
1730
2206
|
project: DEMO.project,
|
|
1731
|
-
|
|
2207
|
+
design: DEMO.slug,
|
|
2208
|
+
kind: "screen",
|
|
1732
2209
|
title: DEMO.title,
|
|
1733
2210
|
state: state.label,
|
|
1734
2211
|
variant: variant.name,
|
|
@@ -1738,7 +2215,7 @@ const commands = {
|
|
|
1738
2215
|
});
|
|
1739
2216
|
}
|
|
1740
2217
|
}
|
|
1741
|
-
await api(`${
|
|
2218
|
+
await api(`${designPath(DEMO.slug)}/asks`, {
|
|
1742
2219
|
method: "POST",
|
|
1743
2220
|
body: JSON.stringify({ project: DEMO.project, session: session.id, asks: DEMO.asks }),
|
|
1744
2221
|
});
|
|
@@ -1791,14 +2268,14 @@ const commands = {
|
|
|
1791
2268
|
if (sub === "add") {
|
|
1792
2269
|
const result = await installIconSets(project, sets);
|
|
1793
2270
|
if (result.errors.length) fail(result.errors.map((e) => e.message).join("; "));
|
|
1794
|
-
if (flags.json) return out(result.
|
|
2271
|
+
if (flags.json) return out(result.designSystem);
|
|
1795
2272
|
for (const a of result.added) {
|
|
1796
2273
|
if (!flags.quiet) console.log(`added ${a.prefix} (${a.count} icons)`);
|
|
1797
2274
|
}
|
|
1798
2275
|
return;
|
|
1799
2276
|
}
|
|
1800
2277
|
if (sub === "remove") {
|
|
1801
|
-
const stored = await api(`${projectPath(project)}/design`);
|
|
2278
|
+
const stored = await api(`${projectPath(project)}/design-system`);
|
|
1802
2279
|
const installed = stored?.iconSets ?? [];
|
|
1803
2280
|
for (const set of sets) {
|
|
1804
2281
|
if (installed.some((s) => s.prefix === set)) continue;
|
|
@@ -1808,14 +2285,14 @@ const commands = {
|
|
|
1808
2285
|
: `${set} is not installed for ${project}`,
|
|
1809
2286
|
);
|
|
1810
2287
|
}
|
|
1811
|
-
const
|
|
2288
|
+
const designSystem = await api(`${projectPath(project)}/design-system`, {
|
|
1812
2289
|
method: "PUT",
|
|
1813
2290
|
body: JSON.stringify({
|
|
1814
2291
|
...stored,
|
|
1815
2292
|
iconSets: installed.filter((s) => !sets.includes(s.prefix)),
|
|
1816
2293
|
}),
|
|
1817
2294
|
});
|
|
1818
|
-
if (flags.json) return out(
|
|
2295
|
+
if (flags.json) return out(designSystem);
|
|
1819
2296
|
if (!flags.quiet) console.log(`removed ${sets.join(", ")}`);
|
|
1820
2297
|
return;
|
|
1821
2298
|
}
|
|
@@ -1842,13 +2319,13 @@ const commands = {
|
|
|
1842
2319
|
const { name: project } = resolveProject(flags);
|
|
1843
2320
|
if ((sub !== "add" && sub !== "remove") || !id)
|
|
1844
2321
|
fail("usage: mockpit kit add <id> --url <url> --doc <file|text> | mockpit kit remove <id>");
|
|
1845
|
-
const
|
|
2322
|
+
const designSystem =
|
|
1846
2323
|
sub === "add"
|
|
1847
2324
|
? await putProjectKit(project, id, flags)
|
|
1848
2325
|
: await api(`${projectPath(project)}/kits/${encodeURIComponent(id)}`, {
|
|
1849
2326
|
method: "DELETE",
|
|
1850
2327
|
});
|
|
1851
|
-
if (flags.json) return out(
|
|
2328
|
+
if (flags.json) return out(designSystem);
|
|
1852
2329
|
if (!flags.quiet) console.log(`${sub === "add" ? "added" : "removed"} kit ${id} (${project})`);
|
|
1853
2330
|
},
|
|
1854
2331
|
|
|
@@ -1860,6 +2337,24 @@ const commands = {
|
|
|
1860
2337
|
return printBrief(flags);
|
|
1861
2338
|
},
|
|
1862
2339
|
|
|
2340
|
+
// A rules file is the user's or the project's own guidance on top of the
|
|
2341
|
+
// core brief; the brief prints it, so this only has to put it on the server.
|
|
2342
|
+
async rules() {
|
|
2343
|
+
const { values: flags } = parse({
|
|
2344
|
+
options: { mine: { type: "boolean" }, project: { type: "string" } },
|
|
2345
|
+
});
|
|
2346
|
+
const mine = !!flags.mine;
|
|
2347
|
+
const file = mine ? myRulesFile() : join(repoRoot(), ".mockpit", "rules.md");
|
|
2348
|
+
const project = mine ? null : resolveProject(flags).name;
|
|
2349
|
+
const result = await sendRules(file, mine ? "/api/rules" : `${projectPath(project)}/rules`);
|
|
2350
|
+
if (flags.json) return out(result);
|
|
2351
|
+
if (flags.quiet) return;
|
|
2352
|
+
const where = mine ? "your rules" : `project ${project}`;
|
|
2353
|
+
const from = result.missing ? `no ${file}` : file;
|
|
2354
|
+
console.log(`${where}: ${rulesSummary(result)} (from ${from})`);
|
|
2355
|
+
if (!result.missing) console.log("next:", "mockpit guide");
|
|
2356
|
+
},
|
|
2357
|
+
|
|
1863
2358
|
async setup() {
|
|
1864
2359
|
parse();
|
|
1865
2360
|
console.log(await fetchTextWithFallback("/setup", join(ROOT, "guide", "AGENT_SETUP.md")));
|