@marver-design/marver 0.15.0 → 0.16.1
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 +101 -0
- package/README.md +2 -1
- package/dist/boards-BmxcT3Lc.mjs +290 -0
- package/dist/boards-PuVzw5Wp.mjs +62 -0
- package/dist/{build-DfuTQZlY.mjs → build-B4yPgFNF.mjs} +72 -29
- package/dist/cli.mjs +33 -13
- package/dist/{daemon-DalgvoA9.mjs → daemon-Bbh_jmui.mjs} +1 -1
- package/dist/{dev-BxCmeU_H.mjs → dev-DH2W7Ffw.mjs} +4 -4
- package/dist/{init-QKNi9gvF.mjs → init-B7YhcN2o.mjs} +6 -3
- package/dist/{manifest-BzxSMoDB.mjs → manifest-CaslQIAO.mjs} +175 -12
- package/dist/{plugin-DJyjmQeh.mjs → plugin-BeBGu3gH.mjs} +239 -82
- package/dist/{poster-CbpzSzJu.mjs → poster-CoyobbGW.mjs} +1 -1
- package/dist/{shot-BWhoz6cU.mjs → shot-z-d-zMzf.mjs} +2 -2
- package/docs/publish.md +4 -1
- package/package.json +1 -1
- package/src/client/shell/App.tsx +45 -259
- package/src/client/shell/BoardList.tsx +438 -0
- package/src/client/shell/ContextMenu.tsx +59 -0
- package/src/client/shell/LockedApp.tsx +2 -1
- package/src/client/shell/Play.tsx +1 -0
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/labels.ts +4 -4
- package/src/client/shell/store.ts +156 -51
- package/src/client/shell/styles.css +40 -4
- package/src/shared/board-tree.ts +343 -0
- package/templates/AGENTS-embedded.md +25 -5
- package/templates/AGENTS-studio.md +25 -5
- package/templates/instructions/boards.md +91 -2
- package/templates/instructions/craft.md +4 -0
- package/templates/instructions/discover.md +20 -3
- package/templates/instructions/iterate.md +5 -4
- package/templates/instructions/review.md +4 -0
- package/templates/instructions/shape.md +1 -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
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from "cac";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
//#region src/client/const.ts
|
|
@@ -51,29 +51,40 @@ function version() {
|
|
|
51
51
|
return "0.0.0";
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/** The project root, REAL path. A root reached through a symlink (macOS's /var → /private/var,
|
|
55
|
+
* a linked projects folder) would be watched under the link while the OS reports events under
|
|
56
|
+
* the real path - and every frame add, brief edit and config change would go unseen. */
|
|
57
|
+
const resolve$1 = (dir) => {
|
|
58
|
+
const p = resolve(dir);
|
|
59
|
+
try {
|
|
60
|
+
return realpathSync(p);
|
|
61
|
+
} catch {
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
54
65
|
const cli = cac(NAME);
|
|
55
66
|
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-
|
|
57
|
-
init(resolve(opts.root), {
|
|
67
|
+
const { init } = await import("./init-B7YhcN2o.mjs");
|
|
68
|
+
init(resolve$1(opts.root), {
|
|
58
69
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
59
70
|
demo: opts.demo !== false
|
|
60
71
|
});
|
|
61
72
|
});
|
|
62
73
|
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-
|
|
74
|
+
const { dev } = await import("./dev-DH2W7Ffw.mjs");
|
|
64
75
|
let port;
|
|
65
76
|
if (opts.port !== void 0) {
|
|
66
77
|
const n = Number(opts.port);
|
|
67
78
|
if (Number.isInteger(n) && n > 0 && n < 65536) port = n;
|
|
68
79
|
else console.warn(`[${NAME}] ignoring invalid --port "${opts.port}"`);
|
|
69
80
|
}
|
|
70
|
-
await dev(resolve(opts.root), port);
|
|
81
|
+
await dev(resolve$1(opts.root), port);
|
|
71
82
|
});
|
|
72
83
|
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-
|
|
84
|
+
const { buildSite } = await import("./build-B4yPgFNF.mjs");
|
|
74
85
|
try {
|
|
75
86
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
76
|
-
await buildSite(resolve(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
|
|
87
|
+
await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true);
|
|
77
88
|
} catch (err) {
|
|
78
89
|
console.error(`[${NAME}] build failed: ${err.message}`);
|
|
79
90
|
process.exit(1);
|
|
@@ -86,12 +97,12 @@ cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").opti
|
|
|
86
97
|
const n = Number(opts.port);
|
|
87
98
|
if (Number.isInteger(n) && n > 0 && n < 65536) port = n;
|
|
88
99
|
}
|
|
89
|
-
serve(resolve(opts.root), port);
|
|
100
|
+
serve(resolve$1(opts.root), port);
|
|
90
101
|
});
|
|
91
102
|
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-
|
|
103
|
+
const { commentsCommand } = await import("./comments-oYcZ3cE-.mjs");
|
|
93
104
|
try {
|
|
94
|
-
await commentsCommand(resolve(opts.root), action, value, opts);
|
|
105
|
+
await commentsCommand(resolve$1(opts.root), action, value, opts);
|
|
95
106
|
} catch (err) {
|
|
96
107
|
console.error(`[${NAME}] ${err.message}`);
|
|
97
108
|
process.exit(1);
|
|
@@ -100,7 +111,7 @@ cli.command("comments <action> [value]", "Comment collaboration: connect <url>
|
|
|
100
111
|
cli.command("share <action> [value]", "Sharing roster (owner): add <who> · remove <who> · block/unblock <email> · general <mode> · list · requests · explain <who> · who").option("--root <dir>", "Host repo root", { default: "." }).option("--role <role>", "add / requests --approve: view (default) or comment").option("--expires <iso>", "add: expiry timestamp, e.g. 2026-12-31T00:00:00Z").option("--approve <email>", "requests: approve this pending request (canvas-wide in v1)").option("--decline <email>", "requests: decline this pending request (silent to the asker)").option("--json", "list: machine-readable output").action(async (action, value, opts) => {
|
|
101
112
|
const { shareCommand } = await import("./share-Gqo_Ygqw.mjs");
|
|
102
113
|
try {
|
|
103
|
-
await shareCommand(resolve(opts.root), action, value, opts);
|
|
114
|
+
await shareCommand(resolve$1(opts.root), action, value, opts);
|
|
104
115
|
} catch (err) {
|
|
105
116
|
console.error(`[${NAME}] ${err.message}`);
|
|
106
117
|
process.exit(1);
|
|
@@ -109,7 +120,16 @@ cli.command("share <action> [value]", "Sharing roster (owner): add <who> · remo
|
|
|
109
120
|
cli.command("work <action> [...frames]", "Working state on the canvas: start <scene/frame ...> · done <scene/frame ...> | --all · list").option("--root <dir>", "Host repo root", { default: "." }).option("--ttl <minutes>", "start: minutes before the glow self-expires (default 10, max 30)").option("--all", "done: clear every working frame").action(async (action, frames, opts) => {
|
|
110
121
|
const { workCommand } = await import("./work-lzC-lPY0.mjs");
|
|
111
122
|
try {
|
|
112
|
-
await workCommand(resolve(opts.root), action, frames ?? [], opts);
|
|
123
|
+
await workCommand(resolve$1(opts.root), action, frames ?? [], opts);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[${NAME}] ${err.message}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
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) => {
|
|
130
|
+
const { boardsCommand } = await import("./boards-PuVzw5Wp.mjs");
|
|
131
|
+
try {
|
|
132
|
+
boardsCommand(resolve$1(opts.root), opts);
|
|
113
133
|
} catch (err) {
|
|
114
134
|
console.error(`[${NAME}] ${err.message}`);
|
|
115
135
|
process.exit(1);
|
|
@@ -118,7 +138,7 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
|
|
|
118
138
|
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) => {
|
|
119
139
|
const { shotCommand } = await import("./shot-By1AItpD.mjs");
|
|
120
140
|
try {
|
|
121
|
-
await shotCommand(resolve(opts.root), frame, opts);
|
|
141
|
+
await shotCommand(resolve$1(opts.root), frame, opts);
|
|
122
142
|
} catch (err) {
|
|
123
143
|
console.error(`[${NAME}] ${err.message}`);
|
|
124
144
|
process.exit(1);
|
|
@@ -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 { i as toFrameId } from "./manifest-
|
|
4
|
+
import { i as toFrameId } from "./manifest-CaslQIAO.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 { l as detectHost, s as loadConfig } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { l as detectHost, s as loadConfig } from "./manifest-CaslQIAO.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BeBGu3gH.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";
|
|
@@ -257,7 +257,7 @@ async function dev(root, portFlag) {
|
|
|
257
257
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
258
258
|
try {
|
|
259
259
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
260
|
-
const { installedVersion } = await import("./plugin-
|
|
260
|
+
const { installedVersion } = await import("./plugin-BeBGu3gH.mjs").then((n) => n.i);
|
|
261
261
|
const stale = staleManagedInstructions(root);
|
|
262
262
|
if (stale.length) {
|
|
263
263
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -295,7 +295,7 @@ async function dev(root, portFlag) {
|
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
if (config.jam) {
|
|
298
|
-
const { startJam } = await import("./daemon-
|
|
298
|
+
const { startJam } = await import("./daemon-Bbh_jmui.mjs");
|
|
299
299
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
300
300
|
if (jam) {
|
|
301
301
|
const close = server.close.bind(server);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as writeManifest, c as detectAgent, l as detectHost,
|
|
2
|
+
import { a as writeManifest, c as detectAgent, l as detectHost, n as scanFrames, o as DEFAULTS, u as readJson } from "./manifest-CaslQIAO.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,9 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as listBoardFiles, d as flatten, f as isBoardName, g as hash, m as readTitle, n as checkBoardsDir, p as readDescription, r as checkRealDirs, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
|
|
3
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, 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 {
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
//#region src/server/detect.ts
|
|
7
8
|
function readJson(file) {
|
|
8
9
|
try {
|
|
@@ -261,6 +262,7 @@ async function loadConfig(root) {
|
|
|
261
262
|
themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
|
|
262
263
|
port: validPort(user.port) ?? DEFAULTS.port,
|
|
263
264
|
zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
|
|
265
|
+
description: readDescription(user.description),
|
|
264
266
|
share: {
|
|
265
267
|
branding: user.share?.branding !== false,
|
|
266
268
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
@@ -379,8 +381,8 @@ function extractMeta(src) {
|
|
|
379
381
|
if (!m) return {};
|
|
380
382
|
const body = m[1];
|
|
381
383
|
const pick = (key) => {
|
|
382
|
-
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['
|
|
383
|
-
return r ? r[2] : void 0;
|
|
384
|
+
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(?:'([^'\\n]*)'|"([^"\\n]*)"|\`([^\`\\n]*)\`)\\s*(?=[,}\\n]|$)`).exec(body);
|
|
385
|
+
return r ? r[1] ?? r[2] ?? r[3] : void 0;
|
|
384
386
|
};
|
|
385
387
|
const pickBool = (key) => {
|
|
386
388
|
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(true|false)\\b`).exec(body);
|
|
@@ -401,6 +403,8 @@ function extractMeta(src) {
|
|
|
401
403
|
if (intent) out.intent = intent;
|
|
402
404
|
const slide = pickBool("slide");
|
|
403
405
|
if (slide !== void 0) out.slide = slide;
|
|
406
|
+
const description = readDescription(pick("description"));
|
|
407
|
+
if (description) out.description = description;
|
|
404
408
|
return out;
|
|
405
409
|
}
|
|
406
410
|
/** Content-frame detection: LEXICAL by convention - the frame file itself imports
|
|
@@ -473,7 +477,159 @@ function walk(dir, out = []) {
|
|
|
473
477
|
}
|
|
474
478
|
return out;
|
|
475
479
|
}
|
|
476
|
-
|
|
480
|
+
/** The YAML front matter at the top of a markdown file, read flat: `key: value` scalars,
|
|
481
|
+
* quoted (JSON-style double quotes, or single quotes with '' for one) or bare. `end` is the
|
|
482
|
+
* line the body starts at (0 = no block; an unclosed block swallows the file). */
|
|
483
|
+
function frontMatter(lines) {
|
|
484
|
+
const fields = {};
|
|
485
|
+
if (lines[0]?.trim() !== "---") return {
|
|
486
|
+
fields,
|
|
487
|
+
end: 0
|
|
488
|
+
};
|
|
489
|
+
let close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
490
|
+
if (close < 0) close = lines.length;
|
|
491
|
+
for (const l of lines.slice(1, close)) {
|
|
492
|
+
const m = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(l.trim());
|
|
493
|
+
if (!m) continue;
|
|
494
|
+
let v = m[2].trim();
|
|
495
|
+
if (v.startsWith("\"")) try {
|
|
496
|
+
v = String(JSON.parse(v));
|
|
497
|
+
} catch {
|
|
498
|
+
v = v.replace(/^"|"$/g, "");
|
|
499
|
+
}
|
|
500
|
+
else if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1).replace(/''/g, "'");
|
|
501
|
+
fields[m[1]] = v;
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
fields,
|
|
505
|
+
end: Math.min(close + 1, lines.length)
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/** A scene's title is `title` in its `_brief.md` front matter; its description is the first
|
|
509
|
+
* non-blank line after the block (a leading `#` stripped) - no new file. */
|
|
510
|
+
function sceneBrief(root, scene) {
|
|
511
|
+
if (!scene) return {};
|
|
512
|
+
const rel = `design/scenes/${scene}/_brief.md`;
|
|
513
|
+
const abs = join(root, ...rel.split("/"));
|
|
514
|
+
if (!existsSync(abs)) return {};
|
|
515
|
+
let title, description;
|
|
516
|
+
try {
|
|
517
|
+
const lines = readFileSync(abs, "utf8").replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
518
|
+
const fm = frontMatter(lines);
|
|
519
|
+
title = readTitle(fm.fields.title);
|
|
520
|
+
const line = lines.slice(fm.end).map((l) => l.trim()).find((l) => l);
|
|
521
|
+
description = readDescription(line?.replace(/^#+\s*/, ""));
|
|
522
|
+
} catch {}
|
|
523
|
+
return {
|
|
524
|
+
brief: rel,
|
|
525
|
+
...title ? { title } : {},
|
|
526
|
+
...description ? { description } : {}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
/** Write a scene's title into its brief's front matter - only that line changes; the body,
|
|
530
|
+
* every other field and the file's own line endings are kept. No brief yet = a front-matter-only
|
|
531
|
+
* brief. An empty title removes the line, and the file when nothing else is in it. The write
|
|
532
|
+
* is atomic (temp + rename); a symlinked scene directory or brief, or a front matter block
|
|
533
|
+
* that never closes, is refused untouched - the brief is the agent's document, never rewritten
|
|
534
|
+
* from a guess. Returns the error, or null. */
|
|
535
|
+
function setSceneTitle(root, scene, title) {
|
|
536
|
+
const design = join(root, "design"), scenes = join(design, "scenes"), dir = join(scenes, scene);
|
|
537
|
+
const de = checkRealDirs(root, [
|
|
538
|
+
[design, "design"],
|
|
539
|
+
[scenes, "design/scenes"],
|
|
540
|
+
[dir, `design/scenes/${scene}`]
|
|
541
|
+
]);
|
|
542
|
+
if (de) return de;
|
|
543
|
+
try {
|
|
544
|
+
if (!lstatSync(dir).isDirectory()) return `"${scene}" is not a scene directory`;
|
|
545
|
+
} catch {
|
|
546
|
+
return `scene "${scene}" does not exist`;
|
|
547
|
+
}
|
|
548
|
+
const file = join(dir, "_brief.md");
|
|
549
|
+
let had = false;
|
|
550
|
+
try {
|
|
551
|
+
if (!lstatSync(file).isFile()) return `design/scenes/${scene}/_brief.md is not a regular file - refusing to write it`;
|
|
552
|
+
had = true;
|
|
553
|
+
} catch {}
|
|
554
|
+
const text = had ? readFileSync(file, "utf8") : "";
|
|
555
|
+
const bom = text.startsWith("") ? "" : "";
|
|
556
|
+
const raw = text.slice(bom.length);
|
|
557
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
558
|
+
const lines = raw.split(/\r?\n/);
|
|
559
|
+
const fm = frontMatter(lines);
|
|
560
|
+
if (fm.end === lines.length && lines[0]?.trim() === "---" && !lines.slice(1).some((l) => l.trim() === "---")) return `design/scenes/${scene}/_brief.md: the front matter never closes - fix the file`;
|
|
561
|
+
const line = title ? [`title: ${JSON.stringify(title)}`] : [];
|
|
562
|
+
let out;
|
|
563
|
+
if (fm.end === 0) {
|
|
564
|
+
if (!title) return null;
|
|
565
|
+
out = [
|
|
566
|
+
"---",
|
|
567
|
+
...line,
|
|
568
|
+
"---",
|
|
569
|
+
...raw.trim() ? ["", ...lines] : [""]
|
|
570
|
+
];
|
|
571
|
+
} else {
|
|
572
|
+
const close = Math.max(1, fm.end - 1);
|
|
573
|
+
const kept = lines.slice(1, close).filter((l) => !/^title\s*:/.test(l.trim()));
|
|
574
|
+
const body = lines.slice(fm.end);
|
|
575
|
+
out = kept.length || title ? [
|
|
576
|
+
"---",
|
|
577
|
+
...line,
|
|
578
|
+
...kept,
|
|
579
|
+
"---",
|
|
580
|
+
...body
|
|
581
|
+
] : body;
|
|
582
|
+
}
|
|
583
|
+
if (!out.join("").trim()) {
|
|
584
|
+
if (had) rmSync(file);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const tmp = join(dir, `.brief-${randomBytes(6).toString("hex")}.tmp`);
|
|
588
|
+
try {
|
|
589
|
+
writeFileSync(tmp, bom + out.join(eol));
|
|
590
|
+
renameSync(tmp, file);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
rmSync(tmp, { force: true });
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
/** The sidebar as files say it is, for the manifest: folders in root order, boards in
|
|
598
|
+
* reading order with their folder and description. A boards dir we may not read (symlink)
|
|
599
|
+
* or a malformed registry yields nothing here - the API and the build say why; the
|
|
600
|
+
* orientation file must never fail to write. */
|
|
601
|
+
function scanBoards(root) {
|
|
602
|
+
const dir = join(root, "design", "boards");
|
|
603
|
+
if (checkBoardsDir(root, dir)) return {};
|
|
604
|
+
const reg = readRegistry(dir);
|
|
605
|
+
if (reg.state === "malformed") return {};
|
|
606
|
+
const rows = listBoardFiles(dir).boards.map((b) => ({
|
|
607
|
+
name: b.name,
|
|
608
|
+
...boardFields(b.json, isBoardName)
|
|
609
|
+
}));
|
|
610
|
+
const tree = buildTree(rows, reg.folders);
|
|
611
|
+
const folderOf = /* @__PURE__ */ new Map();
|
|
612
|
+
for (const it of tree) if (it.kind === "folder") for (const b of it.boards) folderOf.set(b, it.name);
|
|
613
|
+
const folders = tree.filter((it) => it.kind === "folder").map((it) => ({
|
|
614
|
+
name: it.name,
|
|
615
|
+
...it.title ? { title: it.title } : {},
|
|
616
|
+
...it.description ? { description: it.description } : {}
|
|
617
|
+
}));
|
|
618
|
+
const boards = flatten(tree).map((name) => {
|
|
619
|
+
const r = rows.find((x) => x.name === name);
|
|
620
|
+
return {
|
|
621
|
+
name,
|
|
622
|
+
...folderOf.has(name) ? { folder: folderOf.get(name) } : {},
|
|
623
|
+
...r?.title ? { title: r.title } : {},
|
|
624
|
+
...r?.description ? { description: r.description } : {}
|
|
625
|
+
};
|
|
626
|
+
});
|
|
627
|
+
return {
|
|
628
|
+
...folders.length ? { folders } : {},
|
|
629
|
+
...boards.length ? { boards } : {}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function scanFrames(root, project) {
|
|
477
633
|
const design = join(root, "design");
|
|
478
634
|
const frames = [];
|
|
479
635
|
for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
|
|
@@ -502,6 +658,7 @@ function scanFrames(root) {
|
|
|
502
658
|
if (meta.of) entry.variantGroup = meta.of;
|
|
503
659
|
if (meta.variant) entry.variant = meta.variant;
|
|
504
660
|
if (meta.slide) entry.slide = true;
|
|
661
|
+
if (meta.description) entry.description = meta.description;
|
|
505
662
|
const content = contentScan(src);
|
|
506
663
|
if (content || meta.intent) {
|
|
507
664
|
entry.intent = meta.intent ?? content.intent;
|
|
@@ -527,12 +684,19 @@ function scanFrames(root) {
|
|
|
527
684
|
inferVariantGroups(frames);
|
|
528
685
|
const sceneCounts = /* @__PURE__ */ new Map();
|
|
529
686
|
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
687
|
+
const scenes = [...sceneCounts.entries()].map(([name, n]) => ({
|
|
688
|
+
name,
|
|
689
|
+
frames: n,
|
|
690
|
+
...sceneBrief(root, name)
|
|
691
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
530
692
|
return {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
693
|
+
...project && (project.name || project.description) ? { project: {
|
|
694
|
+
...project.name ? { name: project.name } : {},
|
|
695
|
+
...project.description ? { description: project.description } : {}
|
|
696
|
+
} } : {},
|
|
697
|
+
...scanBoards(root),
|
|
698
|
+
scenes,
|
|
699
|
+
frames
|
|
536
700
|
};
|
|
537
701
|
}
|
|
538
702
|
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
@@ -614,6 +778,5 @@ function writeManifest(root, manifest) {
|
|
|
614
778
|
writeFileSync(file, next);
|
|
615
779
|
return true;
|
|
616
780
|
}
|
|
617
|
-
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
618
781
|
//#endregion
|
|
619
|
-
export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l,
|
|
782
|
+
export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l, scanFrames as n, DEFAULTS as o, setSceneTitle as r, loadConfig as s, affectedFrameIds as t, readJson as u };
|