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/bin/poster.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// capture means the page runs live in the overviews, which is what it did before posters.
|
|
9
9
|
//
|
|
10
10
|
// The worker renders the served document and doesn't take a poster's place: one sent by the
|
|
11
|
-
// agent wins, and the server refuses a still of a version that has since changed.
|
|
11
|
+
// agent wins, and the server refuses a still of a version that has since changed. A system
|
|
12
|
+
// write retakes the project's stills: one taken under an older revision no longer shows.
|
|
12
13
|
|
|
13
14
|
import { spawn } from "node:child_process";
|
|
14
15
|
import {
|
|
@@ -34,6 +35,8 @@ const CAPTURE_MS = 15_000;
|
|
|
34
35
|
const LOAD_MS = 8_000;
|
|
35
36
|
// A job this old describes a burst long past, and its version has likely moved on.
|
|
36
37
|
const JOB_MAX_AGE_MS = 2 * 60_000;
|
|
38
|
+
// Stills one system change retakes, latest variants first: the rest run live until then.
|
|
39
|
+
const RETAKE_MAX = 40;
|
|
37
40
|
// Stays up briefly for the next publish of a burst, which saves starting a browser again.
|
|
38
41
|
const WORKER_IDLE_MS = 3_000;
|
|
39
42
|
const WORKER_MAX_MS = 10 * 60_000;
|
|
@@ -111,6 +114,12 @@ export function queuePoster(job) {
|
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
// { base, token, client, session, project }: after a system write that changed something.
|
|
118
|
+
// The worker reads which variants need a new still, so this never waits either.
|
|
119
|
+
export function queueRetakes(job) {
|
|
120
|
+
queuePoster({ ...job, retake: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
// --- the browser, over the DevTools protocol on a local port ---
|
|
115
124
|
|
|
116
125
|
async function launchBrowser(path) {
|
|
@@ -251,12 +260,48 @@ async function shoot(port, job, url, width) {
|
|
|
251
260
|
|
|
252
261
|
// --- one job, end to end ---
|
|
253
262
|
|
|
263
|
+
const headersOf = (job) => ({
|
|
264
|
+
"content-type": "application/json",
|
|
265
|
+
"x-mockpit-client": job.client,
|
|
266
|
+
...(job.token ? { authorization: `Bearer ${job.token}` } : {}),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// A retake becomes one job per live variant whose page shows no still now: the views
|
|
270
|
+
// already leave out one taken under another revision, and keep the agent's own.
|
|
271
|
+
async function expandRetake(dir, job) {
|
|
272
|
+
const res = await fetch(`${job.base}/api/projects/${encodeURIComponent(job.project)}/tree`, {
|
|
273
|
+
headers: headersOf(job),
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) return;
|
|
276
|
+
const { tree } = await res.json();
|
|
277
|
+
const wanted = (tree ?? [])
|
|
278
|
+
.filter((d) => !d.archivedAt)
|
|
279
|
+
.flatMap((d) =>
|
|
280
|
+
(d.variants ?? [])
|
|
281
|
+
.filter((v) => v.status !== "archived" && v.surface >= 0 && !v.poster)
|
|
282
|
+
.map((v) => ({ design: d.id, postId: v.postId, version: v.version, at: v.updatedAt })),
|
|
283
|
+
)
|
|
284
|
+
.sort((a, b) => String(b.at).localeCompare(String(a.at)))
|
|
285
|
+
.slice(0, RETAKE_MAX);
|
|
286
|
+
const { retake: _r, project: _p, ...base } = job;
|
|
287
|
+
for (const [i, v] of wanted.entries()) {
|
|
288
|
+
const name = `${Date.now()}-${String(i).padStart(3, "0")}-retake.job`;
|
|
289
|
+
writeFileSync(
|
|
290
|
+
join(dir, name),
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
...base,
|
|
293
|
+
design: v.design,
|
|
294
|
+
postId: v.postId,
|
|
295
|
+
version: v.version,
|
|
296
|
+
queuedAt: Date.now(),
|
|
297
|
+
}),
|
|
298
|
+
{ mode: 0o600 },
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
async function capture(port, job) {
|
|
255
|
-
const headers =
|
|
256
|
-
"content-type": "application/json",
|
|
257
|
-
"x-mockpit-client": job.client,
|
|
258
|
-
...(job.token ? { authorization: `Bearer ${job.token}` } : {}),
|
|
259
|
-
};
|
|
304
|
+
const headers = headersOf(job);
|
|
260
305
|
const get = async (path) => {
|
|
261
306
|
const res = await fetch(`${job.base}${path}`, { headers });
|
|
262
307
|
if (!res.ok) throw new Error(`${path}: ${res.status}`);
|
|
@@ -283,6 +328,7 @@ async function capture(port, job) {
|
|
|
283
328
|
ver: String(job.version),
|
|
284
329
|
...(mode ? { mode } : {}),
|
|
285
330
|
...(rewrites ? { w: String(rewrites) } : {}),
|
|
331
|
+
...(design.systemRev ? { y: String(design.systemRev) } : {}),
|
|
286
332
|
});
|
|
287
333
|
const width = design.widths?.[0] ?? DEFAULT_WIDTH;
|
|
288
334
|
const data = await shoot(
|
|
@@ -299,7 +345,14 @@ async function capture(port, job) {
|
|
|
299
345
|
});
|
|
300
346
|
await post(
|
|
301
347
|
`/api/designs/${encodeURIComponent(job.design)}/variants/${encodeURIComponent(job.postId)}/poster`,
|
|
302
|
-
{
|
|
348
|
+
{
|
|
349
|
+
version: job.version,
|
|
350
|
+
rewrites,
|
|
351
|
+
poster: asset.id,
|
|
352
|
+
mode,
|
|
353
|
+
system: design.systemRev ?? 0,
|
|
354
|
+
session: job.session,
|
|
355
|
+
},
|
|
303
356
|
);
|
|
304
357
|
}
|
|
305
358
|
|
|
@@ -350,6 +403,10 @@ async function work() {
|
|
|
350
403
|
}
|
|
351
404
|
rmSync(file, { force: true });
|
|
352
405
|
if (!job || !path || Date.now() - job.queuedAt > JOB_MAX_AGE_MS) continue;
|
|
406
|
+
if (job.retake) {
|
|
407
|
+
await expandRetake(dir, job).catch(() => {});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
353
410
|
try {
|
|
354
411
|
if (!browser?.alive()) browser = await launchBrowser(path);
|
|
355
412
|
if (browser) {
|
package/dist/mcp/server.js
CHANGED
|
@@ -68,6 +68,22 @@ async function api(path, init = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
// Stdio runs on the agent's machine, so a value may name a file: markup and
|
|
70
70
|
// asset bytes never have to travel through the model's context.
|
|
71
|
+
// A sheet is CSS text when it has a block, else a path: a mistyped path must fail
|
|
72
|
+
// rather than be sent as the project's stylesheet.
|
|
73
|
+
function readSheet(value) {
|
|
74
|
+
if (value === "" || value.includes("{"))
|
|
75
|
+
return value;
|
|
76
|
+
let stat;
|
|
77
|
+
try {
|
|
78
|
+
stat = statSync(value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error(`sheet: no file at ${value} (pass CSS text, or a path to a .css file)`);
|
|
82
|
+
}
|
|
83
|
+
if (!stat.isFile())
|
|
84
|
+
throw new Error(`sheet: ${value} is not a file`);
|
|
85
|
+
return readFileSync(value, "utf8");
|
|
86
|
+
}
|
|
71
87
|
function readMaybeFile(value) {
|
|
72
88
|
if (value.includes("\n") || value.length > 512)
|
|
73
89
|
return value;
|
|
@@ -284,11 +300,35 @@ const handlers = {
|
|
|
284
300
|
}
|
|
285
301
|
return post(`/api/designs/${enc(args.design)}`, { ...args, project, session }, "PATCH");
|
|
286
302
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
async system(args) {
|
|
304
|
+
const session = await ensureSession();
|
|
305
|
+
const { project: named, ...rest } = args;
|
|
306
|
+
const project = resolveProject(named);
|
|
307
|
+
const writes = Object.keys(rest).filter((k) => rest[k] !== undefined);
|
|
308
|
+
if (writes.length === 0)
|
|
309
|
+
return json(`/api/projects/${enc(project)}/system`);
|
|
310
|
+
const body = typeof rest.sheet === "string" ? { ...rest, sheet: readSheet(rest.sheet) } : rest;
|
|
311
|
+
const result = await post(`/api/projects/${enc(project)}/system`, { ...body, session }, "PUT");
|
|
312
|
+
// Every page looks different now: its stills are taken again in the background.
|
|
313
|
+
const taking = result?.changed?.length ? await posters : null;
|
|
314
|
+
if (taking) {
|
|
315
|
+
taking.queueRetakes({
|
|
316
|
+
base: API.replace(/\/$/, ""),
|
|
317
|
+
token: TOKEN,
|
|
318
|
+
client: CLIENT,
|
|
319
|
+
session,
|
|
320
|
+
project,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
},
|
|
325
|
+
export: (args) => args.design === undefined
|
|
326
|
+
? json(`/api/projects/${enc(resolveProject(args.project))}/system/export`)
|
|
327
|
+
: json(`/api/designs/${enc(args.design)}/export${query({
|
|
328
|
+
project: resolveProject(args.project),
|
|
329
|
+
state: args.state,
|
|
330
|
+
variant: args.variant,
|
|
331
|
+
})}`),
|
|
292
332
|
async upload({ path, data, contentType, filename, kind }) {
|
|
293
333
|
const session = await ensureSession();
|
|
294
334
|
// Stdio shares the agent's filesystem, so a path beats base64 in context.
|
|
@@ -304,7 +344,9 @@ const handlers = {
|
|
|
304
344
|
// agent never restates them; a topic is the same text on every tier.
|
|
305
345
|
guide: (args) => api(`/agent-howto${query(args.topic === undefined
|
|
306
346
|
? { project: resolveProject(args.project) }
|
|
307
|
-
:
|
|
347
|
+
: args.topic === "system"
|
|
348
|
+
? { topic: args.topic, project: resolveProject(args.project) }
|
|
349
|
+
: { topic: args.topic })}`),
|
|
308
350
|
};
|
|
309
351
|
// The script travels as a file path when it can, so it is never JSON-escaped
|
|
310
352
|
// into the model's tool call.
|