@marver-design/marver 0.10.2 → 0.11.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +16 -3
  3. package/dist/auth-B5yuwnOq.mjs +494 -0
  4. package/dist/{build-zUf_kwKp.mjs → build-DI31MFTB.mjs} +3 -2
  5. package/dist/cli.mjs +6 -6
  6. package/dist/{collab-s3k5byM1.mjs → collab-CWaG3Q4w.mjs} +43 -11
  7. package/dist/{comments-J06jqCVV.mjs → comments-CRqaO2MH.mjs} +26 -11
  8. package/dist/{comments-BZBKhKRO.mjs → comments-DZyobpxG.mjs} +8 -2
  9. package/dist/{manifest-DIsp3ldB.mjs → config-t9coJ-Pq.mjs} +3 -233
  10. package/dist/{daemon-BpKBJZee.mjs → daemon-DSmaS453.mjs} +23 -5
  11. package/dist/{dev-D2PG6igI.mjs → dev-BWIL2ewj.mjs} +117 -13
  12. package/dist/{init-DwV0_d9G.mjs → init-BSclDg4I.mjs} +2 -1
  13. package/dist/manifest-C2tzkNaC.mjs +233 -0
  14. package/dist/marver-id-B8-3WiHk.mjs +411 -0
  15. package/dist/marver-id-gate-BDzW6ahN.mjs +728 -0
  16. package/dist/{plugin-C1sDewZ3.mjs → plugin-DjBFxEQ7.mjs} +12 -84
  17. package/dist/{profile-BkiWglVE.mjs → profile-DcsJyppw.mjs} +9 -6
  18. package/dist/secure-cookie-_K1Hsx8H.mjs +26 -0
  19. package/dist/{serve-CZqPnj19.mjs → serve-3APqaNSB.mjs} +196 -43
  20. package/dist/sync-DELGomPk.mjs +397 -0
  21. package/dist/update-DuWDj5nR.mjs +77 -0
  22. package/dist/utm-CxC3QN5X.mjs +20 -0
  23. package/package.json +1 -1
  24. package/src/shared/utm.ts +2 -2
  25. package/templates/instructions/configure.md +4 -0
  26. package/templates/instructions/publish.md +99 -16
  27. package/dist/auth-KQ9Aj-nB.mjs +0 -245
  28. package/dist/events-BMtBvvgU.mjs +0 -101
  29. package/dist/sync-BJKKmy1n.mjs +0 -150
  30. package/dist/{shot-D7I0MCLu.mjs → shot-Cyv3GN79.mjs} +1 -1
@@ -1,8 +1,7 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { n as replay } from "./events-BMtBvvgU.mjs";
3
- import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
4
- import { connect, connectClaim, loadCollab, syncOnce } from "./sync-BJKKmy1n.mjs";
5
- import { n as localProfile } from "./profile-BkiWglVE.mjs";
2
+ import { a as syncOnce, i as loadCollab, n as connectClaim, r as connectToken, s as replay, t as connect } from "./sync-DELGomPk.mjs";
3
+ import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
4
+ import { n as localProfile } from "./profile-DcsJyppw.mjs";
6
5
  import { join } from "node:path";
7
6
  import { createInterface } from "node:readline";
8
7
  import { randomUUID } from "node:crypto";
@@ -10,8 +9,11 @@ import { randomUUID } from "node:crypto";
10
9
  /**
11
10
  * `marver comments <action>` - the collaboration CLI.
12
11
  *
13
- * connect <url> sign in (or --invite <token> to claim) against a published canvas;
14
- * stores the device credential in design/.local/collab.json
12
+ * connect <url> sign in against a published canvas and store the device credential
13
+ * in ~/.marver/canvases/ (outside the repo - `dev` serves the repo).
14
+ * Three ways in: --token <t>, the
15
+ * canvas's MARVER_CLI_TOKEN (the only one an identity-mode canvas
16
+ * has), --invite <t> to claim an invite, or email + password.
15
17
  * sync one full exchange with the publish target (agent/CI path)
16
18
  * list threads from design/comments/ (--open, --board, --json)
17
19
  * reply <thread> append a reply (--body) - the agent's voice in the loop
@@ -53,6 +55,13 @@ async function commentsCommand(root, action, value, opts) {
53
55
  switch (action) {
54
56
  case "connect": {
55
57
  if (!value) throw new Error("usage: comments connect <published-url>");
58
+ const cliToken = opts.token ?? process.env.MARVER_CLI_TOKEN;
59
+ if (cliToken) {
60
+ await connectToken(root, value, String(cliToken));
61
+ console.log(`[${NAME}] connected - the device credential is in ~/.marver/canvases/, outside this repo`);
62
+ console.log(`[${NAME}] \`${NAME} dev\` now syncs comments every 30s; \`${NAME} comments sync\` does one exchange`);
63
+ return;
64
+ }
56
65
  const canvasPassword = opts.canvasPassword ?? process.env.MARVER_PASSWORD ?? await ask("canvas password (blank if the canvas is ungated): ", true);
57
66
  if (opts.invite) {
58
67
  const name = opts.name ?? await ask("display name: ");
@@ -66,7 +75,7 @@ async function commentsCommand(root, action, value, opts) {
66
75
  const password = opts.password ?? await ask("password: ", true);
67
76
  await connect(root, value, email, password, canvasPassword || void 0);
68
77
  }
69
- console.log(`[${NAME}] connected - design/.local/collab.json holds the device credential (gitignored)`);
78
+ console.log(`[${NAME}] connected - the device credential is in ~/.marver/canvases/, outside this repo`);
70
79
  console.log(`[${NAME}] \`${NAME} dev\` now syncs comments every 30s; \`${NAME} comments sync\` does one exchange`);
71
80
  return;
72
81
  }
@@ -123,12 +132,18 @@ async function commentsCommand(root, action, value, opts) {
123
132
  }
124
133
  case "invite": {
125
134
  if (!value || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) throw new Error("usage: comments invite <email>");
126
- const { token } = await ownerApi(root, "invite", { email: value });
135
+ const { token, idMode } = await ownerApi(root, "invite", { email: value });
127
136
  const url = loadCollab(root).url.replace(/\/+$/, "");
128
137
  console.log(` invite for ${value} (single-use, 7 days):`);
129
- console.log(` ${url}/#/i/${token}`);
130
- console.log(` send them that link (plus the canvas password) - it opens straight into`);
131
- console.log(` "pick your name and password".`);
138
+ if (idMode) {
139
+ console.log(` ${url}`);
140
+ console.log(` send them that address - they sign in with ${value} and they are in.`);
141
+ console.log(` no link to forward, no password: the invitation IS the email address.`);
142
+ } else {
143
+ console.log(` ${url}/#/i/${token}`);
144
+ console.log(` send them that link (plus the canvas password) - it opens straight into`);
145
+ console.log(` "pick your name and password".`);
146
+ }
132
147
  return;
133
148
  }
134
149
  case "revoke":
@@ -1,4 +1,4 @@
1
- import "./events-BMtBvvgU.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, writeSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/server/comments.ts
@@ -11,6 +11,12 @@ import { join } from "node:path";
11
11
  * state is derived by replaying events (ordered by ts then id, last writer wins per
12
12
  * field). Files, not a database - tens to low hundreds of events per board.
13
13
  */
14
+ var comments_exports = /* @__PURE__ */ __exportAll({
15
+ appendEvents: () => appendEvents,
16
+ dataDir: () => dataDir,
17
+ listBoards: () => listBoards,
18
+ readLog: () => readLog
19
+ });
14
20
  const BOARD_RE = /^[a-z0-9][a-z0-9-]*$/;
15
21
  const boardFile = (dir, board) => {
16
22
  if (!BOARD_RE.test(board)) throw new Error(`bad board name: ${board}`);
@@ -81,4 +87,4 @@ function dataDir() {
81
87
  return dir;
82
88
  }
83
89
  //#endregion
84
- export { appendEvents, dataDir, listBoards, readLog };
90
+ export { readLog as i, comments_exports as n, listBoards as r, appendEvents as t };
@@ -1,8 +1,6 @@
1
- import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
2
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { delimiter, isAbsolute, join, relative, sep } from "node:path";
1
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, join } from "node:path";
4
3
  import { pathToFileURL } from "node:url";
5
- import { createHash } from "node:crypto";
6
4
  //#region src/server/detect.ts
7
5
  function readJson(file) {
8
6
  try {
@@ -368,232 +366,4 @@ function validViewports(v) {
368
366
  return Object.keys(out).length ? out : null;
369
367
  }
370
368
  //#endregion
371
- //#region src/server/manifest.ts
372
- const FRAME_EXT = /\.(tsx|jsx|html)$/;
373
- const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
374
- /** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
375
- function extractMeta(src) {
376
- const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
377
- if (!m) return {};
378
- const body = m[1];
379
- const pick = (key) => {
380
- const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['"\`])([^'"\`]*)\\1`).exec(body);
381
- return r ? r[2] : void 0;
382
- };
383
- const out = {};
384
- const title = pick("title");
385
- if (title) out.title = title;
386
- const viewport = pick("viewport");
387
- if (viewport) out.viewport = viewport;
388
- const theme = pick("theme");
389
- if (theme) out.theme = theme;
390
- const of = pick("of");
391
- if (of) out.of = of;
392
- const variant = pick("variant");
393
- if (variant) out.variant = variant;
394
- const intent = pick("intent");
395
- if (intent) out.intent = intent;
396
- return out;
397
- }
398
- /** Content-frame detection: LEXICAL by convention - the frame file
399
- * itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
400
- * string in a UI frame can never misbadge it; barrels/re-exports are not
401
- * detected - meta.intent is the taught path and always works). Returns the
402
- * inferred intent + natural width, or null for UI frames. */
403
- const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
404
- const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
405
- const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
406
- function contentScan(src) {
407
- if (!CONTENT_IMPORT.test(src)) return null;
408
- const count = (re) => (src.match(re) ?? []).length;
409
- const diagrams = count(/<Diagram[\s>/]/g);
410
- const imgs = count(/<Img[\s>/]/g);
411
- const mds = count(/<Md[\s>/]/g);
412
- return {
413
- intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
414
- width: contentWidthOf(src)
415
- };
416
- }
417
- /** id = path relative to design/, extension dropped, `scenes/` prefix dropped. Always `/`-separated. */
418
- function toFrameId(designRelPath) {
419
- const noExt = designRelPath.split(sep).join("/").replace(FRAME_EXT, "");
420
- return noExt.startsWith("scenes/") ? noExt.slice(7) : noExt;
421
- }
422
- /**
423
- * A7: which frames does a changed `design/**` file affect? Manifest + directory conventions,
424
- * NOT a module-graph walk (deterministic, cheap, may over-reload but never misses a
425
- * conventionally-affected frame). Returns:
426
- * null -> uncontrolled: leave to default Vite HMR (src/** deps, theme.css, config, assets)
427
- * [] -> controlled but affects no current frame (e.g. a layout in an empty dir)
428
- * [ids] -> controlled: the shell drives a rev-stamped reload of these frames
429
- */
430
- function affectedFrameIds(absFile, root, manifest) {
431
- const design = join(root, "design");
432
- if (absFile !== design && !absFile.startsWith(design + sep)) return null;
433
- const rel = relative(design, absFile).split(sep).join("/");
434
- const name = rel.split("/").pop() ?? rel;
435
- const designRel = `design/${rel}`;
436
- const tsxFrames = manifest.frames.filter((f) => f.kind === "tsx");
437
- if (name === "theme.css") return null;
438
- if (rel === "providers.tsx" || rel === "providers.jsx") return tsxFrames.map((f) => f.id);
439
- if (/(^|\/)(_layout\.(tsx|jsx)|_fixtures\.(ts|tsx|js|jsx|json))$/.test(rel)) {
440
- const prefix = `design/${rel.slice(0, rel.length - name.length)}`;
441
- return tsxFrames.filter((f) => f.file.startsWith(prefix)).map((f) => f.id);
442
- }
443
- const direct = manifest.frames.find((f) => f.file === designRel);
444
- if (direct) return [direct.id];
445
- return null;
446
- }
447
- function walk(dir, out = []) {
448
- if (!existsSync(dir)) return out;
449
- for (const e of readdirSync(dir, { withFileTypes: true })) {
450
- if (e.name.startsWith(".")) continue;
451
- const p = join(dir, e.name);
452
- if (e.isDirectory()) walk(p, out);
453
- else out.push(p);
454
- }
455
- return out;
456
- }
457
- function scanFrames(root) {
458
- const design = join(root, "design");
459
- const frames = [];
460
- for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
461
- const name = abs.split(sep).pop();
462
- if (name.startsWith("_") || !FRAME_EXT.test(name)) continue;
463
- const rel = relative(design, abs).split(sep).join("/");
464
- const id = toFrameId(rel);
465
- const scene = id.includes("/") ? id.split("/")[0] : "";
466
- if (base === "scenes" && RESERVED_SCENES.has(scene)) {
467
- console.error(`[marver] scene "${scene}" is a reserved name - "${rel}" skipped. Rename the scene.`);
468
- continue;
469
- }
470
- const kind = name.endsWith(".html") ? "html" : "tsx";
471
- const entry = {
472
- id,
473
- file: `design/${rel}`,
474
- kind,
475
- scene
476
- };
477
- if (kind === "tsx") {
478
- const src = readFileSync(abs, "utf8");
479
- const meta = extractMeta(src);
480
- if (meta.title) entry.title = meta.title;
481
- if (meta.viewport) entry.viewport = meta.viewport;
482
- if (meta.theme) entry.theme = meta.theme;
483
- if (meta.of) entry.variantGroup = meta.of;
484
- if (meta.variant) entry.variant = meta.variant;
485
- const content = contentScan(src);
486
- if (content || meta.intent) {
487
- entry.intent = meta.intent ?? content.intent;
488
- entry.contentWidth = content?.width ?? contentWidthOf(src);
489
- }
490
- }
491
- frames.push(entry);
492
- }
493
- const extRank = (file) => file.endsWith(".tsx") ? 0 : file.endsWith(".jsx") ? 1 : 2;
494
- frames.sort((a, b) => a.id.localeCompare(b.id) || extRank(a.file) - extRank(b.file) || a.file.localeCompare(b.file));
495
- const seen = /* @__PURE__ */ new Map();
496
- const deduped = frames.filter((f) => {
497
- const prev = seen.get(f.id);
498
- if (prev) {
499
- console.error(`[marver] duplicate frame id "${f.id}" (${prev} vs ${f.file}) - keeping ${prev}.`);
500
- return false;
501
- }
502
- seen.set(f.id, f.file);
503
- return true;
504
- });
505
- frames.length = 0;
506
- frames.push(...deduped);
507
- inferVariantGroups(frames);
508
- const sceneCounts = /* @__PURE__ */ new Map();
509
- for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
510
- return {
511
- frames,
512
- scenes: [...sceneCounts.entries()].map(([name, n]) => ({
513
- name,
514
- frames: n
515
- })).sort((a, b) => a.name.localeCompare(b.name))
516
- };
517
- }
518
- /** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
519
- * are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
520
- * directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
521
- * Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
522
- * States (empty.tsx, error.tsx) never letter-prefix, so they never misgroup.
523
- * Mutates entries in place: only frames whose group materializes keep the fields. */
524
- function inferVariantGroups(frames) {
525
- const dirOf = (id) => {
526
- const i = id.lastIndexOf("/");
527
- return i >= 0 ? id.slice(0, i) : "";
528
- };
529
- const candidates = /* @__PURE__ */ new Map();
530
- for (const f of frames) {
531
- if (f.kind !== "tsx") {
532
- delete f.variantGroup;
533
- delete f.variant;
534
- continue;
535
- }
536
- let group = f.variantGroup;
537
- let key = f.variant;
538
- if (!group) {
539
- const base = f.id.slice(f.id.lastIndexOf("/") + 1);
540
- const m = /^([a-z])-.+$/.exec(base);
541
- if (!m) continue;
542
- group = dirOf(f.id);
543
- if (!group) continue;
544
- key = key ?? m[1];
545
- } else if (!key) {
546
- const m = /^([a-z])-.+$/.exec(f.id.slice(f.id.lastIndexOf("/") + 1));
547
- if (!m) {
548
- console.warn(`[marver] ${f.file} declares of:"${group}" but no variant key (add meta.variant or a letter prefix) - not grouped.`);
549
- delete f.variantGroup;
550
- delete f.variant;
551
- continue;
552
- }
553
- key = m[1];
554
- }
555
- f.variantGroup = group;
556
- f.variant = key;
557
- candidates.set(group, [...candidates.get(group) ?? [], f]);
558
- }
559
- for (const [group, members] of candidates) {
560
- if (new Set(members.map((m) => dirOf(m.id))).size > 1) {
561
- console.warn(`[marver] group "${group}" spans directories - variants must be siblings; not grouped.`);
562
- for (const m of members) {
563
- delete m.variantGroup;
564
- delete m.variant;
565
- }
566
- continue;
567
- }
568
- const seen = /* @__PURE__ */ new Set();
569
- const kept = [];
570
- for (const m of members.sort((a, b) => (a.variant ?? "").localeCompare(b.variant ?? ""))) {
571
- if (seen.has(m.variant)) {
572
- console.warn(`[marver] duplicate variant "${m.variant}" in group "${group}" (${m.file}) - not grouped.`);
573
- delete m.variantGroup;
574
- delete m.variant;
575
- continue;
576
- }
577
- seen.add(m.variant);
578
- kept.push(m);
579
- }
580
- if (kept.length < 2) for (const m of kept) {
581
- delete m.variantGroup;
582
- delete m.variant;
583
- }
584
- }
585
- for (const f of frames) if (f.variant && !f.variantGroup) delete f.variant;
586
- }
587
- /** Write design/manifest.json only when content changed. Returns the manifest either way. */
588
- function writeManifest(root, manifest) {
589
- const file = join(root, "design", "manifest.json");
590
- const next = JSON.stringify(manifest, null, 2) + "\n";
591
- const prev = existsSync(file) ? readFileSync(file, "utf8") : "";
592
- if (hash(prev) === hash(next)) return false;
593
- mkdirSync(join(root, "design"), { recursive: true });
594
- writeFileSync(file, next);
595
- return true;
596
- }
597
- const hash = (s) => createHash("sha256").update(s).digest("hex");
598
- //#endregion
599
- export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l, hash as n, DEFAULTS as o, scanFrames as r, loadConfig as s, affectedFrameIds as t, readJson as u };
369
+ export { readJson as a, detectHost as i, loadConfig as n, detectAgent as r, DEFAULTS as t };
@@ -1,7 +1,7 @@
1
- import { n as replay } from "./events-BMtBvvgU.mjs";
2
- import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
3
- import { n as localProfile } from "./profile-BkiWglVE.mjs";
4
- import { i as toFrameId } from "./manifest-DIsp3ldB.mjs";
1
+ import { s as replay } from "./sync-DELGomPk.mjs";
2
+ import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
3
+ import { n as localProfile } from "./profile-DcsJyppw.mjs";
4
+ import { i as toFrameId } from "./manifest-C2tzkNaC.mjs";
5
5
  import { workActivity } from "./work-CLrmY-vQ.mjs";
6
6
  import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
7
7
  import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
@@ -938,6 +938,24 @@ const fenceGroup = (pid) => {
938
938
  if (pid) process.kill(-pid, "SIGKILL");
939
939
  } catch {}
940
940
  };
941
+ /**
942
+ * What a spawned agent is allowed to inherit.
943
+ *
944
+ * Agents get the environment because they need it - PATH, HOME, the API key
945
+ * their own CLI signs in with. `MARVER_CLI_TOKEN` is the exception: it is the
946
+ * operator's credential for the PUBLISHED canvas, it is accepted as a shell
947
+ * fallback by `comments connect`, and an agent is the one process on this machine
948
+ * that is running somebody else's instructions. Handing it over would let a
949
+ * prompt-injected agent take the published canvas, which is a much larger blast
950
+ * radius than the frames it was asked to edit.
951
+ *
952
+ * Exported so the removal is testable: a spawn option nobody asserts is a spawn
953
+ * option that quietly grows back.
954
+ */
955
+ function agentEnv(from = process.env) {
956
+ const { MARVER_CLI_TOKEN: _withheld, ...rest } = from;
957
+ return rest;
958
+ }
941
959
  const FRAME_FILE = /\.(tsx|jsx|html)$/;
942
960
  /** Every frame FILE under design/scenes, as `frame id -> mtimeMs`. Used to see which frames the
943
961
  * agent creates or edits mid-job (so the working glow can follow the work, not sit on the frame
@@ -987,7 +1005,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
987
1005
  "ignore"
988
1006
  ],
989
1007
  env: {
990
- ...process.env,
1008
+ ...agentEnv(),
991
1009
  ...env,
992
1010
  PWD: root
993
1011
  }
@@ -1,7 +1,8 @@
1
1
  import { n as NAME, r as PKG } from "./cli.mjs";
2
- import { l as detectHost, s as loadConfig } from "./manifest-DIsp3ldB.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-C1sDewZ3.mjs";
4
- import { basename, dirname, join } from "node:path";
2
+ import { i as detectHost, n as loadConfig } from "./config-t9coJ-Pq.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DjBFxEQ7.mjs";
4
+ import { realpathSync } from "node:fs";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
7
8
  import react from "@vitejs/plugin-react";
@@ -44,6 +45,106 @@ async function pickPort(root, desired) {
44
45
  fellBack: true
45
46
  };
46
47
  }
48
+ /**
49
+ * What the dev server must never hand out, however deep in the repo it sits.
50
+ *
51
+ * `marver dev` puts the whole repository on the web so frames can import from it.
52
+ * The collaboration credential no longer lives in there - it moved to
53
+ * ~/.marver/canvases/ precisely because guarding it here could not be finished -
54
+ * but design/.local still holds the local profile and whatever a previous version
55
+ * left behind, and none of that is anybody's business but the author's.
56
+ *
57
+ * Vite's own defaults are restated rather than assumed. `fs.deny` REPLACES the
58
+ * default list, so an array containing only our own rule quietly re-exposes
59
+ * `.env`, private keys, `.npmrc` and `.git` - a much larger hole than the one
60
+ * being closed, opened by closing it. Kept as an exported constant so a test can
61
+ * hold both halves.
62
+ */
63
+ const FS_DENY = [
64
+ ".env",
65
+ ".env.*",
66
+ "*.{crt,pem,key,p12,pfx,cer,der}",
67
+ ".npmrc",
68
+ ".yarnrc.yml",
69
+ "**/.git/**",
70
+ ".dev.vars",
71
+ ".dev.vars.*",
72
+ "**/design/.local/**"
73
+ ];
74
+ /**
75
+ * The half of the credential guard that globs cannot do.
76
+ *
77
+ * `fs.deny` matches the REQUEST path, lexically, and then Vite stats and streams
78
+ * whatever that path resolves to. So a repository containing
79
+ * `leak.json -> design/.local/anything` serves that file at `/leak.json` to any
80
+ * authored frame that asks - the deny rule never sees the name it is looking for.
81
+ * Verified against a real dev server before this existed.
82
+ *
83
+ * More patterns cannot fix that; only resolving can. This runs BEFORE Vite's own
84
+ * middlewares (a `configureServer` hook that does not return a function is
85
+ * installed first) and refuses any request whose real file lands inside
86
+ * design/.local, whatever route it took to get there.
87
+ *
88
+ * "Whatever route" means every place Vite would look, not just the project root.
89
+ * `public/` is mapped onto `/`, so `public/leak.json -> ../design/.local/x` is
90
+ * served at `/leak.json` - and Vite skips its own deny checks entirely for public
91
+ * files, so that spelling was reachable even with the root candidate guarded.
92
+ * Each candidate is resolved; if any lands in design/.local, the request is
93
+ * refused.
94
+ *
95
+ * The boundary this does NOT claim: a repository that can write symlinks can also
96
+ * write `design/config.ts`, which `marver dev` imports and RUNS in this process
97
+ * before any of the above exists. Running `dev` on a repository you do not trust
98
+ * hands it your machine long before it hands it a file, so a symlink is not the
99
+ * interesting way in. What this closes is the frame-in-a-trusted-repo case: a
100
+ * component that runs in the browser, same-origin, that can only ask for URLs and
101
+ * cannot create a file to ask for.
102
+ */
103
+ function denyLocalSecrets(root) {
104
+ const guardedAt = join(root, "design", ".local");
105
+ const publicAt = join(root, "public");
106
+ const inside = (real, dir) => {
107
+ const rel = relative(dir, real);
108
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
109
+ };
110
+ return {
111
+ name: "marver:deny-local-secrets",
112
+ configureServer(server) {
113
+ server.middlewares.use((req, res, next) => {
114
+ let pathname;
115
+ try {
116
+ pathname = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
117
+ } catch {
118
+ return next();
119
+ }
120
+ const rel = "." + (pathname.startsWith("/") ? pathname : `/${pathname}`);
121
+ const derived = (base) => [
122
+ base,
123
+ `${base}.html`,
124
+ join(base, "index"),
125
+ join(base, "index.html")
126
+ ];
127
+ const candidates = pathname.startsWith("/@fs/") ? derived(pathname.slice(4)) : [...derived(resolve(root, rel)), ...derived(resolve(publicAt, rel))];
128
+ let guarded;
129
+ try {
130
+ guarded = realpathSync(guardedAt);
131
+ } catch {
132
+ return next();
133
+ }
134
+ if (!candidates.some((c) => {
135
+ try {
136
+ return inside(realpathSync(c), guarded);
137
+ } catch {
138
+ return false;
139
+ }
140
+ })) return next();
141
+ res.statusCode = 403;
142
+ res.setHeader("content-type", "text/plain; charset=utf-8");
143
+ res.end("design/.local holds this canvas's credentials and is never served");
144
+ });
145
+ }
146
+ };
147
+ }
47
148
  async function dev(root, portFlag) {
48
149
  const config = await loadConfig(root);
49
150
  const host = detectHost(root);
@@ -52,7 +153,7 @@ async function dev(root, portFlag) {
52
153
  const projectName = basename(root);
53
154
  const desiredPort = portFlag ?? (config.port !== 5199 ? config.port : void 0) ?? projectPort(root);
54
155
  const picked = await pickPort(root, desiredPort);
55
- const plugins = [react()];
156
+ const plugins = [denyLocalSecrets(root), react()];
56
157
  if (host.tailwind === 4) {
57
158
  const tw = await tailwind4Plugin(root);
58
159
  if (tw) plugins.push(...tw);
@@ -85,11 +186,14 @@ async function dev(root, portFlag) {
85
186
  server: {
86
187
  port: picked.port,
87
188
  strictPort: false,
88
- fs: { allow: [.../* @__PURE__ */ new Set([
89
- root,
90
- pkgDir,
91
- searchForWorkspaceRoot(root)
92
- ])] },
189
+ fs: {
190
+ allow: [.../* @__PURE__ */ new Set([
191
+ root,
192
+ pkgDir,
193
+ searchForWorkspaceRoot(root)
194
+ ])],
195
+ deny: FS_DENY
196
+ },
93
197
  watch: { ignored: [
94
198
  "**/design/manifest.json",
95
199
  "**/design/boards/**",
@@ -143,7 +247,7 @@ async function dev(root, portFlag) {
143
247
  console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
144
248
  try {
145
249
  const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
146
- const { installedVersion } = await import("./plugin-C1sDewZ3.mjs").then((n) => n.i);
250
+ const { installedVersion } = await import("./update-DuWDj5nR.mjs").then((n) => n.r);
147
251
  const stale = staleManagedInstructions(root);
148
252
  if (stale.length) {
149
253
  const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
@@ -151,8 +255,8 @@ async function dev(root, portFlag) {
151
255
  console.log(` instructions predate ${v ? NAME + " " + v : "the installed version"} (${shown}) - run: npx ${NAME} init\n`);
152
256
  }
153
257
  } catch {}
154
- const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
155
- if (loadCollab(root)) console.log(` comments: syncing with the published canvas (design/.local/collab.json)\n`);
258
+ const { loadCollab, syncOnce } = await import("./sync-DELGomPk.mjs").then((n) => n.o);
259
+ if (loadCollab(root)) console.log(` comments: syncing with the published canvas\n`);
156
260
  let syncing = false;
157
261
  const tick = async () => {
158
262
  if (syncing) return;
@@ -181,7 +285,7 @@ async function dev(root, portFlag) {
181
285
  });
182
286
  }
183
287
  if (config.jam) {
184
- const { startJam } = await import("./daemon-BpKBJZee.mjs");
288
+ const { startJam } = await import("./daemon-DSmaS453.mjs");
185
289
  const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
186
290
  if (jam) {
187
291
  const close = server.close.bind(server);
@@ -1,5 +1,6 @@
1
1
  import { n as NAME } from "./cli.mjs";
2
- import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-DIsp3ldB.mjs";
2
+ import { a as readJson, i as detectHost, r as detectAgent, t as DEFAULTS } from "./config-t9coJ-Pq.mjs";
3
+ import { a as writeManifest, r as scanFrames } from "./manifest-C2tzkNaC.mjs";
3
4
  import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
4
5
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { dirname, join, relative } from "node:path";