@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.
- package/CHANGELOG.md +134 -15
- package/README.md +17 -4
- package/dist/{bake-BID6mo-N.mjs → bake-CvY8QR3L.mjs} +1 -1
- package/dist/board-status-CWGHIdo_.mjs +1307 -0
- package/dist/boards-BwKI0qWd.mjs +188 -0
- package/dist/{build-C7MqQ7hq.mjs → build-B1kavcpc.mjs} +74 -13
- package/dist/cli.mjs +69 -9
- package/dist/config-DJxMRVD8.mjs +373 -0
- package/dist/context-D4t59wDi.mjs +600 -0
- package/dist/{daemon-CRZFpl6K.mjs → daemon-PmNqoOAk.mjs} +1 -1
- package/dist/{dev-BNZF4Mup.mjs → dev-Cf4wLGe2.mjs} +15 -7
- package/dist/{init-C34BY3R4.mjs → init-C_K-cjBQ.mjs} +41 -45
- package/dist/managed-write-Bo-oPc-i.mjs +71 -0
- package/dist/{manifest-mMfUhPtL.mjs → manifest-CcdWx7ud.mjs} +42 -376
- package/dist/{plugin-omHLCn91.mjs → plugin-C5u07Yhv.mjs} +234 -105
- package/dist/{poster-BvxiAzy1.mjs → poster-BduzQBYz.mjs} +1 -1
- package/dist/{publish-bakes-BqzAAa3w.mjs → publish-bakes-b0jUwvM_.mjs} +2 -2
- package/dist/{shot-DswS4iRK.mjs → shot-BAR8hmU9.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 +12 -0
- package/templates/AGENTS-studio.md +12 -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 +33 -0
- package/templates/instructions/context.md +123 -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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { r as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody,
|
|
4
|
-
import {
|
|
2
|
+
import { E as addFolders, J as FOLDER_MODULES, X as KIND_FOLDERS } from "./board-status-CWGHIdo_.mjs";
|
|
3
|
+
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
4
|
+
import { t as writeManaged } from "./managed-write-Bo-oPc-i.mjs";
|
|
5
|
+
import { i as scanFrames, s as writeManifest } from "./manifest-CcdWx7ud.mjs";
|
|
6
|
+
import { a as readJson, i as detectHost, r as detectAgent, t as DEFAULTS } from "./config-DJxMRVD8.mjs";
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
8
|
import { basename, dirname, join, relative } from "node:path";
|
|
6
9
|
//#region src/cli/init.ts
|
|
7
10
|
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
@@ -36,43 +39,15 @@ function init(root, opts) {
|
|
|
36
39
|
writeFileSync(file, content);
|
|
37
40
|
created.push(`design/${rel}`);
|
|
38
41
|
};
|
|
39
|
-
const writeManaged = (rel, body) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (current.startsWith("<!-- marver:managed ")) {
|
|
50
|
-
const recorded = current.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
51
|
-
const nl = current.indexOf("\n");
|
|
52
|
-
const currentBody = nl >= 0 ? current.slice(nl + 1) : "";
|
|
53
|
-
if (nl >= 0 && hashBody(currentBody) === recorded) {
|
|
54
|
-
writeFileSync(file, next);
|
|
55
|
-
rmSync(latest, { force: true });
|
|
56
|
-
created.push(`design/${rel} (updated)`);
|
|
57
|
-
} else if (recorded !== hashBody(body)) {
|
|
58
|
-
mkdirSync(dirname(latest), { recursive: true });
|
|
59
|
-
writeFileSync(latest, body);
|
|
60
|
-
if (nl >= 0) {
|
|
61
|
-
const tmp = file + ".tmp";
|
|
62
|
-
writeFileSync(tmp, managedFile(body).split("\n")[0] + "\n" + currentBody);
|
|
63
|
-
renameSync(tmp, file);
|
|
64
|
-
}
|
|
65
|
-
console.warn(` ~ design/${rel}: you customized it and a newer version exists - your edits are untouched. Merge what you want from design/.local/latest/${rel}`);
|
|
66
|
-
}
|
|
67
|
-
} else if (current.startsWith("<!-- generated by marver init")) {
|
|
68
|
-
writeFileSync(file, next);
|
|
69
|
-
rmSync(latest, { force: true });
|
|
70
|
-
created.push(`design/${rel} (updated)`);
|
|
71
|
-
} else if (current !== body) {
|
|
72
|
-
rmSync(latest, { force: true });
|
|
73
|
-
console.warn(` note: design/${rel} exists without a marver marker - left untouched. If you did not author it, delete it and re-run init to restore the managed version.`);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
42
|
+
const writeManaged$1 = (rel, body) => writeManaged({
|
|
43
|
+
base: design,
|
|
44
|
+
rel,
|
|
45
|
+
body,
|
|
46
|
+
shown: `design/${rel}`,
|
|
47
|
+
stageDir: join(design, ".local", "latest"),
|
|
48
|
+
created,
|
|
49
|
+
rerun: `\`npx ${NAME} init\``
|
|
50
|
+
});
|
|
76
51
|
const jamAgent = detectAgent();
|
|
77
52
|
write("config.ts", configTemplate(opts.mode, jamAgent));
|
|
78
53
|
if (host.themeCss) {
|
|
@@ -80,7 +55,7 @@ function init(root, opts) {
|
|
|
80
55
|
write("theme.css", themeWrapper(relCss, host.tailwind === 4));
|
|
81
56
|
} else console.warn(`[${NAME}] no theme CSS detected - create design/theme.css importing your app's stylesheet when you have one (or set \`theme\` in design/config.ts).`);
|
|
82
57
|
write("providers.tsx", providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
83
|
-
writeManaged("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_GUIDANCE}}", uiGuidance(host, noApp(host))).replace(/\{\{NEXT_NOTES\}\}\n?/, host.router === "next" ? NEXT_NOTES : ""));
|
|
58
|
+
writeManaged$1("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_GUIDANCE}}", uiGuidance(host, noApp(host))).replace(/\{\{NEXT_NOTES\}\}\n?/, host.router === "next" ? NEXT_NOTES : ""));
|
|
84
59
|
const agentsNow = readFileSync(join(design, "AGENTS.md"), "utf8");
|
|
85
60
|
if (!agentsNow.startsWith("<!-- marver:managed ") && !agentsNow.startsWith("<!-- generated by marver init") && agentsNow.includes("# Design canvas - agent contract") && !agentsNow.includes("## The method (binding)")) console.warn(` note: design/AGENTS.md predates managed regeneration - if you never edited it, delete it and re-run init to get the current contract (incl. the design/instructions routing).`);
|
|
86
61
|
const claudeMd = join(root, "CLAUDE.md");
|
|
@@ -107,7 +82,7 @@ function init(root, opts) {
|
|
|
107
82
|
created.push("AGENTS.md (updated)");
|
|
108
83
|
}
|
|
109
84
|
}
|
|
110
|
-
for (const { rel, body } of enumerateInstructionTemplates(templates)) writeManaged(rel, body);
|
|
85
|
+
for (const { rel, body } of enumerateInstructionTemplates(templates)) writeManaged$1(rel, body);
|
|
111
86
|
const setupPath = join(design, "instructions", "setup.md");
|
|
112
87
|
const setupState = () => {
|
|
113
88
|
if (!existsSync(setupPath)) return "absent";
|
|
@@ -123,7 +98,7 @@ function init(root, opts) {
|
|
|
123
98
|
const appJustAppeared = !noApp(host) && (setupWas === "ours-pristine" || setupWas === "ours-edited");
|
|
124
99
|
if (noApp(host)) {
|
|
125
100
|
if (setupWas === "ours-pristine" && !readFileSync(setupPath, "utf8").startsWith("<!-- marver:managed ")) rmSync(setupPath);
|
|
126
|
-
if (setupWas !== "foreign") writeManaged("instructions/setup.md", SETUP_MD);
|
|
101
|
+
if (setupWas !== "foreign") writeManaged$1("instructions/setup.md", SETUP_MD);
|
|
127
102
|
} else if (setupWas === "ours-pristine") {
|
|
128
103
|
rmSync(setupPath);
|
|
129
104
|
console.log(` - design/instructions/setup.md removed (app detected - setup complete)`);
|
|
@@ -143,7 +118,7 @@ function init(root, opts) {
|
|
|
143
118
|
refresh("providers.tsx", [providersTemplate(null, null), providersTemplate(null, host.toaster, host.routerPkg)], providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
144
119
|
refresh("tsconfig.json", [STANDALONE_TSCONFIG], tsconfigNow());
|
|
145
120
|
}
|
|
146
|
-
write(".gitignore", ".local/\n.dist/\n.dist-seeds/\nslides-inspiration/\n");
|
|
121
|
+
write(".gitignore", ".local/\n.dist/\n.dist-seeds/\nslides-inspiration/\nboards/.folders.lock\n");
|
|
147
122
|
{
|
|
148
123
|
const gi = join(design, ".gitignore");
|
|
149
124
|
const cur = readFileSync(gi, "utf8");
|
|
@@ -152,6 +127,14 @@ function init(root, opts) {
|
|
|
152
127
|
created.push("design/.gitignore (+ slides-inspiration/)");
|
|
153
128
|
}
|
|
154
129
|
}
|
|
130
|
+
{
|
|
131
|
+
const gi = join(design, ".gitignore");
|
|
132
|
+
const cur = readFileSync(gi, "utf8");
|
|
133
|
+
if (!cur.split("\n").some((l) => l.trim() === "boards/.folders.lock")) {
|
|
134
|
+
writeFileSync(gi, cur + (cur.endsWith("\n") || cur === "" ? "" : "\n") + "boards/.folders.lock\n");
|
|
135
|
+
created.push("design/.gitignore (+ boards/.folders.lock)");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
155
138
|
write("slides.md", [
|
|
156
139
|
"# This project's deck look and house habits",
|
|
157
140
|
"",
|
|
@@ -216,6 +199,18 @@ function init(root, opts) {
|
|
|
216
199
|
writeFileSync(join(design, "boards", ".gitkeep"), "");
|
|
217
200
|
created.push("design/boards/");
|
|
218
201
|
}
|
|
202
|
+
{
|
|
203
|
+
const boardsDir = join(design, "boards");
|
|
204
|
+
const fresh = !existsSync(join(boardsDir, "_folders.json")) && !readdirSync(boardsDir).some((f) => f.endsWith(".json"));
|
|
205
|
+
if (opts.kind || fresh) {
|
|
206
|
+
const kind = opts.kind ?? (noApp(host) ? "knowledge" : "product");
|
|
207
|
+
const { added } = addFolders(root, KIND_FOLDERS[kind].map((m) => FOLDER_MODULES[m]));
|
|
208
|
+
if (added.length) {
|
|
209
|
+
created.push(`design/boards/_folders.json (${kind}: ${added.join(", ")})`);
|
|
210
|
+
if (!opts.kind) console.log(` folders for ${kind === "product" ? "a product (an app was detected)" : "knowledge work (no app detected)"} - \`npx ${NAME} init --kind ${kind === "product" ? "knowledge" : "product"}\` adds the other set; \`npx ${NAME} folders add decks\` the deck folder`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
219
214
|
const sceneDirs = existsSync(join(design, "scenes")) ? readdirSync(join(design, "scenes"), { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_")).length : 0;
|
|
220
215
|
if (opts.demo && sceneDirs === 0 && !existsSync(join(design, "scenes", "demo"))) {
|
|
221
216
|
cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
|
|
@@ -239,10 +234,11 @@ function init(root, opts) {
|
|
|
239
234
|
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
240
235
|
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
241
236
|
if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
|
|
237
|
+
if (!existsSync(join(root, "context"))) console.log(`\n note: no context/ yet - feature and project boards read Backlog until there is one, and nothing records what shipped. To start it, tell your agent: "Set up our context." (design/instructions/context.md)`);
|
|
242
238
|
if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and it arms itself on the next \`${NAME} dev\`.`);
|
|
243
239
|
else if (created.includes("design/config.ts")) console.log(`\n Live Jam is on (${jamAgent}): tag @${NAME} in a canvas comment and your agent does the work, then replies in the thread.`);
|
|
244
240
|
console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
245
|
-
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
241
|
+
if (!noApp(host) && created.includes("design/config.ts")) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
246
242
|
}
|
|
247
243
|
/** The host's path aliases, re-rooted one level down for design/tsconfig.json.
|
|
248
244
|
* "./src/*" becomes "../src/*" so `@/` imports resolve from inside design/. */
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { MANAGED_PREFIX, hashBody, managedFile } from "./managed-HwHVNI3h.mjs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
//#region src/cli/managed-write.ts
|
|
5
|
+
/**
|
|
6
|
+
* Write one managed file - the marker records a HASH of the body Marver generated, which makes
|
|
7
|
+
* three states distinguishable with no other stored state:
|
|
8
|
+
* pristine (body matches the hash) -> updates flow through on upgrade
|
|
9
|
+
* edited (body differs from the hash) -> NEVER overwritten; when upstream also moved, the
|
|
10
|
+
* fresh version is staged under `stageDir` and one line says to merge - an agent
|
|
11
|
+
* merges semantically, which is why no merge machinery ships here
|
|
12
|
+
* detached (marker line deleted) -> never touched, never mentioned
|
|
13
|
+
*
|
|
14
|
+
* `base` + `rel` is the file; `shown` is how messages name it (`design/AGENTS.md`,
|
|
15
|
+
* `context/playbooks/release/PLAYBOOK.md`). Shared by `init` (design/) and `context init`.
|
|
16
|
+
*/
|
|
17
|
+
function writeManaged(o) {
|
|
18
|
+
const { base, rel, body, shown, stageDir, created, rerun } = o;
|
|
19
|
+
const file = join(base, rel);
|
|
20
|
+
const next = managedFile(body);
|
|
21
|
+
const latest = join(stageDir, rel);
|
|
22
|
+
if (!existsSync(file)) {
|
|
23
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(file, next, { flag: "wx" });
|
|
26
|
+
created.push(shown);
|
|
27
|
+
return;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (e.code !== "EEXIST") throw e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const current = readFileSync(file, "utf8");
|
|
33
|
+
if (current === next) {
|
|
34
|
+
rmSync(latest, { force: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const replace = (content) => {
|
|
38
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
39
|
+
writeFileSync(tmp, content, { flag: "wx" });
|
|
40
|
+
if (readFileSync(file, "utf8") !== current) {
|
|
41
|
+
rmSync(tmp, { force: true });
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
renameSync(tmp, file);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
if (current.startsWith("<!-- marver:managed ")) {
|
|
48
|
+
const recorded = current.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
49
|
+
const nl = current.indexOf("\n");
|
|
50
|
+
const currentBody = nl >= 0 ? current.slice(nl + 1) : "";
|
|
51
|
+
if (nl >= 0 && hashBody(currentBody) === recorded) {
|
|
52
|
+
if (!replace(next)) return;
|
|
53
|
+
rmSync(latest, { force: true });
|
|
54
|
+
created.push(`${shown} (updated)`);
|
|
55
|
+
} else if (recorded !== hashBody(body)) {
|
|
56
|
+
mkdirSync(dirname(latest), { recursive: true });
|
|
57
|
+
writeFileSync(latest, body);
|
|
58
|
+
if (nl >= 0) replace(managedFile(body).split("\n")[0] + "\n" + currentBody);
|
|
59
|
+
console.warn(` ~ ${shown}: you customized it and a newer version exists - your edits are untouched. Merge what you want from ${latest.slice(latest.indexOf("design/.local/"))}`);
|
|
60
|
+
}
|
|
61
|
+
} else if (current.startsWith("<!-- generated by marver init")) {
|
|
62
|
+
if (!replace(next)) return;
|
|
63
|
+
rmSync(latest, { force: true });
|
|
64
|
+
created.push(`${shown} (updated)`);
|
|
65
|
+
} else if (current !== body) {
|
|
66
|
+
rmSync(latest, { force: true });
|
|
67
|
+
console.warn(` note: ${shown} exists without a marver marker - left untouched. If you did not author it, delete it and re-run ${rerun} to restore the managed version.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { writeManaged as t };
|
|
@@ -1,378 +1,29 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
1
2
|
import { i as PKG, n as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { D as boardFields, H as isBoardName, L as buildTree, N as readRegistry, O as checkBoardsDir, R as flatten, U as readDescription, W as readTitle, at as hash, j as listBoardFiles, k as checkRealDirs, t as annotateBoards, z as folderEntries } from "./board-status-CWGHIdo_.mjs";
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, relative, sep } from "node:path";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
//#region src/server/detect.ts
|
|
8
|
-
function readJson(file) {
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(stripJsonComments(readFileSync(file, "utf8")));
|
|
11
|
-
} catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function stripJsonComments(s) {
|
|
16
|
-
let out = "", inStr = false, inLine = false, inBlock = false;
|
|
17
|
-
for (let i = 0; i < s.length; i++) {
|
|
18
|
-
const c = s[i], n = s[i + 1];
|
|
19
|
-
if (inLine) {
|
|
20
|
-
if (c === "\n") {
|
|
21
|
-
inLine = false;
|
|
22
|
-
out += c;
|
|
23
|
-
}
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (inBlock) {
|
|
27
|
-
if (c === "*" && n === "/") {
|
|
28
|
-
inBlock = false;
|
|
29
|
-
i++;
|
|
30
|
-
}
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (inStr) {
|
|
34
|
-
out += c;
|
|
35
|
-
if (c === "\\") {
|
|
36
|
-
out += n ?? "";
|
|
37
|
-
i++;
|
|
38
|
-
} else if (c === "\"") inStr = false;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (c === "\"") {
|
|
42
|
-
inStr = true;
|
|
43
|
-
out += c;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (c === "/" && n === "/") {
|
|
47
|
-
inLine = true;
|
|
48
|
-
i++;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (c === "/" && n === "*") {
|
|
52
|
-
inBlock = true;
|
|
53
|
-
i++;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
out += c;
|
|
57
|
-
}
|
|
58
|
-
let res = "", inS = false;
|
|
59
|
-
for (let i = 0; i < out.length; i++) {
|
|
60
|
-
const c = out[i];
|
|
61
|
-
if (inS) {
|
|
62
|
-
res += c;
|
|
63
|
-
if (c === "\\") {
|
|
64
|
-
res += out[i + 1] ?? "";
|
|
65
|
-
i++;
|
|
66
|
-
} else if (c === "\"") inS = false;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (c === "\"") {
|
|
70
|
-
inS = true;
|
|
71
|
-
res += c;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (c === ",") {
|
|
75
|
-
let j = i + 1;
|
|
76
|
-
while (j < out.length && /\s/.test(out[j])) j++;
|
|
77
|
-
if (out[j] === "}" || out[j] === "]") continue;
|
|
78
|
-
}
|
|
79
|
-
res += c;
|
|
80
|
-
}
|
|
81
|
-
return res;
|
|
82
|
-
}
|
|
83
|
-
function detectHost(root) {
|
|
84
|
-
const pkg = readJson(join(root, "package.json")) ?? {};
|
|
85
|
-
const deps = {
|
|
86
|
-
...pkg.dependencies,
|
|
87
|
-
...pkg.devDependencies
|
|
88
|
-
};
|
|
89
|
-
const twRange = deps["tailwindcss"];
|
|
90
|
-
const tailwind = !twRange ? null : /(^|[^\d])4\./.test(twRange) || twRange.startsWith("^4") || twRange.startsWith("~4") ? 4 : 3;
|
|
91
|
-
const router = deps["next"] ? "next" : deps["react-router"] || deps["react-router-dom"] ? "react-router" : null;
|
|
92
|
-
const routerPkg = deps["react-router-dom"] ? "react-router-dom" : "react-router";
|
|
93
|
-
const toaster = deps["sonner"] ? "sonner" : deps["react-hot-toast"] ? "react-hot-toast" : null;
|
|
94
|
-
let shadcn = null;
|
|
95
|
-
const comp = readJson(join(root, "components.json"));
|
|
96
|
-
if (comp?.tailwind?.css) shadcn = {
|
|
97
|
-
themeCss: comp.tailwind.css,
|
|
98
|
-
uiAlias: comp.aliases?.ui ?? "@/components/ui"
|
|
99
|
-
};
|
|
100
|
-
const themeCss = shadcn?.themeCss && existsSync(join(root, shadcn.themeCss)) ? shadcn.themeCss : firstExisting(root, [
|
|
101
|
-
"src/index.css",
|
|
102
|
-
"src/styles/theme.css",
|
|
103
|
-
"src/styles/globals.css",
|
|
104
|
-
"src/app/globals.css",
|
|
105
|
-
"app/globals.css",
|
|
106
|
-
"src/globals.css",
|
|
107
|
-
"src/style.css",
|
|
108
|
-
"styles/globals.css"
|
|
109
|
-
]);
|
|
110
|
-
const ts = readJson(join(root, "tsconfig.json"));
|
|
111
|
-
const include = ts?.include;
|
|
112
|
-
const exclude = ts?.exclude ?? [];
|
|
113
|
-
const sweeps = ts != null && !exclude.some((e) => e === "design" || e.startsWith("design/")) && (!include || include.some((i) => i === "." || i.startsWith("**") || i.startsWith("design")));
|
|
114
|
-
return {
|
|
115
|
-
tailwind,
|
|
116
|
-
router,
|
|
117
|
-
routerPkg,
|
|
118
|
-
toaster,
|
|
119
|
-
shadcn,
|
|
120
|
-
themeCss,
|
|
121
|
-
tsconfigSweepsDesign: !!sweeps
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function firstExisting(root, candidates) {
|
|
125
|
-
for (const c of candidates) if (existsSync(join(root, c))) return c;
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
//#endregion
|
|
129
|
-
//#region src/server/jam/agent.ts
|
|
130
|
-
/**
|
|
131
|
-
* Which coding agent drives this repo.
|
|
132
|
-
*
|
|
133
|
-
* Live Jam is ON by default, so this is the answer to "on by default with WHAT" - asked
|
|
134
|
-
* once by `init` (which writes the answer into design/config.ts, where it is visible and
|
|
135
|
-
* editable) and again at every dev boot, so a workspace that predates the block - or a
|
|
136
|
-
* human who switched tools - still jams without editing anything.
|
|
137
|
-
*
|
|
138
|
-
* The tool RUNNING us wins: `init` is usually run BY the agent, so its env markers are
|
|
139
|
-
* evidence rather than a guess. Whatever wins must still be on PATH - the daemon has to
|
|
140
|
-
* spawn it.
|
|
141
|
-
*/
|
|
142
|
-
/** Ordered by preference when a machine has several installed and none is running us. */
|
|
143
|
-
const AGENTS = [
|
|
144
|
-
"claude",
|
|
145
|
-
"codex",
|
|
146
|
-
"cursor",
|
|
147
|
-
"droid",
|
|
148
|
-
"opencode",
|
|
149
|
-
"grok",
|
|
150
|
-
"pi"
|
|
151
|
-
];
|
|
152
|
-
/** Env vars each CLI sets in the processes it spawns. Deliberately NOT CODEX_HOME or
|
|
153
|
-
* ANTHROPIC_API_KEY: those are configuration a human exports by hand, not evidence that
|
|
154
|
-
* the tool is running right now. (opencode's `AGENT=1` and pi's `AI_AGENT` are skipped
|
|
155
|
-
* for the same reason in reverse - too generic to prove WHICH tool is running.)
|
|
156
|
-
* droid and grok set no marker at all in the shells they spawn (verified against the
|
|
157
|
-
* grok-build source and droid 0.200.0), so they are found by PATH order only. */
|
|
158
|
-
const MARKERS = {
|
|
159
|
-
claude: ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"],
|
|
160
|
-
codex: ["CODEX_SANDBOX", "CODEX_THREAD_ID"],
|
|
161
|
-
cursor: ["CURSOR_AGENT"],
|
|
162
|
-
droid: [],
|
|
163
|
-
opencode: ["OPENCODE", "OPENCODE_PID"],
|
|
164
|
-
grok: [],
|
|
165
|
-
pi: ["PI_CODING_AGENT", "PI_SESSION_ID"]
|
|
166
|
-
};
|
|
167
|
-
/** The executable each agent name maps to. Only cursor differs: its CLI installs BOTH
|
|
168
|
-
* `cursor-agent` and a bare `agent` - and grok's installer symlinks `agent` too, so the
|
|
169
|
-
* short name is a coin flip on a machine with both. The long name is unambiguous. */
|
|
170
|
-
const AGENT_BIN = {
|
|
171
|
-
claude: "claude",
|
|
172
|
-
codex: "codex",
|
|
173
|
-
cursor: "cursor-agent",
|
|
174
|
-
droid: "droid",
|
|
175
|
-
opencode: "opencode",
|
|
176
|
-
grok: "grok",
|
|
177
|
-
pi: "pi"
|
|
178
|
-
};
|
|
179
|
-
/** The valid `jam.agent` values as an English list - one string, so every surface
|
|
180
|
-
* (config warning, init note, dev boot line) names the same set and none goes stale. */
|
|
181
|
-
const AGENT_NAMES = `${AGENTS.slice(0, -1).map((a) => `"${a}"`).join(", ")}, or "${AGENTS[AGENTS.length - 1]}"`;
|
|
182
|
-
/** Is `cmd` an executable FILE on PATH? A direct stat per PATH entry - no spawn, no shell,
|
|
183
|
-
* so this is safe to call on every dev boot. Two deliberate narrowings, both in service of
|
|
184
|
-
* "found means spawnable", because arming an agent that cannot run is worse than staying off:
|
|
185
|
-
*
|
|
186
|
-
* - `isFile`, because a directory carries the execute bit too (it means "traversable"), so an
|
|
187
|
-
* access check alone would call a folder named `claude` an agent.
|
|
188
|
-
* - the bare name only, no PATHEXT: the daemon spawns without a shell, and Node cannot run a
|
|
189
|
-
* Windows `.cmd`/`.bat` shim that way. Finding one would arm a job that fails on every run.
|
|
190
|
-
*
|
|
191
|
-
* Only ABSOLUTE PATH entries count. An empty entry means the current directory on POSIX, and
|
|
192
|
-
* a relative one (`.`, `bin`) resolves against it too - and the current directory is the repo
|
|
193
|
-
* that was just opened, so a `claude` binary shipped inside it is precisely what must never
|
|
194
|
-
* be found and spawned. */
|
|
195
|
-
function onPath(cmd, env = process.env) {
|
|
196
|
-
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
197
|
-
if (!isAbsolute(dir)) continue;
|
|
198
|
-
try {
|
|
199
|
-
const file = join(dir, cmd);
|
|
200
|
-
if (statSync(file).isFile()) {
|
|
201
|
-
accessSync(file, constants.X_OK);
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
} catch {}
|
|
205
|
-
}
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
/** The agent to jam with, or undefined when this machine has none installed.
|
|
209
|
-
*
|
|
210
|
-
* One knowingly-imperfect case: agents nest, and env is inherited, so a codex run started
|
|
211
|
-
* FROM Claude Code carries both marker families and this picks claude. Env has no depth to
|
|
212
|
-
* read, and walking the process tree to find the nearest agent ancestor costs more than the
|
|
213
|
-
* case is worth - so the answer is made visible instead of clever: `init` prints the agent
|
|
214
|
-
* it chose and writes it into design/config.ts, and instructions/jam.md has the agent confirm
|
|
215
|
-
* that line names the tool it actually is. One word to correct, once per repo. */
|
|
216
|
-
function detectAgent(env = process.env) {
|
|
217
|
-
const running = AGENTS.find((a) => MARKERS[a].some((k) => env[k]));
|
|
218
|
-
if (running && onPath(AGENT_BIN[running], env)) return running;
|
|
219
|
-
return AGENTS.find((a) => onPath(AGENT_BIN[a], env));
|
|
220
|
-
}
|
|
221
|
-
//#endregion
|
|
222
|
-
//#region src/server/config.ts
|
|
223
|
-
const DEFAULTS = {
|
|
224
|
-
mode: "studio",
|
|
225
|
-
theme: null,
|
|
226
|
-
viewports: {
|
|
227
|
-
mobile: {
|
|
228
|
-
width: 390,
|
|
229
|
-
height: 844
|
|
230
|
-
},
|
|
231
|
-
tablet: {
|
|
232
|
-
width: 768,
|
|
233
|
-
height: 1024
|
|
234
|
-
},
|
|
235
|
-
laptop: {
|
|
236
|
-
width: 1280,
|
|
237
|
-
height: 800
|
|
238
|
-
},
|
|
239
|
-
monitor: {
|
|
240
|
-
width: 1920,
|
|
241
|
-
height: 1080
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
themes: ["light", "dark"],
|
|
245
|
-
port: 5199,
|
|
246
|
-
zoomSpeed: 1,
|
|
247
|
-
share: { branding: true }
|
|
248
|
-
};
|
|
249
|
-
/** Load design/config.ts via native TS import (Node >= 22.18). Missing or broken fields fall back to defaults. */
|
|
250
|
-
async function loadConfig(root) {
|
|
251
|
-
const file = join(root, "design", "config.ts");
|
|
252
|
-
if (!existsSync(file)) return {
|
|
253
|
-
...DEFAULTS,
|
|
254
|
-
...jamFields(void 0)
|
|
255
|
-
};
|
|
256
|
-
try {
|
|
257
|
-
const user = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default ?? {};
|
|
258
|
-
return {
|
|
259
|
-
...DEFAULTS,
|
|
260
|
-
...user,
|
|
261
|
-
viewports: validViewports(user.viewports) ?? DEFAULTS.viewports,
|
|
262
|
-
themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
|
|
263
|
-
port: validPort(user.port) ?? DEFAULTS.port,
|
|
264
|
-
zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
|
|
265
|
-
description: readDescription(user.description),
|
|
266
|
-
share: {
|
|
267
|
-
branding: user.share?.branding !== false,
|
|
268
|
-
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
269
|
-
logo: typeof user.share?.logo === "string" ? user.share.logo : void 0,
|
|
270
|
-
frontDoor: user.share?.frontDoor !== false && user.share?.front_door !== false,
|
|
271
|
-
notify: user.share?.notify !== false
|
|
272
|
-
},
|
|
273
|
-
...jamFields(user.jam)
|
|
274
|
-
};
|
|
275
|
-
} catch (err) {
|
|
276
|
-
console.error(`[marver] design/config.ts failed to load, using defaults and leaving Live Jam OFF:\n ${err.message}`);
|
|
277
|
-
return {
|
|
278
|
-
...DEFAULTS,
|
|
279
|
-
jamOff: "unreadable"
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
const validDim = (n) => typeof n === "number" && Number.isFinite(n) && n >= 1 && n <= 2e4;
|
|
284
|
-
function validPort(n) {
|
|
285
|
-
return typeof n === "number" && Number.isInteger(n) && n > 0 && n < 65536 ? n : null;
|
|
286
|
-
}
|
|
287
|
-
function validZoom(n) {
|
|
288
|
-
return typeof n === "number" && Number.isFinite(n) && n >= .1 && n <= 10 ? n : null;
|
|
289
|
-
}
|
|
290
|
-
/** Six frames at once is what a jam actually feels like - at 3, half of a multi-frame
|
|
291
|
-
* ask sat waiting on the other half while the human watched. */
|
|
292
|
-
const DEFAULT_CONCURRENCY = 6;
|
|
293
|
-
/** A config value, printable in a warning. Never throws - a formatter that can crash inside an
|
|
294
|
-
* error path (JSON.stringify does, on a BigInt) turns a clear message into a mystery. */
|
|
295
|
-
const show = (v) => {
|
|
296
|
-
try {
|
|
297
|
-
return JSON.stringify(v) ?? String(v);
|
|
298
|
-
} catch {
|
|
299
|
-
return String(v);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
/** A `{...}` written by hand, not a Date/Map/class instance that merely types as "object". */
|
|
303
|
-
const plainObject = (v) => {
|
|
304
|
-
if (!v || typeof v !== "object") return false;
|
|
305
|
-
const proto = Object.getPrototypeOf(v);
|
|
306
|
-
return proto === Object.prototype || proto === null;
|
|
307
|
-
};
|
|
308
|
-
/** The agent to arm with - or, when the human NAMED one we cannot use, why not (said once,
|
|
309
|
-
* here, because a bad `jam.agent` is a config-file error like any other).
|
|
310
|
-
*
|
|
311
|
-
* A named agent is never quietly swapped for another: running a different tool than the one
|
|
312
|
-
* asked for is worse than not running. Nor is one armed that cannot be spawned - that would
|
|
313
|
-
* claim every @marver mention, fail it, and never retry. */
|
|
314
|
-
function resolveAgent(named) {
|
|
315
|
-
if (named === void 0) return detectAgent() ?? "no-agent";
|
|
316
|
-
if (typeof named !== "string" || !Object.hasOwn(AGENT_BIN, named)) {
|
|
317
|
-
console.warn(`[marver] design/config.ts: jam.agent ${show(named)} is not an agent marver can spawn - Live Jam is off. Use ${AGENT_NAMES}.`);
|
|
318
|
-
return "bad-agent";
|
|
319
|
-
}
|
|
320
|
-
const agent = named;
|
|
321
|
-
if (!onPath(AGENT_BIN[agent])) {
|
|
322
|
-
console.warn(`[marver] design/config.ts names jam.agent "${agent}", but \`${AGENT_BIN[agent]}\` is not on PATH - Live Jam is off until it is installed.`);
|
|
323
|
-
return "bad-agent";
|
|
324
|
-
}
|
|
325
|
-
return agent;
|
|
326
|
-
}
|
|
327
|
-
/** Resolve the jam block. Live Jam is ON by default: an absent or partial `jam` resolves
|
|
328
|
-
* to whatever agent CLI this machine has, so a fresh workspace jams with nothing
|
|
329
|
-
* configured and an old one needs no re-init. `jam: false` is the off switch. */
|
|
330
|
-
function resolveJam(v) {
|
|
331
|
-
if (v === false) return "opted-out";
|
|
332
|
-
const shape = v === void 0 || v === true ? {} : typeof v === "string" ? { agent: v } : plainObject(v) ? v : null;
|
|
333
|
-
if (!shape) {
|
|
334
|
-
console.warn(`[marver] design/config.ts: jam must be false, an agent name, or an options object - got ${show(v)}. Live Jam is off.`);
|
|
335
|
-
return "bad-agent";
|
|
336
|
-
}
|
|
337
|
-
const j = shape;
|
|
338
|
-
const agent = resolveAgent(j.agent);
|
|
339
|
-
if (agent === "no-agent" || agent === "bad-agent") return agent;
|
|
340
|
-
const c = j.concurrency;
|
|
341
|
-
return {
|
|
342
|
-
agent,
|
|
343
|
-
concurrency: typeof c === "number" && Number.isInteger(c) && c >= 1 && c <= 16 ? c : DEFAULT_CONCURRENCY,
|
|
344
|
-
subagents: j.subagents !== false,
|
|
345
|
-
proactive: j.proactive === true
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
/** Exactly one of `jam` (armed) or `jamOff` (why not) - so the dev server can speak up about
|
|
349
|
-
* the one off-state nothing has reported yet, and stay quiet about the rest. BOTH keys are
|
|
350
|
-
* always written: these spread over the user's own `jam`, and a raw `jam: false` reaching the
|
|
351
|
-
* server as config would arm the daemon against a truthy object check. */
|
|
352
|
-
function jamFields(v) {
|
|
353
|
-
const r = resolveJam(v);
|
|
354
|
-
return typeof r === "string" ? {
|
|
355
|
-
jam: void 0,
|
|
356
|
-
jamOff: r
|
|
357
|
-
} : {
|
|
358
|
-
jam: r,
|
|
359
|
-
jamOff: void 0
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
function validViewports(v) {
|
|
363
|
-
if (!v || typeof v !== "object") return null;
|
|
364
|
-
const out = {};
|
|
365
|
-
for (const [k, val] of Object.entries(v)) {
|
|
366
|
-
const vp = val;
|
|
367
|
-
if (validDim(vp?.width) && validDim(vp?.height)) out[k] = {
|
|
368
|
-
width: Math.round(vp.width),
|
|
369
|
-
height: Math.round(vp.height)
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
return Object.keys(out).length ? out : null;
|
|
373
|
-
}
|
|
374
|
-
//#endregion
|
|
375
7
|
//#region src/server/manifest.ts
|
|
8
|
+
var manifest_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
NOTE_MAX: () => NOTE_MAX,
|
|
10
|
+
affectedFrameIds: () => affectedFrameIds,
|
|
11
|
+
codeOnly: () => codeOnly,
|
|
12
|
+
contentScan: () => contentScan,
|
|
13
|
+
contentWidthOf: () => contentWidthOf,
|
|
14
|
+
extractMeta: () => extractMeta,
|
|
15
|
+
frontMatter: () => frontMatter,
|
|
16
|
+
isNoteFile: () => isNoteFile,
|
|
17
|
+
noteFileFor: () => noteFileFor,
|
|
18
|
+
readNote: () => readNote,
|
|
19
|
+
scanFrames: () => scanFrames,
|
|
20
|
+
sceneBrief: () => sceneBrief,
|
|
21
|
+
sceneNote: () => sceneNote,
|
|
22
|
+
setSceneTitle: () => setSceneTitle,
|
|
23
|
+
statusBoards: () => statusBoards,
|
|
24
|
+
toFrameId: () => toFrameId,
|
|
25
|
+
writeManifest: () => writeManifest
|
|
26
|
+
});
|
|
376
27
|
const FRAME_EXT = /\.(tsx|jsx|html)$/;
|
|
377
28
|
const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
|
|
378
29
|
/** Sticky notes (spec 18): the longest note the manifest carries - a note is an aside, not a spec. */
|
|
@@ -625,7 +276,8 @@ function scanBoards(root) {
|
|
|
625
276
|
if (checkBoardsDir(root, dir)) return {};
|
|
626
277
|
const reg = readRegistry(dir);
|
|
627
278
|
if (reg.state === "malformed") return {};
|
|
628
|
-
const
|
|
279
|
+
const files = listBoardFiles(dir).boards;
|
|
280
|
+
const rows = files.map((b) => ({
|
|
629
281
|
name: b.name,
|
|
630
282
|
...boardFields(b.json, isBoardName)
|
|
631
283
|
}));
|
|
@@ -637,15 +289,26 @@ function scanBoards(root) {
|
|
|
637
289
|
name: it.name,
|
|
638
290
|
...parent ? { parent } : {},
|
|
639
291
|
...it.title ? { title: it.title } : {},
|
|
640
|
-
...it.description ? { description: it.description } : {}
|
|
292
|
+
...it.description ? { description: it.description } : {},
|
|
293
|
+
...it.type ? { type: it.type } : {}
|
|
641
294
|
}));
|
|
295
|
+
const notes = annotateBoards(root, files, reg.folders, (n) => folderOf.get(n) ?? null);
|
|
642
296
|
const boards = flatten(tree).map((name) => {
|
|
643
297
|
const r = rows.find((x) => x.name === name);
|
|
298
|
+
const n = notes.get(name);
|
|
299
|
+
const st = n?.status;
|
|
644
300
|
return {
|
|
645
301
|
name,
|
|
646
302
|
...folderOf.has(name) ? { folder: folderOf.get(name) } : {},
|
|
647
303
|
...r?.title ? { title: r.title } : {},
|
|
648
|
-
...r?.description ? { description: r.description } : {}
|
|
304
|
+
...r?.description ? { description: r.description } : {},
|
|
305
|
+
...n && n.type !== "plain" ? { type: n.type } : {},
|
|
306
|
+
...st ? { status: {
|
|
307
|
+
status: st.status,
|
|
308
|
+
...st.fill ? { fill: st.fill } : {},
|
|
309
|
+
...st.reason ? { reason: st.reason } : {},
|
|
310
|
+
audience: st.audience
|
|
311
|
+
} } : {}
|
|
649
312
|
};
|
|
650
313
|
});
|
|
651
314
|
return {
|
|
@@ -653,6 +316,9 @@ function scanBoards(root) {
|
|
|
653
316
|
...boards.length ? { boards } : {}
|
|
654
317
|
};
|
|
655
318
|
}
|
|
319
|
+
/** The boards that wear a status (feature and project, by their resolved type) - for the dev
|
|
320
|
+
* server's word at boot when there is no context/ to read it from. */
|
|
321
|
+
const statusBoards = (root) => (scanBoards(root).boards ?? []).filter((b) => b.type === "feature" || b.type === "project").map((b) => b.name);
|
|
656
322
|
function scanFrames(root, project) {
|
|
657
323
|
const design = join(root, "design");
|
|
658
324
|
const frames = [];
|
|
@@ -809,4 +475,4 @@ function writeManifest(root, manifest) {
|
|
|
809
475
|
return true;
|
|
810
476
|
}
|
|
811
477
|
//#endregion
|
|
812
|
-
export {
|
|
478
|
+
export { setSceneTitle as a, scanFrames as i, isNoteFile as n, toFrameId as o, manifest_exports as r, writeManifest as s, affectedFrameIds as t };
|