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