@marver-design/marver 0.16.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 +26 -0
- package/README.md +2 -2
- package/dist/{boards-BdG1TJwU.mjs → boards-BmxcT3Lc.mjs} +32 -9
- package/dist/{boards-6hKVW42a.mjs → boards-PuVzw5Wp.mjs} +9 -4
- package/dist/{build-Ct0iMoSi.mjs → build-B4yPgFNF.mjs} +12 -8
- package/dist/cli.mjs +25 -14
- package/dist/{daemon-Cb4JjpgL.mjs → daemon-Bbh_jmui.mjs} +1 -1
- package/dist/{dev-CdcIuhZJ.mjs → dev-DH2W7Ffw.mjs} +4 -4
- package/dist/{init-BQIpIpIv.mjs → init-B7YhcN2o.mjs} +1 -1
- package/dist/{manifest-CpbsqQ_v.mjs → manifest-CaslQIAO.mjs} +112 -15
- package/dist/{plugin-BXyezwfN.mjs → plugin-BeBGu3gH.mjs} +85 -23
- package/dist/{poster-DOY7pax8.mjs → poster-CoyobbGW.mjs} +1 -1
- package/dist/{shot-CwmHO5T4.mjs → shot-z-d-zMzf.mjs} +2 -2
- package/docs/publish.md +2 -2
- package/package.json +1 -1
- package/src/client/shell/App.tsx +42 -14
- package/src/client/shell/BoardList.tsx +88 -58
- package/src/client/shell/LockedApp.tsx +2 -1
- package/src/client/shell/Play.tsx +1 -0
- package/src/client/shell/labels.ts +4 -4
- package/src/client/shell/store.ts +76 -29
- package/src/client/shell/styles.css +5 -1
- package/src/shared/board-tree.ts +104 -46
- package/templates/AGENTS-embedded.md +9 -2
- package/templates/AGENTS-studio.md +9 -2
- package/templates/instructions/boards.md +27 -9
- package/templates/instructions/discover.md +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
|
|
4
4
|
|
|
5
|
+
## 0.16.1 - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Titles.** Boards, folders and scenes can be called anything - "MVP", "UI",
|
|
10
|
+
"Checkout (v2) 🛒". Rename in the sidebar (scenes too, now) edits a `title`;
|
|
11
|
+
the file name, folder key and scene directory stay the slug agents address.
|
|
12
|
+
No title = the Title-Cased slug, as before. Stored on the board JSON, the
|
|
13
|
+
`_folders.json` entry and the brief's YAML front matter; shown everywhere the
|
|
14
|
+
object is named and carried by the manifest and `npx marver boards`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Drops land where the seam shows.** A board grabbed by its icon and dragged
|
|
19
|
+
between two folder boards landed after the folder; releases in blank space did
|
|
20
|
+
nothing. The resolver now reads the pointer against the rendered rows, the
|
|
21
|
+
release applies the seam on screen, and a release outside the panel cancels.
|
|
22
|
+
- A project reached through a symlink (macOS `/var`, a linked projects folder)
|
|
23
|
+
lost its live updates; the CLI now resolves the root's real path.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `boards/rename` takes `{ from, to?, title?, baseHash? }` (title-only, CAS);
|
|
28
|
+
new `scenes/rename { scene, title }`; `sh:scenes` relabels scenes without
|
|
29
|
+
re-keying iframes.
|
|
30
|
+
|
|
5
31
|
## 0.16.0 - 2026-09-03
|
|
6
32
|
|
|
7
33
|
### Added
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Frames appear on the canvas the moment the files land. That's the loop.
|
|
|
40
40
|
|
|
41
41
|
## The canvas
|
|
42
42
|
|
|
43
|
-
- **Frames, scenes, boards.** Frames are screens, scenes group them (`design/scenes/<scene>/<frame>.tsx`), boards arrange them. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed. Right-click any board, scene, or frame in the sidebar to copy its path - the exact string to paste to your agent - and rename or drag-reorder boards from there too. Boards can live in folders: right-click the Boards header (or its `+`) for a new one, name it inline, drag boards in and out and folders among boards; agents do the same by writing `"folder": "<name>"` on a board and `design/boards/_folders.json` for empty or ranked folders (`npx marver boards` prints the tree). Every object takes a one-sentence `description` (project in `design/config.ts`, boards and folders in their JSON, a scene's first `_brief.md` line, `meta.description` on a frame) and `design/manifest.json` carries them all - a new agent session orients in one read.
|
|
43
|
+
- **Frames, scenes, boards.** Frames are screens, scenes group them (`design/scenes/<scene>/<frame>.tsx`), boards arrange them. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed. Right-click any board, scene, or frame in the sidebar to copy its path - the exact string to paste to your agent - and rename or drag-reorder boards from there too. Boards can live in folders: right-click the Boards header (or its `+`) for a new one, name it inline, drag boards in and out (the seam shows exactly where a release lands) and folders among boards; agents do the same by writing `"folder": "<name>"` on a board and `design/boards/_folders.json` for empty or ranked folders (`npx marver boards` prints the tree). Boards, folders and scenes take a `title` - any name you like, "MVP", "UI", "Checkout (v2) 🛒" - while their file, key and directory stay the slug agents address; Rename in the sidebar edits the title. Every object takes a one-sentence `description` (project in `design/config.ts`, boards and folders in their JSON, a scene's first `_brief.md` line, `meta.description` on a frame) and `design/manifest.json` carries them all - a new agent session orients in one read.
|
|
44
44
|
- **Devices view.** Hotkeys `1`-`5` (or the Devices menu) size every frame to mobile / tablet / laptop / monitor / tv to sweep your breakpoints; `0` restores your own layout exactly. Widths live in `design/config.ts`.
|
|
45
45
|
- **Prototype links.** `data-goto="scene/frame"` on any element links frames into a walkable prototype - across boards, too.
|
|
46
46
|
- **Five ways to view a board.** The canvas (frames on a plane), the board (the same, tidy), **present** (`p`: a full-screen clickable walkthrough - `data-goto` navigates, arrows step, `[` / `]` cycle variants, laser, comments, theme and device pickers in the toolbar), **focus** (one frame as a document - the reading preset for specs), and **slides** (a deck). A published board names its landing view; a frame deep link opens straight into it.
|
|
@@ -98,7 +98,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
|
|
|
98
98
|
| `npx marver comments …` | The agent's queue: `connect <url>` · `sync` · `list` · `reply` · `resolve` · `invite <email>` · `revoke <email>` |
|
|
99
99
|
| `npx marver work …` | Working glow from the terminal: `start <scene/frame …>` · `done … \| --all` · `list` |
|
|
100
100
|
| `npx marver shot <frame> [--scale 1-4]` | Render one frame headless and print the PNG path (needs `dev` running); 2x by default |
|
|
101
|
-
| `npx marver boards [--json]` | The sidebar as the files say it is: folders, boards in reading order with `order` and description, the landing board |
|
|
101
|
+
| `npx marver boards [--json]` | The sidebar as the files say it is: folders, boards in reading order with their title, `order` and description, the landing board |
|
|
102
102
|
|
|
103
103
|
## Shortcuts
|
|
104
104
|
|
|
@@ -25,7 +25,16 @@ const readDescription = (v) => {
|
|
|
25
25
|
if (typeof v !== "string") return void 0;
|
|
26
26
|
return v.trim().replace(/\s+/g, " ").slice(0, 300) || void 0;
|
|
27
27
|
};
|
|
28
|
+
const readTitle = (v) => {
|
|
29
|
+
if (typeof v !== "string") return void 0;
|
|
30
|
+
return Array.from(v.replace(/[\u0000-\u001f\u007f]/g, " ").trim().replace(/\s+/g, " ")).slice(0, 120).join("").trim() || void 0;
|
|
31
|
+
};
|
|
28
32
|
const rank = (o) => typeof o === "number" && Number.isFinite(o) ? o : Infinity;
|
|
33
|
+
/** A folder's title and description, present only when set. */
|
|
34
|
+
const folderExtras = (it) => ({
|
|
35
|
+
...it.title ? { title: it.title } : {},
|
|
36
|
+
...it.description ? { description: it.description } : {}
|
|
37
|
+
});
|
|
29
38
|
/** The registry file's shape. Returns the rows, or a string naming what is wrong - a
|
|
30
39
|
* malformed registry is an ERROR the human must fix (silently reading it as empty would
|
|
31
40
|
* let the next drag overwrite their folders), while a missing file is simply no folders. */
|
|
@@ -42,10 +51,12 @@ function parseFolders(raw) {
|
|
|
42
51
|
if (seen.has(name)) return `folder "${name}" is listed twice`;
|
|
43
52
|
seen.add(name);
|
|
44
53
|
const o = f.order;
|
|
54
|
+
const t = readTitle(f.title);
|
|
45
55
|
const d = readDescription(f.description);
|
|
46
56
|
out.push({
|
|
47
57
|
name,
|
|
48
58
|
...typeof o === "number" && Number.isFinite(o) ? { order: o } : {},
|
|
59
|
+
...t ? { title: t } : {},
|
|
49
60
|
...d ? { description: d } : {}
|
|
50
61
|
});
|
|
51
62
|
}
|
|
@@ -53,13 +64,15 @@ function parseFolders(raw) {
|
|
|
53
64
|
}
|
|
54
65
|
/** Sidebar order from the files. Root: boards with no folder + every folder (registered
|
|
55
66
|
* or implied by a board), ranked by `order` then kind (board before folder) then name.
|
|
56
|
-
* Inside a folder: its boards by `order` then name. Unranked sorts after ranked.
|
|
67
|
+
* Inside a folder: its boards by `order` then name. Unranked sorts after ranked. A folder's
|
|
68
|
+
* title and description ride on its item (they live in the registry a tree write rewrites);
|
|
69
|
+
* a board's title stays with its row - it lives in the board's own file. */
|
|
57
70
|
function buildTree(boards, folders) {
|
|
58
71
|
const folderOrder = /* @__PURE__ */ new Map();
|
|
59
|
-
const
|
|
72
|
+
const folderMeta = /* @__PURE__ */ new Map();
|
|
60
73
|
for (const f of folders) if (isBoardName(f.name) && !folderOrder.has(f.name)) {
|
|
61
74
|
folderOrder.set(f.name, f.order);
|
|
62
|
-
|
|
75
|
+
folderMeta.set(f.name, folderExtras(f));
|
|
63
76
|
}
|
|
64
77
|
const members = /* @__PURE__ */ new Map();
|
|
65
78
|
const rootBoards = [];
|
|
@@ -87,7 +100,7 @@ function buildTree(boards, folders) {
|
|
|
87
100
|
kind: "folder",
|
|
88
101
|
name,
|
|
89
102
|
boards: (members.get(name) ?? []).sort(byRank).map((b) => b.name),
|
|
90
|
-
...
|
|
103
|
+
...folderMeta.get(name) ?? {}
|
|
91
104
|
},
|
|
92
105
|
order
|
|
93
106
|
}))];
|
|
@@ -117,8 +130,9 @@ function validateWire(wire) {
|
|
|
117
130
|
continue;
|
|
118
131
|
}
|
|
119
132
|
if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
|
|
120
|
-
const { folder, boards: kids, description } = w;
|
|
133
|
+
const { folder, boards: kids, title, description } = w;
|
|
121
134
|
if (!isBoardName(folder)) return "invalid folder name in tree";
|
|
135
|
+
if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
|
|
122
136
|
if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
|
|
123
137
|
if (folders.has(folder)) return `folder "${folder}" appears twice`;
|
|
124
138
|
folders.add(folder);
|
|
@@ -155,8 +169,14 @@ function underRoot(root, dir) {
|
|
|
155
169
|
* it; a link outside would publish foreign JSON) and must resolve inside the root. Absent
|
|
156
170
|
* is fine (no boards yet). Returns the error, or null. */
|
|
157
171
|
function checkBoardsDir(root, boardsDir) {
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
return checkRealDirs(root, [[join(boardsDir, ".."), "design"], [boardsDir, "design/boards"]]);
|
|
173
|
+
}
|
|
174
|
+
/** Every EXISTING path in `dirs` (root-down order) must be a real directory inside the root -
|
|
175
|
+
* a symlinked `design` with no boards dir yet would otherwise be followed by the mkdir that
|
|
176
|
+
* creates it; a symlinked `design/scenes` would let a brief write land outside the project.
|
|
177
|
+
* An absent one ends the walk (nothing beneath it exists either). Returns the error, or null. */
|
|
178
|
+
function checkRealDirs(root, dirs) {
|
|
179
|
+
for (const [p, label] of dirs) {
|
|
160
180
|
try {
|
|
161
181
|
if (lstatSync(p).isSymbolicLink()) return `${label} must be a real directory, not a symlink`;
|
|
162
182
|
} catch {
|
|
@@ -216,13 +236,16 @@ function listBoardFiles(boardsDir) {
|
|
|
216
236
|
skipped
|
|
217
237
|
};
|
|
218
238
|
}
|
|
219
|
-
/** The author-owned sidebar fields off a board's JSON, leniently
|
|
239
|
+
/** The author-owned sidebar fields off a board's JSON, leniently: rank, folder, the title
|
|
240
|
+
* humans see, the sentence agents read. */
|
|
220
241
|
function boardFields(json, validName) {
|
|
221
242
|
const o = json;
|
|
243
|
+
const title = readTitle(o?.title);
|
|
222
244
|
const description = readDescription(o?.description);
|
|
223
245
|
return {
|
|
224
246
|
...typeof o?.order === "number" && Number.isFinite(o.order) ? { order: o.order } : {},
|
|
225
247
|
...validName(o?.folder) ? { folder: o.folder } : {},
|
|
248
|
+
...title ? { title } : {},
|
|
226
249
|
...description ? { description } : {}
|
|
227
250
|
};
|
|
228
251
|
}
|
|
@@ -264,4 +287,4 @@ function readRegistry(boardsDir) {
|
|
|
264
287
|
};
|
|
265
288
|
}
|
|
266
289
|
//#endregion
|
|
267
|
-
export {
|
|
290
|
+
export { listBoardFiles as a, BOARD_NAME as c, flatten as d, isBoardName as f, hash as g, validateWire as h, isRegularFile as i, FOLDERS_FILE as l, readTitle as m, checkBoardsDir as n, nodeExists as o, readDescription as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as listBoardFiles, d as flatten, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
//#region src/cli/boards.ts
|
|
4
4
|
/**
|
|
@@ -37,14 +37,19 @@ function boardsCommand(root, opts) {
|
|
|
37
37
|
const o = rows.find((r) => r.name === n)?.order;
|
|
38
38
|
return o === void 0 ? "" : ` order ${o}`;
|
|
39
39
|
};
|
|
40
|
+
const title = (t) => t ? ` "${t}"` : "";
|
|
40
41
|
const desc = (d) => d ? ` - ${d}` : "";
|
|
42
|
+
const boardLine = (n) => {
|
|
43
|
+
const r = rows.find((x) => x.name === n);
|
|
44
|
+
return `${n}${title(r?.title)}${order(n)}${desc(r?.description)}`;
|
|
45
|
+
};
|
|
41
46
|
for (const it of tree) {
|
|
42
47
|
if (it.kind === "board") {
|
|
43
|
-
console.log(
|
|
48
|
+
console.log(boardLine(it.name));
|
|
44
49
|
continue;
|
|
45
50
|
}
|
|
46
|
-
console.log(`${it.name}
|
|
47
|
-
for (const b of it.boards) console.log(` ${
|
|
51
|
+
console.log(`${it.name}/${title(it.title)} (folder, ${it.boards.length} board${it.boards.length === 1 ? "" : "s"}${reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json"})${desc(it.description)}`);
|
|
52
|
+
for (const b of it.boards) console.log(` ${boardLine(b)}`);
|
|
48
53
|
if (!it.boards.length) console.log(" (empty)");
|
|
49
54
|
}
|
|
50
55
|
if (hasAll) console.log("all-scenes (auto, always last)");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as listBoardFiles, d as flatten, f as isBoardName, n as checkBoardsDir, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
|
|
3
|
+
import { l as detectHost, n as scanFrames, s as loadConfig } from "./manifest-CaslQIAO.mjs";
|
|
4
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BeBGu3gH.mjs";
|
|
5
5
|
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -171,11 +171,9 @@ function readBoards(root) {
|
|
|
171
171
|
* the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
|
|
172
172
|
* like board names: they ship. */
|
|
173
173
|
function publishedTree(published, allBoards, folders) {
|
|
174
|
-
const field = (n, k) => allBoards[n]?.[k];
|
|
175
174
|
const tree = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
|
|
176
175
|
name: n,
|
|
177
|
-
|
|
178
|
-
folder: field(n, "folder")
|
|
176
|
+
...boardFields(allBoards[n], isBoardName)
|
|
179
177
|
})), folders).filter((it) => it.kind === "board" || it.boards.length > 0);
|
|
180
178
|
return {
|
|
181
179
|
tree,
|
|
@@ -195,9 +193,10 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
|
195
193
|
...manifest.project ? { project: manifest.project } : {},
|
|
196
194
|
...pubFolders.length ? { folders: pubFolders } : {},
|
|
197
195
|
...pubBoards.length ? { boards: pubBoards } : {},
|
|
198
|
-
scenes: manifest.scenes.filter((s) => pubScenes.has(s.name)).map(({ name, description, brief }) => ({
|
|
196
|
+
scenes: manifest.scenes.filter((s) => pubScenes.has(s.name)).map(({ name, title, description, brief }) => ({
|
|
199
197
|
name,
|
|
200
198
|
frames: pubFrames.filter((f) => f.scene === name).length,
|
|
199
|
+
...title ? { title } : {},
|
|
201
200
|
...description ? { description } : {},
|
|
202
201
|
...brief && !strip ? { brief } : {}
|
|
203
202
|
})),
|
|
@@ -364,11 +363,16 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
364
363
|
"slides"
|
|
365
364
|
].includes(p.open);
|
|
366
365
|
});
|
|
366
|
+
const titles = Object.fromEntries(publishedNames.flatMap((n) => {
|
|
367
|
+
const t = boardFields(allBoards[n], isBoardName).title;
|
|
368
|
+
return t ? [[n, t]] : [];
|
|
369
|
+
}));
|
|
367
370
|
const data = {
|
|
368
371
|
manifest: pubManifest,
|
|
369
372
|
boards,
|
|
370
373
|
names: publishedNames,
|
|
371
374
|
tree,
|
|
375
|
+
...Object.keys(titles).length ? { titles } : {},
|
|
372
376
|
default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
|
|
373
377
|
rights,
|
|
374
378
|
policy: {
|
|
@@ -500,7 +504,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds) {
|
|
|
500
504
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
501
505
|
for (const r of refs) {
|
|
502
506
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
503
|
-
const { ensurePoster } = await import("./poster-
|
|
507
|
+
const { ensurePoster } = await import("./poster-CoyobbGW.mjs");
|
|
504
508
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
505
509
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
506
510
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
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
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,16 +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);
|
|
113
124
|
} catch (err) {
|
|
114
125
|
console.error(`[${NAME}] ${err.message}`);
|
|
115
126
|
process.exit(1);
|
|
116
127
|
}
|
|
117
128
|
});
|
|
118
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) => {
|
|
119
|
-
const { boardsCommand } = await import("./boards-
|
|
130
|
+
const { boardsCommand } = await import("./boards-PuVzw5Wp.mjs");
|
|
120
131
|
try {
|
|
121
|
-
boardsCommand(resolve(opts.root), opts);
|
|
132
|
+
boardsCommand(resolve$1(opts.root), opts);
|
|
122
133
|
} catch (err) {
|
|
123
134
|
console.error(`[${NAME}] ${err.message}`);
|
|
124
135
|
process.exit(1);
|
|
@@ -127,7 +138,7 @@ cli.command("boards", "The sidebar as files: every folder and board in reading o
|
|
|
127
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) => {
|
|
128
139
|
const { shotCommand } = await import("./shot-By1AItpD.mjs");
|
|
129
140
|
try {
|
|
130
|
-
await shotCommand(resolve(opts.root), frame, opts);
|
|
141
|
+
await shotCommand(resolve$1(opts.root), frame, opts);
|
|
131
142
|
} catch (err) {
|
|
132
143
|
console.error(`[${NAME}] ${err.message}`);
|
|
133
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 {
|
|
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 {
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
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
5
|
import { basename, dirname, join, relative } from "node:path";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
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";
|
|
4
4
|
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
//#region src/server/detect.ts
|
|
7
8
|
function readJson(file) {
|
|
8
9
|
try {
|
|
@@ -476,29 +477,123 @@ function walk(dir, out = []) {
|
|
|
476
477
|
}
|
|
477
478
|
return out;
|
|
478
479
|
}
|
|
479
|
-
/**
|
|
480
|
-
*
|
|
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. */
|
|
481
510
|
function sceneBrief(root, scene) {
|
|
482
511
|
if (!scene) return {};
|
|
483
512
|
const rel = `design/scenes/${scene}/_brief.md`;
|
|
484
513
|
const abs = join(root, ...rel.split("/"));
|
|
485
514
|
if (!existsSync(abs)) return {};
|
|
486
|
-
let description;
|
|
515
|
+
let title, description;
|
|
487
516
|
try {
|
|
488
|
-
const lines = readFileSync(abs, "utf8").split(/\r?\n/)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (i === 0) i = lines.length;
|
|
493
|
-
}
|
|
494
|
-
const line = lines.slice(i).find((l) => l);
|
|
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);
|
|
495
521
|
description = readDescription(line?.replace(/^#+\s*/, ""));
|
|
496
522
|
} catch {}
|
|
497
523
|
return {
|
|
498
524
|
brief: rel,
|
|
525
|
+
...title ? { title } : {},
|
|
499
526
|
...description ? { description } : {}
|
|
500
527
|
};
|
|
501
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
|
+
}
|
|
502
597
|
/** The sidebar as files say it is, for the manifest: folders in root order, boards in
|
|
503
598
|
* reading order with their folder and description. A boards dir we may not read (symlink)
|
|
504
599
|
* or a malformed registry yields nothing here - the API and the build say why; the
|
|
@@ -517,14 +612,16 @@ function scanBoards(root) {
|
|
|
517
612
|
for (const it of tree) if (it.kind === "folder") for (const b of it.boards) folderOf.set(b, it.name);
|
|
518
613
|
const folders = tree.filter((it) => it.kind === "folder").map((it) => ({
|
|
519
614
|
name: it.name,
|
|
615
|
+
...it.title ? { title: it.title } : {},
|
|
520
616
|
...it.description ? { description: it.description } : {}
|
|
521
617
|
}));
|
|
522
618
|
const boards = flatten(tree).map((name) => {
|
|
523
|
-
const
|
|
619
|
+
const r = rows.find((x) => x.name === name);
|
|
524
620
|
return {
|
|
525
621
|
name,
|
|
526
622
|
...folderOf.has(name) ? { folder: folderOf.get(name) } : {},
|
|
527
|
-
...
|
|
623
|
+
...r?.title ? { title: r.title } : {},
|
|
624
|
+
...r?.description ? { description: r.description } : {}
|
|
528
625
|
};
|
|
529
626
|
});
|
|
530
627
|
return {
|
|
@@ -682,4 +779,4 @@ function writeManifest(root, manifest) {
|
|
|
682
779
|
return true;
|
|
683
780
|
}
|
|
684
781
|
//#endregion
|
|
685
|
-
export {
|
|
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 };
|