dsh-claude-design 2.6.0 → 2.7.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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // claude-design: local editor server (prebuilt; do not edit).
3
3
 
4
- import fs8 from "node:fs";
5
- import path7 from "node:path";
4
+ import fs11 from "node:fs";
5
+ import path11 from "node:path";
6
6
 
7
7
  import { createHash as createHash3, randomBytes as randomBytes2, randomUUID } from "node:crypto";
8
- import fs7 from "node:fs";
9
- import fsp2 from "node:fs/promises";
8
+ import fs10 from "node:fs";
9
+ import fsp4 from "node:fs/promises";
10
10
  import http from "node:http";
11
- import path6 from "node:path";
11
+ import path10 from "node:path";
12
12
  import { fileURLToPath, pathToFileURL } from "node:url";
13
13
 
14
14
  import { spawn } from "node:child_process";
@@ -773,6 +773,8 @@ import net from "node:net";
773
773
  import os2 from "node:os";
774
774
  import path2 from "node:path";
775
775
  var STATE_FILE = ".artifact-server.json";
776
+ var LOG_FILE = ".artifact-server.log";
777
+ var EXPORTS_DIR = "exports";
776
778
  function readState(root) {
777
779
  try {
778
780
  return JSON.parse(fs3.readFileSync(path2.join(root, STATE_FILE), "utf8"));
@@ -802,6 +804,7 @@ function recordServer(state, close, signals = ["SIGINT", "SIGTERM"]) {
802
804
  const file = path2.join(state.root, STATE_FILE);
803
805
  fs3.writeFileSync(file, `${JSON.stringify(state, null, 2)}
804
806
  `);
807
+ writeGitignore(state.root, state.kind);
805
808
  const shutdown = () => {
806
809
  if (readState(state.root)?.pid === process.pid) fs3.rmSync(file, { force: true });
807
810
  close();
@@ -809,6 +812,15 @@ function recordServer(state, close, signals = ["SIGINT", "SIGTERM"]) {
809
812
  };
810
813
  for (const signal of signals) process.on(signal, shutdown);
811
814
  }
815
+ var GITIGNORE_FILE = ".gitignore";
816
+ var gitignoreLines = (kind) => [STATE_FILE, LOG_FILE, ".sharing.json", ...kind === "docs" ? [] : ["history/"], ".*.tmp", ".DS_Store"];
817
+ function writeGitignore(root, kind) {
818
+ const file = path2.join(root, GITIGNORE_FILE);
819
+ try {
820
+ fs3.writeFileSync(file, ["# Written by baoyu-design when the editor first opened this folder: what only this computer's editor server needs.", ...gitignoreLines(kind), ""].join("\n"), { flag: "wx" });
821
+ } catch {
822
+ }
823
+ }
812
824
  async function listen(server2, port, host) {
813
825
  await new Promise((resolve, reject) => {
814
826
  server2.once("error", reject);
@@ -830,6 +842,10 @@ var PortsTaken = class extends Error {
830
842
  }
831
843
  };
832
844
  var isBenchmarkRange = (address) => /^198\.1[89]\./.test(address);
845
+ function isTailnet(address) {
846
+ const m = /^100\.(\d{1,3})\./.exec(address);
847
+ return !!m && Number(m[1]) >= 64 && Number(m[1]) <= 127;
848
+ }
833
849
  function lanAddresses() {
834
850
  const out = [];
835
851
  for (const list of Object.values(os2.networkInterfaces())) {
@@ -838,7 +854,7 @@ function lanAddresses() {
838
854
  out.push(entry.address);
839
855
  }
840
856
  }
841
- return [...new Set(out)];
857
+ return [...new Set(out)].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
842
858
  }
843
859
  function answers(host, port) {
844
860
  return new Promise((resolve) => {
@@ -890,8 +906,8 @@ async function listenFree(server2, from, host, first) {
890
906
  throw new PortsTaken(from, to);
891
907
  }
892
908
  function serverOrigin(host, port) {
893
- const shown = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
894
- return `http://${shown}:${port}`;
909
+ const shown2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
910
+ return `http://${shown2}:${port}`;
895
911
  }
896
912
  function sendJson2(response, status, body, headers = {}) {
897
913
  const text = JSON.stringify(body);
@@ -1088,9 +1104,401 @@ function mergeTextBytes(base, mine, current) {
1088
1104
  }
1089
1105
  }
1090
1106
 
1091
- import { createHash, randomBytes } from "node:crypto";
1107
+ import path4 from "node:path";
1108
+
1092
1109
  import fs4 from "node:fs";
1093
1110
  import path3 from "node:path";
1111
+ var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1112
+ var COMPONENT = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
1113
+ var NOT_COMPONENTS = /* @__PURE__ */ new Set(["lib", "src", "api", "archived"]);
1114
+ var MAX_COMPONENTS = 400;
1115
+ var MAX_TOKENS = 2e3;
1116
+ var MAX_README = 24e3;
1117
+ var MAX_COMPONENT_README = 8e3;
1118
+ var MAX_TYPES = 4e3;
1119
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
1120
+ var str = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : void 0;
1121
+ function noLink(p) {
1122
+ try {
1123
+ return fs4.realpathSync(p) === path3.resolve(p);
1124
+ } catch {
1125
+ return false;
1126
+ }
1127
+ }
1128
+ function readText(file, max) {
1129
+ try {
1130
+ if (!noLink(file)) return void 0;
1131
+ const stat = fs4.lstatSync(file);
1132
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
1133
+ const text = fs4.readFileSync(file, "utf8");
1134
+ return text.length > max ? `${text.slice(0, max)}
1135
+ \u2026` : text;
1136
+ } catch {
1137
+ return void 0;
1138
+ }
1139
+ }
1140
+ function readable(file) {
1141
+ return noLink(file) && fs4.lstatSync(file).isFile();
1142
+ }
1143
+ function readJson2(file) {
1144
+ const text = readText(file, MAX_FILE_BYTES);
1145
+ if (text === void 0) return void 0;
1146
+ try {
1147
+ return JSON.parse(text);
1148
+ } catch {
1149
+ return void 0;
1150
+ }
1151
+ }
1152
+ function tokensOf(tokens) {
1153
+ const themes = [];
1154
+ const families = [];
1155
+ let count = 0;
1156
+ for (const [name, family] of Object.entries(tokens)) {
1157
+ if (name === "type" || !family || typeof family !== "object" || Array.isArray(family)) continue;
1158
+ const list = family.tokens;
1159
+ if (!Array.isArray(list)) continue;
1160
+ if (name === "color") {
1161
+ for (const theme of family.themes ?? []) {
1162
+ const t = theme;
1163
+ const id = str(t?.id);
1164
+ if (id) themes.push({ id, name: str(t.name) ?? id });
1165
+ }
1166
+ }
1167
+ const out = [];
1168
+ for (const raw of list) {
1169
+ if (count >= MAX_TOKENS) break;
1170
+ const t = raw;
1171
+ const tokenName = str(t?.name);
1172
+ if (!tokenName) continue;
1173
+ let value = str(t.value);
1174
+ if (value === void 0 && t.value && typeof t.value === "object" && !Array.isArray(t.value)) {
1175
+ const byTheme = {};
1176
+ for (const [k, v] of Object.entries(t.value)) if (str(v) !== void 0) byTheme[k] = str(v);
1177
+ value = byTheme;
1178
+ }
1179
+ if (value === void 0) continue;
1180
+ out.push({ name: tokenName, value, ...str(t.usage) ? { usage: str(t.usage) } : {} });
1181
+ count += 1;
1182
+ }
1183
+ if (out.length) families.push({ name, tokens: out });
1184
+ }
1185
+ families.sort((a, b) => Number(b.name === "color") - Number(a.name === "color"));
1186
+ const type = tokens.type ?? {};
1187
+ const fontFamilies = {};
1188
+ if (type.families && typeof type.families === "object") {
1189
+ for (const [k, v] of Object.entries(type.families)) if (str(v)) fontFamilies[k] = str(v);
1190
+ }
1191
+ const textStyles = [];
1192
+ for (const g of Array.isArray(type.groups) ? type.groups : []) {
1193
+ const group = g;
1194
+ for (const st of Array.isArray(group.styles) ? group.styles : []) {
1195
+ const s = st;
1196
+ const name = str(s?.name);
1197
+ if (!name || textStyles.length >= MAX_TOKENS) continue;
1198
+ textStyles.push({
1199
+ group: str(group.name) ?? "",
1200
+ name,
1201
+ ...str(s.family) ?? str(group.family) ? { family: str(s.family) ?? str(group.family) } : {},
1202
+ ...str(s.fontSize) ? { fontSize: str(s.fontSize) } : {},
1203
+ ...str(s.lineHeight) ? { lineHeight: str(s.lineHeight) } : {},
1204
+ ...str(s.fontWeight) ? { fontWeight: str(s.fontWeight) } : {}
1205
+ });
1206
+ }
1207
+ }
1208
+ return { themes, families, fontFamilies, textStyles };
1209
+ }
1210
+ function summaryOf(readme) {
1211
+ const body = readme.split("\n").filter((line) => !/^\s*#/.test(line)).join(" ").replace(/\s+/g, " ").trim();
1212
+ if (!body) return void 0;
1213
+ const sentence = /^(.+?[.!?。!?])(\s|$)/.exec(body)?.[1] ?? body;
1214
+ return sentence.length > 200 ? `${sentence.slice(0, 200)}\u2026` : sentence;
1215
+ }
1216
+ function typesOf(dts, name) {
1217
+ const lines = dts.split("\n");
1218
+ const named = new RegExp(`^\\s*(?:export\\s+)?(?:declare\\s+)?(?:function|const|let|class|interface|type|namespace)\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:Props)?\\b`);
1219
+ const out = [];
1220
+ for (let i = 0; i < lines.length; i += 1) {
1221
+ if (!named.test(lines[i])) continue;
1222
+ let start = i;
1223
+ while (start > 0 && /^\s*(\*|\/\*\*|\*\/)/.test(lines[start - 1])) start -= 1;
1224
+ let end = i;
1225
+ let depth = 0;
1226
+ for (; end < lines.length; end += 1) {
1227
+ for (const ch of lines[end]) depth += ch === "{" ? 1 : ch === "}" ? -1 : 0;
1228
+ if (depth <= 0 && (/[;}]\s*$/.test(lines[end]) || end > i)) break;
1229
+ }
1230
+ out.push(lines.slice(start, end + 1).join("\n"));
1231
+ i = end;
1232
+ if (out.join("\n\n").length > MAX_TYPES) break;
1233
+ }
1234
+ const text = out.join("\n\n");
1235
+ return text ? text.length > MAX_TYPES ? `${text.slice(0, MAX_TYPES)}
1236
+ \u2026` : text : void 0;
1237
+ }
1238
+ function componentsOf(dir) {
1239
+ const components = path3.join(dir, "components");
1240
+ if (!noLink(components)) return [];
1241
+ let entries;
1242
+ try {
1243
+ entries = fs4.readdirSync(components, { withFileTypes: true });
1244
+ } catch {
1245
+ return [];
1246
+ }
1247
+ const dts = readText(path3.join(components, "index.d.ts"), MAX_FILE_BYTES) ?? "";
1248
+ const out = [];
1249
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1250
+ if (out.length >= MAX_COMPONENTS) break;
1251
+ if (!entry.isDirectory() || !COMPONENT.test(entry.name) || NOT_COMPONENTS.has(entry.name)) continue;
1252
+ const own = path3.join(components, entry.name);
1253
+ const readme = readText(path3.join(own, "README.md"), MAX_COMPONENT_README);
1254
+ const types = dts ? typesOf(dts, entry.name) : void 0;
1255
+ out.push({
1256
+ name: entry.name,
1257
+ ...readme ? { readme } : {},
1258
+ ...readme && summaryOf(readme) ? { summary: summaryOf(readme) } : {},
1259
+ ...types ? { types } : {},
1260
+ preview: readable(path3.join(own, "preview.html"))
1261
+ });
1262
+ }
1263
+ return out;
1264
+ }
1265
+ function insideDs(projectDir, dir) {
1266
+ try {
1267
+ if (!noLink(dir)) return false;
1268
+ const real = fs4.realpathSync(dir);
1269
+ const ds = fs4.realpathSync(path3.join(projectDir, "ds"));
1270
+ return real === ds || real.startsWith(ds + path3.sep);
1271
+ } catch {
1272
+ return false;
1273
+ }
1274
+ }
1275
+ function designSystemOf(projectDir, indexFile, folder) {
1276
+ if (!FOLDER.test(folder)) return null;
1277
+ const dir = path3.join(projectDir, "ds", folder);
1278
+ if (!fs4.existsSync(dir) || !insideDs(projectDir, dir) || !fs4.lstatSync(dir).isDirectory()) return null;
1279
+ const index = readJson2(path3.join(projectDir, indexFile));
1280
+ const records = Array.isArray(index?.designSystems) ? index.designSystems : [];
1281
+ const record = records.find((r) => str(r?.namespace) === folder);
1282
+ const tokens = readJson2(path3.join(dir, "tokens.json"));
1283
+ const usable = tokens && typeof tokens === "object" && !Array.isArray(tokens) ? tokens : void 0;
1284
+ const readme = readText(path3.join(dir, "README.md"), MAX_README);
1285
+ return {
1286
+ folder,
1287
+ title: (str(record?.title) ?? str(usable?.name) ?? folder).replace(/\s+[-\u2013\u2014]\s+design system$/i, ""),
1288
+ ...str(record?.version) ? { version: str(record.version) } : {},
1289
+ ...str(record?.copiedAt) ? { copiedAt: str(record.copiedAt) } : {},
1290
+ ...readme ? { readme } : {},
1291
+ ...usable ? tokensOf(usable) : { themes: [], families: [], fontFamilies: {}, textStyles: [] },
1292
+ components: componentsOf(dir)
1293
+ };
1294
+ }
1295
+
1296
+ var DS_PAGE = /^__ds\/([^/]+)\/$/;
1297
+ var esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1298
+ var copyHref = (folder, rel) => `../../project/ds/${folder}/${rel}`;
1299
+ function linkHref(href, folder, dir) {
1300
+ if (/^(https?:|mailto:)/i.test(href)) return href;
1301
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("/") || href.startsWith("#")) return null;
1302
+ const rel = path4.posix.normalize(path4.posix.join(dir, href));
1303
+ return rel.startsWith("..") ? null : copyHref(folder, rel);
1304
+ }
1305
+ var INLINE = /(`+)(.+?)\1|!?\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*(.+?)\*\*|\*([^*\s][^*]*?)\*/g;
1306
+ function inline(text, folder, dir) {
1307
+ let out = "";
1308
+ let at = 0;
1309
+ for (const m of text.matchAll(INLINE)) {
1310
+ out += esc(text.slice(at, m.index));
1311
+ at = m.index + m[0].length;
1312
+ if (m[2] !== void 0) out += `<code>${esc(m[2])}</code>`;
1313
+ else if (m[4] !== void 0) {
1314
+ const href = linkHref(m[4], folder, dir);
1315
+ const label = inline(m[3] || m[4], folder, dir);
1316
+ out += href === null ? label : `<a href="${esc(href)}" rel="noreferrer">${label}</a>`;
1317
+ } else if (m[5] !== void 0) out += `<strong>${inline(m[5], folder, dir)}</strong>`;
1318
+ else out += `<em>${inline(m[6], folder, dir)}</em>`;
1319
+ }
1320
+ return out + esc(text.slice(at));
1321
+ }
1322
+ var tableCells = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
1323
+ function markdown(source, folder, dir) {
1324
+ const lines = source.replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n/, "").split("\n");
1325
+ const out = [];
1326
+ let i = 0;
1327
+ const startsBlock = (line) => /^\s*(#{1,6}\s|```|>|[-*+]\s|\d+[.)]\s)/.test(line) || /^\s*([-*_])\1{2,}\s*$/.test(line);
1328
+ while (i < lines.length) {
1329
+ const line = lines[i];
1330
+ const fence = /^\s*(```+)/.exec(line);
1331
+ if (fence) {
1332
+ const code = [];
1333
+ for (i += 1; i < lines.length && !lines[i].trim().startsWith(fence[1]); i += 1) code.push(lines[i]);
1334
+ i += 1;
1335
+ out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`);
1336
+ } else if (!line.trim()) {
1337
+ i += 1;
1338
+ } else if (/^#{1,6}\s/.test(line)) {
1339
+ const hashes = /^#+/.exec(line)[0].length;
1340
+ const level = Math.min(hashes + 3, 6);
1341
+ out.push(`<h${level}>${inline(line.slice(hashes).trim().replace(/\s#+$/, ""), folder, dir)}</h${level}>`);
1342
+ i += 1;
1343
+ } else if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
1344
+ out.push("<hr>");
1345
+ i += 1;
1346
+ } else if (line.includes("|") && /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(lines[i + 1] ?? "")) {
1347
+ const rows = [`<tr>${tableCells(line).map((c) => `<th>${inline(c, folder, dir)}</th>`).join("")}</tr>`];
1348
+ for (i += 2; i < lines.length && lines[i].includes("|"); i += 1) {
1349
+ rows.push(`<tr>${tableCells(lines[i]).map((c) => `<td>${inline(c, folder, dir)}</td>`).join("")}</tr>`);
1350
+ }
1351
+ out.push(`<div class="scroll"><table>${rows.join("")}</table></div>`);
1352
+ } else if (/^\s*>/.test(line)) {
1353
+ const quote = [];
1354
+ for (; i < lines.length && /^\s*>/.test(lines[i]); i += 1) quote.push(lines[i].replace(/^\s*>\s?/, ""));
1355
+ out.push(`<blockquote>${markdown(quote.join("\n"), folder, dir)}</blockquote>`);
1356
+ } else if (/^\s*([-*+]|\d+[.)])\s/.test(line)) {
1357
+ const ordered = /^\s*\d/.test(line);
1358
+ const items = [];
1359
+ for (; i < lines.length && /^\s*([-*+]|\d+[.)])\s/.test(lines[i]); i += 1) {
1360
+ const item = /^(\s*)(?:[-*+]|\d+[.)])\s+(.*)$/.exec(lines[i]);
1361
+ const depth = Math.floor(item[1].length / 2);
1362
+ items.push(`<li${depth ? ` style="margin-left:${Math.min(depth, 4)}em"` : ""}>${inline(item[2], folder, dir)}</li>`);
1363
+ }
1364
+ out.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
1365
+ } else {
1366
+ const text = [];
1367
+ for (; i < lines.length && lines[i].trim() && !startsBlock(lines[i]); i += 1) text.push(lines[i].trim());
1368
+ if (!text.length) text.push(lines[i++].trim());
1369
+ out.push(`<p>${inline(text.join(" "), folder, dir)}</p>`);
1370
+ }
1371
+ }
1372
+ return out.join("\n");
1373
+ }
1374
+ function colorOf(token2, theme, all, depth = 0) {
1375
+ const value = typeof token2.value === "string" ? token2.value : token2.value[theme ?? ""] ?? Object.values(token2.value)[0] ?? "";
1376
+ const ref = /^\{([^{}]+)\}$/.exec(value.trim());
1377
+ const target = ref ? all.get(ref[1]) : void 0;
1378
+ return target && depth < 8 ? colorOf(target, theme, all, depth + 1) : value;
1379
+ }
1380
+ function colorsSection(family, system) {
1381
+ const all = new Map(family.tokens.map((token2) => [token2.name, token2]));
1382
+ const columns = system.themes.length ? system.themes : [{ id: "", name: "" }];
1383
+ const head = system.themes.length ? `<tr><th></th>${columns.map((t) => `<th>${esc(t.name)}</th>`).join("")}<th></th></tr>` : "";
1384
+ const rows = family.tokens.map((token2) => {
1385
+ const cells = columns.map((theme) => {
1386
+ const value = colorOf(token2, theme.id || void 0, all);
1387
+ return `<td><span class="swatch" style="background:${esc(value)}"></span><code>${esc(value)}</code></td>`;
1388
+ });
1389
+ return `<tr><th scope="row">${esc(token2.name)}</th>${cells.join("")}<td class="muted">${esc(token2.usage ?? "")}</td></tr>`;
1390
+ });
1391
+ return `<h3>Colours</h3><div class="scroll"><table class="tokens">${head}${rows.join("")}</table></div>`;
1392
+ }
1393
+ var tokenValue = (token2) => typeof token2.value === "string" ? token2.value : Object.entries(token2.value).map(([theme, value]) => `${theme}: ${value}`).join(", ");
1394
+ function familySection(family) {
1395
+ const rows = family.tokens.map(
1396
+ (token2) => `<tr><th scope="row">${esc(token2.name)}</th><td><code>${esc(tokenValue(token2))}</code></td><td class="muted">${esc(token2.usage ?? "")}</td></tr>`
1397
+ );
1398
+ const title = family.name.charAt(0).toUpperCase() + family.name.slice(1);
1399
+ return `<h3>${esc(title)}</h3><div class="scroll"><table class="tokens">${rows.join("")}</table></div>`;
1400
+ }
1401
+ function textSection(system) {
1402
+ if (!system.textStyles.length) return "";
1403
+ const rows = system.textStyles.map((style) => {
1404
+ const stack = style.family ? system.fontFamilies[style.family] ?? style.family : void 0;
1405
+ const css = [
1406
+ stack ? `font-family:${stack}` : "",
1407
+ style.fontSize ? `font-size:${style.fontSize}` : "",
1408
+ style.lineHeight ? `line-height:${style.lineHeight}` : "",
1409
+ style.fontWeight ? `font-weight:${style.fontWeight}` : ""
1410
+ ].filter(Boolean).join(";");
1411
+ const facts = [style.fontSize, style.lineHeight, style.fontWeight, style.family].filter(Boolean).join(" \xB7 ");
1412
+ return `<tr><td class="sample" style="${esc(css)}">${esc(style.name)}</td><td class="muted">${esc(style.group)}</td><td><code>${esc(facts)}</code></td></tr>`;
1413
+ });
1414
+ return `<h3>Text styles</h3><div class="scroll"><table class="tokens">${rows.join("")}</table></div>`;
1415
+ }
1416
+ function componentBlock(component, folder) {
1417
+ const dir = `components/${component.name}`;
1418
+ const preview = component.preview ? `<p><a href="${esc(copyHref(folder, `${dir}/preview.html`))}">Open its preview</a></p>` : "";
1419
+ const body = component.readme?.replace(/^\s*#\s+.*\n?/, "");
1420
+ const readme = body?.trim() ? `<div class="md">${markdown(body, folder, dir)}</div>` : "";
1421
+ const types = component.types ? `<h4>Types</h4><pre><code>${esc(component.types)}</code></pre>` : "";
1422
+ const empty = preview || readme || types ? "" : '<p class="muted">This component has no guidelines, types or preview.</p>';
1423
+ return `<details><summary><strong>${esc(component.name)}</strong>${component.summary ? `<span class="muted">${inline(component.summary, folder, dir)}</span>` : ""}</summary><div class="body">${preview}${readme}${types}${empty}</div></details>`;
1424
+ }
1425
+ var STYLE = `
1426
+ :root{color-scheme:light dark;--ink:#1f1e1d;--muted:#6f6e69;--line:#e4e2dc;--ground:#faf9f7;--panel:#fff;--code:#f1efea}
1427
+ @media (prefers-color-scheme:dark){:root{--ink:#ecebe7;--muted:#a3a29c;--line:#3a3936;--ground:#1f1e1d;--panel:#262624;--code:#32312e}}
1428
+ *{box-sizing:border-box}
1429
+ body{margin:0;background:var(--ground);color:var(--ink);font:14px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
1430
+ main{max-width:960px;margin:0 auto;padding:40px 24px 80px}
1431
+ h1{font-size:28px;line-height:1.2;margin:0 0 6px}
1432
+ h2{font-size:20px;margin:44px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--line)}
1433
+ h3{font-size:15px;margin:24px 0 8px}
1434
+ h4,h5,h6{font-size:14px;margin:18px 0 6px}
1435
+ a{color:inherit;text-underline-offset:2px}
1436
+ code{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
1437
+ p code,li code,td code{background:var(--code);padding:1px 4px;border-radius:4px}
1438
+ pre{background:var(--code);padding:12px;border-radius:8px;overflow-x:auto;margin:10px 0}
1439
+ nav{display:flex;flex-wrap:wrap;gap:16px;margin-top:14px}
1440
+ hr{border:0;border-top:1px solid var(--line);margin:18px 0}
1441
+ blockquote{margin:10px 0;padding-left:12px;border-left:3px solid var(--line);color:var(--muted)}
1442
+ table{border-collapse:collapse;width:100%}
1443
+ th,td{padding:7px 12px 7px 0;border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;font-weight:400}
1444
+ tr:first-child th:not([scope]){color:var(--muted);font-size:12px}
1445
+ .md th{font-weight:600}
1446
+ th[scope]{font-weight:500;white-space:nowrap}
1447
+ .scroll{overflow-x:auto}
1448
+ .muted{color:var(--muted)}
1449
+ .swatch{display:inline-block;width:18px;height:18px;border-radius:5px;vertical-align:middle;margin-right:7px;box-shadow:inset 0 0 0 1px rgba(127,127,127,.35)}
1450
+ .tokens td code{background:none;padding:0;white-space:nowrap}
1451
+ .sample{white-space:nowrap}
1452
+ details{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin:8px 0}
1453
+ summary{cursor:pointer;padding:10px 14px}
1454
+ summary .muted{margin-left:10px}
1455
+ .body{padding:2px 14px 12px;border-top:1px solid var(--line)}
1456
+ @media (max-width:600px){main{padding:24px 16px 60px}summary .muted{display:block;margin:2px 0 0}}
1457
+ `;
1458
+ function designSystemPage(projectDir, indexFile, folder, noun) {
1459
+ const system = designSystemOf(projectDir, indexFile, folder);
1460
+ if (system === null) return null;
1461
+ const colors = system.families.find((family) => family.name === "color");
1462
+ const others = system.families.filter((family) => family !== colors);
1463
+ const hasTokens = system.families.length > 0 || system.textStyles.length > 0;
1464
+ const facts = [
1465
+ `Read-only copy this ${noun} carries, in <code>project/ds/${esc(folder)}/</code>`,
1466
+ ...system.version ? [`version ${esc(system.version)}`] : [],
1467
+ ...system.copiedAt ? [`copied ${esc(system.copiedAt.slice(0, 10))}`] : []
1468
+ ].join(" \xB7 ");
1469
+ const nav = [
1470
+ hasTokens ? '<a href="#tokens">Tokens</a>' : "",
1471
+ system.components.length ? `<a href="#components">Components (${system.components.length})</a>` : "",
1472
+ system.readme ? '<a href="#guide">Guide</a>' : ""
1473
+ ].filter(Boolean).join("");
1474
+ const sections = [
1475
+ hasTokens ? `<section id="tokens"><h2>Tokens</h2>${colors ? colorsSection(colors, system) : ""}${textSection(system)}${others.map(familySection).join("")}</section>` : "",
1476
+ system.components.length ? `<section id="components"><h2>Components</h2>${system.components.map((component) => componentBlock(component, folder)).join("")}</section>` : "",
1477
+ system.readme ? `<section id="guide"><h2>Guide</h2><div class="md">${markdown(system.readme, folder, "")}</div></section>` : ""
1478
+ ].filter(Boolean).join("");
1479
+ return `<!doctype html>
1480
+ <html lang="en">
1481
+ <head>
1482
+ <meta charset="utf-8">
1483
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1484
+ <title>${esc(system.title)}</title>
1485
+ <style>${STYLE}</style>
1486
+ </head>
1487
+ <body>
1488
+ <main>
1489
+ <h1>${esc(system.title)}</h1>
1490
+ <p class="muted">${facts}</p>
1491
+ ${nav ? `<nav>${nav}</nav>` : ""}
1492
+ ${sections || '<p class="muted">This copy has no tokens, components or guide.</p>'}
1493
+ </main>
1494
+ </body>
1495
+ </html>
1496
+ `;
1497
+ }
1498
+
1499
+ import { createHash, randomBytes } from "node:crypto";
1500
+ import fs5 from "node:fs";
1501
+ import path5 from "node:path";
1094
1502
  var HISTORY_DIR = "history";
1095
1503
  var INDEX = "index.json";
1096
1504
  var OBJECTS = "objects";
@@ -1105,13 +1513,13 @@ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
1105
1513
  var SHA = /^[0-9a-f]{64}$/;
1106
1514
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1107
1515
  function writeAtomic2(file, data) {
1108
- fs4.mkdirSync(path3.dirname(file), { recursive: true });
1109
- const tmp = path3.join(path3.dirname(file), `.${path3.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
1516
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
1517
+ const tmp = path5.join(path5.dirname(file), `.${path5.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
1110
1518
  try {
1111
- fs4.writeFileSync(tmp, data);
1112
- fs4.renameSync(tmp, file);
1519
+ fs5.writeFileSync(tmp, data);
1520
+ fs5.renameSync(tmp, file);
1113
1521
  } catch (error) {
1114
- fs4.rmSync(tmp, { force: true });
1522
+ fs5.rmSync(tmp, { force: true });
1115
1523
  throw error;
1116
1524
  }
1117
1525
  }
@@ -1192,10 +1600,10 @@ var History = class {
1192
1600
  /** path → sha: what the files hold after the newest version. */
1193
1601
  headState;
1194
1602
  constructor(root) {
1195
- this.dir = path3.join(root, HISTORY_DIR);
1603
+ this.dir = path5.join(root, HISTORY_DIR);
1196
1604
  let raw = null;
1197
1605
  try {
1198
- raw = JSON.parse(fs4.readFileSync(path3.join(this.dir, INDEX), "utf8"));
1606
+ raw = JSON.parse(fs5.readFileSync(path5.join(this.dir, INDEX), "utf8"));
1199
1607
  } catch {
1200
1608
  }
1201
1609
  this.index = parseIndex(raw);
@@ -1239,7 +1647,7 @@ var History = class {
1239
1647
  read(sha) {
1240
1648
  if (!SHA.test(sha)) return null;
1241
1649
  try {
1242
- return fs4.readFileSync(path3.join(this.dir, OBJECTS, sha));
1650
+ return fs5.readFileSync(path5.join(this.dir, OBJECTS, sha));
1243
1651
  } catch {
1244
1652
  return null;
1245
1653
  }
@@ -1253,7 +1661,7 @@ var History = class {
1253
1661
  size(sha) {
1254
1662
  if (!SHA.test(sha)) return null;
1255
1663
  try {
1256
- return fs4.statSync(path3.join(this.dir, OBJECTS, sha)).size;
1664
+ return fs5.statSync(path5.join(this.dir, OBJECTS, sha)).size;
1257
1665
  } catch {
1258
1666
  return null;
1259
1667
  }
@@ -1387,8 +1795,8 @@ var History = class {
1387
1795
  return out;
1388
1796
  }
1389
1797
  putObject(sha, bytes) {
1390
- const file = path3.join(this.dir, OBJECTS, sha);
1391
- if (fs4.existsSync(file)) return;
1798
+ const file = path5.join(this.dir, OBJECTS, sha);
1799
+ if (fs5.existsSync(file)) return;
1392
1800
  writeAtomic2(file, bytes);
1393
1801
  }
1394
1802
  /** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
@@ -1425,14 +1833,14 @@ var History = class {
1425
1833
  }
1426
1834
  let names;
1427
1835
  try {
1428
- names = fs4.readdirSync(path3.join(this.dir, OBJECTS));
1836
+ names = fs5.readdirSync(path5.join(this.dir, OBJECTS));
1429
1837
  } catch {
1430
1838
  return;
1431
1839
  }
1432
- for (const name of names) if (SHA.test(name) && !keep.has(name)) fs4.rmSync(path3.join(this.dir, OBJECTS, name), { force: true });
1840
+ for (const name of names) if (SHA.test(name) && !keep.has(name)) fs5.rmSync(path5.join(this.dir, OBJECTS, name), { force: true });
1433
1841
  }
1434
1842
  save() {
1435
- writeAtomic2(path3.join(this.dir, INDEX), `${JSON.stringify(this.index)}
1843
+ writeAtomic2(path5.join(this.dir, INDEX), `${JSON.stringify(this.index)}
1436
1844
  `);
1437
1845
  }
1438
1846
  };
@@ -1502,13 +1910,19 @@ function diffContents(before, after) {
1502
1910
  }
1503
1911
 
1504
1912
  import { createHash as createHash2 } from "node:crypto";
1505
- import fs6 from "node:fs";
1506
- import os3 from "node:os";
1507
- import path5 from "node:path";
1913
+ import fs9 from "node:fs";
1914
+ import os4 from "node:os";
1915
+ import path9 from "node:path";
1916
+
1917
+ import fs7 from "node:fs";
1918
+ import fsp2 from "node:fs/promises";
1919
+ import path7 from "node:path";
1920
+ import { promisify } from "node:util";
1921
+ import zlib from "node:zlib";
1508
1922
 
1509
1923
  import crypto from "node:crypto";
1510
- import fs5 from "node:fs";
1511
- import path4 from "node:path";
1924
+ import fs6 from "node:fs";
1925
+ import path6 from "node:path";
1512
1926
  var GUEST_ROLES = ["editor", "commenter", "viewer"];
1513
1927
  var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
1514
1928
  var SHARING_FILE = ".sharing.json";
@@ -1535,13 +1949,13 @@ var Sharing = class _Sharing {
1535
1949
  file;
1536
1950
  state;
1537
1951
  constructor(root) {
1538
- this.file = path4.join(root, SHARING_FILE);
1952
+ this.file = path6.join(root, SHARING_FILE);
1539
1953
  this.state = _Sharing.read(this.file);
1540
1954
  }
1541
1955
  static read(file) {
1542
1956
  let raw;
1543
1957
  try {
1544
- raw = JSON.parse(fs5.readFileSync(file, "utf8"));
1958
+ raw = JSON.parse(fs6.readFileSync(file, "utf8"));
1545
1959
  } catch {
1546
1960
  return fresh();
1547
1961
  }
@@ -1573,9 +1987,9 @@ var Sharing = class _Sharing {
1573
1987
  }
1574
1988
  save() {
1575
1989
  const tmp = `${this.file}.${process.pid}.tmp`;
1576
- fs5.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
1990
+ fs6.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
1577
1991
  `, { mode: 384 });
1578
- fs5.renameSync(tmp, this.file);
1992
+ fs6.renameSync(tmp, this.file);
1579
1993
  }
1580
1994
  /** The role whose link has this token, while that link is on. */
1581
1995
  linkRole(tokenGiven) {
@@ -1646,6 +2060,293 @@ var Sharing = class _Sharing {
1646
2060
  }
1647
2061
  };
1648
2062
 
2063
+ var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
2064
+ var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
2065
+ var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
2066
+ var isTemp = (name) => name.endsWith(".tmp") || name.startsWith(".tmp-");
2067
+ var STORED = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".heic", ".mp4", ".m4v", ".mov", ".webm", ".mp3", ".m4a", ".aac", ".ogg", ".opus", ".flac", ".woff", ".woff2", ".zip", ".gz", ".br", ".zst", ".7z"]);
2068
+ var MAX_ENTRIES = 65535;
2069
+ var MAX_BYTES = 4294967295;
2070
+ var ArchiveTooLarge = class extends Error {
2071
+ code = "TOO_LARGE";
2072
+ };
2073
+ async function archiveEntries(root, skip = []) {
2074
+ const folder = path7.basename(path7.resolve(root)) || "artifact";
2075
+ const skipped = new Set(skip.map((file) => path7.resolve(file)));
2076
+ const entries = [];
2077
+ const walk = async (dir, rel) => {
2078
+ let names;
2079
+ try {
2080
+ names = await fsp2.readdir(dir, { withFileTypes: true });
2081
+ } catch (error) {
2082
+ if (rel && error.code === "ENOENT") return;
2083
+ throw error;
2084
+ }
2085
+ for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2086
+ const name = dirent.name;
2087
+ if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
2088
+ const file = path7.join(dir, name);
2089
+ if (skipped.has(file)) continue;
2090
+ const inner = rel ? `${rel}/${name}` : name;
2091
+ if (dirent.isDirectory()) await walk(file, inner);
2092
+ else if (dirent.isFile()) {
2093
+ const stat = await fsp2.stat(file).catch(() => null);
2094
+ if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
2095
+ }
2096
+ }
2097
+ };
2098
+ await walk(path7.resolve(root), "");
2099
+ 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.`);
2100
+ const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
2101
+ 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.`);
2102
+ return entries;
2103
+ }
2104
+ var CRC_TABLE = (() => {
2105
+ const table = new Uint32Array(256);
2106
+ for (let n = 0; n < 256; n++) {
2107
+ let c = n;
2108
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2109
+ table[n] = c >>> 0;
2110
+ }
2111
+ return table;
2112
+ })();
2113
+ function crc32(bytes) {
2114
+ let crc = 4294967295;
2115
+ for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
2116
+ return (crc ^ 4294967295) >>> 0;
2117
+ }
2118
+ function dosTime(date) {
2119
+ const year = date.getFullYear();
2120
+ if (year < 1980) return { time: 0, date: 1 << 5 | 1 };
2121
+ if (year > 2107) return { time: 23 << 11 | 59 << 5 | 29, date: 127 << 9 | 12 << 5 | 31 };
2122
+ return {
2123
+ time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
2124
+ date: year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
2125
+ };
2126
+ }
2127
+ var deflateRaw = promisify(zlib.deflateRaw);
2128
+ var UTF8 = 1 << 11;
2129
+ async function writeZip(entries, write) {
2130
+ const central = [];
2131
+ let offset = 0;
2132
+ const put = async (chunk) => {
2133
+ await write(chunk);
2134
+ offset += chunk.length;
2135
+ };
2136
+ for (const entry of entries) {
2137
+ let data;
2138
+ try {
2139
+ data = await fsp2.readFile(entry.file);
2140
+ } catch (error) {
2141
+ if (error.code === "ENOENT") continue;
2142
+ throw error;
2143
+ }
2144
+ const crc = crc32(data);
2145
+ let method = 0;
2146
+ let body = data;
2147
+ if (data.length > 0 && !STORED.has(path7.extname(entry.name).toLowerCase())) {
2148
+ const deflated = await deflateRaw(data);
2149
+ if (deflated.length < data.length) {
2150
+ method = 8;
2151
+ body = deflated;
2152
+ }
2153
+ }
2154
+ if (offset + body.length > MAX_BYTES) throw new ArchiveTooLarge("The folder grew past what a zip holds (4 GiB) while it was being zipped.");
2155
+ const name = Buffer.from(entry.name, "utf8");
2156
+ const { time, date } = dosTime(entry.mtime);
2157
+ const local = Buffer.alloc(30);
2158
+ local.writeUInt32LE(67324752, 0);
2159
+ local.writeUInt16LE(20, 4);
2160
+ local.writeUInt16LE(UTF8, 6);
2161
+ local.writeUInt16LE(method, 8);
2162
+ local.writeUInt16LE(time, 10);
2163
+ local.writeUInt16LE(date, 12);
2164
+ local.writeUInt32LE(crc, 14);
2165
+ local.writeUInt32LE(body.length, 18);
2166
+ local.writeUInt32LE(data.length, 22);
2167
+ local.writeUInt16LE(name.length, 26);
2168
+ local.writeUInt16LE(0, 28);
2169
+ const header = Buffer.alloc(46);
2170
+ header.writeUInt32LE(33639248, 0);
2171
+ header.writeUInt16LE(3 << 8 | 20, 4);
2172
+ header.writeUInt16LE(20, 6);
2173
+ header.writeUInt16LE(UTF8, 8);
2174
+ header.writeUInt16LE(method, 10);
2175
+ header.writeUInt16LE(time, 12);
2176
+ header.writeUInt16LE(date, 14);
2177
+ header.writeUInt32LE(crc, 16);
2178
+ header.writeUInt32LE(body.length, 20);
2179
+ header.writeUInt32LE(data.length, 24);
2180
+ header.writeUInt16LE(name.length, 28);
2181
+ header.writeUInt32LE((32768 | (entry.mode || 420) | 384) << 16 >>> 0, 38);
2182
+ header.writeUInt32LE(offset, 42);
2183
+ central.push(header, name);
2184
+ await put(Buffer.concat([local, name]));
2185
+ await put(body);
2186
+ }
2187
+ const start = offset;
2188
+ const directory = Buffer.concat(central);
2189
+ await put(directory);
2190
+ const end = Buffer.alloc(22);
2191
+ end.writeUInt32LE(101010256, 0);
2192
+ end.writeUInt16LE(central.length / 2, 8);
2193
+ end.writeUInt16LE(central.length / 2, 10);
2194
+ end.writeUInt32LE(directory.length, 12);
2195
+ end.writeUInt32LE(start, 16);
2196
+ await put(end);
2197
+ return offset;
2198
+ }
2199
+ function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
2200
+ const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
2201
+ return `${path7.basename(path7.resolve(root)) || "artifact"}-${day}.zip`;
2202
+ }
2203
+ async function writeArchive(root, out) {
2204
+ const file = path7.resolve(out);
2205
+ const entries = await archiveEntries(root, [file]);
2206
+ const tmp = path7.join(path7.dirname(file), `.${path7.basename(file)}.${process.pid}.tmp`);
2207
+ const handle = await fsp2.open(tmp, "w");
2208
+ let bytes;
2209
+ try {
2210
+ bytes = await writeZip(entries, async (chunk) => {
2211
+ await handle.write(chunk);
2212
+ });
2213
+ await handle.close();
2214
+ } catch (error) {
2215
+ await handle.close().catch(() => {
2216
+ });
2217
+ await fsp2.rm(tmp, { force: true });
2218
+ throw error;
2219
+ }
2220
+ await fsp2.rename(tmp, file);
2221
+ return { file, files: entries.length, bytes };
2222
+ }
2223
+ async function archiveCommand(root, out) {
2224
+ const where = out ? path7.resolve(out) : path7.dirname(path7.resolve(root));
2225
+ const target = fs7.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path7.join(where, archiveFileName(root)) : where;
2226
+ const { file, files, bytes } = await writeArchive(root, target);
2227
+ return { file, folder: path7.basename(path7.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
2228
+ }
2229
+ function attachment(name) {
2230
+ const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
2231
+ return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}`;
2232
+ }
2233
+ async function sendArchive(request, response, root) {
2234
+ request.resume();
2235
+ let entries;
2236
+ try {
2237
+ entries = await archiveEntries(root);
2238
+ } catch (error) {
2239
+ const text = JSON.stringify({ code: error instanceof ArchiveTooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
2240
+ response.writeHead(error instanceof ArchiveTooLarge ? 413 : 500, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }).end(text);
2241
+ return;
2242
+ }
2243
+ response.writeHead(200, {
2244
+ "Content-Type": "application/zip",
2245
+ "Content-Disposition": attachment(archiveFileName(root)),
2246
+ "Cache-Control": "no-store",
2247
+ "X-Content-Type-Options": "nosniff"
2248
+ });
2249
+ if (request.method === "HEAD") {
2250
+ response.end();
2251
+ return;
2252
+ }
2253
+ const write = (chunk) => new Promise((resolve, reject) => {
2254
+ if (response.destroyed) return reject(new Error("The download was cancelled."));
2255
+ if (response.write(chunk)) return resolve();
2256
+ const drained = () => {
2257
+ response.off("close", closed);
2258
+ resolve();
2259
+ };
2260
+ const closed = () => {
2261
+ response.off("drain", drained);
2262
+ reject(new Error("The download was cancelled."));
2263
+ };
2264
+ response.once("drain", drained);
2265
+ response.once("close", closed);
2266
+ });
2267
+ try {
2268
+ await writeZip(entries, write);
2269
+ response.end();
2270
+ } catch {
2271
+ response.destroy();
2272
+ }
2273
+ }
2274
+
2275
+ import fs8 from "node:fs";
2276
+ import fsp3 from "node:fs/promises";
2277
+ import os3 from "node:os";
2278
+ import path8 from "node:path";
2279
+ var MAX_BYTES2 = 4294967295;
2280
+ function shown(file) {
2281
+ const home = os3.homedir();
2282
+ return home && (file === home || file.startsWith(home + path8.sep)) ? `~${file.slice(home.length)}` : file;
2283
+ }
2284
+ function saved(file, bytes) {
2285
+ return { path: file, name: path8.basename(file), folder: path8.dirname(file), shown: shown(file), bytes };
2286
+ }
2287
+ function safeFileName(raw) {
2288
+ let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
2289
+ if (name.length > 200) {
2290
+ const ext = path8.extname(name).slice(0, 16);
2291
+ name = name.slice(0, 200 - ext.length) + ext;
2292
+ }
2293
+ return name || "download";
2294
+ }
2295
+ async function openFree(dir, name) {
2296
+ const ext = path8.extname(name);
2297
+ const stem = name.slice(0, name.length - ext.length);
2298
+ for (let n = 0; ; n++) {
2299
+ const file = path8.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
2300
+ try {
2301
+ return { file, handle: await fsp3.open(file, "wx") };
2302
+ } catch (error) {
2303
+ if (error.code !== "EEXIST" || n >= 999) throw error;
2304
+ }
2305
+ }
2306
+ }
2307
+ async function saveDownload(request, response, root) {
2308
+ const declared = Number(request.headers["content-length"]);
2309
+ if (Number.isFinite(declared) && declared > MAX_BYTES2) {
2310
+ request.resume();
2311
+ return sendJson2(response, 413, { code: "too_large", message: "The file is over 4 GiB." });
2312
+ }
2313
+ const name = safeFileName(new URL(request.url ?? "/", "http://local").searchParams.get("name") ?? "");
2314
+ let file = "";
2315
+ let handle = null;
2316
+ let bytes = 0;
2317
+ try {
2318
+ const dir = path8.join(root, EXPORTS_DIR);
2319
+ await fsp3.mkdir(dir, { recursive: true });
2320
+ ({ file, handle } = await openFree(dir, name));
2321
+ for await (const chunk of request) {
2322
+ bytes += chunk.length;
2323
+ if (bytes > MAX_BYTES2) throw Object.assign(new Error("The file is over 4 GiB."), { status: 413 });
2324
+ await handle.write(chunk);
2325
+ }
2326
+ await handle.close();
2327
+ handle = null;
2328
+ } catch (error) {
2329
+ await handle?.close().catch(() => {
2330
+ });
2331
+ if (file) await fsp3.rm(file, { force: true });
2332
+ request.resume();
2333
+ const status = error.status ?? 500;
2334
+ if (!response.headersSent) sendJson2(response, status, { code: status === 413 ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
2335
+ return;
2336
+ }
2337
+ sendJson2(response, 200, saved(file, bytes));
2338
+ }
2339
+ async function saveArchive(request, response, root) {
2340
+ request.resume();
2341
+ try {
2342
+ const { file } = await archiveCommand(root, void 0);
2343
+ sendJson2(response, 200, saved(file, fs8.statSync(file).size));
2344
+ } catch (error) {
2345
+ const tooLarge = error instanceof ArchiveTooLarge;
2346
+ sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
2347
+ }
2348
+ }
2349
+
1649
2350
  var NOUNS = {
1650
2351
  docs: { en: "doc", zh: "\u6587\u6863", zhThis: "\u8FD9\u7BC7\u6587\u6863" },
1651
2352
  slides: { en: "deck", zh: "\u6F14\u793A\u6587\u7A3F", zhThis: "\u8FD9\u4EFD\u6F14\u793A\u6587\u7A3F" },
@@ -1666,7 +2367,7 @@ function commentsViewer(viewer) {
1666
2367
  };
1667
2368
  }
1668
2369
  var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1669
- var esc = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2370
+ var esc2 = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1670
2371
  var script = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1671
2372
  function ownHosts(host, port) {
1672
2373
  const named = host === "0.0.0.0" || host === "::" ? [] : [host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`];
@@ -1675,12 +2376,12 @@ function ownHosts(host, port) {
1675
2376
  function localOwner() {
1676
2377
  let name = "You";
1677
2378
  try {
1678
- name = os3.userInfo().username.trim() || name;
2379
+ name = os4.userInfo().username.trim() || name;
1679
2380
  } catch {
1680
2381
  }
1681
2382
  return { id: "local", name, email: `${name}@localhost` };
1682
2383
  }
1683
- var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path5.resolve(root)).digest("hex").slice(0, 12)}`;
2384
+ var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path9.resolve(root)).digest("hex").slice(0, 12)}`;
1684
2385
  var ShareHost = class {
1685
2386
  sharing;
1686
2387
  options;
@@ -1731,7 +2432,7 @@ var ShareHost = class {
1731
2432
  const suffix = `:${this.port}`;
1732
2433
  if (!host.endsWith(suffix)) return false;
1733
2434
  const name = host.slice(0, -suffix.length).toLowerCase();
1734
- const machine = os3.hostname().toLowerCase();
2435
+ const machine = os4.hostname().toLowerCase();
1735
2436
  return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
1736
2437
  }
1737
2438
  /**
@@ -1845,13 +2546,16 @@ var ShareHost = class {
1845
2546
  }
1846
2547
  if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
1847
2548
  if (method === "GET" && p === "/__share/ui.js") {
1848
- fs6.stat(this.options.uiFile, (error, stat) => {
2549
+ fs9.stat(this.options.uiFile, (error, stat) => {
1849
2550
  if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
1850
2551
  sendFile(request, response, this.options.uiFile, { "Content-Type": "text/javascript; charset=utf-8", "Content-Length": stat.size, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
1851
2552
  });
1852
2553
  return true;
1853
2554
  }
1854
2555
  if (viewer.role !== "owner") return sendJson2(response, 403, { code: "not_granted", message: "Only the owner can do this." }), true;
2556
+ if ((method === "GET" || method === "HEAD") && p === "/__share/archive") return await sendArchive(request, response, this.options.root), true;
2557
+ if (method === "POST" && p === "/__share/archive") return await saveArchive(request, response, this.options.root), true;
2558
+ if (method === "POST" && p === "/__share/save") return await saveDownload(request, response, this.options.root), true;
1855
2559
  if (p === "/__share") return await this.shareApi(request, response, method), true;
1856
2560
  sendJson2(response, 404, { code: "not_found", message: "Not found" });
1857
2561
  return true;
@@ -1862,7 +2566,7 @@ var ShareHost = class {
1862
2566
  */
1863
2567
  headTags(_viewer, query = "") {
1864
2568
  let tags = `<script src="/__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&amp;")}"></script>`;
1865
- if (fs6.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
2569
+ if (fs9.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
1866
2570
  return tags;
1867
2571
  }
1868
2572
  /** `html` with the head tags first in its <head>; null when it has none. */
@@ -1886,11 +2590,11 @@ var ShareHost = class {
1886
2590
  const navigation = (request.method === "GET" || request.method === "HEAD") && /\btext\/html\b/.test(request.headers.accept ?? "");
1887
2591
  if (!navigation) return sendJson2(response, 403, { code: "not_granted", message: `This ${this.options.noun.en} is not shared with you.` });
1888
2592
  const t = this.words(request);
1889
- this.sendPage(response, 403, t.noAccessTitle, `<h1>${esc(t.noAccessTitle)}</h1><p>${esc(t.noAccess)}</p>${this.ownerHint(request, t)}`);
2593
+ this.sendPage(response, 403, t.noAccessTitle, `<h1>${esc2(t.noAccessTitle)}</h1><p>${esc2(t.noAccess)}</p>${this.ownerHint(request, t)}`);
1890
2594
  }
1891
2595
  /** A small page of our own (the join form, the refusal), in the reader's language. */
1892
2596
  sendPage(response, status, title, body, headers = {}) {
1893
- const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)}</title><style>${PAGE_CSS}</style></head><body><main>${body}</main></body></html>`;
2597
+ const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc2(title)}</title><style>${PAGE_CSS}</style></head><body><main>${body}</main></body></html>`;
1894
2598
  response.writeHead(status, {
1895
2599
  "Content-Type": "text/html; charset=utf-8",
1896
2600
  "Cache-Control": "no-store",
@@ -1904,7 +2608,7 @@ var ShareHost = class {
1904
2608
  const from = (request.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
1905
2609
  if (!this.lanServers.has(from)) return "";
1906
2610
  const url = `http://127.0.0.1:${this.port}/`;
1907
- return `<p class="note">${esc(t.ownerHere)} <a href="${esc(url)}">${esc(url)}</a></p>`;
2611
+ return `<p class="note">${esc2(t.ownerHere)} <a href="${esc2(url)}">${esc2(url)}</a></p>`;
1908
2612
  }
1909
2613
  /** GET /s/<token>: the name form (or straight in, for a guest already in through this link); POST: join. */
1910
2614
  async join(request, response, method, token2) {
@@ -1913,12 +2617,12 @@ var ShareHost = class {
1913
2617
  const local = isLoopback(request.socket.remoteAddress);
1914
2618
  const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
1915
2619
  const title = this.options.title();
1916
- if (!role || !local && !this.lanServers.size) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc(t.linkGoneTitle)}</h1><p>${esc(t.linkGone)}</p>`);
2620
+ if (!role || !local && !this.lanServers.size) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
1917
2621
  if (local) {
1918
2622
  const lan = [...this.lanServers.keys()][0];
1919
2623
  const there = lan ? `http://${lan}:${this.port}/s/${token2}` : "";
1920
- const tryIt = there ? `<p>${esc(t.tryRole(t.roles[role]))} <a href="${esc(there)}">${esc(there)}</a></p>` : "";
1921
- return this.sendPage(response, 200, title, `<h1>${esc(title)}</h1><p>${esc(t.youOwn)} <a href="/">${esc(t.openAsOwner)}</a></p>${tryIt}`);
2624
+ const tryIt = there ? `<p>${esc2(t.tryRole(t.roles[role]))} <a href="${esc2(there)}">${esc2(there)}</a></p>` : "";
2625
+ return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="/">${esc2(t.openAsOwner)}</a></p>${tryIt}`);
1922
2626
  }
1923
2627
  const current = this.sharing.guest(this.cookie(request));
1924
2628
  if (method === "GET" && current && current.link === this.sharing.state.links[role].token) {
@@ -1930,7 +2634,7 @@ var ShareHost = class {
1930
2634
  response,
1931
2635
  200,
1932
2636
  title,
1933
- `<h1>${esc(title)}</h1><p>${esc(t.invited(t.roles[role]))}</p><form method="post"><label for="n">${esc(t.yourName)}</label><input id="n" name="name" maxlength="${MAX_NAME}" value="${esc(current?.name ?? "")}" required autofocus autocomplete="name"><button type="submit">${esc(t.open)}</button></form><p class="note">${esc(t.nameNote)}</p>`
2637
+ `<h1>${esc2(title)}</h1><p>${esc2(t.invited(t.roles[role]))}</p><form method="post"><label for="n">${esc2(t.yourName)}</label><input id="n" name="name" maxlength="${MAX_NAME}" value="${esc2(current?.name ?? "")}" required autofocus autocomplete="name"><button type="submit">${esc2(t.open)}</button></form><p class="note">${esc2(t.nameNote)}</p>`
1934
2638
  );
1935
2639
  }
1936
2640
  const name = new URLSearchParams(form).get("name") ?? "";
@@ -1951,18 +2655,23 @@ var ShareHost = class {
1951
2655
  }
1952
2656
  /** Links, addresses and people, for the share panel and `artifact.mjs share`. */
1953
2657
  shareView() {
1954
- const addresses = [...this.lanServers.keys()];
1955
- const base = addresses.length ? `http://${addresses[0]}:${this.port}` : `http://127.0.0.1:${this.port}`;
2658
+ const addresses = [...this.lanServers.keys()].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
2659
+ const local = addresses.filter((a) => !isTailnet(a));
2660
+ const base = local.length ? `http://${local[0]}:${this.port}` : addresses.length ? `http://${addresses[0]}:${this.port}` : `http://127.0.0.1:${this.port}`;
2661
+ const tailnet = addresses.find(isTailnet);
1956
2662
  const online = /* @__PURE__ */ new Set();
1957
2663
  for (const live of this.live) if (live.viewer.guest) online.add(live.viewer.guest.id);
1958
2664
  const { links, guests } = this.sharing.state;
2665
+ const lan = addresses.length ? addresses : [...lanAddresses()].sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
2666
+ const network = lan.length ? `http://${lan[0]}:${this.port}` : base;
1959
2667
  return {
1960
2668
  title: this.options.title(),
1961
2669
  lan: this.lanServers.size > 0,
1962
2670
  port: this.port,
1963
2671
  addresses,
1964
2672
  local: `http://127.0.0.1:${this.port}/`,
1965
- links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}/s/${links[role].token}` }])),
2673
+ links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}/s/${links[role].token}`, networkUrl: `${network}/s/${links[role].token}` }])),
2674
+ ...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}/s/${links[role].token}` }])) } } : {},
1966
2675
  guests: guests.filter((g) => this.sharing.valid(g) || links[g.role].token === g.link).map((g) => ({ id: g.id, name: g.name, role: g.role, joined: g.joined, ...g.seen ? { seen: g.seen } : {}, online: online.has(g.id), active: this.sharing.valid(g) }))
1967
2676
  };
1968
2677
  }
@@ -2403,6 +3112,7 @@ data: ${JSON.stringify({ peers: this.list() })}
2403
3112
  var KIND = "";
2404
3113
  var NOUN = "";
2405
3114
  var INDEX_FILE = "";
3115
+ var DESIGN_SYSTEMS = false;
2406
3116
  var LAYOUT;
2407
3117
  var pageScriptSrc;
2408
3118
  var sessionExtras;
@@ -2548,10 +3258,10 @@ async function collectFiles(root, limit) {
2548
3258
  const seen = /* @__PURE__ */ new Set();
2549
3259
  let truncated = false;
2550
3260
  const visit = async (relDir, descend) => {
2551
- const dir = relDir === "" ? root : path6.join(root, ...relDir.split("/"));
3261
+ const dir = relDir === "" ? root : path10.join(root, ...relDir.split("/"));
2552
3262
  let entries;
2553
3263
  try {
2554
- entries = await fsp2.readdir(dir, { withFileTypes: true });
3264
+ entries = await fsp4.readdir(dir, { withFileTypes: true });
2555
3265
  } catch {
2556
3266
  return;
2557
3267
  }
@@ -2579,13 +3289,13 @@ async function collectFiles(root, limit) {
2579
3289
  return { rels, truncated };
2580
3290
  }
2581
3291
  async function writeFileAtomic(file, bytes) {
2582
- await fsp2.mkdir(path6.dirname(file), { recursive: true });
2583
- const tmp = path6.join(path6.dirname(file), `.${path6.basename(file)}.${randomUUID()}.tmp`);
3292
+ await fsp4.mkdir(path10.dirname(file), { recursive: true });
3293
+ const tmp = path10.join(path10.dirname(file), `.${path10.basename(file)}.${randomUUID()}.tmp`);
2584
3294
  try {
2585
- await fsp2.writeFile(tmp, bytes);
2586
- await fsp2.rename(tmp, file);
3295
+ await fsp4.writeFile(tmp, bytes);
3296
+ await fsp4.rename(tmp, file);
2587
3297
  } catch (error) {
2588
- await fsp2.rm(tmp, { force: true }).catch(() => {
3298
+ await fsp4.rm(tmp, { force: true }).catch(() => {
2589
3299
  });
2590
3300
  throw error;
2591
3301
  }
@@ -2598,6 +3308,11 @@ var Store = class {
2598
3308
  ownWrites;
2599
3309
  /** "<user id>\0project/<path>" → when the server last refused that person's save of the file as a conflict. */
2600
3310
  refused;
3311
+ /**
3312
+ * "<page>\0project/<path>" → the sha256 of that page's last save of the file ('' for a delete). A page sends no
3313
+ * ifMatch for a file whose version it believes is still its own last save; the server checks that belief for it.
3314
+ */
3315
+ lastSaved;
2601
3316
  queue;
2602
3317
  seq;
2603
3318
  truncationReported;
@@ -2609,6 +3324,7 @@ var Store = class {
2609
3324
  this.log = log;
2610
3325
  this.ownWrites = /* @__PURE__ */ new Map();
2611
3326
  this.refused = /* @__PURE__ */ new Map();
3327
+ this.lastSaved = /* @__PURE__ */ new Map();
2612
3328
  this.queue = Promise.resolve();
2613
3329
  this.seq = 0;
2614
3330
  this.truncationReported = false;
@@ -2628,19 +3344,19 @@ var Store = class {
2628
3344
  }
2629
3345
  /** The file on disk (it may not exist yet); null when a directory on the way is a symlink or the name is not a file. */
2630
3346
  async diskPath(rel) {
2631
- const realRoot = await fsp2.realpath(this.root).catch(() => null);
3347
+ const realRoot = await fsp4.realpath(this.root).catch(() => null);
2632
3348
  if (realRoot === null) return null;
2633
- const file = path6.join(realRoot, ...rel.split("/"));
2634
- for (let dir = path6.dirname(file); ; dir = path6.dirname(dir)) {
2635
- const real = await fsp2.realpath(dir).catch(() => null);
3349
+ const file = path10.join(realRoot, ...rel.split("/"));
3350
+ for (let dir = path10.dirname(file); ; dir = path10.dirname(dir)) {
3351
+ const real = await fsp4.realpath(dir).catch(() => null);
2636
3352
  if (real === null) {
2637
- if (path6.dirname(dir) === dir) return null;
3353
+ if (path10.dirname(dir) === dir) return null;
2638
3354
  continue;
2639
3355
  }
2640
3356
  if (real !== dir) return null;
2641
3357
  break;
2642
3358
  }
2643
- const info = await fsp2.lstat(file).catch(() => null);
3359
+ const info = await fsp4.lstat(file).catch(() => null);
2644
3360
  return info && !info.isFile() ? null : file;
2645
3361
  }
2646
3362
  async listing() {
@@ -2655,7 +3371,7 @@ var Store = class {
2655
3371
  for (const rel of rels.sort()) {
2656
3372
  const file = await this.diskPath(rel);
2657
3373
  if (file === null) continue;
2658
- const bytes = await fsp2.readFile(file).catch(() => null);
3374
+ const bytes = await fsp4.readFile(file).catch(() => null);
2659
3375
  if (bytes === null) continue;
2660
3376
  files[`project/${rel}`] = { sha256: sha2562(bytes), size: bytes.length, contentType: contentTypeOf(rel) };
2661
3377
  }
@@ -2665,16 +3381,18 @@ var Store = class {
2665
3381
  const rel = this.fileOf(p);
2666
3382
  const file = rel === null ? null : await this.diskPath(rel);
2667
3383
  if (file === null) return null;
2668
- return fsp2.readFile(file).catch(() => null);
3384
+ return fsp4.readFile(file).catch(() => null);
2669
3385
  }
2670
3386
  /**
2671
3387
  * A save from the page by `who`: all entries or none (ifMatch is checked for all before anything is written). It is
2672
3388
  * a version in the history under their name; what changed on disk before it is one of its own, Claude's.
2673
3389
  */
2674
- publish(batch, who, over = [], merged = []) {
2675
- return this.exclusive(() => this.publishNow(batch, who, over, merged));
3390
+ publish(batch, who, over = [], merged = [], page = "") {
3391
+ return this.exclusive(() => this.publishNow(batch, who, over, merged, page));
2676
3392
  }
2677
- async publishNow(batch, who, over, merged) {
3393
+ async publishNow(batch, who, over, merged, page) {
3394
+ const saver = page ? `page:${page}` : `user:${who.user.id}`;
3395
+ const named = new Set([...over, ...merged].map((o) => o.path));
2678
3396
  const files = /* @__PURE__ */ new Map();
2679
3397
  let total = 0;
2680
3398
  for (const [p, entry] of Object.entries(batch)) {
@@ -2697,8 +3415,12 @@ var Store = class {
2697
3415
  const conflicts = [];
2698
3416
  for (const [p, entry] of Object.entries(batch)) {
2699
3417
  const expected = entry?.ifMatch;
2700
- if (expected === void 0) continue;
2701
3418
  const actual = before.files[p]?.sha256 ?? null;
3419
+ if (expected === void 0) {
3420
+ const own = this.lastSaved.get(`${saver}\0${p}`);
3421
+ if (own !== void 0 && !named.has(p) && (actual ?? "") !== own) conflicts.push({ path: p, actual, expected: own || null });
3422
+ continue;
3423
+ }
2702
3424
  if (actual !== expected) conflicts.push({ path: p, actual, expected });
2703
3425
  }
2704
3426
  if (conflicts.length > 0) {
@@ -2722,13 +3444,15 @@ var Store = class {
2722
3444
  const file = files.get(p);
2723
3445
  if (!isWriteEntry(entry)) {
2724
3446
  this.ownWrites.set(p, "");
2725
- await fsp2.rm(file, { force: true });
3447
+ this.rememberSave(saver, p, "");
3448
+ await fsp4.rm(file, { force: true });
2726
3449
  written.set(p, null);
2727
3450
  continue;
2728
3451
  }
2729
3452
  const bytes = Buffer.from(entry.base64, "base64");
2730
3453
  const sha = sha2562(bytes);
2731
3454
  this.ownWrites.set(p, sha);
3455
+ this.rememberSave(saver, p, sha);
2732
3456
  await writeFileAtomic(file, bytes);
2733
3457
  shas[p] = sha;
2734
3458
  written.set(p, bytes);
@@ -2738,6 +3462,13 @@ var Store = class {
2738
3462
  this.seq += 1;
2739
3463
  return { status: 200, body: { version: after.ver, seq: this.seq, shas }, version };
2740
3464
  }
3465
+ /** A page's last save of a file (lastSaved), the oldest forgotten past a few thousand. */
3466
+ rememberSave(saver, p, sha) {
3467
+ const key = `${saver}\0${p}`;
3468
+ this.lastSaved.delete(key);
3469
+ this.lastSaved.set(key, sha);
3470
+ if (this.lastSaved.size > 4e3) this.lastSaved.delete(this.lastSaved.keys().next().value);
3471
+ }
2741
3472
  /**
2742
3473
  * The files this save replaces without its page having seen them: the server refused this person's save of the file
2743
3474
  * as a conflict moments ago (someone else had saved it since their page loaded it), and now the page saves on top of
@@ -2807,7 +3538,7 @@ var Store = class {
2807
3538
  if (file === null) continue;
2808
3539
  if (bytes === null) {
2809
3540
  this.ownWrites.set(p, "");
2810
- await fsp2.rm(file, { force: true });
3541
+ await fsp4.rm(file, { force: true });
2811
3542
  } else {
2812
3543
  this.ownWrites.set(p, sha2562(bytes));
2813
3544
  await writeFileAtomic(file, bytes);
@@ -2821,7 +3552,7 @@ var Store = class {
2821
3552
  try {
2822
3553
  const file = await this.diskPath(`${LAYOUT.blobDir}/${ASSET_INDEX}`);
2823
3554
  if (file === null) return {};
2824
- const parsed = JSON.parse(await fsp2.readFile(file, "utf8"));
3555
+ const parsed = JSON.parse(await fsp4.readFile(file, "utf8"));
2825
3556
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2826
3557
  } catch {
2827
3558
  return {};
@@ -2849,11 +3580,11 @@ var Store = class {
2849
3580
  }
2850
3581
  async findBlobFile(id) {
2851
3582
  try {
2852
- const names = await fsp2.readdir(path6.join(this.root, LAYOUT.blobDir));
3583
+ const names = await fsp4.readdir(path10.join(this.root, LAYOUT.blobDir));
2853
3584
  const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
2854
3585
  if (name === void 0) return null;
2855
3586
  const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
2856
- return file !== null && (await fsp2.lstat(file).catch(() => null))?.isFile() ? file : null;
3587
+ return file !== null && (await fsp4.lstat(file).catch(() => null))?.isFile() ? file : null;
2857
3588
  } catch {
2858
3589
  return null;
2859
3590
  }
@@ -2876,7 +3607,7 @@ var Store = class {
2876
3607
  let sha = "";
2877
3608
  try {
2878
3609
  const file = await this.diskPath(rel);
2879
- if (file !== null && (await fsp2.lstat(file)).isFile()) sha = sha2562(await fsp2.readFile(file));
3610
+ if (file !== null && (await fsp4.lstat(file)).isFile()) sha = sha2562(await fsp4.readFile(file));
2880
3611
  } catch {
2881
3612
  sha = "";
2882
3613
  }
@@ -2904,13 +3635,13 @@ function watchArtifact(store, onChange, onError) {
2904
3635
  const directories = /* @__PURE__ */ new Map();
2905
3636
  const visit = (rel, descend) => {
2906
3637
  if (directories.size >= maxDirectories) return;
2907
- const dir = rel === "" ? store.root : path6.join(store.root, ...rel.split("/"));
2908
- if (directories.has(dir) || !fs7.existsSync(dir)) return;
2909
- const info = fs7.lstatSync(dir);
3638
+ const dir = rel === "" ? store.root : path10.join(store.root, ...rel.split("/"));
3639
+ if (directories.has(dir) || !fs10.existsSync(dir)) return;
3640
+ const info = fs10.lstatSync(dir);
2910
3641
  if (!info.isDirectory()) return;
2911
3642
  directories.set(dir, `${info.dev}:${info.ino}`);
2912
3643
  if (!descend) return;
2913
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
3644
+ for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
2914
3645
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2915
3646
  const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
2916
3647
  if (LAYOUT.descendInto(child)) visit(child, true);
@@ -2927,19 +3658,19 @@ function watchArtifact(store, onChange, onError) {
2927
3658
  if (watchers.has(dir)) continue;
2928
3659
  let watcher;
2929
3660
  try {
2930
- watcher = fs7.watch(dir, (event, name) => {
3661
+ watcher = fs10.watch(dir, (event, name) => {
2931
3662
  if (stopped) return;
2932
3663
  if (name) {
2933
- const changed = path6.join(dir, String(name));
3664
+ const changed = path10.join(dir, String(name));
2934
3665
  if (event === "rename" && watchers.has(changed)) {
2935
3666
  for (const [watchedDir, item] of watchers) {
2936
- if (watchedDir === changed || watchedDir.startsWith(changed + path6.sep)) {
3667
+ if (watchedDir === changed || watchedDir.startsWith(changed + path10.sep)) {
2937
3668
  item.watcher.close();
2938
3669
  watchers.delete(watchedDir);
2939
3670
  }
2940
3671
  }
2941
3672
  }
2942
- const rel = path6.relative(store.root, changed).split(path6.sep).join("/");
3673
+ const rel = path10.relative(store.root, changed).split(path10.sep).join("/");
2943
3674
  if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
2944
3675
  }
2945
3676
  schedule();
@@ -2975,7 +3706,7 @@ function watchArtifact(store, onChange, onError) {
2975
3706
  const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
2976
3707
  const parts = [];
2977
3708
  for (const rel of rels) {
2978
- const info = await fsp2.lstat(path6.join(store.root, ...rel.split("/"))).catch(() => null);
3709
+ const info = await fsp4.lstat(path10.join(store.root, ...rel.split("/"))).catch(() => null);
2979
3710
  parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
2980
3711
  }
2981
3712
  return parts.join("\n");
@@ -3006,9 +3737,9 @@ function watchArtifact(store, onChange, onError) {
3006
3737
  var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
3007
3738
  async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1", log = (line) => process.stderr.write(`${line}
3008
3739
  `), agentHook, lan = false, portGiven = false }) {
3009
- const project = path6.join(root, "project");
3740
+ const project = path10.join(root, "project");
3010
3741
  const stamp = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
3011
- const projectDir = await fsp2.realpath(project);
3742
+ const projectDir = await fsp4.realpath(project);
3012
3743
  const history = new History(root);
3013
3744
  const store = new Store(projectDir, history, stamp);
3014
3745
  const owner = localOwner();
@@ -3020,7 +3751,7 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
3020
3751
  title: () => artifactTitle(root, projectDir),
3021
3752
  log: stamp,
3022
3753
  createServer: () => http.createServer(handle),
3023
- uiFile: path6.join(EDITOR_DIR, "share-ui.js"),
3754
+ uiFile: path10.join(EDITOR_DIR, "share-ui.js"),
3024
3755
  // The History panel's address and what this person may do there; on a version's page (?at=n), which version.
3025
3756
  globals: (viewer, query) => {
3026
3757
  const at = Number(query.get("at") ?? "");
@@ -3153,7 +3884,7 @@ data: ${JSON.stringify(event)}
3153
3884
  return;
3154
3885
  }
3155
3886
  const found = await store.blobFile(id);
3156
- const info = found === null ? null : await fsp2.stat(found.file).catch(() => null);
3887
+ const info = found === null ? null : await fsp4.stat(found.file).catch(() => null);
3157
3888
  if (found === null || !info?.isFile()) {
3158
3889
  response.writeHead(404, { "Cache-Control": "no-store" }).end();
3159
3890
  return;
@@ -3170,10 +3901,10 @@ data: ${JSON.stringify(event)}
3170
3901
  const serveEditor = async (request, response, rest, viewer, query = "") => {
3171
3902
  const parts = rest === "" ? ["index.html"] : rest.split("/");
3172
3903
  const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
3173
- const realDist = invalid ? null : await fsp2.realpath(EDITOR_DIR).catch(() => null);
3174
- const file = realDist === null ? null : path6.join(realDist, ...parts);
3175
- const real = file === null ? null : await fsp2.realpath(file).catch(() => null);
3176
- const info = real === null ? null : await fsp2.lstat(real).catch(() => null);
3904
+ const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
3905
+ const file = realDist === null ? null : path10.join(realDist, ...parts);
3906
+ const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
3907
+ const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
3177
3908
  if (file === null || real !== file || !info?.isFile()) {
3178
3909
  if (realDist === null && !invalid && parts[0] === "index.html") {
3179
3910
  response.writeHead(503, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("The editor page is missing; reinstall the skill.");
@@ -3182,13 +3913,13 @@ data: ${JSON.stringify(event)}
3182
3913
  response.writeHead(404, { "Cache-Control": "no-store" }).end();
3183
3914
  return;
3184
3915
  }
3185
- const ext = path6.extname(file).toLowerCase();
3916
+ const ext = path10.extname(file).toLowerCase();
3186
3917
  const type = STATIC_TYPES[ext] ?? "application/octet-stream";
3187
3918
  if (ext === ".html") {
3188
3919
  const key = parts.join("/");
3189
3920
  let page = pageCache.get(key);
3190
3921
  if (!page || page.mtimeMs !== info.mtimeMs) {
3191
- const html = await fsp2.readFile(file, "utf8");
3922
+ const html = await fsp4.readFile(file, "utf8");
3192
3923
  page = { mtimeMs: info.mtimeMs, html, csp: pageCsp(pageScriptSrc(html)) };
3193
3924
  pageCache.set(key, page);
3194
3925
  }
@@ -3233,7 +3964,8 @@ data: ${JSON.stringify(event)}
3233
3964
  publishError(response, 400, "invalid_content", "malformed publish request");
3234
3965
  return;
3235
3966
  }
3236
- const outcome = await store.publish(files, whoOf(viewer), over, merged);
3967
+ const page = String(request.headers["x-deck-client"] ?? "");
3968
+ const outcome = await store.publish(files, whoOf(viewer), over, merged, CLIENT_ID.test(page) ? page : "");
3237
3969
  const names = Object.keys(files).map((p) => p.replace(/^project\//, ""));
3238
3970
  const made = outcome.version ? ` as version ${outcome.version.n}` : "";
3239
3971
  stamp(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
@@ -3377,6 +4109,26 @@ data: ${JSON.stringify(event)}
3377
4109
  if (rest === ROUTES.listing) return sendJson2(response, 200, await store.listing());
3378
4110
  if (rest === ROUTES.session) return sendJson2(response, 200, session(viewer));
3379
4111
  if (rest === ROUTES.events) return events(request, response, viewer);
4112
+ const dsPage = DESIGN_SYSTEMS ? DS_PAGE.exec(rest) : null;
4113
+ if (dsPage) {
4114
+ const html = designSystemPage(projectDir, INDEX_FILE, dsPage[1], NOUN);
4115
+ if (html === null) {
4116
+ response.writeHead(404, { "Cache-Control": "no-store" }).end();
4117
+ return;
4118
+ }
4119
+ const body = Buffer.from(html);
4120
+ response.writeHead(200, {
4121
+ "Content-Type": "text/html; charset=utf-8",
4122
+ "Content-Length": body.length,
4123
+ "Cache-Control": "no-store",
4124
+ "X-Content-Type-Options": "nosniff",
4125
+ // The address holds the grant, and a README may link anywhere.
4126
+ "Referrer-Policy": "no-referrer",
4127
+ "Content-Security-Policy": DATA_CSP
4128
+ });
4129
+ response.end(method === "HEAD" ? void 0 : body);
4130
+ return;
4131
+ }
3380
4132
  if (rest.startsWith("project/")) {
3381
4133
  const bytes = await store.readProjectFile(rest);
3382
4134
  if (bytes === null) {
@@ -3474,11 +4226,11 @@ data: ${JSON.stringify(event)}
3474
4226
  }
3475
4227
  function artifactTitle(root, projectDir) {
3476
4228
  try {
3477
- const index = JSON.parse(fs7.readFileSync(path6.join(projectDir, INDEX_FILE), "utf8"));
4229
+ const index = JSON.parse(fs10.readFileSync(path10.join(projectDir, INDEX_FILE), "utf8"));
3478
4230
  for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
3479
4231
  } catch {
3480
4232
  }
3481
- return path6.basename(root);
4233
+ return path10.basename(root);
3482
4234
  }
3483
4235
  var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
3484
4236
  async function serveForeground({ root, port, host, agentHook, lan, portGiven }) {
@@ -3499,18 +4251,18 @@ async function main(argv) {
3499
4251
  }
3500
4252
  if (positional.length !== 1 || flags.has("help")) {
3501
4253
  process.stderr.write(
3502
- `Usage: node ${path6.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
4254
+ `Usage: node ${path10.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
3503
4255
  <root> holds project/${INDEX_FILE}.
3504
4256
  `
3505
4257
  );
3506
4258
  return 2;
3507
4259
  }
3508
- let root = path6.resolve(positional[0]);
3509
- if (path6.basename(root) === "project" && fs7.existsSync(path6.join(root, INDEX_FILE)) && !fs7.existsSync(path6.join(root, "project"))) {
3510
- root = path6.dirname(root);
4260
+ let root = path10.resolve(positional[0]);
4261
+ if (path10.basename(root) === "project" && fs10.existsSync(path10.join(root, INDEX_FILE)) && !fs10.existsSync(path10.join(root, "project"))) {
4262
+ root = path10.dirname(root);
3511
4263
  }
3512
- if (!fs7.statSync(path6.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
3513
- process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path6.join(root, "project", INDEX_FILE)} does not exist.` })}
4264
+ if (!fs10.statSync(path10.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
4265
+ process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path10.join(root, "project", INDEX_FILE)} does not exist.` })}
3514
4266
  `);
3515
4267
  return 2;
3516
4268
  }
@@ -3534,7 +4286,7 @@ async function main(argv) {
3534
4286
  }
3535
4287
  function isMain(moduleUrl) {
3536
4288
  try {
3537
- return Boolean(process.argv[1]) && pathToFileURL(fs7.realpathSync(process.argv[1])).href === moduleUrl;
4289
+ return Boolean(process.argv[1]) && pathToFileURL(fs10.realpathSync(process.argv[1])).href === moduleUrl;
3538
4290
  } catch {
3539
4291
  return false;
3540
4292
  }
@@ -3543,6 +4295,7 @@ function createArtifactServer(kind, moduleUrl) {
3543
4295
  KIND = kind.kind;
3544
4296
  NOUN = kind.noun;
3545
4297
  INDEX_FILE = kind.indexFile;
4298
+ DESIGN_SYSTEMS = kind.designSystems === true;
3546
4299
  LAYOUT = kind.layout;
3547
4300
  pageScriptSrc = kind.pageScriptSrc;
3548
4301
  sessionExtras = kind.sessionExtras;
@@ -3550,7 +4303,7 @@ function createArtifactServer(kind, moduleUrl) {
3550
4303
  kindLabels = kind.labels;
3551
4304
  kindAgentContext = kind.agentContext;
3552
4305
  SCRIPT = fileURLToPath(moduleUrl);
3553
- EDITOR_DIR = path6.join(path6.dirname(SCRIPT), "editor");
4306
+ EDITOR_DIR = path10.join(path10.dirname(SCRIPT), "editor");
3554
4307
  const runIfMain = () => {
3555
4308
  if (!isMain(moduleUrl)) return;
3556
4309
  main(process.argv.slice(2)).then(
@@ -3600,7 +4353,7 @@ function commentLocation2(input, projectDir) {
3600
4353
  });
3601
4354
  let boards = [];
3602
4355
  try {
3603
- const listed = JSON.parse(fs8.readFileSync(path7.join(projectDir, INDEX_FILE2), "utf8")).boards;
4356
+ const listed = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE2), "utf8")).boards;
3604
4357
  if (listed && typeof listed === "object") boards = Object.keys(listed);
3605
4358
  } catch {
3606
4359
  }
@@ -3611,7 +4364,7 @@ function commentLocation2(input, projectDir) {
3611
4364
  function labels(projectDir) {
3612
4365
  const titles = /* @__PURE__ */ new Map();
3613
4366
  try {
3614
- const listed = JSON.parse(fs8.readFileSync(path7.join(projectDir, INDEX_FILE2), "utf8")).boards;
4367
+ const listed = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE2), "utf8")).boards;
3615
4368
  if (listed && typeof listed === "object") {
3616
4369
  for (const [file, board] of Object.entries(listed)) {
3617
4370
  const title = board?.title;
@@ -3635,6 +4388,7 @@ var server = createArtifactServer(
3635
4388
  kind: KIND2,
3636
4389
  noun: NOUN2,
3637
4390
  indexFile: INDEX_FILE2,
4391
+ designSystems: true,
3638
4392
  layout: LAYOUT2,
3639
4393
  /** Boards run inline scripts, compile JSX with Babel (eval) and load from blob:. */
3640
4394
  pageScriptSrc: () => "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",