dsh-claude-design 2.7.0 → 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 2.7.0: built from baoyu-design.
1
+ // dsh-claude-design 2.7.2: built from baoyu-design.
2
2
  window.__ModuleLoader__.load({
3
3
  id: "dsh-claude-design",
4
4
  factory: (require) => {
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
- // dsh-claude-design 2.7.0: built from baoyu-design.
1
+ // dsh-claude-design 2.7.2: built from baoyu-design.
2
2
  var define_PLUGIN_default = { name: "claude-design", provider: "dsh-claude-design", skills: [{ name: "claude-design", description: "Make and revise designs in the Design (canvas) artifact format (project/canvas.json plus one .dc.html Design Component file per artboard) as local files, then open them in a local canvas editor where the user can view the artboards side by side, retype copy and adjust tweaks. Use when the user asks for an editable canvas of artboards in this format, names claude-design or the Design (canvas) type, or wants to revise a folder that holds project/canvas.json. A one-off HTML prototype or design exploration outside this format is baoyu-design's.", comments: true }] };
3
3
 
4
4
  import { spawn as spawn2 } from "node:child_process";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { statSync } from "node:fs";
7
7
  import { readFile } from "node:fs/promises";
8
- import { delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
8
+ import { basename as basename2, delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  import { spawn } from "node:child_process";
@@ -345,13 +345,19 @@ function openedEditor(script, exec, result) {
345
345
  const args = exec.arguments;
346
346
  const command = args?.command;
347
347
  if (typeof command !== "string" || args?.run_in_background === true) return null;
348
- const at = command.indexOf(script);
349
- if (at < 0 || !/(?:^|[\s"'])open(?:[\s"']|$)/.test(command.slice(at + script.length))) return null;
348
+ if (!/(?:^|[\s"'])open(?:[\s"']|$)/.test(command)) return null;
350
349
  const stdout = bashStdout(result);
351
350
  const opened = stdout === null ? void 0 : jsonObjects(stdout).findLast((o) => typeof o.root === "string" && typeof o.url === "string");
352
351
  if (typeof opened?.root !== "string" || typeof opened.url !== "string") return null;
352
+ if (!ranScript(script, command, opened.stop)) return null;
353
353
  return { session: String(exec.agent.id), root: opened.root, url: opened.url };
354
354
  }
355
+ function ranScript(script, command, stop) {
356
+ if (typeof stop !== "string") return command.includes(script);
357
+ if (stop.includes(script)) return true;
358
+ const folder = basename2(dirname(dirname(script)));
359
+ return new RegExp(`[/\\\\]${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[/\\\\]scripts[/\\\\]artifact\\.mjs`).test(stop);
360
+ }
355
361
  var bound = (text) => text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX - 1)}…`;
356
362
  function commentMessage(skill, plugin, out, root) {
357
363
  const title = typeof out.title === "string" && out.title ? out.title : root.split(/[\\/]/).pop() || root;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-design",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "DeepSeek Harness plugin: the claude-design skill from baoyu-design. The agent writes canvases of artboards as files and opens them in a local canvas editor.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -61,6 +61,7 @@ leave alone stays as it is, and removing an artboard's file is deleting it. The
61
61
  - `upload <root> <file>` → `{url:"/_blob/<id>", …}`: stores a copy under `project/assets/` (png, jpg, gif, webp, avif, svg; woff2, woff, ttf, otf; .css .js .json; ≤20 MB); the same bytes always get the same `url`. `<root>` must exist.
62
62
  - `list [dir …]` → the design systems in the working directory (or the folders given, and those in `$CLAUDE_DESIGN_SYSTEMS`), each `{title, namespace, folder, root, readme, tokens}`: its files are at `<its root>/project/<path>`, the files it keeps by id at `<its root>/project/_blobs/<id>.<ext>`.
63
63
  - `check <root>` → `problems` (0: done); when any, `issues`, each a `file` under `project/` and its fix; `designSystemNotes` when an install looks incomplete; it exits 1 while problems remain. It reads the files only: whether scripts run and how the layout looks is not checked.
64
+ - `ds-sync <root> [<folder>] [--check]` → an installed design system is a copy, and a copy never changes by itself. This compares each file of `project/ds/<folder>/` with the same file in the system's own folder (the record's `artifact`, a path on this machine; a system installed from claude.ai has none, and `--from <folder>` names one), copies the ones that differ byte for byte, and sets the record's `copiedAt`. `--check` only lists them (`changed`, and `goneFromSource` for a file the system dropped, which stays in the copy) and exits 1 while a copy is behind. Files the system gained are not copied: compare its README with the copy. A relative `artifact` is looked for from the working directory, then the artifact's folder and the folders above it. `check` notes a copy that is behind in `designSystemNotes`, and the Theme tab says so beside "View … design system" with a Sync button (the owner's), or says its source folder wasn't found.
64
65
  - `open <root>` → `{url, pid, stop, …}`: starts the editor page on this machine in the background (once; later calls reuse it, `reused: true`) and prints its address, 127.0.0.1:4720 (the next free port up when 4720 is taken; `--port n` starts from n, `--port 0` takes any). `stop <root>` ends it (`serve <root>` runs it in the foreground instead). The page saves the user's edits into the same files, and shows yours as soon as you write them. `.artifact-server.json` and `.artifact-server.log` beside `project/` are its own: leave them; so is `comments.json` (the comments, see Comments), which only the comment commands below change.
65
66
  - `share <root>` (or `open <root> --lan`): opens the page to the local network and prints a link per role (Sharing, below).
66
67
  - `archive <root> [--out file.zip|dir]` → `{file, files, bytes, open}`: the whole folder, history and comments too, as one zip beside it (`--out`: another file or folder), to back it up, move it to another computer or send it to someone; unzipped anywhere, `open` carries on. The owner's Download folder button in the page (between History and Share) does the same. The server's own files and the share links stay out.
@@ -135,7 +136,7 @@ The reference pages address the appifacts-design skill's files; their FORMAT rul
135
136
  - an Artifact call (`publish`, `url`, `root`, `file_path`, `files`) → writing those files at `<root>/project/<path>` with your file tool; a `files` entry set to `null` → deleting that file; "ONE Artifact call", "in the same call" → the same message (still one artboard per message while Creating, step 6); a refusal because someone saved meanwhile → nothing refuses here: read a file again right before you change it.
136
137
  - `read` (a file, on a url) → reading `<root>/project/<path>`; an asset id → `project/assets/<id>.<ext>` (a design system's: `<its root>/project/_blobs/<id>.<ext>`); `list` with `scope: "files"` → listing the system's `project/` folder. An upload (`publish` with `asset:true`, `upload_asset`, `design_import_asset` in a system's cards) → `upload`; `list` with `type` "Design System" → `list`.
137
138
  - a design system's address or url (`https://<host>/artifact/<id>`, its host rule) → its root folder, as Creating's step 4 says; its version id → `null`.
138
- - copying a system's files (`{"artifact","path"}` entries, "Sending the bytes", a font kept by id) → `cp` each file from `<its root>/project/<path>` to `<root>/project/ds/<folder>/<path>` (paths quoted; a file it keeps by id: `upload` it from its `_blobs/` folder instead); never retype a file or pass its content through a shell.
139
+ - copying a system's files (`{"artifact","path"}` entries, "Sending the bytes", a font kept by id) → `cp` each file from `<its root>/project/<path>` to `<root>/project/ds/<folder>/<path>` (paths quoted; a file it keeps by id: `upload` it from its `_blobs/` folder instead); never retype a file or pass its content through a shell. Update (the reference's "Update" paragraph) → `ds-sync <root> <folder>` copies what changed in one call and stamps the record; an update that removes or renames a file still needs the artboards' tags changed.
139
140
  - an `<artifact-view-context>` block (view-state.md) → never arrives here: ask which artboard the user means.
140
141
  - the link card for an artifact made from this canvas (make-from-this.md) → it needs that artifact's claude.ai link, and there is none here: add no card, and say in your reply where the new artifact's folder is.
141
142
 
@@ -2,9 +2,9 @@
2
2
  // claude-design: local Artifact tool (prebuilt; do not edit).
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import 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";
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), ...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")));