dsh-claude-design 2.4.1 → 2.5.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/lib/client.js +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/skills/claude-design/SKILL.md +13 -1
- package/skills/claude-design/scripts/artifact.mjs +316 -78
- package/skills/claude-design/scripts/editor/assets/{index-9BhYeCfK.js → index-Cs8iq2HP.js} +2 -2
- package/skills/claude-design/scripts/editor/index.html +1 -1
- package/skills/claude-design/scripts/editor/share-ui.js +83 -0
- package/skills/claude-design/scripts/server.mjs +818 -96
|
@@ -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
|
|
5
|
+
import fs10 from "node:fs";
|
|
6
6
|
import fsp2 from "node:fs/promises";
|
|
7
|
-
import
|
|
7
|
+
import path12 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}$/;
|
|
@@ -1477,15 +1477,15 @@ function normalizeCssLength(value, allowFullRadius) {
|
|
|
1477
1477
|
return CSS_LENGTH_PATTERN.test(normalized) ? normalized : void 0;
|
|
1478
1478
|
}
|
|
1479
1479
|
var colorAliasTarget = (value) => value === void 0 ? void 0 : COLOR_ALIAS_PATTERN.exec(value)?.[1];
|
|
1480
|
-
function explainColorAliasError(
|
|
1481
|
-
const seen = /* @__PURE__ */ new Set([
|
|
1482
|
-
let current =
|
|
1480
|
+
function explainColorAliasError(token2, themeId, baseThemeId, byName) {
|
|
1481
|
+
const seen = /* @__PURE__ */ new Set([token2.name]);
|
|
1482
|
+
let current = token2;
|
|
1483
1483
|
for (let index = 0; index <= TOKENS_LIMITS.colorAliasDepth; index++) {
|
|
1484
1484
|
const target = colorAliasTarget(current.value[themeId] ?? current.value[baseThemeId]);
|
|
1485
1485
|
if (target === void 0) return void 0;
|
|
1486
1486
|
const entry = byName.get(target);
|
|
1487
1487
|
if (entry === void 0) return { why: `${index === 0 ? "" : `reaches {${target}}, which `}names no color token`, own: index === 0 };
|
|
1488
|
-
if (seen.has(target)) return { why: "is a cycle", own: target ===
|
|
1488
|
+
if (seen.has(target)) return { why: "is a cycle", own: target === token2.name };
|
|
1489
1489
|
seen.add(target);
|
|
1490
1490
|
current = entry;
|
|
1491
1491
|
}
|
|
@@ -1496,30 +1496,30 @@ function removeInvalidColorAliases(themes, tokens, dropped) {
|
|
|
1496
1496
|
for (; ; ) {
|
|
1497
1497
|
const byName = new Map(tokens.map((t) => [t.name, t]));
|
|
1498
1498
|
const items = [];
|
|
1499
|
-
for (const
|
|
1499
|
+
for (const token2 of tokens) {
|
|
1500
1500
|
for (const theme of themes) {
|
|
1501
|
-
if (colorAliasTarget(
|
|
1502
|
-
const error = explainColorAliasError(
|
|
1503
|
-
if (error !== void 0) items.push([
|
|
1501
|
+
if (colorAliasTarget(token2.value[theme.id]) === void 0) continue;
|
|
1502
|
+
const error = explainColorAliasError(token2, theme.id, baseThemeId, byName);
|
|
1503
|
+
if (error !== void 0) items.push([token2, theme.id, error.why, error.own]);
|
|
1504
1504
|
}
|
|
1505
1505
|
}
|
|
1506
1506
|
if (items.length === 0) return;
|
|
1507
1507
|
const anyOwn = items.some((item) => item[3]);
|
|
1508
|
-
for (const [
|
|
1508
|
+
for (const [token2, themeId, why, own] of items) {
|
|
1509
1509
|
if (anyOwn && !own) continue;
|
|
1510
|
-
dropped.push(`color ${
|
|
1511
|
-
delete
|
|
1510
|
+
dropped.push(`color ${token2.name} [${themeId}]: ${String(token2.value[themeId])} ${why}`);
|
|
1511
|
+
delete token2.value[themeId];
|
|
1512
1512
|
}
|
|
1513
1513
|
for (let index = 0; index < tokens.length; index++) {
|
|
1514
|
-
const
|
|
1515
|
-
if (Object.hasOwn(
|
|
1516
|
-
const theme = themes.find((t) => Object.hasOwn(
|
|
1514
|
+
const token2 = tokens[index];
|
|
1515
|
+
if (Object.hasOwn(token2.value, baseThemeId)) continue;
|
|
1516
|
+
const theme = themes.find((t) => Object.hasOwn(token2.value, t.id));
|
|
1517
1517
|
if (theme === void 0) {
|
|
1518
|
-
dropped.push(`color ${
|
|
1518
|
+
dropped.push(`color ${token2.name}: no valid value`);
|
|
1519
1519
|
tokens.splice(index--, 1);
|
|
1520
1520
|
} else {
|
|
1521
|
-
|
|
1522
|
-
delete
|
|
1521
|
+
token2.value[baseThemeId] = token2.value[theme.id];
|
|
1522
|
+
delete token2.value[theme.id];
|
|
1523
1523
|
}
|
|
1524
1524
|
}
|
|
1525
1525
|
}
|
|
@@ -1575,23 +1575,23 @@ function checkTokensText(text) {
|
|
|
1575
1575
|
problems.push('color has both "tokens" and a legacy "groups"; only "tokens" is read');
|
|
1576
1576
|
}
|
|
1577
1577
|
if (sourceColorTokens.length > TOKENS_LIMITS.colors) problems.push(`color tokens capped at ${TOKENS_LIMITS.colors}; the rest are ignored`);
|
|
1578
|
-
for (const
|
|
1578
|
+
for (const token2 of sourceColorTokens) {
|
|
1579
1579
|
if (colorTokens.length >= TOKENS_LIMITS.colors) break;
|
|
1580
|
-
if (!isObject(
|
|
1581
|
-
const name = typeof
|
|
1580
|
+
if (!isObject(token2)) continue;
|
|
1581
|
+
const name = typeof token2.name === "string" ? token2.name.trim().replace(/^--/, "") : "";
|
|
1582
1582
|
if (!TOKEN_NAME_PATTERN.test(name) || colorNames.has(name)) {
|
|
1583
1583
|
dropped.push(`color ${JSON.stringify(name)}: bad or duplicate name`);
|
|
1584
1584
|
continue;
|
|
1585
1585
|
}
|
|
1586
1586
|
const value = {};
|
|
1587
|
-
if (isObject(
|
|
1587
|
+
if (isObject(token2.value)) {
|
|
1588
1588
|
for (const theme of themes) {
|
|
1589
|
-
const normalized = Object.hasOwn(
|
|
1589
|
+
const normalized = Object.hasOwn(token2.value, theme.id) ? normalizeColorOrAlias(token2.value[theme.id]) : void 0;
|
|
1590
1590
|
if (normalized !== void 0) value[theme.id] = normalized;
|
|
1591
|
-
else if (Object.hasOwn(
|
|
1591
|
+
else if (Object.hasOwn(token2.value, theme.id)) dropped.push(`color ${name} [${theme.id}]: not a color value`);
|
|
1592
1592
|
}
|
|
1593
1593
|
} else {
|
|
1594
|
-
const normalized = normalizeColorOrAlias(
|
|
1594
|
+
const normalized = normalizeColorOrAlias(token2.value);
|
|
1595
1595
|
if (normalized !== void 0) value[baseThemeId] = normalized;
|
|
1596
1596
|
}
|
|
1597
1597
|
if (!Object.hasOwn(value, baseThemeId)) {
|
|
@@ -1676,10 +1676,10 @@ function checkTokensText(text) {
|
|
|
1676
1676
|
const tokens = asArray(isObject(input) ? input.tokens : void 0);
|
|
1677
1677
|
if (tokens.length > TOKENS_LIMITS.tokensPerFamily) problems.push(`${family} has ${tokens.length} tokens; at most ${TOKENS_LIMITS.tokensPerFamily} are read`);
|
|
1678
1678
|
let count = 0;
|
|
1679
|
-
for (const
|
|
1679
|
+
for (const token2 of tokens) {
|
|
1680
1680
|
if (count >= TOKENS_LIMITS.tokensPerFamily) break;
|
|
1681
|
-
if (!isObject(
|
|
1682
|
-
const name = typeof
|
|
1681
|
+
if (!isObject(token2)) continue;
|
|
1682
|
+
const name = typeof token2.name === "string" ? token2.name.trim().replace(/^--/, "") : "";
|
|
1683
1683
|
const other = familyOf.get(name);
|
|
1684
1684
|
if (!TOKEN_NAME_PATTERN.test(name) || other === family) {
|
|
1685
1685
|
dropped.push(`${family} ${JSON.stringify(name)}: bad or duplicate name`);
|
|
@@ -1689,7 +1689,7 @@ function checkTokensText(text) {
|
|
|
1689
1689
|
dropped.push(`${family} ${name}: a ${other} token already has that name (one --${name} across every family)`);
|
|
1690
1690
|
continue;
|
|
1691
1691
|
}
|
|
1692
|
-
if (normalize(
|
|
1692
|
+
if (normalize(token2.value, name) === void 0) {
|
|
1693
1693
|
dropped.push(`${family} ${name}: bad value`);
|
|
1694
1694
|
continue;
|
|
1695
1695
|
}
|
|
@@ -2337,7 +2337,10 @@ function parseDoc(text) {
|
|
|
2337
2337
|
const threads = data.threads.filter(
|
|
2338
2338
|
(t) => isRecord4(t) && typeof t.id === "string" && typeof t.anchor === "string" && typeof t.label === "string" && (t.status === "open" || t.status === "resolved") && Array.isArray(t.messages) && t.messages.every((m) => isRecord4(m) && typeof m.text === "string" && isRecord4(m.author) && (m.author.kind === "user" || m.author.kind === "agent"))
|
|
2339
2339
|
);
|
|
2340
|
-
for (const t of threads)
|
|
2340
|
+
for (const t of threads) {
|
|
2341
|
+
if (!isRecord4(t.location)) t.location = {};
|
|
2342
|
+
for (const m of t.messages) if (m.author.principal !== void 0 && typeof m.author.principal !== "string") delete m.author.principal;
|
|
2343
|
+
}
|
|
2341
2344
|
return { version: COMMENTS_VERSION, threads };
|
|
2342
2345
|
}
|
|
2343
2346
|
var formatDoc = (doc) => `${JSON.stringify(doc, null, 2)}
|
|
@@ -2496,6 +2499,206 @@ async function runningServer(root, kind, probe) {
|
|
|
2496
2499
|
return null;
|
|
2497
2500
|
}
|
|
2498
2501
|
}
|
|
2502
|
+
var DEFAULT_PORTS = { slides: 4710, design: 4720, "design-system": 4730, dashboard: 4740, docs: 4750, motion: 4760 };
|
|
2503
|
+
|
|
2504
|
+
import crypto from "node:crypto";
|
|
2505
|
+
import fs9 from "node:fs";
|
|
2506
|
+
import path11 from "node:path";
|
|
2507
|
+
var GUEST_ROLES = ["editor", "commenter", "viewer"];
|
|
2508
|
+
var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
|
|
2509
|
+
var SHARING_FILE = ".sharing.json";
|
|
2510
|
+
var MAX_GUESTS = 200;
|
|
2511
|
+
var MAX_NAME = 40;
|
|
2512
|
+
var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
|
|
2513
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2514
|
+
function fresh() {
|
|
2515
|
+
return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
|
|
2516
|
+
}
|
|
2517
|
+
function cleanName(raw) {
|
|
2518
|
+
const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
|
|
2519
|
+
return [...name].slice(0, MAX_NAME).join("");
|
|
2520
|
+
}
|
|
2521
|
+
var Sharing = class _Sharing {
|
|
2522
|
+
file;
|
|
2523
|
+
state;
|
|
2524
|
+
constructor(root) {
|
|
2525
|
+
this.file = path11.join(root, SHARING_FILE);
|
|
2526
|
+
this.state = _Sharing.read(this.file);
|
|
2527
|
+
}
|
|
2528
|
+
static read(file) {
|
|
2529
|
+
let raw;
|
|
2530
|
+
try {
|
|
2531
|
+
raw = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
2532
|
+
} catch {
|
|
2533
|
+
return fresh();
|
|
2534
|
+
}
|
|
2535
|
+
const state = fresh();
|
|
2536
|
+
if (!isRecord5(raw)) return state;
|
|
2537
|
+
state.lan = raw.lan === true;
|
|
2538
|
+
if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
|
|
2539
|
+
if (isRecord5(raw.links)) {
|
|
2540
|
+
for (const role of GUEST_ROLES) {
|
|
2541
|
+
const link = raw.links[role];
|
|
2542
|
+
if (isRecord5(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (Array.isArray(raw.guests)) {
|
|
2546
|
+
for (const g of raw.guests) {
|
|
2547
|
+
if (!isRecord5(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
|
|
2548
|
+
state.guests.push({
|
|
2549
|
+
id: g.id,
|
|
2550
|
+
secret: g.secret,
|
|
2551
|
+
name: cleanName(g.name) || "Guest",
|
|
2552
|
+
role: g.role,
|
|
2553
|
+
link: g.link,
|
|
2554
|
+
joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2555
|
+
...typeof g.seen === "string" ? { seen: g.seen } : {}
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return state;
|
|
2560
|
+
}
|
|
2561
|
+
save() {
|
|
2562
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
2563
|
+
fs9.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
|
|
2564
|
+
`, { mode: 384 });
|
|
2565
|
+
fs9.renameSync(tmp, this.file);
|
|
2566
|
+
}
|
|
2567
|
+
/** The role whose link has this token, while that link is on. */
|
|
2568
|
+
linkRole(tokenGiven) {
|
|
2569
|
+
for (const role of GUEST_ROLES) {
|
|
2570
|
+
const link = this.state.links[role];
|
|
2571
|
+
if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
|
|
2572
|
+
}
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
/** The guest a cookie names, if they still have access (their link on and not reset). */
|
|
2576
|
+
guest(secret) {
|
|
2577
|
+
if (!secret) return null;
|
|
2578
|
+
const guest = this.state.guests.find((g) => g.secret === secret);
|
|
2579
|
+
return guest && this.valid(guest) ? guest : null;
|
|
2580
|
+
}
|
|
2581
|
+
valid(guest) {
|
|
2582
|
+
const link = this.state.links[guest.role];
|
|
2583
|
+
return link.on && link.token === guest.link && this.state.guests.includes(guest);
|
|
2584
|
+
}
|
|
2585
|
+
join(role, name) {
|
|
2586
|
+
const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2587
|
+
this.state.guests.push(guest);
|
|
2588
|
+
if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
|
|
2589
|
+
this.save();
|
|
2590
|
+
return guest;
|
|
2591
|
+
}
|
|
2592
|
+
/** Marks a guest seen now (saved at most once a minute per guest). */
|
|
2593
|
+
seen(guest) {
|
|
2594
|
+
const now = /* @__PURE__ */ new Date();
|
|
2595
|
+
if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
|
|
2596
|
+
guest.seen = now.toISOString();
|
|
2597
|
+
this.save();
|
|
2598
|
+
}
|
|
2599
|
+
setLan(on, port) {
|
|
2600
|
+
this.state.lan = on;
|
|
2601
|
+
if (on && port) this.state.port = port;
|
|
2602
|
+
this.save();
|
|
2603
|
+
}
|
|
2604
|
+
setLink(role, on) {
|
|
2605
|
+
this.state.links[role].on = on;
|
|
2606
|
+
this.save();
|
|
2607
|
+
}
|
|
2608
|
+
/** A new token for the link: the old link stops working and the guests who came through it are dropped. */
|
|
2609
|
+
resetLink(role) {
|
|
2610
|
+
const old = this.state.links[role].token;
|
|
2611
|
+
this.state.links[role] = { token: token(), on: this.state.links[role].on };
|
|
2612
|
+
this.state.guests = this.state.guests.filter((g) => g.link !== old);
|
|
2613
|
+
this.save();
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
|
|
2617
|
+
* whose link is off would shut them out: false then, as for a guest who isn't there.
|
|
2618
|
+
*/
|
|
2619
|
+
setRole(id, role) {
|
|
2620
|
+
const guest = this.state.guests.find((g) => g.id === id);
|
|
2621
|
+
if (!guest || !this.state.links[role].on) return false;
|
|
2622
|
+
guest.role = role;
|
|
2623
|
+
guest.link = this.state.links[role].token;
|
|
2624
|
+
this.save();
|
|
2625
|
+
return true;
|
|
2626
|
+
}
|
|
2627
|
+
removeGuest(id) {
|
|
2628
|
+
const before = this.state.guests.length;
|
|
2629
|
+
this.state.guests = this.state.guests.filter((g) => g.id !== id);
|
|
2630
|
+
if (this.state.guests.length === before) return false;
|
|
2631
|
+
this.save();
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
async function shareCall(url, action, fail2) {
|
|
2637
|
+
const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
|
|
2638
|
+
const body = await response.json().catch(() => ({}));
|
|
2639
|
+
if (!response.ok) fail2("SHARE_REFUSED", String(body.message ?? `the server answered ${response.status}`));
|
|
2640
|
+
return body;
|
|
2641
|
+
}
|
|
2642
|
+
function rolesOf(flags, key, fail2) {
|
|
2643
|
+
const given = flags.get(key);
|
|
2644
|
+
if (given === void 0) return [];
|
|
2645
|
+
const roles = given === "all" || given === "true" ? [...GUEST_ROLES] : given.split(",").map((r) => r.trim());
|
|
2646
|
+
for (const role of roles) if (!isGuestRole(role)) fail2("INVALID_ROLE", `--${key}: editor, commenter, viewer or all (got ${role}).`);
|
|
2647
|
+
return roles;
|
|
2648
|
+
}
|
|
2649
|
+
function guestRoles(flags, fail2) {
|
|
2650
|
+
const given = flags.get("role");
|
|
2651
|
+
if (given === void 0) return [];
|
|
2652
|
+
return given.split(",").map((pair) => {
|
|
2653
|
+
const [guest = "", role = ""] = pair.split("=").map((part) => part.trim());
|
|
2654
|
+
if (!guest || !isGuestRole(role)) fail2("INVALID_ROLE", `--role: <guest id>=editor|commenter|viewer, comma separated (got ${pair}).`);
|
|
2655
|
+
return { action: "role", guest, role };
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
async function shareCommand(o) {
|
|
2659
|
+
const off = o.flags.has("off");
|
|
2660
|
+
const changes = [
|
|
2661
|
+
...rolesOf(o.flags, "reset", o.fail).map((role) => ({ action: "reset", role })),
|
|
2662
|
+
...rolesOf(o.flags, "enable", o.fail).map((role) => ({ action: "link", role, on: true })),
|
|
2663
|
+
...rolesOf(o.flags, "disable", o.fail).map((role) => ({ action: "link", role, on: false })),
|
|
2664
|
+
...guestRoles(o.flags, o.fail)
|
|
2665
|
+
];
|
|
2666
|
+
let server = await o.running();
|
|
2667
|
+
if (!server) {
|
|
2668
|
+
const sharing = new Sharing(o.root);
|
|
2669
|
+
for (const change of changes) {
|
|
2670
|
+
if (change.action === "reset") sharing.resetLink(change.role);
|
|
2671
|
+
else if (change.action === "role") {
|
|
2672
|
+
if (!sharing.setRole(String(change.guest), change.role)) o.fail("SHARE_REFUSED", `No guest ${String(change.guest)}, or the ${String(change.role)} link is off.`);
|
|
2673
|
+
} else sharing.setLink(change.role, change.on === true);
|
|
2674
|
+
}
|
|
2675
|
+
if (off) {
|
|
2676
|
+
sharing.setLan(false);
|
|
2677
|
+
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)." };
|
|
2678
|
+
}
|
|
2679
|
+
server = await o.startShared();
|
|
2680
|
+
if (!server) o.fail("SERVER_FAILED", `The server did not start. Run ${o.serveCommand} in the foreground to see why.`);
|
|
2681
|
+
}
|
|
2682
|
+
let view = await shareCall(server.url, null, o.fail);
|
|
2683
|
+
for (const change of changes) view = await shareCall(server.url, change, o.fail);
|
|
2684
|
+
if (off || !view.lan) view = await shareCall(server.url, { action: "lan", on: !off }, o.fail);
|
|
2685
|
+
const links = view.links;
|
|
2686
|
+
return {
|
|
2687
|
+
lan: view.lan,
|
|
2688
|
+
...view.lan ? { addresses: lanUrls(view) } : {},
|
|
2689
|
+
links: Object.fromEntries(Object.entries(links).map(([role, link]) => [role, link.on ? link.url : "off"])),
|
|
2690
|
+
guests: view.guests,
|
|
2691
|
+
...view.lan ? {
|
|
2692
|
+
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.",
|
|
2693
|
+
stop: o.offCommand
|
|
2694
|
+
} : { note: "Not shared on the network: the links work on this computer only." }
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
var lanUrls = (view) => (Array.isArray(view.addresses) ? view.addresses : []).map((a) => `http://${String(a)}:${String(view.port)}/`);
|
|
2698
|
+
async function lanOn(url, flags, fail2) {
|
|
2699
|
+
if (!flags.has("lan")) return {};
|
|
2700
|
+
return { lan: lanUrls(await shareCall(url, { action: "lan", on: true }, fail2)) };
|
|
2701
|
+
}
|
|
2499
2702
|
|
|
2500
2703
|
var emitWarning = process.emitWarning.bind(process);
|
|
2501
2704
|
process.emitWarning = ((warning, ...rest) => {
|
|
@@ -2504,8 +2707,8 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
2504
2707
|
});
|
|
2505
2708
|
var KIND = "design";
|
|
2506
2709
|
var SCRIPT = fileURLToPath2(import.meta.url);
|
|
2507
|
-
var EDITOR_DIR =
|
|
2508
|
-
var SERVER_SCRIPT =
|
|
2710
|
+
var EDITOR_DIR = path12.join(path12.dirname(SCRIPT), "editor");
|
|
2711
|
+
var SERVER_SCRIPT = path12.join(path12.dirname(SCRIPT), "server.mjs");
|
|
2509
2712
|
var MAX_LISTED = 200;
|
|
2510
2713
|
var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
|
|
2511
2714
|
var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
|
|
@@ -2518,7 +2721,7 @@ var CliError = class extends Error {
|
|
|
2518
2721
|
this.code = code;
|
|
2519
2722
|
}
|
|
2520
2723
|
};
|
|
2521
|
-
var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen"]);
|
|
2724
|
+
var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off"]);
|
|
2522
2725
|
function parseArgs(argv) {
|
|
2523
2726
|
const positional = [];
|
|
2524
2727
|
const flags = /* @__PURE__ */ new Map();
|
|
@@ -2544,13 +2747,13 @@ function quote(s) {
|
|
|
2544
2747
|
}
|
|
2545
2748
|
function resolveArtifact(given, mustExist = true) {
|
|
2546
2749
|
if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
|
|
2547
|
-
let root =
|
|
2548
|
-
if (
|
|
2549
|
-
root =
|
|
2750
|
+
let root = path12.resolve(given);
|
|
2751
|
+
if (path12.basename(root) === "project" && fs10.existsSync(path12.join(root, INDEX_FILE)) && !fs10.existsSync(path12.join(root, "project"))) {
|
|
2752
|
+
root = path12.dirname(root);
|
|
2550
2753
|
}
|
|
2551
|
-
const project =
|
|
2754
|
+
const project = path12.join(root, "project");
|
|
2552
2755
|
if (mustExist) {
|
|
2553
|
-
const stat =
|
|
2756
|
+
const stat = fs10.statSync(project, { throwIfNoEntry: false });
|
|
2554
2757
|
if (!stat?.isDirectory()) {
|
|
2555
2758
|
throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
|
|
2556
2759
|
}
|
|
@@ -2572,7 +2775,7 @@ function localize(value) {
|
|
|
2572
2775
|
function designSystemNotes(project, indexFile) {
|
|
2573
2776
|
let index;
|
|
2574
2777
|
try {
|
|
2575
|
-
index = JSON.parse(
|
|
2778
|
+
index = JSON.parse(fs10.readFileSync(path12.join(project, indexFile), "utf8"));
|
|
2576
2779
|
} catch {
|
|
2577
2780
|
return [];
|
|
2578
2781
|
}
|
|
@@ -2586,10 +2789,10 @@ function designSystemNotes(project, indexFile) {
|
|
|
2586
2789
|
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>/.`);
|
|
2587
2790
|
continue;
|
|
2588
2791
|
}
|
|
2589
|
-
const folder =
|
|
2590
|
-
if (!
|
|
2792
|
+
const folder = path12.join(project, "ds", ns);
|
|
2793
|
+
if (!fs10.existsSync(folder)) {
|
|
2591
2794
|
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.`);
|
|
2592
|
-
} else if (!
|
|
2795
|
+
} else if (!fs10.existsSync(path12.join(folder, "tokens.json"))) {
|
|
2593
2796
|
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).`);
|
|
2594
2797
|
}
|
|
2595
2798
|
}
|
|
@@ -2601,9 +2804,9 @@ async function check(a) {
|
|
|
2601
2804
|
try {
|
|
2602
2805
|
rt = loadSlidesCompiler(EDITOR_DIR);
|
|
2603
2806
|
} catch {
|
|
2604
|
-
throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${
|
|
2807
|
+
throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path12.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
|
|
2605
2808
|
}
|
|
2606
|
-
if (!
|
|
2809
|
+
if (!fs10.existsSync(path12.join(a.project, "deck.json"))) {
|
|
2607
2810
|
return {
|
|
2608
2811
|
problems: 1,
|
|
2609
2812
|
result: {
|
|
@@ -2667,11 +2870,11 @@ async function check(a) {
|
|
|
2667
2870
|
}
|
|
2668
2871
|
const r = await checkDesignSystem(a.project);
|
|
2669
2872
|
const problems = r.issues.length;
|
|
2670
|
-
if (
|
|
2671
|
-
if (!
|
|
2873
|
+
if (fs10.existsSync(path12.join(a.project, DESIGN_SYSTEM_FILE))) {
|
|
2874
|
+
if (!fs10.existsSync(path12.join(a.project, "README.md"))) {
|
|
2672
2875
|
r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
|
|
2673
2876
|
}
|
|
2674
|
-
if (!
|
|
2877
|
+
if (!fs10.existsSync(path12.join(a.project, "components", "Cover", "preview.html"))) {
|
|
2675
2878
|
r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
|
|
2676
2879
|
}
|
|
2677
2880
|
}
|
|
@@ -2697,11 +2900,11 @@ function formatBytes(n) {
|
|
|
2697
2900
|
}
|
|
2698
2901
|
async function upload(a, given, group) {
|
|
2699
2902
|
if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
|
|
2700
|
-
const file =
|
|
2903
|
+
const file = path12.resolve(given);
|
|
2701
2904
|
const stat = await fsp2.stat(file).catch(() => null);
|
|
2702
2905
|
if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
|
|
2703
2906
|
if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
|
|
2704
|
-
const ext =
|
|
2907
|
+
const ext = path12.extname(file).slice(1).toLowerCase();
|
|
2705
2908
|
const type = contentTypeOf(file);
|
|
2706
2909
|
const isMedia = type.startsWith("image/") || type.startsWith("font/");
|
|
2707
2910
|
let limit = UPLOAD_MAX_BYTES;
|
|
@@ -2726,7 +2929,7 @@ async function upload(a, given, group) {
|
|
|
2726
2929
|
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.`);
|
|
2727
2930
|
const bytes = await fsp2.readFile(file);
|
|
2728
2931
|
const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
|
|
2729
|
-
const base =
|
|
2932
|
+
const base = path12.basename(file);
|
|
2730
2933
|
if (KIND !== "design-system") {
|
|
2731
2934
|
return {
|
|
2732
2935
|
url: uploaded.url,
|
|
@@ -2758,12 +2961,12 @@ function folderName(name) {
|
|
|
2758
2961
|
}
|
|
2759
2962
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
|
|
2760
2963
|
async function listSystems(dirs) {
|
|
2761
|
-
const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(
|
|
2762
|
-
const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) =>
|
|
2964
|
+
const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path12.delimiter).filter(Boolean);
|
|
2965
|
+
const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path12.resolve(d));
|
|
2763
2966
|
const found = /* @__PURE__ */ new Map();
|
|
2764
2967
|
const visit = async (dir, depth) => {
|
|
2765
|
-
const project =
|
|
2766
|
-
if (
|
|
2968
|
+
const project = path12.join(dir, "project");
|
|
2969
|
+
if (fs10.existsSync(path12.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
|
|
2767
2970
|
const info = await readDesignSystemInfo(project).catch(() => null);
|
|
2768
2971
|
if (info) {
|
|
2769
2972
|
const namespace = info.namespace;
|
|
@@ -2772,8 +2975,8 @@ async function listSystems(dirs) {
|
|
|
2772
2975
|
namespace: namespace ?? null,
|
|
2773
2976
|
folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
|
|
2774
2977
|
root: dir,
|
|
2775
|
-
readme:
|
|
2776
|
-
tokens:
|
|
2978
|
+
readme: fs10.existsSync(path12.join(project, "README.md")) ? path12.join(project, "README.md") : null,
|
|
2979
|
+
tokens: fs10.existsSync(path12.join(project, "tokens.json")) ? path12.join(project, "tokens.json") : null
|
|
2777
2980
|
});
|
|
2778
2981
|
return;
|
|
2779
2982
|
}
|
|
@@ -2782,7 +2985,7 @@ async function listSystems(dirs) {
|
|
|
2782
2985
|
const entries = await fsp2.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2783
2986
|
for (const entry of entries) {
|
|
2784
2987
|
if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
|
|
2785
|
-
await visit(
|
|
2988
|
+
await visit(path12.join(dir, entry.name), depth - 1);
|
|
2786
2989
|
}
|
|
2787
2990
|
};
|
|
2788
2991
|
for (const start of starts) await visit(start, 4);
|
|
@@ -2794,16 +2997,22 @@ function serverArgs(flags) {
|
|
|
2794
2997
|
if (flags.has("port")) out.push("--port", flags.get("port"));
|
|
2795
2998
|
if (flags.has("host")) out.push("--host", flags.get("host"));
|
|
2796
2999
|
if (flags.get("agent-hook")) out.push("--agent-hook", flags.get("agent-hook"));
|
|
3000
|
+
if (flags.has("lan")) out.push("--lan");
|
|
2797
3001
|
return out;
|
|
2798
3002
|
}
|
|
3003
|
+
var fail = (code, message) => {
|
|
3004
|
+
throw new CliError(code, message);
|
|
3005
|
+
};
|
|
3006
|
+
var shareCall2 = (url, action) => shareCall(url, action, fail);
|
|
3007
|
+
var lanOn2 = (url, flags) => lanOn(url, flags, fail);
|
|
2799
3008
|
function reusedNote(a, flags) {
|
|
2800
3009
|
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.` } : {};
|
|
2801
3010
|
}
|
|
2802
3011
|
function assertServerFiles() {
|
|
2803
|
-
if (!
|
|
3012
|
+
if (!fs10.existsSync(path12.join(EDITOR_DIR, "index.html"))) {
|
|
2804
3013
|
throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
|
|
2805
3014
|
}
|
|
2806
|
-
if (!
|
|
3015
|
+
if (!fs10.existsSync(SERVER_SCRIPT)) {
|
|
2807
3016
|
throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
|
|
2808
3017
|
}
|
|
2809
3018
|
}
|
|
@@ -2814,15 +3023,15 @@ async function serve(a, flags) {
|
|
|
2814
3023
|
assertServerFiles();
|
|
2815
3024
|
const existing = await runningServer2(a.root);
|
|
2816
3025
|
if (existing && existing.pid !== process.pid) {
|
|
2817
|
-
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...reusedNote(a, flags) });
|
|
3026
|
+
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), ...reusedNote(a, flags) });
|
|
2818
3027
|
return;
|
|
2819
3028
|
}
|
|
2820
3029
|
const host = flags.get("host") ?? "127.0.0.1";
|
|
2821
|
-
const port = Number(flags.get("port")
|
|
3030
|
+
const port = flags.has("port") ? Number(flags.get("port")) : DEFAULT_PORTS[KIND];
|
|
2822
3031
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
|
|
2823
3032
|
const server = await import(pathToFileURL(SERVER_SCRIPT).href);
|
|
2824
|
-
const state = await server.serveForeground({ root: a.root, port, host, agentHook: flags.get("agent-hook") || void 0 });
|
|
2825
|
-
print({ url: state.url, root: a.root, pid: state.pid });
|
|
3033
|
+
const state = await server.serveForeground({ root: a.root, port, host, agentHook: flags.get("agent-hook") || void 0, lan: flags.has("lan"), portGiven: flags.has("port") });
|
|
3034
|
+
print({ url: state.url, root: a.root, pid: state.pid, ...state.lan ? { lan: state.lan } : {} });
|
|
2826
3035
|
}
|
|
2827
3036
|
async function open(a, flags) {
|
|
2828
3037
|
if (!await readInfo(a.project)) {
|
|
@@ -2831,36 +3040,38 @@ async function open(a, flags) {
|
|
|
2831
3040
|
assertServerFiles();
|
|
2832
3041
|
const existing = await runningServer2(a.root);
|
|
2833
3042
|
if (existing) {
|
|
2834
|
-
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...reusedNote(a, flags) });
|
|
3043
|
+
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), ...reusedNote(a, flags) });
|
|
2835
3044
|
return;
|
|
2836
3045
|
}
|
|
3046
|
+
const state = await startBackground(a, flags);
|
|
3047
|
+
const view = await shareCall2(state.url, null).catch(() => null);
|
|
3048
|
+
print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log: path12.join(a.root, LOG_FILE), stop: script("stop", a.root) });
|
|
3049
|
+
}
|
|
3050
|
+
async function startBackground(a, flags) {
|
|
2837
3051
|
const port = Number(flags.get("port") ?? 0);
|
|
2838
3052
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
|
|
2839
|
-
const logFile =
|
|
2840
|
-
const fd =
|
|
3053
|
+
const logFile = path12.join(a.root, LOG_FILE);
|
|
3054
|
+
const fd = fs10.openSync(logFile, "a");
|
|
2841
3055
|
const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
|
|
2842
3056
|
child.unref();
|
|
2843
|
-
|
|
3057
|
+
fs10.closeSync(fd);
|
|
2844
3058
|
let exited = false;
|
|
2845
3059
|
child.once("exit", () => exited = true);
|
|
2846
3060
|
const deadline = Date.now() + 15e3;
|
|
2847
3061
|
while (Date.now() < deadline) {
|
|
2848
3062
|
await new Promise((r) => setTimeout(r, 150));
|
|
2849
3063
|
const state = readState(a.root);
|
|
2850
|
-
if (state && state.pid === child.pid)
|
|
2851
|
-
print({ url: state.url, root: a.root, pid: state.pid, log: logFile, stop: script("stop", a.root) });
|
|
2852
|
-
return;
|
|
2853
|
-
}
|
|
3064
|
+
if (state && state.pid === child.pid) return state;
|
|
2854
3065
|
if (exited) break;
|
|
2855
3066
|
}
|
|
2856
|
-
const tail =
|
|
3067
|
+
const tail = fs10.existsSync(logFile) ? fs10.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
|
|
2857
3068
|
throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
|
|
2858
3069
|
${tail}` : ""}`);
|
|
2859
3070
|
}
|
|
2860
3071
|
async function stop(a) {
|
|
2861
3072
|
const state = readState(a.root);
|
|
2862
3073
|
if (!state || !alive(state.pid)) {
|
|
2863
|
-
|
|
3074
|
+
fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
|
|
2864
3075
|
print({ stopped: false, message: "No editor server is running for this folder." });
|
|
2865
3076
|
return;
|
|
2866
3077
|
}
|
|
@@ -2871,9 +3082,28 @@ async function stop(a) {
|
|
|
2871
3082
|
print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
|
|
2872
3083
|
return;
|
|
2873
3084
|
}
|
|
2874
|
-
|
|
3085
|
+
fs10.rmSync(path12.join(a.root, STATE_FILE), { force: true });
|
|
2875
3086
|
print({ stopped: true, pid: state.pid });
|
|
2876
3087
|
}
|
|
3088
|
+
async function share(a, flags) {
|
|
3089
|
+
if (!await readInfo(a.project)) {
|
|
3090
|
+
throw new CliError("NOT_A_" + KIND.toUpperCase().replace("-", "_"), `project/${INDEX_FILE} is missing or not a valid ${NOUN} index; write it, then run ${script("check", a.root)}.`);
|
|
3091
|
+
}
|
|
3092
|
+
print(
|
|
3093
|
+
await shareCommand({
|
|
3094
|
+
root: a.root,
|
|
3095
|
+
flags,
|
|
3096
|
+
running: () => runningServer2(a.root),
|
|
3097
|
+
startShared: async () => {
|
|
3098
|
+
assertServerFiles();
|
|
3099
|
+
return startBackground(a, new Map([...flags, ["lan", "true"]]));
|
|
3100
|
+
},
|
|
3101
|
+
offCommand: script("share", a.root, "--off"),
|
|
3102
|
+
serveCommand: script("serve", a.root, "--lan"),
|
|
3103
|
+
fail
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
2877
3107
|
var WAIT_CHUNK_S = 300;
|
|
2878
3108
|
function commentHints(a) {
|
|
2879
3109
|
return {
|
|
@@ -2896,7 +3126,7 @@ async function listComments(a, all) {
|
|
|
2896
3126
|
async function artifactTitle(a) {
|
|
2897
3127
|
const info = await readInfo(a.project).catch(() => null);
|
|
2898
3128
|
const title = typeof info?.title === "string" ? info.title.trim() : "";
|
|
2899
|
-
return title && title !== "project" ? title :
|
|
3129
|
+
return title && title !== "project" ? title : path12.basename(a.root);
|
|
2900
3130
|
}
|
|
2901
3131
|
async function waitComments(a, flags) {
|
|
2902
3132
|
const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
|
|
@@ -2982,7 +3212,12 @@ Usage:
|
|
|
2982
3212
|
node scripts/artifact.mjs serve <root> [--port n] [--host h] run the editor server in the foreground
|
|
2983
3213
|
[--agent-hook "<command>"] open/serve: run this for each comment sent to the agent
|
|
2984
3214
|
(the comment as JSON on stdin)
|
|
3215
|
+
[--lan] open/serve: also share on the local network, as share does
|
|
2985
3216
|
node scripts/artifact.mjs stop <root> stop the background editor server
|
|
3217
|
+
node scripts/artifact.mjs share <root> share on the local network; prints a link per role
|
|
3218
|
+
[--reset R] [--enable R] [--disable R] R: editor, commenter, viewer (comma list) or all
|
|
3219
|
+
[--role G=R,...] give guest G (an id it prints) role R
|
|
3220
|
+
[--off] stop sharing (guests keep their place for next time)
|
|
2986
3221
|
node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
|
|
2987
3222
|
node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
|
|
2988
3223
|
node scripts/artifact.mjs list [dir \u2026] find design systems (folders holding project/design-system.json)
|
|
@@ -3005,6 +3240,9 @@ async function main() {
|
|
|
3005
3240
|
case "stop":
|
|
3006
3241
|
await stop(resolveArtifact(positional[0], false));
|
|
3007
3242
|
return 0;
|
|
3243
|
+
case "share":
|
|
3244
|
+
await share(resolveArtifact(positional[0]), flags);
|
|
3245
|
+
return 0;
|
|
3008
3246
|
case "check": {
|
|
3009
3247
|
const { result, problems } = await check(resolveArtifact(positional[0]));
|
|
3010
3248
|
print(localize(result));
|
|
@@ -3012,10 +3250,10 @@ async function main() {
|
|
|
3012
3250
|
}
|
|
3013
3251
|
case "upload": {
|
|
3014
3252
|
const a = resolveArtifact(positional[0], false);
|
|
3015
|
-
if (!
|
|
3253
|
+
if (!fs10.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
3016
3254
|
throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
|
|
3017
3255
|
}
|
|
3018
|
-
|
|
3256
|
+
fs10.mkdirSync(a.project, { recursive: true });
|
|
3019
3257
|
print(await upload(a, positional[1], flags.get("group")));
|
|
3020
3258
|
return 0;
|
|
3021
3259
|
}
|