dsh-claude-design-system 2.8.2 → 2.9.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 +3 -2
- package/lib/client.js +1126 -152
- package/lib/index.js +311 -27
- package/package.json +1 -1
- package/skills/claude-design-system/SKILL.md +1 -0
- package/skills/claude-design-system/scripts/artifact.mjs +22 -0
- package/skills/claude-design-system/scripts/server.mjs +23 -1
package/lib/index.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
// dsh-claude-design-system 2.
|
|
1
|
+
// dsh-claude-design-system 2.9.0: 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. Not for a free-form HTML style guide, UI kit or token page.", comments: true }] };
|
|
3
3
|
|
|
4
4
|
import { spawn as spawn2 } from "node:child_process";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { statSync } from "node:fs";
|
|
6
|
+
import { statSync as statSync2 } from "node:fs";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { basename as basename2, delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
|
|
8
|
+
import { basename as basename2, delimiter, dirname as dirname2, 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";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
-
import { basename, join } from "node:path";
|
|
15
|
+
import { basename, dirname, join, parse } from "node:path";
|
|
16
16
|
|
|
17
|
-
var
|
|
18
|
-
var
|
|
17
|
+
var base = (skill) => `/api/baoyu-design.${skill}.artifacts`;
|
|
18
|
+
var listPath = base;
|
|
19
|
+
var openPath = (skill) => `${base(skill)}.open`;
|
|
20
|
+
var previewPath = (skill) => `${base(skill)}.preview`;
|
|
21
|
+
var releasePath = (skill) => `${base(skill)}.release`;
|
|
22
|
+
var thumbnailPath = (skill) => `${base(skill)}.thumbnail`;
|
|
23
|
+
var updatePath = (skill) => `${base(skill)}.update`;
|
|
24
|
+
var deletePath = (skill) => `${base(skill)}.delete`;
|
|
25
|
+
var restorePath = (skill) => `${base(skill)}.restore`;
|
|
19
26
|
|
|
20
27
|
var TITLES = {
|
|
21
28
|
"claude-slides": ["project/deck.json", "title"],
|
|
@@ -26,6 +33,18 @@ var TITLES = {
|
|
|
26
33
|
"claude-motion": ["db/film/meta.json", "title"]
|
|
27
34
|
};
|
|
28
35
|
var OPEN_TIMEOUT_MS = 3e4;
|
|
36
|
+
var TITLE_MAX = 200;
|
|
37
|
+
var EDITOR_FILES = [".artifact-server.json", ".artifact-server.log", ".sharing.json", ".gitignore", ".DS_Store", "comments.json", "history", "exports"];
|
|
38
|
+
var FOLDER = {
|
|
39
|
+
"claude-slides": ["project"],
|
|
40
|
+
"claude-design": ["project"],
|
|
41
|
+
"claude-design-system": ["project"],
|
|
42
|
+
"claude-dashboard": ["db", "_blobs", "src"],
|
|
43
|
+
"claude-docs": ["doc.json", "tabs", "_blobs", ".sessions.json"],
|
|
44
|
+
"claude-motion": ["db", "_blobs", "src"]
|
|
45
|
+
};
|
|
46
|
+
var NOT_CONTENT = /* @__PURE__ */ new Set([...EDITOR_FILES, ".sessions.json", ".git", "node_modules"]);
|
|
47
|
+
var STAMP_MAX_ENTRIES = 2e4;
|
|
29
48
|
function dshHome() {
|
|
30
49
|
const set = process.env.DSH_HOME?.trim();
|
|
31
50
|
return set ? set : join(homedir(), ".dsh");
|
|
@@ -33,8 +52,15 @@ function dshHome() {
|
|
|
33
52
|
function artifactsDir() {
|
|
34
53
|
return join(dshHome(), "baoyu-design", "artifacts");
|
|
35
54
|
}
|
|
55
|
+
var folderHash = (root) => createHash("sha256").update(root).digest("hex").slice(0, 24);
|
|
36
56
|
function recordFile(root) {
|
|
37
|
-
return join(artifactsDir(), `${
|
|
57
|
+
return join(artifactsDir(), `${folderHash(root)}.json`);
|
|
58
|
+
}
|
|
59
|
+
function thumbnailFile(root) {
|
|
60
|
+
return join(dshHome(), "baoyu-design", "thumbnails", `${folderHash(root)}.jpg`);
|
|
61
|
+
}
|
|
62
|
+
function trashDir() {
|
|
63
|
+
return join(dshHome(), "baoyu-design", "trash");
|
|
38
64
|
}
|
|
39
65
|
function artifactTitle(skill, root) {
|
|
40
66
|
const at = TITLES[skill];
|
|
@@ -59,9 +85,20 @@ function readRecord(file) {
|
|
|
59
85
|
return null;
|
|
60
86
|
}
|
|
61
87
|
}
|
|
88
|
+
function writeWhole(file, data) {
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
91
|
+
writeFileSync(tmp, data);
|
|
92
|
+
renameSync(tmp, file);
|
|
93
|
+
}
|
|
94
|
+
function writeRecord(record) {
|
|
95
|
+
writeWhole(recordFile(record.root), `${JSON.stringify(record, null, 2)}
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
62
98
|
function recordOpened(skill, plugin, sessionId, root, now = /* @__PURE__ */ new Date()) {
|
|
63
99
|
const file = recordFile(root);
|
|
64
100
|
const before = readRecord(file);
|
|
101
|
+
const mine = before?.root === root ? before : null;
|
|
65
102
|
const record = {
|
|
66
103
|
v: 1,
|
|
67
104
|
root,
|
|
@@ -70,15 +107,55 @@ function recordOpened(skill, plugin, sessionId, root, now = /* @__PURE__ */ new
|
|
|
70
107
|
sessionId,
|
|
71
108
|
title: artifactTitle(skill, root) ?? before?.title ?? basename(root),
|
|
72
109
|
openedAt: now.toISOString(),
|
|
73
|
-
firstOpenedAt:
|
|
110
|
+
firstOpenedAt: mine ? mine.firstOpenedAt ?? mine.openedAt : now.toISOString(),
|
|
111
|
+
...mine?.pinned ? { pinned: true } : {},
|
|
112
|
+
...mine?.thumbStamp ? { thumbStamp: mine.thumbStamp } : {}
|
|
74
113
|
};
|
|
75
|
-
|
|
76
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
77
|
-
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}
|
|
78
|
-
`);
|
|
79
|
-
renameSync(tmp, file);
|
|
114
|
+
writeRecord(record);
|
|
80
115
|
return record;
|
|
81
116
|
}
|
|
117
|
+
function contentStamp(root) {
|
|
118
|
+
let newest = 0;
|
|
119
|
+
let entries = 0;
|
|
120
|
+
const walk = (dir, top) => {
|
|
121
|
+
let names;
|
|
122
|
+
try {
|
|
123
|
+
names = readdirSync(dir);
|
|
124
|
+
} catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const name2 of names) {
|
|
128
|
+
if (++entries > STAMP_MAX_ENTRIES) return;
|
|
129
|
+
if (name2.endsWith(".tmp") || top && NOT_CONTENT.has(name2)) continue;
|
|
130
|
+
const path = join(dir, name2);
|
|
131
|
+
try {
|
|
132
|
+
const stat = statSync(path);
|
|
133
|
+
if (stat.isDirectory()) walk(path, false);
|
|
134
|
+
else if (stat.mtimeMs > newest) newest = stat.mtimeMs;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(root, true);
|
|
140
|
+
return newest > 0 ? new Date(newest).toISOString() : "";
|
|
141
|
+
}
|
|
142
|
+
function isShared(root) {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(readFileSync(join(root, ".sharing.json"), "utf8")).lan === true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isDeletable(skill, root) {
|
|
150
|
+
const own = FOLDER[skill];
|
|
151
|
+
if (own === void 0) return false;
|
|
152
|
+
if (root === homedir() || root === parse(root).root) return false;
|
|
153
|
+
try {
|
|
154
|
+
return readdirSync(root).every((name2) => own.includes(name2) || EDITOR_FILES.includes(name2) || name2.endsWith(".tmp"));
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
82
159
|
function listArtifacts() {
|
|
83
160
|
let names;
|
|
84
161
|
try {
|
|
@@ -97,7 +174,13 @@ function listArtifacts() {
|
|
|
97
174
|
plugin: record.plugin,
|
|
98
175
|
sessionId: record.sessionId,
|
|
99
176
|
title: artifactTitle(record.skill, record.root) ?? record.title,
|
|
100
|
-
openedAt: record.openedAt
|
|
177
|
+
openedAt: record.openedAt,
|
|
178
|
+
firstOpenedAt: record.firstOpenedAt ?? record.openedAt,
|
|
179
|
+
updatedAt: contentStamp(record.root) || record.openedAt,
|
|
180
|
+
pinned: record.pinned === true,
|
|
181
|
+
shared: isShared(record.root),
|
|
182
|
+
deletable: isDeletable(record.skill, record.root),
|
|
183
|
+
thumbnail: record.thumbStamp !== void 0 && existsSync(thumbnailFile(record.root)) ? record.thumbStamp : null
|
|
101
184
|
});
|
|
102
185
|
}
|
|
103
186
|
return views.sort((a, b) => a.openedAt < b.openedAt ? 1 : a.openedAt > b.openedAt ? -1 : 0);
|
|
@@ -114,31 +197,109 @@ function scriptEnv(node) {
|
|
|
114
197
|
}
|
|
115
198
|
return env;
|
|
116
199
|
}
|
|
117
|
-
function
|
|
200
|
+
function runScript(node, script, args, input) {
|
|
118
201
|
return new Promise((resolve, reject) => {
|
|
119
|
-
const child = spawn(node.path, [script,
|
|
202
|
+
const child = spawn(node.path, [script, ...args], { env: scriptEnv(node), stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "pipe"], windowsHide: true });
|
|
120
203
|
let stdout = "";
|
|
121
204
|
let stderr = "";
|
|
122
205
|
const timer = setTimeout(() => {
|
|
123
206
|
child.kill();
|
|
124
|
-
reject(new Error(
|
|
207
|
+
reject(new Error(`${args[0]} did not finish in time`));
|
|
125
208
|
}, OPEN_TIMEOUT_MS);
|
|
126
209
|
child.stdout?.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
|
|
127
210
|
child.stderr?.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
|
|
211
|
+
child.stdin?.end(input);
|
|
128
212
|
child.on("error", (error) => {
|
|
129
213
|
clearTimeout(timer);
|
|
130
214
|
reject(error);
|
|
131
215
|
});
|
|
132
216
|
child.on("close", (code) => {
|
|
133
217
|
clearTimeout(timer);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const message = typeof failure?.message === "string" ? failure.message : stderr.trim() || `open exited with ${code ?? "a signal"}`;
|
|
218
|
+
if (code === 0) return resolve(firstJson(stdout));
|
|
219
|
+
const failure = firstJson(stderr) ?? firstJson(stdout);
|
|
220
|
+
const message = typeof failure?.message === "string" ? failure.message : stderr.trim() || `${args[0]} exited with ${code ?? "a signal"}`;
|
|
138
221
|
reject(new Error(message));
|
|
139
222
|
});
|
|
140
223
|
});
|
|
141
224
|
}
|
|
225
|
+
async function runOpen(node, script, root) {
|
|
226
|
+
const out = await runScript(node, script, ["open", root]);
|
|
227
|
+
if (typeof out?.url !== "string") throw new Error("open printed no address");
|
|
228
|
+
return { url: out.url, reused: out.reused === true };
|
|
229
|
+
}
|
|
230
|
+
async function renameArtifact(node, script, record, title) {
|
|
231
|
+
const { root, skill } = record;
|
|
232
|
+
const at = TITLES[skill];
|
|
233
|
+
if (at === void 0) throw new Error(`no title to change for ${skill}`);
|
|
234
|
+
if (skill === "claude-dashboard" || skill === "claude-motion") {
|
|
235
|
+
const path = at[0].replace(/^db\//, "").replace(/\.json$/, "");
|
|
236
|
+
await runScript(node, script, ["write-db", root, "-"], JSON.stringify([{ op: "update", path, data: { [at[1]]: title } }]));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const file = join(root, at[0]);
|
|
240
|
+
const index = JSON.parse(readFileSync(file, "utf8"));
|
|
241
|
+
if (skill === "claude-docs") {
|
|
242
|
+
await runScript(node, script, ["update", root, JSON.stringify({ ref: { object: "project", id: String(index.id) }, payload: { name: title } })]);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
writeWhole(file, `${JSON.stringify({ ...index, [at[1]]: title }, null, 2)}
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
function cleanTitle(raw) {
|
|
249
|
+
if (typeof raw !== "string") return null;
|
|
250
|
+
const title = raw.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX);
|
|
251
|
+
return title || null;
|
|
252
|
+
}
|
|
253
|
+
function moveFolder(from, to) {
|
|
254
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
255
|
+
try {
|
|
256
|
+
renameSync(from, to);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error.code !== "EXDEV") throw error;
|
|
259
|
+
cpSync(from, to, { recursive: true, preserveTimestamps: true });
|
|
260
|
+
rmSync(from, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var TRASH_ID = /^[0-9a-f]{24}-\d+$/;
|
|
264
|
+
function trashArtifact(record) {
|
|
265
|
+
const id = `${folderHash(record.root)}-${Date.now()}`;
|
|
266
|
+
const entry = join(trashDir(), id);
|
|
267
|
+
try {
|
|
268
|
+
for (const name2 of readdirSync(trashDir())) {
|
|
269
|
+
if (TRASH_ID.test(name2) && !existsSync(join(trashDir(), name2, "folder"))) rmSync(join(trashDir(), name2), { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
mkdirSync(entry, { recursive: true });
|
|
274
|
+
writeFileSync(join(entry, "record.json"), `${JSON.stringify(record, null, 2)}
|
|
275
|
+
`);
|
|
276
|
+
const thumbnail = thumbnailFile(record.root);
|
|
277
|
+
if (existsSync(thumbnail)) renameSync(thumbnail, join(entry, "thumbnail.jpg"));
|
|
278
|
+
const moved = isDeletable(record.skill, record.root);
|
|
279
|
+
const path = join(entry, "folder", basename(record.root));
|
|
280
|
+
if (moved) moveFolder(record.root, path);
|
|
281
|
+
rmSync(recordFile(record.root), { force: true });
|
|
282
|
+
return { trash: id, moved, path: moved ? path : record.root };
|
|
283
|
+
}
|
|
284
|
+
function restoreArtifact(id) {
|
|
285
|
+
if (!TRASH_ID.test(id)) throw new Error("not a trash entry");
|
|
286
|
+
const entry = join(trashDir(), id);
|
|
287
|
+
const record = readRecord(join(entry, "record.json"));
|
|
288
|
+
if (record === null) throw new Error("nothing to put back");
|
|
289
|
+
const folder = join(entry, "folder", basename(record.root));
|
|
290
|
+
if (existsSync(folder)) {
|
|
291
|
+
if (existsSync(record.root)) throw new Error(`${record.root} is taken`);
|
|
292
|
+
moveFolder(folder, record.root);
|
|
293
|
+
}
|
|
294
|
+
writeRecord(record);
|
|
295
|
+
const thumbnail = join(entry, "thumbnail.jpg");
|
|
296
|
+
if (existsSync(thumbnail)) {
|
|
297
|
+
mkdirSync(dirname(thumbnailFile(record.root)), { recursive: true });
|
|
298
|
+
renameSync(thumbnail, thumbnailFile(record.root));
|
|
299
|
+
}
|
|
300
|
+
rmSync(entry, { recursive: true, force: true });
|
|
301
|
+
return record;
|
|
302
|
+
}
|
|
142
303
|
function firstJson(text) {
|
|
143
304
|
const start = text.indexOf("{");
|
|
144
305
|
const end = text.lastIndexOf("}");
|
|
@@ -177,6 +338,129 @@ function registerRoutes(routes, skill, opened) {
|
|
|
177
338
|
}
|
|
178
339
|
}
|
|
179
340
|
});
|
|
341
|
+
const rootOf = (request) => new URL(request.url).searchParams.get("root") ?? "";
|
|
342
|
+
const mine = (root) => {
|
|
343
|
+
const record = root ? findArtifact(root) : null;
|
|
344
|
+
return record?.skill === skill.name && existsSync(root) ? record : null;
|
|
345
|
+
};
|
|
346
|
+
const notMine = () => json({ error: "not a recorded artifact of this skill" }, 404);
|
|
347
|
+
const started = /* @__PURE__ */ new Map();
|
|
348
|
+
routes.register({
|
|
349
|
+
path: previewPath(skill.name),
|
|
350
|
+
methods: ["POST"],
|
|
351
|
+
requestBody: "buffered",
|
|
352
|
+
fetch: async (request) => {
|
|
353
|
+
const root = rootOf(request);
|
|
354
|
+
const record = mine(root);
|
|
355
|
+
if (record === null) return notMine();
|
|
356
|
+
try {
|
|
357
|
+
const { url, reused } = await runOpen(skill.node, skill.script, root);
|
|
358
|
+
if (!reused) started.set(root, record.openedAt);
|
|
359
|
+
return json({ url, reused });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return json({ error: error.message }, 500);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
routes.register({
|
|
366
|
+
path: releasePath(skill.name),
|
|
367
|
+
methods: ["POST"],
|
|
368
|
+
requestBody: "buffered",
|
|
369
|
+
fetch: async (request) => {
|
|
370
|
+
const root = rootOf(request);
|
|
371
|
+
const since = started.get(root);
|
|
372
|
+
started.delete(root);
|
|
373
|
+
const record = mine(root);
|
|
374
|
+
if (record === null) return notMine();
|
|
375
|
+
if (since === void 0 || since !== record.openedAt || new URL(request.url).searchParams.get("keep") === "1") return json({ stopped: false });
|
|
376
|
+
try {
|
|
377
|
+
await runScript(skill.node, skill.script, ["stop", root]);
|
|
378
|
+
return json({ stopped: true });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return json({ error: error.message }, 500);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
routes.register({
|
|
385
|
+
path: thumbnailPath(skill.name),
|
|
386
|
+
methods: ["GET", "POST"],
|
|
387
|
+
requestBody: "buffered",
|
|
388
|
+
fetch: async (request) => {
|
|
389
|
+
const root = rootOf(request);
|
|
390
|
+
const record = root ? findArtifact(root) : null;
|
|
391
|
+
if (record === null) return json({ error: "not a recorded artifact" }, 404);
|
|
392
|
+
if (request.method === "GET") {
|
|
393
|
+
try {
|
|
394
|
+
const bytes2 = readFileSync(thumbnailFile(root));
|
|
395
|
+
return new Response(bytes2, { headers: { "content-type": "image/jpeg", "cache-control": "private, max-age=31536000, immutable" } });
|
|
396
|
+
} catch {
|
|
397
|
+
return json({ error: "no thumbnail yet" }, 404);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
401
|
+
if (bytes.length < 3 || bytes[0] !== 255 || bytes[1] !== 216) return json({ error: "a thumbnail is a JPEG" }, 400);
|
|
402
|
+
writeWhole(thumbnailFile(root), bytes);
|
|
403
|
+
const stamp = contentStamp(root) || record.openedAt;
|
|
404
|
+
writeRecord({ ...findArtifact(root) ?? record, thumbStamp: stamp });
|
|
405
|
+
return json({ thumbnail: stamp, updatedAt: stamp });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
routes.register({
|
|
409
|
+
path: updatePath(skill.name),
|
|
410
|
+
methods: ["POST"],
|
|
411
|
+
requestBody: "buffered",
|
|
412
|
+
fetch: async (request) => {
|
|
413
|
+
const root = rootOf(request);
|
|
414
|
+
const record = mine(root);
|
|
415
|
+
if (record === null) return notMine();
|
|
416
|
+
const patch = await request.json().catch(() => null);
|
|
417
|
+
if (patch === null || typeof patch !== "object") return json({ error: "not a JSON object" }, 400);
|
|
418
|
+
try {
|
|
419
|
+
if (patch.title !== void 0) {
|
|
420
|
+
const title = cleanTitle(patch.title);
|
|
421
|
+
if (title === null) return json({ error: "a title cannot be empty" }, 400);
|
|
422
|
+
await renameArtifact(skill.node, skill.script, record, title);
|
|
423
|
+
}
|
|
424
|
+
const now = findArtifact(root) ?? record;
|
|
425
|
+
const next = { ...now, title: artifactTitle(skill.name, root) ?? now.title };
|
|
426
|
+
if (patch.pinned === true) next.pinned = true;
|
|
427
|
+
else if (patch.pinned === false) delete next.pinned;
|
|
428
|
+
writeRecord(next);
|
|
429
|
+
return json({ title: next.title, pinned: next.pinned === true });
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return json({ error: error.message }, 500);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
routes.register({
|
|
436
|
+
path: deletePath(skill.name),
|
|
437
|
+
methods: ["POST"],
|
|
438
|
+
requestBody: "buffered",
|
|
439
|
+
fetch: async (request) => {
|
|
440
|
+
const root = rootOf(request);
|
|
441
|
+
const record = mine(root);
|
|
442
|
+
if (record === null) return notMine();
|
|
443
|
+
try {
|
|
444
|
+
if (isDeletable(skill.name, root)) await runScript(skill.node, skill.script, ["stop", root]).catch(() => null);
|
|
445
|
+
started.delete(root);
|
|
446
|
+
return json(trashArtifact(record));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return json({ error: error.message }, 500);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
routes.register({
|
|
453
|
+
path: restorePath(skill.name),
|
|
454
|
+
methods: ["POST"],
|
|
455
|
+
requestBody: "buffered",
|
|
456
|
+
fetch: (request) => {
|
|
457
|
+
try {
|
|
458
|
+
return Promise.resolve(json({ root: restoreArtifact(new URL(request.url).searchParams.get("trash") ?? "").root }));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return Promise.resolve(json({ error: error.message }, 400));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
180
464
|
}
|
|
181
465
|
|
|
182
466
|
var PLUGIN = define_PLUGIN_default;
|
|
@@ -186,7 +470,7 @@ var BUNDLED_SKILL_RANK = 600;
|
|
|
186
470
|
var packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
187
471
|
function isFile(path) {
|
|
188
472
|
try {
|
|
189
|
-
return
|
|
473
|
+
return statSync2(path).isFile();
|
|
190
474
|
} catch {
|
|
191
475
|
return false;
|
|
192
476
|
}
|
|
@@ -204,10 +488,10 @@ function resolveNode(config = {}) {
|
|
|
204
488
|
const roots = [];
|
|
205
489
|
const resourcesPath = process.resourcesPath;
|
|
206
490
|
if (typeof resourcesPath === "string") roots.push(resourcesPath);
|
|
207
|
-
let dir =
|
|
491
|
+
let dir = dirname2(process.execPath);
|
|
208
492
|
for (let i = 0; i < 6; i++) {
|
|
209
493
|
roots.push(join2(dir, "Resources"), join2(dir, "resources"));
|
|
210
|
-
const parent =
|
|
494
|
+
const parent = dirname2(dir);
|
|
211
495
|
if (parent === dir) break;
|
|
212
496
|
dir = parent;
|
|
213
497
|
}
|
|
@@ -355,7 +639,7 @@ function openedEditor(script, exec, result) {
|
|
|
355
639
|
function ranScript(script, command, stop) {
|
|
356
640
|
if (typeof stop !== "string") return command.includes(script);
|
|
357
641
|
if (stop.includes(script)) return true;
|
|
358
|
-
const folder = basename2(
|
|
642
|
+
const folder = basename2(dirname2(dirname2(script)));
|
|
359
643
|
return new RegExp(`[/\\\\]${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[/\\\\]scripts[/\\\\]artifact\\.mjs`).test(stop);
|
|
360
644
|
}
|
|
361
645
|
var bound = (text) => text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX - 1)}…`;
|
|
@@ -554,7 +838,7 @@ function apply(ctx, config = {}) {
|
|
|
554
838
|
const skill = skills.get(candidate.name);
|
|
555
839
|
if (skill === void 0) throw new Error(`${PLUGIN.name}: no skill named ${candidate.name}`);
|
|
556
840
|
const raw = await readFile(candidate.path, { encoding: "utf8", signal: options?.signal });
|
|
557
|
-
const directory =
|
|
841
|
+
const directory = dirname2(candidate.path);
|
|
558
842
|
return { ...summary, content: skillBody(raw, candidate.path) + runtimeSection(skill, directory, node) };
|
|
559
843
|
}
|
|
560
844
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-claude-design-system",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
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",
|
|
@@ -164,6 +164,7 @@ leave alone stays as it is, and removing a file is deleting it. The rest goes th
|
|
|
164
164
|
- `check <root>` → `problems` (0: done); `issues` (each a `file` under `project/` and its fix) and `notes`, each when any; it exits 1 while any problem remains. The generated files and whether previews render are not checked.
|
|
165
165
|
- `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}`.
|
|
166
166
|
- `open <root>` → `{url, pid, stop, …}`: starts the page on this machine in the background (once; later calls reuse it, `reused: true`) and prints its address, `http://127.0.0.1:4730/design-system/<id>/`. All the systems open on this computer share one server and port (4730, the next free port up when that is taken; `--port n` looks for it at n and starts from n, `--port 0` runs a server of its own on any port), each at its own path, which stays the same from one start to the next. `stop <root>` takes this one off, and the server ends with the last (`serve <root>` runs it in the foreground instead). The page saves the user's edits (and the generated files) into the same folder, 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.
|
|
167
|
+
`NETWORK_BLOCKED` from `open` means your commands run in a sandbox without local network. Run `open`, `share` and `comments <root> --wait` with local network allowed, outside the sandbox, asking for that permission if your commands need approval; in the sandbox `comments --wait` says `NO_SERVER` even while the page is open. Commands that only read and write files work in the sandbox.
|
|
167
168
|
- `share <root>` (or `open <root> --lan`): opens the page to the local network and prints a link per role (Sharing, below).
|
|
168
169
|
- `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.
|
|
169
170
|
|
|
@@ -2470,6 +2470,7 @@ async function updateComments(root, change) {
|
|
|
2470
2470
|
}
|
|
2471
2471
|
|
|
2472
2472
|
import fs8 from "node:fs";
|
|
2473
|
+
import net from "node:net";
|
|
2473
2474
|
import path10 from "node:path";
|
|
2474
2475
|
var STATE_FILE = ".artifact-server.json";
|
|
2475
2476
|
var LOG_FILE = ".artifact-server.log";
|
|
@@ -2501,6 +2502,26 @@ async function runningServer(root, kind, probe) {
|
|
|
2501
2502
|
}
|
|
2502
2503
|
var DEFAULT_PORTS = { slides: 4710, design: 4720, "design-system": 4730, dashboard: 4740, docs: 4750, motion: 4760 };
|
|
2503
2504
|
var PORT_TRIES = 100;
|
|
2505
|
+
var NetworkBlocked = class extends Error {
|
|
2506
|
+
code = "NETWORK_BLOCKED";
|
|
2507
|
+
constructor() {
|
|
2508
|
+
super(
|
|
2509
|
+
"A sandbox blocks local network here (127.0.0.1), so the server cannot start and no port is really in use. Run this same command with local network allowed, outside the sandbox, asking for that permission if your commands need approval."
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
function loopbackBlocked(port) {
|
|
2514
|
+
return new Promise((resolve) => {
|
|
2515
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
2516
|
+
const done = (blocked) => {
|
|
2517
|
+
socket.destroy();
|
|
2518
|
+
resolve(blocked);
|
|
2519
|
+
};
|
|
2520
|
+
socket.setTimeout(300, () => done(false));
|
|
2521
|
+
socket.once("connect", () => done(false));
|
|
2522
|
+
socket.once("error", (error) => done(error.code === "EPERM" || error.code === "EACCES"));
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2504
2525
|
function serverOrigin(host, port) {
|
|
2505
2526
|
const shown = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
|
|
2506
2527
|
return `http://${shown}:${port}`;
|
|
@@ -2700,6 +2721,7 @@ async function leaveHub(state) {
|
|
|
2700
2721
|
return true;
|
|
2701
2722
|
}
|
|
2702
2723
|
async function openOnHub(call) {
|
|
2724
|
+
if (await loopbackBlocked(call.port || 1)) throw new NetworkBlocked();
|
|
2703
2725
|
const joined = await joinHub(call);
|
|
2704
2726
|
if (joined) return { state: joined };
|
|
2705
2727
|
const logFile = path12.join(call.root, LOG_FILE);
|
|
@@ -838,6 +838,26 @@ var PortsTaken = class extends Error {
|
|
|
838
838
|
this.to = to;
|
|
839
839
|
}
|
|
840
840
|
};
|
|
841
|
+
var NetworkBlocked = class extends Error {
|
|
842
|
+
code = "NETWORK_BLOCKED";
|
|
843
|
+
constructor() {
|
|
844
|
+
super(
|
|
845
|
+
"A sandbox blocks local network here (127.0.0.1), so the server cannot start and no port is really in use. Run this same command with local network allowed, outside the sandbox, asking for that permission if your commands need approval."
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function loopbackBlocked(port) {
|
|
850
|
+
return new Promise((resolve) => {
|
|
851
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
852
|
+
const done = (blocked) => {
|
|
853
|
+
socket.destroy();
|
|
854
|
+
resolve(blocked);
|
|
855
|
+
};
|
|
856
|
+
socket.setTimeout(300, () => done(false));
|
|
857
|
+
socket.once("connect", () => done(false));
|
|
858
|
+
socket.once("error", (error) => done(error.code === "EPERM" || error.code === "EACCES"));
|
|
859
|
+
});
|
|
860
|
+
}
|
|
841
861
|
var isBenchmarkRange = (address) => /^198\.1[89]\./.test(address);
|
|
842
862
|
function isTailnet(address) {
|
|
843
863
|
const m = /^100\.(\d{1,3})\./.exec(address);
|
|
@@ -884,6 +904,7 @@ async function answersAnyPort(address) {
|
|
|
884
904
|
return port > 0 && answers(address, port);
|
|
885
905
|
}
|
|
886
906
|
async function listenFree(server2, from, host, first) {
|
|
907
|
+
if (await loopbackBlocked(first || from || 1)) throw new NetworkBlocked();
|
|
887
908
|
if (from === 0) return listen(server2, 0, host);
|
|
888
909
|
const to = Math.min(from + PORT_TRIES - 1, 65535);
|
|
889
910
|
const range = Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
@@ -5048,7 +5069,8 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
5048
5069
|
if (code >= 0) process.exitCode = code;
|
|
5049
5070
|
},
|
|
5050
5071
|
(error) => {
|
|
5051
|
-
const
|
|
5072
|
+
const given = error.code;
|
|
5073
|
+
const code = given === "PORT_IN_USE" || given === "NETWORK_BLOCKED" ? given : "SERVER_FAILED";
|
|
5052
5074
|
process.stderr.write(`${JSON.stringify({ error: code, message: errorText(error) })}
|
|
5053
5075
|
`);
|
|
5054
5076
|
process.exitCode = 2;
|