@marver-design/marver 0.21.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 +112 -15
- package/README.md +17 -4
- package/dist/{bake-BID6mo-N.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-C7MqQ7hq.mjs → build-Dd2Qm3OG.mjs} +74 -13
- package/dist/cli.mjs +67 -8
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-DUAENlJ6.mjs +600 -0
- package/dist/{daemon-CRZFpl6K.mjs → daemon-BmkwErpC.mjs} +1 -1
- package/dist/{dev-BNZF4Mup.mjs → dev-BW7a5kDF.mjs} +8 -6
- package/dist/{init-C34BY3R4.mjs → init-NJ3xjLVu.mjs} +39 -44
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-mMfUhPtL.mjs → manifest-BqBcMJcd.mjs} +19 -376
- package/dist/{plugin-omHLCn91.mjs → plugin-8XntSCx_.mjs} +234 -105
- package/dist/{poster-BvxiAzy1.mjs → poster-DRZDszTI.mjs} +1 -1
- package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-CCWRV9iX.mjs} +2 -2
- package/dist/{shot-DswS4iRK.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 +33 -3
- 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 +88 -9
- package/src/client/shell/styles.css +34 -3
- package/src/shared/board-tree.ts +30 -13
- 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 +1 -0
- package/templates/AGENTS-studio.md +1 -0
- 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 +31 -0
- 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-BwiDAmPf.mjs +0 -337
- package/dist/boards-DnLewfj8.mjs +0 -71
|
@@ -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 } : {},
|
|
@@ -365,10 +368,16 @@ function publishedTree(published, allBoards, folders) {
|
|
|
365
368
|
/** The bundle's manifest: the published frames, and descriptions of published things ONLY -
|
|
366
369
|
* the project's, the published scenes' (their brief path only when source is revealed),
|
|
367
370
|
* the published boards' and the folders those sit in. No unpublished name or sentence. */
|
|
368
|
-
function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
371
|
+
function publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus = /* @__PURE__ */ new Set()) {
|
|
369
372
|
const pubScenes = new Set(pubFrames.map((f) => f.scene));
|
|
370
373
|
const pubBoardSet = new Set(publishedNames);
|
|
371
|
-
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
|
+
});
|
|
372
381
|
const pubFolderSet = new Set(pubBoards.map((b) => b.folder).filter(Boolean));
|
|
373
382
|
for (const f of manifest.folders ?? []) if (f.parent && pubFolderSet.has(f.name)) pubFolderSet.add(f.parent);
|
|
374
383
|
const pubFolders = (manifest.folders ?? []).filter((f) => pubFolderSet.has(f.name));
|
|
@@ -387,6 +396,39 @@ function publishedManifest(manifest, pubFrames, publishedNames, strip) {
|
|
|
387
396
|
frames: pubFrames
|
|
388
397
|
};
|
|
389
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
|
+
}
|
|
390
432
|
/** The folder registry: absent = no folders (boards still imply theirs); malformed fails the build. */
|
|
391
433
|
function readFolders(root) {
|
|
392
434
|
const reg = readRegistry(join(root, "design", "boards"));
|
|
@@ -502,6 +544,17 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
502
544
|
});
|
|
503
545
|
const allBoards = readBoards(root);
|
|
504
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
|
+
}
|
|
505
558
|
const rights = Object.fromEntries(Object.entries(policy.boards).map(([n, p]) => [n, p.max]));
|
|
506
559
|
const strip = !policy.reveal.source;
|
|
507
560
|
const buildSalt = randomBytes(16).toString("hex");
|
|
@@ -509,7 +562,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
509
562
|
const { tree, names: publishedNames } = publishedTree(Object.keys(rights), allBoards, readFolders(root));
|
|
510
563
|
const includeAll = publishedNames.includes("all-scenes");
|
|
511
564
|
const boards = {};
|
|
512
|
-
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]);
|
|
513
566
|
let frames = manifest.frames;
|
|
514
567
|
if (!includeAll) {
|
|
515
568
|
const wanted = /* @__PURE__ */ new Set();
|
|
@@ -520,7 +573,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
520
573
|
...f,
|
|
521
574
|
file: opaquePath(f.id, f.kind === "html" ? ".html" : "")
|
|
522
575
|
})) : frames;
|
|
523
|
-
const
|
|
576
|
+
const showStatus = new Set(publishedNames.filter((n) => policy.boards[n]?.showStatus));
|
|
577
|
+
const pubManifest = publishedManifest(manifest, pubFrames, publishedNames, strip, showStatus);
|
|
524
578
|
for (const n of publishedNames) {
|
|
525
579
|
const pb = policy.boards[n];
|
|
526
580
|
if (pb?.type !== "slides" && pb?.open !== "slides") continue;
|
|
@@ -552,6 +606,10 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
552
606
|
const t = boardFields(allBoards[n], isBoardName).title;
|
|
553
607
|
return t ? [[n, t]] : [];
|
|
554
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
|
+
}]] : []));
|
|
555
613
|
const bakeGen = textures ? Date.now() : void 0;
|
|
556
614
|
const data = {
|
|
557
615
|
bakes: bakeGen,
|
|
@@ -560,6 +618,8 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
560
618
|
names: publishedNames,
|
|
561
619
|
tree,
|
|
562
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() } : {},
|
|
563
623
|
default: publishedNames.find((n) => n !== "all-scenes") ?? publishedNames[0],
|
|
564
624
|
rights,
|
|
565
625
|
policy: {
|
|
@@ -568,6 +628,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
568
628
|
...lockedShell ? { lockedShell: true } : {}
|
|
569
629
|
}
|
|
570
630
|
};
|
|
631
|
+
assertProjected(data, showStatus);
|
|
571
632
|
const registryFile = posix(join(clientDir, "frame-host", "registry.ts"));
|
|
572
633
|
const lockedFile = posix(join(clientDir, "shell", "LockedApp.tsx"));
|
|
573
634
|
const tsxFrames = frames.filter((f) => f.kind === "tsx");
|
|
@@ -693,7 +754,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
693
754
|
const realAssets = existsSync(assetsDir) ? realpathSync(assetsDir) : null;
|
|
694
755
|
for (const r of refs) {
|
|
695
756
|
if (!isLocalAssetRef(r) || !r.endsWith(".poster.png") || existsSync(join(assetsDir, r))) continue;
|
|
696
|
-
const { ensurePoster } = await import("./poster-
|
|
757
|
+
const { ensurePoster } = await import("./poster-DRZDszTI.mjs");
|
|
697
758
|
const g = await ensurePoster(assetsDir, r.slice(0, -11));
|
|
698
759
|
if (!g.ok) throw new Error(`design/assets/${r}: ${g.error}`);
|
|
699
760
|
console.log(` poster: rendered design/assets/${r} (${g.width}×${g.height})`);
|
|
@@ -760,7 +821,7 @@ async function buildSite(root, boardsFlag, allBoardsFlag, embedSeeds, textures =
|
|
|
760
821
|
}));
|
|
761
822
|
let textureLine = " textures: skipped (--no-textures)";
|
|
762
823
|
if (textures && bakeGen) {
|
|
763
|
-
const { bakePublished } = await import("./publish-bakes-
|
|
824
|
+
const { bakePublished } = await import("./publish-bakes-CCWRV9iX.mjs");
|
|
764
825
|
const pubFrameFile = new Map(pubFrames.map((f) => [f.id, f]));
|
|
765
826
|
const r = await bakePublished({
|
|
766
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);
|