@marver-design/marver 0.15.0 → 0.16.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 +101 -0
- package/README.md +2 -1
- package/dist/boards-BmxcT3Lc.mjs +290 -0
- package/dist/boards-PuVzw5Wp.mjs +62 -0
- package/dist/{build-DfuTQZlY.mjs → build-B4yPgFNF.mjs} +72 -29
- package/dist/cli.mjs +33 -13
- package/dist/{daemon-DalgvoA9.mjs → daemon-Bbh_jmui.mjs} +1 -1
- package/dist/{dev-BxCmeU_H.mjs → dev-DH2W7Ffw.mjs} +4 -4
- package/dist/{init-QKNi9gvF.mjs → init-B7YhcN2o.mjs} +6 -3
- package/dist/{manifest-BzxSMoDB.mjs → manifest-CaslQIAO.mjs} +175 -12
- package/dist/{plugin-DJyjmQeh.mjs → plugin-BeBGu3gH.mjs} +239 -82
- package/dist/{poster-CbpzSzJu.mjs → poster-CoyobbGW.mjs} +1 -1
- package/dist/{shot-BWhoz6cU.mjs → shot-z-d-zMzf.mjs} +2 -2
- package/docs/publish.md +4 -1
- package/package.json +1 -1
- package/src/client/shell/App.tsx +45 -259
- package/src/client/shell/BoardList.tsx +438 -0
- package/src/client/shell/ContextMenu.tsx +59 -0
- package/src/client/shell/LockedApp.tsx +2 -1
- package/src/client/shell/Play.tsx +1 -0
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/labels.ts +4 -4
- package/src/client/shell/store.ts +156 -51
- package/src/client/shell/styles.css +40 -4
- package/src/shared/board-tree.ts +343 -0
- package/templates/AGENTS-embedded.md +25 -5
- package/templates/AGENTS-studio.md +25 -5
- package/templates/instructions/boards.md +91 -2
- package/templates/instructions/craft.md +4 -0
- package/templates/instructions/discover.md +20 -3
- 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,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 listBoardFiles, c as BOARD_NAME, g as hash, h as validateWire, i as isRegularFile, l as FOLDERS_FILE, m as readTitle, n as checkBoardsDir, o as nodeExists, p as readDescription, s as readRegistry, t as boardFields } from "./boards-BmxcT3Lc.mjs";
|
|
3
4
|
import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
|
|
4
|
-
import { a as writeManifest, n as
|
|
5
|
-
import { copyFileSync, existsSync, linkSync,
|
|
5
|
+
import { a as writeManifest, n as scanFrames, r as setSceneTitle, s as loadConfig, t as affectedFrameIds } from "./manifest-CaslQIAO.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" });
|
|
@@ -170,29 +155,89 @@ function apiMiddleware(root, opts = {}) {
|
|
|
170
155
|
return json(res, 400, { error: "malformed JSON" });
|
|
171
156
|
}
|
|
172
157
|
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
173
|
-
const { from, to } = parsed;
|
|
158
|
+
const { from, to: toRaw, title: titleRaw, baseHash } = parsed;
|
|
159
|
+
const to = toRaw === void 0 ? from : toRaw;
|
|
174
160
|
if (!validName(from) || !validName(to)) return json(res, 400, { error: "invalid board name" });
|
|
175
|
-
if (
|
|
176
|
-
if (
|
|
161
|
+
if (titleRaw !== void 0 && (typeof titleRaw !== "string" || Array.from(titleRaw).length > 120)) return json(res, 400, { error: "invalid title" });
|
|
162
|
+
if (baseHash !== void 0 && typeof baseHash !== "string") return json(res, 400, { error: "invalid baseHash" });
|
|
163
|
+
if (to === "all-scenes" || from === "all-scenes") return json(res, 400, { error: "invalid rename" });
|
|
164
|
+
if (to === from && titleRaw === void 0) return json(res, 400, { error: "invalid rename" });
|
|
165
|
+
{
|
|
166
|
+
const de = dirError();
|
|
167
|
+
if (de) return json(res, 400, { error: de });
|
|
168
|
+
}
|
|
177
169
|
const fromPath = boardPath(from), toPath = boardPath(to);
|
|
178
170
|
if (!fromPath || !toPath) return json(res, 400, { error: "invalid board name" });
|
|
179
171
|
if (!existsSync(fromPath)) return json(res, 404, { error: `board "${from}" does not exist` });
|
|
180
172
|
if (!notSymlink(fromPath)) return json(res, 400, { error: "refusing to rename a symlinked board file" });
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
173
|
+
const current = readFileSync(fromPath, "utf8");
|
|
174
|
+
if (baseHash !== void 0 && baseHash !== hash(current)) return json(res, 409, {
|
|
175
|
+
error: "board changed on disk",
|
|
176
|
+
sha256: hash(current)
|
|
177
|
+
});
|
|
178
|
+
let obj = null;
|
|
179
|
+
if (titleRaw !== void 0) {
|
|
180
|
+
let parsedBoard;
|
|
181
|
+
try {
|
|
182
|
+
parsedBoard = JSON.parse(current);
|
|
183
|
+
} catch {
|
|
184
|
+
return json(res, 422, { error: `board "${from}" is not valid JSON - fix the file` });
|
|
185
|
+
}
|
|
186
|
+
if (!parsedBoard || typeof parsedBoard !== "object" || Array.isArray(parsedBoard)) return json(res, 422, { error: `board "${from}" is not a JSON object - fix the file` });
|
|
187
|
+
obj = parsedBoard;
|
|
188
|
+
}
|
|
189
|
+
let next;
|
|
190
|
+
if (obj) {
|
|
191
|
+
const title = readTitle(titleRaw);
|
|
192
|
+
if (title) obj.title = title;
|
|
193
|
+
else delete obj.title;
|
|
194
|
+
next = JSON.stringify(obj, null, 2) + "\n";
|
|
195
|
+
}
|
|
196
|
+
if (to !== from) {
|
|
197
|
+
if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
|
|
198
|
+
if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
|
|
199
|
+
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
200
|
+
try {
|
|
201
|
+
if (next !== void 0) writeFileSync(toPath, next, { flag: "wx" });
|
|
202
|
+
else linkSync(fromPath, toPath);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const code = err.code;
|
|
205
|
+
if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
206
|
+
if (next !== void 0 || code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
|
|
207
|
+
if (nodeExists$1(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
208
|
+
renameSync(fromPath, toPath);
|
|
209
|
+
return json(res, 200, { name: to });
|
|
210
|
+
}
|
|
211
|
+
rmSync(fromPath);
|
|
212
|
+
return json(res, 200, {
|
|
213
|
+
name: to,
|
|
214
|
+
...next !== void 0 ? { sha256: hash(next) } : {}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
atomicWrite(toPath, next);
|
|
218
|
+
return json(res, 200, {
|
|
219
|
+
name: to,
|
|
220
|
+
sha256: hash(next)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (path === "scenes/rename" && req.method === "POST") {
|
|
224
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
225
|
+
const raw = await readBody(req);
|
|
226
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
227
|
+
let parsed;
|
|
184
228
|
try {
|
|
185
|
-
|
|
186
|
-
} catch
|
|
187
|
-
|
|
188
|
-
if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
189
|
-
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` });
|
|
191
|
-
renameSync(fromPath, toPath);
|
|
192
|
-
return json(res, 200, { name: to });
|
|
229
|
+
parsed = JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
193
232
|
}
|
|
194
|
-
|
|
195
|
-
|
|
233
|
+
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
234
|
+
const { scene, title } = parsed;
|
|
235
|
+
if (typeof scene !== "string" || !scene || scene.length > 128 || /[\\/]/.test(scene) || scene === "." || scene === "..") return json(res, 400, { error: "invalid scene name" });
|
|
236
|
+
if (typeof title !== "string" || Array.from(title).length > 120) return json(res, 400, { error: "invalid title" });
|
|
237
|
+
if (!scanFrames(root).scenes.some((sc) => sc.name === scene)) return json(res, 400, { error: `scene "${scene}" does not exist (no frames under design/scenes/${scene}/)` });
|
|
238
|
+
const e = setSceneTitle(root, scene, readTitle(title) ?? "");
|
|
239
|
+
if (e) return json(res, 400, { error: e });
|
|
240
|
+
return json(res, 200, { ok: true });
|
|
196
241
|
}
|
|
197
242
|
if (path === "boards/reorder" && req.method === "POST") {
|
|
198
243
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
@@ -205,25 +250,105 @@ function apiMiddleware(root, opts = {}) {
|
|
|
205
250
|
return json(res, 400, { error: "malformed JSON" });
|
|
206
251
|
}
|
|
207
252
|
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 (
|
|
253
|
+
const { tree, base } = parsed;
|
|
254
|
+
const bad = validateWire(tree);
|
|
255
|
+
if (bad) return json(res, 400, { error: bad });
|
|
256
|
+
const b = base;
|
|
257
|
+
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" });
|
|
258
|
+
{
|
|
259
|
+
const de = dirError();
|
|
260
|
+
if (de) return json(res, 400, { error: de });
|
|
261
|
+
}
|
|
262
|
+
const reg = readRegistry(boardsDir);
|
|
263
|
+
if (reg.state === "malformed") return json(res, 422, { error: reg.error });
|
|
264
|
+
if (reg.sha256 !== b.folders) return json(res, 409, {
|
|
265
|
+
error: "folders changed on disk",
|
|
266
|
+
stale: [FOLDERS_FILE]
|
|
267
|
+
});
|
|
268
|
+
const unseen = listBoardFiles(boardsDir).boards.map((x) => x.name).filter((n) => !(n in b.boards));
|
|
269
|
+
if (unseen.length) return json(res, 409, {
|
|
270
|
+
error: "boards changed on disk",
|
|
271
|
+
stale: unseen
|
|
272
|
+
});
|
|
273
|
+
const plan = [];
|
|
274
|
+
const stale = [];
|
|
275
|
+
const consider = (name, order, folder) => {
|
|
276
|
+
const p = boardPath(name);
|
|
277
|
+
if (!p || !isRegularFile(p)) {
|
|
278
|
+
stale.push(name);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const content = readFileSync(p, "utf8");
|
|
282
|
+
if (b.boards[name] !== hash(content)) {
|
|
283
|
+
stale.push(name);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
216
286
|
let obj;
|
|
217
287
|
try {
|
|
218
|
-
obj = JSON.parse(
|
|
288
|
+
obj = JSON.parse(content);
|
|
219
289
|
} catch {
|
|
290
|
+
return `board "${name}" is not valid JSON - fix the file`;
|
|
291
|
+
}
|
|
292
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `board "${name}" is not a JSON object - fix the file`;
|
|
293
|
+
plan.push({
|
|
294
|
+
name,
|
|
295
|
+
path: p,
|
|
296
|
+
obj,
|
|
297
|
+
order,
|
|
298
|
+
folder
|
|
299
|
+
});
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
const folders = [];
|
|
303
|
+
for (const [i, it] of tree.entries()) {
|
|
304
|
+
if (typeof it === "string") {
|
|
305
|
+
const e = consider(it, i, null);
|
|
306
|
+
if (e) return json(res, 422, { error: e });
|
|
220
307
|
continue;
|
|
221
308
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
309
|
+
const title = readTitle(it.title), description = readDescription(it.description);
|
|
310
|
+
folders.push({
|
|
311
|
+
name: it.folder,
|
|
312
|
+
order: i,
|
|
313
|
+
...title ? { title } : {},
|
|
314
|
+
...description ? { description } : {}
|
|
315
|
+
});
|
|
316
|
+
for (const [j, kid] of it.boards.entries()) {
|
|
317
|
+
const e = consider(kid, j, it.folder);
|
|
318
|
+
if (e) return json(res, 422, { error: e });
|
|
319
|
+
}
|
|
225
320
|
}
|
|
226
|
-
return json(res,
|
|
321
|
+
if (stale.length) return json(res, 409, {
|
|
322
|
+
error: "boards changed on disk",
|
|
323
|
+
stale
|
|
324
|
+
});
|
|
325
|
+
const sha256 = {};
|
|
326
|
+
for (const w of plan) {
|
|
327
|
+
if (w.obj.order === w.order && (w.folder ? w.obj.folder === w.folder : w.obj.folder === void 0)) continue;
|
|
328
|
+
w.obj.order = w.order;
|
|
329
|
+
if (w.folder) w.obj.folder = w.folder;
|
|
330
|
+
else delete w.obj.folder;
|
|
331
|
+
const next = JSON.stringify(w.obj, null, 2) + "\n";
|
|
332
|
+
atomicWrite(w.path, next);
|
|
333
|
+
sha256[w.name] = hash(next);
|
|
334
|
+
}
|
|
335
|
+
mkdirSync(boardsDir, { recursive: true });
|
|
336
|
+
let foldersSha = null;
|
|
337
|
+
if (folders.length) {
|
|
338
|
+
const next = JSON.stringify({
|
|
339
|
+
version: 1,
|
|
340
|
+
folders
|
|
341
|
+
}, null, 2) + "\n";
|
|
342
|
+
atomicWrite(foldersPath, next);
|
|
343
|
+
foldersSha = hash(next);
|
|
344
|
+
} else rmSync(foldersPath, { force: true });
|
|
345
|
+
return json(res, 200, {
|
|
346
|
+
ok: true,
|
|
347
|
+
sha256: {
|
|
348
|
+
boards: sha256,
|
|
349
|
+
folders: foldersSha
|
|
350
|
+
}
|
|
351
|
+
});
|
|
227
352
|
}
|
|
228
353
|
const boardMatch = /^boards\/([^/]+)$/.exec(path);
|
|
229
354
|
if (boardMatch) {
|
|
@@ -268,9 +393,12 @@ function apiMiddleware(root, opts = {}) {
|
|
|
268
393
|
}
|
|
269
394
|
mkdirSync(boardsDir, { recursive: true });
|
|
270
395
|
const incoming = body.board;
|
|
271
|
-
if (incoming && typeof incoming === "object" &&
|
|
272
|
-
const
|
|
273
|
-
if (typeof
|
|
396
|
+
if (incoming && typeof incoming === "object" && current) try {
|
|
397
|
+
const disk = JSON.parse(current);
|
|
398
|
+
if (incoming.order === void 0 && typeof disk.order === "number" && Number.isFinite(disk.order)) incoming.order = disk.order;
|
|
399
|
+
if (incoming.folder === void 0 && validName(disk.folder)) incoming.folder = disk.folder;
|
|
400
|
+
if (incoming.title === void 0 && readTitle(disk.title)) incoming.title = disk.title;
|
|
401
|
+
if (incoming.description === void 0 && readDescription(disk.description)) incoming.description = disk.description;
|
|
274
402
|
} catch {}
|
|
275
403
|
const next2 = JSON.stringify(body.board, null, 2) + "\n";
|
|
276
404
|
atomicWrite(p, next2);
|
|
@@ -294,7 +422,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
294
422
|
};
|
|
295
423
|
const asPng = url.searchParams.get("format") === "png";
|
|
296
424
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
297
|
-
const { shootFrame } = await import("./shot-
|
|
425
|
+
const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
|
|
298
426
|
const r = await shootFrame({
|
|
299
427
|
root,
|
|
300
428
|
viewports: opts.viewports ?? {},
|
|
@@ -328,8 +456,8 @@ function apiMiddleware(root, opts = {}) {
|
|
|
328
456
|
if (path === "poster" && req.method === "GET") {
|
|
329
457
|
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
330
458
|
const src = url.searchParams.get("src") ?? "";
|
|
331
|
-
const { ensurePoster, isLocalClip } = await import("./poster-
|
|
332
|
-
const { isLocalAssetRef } = await import("./build-
|
|
459
|
+
const { ensurePoster, isLocalClip } = await import("./poster-CoyobbGW.mjs");
|
|
460
|
+
const { isLocalAssetRef } = await import("./build-B4yPgFNF.mjs");
|
|
333
461
|
if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
|
|
334
462
|
const r = await ensurePoster(join(root, "design", "assets"), src);
|
|
335
463
|
if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
|
|
@@ -611,7 +739,12 @@ const VIRTUAL_CONFIG = "virtual:sh-config";
|
|
|
611
739
|
const VIRTUAL_DATA = "virtual:sh-data";
|
|
612
740
|
function marverPlugin(ctx) {
|
|
613
741
|
const { root, clientDir, config } = ctx;
|
|
614
|
-
let
|
|
742
|
+
let project = {
|
|
743
|
+
name: config.share.name || basename(root),
|
|
744
|
+
description: config.description
|
|
745
|
+
};
|
|
746
|
+
const projectInfo = () => project;
|
|
747
|
+
let manifest = scanFrames(root, projectInfo());
|
|
615
748
|
let devServer = null;
|
|
616
749
|
const bootId = String(process.hrtime.bigint());
|
|
617
750
|
let invRev = 0;
|
|
@@ -752,9 +885,16 @@ function marverPlugin(ctx) {
|
|
|
752
885
|
}));
|
|
753
886
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
754
887
|
devServer = server;
|
|
888
|
+
let framesKey = JSON.stringify(manifest.frames);
|
|
889
|
+
let scenesKey = JSON.stringify(manifest.scenes);
|
|
755
890
|
const regen = debounce(() => {
|
|
756
|
-
manifest = scanFrames(root);
|
|
757
|
-
|
|
891
|
+
manifest = scanFrames(root, projectInfo());
|
|
892
|
+
const changed = writeManifest(root, manifest);
|
|
893
|
+
const key = JSON.stringify(manifest.frames), sKey = JSON.stringify(manifest.scenes);
|
|
894
|
+
if (changed && key !== framesKey) server.ws.send("sh:manifest", manifest);
|
|
895
|
+
else if (changed && sKey !== scenesKey) server.ws.send("sh:scenes", { scenes: manifest.scenes });
|
|
896
|
+
framesKey = key;
|
|
897
|
+
scenesKey = sKey;
|
|
758
898
|
}, 150);
|
|
759
899
|
const watched = [join(root, "design", "scenes"), join(root, "design", "components")];
|
|
760
900
|
const inScope = (file) => watched.some((w) => file.startsWith(w));
|
|
@@ -791,14 +931,31 @@ function marverPlugin(ctx) {
|
|
|
791
931
|
rescanTheme();
|
|
792
932
|
}
|
|
793
933
|
});
|
|
794
|
-
server.watcher.on("change", (f) => inScope(f) && /\.(tsx|jsx)$/.test(f) && regen());
|
|
795
|
-
|
|
934
|
+
server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md")) && regen());
|
|
935
|
+
const configFile = join(root, "design", "config.ts");
|
|
936
|
+
server.watcher.on("change", (f) => {
|
|
937
|
+
if (f !== configFile) return;
|
|
938
|
+
loadConfig(root).then((c) => {
|
|
939
|
+
project = {
|
|
940
|
+
name: c.share.name || basename(root),
|
|
941
|
+
description: c.description
|
|
942
|
+
};
|
|
943
|
+
regen();
|
|
944
|
+
}).catch(() => {});
|
|
945
|
+
});
|
|
946
|
+
writeManifest(root, manifest);
|
|
796
947
|
const boardsDir = join(root, "design", "boards");
|
|
797
948
|
try {
|
|
798
949
|
mkdirSync(boardsDir, { recursive: true });
|
|
799
950
|
const timers = /* @__PURE__ */ new Map();
|
|
951
|
+
let listTimer;
|
|
800
952
|
const watcher = watch(boardsDir, (_event, file) => {
|
|
801
953
|
if (!file || !file.endsWith(".json")) return;
|
|
954
|
+
clearTimeout(listTimer);
|
|
955
|
+
listTimer = setTimeout(() => {
|
|
956
|
+
server.ws.send("sh:boards", {});
|
|
957
|
+
regen();
|
|
958
|
+
}, 150);
|
|
802
959
|
clearTimeout(timers.get(file));
|
|
803
960
|
timers.set(file, setTimeout(() => {
|
|
804
961
|
timers.delete(file);
|
|
@@ -849,7 +1006,7 @@ function marverPlugin(ctx) {
|
|
|
849
1006
|
});
|
|
850
1007
|
return;
|
|
851
1008
|
}
|
|
852
|
-
const { shootFrame } = await import("./shot-
|
|
1009
|
+
const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
|
|
853
1010
|
write(await shootFrame({
|
|
854
1011
|
root,
|
|
855
1012
|
viewports: config.viewports,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capture, findChrome } from "./shot-
|
|
1
|
+
import { capture, findChrome } from "./shot-z-d-zMzf.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-B4yPgFNF.mjs");
|
|
99
|
+
const { ensurePoster } = await import("./poster-CoyobbGW.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 `title`s and `description`s ship only for published things - the project's, the
|
|
37
|
+
published boards', 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.1",
|
|
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,
|