@marver-design/marver 0.8.0 → 0.8.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 +97 -5
- package/README.md +72 -16
- package/dist/{auth-B36fMCM3.mjs → auth-KQ9Aj-nB.mjs} +1 -1
- package/dist/{build-B8z902TW.mjs → build-BBVQRetk.mjs} +5 -5
- package/dist/cli.mjs +16 -7
- package/dist/{collab-pLGzSvm5.mjs → collab-s3k5byM1.mjs} +3 -3
- package/dist/{comments-DSwMhdbZ.mjs → comments-BZBKhKRO.mjs} +2 -2
- package/dist/{comments-BrpC86Kf.mjs → comments-J06jqCVV.mjs} +3 -3
- package/dist/{daemon-C-huGHAM.mjs → daemon-DkyNOwIt.mjs} +30 -51
- package/dist/{dev-BYm9ebfN.mjs → dev-BTAhTie-.mjs} +19 -5
- package/dist/{init-DrLomSWq.mjs → init-DWdhjJD5.mjs} +16 -3
- package/dist/{ledger-wFvEIEGi.mjs → ledger-CbzTJrV2.mjs} +1 -1
- package/dist/{manifest-D3eaARf4.mjs → manifest-B4zcDGBf.mjs} +3 -3
- package/dist/{plugin-BVFuRfEo.mjs → plugin-BdQEeTLg.mjs} +41 -9
- package/dist/{serve-CwAfayJk.mjs → serve-CZqPnj19.mjs} +32 -8
- package/dist/{sync-Ch4Bymb1.mjs → sync-BJKKmy1n.mjs} +2 -2
- package/dist/work-CLrmY-vQ.mjs +97 -0
- package/dist/work-lzC-lPY0.mjs +76 -0
- package/package.json +16 -1
- package/src/client/const.ts +1 -1
- package/src/client/content/diagram.tsx +1 -1
- package/src/client/content/index.tsx +2 -2
- package/src/client/content/md.ts +1 -1
- package/src/client/content/palette.ts +2 -2
- package/src/client/frame-host/bridge.js +1 -1
- package/src/client/frame-host/inspect.js +1 -1
- package/src/client/frame-host/serialize.ts +2 -2
- package/src/client/shell/App.tsx +79 -22
- package/src/client/shell/Comments.tsx +24 -18
- package/src/client/shell/Play.tsx +2 -2
- package/src/client/shell/canvas/Canvas.tsx +1 -1
- package/src/client/shell/canvas/FrameNode.tsx +16 -11
- package/src/client/shell/canvas/snapshots.ts +3 -3
- package/src/client/shell/comments-store.ts +115 -42
- package/src/client/shell/hash.ts +2 -2
- package/src/client/shell/icons.tsx +1 -1
- package/src/client/shell/keys.ts +39 -0
- package/src/client/shell/mentions.ts +1 -1
- package/src/client/shell/perf.ts +1 -1
- package/src/client/shell/store.ts +99 -39
- package/src/client/shell/styles.css +12 -12
- package/src/client/shell/tidy.ts +5 -5
- package/src/client/stage/main.tsx +2 -2
- package/src/shared/events.ts +2 -2
- package/src/shared/utm.ts +22 -0
- package/templates/AGENTS-embedded.md +48 -1
- package/templates/AGENTS-studio.md +48 -1
- package/templates/instructions/publish.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import { o as loadConfig, s as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { o as loadConfig, s as detectHost } from "./manifest-B4zcDGBf.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BdQEeTLg.mjs";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
@@ -141,7 +141,7 @@ async function dev(root, portFlag) {
|
|
|
141
141
|
const port = typeof addr === "object" && addr ? addr.port : picked.port;
|
|
142
142
|
if (picked.fellBack) console.log(`\n port ${desiredPort} is in use - serving "${projectName}" on ${port} instead`);
|
|
143
143
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
144
|
-
const { loadCollab, syncOnce } = await import("./sync-
|
|
144
|
+
const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
|
|
145
145
|
if (loadCollab(root)) console.log(` comments: syncing with the published canvas (design/.local/collab.json)\n`);
|
|
146
146
|
let syncing = false;
|
|
147
147
|
const tick = async () => {
|
|
@@ -156,9 +156,23 @@ async function dev(root, portFlag) {
|
|
|
156
156
|
};
|
|
157
157
|
tick();
|
|
158
158
|
setInterval(tick, 3e4).unref();
|
|
159
|
+
const { workActivity, writeDevInfo, removeDevInfo } = await import("./work-CLrmY-vQ.mjs");
|
|
160
|
+
const unsubscribe = workActivity.onChange((frames) => server.ws.send("sh:jam-activity", { frames }));
|
|
161
|
+
const sweep = setInterval(() => workActivity.sweep(), 15e3);
|
|
162
|
+
sweep.unref?.();
|
|
163
|
+
writeDevInfo(root, port);
|
|
164
|
+
{
|
|
165
|
+
const close = server.close.bind(server);
|
|
166
|
+
server.close = (async () => {
|
|
167
|
+
unsubscribe();
|
|
168
|
+
clearInterval(sweep);
|
|
169
|
+
removeDevInfo(root);
|
|
170
|
+
return close();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
159
173
|
if (config.jam?.agent) {
|
|
160
|
-
const { startJam } = await import("./daemon-
|
|
161
|
-
const jam = startJam(root, config.jam, (m) => console.log(m), (
|
|
174
|
+
const { startJam } = await import("./daemon-DkyNOwIt.mjs");
|
|
175
|
+
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
162
176
|
if (jam) {
|
|
163
177
|
const close = server.close.bind(server);
|
|
164
178
|
server.close = (async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as DEFAULTS, c as readJson, i as writeManifest, r as scanFrames, s as detectHost } from "./manifest-
|
|
2
|
+
import { a as DEFAULTS, c as readJson, i as writeManifest, r as scanFrames, s as detectHost } from "./manifest-B4zcDGBf.mjs";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -105,6 +105,18 @@ function init(root, opts) {
|
|
|
105
105
|
created.push("CLAUDE.md (updated)");
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
const rootAgentsMd = join(root, "AGENTS.md");
|
|
109
|
+
const pointerLine = `Before any design or canvas work, read design/AGENTS.md and follow it - it is binding.`;
|
|
110
|
+
if (!existsSync(rootAgentsMd)) {
|
|
111
|
+
writeFileSync(rootAgentsMd, `# ${NAME}\n\nThis repo designs with ${NAME}. ${pointerLine}\n`);
|
|
112
|
+
created.push("AGENTS.md");
|
|
113
|
+
} else {
|
|
114
|
+
const cur = readFileSync(rootAgentsMd, "utf8");
|
|
115
|
+
if (!cur.includes("design/AGENTS.md")) {
|
|
116
|
+
writeFileSync(rootAgentsMd, cur.replace(/\n*$/, "") + `\n\n${pointerLine}\n`);
|
|
117
|
+
created.push("AGENTS.md (updated)");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
108
120
|
const instrRoot = join(templates, "instructions");
|
|
109
121
|
for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
|
|
110
122
|
for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) writeManaged(`instructions/${e.name}/${f}`, readFileSync(join(instrRoot, e.name, f), "utf8"));
|
|
@@ -152,7 +164,8 @@ function init(root, opts) {
|
|
|
152
164
|
writeFileSync(join(design, "boards", ".gitkeep"), "");
|
|
153
165
|
created.push("design/boards/");
|
|
154
166
|
}
|
|
155
|
-
|
|
167
|
+
const sceneDirs = existsSync(join(design, "scenes")) ? readdirSync(join(design, "scenes"), { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_")).length : 0;
|
|
168
|
+
if (opts.demo && sceneDirs === 0 && !existsSync(join(design, "scenes", "demo"))) {
|
|
156
169
|
cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
|
|
157
170
|
created.push("design/scenes/demo/ (3 frames)");
|
|
158
171
|
}
|
|
@@ -174,7 +187,7 @@ function init(root, opts) {
|
|
|
174
187
|
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
175
188
|
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
176
189
|
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.`);
|
|
177
|
-
console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
190
|
+
console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
178
191
|
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
179
192
|
}
|
|
180
193
|
const MANAGED_PREFIX = "<!-- marver:managed ";
|
|
@@ -14,7 +14,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/server/jam/ledger.ts
|
|
16
16
|
/**
|
|
17
|
-
* The device-bound authorization ledger
|
|
17
|
+
* The device-bound authorization ledger - the whole trust boundary.
|
|
18
18
|
*
|
|
19
19
|
* When the dev POST accepts an owner-gated write, it records that event's id here. The
|
|
20
20
|
* daemon's owner-trigger check is `has(root, id)`, never a synced field: sync copies
|
|
@@ -214,7 +214,7 @@ function validViewports(v) {
|
|
|
214
214
|
//#region src/server/manifest.ts
|
|
215
215
|
const FRAME_EXT = /\.(tsx|jsx|html)$/;
|
|
216
216
|
const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
|
|
217
|
-
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted
|
|
217
|
+
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
|
|
218
218
|
function extractMeta(src) {
|
|
219
219
|
const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
|
|
220
220
|
if (!m) return {};
|
|
@@ -238,7 +238,7 @@ function extractMeta(src) {
|
|
|
238
238
|
if (intent) out.intent = intent;
|
|
239
239
|
return out;
|
|
240
240
|
}
|
|
241
|
-
/** Content-frame detection
|
|
241
|
+
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
242
242
|
* itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
|
|
243
243
|
* string in a UI frame can never misbadge it; barrels/re-exports are not
|
|
244
244
|
* detected - meta.intent is the taught path and always works). Returns the
|
|
@@ -358,7 +358,7 @@ function scanFrames(root) {
|
|
|
358
358
|
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
359
359
|
};
|
|
360
360
|
}
|
|
361
|
-
/** Variant groups
|
|
361
|
+
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
362
362
|
* are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
|
|
363
363
|
* directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
|
|
364
364
|
* Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
2
2
|
import { n as localProfile, t as isConnected } from "./profile-BkiWglVE.mjs";
|
|
3
|
-
import { i as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-
|
|
3
|
+
import { i as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-B4zcDGBf.mjs";
|
|
4
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -14,7 +14,7 @@ function cookie(req, name) {
|
|
|
14
14
|
const m = new RegExp(`(?:^|;\\s*)${name}=([^;]+)`).exec(String(req.headers.cookie ?? ""));
|
|
15
15
|
return m ? m[1] : "";
|
|
16
16
|
}
|
|
17
|
-
/** The owner gate
|
|
17
|
+
/** The owner gate. The dev server is 127.0.0.1-only, so the live threat is a
|
|
18
18
|
* cross-origin drive-by page firing a POST at localhost. Two defenses, mirroring the published
|
|
19
19
|
* side (collab.ts:206): a double-submit cookie (mv_c is JS-readable same-origin, so a cross-origin
|
|
20
20
|
* page cannot read it to echo x-mv-c), plus an Origin allowlist. Only a POST that passes this is
|
|
@@ -185,7 +185,7 @@ function apiMiddleware(root) {
|
|
|
185
185
|
}
|
|
186
186
|
const dir = join(root, "design", ".local");
|
|
187
187
|
mkdirSync(dir, { recursive: true });
|
|
188
|
-
const { validAvatar } = await import("./collab-
|
|
188
|
+
const { validAvatar } = await import("./collab-s3k5byM1.mjs");
|
|
189
189
|
let cur = {};
|
|
190
190
|
try {
|
|
191
191
|
cur = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8"));
|
|
@@ -199,8 +199,40 @@ function apiMiddleware(root) {
|
|
|
199
199
|
atomicWrite(join(dir, "profile.json"), JSON.stringify(prof, null, 2) + "\n");
|
|
200
200
|
return json(res, 200, { user: localProfile(root) });
|
|
201
201
|
}
|
|
202
|
+
if (path === "work") {
|
|
203
|
+
const { workActivity, readDevInfo, WORK_TTL_DEFAULT, WORK_TTL_MAX } = await import("./work-CLrmY-vQ.mjs");
|
|
204
|
+
const tokenOk = () => {
|
|
205
|
+
const t = readDevInfo(root)?.token;
|
|
206
|
+
return !!t && req.headers["x-mv-work"] === t;
|
|
207
|
+
};
|
|
208
|
+
if (req.method === "GET") {
|
|
209
|
+
if (!ownerGated(req) && !tokenOk()) return json(res, 403, { error: "forbidden" });
|
|
210
|
+
return json(res, 200, { frames: workActivity.active() });
|
|
211
|
+
}
|
|
212
|
+
if (req.method === "POST") {
|
|
213
|
+
if (!ownerGated(req) && !tokenOk()) return json(res, 403, { error: "forbidden" });
|
|
214
|
+
const raw = await readBody(req);
|
|
215
|
+
if (raw == null) return json(res, 400, { error: "body too large" });
|
|
216
|
+
let b;
|
|
217
|
+
try {
|
|
218
|
+
b = JSON.parse(raw);
|
|
219
|
+
} catch {
|
|
220
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
221
|
+
}
|
|
222
|
+
const frames = Array.isArray(b.frames) ? b.frames.filter((f) => typeof f === "string" && f) : [];
|
|
223
|
+
const on = b.on !== false;
|
|
224
|
+
if (!on && b.all === true) {
|
|
225
|
+
workActivity.clearAll("cli");
|
|
226
|
+
return json(res, 200, { frames: workActivity.active() });
|
|
227
|
+
}
|
|
228
|
+
if (!frames.length) return json(res, 400, { error: "frames required" });
|
|
229
|
+
const ttl = Math.min(Math.max(Number(b.ttlMs) || WORK_TTL_DEFAULT, 1e4), WORK_TTL_MAX);
|
|
230
|
+
for (const f of frames) on ? workActivity.mark(f, ttl, "cli") : workActivity.clear(f, "cli");
|
|
231
|
+
return json(res, 200, { frames: workActivity.active() });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
202
234
|
if (path === "boards.rights" && req.method === "GET") {
|
|
203
|
-
const { listBoards } = await import("./comments-
|
|
235
|
+
const { listBoards } = await import("./comments-BZBKhKRO.mjs");
|
|
204
236
|
return json(res, 200, {
|
|
205
237
|
local: true,
|
|
206
238
|
stored: listBoards(join(root, "design", "comments"))
|
|
@@ -208,7 +240,7 @@ function apiMiddleware(root) {
|
|
|
208
240
|
}
|
|
209
241
|
const cm = /^comments\/([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
210
242
|
if (cm) {
|
|
211
|
-
const { appendEvents, readLog } = await import("./comments-
|
|
243
|
+
const { appendEvents, readLog } = await import("./comments-BZBKhKRO.mjs");
|
|
212
244
|
const dir = join(root, "design", "comments");
|
|
213
245
|
if (req.method === "GET") return json(res, 200, { events: readLog(dir, cm[1]) });
|
|
214
246
|
if (req.method === "POST") {
|
|
@@ -237,14 +269,14 @@ function apiMiddleware(root) {
|
|
|
237
269
|
};
|
|
238
270
|
});
|
|
239
271
|
const fresh = appendEvents(dir, cm[1], stamped);
|
|
240
|
-
const { record } = await import("./ledger-
|
|
272
|
+
const { record } = await import("./ledger-CbzTJrV2.mjs").then((n) => n.n);
|
|
241
273
|
for (const ev of fresh) if (ev.type === "create" || ev.type === "reply") record(root, cm[1], ev.id);
|
|
242
274
|
backgroundPush(root);
|
|
243
275
|
return json(res, 200, { accepted: fresh.length });
|
|
244
276
|
}
|
|
245
277
|
}
|
|
246
278
|
if (path === "sync" && req.method === "POST") {
|
|
247
|
-
const { loadCollab, syncOnce } = await import("./sync-
|
|
279
|
+
const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
|
|
248
280
|
const collab = loadCollab(root);
|
|
249
281
|
if (!collab) return json(res, 200, { connected: false });
|
|
250
282
|
try {
|
|
@@ -273,7 +305,7 @@ function backgroundPush(root) {
|
|
|
273
305
|
pushTimer = setTimeout(async () => {
|
|
274
306
|
pushTimer = null;
|
|
275
307
|
try {
|
|
276
|
-
const { loadCollab, syncOnce } = await import("./sync-
|
|
308
|
+
const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
|
|
277
309
|
const collab = loadCollab(root);
|
|
278
310
|
if (collab) await syncOnce(root, collab);
|
|
279
311
|
} catch {}
|
|
@@ -454,7 +486,7 @@ function marverPlugin(ctx) {
|
|
|
454
486
|
zoomSpeed: config.zoomSpeed,
|
|
455
487
|
noTheme: themeFile() == null,
|
|
456
488
|
setup: setupPending,
|
|
457
|
-
projectName: basename(root)
|
|
489
|
+
projectName: config.share.name || basename(root)
|
|
458
490
|
})}`;
|
|
459
491
|
}
|
|
460
492
|
if (id === "\0virtual:sh-data") return "export default null";
|
|
@@ -3,10 +3,29 @@ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { createHmac, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
+
//#region src/shared/utm.ts
|
|
7
|
+
/** Automatic attribution for the powered-by links: every canvas tags its marver.design
|
|
8
|
+
* link so site analytics can see WHICH canvas sent a visitor - zero user setup.
|
|
9
|
+
* Conventional UTM semantics (stable groupings first, the variable last):
|
|
10
|
+
* utm_source = the surface class ('published-canvas' | 'dev-canvas')
|
|
11
|
+
* utm_medium = the link unit ('powered-by')
|
|
12
|
+
* utm_campaign = THIS canvas's name, slugged ("Marver tour" -> "marver-tour")
|
|
13
|
+
* utm_content = the placement ('gate' badge | 'shell' wordmark)
|
|
14
|
+
*/
|
|
15
|
+
function poweredByUrl(canvasName, source, content) {
|
|
16
|
+
const slug = (canvasName ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
17
|
+
return `https://marver.design/?${new URLSearchParams({
|
|
18
|
+
utm_source: source,
|
|
19
|
+
utm_medium: "powered-by",
|
|
20
|
+
...slug ? { utm_campaign: slug } : {},
|
|
21
|
+
utm_content: content
|
|
22
|
+
}).toString()}`;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
6
25
|
//#region src/server/serve.ts
|
|
7
26
|
/**
|
|
8
27
|
* `marver serve` - a zero-dependency static server for design/.dist with an optional
|
|
9
|
-
* password gate
|
|
28
|
+
* password gate. No MARVER_PASSWORD → plain static serving. With it, the
|
|
10
29
|
* bundle is never sent pre-auth: every unauthenticated request gets the gate page, auth
|
|
11
30
|
* is a POST compare + an HMAC-signed 30-day cookie keyed off the password itself (all
|
|
12
31
|
* instances agree, nothing stored server-side).
|
|
@@ -49,9 +68,9 @@ async function serve(root, portFlag) {
|
|
|
49
68
|
let bearerCheck = null;
|
|
50
69
|
let sessionCheck = null;
|
|
51
70
|
if (process.env.MARVER_DATA_DIR) {
|
|
52
|
-
const { collabHandler } = await import("./collab-
|
|
53
|
-
const { appendEvents, readLog } = await import("./comments-
|
|
54
|
-
const { dataDir } = await import("./comments-
|
|
71
|
+
const { collabHandler } = await import("./collab-s3k5byM1.mjs");
|
|
72
|
+
const { appendEvents, readLog } = await import("./comments-BZBKhKRO.mjs");
|
|
73
|
+
const { dataDir } = await import("./comments-BZBKhKRO.mjs");
|
|
55
74
|
const dir = dataDir();
|
|
56
75
|
const seedDir = join(dist, "design", "comments");
|
|
57
76
|
if (existsSync(seedDir)) for (const f of readdirSync(seedDir)) {
|
|
@@ -60,7 +79,7 @@ async function serve(root, portFlag) {
|
|
|
60
79
|
appendEvents(join(dir, "comments"), board, readLog(seedDir, board));
|
|
61
80
|
}
|
|
62
81
|
collab = collabHandler(dir, dist);
|
|
63
|
-
const { loadStore, createInvite, normEmail, sessionUser } = await import("./auth-
|
|
82
|
+
const { loadStore, createInvite, normEmail, sessionUser } = await import("./auth-KQ9Aj-nB.mjs");
|
|
64
83
|
bearerCheck = (token) => sessionUser(dir, token);
|
|
65
84
|
sessionCheck = (req) => {
|
|
66
85
|
const tok = /(?:^|;\s*)mv_s=([\w-]+)/.exec(String(req.headers.cookie ?? ""))?.[1];
|
|
@@ -128,7 +147,12 @@ async function serve(root, portFlag) {
|
|
|
128
147
|
if (!bearerOk && !preGate) return gate(res, meta, !!collab);
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
|
-
if (
|
|
150
|
+
if (url.pathname.startsWith("/__mv/api/")) {
|
|
151
|
+
if (!collab) {
|
|
152
|
+
res.statusCode = 404;
|
|
153
|
+
res.setHeader("content-type", "application/json");
|
|
154
|
+
return res.end("{\"error\":\"collaboration is not enabled on this canvas\"}");
|
|
155
|
+
}
|
|
132
156
|
collab(req, res, url).then((handled) => {
|
|
133
157
|
if (!handled) {
|
|
134
158
|
res.statusCode = 404;
|
|
@@ -188,7 +212,7 @@ const MARK_AT = (size) => `<svg viewBox="0 0 256 256" width="${size}" height="${
|
|
|
188
212
|
const MARK = MARK_AT(17);
|
|
189
213
|
const MARK_LG = MARK_AT(24);
|
|
190
214
|
/** The gate: the canvas's own antechamber - light ground, dot grid, glass card in the
|
|
191
|
-
* shell's exact token language. Three doors, one credential each
|
|
215
|
+
* shell's exact token language. Three doors, one credential each:
|
|
192
216
|
* guests pay the canvas password, members sign in with their OWN password, and an
|
|
193
217
|
* invite link (#/i/<token>) opens straight into the claim. The member and claim
|
|
194
218
|
* states exist only on collaboration canvases - a static canvas keeps one field. */
|
|
@@ -423,7 +447,7 @@ ${collabOn ? `
|
|
|
423
447
|
` : ""}
|
|
424
448
|
})()
|
|
425
449
|
<\/script>
|
|
426
|
-
${meta.branding ? `<footer><a href="
|
|
450
|
+
${meta.branding ? `<footer><a href="${poweredByUrl(meta.name, "published-canvas", "gate")}" target="_blank" rel="noopener">${MARK} <span>Powered by <span class="md">Marver.design</span></span> <svg class="up" viewBox="0 0 256 256" width="11" height="11" fill="currentColor" aria-hidden><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"/></svg></a></footer>` : ""}
|
|
427
451
|
</main></body></html>`);
|
|
428
452
|
}
|
|
429
453
|
const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { t as diffEvents } from "./events-BMtBvvgU.mjs";
|
|
2
|
-
import { appendEvents, listBoards, readLog } from "./comments-
|
|
2
|
+
import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
//#region src/server/sync.ts
|
|
6
6
|
/**
|
|
7
|
-
* Dev ↔ published comment sync
|
|
7
|
+
* Dev ↔ published comment sync - one merge rule, run from the dev side.
|
|
8
8
|
*
|
|
9
9
|
* The published serve is the canonical home; the repo's design/comments/ is the
|
|
10
10
|
* mirror the agent reads. Each exchange is: pull remote events → union into local
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
//#region src/server/jam/activity.ts
|
|
5
|
+
function createActivity(ttlMs = 9e4) {
|
|
6
|
+
const m = /* @__PURE__ */ new Map();
|
|
7
|
+
const cbs = [];
|
|
8
|
+
const frames = () => [...new Set([...m.keys()].map((k) => k.slice(k.indexOf("|") + 1)))];
|
|
9
|
+
const emit = () => {
|
|
10
|
+
const f = frames();
|
|
11
|
+
for (const cb of cbs) cb(f);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
mark(frame, ttl, src = "cli") {
|
|
15
|
+
if (!frame) return;
|
|
16
|
+
m.set(`${src}|${frame}`, Date.now() + (ttl ?? ttlMs));
|
|
17
|
+
emit();
|
|
18
|
+
},
|
|
19
|
+
clear(frame, src = "cli") {
|
|
20
|
+
if (m.delete(`${src}|${frame}`)) emit();
|
|
21
|
+
},
|
|
22
|
+
clearAll(src = "cli") {
|
|
23
|
+
let changed = false;
|
|
24
|
+
for (const k of [...m.keys()]) if (k.startsWith(`${src}|`)) {
|
|
25
|
+
m.delete(k);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
if (changed) emit();
|
|
29
|
+
},
|
|
30
|
+
active: frames,
|
|
31
|
+
sweep() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
let changed = false;
|
|
34
|
+
for (const [k, until] of m) if (until <= now) {
|
|
35
|
+
m.delete(k);
|
|
36
|
+
changed = true;
|
|
37
|
+
}
|
|
38
|
+
if (changed) emit();
|
|
39
|
+
},
|
|
40
|
+
onChange(fn) {
|
|
41
|
+
cbs.push(fn);
|
|
42
|
+
return () => {
|
|
43
|
+
const i = cbs.indexOf(fn);
|
|
44
|
+
if (i !== -1) cbs.splice(i, 1);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/server/work.ts
|
|
51
|
+
/**
|
|
52
|
+
* The working-state rail for CHAT-driven agents (the jam daemon has its own in-process
|
|
53
|
+
* path into the same set). A coding agent that just accepted a request creates the frame
|
|
54
|
+
* files first, then `npx marver work start <frame...>` - the canvas shows the live
|
|
55
|
+
* working shimmer before a single component exists. `marver work done` clears it.
|
|
56
|
+
*
|
|
57
|
+
* Transport: the dev server writes design/.local/dev.json ({port, token}) at boot; the
|
|
58
|
+
* CLI reads it to find the server and authenticate. The token makes the endpoint
|
|
59
|
+
* unreachable from a drive-by browser page (which cannot read local files), while any
|
|
60
|
+
* process that can read the repo - the owner's own tools - is trusted by definition.
|
|
61
|
+
* Presence itself NEVER touches disk (activity.ts).
|
|
62
|
+
*/
|
|
63
|
+
/** One per process - the dev server, the jam daemon, and the API all share it. */
|
|
64
|
+
const workActivity = createActivity();
|
|
65
|
+
/** The longest a CLI mark may glow unrefreshed - a forgotten `done` self-heals. */
|
|
66
|
+
const WORK_TTL_MAX = 18e5;
|
|
67
|
+
const WORK_TTL_DEFAULT = 6e5;
|
|
68
|
+
const infoPath = (root) => join(root, "design", ".local", "dev.json");
|
|
69
|
+
/** Written once per boot (dev.ts), removed on close - the CLI's discovery + credential. */
|
|
70
|
+
function writeDevInfo(root, port) {
|
|
71
|
+
const token = randomBytes(24).toString("base64url");
|
|
72
|
+
mkdirSync(join(root, "design", ".local"), { recursive: true });
|
|
73
|
+
writeFileSync(infoPath(root), JSON.stringify({
|
|
74
|
+
port,
|
|
75
|
+
token,
|
|
76
|
+
ts: Date.now()
|
|
77
|
+
}, null, 2) + "\n");
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
function removeDevInfo(root) {
|
|
81
|
+
try {
|
|
82
|
+
rmSync(infoPath(root), { force: true });
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
function readDevInfo(root) {
|
|
86
|
+
try {
|
|
87
|
+
const v = JSON.parse(readFileSync(infoPath(root), "utf8"));
|
|
88
|
+
return typeof v?.port === "number" && typeof v?.token === "string" ? {
|
|
89
|
+
port: v.port,
|
|
90
|
+
token: v.token
|
|
91
|
+
} : null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo, removeDevInfo, workActivity, writeDevInfo };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { n as NAME } from "./cli.mjs";
|
|
2
|
+
import { WORK_TTL_DEFAULT, WORK_TTL_MAX, readDevInfo } from "./work-CLrmY-vQ.mjs";
|
|
3
|
+
//#region src/cli/work.ts
|
|
4
|
+
/**
|
|
5
|
+
* `marver work` - the coding agent's hand on the canvas working state.
|
|
6
|
+
*
|
|
7
|
+
* work start <scene/frame ...> [--ttl <minutes>] mark frames actively working
|
|
8
|
+
* work done <scene/frame ...> | --all clear the glow
|
|
9
|
+
* work list what is glowing right now
|
|
10
|
+
*
|
|
11
|
+
* The intended choreography (taught in design/AGENTS.md): on accepting a request,
|
|
12
|
+
* create the frame FILES first (name, meta, a minimal skeleton), pin them on the
|
|
13
|
+
* right board, `work start` them - the human sees the request land on the canvas
|
|
14
|
+
* within seconds - build (independent frames in parallel, one subagent each), then
|
|
15
|
+
* `work done`. Marks self-expire (default 10 min, max 30) so a crashed agent can
|
|
16
|
+
* never leave a frame glowing forever.
|
|
17
|
+
*/
|
|
18
|
+
async function workCommand(root, action, frames, opts) {
|
|
19
|
+
const info = readDevInfo(root);
|
|
20
|
+
if (!info) throw new Error(`\`${NAME} dev\` is not running in this repo (design/.local/dev.json not found) - start it first.`);
|
|
21
|
+
const call = async (method, body) => {
|
|
22
|
+
let res;
|
|
23
|
+
try {
|
|
24
|
+
res = await fetch(`http://localhost:${info.port}/__mv/api/work`, {
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
"x-mv-work": info.token
|
|
29
|
+
},
|
|
30
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`could not reach \`${NAME} dev\` on port ${info.port} - is it still running?`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json().catch(() => ({}));
|
|
36
|
+
if (res.status === 403) throw new Error(`the dev server refused the token - it likely restarted; try again (design/.local/dev.json is re-read each run)`);
|
|
37
|
+
if (!res.ok) throw new Error(String(data?.error ?? `work ${action} failed (${res.status})`));
|
|
38
|
+
if (!Array.isArray(data?.frames)) throw new Error(`port ${info.port} did not answer like \`${NAME} dev\` - is it still running?`);
|
|
39
|
+
return data;
|
|
40
|
+
};
|
|
41
|
+
switch (action) {
|
|
42
|
+
case "start": {
|
|
43
|
+
if (!frames.length) throw new Error("name the frames: work start <scene/frame ...>");
|
|
44
|
+
const min = Number(opts.ttl);
|
|
45
|
+
const ttlMs = Number.isFinite(min) && min > 0 ? Math.min(Math.max(min * 6e4, 1e4), WORK_TTL_MAX) : WORK_TTL_DEFAULT;
|
|
46
|
+
const { frames: active } = await call("POST", {
|
|
47
|
+
frames,
|
|
48
|
+
on: true,
|
|
49
|
+
ttlMs
|
|
50
|
+
});
|
|
51
|
+
const pretty = ttlMs >= 6e4 ? `${Math.round(ttlMs / 6e4)} min` : `${Math.round(ttlMs / 1e3)} s`;
|
|
52
|
+
console.log(`working: ${active.join(", ")} (auto-expires in ${pretty} - re-run to extend, \`work done\` to clear)`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case "done": {
|
|
56
|
+
if (!frames.length && !opts.all) throw new Error("name the frames (or --all): work done <scene/frame ...>");
|
|
57
|
+
const { frames: active } = await call("POST", opts.all ? {
|
|
58
|
+
on: false,
|
|
59
|
+
all: true
|
|
60
|
+
} : {
|
|
61
|
+
frames,
|
|
62
|
+
on: false
|
|
63
|
+
});
|
|
64
|
+
console.log(active.length ? `still working: ${active.join(", ")}` : "nothing working - all clear");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case "list": {
|
|
68
|
+
const { frames: active } = await call("GET");
|
|
69
|
+
console.log(active.length ? active.join("\n") : "nothing working");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
default: throw new Error(`unknown action "${action}" - use start · done · list`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { workCommand };
|
package/package.json
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marver-design/marver",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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. The tool ships no AI - your coding agent is the designer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
|
+
"author": "Nic Touron (https://github.com/TNEP4)",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"design",
|
|
11
|
+
"canvas",
|
|
12
|
+
"agent",
|
|
13
|
+
"prototyping",
|
|
14
|
+
"design-tool",
|
|
15
|
+
"react",
|
|
16
|
+
"vite",
|
|
17
|
+
"tailwind"
|
|
18
|
+
],
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/TNEP4/marver/issues"
|
|
21
|
+
},
|
|
8
22
|
"bin": {
|
|
9
23
|
"marver": "dist/cli.mjs"
|
|
10
24
|
},
|
|
@@ -55,6 +69,7 @@
|
|
|
55
69
|
"engines": {
|
|
56
70
|
"node": ">=22.18.0"
|
|
57
71
|
},
|
|
72
|
+
"packageManager": "pnpm@11.0.8",
|
|
58
73
|
"homepage": "https://marver.design",
|
|
59
74
|
"repository": {
|
|
60
75
|
"type": "git",
|
package/src/client/const.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const NAME = 'marver'
|
|
|
4
4
|
export const PKG = '@marver-design/marver' // registry identity; bin stays `marver`
|
|
5
5
|
export const ROUTE = '/__mv'
|
|
6
6
|
|
|
7
|
-
/** Content-frame natural widths
|
|
7
|
+
/** Content-frame natural widths: Doc layout -> own-size width.
|
|
8
8
|
* Shared by the Doc primitive (measurement messages) and the server-side
|
|
9
9
|
* manifest scan (defaultSize for content frames) - one source, no drift. */
|
|
10
10
|
export const CONTENT_WIDTH: Record<string, number> = { document: 760, wide: 1280 }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Diagram block - first-class Mermaid
|
|
2
|
+
* The Diagram block - first-class Mermaid.
|
|
3
3
|
* Mermaid is marver's dependency behind a dynamic import: a workspace with no
|
|
4
4
|
* Diagram loads no mermaid bytes. Source-level theme overrides (%%{init}%%,
|
|
5
5
|
* yaml frontmatter) are stripped so the marver palette always holds; the
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content-frame block primitives
|
|
2
|
+
* Content-frame block primitives: Doc, Row, Col, Space, Md, Diagram, Img.
|
|
3
3
|
* One vocabulary at two scales: boards arrange frames in rows/columns with space
|
|
4
4
|
* units; these primitives do the same for blocks INSIDE a frame - as CSS flex,
|
|
5
|
-
* deliberately NOT the
|
|
5
|
+
* deliberately NOT the board lane-flow grammar (shared vocabulary, not semantics).
|
|
6
6
|
*
|
|
7
7
|
* Doc owns the measurement protocol: it reports its content height so the shell
|
|
8
8
|
* can give the frame a natural size (the shell alone owns node dimensions).
|
package/src/client/content/md.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Markdown rendering for the Md block
|
|
2
|
+
* Markdown rendering for the Md block.
|
|
3
3
|
* Raw HTML is inert (escaped, never parsed). Links and images have SEPARATE
|
|
4
4
|
* policies: links may be goto:/http(s)/mailto (external ones open a new tab,
|
|
5
5
|
* never navigate the iframe); images are local-only - design/assets/ paths.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The marver diagram identity
|
|
2
|
+
* The marver diagram identity: the full Apple system palette (HIG),
|
|
3
3
|
* every value the HIG's own light/dark pair - never inverted. Structure comes
|
|
4
4
|
* from the systemGray ramp, emphasis from blue+purple, series diversity from
|
|
5
5
|
* the 12-color set.
|
|
@@ -43,7 +43,7 @@ export function themeVars(dark: boolean): Record<string, string> {
|
|
|
43
43
|
const purple = dark ? ACCENT.purple.dark : ACCENT.purple.light
|
|
44
44
|
// Nodes are ACCENTED by default - a plain flowchart only ever touches
|
|
45
45
|
// primaryColor, so a gray primary meant every default diagram rendered
|
|
46
|
-
// gray-on-gray
|
|
46
|
+
// gray-on-gray. Structure (lines, clusters,
|
|
47
47
|
// labels) stays grayscale; the shapes carry the marver blue.
|
|
48
48
|
const blueWash = dark ? '#123A5C' : '#E4F0FF'
|
|
49
49
|
const purpleWash = dark ? '#3A1440' : '#F8E4FC'
|
|
@@ -12,7 +12,7 @@ const isHtmlFrame = new URL(import.meta.url).searchParams.get('html') === '1'
|
|
|
12
12
|
const post = (msg) => { if (window.parent !== window) window.parent.postMessage(msg, location.origin) }
|
|
13
13
|
const id = new URLSearchParams(location.search).get('id') ?? location.pathname
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// The shell serialises this frame's DOM (same origin) for the lean facade. Open shadow roots
|
|
16
16
|
// are walkable, but a CLOSED root is invisible after the fact - flag it at creation so the serialiser
|
|
17
17
|
// degrades the frame (keeps it live) instead of shipping a lean copy missing its shadow content.
|
|
18
18
|
const _attachShadow = Element.prototype.attachShadow
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shared frame-inspect controller
|
|
1
|
+
// Shared frame-inspect controller.
|
|
2
2
|
//
|
|
3
3
|
// Laser outlines, comment-pick, anchor capture/resolve, and a persistent element
|
|
4
4
|
// lock ("highlight") - the review machinery shared by the canvas frame bridge
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The DOM-snapshot serializer. Turns a live, same-origin frame Document into a
|
|
2
2
|
// self-contained STATIC html string: post-render DOM + full inlined CSS (styleSheets +
|
|
3
3
|
// adoptedStyleSheets), JS stripped. Rendered in a `sandbox="allow-same-origin"` (NO allow-scripts)
|
|
4
4
|
// iframe srcdoc, it reflows / theme-flips / device-sweeps with the browser's own layout engine and
|
|
@@ -19,7 +19,7 @@ export interface SerializeResult {
|
|
|
19
19
|
|
|
20
20
|
const STRIP_TAGS = new Set(['SCRIPT', 'NOSCRIPT'])
|
|
21
21
|
|
|
22
|
-
/** Absolutize relative url() in a sheet's cssText against that sheet's href (
|
|
22
|
+
/** Absolutize relative url() in a sheet's cssText against that sheet's href (consolidating
|
|
23
23
|
* rules into one <style> moves the url() base from each sheet to the srcdoc base). */
|
|
24
24
|
function absolutizeUrls(css: string, sheetHref: string | null): string {
|
|
25
25
|
if (!sheetHref) return css
|