dsh-claude-slides 2.4.1 → 2.5.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.
@@ -2,9 +2,9 @@
2
2
  // claude-slides: local Artifact tool (prebuilt; do not edit).
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import fs9 from "node:fs";
5
+ import fs10 from "node:fs";
6
6
  import fsp2 from "node:fs/promises";
7
- import path11 from "node:path";
7
+ import path12 from "node:path";
8
8
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
9
9
 
10
10
  var BLOB_ID_PATTERN = /^[0-9a-f]{32}$/;
@@ -1477,15 +1477,15 @@ function normalizeCssLength(value, allowFullRadius) {
1477
1477
  return CSS_LENGTH_PATTERN.test(normalized) ? normalized : void 0;
1478
1478
  }
1479
1479
  var colorAliasTarget = (value) => value === void 0 ? void 0 : COLOR_ALIAS_PATTERN.exec(value)?.[1];
1480
- function explainColorAliasError(token, themeId, baseThemeId, byName) {
1481
- const seen = /* @__PURE__ */ new Set([token.name]);
1482
- let current = token;
1480
+ function explainColorAliasError(token2, themeId, baseThemeId, byName) {
1481
+ const seen = /* @__PURE__ */ new Set([token2.name]);
1482
+ let current = token2;
1483
1483
  for (let index = 0; index <= TOKENS_LIMITS.colorAliasDepth; index++) {
1484
1484
  const target = colorAliasTarget(current.value[themeId] ?? current.value[baseThemeId]);
1485
1485
  if (target === void 0) return void 0;
1486
1486
  const entry = byName.get(target);
1487
1487
  if (entry === void 0) return { why: `${index === 0 ? "" : `reaches {${target}}, which `}names no color token`, own: index === 0 };
1488
- if (seen.has(target)) return { why: "is a cycle", own: target === token.name };
1488
+ if (seen.has(target)) return { why: "is a cycle", own: target === token2.name };
1489
1489
  seen.add(target);
1490
1490
  current = entry;
1491
1491
  }
@@ -1496,30 +1496,30 @@ function removeInvalidColorAliases(themes, tokens, dropped) {
1496
1496
  for (; ; ) {
1497
1497
  const byName = new Map(tokens.map((t) => [t.name, t]));
1498
1498
  const items = [];
1499
- for (const token of tokens) {
1499
+ for (const token2 of tokens) {
1500
1500
  for (const theme of themes) {
1501
- if (colorAliasTarget(token.value[theme.id]) === void 0) continue;
1502
- const error = explainColorAliasError(token, theme.id, baseThemeId, byName);
1503
- if (error !== void 0) items.push([token, theme.id, error.why, error.own]);
1501
+ if (colorAliasTarget(token2.value[theme.id]) === void 0) continue;
1502
+ const error = explainColorAliasError(token2, theme.id, baseThemeId, byName);
1503
+ if (error !== void 0) items.push([token2, theme.id, error.why, error.own]);
1504
1504
  }
1505
1505
  }
1506
1506
  if (items.length === 0) return;
1507
1507
  const anyOwn = items.some((item) => item[3]);
1508
- for (const [token, themeId, why, own] of items) {
1508
+ for (const [token2, themeId, why, own] of items) {
1509
1509
  if (anyOwn && !own) continue;
1510
- dropped.push(`color ${token.name} [${themeId}]: ${String(token.value[themeId])} ${why}`);
1511
- delete token.value[themeId];
1510
+ dropped.push(`color ${token2.name} [${themeId}]: ${String(token2.value[themeId])} ${why}`);
1511
+ delete token2.value[themeId];
1512
1512
  }
1513
1513
  for (let index = 0; index < tokens.length; index++) {
1514
- const token = tokens[index];
1515
- if (Object.hasOwn(token.value, baseThemeId)) continue;
1516
- const theme = themes.find((t) => Object.hasOwn(token.value, t.id));
1514
+ const token2 = tokens[index];
1515
+ if (Object.hasOwn(token2.value, baseThemeId)) continue;
1516
+ const theme = themes.find((t) => Object.hasOwn(token2.value, t.id));
1517
1517
  if (theme === void 0) {
1518
- dropped.push(`color ${token.name}: no valid value`);
1518
+ dropped.push(`color ${token2.name}: no valid value`);
1519
1519
  tokens.splice(index--, 1);
1520
1520
  } else {
1521
- token.value[baseThemeId] = token.value[theme.id];
1522
- delete token.value[theme.id];
1521
+ token2.value[baseThemeId] = token2.value[theme.id];
1522
+ delete token2.value[theme.id];
1523
1523
  }
1524
1524
  }
1525
1525
  }
@@ -1575,23 +1575,23 @@ function checkTokensText(text) {
1575
1575
  problems.push('color has both "tokens" and a legacy "groups"; only "tokens" is read');
1576
1576
  }
1577
1577
  if (sourceColorTokens.length > TOKENS_LIMITS.colors) problems.push(`color tokens capped at ${TOKENS_LIMITS.colors}; the rest are ignored`);
1578
- for (const token of sourceColorTokens) {
1578
+ for (const token2 of sourceColorTokens) {
1579
1579
  if (colorTokens.length >= TOKENS_LIMITS.colors) break;
1580
- if (!isObject(token)) continue;
1581
- const name = typeof token.name === "string" ? token.name.trim().replace(/^--/, "") : "";
1580
+ if (!isObject(token2)) continue;
1581
+ const name = typeof token2.name === "string" ? token2.name.trim().replace(/^--/, "") : "";
1582
1582
  if (!TOKEN_NAME_PATTERN.test(name) || colorNames.has(name)) {
1583
1583
  dropped.push(`color ${JSON.stringify(name)}: bad or duplicate name`);
1584
1584
  continue;
1585
1585
  }
1586
1586
  const value = {};
1587
- if (isObject(token.value)) {
1587
+ if (isObject(token2.value)) {
1588
1588
  for (const theme of themes) {
1589
- const normalized = Object.hasOwn(token.value, theme.id) ? normalizeColorOrAlias(token.value[theme.id]) : void 0;
1589
+ const normalized = Object.hasOwn(token2.value, theme.id) ? normalizeColorOrAlias(token2.value[theme.id]) : void 0;
1590
1590
  if (normalized !== void 0) value[theme.id] = normalized;
1591
- else if (Object.hasOwn(token.value, theme.id)) dropped.push(`color ${name} [${theme.id}]: not a color value`);
1591
+ else if (Object.hasOwn(token2.value, theme.id)) dropped.push(`color ${name} [${theme.id}]: not a color value`);
1592
1592
  }
1593
1593
  } else {
1594
- const normalized = normalizeColorOrAlias(token.value);
1594
+ const normalized = normalizeColorOrAlias(token2.value);
1595
1595
  if (normalized !== void 0) value[baseThemeId] = normalized;
1596
1596
  }
1597
1597
  if (!Object.hasOwn(value, baseThemeId)) {
@@ -1676,10 +1676,10 @@ function checkTokensText(text) {
1676
1676
  const tokens = asArray(isObject(input) ? input.tokens : void 0);
1677
1677
  if (tokens.length > TOKENS_LIMITS.tokensPerFamily) problems.push(`${family} has ${tokens.length} tokens; at most ${TOKENS_LIMITS.tokensPerFamily} are read`);
1678
1678
  let count = 0;
1679
- for (const token of tokens) {
1679
+ for (const token2 of tokens) {
1680
1680
  if (count >= TOKENS_LIMITS.tokensPerFamily) break;
1681
- if (!isObject(token)) continue;
1682
- const name = typeof token.name === "string" ? token.name.trim().replace(/^--/, "") : "";
1681
+ if (!isObject(token2)) continue;
1682
+ const name = typeof token2.name === "string" ? token2.name.trim().replace(/^--/, "") : "";
1683
1683
  const other = familyOf.get(name);
1684
1684
  if (!TOKEN_NAME_PATTERN.test(name) || other === family) {
1685
1685
  dropped.push(`${family} ${JSON.stringify(name)}: bad or duplicate name`);
@@ -1689,7 +1689,7 @@ function checkTokensText(text) {
1689
1689
  dropped.push(`${family} ${name}: a ${other} token already has that name (one --${name} across every family)`);
1690
1690
  continue;
1691
1691
  }
1692
- if (normalize(token.value, name) === void 0) {
1692
+ if (normalize(token2.value, name) === void 0) {
1693
1693
  dropped.push(`${family} ${name}: bad value`);
1694
1694
  continue;
1695
1695
  }
@@ -2337,7 +2337,10 @@ function parseDoc(text) {
2337
2337
  const threads = data.threads.filter(
2338
2338
  (t) => isRecord4(t) && typeof t.id === "string" && typeof t.anchor === "string" && typeof t.label === "string" && (t.status === "open" || t.status === "resolved") && Array.isArray(t.messages) && t.messages.every((m) => isRecord4(m) && typeof m.text === "string" && isRecord4(m.author) && (m.author.kind === "user" || m.author.kind === "agent"))
2339
2339
  );
2340
- for (const t of threads) if (!isRecord4(t.location)) t.location = {};
2340
+ for (const t of threads) {
2341
+ if (!isRecord4(t.location)) t.location = {};
2342
+ for (const m of t.messages) if (m.author.principal !== void 0 && typeof m.author.principal !== "string") delete m.author.principal;
2343
+ }
2341
2344
  return { version: COMMENTS_VERSION, threads };
2342
2345
  }
2343
2346
  var formatDoc = (doc) => `${JSON.stringify(doc, null, 2)}
@@ -2496,6 +2499,206 @@ async function runningServer(root, kind, probe) {
2496
2499
  return null;
2497
2500
  }
2498
2501
  }
2502
+ var DEFAULT_PORTS = { slides: 4710, design: 4720, "design-system": 4730, dashboard: 4740, docs: 4750, motion: 4760 };
2503
+
2504
+ import crypto from "node:crypto";
2505
+ import fs9 from "node:fs";
2506
+ import path11 from "node:path";
2507
+ var GUEST_ROLES = ["editor", "commenter", "viewer"];
2508
+ var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
2509
+ var SHARING_FILE = ".sharing.json";
2510
+ var MAX_GUESTS = 200;
2511
+ var MAX_NAME = 40;
2512
+ var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
2513
+ var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2514
+ function fresh() {
2515
+ return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
2516
+ }
2517
+ function cleanName(raw) {
2518
+ const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
2519
+ return [...name].slice(0, MAX_NAME).join("");
2520
+ }
2521
+ var Sharing = class _Sharing {
2522
+ file;
2523
+ state;
2524
+ constructor(root) {
2525
+ this.file = path11.join(root, SHARING_FILE);
2526
+ this.state = _Sharing.read(this.file);
2527
+ }
2528
+ static read(file) {
2529
+ let raw;
2530
+ try {
2531
+ raw = JSON.parse(fs9.readFileSync(file, "utf8"));
2532
+ } catch {
2533
+ return fresh();
2534
+ }
2535
+ const state = fresh();
2536
+ if (!isRecord5(raw)) return state;
2537
+ state.lan = raw.lan === true;
2538
+ if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
2539
+ if (isRecord5(raw.links)) {
2540
+ for (const role of GUEST_ROLES) {
2541
+ const link = raw.links[role];
2542
+ if (isRecord5(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
2543
+ }
2544
+ }
2545
+ if (Array.isArray(raw.guests)) {
2546
+ for (const g of raw.guests) {
2547
+ if (!isRecord5(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
2548
+ state.guests.push({
2549
+ id: g.id,
2550
+ secret: g.secret,
2551
+ name: cleanName(g.name) || "Guest",
2552
+ role: g.role,
2553
+ link: g.link,
2554
+ joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
2555
+ ...typeof g.seen === "string" ? { seen: g.seen } : {}
2556
+ });
2557
+ }
2558
+ }
2559
+ return state;
2560
+ }
2561
+ save() {
2562
+ const tmp = `${this.file}.${process.pid}.tmp`;
2563
+ fs9.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
2564
+ `, { mode: 384 });
2565
+ fs9.renameSync(tmp, this.file);
2566
+ }
2567
+ /** The role whose link has this token, while that link is on. */
2568
+ linkRole(tokenGiven) {
2569
+ for (const role of GUEST_ROLES) {
2570
+ const link = this.state.links[role];
2571
+ if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
2572
+ }
2573
+ return null;
2574
+ }
2575
+ /** The guest a cookie names, if they still have access (their link on and not reset). */
2576
+ guest(secret) {
2577
+ if (!secret) return null;
2578
+ const guest = this.state.guests.find((g) => g.secret === secret);
2579
+ return guest && this.valid(guest) ? guest : null;
2580
+ }
2581
+ valid(guest) {
2582
+ const link = this.state.links[guest.role];
2583
+ return link.on && link.token === guest.link && this.state.guests.includes(guest);
2584
+ }
2585
+ join(role, name) {
2586
+ const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
2587
+ this.state.guests.push(guest);
2588
+ if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
2589
+ this.save();
2590
+ return guest;
2591
+ }
2592
+ /** Marks a guest seen now (saved at most once a minute per guest). */
2593
+ seen(guest) {
2594
+ const now = /* @__PURE__ */ new Date();
2595
+ if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
2596
+ guest.seen = now.toISOString();
2597
+ this.save();
2598
+ }
2599
+ setLan(on, port) {
2600
+ this.state.lan = on;
2601
+ if (on && port) this.state.port = port;
2602
+ this.save();
2603
+ }
2604
+ setLink(role, on) {
2605
+ this.state.links[role].on = on;
2606
+ this.save();
2607
+ }
2608
+ /** A new token for the link: the old link stops working and the guests who came through it are dropped. */
2609
+ resetLink(role) {
2610
+ const old = this.state.links[role].token;
2611
+ this.state.links[role] = { token: token(), on: this.state.links[role].on };
2612
+ this.state.guests = this.state.guests.filter((g) => g.link !== old);
2613
+ this.save();
2614
+ }
2615
+ /**
2616
+ * Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
2617
+ * whose link is off would shut them out: false then, as for a guest who isn't there.
2618
+ */
2619
+ setRole(id, role) {
2620
+ const guest = this.state.guests.find((g) => g.id === id);
2621
+ if (!guest || !this.state.links[role].on) return false;
2622
+ guest.role = role;
2623
+ guest.link = this.state.links[role].token;
2624
+ this.save();
2625
+ return true;
2626
+ }
2627
+ removeGuest(id) {
2628
+ const before = this.state.guests.length;
2629
+ this.state.guests = this.state.guests.filter((g) => g.id !== id);
2630
+ if (this.state.guests.length === before) return false;
2631
+ this.save();
2632
+ return true;
2633
+ }
2634
+ };
2635
+
2636
+ async function shareCall(url, action, fail2) {
2637
+ const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
2638
+ const body = await response.json().catch(() => ({}));
2639
+ if (!response.ok) fail2("SHARE_REFUSED", String(body.message ?? `the server answered ${response.status}`));
2640
+ return body;
2641
+ }
2642
+ function rolesOf(flags, key, fail2) {
2643
+ const given = flags.get(key);
2644
+ if (given === void 0) return [];
2645
+ const roles = given === "all" || given === "true" ? [...GUEST_ROLES] : given.split(",").map((r) => r.trim());
2646
+ for (const role of roles) if (!isGuestRole(role)) fail2("INVALID_ROLE", `--${key}: editor, commenter, viewer or all (got ${role}).`);
2647
+ return roles;
2648
+ }
2649
+ function guestRoles(flags, fail2) {
2650
+ const given = flags.get("role");
2651
+ if (given === void 0) return [];
2652
+ return given.split(",").map((pair) => {
2653
+ const [guest = "", role = ""] = pair.split("=").map((part) => part.trim());
2654
+ if (!guest || !isGuestRole(role)) fail2("INVALID_ROLE", `--role: <guest id>=editor|commenter|viewer, comma separated (got ${pair}).`);
2655
+ return { action: "role", guest, role };
2656
+ });
2657
+ }
2658
+ async function shareCommand(o) {
2659
+ const off = o.flags.has("off");
2660
+ const changes = [
2661
+ ...rolesOf(o.flags, "reset", o.fail).map((role) => ({ action: "reset", role })),
2662
+ ...rolesOf(o.flags, "enable", o.fail).map((role) => ({ action: "link", role, on: true })),
2663
+ ...rolesOf(o.flags, "disable", o.fail).map((role) => ({ action: "link", role, on: false })),
2664
+ ...guestRoles(o.flags, o.fail)
2665
+ ];
2666
+ let server = await o.running();
2667
+ if (!server) {
2668
+ const sharing = new Sharing(o.root);
2669
+ for (const change of changes) {
2670
+ if (change.action === "reset") sharing.resetLink(change.role);
2671
+ else if (change.action === "role") {
2672
+ if (!sharing.setRole(String(change.guest), change.role)) o.fail("SHARE_REFUSED", `No guest ${String(change.guest)}, or the ${String(change.role)} link is off.`);
2673
+ } else sharing.setLink(change.role, change.on === true);
2674
+ }
2675
+ if (off) {
2676
+ sharing.setLan(false);
2677
+ return { lan: false, message: "Not shared on the local network. Guests already in get their place back when it is shared again (unless their link was reset)." };
2678
+ }
2679
+ server = await o.startShared();
2680
+ if (!server) o.fail("SERVER_FAILED", `The server did not start. Run ${o.serveCommand} in the foreground to see why.`);
2681
+ }
2682
+ let view = await shareCall(server.url, null, o.fail);
2683
+ for (const change of changes) view = await shareCall(server.url, change, o.fail);
2684
+ if (off || !view.lan) view = await shareCall(server.url, { action: "lan", on: !off }, o.fail);
2685
+ const links = view.links;
2686
+ return {
2687
+ lan: view.lan,
2688
+ ...view.lan ? { addresses: lanUrls(view) } : {},
2689
+ links: Object.fromEntries(Object.entries(links).map(([role, link]) => [role, link.on ? link.url : "off"])),
2690
+ guests: view.guests,
2691
+ ...view.lan ? {
2692
+ note: "Send the link for the role you want to give: editor edits, commenter comments, viewer reads. Anyone on this network with a link joins under a name they type. The owner (this computer) shares from the Share button too.",
2693
+ stop: o.offCommand
2694
+ } : { note: "Not shared on the network: the links work on this computer only." }
2695
+ };
2696
+ }
2697
+ var lanUrls = (view) => (Array.isArray(view.addresses) ? view.addresses : []).map((a) => `http://${String(a)}:${String(view.port)}/`);
2698
+ async function lanOn(url, flags, fail2) {
2699
+ if (!flags.has("lan")) return {};
2700
+ return { lan: lanUrls(await shareCall(url, { action: "lan", on: true }, fail2)) };
2701
+ }
2499
2702
 
2500
2703
  var emitWarning = process.emitWarning.bind(process);
2501
2704
  process.emitWarning = ((warning, ...rest) => {
@@ -2504,8 +2707,8 @@ process.emitWarning = ((warning, ...rest) => {
2504
2707
  });
2505
2708
  var KIND = "slides";
2506
2709
  var SCRIPT = fileURLToPath2(import.meta.url);
2507
- var EDITOR_DIR = path11.join(path11.dirname(SCRIPT), "editor");
2508
- var SERVER_SCRIPT = path11.join(path11.dirname(SCRIPT), "server.mjs");
2710
+ var EDITOR_DIR = path12.join(path12.dirname(SCRIPT), "editor");
2711
+ var SERVER_SCRIPT = path12.join(path12.dirname(SCRIPT), "server.mjs");
2509
2712
  var MAX_LISTED = 200;
2510
2713
  var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
2511
2714
  var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
@@ -2518,7 +2721,7 @@ var CliError = class extends Error {
2518
2721
  this.code = code;
2519
2722
  }
2520
2723
  };
2521
- var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen"]);
2724
+ var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off"]);
2522
2725
  function parseArgs(argv) {
2523
2726
  const positional = [];
2524
2727
  const flags = /* @__PURE__ */ new Map();
@@ -2544,13 +2747,13 @@ function quote(s) {
2544
2747
  }
2545
2748
  function resolveArtifact(given, mustExist = true) {
2546
2749
  if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
2547
- let root = path11.resolve(given);
2548
- if (path11.basename(root) === "project" && fs9.existsSync(path11.join(root, INDEX_FILE)) && !fs9.existsSync(path11.join(root, "project"))) {
2549
- root = path11.dirname(root);
2750
+ let root = path12.resolve(given);
2751
+ if (path12.basename(root) === "project" && fs10.existsSync(path12.join(root, INDEX_FILE)) && !fs10.existsSync(path12.join(root, "project"))) {
2752
+ root = path12.dirname(root);
2550
2753
  }
2551
- const project = path11.join(root, "project");
2754
+ const project = path12.join(root, "project");
2552
2755
  if (mustExist) {
2553
- const stat = fs9.statSync(project, { throwIfNoEntry: false });
2756
+ const stat = fs10.statSync(project, { throwIfNoEntry: false });
2554
2757
  if (!stat?.isDirectory()) {
2555
2758
  throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
2556
2759
  }
@@ -2572,7 +2775,7 @@ function localize(value) {
2572
2775
  function designSystemNotes(project, indexFile) {
2573
2776
  let index;
2574
2777
  try {
2575
- index = JSON.parse(fs9.readFileSync(path11.join(project, indexFile), "utf8"));
2778
+ index = JSON.parse(fs10.readFileSync(path12.join(project, indexFile), "utf8"));
2576
2779
  } catch {
2577
2780
  return [];
2578
2781
  }
@@ -2586,10 +2789,10 @@ function designSystemNotes(project, indexFile) {
2586
2789
  notes.push(`designSystems namespace ${JSON.stringify(ns)} is not a folder name ([a-z0-9][a-z0-9_-]{0,63}): the page skips it. Use the lower-case folder (list prints it as folder), the same in the record and in project/ds/<folder>/.`);
2587
2790
  continue;
2588
2791
  }
2589
- const folder = path11.join(project, "ds", ns);
2590
- if (!fs9.existsSync(folder)) {
2792
+ const folder = path12.join(project, "ds", ns);
2793
+ if (!fs10.existsSync(folder)) {
2591
2794
  notes.push(`designSystems names "${ns}" but project/ds/${ns}/ does not exist: copy the system's project/tokens.json (and the files you use) there, or take the record out.`);
2592
- } else if (!fs9.existsSync(path11.join(folder, "tokens.json"))) {
2795
+ } else if (!fs10.existsSync(path12.join(folder, "tokens.json"))) {
2593
2796
  notes.push(`project/ds/${ns}/ has no tokens.json, so the Theme and color menus show bare hexes: copy the system's project/tokens.json there (a system with none: nothing to do).`);
2594
2797
  }
2595
2798
  }
@@ -2601,9 +2804,9 @@ async function check(a) {
2601
2804
  try {
2602
2805
  rt = loadSlidesCompiler(EDITOR_DIR);
2603
2806
  } catch {
2604
- throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path11.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
2807
+ throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path12.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
2605
2808
  }
2606
- if (!fs9.existsSync(path11.join(a.project, "deck.json"))) {
2809
+ if (!fs10.existsSync(path12.join(a.project, "deck.json"))) {
2607
2810
  return {
2608
2811
  problems: 1,
2609
2812
  result: {
@@ -2667,11 +2870,11 @@ async function check(a) {
2667
2870
  }
2668
2871
  const r = await checkDesignSystem(a.project);
2669
2872
  const problems = r.issues.length;
2670
- if (fs9.existsSync(path11.join(a.project, DESIGN_SYSTEM_FILE))) {
2671
- if (!fs9.existsSync(path11.join(a.project, "README.md"))) {
2873
+ if (fs10.existsSync(path12.join(a.project, DESIGN_SYSTEM_FILE))) {
2874
+ if (!fs10.existsSync(path12.join(a.project, "README.md"))) {
2672
2875
  r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
2673
2876
  }
2674
- if (!fs9.existsSync(path11.join(a.project, "components", "Cover", "preview.html"))) {
2877
+ if (!fs10.existsSync(path12.join(a.project, "components", "Cover", "preview.html"))) {
2675
2878
  r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
2676
2879
  }
2677
2880
  }
@@ -2697,11 +2900,11 @@ function formatBytes(n) {
2697
2900
  }
2698
2901
  async function upload(a, given, group) {
2699
2902
  if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
2700
- const file = path11.resolve(given);
2903
+ const file = path12.resolve(given);
2701
2904
  const stat = await fsp2.stat(file).catch(() => null);
2702
2905
  if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
2703
2906
  if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
2704
- const ext = path11.extname(file).slice(1).toLowerCase();
2907
+ const ext = path12.extname(file).slice(1).toLowerCase();
2705
2908
  const type = contentTypeOf(file);
2706
2909
  const isMedia = type.startsWith("image/") || type.startsWith("font/");
2707
2910
  let limit = UPLOAD_MAX_BYTES;
@@ -2726,7 +2929,7 @@ async function upload(a, given, group) {
2726
2929
  if (stat.size > limit) throw new CliError("FILE_TOO_LARGE", `${given} is ${formatBytes(stat.size)}, over the ${formatBytes(limit)} limit: compress or shrink it first.`);
2727
2930
  const bytes = await fsp2.readFile(file);
2728
2931
  const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
2729
- const base = path11.basename(file);
2932
+ const base = path12.basename(file);
2730
2933
  if (KIND !== "design-system") {
2731
2934
  return {
2732
2935
  url: uploaded.url,
@@ -2758,12 +2961,12 @@ function folderName(name) {
2758
2961
  }
2759
2962
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
2760
2963
  async function listSystems(dirs) {
2761
- const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path11.delimiter).filter(Boolean);
2762
- const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path11.resolve(d));
2964
+ const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path12.delimiter).filter(Boolean);
2965
+ const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path12.resolve(d));
2763
2966
  const found = /* @__PURE__ */ new Map();
2764
2967
  const visit = async (dir, depth) => {
2765
- const project = path11.join(dir, "project");
2766
- if (fs9.existsSync(path11.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
2968
+ const project = path12.join(dir, "project");
2969
+ if (fs10.existsSync(path12.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
2767
2970
  const info = await readDesignSystemInfo(project).catch(() => null);
2768
2971
  if (info) {
2769
2972
  const namespace = info.namespace;
@@ -2772,8 +2975,8 @@ async function listSystems(dirs) {
2772
2975
  namespace: namespace ?? null,
2773
2976
  folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
2774
2977
  root: dir,
2775
- readme: fs9.existsSync(path11.join(project, "README.md")) ? path11.join(project, "README.md") : null,
2776
- tokens: fs9.existsSync(path11.join(project, "tokens.json")) ? path11.join(project, "tokens.json") : null
2978
+ readme: fs10.existsSync(path12.join(project, "README.md")) ? path12.join(project, "README.md") : null,
2979
+ tokens: fs10.existsSync(path12.join(project, "tokens.json")) ? path12.join(project, "tokens.json") : null
2777
2980
  });
2778
2981
  return;
2779
2982
  }
@@ -2782,7 +2985,7 @@ async function listSystems(dirs) {
2782
2985
  const entries = await fsp2.readdir(dir, { withFileTypes: true }).catch(() => []);
2783
2986
  for (const entry of entries) {
2784
2987
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
2785
- await visit(path11.join(dir, entry.name), depth - 1);
2988
+ await visit(path12.join(dir, entry.name), depth - 1);
2786
2989
  }
2787
2990
  };
2788
2991
  for (const start of starts) await visit(start, 4);
@@ -2794,16 +2997,22 @@ function serverArgs(flags) {
2794
2997
  if (flags.has("port")) out.push("--port", flags.get("port"));
2795
2998
  if (flags.has("host")) out.push("--host", flags.get("host"));
2796
2999
  if (flags.get("agent-hook")) out.push("--agent-hook", flags.get("agent-hook"));
3000
+ if (flags.has("lan")) out.push("--lan");
2797
3001
  return out;
2798
3002
  }
3003
+ var fail = (code, message) => {
3004
+ throw new CliError(code, message);
3005
+ };
3006
+ var shareCall2 = (url, action) => shareCall(url, action, fail);
3007
+ var lanOn2 = (url, flags) => lanOn(url, flags, fail);
2799
3008
  function reusedNote(a, flags) {
2800
3009
  return flags.get("agent-hook") ? { note: `--agent-hook was not applied: the server was already running. To use it: ${script("stop", a.root)}, then open again.` } : {};
2801
3010
  }
2802
3011
  function assertServerFiles() {
2803
- if (!fs9.existsSync(path11.join(EDITOR_DIR, "index.html"))) {
3012
+ if (!fs10.existsSync(path12.join(EDITOR_DIR, "index.html"))) {
2804
3013
  throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
2805
3014
  }
2806
- if (!fs9.existsSync(SERVER_SCRIPT)) {
3015
+ if (!fs10.existsSync(SERVER_SCRIPT)) {
2807
3016
  throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
2808
3017
  }
2809
3018
  }
@@ -2814,15 +3023,15 @@ async function serve(a, flags) {
2814
3023
  assertServerFiles();
2815
3024
  const existing = await runningServer2(a.root);
2816
3025
  if (existing && existing.pid !== process.pid) {
2817
- print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...reusedNote(a, flags) });
3026
+ print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), ...reusedNote(a, flags) });
2818
3027
  return;
2819
3028
  }
2820
3029
  const host = flags.get("host") ?? "127.0.0.1";
2821
- const port = Number(flags.get("port") ?? 0);
3030
+ const port = flags.has("port") ? Number(flags.get("port")) : DEFAULT_PORTS[KIND];
2822
3031
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
2823
3032
  const server = await import(pathToFileURL(SERVER_SCRIPT).href);
2824
- const state = await server.serveForeground({ root: a.root, port, host, agentHook: flags.get("agent-hook") || void 0 });
2825
- print({ url: state.url, root: a.root, pid: state.pid });
3033
+ const state = await server.serveForeground({ root: a.root, port, host, agentHook: flags.get("agent-hook") || void 0, lan: flags.has("lan"), portGiven: flags.has("port") });
3034
+ print({ url: state.url, root: a.root, pid: state.pid, ...state.lan ? { lan: state.lan } : {} });
2826
3035
  }
2827
3036
  async function open(a, flags) {
2828
3037
  if (!await readInfo(a.project)) {
@@ -2831,36 +3040,38 @@ async function open(a, flags) {
2831
3040
  assertServerFiles();
2832
3041
  const existing = await runningServer2(a.root);
2833
3042
  if (existing) {
2834
- print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...reusedNote(a, flags) });
3043
+ print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), ...reusedNote(a, flags) });
2835
3044
  return;
2836
3045
  }
3046
+ const state = await startBackground(a, flags);
3047
+ const view = await shareCall2(state.url, null).catch(() => null);
3048
+ print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log: path12.join(a.root, LOG_FILE), stop: script("stop", a.root) });
3049
+ }
3050
+ async function startBackground(a, flags) {
2837
3051
  const port = Number(flags.get("port") ?? 0);
2838
3052
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
2839
- const logFile = path11.join(a.root, LOG_FILE);
2840
- const fd = fs9.openSync(logFile, "a");
3053
+ const logFile = path12.join(a.root, LOG_FILE);
3054
+ const fd = fs10.openSync(logFile, "a");
2841
3055
  const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
2842
3056
  child.unref();
2843
- fs9.closeSync(fd);
3057
+ fs10.closeSync(fd);
2844
3058
  let exited = false;
2845
3059
  child.once("exit", () => exited = true);
2846
3060
  const deadline = Date.now() + 15e3;
2847
3061
  while (Date.now() < deadline) {
2848
3062
  await new Promise((r) => setTimeout(r, 150));
2849
3063
  const state = readState(a.root);
2850
- if (state && state.pid === child.pid) {
2851
- print({ url: state.url, root: a.root, pid: state.pid, log: logFile, stop: script("stop", a.root) });
2852
- return;
2853
- }
3064
+ if (state && state.pid === child.pid) return state;
2854
3065
  if (exited) break;
2855
3066
  }
2856
- const tail = fs9.existsSync(logFile) ? fs9.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3067
+ const tail = fs10.existsSync(logFile) ? fs10.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
2857
3068
  throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
2858
3069
  ${tail}` : ""}`);
2859
3070
  }
2860
3071
  async function stop(a) {
2861
3072
  const state = readState(a.root);
2862
3073
  if (!state || !alive(state.pid)) {
2863
- fs9.rmSync(path11.join(a.root, STATE_FILE), { force: true });
3074
+ fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
2864
3075
  print({ stopped: false, message: "No editor server is running for this folder." });
2865
3076
  return;
2866
3077
  }
@@ -2871,9 +3082,28 @@ async function stop(a) {
2871
3082
  print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
2872
3083
  return;
2873
3084
  }
2874
- fs9.rmSync(path11.join(a.root, STATE_FILE), { force: true });
3085
+ fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
2875
3086
  print({ stopped: true, pid: state.pid });
2876
3087
  }
3088
+ async function share(a, flags) {
3089
+ if (!await readInfo(a.project)) {
3090
+ throw new CliError("NOT_A_" + KIND.toUpperCase().replace("-", "_"), `project/${INDEX_FILE} is missing or not a valid ${NOUN} index; write it, then run ${script("check", a.root)}.`);
3091
+ }
3092
+ print(
3093
+ await shareCommand({
3094
+ root: a.root,
3095
+ flags,
3096
+ running: () => runningServer2(a.root),
3097
+ startShared: async () => {
3098
+ assertServerFiles();
3099
+ return startBackground(a, new Map([...flags, ["lan", "true"]]));
3100
+ },
3101
+ offCommand: script("share", a.root, "--off"),
3102
+ serveCommand: script("serve", a.root, "--lan"),
3103
+ fail
3104
+ })
3105
+ );
3106
+ }
2877
3107
  var WAIT_CHUNK_S = 300;
2878
3108
  function commentHints(a) {
2879
3109
  return {
@@ -2896,7 +3126,7 @@ async function listComments(a, all) {
2896
3126
  async function artifactTitle(a) {
2897
3127
  const info = await readInfo(a.project).catch(() => null);
2898
3128
  const title = typeof info?.title === "string" ? info.title.trim() : "";
2899
- return title && title !== "project" ? title : path11.basename(a.root);
3129
+ return title && title !== "project" ? title : path12.basename(a.root);
2900
3130
  }
2901
3131
  async function waitComments(a, flags) {
2902
3132
  const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
@@ -2982,7 +3212,12 @@ Usage:
2982
3212
  node scripts/artifact.mjs serve <root> [--port n] [--host h] run the editor server in the foreground
2983
3213
  [--agent-hook "<command>"] open/serve: run this for each comment sent to the agent
2984
3214
  (the comment as JSON on stdin)
3215
+ [--lan] open/serve: also share on the local network, as share does
2985
3216
  node scripts/artifact.mjs stop <root> stop the background editor server
3217
+ node scripts/artifact.mjs share <root> share on the local network; prints a link per role
3218
+ [--reset R] [--enable R] [--disable R] R: editor, commenter, viewer (comma list) or all
3219
+ [--role G=R,...] give guest G (an id it prints) role R
3220
+ [--off] stop sharing (guests keep their place for next time)
2986
3221
  node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
2987
3222
  node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
2988
3223
  node scripts/artifact.mjs list [dir \u2026] find design systems (folders holding project/design-system.json)
@@ -3005,6 +3240,9 @@ async function main() {
3005
3240
  case "stop":
3006
3241
  await stop(resolveArtifact(positional[0], false));
3007
3242
  return 0;
3243
+ case "share":
3244
+ await share(resolveArtifact(positional[0]), flags);
3245
+ return 0;
3008
3246
  case "check": {
3009
3247
  const { result, problems } = await check(resolveArtifact(positional[0]));
3010
3248
  print(localize(result));
@@ -3012,10 +3250,10 @@ async function main() {
3012
3250
  }
3013
3251
  case "upload": {
3014
3252
  const a = resolveArtifact(positional[0], false);
3015
- if (!fs9.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3253
+ if (!fs10.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3016
3254
  throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
3017
3255
  }
3018
- fs9.mkdirSync(a.project, { recursive: true });
3256
+ fs10.mkdirSync(a.project, { recursive: true });
3019
3257
  print(await upload(a, positional[1], flags.get("group")));
3020
3258
  return 0;
3021
3259
  }