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/dist/server/sqlStore.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, newId, normalizeSurfaceIds, projectFromCwd, reservedAgent, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
|
|
1
|
+
import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, isMockKind, newId, normalizeSurfaceIds, projectFromCwd, AREA_ORDER_SETTING, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
|
|
2
|
+
import { renameSlugRefs } from "./embeds.js";
|
|
3
|
+
import { RULES_SETTING } from "./guidance.js";
|
|
4
|
+
// The settings keyed `<prefix>:<project>`, which a project rename carries.
|
|
5
|
+
const PROJECT_SETTINGS = ["design", AREA_ORDER_SETTING, RULES_SETTING];
|
|
6
|
+
// An optional free-text label: blank means none.
|
|
7
|
+
const label = (v) => stripNul(v ?? "").trim() || null;
|
|
2
8
|
function parseJson(raw, fallback) {
|
|
3
9
|
if (typeof raw !== "string" || !raw)
|
|
4
10
|
return fallback;
|
|
@@ -9,12 +15,41 @@ function parseJson(raw, fallback) {
|
|
|
9
15
|
return fallback;
|
|
10
16
|
}
|
|
11
17
|
}
|
|
18
|
+
// A stored reply in the current names, or null when it already uses them.
|
|
19
|
+
function contractReply(payload) {
|
|
20
|
+
if (!("mix" in payload) && !("decision" in payload) && !("resolved" in payload))
|
|
21
|
+
return null;
|
|
22
|
+
const { mix, decision, resolved, ...rest } = payload;
|
|
23
|
+
const out = { ...rest };
|
|
24
|
+
if (mix !== undefined)
|
|
25
|
+
out.borrowed = mix;
|
|
26
|
+
const kind = decision?.kind;
|
|
27
|
+
if (kind === "accept" || kind === "revise") {
|
|
28
|
+
out.verdict = { ...decision, kind: kind === "accept" ? "use" : "revise" };
|
|
29
|
+
}
|
|
30
|
+
if (resolved !== undefined)
|
|
31
|
+
out.closed = resolved;
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
// `surfaces` with every `<mockpit-slot>` tag spelled `<mockpit-embed>`, or null
|
|
35
|
+
// when none has one. Only html is markup; a code or markdown surface may quote
|
|
36
|
+
// the tag as text.
|
|
37
|
+
function slotTagsToEmbed(surfaces) {
|
|
38
|
+
let changed = false;
|
|
39
|
+
const out = surfaces.map((s) => {
|
|
40
|
+
if (s.kind !== "html" || !/<\/?mockpit-slot\b/i.test(s.html))
|
|
41
|
+
return s;
|
|
42
|
+
changed = true;
|
|
43
|
+
return { ...s, html: s.html.replace(/<(\/?)mockpit-slot\b/gi, "<$1mockpit-embed") };
|
|
44
|
+
});
|
|
45
|
+
return changed ? out : null;
|
|
46
|
+
}
|
|
12
47
|
const POST_COLUMNS = `
|
|
13
48
|
id TEXT PRIMARY KEY, sessionId TEXT NOT NULL, mockId TEXT NOT NULL, state TEXT,
|
|
14
49
|
variant TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', title TEXT NOT NULL,
|
|
15
50
|
surfaces TEXT NOT NULL, knobs TEXT, slots TEXT NOT NULL DEFAULT '[]',
|
|
16
51
|
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, version INTEGER NOT NULL,
|
|
17
|
-
curFrom INTEGER, curPrompt TEXT, curAuthor TEXT`;
|
|
52
|
+
curFrom INTEGER, curPrompt TEXT, curAuthor TEXT, note TEXT`;
|
|
18
53
|
// Store implementation on SQLite — a Durable Object's `ctx.storage.sql` in the
|
|
19
54
|
// Worker, or node:sqlite via an adapter on Node (see server/sqliteStorage.ts).
|
|
20
55
|
// One workspace = one database, so plain SQL with no tenant columns.
|
|
@@ -69,9 +104,10 @@ export class SqlStore {
|
|
|
69
104
|
);
|
|
70
105
|
CREATE TABLE IF NOT EXISTS mocks (
|
|
71
106
|
id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
|
|
72
|
-
kind TEXT NOT NULL DEFAULT '
|
|
107
|
+
kind TEXT NOT NULL DEFAULT 'sketch', states TEXT NOT NULL DEFAULT '[]',
|
|
73
108
|
asks TEXT NOT NULL DEFAULT '[]', knobs TEXT NOT NULL DEFAULT '{}', draft TEXT,
|
|
74
|
-
sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL
|
|
109
|
+
sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
|
110
|
+
widths TEXT NOT NULL DEFAULT '[]'
|
|
75
111
|
);
|
|
76
112
|
CREATE TABLE IF NOT EXISTS posts (${POST_COLUMNS});
|
|
77
113
|
CREATE TABLE IF NOT EXISTS post_versions (
|
|
@@ -95,6 +131,11 @@ export class SqlStore {
|
|
|
95
131
|
CREATE TABLE IF NOT EXISTS settings (
|
|
96
132
|
key TEXT PRIMARY KEY, value TEXT NOT NULL
|
|
97
133
|
);
|
|
134
|
+
CREATE TABLE IF NOT EXISTS flows (
|
|
135
|
+
project TEXT NOT NULL, name TEXT NOT NULL, steps TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
|
137
|
+
PRIMARY KEY (project, name)
|
|
138
|
+
);
|
|
98
139
|
`);
|
|
99
140
|
this.addMissing("sessions", {
|
|
100
141
|
agentSeq: "INTEGER NOT NULL DEFAULT 0",
|
|
@@ -102,10 +143,84 @@ export class SqlStore {
|
|
|
102
143
|
key: "TEXT",
|
|
103
144
|
agentActiveAt: "TEXT",
|
|
104
145
|
});
|
|
146
|
+
// The close column's first name.
|
|
147
|
+
const mockCols = this.columns("mocks");
|
|
148
|
+
if (mockCols.has("resolvedAt") && !mockCols.has("closedAt")) {
|
|
149
|
+
this.sql.exec("ALTER TABLE mocks RENAME COLUMN resolvedAt TO closedAt");
|
|
150
|
+
}
|
|
151
|
+
this.addMissing("mocks", {
|
|
152
|
+
widths: "TEXT NOT NULL DEFAULT '[]'",
|
|
153
|
+
closedAt: "TEXT",
|
|
154
|
+
area: "TEXT",
|
|
155
|
+
source: "TEXT",
|
|
156
|
+
situations: "TEXT NOT NULL DEFAULT '{}'",
|
|
157
|
+
});
|
|
105
158
|
if (legacy)
|
|
106
159
|
this.migrateLegacy();
|
|
160
|
+
// After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
|
|
161
|
+
const pinned = this.columns("posts").has("acceptedVersion");
|
|
162
|
+
this.addMissing("posts", {
|
|
163
|
+
note: "TEXT",
|
|
164
|
+
forkedFrom: "TEXT",
|
|
165
|
+
starred: "INTEGER NOT NULL DEFAULT 0",
|
|
166
|
+
acceptedVersion: "INTEGER",
|
|
167
|
+
hiddenVersions: "TEXT",
|
|
168
|
+
});
|
|
169
|
+
// Before the pin, accepted meant "its latest": freeze that as the version in use.
|
|
170
|
+
if (!pinned) {
|
|
171
|
+
this.sql.exec("UPDATE posts SET acceptedVersion = version WHERE status = 'accepted'");
|
|
172
|
+
}
|
|
173
|
+
if (this.settingSync("sketchKind") !== "1")
|
|
174
|
+
this.migrateToSketches();
|
|
175
|
+
if (this.settingSync("contractTerms") !== "1")
|
|
176
|
+
this.migrateToContractTerms();
|
|
107
177
|
this.createIndexes();
|
|
108
178
|
}
|
|
179
|
+
// The agent contract renamed what replies and drafts store (mix → borrowed,
|
|
180
|
+
// decision → verdict with accept → use, resolved → closed) and retired drop
|
|
181
|
+
// and the `<mockpit-slot>` embed alias. Stored data is rewritten once, here,
|
|
182
|
+
// so nothing past the store reads an old name: a dropped variant is archived
|
|
183
|
+
// (both were hidden from the choice and restorable), a drop verdict loses its
|
|
184
|
+
// verdict (the variant's archived status says the rest), and every stored
|
|
185
|
+
// version spells its embeds `<mockpit-embed>` so it still renders them.
|
|
186
|
+
migrateToContractTerms() {
|
|
187
|
+
this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE status = 'dropped'");
|
|
188
|
+
for (const r of this.sql
|
|
189
|
+
.exec("SELECT seq, payload FROM comments WHERE kind = 'reply' AND payload IS NOT NULL")
|
|
190
|
+
.toArray()) {
|
|
191
|
+
const payload = parseJson(r.payload, null);
|
|
192
|
+
const next = payload && contractReply(payload);
|
|
193
|
+
if (next) {
|
|
194
|
+
this.sql.exec("UPDATE comments SET payload = ? WHERE seq = ?", JSON.stringify(next), r.seq);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const r of this.sql
|
|
198
|
+
.exec("SELECT id, draft FROM mocks WHERE draft IS NOT NULL")
|
|
199
|
+
.toArray()) {
|
|
200
|
+
const draft = parseJson(r.draft, null);
|
|
201
|
+
if (!draft || !("mix" in draft))
|
|
202
|
+
continue;
|
|
203
|
+
const { mix, ...rest } = draft;
|
|
204
|
+
this.sql.exec("UPDATE mocks SET draft = ? WHERE id = ?", JSON.stringify({ ...rest, borrowed: mix ?? {} }), r.id);
|
|
205
|
+
}
|
|
206
|
+
for (const r of this.sql
|
|
207
|
+
.exec("SELECT id, surfaces FROM posts WHERE surfaces LIKE '%mockpit-slot%'")
|
|
208
|
+
.toArray()) {
|
|
209
|
+
const surfaces = slotTagsToEmbed(parseJson(r.surfaces, []));
|
|
210
|
+
if (surfaces) {
|
|
211
|
+
this.sql.exec("UPDATE posts SET surfaces = ? WHERE id = ?", JSON.stringify(surfaces), r.id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const r of this.sql
|
|
215
|
+
.exec("SELECT postId, version, surfaces FROM post_versions WHERE surfaces LIKE '%mockpit-slot%'")
|
|
216
|
+
.toArray()) {
|
|
217
|
+
const surfaces = slotTagsToEmbed(parseJson(r.surfaces, []));
|
|
218
|
+
if (surfaces) {
|
|
219
|
+
this.sql.exec("UPDATE post_versions SET surfaces = ? WHERE postId = ? AND version = ?", JSON.stringify(surfaces), r.postId, r.version);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('contractTerms', '1')");
|
|
223
|
+
}
|
|
109
224
|
createIndexes() {
|
|
110
225
|
this.sql.exec(`
|
|
111
226
|
CREATE UNIQUE INDEX IF NOT EXISTS mockpit_mocks_slug_idx ON mocks (project, slug);
|
|
@@ -316,7 +431,7 @@ export class SqlStore {
|
|
|
316
431
|
draft = {
|
|
317
432
|
version: 1,
|
|
318
433
|
answers: {},
|
|
319
|
-
|
|
434
|
+
borrowed: {},
|
|
320
435
|
tuned: {},
|
|
321
436
|
comments: [],
|
|
322
437
|
updatedAt: c.createdAt,
|
|
@@ -332,6 +447,33 @@ export class SqlStore {
|
|
|
332
447
|
}
|
|
333
448
|
this.sql.exec("DELETE FROM comments WHERE draft = 1");
|
|
334
449
|
}
|
|
450
|
+
// A mock no one declared used to be a component. Keep that only where it acts
|
|
451
|
+
// as one, embedded somewhere or a step of a flow; the rest become sketches.
|
|
452
|
+
migrateToSketches() {
|
|
453
|
+
const projectOf = new Map();
|
|
454
|
+
for (const r of this.sql.exec("SELECT id, project FROM mocks").toArray()) {
|
|
455
|
+
projectOf.set(r.id, r.project);
|
|
456
|
+
}
|
|
457
|
+
const used = new Set();
|
|
458
|
+
for (const r of this.sql.exec("SELECT mockId, slots FROM posts").toArray()) {
|
|
459
|
+
const project = projectOf.get(r.mockId);
|
|
460
|
+
for (const e of parseJson(r.slots, []))
|
|
461
|
+
used.add(`${project}\u0000${e.slug}`);
|
|
462
|
+
}
|
|
463
|
+
for (const r of this.sql.exec("SELECT project, steps FROM flows").toArray()) {
|
|
464
|
+
for (const x of parseJson(r.steps, [])) {
|
|
465
|
+
used.add(`${r.project}\u0000${x.mock}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const r of this.sql
|
|
469
|
+
.exec("SELECT id, project, slug FROM mocks WHERE kind = 'component'")
|
|
470
|
+
.toArray()) {
|
|
471
|
+
if (used.has(`${r.project}\u0000${r.slug}`))
|
|
472
|
+
continue;
|
|
473
|
+
this.sql.exec("UPDATE mocks SET kind = 'sketch' WHERE id = ?", r.id);
|
|
474
|
+
}
|
|
475
|
+
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('sketchKind', '1')");
|
|
476
|
+
}
|
|
335
477
|
settingSync(key) {
|
|
336
478
|
const rows = this.sql.exec("SELECT value FROM settings WHERE key = ?", key).toArray();
|
|
337
479
|
return rows.length ? rows[0].value : null;
|
|
@@ -392,18 +534,24 @@ export class SqlStore {
|
|
|
392
534
|
project: r.project,
|
|
393
535
|
slug: r.slug,
|
|
394
536
|
title: r.title,
|
|
395
|
-
kind: (r.kind
|
|
537
|
+
kind: isMockKind(r.kind) ? r.kind : "sketch",
|
|
538
|
+
area: r.area ?? null,
|
|
539
|
+
source: r.source ?? null,
|
|
396
540
|
states: parseJson(r.states, []),
|
|
541
|
+
situations: parseJson(r.situations, {}),
|
|
397
542
|
asks: parseJson(r.asks, []),
|
|
398
543
|
knobs: parseJson(r.knobs, {}),
|
|
544
|
+
widths: parseJson(r.widths, []),
|
|
399
545
|
draft: parseJson(r.draft, null),
|
|
400
546
|
sessionId: r.sessionId ?? null,
|
|
547
|
+
closedAt: r.closedAt ?? null,
|
|
401
548
|
createdAt: r.createdAt,
|
|
402
549
|
updatedAt: r.updatedAt,
|
|
403
550
|
};
|
|
404
551
|
}
|
|
405
552
|
rowToPost(r, history) {
|
|
406
553
|
const knobs = parseJson(r.knobs, null);
|
|
554
|
+
const hidden = parseJson(r.hiddenVersions, []);
|
|
407
555
|
return {
|
|
408
556
|
id: r.id,
|
|
409
557
|
sessionId: r.sessionId,
|
|
@@ -418,7 +566,15 @@ export class SqlStore {
|
|
|
418
566
|
version: r.version,
|
|
419
567
|
history,
|
|
420
568
|
...(knobs ? { knobs } : {}),
|
|
421
|
-
|
|
569
|
+
...(r.note == null ? {} : { note: r.note }),
|
|
570
|
+
// The column keeps its first name, from when embeds were called slots.
|
|
571
|
+
embeds: parseJson(r.slots, []),
|
|
572
|
+
...(r.forkedFrom == null ? {} : { forkedFrom: parseJson(r.forkedFrom, null) }),
|
|
573
|
+
...(r.starred ? { starred: true } : {}),
|
|
574
|
+
...(r.status === "accepted" && r.acceptedVersion != null
|
|
575
|
+
? { acceptedVersion: r.acceptedVersion }
|
|
576
|
+
: {}),
|
|
577
|
+
...(hidden.length ? { hiddenVersions: hidden } : {}),
|
|
422
578
|
...(r.curFrom == null ? {} : { from: r.curFrom }),
|
|
423
579
|
...(r.curPrompt == null ? {} : { prompt: r.curPrompt }),
|
|
424
580
|
...(r.curAuthor == null ? {} : { author: r.curAuthor }),
|
|
@@ -581,16 +737,21 @@ export class SqlStore {
|
|
|
581
737
|
project: stripNul(input.project).trim() || DEFAULT_PROJECT,
|
|
582
738
|
slug: slugify(stripNul(input.slug)),
|
|
583
739
|
title: stripNul(input.title)?.trim() || input.slug,
|
|
584
|
-
kind: input.kind
|
|
740
|
+
kind: isMockKind(input.kind) ? input.kind : "sketch",
|
|
741
|
+
area: label(input.area),
|
|
742
|
+
source: label(input.source),
|
|
585
743
|
states: (input.states ?? []).map((s) => stripNul(s)),
|
|
744
|
+
situations: {},
|
|
586
745
|
asks: [],
|
|
587
746
|
knobs: input.knobs ?? {},
|
|
747
|
+
widths: input.widths ?? [],
|
|
588
748
|
draft: null,
|
|
589
749
|
sessionId: input.sessionId ?? null,
|
|
750
|
+
closedAt: null,
|
|
590
751
|
createdAt: now,
|
|
591
752
|
updatedAt: now,
|
|
592
753
|
};
|
|
593
|
-
this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, states, asks, knobs, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, '[]', ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, JSON.stringify(mock.states), JSON.stringify(mock.knobs), mock.sessionId, mock.createdAt, mock.updatedAt);
|
|
754
|
+
this.sql.exec("INSERT INTO mocks (id, project, slug, title, kind, area, source, states, asks, knobs, widths, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?)", mock.id, mock.project, mock.slug, mock.title, mock.kind, mock.area, mock.source, JSON.stringify(mock.states), JSON.stringify(mock.knobs), JSON.stringify(mock.widths), mock.sessionId, mock.createdAt, mock.updatedAt);
|
|
594
755
|
return mock;
|
|
595
756
|
}
|
|
596
757
|
async updateMock(id, patch) {
|
|
@@ -601,29 +762,172 @@ export class SqlStore {
|
|
|
601
762
|
...mock,
|
|
602
763
|
...(patch.title !== undefined && { title: stripNul(patch.title).trim() || mock.title }),
|
|
603
764
|
...(patch.kind !== undefined && { kind: patch.kind }),
|
|
765
|
+
...(patch.area !== undefined && { area: label(patch.area) }),
|
|
766
|
+
...(patch.source !== undefined && { source: label(patch.source) }),
|
|
604
767
|
...(patch.states !== undefined && { states: patch.states.map((s) => stripNul(s)) }),
|
|
768
|
+
...(patch.situations !== undefined && { situations: patch.situations }),
|
|
605
769
|
...(patch.asks !== undefined && { asks: patch.asks }),
|
|
606
770
|
...(patch.knobs !== undefined && { knobs: patch.knobs }),
|
|
771
|
+
...(patch.widths !== undefined && { widths: patch.widths }),
|
|
607
772
|
...(patch.sessionId !== undefined && { sessionId: patch.sessionId }),
|
|
773
|
+
...(patch.closedAt !== undefined && { closedAt: patch.closedAt }),
|
|
608
774
|
updatedAt: new Date().toISOString(),
|
|
609
775
|
};
|
|
610
|
-
this.sql.exec("UPDATE mocks SET title = ?, kind = ?, states = ?, asks = ?, knobs = ?, sessionId = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), next.sessionId, next.updatedAt, id);
|
|
776
|
+
this.sql.exec("UPDATE mocks SET title = ?, kind = ?, area = ?, source = ?, states = ?, situations = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, closedAt = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, next.area, next.source, JSON.stringify(next.states), JSON.stringify(next.situations), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.closedAt, next.updatedAt, id);
|
|
611
777
|
if (patch.asks !== undefined)
|
|
612
778
|
this.invalidateAssetRefs();
|
|
613
779
|
return next;
|
|
614
780
|
}
|
|
615
781
|
async removeMock(id) {
|
|
616
|
-
|
|
782
|
+
const mock = await this.getMock(id);
|
|
783
|
+
if (!mock)
|
|
617
784
|
return false;
|
|
618
785
|
this.atomic(() => {
|
|
619
786
|
this.sql.exec("DELETE FROM comments WHERE mockId = ?", id);
|
|
620
787
|
this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE mockId = ?)", id);
|
|
621
788
|
this.sql.exec("DELETE FROM posts WHERE mockId = ?", id);
|
|
622
789
|
this.sql.exec("DELETE FROM mocks WHERE id = ?", id);
|
|
790
|
+
for (const flow of this.flowsOf(mock.project)) {
|
|
791
|
+
const steps = flow.steps.filter((x) => x.mock !== mock.slug);
|
|
792
|
+
if (steps.length !== flow.steps.length)
|
|
793
|
+
this.writeFlow(flow, steps);
|
|
794
|
+
}
|
|
623
795
|
});
|
|
624
796
|
this.invalidateAssetRefs();
|
|
625
797
|
return true;
|
|
626
798
|
}
|
|
799
|
+
async renameMock(id, slug) {
|
|
800
|
+
const to = slugify(stripNul(slug));
|
|
801
|
+
return this.atomic(() => {
|
|
802
|
+
const rows = this.sql.exec("SELECT * FROM mocks WHERE id = ?", id).toArray();
|
|
803
|
+
if (rows.length === 0)
|
|
804
|
+
return null;
|
|
805
|
+
const mock = this.rowToMock(rows[0]);
|
|
806
|
+
const from = mock.slug;
|
|
807
|
+
if (to === from)
|
|
808
|
+
return mock;
|
|
809
|
+
const taken = this.sql
|
|
810
|
+
.exec("SELECT id FROM mocks WHERE project = ? AND slug = ?", mock.project, to)
|
|
811
|
+
.toArray();
|
|
812
|
+
if (taken.length > 0)
|
|
813
|
+
return null;
|
|
814
|
+
const updatedAt = new Date().toISOString();
|
|
815
|
+
this.sql.exec("UPDATE mocks SET slug = ?, updatedAt = ? WHERE id = ?", to, updatedAt, id);
|
|
816
|
+
// History is rewritten too: embeds resolve by slug at render time, so an
|
|
817
|
+
// old version left naming the old slug would show its components missing.
|
|
818
|
+
const rewrite = (raw) => {
|
|
819
|
+
let changed = false;
|
|
820
|
+
const next = parseJson(raw, []).map((x) => {
|
|
821
|
+
if (x.kind !== "html")
|
|
822
|
+
return x;
|
|
823
|
+
const html = renameSlugRefs(x.html, from, to);
|
|
824
|
+
if (html === x.html)
|
|
825
|
+
return x;
|
|
826
|
+
changed = true;
|
|
827
|
+
return { ...x, html };
|
|
828
|
+
});
|
|
829
|
+
return changed ? JSON.stringify(next) : null;
|
|
830
|
+
};
|
|
831
|
+
const posts = this.sql
|
|
832
|
+
.exec("SELECT id, surfaces, slots FROM posts WHERE mockId IN (SELECT id FROM mocks WHERE project = ?)", mock.project)
|
|
833
|
+
.toArray();
|
|
834
|
+
for (const r of posts) {
|
|
835
|
+
const postId = r.id;
|
|
836
|
+
const surfaces = rewrite(r.surfaces);
|
|
837
|
+
if (surfaces)
|
|
838
|
+
this.sql.exec("UPDATE posts SET surfaces = ? WHERE id = ?", surfaces, postId);
|
|
839
|
+
const embeds = parseJson(r.slots, []);
|
|
840
|
+
if (embeds.some((e) => e.slug === from)) {
|
|
841
|
+
this.sql.exec("UPDATE posts SET slots = ? WHERE id = ?", JSON.stringify(embeds.map((e) => (e.slug === from ? { ...e, slug: to } : e))), postId);
|
|
842
|
+
}
|
|
843
|
+
for (const v of this.sql
|
|
844
|
+
.exec("SELECT version, surfaces FROM post_versions WHERE postId = ?", postId)
|
|
845
|
+
.toArray()) {
|
|
846
|
+
const old = rewrite(v.surfaces);
|
|
847
|
+
if (!old)
|
|
848
|
+
continue;
|
|
849
|
+
this.sql.exec("UPDATE post_versions SET surfaces = ? WHERE postId = ? AND version = ?", old, postId, v.version);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
for (const flow of this.flowsOf(mock.project)) {
|
|
853
|
+
if (!flow.steps.some((x) => x.mock === from))
|
|
854
|
+
continue;
|
|
855
|
+
this.writeFlow(flow, flow.steps.map((x) => (x.mock === from ? { ...x, mock: to } : x)));
|
|
856
|
+
}
|
|
857
|
+
return { ...mock, slug: to, updatedAt };
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
async renameProject(from, to) {
|
|
861
|
+
const target = stripNul(to).trim();
|
|
862
|
+
return this.atomic(() => {
|
|
863
|
+
const occupied = this.sql
|
|
864
|
+
.exec("SELECT COUNT(*) AS n FROM mocks WHERE project = ?", target)
|
|
865
|
+
.one().n;
|
|
866
|
+
if (occupied > 0)
|
|
867
|
+
return false;
|
|
868
|
+
// A project with no mocks has nothing a leftover flow could walk.
|
|
869
|
+
this.sql.exec("DELETE FROM flows WHERE project = ?", target);
|
|
870
|
+
this.sql.exec("UPDATE mocks SET project = ? WHERE project = ?", target, from);
|
|
871
|
+
this.sql.exec("UPDATE sessions SET project = ? WHERE project = ?", target, from);
|
|
872
|
+
this.sql.exec("UPDATE flows SET project = ? WHERE project = ?", target, from);
|
|
873
|
+
for (const prefix of PROJECT_SETTINGS) {
|
|
874
|
+
const value = this.settingSync(`${prefix}:${from}`);
|
|
875
|
+
if (value === null)
|
|
876
|
+
continue;
|
|
877
|
+
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", `${prefix}:${target}`, value);
|
|
878
|
+
this.sql.exec("DELETE FROM settings WHERE key = ?", `${prefix}:${from}`);
|
|
879
|
+
if (prefix === "design")
|
|
880
|
+
this.invalidateAssetRefs();
|
|
881
|
+
}
|
|
882
|
+
return true;
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
// --- flows ---
|
|
886
|
+
flowsOf(project) {
|
|
887
|
+
return this.sql
|
|
888
|
+
.exec("SELECT * FROM flows WHERE project = ? ORDER BY createdAt, rowid", project)
|
|
889
|
+
.toArray()
|
|
890
|
+
.map((r) => ({
|
|
891
|
+
project: r.project,
|
|
892
|
+
name: r.name,
|
|
893
|
+
steps: parseJson(r.steps, []),
|
|
894
|
+
createdAt: r.createdAt,
|
|
895
|
+
updatedAt: r.updatedAt,
|
|
896
|
+
}));
|
|
897
|
+
}
|
|
898
|
+
// An emptied flow is deleted rather than kept as a name with nothing to walk.
|
|
899
|
+
writeFlow(flow, steps) {
|
|
900
|
+
if (steps.length === 0) {
|
|
901
|
+
this.sql.exec("DELETE FROM flows WHERE project = ? AND name = ?", flow.project, flow.name);
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
const next = { ...flow, steps, updatedAt: new Date().toISOString() };
|
|
905
|
+
this.sql.exec("INSERT INTO flows (project, name, steps, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?) ON CONFLICT(project, name) DO UPDATE SET steps = excluded.steps, updatedAt = excluded.updatedAt", next.project, next.name, JSON.stringify(next.steps), next.createdAt, next.updatedAt);
|
|
906
|
+
return next;
|
|
907
|
+
}
|
|
908
|
+
async listFlows(project) {
|
|
909
|
+
return this.flowsOf(project);
|
|
910
|
+
}
|
|
911
|
+
async placeFlowStep(project, name, screen, step) {
|
|
912
|
+
const clean = { mock: stripNul(screen.mock), state: stripNul(screen.state) };
|
|
913
|
+
const label = stripNul(name);
|
|
914
|
+
return this.atomic(() => {
|
|
915
|
+
const now = new Date().toISOString();
|
|
916
|
+
const flow = this.flowsOf(project).find((f) => f.name === label) ?? {
|
|
917
|
+
project,
|
|
918
|
+
name: label,
|
|
919
|
+
steps: [],
|
|
920
|
+
createdAt: now,
|
|
921
|
+
updatedAt: now,
|
|
922
|
+
};
|
|
923
|
+
const steps = flow.steps.filter((x) => !sameScreen(x, clean));
|
|
924
|
+
if (step > 0)
|
|
925
|
+
steps.splice(Math.min(step, steps.length + 1) - 1, 0, clean);
|
|
926
|
+
if (steps.length === 0 && flow.steps.length === 0)
|
|
927
|
+
return null;
|
|
928
|
+
return this.writeFlow(flow, steps);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
627
931
|
async putDraft(mockId, draft) {
|
|
628
932
|
const mock = await this.getMock(mockId);
|
|
629
933
|
if (!mock)
|
|
@@ -692,12 +996,14 @@ export class SqlStore {
|
|
|
692
996
|
version: 1,
|
|
693
997
|
history: [],
|
|
694
998
|
...(input.knobs ? { knobs: input.knobs } : {}),
|
|
695
|
-
|
|
999
|
+
...(stripNul(input.note ?? "").trim() ? { note: stripNul(input.note ?? "").trim() } : {}),
|
|
1000
|
+
embeds: input.embeds ?? [],
|
|
1001
|
+
...(input.forkedFrom ? { forkedFrom: input.forkedFrom } : {}),
|
|
696
1002
|
...(input.from === undefined ? {} : { from: input.from }),
|
|
697
1003
|
...(input.prompt === undefined ? {} : { prompt: stripNul(input.prompt) }),
|
|
698
1004
|
...(input.author === undefined ? {} : { author: stripNul(input.author) }),
|
|
699
1005
|
};
|
|
700
|
-
this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.
|
|
1006
|
+
this.sql.exec("INSERT INTO posts (id, sessionId, mockId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, note, forkedFrom) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.mock, post.state, post.variant, post.title, JSON.stringify(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.note ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null);
|
|
701
1007
|
this.touch(input.sessionId);
|
|
702
1008
|
this.touchMock(post.mock, now);
|
|
703
1009
|
this.addAssetRefs(post.surfaces);
|
|
@@ -719,13 +1025,17 @@ export class SqlStore {
|
|
|
719
1025
|
...(post.prompt === undefined ? {} : { prompt: post.prompt }),
|
|
720
1026
|
...(post.author === undefined ? {} : { author: post.author }),
|
|
721
1027
|
};
|
|
1028
|
+
// The version in use is kept past the limit: export and embeds resolve to it.
|
|
1029
|
+
const pin = post.acceptedVersion ?? -1;
|
|
722
1030
|
const history = [...post.history, archived];
|
|
723
|
-
|
|
724
|
-
|
|
1031
|
+
const prunable = history.filter((h) => h.version !== pin);
|
|
1032
|
+
if (prunable.length > HISTORY_LIMIT)
|
|
1033
|
+
history.splice(history.indexOf(prunable[0]), 1);
|
|
725
1034
|
const title = patch.title !== undefined ? stripNul(patch.title).trim() || post.title : post.title;
|
|
726
1035
|
const surfaces = patch.surfaces !== undefined ? normalizeSurfaceIds(patch.surfaces) : post.surfaces;
|
|
727
1036
|
const knobs = patch.knobs === undefined ? post.knobs : (patch.knobs ?? undefined);
|
|
728
|
-
const
|
|
1037
|
+
const note = patch.note === undefined ? post.note : stripNul(patch.note).trim() || undefined;
|
|
1038
|
+
const embeds = patch.embeds ?? post.embeds;
|
|
729
1039
|
const state = patch.state === undefined ? post.state : patch.state;
|
|
730
1040
|
const version = post.version + 1;
|
|
731
1041
|
const updatedAt = new Date().toISOString();
|
|
@@ -733,22 +1043,23 @@ export class SqlStore {
|
|
|
733
1043
|
const from = patch.from ?? post.version;
|
|
734
1044
|
const prompt = stripNul(patch.prompt ?? "");
|
|
735
1045
|
const author = patch.author === undefined ? null : stripNul(patch.author);
|
|
736
|
-
this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, JSON.stringify(
|
|
1046
|
+
this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, knobs = ?, note = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), knobs ? JSON.stringify(knobs) : null, note ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, id, post.version);
|
|
737
1047
|
const affected = this.sql.exec("SELECT changes() AS n").one().n;
|
|
738
1048
|
if (affected > 0) {
|
|
739
1049
|
this.insertVersion(id, archived);
|
|
740
|
-
this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ?", id, version - 1 - HISTORY_LIMIT);
|
|
1050
|
+
this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ? AND version != ?", id, version - 1 - HISTORY_LIMIT, pin);
|
|
741
1051
|
this.touch(post.sessionId);
|
|
742
1052
|
this.touchMock(post.mock, updatedAt);
|
|
743
1053
|
if (patch.surfaces !== undefined)
|
|
744
1054
|
this.addAssetRefs(surfaces);
|
|
745
|
-
const { knobs: _knobs, author: _author, ...rest } = post;
|
|
1055
|
+
const { knobs: _knobs, note: _note, author: _author, ...rest } = post;
|
|
746
1056
|
return {
|
|
747
1057
|
...rest,
|
|
748
1058
|
title,
|
|
749
1059
|
surfaces,
|
|
750
1060
|
...(knobs ? { knobs } : {}),
|
|
751
|
-
|
|
1061
|
+
...(note ? { note } : {}),
|
|
1062
|
+
embeds,
|
|
752
1063
|
state,
|
|
753
1064
|
version,
|
|
754
1065
|
updatedAt,
|
|
@@ -772,12 +1083,36 @@ export class SqlStore {
|
|
|
772
1083
|
this.invalidateAssetRefs();
|
|
773
1084
|
return true;
|
|
774
1085
|
}
|
|
775
|
-
async setPostStatus(id, status) {
|
|
1086
|
+
async setPostStatus(id, status, acceptedVersion) {
|
|
1087
|
+
const post = await this.getPost(id);
|
|
1088
|
+
if (!post)
|
|
1089
|
+
return null;
|
|
1090
|
+
const pin = status === "accepted" ? (acceptedVersion ?? post.version) : null;
|
|
1091
|
+
this.sql.exec("UPDATE posts SET status = ?, acceptedVersion = ? WHERE id = ?", status, pin, id);
|
|
1092
|
+
const { acceptedVersion: _, ...rest } = post;
|
|
1093
|
+
return pin === null ? { ...rest, status } : { ...rest, status, acceptedVersion: pin };
|
|
1094
|
+
}
|
|
1095
|
+
async setPostStarred(id, starred) {
|
|
776
1096
|
const post = await this.getPost(id);
|
|
777
1097
|
if (!post)
|
|
778
1098
|
return null;
|
|
779
|
-
this.sql.exec("UPDATE posts SET
|
|
780
|
-
|
|
1099
|
+
this.sql.exec("UPDATE posts SET starred = ? WHERE id = ?", starred ? 1 : 0, id);
|
|
1100
|
+
const { starred: _, ...rest } = post;
|
|
1101
|
+
return starred ? { ...rest, starred: true } : rest;
|
|
1102
|
+
}
|
|
1103
|
+
async setVersionHidden(id, version, hidden) {
|
|
1104
|
+
const post = await this.getPost(id);
|
|
1105
|
+
if (!post)
|
|
1106
|
+
return null;
|
|
1107
|
+
const set = new Set(post.hiddenVersions ?? []);
|
|
1108
|
+
if (hidden)
|
|
1109
|
+
set.add(version);
|
|
1110
|
+
else
|
|
1111
|
+
set.delete(version);
|
|
1112
|
+
const list = [...set].sort((a, b) => a - b);
|
|
1113
|
+
this.sql.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", list.length ? JSON.stringify(list) : null, id);
|
|
1114
|
+
const { hiddenVersions: _, ...rest } = post;
|
|
1115
|
+
return list.length ? { ...rest, hiddenVersions: list } : rest;
|
|
781
1116
|
}
|
|
782
1117
|
// --- comments ---
|
|
783
1118
|
async listComments(query) {
|
|
@@ -864,12 +1199,17 @@ export class SqlStore {
|
|
|
864
1199
|
postVersion: input.payload.version,
|
|
865
1200
|
payload: input.payload,
|
|
866
1201
|
});
|
|
867
|
-
this.sql.exec(
|
|
868
|
-
|
|
869
|
-
|
|
1202
|
+
this.sql.exec(input.keepDraft
|
|
1203
|
+
? "UPDATE mocks SET asks = ? WHERE id = ?"
|
|
1204
|
+
: "UPDATE mocks SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.mockId);
|
|
1205
|
+
if (input.closedAt !== undefined) {
|
|
1206
|
+
this.sql.exec("UPDATE mocks SET closedAt = ?, updatedAt = ? WHERE id = ?", input.closedAt, input.closedAt, input.mockId);
|
|
1207
|
+
}
|
|
1208
|
+
for (const { id, version } of input.accept) {
|
|
1209
|
+
this.sql.exec("UPDATE posts SET status = 'accepted', acceptedVersion = ? WHERE id = ? AND mockId = ?", version, id, input.mockId);
|
|
870
1210
|
}
|
|
871
1211
|
for (const id of input.archive) {
|
|
872
|
-
this.sql.exec("UPDATE posts SET status = 'archived' WHERE id = ? AND mockId = ?", id, input.mockId);
|
|
1212
|
+
this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE id = ? AND mockId = ?", id, input.mockId);
|
|
873
1213
|
}
|
|
874
1214
|
this.invalidateAssetRefs();
|
|
875
1215
|
return comment;
|