@marver-design/marver 0.20.0 → 0.22.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 +123 -0
- package/README.md +17 -4
- package/dist/{bake-tAb0D6Rc.mjs → bake-BSX4XR_U.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BTGNPVMx.mjs +187 -0
- package/dist/{build-DwpNPl6Z.mjs → build-Dd2Qm3OG.mjs} +87 -16
- package/dist/cli.mjs +67 -8
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-DUAENlJ6.mjs +600 -0
- package/dist/{daemon-DbHvLQUL.mjs → daemon-BmkwErpC.mjs} +1 -1
- package/dist/{dev-BjdDP69b.mjs → dev-BW7a5kDF.mjs} +8 -6
- package/dist/{init-DKxuRxBr.mjs → init-NJ3xjLVu.mjs} +39 -44
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-B01PSyDc.mjs → manifest-BqBcMJcd.mjs} +26 -380
- package/dist/{plugin-y4Ch7o_A.mjs → plugin-8XntSCx_.mjs} +234 -97
- package/dist/{poster-FCv_nXyP.mjs → poster-DRZDszTI.mjs} +1 -1
- package/dist/{publish-bakes-XH6Bac58.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
- package/dist/{shot-iw3SEcpn.mjs → shot-C22Ues04.mjs} +2 -2
- package/docs/boards-and-folders.md +161 -0
- package/docs/context.md +117 -0
- package/docs/sharing.md +12 -2
- package/package.json +1 -1
- package/src/client/shell/BoardList.tsx +143 -54
- package/src/client/shell/ContextMenu.tsx +16 -5
- package/src/client/shell/StatusPicker.tsx +91 -0
- package/src/client/shell/board-icons.tsx +75 -0
- package/src/client/shell/store.ts +93 -14
- package/src/client/shell/styles.css +41 -9
- package/src/shared/board-tree.ts +291 -143
- package/src/shared/board-types.ts +103 -0
- package/src/shared/context.ts +265 -0
- package/src/shared/status.ts +157 -0
- package/templates/AGENTS-embedded.md +4 -3
- package/templates/AGENTS-studio.md +4 -3
- package/templates/context/INDEX.md +50 -0
- package/templates/context/map.json +6 -0
- package/templates/context/shipped-knowledge.md +19 -0
- package/templates/context/shipped.md +28 -0
- package/templates/instructions/boards.md +78 -22
- package/templates/instructions/context.md +114 -0
- package/templates/playbooks/publish-canvas/PLAYBOOK.md +72 -0
- package/templates/playbooks/reorganize-context/PLAYBOOK.md +232 -0
- package/templates/playbooks/reorganize-context/eval.md +93 -0
- package/dist/boards-BmxcT3Lc.mjs +0 -290
- package/dist/boards-PuVzw5Wp.mjs +0 -62
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { r as NAME } from "./cli.mjs";
|
|
2
|
+
import { $ as readCapability, B as folderMap, D as boardFields, E as addFolders, H as isBoardName, J as FOLDER_MODULES, L as buildTree, N as readRegistry, O as checkBoardsDir, Q as knownType, R as flatten, V as humanize, a as STATUS_LABEL, j as listBoardFiles, k as checkRealDirs, q as BOARD_TYPES, r as PHASE_LABEL, rt as resolveType, t as annotateBoards } from "./board-status-CWGHIdo_.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/cli/boards.ts
|
|
6
|
+
/**
|
|
7
|
+
* `marver boards` - the sidebar as the agent sees it: every folder and board in reading
|
|
8
|
+
* order - sub-folders indented under their parent - from the files (no dev server needed). One
|
|
9
|
+
* call answers "what folders exist, what is in them, what ranks where" before the agent writes `folder` on a board or edits
|
|
10
|
+
* `design/boards/_folders.json`. `--json` gives the tree shape the shell uses.
|
|
11
|
+
*/
|
|
12
|
+
function boardsCommand(root, opts) {
|
|
13
|
+
const dir = join(root, "design", "boards");
|
|
14
|
+
const de = checkBoardsDir(root, dir);
|
|
15
|
+
if (de) throw new Error(de);
|
|
16
|
+
const { boards, skipped } = listBoardFiles(dir);
|
|
17
|
+
const reg = readRegistry(dir);
|
|
18
|
+
if (reg.state === "malformed") throw new Error(reg.error);
|
|
19
|
+
const rows = boards.map((b) => ({
|
|
20
|
+
name: b.name,
|
|
21
|
+
...boardFields(b.json, isBoardName)
|
|
22
|
+
}));
|
|
23
|
+
const tree = buildTree(rows, reg.folders);
|
|
24
|
+
const hasAll = boards.some((b) => b.name === "all-scenes");
|
|
25
|
+
const fm = folderMap(tree);
|
|
26
|
+
const notes = annotateBoards(root, boards.filter((b) => b.name !== "all-scenes"), reg.folders, (n) => fm.get(n) ?? null);
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
const out = rows.filter((r) => r.name !== "all-scenes").map((r) => ({
|
|
29
|
+
...r,
|
|
30
|
+
...notes.get(r.name) ?? {}
|
|
31
|
+
}));
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
tree,
|
|
34
|
+
boards: out,
|
|
35
|
+
landing: flatten(tree)[0] ?? (hasAll ? "all-scenes" : null),
|
|
36
|
+
registry: reg.state === "ok" ? "design/boards/_folders.json" : null
|
|
37
|
+
}, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!tree.length && !hasAll) {
|
|
41
|
+
console.log("no boards yet - design/boards/ is empty");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const order = (n) => {
|
|
45
|
+
const o = rows.find((r) => r.name === n)?.order;
|
|
46
|
+
return o === void 0 ? "" : ` order ${o}`;
|
|
47
|
+
};
|
|
48
|
+
const title = (t) => t ? ` "${t}"` : "";
|
|
49
|
+
const desc = (d) => d ? ` - ${d}` : "";
|
|
50
|
+
const kind = (n) => {
|
|
51
|
+
const a = notes.get(n);
|
|
52
|
+
if (!a) return "";
|
|
53
|
+
const st = a.status ? ` · ${STATUS_LABEL[a.status.status]}${a.status.fill ? ` (${PHASE_LABEL[a.status.fill]})` : ""}${a.status.reason ? `: ${a.status.reason}` : ""}` : "";
|
|
54
|
+
return a.type === "plain" && !st ? "" : ` [${a.type}${st}]`;
|
|
55
|
+
};
|
|
56
|
+
const boardLine = (n) => {
|
|
57
|
+
const r = rows.find((x) => x.name === n);
|
|
58
|
+
return `${n}${title(r?.title)}${kind(n)}${order(n)}${desc(r?.description)}`;
|
|
59
|
+
};
|
|
60
|
+
const count = (f) => {
|
|
61
|
+
const boards = f.items.filter((k) => k.kind === "board").length, subs = f.items.length - boards;
|
|
62
|
+
return `${boards} board${boards === 1 ? "" : "s"}${subs ? `, ${subs} folder${subs === 1 ? "" : "s"}` : ""}`;
|
|
63
|
+
};
|
|
64
|
+
const print = (items, pad, parent) => {
|
|
65
|
+
for (const it of items) {
|
|
66
|
+
if (it.kind === "board") {
|
|
67
|
+
console.log(`${pad}${boardLine(it.name)}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const where = parent ? `sub-folder of ${parent}` : "folder";
|
|
71
|
+
const implied = reg.folders.some((f) => f.name === it.name) ? "" : ", implied by its boards - not in _folders.json";
|
|
72
|
+
console.log(`${pad}${it.name}/${title(it.title)} (${where}${it.type ? `, ${it.type}` : ""}, ${count(it)}${implied})${desc(it.description)}`);
|
|
73
|
+
print(it.items, `${pad} `, it.name);
|
|
74
|
+
if (!it.items.length) console.log(`${pad} (empty)`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
print(tree, "", null);
|
|
78
|
+
if (hasAll) console.log("all-scenes (auto, always last)");
|
|
79
|
+
const landing = flatten(tree)[0];
|
|
80
|
+
if (landing) console.log(`\nlanding board: ${landing}`);
|
|
81
|
+
console.log(`registry: ${reg.state === "ok" ? "design/boards/_folders.json" : "none (no empty or ranked folders yet)"}`);
|
|
82
|
+
if (skipped.length) console.log(`skipped (not regular files): ${skipped.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
/** `marver boards new <name>` - a board in its type's starting layout (spec 20): a feature's three
|
|
85
|
+
* phase scenes (spec, lo-fi, hi-fi) as three bands, a start board rendering context/INDEX.md and
|
|
86
|
+
* context/shipped.md, a deck's title slide. The type is `--type`, else the folder's. Never
|
|
87
|
+
* overwrites a board, a scene brief or a frame that exists. */
|
|
88
|
+
function boardsNew(root, name, opts) {
|
|
89
|
+
if (!isBoardName(name) || name === "all-scenes") throw new Error(`"${name}" is not a board name - lowercase letters, numbers and dashes`);
|
|
90
|
+
const dir = join(root, "design", "boards");
|
|
91
|
+
const de = checkBoardsDir(root, dir);
|
|
92
|
+
if (de) throw new Error(de);
|
|
93
|
+
const file = join(dir, `${name}.json`);
|
|
94
|
+
if (existsSync(file)) throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
|
|
95
|
+
if (opts.type !== void 0 && !knownType(opts.type)) throw new Error(`--type ${opts.type}: use ${BOARD_TYPES.join(", ")}`);
|
|
96
|
+
if (opts.folder !== void 0 && !isBoardName(opts.folder)) throw new Error(`--folder ${opts.folder} is not a folder name`);
|
|
97
|
+
if (opts.capability !== void 0 && !readCapability(opts.capability)) throw new Error(`--capability ${opts.capability} is not a capability slug`);
|
|
98
|
+
const reg = readRegistry(dir);
|
|
99
|
+
if (reg.state === "malformed") throw new Error(reg.error);
|
|
100
|
+
const folder = opts.folder ? reg.state === "ok" ? reg.folders.find((f) => f.name === opts.folder) : void 0 : void 0;
|
|
101
|
+
const parent = folder?.parent && reg.state === "ok" ? reg.folders.find((f) => f.name === folder.parent) : void 0;
|
|
102
|
+
const type = resolveType(opts.type, folder?.type, parent?.type);
|
|
103
|
+
const label = opts.title ?? humanize(name);
|
|
104
|
+
const created = [];
|
|
105
|
+
const put = (rel, body) => {
|
|
106
|
+
const f = join(root, rel);
|
|
107
|
+
const scene = rel.split("/")[2];
|
|
108
|
+
const bad = checkRealDirs(root, [
|
|
109
|
+
[join(root, "design"), "design"],
|
|
110
|
+
[join(root, "design", "scenes"), "design/scenes"],
|
|
111
|
+
[join(root, "design", "scenes", scene), `design/scenes/${scene}`]
|
|
112
|
+
]);
|
|
113
|
+
if (bad) throw new Error(bad);
|
|
114
|
+
mkdirSync(join(f, ".."), { recursive: true });
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(f, body, { flag: "wx" });
|
|
117
|
+
created.push(rel);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (e.code !== "EEXIST") throw e;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const brief = (scene, title, body, phase) => put(`design/scenes/${scene}/_brief.md`, `---\ntitle: ${title}\n${phase ? `phase: ${phase}\n` : ""}---\n${body}\n`);
|
|
123
|
+
let nodes = [];
|
|
124
|
+
let layout;
|
|
125
|
+
if (type === "feature") {
|
|
126
|
+
const cap = opts.capability ?? name;
|
|
127
|
+
const [specs, lofi, hifi] = [
|
|
128
|
+
`${name}-specs`,
|
|
129
|
+
`${name}-lofi`,
|
|
130
|
+
name
|
|
131
|
+
];
|
|
132
|
+
brief(specs, `${label} - spec`, `${label}: what it does, for whom, and its limits - the thinking before the screens. Its contract is context/product/${cap}.md once accepted.`, "spec");
|
|
133
|
+
brief(lofi, `${label} - lo-fi`, `${label} in throwaway wireframes: structure and copy, no styling.`, "lofi");
|
|
134
|
+
brief(hifi, label, `${label} in hi-fi, from the product's real components.`, "hifi");
|
|
135
|
+
layout = { rows: [
|
|
136
|
+
[specs],
|
|
137
|
+
{ space: 3 },
|
|
138
|
+
[lofi],
|
|
139
|
+
{ space: 4 },
|
|
140
|
+
[hifi]
|
|
141
|
+
] };
|
|
142
|
+
} else if (type === "start") {
|
|
143
|
+
const strip = "(md: string) => md.replace(/^<!--[^\\n]*-->\\n/, '').replace(/^---\\n[\\s\\S]*?\\n---\\n/, '')";
|
|
144
|
+
const frame = (file, title, src, desc) => put(`design/scenes/${name}/${file}.tsx`, `// Renders ${src} - the file is the truth; this frame only shows it.\nimport { Doc, Md } from '@marver-design/marver/content'\nimport text from '../../../${src}?raw'\n\nexport const meta = { title: '${title}', intent: 'spec', description: '${desc}' }\n\nconst body = ${strip}\n\nexport default () => (\n <Doc>\n <Md>{body(text)}</Md>\n </Doc>\n)\n`);
|
|
145
|
+
brief(name, label, "The way in: the index and the shipped record, rendered from context/.");
|
|
146
|
+
frame("index", "The index", "context/INDEX.md", "context/INDEX.md, rendered - where every question is answered");
|
|
147
|
+
frame("shipped", "Shipped", "context/shipped.md", "context/shipped.md, rendered - what runs where, with its evidence");
|
|
148
|
+
nodes = [{ frame: `${name}/index` }, { frame: `${name}/shipped` }];
|
|
149
|
+
layout = { rows: [[name]] };
|
|
150
|
+
} else if (type === "deck") {
|
|
151
|
+
brief(name, label, `${label}: the deck. Read design/instructions/slides.md before the first slide.`);
|
|
152
|
+
put(`design/scenes/${name}/01-title.tsx`, `export const meta = { title: '${label.replace(/'/g, "\\'")}', slide: true }\n\nexport default function Frame() {\n return (\n <main style={{ minHeight: '100vh', display: 'grid', placeItems: 'center', padding: 96 }}>\n <h1 style={{ fontSize: 72, margin: 0 }}>${label.replace(/[<>{}]/g, "")}</h1>\n </main>\n )\n}\n`);
|
|
153
|
+
nodes = [{ frame: `${name}/01-title` }];
|
|
154
|
+
layout = { rows: [[name]] };
|
|
155
|
+
}
|
|
156
|
+
const board = {
|
|
157
|
+
version: 1,
|
|
158
|
+
name,
|
|
159
|
+
auto: false,
|
|
160
|
+
...opts.type ? { type: opts.type } : {},
|
|
161
|
+
...opts.folder ? { folder: opts.folder } : {},
|
|
162
|
+
...opts.title ? { title: opts.title } : {},
|
|
163
|
+
...opts.description ? { description: opts.description } : {},
|
|
164
|
+
...opts.capability ? { capability: opts.capability } : {},
|
|
165
|
+
nodes,
|
|
166
|
+
...layout ? { layout } : {}
|
|
167
|
+
};
|
|
168
|
+
mkdirSync(dir, { recursive: true });
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(file, JSON.stringify(board, null, 2) + "\n", { flag: "wx" });
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e.code === "EEXIST") throw new Error(`design/boards/${name}.json exists - a board is never overwritten`);
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
created.unshift(`design/boards/${name}.json (${type}${opts.type ? "" : type === "plain" ? "" : `, from ${opts.folder}`})`);
|
|
176
|
+
return created;
|
|
177
|
+
}
|
|
178
|
+
/** `marver folders add <module...>` - the shared sidebar's typed folders (spec 20): start, features,
|
|
179
|
+
* surfaces, projects, feedback, context, decks, archive. Appended; never renames or moves one. */
|
|
180
|
+
function foldersAdd(root, modules) {
|
|
181
|
+
const unknown = modules.filter((m) => !FOLDER_MODULES[m]);
|
|
182
|
+
if (unknown.length) throw new Error(`unknown folder module: ${unknown.join(", ")} - use ${Object.keys(FOLDER_MODULES).join(", ")}`);
|
|
183
|
+
if (!modules.length) throw new Error(`name a module: ${Object.keys(FOLDER_MODULES).join(", ")} (e.g. \`npx ${NAME} folders add decks\`)`);
|
|
184
|
+
return addFolders(root, modules.map((m) => FOLDER_MODULES[m]));
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { boardsCommand, boardsNew, foldersAdd };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { a as ROUTE, r as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { D as boardFields, H as isBoardName, L as buildTree, N as readRegistry, O as checkBoardsDir, R as flatten, Z as PROPOSED_PUBLISH, i as PUBLISHABLE, j as listBoardFiles, o as publishableStatus } from "./board-status-CWGHIdo_.mjs";
|
|
3
|
+
import { r as scanFrames } from "./manifest-BqBcMJcd.mjs";
|
|
4
|
+
import { i as detectHost, n as loadConfig } from "./config-DJxMRVD8.mjs";
|
|
5
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-8XntSCx_.mjs";
|
|
5
6
|
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { basename, dirname, join, sep } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -258,7 +259,7 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
|
|
|
258
259
|
structure: true,
|
|
259
260
|
source: false
|
|
260
261
|
};
|
|
261
|
-
const row = (max) => ({
|
|
262
|
+
const row = (max, _n) => ({
|
|
262
263
|
max,
|
|
263
264
|
type: "mix"
|
|
264
265
|
});
|
|
@@ -268,12 +269,12 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
|
|
|
268
269
|
const missing = names.filter((n) => !known(n));
|
|
269
270
|
if (missing.length) throw new Error(`--boards names not found in design/boards/: ${missing.join(", ")}`);
|
|
270
271
|
return {
|
|
271
|
-
boards: Object.fromEntries(names.map((n) => [n, row("comment")])),
|
|
272
|
+
boards: Object.fromEntries(names.map((n) => [n, row("comment", n)])),
|
|
272
273
|
reveal
|
|
273
274
|
};
|
|
274
275
|
}
|
|
275
276
|
if (allBoardsFlag) return {
|
|
276
|
-
boards: Object.fromEntries(["all-scenes", ...Object.keys(allBoards).filter((n) => n !== "all-scenes")].map((n) => [n, row("comment")])),
|
|
277
|
+
boards: Object.fromEntries(["all-scenes", ...Object.keys(allBoards).filter((n) => n !== "all-scenes")].map((n) => [n, row("comment", n)])),
|
|
277
278
|
reveal
|
|
278
279
|
};
|
|
279
280
|
const policyFile = join(root, "design", "publish.json");
|
|
@@ -290,13 +291,14 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
|
|
|
290
291
|
for (const [n, level] of entries) {
|
|
291
292
|
if (!known(n)) throw new Error(`design/publish.json names an unknown board: ${n}`);
|
|
292
293
|
if (level === "read" || level === "comment") {
|
|
293
|
-
out[n] = row(level);
|
|
294
|
+
out[n] = row(level, n);
|
|
294
295
|
continue;
|
|
295
296
|
}
|
|
296
297
|
if (typeof level !== "object" || level === null) throw new Error(`design/publish.json: board "${n}" has level "${level}" - use "read" or "comment", or a v2 object`);
|
|
297
298
|
const p = level;
|
|
298
299
|
if (p.max !== "read" && p.max !== "comment") throw new Error(`design/publish.json: board "${n}" needs "max": "read" | "comment"`);
|
|
299
300
|
const type = p.type ?? "mix";
|
|
301
|
+
if (p.showStatus !== void 0 && typeof p.showStatus !== "boolean") throw new Error(`design/publish.json: board "${n}" has showStatus "${p.showStatus}" - use true or false`);
|
|
300
302
|
if (!ARTIFACT_TYPES.includes(type)) throw new Error(`design/publish.json: board "${n}" has type "${type}" - use ${ARTIFACT_TYPES.join(" | ")}`);
|
|
301
303
|
if (p.open !== void 0 && !VIEW_MODES.includes(p.open)) throw new Error(`design/publish.json: board "${n}" has open "${p.open}" - use ${VIEW_MODES.join(" | ")}`);
|
|
302
304
|
if (p.transition !== void 0 && !DECK_TRANSITIONS.includes(p.transition)) throw new Error(`design/publish.json: board "${n}" has transition "${p.transition}" - use ${DECK_TRANSITIONS.join(" | ")}`);
|
|
@@ -306,6 +308,7 @@ function resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag) {
|
|
|
306
308
|
out[n] = {
|
|
307
309
|
max: p.max,
|
|
308
310
|
type,
|
|
311
|
+
...p.showStatus === true ? { showStatus: true } : {},
|
|
309
312
|
...p.open ? { open: p.open } : {},
|
|
310
313
|
...p.lock ? { lock: true } : {},
|
|
311
314
|
...p.transition ? { transition: p.transition } : {},
|
|
@@ -339,15 +342,24 @@ function readBoards(root) {
|
|
|
339
342
|
return out;
|
|
340
343
|
}
|
|
341
344
|
/** Switcher order = the sidebar's reading order: the folder tree over the PUBLISHED boards only
|
|
342
|
-
* (a folder left with no published board drops out - its name never reaches the bundle),
|
|
345
|
+
* (a folder left with no published board at any depth drops out - its name never reaches the bundle),
|
|
343
346
|
* flattened depth-first; all-scenes always LAST (it is the expensive everything-board, never
|
|
344
347
|
* the landing). `names[0]` is where `/` opens. Folder names of published boards are structure,
|
|
345
348
|
* like board names: they ship. */
|
|
346
349
|
function publishedTree(published, allBoards, folders) {
|
|
347
|
-
const
|
|
350
|
+
const built = buildTree(published.filter((n) => n !== "all-scenes").map((n) => ({
|
|
348
351
|
name: n,
|
|
349
352
|
...boardFields(allBoards[n], isBoardName)
|
|
350
|
-
})), folders)
|
|
353
|
+
})), folders);
|
|
354
|
+
const prune = (items) => items.flatMap((it) => {
|
|
355
|
+
if (it.kind === "board") return [it];
|
|
356
|
+
const kids = prune(it.items);
|
|
357
|
+
return kids.length ? [{
|
|
358
|
+
...it,
|
|
359
|
+
items: kids
|
|
360
|
+
}] : [];
|
|
361
|
+
});
|
|
362
|
+
const tree = prune(built);
|
|
351
363
|
return {
|
|
352
364
|
tree,
|
|
353
365
|
names: [...flatten(tree), ...published.includes("all-scenes") ? ["all-scenes"] : []]
|
|
@@ -356,11 +368,18 @@ function publishedTree(published, allBoards, folders) {
|
|
|
356
368
|
/** The bundle's manifest: the published frames, and descriptions of published things ONLY -
|
|
357
369
|
* the project's, the published scenes' (their brief path only when source is revealed),
|
|
358
370
|
* the published boards' and the folders those sit in. No unpublished name or sentence. */
|
|
359
|
-
function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
371
|
+
function publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus = /* @__PURE__ */ new Set()) {
|
|
360
372
|
const pubScenes = new Set(pubFrames.map((f) => f.scene));
|
|
361
373
|
const pubBoardSet = new Set(publishedNames);
|
|
362
|
-
const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name))
|
|
374
|
+
const pubBoards = (manifest.boards ?? []).filter((b) => pubBoardSet.has(b.name)).map(({ status, ...b }) => {
|
|
375
|
+
const shown = showStatus.has(b.name) ? publishableStatus(status) : null;
|
|
376
|
+
return shown ? {
|
|
377
|
+
...b,
|
|
378
|
+
status: shown
|
|
379
|
+
} : b;
|
|
380
|
+
});
|
|
363
381
|
const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
|
|
382
|
+
for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
|
|
364
383
|
const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
|
|
365
384
|
return {
|
|
366
385
|
...manifest.project ? { project: manifest.project } : {},
|
|
@@ -377,6 +396,39 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
|
377
396
|
frames: pubFrames
|
|
378
397
|
};
|
|
379
398
|
}
|
|
399
|
+
/** What a published status may carry, and a published board's meta - nothing else (spec 20). */
|
|
400
|
+
const STATUS_KEYS = /* @__PURE__ */ new Set(["status", "fill"]);
|
|
401
|
+
const META_KEYS = /* @__PURE__ */ new Set(["type", "status"]);
|
|
402
|
+
/** The fields of a board file that point into context/ - stripped from every published board. */
|
|
403
|
+
const EVIDENCE_FIELDS = [
|
|
404
|
+
"status",
|
|
405
|
+
"reason",
|
|
406
|
+
"capability"
|
|
407
|
+
];
|
|
408
|
+
function withoutEvidence(board) {
|
|
409
|
+
if (!board || typeof board !== "object" || Array.isArray(board)) return board;
|
|
410
|
+
const out = { ...board };
|
|
411
|
+
for (const k of EVIDENCE_FIELDS) delete out[k];
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
/** The build's own check of the projection: if a stripped field survives into the bundle, the
|
|
415
|
+
* build fails - a later edit to the assembly must never quietly ship the evidence trail. */
|
|
416
|
+
function assertProjected(data, showStatus) {
|
|
417
|
+
const statusOk = (n, st) => {
|
|
418
|
+
if (!st || typeof st !== "object" || Array.isArray(st)) return false;
|
|
419
|
+
const o = st;
|
|
420
|
+
return showStatus.has(n) && PUBLISHABLE.has(o.status) && Object.keys(o).every((k) => STATUS_KEYS.has(k));
|
|
421
|
+
};
|
|
422
|
+
for (const [n, b] of Object.entries(data.boards)) for (const k of EVIDENCE_FIELDS) if (b && typeof b === "object" && k in b) throw new Error(`build: board "${n}" would ship its "${k}" - the publication projection failed`);
|
|
423
|
+
for (const b of data.manifest.boards ?? []) {
|
|
424
|
+
const st = b.status;
|
|
425
|
+
if (st !== void 0 && !statusOk(b.name, st)) throw new Error(`build: board "${b.name}" would ship a status its publish row did not allow`);
|
|
426
|
+
}
|
|
427
|
+
for (const [n, m] of Object.entries(data.meta ?? {})) {
|
|
428
|
+
if (Object.keys(m).some((k) => !META_KEYS.has(k))) throw new Error(`build: board "${n}" would ship meta beyond its type and status`);
|
|
429
|
+
if (m.status !== void 0 && !statusOk(n, m.status)) throw new Error(`build: board "${n}" would ship a status its publish row did not allow`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
380
432
|
/** The folder registry: absent = no folders (boards still imply theirs); malformed fails the build. */
|
|
381
433
|
function readFolders(root) {
|
|
382
434
|
const reg = readRegistry(join(root, "design", "boards"));
|
|
@@ -492,6 +544,17 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
492
544
|
});
|
|
493
545
|
const allBoards = readBoards(root);
|
|
494
546
|
const policy = resolvePolicy(root, allBoards, boardsFlag, allBoardsFlag);
|
|
547
|
+
let named = {};
|
|
548
|
+
try {
|
|
549
|
+
named = JSON.parse(readFileSync(join(root, "design", "publish.json"), "utf8")).boards ?? {};
|
|
550
|
+
} catch {}
|
|
551
|
+
for (const [n, pb] of Object.entries(policy.boards)) {
|
|
552
|
+
const row = named[n];
|
|
553
|
+
if (row && typeof row === "object" && row.type !== void 0) continue;
|
|
554
|
+
const bt = manifest.boards?.find((b) => b.name === n)?.type;
|
|
555
|
+
const suggested = bt ? PROPOSED_PUBLISH[bt] : void 0;
|
|
556
|
+
if (suggested && suggested !== pb.type) console.log(` note: board "${n}" is a ${bt} board - its publish row could say "type": "${suggested}" (it publishes as "${pb.type}" until it does)`);
|
|
557
|
+
}
|
|
495
558
|
const rights = Object.fromEntries(Object.entries(policy.boards).map(([n, p]) => [n, p.max]));
|
|
496
559
|
const strip = !policy.reveal.source;
|
|
497
560
|
const buildSalt = randomBytes(16).toString("hex");
|
|
@@ -499,7 +562,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
499
562
|
const { tree, names: publishedNames } = publishedTree(Object.keys(rights), allBoards, readFolders(root));
|
|
500
563
|
const includeAll = publishedNames.includes("all-scenes");
|
|
501
564
|
const boards = {};
|
|
502
|
-
for (const n of publishedNames) if (allBoards[n]) boards[n] = allBoards[n];
|
|
565
|
+
for (const n of publishedNames) if (allBoards[n]) boards[n] = withoutEvidence(allBoards[n]);
|
|
503
566
|
let frames = manifest.frames;
|
|
504
567
|
if (!includeAll) {
|
|
505
568
|
const wanted = /* @__PURE__ */ new Set();
|
|
@@ -510,7 +573,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
510
573
|
...f,
|
|
511
574
|
file: opaquePath(f.id, f.kind === "html" ? ".html" : "")
|
|
512
575
|
})) : frames;
|
|
513
|
-
const
|
|
576
|
+
const showStatus = new Set(publishedNames.filter((n) => policy.boards[n]?.showStatus));
|
|
577
|
+
const pubManifest = publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus);
|
|
514
578
|
for (const n of publishedNames) {
|
|
515
579
|
const pb = policy.boards[n];
|
|
516
580
|
if (pb?.type !== "slides" && pb?.open !== "slides") continue;
|
|
@@ -542,6 +606,10 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
542
606
|
const t = boardFields(allBoards[n], isBoardName).title;
|
|
543
607
|
return t ? [[n, t]] : [];
|
|
544
608
|
}));
|
|
609
|
+
const meta = Object.fromEntries((pubManifest.boards ?? []).flatMap((b) => b.type || b.status ? [[b.name, {
|
|
610
|
+
...b.type ? { type: b.type } : {},
|
|
611
|
+
...b.status ? { status: b.status } : {}
|
|
612
|
+
}]] : []));
|
|
545
613
|
const bakeGen = textures ? Date.now() : void 0;
|
|
546
614
|
const data = {
|
|
547
615
|
bakes: bakeGen,
|
|
@@ -550,6 +618,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
550
618
|
names: publishedNames,
|
|
551
619
|
tree,
|
|
552
620
|
...Object.keys(titles).length ? { titles } : {},
|
|
621
|
+
...Object.keys(meta).length ? { meta } : {},
|
|
622
|
+
...Object.values(meta).some((m) => m.status) ? { statusAsOf: (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
553
623
|
default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
|
|
554
624
|
rights,
|
|
555
625
|
policy: {
|
|
@@ -558,6 +628,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
558
628
|
...lockedShell ? { lockedShell: true } : {}
|
|
559
629
|
}
|
|
560
630
|
};
|
|
631
|
+
assertProjected(data, showStatus);
|
|
561
632
|
const registryFile = posix(join(clientDir, "frame-host", "registry.ts"));
|
|
562
633
|
const lockedFile = posix(join(clientDir, "shell", "LockedApp.tsx"));
|
|
563
634
|
const tsxFrames = frames.filter((f) => f.kind === "tsx");
|
|
@@ -683,7 +754,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
683
754
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
684
755
|
for (const r of refs) {
|
|
685
756
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
686
|
-
const { ensurePoster } = await import("./poster-
|
|
757
|
+
const { ensurePoster } = await import("./poster-DRZDszTI.mjs");
|
|
687
758
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
688
759
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
689
760
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
@@ -750,7 +821,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
750
821
|
}));
|
|
751
822
|
let textureLine = " textures: skipped (--no-textures)";
|
|
752
823
|
if (textures && bakeGen) {
|
|
753
|
-
const { bakePublished } = await import("./publish-bakes-
|
|
824
|
+
const { bakePublished } = await import("./publish-bakes-CCWRV9iX.mjs");
|
|
754
825
|
const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
|
|
755
826
|
const r = await bakePublished({
|
|
756
827
|
root,
|
package/dist/cli.mjs
CHANGED
|
@@ -77,15 +77,20 @@ const resolve$1 = (dir) => {
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
const cli = cac(NAME);
|
|
80
|
-
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) => {
|
|
81
|
-
|
|
80
|
+
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("--kind <kind>", "product | knowledge - the sidebar's typed folders (a fresh canvas: product when an app is detected)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
81
|
+
if (opts.kind !== void 0 && opts.kind !== "product" && opts.kind !== "knowledge") {
|
|
82
|
+
console.error(`[${NAME}] --kind ${opts.kind}: use product or knowledge`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const { init } = await import("./init-NJ3xjLVu.mjs");
|
|
82
86
|
init(resolve$1(opts.root), {
|
|
83
87
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
84
|
-
demo: opts.demo !== false
|
|
88
|
+
demo: opts.demo !== false,
|
|
89
|
+
kind: opts.kind
|
|
85
90
|
});
|
|
86
91
|
});
|
|
87
92
|
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) => {
|
|
88
|
-
const { dev } = await import("./dev-
|
|
93
|
+
const { dev } = await import("./dev-BW7a5kDF.mjs");
|
|
89
94
|
let port;
|
|
90
95
|
if (opts.port !== void 0) {
|
|
91
96
|
const n = Number(opts.port);
|
|
@@ -95,7 +100,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
95
100
|
await dev(resolve$1(opts.root), port);
|
|
96
101
|
});
|
|
97
102
|
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("--no-textures", "Skip compiling the glass textures the published frames rest under (needs Chrome; a CI without one skips on its own)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
98
|
-
const { buildSite } = await import("./build-
|
|
103
|
+
const { buildSite } = await import("./build-Dd2Qm3OG.mjs");
|
|
99
104
|
try {
|
|
100
105
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
101
106
|
await buildSite(resolve$1(opts.root), boards, opts.allBoards === true, opts.embedSeeds === true, opts.textures !== false && !process.env.MARVER_NO_TEXTURES);
|
|
@@ -140,10 +145,64 @@ cli.command("work <action> [...frames]", "Working state on the canvas: start <sc
|
|
|
140
145
|
process.exit(1);
|
|
141
146
|
}
|
|
142
147
|
});
|
|
143
|
-
cli.command("boards", "The sidebar as files: every folder and board in reading order,
|
|
144
|
-
const { boardsCommand } = await import("./boards-
|
|
148
|
+
cli.command("boards [action] [name]", "The sidebar as files: every folder and board in reading order, with types and statuses · new <name>: a board in its type's starting layout").option("--root <dir>", "Host repo root", { default: "." }).option("--json", "The tree as JSON ({ tree, boards, landing, registry })").option("--type <type>", "new: start | feature | surface | project | feedback | context | deck | archive (default: the folder's)").option("--folder <folder>", "new: the folder it sits in").option("--title <title>", "new: what humans see").option("--description <sentence>", "new: what agents read").option("--capability <slug>", "new: the capability it shows, when not its own name").action(async (action, name, opts) => {
|
|
149
|
+
const { boardsCommand, boardsNew } = await import("./boards-BTGNPVMx.mjs");
|
|
150
|
+
try {
|
|
151
|
+
if (action === void 0) boardsCommand(resolve$1(opts.root), opts);
|
|
152
|
+
else if (action === "new") {
|
|
153
|
+
if (!name) throw new Error("name the board: `boards new <name>`");
|
|
154
|
+
const made = boardsNew(resolve$1(opts.root), name, opts);
|
|
155
|
+
for (const c of made) console.log(` + ${c}`);
|
|
156
|
+
if (made.some((c) => c.endsWith("/_brief.md")) && !made.some((c) => /\.(tsx|jsx|html)$/.test(c))) console.log(`\n next: a frame you make in these scenes shows on the board once it is pinned - a node in\n design/boards/${name}.json (an agent does this as it works: design/AGENTS.md)`);
|
|
157
|
+
} else throw new Error(`unknown action "${action}" - \`boards\` lists, \`boards new <name>\` creates`);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`[${NAME}] ${err.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
cli.command("folders <action> [...modules]", "The shared sidebar's typed folders · add <start|features|surfaces|projects|feedback|context|decks|archive ...>").option("--root <dir>", "Host repo root", { default: "." }).action(async (action, modules, opts) => {
|
|
164
|
+
const { foldersAdd } = await import("./boards-BTGNPVMx.mjs");
|
|
165
|
+
try {
|
|
166
|
+
if (action !== "add") throw new Error(`unknown action "${action}" - \`folders add <module ...>\``);
|
|
167
|
+
const { added, existing } = foldersAdd(resolve$1(opts.root), modules);
|
|
168
|
+
for (const a of added) console.log(` + design/boards/_folders.json: ${a}`);
|
|
169
|
+
for (const e of existing) console.log(` = ${e} exists - left as it is`);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(`[${NAME}] ${err.message}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
cli.command("context <action>", "The project's context/ (spec 19) · init: create it · check: keep it true (exit 0 pass, 1 fail, 2 cannot determine) · index: regenerate the index's capability table").option("--root <dir>", "Host repo root", { default: "." }).option("--base <ref>", "check: the pull request's base - a change to a contracted capability's code must change its contract or say why (in ci, read from the event)").option("--head <ref>", "check: the pull request's head (default HEAD)").option("--body-file <file>", "check: the pull request's body, for `no-contract-change: <capability> - <why>`").option("--json", "check: the findings as JSON ({ exit, failures, unsure, notes })").option("--kind <kind>", "init: product (a shipped record) or knowledge (a delivered record)", { default: "product" }).action(async (action, opts) => {
|
|
176
|
+
const ctx = await import("./context-DUAENlJ6.mjs");
|
|
177
|
+
const root = resolve$1(opts.root);
|
|
145
178
|
try {
|
|
146
|
-
|
|
179
|
+
if (action === "init") {
|
|
180
|
+
if (opts.kind !== "product" && opts.kind !== "knowledge") throw new Error(`--kind ${opts.kind}: use product or knowledge`);
|
|
181
|
+
const created = ctx.contextInit(root, opts.kind);
|
|
182
|
+
for (const c of created) console.log(` + ${c}`);
|
|
183
|
+
if (!created.length) console.log(" context/ is already set up - nothing to add");
|
|
184
|
+
console.log(`\n next: tell your agent "Read ${ctx.conventionsPath(root)} and set up our context" - the setup interview, then a first draft from evidence.\n in ci: npx ${NAME} context check (with full history: fetch-depth: 0)\n`);
|
|
185
|
+
} else if (action === "check") {
|
|
186
|
+
const { readFileSync } = await import("node:fs");
|
|
187
|
+
const fromEnv = ctx.pullRequestFromEnv() ?? {};
|
|
188
|
+
const given = {
|
|
189
|
+
...opts.base !== void 0 ? { base: String(opts.base) } : {},
|
|
190
|
+
...opts.head !== void 0 ? { head: String(opts.head) } : {},
|
|
191
|
+
...opts.bodyFile !== void 0 ? { body: readFileSync(opts.bodyFile, "utf8") } : {}
|
|
192
|
+
};
|
|
193
|
+
const merged = {
|
|
194
|
+
...fromEnv,
|
|
195
|
+
...given
|
|
196
|
+
};
|
|
197
|
+
if (given.base !== void 0) delete merged.prError;
|
|
198
|
+
const r = ctx.contextCheck(root, merged);
|
|
199
|
+
if (opts.json) console.log(JSON.stringify(r, null, 2));
|
|
200
|
+
else ctx.printCheck(r);
|
|
201
|
+
process.exit(r.exit);
|
|
202
|
+
} else if (action === "index") {
|
|
203
|
+
const { words, changed } = ctx.contextIndex(root);
|
|
204
|
+
console.log(` context/INDEX.md: ${changed ? "capability table regenerated" : "already current"} - ${words} / ${ctx.INDEX_BUDGET} words`);
|
|
205
|
+
} else throw new Error(`unknown action "${action}" - init, check or index`);
|
|
147
206
|
} catch (err) {
|
|
148
207
|
console.error(`[${NAME}] ${err.message}`);
|
|
149
208
|
process.exit(1);
|