dsh-claude-design 2.6.0 → 2.7.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-design: local Artifact tool (prebuilt; do not edit).
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import fs10 from "node:fs";
6
- import fsp2 from "node:fs/promises";
7
- import path12 from "node:path";
5
+ import fs11 from "node:fs";
6
+ import fsp3 from "node:fs/promises";
7
+ import path13 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}$/;
@@ -2474,6 +2474,7 @@ import fs8 from "node:fs";
2474
2474
  import path10 from "node:path";
2475
2475
  var STATE_FILE = ".artifact-server.json";
2476
2476
  var LOG_FILE = ".artifact-server.log";
2477
+ var EXPORTS_DIR = "exports";
2477
2478
  function readState(root) {
2478
2479
  try {
2479
2480
  return JSON.parse(fs8.readFileSync(path10.join(root, STATE_FILE), "utf8"));
@@ -2501,6 +2502,12 @@ async function runningServer(root, kind, probe) {
2501
2502
  }
2502
2503
  var DEFAULT_PORTS = { slides: 4710, design: 4720, "design-system": 4730, dashboard: 4740, docs: 4750, motion: 4760 };
2503
2504
 
2505
+ import fs10 from "node:fs";
2506
+ import fsp2 from "node:fs/promises";
2507
+ import path12 from "node:path";
2508
+ import { promisify } from "node:util";
2509
+ import zlib from "node:zlib";
2510
+
2504
2511
  import crypto from "node:crypto";
2505
2512
  import fs9 from "node:fs";
2506
2513
  import path11 from "node:path";
@@ -2633,6 +2640,173 @@ var Sharing = class _Sharing {
2633
2640
  }
2634
2641
  };
2635
2642
 
2643
+ var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
2644
+ var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
2645
+ var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
2646
+ var isTemp = (name) => name.endsWith(".tmp") || name.startsWith(".tmp-");
2647
+ var STORED = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".heic", ".mp4", ".m4v", ".mov", ".webm", ".mp3", ".m4a", ".aac", ".ogg", ".opus", ".flac", ".woff", ".woff2", ".zip", ".gz", ".br", ".zst", ".7z"]);
2648
+ var MAX_ENTRIES = 65535;
2649
+ var MAX_BYTES = 4294967295;
2650
+ var ArchiveTooLarge = class extends Error {
2651
+ code = "TOO_LARGE";
2652
+ };
2653
+ 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)));
2656
+ const entries = [];
2657
+ const walk = async (dir, rel) => {
2658
+ let names;
2659
+ try {
2660
+ names = await fsp2.readdir(dir, { withFileTypes: true });
2661
+ } catch (error) {
2662
+ if (rel && error.code === "ENOENT") return;
2663
+ throw error;
2664
+ }
2665
+ for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2666
+ const name = dirent.name;
2667
+ 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);
2669
+ if (skipped.has(file)) continue;
2670
+ const inner = rel ? `${rel}/${name}` : name;
2671
+ if (dirent.isDirectory()) await walk(file, inner);
2672
+ else if (dirent.isFile()) {
2673
+ const stat = await fsp2.stat(file).catch(() => null);
2674
+ if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
2675
+ }
2676
+ }
2677
+ };
2678
+ await walk(path12.resolve(root), "");
2679
+ 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
+ const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2681
+ 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.`);
2682
+ return entries;
2683
+ }
2684
+ var CRC_TABLE = (() => {
2685
+ const table = new Uint32Array(256);
2686
+ for (let n = 0; n < 256; n++) {
2687
+ let c = n;
2688
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2689
+ table[n] = c >>> 0;
2690
+ }
2691
+ return table;
2692
+ })();
2693
+ function crc32(bytes) {
2694
+ let crc = 4294967295;
2695
+ for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
2696
+ return (crc ^ 4294967295) >>> 0;
2697
+ }
2698
+ function dosTime(date) {
2699
+ const year = date.getFullYear();
2700
+ if (year < 1980) return { time: 0, date: 1 << 5 | 1 };
2701
+ if (year > 2107) return { time: 23 << 11 | 59 << 5 | 29, date: 127 << 9 | 12 << 5 | 31 };
2702
+ return {
2703
+ time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
2704
+ date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
2705
+ };
2706
+ }
2707
+ var deflateRaw = promisify(zlib.deflateRaw);
2708
+ var UTF8 = 1 << 11;
2709
+ async function writeZip(entries, write) {
2710
+ const central = [];
2711
+ let offset = 0;
2712
+ const put = async (chunk) => {
2713
+ await write(chunk);
2714
+ offset += chunk.length;
2715
+ };
2716
+ for (const entry of entries) {
2717
+ let data;
2718
+ try {
2719
+ data = await fsp2.readFile(entry.file);
2720
+ } catch (error) {
2721
+ if (error.code === "ENOENT") continue;
2722
+ throw error;
2723
+ }
2724
+ const crc = crc32(data);
2725
+ let method = 0;
2726
+ let body = data;
2727
+ if (data.length > 0 && !STORED.has(path12.extname(entry.name).toLowerCase())) {
2728
+ const deflated = await deflateRaw(data);
2729
+ if (deflated.length < data.length) {
2730
+ method = 8;
2731
+ body = deflated;
2732
+ }
2733
+ }
2734
+ if (offset + body.length > MAX_BYTES) throw new ArchiveTooLarge("The folder grew past what a zip holds (4 GiB) while it was being zipped.");
2735
+ const name = Buffer.from(entry.name, "utf8");
2736
+ const { time, date } = dosTime(entry.mtime);
2737
+ const local = Buffer.alloc(30);
2738
+ local.writeUInt32LE(67324752, 0);
2739
+ local.writeUInt16LE(20, 4);
2740
+ local.writeUInt16LE(UTF8, 6);
2741
+ local.writeUInt16LE(method, 8);
2742
+ local.writeUInt16LE(time, 10);
2743
+ local.writeUInt16LE(date, 12);
2744
+ local.writeUInt32LE(crc, 14);
2745
+ local.writeUInt32LE(body.length, 18);
2746
+ local.writeUInt32LE(data.length, 22);
2747
+ local.writeUInt16LE(name.length, 26);
2748
+ local.writeUInt16LE(0, 28);
2749
+ const header = Buffer.alloc(46);
2750
+ header.writeUInt32LE(33639248, 0);
2751
+ header.writeUInt16LE(3 << 8 | 20, 4);
2752
+ header.writeUInt16LE(20, 6);
2753
+ header.writeUInt16LE(UTF8, 8);
2754
+ header.writeUInt16LE(method, 10);
2755
+ header.writeUInt16LE(time, 12);
2756
+ header.writeUInt16LE(date, 14);
2757
+ header.writeUInt32LE(crc, 16);
2758
+ header.writeUInt32LE(body.length, 20);
2759
+ header.writeUInt32LE(data.length, 24);
2760
+ header.writeUInt16LE(name.length, 28);
2761
+ header.writeUInt32LE((32768 | (entry.mode || 420) | 384) << 16 >>> 0, 38);
2762
+ header.writeUInt32LE(offset, 42);
2763
+ central.push(header, name);
2764
+ await put(Buffer.concat([local, name]));
2765
+ await put(body);
2766
+ }
2767
+ const start = offset;
2768
+ const directory = Buffer.concat(central);
2769
+ await put(directory);
2770
+ const end = Buffer.alloc(22);
2771
+ end.writeUInt32LE(101010256, 0);
2772
+ end.writeUInt16LE(central.length / 2, 8);
2773
+ end.writeUInt16LE(central.length / 2, 10);
2774
+ end.writeUInt32LE(directory.length, 12);
2775
+ end.writeUInt32LE(start, 16);
2776
+ await put(end);
2777
+ return offset;
2778
+ }
2779
+ function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
2780
+ 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`;
2782
+ }
2783
+ async function writeArchive(root, out) {
2784
+ const file = path12.resolve(out);
2785
+ const entries = await archiveEntries(root, [file]);
2786
+ const tmp = path12.join(path12.dirname(file), `.${path12.basename(file)}.${process.pid}.tmp`);
2787
+ const handle = await fsp2.open(tmp, "w");
2788
+ let bytes;
2789
+ try {
2790
+ bytes = await writeZip(entries, async (chunk) => {
2791
+ await handle.write(chunk);
2792
+ });
2793
+ await handle.close();
2794
+ } catch (error) {
2795
+ await handle.close().catch(() => {
2796
+ });
2797
+ await fsp2.rm(tmp, { force: true });
2798
+ throw error;
2799
+ }
2800
+ await fsp2.rename(tmp, file);
2801
+ return { file, files: entries.length, bytes };
2802
+ }
2803
+ 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;
2806
+ const { file, files, bytes } = await writeArchive(root, target);
2807
+ return { file, folder: path12.basename(path12.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2808
+ }
2809
+
2636
2810
  async function shareCall(url, action, fail2) {
2637
2811
  const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
2638
2812
  const body = await response.json().catch(() => ({}));
@@ -2683,13 +2857,20 @@ async function shareCommand(o) {
2683
2857
  for (const change of changes) view = await shareCall(server.url, change, o.fail);
2684
2858
  if (off || !view.lan) view = await shareCall(server.url, { action: "lan", on: !off }, o.fail);
2685
2859
  const links = view.links;
2860
+ const shown = (list) => Object.fromEntries(Object.entries(list).map(([role, link]) => [role, link.on ? link.url : "off"]));
2861
+ const tailnet = view.lan ? view.tailnet : void 0;
2686
2862
  return {
2687
2863
  lan: view.lan,
2688
2864
  ...view.lan ? { addresses: lanUrls(view) } : {},
2689
- links: Object.fromEntries(Object.entries(links).map(([role, link]) => [role, link.on ? link.url : "off"])),
2865
+ links: shown(links),
2866
+ ...tailnet ? {
2867
+ tailscale: shown(tailnet.links),
2868
+ tailscaleNote: `This computer has a Tailscale address (${tailnet.address}): the tailscale links open the same roles for people on your tailnet, from any network. Use the links above for people on this Wi-Fi or LAN.`
2869
+ } : {},
2690
2870
  guests: view.guests,
2691
2871
  ...view.lan ? {
2692
2872
  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.",
2873
+ 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.",
2693
2874
  stop: o.offCommand
2694
2875
  } : { note: "Not shared on the network: the links work on this computer only." }
2695
2876
  };
@@ -2707,8 +2888,8 @@ process.emitWarning = ((warning, ...rest) => {
2707
2888
  });
2708
2889
  var KIND = "design";
2709
2890
  var SCRIPT = fileURLToPath2(import.meta.url);
2710
- var EDITOR_DIR = path12.join(path12.dirname(SCRIPT), "editor");
2711
- var SERVER_SCRIPT = path12.join(path12.dirname(SCRIPT), "server.mjs");
2891
+ var EDITOR_DIR = path13.join(path13.dirname(SCRIPT), "editor");
2892
+ var SERVER_SCRIPT = path13.join(path13.dirname(SCRIPT), "server.mjs");
2712
2893
  var MAX_LISTED = 200;
2713
2894
  var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
2714
2895
  var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
@@ -2747,13 +2928,13 @@ function quote(s) {
2747
2928
  }
2748
2929
  function resolveArtifact(given, mustExist = true) {
2749
2930
  if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
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);
2931
+ let root = path13.resolve(given);
2932
+ if (path13.basename(root) === "project" && fs11.existsSync(path13.join(root, INDEX_FILE)) && !fs11.existsSync(path13.join(root, "project"))) {
2933
+ root = path13.dirname(root);
2753
2934
  }
2754
- const project = path12.join(root, "project");
2935
+ const project = path13.join(root, "project");
2755
2936
  if (mustExist) {
2756
- const stat = fs10.statSync(project, { throwIfNoEntry: false });
2937
+ const stat = fs11.statSync(project, { throwIfNoEntry: false });
2757
2938
  if (!stat?.isDirectory()) {
2758
2939
  throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
2759
2940
  }
@@ -2775,7 +2956,7 @@ function localize(value) {
2775
2956
  function designSystemNotes(project, indexFile) {
2776
2957
  let index;
2777
2958
  try {
2778
- index = JSON.parse(fs10.readFileSync(path12.join(project, indexFile), "utf8"));
2959
+ index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
2779
2960
  } catch {
2780
2961
  return [];
2781
2962
  }
@@ -2789,10 +2970,10 @@ function designSystemNotes(project, indexFile) {
2789
2970
  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>/.`);
2790
2971
  continue;
2791
2972
  }
2792
- const folder = path12.join(project, "ds", ns);
2793
- if (!fs10.existsSync(folder)) {
2973
+ const folder = path13.join(project, "ds", ns);
2974
+ if (!fs11.existsSync(folder)) {
2794
2975
  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.`);
2795
- } else if (!fs10.existsSync(path12.join(folder, "tokens.json"))) {
2976
+ } else if (!fs11.existsSync(path13.join(folder, "tokens.json"))) {
2796
2977
  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).`);
2797
2978
  }
2798
2979
  }
@@ -2804,9 +2985,9 @@ async function check(a) {
2804
2985
  try {
2805
2986
  rt = loadSlidesCompiler(EDITOR_DIR);
2806
2987
  } catch {
2807
- throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path12.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
2988
+ throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path13.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
2808
2989
  }
2809
- if (!fs10.existsSync(path12.join(a.project, "deck.json"))) {
2990
+ if (!fs11.existsSync(path13.join(a.project, "deck.json"))) {
2810
2991
  return {
2811
2992
  problems: 1,
2812
2993
  result: {
@@ -2870,11 +3051,11 @@ async function check(a) {
2870
3051
  }
2871
3052
  const r = await checkDesignSystem(a.project);
2872
3053
  const problems = r.issues.length;
2873
- if (fs10.existsSync(path12.join(a.project, DESIGN_SYSTEM_FILE))) {
2874
- if (!fs10.existsSync(path12.join(a.project, "README.md"))) {
3054
+ if (fs11.existsSync(path13.join(a.project, DESIGN_SYSTEM_FILE))) {
3055
+ if (!fs11.existsSync(path13.join(a.project, "README.md"))) {
2875
3056
  r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
2876
3057
  }
2877
- if (!fs10.existsSync(path12.join(a.project, "components", "Cover", "preview.html"))) {
3058
+ if (!fs11.existsSync(path13.join(a.project, "components", "Cover", "preview.html"))) {
2878
3059
  r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
2879
3060
  }
2880
3061
  }
@@ -2900,11 +3081,11 @@ function formatBytes(n) {
2900
3081
  }
2901
3082
  async function upload(a, given, group) {
2902
3083
  if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
2903
- const file = path12.resolve(given);
2904
- const stat = await fsp2.stat(file).catch(() => null);
3084
+ const file = path13.resolve(given);
3085
+ const stat = await fsp3.stat(file).catch(() => null);
2905
3086
  if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
2906
3087
  if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
2907
- const ext = path12.extname(file).slice(1).toLowerCase();
3088
+ const ext = path13.extname(file).slice(1).toLowerCase();
2908
3089
  const type = contentTypeOf(file);
2909
3090
  const isMedia = type.startsWith("image/") || type.startsWith("font/");
2910
3091
  let limit = UPLOAD_MAX_BYTES;
@@ -2927,9 +3108,9 @@ async function upload(a, given, group) {
2927
3108
  limit = designSystemUploadLimit(type);
2928
3109
  }
2929
3110
  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.`);
2930
- const bytes = await fsp2.readFile(file);
3111
+ const bytes = await fsp3.readFile(file);
2931
3112
  const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
2932
- const base = path12.basename(file);
3113
+ const base = path13.basename(file);
2933
3114
  if (KIND !== "design-system") {
2934
3115
  return {
2935
3116
  url: uploaded.url,
@@ -2961,12 +3142,12 @@ function folderName(name) {
2961
3142
  }
2962
3143
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
2963
3144
  async function listSystems(dirs) {
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));
3145
+ const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path13.delimiter).filter(Boolean);
3146
+ const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path13.resolve(d));
2966
3147
  const found = /* @__PURE__ */ new Map();
2967
3148
  const visit = async (dir, depth) => {
2968
- const project = path12.join(dir, "project");
2969
- if (fs10.existsSync(path12.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
3149
+ const project = path13.join(dir, "project");
3150
+ if (fs11.existsSync(path13.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
2970
3151
  const info = await readDesignSystemInfo(project).catch(() => null);
2971
3152
  if (info) {
2972
3153
  const namespace = info.namespace;
@@ -2975,17 +3156,17 @@ async function listSystems(dirs) {
2975
3156
  namespace: namespace ?? null,
2976
3157
  folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
2977
3158
  root: dir,
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
3159
+ readme: fs11.existsSync(path13.join(project, "README.md")) ? path13.join(project, "README.md") : null,
3160
+ tokens: fs11.existsSync(path13.join(project, "tokens.json")) ? path13.join(project, "tokens.json") : null
2980
3161
  });
2981
3162
  return;
2982
3163
  }
2983
3164
  }
2984
3165
  if (depth <= 0) return;
2985
- const entries = await fsp2.readdir(dir, { withFileTypes: true }).catch(() => []);
3166
+ const entries = await fsp3.readdir(dir, { withFileTypes: true }).catch(() => []);
2986
3167
  for (const entry of entries) {
2987
3168
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
2988
- await visit(path12.join(dir, entry.name), depth - 1);
3169
+ await visit(path13.join(dir, entry.name), depth - 1);
2989
3170
  }
2990
3171
  };
2991
3172
  for (const start of starts) await visit(start, 4);
@@ -3009,10 +3190,10 @@ function reusedNote(a, flags) {
3009
3190
  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.` } : {};
3010
3191
  }
3011
3192
  function assertServerFiles() {
3012
- if (!fs10.existsSync(path12.join(EDITOR_DIR, "index.html"))) {
3193
+ if (!fs11.existsSync(path13.join(EDITOR_DIR, "index.html"))) {
3013
3194
  throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
3014
3195
  }
3015
- if (!fs10.existsSync(SERVER_SCRIPT)) {
3196
+ if (!fs11.existsSync(SERVER_SCRIPT)) {
3016
3197
  throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
3017
3198
  }
3018
3199
  }
@@ -3045,16 +3226,16 @@ async function open(a, flags) {
3045
3226
  }
3046
3227
  const state = await startBackground(a, flags);
3047
3228
  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) });
3229
+ print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log: path13.join(a.root, LOG_FILE), stop: script("stop", a.root) });
3049
3230
  }
3050
3231
  async function startBackground(a, flags) {
3051
3232
  const port = Number(flags.get("port") ?? 0);
3052
3233
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
3053
- const logFile = path12.join(a.root, LOG_FILE);
3054
- const fd = fs10.openSync(logFile, "a");
3234
+ const logFile = path13.join(a.root, LOG_FILE);
3235
+ const fd = fs11.openSync(logFile, "a");
3055
3236
  const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
3056
3237
  child.unref();
3057
- fs10.closeSync(fd);
3238
+ fs11.closeSync(fd);
3058
3239
  let exited = false;
3059
3240
  child.once("exit", () => exited = true);
3060
3241
  const deadline = Date.now() + 15e3;
@@ -3064,14 +3245,14 @@ async function startBackground(a, flags) {
3064
3245
  if (state && state.pid === child.pid) return state;
3065
3246
  if (exited) break;
3066
3247
  }
3067
- const tail = fs10.existsSync(logFile) ? fs10.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3248
+ const tail = fs11.existsSync(logFile) ? fs11.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3068
3249
  throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
3069
3250
  ${tail}` : ""}`);
3070
3251
  }
3071
3252
  async function stop(a) {
3072
3253
  const state = readState(a.root);
3073
3254
  if (!state || !alive(state.pid)) {
3074
- fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
3255
+ fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3075
3256
  print({ stopped: false, message: "No editor server is running for this folder." });
3076
3257
  return;
3077
3258
  }
@@ -3082,7 +3263,7 @@ async function stop(a) {
3082
3263
  print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
3083
3264
  return;
3084
3265
  }
3085
- fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
3266
+ fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3086
3267
  print({ stopped: true, pid: state.pid });
3087
3268
  }
3088
3269
  async function share(a, flags) {
@@ -3126,7 +3307,7 @@ async function listComments(a, all) {
3126
3307
  async function artifactTitle(a) {
3127
3308
  const info = await readInfo(a.project).catch(() => null);
3128
3309
  const title = typeof info?.title === "string" ? info.title.trim() : "";
3129
- return title && title !== "project" ? title : path12.basename(a.root);
3310
+ return title && title !== "project" ? title : path13.basename(a.root);
3130
3311
  }
3131
3312
  async function waitComments(a, flags) {
3132
3313
  const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
@@ -3226,6 +3407,8 @@ Usage:
3226
3407
  node scripts/artifact.mjs comments <root> --wait [--timeout s] wait for a comment sent to the agent from the open editor
3227
3408
  node scripts/artifact.mjs comment-reply <root> <id> <text|-> reply as the agent (-: text on stdin) [--resolve]
3228
3409
  node scripts/artifact.mjs comment-resolve <root> <id> [--reopen] mark a comment resolved (or open again)
3410
+ node scripts/artifact.mjs archive <root> [--out file.zip|dir] the whole folder as one zip (backup, another computer,
3411
+ someone else); beside the folder unless --out says
3229
3412
 
3230
3413
  <root> is the artifact folder: its files live at <root>/project/<path> (project/${INDEX_FILE}, \u2026).`;
3231
3414
  async function main() {
@@ -3251,10 +3434,10 @@ async function main() {
3251
3434
  }
3252
3435
  case "upload": {
3253
3436
  const a = resolveArtifact(positional[0], false);
3254
- if (!fs10.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3437
+ if (!fs11.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3255
3438
  throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
3256
3439
  }
3257
- fs10.mkdirSync(a.project, { recursive: true });
3440
+ fs11.mkdirSync(a.project, { recursive: true });
3258
3441
  print(await upload(a, positional[1], flags.get("group")));
3259
3442
  return 0;
3260
3443
  }
@@ -3272,6 +3455,12 @@ async function main() {
3272
3455
  case "comment-resolve":
3273
3456
  print(await resolveComment(resolveArtifact(positional[0]), positional[1], flags.has("reopen")));
3274
3457
  return 0;
3458
+ case "archive": {
3459
+ const a = resolveArtifact(positional[0]);
3460
+ const made = await archiveCommand(a.root, flags.get("out"));
3461
+ print({ ...made, open: `unzip it anywhere, then ${script("open", `<there>/${made.folder}`)}` });
3462
+ return 0;
3463
+ }
3275
3464
  case void 0:
3276
3465
  case "help":
3277
3466
  case "--help":