@marver-design/marver 0.15.0 → 0.16.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 +75 -0
- package/README.md +2 -1
- package/dist/boards-6hKVW42a.mjs +57 -0
- package/dist/boards-BdG1TJwU.mjs +267 -0
- package/dist/{build-DfuTQZlY.mjs → build-Ct0iMoSi.mjs} +68 -29
- package/dist/cli.mjs +13 -4
- package/dist/{daemon-DalgvoA9.mjs → daemon-Cb4JjpgL.mjs} +1 -1
- package/dist/{dev-BxCmeU_H.mjs → dev-CdcIuhZJ.mjs} +4 -4
- package/dist/{init-QKNi9gvF.mjs → init-BQIpIpIv.mjs} +6 -3
- package/dist/{manifest-BzxSMoDB.mjs → manifest-CpbsqQ_v.mjs} +77 -11
- package/dist/{plugin-DJyjmQeh.mjs → plugin-BXyezwfN.mjs} +164 -69
- package/dist/{poster-CbpzSzJu.mjs → poster-DOY7pax8.mjs} +1 -1
- package/dist/{shot-BWhoz6cU.mjs → shot-CwmHO5T4.mjs} +2 -2
- package/docs/publish.md +4 -1
- package/package.json +1 -1
- package/src/client/shell/App.tsx +4 -246
- package/src/client/shell/BoardList.tsx +408 -0
- package/src/client/shell/ContextMenu.tsx +59 -0
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/store.ts +105 -47
- package/src/client/shell/styles.css +36 -4
- package/src/shared/board-tree.ts +285 -0
- package/templates/AGENTS-embedded.md +18 -5
- package/templates/AGENTS-studio.md +18 -5
- package/templates/instructions/boards.md +73 -2
- package/templates/instructions/craft.md +4 -0
- package/templates/instructions/discover.md +7 -2
- package/templates/instructions/iterate.md +5 -4
- package/templates/instructions/review.md +4 -0
- package/templates/instructions/shape.md +1 -1
- package/templates/instructions/welcome.md +4 -1
- package/templates/instructions/wireframe.md +3 -0
- package/dist/{comments-DHB_8BRa.mjs → comments-oYcZ3cE-.mjs} +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { c as detectHost, o as loadConfig } from "./manifest-CpbsqQ_v.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BXyezwfN.mjs";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -257,7 +257,7 @@ async function dev(root, portFlag) {
|
|
|
257
257
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
258
258
|
try {
|
|
259
259
|
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
260
|
-
const { installedVersion } = await import("./plugin-
|
|
260
|
+
const { installedVersion } = await import("./plugin-BXyezwfN.mjs").then((n) => n.i);
|
|
261
261
|
const stale = staleManagedInstructions(root);
|
|
262
262
|
if (stale.length) {
|
|
263
263
|
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
@@ -295,7 +295,7 @@ async function dev(root, portFlag) {
|
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
297
|
if (config.jam) {
|
|
298
|
-
const { startJam } = await import("./daemon-
|
|
298
|
+
const { startJam } = await import("./daemon-Cb4JjpgL.mjs");
|
|
299
299
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
300
300
|
if (jam) {
|
|
301
301
|
const close = server.close.bind(server);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as DEFAULTS, c as detectHost, i as writeManifest, l as readJson, n as scanFrames, s as detectAgent } from "./manifest-CpbsqQ_v.mjs";
|
|
3
3
|
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
6
6
|
//#region src/cli/init.ts
|
|
7
7
|
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
8
8
|
function init(root, opts) {
|
|
@@ -221,7 +221,7 @@ function init(root, opts) {
|
|
|
221
221
|
created.push("design/scenes/demo/ (3 frames)");
|
|
222
222
|
}
|
|
223
223
|
if (host.tsconfigSweepsDesign) patchTsconfigExclude(root);
|
|
224
|
-
writeManifest(root, scanFrames(root));
|
|
224
|
+
writeManifest(root, scanFrames(root, { name: basename(root) }));
|
|
225
225
|
console.log(`\n${NAME} initialized (${opts.mode} mode). Created:`);
|
|
226
226
|
for (const f of created) console.log(` + ${f}`);
|
|
227
227
|
if (host.router === "next") console.log(`\n note: Next.js support is partial - frames render outside Next, so next/font, next/image and Server Components do not exist inside them (details in design/AGENTS.md).`);
|
|
@@ -540,6 +540,9 @@ const configTemplate = (mode, jamAgent) => `// ${NAME} config - OPTIONAL. Delete
|
|
|
540
540
|
// relative imports need extensions, tsconfig paths are ignored here.
|
|
541
541
|
export default {
|
|
542
542
|
mode: ${JSON.stringify(mode)},
|
|
543
|
+
// One sentence on what this product is and for whom. It lands in design/manifest.json
|
|
544
|
+
// as the project's description - the first thing a new agent session reads.
|
|
545
|
+
// description: "Analytics for indie SaaS - the founder's Monday-morning read",
|
|
543
546
|
// Device widths for frames and the Devices view. Rename, retune, or uncomment tv.
|
|
544
547
|
viewports: {
|
|
545
548
|
mobile: { width: 390, height: 844 },
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
+
import { d as isBoardName, f as readDescription, i as listBoardFiles, l as buildTree, m as hash, n as checkBoardsDir, o as readRegistry, t as boardFields, u as flatten } from "./boards-BdG1TJwU.mjs";
|
|
2
3
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
6
|
//#region src/server/detect.ts
|
|
7
7
|
function readJson(file) {
|
|
8
8
|
try {
|
|
@@ -261,6 +261,7 @@ async function loadConfig(root) {
|
|
|
261
261
|
themes: Array.isArray(user.themes) && user.themes.length ? user.themes.map(String) : DEFAULTS.themes,
|
|
262
262
|
port: validPort(user.port) ?? DEFAULTS.port,
|
|
263
263
|
zoomSpeed: validZoom(user.zoomSpeed) ?? DEFAULTS.zoomSpeed,
|
|
264
|
+
description: readDescription(user.description),
|
|
264
265
|
share: {
|
|
265
266
|
branding: user.share?.branding !== false,
|
|
266
267
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
@@ -379,8 +380,8 @@ function extractMeta(src) {
|
|
|
379
380
|
if (!m) return {};
|
|
380
381
|
const body = m[1];
|
|
381
382
|
const pick = (key) => {
|
|
382
|
-
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['
|
|
383
|
-
return r ? r[2] : void 0;
|
|
383
|
+
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(?:'([^'\\n]*)'|"([^"\\n]*)"|\`([^\`\\n]*)\`)\\s*(?=[,}\\n]|$)`).exec(body);
|
|
384
|
+
return r ? r[1] ?? r[2] ?? r[3] : void 0;
|
|
384
385
|
};
|
|
385
386
|
const pickBool = (key) => {
|
|
386
387
|
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(true|false)\\b`).exec(body);
|
|
@@ -401,6 +402,8 @@ function extractMeta(src) {
|
|
|
401
402
|
if (intent) out.intent = intent;
|
|
402
403
|
const slide = pickBool("slide");
|
|
403
404
|
if (slide !== void 0) out.slide = slide;
|
|
405
|
+
const description = readDescription(pick("description"));
|
|
406
|
+
if (description) out.description = description;
|
|
404
407
|
return out;
|
|
405
408
|
}
|
|
406
409
|
/** Content-frame detection: LEXICAL by convention - the frame file itself imports
|
|
@@ -473,7 +476,63 @@ function walk(dir, out = []) {
|
|
|
473
476
|
}
|
|
474
477
|
return out;
|
|
475
478
|
}
|
|
476
|
-
|
|
479
|
+
/** A scene's description is the first non-blank line of its `_brief.md` (a leading `#`
|
|
480
|
+
* stripped, a YAML front-matter block skipped) - no new file. */
|
|
481
|
+
function sceneBrief(root, scene) {
|
|
482
|
+
if (!scene) return {};
|
|
483
|
+
const rel = `design/scenes/${scene}/_brief.md`;
|
|
484
|
+
const abs = join(root, ...rel.split("/"));
|
|
485
|
+
if (!existsSync(abs)) return {};
|
|
486
|
+
let description;
|
|
487
|
+
try {
|
|
488
|
+
const lines = readFileSync(abs, "utf8").split(/\r?\n/).map((l) => l.trim());
|
|
489
|
+
let i = 0;
|
|
490
|
+
if (lines[0] === "---") {
|
|
491
|
+
i = lines.indexOf("---", 1) + 1;
|
|
492
|
+
if (i === 0) i = lines.length;
|
|
493
|
+
}
|
|
494
|
+
const line = lines.slice(i).find((l) => l);
|
|
495
|
+
description = readDescription(line?.replace(/^#+\s*/, ""));
|
|
496
|
+
} catch {}
|
|
497
|
+
return {
|
|
498
|
+
brief: rel,
|
|
499
|
+
...description ? { description } : {}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** The sidebar as files say it is, for the manifest: folders in root order, boards in
|
|
503
|
+
* reading order with their folder and description. A boards dir we may not read (symlink)
|
|
504
|
+
* or a malformed registry yields nothing here - the API and the build say why; the
|
|
505
|
+
* orientation file must never fail to write. */
|
|
506
|
+
function scanBoards(root) {
|
|
507
|
+
const dir = join(root, "design", "boards");
|
|
508
|
+
if (checkBoardsDir(root, dir)) return {};
|
|
509
|
+
const reg = readRegistry(dir);
|
|
510
|
+
if (reg.state === "malformed") return {};
|
|
511
|
+
const rows = listBoardFiles(dir).boards.map((b) => ({
|
|
512
|
+
name: b.name,
|
|
513
|
+
...boardFields(b.json, isBoardName)
|
|
514
|
+
}));
|
|
515
|
+
const tree = buildTree(rows, reg.folders);
|
|
516
|
+
const folderOf = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const it of tree) if (it.kind === "folder") for (const b of it.boards) folderOf.set(b, it.name);
|
|
518
|
+
const folders = tree.filter((it) => it.kind === "folder").map((it) => ({
|
|
519
|
+
name: it.name,
|
|
520
|
+
...it.description ? { description: it.description } : {}
|
|
521
|
+
}));
|
|
522
|
+
const boards = flatten(tree).map((name) => {
|
|
523
|
+
const d = rows.find((r) => r.name === name)?.description;
|
|
524
|
+
return {
|
|
525
|
+
name,
|
|
526
|
+
...folderOf.has(name) ? { folder: folderOf.get(name) } : {},
|
|
527
|
+
...d ? { description: d } : {}
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
...folders.length ? { folders } : {},
|
|
532
|
+
...boards.length ? { boards } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function scanFrames(root, project) {
|
|
477
536
|
const design = join(root, "design");
|
|
478
537
|
const frames = [];
|
|
479
538
|
for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
|
|
@@ -502,6 +561,7 @@ function scanFrames(root) {
|
|
|
502
561
|
if (meta.of) entry.variantGroup = meta.of;
|
|
503
562
|
if (meta.variant) entry.variant = meta.variant;
|
|
504
563
|
if (meta.slide) entry.slide = true;
|
|
564
|
+
if (meta.description) entry.description = meta.description;
|
|
505
565
|
const content = contentScan(src);
|
|
506
566
|
if (content || meta.intent) {
|
|
507
567
|
entry.intent = meta.intent ?? content.intent;
|
|
@@ -527,12 +587,19 @@ function scanFrames(root) {
|
|
|
527
587
|
inferVariantGroups(frames);
|
|
528
588
|
const sceneCounts = /* @__PURE__ */ new Map();
|
|
529
589
|
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
590
|
+
const scenes = [...sceneCounts.entries()].map(([name, n]) => ({
|
|
591
|
+
name,
|
|
592
|
+
frames: n,
|
|
593
|
+
...sceneBrief(root, name)
|
|
594
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
530
595
|
return {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
596
|
+
...project && (project.name || project.description) ? { project: {
|
|
597
|
+
...project.name ? { name: project.name } : {},
|
|
598
|
+
...project.description ? { description: project.description } : {}
|
|
599
|
+
} } : {},
|
|
600
|
+
...scanBoards(root),
|
|
601
|
+
scenes,
|
|
602
|
+
frames
|
|
536
603
|
};
|
|
537
604
|
}
|
|
538
605
|
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
@@ -614,6 +681,5 @@ function writeManifest(root, manifest) {
|
|
|
614
681
|
writeFileSync(file, next);
|
|
615
682
|
return true;
|
|
616
683
|
}
|
|
617
|
-
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
618
684
|
//#endregion
|
|
619
|
-
export {
|
|
685
|
+
export { DEFAULTS as a, detectHost as c, writeManifest as i, readJson as l, scanFrames as n, loadConfig as o, toFrameId as r, detectAgent as s, affectedFrameIds as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
3
|
+
import { a as nodeExists, c as FOLDERS_FILE, f as readDescription, i as listBoardFiles, m as hash, n as checkBoardsDir, o as readRegistry, p as validateWire, r as isRegularFile, s as BOARD_NAME, t as boardFields } from "./boards-BdG1TJwU.mjs";
|
|
3
4
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { copyFileSync, existsSync, linkSync,
|
|
5
|
+
import { i as writeManifest, n as scanFrames, o as loadConfig, t as affectedFrameIds } from "./manifest-CpbsqQ_v.mjs";
|
|
6
|
+
import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
6
7
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { randomBytes } from "node:crypto";
|
|
9
10
|
//#region src/server/api.ts
|
|
10
|
-
const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
11
11
|
const BODY_LIMIT = 1e6;
|
|
12
12
|
const CSRF_MAX_AGE = 2592e3;
|
|
13
13
|
/** Read one cookie value off the request. */
|
|
@@ -85,37 +85,18 @@ function contained(target, base) {
|
|
|
85
85
|
}
|
|
86
86
|
function apiMiddleware(root, opts = {}) {
|
|
87
87
|
const boardsDir = join(root, "design", "boards");
|
|
88
|
+
const foldersPath = join(boardsDir, FOLDERS_FILE);
|
|
88
89
|
const boardPath = (name) => {
|
|
89
|
-
if (!
|
|
90
|
+
if (!validName(name)) return null;
|
|
91
|
+
if (checkBoardsDir(root, boardsDir)) return null;
|
|
90
92
|
mkdirSync(boardsDir, { recursive: true });
|
|
91
93
|
const p = resolve(boardsDir, `${name}.json`);
|
|
92
94
|
return contained(p, boardsDir) ? p : null;
|
|
93
95
|
};
|
|
94
96
|
const validName = (n) => typeof n === "string" && n.length >= 1 && n.length <= 64 && BOARD_NAME.test(n);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const rd = realpathSync(dir);
|
|
99
|
-
return rd === rr || rd.startsWith(rr + sep);
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const notSymlink = (p) => {
|
|
105
|
-
try {
|
|
106
|
-
return !existsSync(p) || !lstatSync(p).isSymbolicLink();
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const nodeExists = (p) => {
|
|
112
|
-
try {
|
|
113
|
-
lstatSync(p);
|
|
114
|
-
return true;
|
|
115
|
-
} catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
97
|
+
const dirError = () => checkBoardsDir(root, boardsDir);
|
|
98
|
+
const notSymlink = (p) => !nodeExists(p) || isRegularFile(p);
|
|
99
|
+
const nodeExists$1 = nodeExists;
|
|
119
100
|
try {
|
|
120
101
|
for (const f of readdirSync(boardsDir)) if (f.endsWith(".tmp")) rmSync(join(boardsDir, f), { force: true });
|
|
121
102
|
} catch {}
|
|
@@ -144,20 +125,24 @@ function apiMiddleware(root, opts = {}) {
|
|
|
144
125
|
return json(res, 200, { boards: {} });
|
|
145
126
|
}
|
|
146
127
|
if (path === "boards" && req.method === "GET") {
|
|
128
|
+
const de = dirError();
|
|
129
|
+
if (de) return json(res, 422, { error: de });
|
|
147
130
|
mkdirSync(boardsDir, { recursive: true });
|
|
148
|
-
return json(res, 200,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
131
|
+
return json(res, 200, listBoardFiles(boardsDir).boards.map((b) => ({
|
|
132
|
+
name: b.name,
|
|
133
|
+
sha256: b.sha256,
|
|
134
|
+
...boardFields(b.json, validName)
|
|
135
|
+
})));
|
|
136
|
+
}
|
|
137
|
+
if (path === "folders" && req.method === "GET") {
|
|
138
|
+
const de = dirError();
|
|
139
|
+
if (de) return json(res, 422, { error: de });
|
|
140
|
+
const reg = readRegistry(boardsDir);
|
|
141
|
+
if (reg.state === "malformed") return json(res, 422, { error: reg.error });
|
|
142
|
+
return json(res, 200, {
|
|
143
|
+
folders: reg.folders,
|
|
144
|
+
sha256: reg.sha256
|
|
145
|
+
});
|
|
161
146
|
}
|
|
162
147
|
if (path === "boards/rename" && req.method === "POST") {
|
|
163
148
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
@@ -173,21 +158,24 @@ function apiMiddleware(root, opts = {}) {
|
|
|
173
158
|
const { from, to } = parsed;
|
|
174
159
|
if (!validName(from) || !validName(to)) return json(res, 400, { error: "invalid board name" });
|
|
175
160
|
if (to === "all-scenes" || from === "all-scenes" || to === from) return json(res, 400, { error: "invalid rename" });
|
|
176
|
-
|
|
161
|
+
{
|
|
162
|
+
const de = dirError();
|
|
163
|
+
if (de) return json(res, 400, { error: de });
|
|
164
|
+
}
|
|
177
165
|
const fromPath = boardPath(from), toPath = boardPath(to);
|
|
178
166
|
if (!fromPath || !toPath) return json(res, 400, { error: "invalid board name" });
|
|
179
167
|
if (!existsSync(fromPath)) return json(res, 404, { error: `board "${from}" does not exist` });
|
|
180
168
|
if (!notSymlink(fromPath)) return json(res, 400, { error: "refusing to rename a symlinked board file" });
|
|
181
169
|
if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
|
|
182
170
|
if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
|
|
183
|
-
if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
171
|
+
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
184
172
|
try {
|
|
185
173
|
linkSync(fromPath, toPath);
|
|
186
174
|
} catch (err) {
|
|
187
175
|
const code = err.code;
|
|
188
176
|
if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
189
177
|
if (code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
|
|
190
|
-
if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
178
|
+
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
191
179
|
renameSync(fromPath, toPath);
|
|
192
180
|
return json(res, 200, { name: to });
|
|
193
181
|
}
|
|
@@ -205,25 +193,104 @@ function apiMiddleware(root, opts = {}) {
|
|
|
205
193
|
return json(res, 400, { error: "malformed JSON" });
|
|
206
194
|
}
|
|
207
195
|
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
if (!
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
196
|
+
const { tree, base } = parsed;
|
|
197
|
+
const bad = validateWire(tree);
|
|
198
|
+
if (bad) return json(res, 400, { error: bad });
|
|
199
|
+
const b = base;
|
|
200
|
+
if (!b || typeof b !== "object" || !b.boards || typeof b.boards !== "object" || b.folders !== null && typeof b.folders !== "string") return json(res, 400, { error: "expected base hashes" });
|
|
201
|
+
{
|
|
202
|
+
const de = dirError();
|
|
203
|
+
if (de) return json(res, 400, { error: de });
|
|
204
|
+
}
|
|
205
|
+
const reg = readRegistry(boardsDir);
|
|
206
|
+
if (reg.state === "malformed") return json(res, 422, { error: reg.error });
|
|
207
|
+
if (reg.sha256 !== b.folders) return json(res, 409, {
|
|
208
|
+
error: "folders changed on disk",
|
|
209
|
+
stale: [FOLDERS_FILE]
|
|
210
|
+
});
|
|
211
|
+
const unseen = listBoardFiles(boardsDir).boards.map((x) => x.name).filter((n) => !(n in b.boards));
|
|
212
|
+
if (unseen.length) return json(res, 409, {
|
|
213
|
+
error: "boards changed on disk",
|
|
214
|
+
stale: unseen
|
|
215
|
+
});
|
|
216
|
+
const plan = [];
|
|
217
|
+
const stale = [];
|
|
218
|
+
const consider = (name, order, folder) => {
|
|
219
|
+
const p = boardPath(name);
|
|
220
|
+
if (!p || !isRegularFile(p)) {
|
|
221
|
+
stale.push(name);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const content = readFileSync(p, "utf8");
|
|
225
|
+
if (b.boards[name] !== hash(content)) {
|
|
226
|
+
stale.push(name);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
216
229
|
let obj;
|
|
217
230
|
try {
|
|
218
|
-
obj = JSON.parse(
|
|
231
|
+
obj = JSON.parse(content);
|
|
219
232
|
} catch {
|
|
233
|
+
return `board "${name}" is not valid JSON - fix the file`;
|
|
234
|
+
}
|
|
235
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `board "${name}" is not a JSON object - fix the file`;
|
|
236
|
+
plan.push({
|
|
237
|
+
name,
|
|
238
|
+
path: p,
|
|
239
|
+
obj,
|
|
240
|
+
order,
|
|
241
|
+
folder
|
|
242
|
+
});
|
|
243
|
+
return null;
|
|
244
|
+
};
|
|
245
|
+
const folders = [];
|
|
246
|
+
for (const [i, it] of tree.entries()) {
|
|
247
|
+
if (typeof it === "string") {
|
|
248
|
+
const e = consider(it, i, null);
|
|
249
|
+
if (e) return json(res, 422, { error: e });
|
|
220
250
|
continue;
|
|
221
251
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
const description = readDescription(it.description);
|
|
253
|
+
folders.push({
|
|
254
|
+
name: it.folder,
|
|
255
|
+
order: i,
|
|
256
|
+
...description ? { description } : {}
|
|
257
|
+
});
|
|
258
|
+
for (const [j, kid] of it.boards.entries()) {
|
|
259
|
+
const e = consider(kid, j, it.folder);
|
|
260
|
+
if (e) return json(res, 422, { error: e });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (stale.length) return json(res, 409, {
|
|
264
|
+
error: "boards changed on disk",
|
|
265
|
+
stale
|
|
266
|
+
});
|
|
267
|
+
const sha256 = {};
|
|
268
|
+
for (const w of plan) {
|
|
269
|
+
if (w.obj.order === w.order && (w.folder ? w.obj.folder === w.folder : w.obj.folder === void 0)) continue;
|
|
270
|
+
w.obj.order = w.order;
|
|
271
|
+
if (w.folder) w.obj.folder = w.folder;
|
|
272
|
+
else delete w.obj.folder;
|
|
273
|
+
const next = JSON.stringify(w.obj, null, 2) + "\n";
|
|
274
|
+
atomicWrite(w.path, next);
|
|
275
|
+
sha256[w.name] = hash(next);
|
|
225
276
|
}
|
|
226
|
-
|
|
277
|
+
mkdirSync(boardsDir, { recursive: true });
|
|
278
|
+
let foldersSha = null;
|
|
279
|
+
if (folders.length) {
|
|
280
|
+
const next = JSON.stringify({
|
|
281
|
+
version: 1,
|
|
282
|
+
folders
|
|
283
|
+
}, null, 2) + "\n";
|
|
284
|
+
atomicWrite(foldersPath, next);
|
|
285
|
+
foldersSha = hash(next);
|
|
286
|
+
} else rmSync(foldersPath, { force: true });
|
|
287
|
+
return json(res, 200, {
|
|
288
|
+
ok: true,
|
|
289
|
+
sha256: {
|
|
290
|
+
boards: sha256,
|
|
291
|
+
folders: foldersSha
|
|
292
|
+
}
|
|
293
|
+
});
|
|
227
294
|
}
|
|
228
295
|
const boardMatch = /^boards\/([^/]+)$/.exec(path);
|
|
229
296
|
if (boardMatch) {
|
|
@@ -268,9 +335,11 @@ function apiMiddleware(root, opts = {}) {
|
|
|
268
335
|
}
|
|
269
336
|
mkdirSync(boardsDir, { recursive: true });
|
|
270
337
|
const incoming = body.board;
|
|
271
|
-
if (incoming && typeof incoming === "object" &&
|
|
272
|
-
const
|
|
273
|
-
if (typeof
|
|
338
|
+
if (incoming && typeof incoming === "object" && current) try {
|
|
339
|
+
const disk = JSON.parse(current);
|
|
340
|
+
if (incoming.order === void 0 && typeof disk.order === "number" && Number.isFinite(disk.order)) incoming.order = disk.order;
|
|
341
|
+
if (incoming.folder === void 0 && validName(disk.folder)) incoming.folder = disk.folder;
|
|
342
|
+
if (incoming.description === void 0 && readDescription(disk.description)) incoming.description = disk.description;
|
|
274
343
|
} catch {}
|
|
275
344
|
const next2 = JSON.stringify(body.board, null, 2) + "\n";
|
|
276
345
|
atomicWrite(p, next2);
|
|
@@ -294,7 +363,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
294
363
|
};
|
|
295
364
|
const asPng = url.searchParams.get("format") === "png";
|
|
296
365
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
297
|
-
const { shootFrame } = await import("./shot-
|
|
366
|
+
const { shootFrame } = await import("./shot-CwmHO5T4.mjs");
|
|
298
367
|
const r = await shootFrame({
|
|
299
368
|
root,
|
|
300
369
|
viewports: opts.viewports ?? {},
|
|
@@ -328,8 +397,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
328
397
|
if (path === "poster" && req.method === "GET") {
|
|
329
398
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
330
399
|
const src = url.searchParams.get("src") ?? "";
|
|
331
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
332
|
-
const { isLocalAssetRef } = await import("./build-
|
|
400
|
+
const { ensurePoster, isLocalClip } = await import("./poster-DOY7pax8.mjs");
|
|
401
|
+
const { isLocalAssetRef } = await import("./build-Ct0iMoSi.mjs");
|
|
333
402
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
334
403
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
335
404
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -611,7 +680,12 @@ const VIRTUAL_CONFIG = "virtual:sh-config";
|
|
|
611
680
|
const VIRTUAL_DATA = "virtual:sh-data";
|
|
612
681
|
function marverPlugin(ctx) {
|
|
613
682
|
const { root, clientDir, config } = ctx;
|
|
614
|
-
let
|
|
683
|
+
let project = {
|
|
684
|
+
name: config.share.name || basename(root),
|
|
685
|
+
description: config.description
|
|
686
|
+
};
|
|
687
|
+
const projectInfo = () => project;
|
|
688
|
+
let manifest = scanFrames(root, projectInfo());
|
|
615
689
|
let devServer = null;
|
|
616
690
|
const bootId = String(process.hrtime.bigint());
|
|
617
691
|
let invRev = 0;
|
|
@@ -752,9 +826,13 @@ function marverPlugin(ctx) {
|
|
|
752
826
|
}));
|
|
753
827
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
754
828
|
devServer = server;
|
|
829
|
+
let framesKey = JSON.stringify(manifest.frames);
|
|
755
830
|
const regen = debounce(() => {
|
|
756
|
-
manifest = scanFrames(root);
|
|
757
|
-
|
|
831
|
+
manifest = scanFrames(root, projectInfo());
|
|
832
|
+
const changed = writeManifest(root, manifest);
|
|
833
|
+
const key = JSON.stringify(manifest.frames);
|
|
834
|
+
if (changed && key !== framesKey) server.ws.send("sh:manifest", manifest);
|
|
835
|
+
framesKey = key;
|
|
758
836
|
}, 150);
|
|
759
837
|
const watched = [join(root, "design", "scenes"), join(root, "design", "components")];
|
|
760
838
|
const inScope = (file) => watched.some((w) => file.startsWith(w));
|
|
@@ -791,14 +869,31 @@ function marverPlugin(ctx) {
|
|
|
791
869
|
rescanTheme();
|
|
792
870
|
}
|
|
793
871
|
});
|
|
794
|
-
server.watcher.on("change", (f) => inScope(f) && /\.(tsx|jsx)$/.test(f) && regen());
|
|
795
|
-
|
|
872
|
+
server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md")) && regen());
|
|
873
|
+
const configFile = join(root, "design", "config.ts");
|
|
874
|
+
server.watcher.on("change", (f) => {
|
|
875
|
+
if (f !== configFile) return;
|
|
876
|
+
loadConfig(root).then((c) => {
|
|
877
|
+
project = {
|
|
878
|
+
name: c.share.name || basename(root),
|
|
879
|
+
description: c.description
|
|
880
|
+
};
|
|
881
|
+
regen();
|
|
882
|
+
}).catch(() => {});
|
|
883
|
+
});
|
|
884
|
+
writeManifest(root, manifest);
|
|
796
885
|
const boardsDir = join(root, "design", "boards");
|
|
797
886
|
try {
|
|
798
887
|
mkdirSync(boardsDir, { recursive: true });
|
|
799
888
|
const timers = /* @__PURE__ */ new Map();
|
|
889
|
+
let listTimer;
|
|
800
890
|
const watcher = watch(boardsDir, (_event, file) => {
|
|
801
891
|
if (!file || !file.endsWith(".json")) return;
|
|
892
|
+
clearTimeout(listTimer);
|
|
893
|
+
listTimer = setTimeout(() => {
|
|
894
|
+
server.ws.send("sh:boards", {});
|
|
895
|
+
regen();
|
|
896
|
+
}, 150);
|
|
802
897
|
clearTimeout(timers.get(file));
|
|
803
898
|
timers.set(file, setTimeout(() => {
|
|
804
899
|
timers.delete(file);
|
|
@@ -849,7 +944,7 @@ function marverPlugin(ctx) {
|
|
|
849
944
|
});
|
|
850
945
|
return;
|
|
851
946
|
}
|
|
852
|
-
const { shootFrame } = await import("./shot-
|
|
947
|
+
const { shootFrame } = await import("./shot-CwmHO5T4.mjs");
|
|
853
948
|
write(await shootFrame({
|
|
854
949
|
root,
|
|
855
950
|
viewports: config.viewports,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capture, findChrome } from "./shot-
|
|
1
|
+
import { capture, findChrome } from "./shot-CwmHO5T4.mjs";
|
|
2
2
|
import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -95,8 +95,8 @@ async function shootFrame(opts) {
|
|
|
95
95
|
};
|
|
96
96
|
const plan = planShot(frame, viewports, size);
|
|
97
97
|
if (frame.kind !== "html") try {
|
|
98
|
-
const { scanAssetRefs } = await import("./build-
|
|
99
|
-
const { ensurePoster } = await import("./poster-
|
|
98
|
+
const { scanAssetRefs } = await import("./build-Ct0iMoSi.mjs");
|
|
99
|
+
const { ensurePoster } = await import("./poster-DOY7pax8.mjs");
|
|
100
100
|
const refs = scanAssetRefs(readFileSync(join(root, frame.file), "utf8"), frame.file);
|
|
101
101
|
for (const r of refs) if (r.endsWith(".poster.png")) await ensurePoster(join(root, "design", "assets"), r.slice(0, -11));
|
|
102
102
|
} catch {}
|
package/docs/publish.md
CHANGED
|
@@ -31,7 +31,10 @@ opens the same view on the published site, as long as its board and frame shippe
|
|
|
31
31
|
|
|
32
32
|
**What ships**: the boards you list, the frames they reference, and the host `public/`
|
|
33
33
|
directory in full (the `--boards` filter covers frames, not public assets). A flow you
|
|
34
|
-
publish must have all its `data-goto` targets on a published board.
|
|
34
|
+
publish must have all its `data-goto` targets on a published board. Sidebar folders ship
|
|
35
|
+
only around published boards (a folder with nothing published never reaches the bundle),
|
|
36
|
+
and `description`s ship only for published things - the project's, the published boards',
|
|
37
|
+
their folders', scenes' and frames'.
|
|
35
38
|
|
|
36
39
|
## Who can open your canvas
|
|
37
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components - comment @marver and your own coding agent does the work. The tool ships no AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|