@marver-design/marver 0.21.0 → 0.22.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +134 -15
  2. package/README.md +17 -4
  3. package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
  4. package/dist/board-status-CWGHIdo_.mjs +1307 -0
  5. package/dist/boards-BwKI0qWd.mjs +188 -0
  6. package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
  7. package/dist/cli.mjs +69 -9
  8. package/dist/config-DJxMRVD8.mjs +373 -0
  9. package/dist/context-D4t59wDi.mjs +600 -0
  10. package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
  11. package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
  12. package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
  13. package/dist/managed-write-Bo-oPc-i.mjs +71 -0
  14. package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
  15. package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
  16. package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
  17. package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
  18. package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.mjs} +2 -2
  19. package/docs/boards-and-folders.md +161 -0
  20. package/docs/context.md +117 -0
  21. package/docs/sharing.md +12 -2
  22. package/package.json +1 -1
  23. package/src/client/shell/BoardList.tsx +33 -3
  24. package/src/client/shell/ContextMenu.tsx +16 -5
  25. package/src/client/shell/StatusPicker.tsx +91 -0
  26. package/src/client/shell/board-icons.tsx +75 -0
  27. package/src/client/shell/store.ts +88 -9
  28. package/src/client/shell/styles.css +34 -3
  29. package/src/shared/board-tree.ts +30 -13
  30. package/src/shared/board-types.ts +103 -0
  31. package/src/shared/context.ts +265 -0
  32. package/src/shared/status.ts +157 -0
  33. package/templates/AGENTS-embedded.md +12 -0
  34. package/templates/AGENTS-studio.md +12 -0
  35. package/templates/context/INDEX.md +50 -0
  36. package/templates/context/map.json +6 -0
  37. package/templates/context/shipped-knowledge.md +19 -0
  38. package/templates/context/shipped.md +28 -0
  39. package/templates/instructions/boards.md +33 -0
  40. package/templates/instructions/context.md +123 -0
  41. package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
  42. package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
  43. package/templates/playbooks/reorganize-context/eval.md +93 -0
  44. package/dist/boards-BwiDAmPf.mjs +0 -337
  45. package/dist/boards-DnLewfj8.mjs +0 -71
@@ -1,337 +0,0 @@
1
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
- import { join, sep } from "node:path";
3
- import { createHash } from "node:crypto";
4
- //#region src/server/hash.ts
5
- /** sha256 hex of a string - the CAS token for board files, the registry, the manifest. */
6
- const hash = (s) => createHash("sha256").update(s).digest("hex");
7
- //#endregion
8
- //#region src/shared/board-tree.ts
9
- /**
10
- * Board folders - the pure tree shared by the sidebar, the dev API, the build and the
11
- * tests. Files are the truth: a board says which folder it sits in (`folder` on the
12
- * board file - one slug, the folder it sits in directly), ranked among its siblings by
13
- * `order`, and `design/boards/_folders.json` says which folders exist, where they rank, and
14
- * which folder holds which (`parent`). Two levels: a folder holds boards and folders, a
15
- * folder inside a folder (a sub-folder) holds boards only. `all-scenes` never enters the
16
- * tree - callers pin it last.
17
- */
18
- /** The on-disk name grammar shared by boards and folders (a board name is a filename). */
19
- const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
20
- const isBoardName = (n) => typeof n === "string" && n.length >= 1 && n.length <= 64 && BOARD_NAME.test(n);
21
- /** The folder registry beside the boards - underscore = infrastructure, never a board. */
22
- const FOLDERS_FILE = "_folders.json";
23
- /** Is this basename in design/boards/ a board file? `_folders.json`, temp files and any
24
- * off-grammar name are not - every lister (dev API, build, watcher) shares this rule. */
25
- const isBoardFile = (f) => f.endsWith(".json") && isBoardName(f.slice(0, -5));
26
- const readDescription = (v) => {
27
- if (typeof v !== "string") return void 0;
28
- return v.trim().replace(/\s+/g, " ").slice(0, 300) || void 0;
29
- };
30
- const readTitle = (v) => {
31
- if (typeof v !== "string") return void 0;
32
- return Array.from(v.replace(/[\u0000-\u001f\u007f]/g, " ").trim().replace(/\s+/g, " ")).slice(0, 120).join("").trim() || void 0;
33
- };
34
- const rank = (o) => typeof o === "number" && Number.isFinite(o) ? o : Infinity;
35
- /** A folder's title and description, present only when set. */
36
- const folderExtras = (it) => ({
37
- ...it.title ? { title: it.title } : {},
38
- ...it.description ? { description: it.description } : {}
39
- });
40
- /** The registry file's shape. Returns the rows, or a string naming what is wrong - a
41
- * malformed registry is an ERROR the human must fix (silently reading it as empty, or
42
- * flattening a broken nesting, would let the next drag overwrite their folders), while a
43
- * missing file is simply no folders. */
44
- function parseFolders(raw) {
45
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "expected an object";
46
- const { version, folders } = raw;
47
- if (version !== void 0 && version !== 1 && version !== 2) return `unsupported version ${String(version)}`;
48
- if (!Array.isArray(folders)) return "expected a \"folders\" array";
49
- const out = [];
50
- const seen = /* @__PURE__ */ new Set();
51
- for (const f of folders) {
52
- const name = f?.name;
53
- if (!isBoardName(name)) return "a folder needs a name - lowercase letters, numbers and dashes";
54
- if (seen.has(name)) return `folder "${name}" is listed twice`;
55
- seen.add(name);
56
- const o = f.order;
57
- const p = f.parent;
58
- if (p !== void 0 && !isBoardName(p)) return `folder "${name}" has an invalid parent - a folder name`;
59
- const t = readTitle(f.title);
60
- const d = readDescription(f.description);
61
- out.push({
62
- name,
63
- ...typeof o === "number" && Number.isFinite(o) ? { order: o } : {},
64
- ...p !== void 0 ? { parent: p } : {},
65
- ...t ? { title: t } : {},
66
- ...d ? { description: d } : {}
67
- });
68
- }
69
- const byName = new Map(out.map((f) => [f.name, f]));
70
- for (const f of out) {
71
- if (f.parent === void 0) continue;
72
- if (version !== 2) return `folder "${f.name}" names a parent - nested folders need "version": 2`;
73
- if (f.parent === f.name) return `folder "${f.name}" cannot be its own parent`;
74
- const p = byName.get(f.parent);
75
- if (!p) return `folder "${f.name}" names an unknown parent "${f.parent}"`;
76
- if (p.parent !== void 0) return `folder "${f.name}" would sit three levels deep - folders nest one level only`;
77
- }
78
- return out;
79
- }
80
- /** Sidebar order from the files. At every level the boards and folders there share one
81
- * sequence, ranked by `order`, then kind (board before folder), then name; unranked sorts
82
- * after ranked. The root holds root boards and top-level folders (registered without a
83
- * parent, or implied by a board that names an unregistered folder); a top-level folder holds
84
- * its boards and its sub-folders; a sub-folder holds its boards. A folder's title and
85
- * description ride on its item (they live in the registry a tree write rewrites); a board's
86
- * title stays with its row - it lives in the board's own file. */
87
- function buildTree(boards, folders) {
88
- const reg = /* @__PURE__ */ new Map();
89
- for (const f of folders) if (isBoardName(f.name) && !reg.has(f.name)) reg.set(f.name, f);
90
- const parentOf = (n) => {
91
- const p = reg.get(n)?.parent;
92
- return p !== void 0 && p !== n && reg.has(p) && reg.get(p).parent === void 0 ? p : void 0;
93
- };
94
- const members = /* @__PURE__ */ new Map();
95
- const implied = [];
96
- const rootBoards = [];
97
- for (const b of boards) {
98
- if (!isBoardName(b.name) || b.name === "all-scenes") continue;
99
- const folder = isBoardName(b.folder) ? b.folder : void 0;
100
- if (!folder) {
101
- rootBoards.push(b);
102
- continue;
103
- }
104
- if (!reg.has(folder) && !implied.includes(folder)) implied.push(folder);
105
- const list = members.get(folder) ?? [];
106
- list.push(b);
107
- members.set(folder, list);
108
- }
109
- const sorted = (xs) => xs.sort((a, b) => rank(a.order) - rank(b.order) || (a.item.kind === b.item.kind ? 0 : a.item.kind === "board" ? -1 : 1) || a.item.name.localeCompare(b.item.name)).map((r) => r.item);
110
- const boardsHere = (name) => (name === null ? rootBoards : members.get(name) ?? []).map((b) => ({
111
- item: {
112
- kind: "board",
113
- name: b.name
114
- },
115
- order: b.order
116
- }));
117
- const folder = (name, kids) => ({
118
- item: {
119
- kind: "folder",
120
- name,
121
- items: sorted(kids),
122
- ...folderExtras(reg.get(name) ?? {})
123
- },
124
- order: reg.get(name)?.order
125
- });
126
- const subsOf = (top) => [...reg.keys()].filter((n) => parentOf(n) === top);
127
- const tops = [...[...reg.keys()].filter((n) => !parentOf(n)), ...implied];
128
- return sorted([...boardsHere(null), ...tops.map((t) => folder(t, [...boardsHere(t), ...subsOf(t).map((s) => folder(s, boardsHere(s)))]))]);
129
- }
130
- /** Every board in reading order, depth-first - the order the switchers and the landing pick use. */
131
- function flatten(tree) {
132
- const out = [];
133
- const walk = (items) => {
134
- for (const it of items) if (it.kind === "board") out.push(it.name);
135
- else walk(it.items);
136
- };
137
- walk(tree);
138
- return out;
139
- }
140
- const wireKids = (w) => w.items ?? w.boards ?? [];
141
- function validateWire(wire) {
142
- if (!Array.isArray(wire)) return "invalid tree";
143
- const boards = /* @__PURE__ */ new Set(), folders = /* @__PURE__ */ new Set();
144
- const board = (n) => {
145
- if (!isBoardName(n) || n === "all-scenes") return "invalid board name in tree";
146
- if (boards.has(n)) return `board "${n}" appears twice`;
147
- boards.add(n);
148
- return null;
149
- };
150
- const walk = (list, depth) => {
151
- for (const w of list) {
152
- if (typeof w === "string") {
153
- const e = board(w);
154
- if (e) return e;
155
- continue;
156
- }
157
- if (!w || typeof w !== "object" || Array.isArray(w)) return "invalid tree item";
158
- if (depth >= 2) return "folders nest one level only";
159
- const { folder, items, boards: legacy, title, description } = w;
160
- if (!isBoardName(folder)) return "invalid folder name in tree";
161
- if (title !== void 0 && (typeof title !== "string" || Array.from(title).length > 120)) return "invalid folder title";
162
- if (description !== void 0 && (typeof description !== "string" || description.length > 300)) return "invalid folder description";
163
- if (folders.has(folder)) return `folder "${folder}" appears twice`;
164
- folders.add(folder);
165
- const kids = items ?? legacy;
166
- if (!Array.isArray(kids)) return "invalid folder in tree";
167
- if (items === void 0 && kids.some((k) => typeof k !== "string")) return "invalid folder in tree";
168
- const e = walk(kids, depth + 1);
169
- if (e) return e;
170
- }
171
- return null;
172
- };
173
- const e = walk(wire, 0);
174
- if (e) return e;
175
- if (boards.size > 200 || folders.size > 50) return "tree too large";
176
- return null;
177
- }
178
- /** Every folder with the folder it sits in (null = the root), top-level folders first in
179
- * reading order, each followed by its sub-folders. */
180
- function folderEntries(t) {
181
- const out = [];
182
- for (const it of t) {
183
- if (it.kind !== "folder") continue;
184
- out.push({
185
- folder: it,
186
- parent: null
187
- });
188
- for (const k of it.items) if (k.kind === "folder") out.push({
189
- folder: k,
190
- parent: it.name
191
- });
192
- }
193
- return out;
194
- }
195
- //#endregion
196
- //#region src/server/boards.ts
197
- /**
198
- * Reading design/boards/ safely - the one enumerator the dev API, the build and the tests
199
- * share. Only REGULAR files on the board-name grammar count as boards (a symlink, dangling or
200
- * live, could read or publish JSON from outside the project - it is skipped, and reported so
201
- * the build can fail closed). The folder registry is read the same way: absent = no folders,
202
- * malformed = an error the human must fix, never a silently empty registry.
203
- */
204
- /** Does realpath(dir) stay inside realpath(root)? A symlinked design/boards can't escape. */
205
- function underRoot(root, dir) {
206
- try {
207
- const rr = realpathSync(root);
208
- const rd = realpathSync(dir);
209
- return rd === rr || rd.startsWith(rr + sep);
210
- } catch {
211
- return false;
212
- }
213
- }
214
- /** Is design/boards a directory we may read and write? It must not be a symlink at all (a
215
- * link to the repo root would list package.json as a board and let a tree write rewrite
216
- * it; a link outside would publish foreign JSON) and must resolve inside the root. Absent
217
- * is fine (no boards yet). Returns the error, or null. */
218
- function checkBoardsDir(root, boardsDir) {
219
- return checkRealDirs(root, [[join(boardsDir, ".."), "design"], [boardsDir, "design/boards"]]);
220
- }
221
- /** Every EXISTING path in `dirs` (root-down order) must be a real directory inside the root -
222
- * a symlinked `design` with no boards dir yet would otherwise be followed by the mkdir that
223
- * creates it; a symlinked `design/scenes` would let a brief write land outside the project.
224
- * An absent one ends the walk (nothing beneath it exists either). Returns the error, or null. */
225
- function checkRealDirs(root, dirs) {
226
- for (const [p, label] of dirs) {
227
- try {
228
- if (lstatSync(p).isSymbolicLink()) return `${label} must be a real directory, not a symlink`;
229
- } catch {
230
- return null;
231
- }
232
- if (!underRoot(root, p)) return `${label} escapes the project`;
233
- }
234
- return null;
235
- }
236
- /** A regular file (lstat: a symlink is never followed, a dangling one is not "absent"). */
237
- const isRegularFile = (p) => {
238
- try {
239
- return lstatSync(p).isFile();
240
- } catch {
241
- return false;
242
- }
243
- };
244
- /** Is there ANY node at p (a dangling symlink counts)? */
245
- const nodeExists = (p) => {
246
- try {
247
- lstatSync(p);
248
- return true;
249
- } catch {
250
- return false;
251
- }
252
- };
253
- /** Every board file: name, raw content, hash, and its JSON (null when malformed). `skipped`
254
- * names the entries that looked like boards but were not regular files. */
255
- function listBoardFiles(boardsDir) {
256
- const boards = [], skipped = [];
257
- if (!existsSync(boardsDir)) return {
258
- boards,
259
- skipped
260
- };
261
- for (const f of readdirSync(boardsDir)) {
262
- if (!isBoardFile(f)) continue;
263
- const file = join(boardsDir, f);
264
- if (!isRegularFile(file)) {
265
- skipped.push(f);
266
- continue;
267
- }
268
- const content = readFileSync(file, "utf8");
269
- let json = null;
270
- try {
271
- json = JSON.parse(content);
272
- } catch {}
273
- boards.push({
274
- name: f.slice(0, -5),
275
- file,
276
- content,
277
- sha256: hash(content),
278
- json
279
- });
280
- }
281
- return {
282
- boards,
283
- skipped
284
- };
285
- }
286
- /** The author-owned sidebar fields off a board's JSON, leniently: rank, folder, the title
287
- * humans see, the sentence agents read. */
288
- function boardFields(json, validName) {
289
- const o = json;
290
- const title = readTitle(o?.title);
291
- const description = readDescription(o?.description);
292
- return {
293
- ...typeof o?.order === "number" && Number.isFinite(o.order) ? { order: o.order } : {},
294
- ...validName(o?.folder) ? { folder: o.folder } : {},
295
- ...title ? { title } : {},
296
- ...description ? { description } : {}
297
- };
298
- }
299
- /** The folder registry. `sha256` is the CAS token a tree write must echo (null = "there was
300
- * no file"), so a write can never silently replace a registry it never saw. */
301
- function readRegistry(boardsDir) {
302
- const p = join(boardsDir, FOLDERS_FILE);
303
- if (!nodeExists(p)) return {
304
- state: "absent",
305
- folders: [],
306
- sha256: null
307
- };
308
- if (!isRegularFile(p)) return {
309
- state: "malformed",
310
- error: `design/boards/${FOLDERS_FILE} must be a regular file, not a symlink`,
311
- sha256: null
312
- };
313
- const content = readFileSync(p, "utf8");
314
- let raw;
315
- try {
316
- raw = JSON.parse(content);
317
- } catch {
318
- return {
319
- state: "malformed",
320
- error: `design/boards/${FOLDERS_FILE} is not valid JSON - fix the file`,
321
- sha256: hash(content)
322
- };
323
- }
324
- const parsed = parseFolders(raw);
325
- if (typeof parsed === "string") return {
326
- state: "malformed",
327
- error: `design/boards/${FOLDERS_FILE}: ${parsed}`,
328
- sha256: hash(content)
329
- };
330
- return {
331
- state: "ok",
332
- folders: parsed,
333
- sha256: hash(content)
334
- };
335
- }
336
- //#endregion
337
- export { wireKids as _, listBoardFiles as a, BOARD_NAME as c, flatten as d, folderEntries as f, validateWire as g, readTitle as h, isRegularFile as i, FOLDERS_FILE as l, readDescription as m, checkBoardsDir as n, nodeExists as o, isBoardName as p, checkRealDirs as r, readRegistry as s, boardFields as t, buildTree as u, hash as v };
@@ -1,71 +0,0 @@
1
- import { a as listBoardFiles, d as flatten, n as checkBoardsDir, p as isBoardName, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BwiDAmPf.mjs";
2
- import { join } from "node:path";
3
- //#region src/cli/boards.ts
4
- /**
5
- * `marver boards` - the sidebar as the agent sees it: every folder and board in reading
6
- * order - sub-folders indented under their parent - from the files (no dev server needed). One
7
- * call answers "what folders exist, what is in them, what ranks where" before the agent writes `folder` on a board or edits
8
- * `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
9
- */
10
- function boardsCommand(root, opts) {
11
- const dir = join(root, "design", "boards");
12
- const de = checkBoardsDir(root, dir);
13
- if (de) throw new Error(de);
14
- const { boards, skipped } = listBoardFiles(dir);
15
- const reg = readRegistry(dir);
16
- if (reg.state === "malformed") throw new Error(reg.error);
17
- const rows = boards.map((b) => ({
18
- name: b.name,
19
- ...boardFields(b.json, isBoardName)
20
- }));
21
- const tree = buildTree(rows, reg.folders);
22
- const hasAll = boards.some((b) => b.name === "all-scenes");
23
- if (opts.json) {
24
- console.log(JSON.stringify({
25
- tree,
26
- boards: rows.filter((r) => r.name !== "all-scenes"),
27
- landing: flatten(tree)[0] ?? (hasAll ? "all-scenes" : null),
28
- registry: reg.state === "ok" ? "design/boards/_folders.json" : null
29
- }, null, 2));
30
- return;
31
- }
32
- if (!tree.length && !hasAll) {
33
- console.log("no boards yet - design/boards/ is empty");
34
- return;
35
- }
36
- const order = (n) => {
37
- const o = rows.find((r) => r.name === n)?.order;
38
- return o === void 0 ? "" : ` order ${o}`;
39
- };
40
- const title = (t) => t ? ` "${t}"` : "";
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
- };
46
- const count = (f) => {
47
- const boards = f.items.filter((k) => k.kind === "board").length, subs = f.items.length - boards;
48
- return `${boards} board${boards === 1 ? "" : "s"}${subs ? `, ${subs} folder${subs === 1 ? "" : "s"}` : ""}`;
49
- };
50
- const print = (items, pad, parent) => {
51
- for (const it of items) {
52
- if (it.kind === "board") {
53
- console.log(`${pad}${boardLine(it.name)}`);
54
- continue;
55
- }
56
- const where = parent ? `sub-folder of ${parent}` : "folder";
57
- const implied = reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json";
58
- console.log(`${pad}${it.name}/${title(it.title)} (${where}, ${count(it)}${implied})${desc(it.description)}`);
59
- print(it.items, `${pad} `, it.name);
60
- if (!it.items.length) console.log(`${pad} (empty)`);
61
- }
62
- };
63
- print(tree, "", null);
64
- if (hasAll) console.log("all-scenes (auto, always last)");
65
- const landing = flatten(tree)[0];
66
- if (landing) console.log(`\nlanding board: ${landing}`);
67
- console.log(`registry: ${reg.state === "ok" ? "design/boards/_folders.json" : "none (no empty or ranked folders yet)"}`);
68
- if (skipped.length) console.log(`skipped (not regular files): ${skipped.join(", ")}`);
69
- }
70
- //#endregion
71
- export { boardsCommand };