dsh-claude-slides 2.7.2 → 2.8.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.
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // claude-slides: local Artifact tool (prebuilt; do not edit).
3
3
 
4
- import { spawn } from "node:child_process";
5
- import fs12 from "node:fs";
4
+ import fs13 from "node:fs";
6
5
  import fsp3 from "node:fs/promises";
7
- import path14 from "node:path";
6
+ import path15 from "node:path";
8
7
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
9
8
 
10
9
  var BLOB_ID_PATTERN = /^[0-9a-f]{32}$/;
@@ -2501,12 +2500,15 @@ async function runningServer(root, kind, probe) {
2501
2500
  }
2502
2501
  }
2503
2502
  var DEFAULT_PORTS = { slides: 4710, design: 4720, "design-system": 4730, dashboard: 4740, docs: 4750, motion: 4760 };
2503
+ var PORT_TRIES = 100;
2504
+ function serverOrigin(host, port) {
2505
+ const shown = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
2506
+ return `http://${shown}:${port}`;
2507
+ }
2504
2508
 
2509
+ import { spawn } from "node:child_process";
2505
2510
  import fs10 from "node:fs";
2506
- import fsp2 from "node:fs/promises";
2507
2511
  import path12 from "node:path";
2508
- import { promisify } from "node:util";
2509
- import zlib from "node:zlib";
2510
2512
 
2511
2513
  import crypto from "node:crypto";
2512
2514
  import fs9 from "node:fs";
@@ -2640,6 +2642,88 @@ var Sharing = class _Sharing {
2640
2642
  }
2641
2643
  };
2642
2644
 
2645
+ var MAX_MESSAGE = 64 * 1024 * 1024;
2646
+
2647
+ var HUB_ROUTE = "/__hub";
2648
+ function buildOf(script2) {
2649
+ const real = fs10.realpathSync(script2);
2650
+ const { mtimeMs, size } = fs10.statSync(real);
2651
+ return `${real}:${mtimeMs}:${size}`;
2652
+ }
2653
+ async function hubAt(origin) {
2654
+ try {
2655
+ const response = await fetch(`${origin}${HUB_ROUTE}`, { signal: AbortSignal.timeout(1e3) });
2656
+ if (!response.ok) return null;
2657
+ const info = await response.json();
2658
+ return info.hub === 1 ? info : null;
2659
+ } catch {
2660
+ return null;
2661
+ }
2662
+ }
2663
+ async function joinHub(call, below = Infinity) {
2664
+ if (call.port === 0) return null;
2665
+ const ports = call.portGiven ? [call.port] : Array.from({ length: PORT_TRIES }, (_, i) => call.port + i).filter((port) => port <= 65535 && port < below);
2666
+ const build = buildOf(call.script);
2667
+ const origins = ports.map((port) => serverOrigin(call.host, port));
2668
+ const infos = await Promise.all(origins.map(hubAt));
2669
+ for (const [i, info] of infos.entries()) {
2670
+ if (!info || info.kind !== call.kind || info.build !== build || info.host !== call.host) continue;
2671
+ const response = await fetch(`${origins[i]}${HUB_ROUTE}`, {
2672
+ method: "POST",
2673
+ headers: { "Content-Type": "application/json" },
2674
+ body: JSON.stringify({ action: "mount", root: call.root, ...call.agentHook ? { agentHook: call.agentHook } : {}, lan: call.lan === true }),
2675
+ signal: AbortSignal.timeout(2e4)
2676
+ }).catch(() => null);
2677
+ if (!response) continue;
2678
+ const body = await response.json().catch(() => ({}));
2679
+ if (response.ok && body.state) return body.state;
2680
+ if (response.status === 409) continue;
2681
+ throw Object.assign(new Error(body.message ?? `the server answered ${response.status}`), { code: body.code ?? "SERVER_FAILED" });
2682
+ }
2683
+ return null;
2684
+ }
2685
+ async function leaveHub(state) {
2686
+ let remaining = 0;
2687
+ const response = await fetch(`${new URL(state.url).origin}${HUB_ROUTE}`, {
2688
+ method: "POST",
2689
+ headers: { "Content-Type": "application/json" },
2690
+ body: JSON.stringify({ action: "unmount", root: state.root }),
2691
+ signal: AbortSignal.timeout(5e3)
2692
+ }).catch(() => null);
2693
+ const body = response ? await response.json().catch(() => null) : null;
2694
+ if (body?.hub === 1) remaining = Number(body.remaining ?? 0);
2695
+ else if (alive(state.pid)) process.kill(state.pid, "SIGTERM");
2696
+ const deadline = Date.now() + 5e3;
2697
+ while (remaining === 0 && alive(state.pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
2698
+ if (remaining === 0 && alive(state.pid)) return false;
2699
+ fs10.rmSync(path12.join(state.root, STATE_FILE), { force: true });
2700
+ return true;
2701
+ }
2702
+ async function openOnHub(call) {
2703
+ const joined = await joinHub(call);
2704
+ if (joined) return { state: joined };
2705
+ const logFile = path12.join(call.root, LOG_FILE);
2706
+ const fd = fs10.openSync(logFile, "a");
2707
+ const child = spawn(process.execPath, [...call.command, "--background"], { detached: true, stdio: ["ignore", "ignore", fd] });
2708
+ child.unref();
2709
+ fs10.closeSync(fd);
2710
+ let exited = false;
2711
+ child.once("exit", () => exited = true);
2712
+ const deadline = Date.now() + 15e3;
2713
+ while (Date.now() < deadline) {
2714
+ await new Promise((resolve) => setTimeout(resolve, 120));
2715
+ const state = await call.running();
2716
+ if (state) return { state };
2717
+ if (exited && !readState(call.root)) break;
2718
+ }
2719
+ return { tail: fs10.existsSync(logFile) ? fs10.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "" };
2720
+ }
2721
+
2722
+ import fs11 from "node:fs";
2723
+ import fsp2 from "node:fs/promises";
2724
+ import path13 from "node:path";
2725
+ import { promisify } from "node:util";
2726
+ import zlib from "node:zlib";
2643
2727
  var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
2644
2728
  var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
2645
2729
  var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
@@ -2651,8 +2735,8 @@ var ArchiveTooLarge = class extends Error {
2651
2735
  code = "TOO_LARGE";
2652
2736
  };
2653
2737
  async function archiveEntries(root, skip = []) {
2654
- const folder = path12.basename(path12.resolve(root)) || "artifact";
2655
- const skipped = new Set(skip.map((file) => path12.resolve(file)));
2738
+ const folder = path13.basename(path13.resolve(root)) || "artifact";
2739
+ const skipped = new Set(skip.map((file) => path13.resolve(file)));
2656
2740
  const entries = [];
2657
2741
  const walk2 = async (dir, rel) => {
2658
2742
  let names;
@@ -2665,7 +2749,7 @@ async function archiveEntries(root, skip = []) {
2665
2749
  for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2666
2750
  const name = dirent.name;
2667
2751
  if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
2668
- const file = path12.join(dir, name);
2752
+ const file = path13.join(dir, name);
2669
2753
  if (skipped.has(file)) continue;
2670
2754
  const inner = rel ? `${rel}/${name}` : name;
2671
2755
  if (dirent.isDirectory()) await walk2(file, inner);
@@ -2675,7 +2759,7 @@ async function archiveEntries(root, skip = []) {
2675
2759
  }
2676
2760
  }
2677
2761
  };
2678
- await walk2(path12.resolve(root), "");
2762
+ await walk2(path13.resolve(root), "");
2679
2763
  if (entries.length > MAX_ENTRIES) throw new ArchiveTooLarge(`The folder holds ${entries.length} files, more than a zip holds (${MAX_ENTRIES}); copy the folder itself instead.`);
2680
2764
  const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2681
2765
  if (bound > MAX_BYTES) throw new ArchiveTooLarge(`The folder holds ${(bound / 2 ** 30).toFixed(1)} GiB, more than a zip holds (4 GiB); copy the folder itself instead.`);
@@ -2724,7 +2808,7 @@ async function writeZip(entries, write) {
2724
2808
  const crc = crc32(data);
2725
2809
  let method = 0;
2726
2810
  let body = data;
2727
- if (data.length > 0 && !STORED.has(path12.extname(entry.name).toLowerCase())) {
2811
+ if (data.length > 0 && !STORED.has(path13.extname(entry.name).toLowerCase())) {
2728
2812
  const deflated = await deflateRaw(data);
2729
2813
  if (deflated.length < data.length) {
2730
2814
  method = 8;
@@ -2778,12 +2862,12 @@ async function writeZip(entries, write) {
2778
2862
  }
2779
2863
  function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
2780
2864
  const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2781
- return `${path12.basename(path12.resolve(root)) || "artifact"}-${day}.zip`;
2865
+ return `${path13.basename(path13.resolve(root)) || "artifact"}-${day}.zip`;
2782
2866
  }
2783
2867
  async function writeArchive(root, out) {
2784
- const file = path12.resolve(out);
2868
+ const file = path13.resolve(out);
2785
2869
  const entries = await archiveEntries(root, [file]);
2786
- const tmp = path12.join(path12.dirname(file), `.${path12.basename(file)}.${process.pid}.tmp`);
2870
+ const tmp = path13.join(path13.dirname(file), `.${path13.basename(file)}.${process.pid}.tmp`);
2787
2871
  const handle = await fsp2.open(tmp, "w");
2788
2872
  let bytes;
2789
2873
  try {
@@ -2801,14 +2885,14 @@ async function writeArchive(root, out) {
2801
2885
  return { file, files: entries.length, bytes };
2802
2886
  }
2803
2887
  async function archiveCommand(root, out) {
2804
- const where = out ? path12.resolve(out) : path12.dirname(path12.resolve(root));
2805
- const target = fs10.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path12.join(where, archiveFileName(root)) : where;
2888
+ const where = out ? path13.resolve(out) : path13.dirname(path13.resolve(root));
2889
+ const target = fs11.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path13.join(where, archiveFileName(root)) : where;
2806
2890
  const { file, files, bytes } = await writeArchive(root, target);
2807
- return { file, folder: path12.basename(path12.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2891
+ return { file, folder: path13.basename(path13.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2808
2892
  }
2809
2893
 
2810
- import fs11 from "node:fs";
2811
- import path13 from "node:path";
2894
+ import fs12 from "node:fs";
2895
+ import path14 from "node:path";
2812
2896
  var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
2813
2897
  var MAX_FILES = 2e3;
2814
2898
  var MAX_FILE_BYTES = 64 * 1024 * 1024;
@@ -2816,7 +2900,7 @@ var DESIGN_SYSTEM_FILE2 = "design-system.json";
2816
2900
  function readRecords(project, indexFile) {
2817
2901
  let index;
2818
2902
  try {
2819
- index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
2903
+ index = JSON.parse(fs12.readFileSync(path14.join(project, indexFile), "utf8"));
2820
2904
  } catch {
2821
2905
  return [];
2822
2906
  }
@@ -2835,11 +2919,11 @@ function readRecords(project, indexFile) {
2835
2919
  return out;
2836
2920
  }
2837
2921
  function plain(p, want) {
2838
- const stat = fs11.lstatSync(p, { throwIfNoEntry: false });
2922
+ const stat = fs12.lstatSync(p, { throwIfNoEntry: false });
2839
2923
  return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
2840
2924
  }
2841
2925
  function walk(dir, prefix = "", out = []) {
2842
- for (const entry of fs11.readdirSync(path13.join(dir, prefix), { withFileTypes: true })) {
2926
+ for (const entry of fs12.readdirSync(path14.join(dir, prefix), { withFileTypes: true })) {
2843
2927
  if (out.length >= MAX_FILES) break;
2844
2928
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2845
2929
  if (entry.isDirectory()) walk(dir, rel, out);
@@ -2847,22 +2931,22 @@ function walk(dir, prefix = "", out = []) {
2847
2931
  }
2848
2932
  return out;
2849
2933
  }
2850
- var projectOf = (candidate) => path13.basename(candidate) === "project" ? candidate : path13.join(candidate, "project");
2934
+ var projectOf = (candidate) => path14.basename(candidate) === "project" ? candidate : path14.join(candidate, "project");
2851
2935
  function sourceProject(artifact, root, from) {
2852
2936
  const given = from ?? artifact;
2853
2937
  if (!given) return { reason: "the record names no source folder", kind: "none" };
2854
2938
  if (!from && /^[a-z][a-z0-9+.-]*:\/\//i.test(given)) return { reason: `its source is ${given}, which is not a folder on this machine`, kind: "remote" };
2855
- const candidates = path13.isAbsolute(given) ? [given] : [path13.resolve(given), path13.resolve(root, given), path13.resolve(root, "..", given)];
2939
+ const candidates = path14.isAbsolute(given) ? [given] : [path14.resolve(given), path14.resolve(root, given), path14.resolve(root, "..", given)];
2856
2940
  for (const candidate of candidates) {
2857
2941
  const project = projectOf(candidate);
2858
2942
  if (plain(project, "dir")) return { project };
2859
2943
  }
2860
- if (!from && !path13.isAbsolute(given)) {
2861
- let dir = path13.resolve(root, "..");
2862
- for (let up = path13.dirname(dir); up !== dir; up = path13.dirname(dir)) {
2944
+ if (!from && !path14.isAbsolute(given)) {
2945
+ let dir = path14.resolve(root, "..");
2946
+ for (let up = path14.dirname(dir); up !== dir; up = path14.dirname(dir)) {
2863
2947
  dir = up;
2864
- const project = projectOf(path13.resolve(dir, given));
2865
- if (plain(path13.join(project, DESIGN_SYSTEM_FILE2), "file")) return { project };
2948
+ const project = projectOf(path14.resolve(dir, given));
2949
+ if (plain(path14.join(project, DESIGN_SYSTEM_FILE2), "file")) return { project };
2866
2950
  }
2867
2951
  }
2868
2952
  return { reason: `its source folder ${given} was not found${from ? "" : ` (tried from ${process.cwd()} and the folders above the artifact); pass --from <folder>`}`, kind: "not-found" };
@@ -2871,21 +2955,21 @@ function compareSystem(project, root, record, from) {
2871
2955
  const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
2872
2956
  const found = sourceProject(record.artifact, root, from);
2873
2957
  if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
2874
- diff.source = path13.dirname(found.project);
2875
- const copy = path13.join(project, "ds", record.namespace);
2958
+ diff.source = path14.dirname(found.project);
2959
+ const copy = path14.join(project, "ds", record.namespace);
2876
2960
  if (!plain(copy, "dir")) return { ...diff, missing: true };
2877
2961
  for (const rel of walk(copy)) {
2878
- const mine = path13.join(copy, rel);
2879
- const theirs = path13.join(found.project, rel);
2962
+ const mine = path14.join(copy, rel);
2963
+ const theirs = path14.join(found.project, rel);
2880
2964
  if (!plain(mine, "file")) continue;
2881
2965
  if (!plain(theirs, "file")) {
2882
2966
  diff.removed.push(rel);
2883
2967
  continue;
2884
2968
  }
2885
- const a = fs11.statSync(mine);
2886
- const b = fs11.statSync(theirs);
2969
+ const a = fs12.statSync(mine);
2970
+ const b = fs12.statSync(theirs);
2887
2971
  if (a.size > MAX_FILE_BYTES || b.size > MAX_FILE_BYTES) continue;
2888
- if (a.size !== b.size || !fs11.readFileSync(mine).equals(fs11.readFileSync(theirs))) diff.changed.push(rel);
2972
+ if (a.size !== b.size || !fs12.readFileSync(mine).equals(fs12.readFileSync(theirs))) diff.changed.push(rel);
2889
2973
  }
2890
2974
  return diff;
2891
2975
  }
@@ -2915,8 +2999,8 @@ function staleNotes(project, root, indexFile, command) {
2915
2999
  return notes;
2916
3000
  }
2917
3001
  function stamp(project, indexFile, namespaces, now) {
2918
- const file = path13.join(project, indexFile);
2919
- const text = fs11.readFileSync(file, "utf8");
3002
+ const file = path14.join(project, indexFile);
3003
+ const text = fs12.readFileSync(file, "utf8");
2920
3004
  const index = JSON.parse(text);
2921
3005
  const list = index.designSystems;
2922
3006
  const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
@@ -2927,9 +3011,9 @@ function stamp(project, indexFile, namespaces, now) {
2927
3011
  const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
2928
3012
  const out = JSON.stringify(index, null, indent);
2929
3013
  const tmp = `${file}.${process.pid}.tmp`;
2930
- fs11.writeFileSync(tmp, text.endsWith("\n") ? `${out}
3014
+ fs12.writeFileSync(tmp, text.endsWith("\n") ? `${out}
2931
3015
  ` : out);
2932
- fs11.renameSync(tmp, file);
3016
+ fs12.renameSync(tmp, file);
2933
3017
  }
2934
3018
  function syncSystems(project, root, indexFile, opts) {
2935
3019
  const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
@@ -2940,10 +3024,10 @@ function syncSystems(project, root, indexFile, opts) {
2940
3024
  const result = { ...diff, copied: [] };
2941
3025
  if (!opts.check && diff.source && !diff.missing) {
2942
3026
  for (const rel of diff.changed) {
2943
- const target = path13.join(project, "ds", diff.namespace, rel);
3027
+ const target = path14.join(project, "ds", diff.namespace, rel);
2944
3028
  const tmp = `${target}.${process.pid}.tmp`;
2945
- fs11.copyFileSync(path13.join(diff.source, "project", rel), tmp);
2946
- fs11.renameSync(tmp, target);
3029
+ fs12.copyFileSync(path14.join(diff.source, "project", rel), tmp);
3030
+ fs12.renameSync(tmp, target);
2947
3031
  result.copied.push(rel);
2948
3032
  }
2949
3033
  if (diff.changed.length > 0) synced.add(diff.namespace);
@@ -2958,7 +3042,7 @@ function syncSystems(project, root, indexFile, opts) {
2958
3042
  }
2959
3043
 
2960
3044
  async function shareCall(url, action, fail2) {
2961
- const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
3045
+ const response = await fetch(new URL("__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
2962
3046
  const body = await response.json().catch(() => ({}));
2963
3047
  if (!response.ok) fail2("SHARE_REFUSED", String(body.message ?? `the server answered ${response.status}`));
2964
3048
  return body;
@@ -2987,6 +3071,7 @@ async function shareCommand(o) {
2987
3071
  ...rolesOf(o.flags, "disable", o.fail).map((role) => ({ action: "link", role, on: false })),
2988
3072
  ...guestRoles(o.flags, o.fail)
2989
3073
  ];
3074
+ const open2 = !off && changes.length === 0;
2990
3075
  let server = await o.running();
2991
3076
  if (!server) {
2992
3077
  const sharing = new Sharing(o.root);
@@ -3000,12 +3085,19 @@ async function shareCommand(o) {
3000
3085
  sharing.setLan(false);
3001
3086
  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)." };
3002
3087
  }
3088
+ if (!open2 && !sharing.state.lan) {
3089
+ return {
3090
+ lan: false,
3091
+ links: Object.fromEntries(GUEST_ROLES.map((role) => [role, sharing.state.links[role].on ? "on" : "off"])),
3092
+ message: `Links changed. Not shared on the local network: ${NOT_SHARED_HINT}`
3093
+ };
3094
+ }
3003
3095
  server = await o.startShared();
3004
3096
  if (!server) o.fail("SERVER_FAILED", `The server did not start. Run ${o.serveCommand} in the foreground to see why.`);
3005
3097
  }
3006
3098
  let view = await shareCall(server.url, null, o.fail);
3007
3099
  for (const change of changes) view = await shareCall(server.url, change, o.fail);
3008
- if (off || !view.lan) view = await shareCall(server.url, { action: "lan", on: !off }, o.fail);
3100
+ if (off || open2 && !view.lan) view = await shareCall(server.url, { action: "lan", on: !off }, o.fail);
3009
3101
  const links = view.links;
3010
3102
  const shown = (list) => Object.fromEntries(Object.entries(list).map(([role, link]) => [role, link.on ? link.url : "off"]));
3011
3103
  const tailnet = view.lan ? view.tailnet : void 0;
@@ -3022,10 +3114,11 @@ async function shareCommand(o) {
3022
3114
  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.",
3023
3115
  security: "It is plain http: others on the network can read what passes. A link is the permission: whoever has it gets its role, so send it only to the people meant to have it, and reset a role's link if it went further.",
3024
3116
  stop: o.offCommand
3025
- } : { note: "Not shared on the network: the links work on this computer only." }
3117
+ } : { note: `Not shared on the network: the links work on this computer only. ${NOT_SHARED_HINT}` }
3026
3118
  };
3027
3119
  }
3028
- var lanUrls = (view) => (Array.isArray(view.addresses) ? view.addresses : []).map((a) => `http://${String(a)}:${String(view.port)}/`);
3120
+ var NOT_SHARED_HINT = "share <root> with no options shares it.";
3121
+ var lanUrls = (view) => (Array.isArray(view.addresses) ? view.addresses : []).map((a) => `http://${String(a)}:${String(view.port)}${String(view.page ?? "/")}`);
3029
3122
  async function lanOn(url, flags, fail2) {
3030
3123
  if (!flags.has("lan")) return {};
3031
3124
  return { lan: lanUrls(await shareCall(url, { action: "lan", on: true }, fail2)) };
@@ -3038,8 +3131,8 @@ process.emitWarning = ((warning, ...rest) => {
3038
3131
  });
3039
3132
  var KIND = "slides";
3040
3133
  var SCRIPT = fileURLToPath2(import.meta.url);
3041
- var EDITOR_DIR = path14.join(path14.dirname(SCRIPT), "editor");
3042
- var SERVER_SCRIPT = path14.join(path14.dirname(SCRIPT), "server.mjs");
3134
+ var EDITOR_DIR = path15.join(path15.dirname(SCRIPT), "editor");
3135
+ var SERVER_SCRIPT = path15.join(path15.dirname(SCRIPT), "server.mjs");
3043
3136
  var MAX_LISTED = 200;
3044
3137
  var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
3045
3138
  var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
@@ -3078,13 +3171,13 @@ function quote(s) {
3078
3171
  }
3079
3172
  function resolveArtifact(given, mustExist = true) {
3080
3173
  if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
3081
- let root = path14.resolve(given);
3082
- if (path14.basename(root) === "project" && fs12.existsSync(path14.join(root, INDEX_FILE)) && !fs12.existsSync(path14.join(root, "project"))) {
3083
- root = path14.dirname(root);
3174
+ let root = path15.resolve(given);
3175
+ if (path15.basename(root) === "project" && fs13.existsSync(path15.join(root, INDEX_FILE)) && !fs13.existsSync(path15.join(root, "project"))) {
3176
+ root = path15.dirname(root);
3084
3177
  }
3085
- const project = path14.join(root, "project");
3178
+ const project = path15.join(root, "project");
3086
3179
  if (mustExist) {
3087
- const stat = fs12.statSync(project, { throwIfNoEntry: false });
3180
+ const stat = fs13.statSync(project, { throwIfNoEntry: false });
3088
3181
  if (!stat?.isDirectory()) {
3089
3182
  throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
3090
3183
  }
@@ -3106,7 +3199,7 @@ function localize(value) {
3106
3199
  function designSystemNotes(project, indexFile) {
3107
3200
  let index;
3108
3201
  try {
3109
- index = JSON.parse(fs12.readFileSync(path14.join(project, indexFile), "utf8"));
3202
+ index = JSON.parse(fs13.readFileSync(path15.join(project, indexFile), "utf8"));
3110
3203
  } catch {
3111
3204
  return [];
3112
3205
  }
@@ -3120,14 +3213,14 @@ function designSystemNotes(project, indexFile) {
3120
3213
  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>/.`);
3121
3214
  continue;
3122
3215
  }
3123
- const folder = path14.join(project, "ds", ns);
3124
- if (!fs12.existsSync(folder)) {
3216
+ const folder = path15.join(project, "ds", ns);
3217
+ if (!fs13.existsSync(folder)) {
3125
3218
  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.`);
3126
- } else if (!fs12.existsSync(path14.join(folder, "tokens.json"))) {
3219
+ } else if (!fs13.existsSync(path15.join(folder, "tokens.json"))) {
3127
3220
  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).`);
3128
3221
  }
3129
3222
  }
3130
- const root = path14.dirname(project);
3223
+ const root = path15.dirname(project);
3131
3224
  return [...notes, ...staleNotes(project, root, indexFile, (ns, listOnly) => script("ds-sync", root, ns, ...listOnly ? ["--check"] : []))];
3132
3225
  }
3133
3226
  async function check(a) {
@@ -3136,9 +3229,9 @@ async function check(a) {
3136
3229
  try {
3137
3230
  rt = loadSlidesCompiler(EDITOR_DIR);
3138
3231
  } catch {
3139
- throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path14.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
3232
+ throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path15.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
3140
3233
  }
3141
- if (!fs12.existsSync(path14.join(a.project, "deck.json"))) {
3234
+ if (!fs13.existsSync(path15.join(a.project, "deck.json"))) {
3142
3235
  return {
3143
3236
  problems: 1,
3144
3237
  result: {
@@ -3202,11 +3295,11 @@ async function check(a) {
3202
3295
  }
3203
3296
  const r = await checkDesignSystem(a.project);
3204
3297
  const problems = r.issues.length;
3205
- if (fs12.existsSync(path14.join(a.project, DESIGN_SYSTEM_FILE))) {
3206
- if (!fs12.existsSync(path14.join(a.project, "README.md"))) {
3298
+ if (fs13.existsSync(path15.join(a.project, DESIGN_SYSTEM_FILE))) {
3299
+ if (!fs13.existsSync(path15.join(a.project, "README.md"))) {
3207
3300
  r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
3208
3301
  }
3209
- if (!fs12.existsSync(path14.join(a.project, "components", "Cover", "preview.html"))) {
3302
+ if (!fs13.existsSync(path15.join(a.project, "components", "Cover", "preview.html"))) {
3210
3303
  r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
3211
3304
  }
3212
3305
  }
@@ -3232,11 +3325,11 @@ function formatBytes(n) {
3232
3325
  }
3233
3326
  async function upload(a, given, group) {
3234
3327
  if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
3235
- const file = path14.resolve(given);
3328
+ const file = path15.resolve(given);
3236
3329
  const stat = await fsp3.stat(file).catch(() => null);
3237
3330
  if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
3238
3331
  if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
3239
- const ext = path14.extname(file).slice(1).toLowerCase();
3332
+ const ext = path15.extname(file).slice(1).toLowerCase();
3240
3333
  const type = contentTypeOf(file);
3241
3334
  const isMedia = type.startsWith("image/") || type.startsWith("font/");
3242
3335
  let limit = UPLOAD_MAX_BYTES;
@@ -3261,7 +3354,7 @@ async function upload(a, given, group) {
3261
3354
  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.`);
3262
3355
  const bytes = await fsp3.readFile(file);
3263
3356
  const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
3264
- const base = path14.basename(file);
3357
+ const base = path15.basename(file);
3265
3358
  if (KIND !== "design-system") {
3266
3359
  return {
3267
3360
  url: uploaded.url,
@@ -3293,12 +3386,12 @@ function folderName(name) {
3293
3386
  }
3294
3387
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
3295
3388
  async function listSystems(dirs) {
3296
- const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path14.delimiter).filter(Boolean);
3297
- const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path14.resolve(d));
3389
+ const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path15.delimiter).filter(Boolean);
3390
+ const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path15.resolve(d));
3298
3391
  const found = /* @__PURE__ */ new Map();
3299
3392
  const visit = async (dir, depth) => {
3300
- const project = path14.join(dir, "project");
3301
- if (fs12.existsSync(path14.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
3393
+ const project = path15.join(dir, "project");
3394
+ if (fs13.existsSync(path15.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
3302
3395
  const info = await readDesignSystemInfo(project).catch(() => null);
3303
3396
  if (info) {
3304
3397
  const namespace = info.namespace;
@@ -3307,8 +3400,8 @@ async function listSystems(dirs) {
3307
3400
  namespace: namespace ?? null,
3308
3401
  folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
3309
3402
  root: dir,
3310
- readme: fs12.existsSync(path14.join(project, "README.md")) ? path14.join(project, "README.md") : null,
3311
- tokens: fs12.existsSync(path14.join(project, "tokens.json")) ? path14.join(project, "tokens.json") : null
3403
+ readme: fs13.existsSync(path15.join(project, "README.md")) ? path15.join(project, "README.md") : null,
3404
+ tokens: fs13.existsSync(path15.join(project, "tokens.json")) ? path15.join(project, "tokens.json") : null
3312
3405
  });
3313
3406
  return;
3314
3407
  }
@@ -3317,7 +3410,7 @@ async function listSystems(dirs) {
3317
3410
  const entries = await fsp3.readdir(dir, { withFileTypes: true }).catch(() => []);
3318
3411
  for (const entry of entries) {
3319
3412
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
3320
- await visit(path14.join(dir, entry.name), depth - 1);
3413
+ await visit(path15.join(dir, entry.name), depth - 1);
3321
3414
  }
3322
3415
  };
3323
3416
  for (const start of starts) await visit(start, 4);
@@ -3341,10 +3434,10 @@ function reusedNote(a, flags) {
3341
3434
  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.` } : {};
3342
3435
  }
3343
3436
  function assertServerFiles() {
3344
- if (!fs12.existsSync(path14.join(EDITOR_DIR, "index.html"))) {
3437
+ if (!fs13.existsSync(path15.join(EDITOR_DIR, "index.html"))) {
3345
3438
  throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
3346
3439
  }
3347
- if (!fs12.existsSync(SERVER_SCRIPT)) {
3440
+ if (!fs13.existsSync(SERVER_SCRIPT)) {
3348
3441
  throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
3349
3442
  }
3350
3443
  }
@@ -3372,49 +3465,48 @@ async function open(a, flags) {
3372
3465
  assertServerFiles();
3373
3466
  const existing = await runningServer2(a.root);
3374
3467
  if (existing) {
3375
- print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), log: path14.join(a.root, LOG_FILE), stop: script("stop", a.root), ...reusedNote(a, flags) });
3468
+ print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), log: path15.join(a.root, LOG_FILE), stop: script("stop", a.root), ...reusedNote(a, flags) });
3376
3469
  return;
3377
3470
  }
3378
3471
  const state = await startBackground(a, flags);
3379
- const view = await shareCall2(state.url, null).catch(() => null);
3380
- print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log: path14.join(a.root, LOG_FILE), stop: script("stop", a.root) });
3472
+ print({ url: state.url, root: a.root, pid: state.pid, ...state.lan?.length ? { lan: state.lan } : {}, log: path15.join(a.root, LOG_FILE), stop: script("stop", a.root) });
3381
3473
  }
3382
3474
  async function startBackground(a, flags) {
3383
- const port = Number(flags.get("port") ?? 0);
3475
+ const host = flags.get("host") ?? "127.0.0.1";
3476
+ const port = flags.has("port") ? Number(flags.get("port")) : DEFAULT_PORTS[KIND];
3384
3477
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
3385
- const logFile = path14.join(a.root, LOG_FILE);
3386
- const fd = fs12.openSync(logFile, "a");
3387
- const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
3388
- child.unref();
3389
- fs12.closeSync(fd);
3390
- let exited = false;
3391
- child.once("exit", () => exited = true);
3392
- const deadline = Date.now() + 15e3;
3393
- while (Date.now() < deadline) {
3394
- await new Promise((r) => setTimeout(r, 150));
3395
- const state = readState(a.root);
3396
- if (state && state.pid === child.pid) return state;
3397
- if (exited) break;
3478
+ const hook = flags.get("agent-hook") || void 0;
3479
+ const opened = await openOnHub({
3480
+ kind: KIND,
3481
+ script: SERVER_SCRIPT,
3482
+ root: a.root,
3483
+ port,
3484
+ portGiven: flags.has("port"),
3485
+ host,
3486
+ ...hook ? { agentHook: hook } : {},
3487
+ lan: flags.has("lan"),
3488
+ command: [SERVER_SCRIPT, a.root, ...serverArgs(flags)],
3489
+ running: () => runningServer2(a.root)
3490
+ });
3491
+ if ("state" in opened) {
3492
+ const view = await shareCall2(opened.state.url, null).catch(() => null);
3493
+ return { ...opened.state, ...view?.lan ? { lan: lanUrls(view) } : {} };
3398
3494
  }
3399
- const tail = fs12.existsSync(logFile) ? fs12.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3400
- throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
3401
- ${tail}` : ""}`);
3495
+ throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${opened.tail ? `
3496
+ ${opened.tail}` : ""}`);
3402
3497
  }
3403
3498
  async function stop(a) {
3404
3499
  const state = readState(a.root);
3405
3500
  if (!state || !alive(state.pid)) {
3406
- fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
3501
+ fs13.rmSync(path15.join(a.root, STATE_FILE), { force: true });
3407
3502
  print({ stopped: false, message: "No editor server is running for this folder." });
3408
3503
  return;
3409
3504
  }
3410
- process.kill(state.pid, "SIGTERM");
3411
- const deadline = Date.now() + 5e3;
3412
- while (alive(state.pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
3413
- if (alive(state.pid)) {
3505
+ if (!await leaveHub(state)) {
3414
3506
  print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
3415
3507
  return;
3416
3508
  }
3417
- fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
3509
+ fs13.rmSync(path15.join(a.root, STATE_FILE), { force: true });
3418
3510
  print({ stopped: true, pid: state.pid });
3419
3511
  }
3420
3512
  async function share(a, flags) {
@@ -3458,7 +3550,7 @@ async function listComments(a, all) {
3458
3550
  async function artifactTitle(a) {
3459
3551
  const info = await readInfo(a.project).catch(() => null);
3460
3552
  const title = typeof info?.title === "string" ? info.title.trim() : "";
3461
- return title && title !== "project" ? title : path14.basename(a.root);
3553
+ return title && title !== "project" ? title : path15.basename(a.root);
3462
3554
  }
3463
3555
  async function waitComments(a, flags) {
3464
3556
  const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
@@ -3550,6 +3642,7 @@ Usage:
3550
3642
  node scripts/artifact.mjs share <root> share on the local network; prints a link per role
3551
3643
  [--reset R] [--enable R] [--disable R] R: editor, commenter, viewer (comma list) or all
3552
3644
  [--role G=R,...] give guest G (an id it prints) role R
3645
+ (these change links only: shared or not stays as it was)
3553
3646
  [--off] stop sharing (guests keep their place for next time)
3554
3647
  node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
3555
3648
  node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
@@ -3588,10 +3681,10 @@ async function main() {
3588
3681
  }
3589
3682
  case "upload": {
3590
3683
  const a = resolveArtifact(positional[0], false);
3591
- if (!fs12.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3684
+ if (!fs13.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3592
3685
  throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
3593
3686
  }
3594
- fs12.mkdirSync(a.project, { recursive: true });
3687
+ fs13.mkdirSync(a.project, { recursive: true });
3595
3688
  print(await upload(a, positional[1], flags.get("group")));
3596
3689
  return 0;
3597
3690
  }