@marver-design/marver 0.14.0 → 0.16.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/CHANGELOG.md +164 -0
- package/README.md +7 -4
- package/dist/boards-6hKVW42a.mjs +57 -0
- package/dist/boards-BdG1TJwU.mjs +267 -0
- package/dist/{build-ByYafIhj.mjs → build-Ct0iMoSi.mjs} +77 -30
- package/dist/cli.mjs +15 -6
- package/dist/{daemon-Bfucyf1o.mjs → daemon-Cb4JjpgL.mjs} +1 -1
- package/dist/{dev-yZMyQeUj.mjs → dev-CdcIuhZJ.mjs} +14 -4
- package/dist/{init-Dvaso7YO.mjs → init-BQIpIpIv.mjs} +6 -3
- package/dist/{manifest-DvOmglFp.mjs → manifest-CpbsqQ_v.mjs} +94 -17
- package/dist/{plugin-BsmG5i2X.mjs → plugin-BXyezwfN.mjs} +201 -74
- package/dist/poster-DOY7pax8.mjs +143 -0
- package/dist/{shot-DkkwuCZ2.mjs → shot-By1AItpD.mjs} +3 -2
- package/dist/{shot-kbR_xzJH.mjs → shot-CwmHO5T4.mjs} +197 -56
- package/docs/live-jam.md +6 -2
- package/docs/publish.md +4 -1
- package/docs/slides.md +9 -3
- package/package.json +1 -1
- package/src/client/content/chart.tsx +62 -24
- package/src/client/content/index.tsx +5 -2
- package/src/client/content/video.tsx +132 -35
- package/src/client/frame-host/bridge.js +6 -1
- package/src/client/shell/App.tsx +33 -247
- package/src/client/shell/BoardList.tsx +408 -0
- package/src/client/shell/ContextMenu.tsx +59 -0
- package/src/client/shell/icons.tsx +7 -0
- package/src/client/shell/store.ts +156 -48
- package/src/client/shell/styles.css +42 -4
- package/src/shared/board-tree.ts +285 -0
- package/templates/AGENTS-embedded.md +35 -7
- package/templates/AGENTS-studio.md +35 -7
- package/templates/instructions/boards.md +120 -7
- package/templates/instructions/craft.md +21 -0
- package/templates/instructions/discover.md +7 -2
- package/templates/instructions/iterate.md +114 -18
- package/templates/instructions/jam.md +18 -2
- package/templates/instructions/review.md +4 -0
- package/templates/instructions/shape.md +16 -2
- package/templates/instructions/slides.md +5 -1
- package/templates/instructions/welcome.md +4 -1
- package/templates/instructions/wireframe.md +3 -0
- package/dist/{comments-DHB_8BRa.mjs → comments-oYcZ3cE-.mjs} +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME } from "./cli.mjs";
|
|
2
|
-
import { l as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { i as listBoardFiles, l as buildTree, n as checkBoardsDir, o as readRegistry, u as flatten } from "./boards-BdG1TJwU.mjs";
|
|
3
|
+
import { c as detectHost, n as scanFrames, o as loadConfig } from "./manifest-CpbsqQ_v.mjs";
|
|
4
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BXyezwfN.mjs";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join, sep } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -44,8 +45,9 @@ function scanAssetRefs(src, moduleId) {
|
|
|
44
45
|
const srcM = /\bsrc\s*=\s*["']([^"']+)["']/.exec(tag);
|
|
45
46
|
const posterM = /\bposter\s*=\s*["']([^"']+)["']/.exec(tag);
|
|
46
47
|
if (srcM) {
|
|
47
|
-
|
|
48
|
+
const remote = /^https:\/\//.test(srcM[1]);
|
|
48
49
|
out.push(srcM[1]);
|
|
50
|
+
if (!remote && !posterM) out.push(`${srcM[1]}.poster.png`);
|
|
49
51
|
}
|
|
50
52
|
if (posterM) out.push(posterM[1]);
|
|
51
53
|
}
|
|
@@ -147,22 +149,67 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
|
|
|
147
149
|
reveal
|
|
148
150
|
};
|
|
149
151
|
}
|
|
150
|
-
/** Read every board file; returns name -> parsed json. Bad JSON fails the build loudly
|
|
152
|
+
/** Read every board file; returns name -> parsed json. Bad JSON fails the build loudly, and so
|
|
153
|
+
* does a board-named entry that is not a regular file (a symlink could publish JSON from
|
|
154
|
+
* outside the project - the build fails closed rather than skipping it quietly). */
|
|
151
155
|
function readBoards(root) {
|
|
152
156
|
const dir = join(root, "design", "boards");
|
|
157
|
+
const de = checkBoardsDir(root, dir);
|
|
158
|
+
if (de) throw new Error(de);
|
|
159
|
+
const { boards, skipped } = listBoardFiles(dir);
|
|
160
|
+
if (skipped.length) throw new Error(`design/boards/${skipped[0]} is not a regular file (a symlinked board cannot be published)`);
|
|
153
161
|
const out = {};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const name = f.replace(/\.json$/, "");
|
|
158
|
-
try {
|
|
159
|
-
out[name] = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
160
|
-
} catch {
|
|
161
|
-
throw new Error(`design/boards/${f} is not valid JSON`);
|
|
162
|
-
}
|
|
162
|
+
for (const b of boards) {
|
|
163
|
+
if (b.json === null) throw new Error(`design/boards/${b.name}.json is not valid JSON`);
|
|
164
|
+
out[b.name] = b.json;
|
|
163
165
|
}
|
|
164
166
|
return out;
|
|
165
167
|
}
|
|
168
|
+
/** Switcher order = the sidebar's reading order: the folder tree over the PUBLISHED boards only
|
|
169
|
+
* (a folder left with no published board drops out - its name never reaches the bundle),
|
|
170
|
+
* flattened depth-first; all-scenes always LAST (it is the expensive everything-board, never
|
|
171
|
+
* the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
|
|
172
|
+
* like board names: they ship. */
|
|
173
|
+
function publishedTree(published, allBoards, folders) {
|
|
174
|
+
const field = (n, k) => allBoards[n]?.[k];
|
|
175
|
+
const tree = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
|
|
176
|
+
name: n,
|
|
177
|
+
order: field(n, "order"),
|
|
178
|
+
folder: field(n, "folder")
|
|
179
|
+
})), folders).filter((it) => it.kind === "board" || it.boards.length > 0);
|
|
180
|
+
return {
|
|
181
|
+
tree,
|
|
182
|
+
names: [...flatten(tree), ...published.includes("all-scenes") ? ["all-scenes"] : []]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** The bundle's manifest: the published frames, and descriptions of published things ONLY -
|
|
186
|
+
* the project's, the published scenes' (their brief path only when source is revealed),
|
|
187
|
+
* the published boards' and the folders those sit in. No unpublished name or sentence. */
|
|
188
|
+
function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
189
|
+
const pubScenes = new Set(pubFrames.map((f) => f.scene));
|
|
190
|
+
const pubBoardSet = new Set(publishedNames);
|
|
191
|
+
const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name));
|
|
192
|
+
const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
|
|
193
|
+
const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
|
|
194
|
+
return {
|
|
195
|
+
...manifest.project ? { project: manifest.project } : {},
|
|
196
|
+
...pubFolders.length ? { folders: pubFolders } : {},
|
|
197
|
+
...pubBoards.length ? { boards: pubBoards } : {},
|
|
198
|
+
scenes: manifest.scenes.filter((s) => pubScenes.has(s.name)).map(({ name, description, brief }) => ({
|
|
199
|
+
name,
|
|
200
|
+
frames: pubFrames.filter((f) => f.scene === name).length,
|
|
201
|
+
...description ? { description } : {},
|
|
202
|
+
...brief && !strip ? { brief } : {}
|
|
203
|
+
})),
|
|
204
|
+
frames: pubFrames
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** The folder registry: absent = no folders (boards still imply theirs); malformed fails the build. */
|
|
208
|
+
function readFolders(root) {
|
|
209
|
+
const reg = readRegistry(join(root, "design", "boards"));
|
|
210
|
+
if (reg.state === "malformed") throw new Error(reg.error);
|
|
211
|
+
return reg.folders;
|
|
212
|
+
}
|
|
166
213
|
/**
|
|
167
214
|
* The generated registry with OPAQUE keys - the source strip's half of the
|
|
168
215
|
* bundle (01-sharing §6.2). Object keys are string literals that survive
|
|
@@ -266,18 +313,17 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
266
313
|
const pkgDir = packageDir();
|
|
267
314
|
const clientDir = join(pkgDir, "src", "client");
|
|
268
315
|
const outDir = join(root, "design", ".dist");
|
|
269
|
-
const manifest = scanFrames(root
|
|
316
|
+
const manifest = scanFrames(root, {
|
|
317
|
+
name: config.share.name || basename(root),
|
|
318
|
+
description: config.description
|
|
319
|
+
});
|
|
270
320
|
const allBoards = readBoards(root);
|
|
271
321
|
const policy = resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag);
|
|
272
322
|
const rights = Object.fromEntries(Object.entries(policy.boards).map(([n, p]) => [n, p.max]));
|
|
273
323
|
const strip = !policy.reveal.source;
|
|
274
324
|
const buildSalt = randomBytes(16).toString("hex");
|
|
275
325
|
const opaquePath = (id, ext) => `__mv/f/mv${createHash("sha256").update(buildSalt).update(id).digest("hex").slice(0, 12)}${ext}`;
|
|
276
|
-
const
|
|
277
|
-
const o = allBoards[n]?.order;
|
|
278
|
-
return typeof o === "number" && Number.isFinite(o) ? o : Infinity;
|
|
279
|
-
};
|
|
280
|
-
const publishedNames = Object.keys(rights).sort((a, b) => a === "all-scenes" ? 1 : b === "all-scenes" ? -1 : boardOrder(a) - boardOrder(b) || a.localeCompare(b));
|
|
326
|
+
const { tree, names: publishedNames } = publishedTree(Object.keys(rights), allBoards, readFolders(root));
|
|
281
327
|
const includeAll = publishedNames.includes("all-scenes");
|
|
282
328
|
const boards = {};
|
|
283
329
|
for (const n of publishedNames) if (allBoards[n]) boards[n] = allBoards[n];
|
|
@@ -287,17 +333,10 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
287
333
|
for (const b of Object.values(boards)) for (const node of Array.isArray(b?.nodes) ? b.nodes : []) if (typeof node?.frame === "string") wanted.add(node.frame);
|
|
288
334
|
frames = manifest.frames.filter((f) => wanted.has(f.id));
|
|
289
335
|
}
|
|
290
|
-
const
|
|
336
|
+
const pubManifest = publishedManifest(manifest, strip ? frames.map((f) => ({
|
|
291
337
|
...f,
|
|
292
338
|
file: opaquePath(f.id, f.kind === "html" ? ".html" : "")
|
|
293
|
-
})) : frames;
|
|
294
|
-
const pubManifest = {
|
|
295
|
-
frames: pubFrames,
|
|
296
|
-
scenes: [...new Set(pubFrames.map((f) => f.scene))].sort().map((name) => ({
|
|
297
|
-
name,
|
|
298
|
-
frames: pubFrames.filter((f) => f.scene === name).length
|
|
299
|
-
}))
|
|
300
|
-
};
|
|
339
|
+
})) : frames, publishedNames, strip);
|
|
301
340
|
for (const n of publishedNames) {
|
|
302
341
|
const pb = policy.boards[n];
|
|
303
342
|
if (pb?.type !== "slides" && pb?.open !== "slides") continue;
|
|
@@ -329,6 +368,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
329
368
|
manifest: pubManifest,
|
|
330
369
|
boards,
|
|
331
370
|
names: publishedNames,
|
|
371
|
+
tree,
|
|
332
372
|
default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
|
|
333
373
|
rights,
|
|
334
374
|
policy: {
|
|
@@ -458,6 +498,13 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
458
498
|
}
|
|
459
499
|
let copiedAssets = 0;
|
|
460
500
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
501
|
+
for (const r of refs) {
|
|
502
|
+
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
503
|
+
const { ensurePoster } = await import("./poster-DOY7pax8.mjs");
|
|
504
|
+
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
505
|
+
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
506
|
+
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
507
|
+
}
|
|
461
508
|
for (const r of refs) {
|
|
462
509
|
if (!isLocalAssetRef(r)) continue;
|
|
463
510
|
const srcFile = join(assetsDir, r);
|
|
@@ -526,4 +573,4 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
526
573
|
console.log(`\n serve it: npx ${NAME} serve (set MARVER_PASSWORD to gate it)\n`);
|
|
527
574
|
}
|
|
528
575
|
//#endregion
|
|
529
|
-
export { buildSite };
|
|
576
|
+
export { buildSite, isLocalAssetRef, scanAssetRefs };
|
package/dist/cli.mjs
CHANGED
|
@@ -53,14 +53,14 @@ function version() {
|
|
|
53
53
|
}
|
|
54
54
|
const cli = cac(NAME);
|
|
55
55
|
cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
56
|
-
const { init } = await import("./init-
|
|
56
|
+
const { init } = await import("./init-BQIpIpIv.mjs");
|
|
57
57
|
init(resolve(opts.root), {
|
|
58
58
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
59
59
|
demo: opts.demo !== false
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
63
|
-
const { dev } = await import("./dev-
|
|
63
|
+
const { dev } = await import("./dev-CdcIuhZJ.mjs");
|
|
64
64
|
let port;
|
|
65
65
|
if (opts.port !== void 0) {
|
|
66
66
|
const n = Number(opts.port);
|
|
@@ -70,7 +70,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
70
70
|
await dev(resolve(opts.root), port);
|
|
71
71
|
});
|
|
72
72
|
cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--embed-seeds", "Copy comment history INTO the web root (identifying - every event carries its author's email)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
73
|
-
const { buildSite } = await import("./build-
|
|
73
|
+
const { buildSite } = await import("./build-Ct0iMoSi.mjs");
|
|
74
74
|
try {
|
|
75
75
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
76
76
|
await buildSite(resolve(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
|
|
@@ -89,7 +89,7 @@ cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").opti
|
|
|
89
89
|
serve(resolve(opts.root), port);
|
|
90
90
|
});
|
|
91
91
|
cli.command("comments <action> [value]", "Comment collaboration: connect <url> · sync · list · reply <thread> · resolve <thread> · invite <email> · revoke <email>").option("--root <dir>", "Host repo root", { default: "." }).option("--token <token>", "connect: the canvas's MARVER_CLI_TOKEN (default $MARVER_CLI_TOKEN) - the identity-mode path").option("--invite <token>", "connect: claim this invite instead of signing in").option("--canvas-password <password>", "connect: the canvas gate password (default $MARVER_PASSWORD or prompt)").option("--email <email>", "connect: account email (skips the prompt)").option("--password <password>", "connect: account password (skips the prompt - mind your shell history)").option("--name <name>", "connect --invite: display name for the new account").option("--open", "list: only unresolved threads").option("--json", "list: machine-readable output").option("--board <board>", "scope to one board").option("--body <text>", "reply: the reply text").option("--addressed-in <frame>", "resolve: the variant frame that answered the feedback").action(async (action, value, opts) => {
|
|
92
|
-
const { commentsCommand } = await import("./comments-
|
|
92
|
+
const { commentsCommand } = await import("./comments-oYcZ3cE-.mjs");
|
|
93
93
|
try {
|
|
94
94
|
await commentsCommand(resolve(opts.root), action, value, opts);
|
|
95
95
|
} catch (err) {
|
|
@@ -115,8 +115,17 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
|
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
|
-
cli.command("
|
|
119
|
-
const {
|
|
118
|
+
cli.command("boards", "The sidebar as files: every folder and board in reading order, the landing board, the registry").option("--root <dir>", "Host repo root", { default: "." }).option("--json", "The tree as JSON ({ tree, landing, registry })").action(async (opts) => {
|
|
119
|
+
const { boardsCommand } = await import("./boards-6hKVW42a.mjs");
|
|
120
|
+
try {
|
|
121
|
+
boardsCommand(resolve(opts.root), opts);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`[${NAME}] ${err.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
cli.command("shot <frame>", "Render one frame headless and print the PNG path (needs `dev` running)").option("--root <dir>", "Host repo root", { default: "." }).option("--theme <name>", "Theme to render (default: light)").option("--scale <n>", "Device pixels per CSS px, 1-4 (default: 2; 4 for print-quality stills)").action(async (frame, opts) => {
|
|
128
|
+
const { shotCommand } = await import("./shot-By1AItpD.mjs");
|
|
120
129
|
try {
|
|
121
130
|
await shotCommand(resolve(opts.root), frame, opts);
|
|
122
131
|
} catch (err) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as replay } from "./events-B3LBn74P.mjs";
|
|
2
2
|
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
3
3
|
import { n as localProfile } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { r as toFrameId } from "./manifest-CpbsqQ_v.mjs";
|
|
5
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
6
|
import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
|
|
7
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { c as detectHost, o as loadConfig } from "./manifest-CpbsqQ_v.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BXyezwfN.mjs";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,6 +18,14 @@ const portFree = (p) => new Promise((res) => {
|
|
|
18
18
|
s.once("listening", () => s.close(() => res(true)));
|
|
19
19
|
s.listen(p, "127.0.0.1");
|
|
20
20
|
});
|
|
21
|
+
/** realpath, or the path itself when it cannot be resolved (never throws at boot). */
|
|
22
|
+
const safeReal = (p) => {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(p);
|
|
25
|
+
} catch {
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
21
29
|
/** C1: a deterministic per-project port in [5200,5399] derived from the root path. Two projects
|
|
22
30
|
* never collide, and the same project always lands on the same port across restarts. */
|
|
23
31
|
const projectPort = (root) => {
|
|
@@ -189,7 +197,9 @@ async function dev(root, portFlag) {
|
|
|
189
197
|
fs: {
|
|
190
198
|
allow: [.../* @__PURE__ */ new Set([
|
|
191
199
|
root,
|
|
200
|
+
safeReal(root),
|
|
192
201
|
pkgDir,
|
|
202
|
+
safeReal(pkgDir),
|
|
193
203
|
searchForWorkspaceRoot(root)
|
|
194
204
|
])],
|
|
195
205
|
deny: FS_DENY
|
|
@@ -247,7 +257,7 @@ async function dev(root, portFlag) {
|
|
|
247
257
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
248
258
|
try {
|
|
249
259
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
250
|
-
const { installedVersion } = await import("./plugin-
|
|
260
|
+
const { installedVersion } = await import("./plugin-BXyezwfN.mjs").then((n) => n.i);
|
|
251
261
|
const stale = staleManagedInstructions(root);
|
|
252
262
|
if (stale.length) {
|
|
253
263
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -285,7 +295,7 @@ async function dev(root, portFlag) {
|
|
|
285
295
|
});
|
|
286
296
|
}
|
|
287
297
|
if (config.jam) {
|
|
288
|
-
const { startJam } = await import("./daemon-
|
|
298
|
+
const { startJam } = await import("./daemon-Cb4JjpgL.mjs");
|
|
289
299
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
290
300
|
if (jam) {
|
|
291
301
|
const close = server.close.bind(server);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as DEFAULTS, c as detectHost, i as writeManifest, l as readJson, n as scanFrames, s as detectAgent } from "./manifest-CpbsqQ_v.mjs";
|
|
3
3
|
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
6
6
|
//#region src/cli/init.ts
|
|
7
7
|
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
8
8
|
function init(root, opts) {
|
|
@@ -221,7 +221,7 @@ function init(root, opts) {
|
|
|
221
221
|
created.push("design/scenes/demo/ (3 frames)");
|
|
222
222
|
}
|
|
223
223
|
if (host.tsconfigSweepsDesign) patchTsconfigExclude(root);
|
|
224
|
-
writeManifest(root, scanFrames(root));
|
|
224
|
+
writeManifest(root, scanFrames(root, { name: basename(root) }));
|
|
225
225
|
console.log(`\n${NAME} initialized (${opts.mode} mode). Created:`);
|
|
226
226
|
for (const f of created) console.log(` + ${f}`);
|
|
227
227
|
if (host.router === "next") console.log(`\n note: Next.js support is partial - frames render outside Next, so next/font, next/image and Server Components do not exist inside them (details in design/AGENTS.md).`);
|
|
@@ -540,6 +540,9 @@ const configTemplate = (mode, jamAgent) => `// ${NAME} config - OPTIONAL. Delete
|
|
|
540
540
|
// relative imports need extensions, tsconfig paths are ignored here.
|
|
541
541
|
export default {
|
|
542
542
|
mode: ${JSON.stringify(mode)},
|
|
543
|
+
// One sentence on what this product is and for whom. It lands in design/manifest.json
|
|
544
|
+
// as the project's description - the first thing a new agent session reads.
|
|
545
|
+
// description: "Analytics for indie SaaS - the founder's Monday-morning read",
|
|
543
546
|
// Device widths for frames and the Devices view. Rename, retune, or uncomment tv.
|
|
544
547
|
viewports: {
|
|
545
548
|
mobile: { width: 390, height: 844 },
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
+
import { d as isBoardName, f as readDescription, i as listBoardFiles, l as buildTree, m as hash, n as checkBoardsDir, o as readRegistry, t as boardFields, u as flatten } from "./boards-BdG1TJwU.mjs";
|
|
2
3
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
6
|
//#region src/server/detect.ts
|
|
7
7
|
function readJson(file) {
|
|
8
8
|
try {
|
|
@@ -261,6 +261,7 @@ async function loadConfig(root) {
|
|
|
261
261
|
themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
|
|
262
262
|
port: validPort(user.port) ?? DEFAULTS.port,
|
|
263
263
|
zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
|
|
264
|
+
description: readDescription(user.description),
|
|
264
265
|
share: {
|
|
265
266
|
branding: user.share?.branding !== false,
|
|
266
267
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
@@ -379,8 +380,8 @@ function extractMeta(src) {
|
|
|
379
380
|
if (!m) return {};
|
|
380
381
|
const body = m[1];
|
|
381
382
|
const pick = (key) => {
|
|
382
|
-
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['
|
|
383
|
-
return r ? r[2] : void 0;
|
|
383
|
+
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(?:'([^'\\n]*)'|"([^"\\n]*)"|\`([^\`\\n]*)\`)\\s*(?=[,}\\n]|$)`).exec(body);
|
|
384
|
+
return r ? r[1] ?? r[2] ?? r[3] : void 0;
|
|
384
385
|
};
|
|
385
386
|
const pickBool = (key) => {
|
|
386
387
|
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(true|false)\\b`).exec(body);
|
|
@@ -401,22 +402,35 @@ function extractMeta(src) {
|
|
|
401
402
|
if (intent) out.intent = intent;
|
|
402
403
|
const slide = pickBool("slide");
|
|
403
404
|
if (slide !== void 0) out.slide = slide;
|
|
405
|
+
const description = readDescription(pick("description"));
|
|
406
|
+
if (description) out.description = description;
|
|
404
407
|
return out;
|
|
405
408
|
}
|
|
406
|
-
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
409
|
+
/** Content-frame detection: LEXICAL by convention - the frame file itself imports
|
|
410
|
+
* PKG/content (an import specifier scan, so "<Diagram" inside a string in a UI frame can
|
|
411
|
+
* never misbadge it; barrels/re-exports are not detected - meta.intent is the taught path
|
|
412
|
+
* and always works) AND renders a DOCUMENT primitive: Doc, Md, Diagram or Img (reference
|
|
413
|
+
* imagery - a moodboard is Rows of Img). The import alone is not enough - Chart, Video, Row,
|
|
414
|
+
* Col and Space are shared blocks a UI screen or a slide uses too, and a dashboard with one
|
|
415
|
+
* <Chart> is still a screen (device height, no document measuring), never a spec. Returns
|
|
416
|
+
* the inferred intent + natural width, or null for UI frames. */
|
|
411
417
|
const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
|
|
412
418
|
const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
|
|
413
419
|
const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
|
|
420
|
+
/** The source with comments, template literals and CODE-shaped strings blanked (a JSX
|
|
421
|
+
* expression string `{'<Diagram>'}`, an attribute value), so a tag name quoted in an example
|
|
422
|
+
* or a comment never counts as a rendered element. Prose is left alone: JSX text has
|
|
423
|
+
* apostrophes ("it's"), and a blanket quote match would swallow real tags between two of them. */
|
|
424
|
+
const codeOnly = (src) => src.replace(/`(?:[^`\\]|\\[\s\S])*`/g, "``").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\{\s*(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')\s*\}/g, "{\"\"}").replace(/([=(,:[{?+|&!]|\breturn)\s*(?:"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')/g, "$1\"\"").replace(/([=(,:[{?+|&!]|\breturn)\s*\/(?:[^/\\\n[]|\\.|\[(?:[^\]\\\n]|\\.)*\])+\/[gimsuy]*/g, "$1/re/");
|
|
414
425
|
function contentScan(src) {
|
|
415
426
|
if (!CONTENT_IMPORT.test(src)) return null;
|
|
416
|
-
const
|
|
427
|
+
const code = codeOnly(src);
|
|
428
|
+
const count = (re) => (code.match(re) ?? []).length;
|
|
429
|
+
const docs = count(/<Doc[\s>/]/g);
|
|
417
430
|
const diagrams = count(/<Diagram[\s>/]/g);
|
|
418
431
|
const imgs = count(/<Img[\s>/]/g);
|
|
419
432
|
const mds = count(/<Md[\s>/]/g);
|
|
433
|
+
if (docs + diagrams + mds + imgs === 0) return null;
|
|
420
434
|
return {
|
|
421
435
|
intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
|
|
422
436
|
width: contentWidthOf(src)
|
|
@@ -462,7 +476,63 @@ function walk(dir, out = []) {
|
|
|
462
476
|
}
|
|
463
477
|
return out;
|
|
464
478
|
}
|
|
465
|
-
|
|
479
|
+
/** A scene's description is the first non-blank line of its `_brief.md` (a leading `#`
|
|
480
|
+
* stripped, a YAML front-matter block skipped) - no new file. */
|
|
481
|
+
function sceneBrief(root, scene) {
|
|
482
|
+
if (!scene) return {};
|
|
483
|
+
const rel = `design/scenes/${scene}/_brief.md`;
|
|
484
|
+
const abs = join(root, ...rel.split("/"));
|
|
485
|
+
if (!existsSync(abs)) return {};
|
|
486
|
+
let description;
|
|
487
|
+
try {
|
|
488
|
+
const lines = readFileSync(abs, "utf8").split(/\r?\n/).map((l) => l.trim());
|
|
489
|
+
let i = 0;
|
|
490
|
+
if (lines[0] === "---") {
|
|
491
|
+
i = lines.indexOf("---", 1) + 1;
|
|
492
|
+
if (i === 0) i = lines.length;
|
|
493
|
+
}
|
|
494
|
+
const line = lines.slice(i).find((l) => l);
|
|
495
|
+
description = readDescription(line?.replace(/^#+\s*/, ""));
|
|
496
|
+
} catch {}
|
|
497
|
+
return {
|
|
498
|
+
brief: rel,
|
|
499
|
+
...description ? { description } : {}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** The sidebar as files say it is, for the manifest: folders in root order, boards in
|
|
503
|
+
* reading order with their folder and description. A boards dir we may not read (symlink)
|
|
504
|
+
* or a malformed registry yields nothing here - the API and the build say why; the
|
|
505
|
+
* orientation file must never fail to write. */
|
|
506
|
+
function scanBoards(root) {
|
|
507
|
+
const dir = join(root, "design", "boards");
|
|
508
|
+
if (checkBoardsDir(root, dir)) return {};
|
|
509
|
+
const reg = readRegistry(dir);
|
|
510
|
+
if (reg.state === "malformed") return {};
|
|
511
|
+
const rows = listBoardFiles(dir).boards.map((b) => ({
|
|
512
|
+
name: b.name,
|
|
513
|
+
...boardFields(b.json, isBoardName)
|
|
514
|
+
}));
|
|
515
|
+
const tree = buildTree(rows, reg.folders);
|
|
516
|
+
const folderOf = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const it of tree) if (it.kind === "folder") for (const b of it.boards) folderOf.set(b, it.name);
|
|
518
|
+
const folders = tree.filter((it) => it.kind === "folder").map((it) => ({
|
|
519
|
+
name: it.name,
|
|
520
|
+
...it.description ? { description: it.description } : {}
|
|
521
|
+
}));
|
|
522
|
+
const boards = flatten(tree).map((name) => {
|
|
523
|
+
const d = rows.find((r) => r.name === name)?.description;
|
|
524
|
+
return {
|
|
525
|
+
name,
|
|
526
|
+
...folderOf.has(name) ? { folder: folderOf.get(name) } : {},
|
|
527
|
+
...d ? { description: d } : {}
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
...folders.length ? { folders } : {},
|
|
532
|
+
...boards.length ? { boards } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function scanFrames(root, project) {
|
|
466
536
|
const design = join(root, "design");
|
|
467
537
|
const frames = [];
|
|
468
538
|
for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
|
|
@@ -491,6 +561,7 @@ function scanFrames(root) {
|
|
|
491
561
|
if (meta.of) entry.variantGroup = meta.of;
|
|
492
562
|
if (meta.variant) entry.variant = meta.variant;
|
|
493
563
|
if (meta.slide) entry.slide = true;
|
|
564
|
+
if (meta.description) entry.description = meta.description;
|
|
494
565
|
const content = contentScan(src);
|
|
495
566
|
if (content || meta.intent) {
|
|
496
567
|
entry.intent = meta.intent ?? content.intent;
|
|
@@ -516,12 +587,19 @@ function scanFrames(root) {
|
|
|
516
587
|
inferVariantGroups(frames);
|
|
517
588
|
const sceneCounts = /* @__PURE__ */ new Map();
|
|
518
589
|
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
590
|
+
const scenes = [...sceneCounts.entries()].map(([name, n]) => ({
|
|
591
|
+
name,
|
|
592
|
+
frames: n,
|
|
593
|
+
...sceneBrief(root, name)
|
|
594
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
519
595
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
596
|
+
...project && (project.name || project.description) ? { project: {
|
|
597
|
+
...project.name ? { name: project.name } : {},
|
|
598
|
+
...project.description ? { description: project.description } : {}
|
|
599
|
+
} } : {},
|
|
600
|
+
...scanBoards(root),
|
|
601
|
+
scenes,
|
|
602
|
+
frames
|
|
525
603
|
};
|
|
526
604
|
}
|
|
527
605
|
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
@@ -603,6 +681,5 @@ function writeManifest(root, manifest) {
|
|
|
603
681
|
writeFileSync(file, next);
|
|
604
682
|
return true;
|
|
605
683
|
}
|
|
606
|
-
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
607
684
|
//#endregion
|
|
608
|
-
export {
|
|
685
|
+
export { DEFAULTS as a, detectHost as c, writeManifest as i, readJson as l, scanFrames as n, loadConfig as o, toFrameId as r, detectAgent as s, affectedFrameIds as t };
|