dsh-claude-design-system 2.7.1 → 2.7.2

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.
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-claude-design-system 2.7.1: built from baoyu-design.
1
+ // dsh-claude-design-system 2.7.2: built from baoyu-design.
2
2
  window.__ModuleLoader__.load({
3
3
  id: "dsh-claude-design-system",
4
4
  factory: (require) => {
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-claude-design-system 2.7.1: built from baoyu-design.
1
+ // dsh-claude-design-system 2.7.2: built from baoyu-design.
2
2
  var define_PLUGIN_default = { name: "claude-design-system", provider: "dsh-claude-design-system", skills: [{ name: "claude-design-system", description: "Create and revise design systems in the Design System artifact format (project/design-system.json, tokens.json, a README brand book, component previews, fonts and asset groups) as local files, then open them in a local design-system page where the user can browse and edit tokens, type, components and assets. Use when the user asks for a design system or brand kit in this format, built from a code repository, a design tool or scratch, names claude-design-system or the Design System type, or wants to revise a folder that holds project/design-system.json. claude-slides and claude-design install these systems. A free-form HTML style guide, UI kit or token page is baoyu-design's.", comments: true }] };
3
3
 
4
4
  import { spawn as spawn2 } from "node:child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-design-system",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "DeepSeek Harness plugin: the claude-design-system skill from baoyu-design. The agent writes design systems (tokens, type, components, assets) as files and opens them in a local design-system page.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -2,9 +2,9 @@
2
2
  // claude-design-system: local Artifact tool (prebuilt; do not edit).
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import fs11 from "node:fs";
5
+ import fs12 from "node:fs";
6
6
  import fsp3 from "node:fs/promises";
7
- import path13 from "node:path";
7
+ import path14 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}$/;
@@ -2654,7 +2654,7 @@ async function archiveEntries(root, skip = []) {
2654
2654
  const folder = path12.basename(path12.resolve(root)) || "artifact";
2655
2655
  const skipped = new Set(skip.map((file) => path12.resolve(file)));
2656
2656
  const entries = [];
2657
- const walk = async (dir, rel) => {
2657
+ const walk2 = async (dir, rel) => {
2658
2658
  let names;
2659
2659
  try {
2660
2660
  names = await fsp2.readdir(dir, { withFileTypes: true });
@@ -2668,14 +2668,14 @@ async function archiveEntries(root, skip = []) {
2668
2668
  const file = path12.join(dir, name);
2669
2669
  if (skipped.has(file)) continue;
2670
2670
  const inner = rel ? `${rel}/${name}` : name;
2671
- if (dirent.isDirectory()) await walk(file, inner);
2671
+ if (dirent.isDirectory()) await walk2(file, inner);
2672
2672
  else if (dirent.isFile()) {
2673
2673
  const stat = await fsp2.stat(file).catch(() => null);
2674
2674
  if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
2675
2675
  }
2676
2676
  }
2677
2677
  };
2678
- await walk(path12.resolve(root), "");
2678
+ await walk2(path12.resolve(root), "");
2679
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
2680
  const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2681
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.`);
@@ -2807,6 +2807,156 @@ async function archiveCommand(root, out) {
2807
2807
  return { file, folder: path12.basename(path12.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2808
2808
  }
2809
2809
 
2810
+ import fs11 from "node:fs";
2811
+ import path13 from "node:path";
2812
+ var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
2813
+ var MAX_FILES = 2e3;
2814
+ var MAX_FILE_BYTES = 64 * 1024 * 1024;
2815
+ var DESIGN_SYSTEM_FILE2 = "design-system.json";
2816
+ function readRecords(project, indexFile) {
2817
+ let index;
2818
+ try {
2819
+ index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
2820
+ } catch {
2821
+ return [];
2822
+ }
2823
+ const list = index?.designSystems;
2824
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
2825
+ const out = [];
2826
+ for (const raw of records) {
2827
+ const r = raw;
2828
+ if (typeof r?.namespace !== "string" || !FOLDER.test(r.namespace)) continue;
2829
+ out.push({
2830
+ namespace: r.namespace,
2831
+ title: typeof r.title === "string" ? r.title : r.namespace,
2832
+ artifact: typeof r.artifact === "string" ? r.artifact : ""
2833
+ });
2834
+ }
2835
+ return out;
2836
+ }
2837
+ function plain(p, want) {
2838
+ const stat = fs11.lstatSync(p, { throwIfNoEntry: false });
2839
+ return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
2840
+ }
2841
+ function walk(dir, prefix = "", out = []) {
2842
+ for (const entry of fs11.readdirSync(path13.join(dir, prefix), { withFileTypes: true })) {
2843
+ if (out.length >= MAX_FILES) break;
2844
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2845
+ if (entry.isDirectory()) walk(dir, rel, out);
2846
+ else if (entry.isFile()) out.push(rel);
2847
+ }
2848
+ return out;
2849
+ }
2850
+ var projectOf = (candidate) => path13.basename(candidate) === "project" ? candidate : path13.join(candidate, "project");
2851
+ function sourceProject(artifact, root, from) {
2852
+ const given = from ?? artifact;
2853
+ if (!given) return { reason: "the record names no source folder", kind: "none" };
2854
+ 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)];
2856
+ for (const candidate of candidates) {
2857
+ const project = projectOf(candidate);
2858
+ if (plain(project, "dir")) return { project };
2859
+ }
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)) {
2863
+ dir = up;
2864
+ const project = projectOf(path13.resolve(dir, given));
2865
+ if (plain(path13.join(project, DESIGN_SYSTEM_FILE2), "file")) return { project };
2866
+ }
2867
+ }
2868
+ 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" };
2869
+ }
2870
+ function compareSystem(project, root, record, from) {
2871
+ const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
2872
+ const found = sourceProject(record.artifact, root, from);
2873
+ 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);
2876
+ if (!plain(copy, "dir")) return { ...diff, missing: true };
2877
+ for (const rel of walk(copy)) {
2878
+ const mine = path13.join(copy, rel);
2879
+ const theirs = path13.join(found.project, rel);
2880
+ if (!plain(mine, "file")) continue;
2881
+ if (!plain(theirs, "file")) {
2882
+ diff.removed.push(rel);
2883
+ continue;
2884
+ }
2885
+ const a = fs11.statSync(mine);
2886
+ const b = fs11.statSync(theirs);
2887
+ 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);
2889
+ }
2890
+ return diff;
2891
+ }
2892
+ function pick(records, namespace) {
2893
+ if (namespace === void 0) return records;
2894
+ const one = records.filter((r) => r.namespace === namespace);
2895
+ if (one.length === 0) {
2896
+ const names = records.map((r) => r.namespace);
2897
+ throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
2898
+ }
2899
+ return one;
2900
+ }
2901
+ function compareSystems(project, root, indexFile, namespace, from) {
2902
+ return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
2903
+ }
2904
+ function staleNotes(project, root, indexFile, command) {
2905
+ const notes = [];
2906
+ for (const diff of compareSystems(project, root, indexFile)) {
2907
+ if (diff.changed.length === 0 && diff.removed.length === 0) continue;
2908
+ const parts = [];
2909
+ if (diff.changed.length > 0) parts.push(`${diff.changed.length} changed (${diff.changed.slice(0, 3).join(", ")}${diff.changed.length > 3 ? ", \u2026" : ""})`);
2910
+ if (diff.removed.length > 0) parts.push(`${diff.removed.length} gone from the source`);
2911
+ notes.push(
2912
+ `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).`
2913
+ );
2914
+ }
2915
+ return notes;
2916
+ }
2917
+ function stamp(project, indexFile, namespaces, now) {
2918
+ const file = path13.join(project, indexFile);
2919
+ const text = fs11.readFileSync(file, "utf8");
2920
+ const index = JSON.parse(text);
2921
+ const list = index.designSystems;
2922
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
2923
+ for (const r of records) {
2924
+ const record = r;
2925
+ if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
2926
+ }
2927
+ const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
2928
+ const out = JSON.stringify(index, null, indent);
2929
+ const tmp = `${file}.${process.pid}.tmp`;
2930
+ fs11.writeFileSync(tmp, text.endsWith("\n") ? `${out}
2931
+ ` : out);
2932
+ fs11.renameSync(tmp, file);
2933
+ }
2934
+ function syncSystems(project, root, indexFile, opts) {
2935
+ const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
2936
+ const results = [];
2937
+ const synced = /* @__PURE__ */ new Set();
2938
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2939
+ for (const diff of diffs) {
2940
+ const result = { ...diff, copied: [] };
2941
+ if (!opts.check && diff.source && !diff.missing) {
2942
+ for (const rel of diff.changed) {
2943
+ const target = path13.join(project, "ds", diff.namespace, rel);
2944
+ const tmp = `${target}.${process.pid}.tmp`;
2945
+ fs11.copyFileSync(path13.join(diff.source, "project", rel), tmp);
2946
+ fs11.renameSync(tmp, target);
2947
+ result.copied.push(rel);
2948
+ }
2949
+ if (diff.changed.length > 0) synced.add(diff.namespace);
2950
+ }
2951
+ results.push(result);
2952
+ }
2953
+ if (synced.size > 0) {
2954
+ stamp(project, indexFile, synced, now);
2955
+ for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
2956
+ }
2957
+ return results;
2958
+ }
2959
+
2810
2960
  async function shareCall(url, action, fail2) {
2811
2961
  const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
2812
2962
  const body = await response.json().catch(() => ({}));
@@ -2888,8 +3038,8 @@ process.emitWarning = ((warning, ...rest) => {
2888
3038
  });
2889
3039
  var KIND = "design-system";
2890
3040
  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");
3041
+ var EDITOR_DIR = path14.join(path14.dirname(SCRIPT), "editor");
3042
+ var SERVER_SCRIPT = path14.join(path14.dirname(SCRIPT), "server.mjs");
2893
3043
  var MAX_LISTED = 200;
2894
3044
  var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
2895
3045
  var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
@@ -2902,7 +3052,7 @@ var CliError = class extends Error {
2902
3052
  this.code = code;
2903
3053
  }
2904
3054
  };
2905
- var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off"]);
3055
+ var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off", "check"]);
2906
3056
  function parseArgs(argv) {
2907
3057
  const positional = [];
2908
3058
  const flags = /* @__PURE__ */ new Map();
@@ -2928,13 +3078,13 @@ function quote(s) {
2928
3078
  }
2929
3079
  function resolveArtifact(given, mustExist = true) {
2930
3080
  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);
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);
2934
3084
  }
2935
- const project = path13.join(root, "project");
3085
+ const project = path14.join(root, "project");
2936
3086
  if (mustExist) {
2937
- const stat = fs11.statSync(project, { throwIfNoEntry: false });
3087
+ const stat = fs12.statSync(project, { throwIfNoEntry: false });
2938
3088
  if (!stat?.isDirectory()) {
2939
3089
  throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
2940
3090
  }
@@ -2956,7 +3106,7 @@ function localize(value) {
2956
3106
  function designSystemNotes(project, indexFile) {
2957
3107
  let index;
2958
3108
  try {
2959
- index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
3109
+ index = JSON.parse(fs12.readFileSync(path14.join(project, indexFile), "utf8"));
2960
3110
  } catch {
2961
3111
  return [];
2962
3112
  }
@@ -2970,14 +3120,15 @@ function designSystemNotes(project, indexFile) {
2970
3120
  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
3121
  continue;
2972
3122
  }
2973
- const folder = path13.join(project, "ds", ns);
2974
- if (!fs11.existsSync(folder)) {
3123
+ const folder = path14.join(project, "ds", ns);
3124
+ if (!fs12.existsSync(folder)) {
2975
3125
  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"))) {
3126
+ } else if (!fs12.existsSync(path14.join(folder, "tokens.json"))) {
2977
3127
  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
3128
  }
2979
3129
  }
2980
- return notes;
3130
+ const root = path14.dirname(project);
3131
+ return [...notes, ...staleNotes(project, root, indexFile, (ns, listOnly) => script("ds-sync", root, ns, ...listOnly ? ["--check"] : []))];
2981
3132
  }
2982
3133
  async function check(a) {
2983
3134
  if (KIND === "slides") {
@@ -2985,9 +3136,9 @@ async function check(a) {
2985
3136
  try {
2986
3137
  rt = loadSlidesCompiler(EDITOR_DIR);
2987
3138
  } catch {
2988
- throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path13.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
3139
+ throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path14.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
2989
3140
  }
2990
- if (!fs11.existsSync(path13.join(a.project, "deck.json"))) {
3141
+ if (!fs12.existsSync(path14.join(a.project, "deck.json"))) {
2991
3142
  return {
2992
3143
  problems: 1,
2993
3144
  result: {
@@ -3026,7 +3177,7 @@ async function check(a) {
3026
3177
  ...slides.length > 0 ? { slides } : {},
3027
3178
  ...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
3028
3179
  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." : "")
3180
+ 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
3181
  }
3031
3182
  };
3032
3183
  }
@@ -3045,17 +3196,17 @@ async function check(a) {
3045
3196
  ...problems2 > MAX_LISTED ? { more: `${problems2 - MAX_LISTED} more not listed: fix these, then check again` } : {},
3046
3197
  ...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
3047
3198
  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." : "")
3199
+ 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
3200
  }
3050
3201
  };
3051
3202
  }
3052
3203
  const r = await checkDesignSystem(a.project);
3053
3204
  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"))) {
3205
+ if (fs12.existsSync(path14.join(a.project, DESIGN_SYSTEM_FILE))) {
3206
+ if (!fs12.existsSync(path14.join(a.project, "README.md"))) {
3056
3207
  r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
3057
3208
  }
3058
- if (!fs11.existsSync(path13.join(a.project, "components", "Cover", "preview.html"))) {
3209
+ if (!fs12.existsSync(path14.join(a.project, "components", "Cover", "preview.html"))) {
3059
3210
  r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
3060
3211
  }
3061
3212
  }
@@ -3081,11 +3232,11 @@ function formatBytes(n) {
3081
3232
  }
3082
3233
  async function upload(a, given, group) {
3083
3234
  if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
3084
- const file = path13.resolve(given);
3235
+ const file = path14.resolve(given);
3085
3236
  const stat = await fsp3.stat(file).catch(() => null);
3086
3237
  if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
3087
3238
  if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
3088
- const ext = path13.extname(file).slice(1).toLowerCase();
3239
+ const ext = path14.extname(file).slice(1).toLowerCase();
3089
3240
  const type = contentTypeOf(file);
3090
3241
  const isMedia = type.startsWith("image/") || type.startsWith("font/");
3091
3242
  let limit = UPLOAD_MAX_BYTES;
@@ -3110,7 +3261,7 @@ async function upload(a, given, group) {
3110
3261
  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
3262
  const bytes = await fsp3.readFile(file);
3112
3263
  const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
3113
- const base = path13.basename(file);
3264
+ const base = path14.basename(file);
3114
3265
  if (KIND !== "design-system") {
3115
3266
  return {
3116
3267
  url: uploaded.url,
@@ -3142,12 +3293,12 @@ function folderName(name) {
3142
3293
  }
3143
3294
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
3144
3295
  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));
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));
3147
3298
  const found = /* @__PURE__ */ new Map();
3148
3299
  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)) {
3300
+ const project = path14.join(dir, "project");
3301
+ if (fs12.existsSync(path14.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
3151
3302
  const info = await readDesignSystemInfo(project).catch(() => null);
3152
3303
  if (info) {
3153
3304
  const namespace = info.namespace;
@@ -3156,8 +3307,8 @@ async function listSystems(dirs) {
3156
3307
  namespace: namespace ?? null,
3157
3308
  folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
3158
3309
  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
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
3161
3312
  });
3162
3313
  return;
3163
3314
  }
@@ -3166,7 +3317,7 @@ async function listSystems(dirs) {
3166
3317
  const entries = await fsp3.readdir(dir, { withFileTypes: true }).catch(() => []);
3167
3318
  for (const entry of entries) {
3168
3319
  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);
3320
+ await visit(path14.join(dir, entry.name), depth - 1);
3170
3321
  }
3171
3322
  };
3172
3323
  for (const start of starts) await visit(start, 4);
@@ -3190,10 +3341,10 @@ function reusedNote(a, flags) {
3190
3341
  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
3342
  }
3192
3343
  function assertServerFiles() {
3193
- if (!fs11.existsSync(path13.join(EDITOR_DIR, "index.html"))) {
3344
+ if (!fs12.existsSync(path14.join(EDITOR_DIR, "index.html"))) {
3194
3345
  throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
3195
3346
  }
3196
- if (!fs11.existsSync(SERVER_SCRIPT)) {
3347
+ if (!fs12.existsSync(SERVER_SCRIPT)) {
3197
3348
  throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
3198
3349
  }
3199
3350
  }
@@ -3221,21 +3372,21 @@ async function open(a, flags) {
3221
3372
  assertServerFiles();
3222
3373
  const existing = await runningServer2(a.root);
3223
3374
  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) });
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) });
3225
3376
  return;
3226
3377
  }
3227
3378
  const state = await startBackground(a, flags);
3228
3379
  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) });
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) });
3230
3381
  }
3231
3382
  async function startBackground(a, flags) {
3232
3383
  const port = Number(flags.get("port") ?? 0);
3233
3384
  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");
3385
+ const logFile = path14.join(a.root, LOG_FILE);
3386
+ const fd = fs12.openSync(logFile, "a");
3236
3387
  const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
3237
3388
  child.unref();
3238
- fs11.closeSync(fd);
3389
+ fs12.closeSync(fd);
3239
3390
  let exited = false;
3240
3391
  child.once("exit", () => exited = true);
3241
3392
  const deadline = Date.now() + 15e3;
@@ -3245,14 +3396,14 @@ async function startBackground(a, flags) {
3245
3396
  if (state && state.pid === child.pid) return state;
3246
3397
  if (exited) break;
3247
3398
  }
3248
- const tail = fs11.existsSync(logFile) ? fs11.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3399
+ const tail = fs12.existsSync(logFile) ? fs12.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
3249
3400
  throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
3250
3401
  ${tail}` : ""}`);
3251
3402
  }
3252
3403
  async function stop(a) {
3253
3404
  const state = readState(a.root);
3254
3405
  if (!state || !alive(state.pid)) {
3255
- fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3406
+ fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
3256
3407
  print({ stopped: false, message: "No editor server is running for this folder." });
3257
3408
  return;
3258
3409
  }
@@ -3263,7 +3414,7 @@ async function stop(a) {
3263
3414
  print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
3264
3415
  return;
3265
3416
  }
3266
- fs11.rmSync(path13.join(a.root, STATE_FILE), { force: true });
3417
+ fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
3267
3418
  print({ stopped: true, pid: state.pid });
3268
3419
  }
3269
3420
  async function share(a, flags) {
@@ -3307,7 +3458,7 @@ async function listComments(a, all) {
3307
3458
  async function artifactTitle(a) {
3308
3459
  const info = await readInfo(a.project).catch(() => null);
3309
3460
  const title = typeof info?.title === "string" ? info.title.trim() : "";
3310
- return title && title !== "project" ? title : path13.basename(a.root);
3461
+ return title && title !== "project" ? title : path14.basename(a.root);
3311
3462
  }
3312
3463
  async function waitComments(a, flags) {
3313
3464
  const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
@@ -3403,7 +3554,10 @@ Usage:
3403
3554
  node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
3404
3555
  node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
3405
3556
  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)
3557
+ ${KIND === "design-system" ? "" : ` node scripts/artifact.mjs ds-sync <root> [namespace] [--check] an installed design system is a copy: copy its changed files again
3558
+ from the system's folder and stamp copiedAt (--check: list only;
3559
+ --from <folder>: the source, when the record's path is not found)
3560
+ `} node scripts/artifact.mjs comments <root> [--all] the open comments people left in the editor (--all: resolved too)
3407
3561
  node scripts/artifact.mjs comments <root> --wait [--timeout s] wait for a comment sent to the agent from the open editor
3408
3562
  node scripts/artifact.mjs comment-reply <root> <id> <text|-> reply as the agent (-: text on stdin) [--resolve]
3409
3563
  node scripts/artifact.mjs comment-resolve <root> <id> [--reopen] mark a comment resolved (or open again)
@@ -3434,16 +3588,43 @@ async function main() {
3434
3588
  }
3435
3589
  case "upload": {
3436
3590
  const a = resolveArtifact(positional[0], false);
3437
- if (!fs11.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3591
+ if (!fs12.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
3438
3592
  throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
3439
3593
  }
3440
- fs11.mkdirSync(a.project, { recursive: true });
3594
+ fs12.mkdirSync(a.project, { recursive: true });
3441
3595
  print(await upload(a, positional[1], flags.get("group")));
3442
3596
  return 0;
3443
3597
  }
3444
3598
  case "list":
3445
3599
  print(await listSystems(positional));
3446
3600
  return 0;
3601
+ case "ds-sync": {
3602
+ if (KIND === "design-system") throw new CliError("NOT_APPLICABLE", "A design system has no installed design systems: ds-sync is for decks and canvases.");
3603
+ const a = resolveArtifact(positional[0]);
3604
+ const check2 = flags.has("check");
3605
+ let results;
3606
+ try {
3607
+ results = syncSystems(a.project, a.root, INDEX_FILE, { namespace: positional[1], from: flags.get("from"), check: check2 });
3608
+ } catch (error) {
3609
+ throw new CliError("NO_SUCH_SYSTEM", error.message);
3610
+ }
3611
+ const behind = results.filter((r) => r.changed.length > 0 || r.removed.length > 0);
3612
+ print({
3613
+ root: a.root,
3614
+ systems: results.map((r) => ({
3615
+ namespace: r.namespace,
3616
+ title: r.title,
3617
+ ...r.skipped ? { skipped: r.skipped } : {},
3618
+ ...r.missing ? { skipped: `project/ds/${r.namespace}/ does not exist: install it first (SKILL.md, Creating, step 4)` } : {},
3619
+ ...r.source ? { source: r.source } : {},
3620
+ changed: r.changed,
3621
+ ...r.removed.length > 0 ? { goneFromSource: r.removed } : {},
3622
+ ...check2 ? {} : { copied: r.copied, ...r.copiedAt ? { copiedAt: r.copiedAt } : {} }
3623
+ })),
3624
+ 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."
3625
+ });
3626
+ return check2 && behind.some((r) => r.changed.length > 0) ? 1 : 0;
3627
+ }
3447
3628
  case "comments": {
3448
3629
  const a = resolveArtifact(positional[0]);
3449
3630
  print(flags.has("wait") ? await waitComments(a, flags) : await listComments(a, flags.has("all")));
@@ -4,10 +4,10 @@
4
4
  import os5 from "node:os";
5
5
 
6
6
  import { createHash as createHash3, randomBytes as randomBytes2, randomUUID } from "node:crypto";
7
- import fs10 from "node:fs";
7
+ import fs11 from "node:fs";
8
8
  import fsp4 from "node:fs/promises";
9
9
  import http from "node:http";
10
- import path10 from "node:path";
10
+ import path11 from "node:path";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
 
13
13
  import { spawn } from "node:child_process";
@@ -1495,9 +1495,155 @@ ${sections || '<p class="muted">This copy has no tokens, components or guide.</p
1495
1495
  `;
1496
1496
  }
1497
1497
 
1498
- import { createHash, randomBytes } from "node:crypto";
1499
1498
  import fs5 from "node:fs";
1500
1499
  import path5 from "node:path";
1500
+ var FOLDER2 = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1501
+ var MAX_FILES = 2e3;
1502
+ var MAX_FILE_BYTES2 = 64 * 1024 * 1024;
1503
+ var DESIGN_SYSTEM_FILE = "design-system.json";
1504
+ function readRecords(project, indexFile) {
1505
+ let index;
1506
+ try {
1507
+ index = JSON.parse(fs5.readFileSync(path5.join(project, indexFile), "utf8"));
1508
+ } catch {
1509
+ return [];
1510
+ }
1511
+ const list = index?.designSystems;
1512
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
1513
+ const out = [];
1514
+ for (const raw of records) {
1515
+ const r = raw;
1516
+ if (typeof r?.namespace !== "string" || !FOLDER2.test(r.namespace)) continue;
1517
+ out.push({
1518
+ namespace: r.namespace,
1519
+ title: typeof r.title === "string" ? r.title : r.namespace,
1520
+ artifact: typeof r.artifact === "string" ? r.artifact : ""
1521
+ });
1522
+ }
1523
+ return out;
1524
+ }
1525
+ function plain(p, want) {
1526
+ const stat = fs5.lstatSync(p, { throwIfNoEntry: false });
1527
+ return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
1528
+ }
1529
+ function walk(dir, prefix = "", out = []) {
1530
+ for (const entry of fs5.readdirSync(path5.join(dir, prefix), { withFileTypes: true })) {
1531
+ if (out.length >= MAX_FILES) break;
1532
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1533
+ if (entry.isDirectory()) walk(dir, rel, out);
1534
+ else if (entry.isFile()) out.push(rel);
1535
+ }
1536
+ return out;
1537
+ }
1538
+ var projectOf = (candidate) => path5.basename(candidate) === "project" ? candidate : path5.join(candidate, "project");
1539
+ function sourceProject(artifact, root, from) {
1540
+ const given = from ?? artifact;
1541
+ if (!given) return { reason: "the record names no source folder", kind: "none" };
1542
+ 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" };
1543
+ const candidates = path5.isAbsolute(given) ? [given] : [path5.resolve(given), path5.resolve(root, given), path5.resolve(root, "..", given)];
1544
+ for (const candidate of candidates) {
1545
+ const project = projectOf(candidate);
1546
+ if (plain(project, "dir")) return { project };
1547
+ }
1548
+ if (!from && !path5.isAbsolute(given)) {
1549
+ let dir = path5.resolve(root, "..");
1550
+ for (let up = path5.dirname(dir); up !== dir; up = path5.dirname(dir)) {
1551
+ dir = up;
1552
+ const project = projectOf(path5.resolve(dir, given));
1553
+ if (plain(path5.join(project, DESIGN_SYSTEM_FILE), "file")) return { project };
1554
+ }
1555
+ }
1556
+ 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" };
1557
+ }
1558
+ function compareSystem(project, root, record, from) {
1559
+ const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
1560
+ const found = sourceProject(record.artifact, root, from);
1561
+ if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
1562
+ diff.source = path5.dirname(found.project);
1563
+ const copy = path5.join(project, "ds", record.namespace);
1564
+ if (!plain(copy, "dir")) return { ...diff, missing: true };
1565
+ for (const rel of walk(copy)) {
1566
+ const mine = path5.join(copy, rel);
1567
+ const theirs = path5.join(found.project, rel);
1568
+ if (!plain(mine, "file")) continue;
1569
+ if (!plain(theirs, "file")) {
1570
+ diff.removed.push(rel);
1571
+ continue;
1572
+ }
1573
+ const a = fs5.statSync(mine);
1574
+ const b = fs5.statSync(theirs);
1575
+ if (a.size > MAX_FILE_BYTES2 || b.size > MAX_FILE_BYTES2) continue;
1576
+ if (a.size !== b.size || !fs5.readFileSync(mine).equals(fs5.readFileSync(theirs))) diff.changed.push(rel);
1577
+ }
1578
+ return diff;
1579
+ }
1580
+ function systemStatus(project, root, indexFile, namespace) {
1581
+ const record = readRecords(project, indexFile).find((r) => r.namespace === namespace);
1582
+ if (!record) return null;
1583
+ const diff = compareSystem(project, root, record);
1584
+ if (diff.skippedKind) return { state: "unknown", changed: 0, removed: 0, reason: diff.skippedKind };
1585
+ if (diff.missing) return { state: "unknown", changed: 0, removed: 0, reason: "missing" };
1586
+ return { state: diff.changed.length > 0 ? "behind" : "current", changed: diff.changed.length, removed: diff.removed.length };
1587
+ }
1588
+ var DS_SYNC = /^__ds-sync\/([a-z0-9][a-z0-9_-]{0,63})$/;
1589
+ function pick(records, namespace) {
1590
+ if (namespace === void 0) return records;
1591
+ const one = records.filter((r) => r.namespace === namespace);
1592
+ if (one.length === 0) {
1593
+ const names = records.map((r) => r.namespace);
1594
+ throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
1595
+ }
1596
+ return one;
1597
+ }
1598
+ function compareSystems(project, root, indexFile, namespace, from) {
1599
+ return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
1600
+ }
1601
+ function stamp(project, indexFile, namespaces, now) {
1602
+ const file = path5.join(project, indexFile);
1603
+ const text = fs5.readFileSync(file, "utf8");
1604
+ const index = JSON.parse(text);
1605
+ const list = index.designSystems;
1606
+ const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
1607
+ for (const r of records) {
1608
+ const record = r;
1609
+ if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
1610
+ }
1611
+ const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
1612
+ const out = JSON.stringify(index, null, indent);
1613
+ const tmp = `${file}.${process.pid}.tmp`;
1614
+ fs5.writeFileSync(tmp, text.endsWith("\n") ? `${out}
1615
+ ` : out);
1616
+ fs5.renameSync(tmp, file);
1617
+ }
1618
+ function syncSystems(project, root, indexFile, opts) {
1619
+ const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
1620
+ const results = [];
1621
+ const synced = /* @__PURE__ */ new Set();
1622
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
1623
+ for (const diff of diffs) {
1624
+ const result = { ...diff, copied: [] };
1625
+ if (!opts.check && diff.source && !diff.missing) {
1626
+ for (const rel of diff.changed) {
1627
+ const target = path5.join(project, "ds", diff.namespace, rel);
1628
+ const tmp = `${target}.${process.pid}.tmp`;
1629
+ fs5.copyFileSync(path5.join(diff.source, "project", rel), tmp);
1630
+ fs5.renameSync(tmp, target);
1631
+ result.copied.push(rel);
1632
+ }
1633
+ if (diff.changed.length > 0) synced.add(diff.namespace);
1634
+ }
1635
+ results.push(result);
1636
+ }
1637
+ if (synced.size > 0) {
1638
+ stamp(project, indexFile, synced, now);
1639
+ for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
1640
+ }
1641
+ return results;
1642
+ }
1643
+
1644
+ import { createHash, randomBytes } from "node:crypto";
1645
+ import fs6 from "node:fs";
1646
+ import path6 from "node:path";
1501
1647
  var HISTORY_DIR = "history";
1502
1648
  var INDEX = "index.json";
1503
1649
  var OBJECTS = "objects";
@@ -1512,13 +1658,13 @@ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
1512
1658
  var SHA = /^[0-9a-f]{64}$/;
1513
1659
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1514
1660
  function writeAtomic2(file, data) {
1515
- fs5.mkdirSync(path5.dirname(file), { recursive: true });
1516
- const tmp = path5.join(path5.dirname(file), `.${path5.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
1661
+ fs6.mkdirSync(path6.dirname(file), { recursive: true });
1662
+ const tmp = path6.join(path6.dirname(file), `.${path6.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
1517
1663
  try {
1518
- fs5.writeFileSync(tmp, data);
1519
- fs5.renameSync(tmp, file);
1664
+ fs6.writeFileSync(tmp, data);
1665
+ fs6.renameSync(tmp, file);
1520
1666
  } catch (error) {
1521
- fs5.rmSync(tmp, { force: true });
1667
+ fs6.rmSync(tmp, { force: true });
1522
1668
  throw error;
1523
1669
  }
1524
1670
  }
@@ -1599,10 +1745,10 @@ var History = class {
1599
1745
  /** path → sha: what the files hold after the newest version. */
1600
1746
  headState;
1601
1747
  constructor(root) {
1602
- this.dir = path5.join(root, HISTORY_DIR);
1748
+ this.dir = path6.join(root, HISTORY_DIR);
1603
1749
  let raw = null;
1604
1750
  try {
1605
- raw = JSON.parse(fs5.readFileSync(path5.join(this.dir, INDEX), "utf8"));
1751
+ raw = JSON.parse(fs6.readFileSync(path6.join(this.dir, INDEX), "utf8"));
1606
1752
  } catch {
1607
1753
  }
1608
1754
  this.index = parseIndex(raw);
@@ -1646,7 +1792,7 @@ var History = class {
1646
1792
  read(sha) {
1647
1793
  if (!SHA.test(sha)) return null;
1648
1794
  try {
1649
- return fs5.readFileSync(path5.join(this.dir, OBJECTS, sha));
1795
+ return fs6.readFileSync(path6.join(this.dir, OBJECTS, sha));
1650
1796
  } catch {
1651
1797
  return null;
1652
1798
  }
@@ -1660,7 +1806,7 @@ var History = class {
1660
1806
  size(sha) {
1661
1807
  if (!SHA.test(sha)) return null;
1662
1808
  try {
1663
- return fs5.statSync(path5.join(this.dir, OBJECTS, sha)).size;
1809
+ return fs6.statSync(path6.join(this.dir, OBJECTS, sha)).size;
1664
1810
  } catch {
1665
1811
  return null;
1666
1812
  }
@@ -1794,8 +1940,8 @@ var History = class {
1794
1940
  return out;
1795
1941
  }
1796
1942
  putObject(sha, bytes) {
1797
- const file = path5.join(this.dir, OBJECTS, sha);
1798
- if (fs5.existsSync(file)) return;
1943
+ const file = path6.join(this.dir, OBJECTS, sha);
1944
+ if (fs6.existsSync(file)) return;
1799
1945
  writeAtomic2(file, bytes);
1800
1946
  }
1801
1947
  /** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
@@ -1832,14 +1978,14 @@ var History = class {
1832
1978
  }
1833
1979
  let names;
1834
1980
  try {
1835
- names = fs5.readdirSync(path5.join(this.dir, OBJECTS));
1981
+ names = fs6.readdirSync(path6.join(this.dir, OBJECTS));
1836
1982
  } catch {
1837
1983
  return;
1838
1984
  }
1839
- for (const name of names) if (SHA.test(name) && !keep.has(name)) fs5.rmSync(path5.join(this.dir, OBJECTS, name), { force: true });
1985
+ for (const name of names) if (SHA.test(name) && !keep.has(name)) fs6.rmSync(path6.join(this.dir, OBJECTS, name), { force: true });
1840
1986
  }
1841
1987
  save() {
1842
- writeAtomic2(path5.join(this.dir, INDEX), `${JSON.stringify(this.index)}
1988
+ writeAtomic2(path6.join(this.dir, INDEX), `${JSON.stringify(this.index)}
1843
1989
  `);
1844
1990
  }
1845
1991
  };
@@ -1909,19 +2055,19 @@ function diffContents(before, after) {
1909
2055
  }
1910
2056
 
1911
2057
  import { createHash as createHash2 } from "node:crypto";
1912
- import fs9 from "node:fs";
2058
+ import fs10 from "node:fs";
1913
2059
  import os4 from "node:os";
1914
- import path9 from "node:path";
2060
+ import path10 from "node:path";
1915
2061
 
1916
- import fs7 from "node:fs";
2062
+ import fs8 from "node:fs";
1917
2063
  import fsp2 from "node:fs/promises";
1918
- import path7 from "node:path";
2064
+ import path8 from "node:path";
1919
2065
  import { promisify } from "node:util";
1920
2066
  import zlib from "node:zlib";
1921
2067
 
1922
2068
  import crypto from "node:crypto";
1923
- import fs6 from "node:fs";
1924
- import path6 from "node:path";
2069
+ import fs7 from "node:fs";
2070
+ import path7 from "node:path";
1925
2071
  var GUEST_ROLES = ["editor", "commenter", "viewer"];
1926
2072
  var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
1927
2073
  var SHARING_FILE = ".sharing.json";
@@ -1948,13 +2094,13 @@ var Sharing = class _Sharing {
1948
2094
  file;
1949
2095
  state;
1950
2096
  constructor(root) {
1951
- this.file = path6.join(root, SHARING_FILE);
2097
+ this.file = path7.join(root, SHARING_FILE);
1952
2098
  this.state = _Sharing.read(this.file);
1953
2099
  }
1954
2100
  static read(file) {
1955
2101
  let raw;
1956
2102
  try {
1957
- raw = JSON.parse(fs6.readFileSync(file, "utf8"));
2103
+ raw = JSON.parse(fs7.readFileSync(file, "utf8"));
1958
2104
  } catch {
1959
2105
  return fresh();
1960
2106
  }
@@ -1986,9 +2132,9 @@ var Sharing = class _Sharing {
1986
2132
  }
1987
2133
  save() {
1988
2134
  const tmp = `${this.file}.${process.pid}.tmp`;
1989
- fs6.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
2135
+ fs7.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
1990
2136
  `, { mode: 384 });
1991
- fs6.renameSync(tmp, this.file);
2137
+ fs7.renameSync(tmp, this.file);
1992
2138
  }
1993
2139
  /** The role whose link has this token, while that link is on. */
1994
2140
  linkRole(tokenGiven) {
@@ -2070,10 +2216,10 @@ var ArchiveTooLarge = class extends Error {
2070
2216
  code = "TOO_LARGE";
2071
2217
  };
2072
2218
  async function archiveEntries(root, skip = []) {
2073
- const folder = path7.basename(path7.resolve(root)) || "artifact";
2074
- const skipped = new Set(skip.map((file) => path7.resolve(file)));
2219
+ const folder = path8.basename(path8.resolve(root)) || "artifact";
2220
+ const skipped = new Set(skip.map((file) => path8.resolve(file)));
2075
2221
  const entries = [];
2076
- const walk = async (dir, rel) => {
2222
+ const walk2 = async (dir, rel) => {
2077
2223
  let names;
2078
2224
  try {
2079
2225
  names = await fsp2.readdir(dir, { withFileTypes: true });
@@ -2084,17 +2230,17 @@ async function archiveEntries(root, skip = []) {
2084
2230
  for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2085
2231
  const name = dirent.name;
2086
2232
  if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
2087
- const file = path7.join(dir, name);
2233
+ const file = path8.join(dir, name);
2088
2234
  if (skipped.has(file)) continue;
2089
2235
  const inner = rel ? `${rel}/${name}` : name;
2090
- if (dirent.isDirectory()) await walk(file, inner);
2236
+ if (dirent.isDirectory()) await walk2(file, inner);
2091
2237
  else if (dirent.isFile()) {
2092
2238
  const stat = await fsp2.stat(file).catch(() => null);
2093
2239
  if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
2094
2240
  }
2095
2241
  }
2096
2242
  };
2097
- await walk(path7.resolve(root), "");
2243
+ await walk2(path8.resolve(root), "");
2098
2244
  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.`);
2099
2245
  const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2100
2246
  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.`);
@@ -2143,7 +2289,7 @@ async function writeZip(entries, write) {
2143
2289
  const crc = crc32(data);
2144
2290
  let method = 0;
2145
2291
  let body = data;
2146
- if (data.length > 0 && !STORED.has(path7.extname(entry.name).toLowerCase())) {
2292
+ if (data.length > 0 && !STORED.has(path8.extname(entry.name).toLowerCase())) {
2147
2293
  const deflated = await deflateRaw(data);
2148
2294
  if (deflated.length < data.length) {
2149
2295
  method = 8;
@@ -2197,12 +2343,12 @@ async function writeZip(entries, write) {
2197
2343
  }
2198
2344
  function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
2199
2345
  const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2200
- return `${path7.basename(path7.resolve(root)) || "artifact"}-${day}.zip`;
2346
+ return `${path8.basename(path8.resolve(root)) || "artifact"}-${day}.zip`;
2201
2347
  }
2202
2348
  async function writeArchive(root, out) {
2203
- const file = path7.resolve(out);
2349
+ const file = path8.resolve(out);
2204
2350
  const entries = await archiveEntries(root, [file]);
2205
- const tmp = path7.join(path7.dirname(file), `.${path7.basename(file)}.${process.pid}.tmp`);
2351
+ const tmp = path8.join(path8.dirname(file), `.${path8.basename(file)}.${process.pid}.tmp`);
2206
2352
  const handle = await fsp2.open(tmp, "w");
2207
2353
  let bytes;
2208
2354
  try {
@@ -2220,10 +2366,10 @@ async function writeArchive(root, out) {
2220
2366
  return { file, files: entries.length, bytes };
2221
2367
  }
2222
2368
  async function archiveCommand(root, out) {
2223
- const where = out ? path7.resolve(out) : path7.dirname(path7.resolve(root));
2224
- const target = fs7.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path7.join(where, archiveFileName(root)) : where;
2369
+ const where = out ? path8.resolve(out) : path8.dirname(path8.resolve(root));
2370
+ const target = fs8.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path8.join(where, archiveFileName(root)) : where;
2225
2371
  const { file, files, bytes } = await writeArchive(root, target);
2226
- return { file, folder: path7.basename(path7.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2372
+ return { file, folder: path8.basename(path8.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2227
2373
  }
2228
2374
  function attachment(name) {
2229
2375
  const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
@@ -2271,31 +2417,31 @@ async function sendArchive(request, response, root) {
2271
2417
  }
2272
2418
  }
2273
2419
 
2274
- import fs8 from "node:fs";
2420
+ import fs9 from "node:fs";
2275
2421
  import fsp3 from "node:fs/promises";
2276
2422
  import os3 from "node:os";
2277
- import path8 from "node:path";
2423
+ import path9 from "node:path";
2278
2424
  var MAX_BYTES2 = 4294967295;
2279
2425
  function shown(file) {
2280
2426
  const home = os3.homedir();
2281
- return home && (file === home || file.startsWith(home + path8.sep)) ? `~${file.slice(home.length)}` : file;
2427
+ return home && (file === home || file.startsWith(home + path9.sep)) ? `~${file.slice(home.length)}` : file;
2282
2428
  }
2283
2429
  function saved(file, bytes) {
2284
- return { path: file, name: path8.basename(file), folder: path8.dirname(file), shown: shown(file), bytes };
2430
+ return { path: file, name: path9.basename(file), folder: path9.dirname(file), shown: shown(file), bytes };
2285
2431
  }
2286
2432
  function safeFileName(raw) {
2287
2433
  let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
2288
2434
  if (name.length > 200) {
2289
- const ext = path8.extname(name).slice(0, 16);
2435
+ const ext = path9.extname(name).slice(0, 16);
2290
2436
  name = name.slice(0, 200 - ext.length) + ext;
2291
2437
  }
2292
2438
  return name || "download";
2293
2439
  }
2294
2440
  async function openFree(dir, name) {
2295
- const ext = path8.extname(name);
2441
+ const ext = path9.extname(name);
2296
2442
  const stem = name.slice(0, name.length - ext.length);
2297
2443
  for (let n = 0; ; n++) {
2298
- const file = path8.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
2444
+ const file = path9.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
2299
2445
  try {
2300
2446
  return { file, handle: await fsp3.open(file, "wx") };
2301
2447
  } catch (error) {
@@ -2314,7 +2460,7 @@ async function saveDownload(request, response, root) {
2314
2460
  let handle = null;
2315
2461
  let bytes = 0;
2316
2462
  try {
2317
- const dir = path8.join(root, EXPORTS_DIR);
2463
+ const dir = path9.join(root, EXPORTS_DIR);
2318
2464
  await fsp3.mkdir(dir, { recursive: true });
2319
2465
  ({ file, handle } = await openFree(dir, name));
2320
2466
  for await (const chunk of request) {
@@ -2339,7 +2485,7 @@ async function saveArchive(request, response, root) {
2339
2485
  request.resume();
2340
2486
  try {
2341
2487
  const { file } = await archiveCommand(root, void 0);
2342
- sendJson2(response, 200, saved(file, fs8.statSync(file).size));
2488
+ sendJson2(response, 200, saved(file, fs9.statSync(file).size));
2343
2489
  } catch (error) {
2344
2490
  const tooLarge = error instanceof ArchiveTooLarge;
2345
2491
  sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
@@ -2380,7 +2526,7 @@ function localOwner() {
2380
2526
  }
2381
2527
  return { id: "local", name, email: `${name}@localhost` };
2382
2528
  }
2383
- var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path9.resolve(root)).digest("hex").slice(0, 12)}`;
2529
+ var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path10.resolve(root)).digest("hex").slice(0, 12)}`;
2384
2530
  var ShareHost = class {
2385
2531
  sharing;
2386
2532
  options;
@@ -2545,7 +2691,7 @@ var ShareHost = class {
2545
2691
  }
2546
2692
  if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
2547
2693
  if (method === "GET" && p === "/__share/ui.js") {
2548
- fs9.stat(this.options.uiFile, (error, stat) => {
2694
+ fs10.stat(this.options.uiFile, (error, stat) => {
2549
2695
  if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
2550
2696
  sendFile(request, response, this.options.uiFile, { "Content-Type": "text/javascript; charset=utf-8", "Content-Length": stat.size, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
2551
2697
  });
@@ -2565,7 +2711,7 @@ var ShareHost = class {
2565
2711
  */
2566
2712
  headTags(_viewer, query = "") {
2567
2713
  let tags = `<script src="/__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&amp;")}"></script>`;
2568
- if (fs9.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
2714
+ if (fs10.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
2569
2715
  return tags;
2570
2716
  }
2571
2717
  /** `html` with the head tags first in its <head>; null when it has none. */
@@ -2957,7 +3103,7 @@ function noticeView(n, label, lostAt, gone) {
2957
3103
  ...n.lost.sha === null ? { deleted: true } : {}
2958
3104
  };
2959
3105
  }
2960
- var plain = (text) => ({ en: text, zh: text });
3106
+ var plain2 = (text) => ({ en: text, zh: text });
2961
3107
 
2962
3108
  var PEER_ID = /^[A-Za-z0-9_-]{8,64}$/;
2963
3109
  function agentWhere(main2, count, label) {
@@ -3257,7 +3403,7 @@ async function collectFiles(root, limit) {
3257
3403
  const seen = /* @__PURE__ */ new Set();
3258
3404
  let truncated = false;
3259
3405
  const visit = async (relDir, descend) => {
3260
- const dir = relDir === "" ? root : path10.join(root, ...relDir.split("/"));
3406
+ const dir = relDir === "" ? root : path11.join(root, ...relDir.split("/"));
3261
3407
  let entries;
3262
3408
  try {
3263
3409
  entries = await fsp4.readdir(dir, { withFileTypes: true });
@@ -3288,8 +3434,8 @@ async function collectFiles(root, limit) {
3288
3434
  return { rels, truncated };
3289
3435
  }
3290
3436
  async function writeFileAtomic(file, bytes) {
3291
- await fsp4.mkdir(path10.dirname(file), { recursive: true });
3292
- const tmp = path10.join(path10.dirname(file), `.${path10.basename(file)}.${randomUUID()}.tmp`);
3437
+ await fsp4.mkdir(path11.dirname(file), { recursive: true });
3438
+ const tmp = path11.join(path11.dirname(file), `.${path11.basename(file)}.${randomUUID()}.tmp`);
3293
3439
  try {
3294
3440
  await fsp4.writeFile(tmp, bytes);
3295
3441
  await fsp4.rename(tmp, file);
@@ -3345,11 +3491,11 @@ var Store = class {
3345
3491
  async diskPath(rel) {
3346
3492
  const realRoot = await fsp4.realpath(this.root).catch(() => null);
3347
3493
  if (realRoot === null) return null;
3348
- const file = path10.join(realRoot, ...rel.split("/"));
3349
- for (let dir = path10.dirname(file); ; dir = path10.dirname(dir)) {
3494
+ const file = path11.join(realRoot, ...rel.split("/"));
3495
+ for (let dir = path11.dirname(file); ; dir = path11.dirname(dir)) {
3350
3496
  const real = await fsp4.realpath(dir).catch(() => null);
3351
3497
  if (real === null) {
3352
- if (path10.dirname(dir) === dir) return null;
3498
+ if (path11.dirname(dir) === dir) return null;
3353
3499
  continue;
3354
3500
  }
3355
3501
  if (real !== dir) return null;
@@ -3579,7 +3725,7 @@ var Store = class {
3579
3725
  }
3580
3726
  async findBlobFile(id) {
3581
3727
  try {
3582
- const names = await fsp4.readdir(path10.join(this.root, LAYOUT.blobDir));
3728
+ const names = await fsp4.readdir(path11.join(this.root, LAYOUT.blobDir));
3583
3729
  const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
3584
3730
  if (name === void 0) return null;
3585
3731
  const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
@@ -3634,13 +3780,13 @@ function watchArtifact(store, onChange, onError) {
3634
3780
  const directories = /* @__PURE__ */ new Map();
3635
3781
  const visit = (rel, descend) => {
3636
3782
  if (directories.size >= maxDirectories) return;
3637
- const dir = rel === "" ? store.root : path10.join(store.root, ...rel.split("/"));
3638
- if (directories.has(dir) || !fs10.existsSync(dir)) return;
3639
- const info = fs10.lstatSync(dir);
3783
+ const dir = rel === "" ? store.root : path11.join(store.root, ...rel.split("/"));
3784
+ if (directories.has(dir) || !fs11.existsSync(dir)) return;
3785
+ const info = fs11.lstatSync(dir);
3640
3786
  if (!info.isDirectory()) return;
3641
3787
  directories.set(dir, `${info.dev}:${info.ino}`);
3642
3788
  if (!descend) return;
3643
- for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
3789
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
3644
3790
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
3645
3791
  const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
3646
3792
  if (LAYOUT.descendInto(child)) visit(child, true);
@@ -3657,19 +3803,19 @@ function watchArtifact(store, onChange, onError) {
3657
3803
  if (watchers.has(dir)) continue;
3658
3804
  let watcher;
3659
3805
  try {
3660
- watcher = fs10.watch(dir, (event, name) => {
3806
+ watcher = fs11.watch(dir, (event, name) => {
3661
3807
  if (stopped) return;
3662
3808
  if (name) {
3663
- const changed = path10.join(dir, String(name));
3809
+ const changed = path11.join(dir, String(name));
3664
3810
  if (event === "rename" && watchers.has(changed)) {
3665
3811
  for (const [watchedDir, item] of watchers) {
3666
- if (watchedDir === changed || watchedDir.startsWith(changed + path10.sep)) {
3812
+ if (watchedDir === changed || watchedDir.startsWith(changed + path11.sep)) {
3667
3813
  item.watcher.close();
3668
3814
  watchers.delete(watchedDir);
3669
3815
  }
3670
3816
  }
3671
3817
  }
3672
- const rel = path10.relative(store.root, changed).split(path10.sep).join("/");
3818
+ const rel = path11.relative(store.root, changed).split(path11.sep).join("/");
3673
3819
  if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
3674
3820
  }
3675
3821
  schedule();
@@ -3705,7 +3851,7 @@ function watchArtifact(store, onChange, onError) {
3705
3851
  const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
3706
3852
  const parts = [];
3707
3853
  for (const rel of rels) {
3708
- const info = await fsp4.lstat(path10.join(store.root, ...rel.split("/"))).catch(() => null);
3854
+ const info = await fsp4.lstat(path11.join(store.root, ...rel.split("/"))).catch(() => null);
3709
3855
  parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
3710
3856
  }
3711
3857
  return parts.join("\n");
@@ -3736,11 +3882,11 @@ function watchArtifact(store, onChange, onError) {
3736
3882
  var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
3737
3883
  async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1", log = (line) => process.stderr.write(`${line}
3738
3884
  `), agentHook, lan = false, portGiven = false }) {
3739
- const project = path10.join(root, "project");
3740
- const stamp = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
3885
+ const project = path11.join(root, "project");
3886
+ const stamp2 = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
3741
3887
  const projectDir = await fsp4.realpath(project);
3742
3888
  const history = new History(root);
3743
- const store = new Store(projectDir, history, stamp);
3889
+ const store = new Store(projectDir, history, stamp2);
3744
3890
  const owner = localOwner();
3745
3891
  const share = new ShareHost({
3746
3892
  root,
@@ -3748,9 +3894,9 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
3748
3894
  owner,
3749
3895
  noun: NOUNS[KIND] ?? { en: NOUN, zh: NOUN, zhThis: NOUN },
3750
3896
  title: () => artifactTitle(root, projectDir),
3751
- log: stamp,
3897
+ log: stamp2,
3752
3898
  createServer: () => http.createServer(handle),
3753
- uiFile: path10.join(EDITOR_DIR, "share-ui.js"),
3899
+ uiFile: path11.join(EDITOR_DIR, "share-ui.js"),
3754
3900
  // The History panel's address and what this person may do there; on a version's page (?at=n), which version.
3755
3901
  globals: (viewer, query) => {
3756
3902
  const at = Number(query.get("at") ?? "");
@@ -3759,7 +3905,7 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
3759
3905
  });
3760
3906
  const comments = createCommentsService({
3761
3907
  root,
3762
- log: stamp,
3908
+ log: stamp2,
3763
3909
  locate: commentLocation ? (input) => commentLocation(input, projectDir) : void 0,
3764
3910
  agentHook,
3765
3911
  userName: owner.name,
@@ -3798,7 +3944,7 @@ data: ${JSON.stringify(event)}
3798
3944
  }
3799
3945
  return (p) => {
3800
3946
  const rel = p.replace(/^project\//, "");
3801
- return named?.(rel) ?? plain(rel);
3947
+ return named?.(rel) ?? plain2(rel);
3802
3948
  };
3803
3949
  };
3804
3950
  const agentWorked = (paths) => {
@@ -3829,16 +3975,16 @@ data: ${JSON.stringify(event)}
3829
3975
  // A notice whose overwrite named the version its saver's edits started from: what was lost goes back by line into
3830
3976
  // the file as it is now, the lost lines winning where both changed them; a deleted file gets all of it back.
3831
3977
  recover: (_path, base, lost, now) => now === null ? lost : mergeTextBytes(base, lost, now),
3832
- log: stamp
3978
+ log: stamp2
3833
3979
  });
3834
3980
  const stopWatch = watchArtifact(
3835
3981
  store,
3836
3982
  (event) => {
3837
- stamp(`changed on disk: ${event.paths.join(", ")}${clients.size ? "" : " (no page open)"}`);
3983
+ stamp2(`changed on disk: ${event.paths.join(", ")}${clients.size ? "" : " (no page open)"}`);
3838
3984
  changed({ ...event, by: authorOf(AGENT) });
3839
3985
  agentWorked(event.paths);
3840
3986
  },
3841
- (error) => stamp(`watching failed: ${errorText(error)}`)
3987
+ (error) => stamp2(`watching failed: ${errorText(error)}`)
3842
3988
  );
3843
3989
  const events = async (request, response, viewer) => {
3844
3990
  const given = new URL(request.url ?? "/", "http://local").searchParams.get("client") ?? "";
@@ -3901,7 +4047,7 @@ data: ${JSON.stringify(event)}
3901
4047
  const parts = rest === "" ? ["index.html"] : rest.split("/");
3902
4048
  const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
3903
4049
  const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
3904
- const file = realDist === null ? null : path10.join(realDist, ...parts);
4050
+ const file = realDist === null ? null : path11.join(realDist, ...parts);
3905
4051
  const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
3906
4052
  const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
3907
4053
  if (file === null || real !== file || !info?.isFile()) {
@@ -3912,7 +4058,7 @@ data: ${JSON.stringify(event)}
3912
4058
  response.writeHead(404, { "Cache-Control": "no-store" }).end();
3913
4059
  return;
3914
4060
  }
3915
- const ext = path10.extname(file).toLowerCase();
4061
+ const ext = path11.extname(file).toLowerCase();
3916
4062
  const type = STATIC_TYPES[ext] ?? "application/octet-stream";
3917
4063
  if (ext === ".html") {
3918
4064
  const key = parts.join("/");
@@ -3967,7 +4113,7 @@ data: ${JSON.stringify(event)}
3967
4113
  const outcome = await store.publish(files, whoOf(viewer), over, merged, CLIENT_ID.test(page) ? page : "");
3968
4114
  const names = Object.keys(files).map((p) => p.replace(/^project\//, ""));
3969
4115
  const made = outcome.version ? ` as version ${outcome.version.n}` : "";
3970
- stamp(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
4116
+ stamp2(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
3971
4117
  sendJson2(response, outcome.status, outcome.body);
3972
4118
  if (outcome.status === 200 && clients.size > 0) {
3973
4119
  const from = String(request.headers["x-deck-client"] ?? "");
@@ -3988,7 +4134,7 @@ data: ${JSON.stringify(event)}
3988
4134
  return;
3989
4135
  }
3990
4136
  const result = await store.putBlob(bytes, contentType);
3991
- stamp(`uploaded from the page: ${result.url} (${contentType}, ${bytes.length} bytes)`);
4137
+ stamp2(`uploaded from the page: ${result.url} (${contentType}, ${bytes.length} bytes)`);
3992
4138
  sendJson2(response, 200, result);
3993
4139
  };
3994
4140
  const session = (viewer) => {
@@ -4101,6 +4247,24 @@ data: ${JSON.stringify(event)}
4101
4247
  else await upload(request, response);
4102
4248
  return;
4103
4249
  }
4250
+ const dsSync = DESIGN_SYSTEMS ? DS_SYNC.exec(rest) : null;
4251
+ if (dsSync) {
4252
+ const folder = dsSync[1];
4253
+ const status = systemStatus(projectDir, root, INDEX_FILE, folder);
4254
+ if (method === "POST") {
4255
+ request.resume();
4256
+ if (!sameOrigin(request)) return publishError(response, 403, "not_writer", "cross-origin request");
4257
+ if (viewer.guest) return publishError(response, 403, "not_owner", "Only the owner of this computer can sync a design system from its source.");
4258
+ if (status === null) return sendJson2(response, 404, { error: "not_found" });
4259
+ const [result] = syncSystems(projectDir, root, INDEX_FILE, { namespace: folder });
4260
+ return sendJson2(response, 200, { copied: result.copied.length, ...systemStatus(projectDir, root, INDEX_FILE, folder), canSync: true });
4261
+ }
4262
+ if (method !== "GET" && method !== "HEAD") {
4263
+ response.writeHead(405, { Allow: "GET, HEAD, POST" }).end();
4264
+ return;
4265
+ }
4266
+ return status === null ? sendJson2(response, 404, { error: "not_found" }, { "Cache-Control": "no-store" }) : sendJson2(response, 200, { ...status, canSync: !viewer.guest }, { "Cache-Control": "no-store" });
4267
+ }
4104
4268
  if (method !== "GET" && method !== "HEAD") {
4105
4269
  response.writeHead(405, { Allow: "GET, HEAD" }).end();
4106
4270
  return;
@@ -4156,7 +4320,7 @@ data: ${JSON.stringify(event)}
4156
4320
  if (!response.headersSent) publishError(response, 413, "too_large", "request body is too large");
4157
4321
  return;
4158
4322
  }
4159
- stamp(`request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
4323
+ stamp2(`request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
4160
4324
  if (!response.headersSent) publishError(response, 500, "upstream_error", "internal error");
4161
4325
  else response.destroy();
4162
4326
  }
@@ -4219,17 +4383,17 @@ data: ${JSON.stringify(event)}
4219
4383
  throw error;
4220
4384
  });
4221
4385
  const url = `${serverOrigin(host, actualPort)}${editorPath}`;
4222
- stamp(`serving ${project} at ${url}`);
4386
+ stamp2(`serving ${project} at ${url}`);
4223
4387
  const lanUrls = share.lanUrls();
4224
4388
  return { url, port: actualPort, grant, close, ...lanUrls.length ? { lan: lanUrls } : {} };
4225
4389
  }
4226
4390
  function artifactTitle(root, projectDir) {
4227
4391
  try {
4228
- const index = JSON.parse(fs10.readFileSync(path10.join(projectDir, INDEX_FILE), "utf8"));
4392
+ const index = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE), "utf8"));
4229
4393
  for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
4230
4394
  } catch {
4231
4395
  }
4232
- return path10.basename(root);
4396
+ return path11.basename(root);
4233
4397
  }
4234
4398
  var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
4235
4399
  async function serveForeground({ root, port, host, agentHook, lan, portGiven }) {
@@ -4250,18 +4414,18 @@ async function main(argv) {
4250
4414
  }
4251
4415
  if (positional.length !== 1 || flags.has("help")) {
4252
4416
  process.stderr.write(
4253
- `Usage: node ${path10.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
4417
+ `Usage: node ${path11.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
4254
4418
  <root> holds project/${INDEX_FILE}.
4255
4419
  `
4256
4420
  );
4257
4421
  return 2;
4258
4422
  }
4259
- let root = path10.resolve(positional[0]);
4260
- if (path10.basename(root) === "project" && fs10.existsSync(path10.join(root, INDEX_FILE)) && !fs10.existsSync(path10.join(root, "project"))) {
4261
- root = path10.dirname(root);
4423
+ let root = path11.resolve(positional[0]);
4424
+ if (path11.basename(root) === "project" && fs11.existsSync(path11.join(root, INDEX_FILE)) && !fs11.existsSync(path11.join(root, "project"))) {
4425
+ root = path11.dirname(root);
4262
4426
  }
4263
- if (!fs10.statSync(path10.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
4264
- process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path10.join(root, "project", INDEX_FILE)} does not exist.` })}
4427
+ if (!fs11.statSync(path11.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
4428
+ process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path11.join(root, "project", INDEX_FILE)} does not exist.` })}
4265
4429
  `);
4266
4430
  return 2;
4267
4431
  }
@@ -4285,7 +4449,7 @@ async function main(argv) {
4285
4449
  }
4286
4450
  function isMain(moduleUrl) {
4287
4451
  try {
4288
- return Boolean(process.argv[1]) && pathToFileURL(fs10.realpathSync(process.argv[1])).href === moduleUrl;
4452
+ return Boolean(process.argv[1]) && pathToFileURL(fs11.realpathSync(process.argv[1])).href === moduleUrl;
4289
4453
  } catch {
4290
4454
  return false;
4291
4455
  }
@@ -4302,7 +4466,7 @@ function createArtifactServer(kind, moduleUrl) {
4302
4466
  kindLabels = kind.labels;
4303
4467
  kindAgentContext = kind.agentContext;
4304
4468
  SCRIPT = fileURLToPath(moduleUrl);
4305
- EDITOR_DIR = path10.join(path10.dirname(SCRIPT), "editor");
4469
+ EDITOR_DIR = path11.join(path11.dirname(SCRIPT), "editor");
4306
4470
  const runIfMain = () => {
4307
4471
  if (!isMain(moduleUrl)) return;
4308
4472
  main(process.argv.slice(2)).then(