mockpit 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -23
- package/bin/initDesign.js +11 -16
- package/bin/mockpit.js +242 -52
- package/bin/poster.js +64 -7
- package/dist/mcp/server.js +48 -6
- package/dist/server/apiViews.d.ts +562 -557
- package/dist/server/apiViews.js +18 -9
- package/dist/server/app.js +723 -206
- package/dist/server/cdnInline.js +6 -4
- package/dist/server/codeWorker.js +2 -2
- package/dist/server/contrast.d.ts +4 -0
- package/dist/server/contrast.js +157 -0
- package/dist/server/decisionList.d.ts +4 -1
- package/dist/server/decisionList.js +43 -5
- package/dist/server/designGuide.d.ts +13 -6
- package/dist/server/designGuide.js +206 -41
- package/dist/server/events.d.ts +6 -0
- package/dist/server/feedbackBatch.d.ts +4 -0
- package/dist/server/feedbackBatch.js +19 -1
- package/dist/server/guidance.d.ts +1 -1
- package/dist/server/guidance.js +2 -2
- package/dist/server/kits.d.ts +2 -2
- package/dist/server/kits.js +54 -1
- package/dist/server/mcpHttp.d.ts +2 -0
- package/dist/server/mcpHttp.js +13 -1
- package/dist/server/mcpSpec.d.ts +27 -27
- package/dist/server/mcpSpec.js +78 -10
- package/dist/server/run.js +22 -1
- package/dist/server/runApi.d.ts +2 -2
- package/dist/server/runApi.js +9 -2
- package/dist/server/sqlStore.d.ts +68 -39
- package/dist/server/sqlStore.js +163 -26
- package/dist/server/surfacePage.d.ts +4 -2
- package/dist/server/surfacePage.js +80 -32
- package/dist/server/system.d.ts +234 -0
- package/dist/server/system.js +1508 -0
- package/dist/server/systemChecks.d.ts +15 -0
- package/dist/server/systemChecks.js +224 -0
- package/dist/server/systemSpecimen.d.ts +2 -0
- package/dist/server/systemSpecimen.js +49 -0
- package/dist/server/themes.d.ts +9 -7
- package/dist/server/themes.js +43 -90
- package/dist/server/types.d.ts +43 -24
- package/dist/server/types.js +3 -1
- package/guide/AGENT_SETUP.md +3 -1
- package/guide/packs/foundations.md +35 -0
- package/guide/topics/asks.md +2 -1
- package/guide/topics/feedback.md +5 -3
- package/guide/topics/html.md +2 -2
- package/guide/topics/http.md +3 -1
- package/guide/topics/system.md +157 -0
- package/package.json +12 -12
- package/skills/mockpit/SKILL.md +7 -4
- package/viewer/dist/index.html +9 -8
package/README.md
CHANGED
|
@@ -7,19 +7,7 @@ The agent publishes a screen in its states and a few variants, then ends its tur
|
|
|
7
7
|
them from pictures, tune knobs, comment on parts and keep the runners-up. One Send gives the
|
|
8
8
|
agent the whole reply. Nothing it calls waits for you.
|
|
9
9
|
|
|
10
|
-
<
|
|
11
|
-
<tr>
|
|
12
|
-
<td width="50%" valign="top">
|
|
13
|
-
<picture>
|
|
14
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/mockpit-dark.png">
|
|
15
|
-
<img width="100%" alt="The Writer design asking Quiet, dark or editorial?, with the three variants as picture answers beside the stage" src="docs/mockpit-light.png">
|
|
16
|
-
</picture>
|
|
17
|
-
</td>
|
|
18
|
-
<td width="50%" valign="top">
|
|
19
|
-
<img width="100%" alt="Animated demo: the user keeps Dark, hides Editorial, picks Quiet, answers where the trim goes and presses Send; the reply shows Not seen yet, then Delivered, and the agent publishes version 2" src="docs/mockpit-demo.gif">
|
|
20
|
-
</td>
|
|
21
|
-
</tr>
|
|
22
|
-
</table>
|
|
10
|
+
<a href="docs/mockpit-demo.gif"><img width="100%" alt="Animated demo: on HALDE, a shop design with three product grids, the pointer hovers Gallery and Editorial, each previewing on the stage, then picks Gallery" src="docs/mockpit-demo.gif"></a>
|
|
23
11
|
|
|
24
12
|
## Quickstart
|
|
25
13
|
|
|
@@ -44,9 +32,9 @@ Then ask it to "design this up on mockpit". Claude Code, Codex, OpenCode, Pi and
|
|
|
44
32
|
## A turn
|
|
45
33
|
|
|
46
34
|
```sh
|
|
47
|
-
mockpit publish --design
|
|
48
|
-
mockpit publish --design
|
|
49
|
-
mockpit ask --design
|
|
35
|
+
mockpit publish --design shop --state Grid --variant index --html index.html
|
|
36
|
+
mockpit publish --design shop --state Grid --variant gallery --html gallery.html
|
|
37
|
+
mockpit ask --design shop "Which grid?" --option Index=index --option Gallery=gallery
|
|
50
38
|
# the agent says where to look and stops. After your Send:
|
|
51
39
|
mockpit feedback
|
|
52
40
|
```
|
|
@@ -64,18 +52,18 @@ mockpit feedback
|
|
|
64
52
|
Studio puts every variant side by side. Home groups a project's ideas, screens, flows and
|
|
65
53
|
components, and Needs you lists what's waiting on you.
|
|
66
54
|
|
|
67
|
-
## What it shows
|
|
68
|
-
|
|
69
|
-
Html with parts and knobs, plus markdown, diff, terminal, image, mermaid, json and code. All of
|
|
70
|
-
it renders in a sandboxed frame.
|
|
71
|
-
|
|
72
55
|
<table>
|
|
73
56
|
<tr>
|
|
74
|
-
<td width="50%"><img src="docs/
|
|
75
|
-
<td width="50%"><img src="docs/
|
|
57
|
+
<td width="50%"><a href="docs/mockpit-question.png"><img src="docs/mockpit-question.png" width="100%" alt="The HALDE shop asking Which grid?, with Index, Gallery and Editorial as picture answers beside the stage"></a></td>
|
|
58
|
+
<td width="50%"><a href="docs/mockpit-preview.png"><img src="docs/mockpit-preview.png" width="100%" alt="The product page previewing the Bone answer to What colour behind the products? before it is picked"></a></td>
|
|
76
59
|
</tr>
|
|
77
60
|
</table>
|
|
78
61
|
|
|
62
|
+
## What it shows
|
|
63
|
+
|
|
64
|
+
Html with parts and knobs, plus markdown, diff, terminal, image, mermaid, json and code. All of
|
|
65
|
+
it renders in a sandboxed frame.
|
|
66
|
+
|
|
79
67
|
## Agent side
|
|
80
68
|
|
|
81
69
|
Eleven verbs, the same on the zero-dependency CLI, MCP (stdio, or HTTP at `/mcp`) and plain
|
package/bin/initDesign.js
CHANGED
|
@@ -14,17 +14,6 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { basename, join, relative } from "node:path";
|
|
16
16
|
|
|
17
|
-
// The palette mapper lives in server/themes.ts so the server and the CLI can
|
|
18
|
-
// never disagree about what a repo's tokens mean. Node type-strips the .ts in a
|
|
19
|
-
// checkout; the packed CLI has only the compiled tree, hence the fallback.
|
|
20
|
-
async function loadThemes() {
|
|
21
|
-
try {
|
|
22
|
-
return await import("../server/themes.ts");
|
|
23
|
-
} catch {
|
|
24
|
-
return await import("../dist/server/themes.js");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
17
|
// Where repos actually keep their global stylesheet. Ordered by how likely the
|
|
29
18
|
// first hit is to be THE token file, because the first file with the most
|
|
30
19
|
// custom properties wins.
|
|
@@ -262,7 +251,6 @@ function hasTailwind(cwd) {
|
|
|
262
251
|
* builtin kit with no palette, which is a perfectly good starting point.
|
|
263
252
|
*/
|
|
264
253
|
export async function detectDesignSystem(cwd) {
|
|
265
|
-
const { paletteFromCssVars } = await loadThemes();
|
|
266
254
|
const budget = { left: MAX_FILES };
|
|
267
255
|
const cssFiles = findCssFiles(cwd, budget);
|
|
268
256
|
|
|
@@ -309,7 +297,6 @@ export async function detectDesignSystem(cwd) {
|
|
|
309
297
|
cssVars: best ? best.count : 0,
|
|
310
298
|
fonts: [...fonts].slice(0, 8),
|
|
311
299
|
},
|
|
312
|
-
palette: cssVars ? paletteFromCssVars(cssVars) : null,
|
|
313
300
|
cssVars,
|
|
314
301
|
// Where the tokens came from, relative to the repo — `init` prints it so
|
|
315
302
|
// the operator can see which file was imported.
|
|
@@ -494,6 +481,7 @@ export function readTokenFiles(files) {
|
|
|
494
481
|
if (isMap(json)) collectTokens(json, [], undefined, all);
|
|
495
482
|
}
|
|
496
483
|
const values = {};
|
|
484
|
+
const types = {};
|
|
497
485
|
let count = 0;
|
|
498
486
|
for (const [name, token] of all) {
|
|
499
487
|
let { type, value } = token;
|
|
@@ -508,9 +496,12 @@ export function readTokenFiles(files) {
|
|
|
508
496
|
const text = ref ? `{${ref}}` : tokenText(value);
|
|
509
497
|
if (text === null) continue;
|
|
510
498
|
count++;
|
|
511
|
-
if (count <= TOKENS_MAX)
|
|
499
|
+
if (count <= TOKENS_MAX) {
|
|
500
|
+
values[name] = text.slice(0, VALUE_MAX);
|
|
501
|
+
types[name] = type;
|
|
502
|
+
}
|
|
512
503
|
}
|
|
513
|
-
return count ? { count, values } : null;
|
|
504
|
+
return count ? { count, values, types } : null;
|
|
514
505
|
}
|
|
515
506
|
|
|
516
507
|
// tsconfig is JSONC: comments and trailing commas, outside strings.
|
|
@@ -613,10 +604,14 @@ export function readDesignFiles(cwd, css = "") {
|
|
|
613
604
|
cssVars:
|
|
614
605
|
tokens.count === names.length && names.every((n) => declared.has(n.replace(/\./g, "-"))),
|
|
615
606
|
values: tokens.values,
|
|
607
|
+
types: tokens.types,
|
|
616
608
|
};
|
|
617
609
|
// Tokens are the one unbounded part; shed them until the blob fits.
|
|
618
610
|
while (JSON.stringify(files).length > DESIGN_FILES_MAX && names.length) {
|
|
619
|
-
for (const n of names.splice(Math.floor(names.length / 2)))
|
|
611
|
+
for (const n of names.splice(Math.floor(names.length / 2))) {
|
|
612
|
+
delete files.tokens.values[n];
|
|
613
|
+
delete files.tokens.types[n];
|
|
614
|
+
}
|
|
620
615
|
}
|
|
621
616
|
}
|
|
622
617
|
const shadcn = readShadcn(cwd);
|
package/bin/mockpit.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
mkdirSync,
|
|
7
7
|
readdirSync,
|
|
8
8
|
readFileSync,
|
|
9
|
+
statSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
writeSync,
|
|
11
12
|
} from "node:fs";
|
|
@@ -13,7 +14,7 @@ import { homedir, tmpdir, userInfo } from "node:os";
|
|
|
13
14
|
import { dirname, join } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { parseArgs } from "node:util";
|
|
16
|
-
import { findBrowser, queuePoster } from "./poster.js";
|
|
17
|
+
import { findBrowser, queuePoster, queueRetakes } from "./poster.js";
|
|
17
18
|
import { serveUrl } from "./serveUrl.js";
|
|
18
19
|
|
|
19
20
|
const BASE = (process.env.MOCKPIT_URL ?? "http://localhost:8228").replace(/\/$/, "");
|
|
@@ -53,8 +54,8 @@ the loop (never blocks on the user):
|
|
|
53
54
|
|
|
54
55
|
verbs:
|
|
55
56
|
mockpit init [--project name] [--kit <id>] [--kit-url <url> --kit-doc <file|text>]
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
read the repo's tokens, kit and icon sets
|
|
58
|
+
into the project's system, write .mockpit/starter.html
|
|
58
59
|
mockpit publish --design <slug> --html <file> [options]
|
|
59
60
|
create or version one variant of one state
|
|
60
61
|
--state <label> state, in the user's words (omit for a single-state design)
|
|
@@ -92,7 +93,8 @@ verbs:
|
|
|
92
93
|
mockpit ask --design <slug> "<question>" --option <label[=variant]> ...
|
|
93
94
|
ask the user; bind options to variants
|
|
94
95
|
--option <l[=v]> an option (repeatable); "=variant" binds it to a variant,
|
|
95
|
-
"=variant@3" to its version 3, "=knob:path=value" sets one knob
|
|
96
|
+
"=variant@3" to its version 3, "=knob:path=value" sets one knob,
|
|
97
|
+
"=palette:id" chooses a palette of the project's system
|
|
96
98
|
--recommend <l> the option you would pick, by label; a quiet mark on it
|
|
97
99
|
--scope <s> design|state|part (default design)
|
|
98
100
|
--state <label> the state a state-scoped question is about
|
|
@@ -118,6 +120,9 @@ verbs:
|
|
|
118
120
|
rename a project
|
|
119
121
|
mockpit edit --project <name> --area-order "Shell,Food"
|
|
120
122
|
the order Home lists areas in
|
|
123
|
+
mockpit system [--sheet f.css] [--classes j] [--tokens j] [--scales j] [--palettes j] ...
|
|
124
|
+
the project's system: read it, or change a part
|
|
125
|
+
(applies to every design at once)
|
|
121
126
|
mockpit export --design <slug> [--state s] [--variant v] [--out <dir>]
|
|
122
127
|
write the html in use + history per state
|
|
123
128
|
to .mockpit/accepted/<design>/<state>/
|
|
@@ -169,7 +174,7 @@ environment:
|
|
|
169
174
|
const COMMAND_HELP = {
|
|
170
175
|
init: `mockpit init [--project <name>] [--kit <id>]
|
|
171
176
|
[--kit-url <https url> [--kit-script <url>] --kit-doc <file.md|text>]
|
|
172
|
-
|
|
177
|
+
Read the repo's tokens and kit into the project's system, add the
|
|
173
178
|
Iconify sets matching its icon packages, and write .mockpit/starter.html
|
|
174
179
|
(gitignored). --kit makes a bundled or project kit the default for every html
|
|
175
180
|
surface. --kit-url defines the project's own kit (id from --kit, else
|
|
@@ -178,7 +183,8 @@ const COMMAND_HELP = {
|
|
|
178
183
|
mockpit rules --mine
|
|
179
184
|
Guidance on top of the core brief, highest first: the project's
|
|
180
185
|
.mockpit/rules.md (committed with the repo), your ~/.mockpit/rules.md, then
|
|
181
|
-
the packs either names (\`packs: [design-craft]\` in its frontmatter
|
|
186
|
+
the packs either names (\`packs: [design-craft, foundations]\` in its frontmatter:
|
|
187
|
+
design-craft for how to design, foundations for starting a project's system). A file
|
|
182
188
|
has "## Rules" (always followed) and "## Suggestions" (aimed for).
|
|
183
189
|
This sends the file as it is on disk; a missing file clears the stored copy.
|
|
184
190
|
init sends the project's too. A local server reads yours live, so --mine is
|
|
@@ -297,11 +303,24 @@ mockpit edit --project <name> --area-order "Shell,Food,Chat"
|
|
|
297
303
|
edit of it stands, so the draft is then not applied and the result says so.
|
|
298
304
|
--first-state names a design's one unnamed state, and every variant it holds goes
|
|
299
305
|
there, so the design can then have more states.
|
|
300
|
-
--rename moves the project's designs, sessions, flows and
|
|
306
|
+
--rename moves the project's designs, sessions, flows, system and settings to the
|
|
301
307
|
new name (refused if that project has designs); pass --project <new> or set
|
|
302
308
|
MOCKPIT_PROJECT afterwards if the repo still resolves to the old name.
|
|
303
309
|
--area-order lists the project's areas in the order Home shows them; areas it
|
|
304
310
|
leaves out follow alphabetically, "" goes back to alphabetical.`,
|
|
311
|
+
system: `mockpit system [--project <name>] [--json]
|
|
312
|
+
mockpit system [--sheet <file.css>|""] [--classes <json>] [--fonts <json>] [--tokens <json>]
|
|
313
|
+
[--scales <json>] [--roles <json>] [--palettes <json>] [--pairs <json>] [--as <json>]
|
|
314
|
+
[--knobs <json>] [--remove <json>]
|
|
315
|
+
mockpit system --restore <rev>
|
|
316
|
+
The project's system: a shared stylesheet, typed tokens, type and space scales, colour
|
|
317
|
+
palettes and project knobs that every render of the project wears. With no flags it
|
|
318
|
+
prints the system, its checks and its latest revisions (--json: all of it). Each flag
|
|
319
|
+
replaces that part and leaves the rest; --tokens, --scales and --palettes upsert by
|
|
320
|
+
name or id, --remove '{"tokens":["x"]}' takes them out. A JSON flag may name a file.
|
|
321
|
+
Every change is a new revision the user can restore. The repo's own tokens (from
|
|
322
|
+
mockpit init) can't be written here: change the code and rerun init.
|
|
323
|
+
mockpit guide --topic system has the contract.`,
|
|
305
324
|
run: `mockpit run <file|-> [--session <id>] [--project <name>] [--json]
|
|
306
325
|
Run a script (the body of an async function, plain JavaScript) in the server's
|
|
307
326
|
sandbox against the mockpit API; see mockpit guide --topic scripts. Prints each
|
|
@@ -322,8 +341,8 @@ mockpit edit --project <name> --area-order "Shell,Food,Chat"
|
|
|
322
341
|
Upload an asset; prints its id and URL. Use the id as an image surface's assetId.`,
|
|
323
342
|
guide: `mockpit guide [--topic <id>] [--project <name>]
|
|
324
343
|
No flag prints the brief: the loop, parts, questions and knobs, feedback, and
|
|
325
|
-
this project's
|
|
326
|
-
(knobs, asks, surfaces, html, feedback, http, scripts).`,
|
|
344
|
+
this project's system, kit and icons. --topic prints one reference topic
|
|
345
|
+
(system, knobs, asks, surfaces, html, feedback, http, scripts).`,
|
|
327
346
|
};
|
|
328
347
|
|
|
329
348
|
// `console.log(...)` on a pipe is asynchronous, so exiting on the next line
|
|
@@ -728,20 +747,55 @@ async function installIconSets(project, prefixes) {
|
|
|
728
747
|
errors.push({ prefix, message: err instanceof Error ? err.message : String(err) });
|
|
729
748
|
}
|
|
730
749
|
}
|
|
731
|
-
if (added.length === 0) return { added: [], errors,
|
|
732
|
-
const stored = (await api(`${projectPath(project)}/
|
|
750
|
+
if (added.length === 0) return { added: [], errors, system: null };
|
|
751
|
+
const stored = (await api(`${projectPath(project)}/system`)) ?? {};
|
|
733
752
|
const keep = (stored.iconSets ?? []).filter((s) => !added.some((a) => a.prefix === s.prefix));
|
|
734
|
-
|
|
753
|
+
await api(`${projectPath(project)}/system`, {
|
|
735
754
|
method: "PUT",
|
|
736
|
-
body: JSON.stringify({
|
|
755
|
+
body: JSON.stringify({ iconSets: [...keep, ...added] }),
|
|
737
756
|
});
|
|
757
|
+
const system = await api(`${projectPath(project)}/system`);
|
|
738
758
|
return {
|
|
739
|
-
added:
|
|
759
|
+
added: system.iconSets.filter((s) => added.some((a) => a.prefix === s.prefix)),
|
|
740
760
|
errors,
|
|
741
|
-
|
|
761
|
+
system,
|
|
742
762
|
};
|
|
743
763
|
}
|
|
744
764
|
|
|
765
|
+
// Every page of the project looks different after a system change, so its stills are
|
|
766
|
+
// taken again in the background; until then the overviews run the pages live.
|
|
767
|
+
function retakeStills(project, session) {
|
|
768
|
+
queueRetakes({ base: BASE, token: TOKEN, client: HEADERS["x-mockpit-client"], session, project });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// The system at a glance; `--json` prints all of it.
|
|
772
|
+
function printSystem(s) {
|
|
773
|
+
const palettes = s.color?.palettes ?? [];
|
|
774
|
+
const lines = [
|
|
775
|
+
`${s.project} · r${s.rev}${s.kit && s.kit !== "none" ? ` · kit ${s.kit}` : ""}`,
|
|
776
|
+
s.sheet
|
|
777
|
+
? `sheet ${(s.sheet.css.length / 1000).toFixed(1)} KB · ${s.sheet.classes.length} classes`
|
|
778
|
+
: null,
|
|
779
|
+
s.fonts?.length ? `fonts ${s.fonts.map((f) => f.family).join(", ")}` : null,
|
|
780
|
+
s.scales?.length
|
|
781
|
+
? `scales ${s.scales.map((x) => `${x.id} (${x.steps.join(" ")})`).join(", ")}`
|
|
782
|
+
: null,
|
|
783
|
+
s.tokens?.length
|
|
784
|
+
? `tokens ${s.tokens.filter((t) => !t.scale).length} (+${s.tokens.filter((t) => t.scale).length} from scales)`
|
|
785
|
+
: null,
|
|
786
|
+
s.color?.roles?.length ? `roles ${s.color.roles.map((r) => r.name).join(", ")}` : null,
|
|
787
|
+
palettes.length
|
|
788
|
+
? `palettes ${palettes.map((p) => `${p.label} (${p.status})`).join(", ")}`
|
|
789
|
+
: null,
|
|
790
|
+
Object.keys(s.knobs ?? {}).length ? `knobs ${Object.keys(s.knobs).join(", ")}` : null,
|
|
791
|
+
].filter(Boolean);
|
|
792
|
+
for (const l of lines) console.log(l);
|
|
793
|
+
for (const c of s.checks ?? []) console.log(`check: ${c}`);
|
|
794
|
+
for (const r of (s.revisions ?? []).slice(0, 5))
|
|
795
|
+
console.log(`r${r.rev} ${r.at.slice(0, 16)} ${r.summary}`);
|
|
796
|
+
if (s.url) console.log(s.url);
|
|
797
|
+
}
|
|
798
|
+
|
|
745
799
|
// `--doc` is a file when one exists at that path, else the cheat sheet itself.
|
|
746
800
|
function readKitDoc(doc) {
|
|
747
801
|
if (doc === undefined) return undefined;
|
|
@@ -754,9 +808,9 @@ function readKitDoc(doc) {
|
|
|
754
808
|
// The server checks the URLs against the CDN allowlist and the doc's length;
|
|
755
809
|
// its one-line error is the message.
|
|
756
810
|
function putProjectKit(project, id, { url, script, doc }) {
|
|
757
|
-
return api(`${projectPath(project)}/
|
|
811
|
+
return api(`${projectPath(project)}/system`, {
|
|
758
812
|
method: "PUT",
|
|
759
|
-
body: JSON.stringify({ href: url, script, doc: readKitDoc(doc) }),
|
|
813
|
+
body: JSON.stringify({ projectKits: [{ id, href: url, script, doc: readKitDoc(doc) }] }),
|
|
760
814
|
});
|
|
761
815
|
}
|
|
762
816
|
|
|
@@ -902,6 +956,30 @@ const DESIGN_FLAGS = {
|
|
|
902
956
|
session: { type: "string" },
|
|
903
957
|
};
|
|
904
958
|
|
|
959
|
+
// `mockpit export` with no design: the project's system as files code can import.
|
|
960
|
+
async function exportSystem(flags) {
|
|
961
|
+
const project = resolveProject(flags).name;
|
|
962
|
+
const data = await api(
|
|
963
|
+
`/api/projects/${encodeURIComponent(project)}/system/export`,
|
|
964
|
+
{},
|
|
965
|
+
{ report: "die", fix: "mockpit system" },
|
|
966
|
+
);
|
|
967
|
+
if (flags.json) return out(data);
|
|
968
|
+
const dir = flags.out ? flags.out : join(process.cwd(), ".mockpit", "system");
|
|
969
|
+
mkdirSync(dir, { recursive: true });
|
|
970
|
+
const files = [];
|
|
971
|
+
const write = (name, text) => {
|
|
972
|
+
if (!text) return;
|
|
973
|
+
writeFileSync(join(dir, name), text);
|
|
974
|
+
files.push(name);
|
|
975
|
+
};
|
|
976
|
+
write("sheet.css", data.sheet);
|
|
977
|
+
write("tokens.css", data.css);
|
|
978
|
+
const { sheet: _s, css: _c, ...rest } = data;
|
|
979
|
+
write("system.json", JSON.stringify(rest, null, 2) + "\n");
|
|
980
|
+
if (!flags.quiet) console.log(`${project} system r${data.rev} → ${dir} (${files.join(", ")})`);
|
|
981
|
+
}
|
|
982
|
+
|
|
905
983
|
function requireDesign(flags, example) {
|
|
906
984
|
const slug = flags.design ? slugify(flags.design) : "";
|
|
907
985
|
if (!slug) die("--design needs a slug", example);
|
|
@@ -1208,7 +1286,14 @@ async function publishDesign() {
|
|
|
1208
1286
|
// "Before=quiet@2" to its version 2 ("=@2" names the state's only variant);
|
|
1209
1287
|
// "Above=knob:trim.position=top" sets one knob. The value is JSON when it
|
|
1210
1288
|
// parses (17, true), else a string, so slider and toggle values keep their type.
|
|
1289
|
+
// "Grape=palette:grape" chooses a palette of the project's system.
|
|
1211
1290
|
function parseOption(raw) {
|
|
1291
|
+
const palette = raw.indexOf("=palette:");
|
|
1292
|
+
if (palette > 0) {
|
|
1293
|
+
const id = raw.slice(palette + "=palette:".length);
|
|
1294
|
+
if (!id) die(`option "${raw}" needs palette:<id>`, "--option Grape=palette:grape");
|
|
1295
|
+
return { label: raw.slice(0, palette), palette: id };
|
|
1296
|
+
}
|
|
1212
1297
|
const knob = raw.indexOf("=knob:");
|
|
1213
1298
|
if (knob > 0) {
|
|
1214
1299
|
const pair = raw.slice(knob + "=knob:".length);
|
|
@@ -1530,7 +1615,7 @@ const commands = {
|
|
|
1530
1615
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1531
1616
|
},
|
|
1532
1617
|
|
|
1533
|
-
//
|
|
1618
|
+
// Read the repo's tokens and kit once into the project's system, and leave a
|
|
1534
1619
|
// starter file behind — so every later publish is markup only, with no CSS or
|
|
1535
1620
|
// icon paths pasted into the agent's context.
|
|
1536
1621
|
async init() {
|
|
@@ -1553,7 +1638,7 @@ const commands = {
|
|
|
1553
1638
|
const designSystem = await detectDesignSystem(process.cwd());
|
|
1554
1639
|
const d = designSystem.detected;
|
|
1555
1640
|
// `detected` is always an object, so emptiness has to be read off the
|
|
1556
|
-
// fields — otherwise a repo with
|
|
1641
|
+
// fields — otherwise a repo with nothing to read printed a blank line.
|
|
1557
1642
|
const stripped = designSystem.strippedImports?.length
|
|
1558
1643
|
? ` (stripped: ${designSystem.strippedImports.join(", ")})`
|
|
1559
1644
|
: "";
|
|
@@ -1573,14 +1658,18 @@ const commands = {
|
|
|
1573
1658
|
found.length ? found.join(" · ") : "nothing detected — using the built-in palette",
|
|
1574
1659
|
);
|
|
1575
1660
|
let kit = flags.kit ?? designSystem.kit ?? "builtin";
|
|
1576
|
-
//
|
|
1661
|
+
// A project kit goes in the same write, so the system can name it as default.
|
|
1662
|
+
let projectKits;
|
|
1577
1663
|
if (flags["kit-url"] !== undefined) {
|
|
1578
1664
|
kit = flags.kit ?? "project";
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1665
|
+
projectKits = [
|
|
1666
|
+
{
|
|
1667
|
+
id: kit,
|
|
1668
|
+
href: flags["kit-url"],
|
|
1669
|
+
script: flags["kit-script"],
|
|
1670
|
+
doc: readKitDoc(flags["kit-doc"]),
|
|
1671
|
+
},
|
|
1672
|
+
];
|
|
1584
1673
|
}
|
|
1585
1674
|
say("kit:", kit);
|
|
1586
1675
|
const files = designSystem.designFiles;
|
|
@@ -1598,18 +1687,45 @@ const commands = {
|
|
|
1598
1687
|
);
|
|
1599
1688
|
}
|
|
1600
1689
|
|
|
1601
|
-
|
|
1690
|
+
const wrote = await api(`${projectPath(project)}/system`, {
|
|
1602
1691
|
method: "PUT",
|
|
1603
1692
|
body: JSON.stringify({
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1693
|
+
repo: {
|
|
1694
|
+
detected: designSystem.detected ?? null,
|
|
1695
|
+
cssVars: designSystem.cssVars ?? "",
|
|
1696
|
+
tailwindCss: designSystem.tailwindCss ?? "",
|
|
1697
|
+
strippedImports: designSystem.strippedImports ?? [],
|
|
1698
|
+
designFiles: designSystem.designFiles ?? null,
|
|
1699
|
+
},
|
|
1609
1700
|
kit,
|
|
1610
|
-
|
|
1701
|
+
...(projectKits ? { projectKits } : {}),
|
|
1611
1702
|
}),
|
|
1612
1703
|
});
|
|
1704
|
+
if (wrote?.changed?.length) {
|
|
1705
|
+
const session = await resolveSession(flags);
|
|
1706
|
+
if (session) retakeStills(project, session);
|
|
1707
|
+
}
|
|
1708
|
+
const derived = wrote?.derived;
|
|
1709
|
+
if (derived) {
|
|
1710
|
+
say(
|
|
1711
|
+
"system:",
|
|
1712
|
+
[
|
|
1713
|
+
`r${wrote.rev}`,
|
|
1714
|
+
derived.roles
|
|
1715
|
+
? `${derived.roles} colour role${derived.roles === 1 ? "" : "s"} (palette repo)`
|
|
1716
|
+
: null,
|
|
1717
|
+
derived.tokens
|
|
1718
|
+
? `${derived.tokens} token${derived.tokens === 1 ? "" : "s"}, repo-owned`
|
|
1719
|
+
: null,
|
|
1720
|
+
derived.untyped?.length
|
|
1721
|
+
? `${derived.untyped.length} untyped, from the raw block only`
|
|
1722
|
+
: null,
|
|
1723
|
+
]
|
|
1724
|
+
.filter(Boolean)
|
|
1725
|
+
.join(" · "),
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
let stored = await api(`${projectPath(project)}/system`);
|
|
1613
1729
|
|
|
1614
1730
|
// Sets the repo already uses are added the same way `icons add` does; one
|
|
1615
1731
|
// that cannot be fetched (offline) is reported, never fatal to init.
|
|
@@ -1630,7 +1746,7 @@ const commands = {
|
|
|
1630
1746
|
if (added) say("icons:", `${f.prefix} (${added.count} icons, from ${f.from})`);
|
|
1631
1747
|
else say("icons:", `${f.prefix} not added: ${error?.message ?? "unknown error"}`);
|
|
1632
1748
|
}
|
|
1633
|
-
if (result.
|
|
1749
|
+
if (result.system) stored = result.system;
|
|
1634
1750
|
}
|
|
1635
1751
|
const iconSets = [
|
|
1636
1752
|
...new Set([...(stored?.iconSets ?? []).map((s) => s.prefix), ...BUNDLED_ICON_SETS]),
|
|
@@ -1647,7 +1763,7 @@ const commands = {
|
|
|
1647
1763
|
say("rules:", rulesSummary(await sendRules(rulesFile, `${projectPath(project)}/rules`)));
|
|
1648
1764
|
}
|
|
1649
1765
|
say("next:", "mockpit guide");
|
|
1650
|
-
if (flags.json) out({ project,
|
|
1766
|
+
if (flags.json) out({ project, system: stored, derived: derived ?? null, starter });
|
|
1651
1767
|
},
|
|
1652
1768
|
|
|
1653
1769
|
async publish() {
|
|
@@ -1808,6 +1924,7 @@ const commands = {
|
|
|
1808
1924
|
? ` · ${list.open} open question${list.open === 1 ? "" : "s"}`
|
|
1809
1925
|
: " · nothing waiting on the user"),
|
|
1810
1926
|
);
|
|
1927
|
+
if (list.system) console.log(` system ${list.system}`);
|
|
1811
1928
|
for (const m of designs) {
|
|
1812
1929
|
const states = m.states.length ? m.states.join(" / ") : "single state";
|
|
1813
1930
|
console.log(
|
|
@@ -1820,7 +1937,8 @@ const commands = {
|
|
|
1820
1937
|
console.log(
|
|
1821
1938
|
`Decided, ${list.decisionList.total}. Treat as settled unless the user says otherwise:`,
|
|
1822
1939
|
);
|
|
1823
|
-
for (const d of list.decisionList.lines)
|
|
1940
|
+
for (const d of list.decisionList.lines)
|
|
1941
|
+
console.log(` ${d.system ? "project" : d.design}: ${d.text}`);
|
|
1824
1942
|
if (list.decisionList.more) console.log(` ${list.decisionList.more} (--decisions)`);
|
|
1825
1943
|
}
|
|
1826
1944
|
},
|
|
@@ -1834,6 +1952,7 @@ const commands = {
|
|
|
1834
1952
|
out: { type: "string" },
|
|
1835
1953
|
},
|
|
1836
1954
|
});
|
|
1955
|
+
if (!flags.design) return exportSystem(flags);
|
|
1837
1956
|
const slug = requireDesign(flags, "mockpit export --design writer");
|
|
1838
1957
|
const project = resolveProject(flags).name;
|
|
1839
1958
|
const data = await api(
|
|
@@ -2268,14 +2387,14 @@ const commands = {
|
|
|
2268
2387
|
if (sub === "add") {
|
|
2269
2388
|
const result = await installIconSets(project, sets);
|
|
2270
2389
|
if (result.errors.length) fail(result.errors.map((e) => e.message).join("; "));
|
|
2271
|
-
if (flags.json) return out(result.
|
|
2390
|
+
if (flags.json) return out(result.system);
|
|
2272
2391
|
for (const a of result.added) {
|
|
2273
2392
|
if (!flags.quiet) console.log(`added ${a.prefix} (${a.count} icons)`);
|
|
2274
2393
|
}
|
|
2275
2394
|
return;
|
|
2276
2395
|
}
|
|
2277
2396
|
if (sub === "remove") {
|
|
2278
|
-
const stored = await api(`${projectPath(project)}/
|
|
2397
|
+
const stored = await api(`${projectPath(project)}/system`);
|
|
2279
2398
|
const installed = stored?.iconSets ?? [];
|
|
2280
2399
|
for (const set of sets) {
|
|
2281
2400
|
if (installed.some((s) => s.prefix === set)) continue;
|
|
@@ -2285,14 +2404,11 @@ const commands = {
|
|
|
2285
2404
|
: `${set} is not installed for ${project}`,
|
|
2286
2405
|
);
|
|
2287
2406
|
}
|
|
2288
|
-
const
|
|
2407
|
+
const result = await api(`${projectPath(project)}/system`, {
|
|
2289
2408
|
method: "PUT",
|
|
2290
|
-
body: JSON.stringify({
|
|
2291
|
-
...stored,
|
|
2292
|
-
iconSets: installed.filter((s) => !sets.includes(s.prefix)),
|
|
2293
|
-
}),
|
|
2409
|
+
body: JSON.stringify({ iconSets: installed.filter((s) => !sets.includes(s.prefix)) }),
|
|
2294
2410
|
});
|
|
2295
|
-
if (flags.json) return out(
|
|
2411
|
+
if (flags.json) return out(result);
|
|
2296
2412
|
if (!flags.quiet) console.log(`removed ${sets.join(", ")}`);
|
|
2297
2413
|
return;
|
|
2298
2414
|
}
|
|
@@ -2319,21 +2435,93 @@ const commands = {
|
|
|
2319
2435
|
const { name: project } = resolveProject(flags);
|
|
2320
2436
|
if ((sub !== "add" && sub !== "remove") || !id)
|
|
2321
2437
|
fail("usage: mockpit kit add <id> --url <url> --doc <file|text> | mockpit kit remove <id>");
|
|
2322
|
-
const
|
|
2438
|
+
const result =
|
|
2323
2439
|
sub === "add"
|
|
2324
2440
|
? await putProjectKit(project, id, flags)
|
|
2325
|
-
: await api(`${projectPath(project)}/
|
|
2326
|
-
method: "
|
|
2441
|
+
: await api(`${projectPath(project)}/system`, {
|
|
2442
|
+
method: "PUT",
|
|
2443
|
+
body: JSON.stringify({ removeKits: [id] }),
|
|
2327
2444
|
});
|
|
2328
|
-
if (flags.json) return out(
|
|
2445
|
+
if (flags.json) return out(result);
|
|
2329
2446
|
if (!flags.quiet) console.log(`${sub === "add" ? "added" : "removed"} kit ${id} (${project})`);
|
|
2330
2447
|
},
|
|
2331
2448
|
|
|
2449
|
+
// The project's system: no flags reads it, any flag writes that part.
|
|
2450
|
+
async system() {
|
|
2451
|
+
const JSON_FLAGS = [
|
|
2452
|
+
"classes",
|
|
2453
|
+
"fonts",
|
|
2454
|
+
"tokens",
|
|
2455
|
+
"scales",
|
|
2456
|
+
"roles",
|
|
2457
|
+
"palettes",
|
|
2458
|
+
"pairs",
|
|
2459
|
+
"as",
|
|
2460
|
+
"knobs",
|
|
2461
|
+
"remove",
|
|
2462
|
+
];
|
|
2463
|
+
const { values: flags } = parse({
|
|
2464
|
+
options: {
|
|
2465
|
+
project: { type: "string" },
|
|
2466
|
+
session: { type: "string" },
|
|
2467
|
+
sheet: { type: "string" },
|
|
2468
|
+
restore: { type: "string" },
|
|
2469
|
+
...Object.fromEntries(JSON_FLAGS.map((f) => [f, { type: "string" }])),
|
|
2470
|
+
},
|
|
2471
|
+
});
|
|
2472
|
+
const project = resolveProject(flags).name;
|
|
2473
|
+
const body = {};
|
|
2474
|
+
if (flags.sheet !== undefined) {
|
|
2475
|
+
// CSS text has a block; anything else is a path, which must be a file.
|
|
2476
|
+
const sheet = flags.sheet;
|
|
2477
|
+
if (sheet === "") body.sheet = null;
|
|
2478
|
+
else if (sheet.includes("{")) body.sheet = sheet;
|
|
2479
|
+
else if (!existsSync(sheet))
|
|
2480
|
+
die(`--sheet: no file at ${sheet}`, "mockpit system --sheet sheet.css");
|
|
2481
|
+
else if (!statSync(sheet).isFile())
|
|
2482
|
+
die(`--sheet: ${sheet} is not a file`, "mockpit system --sheet sheet.css");
|
|
2483
|
+
else body.sheet = readFileSync(sheet, "utf8");
|
|
2484
|
+
}
|
|
2485
|
+
for (const f of JSON_FLAGS) {
|
|
2486
|
+
if (flags[f] === undefined) continue;
|
|
2487
|
+
const raw = existsSync(flags[f]) ? readFileSync(flags[f], "utf8") : flags[f];
|
|
2488
|
+
try {
|
|
2489
|
+
body[f] = JSON.parse(raw);
|
|
2490
|
+
} catch {
|
|
2491
|
+
die(`--${f} must be JSON (or a JSON file)`, `mockpit guide --topic system`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (flags.restore !== undefined) body.restore = Number(flags.restore);
|
|
2495
|
+
if (Object.keys(body).length === 0) {
|
|
2496
|
+
const system = await api(`${projectPath(project)}/system`, {}, { report: "die" });
|
|
2497
|
+
if (flags.json || !process.stdout.isTTY) return out(system);
|
|
2498
|
+
printSystem(system);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const session = (await resolveSession(flags, { create: true })) ?? undefined;
|
|
2502
|
+
const result = await api(
|
|
2503
|
+
`${projectPath(project)}/system`,
|
|
2504
|
+
{ method: "PUT", body: JSON.stringify({ ...body, session }) },
|
|
2505
|
+
{ report: "die", fix: "mockpit guide --topic system" },
|
|
2506
|
+
);
|
|
2507
|
+
if (result.changed?.length) retakeStills(project, session);
|
|
2508
|
+
if (flags.json) return out(result);
|
|
2509
|
+
if (flags.quiet) return;
|
|
2510
|
+
console.log(
|
|
2511
|
+
result.changed?.length
|
|
2512
|
+
? `r${result.rev}: ${result.changed.join("; ")}`
|
|
2513
|
+
: `r${result.rev}: nothing changed`,
|
|
2514
|
+
);
|
|
2515
|
+
for (const c of result.checks ?? []) console.log(`check: ${c}`);
|
|
2516
|
+
console.log(result.url);
|
|
2517
|
+
printFeedback(result.feedback);
|
|
2518
|
+
},
|
|
2519
|
+
|
|
2332
2520
|
async guide() {
|
|
2333
2521
|
const { values: flags } = parse({
|
|
2334
2522
|
options: { topic: { type: "string" }, project: { type: "string" } },
|
|
2335
2523
|
});
|
|
2336
|
-
if (flags.topic !== undefined) return printTopic(flags.topic);
|
|
2524
|
+
if (flags.topic !== undefined) return printTopic(flags.topic, flags);
|
|
2337
2525
|
return printBrief(flags);
|
|
2338
2526
|
},
|
|
2339
2527
|
|
|
@@ -2400,7 +2588,7 @@ const commands = {
|
|
|
2400
2588
|
// no server running.
|
|
2401
2589
|
const TOPICS_DIR = join(ROOT, "guide", "topics");
|
|
2402
2590
|
|
|
2403
|
-
async function printTopic(topic) {
|
|
2591
|
+
async function printTopic(topic, flags = {}) {
|
|
2404
2592
|
const topics = readdirSync(TOPICS_DIR)
|
|
2405
2593
|
.filter((f) => f.endsWith(".md"))
|
|
2406
2594
|
.map((f) => f.slice(0, -3))
|
|
@@ -2411,11 +2599,13 @@ async function printTopic(topic) {
|
|
|
2411
2599
|
`mockpit guide --topic ${topics[0]}`,
|
|
2412
2600
|
);
|
|
2413
2601
|
}
|
|
2414
|
-
const path = `/agent-howto?${new URLSearchParams(
|
|
2602
|
+
const path = `/agent-howto?${new URLSearchParams(
|
|
2603
|
+
topic === "system" ? { topic, project: resolveProject(flags).name } : { topic },
|
|
2604
|
+
)}`;
|
|
2415
2605
|
console.log(await fetchTextWithFallback(path, join(TOPICS_DIR, `${topic}.md`)));
|
|
2416
2606
|
}
|
|
2417
2607
|
|
|
2418
|
-
// The brief is rendered from the project's stored
|
|
2608
|
+
// The brief is rendered from the project's stored system, so the
|
|
2419
2609
|
// server's copy is the real one. With no server it still has to print: an
|
|
2420
2610
|
// agent reads it before anything else, so render the generic brief locally
|
|
2421
2611
|
// (the same code the server runs) and say where the project-aware one comes from.
|
|
@@ -2428,7 +2618,7 @@ async function printBrief(flags) {
|
|
|
2428
2618
|
const { renderBriefGuide } = await import(entrypoint("server", "designGuide.ts"));
|
|
2429
2619
|
console.log(renderBriefGuide(null));
|
|
2430
2620
|
console.error(
|
|
2431
|
-
`note: no mockpit at ${BASE}; this is the generic brief. Run mockpit serve, then mockpit init for this project's
|
|
2621
|
+
`note: no mockpit at ${BASE}; this is the generic brief. Run mockpit serve, then mockpit init for this project's system, kit and icons.`,
|
|
2432
2622
|
);
|
|
2433
2623
|
}
|
|
2434
2624
|
|