dsh-claude-design 2.7.1 → 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-design: local Artifact tool (prebuilt; do not edit).
3
3
 
4
- import { spawn } from "node:child_process";
5
- import fs11 from "node:fs";
4
+ import fs13 from "node:fs";
6
5
  import fsp3 from "node:fs/promises";
7
- import path13 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,10 +2735,10 @@ 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
- const walk = async (dir, rel) => {
2741
+ const walk2 = async (dir, rel) => {
2658
2742
  let names;
2659
2743
  try {
2660
2744
  names = await fsp2.readdir(dir, { withFileTypes: true });
@@ -2665,17 +2749,17 @@ 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
- if (dirent.isDirectory()) await walk(file, inner);
2755
+ if (dirent.isDirectory()) await walk2(file, inner);
2672
2756
  else if (dirent.isFile()) {
2673
2757
  const stat = await fsp2.stat(file).catch(() => null);
2674
2758
  if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
2675
2759
  }
2676
2760
  }
2677
2761
  };
2678
- await walk(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,164 @@ 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] };
2892
+ }
2893
+
2894
+ import fs12 from "node:fs";
2895
+ import path14 from "node:path";
2896
+ var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
2897
+ var MAX_FILES = 2e3;
2898
+ var MAX_FILE_BYTES = 64 * 1024 * 1024;
2899
+ var DESIGN_SYSTEM_FILE2 = "design-system.json";
2900
+ function readRecords(project, indexFile) {
2901
+ let index;
2902
+ try {
2903
+ index = JSON.parse(fs12.readFileSync(path14.join(project, indexFile), "utf8"));
2904
+ } catch {
2905
+ return [];
2906
+ }
2907
+ const list = index?.designSystems;
2908
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
2909
+ const out = [];
2910
+ for (const raw of records) {
2911
+ const r = raw;
2912
+ if (typeof r?.namespace !== "string" || !FOLDER.test(r.namespace)) continue;
2913
+ out.push({
2914
+ namespace: r.namespace,
2915
+ title: typeof r.title === "string" ? r.title : r.namespace,
2916
+ artifact: typeof r.artifact === "string" ? r.artifact : ""
2917
+ });
2918
+ }
2919
+ return out;
2920
+ }
2921
+ function plain(p, want) {
2922
+ const stat = fs12.lstatSync(p, { throwIfNoEntry: false });
2923
+ return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
2924
+ }
2925
+ function walk(dir, prefix = "", out = []) {
2926
+ for (const entry of fs12.readdirSync(path14.join(dir, prefix), { withFileTypes: true })) {
2927
+ if (out.length >= MAX_FILES) break;
2928
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2929
+ if (entry.isDirectory()) walk(dir, rel, out);
2930
+ else if (entry.isFile()) out.push(rel);
2931
+ }
2932
+ return out;
2933
+ }
2934
+ var projectOf = (candidate) => path14.basename(candidate) === "project" ? candidate : path14.join(candidate, "project");
2935
+ function sourceProject(artifact, root, from) {
2936
+ const given = from ?? artifact;
2937
+ if (!given) return { reason: "the record names no source folder", kind: "none" };
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" };
2939
+ const candidates = path14.isAbsolute(given) ? [given] : [path14.resolve(given), path14.resolve(root, given), path14.resolve(root, "..", given)];
2940
+ for (const candidate of candidates) {
2941
+ const project = projectOf(candidate);
2942
+ if (plain(project, "dir")) return { project };
2943
+ }
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)) {
2947
+ dir = up;
2948
+ const project = projectOf(path14.resolve(dir, given));
2949
+ if (plain(path14.join(project, DESIGN_SYSTEM_FILE2), "file")) return { project };
2950
+ }
2951
+ }
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" };
2953
+ }
2954
+ function compareSystem(project, root, record, from) {
2955
+ const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
2956
+ const found = sourceProject(record.artifact, root, from);
2957
+ if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
2958
+ diff.source = path14.dirname(found.project);
2959
+ const copy = path14.join(project, "ds", record.namespace);
2960
+ if (!plain(copy, "dir")) return { ...diff, missing: true };
2961
+ for (const rel of walk(copy)) {
2962
+ const mine = path14.join(copy, rel);
2963
+ const theirs = path14.join(found.project, rel);
2964
+ if (!plain(mine, "file")) continue;
2965
+ if (!plain(theirs, "file")) {
2966
+ diff.removed.push(rel);
2967
+ continue;
2968
+ }
2969
+ const a = fs12.statSync(mine);
2970
+ const b = fs12.statSync(theirs);
2971
+ if (a.size > MAX_FILE_BYTES || b.size > MAX_FILE_BYTES) continue;
2972
+ if (a.size !== b.size || !fs12.readFileSync(mine).equals(fs12.readFileSync(theirs))) diff.changed.push(rel);
2973
+ }
2974
+ return diff;
2975
+ }
2976
+ function pick(records, namespace) {
2977
+ if (namespace === void 0) return records;
2978
+ const one = records.filter((r) => r.namespace === namespace);
2979
+ if (one.length === 0) {
2980
+ const names = records.map((r) => r.namespace);
2981
+ throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
2982
+ }
2983
+ return one;
2984
+ }
2985
+ function compareSystems(project, root, indexFile, namespace, from) {
2986
+ return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
2987
+ }
2988
+ function staleNotes(project, root, indexFile, command) {
2989
+ const notes = [];
2990
+ for (const diff of compareSystems(project, root, indexFile)) {
2991
+ if (diff.changed.length === 0 && diff.removed.length === 0) continue;
2992
+ const parts = [];
2993
+ if (diff.changed.length > 0) parts.push(`${diff.changed.length} changed (${diff.changed.slice(0, 3).join(", ")}${diff.changed.length > 3 ? ", \u2026" : ""})`);
2994
+ if (diff.removed.length > 0) parts.push(`${diff.removed.length} gone from the source`);
2995
+ notes.push(
2996
+ `project/ds/${diff.namespace}/ is behind its source system ${diff.source}: ${parts.join(", ")}. A copy never changes by itself: run \`${command(diff.namespace, false)}\` to copy the changed files again (\`${command(diff.namespace, true)}\` lists them).`
2997
+ );
2998
+ }
2999
+ return notes;
3000
+ }
3001
+ function stamp(project, indexFile, namespaces, now) {
3002
+ const file = path14.join(project, indexFile);
3003
+ const text = fs12.readFileSync(file, "utf8");
3004
+ const index = JSON.parse(text);
3005
+ const list = index.designSystems;
3006
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
3007
+ for (const r of records) {
3008
+ const record = r;
3009
+ if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
3010
+ }
3011
+ const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
3012
+ const out = JSON.stringify(index, null, indent);
3013
+ const tmp = `${file}.${process.pid}.tmp`;
3014
+ fs12.writeFileSync(tmp, text.endsWith("\n") ? `${out}
3015
+ ` : out);
3016
+ fs12.renameSync(tmp, file);
3017
+ }
3018
+ function syncSystems(project, root, indexFile, opts) {
3019
+ const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
3020
+ const results = [];
3021
+ const synced = /* @__PURE__ */ new Set();
3022
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
3023
+ for (const diff of diffs) {
3024
+ const result = { ...diff, copied: [] };
3025
+ if (!opts.check && diff.source && !diff.missing) {
3026
+ for (const rel of diff.changed) {
3027
+ const target = path14.join(project, "ds", diff.namespace, rel);
3028
+ const tmp = `${target}.${process.pid}.tmp`;
3029
+ fs12.copyFileSync(path14.join(diff.source, "project", rel), tmp);
3030
+ fs12.renameSync(tmp, target);
3031
+ result.copied.push(rel);
3032
+ }
3033
+ if (diff.changed.length > 0) synced.add(diff.namespace);
3034
+ }
3035
+ results.push(result);
3036
+ }
3037
+ if (synced.size > 0) {
3038
+ stamp(project, indexFile, synced, now);
3039
+ for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
3040
+ }
3041
+ return results;
2808
3042
  }
2809
3043
 
2810
3044
  async function shareCall(url, action, fail2) {
2811
- 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) } : {});
2812
3046
  const body = await response.json().catch(() => ({}));
2813
3047
  if (!response.ok) fail2("SHARE_REFUSED", String(body.message ?? `the server answered ${response.status}`));
2814
3048
  return body;
@@ -2837,6 +3071,7 @@ async function shareCommand(o) {
2837
3071
  ...rolesOf(o.flags, "disable", o.fail).map((role) => ({ action: "link", role, on: false })),
2838
3072
  ...guestRoles(o.flags, o.fail)
2839
3073
  ];
3074
+ const open2 = !off && changes.length === 0;
2840
3075
  let server = await o.running();
2841
3076
  if (!server) {
2842
3077
  const sharing = new Sharing(o.root);
@@ -2850,12 +3085,19 @@ async function shareCommand(o) {
2850
3085
  sharing.setLan(false);
2851
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)." };
2852
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
+ }
2853
3095
  server = await o.startShared();
2854
3096
  if (!server) o.fail("SERVER_FAILED", `The server did not start. Run ${o.serveCommand} in the foreground to see why.`);
2855
3097
  }
2856
3098
  let view = await shareCall(server.url, null, o.fail);
2857
3099
  for (const change of changes) view = await shareCall(server.url, change, o.fail);
2858
- 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);
2859
3101
  const links = view.links;
2860
3102
  const shown = (list) => Object.fromEntries(Object.entries(list).map(([role, link]) => [role, link.on ? link.url : "off"]));
2861
3103
  const tailnet = view.lan ? view.tailnet : void 0;
@@ -2872,10 +3114,11 @@ async function shareCommand(o) {
2872
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.",
2873
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.",
2874
3116
  stop: o.offCommand
2875
- } : { 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}` }
2876
3118
  };
2877
3119
  }
2878
- 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 ?? "/")}`);
2879
3122
  async function lanOn(url, flags, fail2) {
2880
3123
  if (!flags.has("lan")) return {};
2881
3124
  return { lan: lanUrls(await shareCall(url, { action: "lan", on: true }, fail2)) };
@@ -2888,8 +3131,8 @@ process.emitWarning = ((warning, ...rest) => {
2888
3131
  });
2889
3132
  var KIND = "design";
2890
3133
  var SCRIPT = fileURLToPath2(import.meta.url);
2891
- var EDITOR_DIR = path13.join(path13.dirname(SCRIPT), "editor");
2892
- var SERVER_SCRIPT = path13.join(path13.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");
2893
3136
  var MAX_LISTED = 200;
2894
3137
  var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
2895
3138
  var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
@@ -2902,7 +3145,7 @@ var CliError = class extends Error {
2902
3145
  this.code = code;
2903
3146
  }
2904
3147
  };
2905
- var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off"]);
3148
+ var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off", "check"]);
2906
3149
  function parseArgs(argv) {
2907
3150
  const positional = [];
2908
3151
  const flags = /* @__PURE__ */ new Map();
@@ -2928,13 +3171,13 @@ function quote(s) {
2928
3171
  }
2929
3172
  function resolveArtifact(given, mustExist = true) {
2930
3173
  if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
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);
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);
2934
3177
  }
2935
- const project = path13.join(root, "project");
3178
+ const project = path15.join(root, "project");
2936
3179
  if (mustExist) {
2937
- const stat = fs11.statSync(project, { throwIfNoEntry: false });
3180
+ const stat = fs13.statSync(project, { throwIfNoEntry: false });
2938
3181
  if (!stat?.isDirectory()) {
2939
3182
  throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
2940
3183
  }
@@ -2956,7 +3199,7 @@ function localize(value) {
2956
3199
  function designSystemNotes(project, indexFile) {
2957
3200
  let index;
2958
3201
  try {
2959
- index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
3202
+ index = JSON.parse(fs13.readFileSync(path15.join(project, indexFile), "utf8"));
2960
3203
  } catch {
2961
3204
  return [];
2962
3205
  }
@@ -2970,14 +3213,15 @@ function designSystemNotes(project, indexFile) {
2970
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>/.`);
2971
3214
  continue;
2972
3215
  }
2973
- const folder = path13.join(project, "ds", ns);
2974
- if (!fs11.existsSync(folder)) {
3216
+ const folder = path15.join(project, "ds", ns);
3217
+ if (!fs13.existsSync(folder)) {
2975
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.`);
2976
- } else if (!fs11.existsSync(path13.join(folder, "tokens.json"))) {
3219
+ } else if (!fs13.existsSync(path15.join(folder, "tokens.json"))) {
2977
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).`);
2978
3221
  }
2979
3222
  }
2980
- return notes;
3223
+ const root = path15.dirname(project);
3224
+ return [...notes, ...staleNotes(project, root, indexFile, (ns, listOnly) => script("ds-sync", root, ns, ...listOnly ? ["--check"] : []))];
2981
3225
  }
2982
3226
  async function check(a) {
2983
3227
  if (KIND === "slides") {
@@ -2985,9 +3229,9 @@ async function check(a) {
2985
3229
  try {
2986
3230
  rt = loadSlidesCompiler(EDITOR_DIR);
2987
3231
  } catch {
2988
- throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path13.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.`);
2989
3233
  }
2990
- if (!fs11.existsSync(path13.join(a.project, "deck.json"))) {
3234
+ if (!fs13.existsSync(path15.join(a.project, "deck.json"))) {
2991
3235
  return {
2992
3236
  problems: 1,
2993
3237
  result: {
@@ -3026,7 +3270,7 @@ async function check(a) {
3026
3270
  ...slides.length > 0 ? { slides } : {},
3027
3271
  ...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
3028
3272
  notChecked: "Text sizes, contrast, whether a section id matches its file name and whether an image path exists are not checked (nothing is rendered).",
3029
- next: problems2 > 0 ? `Fix the files named in deckProblems and in each slide's problems (paths are under project/), then run check again until problems is 0. notes are hints and lose no content; fix the layout ones (starting "\u6392\u7248") and the warnings (starting "\u8B66\u544A": a font or image falls back) too.` : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (layoutNotes ? ' Some slides carry layout notes ("\u6392\u7248": content may overflow or overlap) or warnings ("\u8B66\u544A": a font or image falls back); fix them and check again.' : "") + (report.orphans.length > 0 ? " orphans are slide files missing from order: they do not show; delete the unneeded ones." : "") + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete." : "")
3273
+ next: problems2 > 0 ? `Fix the files named in deckProblems and in each slide's problems (paths are under project/), then run check again until problems is 0. notes are hints and lose no content; fix the layout ones (starting "\u6392\u7248") and the warnings (starting "\u8B66\u544A": a font or image falls back) too.` : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (layoutNotes ? ' Some slides carry layout notes ("\u6392\u7248": content may overflow or overlap) or warnings ("\u8B66\u544A": a font or image falls back); fix them and check again.' : "") + (report.orphans.length > 0 ? " orphans are slide files missing from order: they do not show; delete the unneeded ones." : "") + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete or behind its source." : "")
3030
3274
  }
3031
3275
  };
3032
3276
  }
@@ -3045,17 +3289,17 @@ async function check(a) {
3045
3289
  ...problems2 > MAX_LISTED ? { more: `${problems2 - MAX_LISTED} more not listed: fix these, then check again` } : {},
3046
3290
  ...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
3047
3291
  notChecked: "Whether the artboard scripts run, and how the layout looks, is not checked (nothing is rendered).",
3048
- next: problems2 > 0 ? 'Fix the files per issues (file is a path under project/; "." is the whole folder), then run check again until problems is 0.' : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete." : "")
3292
+ next: problems2 > 0 ? 'Fix the files per issues (file is a path under project/; "." is the whole folder), then run check again until problems is 0.' : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete or behind its source." : "")
3049
3293
  }
3050
3294
  };
3051
3295
  }
3052
3296
  const r = await checkDesignSystem(a.project);
3053
3297
  const problems = r.issues.length;
3054
- if (fs11.existsSync(path13.join(a.project, DESIGN_SYSTEM_FILE))) {
3055
- if (!fs11.existsSync(path13.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"))) {
3056
3300
  r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
3057
3301
  }
3058
- if (!fs11.existsSync(path13.join(a.project, "components", "Cover", "preview.html"))) {
3302
+ if (!fs13.existsSync(path15.join(a.project, "components", "Cover", "preview.html"))) {
3059
3303
  r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
3060
3304
  }
3061
3305
  }
@@ -3081,11 +3325,11 @@ function formatBytes(n) {
3081
3325
  }
3082
3326
  async function upload(a, given, group) {
3083
3327
  if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
3084
- const file = path13.resolve(given);
3328
+ const file = path15.resolve(given);
3085
3329
  const stat = await fsp3.stat(file).catch(() => null);
3086
3330
  if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
3087
3331
  if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
3088
- const ext = path13.extname(file).slice(1).toLowerCase();
3332
+ const ext = path15.extname(file).slice(1).toLowerCase();
3089
3333
  const type = contentTypeOf(file);
3090
3334
  const isMedia = type.startsWith("image/") || type.startsWith("font/");
3091
3335
  let limit = UPLOAD_MAX_BYTES;
@@ -3110,7 +3354,7 @@ async function upload(a, given, group) {
3110
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.`);
3111
3355
  const bytes = await fsp3.readFile(file);
3112
3356
  const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
3113
- const base = path13.basename(file);
3357
+ const base = path15.basename(file);
3114
3358
  if (KIND !== "design-system") {
3115
3359
  return {
3116
3360
  url: uploaded.url,
@@ -3142,12 +3386,12 @@ function folderName(name) {
3142
3386
  }
3143
3387
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
3144
3388
  async function listSystems(dirs) {
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));
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));
3147
3391
  const found = /* @__PURE__ */ new Map();
3148
3392
  const visit = async (dir, depth) => {
3149
- const project = path13.join(dir, "project");
3150
- if (fs11.existsSync(path13.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)) {
3151
3395
  const info = await readDesignSystemInfo(project).catch(() => null);
3152
3396
  if (info) {
3153
3397
  const namespace = info.namespace;
@@ -3156,8 +3400,8 @@ async function listSystems(dirs) {
3156
3400
  namespace: namespace ?? null,
3157
3401
  folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
3158
3402
  root: dir,
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
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
3161
3405
  });
3162
3406
  return;
3163
3407
  }
@@ -3166,7 +3410,7 @@ async function listSystems(dirs) {
3166
3410
  const entries = await fsp3.readdir(dir, { withFileTypes: true }).catch(() => []);
3167
3411
  for (const entry of entries) {
3168
3412
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
3169
- await visit(path13.join(dir, entry.name), depth - 1);
3413
+ await visit(path15.join(dir, entry.name), depth - 1);
3170
3414
  }
3171
3415
  };
3172
3416
  for (const start of starts) await visit(start, 4);
@@ -3190,10 +3434,10 @@ function reusedNote(a, flags) {
3190
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.` } : {};
3191
3435
  }
3192
3436
  function assertServerFiles() {
3193
- if (!fs11.existsSync(path13.join(EDITOR_DIR, "index.html"))) {
3437
+ if (!fs13.existsSync(path15.join(EDITOR_DIR, "index.html"))) {
3194
3438
  throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
3195
3439
  }
3196
- if (!fs11.existsSync(SERVER_SCRIPT)) {
3440
+ if (!fs13.existsSync(SERVER_SCRIPT)) {
3197
3441
  throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
3198
3442
  }
3199
3443
  }
@@ -3221,49 +3465,48 @@ async function open(a, flags) {
3221
3465
  assertServerFiles();
3222
3466
  const existing = await runningServer2(a.root);
3223
3467
  if (existing) {
3224
- print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), log: path13.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) });
3225
3469
  return;
3226
3470
  }
3227
3471
  const state = await startBackground(a, flags);
3228
- const view = await shareCall2(state.url, null).catch(() => null);
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) });
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) });
3230
3473
  }
3231
3474
  async function startBackground(a, flags) {
3232
- 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];
3233
3477
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
3234
- const logFile = path13.join(a.root, LOG_FILE);
3235
- const fd = fs11.openSync(logFile, "a");
3236
- const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
3237
- child.unref();
3238
- fs11.closeSync(fd);
3239
- let exited = false;
3240
- child.once("exit", () => exited = true);
3241
- const deadline = Date.now() + 15e3;
3242
- while (Date.now() < deadline) {
3243
- await new Promise((r) => setTimeout(r, 150));
3244
- const state = readState(a.root);
3245
- if (state && state.pid === child.pid) return state;
3246
- 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) } : {} };
3247
3494
  }
3248
- const tail = fs11.existsSync(logFile) ? fs11.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3249
- throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
3250
- ${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}` : ""}`);
3251
3497
  }
3252
3498
  async function stop(a) {
3253
3499
  const state = readState(a.root);
3254
3500
  if (!state || !alive(state.pid)) {
3255
- fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3501
+ fs13.rmSync(path15.join(a.root, STATE_FILE), { force: true });
3256
3502
  print({ stopped: false, message: "No editor server is running for this folder." });
3257
3503
  return;
3258
3504
  }
3259
- process.kill(state.pid, "SIGTERM");
3260
- const deadline = Date.now() + 5e3;
3261
- while (alive(state.pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
3262
- if (alive(state.pid)) {
3505
+ if (!await leaveHub(state)) {
3263
3506
  print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
3264
3507
  return;
3265
3508
  }
3266
- fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3509
+ fs13.rmSync(path15.join(a.root, STATE_FILE), { force: true });
3267
3510
  print({ stopped: true, pid: state.pid });
3268
3511
  }
3269
3512
  async function share(a, flags) {
@@ -3307,7 +3550,7 @@ async function listComments(a, all) {
3307
3550
  async function artifactTitle(a) {
3308
3551
  const info = await readInfo(a.project).catch(() => null);
3309
3552
  const title = typeof info?.title === "string" ? info.title.trim() : "";
3310
- return title && title !== "project" ? title : path13.basename(a.root);
3553
+ return title && title !== "project" ? title : path15.basename(a.root);
3311
3554
  }
3312
3555
  async function waitComments(a, flags) {
3313
3556
  const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
@@ -3399,11 +3642,15 @@ Usage:
3399
3642
  node scripts/artifact.mjs share <root> share on the local network; prints a link per role
3400
3643
  [--reset R] [--enable R] [--disable R] R: editor, commenter, viewer (comma list) or all
3401
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)
3402
3646
  [--off] stop sharing (guests keep their place for next time)
3403
3647
  node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
3404
3648
  node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
3405
3649
  node scripts/artifact.mjs list [dir \u2026] find design systems (folders holding project/design-system.json)
3406
- node scripts/artifact.mjs comments <root> [--all] the open comments people left in the editor (--all: resolved too)
3650
+ ${KIND === "design-system" ? "" : ` node scripts/artifact.mjs ds-sync <root> [namespace] [--check] an installed design system is a copy: copy its changed files again
3651
+ from the system's folder and stamp copiedAt (--check: list only;
3652
+ --from <folder>: the source, when the record's path is not found)
3653
+ `} node scripts/artifact.mjs comments <root> [--all] the open comments people left in the editor (--all: resolved too)
3407
3654
  node scripts/artifact.mjs comments <root> --wait [--timeout s] wait for a comment sent to the agent from the open editor
3408
3655
  node scripts/artifact.mjs comment-reply <root> <id> <text|-> reply as the agent (-: text on stdin) [--resolve]
3409
3656
  node scripts/artifact.mjs comment-resolve <root> <id> [--reopen] mark a comment resolved (or open again)
@@ -3434,16 +3681,43 @@ async function main() {
3434
3681
  }
3435
3682
  case "upload": {
3436
3683
  const a = resolveArtifact(positional[0], false);
3437
- if (!fs11.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3684
+ if (!fs13.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3438
3685
  throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
3439
3686
  }
3440
- fs11.mkdirSync(a.project, { recursive: true });
3687
+ fs13.mkdirSync(a.project, { recursive: true });
3441
3688
  print(await upload(a, positional[1], flags.get("group")));
3442
3689
  return 0;
3443
3690
  }
3444
3691
  case "list":
3445
3692
  print(await listSystems(positional));
3446
3693
  return 0;
3694
+ case "ds-sync": {
3695
+ if (KIND === "design-system") throw new CliError("NOT_APPLICABLE", "A design system has no installed design systems: ds-sync is for decks and canvases.");
3696
+ const a = resolveArtifact(positional[0]);
3697
+ const check2 = flags.has("check");
3698
+ let results;
3699
+ try {
3700
+ results = syncSystems(a.project, a.root, INDEX_FILE, { namespace: positional[1], from: flags.get("from"), check: check2 });
3701
+ } catch (error) {
3702
+ throw new CliError("NO_SUCH_SYSTEM", error.message);
3703
+ }
3704
+ const behind = results.filter((r) => r.changed.length > 0 || r.removed.length > 0);
3705
+ print({
3706
+ root: a.root,
3707
+ systems: results.map((r) => ({
3708
+ namespace: r.namespace,
3709
+ title: r.title,
3710
+ ...r.skipped ? { skipped: r.skipped } : {},
3711
+ ...r.missing ? { skipped: `project/ds/${r.namespace}/ does not exist: install it first (SKILL.md, Creating, step 4)` } : {},
3712
+ ...r.source ? { source: r.source } : {},
3713
+ changed: r.changed,
3714
+ ...r.removed.length > 0 ? { goneFromSource: r.removed } : {},
3715
+ ...check2 ? {} : { copied: r.copied, ...r.copiedAt ? { copiedAt: r.copiedAt } : {} }
3716
+ })),
3717
+ next: check2 ? behind.length > 0 ? `Behind its source. Run ${script("ds-sync", a.root)} to copy the changed files again.` : "Every copy matches its source (or its source was not found: see skipped)." : results.some((r) => r.copied.length > 0) ? "Copied. The open editor shows the new files at once (a file over 2 MB, or a types file, after a reload). Files the source dropped stay in the copy: remove one, and change the artboards that load it, if the source no longer has it. Files the source gained are not copied: compare its README with project/ds/<folder>/." : "Nothing to copy."
3718
+ });
3719
+ return check2 && behind.some((r) => r.changed.length > 0) ? 1 : 0;
3720
+ }
3447
3721
  case "comments": {
3448
3722
  const a = resolveArtifact(positional[0]);
3449
3723
  print(flags.has("wait") ? await waitComments(a, flags) : await listComments(a, flags.has("all")));