mockpit 0.0.0-stage → 1.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.
Files changed (86) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +239 -2
  3. package/bin/demoData.js +132 -0
  4. package/bin/initDesign.js +846 -0
  5. package/bin/mockpit.js +1835 -0
  6. package/bin/serveUrl.d.ts +1 -0
  7. package/bin/serveUrl.js +7 -0
  8. package/dist/mcp/server.d.ts +2 -0
  9. package/dist/mcp/server.js +252 -0
  10. package/dist/server/apiViews.d.ts +329 -0
  11. package/dist/server/apiViews.js +135 -0
  12. package/dist/server/app.d.ts +55 -0
  13. package/dist/server/app.js +3134 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +16 -0
  16. package/dist/server/cdn.d.ts +6 -0
  17. package/dist/server/cdn.js +33 -0
  18. package/dist/server/codeRunner.d.ts +5 -0
  19. package/dist/server/codeRunner.js +89 -0
  20. package/dist/server/codeWorker.d.ts +1 -0
  21. package/dist/server/codeWorker.js +196 -0
  22. package/dist/server/designGuide.d.ts +16 -0
  23. package/dist/server/designGuide.js +311 -0
  24. package/dist/server/eventLog.d.ts +27 -0
  25. package/dist/server/eventLog.js +187 -0
  26. package/dist/server/events.d.ts +53 -0
  27. package/dist/server/events.js +14 -0
  28. package/dist/server/feedbackBatch.d.ts +43 -0
  29. package/dist/server/feedbackBatch.js +98 -0
  30. package/dist/server/icons.d.ts +50 -0
  31. package/dist/server/icons.js +225 -0
  32. package/dist/server/index.d.ts +1 -0
  33. package/dist/server/index.js +76 -0
  34. package/dist/server/kits.d.ts +36 -0
  35. package/dist/server/kits.js +279 -0
  36. package/dist/server/knobs.d.ts +15 -0
  37. package/dist/server/knobs.js +333 -0
  38. package/dist/server/logFile.d.ts +2 -0
  39. package/dist/server/logFile.js +27 -0
  40. package/dist/server/mcpHttp.d.ts +53 -0
  41. package/dist/server/mcpHttp.js +143 -0
  42. package/dist/server/mcpSpec.d.ts +191 -0
  43. package/dist/server/mcpSpec.js +402 -0
  44. package/dist/server/parts.d.ts +31 -0
  45. package/dist/server/parts.js +338 -0
  46. package/dist/server/postMarkdown.d.ts +10 -0
  47. package/dist/server/postMarkdown.js +271 -0
  48. package/dist/server/postSurfaces.d.ts +40 -0
  49. package/dist/server/postSurfaces.js +407 -0
  50. package/dist/server/public.d.ts +5 -0
  51. package/dist/server/public.js +5 -0
  52. package/dist/server/push.d.ts +45 -0
  53. package/dist/server/push.js +234 -0
  54. package/dist/server/richRender.d.ts +15 -0
  55. package/dist/server/richRender.js +392 -0
  56. package/dist/server/run.d.ts +82 -0
  57. package/dist/server/run.js +290 -0
  58. package/dist/server/runApi.d.ts +6 -0
  59. package/dist/server/runApi.js +82 -0
  60. package/dist/server/slots.d.ts +14 -0
  61. package/dist/server/slots.js +71 -0
  62. package/dist/server/sqlStore.d.ts +119 -0
  63. package/dist/server/sqlStore.js +954 -0
  64. package/dist/server/sqliteStorage.d.ts +2 -0
  65. package/dist/server/sqliteStorage.js +90 -0
  66. package/dist/server/surfacePage.d.ts +48 -0
  67. package/dist/server/surfacePage.js +1099 -0
  68. package/dist/server/themes.d.ts +48 -0
  69. package/dist/server/themes.js +305 -0
  70. package/dist/server/types.d.ts +599 -0
  71. package/dist/server/types.js +299 -0
  72. package/dist/workers/public.d.ts +1 -0
  73. package/dist/workers/public.js +3 -0
  74. package/extensions/mockpit.js +717 -0
  75. package/guide/AGENT_SETUP.md +23 -0
  76. package/guide/topics/asks.md +87 -0
  77. package/guide/topics/feedback.md +127 -0
  78. package/guide/topics/html.md +175 -0
  79. package/guide/topics/http.md +76 -0
  80. package/guide/topics/knobs.md +72 -0
  81. package/guide/topics/scripts.md +129 -0
  82. package/guide/topics/surfaces.md +100 -0
  83. package/package.json +165 -4
  84. package/skills/launch-video/SKILL.md +159 -0
  85. package/skills/mockpit/SKILL.md +40 -0
  86. package/viewer/dist/index.html +1922 -0
@@ -0,0 +1,98 @@
1
+ // The agent-facing shape of feedback: one batch per mock, in delivery order.
2
+ // The `feedback` verb, the watch long-poll and the write piggyback all return this,
3
+ // so an agent reads one grouped object instead of re-deriving which mock a flat
4
+ // comment list belongs to. Runtime-agnostic (no node imports).
5
+ import { builtinAsk, OTHER_ID, } from "./types.js";
6
+ const ref = (p) => ({ state: p.state, variant: p.variant });
7
+ function answered(reply, mock, posts) {
8
+ const others = reply.others ?? {};
9
+ const notes = reply.notes ?? {};
10
+ const askIds = new Set([
11
+ ...Object.keys(reply.answers),
12
+ ...Object.keys(others),
13
+ ...Object.keys(notes),
14
+ ]);
15
+ const out = [];
16
+ for (const askId of askIds) {
17
+ const ask = mock?.asks.find((a) => a.id === askId) ?? (mock && builtinAsk(mock, posts, askId));
18
+ const answer = reply.answers[askId];
19
+ const ids = answer === undefined ? [] : Array.isArray(answer) ? answer : [answer];
20
+ const chosen = ids.map((id) => ask?.options.find((o) => o.id === id) ?? { id, label: id });
21
+ if (others[askId] !== undefined)
22
+ chosen.push({ id: OTHER_ID, label: others[askId], other: true });
23
+ out.push({
24
+ ask: askId,
25
+ text: ask?.text ?? "",
26
+ chosen,
27
+ ...(notes[askId] !== undefined ? { note: notes[askId] } : {}),
28
+ });
29
+ }
30
+ return out;
31
+ }
32
+ // Comments arrive in seq order; keep that order both between batches and inside
33
+ // one. A mock gets a fresh batch whenever a second reply arrives for it, so a
34
+ // batch carries at most one reply and nothing is merged away. A comment whose
35
+ // mock is unknown still gets a batch — feedback is never dropped for want of
36
+ // context.
37
+ export function groupFeedback(comments, mocks, posts) {
38
+ const batches = [];
39
+ const open = new Map();
40
+ for (const c of comments) {
41
+ const key = c.mockId ?? "";
42
+ const mock = c.mockId ? mocks.get(c.mockId) : undefined;
43
+ const mockPosts = (c.mockId && posts.get(c.mockId)) || [];
44
+ let batch = open.get(key);
45
+ if (!batch || (c.kind === "reply" && batch.reply)) {
46
+ batch = {
47
+ mockId: c.mockId,
48
+ project: mock?.project ?? null,
49
+ mock: mock?.slug ?? null,
50
+ title: mock?.title ?? null,
51
+ reply: null,
52
+ comments: [],
53
+ accepted: [],
54
+ archived: [],
55
+ };
56
+ open.set(key, batch);
57
+ batches.push(batch);
58
+ }
59
+ if (c.kind === "reply" && c.payload) {
60
+ const { others: _others, notes: _notes, ...payload } = c.payload;
61
+ batch.reply = {
62
+ ...payload,
63
+ seq: c.seq,
64
+ at: c.createdAt,
65
+ asks: answered(c.payload, mock, mockPosts),
66
+ };
67
+ batch.accepted = mockPosts.filter((p) => p.status === "accepted").map(ref);
68
+ batch.archived = mockPosts.filter((p) => p.status === "archived").map(ref);
69
+ continue;
70
+ }
71
+ const post = c.postId ? mockPosts.find((p) => p.id === c.postId) : undefined;
72
+ batch.comments.push({
73
+ seq: c.seq,
74
+ text: c.text,
75
+ state: post?.state ?? null,
76
+ variant: post?.variant ?? null,
77
+ version: c.postVersion,
78
+ ...(c.anchor ? { anchor: c.anchor } : {}),
79
+ ...(c.anchors.length ? { anchors: c.anchors } : {}),
80
+ ...(c.viewport != null ? { viewport: c.viewport } : {}),
81
+ });
82
+ }
83
+ return batches;
84
+ }
85
+ export async function buildFeedbackBatches(store, comments) {
86
+ const mocks = new Map();
87
+ const posts = new Map();
88
+ for (const c of comments) {
89
+ if (!c.mockId || mocks.has(c.mockId))
90
+ continue;
91
+ const mock = await store.getMock(c.mockId);
92
+ if (!mock)
93
+ continue;
94
+ mocks.set(c.mockId, mock);
95
+ posts.set(c.mockId, await store.listPosts({ mockId: c.mockId }));
96
+ }
97
+ return groupFeedback(comments, mocks, posts);
98
+ }
@@ -0,0 +1,50 @@
1
+ export interface IconifyProps {
2
+ width?: number;
3
+ height?: number;
4
+ left?: number;
5
+ top?: number;
6
+ rotate?: number;
7
+ hFlip?: boolean;
8
+ vFlip?: boolean;
9
+ }
10
+ export interface IconifyJSON extends Pick<IconifyProps, "width" | "height" | "left" | "top"> {
11
+ prefix: string;
12
+ icons: Record<string, IconifyProps & {
13
+ body: string;
14
+ hidden?: boolean;
15
+ }>;
16
+ aliases?: Record<string, IconifyProps & {
17
+ parent: string;
18
+ hidden?: boolean;
19
+ }>;
20
+ }
21
+ export interface IconifyIcon {
22
+ body: string;
23
+ left: number;
24
+ top: number;
25
+ width: number;
26
+ height: number;
27
+ }
28
+ export type IconResolver = (prefix: string, name: string) => IconifyIcon | null;
29
+ export declare const BUNDLED_ICON_PREFIXES: readonly ["lucide", "mage"];
30
+ export declare const isIconName: (s: unknown) => s is string;
31
+ /** A parsed set, or null when `raw` is not a usable Iconify JSON set. */
32
+ export declare function parseIconSet(raw: unknown): IconifyJSON | null;
33
+ /** Icons a listing should show: hidden ones exist only so old names keep resolving. */
34
+ export declare const iconCount: (set: IconifyJSON) => number;
35
+ export declare function bundledIconSets(): Promise<Map<string, IconifyJSON>>;
36
+ /** Resolve `name` in `set`, following aliases and applying their transforms. */
37
+ export declare function iconFromSet(set: IconifyJSON, name: string): IconifyIcon | null;
38
+ /** A resolver over in-memory sets; earlier sets win a prefix. */
39
+ export declare function resolverFor(sets: Iterable<IconifyJSON>): IconResolver;
40
+ /**
41
+ * Inline every `icon="prefix:name"` element (and the older
42
+ * `<svg><use href="#mage-name"/></svg>` sprite form) as an `<svg>`. Names the
43
+ * resolver does not know render as an empty 1em box and are listed in
44
+ * `unknown`, in first-seen order.
45
+ */
46
+ export declare const mayHaveIcons: (html: string) => boolean;
47
+ export declare function expandIcons(html: string, resolve: IconResolver): {
48
+ html: string;
49
+ unknown: string[];
50
+ };
@@ -0,0 +1,225 @@
1
+ // Icons by name: an agent writes `<i icon="lucide:check"></i>` and the server
2
+ // inlines the SVG from an Iconify JSON set at render time, so an icon costs the
3
+ // agent its name and the frame needs no fetch. Runtime-agnostic (no node
4
+ // imports): it runs inside renderHtmlPage on the Worker too.
5
+ // Served with the server rather than installed per project: lucide (ISC) is
6
+ // what agents reach for by default, mage (Apache-2.0) is what `mockpit init`
7
+ // used to upload as a sprite, so mocks written against it keep rendering.
8
+ export const BUNDLED_ICON_PREFIXES = ["lucide", "mage"];
9
+ const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
10
+ export const isIconName = (s) => typeof s === "string" && NAME_RE.test(s);
11
+ /** A parsed set, or null when `raw` is not a usable Iconify JSON set. */
12
+ export function parseIconSet(raw) {
13
+ let data = raw;
14
+ if (typeof data === "string") {
15
+ try {
16
+ data = JSON.parse(data);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ if (!data || typeof data !== "object")
23
+ return null;
24
+ const set = data;
25
+ if (!isIconName(set.prefix) || !set.icons || typeof set.icons !== "object")
26
+ return null;
27
+ return set;
28
+ }
29
+ /** Icons a listing should show: hidden ones exist only so old names keep resolving. */
30
+ export const iconCount = (set) => Object.values(set.icons).filter((i) => !i.hidden).length;
31
+ // Bundled sets are imported on first use, not at module load: they are ~600 KB
32
+ // of JSON each, and a workspace that never renders an icon never pays for it.
33
+ let bundled;
34
+ export function bundledIconSets() {
35
+ bundled ??= (async () => {
36
+ const out = new Map();
37
+ const loaders = {
38
+ lucide: () => import("@iconify-json/lucide/icons.json", { with: { type: "json" } }),
39
+ mage: () => import("@iconify-json/mage/icons.json", { with: { type: "json" } }),
40
+ };
41
+ for (const prefix of BUNDLED_ICON_PREFIXES) {
42
+ try {
43
+ const mod = (await loaders[prefix]());
44
+ const set = parseIconSet(mod.default);
45
+ if (set)
46
+ out.set(prefix, set);
47
+ }
48
+ catch (err) {
49
+ // A missing package costs that set's icons, never the render.
50
+ console.warn(`[mockpit] bundled icon set ${prefix} unavailable`, err);
51
+ }
52
+ }
53
+ return out;
54
+ })();
55
+ return bundled;
56
+ }
57
+ const MAX_ALIAS_DEPTH = 8;
58
+ /** Resolve `name` in `set`, following aliases and applying their transforms. */
59
+ export function iconFromSet(set, name) {
60
+ // Aliases stack: the outermost alias's box wins, flips toggle, rotations add.
61
+ let box = {};
62
+ let rotate = 0;
63
+ let hFlip = false;
64
+ let vFlip = false;
65
+ let current = name;
66
+ for (let depth = 0; depth <= MAX_ALIAS_DEPTH; depth++) {
67
+ const icon = Object.hasOwn(set.icons, current) ? set.icons[current] : undefined;
68
+ const alias = !icon && set.aliases && Object.hasOwn(set.aliases, current)
69
+ ? set.aliases[current]
70
+ : undefined;
71
+ const props = icon ?? alias;
72
+ if (!props)
73
+ return null;
74
+ box = { width: props.width, height: props.height, left: props.left, top: props.top, ...box };
75
+ for (const k of ["width", "height", "left", "top"]) {
76
+ if (box[k] === undefined)
77
+ delete box[k];
78
+ }
79
+ rotate += props.rotate ?? 0;
80
+ if (props.hFlip)
81
+ hFlip = !hFlip;
82
+ if (props.vFlip)
83
+ vFlip = !vFlip;
84
+ if (icon) {
85
+ const width = box.width ?? set.width ?? 16;
86
+ const height = box.height ?? set.height ?? 16;
87
+ const left = box.left ?? set.left ?? 0;
88
+ const top = box.top ?? set.top ?? 0;
89
+ return transform({ body: icon.body, left, top, width, height }, rotate % 4, hFlip, vFlip);
90
+ }
91
+ current = alias.parent;
92
+ }
93
+ return null;
94
+ }
95
+ function transform(icon, rotate, hFlip, vFlip) {
96
+ let { body, left, top, width, height } = icon;
97
+ const flips = [
98
+ hFlip ? `translate(${2 * left + width} 0) scale(-1 1)` : "",
99
+ vFlip ? `translate(0 ${2 * top + height}) scale(1 -1)` : "",
100
+ ].filter(Boolean);
101
+ if (flips.length)
102
+ body = `<g transform="${flips.join(" ")}">${body}</g>`;
103
+ if (rotate) {
104
+ const cx = left + width / 2;
105
+ const cy = top + height / 2;
106
+ body = `<g transform="rotate(${rotate * 90} ${cx} ${cy})">${body}</g>`;
107
+ // A quarter turn swaps the box's sides around its center.
108
+ if (rotate % 2 === 1) {
109
+ [width, height] = [height, width];
110
+ left = cx - width / 2;
111
+ top = cy - height / 2;
112
+ }
113
+ }
114
+ return { body, left, top, width, height };
115
+ }
116
+ /** A resolver over in-memory sets; earlier sets win a prefix. */
117
+ export function resolverFor(sets) {
118
+ const byPrefix = new Map();
119
+ for (const set of sets)
120
+ if (!byPrefix.has(set.prefix))
121
+ byPrefix.set(set.prefix, set);
122
+ return (prefix, name) => {
123
+ const set = byPrefix.get(prefix);
124
+ return set ? iconFromSet(set, name) : null;
125
+ };
126
+ }
127
+ // --- expansion -------------------------------------------------------------
128
+ // Script, style and comment bodies are not markup: an icon attribute inside a
129
+ // JS string must stay a string, or the inlined quotes would break the script.
130
+ const OPAQUE_RE = /<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi;
131
+ const ATTRS = `((?:[^>"']|"[^"]*"|'[^']*')*?)`;
132
+ const TAG_RE = new RegExp(`<([a-zA-Z][\\w-]*)(?:\\s${ATTRS})?(\\/?)>`, "g");
133
+ // The pre-expansion form `mockpit init` taught: a sprite reference.
134
+ const SPRITE_RE = new RegExp(`<svg\\b${ATTRS}>\\s*<use\\b${ATTRS}\\/?>\\s*(?:<\\/use>\\s*)?<\\/svg\\s*>`, "gi");
135
+ const ATTR_RE = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
136
+ function parseAttrs(text) {
137
+ const out = [];
138
+ for (const m of text.matchAll(ATTR_RE)) {
139
+ out.push({ name: m[1].toLowerCase(), raw: m[0], value: m[2] ?? m[3] ?? m[4] ?? null });
140
+ }
141
+ return out;
142
+ }
143
+ const fmt = (n) => String(Math.round(n * 1000) / 1000);
144
+ const viewBox = (i) => `${fmt(i.left)} ${fmt(i.top)} ${fmt(i.width)} ${fmt(i.height)}`;
145
+ // The element's own attributes survive (data-part, style, title…); `class`
146
+ // gains `icon` for the 1em sizing, and a decorative icon is hidden from
147
+ // assistive tech unless the author labelled it.
148
+ function svgFor(attrs, icon) {
149
+ const kept = attrs.filter((a) => a.name !== "icon" && a.name !== "class");
150
+ const cls = attrs.find((a) => a.name === "class")?.value?.trim();
151
+ const has = (n) => attrs.some((a) => a.name === n);
152
+ const extra = [
153
+ `class="${!cls ? "icon" : cls.split(/\s+/).includes("icon") ? cls : `icon ${cls}`}"`,
154
+ ...kept.map((a) => a.raw),
155
+ has("viewbox") ? "" : `viewBox="${icon ? viewBox(icon) : "0 0 24 24"}"`,
156
+ has("aria-hidden") || has("role")
157
+ ? ""
158
+ : has("aria-label") || has("aria-labelledby")
159
+ ? 'role="img"'
160
+ : 'aria-hidden="true"',
161
+ ].filter(Boolean);
162
+ return `<svg ${extra.join(" ")}>${icon?.body ?? ""}</svg>`;
163
+ }
164
+ function expandSegment(html, resolve, unknown) {
165
+ const lookup = (prefix, name) => {
166
+ const icon = resolve(prefix, name);
167
+ if (!icon)
168
+ unknown.add(`${prefix}:${name}`);
169
+ return icon;
170
+ };
171
+ const sprites = html.replace(SPRITE_RE, (whole, svgAttrs, useAttrs) => {
172
+ const href = parseAttrs(useAttrs).find((a) => a.name === "href" || a.name === "xlink:href");
173
+ const m = href?.value?.match(/^#mage-(.+)$/);
174
+ if (!m || !isIconName(m[1]))
175
+ return whole;
176
+ const icon = lookup("mage", m[1]);
177
+ return icon ? svgFor(parseAttrs(svgAttrs), icon) : whole;
178
+ });
179
+ let out = "";
180
+ let last = 0;
181
+ for (const m of sprites.matchAll(TAG_RE)) {
182
+ const attrs = m[2] ? parseAttrs(m[2]) : [];
183
+ const spec = attrs.find((a) => a.name === "icon")?.value;
184
+ const colon = spec?.indexOf(":") ?? -1;
185
+ if (!spec || colon < 0)
186
+ continue;
187
+ const prefix = spec.slice(0, colon);
188
+ const name = spec.slice(colon + 1);
189
+ if (!isIconName(prefix) || !isIconName(name))
190
+ continue;
191
+ let end = m.index + m[0].length;
192
+ // Only an empty element is an icon; one with content is left alone so
193
+ // nothing the author wrote is dropped.
194
+ if (!m[3]) {
195
+ const close = new RegExp(`^\\s*<\\/${m[1]}\\s*>`, "i").exec(sprites.slice(end));
196
+ if (!close)
197
+ continue;
198
+ end += close[0].length;
199
+ }
200
+ out += sprites.slice(last, m.index) + svgFor(attrs, lookup(prefix, name));
201
+ last = end;
202
+ }
203
+ return out + sprites.slice(last);
204
+ }
205
+ /**
206
+ * Inline every `icon="prefix:name"` element (and the older
207
+ * `<svg><use href="#mage-name"/></svg>` sprite form) as an `<svg>`. Names the
208
+ * resolver does not know render as an empty 1em box and are listed in
209
+ * `unknown`, in first-seen order.
210
+ */
211
+ // Cheap pre-check so html without icons never loads a set.
212
+ export const mayHaveIcons = (html) => html.includes("icon=") || html.includes("#mage-");
213
+ export function expandIcons(html, resolve) {
214
+ if (!mayHaveIcons(html))
215
+ return { html, unknown: [] };
216
+ const unknown = new Set();
217
+ let out = "";
218
+ let last = 0;
219
+ for (const m of html.matchAll(OPAQUE_RE)) {
220
+ out += expandSegment(html.slice(last, m.index), resolve, unknown) + m[0];
221
+ last = m.index + m[0].length;
222
+ }
223
+ out += expandSegment(html.slice(last), resolve, unknown);
224
+ return { html: out, unknown: [...unknown] };
225
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createApp } from "./app.js";
7
+ import { createNodeExecutor } from "./codeRunner.js";
8
+ import { GUIDE_TOPICS } from "./designGuide.js";
9
+ import { createFileLog } from "./logFile.js";
10
+ import { SqlStore } from "./sqlStore.js";
11
+ import { createSqliteStorage } from "./sqliteStorage.js";
12
+ // Source layout puts this file at server/index.ts; the published package runs
13
+ // the compiled copy at dist/server/index.js. viewer/ and guide/ live at the
14
+ // package root either way.
15
+ let root = join(dirname(fileURLToPath(import.meta.url)), "..");
16
+ if (basename(root) === "dist")
17
+ root = join(root, "..");
18
+ const [viewerHtml, setupText, topicTexts, pkgJson] = await Promise.all([
19
+ readFile(join(root, "viewer", "dist", "index.html"), "utf8").catch(() => {
20
+ console.error("viewer build missing — run `npm run build:viewer` first");
21
+ return process.exit(1);
22
+ }),
23
+ readFile(join(root, "guide", "AGENT_SETUP.md"), "utf8"),
24
+ Promise.all(GUIDE_TOPICS.map((id) => readFile(join(root, "guide", "topics", `${id}.md`), "utf8"))),
25
+ readFile(join(root, "package.json"), "utf8"),
26
+ ]);
27
+ const topics = Object.fromEntries(GUIDE_TOPICS.map((id, i) => [id, topicTexts[i]]));
28
+ const pr = process.env.MOCKPIT_PUBLIC_READ;
29
+ const publicRead = pr === "session" || pr === "full" ? pr : undefined;
30
+ // Storage: SQLite via node:sqlite, the same SqlStore the Cloudflare Durable
31
+ // Object runs, so local mirrors the deploy. MOCKPIT_DB names the file; the
32
+ // default lives in ~/.mockpit/, a user-owned dir that survives reinstalls and
33
+ // is writable however the package was installed.
34
+ const dbPath = process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db");
35
+ const store = new SqlStore(createSqliteStorage(dbPath));
36
+ console.log(`mockpit store: SQLite at ${dbPath}`);
37
+ // The event log is on by default, next to the database, so a session can be
38
+ // diagnosed after the fact without having planned to (`scripts/inspect.ts`).
39
+ // MOCKPIT_LOG names another file; "off" disables it. An in-memory database
40
+ // gets none unless asked: it is a throwaway run.
41
+ const logSetting = process.env.MOCKPIT_LOG;
42
+ const logPath = logSetting === "off"
43
+ ? undefined
44
+ : logSetting || (dbPath === ":memory:" ? undefined : join(dirname(dbPath), "events.jsonl"));
45
+ if (logPath)
46
+ console.log(`mockpit event log: ${logPath}`);
47
+ const app = createApp({
48
+ store,
49
+ viewerHtml,
50
+ setupText,
51
+ topics,
52
+ authToken: process.env.MOCKPIT_TOKEN,
53
+ publicRead,
54
+ // MOCKPIT_VERSION fakes the running version (manual testing of the
55
+ // notice); set it to the empty string to disable the update check
56
+ version: process.env.MOCKPIT_VERSION ?? JSON.parse(pkgJson).version,
57
+ upgradeCommand: "npm install -g mockpit",
58
+ // `run` scripts execute in QuickJS inside a worker thread per run.
59
+ executor: createNodeExecutor(),
60
+ log: logPath ? createFileLog(logPath) : undefined,
61
+ });
62
+ const port = Number(process.env.PORT ?? 8228);
63
+ // MOCKPIT_HOST (or `serve --host`) restricts the listener to one address.
64
+ // Unset keeps the previous behaviour — node's default, every interface — because
65
+ // that is what a container or a LAN-shared instance needs. Set it to 127.0.0.1
66
+ // when the server shares a host with anything you don't want reaching it; that
67
+ // is stronger than the token, which is a single shared secret by design.
68
+ const hostname = process.env.MOCKPIT_HOST || undefined;
69
+ serve({ fetch: app.fetch, port, hostname }, (info) => {
70
+ // Report the address actually bound. Printing "localhost" unconditionally hid
71
+ // the fact that the default listens on every interface.
72
+ const shown = hostname ?? "localhost";
73
+ const authority = shown.includes(":") ? `[${shown}]` : shown;
74
+ console.log(`mockpit listening on http://${authority}:${info.port}` +
75
+ (hostname ? "" : " (all interfaces — set MOCKPIT_HOST to restrict)"));
76
+ });
@@ -0,0 +1,36 @@
1
+ import type { ProjectKit } from "./types.ts";
2
+ export interface Kit {
3
+ id: string;
4
+ label: string;
5
+ summary: string;
6
+ classes: string;
7
+ css: string;
8
+ js?: string;
9
+ href?: string;
10
+ script?: string;
11
+ }
12
+ export declare const CORE_CSS = "\n.stack{display:flex;flex-direction:column;gap:8px}.stack.sm{gap:4px}.stack.lg{gap:16px}\n.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.row.sm{gap:4px}\n.between{display:flex;align-items:center;justify-content:space-between;gap:12px}\n.grow{flex:1;min-width:0}\n.title{font:500 15px/1.35 var(--font-sans);color:var(--color-text-primary)}\n.dim{color:var(--color-text-secondary)}.faint{color:var(--color-text-tertiary)}\n.mono{font-family:var(--font-mono)}.num{font-variant-numeric:tabular-nums}\n.hr{height:1px;border:0;margin:2px 0;background:var(--color-border-secondary)}\n.kbd{font:400 12px/1 var(--font-mono);padding:2px 6px;border:1px solid var(--color-border-secondary);border-bottom-width:2px;border-radius:6px;background:var(--color-background-secondary);color:var(--color-text-secondary)}\n";
13
+ export declare const KITS: Kit[];
14
+ export declare const isKnownKit: (id: unknown) => id is string;
15
+ export declare const KIT_IDS: string[];
16
+ export declare const kitSummaries: (projectKits?: readonly ProjectKit[]) => ({
17
+ href?: string | undefined;
18
+ id: string;
19
+ source: "bundled";
20
+ label: string;
21
+ summary: string;
22
+ classes: string;
23
+ } | {
24
+ source: "project";
25
+ id: string;
26
+ href: string;
27
+ script?: string;
28
+ doc: string;
29
+ })[];
30
+ export declare const RESERVED_KIT_IDS: string[];
31
+ export declare const PROJECT_KIT_DOC_MAX = 1200;
32
+ export declare function checkProjectKit(raw: unknown): ProjectKit | string;
33
+ export declare function kitAssets(ids: readonly string[] | undefined): {
34
+ css: string;
35
+ js: string;
36
+ };