@marver-design/marver 0.8.0 → 0.9.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 +168 -5
- package/README.md +73 -16
- package/dist/{auth-B36fMCM3.mjs → auth-KQ9Aj-nB.mjs} +1 -1
- package/dist/{build-B8z902TW.mjs → build-D9gimz6K.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-2Lft_lVV.mjs} +60 -66
- package/dist/{dev-BYm9ebfN.mjs → dev-CTtkqVo_.mjs} +21 -7
- package/dist/{init-DrLomSWq.mjs → init-8Giknvy1.mjs} +35 -7
- package/dist/ledger-BgA7nQoH.mjs +103 -0
- package/dist/{manifest-D3eaARf4.mjs → manifest-DJHU7qfu.mjs} +144 -19
- package/dist/{plugin-BVFuRfEo.mjs → plugin-BrBWk2Qn.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 +17 -2
- 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 +2 -2
- 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 +16 -16
- 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 +49 -2
- package/templates/AGENTS-studio.md +49 -2
- package/templates/instructions/configure.md +6 -1
- package/templates/instructions/jam.md +29 -9
- package/templates/instructions/publish.md +1 -1
- package/templates/instructions/welcome.md +7 -1
- package/dist/ledger-wFvEIEGi.mjs +0 -64
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative, sep } from "node:path";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
//#region src/server/detect.ts
|
|
@@ -125,6 +125,68 @@ function firstExisting(root, candidates) {
|
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
|
+
//#region src/server/jam/agent.ts
|
|
129
|
+
/**
|
|
130
|
+
* Which coding agent drives this repo.
|
|
131
|
+
*
|
|
132
|
+
* Live Jam is ON by default, so this is the answer to "on by default with WHAT" - asked
|
|
133
|
+
* once by `init` (which writes the answer into design/config.ts, where it is visible and
|
|
134
|
+
* editable) and again at every dev boot, so a workspace that predates the block - or a
|
|
135
|
+
* human who switched tools - still jams without editing anything.
|
|
136
|
+
*
|
|
137
|
+
* The tool RUNNING us wins: `init` is usually run BY the agent, so its env markers are
|
|
138
|
+
* evidence rather than a guess. Whatever wins must still be on PATH - the daemon has to
|
|
139
|
+
* spawn it.
|
|
140
|
+
*/
|
|
141
|
+
/** Ordered by preference when a machine has both installed and neither is running us. */
|
|
142
|
+
const AGENTS = ["claude", "codex"];
|
|
143
|
+
/** Env vars each CLI sets in the processes it spawns. Deliberately NOT CODEX_HOME or
|
|
144
|
+
* ANTHROPIC_API_KEY: those are configuration a human exports by hand, not evidence that
|
|
145
|
+
* the tool is running right now. */
|
|
146
|
+
const MARKERS = {
|
|
147
|
+
claude: ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"],
|
|
148
|
+
codex: ["CODEX_SANDBOX", "CODEX_THREAD_ID"]
|
|
149
|
+
};
|
|
150
|
+
/** Is `cmd` an executable FILE on PATH? A direct stat per PATH entry - no spawn, no shell,
|
|
151
|
+
* so this is safe to call on every dev boot. Two deliberate narrowings, both in service of
|
|
152
|
+
* "found means spawnable", because arming an agent that cannot run is worse than staying off:
|
|
153
|
+
*
|
|
154
|
+
* - `isFile`, because a directory carries the execute bit too (it means "traversable"), so an
|
|
155
|
+
* access check alone would call a folder named `claude` an agent.
|
|
156
|
+
* - the bare name only, no PATHEXT: the daemon spawns without a shell, and Node cannot run a
|
|
157
|
+
* Windows `.cmd`/`.bat` shim that way. Finding one would arm a job that fails on every run.
|
|
158
|
+
*
|
|
159
|
+
* Only ABSOLUTE PATH entries count. An empty entry means the current directory on POSIX, and
|
|
160
|
+
* a relative one (`.`, `bin`) resolves against it too - and the current directory is the repo
|
|
161
|
+
* that was just opened, so a `claude` binary shipped inside it is precisely what must never
|
|
162
|
+
* be found and spawned. */
|
|
163
|
+
function onPath(cmd, env = process.env) {
|
|
164
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
165
|
+
if (!isAbsolute(dir)) continue;
|
|
166
|
+
try {
|
|
167
|
+
const file = join(dir, cmd);
|
|
168
|
+
if (statSync(file).isFile()) {
|
|
169
|
+
accessSync(file, constants.X_OK);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
/** The agent to jam with, or undefined when this machine has none installed.
|
|
177
|
+
*
|
|
178
|
+
* One knowingly-imperfect case: agents nest, and env is inherited, so a codex run started
|
|
179
|
+
* FROM Claude Code carries both marker families and this picks claude. Env has no depth to
|
|
180
|
+
* read, and walking the process tree to find the nearest agent ancestor costs more than the
|
|
181
|
+
* case is worth - so the answer is made visible instead of clever: `init` prints the agent
|
|
182
|
+
* it chose and writes it into design/config.ts, and instructions/jam.md has the agent confirm
|
|
183
|
+
* that line names the tool it actually is. One word to correct, once per repo. */
|
|
184
|
+
function detectAgent(env = process.env) {
|
|
185
|
+
const running = AGENTS.find((a) => MARKERS[a].some((k) => env[k]));
|
|
186
|
+
if (running && onPath(running, env)) return running;
|
|
187
|
+
return AGENTS.find((a) => onPath(a, env));
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
128
190
|
//#region src/server/config.ts
|
|
129
191
|
const DEFAULTS = {
|
|
130
192
|
mode: "studio",
|
|
@@ -155,7 +217,10 @@ const DEFAULTS = {
|
|
|
155
217
|
/** Load design/config.ts via native TS import (Node >= 22.18). Missing or broken fields fall back to defaults. */
|
|
156
218
|
async function loadConfig(root) {
|
|
157
219
|
const file = join(root, "design", "config.ts");
|
|
158
|
-
if (!existsSync(file)) return {
|
|
220
|
+
if (!existsSync(file)) return {
|
|
221
|
+
...DEFAULTS,
|
|
222
|
+
...jamFields(void 0)
|
|
223
|
+
};
|
|
159
224
|
try {
|
|
160
225
|
const user = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default ?? {};
|
|
161
226
|
return {
|
|
@@ -170,11 +235,14 @@ async function loadConfig(root) {
|
|
|
170
235
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
171
236
|
logo: typeof user.share?.logo === "string" ? user.share.logo : void 0
|
|
172
237
|
},
|
|
173
|
-
|
|
238
|
+
...jamFields(user.jam)
|
|
174
239
|
};
|
|
175
240
|
} catch (err) {
|
|
176
|
-
console.error(`[marver] design/config.ts failed to load, using defaults:\n ${err.message}`);
|
|
177
|
-
return {
|
|
241
|
+
console.error(`[marver] design/config.ts failed to load, using defaults and leaving Live Jam OFF:\n ${err.message}`);
|
|
242
|
+
return {
|
|
243
|
+
...DEFAULTS,
|
|
244
|
+
jamOff: "unreadable"
|
|
245
|
+
};
|
|
178
246
|
}
|
|
179
247
|
}
|
|
180
248
|
const validDim = (n) => typeof n === "number" && Number.isFinite(n) && n >= 1 && n <= 2e4;
|
|
@@ -184,20 +252,77 @@ function validPort(n) {
|
|
|
184
252
|
function validZoom(n) {
|
|
185
253
|
return typeof n === "number" && Number.isFinite(n) && n >= .1 && n <= 10 ? n : null;
|
|
186
254
|
}
|
|
187
|
-
/**
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
255
|
+
/** Six frames at once is what a jam actually feels like - at 3, half of a multi-frame
|
|
256
|
+
* ask sat waiting on the other half while the human watched. */
|
|
257
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
258
|
+
/** A config value, printable in a warning. Never throws - a formatter that can crash inside an
|
|
259
|
+
* error path (JSON.stringify does, on a BigInt) turns a clear message into a mystery. */
|
|
260
|
+
const show = (v) => {
|
|
261
|
+
try {
|
|
262
|
+
return JSON.stringify(v) ?? String(v);
|
|
263
|
+
} catch {
|
|
264
|
+
return String(v);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
/** A `{...}` written by hand, not a Date/Map/class instance that merely types as "object". */
|
|
268
|
+
const plainObject = (v) => {
|
|
269
|
+
if (!v || typeof v !== "object") return false;
|
|
270
|
+
const proto = Object.getPrototypeOf(v);
|
|
271
|
+
return proto === Object.prototype || proto === null;
|
|
272
|
+
};
|
|
273
|
+
/** The agent to arm with - or, when the human NAMED one we cannot use, why not (said once,
|
|
274
|
+
* here, because a bad `jam.agent` is a config-file error like any other).
|
|
275
|
+
*
|
|
276
|
+
* A named agent is never quietly swapped for another: running a different tool than the one
|
|
277
|
+
* asked for is worse than not running. Nor is one armed that cannot be spawned - that would
|
|
278
|
+
* claim every @marver mention, fail it, and never retry. */
|
|
279
|
+
function resolveAgent(named) {
|
|
280
|
+
if (named === void 0) return detectAgent() ?? "no-agent";
|
|
281
|
+
if (named !== "claude" && named !== "codex") {
|
|
282
|
+
console.warn(`[marver] design/config.ts: jam.agent ${show(named)} is not an agent marver can spawn - Live Jam is off. Use "claude" or "codex".`);
|
|
283
|
+
return "bad-agent";
|
|
284
|
+
}
|
|
285
|
+
if (!onPath(named)) {
|
|
286
|
+
console.warn(`[marver] design/config.ts names jam.agent "${named}", which is not on PATH - Live Jam is off until it is installed.`);
|
|
287
|
+
return "bad-agent";
|
|
288
|
+
}
|
|
289
|
+
return named;
|
|
290
|
+
}
|
|
291
|
+
/** Resolve the jam block. Live Jam is ON by default: an absent or partial `jam` resolves
|
|
292
|
+
* to whatever agent CLI this machine has, so a fresh workspace jams with nothing
|
|
293
|
+
* configured and an old one needs no re-init. `jam: false` is the off switch. */
|
|
294
|
+
function resolveJam(v) {
|
|
295
|
+
if (v === false) return "opted-out";
|
|
296
|
+
const shape = v === void 0 || v === true ? {} : typeof v === "string" ? { agent: v } : plainObject(v) ? v : null;
|
|
297
|
+
if (!shape) {
|
|
298
|
+
console.warn(`[marver] design/config.ts: jam must be false, an agent name, or an options object - got ${show(v)}. Live Jam is off.`);
|
|
299
|
+
return "bad-agent";
|
|
300
|
+
}
|
|
301
|
+
const j = shape;
|
|
302
|
+
const agent = resolveAgent(j.agent);
|
|
303
|
+
if (agent === "no-agent" || agent === "bad-agent") return agent;
|
|
304
|
+
const c = j.concurrency;
|
|
194
305
|
return {
|
|
195
|
-
agent
|
|
196
|
-
concurrency,
|
|
306
|
+
agent,
|
|
307
|
+
concurrency: typeof c === "number" && Number.isInteger(c) && c >= 1 && c <= 16 ? c : DEFAULT_CONCURRENCY,
|
|
197
308
|
subagents: j.subagents !== false,
|
|
198
309
|
proactive: j.proactive === true
|
|
199
310
|
};
|
|
200
311
|
}
|
|
312
|
+
/** Exactly one of `jam` (armed) or `jamOff` (why not) - so the dev server can speak up about
|
|
313
|
+
* the one off-state nothing has reported yet, and stay quiet about the rest. BOTH keys are
|
|
314
|
+
* always written: these spread over the user's own `jam`, and a raw `jam: false` reaching the
|
|
315
|
+
* server as config would arm the daemon against a truthy object check. */
|
|
316
|
+
function jamFields(v) {
|
|
317
|
+
const r = resolveJam(v);
|
|
318
|
+
return typeof r === "string" ? {
|
|
319
|
+
jam: void 0,
|
|
320
|
+
jamOff: r
|
|
321
|
+
} : {
|
|
322
|
+
jam: r,
|
|
323
|
+
jamOff: void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
201
326
|
function validViewports(v) {
|
|
202
327
|
if (!v || typeof v !== "object") return null;
|
|
203
328
|
const out = {};
|
|
@@ -214,7 +339,7 @@ function validViewports(v) {
|
|
|
214
339
|
//#region src/server/manifest.ts
|
|
215
340
|
const FRAME_EXT = /\.(tsx|jsx|html)$/;
|
|
216
341
|
const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
|
|
217
|
-
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted
|
|
342
|
+
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
|
|
218
343
|
function extractMeta(src) {
|
|
219
344
|
const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
|
|
220
345
|
if (!m) return {};
|
|
@@ -238,7 +363,7 @@ function extractMeta(src) {
|
|
|
238
363
|
if (intent) out.intent = intent;
|
|
239
364
|
return out;
|
|
240
365
|
}
|
|
241
|
-
/** Content-frame detection
|
|
366
|
+
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
242
367
|
* itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
|
|
243
368
|
* string in a UI frame can never misbadge it; barrels/re-exports are not
|
|
244
369
|
* detected - meta.intent is the taught path and always works). Returns the
|
|
@@ -358,7 +483,7 @@ function scanFrames(root) {
|
|
|
358
483
|
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
359
484
|
};
|
|
360
485
|
}
|
|
361
|
-
/** Variant groups
|
|
486
|
+
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
362
487
|
* are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
|
|
363
488
|
* directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
|
|
364
489
|
* Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
|
|
@@ -439,4 +564,4 @@ function writeManifest(root, manifest) {
|
|
|
439
564
|
}
|
|
440
565
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
441
566
|
//#endregion
|
|
442
|
-
export { DEFAULTS as a,
|
|
567
|
+
export { DEFAULTS as a, detectHost as c, writeManifest as i, readJson as l, hash as n, loadConfig as o, scanFrames as r, detectAgent as s, affectedFrameIds as t };
|
|
@@ -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-DJHU7qfu.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-BgA7nQoH.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.
|
|
4
|
-
"description": "The agent-native design canvas. A design/ folder, one command, a canvas of live frames built from your repo's real components
|
|
3
|
+
"version": "0.9.0",
|
|
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,
|
|
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
|