mockpit 1.2.0 → 2.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 +69 -48
- package/bin/demoData.js +3 -3
- package/bin/mockpit.js +426 -51
- package/dist/mcp/server.js +45 -7
- package/dist/server/apiViews.d.ts +407 -21
- package/dist/server/apiViews.js +110 -11
- package/dist/server/app.d.ts +5 -1
- package/dist/server/app.js +1454 -192
- package/dist/server/codeWorker.js +2 -1
- package/dist/server/decisions.d.ts +23 -0
- package/dist/server/decisions.js +100 -0
- package/dist/server/designGuide.d.ts +6 -2
- package/dist/server/designGuide.js +96 -27
- package/dist/server/embeds.d.ts +26 -0
- package/dist/server/embeds.js +144 -0
- package/dist/server/eventLog.js +2 -1
- package/dist/server/feedbackBatch.d.ts +15 -4
- package/dist/server/feedbackBatch.js +31 -7
- package/dist/server/fills.d.ts +41 -0
- package/dist/server/fills.js +328 -0
- package/dist/server/guidance.d.ts +23 -0
- package/dist/server/guidance.js +71 -0
- package/dist/server/index.js +25 -2
- package/dist/server/knobs.d.ts +6 -2
- package/dist/server/knobs.js +11 -1
- package/dist/server/mcpHttp.d.ts +6 -0
- package/dist/server/mcpHttp.js +35 -0
- package/dist/server/mcpSpec.d.ts +14 -13
- package/dist/server/mcpSpec.js +133 -18
- package/dist/server/postSurfaces.js +6 -12
- package/dist/server/richRender.js +3 -1
- package/dist/server/run.js +10 -1
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +18 -13
- package/dist/server/sqlStore.d.ts +97 -5
- package/dist/server/sqlStore.js +367 -27
- package/dist/server/surfacePage.d.ts +6 -2
- package/dist/server/surfacePage.js +142 -10
- package/dist/server/themes.d.ts +2 -0
- package/dist/server/themes.js +23 -3
- package/dist/server/types.d.ts +107 -16
- package/dist/server/types.js +73 -10
- package/dist/server/viewerRequest.d.ts +2 -0
- package/dist/server/viewerRequest.js +14 -0
- package/extensions/mockpit.js +179 -16
- package/guide/packs/design-craft.md +35 -0
- package/guide/topics/asks.md +82 -31
- package/guide/topics/feedback.md +151 -43
- package/guide/topics/html.md +8 -10
- package/guide/topics/http.md +8 -4
- package/guide/topics/knobs.md +4 -3
- package/guide/topics/scripts.md +2 -2
- package/guide/topics/surfaces.md +45 -11
- package/package.json +3 -15
- package/skills/launch-video/SKILL.md +2 -2
- package/skills/mockpit/SKILL.md +23 -7
- package/viewer/dist/index.html +9 -8
- package/dist/server/slots.d.ts +0 -14
- package/dist/server/slots.js +0 -71
package/bin/mockpit.js
CHANGED
|
@@ -9,7 +9,7 @@ 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";
|
|
@@ -29,23 +29,23 @@ const HEADERS = {
|
|
|
29
29
|
const HELP = `mockpit — a live visual surface for terminal coding agents
|
|
30
30
|
|
|
31
31
|
vocabulary: project › mock › state › variant › version. A project is a repo; a
|
|
32
|
-
mock is
|
|
32
|
+
mock is a sketch, page or component, by slug; a state is one thing the user sees in it ("Lab
|
|
33
33
|
open"); a variant is a parallel design of a state; a version is its history.
|
|
34
|
-
|
|
34
|
+
Declare the parts you want feedback on with data-part="name" in the html.
|
|
35
35
|
|
|
36
36
|
the loop (never blocks on the user):
|
|
37
37
|
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
|
|
38
|
+
2. mockpit publish once per variant; declare parts with data-part.
|
|
39
|
+
3. A choice is several variants plus one question that binds them: mockpit ask
|
|
40
40
|
with options bound to variants. A variant decides everything it differs
|
|
41
41
|
on; don't re-ask it. Visual options carry a variant or knob set. One
|
|
42
42
|
render plus a control: --knobs.
|
|
43
43
|
4. Tell the user in one line where to look, then end your turn. The question
|
|
44
|
-
itself lives in the mock
|
|
44
|
+
itself lives in the mock, never in chat. The user answers in the
|
|
45
45
|
browser at their own pace. Never poll.
|
|
46
46
|
5. When the user says they answered (or a mockpit watch line wakes you), run
|
|
47
47
|
mockpit feedback. Every write also returns feedback; read it.
|
|
48
|
-
6. mockpit publish the revision; mockpit export once a variant is
|
|
48
|
+
6. mockpit publish the revision; mockpit export once a variant is in use.
|
|
49
49
|
In Claude Code, arm mockpit watch under Monitor after asking, then end the
|
|
50
50
|
turn; elsewhere the user's next message is the wake-up.
|
|
51
51
|
|
|
@@ -56,32 +56,45 @@ verbs:
|
|
|
56
56
|
mockpit publish --mock <slug> --html <file> [options]
|
|
57
57
|
create or version one variant of one state
|
|
58
58
|
--state <label> state, in the user's words (omit for a single-state mock)
|
|
59
|
+
--situation <s> another render of that state ("Couldn't save", "Loading")
|
|
59
60
|
--variant <name> variant label (default "default")
|
|
60
61
|
--html <file|-> the html body (or a positional file)
|
|
61
62
|
--surfaces <json|file> instead: the full ordered surface list; {"id"}
|
|
62
63
|
alone keeps a surface, a missing id removes it
|
|
63
64
|
--parts <name=file|-> instead: replace just that data-part's element in
|
|
64
65
|
the latest version (repeatable; name#key targets one instance)
|
|
66
|
+
--fills <part#fill=file|-> instead: replace or add one fill of a part in
|
|
67
|
+
the latest version (repeatable)
|
|
65
68
|
--title <t> mock title
|
|
66
|
-
--kind <k> component
|
|
69
|
+
--kind <k> page|component, only for an app's screens (default sketch)
|
|
70
|
+
--area <name> where in the product it belongs ("Shell"); Home groups by it
|
|
71
|
+
--source <path> the code path that implements it
|
|
67
72
|
--knobs <json|file> knobs in tunekit usePane shape, keyed by path
|
|
68
73
|
--from <N> branch from version N
|
|
69
74
|
--prompt <text> what prompted this version
|
|
75
|
+
--note <text> one line on what this variant is, shown beside it
|
|
76
|
+
--widths <list> device widths to show it at, e.g. 390,1280
|
|
77
|
+
--flow <name> [--step <N>] make this state step N of a flow (0 takes
|
|
78
|
+
it out); with no html it only places the state
|
|
70
79
|
--project <name> project (default: git remote, else directory name)
|
|
71
80
|
--kit <id> opt the html surface into a kit (repeatable; see "mockpit kits")
|
|
81
|
+
--poster <file> a still of the page (png, jpg); Studio and Home show it
|
|
82
|
+
instead of running the page. Send one with every version
|
|
83
|
+
that changes the page, or it goes
|
|
72
84
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file|->
|
|
73
85
|
add a surface of that kind after the html (repeatable;
|
|
74
86
|
--data is a JSON tree)
|
|
75
87
|
mockpit ask --mock <slug> "<question>" --option <label[=variant]> ...
|
|
76
88
|
ask the user; bind options to variants
|
|
77
89
|
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
|
|
78
|
-
"=knob:path=value" sets one knob
|
|
90
|
+
"=variant@3" to its version 3, "=knob:path=value" sets one knob
|
|
91
|
+
--recommend <l> the option you would pick, by label; a quiet mark on it
|
|
79
92
|
--scope <s> mock|state|part (default mock)
|
|
80
|
-
--state <label> the state a state-scoped
|
|
81
|
-
--part <name> the part a part-scoped
|
|
93
|
+
--state <label> the state a state-scoped question is about
|
|
94
|
+
--part <name> the part a part-scoped question is about
|
|
82
95
|
--multi allow several answers
|
|
83
|
-
--id <id> stable id; reusing one replaces that
|
|
84
|
-
--asks <json|file> several
|
|
96
|
+
--id <id> stable id; reusing one replaces that question
|
|
97
|
+
--asks <json|file> several questions at once, full shape
|
|
85
98
|
mockpit read [slug] [--body] [--history]
|
|
86
99
|
no slug: every mock plus what the user
|
|
87
100
|
is doing now; a slug: that mock
|
|
@@ -89,12 +102,20 @@ verbs:
|
|
|
89
102
|
plus pending; returns at once
|
|
90
103
|
mockpit say "<text>" --mock <slug> [--state s] [--variant v]
|
|
91
104
|
a plain-text message in a mock's thread
|
|
105
|
+
mockpit edit --mock <slug> [--slug s] [--title t] [--kind k] [--area a] [--source p]
|
|
106
|
+
change a mock without a new version ("" clears)
|
|
107
|
+
mockpit edit --project <name> --rename <new>
|
|
108
|
+
rename a project
|
|
109
|
+
mockpit edit --project <name> --area-order "Shell,Food"
|
|
110
|
+
the order Home lists areas in
|
|
92
111
|
mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
|
|
93
|
-
write the
|
|
112
|
+
write the html in use + history per state
|
|
94
113
|
to .mockpit/accepted/<mock>/<state>/
|
|
95
114
|
mockpit upload <file> [--kind image|file]
|
|
96
115
|
upload an asset, print its id and URL
|
|
97
116
|
mockpit guide [--topic <id>] the project-aware brief, or one reference topic
|
|
117
|
+
mockpit rules [--mine] send .mockpit/rules.md (or, with --mine, your
|
|
118
|
+
~/.mockpit/rules.md) to the server for the brief
|
|
98
119
|
mockpit run <file|-> run a script against the mockpit API on the
|
|
99
120
|
server (mockpit guide --topic scripts)
|
|
100
121
|
mockpit watch [--session <id>] stream user feedback, one line per Send
|
|
@@ -142,6 +163,15 @@ const COMMAND_HELP = {
|
|
|
142
163
|
(gitignored). --kit makes a bundled or project kit the default for every html
|
|
143
164
|
surface. --kit-url defines the project's own kit (id from --kit, else
|
|
144
165
|
"project") and makes it the default; the doc is the cheat sheet the brief prints.`,
|
|
166
|
+
rules: `mockpit rules [--project <name>]
|
|
167
|
+
mockpit rules --mine
|
|
168
|
+
Guidance on top of the core brief, highest first: the project's
|
|
169
|
+
.mockpit/rules.md (committed with the repo), your ~/.mockpit/rules.md, then
|
|
170
|
+
the packs either names (\`packs: [design-craft]\` in its frontmatter). A file
|
|
171
|
+
has "## Rules" (always followed) and "## Suggestions" (aimed for).
|
|
172
|
+
This sends the file as it is on disk; a missing file clears the stored copy.
|
|
173
|
+
init sends the project's too. A local server reads yours live, so --mine is
|
|
174
|
+
for a deployed one.`,
|
|
145
175
|
kit: `mockpit kit add <id> --url <https url> --doc <file.md|text> [--script <url>]
|
|
146
176
|
mockpit kit remove <id>
|
|
147
177
|
A project kit is a stylesheet (and optional script) on the CDN allowlist plus a
|
|
@@ -156,42 +186,84 @@ mockpit icons remove <set> [<set>...]
|
|
|
156
186
|
publish: `mockpit publish --mock <slug> --html <file> [options]
|
|
157
187
|
mockpit publish --mock <slug> --surfaces <json|file> [options]
|
|
158
188
|
mockpit publish --mock <slug> --parts <name=file|-> ... [options]
|
|
189
|
+
mockpit publish --mock <slug> --fills <part#fill=file|-> ... [options]
|
|
159
190
|
Creates the mock, state or variant, or the next version of an existing one.
|
|
160
191
|
--state <label> state in the user's words (omit for a single-state mock)
|
|
192
|
+
--situation <s> another render of that state ("Couldn't save", "Loading"),
|
|
193
|
+
after the state itself is published
|
|
161
194
|
--variant <name> variant label (default "default")
|
|
162
195
|
--surfaces <json|file> the full ordered list: {"id"} alone keeps a surface,
|
|
163
196
|
a missing id removes it, order is the list order
|
|
164
197
|
--parts <name=file|-> replace only the element carrying data-part="name"
|
|
165
198
|
(name#key for one instance) in the latest version; repeatable
|
|
199
|
+
--fills <part#fill=file|-> replace the element carrying data-part="part"
|
|
200
|
+
data-fill="fill" in the latest version, or add it after
|
|
201
|
+
that part's other fills; the marks are added if missing
|
|
166
202
|
--title <t> mock title
|
|
167
|
-
--kind <k> component|page
|
|
203
|
+
--kind <k> page|component|sketch: page for a screen of the product,
|
|
204
|
+
component for a piece pages embed (embedding makes one);
|
|
205
|
+
omit for sketches, diagrams, reviews (default sketch)
|
|
206
|
+
--area <name> where in the product it belongs, in the project's words
|
|
207
|
+
("Shell", "Food"); Home groups each tab by it. "" clears
|
|
208
|
+
--source <path> the code path that implements it (apps/web/src/shell). "" clears
|
|
168
209
|
--knobs <json|file> knobs in tunekit usePane shape
|
|
169
210
|
--from <N> branch from version N
|
|
211
|
+
--fork <v[@N]> start this new variant from sibling v (its version N, else
|
|
212
|
+
its latest); alone it copies v, with --parts/--fills it
|
|
213
|
+
edits the copy
|
|
170
214
|
--prompt <text> what prompted this version
|
|
215
|
+
--note <text> one line on what this variant is, shown beside it (never
|
|
216
|
+
inside the design)
|
|
217
|
+
--widths <list> device widths the mock is judged at, side by side, e.g.
|
|
218
|
+
390,1280 (the ones the user ships to)
|
|
219
|
+
--flow <name> a flow this state is a step of: the states the user moves
|
|
220
|
+
between, in order, across mocks; without html it only
|
|
221
|
+
places the existing state
|
|
222
|
+
--step <N> its 1-based place in the flow (moves it if it is there);
|
|
223
|
+
0 takes it out; omitted, it joins at the end
|
|
171
224
|
--project <name> project (default: git remote, else directory name)
|
|
225
|
+
--poster <file> a still of the page (png, jpg) that Studio and Home show
|
|
226
|
+
instead of running it; with --parts or --fills it is the
|
|
227
|
+
still of the spliced page. A version that changes the page
|
|
228
|
+
without one drops the old still
|
|
172
229
|
--md/--mermaid/--diff/--terminal/--data/--code/--image <file> more surfaces
|
|
173
230
|
--json / --quiet
|
|
174
231
|
Prints the parts found per state, parts that vanished or were renamed,
|
|
175
|
-
nudges, a ready-to-run "mockpit ask" when variants have no
|
|
232
|
+
nudges, a ready-to-run "mockpit ask" when variants have no question, and feedback.`,
|
|
176
233
|
ask: `mockpit ask --mock <slug> "<question>" --option <label[=variant]> ... [--scope mock|state|part]
|
|
177
|
-
[--state s] [--part p] [--multi] [--id id]
|
|
234
|
+
[--state s] [--part p] [--multi] [--id id] [--after <ask id>] [--recommend <label>]
|
|
178
235
|
mockpit ask --mock <slug> --asks <json|file>
|
|
179
|
-
Ask the user. A choice is several variants plus one
|
|
236
|
+
Ask the user. A choice is several variants plus one question that binds them.
|
|
180
237
|
Then tell the user where to look and end your turn.`,
|
|
181
|
-
read: `mockpit read [slug] [--project <name>] [--body] [--history]
|
|
182
|
-
No slug: one line per mock (states, variants, open
|
|
183
|
-
the user is doing now. A slug: the mock's states, variants,
|
|
184
|
-
answers, parts, knobs and
|
|
185
|
-
--body includes surfaces, --history version rows.
|
|
238
|
+
read: `mockpit read [slug] [--project <name>] [--body] [--history] [--tree]
|
|
239
|
+
No slug: one line per mock (states, variants, open questions), one per flow, plus pending, what
|
|
240
|
+
the user is doing now. A slug: the mock's states, variants, questions with
|
|
241
|
+
answers, parts, knobs and settled values, plus its pending, as JSON.
|
|
242
|
+
--body includes surfaces, --history version rows. --tree (no slug): the
|
|
243
|
+
project as a tree, each mock's variants with status and forkedFrom, and the
|
|
244
|
+
mocks it embeds and is embedded in, as JSON. Never consumes feedback.`,
|
|
186
245
|
feedback: `mockpit feedback [--project <name>] [--session <id>]
|
|
187
246
|
Returns at once with {feedback, pending} as JSON: one batch per mock the user
|
|
188
|
-
sent since you last heard ({mock, reply: {asks,
|
|
189
|
-
|
|
247
|
+
sent since you last heard ({mock, reply: {asks, borrowed, tuned, comments,
|
|
248
|
+
verdict}, messages, inUse, archived}, or {mock, closed: true, dismissed} when the
|
|
249
|
+
user closed it unanswered), delivered once across feedback, watch and
|
|
190
250
|
every write; and pending ({mock, viewerOpen, draft}) so you can tell the
|
|
191
251
|
user "take your time" or "2 of 3 answered". Run it when the user says they
|
|
192
252
|
answered.`,
|
|
193
253
|
say: `mockpit say "<text>" --mock <slug> [--state s] [--variant v]
|
|
194
254
|
A plain-text message in the mock's thread, as your session's agent.`,
|
|
255
|
+
edit: `mockpit edit --mock <slug> [--slug <new>] [--title <t>] [--kind page|component|sketch]
|
|
256
|
+
[--area <name>|""] [--source <path>|""] [--project <name>]
|
|
257
|
+
mockpit edit --project <name> --rename <new name>
|
|
258
|
+
mockpit edit --project <name> --area-order "Shell,Food,Chat"
|
|
259
|
+
Change a mock without publishing a new version; "" clears area or source. A new
|
|
260
|
+
slug rewrites every embed, data-goto and flow step that names the old one, in
|
|
261
|
+
every version. A mock still embedded somewhere stays a component, not a sketch.
|
|
262
|
+
--rename moves the project's mocks, sessions, flows and design settings to the
|
|
263
|
+
new name (refused if that project has mocks); pass --project <new> or set
|
|
264
|
+
MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
|
|
265
|
+
--area-order lists the project's areas in the order Home shows them; areas it
|
|
266
|
+
leaves out follow alphabetically, "" goes back to alphabetical.`,
|
|
195
267
|
run: `mockpit run <file|-> [--session <id>] [--project <name>] [--json]
|
|
196
268
|
Run a script (the body of an async function, plain JavaScript) in the server's
|
|
197
269
|
sandbox against the mockpit API; see mockpit guide --topic scripts. Prints each
|
|
@@ -203,11 +275,12 @@ mockpit publish --mock <slug> --parts <name=file|-> ... [options]
|
|
|
203
275
|
Claude Code, arm it under Monitor after asking, then end the turn).
|
|
204
276
|
Delivery is shared with feedback and every write: each Send arrives once.`,
|
|
205
277
|
export: `mockpit export --mock <slug> [--state s] [--variant v] [--out <dir>]
|
|
206
|
-
Write index.html + history.json per state to .mockpit/accepted/<mock>/<state
|
|
278
|
+
Write index.html + history.json per state to .mockpit/accepted/<mock>/<state>/;
|
|
279
|
+
with fills, index.html is the chosen state, plus all-fills.html and decisions.json.`,
|
|
207
280
|
upload: `mockpit upload <file> [--kind image|file] [--session <id>]
|
|
208
281
|
Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
|
|
209
282
|
guide: `mockpit guide [--topic <id>] [--project <name>]
|
|
210
|
-
No flag prints the brief: the loop, parts,
|
|
283
|
+
No flag prints the brief: the loop, parts, questions and knobs, feedback, and
|
|
211
284
|
this project's palette, kit and icons. --topic prints one reference topic
|
|
212
285
|
(knobs, asks, surfaces, html, feedback, http, scripts).`,
|
|
213
286
|
};
|
|
@@ -396,6 +469,19 @@ async function resolveSession(flags, { create = false } = {}) {
|
|
|
396
469
|
return session.id;
|
|
397
470
|
}
|
|
398
471
|
|
|
472
|
+
// The harness's conversation id names exactly one session, where the cwd can
|
|
473
|
+
// match several agents working in one repo; the watcher would then take a
|
|
474
|
+
// sibling's replies. Never creates one: most conversations never use mockpit.
|
|
475
|
+
async function resolveSessionByKey(key) {
|
|
476
|
+
try {
|
|
477
|
+
const res = await fetch(`${BASE}/api/sessions${query({ key })}`, { headers: HEADERS });
|
|
478
|
+
if (!res.ok) return null;
|
|
479
|
+
return (await res.json())[0]?.id ?? null;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
399
485
|
// A monitor process (e.g. the Claude Code plugin) may not share the local
|
|
400
486
|
// state file written by the agent's CLI calls — different spawn tree, so
|
|
401
487
|
// `agentPid()` can hash to a different key. Fall back to asking the server for
|
|
@@ -810,6 +896,7 @@ function printPublished(result, flags) {
|
|
|
810
896
|
console.log(`parts${state ? ` (${state})` : ""}: ${parts.map((p) => p.name).join(", ")}`);
|
|
811
897
|
}
|
|
812
898
|
if (result.applied?.length) console.log(`applied: ${result.applied.join(", ")}`);
|
|
899
|
+
if (result.flow) console.log(flowLine(result.flow));
|
|
813
900
|
const changes = result.partChanges;
|
|
814
901
|
if (changes?.renamed?.length) {
|
|
815
902
|
console.log(`renamed: ${changes.renamed.map((r) => `${r.from} → ${r.to}`).join(", ")}`);
|
|
@@ -822,6 +909,11 @@ function printPublished(result, flags) {
|
|
|
822
909
|
printFeedback(result.feedback);
|
|
823
910
|
}
|
|
824
911
|
|
|
912
|
+
function flowLine(flow) {
|
|
913
|
+
const steps = flow.steps.map((x) => (x.state === null ? x.mock : `${x.mock}/${x.state}`));
|
|
914
|
+
return `flow ${flow.name}: ${steps.length ? steps.join(" → ") : "empty, removed"}`;
|
|
915
|
+
}
|
|
916
|
+
|
|
825
917
|
// Only speak when there IS feedback: an empty line per write is pure token
|
|
826
918
|
// cost, and the cursor guarantees anything pending arrives on some write.
|
|
827
919
|
function printFeedback(feedback) {
|
|
@@ -847,14 +939,24 @@ function parsePublishFlags() {
|
|
|
847
939
|
allowPositionals: true,
|
|
848
940
|
options: {
|
|
849
941
|
...MOCK_FLAGS,
|
|
942
|
+
situation: { type: "string" },
|
|
850
943
|
title: { type: "string" },
|
|
851
944
|
kind: { type: "string" },
|
|
945
|
+
area: { type: "string" },
|
|
946
|
+
source: { type: "string" },
|
|
852
947
|
html: { type: "string" },
|
|
948
|
+
poster: { type: "string" },
|
|
853
949
|
surfaces: { type: "string" },
|
|
854
950
|
parts: { type: "string", multiple: true },
|
|
951
|
+
fills: { type: "string", multiple: true },
|
|
855
952
|
knobs: { type: "string" },
|
|
856
953
|
from: { type: "string" },
|
|
954
|
+
fork: { type: "string" },
|
|
857
955
|
prompt: { type: "string" },
|
|
956
|
+
note: { type: "string" },
|
|
957
|
+
widths: { type: "string" },
|
|
958
|
+
flow: { type: "string" },
|
|
959
|
+
step: { type: "string" },
|
|
858
960
|
kit: { type: "string", multiple: true },
|
|
859
961
|
layout: { type: "string" },
|
|
860
962
|
md: { type: "string", multiple: true },
|
|
@@ -873,18 +975,27 @@ function parsePublishFlags() {
|
|
|
873
975
|
|
|
874
976
|
// `--parts name=file` (repeatable; `name#key` targets one instance): the new
|
|
875
977
|
// outer html of one part, spliced server-side into the latest version.
|
|
876
|
-
|
|
978
|
+
// `--fills part#fill=file` is the same for one fill of a part.
|
|
979
|
+
function readPartFlags(values, slug, flag = "parts") {
|
|
877
980
|
if (!values?.length) return undefined;
|
|
878
|
-
const example =
|
|
981
|
+
const example =
|
|
982
|
+
flag === "parts"
|
|
983
|
+
? `mockpit publish --mock ${slug} --parts body=body.html`
|
|
984
|
+
: `mockpit publish --mock ${slug} --fills entry#skeleton=skeleton.html`;
|
|
879
985
|
const parts = {};
|
|
880
986
|
let stdin = false;
|
|
881
987
|
for (const raw of values) {
|
|
882
988
|
const at = raw.indexOf("=");
|
|
883
|
-
if (at <= 0 || at === raw.length - 1)
|
|
989
|
+
if (at <= 0 || at === raw.length - 1) {
|
|
990
|
+
die(
|
|
991
|
+
`--${flag} needs ${flag === "parts" ? "name" : "part#fill"}=file (got "${raw}")`,
|
|
992
|
+
example,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
884
995
|
const name = raw.slice(0, at);
|
|
885
996
|
const file = raw.slice(at + 1);
|
|
886
997
|
if (file === "-") {
|
|
887
|
-
if (stdin) die(
|
|
998
|
+
if (stdin) die(`only one --${flag} can read stdin`, example);
|
|
888
999
|
stdin = true;
|
|
889
1000
|
} else if (!existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
|
|
890
1001
|
parts[name] = readContent(file);
|
|
@@ -900,6 +1011,13 @@ async function publishMock() {
|
|
|
900
1011
|
if (from !== undefined && !Number.isInteger(from)) {
|
|
901
1012
|
die(`--from must be a version number (got "${flags.from}")`, `mockpit read ${slug} --history`);
|
|
902
1013
|
}
|
|
1014
|
+
const step = flags.step === undefined ? undefined : Number(flags.step);
|
|
1015
|
+
if (step !== undefined && !(Number.isInteger(step) && step >= 0)) {
|
|
1016
|
+
die(
|
|
1017
|
+
`--step must be a place in the flow, 1 or more, or 0 (got "${flags.step}")`,
|
|
1018
|
+
`mockpit read`,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
903
1021
|
const file = flags.html ?? positionals[0];
|
|
904
1022
|
const composed =
|
|
905
1023
|
file !== undefined || tokens.some((t) => t.kind === "option" && SURFACE_FLAGS.has(t.name));
|
|
@@ -907,16 +1025,21 @@ async function publishMock() {
|
|
|
907
1025
|
composed && "--html",
|
|
908
1026
|
flags.surfaces !== undefined && "--surfaces",
|
|
909
1027
|
flags.parts?.length && "--parts",
|
|
1028
|
+
flags.fills?.length && "--fills",
|
|
910
1029
|
].filter(Boolean);
|
|
911
1030
|
if (given.length > 1) {
|
|
912
1031
|
die(
|
|
913
|
-
`pass one of --html, --surfaces or --
|
|
1032
|
+
`pass one of --html, --surfaces, --parts or --fills, not ${given.join(" and ")}`,
|
|
914
1033
|
`mockpit publish --help`,
|
|
915
1034
|
);
|
|
916
1035
|
}
|
|
917
1036
|
// Checked before the session exists so a typo'd path leaves nothing behind on the server.
|
|
918
1037
|
if (file && file !== "-" && !existsSync(file)) die(`cannot read ${file}`, `ls ${file}`);
|
|
1038
|
+
if (flags.poster !== undefined && !existsSync(flags.poster)) {
|
|
1039
|
+
die(`cannot read ${flags.poster}`, `ls ${flags.poster}`);
|
|
1040
|
+
}
|
|
919
1041
|
const parts = readPartFlags(flags.parts, slug);
|
|
1042
|
+
const fills = readPartFlags(flags.fills, slug, "fills");
|
|
920
1043
|
let surfaces =
|
|
921
1044
|
flags.surfaces === undefined ? undefined : readJsonFlag("surfaces", flags.surfaces);
|
|
922
1045
|
if (surfaces !== undefined && !Array.isArray(surfaces)) {
|
|
@@ -936,21 +1059,38 @@ async function publishMock() {
|
|
|
936
1059
|
}
|
|
937
1060
|
surfaces.push(...(await surfacesFromFlags(flags, tokens, { session, layout: flags.layout })));
|
|
938
1061
|
}
|
|
939
|
-
if (!surfaces && !parts)
|
|
1062
|
+
if (!surfaces && !parts && !fills && !flags.fork && flags.flow === undefined)
|
|
940
1063
|
die(`no html for ${slug}`, `mockpit publish --mock ${slug} --html <file>`);
|
|
1064
|
+
const poster =
|
|
1065
|
+
flags.poster === undefined ? undefined : (await uploadFile(flags.poster, { session })).id;
|
|
941
1066
|
const body = {
|
|
942
1067
|
session,
|
|
943
1068
|
project,
|
|
944
1069
|
mock: slug,
|
|
945
1070
|
state: flags.state,
|
|
1071
|
+
situation: flags.situation,
|
|
946
1072
|
variant: flags.variant,
|
|
947
1073
|
title: flags.title,
|
|
948
1074
|
kind: flags.kind,
|
|
1075
|
+
area: flags.area,
|
|
1076
|
+
source: flags.source,
|
|
949
1077
|
...(flags.knobs !== undefined && { knobs: readJsonFlag("knobs", flags.knobs) }),
|
|
950
1078
|
...(from !== undefined && { from }),
|
|
1079
|
+
...(flags.fork !== undefined && { fork: flags.fork }),
|
|
951
1080
|
prompt: flags.prompt,
|
|
1081
|
+
note: flags.note,
|
|
1082
|
+
...(flags.widths !== undefined && {
|
|
1083
|
+
widths: flags.widths
|
|
1084
|
+
.split(",")
|
|
1085
|
+
.filter((w) => w.trim())
|
|
1086
|
+
.map((w) => Number(w)),
|
|
1087
|
+
}),
|
|
1088
|
+
...(flags.flow !== undefined && { flow: flags.flow }),
|
|
1089
|
+
...(step !== undefined && { step }),
|
|
952
1090
|
...(surfaces && { surfaces }),
|
|
953
1091
|
...(parts && { parts }),
|
|
1092
|
+
...(fills && { fills }),
|
|
1093
|
+
...(poster && { poster }),
|
|
954
1094
|
};
|
|
955
1095
|
const result = await api(
|
|
956
1096
|
"/api/mocks",
|
|
@@ -962,6 +1102,7 @@ async function publishMock() {
|
|
|
962
1102
|
}
|
|
963
1103
|
|
|
964
1104
|
// "Quiet" is an unbound option; "Quiet=quiet" binds it to the quiet variant;
|
|
1105
|
+
// "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
|
|
965
1106
|
// "Above=knob:trim.position=top" sets one knob. The value is JSON when it
|
|
966
1107
|
// parses (17, true), else a string, so slider and toggle values keep their type.
|
|
967
1108
|
function parseOption(raw) {
|
|
@@ -980,7 +1121,11 @@ function parseOption(raw) {
|
|
|
980
1121
|
}
|
|
981
1122
|
const at = raw.lastIndexOf("=");
|
|
982
1123
|
if (at <= 0) return { label: raw };
|
|
983
|
-
|
|
1124
|
+
const label = raw.slice(0, at);
|
|
1125
|
+
const bound = raw.slice(at + 1);
|
|
1126
|
+
const pin = /^(.*)@(\d+)$/.exec(bound);
|
|
1127
|
+
if (!pin) return { label, variant: bound };
|
|
1128
|
+
return { label, ...(pin[1] ? { variant: pin[1] } : {}), version: Number(pin[2]) };
|
|
984
1129
|
}
|
|
985
1130
|
|
|
986
1131
|
// One feedback batch, one line — for a background monitor's notifications.
|
|
@@ -1005,6 +1150,20 @@ function watchLines(batch) {
|
|
|
1005
1150
|
const lines = [];
|
|
1006
1151
|
const name = batch.mock ?? "a mock";
|
|
1007
1152
|
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
|
+
if (batch.edited) {
|
|
1160
|
+
const e = batch.edited;
|
|
1161
|
+
const bits = [
|
|
1162
|
+
e.kind ? `now a ${e.kind}` : "",
|
|
1163
|
+
e.area !== undefined ? (e.area ? `area ${e.area}` : "no area") : "",
|
|
1164
|
+
].filter(Boolean);
|
|
1165
|
+
lines.push(`mockpit edited ${name}: the user moved it (${bits.join(", ")})`);
|
|
1166
|
+
}
|
|
1008
1167
|
if (batch.reply) {
|
|
1009
1168
|
const r = batch.reply;
|
|
1010
1169
|
const bits = [
|
|
@@ -1017,21 +1176,30 @@ function watchLines(batch) {
|
|
|
1017
1176
|
.join("; ")
|
|
1018
1177
|
: "",
|
|
1019
1178
|
Object.keys(r.tuned ?? {}).length ? `${Object.keys(r.tuned).length} tuned` : "",
|
|
1020
|
-
Object.keys(r.
|
|
1021
|
-
? `
|
|
1179
|
+
Object.keys(r.borrowed ?? {}).length
|
|
1180
|
+
? `borrowed ${Object.entries(r.borrowed)
|
|
1022
1181
|
.map(([p, v]) => `${p}←${v}`)
|
|
1023
1182
|
.join(", ")}`
|
|
1024
1183
|
: "",
|
|
1025
|
-
r.comments
|
|
1184
|
+
...(r.comments ?? [])
|
|
1185
|
+
.filter((c) => c.borrow)
|
|
1186
|
+
.map(
|
|
1187
|
+
(c) =>
|
|
1188
|
+
`borrow ${c.borrow.part ? `${c.borrow.part} ` : ""}from ${c.borrow.from} into ${c.variant}@${c.version}${c.text ? ` (“${c.text}”)` : ""}`,
|
|
1189
|
+
),
|
|
1190
|
+
(() => {
|
|
1191
|
+
const n = (r.comments ?? []).filter((c) => !c.borrow).length;
|
|
1192
|
+
return n ? `${n} comment${n === 1 ? "" : "s"}` : "";
|
|
1193
|
+
})(),
|
|
1026
1194
|
r.reactions?.length ? r.reactions.join(", ") : "",
|
|
1027
|
-
r.
|
|
1195
|
+
r.verdict ? `${r.verdict.kind} ${r.verdict.variant}` : "",
|
|
1028
1196
|
r.text ?? "",
|
|
1029
1197
|
].filter(Boolean);
|
|
1030
1198
|
lines.push(`mockpit reply on ${name}: ${bits.join(" · ").replace(/\s+/g, " ")}`);
|
|
1031
1199
|
}
|
|
1032
|
-
for (const c of batch.
|
|
1200
|
+
for (const c of batch.messages ?? []) {
|
|
1033
1201
|
lines.push(
|
|
1034
|
-
`mockpit
|
|
1202
|
+
`mockpit message on ${name}: “${String(c.text ?? "")
|
|
1035
1203
|
.replace(/\s+/g, " ")
|
|
1036
1204
|
.trim()}”`,
|
|
1037
1205
|
);
|
|
@@ -1054,8 +1222,13 @@ async function apiSoft(path) {
|
|
|
1054
1222
|
return res.json().catch(() => null);
|
|
1055
1223
|
}
|
|
1056
1224
|
|
|
1057
|
-
// Keep the scratch directory out of the repo
|
|
1058
|
-
//
|
|
1225
|
+
// Keep the scratch directory out of the repo, all but the project's rules
|
|
1226
|
+
// file, which is meant to be committed. Git can't re-include a file under an
|
|
1227
|
+
// ignored directory, so the ignore has to name the directory's contents, and
|
|
1228
|
+
// the `.mockpit/` line older versions wrote is upgraded in place. Returns true
|
|
1229
|
+
// when .gitignore was touched, so init can report it.
|
|
1230
|
+
const MOCKPIT_IGNORE = ".mockpit/*\n!.mockpit/rules.md";
|
|
1231
|
+
|
|
1059
1232
|
function ignoreMockpitDir() {
|
|
1060
1233
|
const file = join(process.cwd(), ".gitignore");
|
|
1061
1234
|
let current = "";
|
|
@@ -1064,14 +1237,56 @@ function ignoreMockpitDir() {
|
|
|
1064
1237
|
} catch {
|
|
1065
1238
|
// no .gitignore yet — create one
|
|
1066
1239
|
}
|
|
1067
|
-
if (/^\.mockpit
|
|
1240
|
+
if (/^\.mockpit\/\*$/m.test(current) && /^!\.mockpit\/rules\.md$/m.test(current)) return false;
|
|
1241
|
+
if (/^\.mockpit\/?$/m.test(current)) {
|
|
1242
|
+
writeFileSync(file, current.replace(/^\.mockpit\/?$/m, MOCKPIT_IGNORE));
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
const lines = /^\.mockpit\/\*$/m.test(current) ? "!.mockpit/rules.md" : MOCKPIT_IGNORE;
|
|
1068
1246
|
writeFileSync(
|
|
1069
1247
|
file,
|
|
1070
|
-
current && !current.endsWith("\n") ? `${current}\n
|
|
1248
|
+
current && !current.endsWith("\n") ? `${current}\n${lines}\n` : `${current}${lines}\n`,
|
|
1071
1249
|
);
|
|
1072
1250
|
return true;
|
|
1073
1251
|
}
|
|
1074
1252
|
|
|
1253
|
+
function repoRoot() {
|
|
1254
|
+
try {
|
|
1255
|
+
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1256
|
+
encoding: "utf8",
|
|
1257
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1258
|
+
}).trim();
|
|
1259
|
+
if (top) return top;
|
|
1260
|
+
} catch {}
|
|
1261
|
+
return process.cwd();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Where `mockpit serve` reads the user's own rules: next to its database.
|
|
1265
|
+
const myRulesFile = () =>
|
|
1266
|
+
join(dirname(process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db")), "rules.md");
|
|
1267
|
+
|
|
1268
|
+
// Sends a rules file as it is on disk: a missing file clears the stored copy,
|
|
1269
|
+
// so deleting the file and re-running is how rules are switched off.
|
|
1270
|
+
async function sendRules(file, path) {
|
|
1271
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
1272
|
+
const result = await api(
|
|
1273
|
+
path,
|
|
1274
|
+
{ method: "PUT", body: JSON.stringify({ text }) },
|
|
1275
|
+
{ report: "die", fix: "mockpit rules --help" },
|
|
1276
|
+
);
|
|
1277
|
+
return { ...result, missing: !text };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function rulesSummary(r) {
|
|
1281
|
+
if (r.cleared) return "none (cleared)";
|
|
1282
|
+
const bits = [
|
|
1283
|
+
r.packs?.length ? `packs ${r.packs.join(", ")}` : null,
|
|
1284
|
+
r.rules ? "own rules" : null,
|
|
1285
|
+
].filter(Boolean);
|
|
1286
|
+
const unknown = r.unknown?.length ? ` · unknown pack ${r.unknown.join(", ")}` : "";
|
|
1287
|
+
return `${bits.join(" · ") || "empty"}${unknown}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1075
1290
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1076
1291
|
|
|
1077
1292
|
// Semver greater-than for plain x.y.z (mirrors server/app.ts versionGt).
|
|
@@ -1318,7 +1533,11 @@ const commands = {
|
|
|
1318
1533
|
mkdirSync(dirname(starter), { recursive: true });
|
|
1319
1534
|
writeFileSync(starter, renderStarter({ ...design, kit }, iconSets));
|
|
1320
1535
|
say("wrote:", ".mockpit/starter.html");
|
|
1321
|
-
if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/)");
|
|
1536
|
+
if (ignoreMockpitDir()) say("wrote:", ".gitignore (+ .mockpit/, keeping .mockpit/rules.md)");
|
|
1537
|
+
const rulesFile = join(repoRoot(), ".mockpit", "rules.md");
|
|
1538
|
+
if (existsSync(rulesFile)) {
|
|
1539
|
+
say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
|
|
1540
|
+
}
|
|
1322
1541
|
say("next:", "mockpit guide");
|
|
1323
1542
|
if (flags.json) out({ project, design: stored, starter });
|
|
1324
1543
|
},
|
|
@@ -1335,15 +1554,20 @@ const commands = {
|
|
|
1335
1554
|
project: { type: "string" },
|
|
1336
1555
|
session: { type: "string" },
|
|
1337
1556
|
option: { type: "string", multiple: true },
|
|
1557
|
+
recommend: { type: "string" },
|
|
1338
1558
|
scope: { type: "string" },
|
|
1339
1559
|
state: { type: "string" },
|
|
1340
1560
|
part: { type: "string" },
|
|
1341
1561
|
multi: { type: "boolean" },
|
|
1342
1562
|
id: { type: "string" },
|
|
1563
|
+
after: { type: "string" },
|
|
1343
1564
|
asks: { type: "string" },
|
|
1344
1565
|
},
|
|
1345
1566
|
});
|
|
1346
|
-
const slug = requireMock(
|
|
1567
|
+
const slug = requireMock(
|
|
1568
|
+
flags,
|
|
1569
|
+
'mockpit ask --mock writer "Quiet or dark?" --option Quiet=quiet',
|
|
1570
|
+
);
|
|
1347
1571
|
let asks;
|
|
1348
1572
|
if (flags.asks !== undefined) {
|
|
1349
1573
|
if (positionals.length > 0) {
|
|
@@ -1359,7 +1583,7 @@ const commands = {
|
|
|
1359
1583
|
if (!text)
|
|
1360
1584
|
die(
|
|
1361
1585
|
"ask needs a question",
|
|
1362
|
-
`mockpit ask --mock ${slug} "
|
|
1586
|
+
`mockpit ask --mock ${slug} "Quiet or dark?" --option Quiet=quiet`,
|
|
1363
1587
|
);
|
|
1364
1588
|
const options = (flags.option ?? []).map(parseOption);
|
|
1365
1589
|
if (options.length === 0) {
|
|
@@ -1368,6 +1592,17 @@ const commands = {
|
|
|
1368
1592
|
`mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark`,
|
|
1369
1593
|
);
|
|
1370
1594
|
}
|
|
1595
|
+
if (flags.recommend !== undefined) {
|
|
1596
|
+
const pick = options.find(
|
|
1597
|
+
(o) => o.label.toLowerCase() === flags.recommend.trim().toLowerCase(),
|
|
1598
|
+
);
|
|
1599
|
+
if (!pick)
|
|
1600
|
+
die(
|
|
1601
|
+
`--recommend "${flags.recommend}" names no option`,
|
|
1602
|
+
`mockpit ask --mock ${slug} "${text}" --option Quiet=quiet --option Dark=dark --recommend Quiet`,
|
|
1603
|
+
);
|
|
1604
|
+
pick.recommended = true;
|
|
1605
|
+
}
|
|
1371
1606
|
asks = [
|
|
1372
1607
|
{
|
|
1373
1608
|
text,
|
|
@@ -1377,6 +1612,7 @@ const commands = {
|
|
|
1377
1612
|
...(flags.state && { state: flags.state }),
|
|
1378
1613
|
...(flags.part && { part: flags.part }),
|
|
1379
1614
|
...(flags.multi && { multi: true }),
|
|
1615
|
+
...(flags.after && { after: flags.after }),
|
|
1380
1616
|
},
|
|
1381
1617
|
];
|
|
1382
1618
|
}
|
|
@@ -1414,10 +1650,20 @@ const commands = {
|
|
|
1414
1650
|
project: { type: "string" },
|
|
1415
1651
|
body: { type: "boolean" },
|
|
1416
1652
|
history: { type: "boolean" },
|
|
1653
|
+
tree: { type: "boolean" },
|
|
1417
1654
|
},
|
|
1418
1655
|
});
|
|
1419
1656
|
const project = resolveProject(flags).name;
|
|
1420
1657
|
if (positionals.length > 1) die("read takes one mock slug", "mockpit read writer");
|
|
1658
|
+
if (flags.tree) {
|
|
1659
|
+
return out(
|
|
1660
|
+
await api(
|
|
1661
|
+
`/api/projects/${encodeURIComponent(project)}/tree`,
|
|
1662
|
+
{},
|
|
1663
|
+
{ report: "die", fix: `mockpit read --project ${project}` },
|
|
1664
|
+
),
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1421
1667
|
if (positionals.length === 1) {
|
|
1422
1668
|
const slug = slugify(positionals[0]);
|
|
1423
1669
|
return out(
|
|
@@ -1439,15 +1685,16 @@ const commands = {
|
|
|
1439
1685
|
console.log(
|
|
1440
1686
|
`${project} · ${mocks.length} mock${mocks.length === 1 ? "" : "s"}` +
|
|
1441
1687
|
(list.open
|
|
1442
|
-
? ` · ${list.open} open
|
|
1688
|
+
? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
|
|
1443
1689
|
: " · nothing waiting on the user"),
|
|
1444
1690
|
);
|
|
1445
1691
|
for (const m of mocks) {
|
|
1446
1692
|
const states = m.states.length ? m.states.join(" / ") : "single state";
|
|
1447
1693
|
console.log(
|
|
1448
|
-
` ${m.slug} · ${m.kind} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.open ? ` · ${m.open} open` : ""}`,
|
|
1694
|
+
` ${m.slug} · ${m.kind}${m.area ? ` · ${m.area}` : ""} · ${states} · ${m.variants} variant${m.variants === 1 ? "" : "s"}${m.closedAt ? " · closed" : m.open ? ` · ${m.open} open` : ""}`,
|
|
1449
1695
|
);
|
|
1450
1696
|
}
|
|
1697
|
+
for (const flow of list.flows ?? []) console.log(` ${flowLine(flow)}`);
|
|
1451
1698
|
for (const line of pendingLines(list.pending)) console.log(line);
|
|
1452
1699
|
},
|
|
1453
1700
|
async export() {
|
|
@@ -1473,6 +1720,15 @@ const commands = {
|
|
|
1473
1720
|
const dir = join(root, entry.state === null ? "default" : slugify(entry.state));
|
|
1474
1721
|
mkdirSync(dir, { recursive: true });
|
|
1475
1722
|
writeFileSync(join(dir, "index.html"), entry.html ?? "");
|
|
1723
|
+
// The state with every fill, when index.html is the composed choice.
|
|
1724
|
+
if (entry.allFills) writeFileSync(join(dir, "all-fills.html"), entry.allFills);
|
|
1725
|
+
const decided = data.decisions?.states?.[entry.state ?? ""];
|
|
1726
|
+
if (decided) {
|
|
1727
|
+
writeFileSync(
|
|
1728
|
+
join(dir, "decisions.json"),
|
|
1729
|
+
JSON.stringify({ ...decided, knobs: data.decisions.knobs }, null, 2) + "\n",
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1476
1732
|
writeFileSync(
|
|
1477
1733
|
join(dir, "history.json"),
|
|
1478
1734
|
JSON.stringify(
|
|
@@ -1560,9 +1816,13 @@ const commands = {
|
|
|
1560
1816
|
// feedback is delivered exactly once across watch, feedback, and piggyback.
|
|
1561
1817
|
let firstAfter = flags.after;
|
|
1562
1818
|
for (;;) {
|
|
1563
|
-
const
|
|
1819
|
+
const key = harnessKey();
|
|
1820
|
+
const session =
|
|
1821
|
+
(await resolveSession(flags)) ??
|
|
1822
|
+
(key ? await resolveSessionByKey(key) : await resolveSessionByCwd());
|
|
1564
1823
|
if (!session) {
|
|
1565
|
-
|
|
1824
|
+
// A conversation that hasn't used mockpit yet may never; ask rarely.
|
|
1825
|
+
await sleep(key ? 10_000 : 2000);
|
|
1566
1826
|
continue;
|
|
1567
1827
|
}
|
|
1568
1828
|
let result;
|
|
@@ -1612,6 +1872,102 @@ const commands = {
|
|
|
1612
1872
|
console.log(`said on ${slug}`);
|
|
1613
1873
|
printFeedback(result.feedback);
|
|
1614
1874
|
},
|
|
1875
|
+
async edit() {
|
|
1876
|
+
const { values: flags } = parse({
|
|
1877
|
+
options: {
|
|
1878
|
+
mock: { type: "string" },
|
|
1879
|
+
project: { type: "string" },
|
|
1880
|
+
session: { type: "string" },
|
|
1881
|
+
slug: { type: "string" },
|
|
1882
|
+
title: { type: "string" },
|
|
1883
|
+
kind: { type: "string" },
|
|
1884
|
+
area: { type: "string" },
|
|
1885
|
+
source: { type: "string" },
|
|
1886
|
+
rename: { type: "string" },
|
|
1887
|
+
"area-order": { type: "string" },
|
|
1888
|
+
},
|
|
1889
|
+
});
|
|
1890
|
+
const project = resolveProject(flags).name;
|
|
1891
|
+
const session = (await resolveSession(flags)) ?? undefined;
|
|
1892
|
+
const areaOrder = flags["area-order"];
|
|
1893
|
+
if (flags.rename !== undefined || areaOrder !== undefined) {
|
|
1894
|
+
if (flags.mock) {
|
|
1895
|
+
die(
|
|
1896
|
+
"--rename and --area-order edit the project; edit the mock without them",
|
|
1897
|
+
"mockpit edit --help",
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
if (flags.rename !== undefined && !flags.rename.trim()) {
|
|
1901
|
+
die("--rename needs the new name", `mockpit edit --rename <name>`);
|
|
1902
|
+
}
|
|
1903
|
+
const result = await api(
|
|
1904
|
+
projectPath(project),
|
|
1905
|
+
{
|
|
1906
|
+
method: "PATCH",
|
|
1907
|
+
body: JSON.stringify({
|
|
1908
|
+
name: flags.rename,
|
|
1909
|
+
...(areaOrder === undefined ? {} : { areaOrder: areaOrder.split(",") }),
|
|
1910
|
+
session,
|
|
1911
|
+
}),
|
|
1912
|
+
},
|
|
1913
|
+
{ report: "die", fix: "mockpit read" },
|
|
1914
|
+
);
|
|
1915
|
+
if (flags.json) return out(result);
|
|
1916
|
+
if (flags.quiet) return;
|
|
1917
|
+
if (result.areaOrder) {
|
|
1918
|
+
console.log(
|
|
1919
|
+
`area order ${result.areaOrder.length ? result.areaOrder.join(", ") : "alphabetical"} · ${result.url}`,
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
1923
|
+
if (!result.renamed) return printFeedback(result.feedback);
|
|
1924
|
+
console.log(
|
|
1925
|
+
`renamed project ${result.renamed.from} → ${result.renamed.to} · ${result.mocks} mock${result.mocks === 1 ? "" : "s"} · ${result.url}`,
|
|
1926
|
+
);
|
|
1927
|
+
if (!flags.project && !process.env.MOCKPIT_PROJECT) {
|
|
1928
|
+
console.log(
|
|
1929
|
+
`note: this repo still resolves to "${project}"; pass --project ${shq(result.project)} or set MOCKPIT_PROJECT`,
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
printFeedback(result.feedback);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const slug = requireMock(flags, "mockpit edit --mock writer --area Shell");
|
|
1936
|
+
const fields = ["slug", "title", "kind", "area", "source"].filter(
|
|
1937
|
+
(k) => flags[k] !== undefined,
|
|
1938
|
+
);
|
|
1939
|
+
if (!fields.length) {
|
|
1940
|
+
die("edit needs --slug, --title, --kind, --area or --source", "mockpit edit --help");
|
|
1941
|
+
}
|
|
1942
|
+
const result = await api(
|
|
1943
|
+
mockPath(slug),
|
|
1944
|
+
{
|
|
1945
|
+
method: "PATCH",
|
|
1946
|
+
body: JSON.stringify({
|
|
1947
|
+
project,
|
|
1948
|
+
session,
|
|
1949
|
+
...Object.fromEntries(fields.map((k) => [k, flags[k]])),
|
|
1950
|
+
}),
|
|
1951
|
+
},
|
|
1952
|
+
{ report: "die", fix: `mockpit read ${slug}` },
|
|
1953
|
+
);
|
|
1954
|
+
if (flags.json) return out(result);
|
|
1955
|
+
if (flags.quiet) return;
|
|
1956
|
+
const m = result.mock;
|
|
1957
|
+
console.log(
|
|
1958
|
+
[
|
|
1959
|
+
result.renamed ? `${result.renamed.from} → ${m.slug}` : m.slug,
|
|
1960
|
+
m.kind,
|
|
1961
|
+
m.area && `area ${m.area}`,
|
|
1962
|
+
m.source && `source ${m.source}`,
|
|
1963
|
+
result.url,
|
|
1964
|
+
]
|
|
1965
|
+
.filter(Boolean)
|
|
1966
|
+
.join(" · "),
|
|
1967
|
+
);
|
|
1968
|
+
for (const nudge of result.nudges ?? []) console.log(`nudge: ${nudge}`);
|
|
1969
|
+
printFeedback(result.feedback);
|
|
1970
|
+
},
|
|
1615
1971
|
async demo() {
|
|
1616
1972
|
parse();
|
|
1617
1973
|
const { DEMO } = await import("./demoData.js");
|
|
@@ -1629,6 +1985,7 @@ const commands = {
|
|
|
1629
1985
|
session: session.id,
|
|
1630
1986
|
project: DEMO.project,
|
|
1631
1987
|
mock: DEMO.slug,
|
|
1988
|
+
kind: "page",
|
|
1632
1989
|
title: DEMO.title,
|
|
1633
1990
|
state: state.label,
|
|
1634
1991
|
variant: variant.name,
|
|
@@ -1760,6 +2117,24 @@ const commands = {
|
|
|
1760
2117
|
return printBrief(flags);
|
|
1761
2118
|
},
|
|
1762
2119
|
|
|
2120
|
+
// A rules file is the user's or the project's own guidance on top of the
|
|
2121
|
+
// core brief; the brief prints it, so this only has to put it on the server.
|
|
2122
|
+
async rules() {
|
|
2123
|
+
const { values: flags } = parse({
|
|
2124
|
+
options: { mine: { type: "boolean" }, project: { type: "string" } },
|
|
2125
|
+
});
|
|
2126
|
+
const mine = !!flags.mine;
|
|
2127
|
+
const file = mine ? myRulesFile() : join(repoRoot(), ".mockpit", "rules.md");
|
|
2128
|
+
const project = mine ? null : resolveProject(flags).name;
|
|
2129
|
+
const result = await sendRules(file, mine ? "/api/rules" : `${projectPath(project)}/rules`);
|
|
2130
|
+
if (flags.json) return out(result);
|
|
2131
|
+
if (flags.quiet) return;
|
|
2132
|
+
const where = mine ? "your rules" : `project ${project}`;
|
|
2133
|
+
const from = result.missing ? `no ${file}` : file;
|
|
2134
|
+
console.log(`${where}: ${rulesSummary(result)} (from ${from})`);
|
|
2135
|
+
if (!result.missing) console.log("next:", "mockpit guide");
|
|
2136
|
+
},
|
|
2137
|
+
|
|
1763
2138
|
async setup() {
|
|
1764
2139
|
parse();
|
|
1765
2140
|
console.log(await fetchTextWithFallback("/setup", join(ROOT, "guide", "AGENT_SETUP.md")));
|