dsh-claude-design-system 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// dsh-claude-design-system 2.7.
|
|
1
|
+
// dsh-claude-design-system 2.7.2: built from baoyu-design.
|
|
2
2
|
var define_PLUGIN_default = { name: "claude-design-system", provider: "dsh-claude-design-system", skills: [{ name: "claude-design-system", description: "Create and revise design systems in the Design System artifact format (project/design-system.json, tokens.json, a README brand book, component previews, fonts and asset groups) as local files, then open them in a local design-system page where the user can browse and edit tokens, type, components and assets. Use when the user asks for a design system or brand kit in this format, built from a code repository, a design tool or scratch, names claude-design-system or the Design System type, or wants to revise a folder that holds project/design-system.json. claude-slides and claude-design install these systems. A free-form HTML style guide, UI kit or token page is baoyu-design's.", comments: true }] };
|
|
3
3
|
|
|
4
4
|
import { spawn as spawn2 } from "node:child_process";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { statSync } from "node:fs";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
|
|
8
|
+
import { basename as basename2, delimiter, dirname, isAbsolute, join as join2, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
@@ -345,13 +345,19 @@ function openedEditor(script, exec, result) {
|
|
|
345
345
|
const args = exec.arguments;
|
|
346
346
|
const command = args?.command;
|
|
347
347
|
if (typeof command !== "string" || args?.run_in_background === true) return null;
|
|
348
|
-
|
|
349
|
-
if (at < 0 || !/(?:^|[\s"'])open(?:[\s"']|$)/.test(command.slice(at + script.length))) return null;
|
|
348
|
+
if (!/(?:^|[\s"'])open(?:[\s"']|$)/.test(command)) return null;
|
|
350
349
|
const stdout = bashStdout(result);
|
|
351
350
|
const opened = stdout === null ? void 0 : jsonObjects(stdout).findLast((o) => typeof o.root === "string" && typeof o.url === "string");
|
|
352
351
|
if (typeof opened?.root !== "string" || typeof opened.url !== "string") return null;
|
|
352
|
+
if (!ranScript(script, command, opened.stop)) return null;
|
|
353
353
|
return { session: String(exec.agent.id), root: opened.root, url: opened.url };
|
|
354
354
|
}
|
|
355
|
+
function ranScript(script, command, stop) {
|
|
356
|
+
if (typeof stop !== "string") return command.includes(script);
|
|
357
|
+
if (stop.includes(script)) return true;
|
|
358
|
+
const folder = basename2(dirname(dirname(script)));
|
|
359
|
+
return new RegExp(`[/\\\\]${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[/\\\\]scripts[/\\\\]artifact\\.mjs`).test(stop);
|
|
360
|
+
}
|
|
355
361
|
var bound = (text) => text.length <= SUMMARY_MAX ? text : `${text.slice(0, SUMMARY_MAX - 1)}…`;
|
|
356
362
|
function commentMessage(skill, plugin, out, root) {
|
|
357
363
|
const title = typeof out.title === "string" && out.title ? out.title : root.split(/[\\/]/).pop() || root;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-claude-design-system",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"description": "DeepSeek Harness plugin: the claude-design-system skill from baoyu-design. The agent writes design systems (tokens, type, components, assets) as files and opens them in a local design-system page.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// claude-design-system: local Artifact tool (prebuilt; do not edit).
|
|
3
3
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import
|
|
5
|
+
import fs12 from "node:fs";
|
|
6
6
|
import fsp3 from "node:fs/promises";
|
|
7
|
-
import
|
|
7
|
+
import path14 from "node:path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
9
9
|
|
|
10
10
|
var BLOB_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
@@ -2654,7 +2654,7 @@ async function archiveEntries(root, skip = []) {
|
|
|
2654
2654
|
const folder = path12.basename(path12.resolve(root)) || "artifact";
|
|
2655
2655
|
const skipped = new Set(skip.map((file) => path12.resolve(file)));
|
|
2656
2656
|
const entries = [];
|
|
2657
|
-
const
|
|
2657
|
+
const walk2 = async (dir, rel) => {
|
|
2658
2658
|
let names;
|
|
2659
2659
|
try {
|
|
2660
2660
|
names = await fsp2.readdir(dir, { withFileTypes: true });
|
|
@@ -2668,14 +2668,14 @@ async function archiveEntries(root, skip = []) {
|
|
|
2668
2668
|
const file = path12.join(dir, name);
|
|
2669
2669
|
if (skipped.has(file)) continue;
|
|
2670
2670
|
const inner = rel ? `${rel}/${name}` : name;
|
|
2671
|
-
if (dirent.isDirectory()) await
|
|
2671
|
+
if (dirent.isDirectory()) await walk2(file, inner);
|
|
2672
2672
|
else if (dirent.isFile()) {
|
|
2673
2673
|
const stat = await fsp2.stat(file).catch(() => null);
|
|
2674
2674
|
if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
|
|
2675
2675
|
}
|
|
2676
2676
|
}
|
|
2677
2677
|
};
|
|
2678
|
-
await
|
|
2678
|
+
await walk2(path12.resolve(root), "");
|
|
2679
2679
|
if (entries.length > MAX_ENTRIES) throw new ArchiveTooLarge(`The folder holds ${entries.length} files, more than a zip holds (${MAX_ENTRIES}); copy the folder itself instead.`);
|
|
2680
2680
|
const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
|
|
2681
2681
|
if (bound > MAX_BYTES) throw new ArchiveTooLarge(`The folder holds ${(bound / 2 ** 30).toFixed(1)} GiB, more than a zip holds (4 GiB); copy the folder itself instead.`);
|
|
@@ -2807,6 +2807,156 @@ async function archiveCommand(root, out) {
|
|
|
2807
2807
|
return { file, folder: path12.basename(path12.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
|
|
2808
2808
|
}
|
|
2809
2809
|
|
|
2810
|
+
import fs11 from "node:fs";
|
|
2811
|
+
import path13 from "node:path";
|
|
2812
|
+
var FOLDER = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
2813
|
+
var MAX_FILES = 2e3;
|
|
2814
|
+
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
2815
|
+
var DESIGN_SYSTEM_FILE2 = "design-system.json";
|
|
2816
|
+
function readRecords(project, indexFile) {
|
|
2817
|
+
let index;
|
|
2818
|
+
try {
|
|
2819
|
+
index = JSON.parse(fs11.readFileSync(path13.join(project, indexFile), "utf8"));
|
|
2820
|
+
} catch {
|
|
2821
|
+
return [];
|
|
2822
|
+
}
|
|
2823
|
+
const list = index?.designSystems;
|
|
2824
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
2825
|
+
const out = [];
|
|
2826
|
+
for (const raw of records) {
|
|
2827
|
+
const r = raw;
|
|
2828
|
+
if (typeof r?.namespace !== "string" || !FOLDER.test(r.namespace)) continue;
|
|
2829
|
+
out.push({
|
|
2830
|
+
namespace: r.namespace,
|
|
2831
|
+
title: typeof r.title === "string" ? r.title : r.namespace,
|
|
2832
|
+
artifact: typeof r.artifact === "string" ? r.artifact : ""
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
return out;
|
|
2836
|
+
}
|
|
2837
|
+
function plain(p, want) {
|
|
2838
|
+
const stat = fs11.lstatSync(p, { throwIfNoEntry: false });
|
|
2839
|
+
return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
|
|
2840
|
+
}
|
|
2841
|
+
function walk(dir, prefix = "", out = []) {
|
|
2842
|
+
for (const entry of fs11.readdirSync(path13.join(dir, prefix), { withFileTypes: true })) {
|
|
2843
|
+
if (out.length >= MAX_FILES) break;
|
|
2844
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2845
|
+
if (entry.isDirectory()) walk(dir, rel, out);
|
|
2846
|
+
else if (entry.isFile()) out.push(rel);
|
|
2847
|
+
}
|
|
2848
|
+
return out;
|
|
2849
|
+
}
|
|
2850
|
+
var projectOf = (candidate) => path13.basename(candidate) === "project" ? candidate : path13.join(candidate, "project");
|
|
2851
|
+
function sourceProject(artifact, root, from) {
|
|
2852
|
+
const given = from ?? artifact;
|
|
2853
|
+
if (!given) return { reason: "the record names no source folder", kind: "none" };
|
|
2854
|
+
if (!from && /^[a-z][a-z0-9+.-]*:\/\//i.test(given)) return { reason: `its source is ${given}, which is not a folder on this machine`, kind: "remote" };
|
|
2855
|
+
const candidates = path13.isAbsolute(given) ? [given] : [path13.resolve(given), path13.resolve(root, given), path13.resolve(root, "..", given)];
|
|
2856
|
+
for (const candidate of candidates) {
|
|
2857
|
+
const project = projectOf(candidate);
|
|
2858
|
+
if (plain(project, "dir")) return { project };
|
|
2859
|
+
}
|
|
2860
|
+
if (!from && !path13.isAbsolute(given)) {
|
|
2861
|
+
let dir = path13.resolve(root, "..");
|
|
2862
|
+
for (let up = path13.dirname(dir); up !== dir; up = path13.dirname(dir)) {
|
|
2863
|
+
dir = up;
|
|
2864
|
+
const project = projectOf(path13.resolve(dir, given));
|
|
2865
|
+
if (plain(path13.join(project, DESIGN_SYSTEM_FILE2), "file")) return { project };
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
return { reason: `its source folder ${given} was not found${from ? "" : ` (tried from ${process.cwd()} and the folders above the artifact); pass --from <folder>`}`, kind: "not-found" };
|
|
2869
|
+
}
|
|
2870
|
+
function compareSystem(project, root, record, from) {
|
|
2871
|
+
const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
|
|
2872
|
+
const found = sourceProject(record.artifact, root, from);
|
|
2873
|
+
if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
|
|
2874
|
+
diff.source = path13.dirname(found.project);
|
|
2875
|
+
const copy = path13.join(project, "ds", record.namespace);
|
|
2876
|
+
if (!plain(copy, "dir")) return { ...diff, missing: true };
|
|
2877
|
+
for (const rel of walk(copy)) {
|
|
2878
|
+
const mine = path13.join(copy, rel);
|
|
2879
|
+
const theirs = path13.join(found.project, rel);
|
|
2880
|
+
if (!plain(mine, "file")) continue;
|
|
2881
|
+
if (!plain(theirs, "file")) {
|
|
2882
|
+
diff.removed.push(rel);
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
const a = fs11.statSync(mine);
|
|
2886
|
+
const b = fs11.statSync(theirs);
|
|
2887
|
+
if (a.size > MAX_FILE_BYTES || b.size > MAX_FILE_BYTES) continue;
|
|
2888
|
+
if (a.size !== b.size || !fs11.readFileSync(mine).equals(fs11.readFileSync(theirs))) diff.changed.push(rel);
|
|
2889
|
+
}
|
|
2890
|
+
return diff;
|
|
2891
|
+
}
|
|
2892
|
+
function pick(records, namespace) {
|
|
2893
|
+
if (namespace === void 0) return records;
|
|
2894
|
+
const one = records.filter((r) => r.namespace === namespace);
|
|
2895
|
+
if (one.length === 0) {
|
|
2896
|
+
const names = records.map((r) => r.namespace);
|
|
2897
|
+
throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
|
|
2898
|
+
}
|
|
2899
|
+
return one;
|
|
2900
|
+
}
|
|
2901
|
+
function compareSystems(project, root, indexFile, namespace, from) {
|
|
2902
|
+
return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
|
|
2903
|
+
}
|
|
2904
|
+
function staleNotes(project, root, indexFile, command) {
|
|
2905
|
+
const notes = [];
|
|
2906
|
+
for (const diff of compareSystems(project, root, indexFile)) {
|
|
2907
|
+
if (diff.changed.length === 0 && diff.removed.length === 0) continue;
|
|
2908
|
+
const parts = [];
|
|
2909
|
+
if (diff.changed.length > 0) parts.push(`${diff.changed.length} changed (${diff.changed.slice(0, 3).join(", ")}${diff.changed.length > 3 ? ", \u2026" : ""})`);
|
|
2910
|
+
if (diff.removed.length > 0) parts.push(`${diff.removed.length} gone from the source`);
|
|
2911
|
+
notes.push(
|
|
2912
|
+
`project/ds/${diff.namespace}/ is behind its source system ${diff.source}: ${parts.join(", ")}. A copy never changes by itself: run \`${command(diff.namespace, false)}\` to copy the changed files again (\`${command(diff.namespace, true)}\` lists them).`
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
return notes;
|
|
2916
|
+
}
|
|
2917
|
+
function stamp(project, indexFile, namespaces, now) {
|
|
2918
|
+
const file = path13.join(project, indexFile);
|
|
2919
|
+
const text = fs11.readFileSync(file, "utf8");
|
|
2920
|
+
const index = JSON.parse(text);
|
|
2921
|
+
const list = index.designSystems;
|
|
2922
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
2923
|
+
for (const r of records) {
|
|
2924
|
+
const record = r;
|
|
2925
|
+
if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
|
|
2926
|
+
}
|
|
2927
|
+
const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
|
|
2928
|
+
const out = JSON.stringify(index, null, indent);
|
|
2929
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2930
|
+
fs11.writeFileSync(tmp, text.endsWith("\n") ? `${out}
|
|
2931
|
+
` : out);
|
|
2932
|
+
fs11.renameSync(tmp, file);
|
|
2933
|
+
}
|
|
2934
|
+
function syncSystems(project, root, indexFile, opts) {
|
|
2935
|
+
const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
|
|
2936
|
+
const results = [];
|
|
2937
|
+
const synced = /* @__PURE__ */ new Set();
|
|
2938
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2939
|
+
for (const diff of diffs) {
|
|
2940
|
+
const result = { ...diff, copied: [] };
|
|
2941
|
+
if (!opts.check && diff.source && !diff.missing) {
|
|
2942
|
+
for (const rel of diff.changed) {
|
|
2943
|
+
const target = path13.join(project, "ds", diff.namespace, rel);
|
|
2944
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
2945
|
+
fs11.copyFileSync(path13.join(diff.source, "project", rel), tmp);
|
|
2946
|
+
fs11.renameSync(tmp, target);
|
|
2947
|
+
result.copied.push(rel);
|
|
2948
|
+
}
|
|
2949
|
+
if (diff.changed.length > 0) synced.add(diff.namespace);
|
|
2950
|
+
}
|
|
2951
|
+
results.push(result);
|
|
2952
|
+
}
|
|
2953
|
+
if (synced.size > 0) {
|
|
2954
|
+
stamp(project, indexFile, synced, now);
|
|
2955
|
+
for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
|
|
2956
|
+
}
|
|
2957
|
+
return results;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2810
2960
|
async function shareCall(url, action, fail2) {
|
|
2811
2961
|
const response = await fetch(new URL("/__share", url), action ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) } : {});
|
|
2812
2962
|
const body = await response.json().catch(() => ({}));
|
|
@@ -2888,8 +3038,8 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
2888
3038
|
});
|
|
2889
3039
|
var KIND = "design-system";
|
|
2890
3040
|
var SCRIPT = fileURLToPath2(import.meta.url);
|
|
2891
|
-
var EDITOR_DIR =
|
|
2892
|
-
var SERVER_SCRIPT =
|
|
3041
|
+
var EDITOR_DIR = path14.join(path14.dirname(SCRIPT), "editor");
|
|
3042
|
+
var SERVER_SCRIPT = path14.join(path14.dirname(SCRIPT), "server.mjs");
|
|
2893
3043
|
var MAX_LISTED = 200;
|
|
2894
3044
|
var UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
|
|
2895
3045
|
var INDEX_FILE = KIND === "slides" ? "deck.json" : KIND === "design" ? CANVAS_FILE : DESIGN_SYSTEM_FILE;
|
|
@@ -2902,7 +3052,7 @@ var CliError = class extends Error {
|
|
|
2902
3052
|
this.code = code;
|
|
2903
3053
|
}
|
|
2904
3054
|
};
|
|
2905
|
-
var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off"]);
|
|
3055
|
+
var SWITCHES = /* @__PURE__ */ new Set(["all", "wait", "resolve", "reopen", "lan", "off", "check"]);
|
|
2906
3056
|
function parseArgs(argv) {
|
|
2907
3057
|
const positional = [];
|
|
2908
3058
|
const flags = /* @__PURE__ */ new Map();
|
|
@@ -2928,13 +3078,13 @@ function quote(s) {
|
|
|
2928
3078
|
}
|
|
2929
3079
|
function resolveArtifact(given, mustExist = true) {
|
|
2930
3080
|
if (!given) throw new CliError("MISSING_ROOT", `Give the artifact folder: the one that holds project/${INDEX_FILE}.`);
|
|
2931
|
-
let root =
|
|
2932
|
-
if (
|
|
2933
|
-
root =
|
|
3081
|
+
let root = path14.resolve(given);
|
|
3082
|
+
if (path14.basename(root) === "project" && fs12.existsSync(path14.join(root, INDEX_FILE)) && !fs12.existsSync(path14.join(root, "project"))) {
|
|
3083
|
+
root = path14.dirname(root);
|
|
2934
3084
|
}
|
|
2935
|
-
const project =
|
|
3085
|
+
const project = path14.join(root, "project");
|
|
2936
3086
|
if (mustExist) {
|
|
2937
|
-
const stat =
|
|
3087
|
+
const stat = fs12.statSync(project, { throwIfNoEntry: false });
|
|
2938
3088
|
if (!stat?.isDirectory()) {
|
|
2939
3089
|
throw new CliError("NOT_FOUND", `${project} does not exist. Write project/${INDEX_FILE} and the other files under ${root} first.`);
|
|
2940
3090
|
}
|
|
@@ -2956,7 +3106,7 @@ function localize(value) {
|
|
|
2956
3106
|
function designSystemNotes(project, indexFile) {
|
|
2957
3107
|
let index;
|
|
2958
3108
|
try {
|
|
2959
|
-
index = JSON.parse(
|
|
3109
|
+
index = JSON.parse(fs12.readFileSync(path14.join(project, indexFile), "utf8"));
|
|
2960
3110
|
} catch {
|
|
2961
3111
|
return [];
|
|
2962
3112
|
}
|
|
@@ -2970,14 +3120,15 @@ function designSystemNotes(project, indexFile) {
|
|
|
2970
3120
|
notes.push(`designSystems namespace ${JSON.stringify(ns)} is not a folder name ([a-z0-9][a-z0-9_-]{0,63}): the page skips it. Use the lower-case folder (list prints it as folder), the same in the record and in project/ds/<folder>/.`);
|
|
2971
3121
|
continue;
|
|
2972
3122
|
}
|
|
2973
|
-
const folder =
|
|
2974
|
-
if (!
|
|
3123
|
+
const folder = path14.join(project, "ds", ns);
|
|
3124
|
+
if (!fs12.existsSync(folder)) {
|
|
2975
3125
|
notes.push(`designSystems names "${ns}" but project/ds/${ns}/ does not exist: copy the system's project/tokens.json (and the files you use) there, or take the record out.`);
|
|
2976
|
-
} else if (!
|
|
3126
|
+
} else if (!fs12.existsSync(path14.join(folder, "tokens.json"))) {
|
|
2977
3127
|
notes.push(`project/ds/${ns}/ has no tokens.json, so the Theme and color menus show bare hexes: copy the system's project/tokens.json there (a system with none: nothing to do).`);
|
|
2978
3128
|
}
|
|
2979
3129
|
}
|
|
2980
|
-
|
|
3130
|
+
const root = path14.dirname(project);
|
|
3131
|
+
return [...notes, ...staleNotes(project, root, indexFile, (ns, listOnly) => script("ds-sync", root, ns, ...listOnly ? ["--check"] : []))];
|
|
2981
3132
|
}
|
|
2982
3133
|
async function check(a) {
|
|
2983
3134
|
if (KIND === "slides") {
|
|
@@ -2985,9 +3136,9 @@ async function check(a) {
|
|
|
2985
3136
|
try {
|
|
2986
3137
|
rt = loadSlidesCompiler(EDITOR_DIR);
|
|
2987
3138
|
} catch {
|
|
2988
|
-
throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${
|
|
3139
|
+
throw new CliError("COMPILER_MISSING", `The slides compiler is missing (${path14.join(EDITOR_DIR, "node", "compiler.cjs")}); reinstall the skill.`);
|
|
2989
3140
|
}
|
|
2990
|
-
if (!
|
|
3141
|
+
if (!fs12.existsSync(path14.join(a.project, "deck.json"))) {
|
|
2991
3142
|
return {
|
|
2992
3143
|
problems: 1,
|
|
2993
3144
|
result: {
|
|
@@ -3026,7 +3177,7 @@ async function check(a) {
|
|
|
3026
3177
|
...slides.length > 0 ? { slides } : {},
|
|
3027
3178
|
...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
|
|
3028
3179
|
notChecked: "Text sizes, contrast, whether a section id matches its file name and whether an image path exists are not checked (nothing is rendered).",
|
|
3029
|
-
next: problems2 > 0 ? `Fix the files named in deckProblems and in each slide's problems (paths are under project/), then run check again until problems is 0. notes are hints and lose no content; fix the layout ones (starting "\u6392\u7248") and the warnings (starting "\u8B66\u544A": a font or image falls back) too.` : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (layoutNotes ? ' Some slides carry layout notes ("\u6392\u7248": content may overflow or overlap) or warnings ("\u8B66\u544A": a font or image falls back); fix them and check again.' : "") + (report.orphans.length > 0 ? " orphans are slide files missing from order: they do not show; delete the unneeded ones." : "") + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete." : "")
|
|
3180
|
+
next: problems2 > 0 ? `Fix the files named in deckProblems and in each slide's problems (paths are under project/), then run check again until problems is 0. notes are hints and lose no content; fix the layout ones (starting "\u6392\u7248") and the warnings (starting "\u8B66\u544A": a font or image falls back) too.` : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (layoutNotes ? ' Some slides carry layout notes ("\u6392\u7248": content may overflow or overlap) or warnings ("\u8B66\u544A": a font or image falls back); fix them and check again.' : "") + (report.orphans.length > 0 ? " orphans are slide files missing from order: they do not show; delete the unneeded ones." : "") + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete or behind its source." : "")
|
|
3030
3181
|
}
|
|
3031
3182
|
};
|
|
3032
3183
|
}
|
|
@@ -3045,17 +3196,17 @@ async function check(a) {
|
|
|
3045
3196
|
...problems2 > MAX_LISTED ? { more: `${problems2 - MAX_LISTED} more not listed: fix these, then check again` } : {},
|
|
3046
3197
|
...dsNotes.length > 0 ? { designSystemNotes: dsNotes } : {},
|
|
3047
3198
|
notChecked: "Whether the artboard scripts run, and how the layout looks, is not checked (nothing is rendered).",
|
|
3048
|
-
next: problems2 > 0 ? 'Fix the files per issues (file is a path under project/; "." is the whole folder), then run check again until problems is 0.' : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete." : "")
|
|
3199
|
+
next: problems2 > 0 ? 'Fix the files per issues (file is a path under project/; "." is the whole folder), then run check again until problems is 0.' : `No problems. Show it to the user: ${script("open", a.root)} prints the editor URL.` + (dsNotes.length > 0 ? " Read designSystemNotes: a design system install looks incomplete or behind its source." : "")
|
|
3049
3200
|
}
|
|
3050
3201
|
};
|
|
3051
3202
|
}
|
|
3052
3203
|
const r = await checkDesignSystem(a.project);
|
|
3053
3204
|
const problems = r.issues.length;
|
|
3054
|
-
if (
|
|
3055
|
-
if (!
|
|
3205
|
+
if (fs12.existsSync(path14.join(a.project, DESIGN_SYSTEM_FILE))) {
|
|
3206
|
+
if (!fs12.existsSync(path14.join(a.project, "README.md"))) {
|
|
3056
3207
|
r.notes.push({ file: "README.md", message: "missing: every system includes project/README.md; every reader starts there (Creating, step 1)" });
|
|
3057
3208
|
}
|
|
3058
|
-
if (!
|
|
3209
|
+
if (!fs12.existsSync(path14.join(a.project, "components", "Cover", "preview.html"))) {
|
|
3059
3210
|
r.notes.push({ file: "components/Cover/preview.html", message: "missing: every system gets a cover, written last before the index (cover.md)" });
|
|
3060
3211
|
}
|
|
3061
3212
|
}
|
|
@@ -3081,11 +3232,11 @@ function formatBytes(n) {
|
|
|
3081
3232
|
}
|
|
3082
3233
|
async function upload(a, given, group) {
|
|
3083
3234
|
if (!given) throw new CliError("MISSING_FILE", "Give the file to upload.");
|
|
3084
|
-
const file =
|
|
3235
|
+
const file = path14.resolve(given);
|
|
3085
3236
|
const stat = await fsp3.stat(file).catch(() => null);
|
|
3086
3237
|
if (!stat) throw new CliError("FILE_NOT_FOUND", `No such file: ${given}`);
|
|
3087
3238
|
if (!stat.isFile()) throw new CliError("NOT_A_FILE", `${given} is not a regular file.`);
|
|
3088
|
-
const ext =
|
|
3239
|
+
const ext = path14.extname(file).slice(1).toLowerCase();
|
|
3089
3240
|
const type = contentTypeOf(file);
|
|
3090
3241
|
const isMedia = type.startsWith("image/") || type.startsWith("font/");
|
|
3091
3242
|
let limit = UPLOAD_MAX_BYTES;
|
|
@@ -3110,7 +3261,7 @@ async function upload(a, given, group) {
|
|
|
3110
3261
|
if (stat.size > limit) throw new CliError("FILE_TOO_LARGE", `${given} is ${formatBytes(stat.size)}, over the ${formatBytes(limit)} limit: compress or shrink it first.`);
|
|
3111
3262
|
const bytes = await fsp3.readFile(file);
|
|
3112
3263
|
const uploaded = await new DeckStore(a.project, LAYOUT).putBlob(bytes, type);
|
|
3113
|
-
const base =
|
|
3264
|
+
const base = path14.basename(file);
|
|
3114
3265
|
if (KIND !== "design-system") {
|
|
3115
3266
|
return {
|
|
3116
3267
|
url: uploaded.url,
|
|
@@ -3142,12 +3293,12 @@ function folderName(name) {
|
|
|
3142
3293
|
}
|
|
3143
3294
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target"]);
|
|
3144
3295
|
async function listSystems(dirs) {
|
|
3145
|
-
const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(
|
|
3146
|
-
const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) =>
|
|
3296
|
+
const extra = (process.env.CLAUDE_DESIGN_SYSTEMS ?? "").split(path14.delimiter).filter(Boolean);
|
|
3297
|
+
const starts = [...dirs.length > 0 ? dirs : [process.cwd()], ...extra].map((d) => path14.resolve(d));
|
|
3147
3298
|
const found = /* @__PURE__ */ new Map();
|
|
3148
3299
|
const visit = async (dir, depth) => {
|
|
3149
|
-
const project =
|
|
3150
|
-
if (
|
|
3300
|
+
const project = path14.join(dir, "project");
|
|
3301
|
+
if (fs12.existsSync(path14.join(project, DESIGN_SYSTEM_FILE)) && !found.has(dir)) {
|
|
3151
3302
|
const info = await readDesignSystemInfo(project).catch(() => null);
|
|
3152
3303
|
if (info) {
|
|
3153
3304
|
const namespace = info.namespace;
|
|
@@ -3156,8 +3307,8 @@ async function listSystems(dirs) {
|
|
|
3156
3307
|
namespace: namespace ?? null,
|
|
3157
3308
|
folder: folderName(typeof namespace === "string" && namespace.trim() ? namespace : String(info.title ?? "")),
|
|
3158
3309
|
root: dir,
|
|
3159
|
-
readme:
|
|
3160
|
-
tokens:
|
|
3310
|
+
readme: fs12.existsSync(path14.join(project, "README.md")) ? path14.join(project, "README.md") : null,
|
|
3311
|
+
tokens: fs12.existsSync(path14.join(project, "tokens.json")) ? path14.join(project, "tokens.json") : null
|
|
3161
3312
|
});
|
|
3162
3313
|
return;
|
|
3163
3314
|
}
|
|
@@ -3166,7 +3317,7 @@ async function listSystems(dirs) {
|
|
|
3166
3317
|
const entries = await fsp3.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
3167
3318
|
for (const entry of entries) {
|
|
3168
3319
|
if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP_DIRS.has(entry.name) || entry.name === "project") continue;
|
|
3169
|
-
await visit(
|
|
3320
|
+
await visit(path14.join(dir, entry.name), depth - 1);
|
|
3170
3321
|
}
|
|
3171
3322
|
};
|
|
3172
3323
|
for (const start of starts) await visit(start, 4);
|
|
@@ -3190,10 +3341,10 @@ function reusedNote(a, flags) {
|
|
|
3190
3341
|
return flags.get("agent-hook") ? { note: `--agent-hook was not applied: the server was already running. To use it: ${script("stop", a.root)}, then open again.` } : {};
|
|
3191
3342
|
}
|
|
3192
3343
|
function assertServerFiles() {
|
|
3193
|
-
if (!
|
|
3344
|
+
if (!fs12.existsSync(path14.join(EDITOR_DIR, "index.html"))) {
|
|
3194
3345
|
throw new CliError("EDITOR_MISSING", `The editor page is missing (${EDITOR_DIR}); reinstall the skill.`);
|
|
3195
3346
|
}
|
|
3196
|
-
if (!
|
|
3347
|
+
if (!fs12.existsSync(SERVER_SCRIPT)) {
|
|
3197
3348
|
throw new CliError("EDITOR_MISSING", `The editor server is missing (${SERVER_SCRIPT}); reinstall the skill.`);
|
|
3198
3349
|
}
|
|
3199
3350
|
}
|
|
@@ -3221,21 +3372,21 @@ async function open(a, flags) {
|
|
|
3221
3372
|
assertServerFiles();
|
|
3222
3373
|
const existing = await runningServer2(a.root);
|
|
3223
3374
|
if (existing) {
|
|
3224
|
-
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), ...reusedNote(a, flags) });
|
|
3375
|
+
print({ url: existing.url, root: a.root, pid: existing.pid, reused: true, ...await lanOn2(existing.url, flags), log: path14.join(a.root, LOG_FILE), stop: script("stop", a.root), ...reusedNote(a, flags) });
|
|
3225
3376
|
return;
|
|
3226
3377
|
}
|
|
3227
3378
|
const state = await startBackground(a, flags);
|
|
3228
3379
|
const view = await shareCall2(state.url, null).catch(() => null);
|
|
3229
|
-
print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log:
|
|
3380
|
+
print({ url: state.url, root: a.root, pid: state.pid, ...view?.lan ? { lan: lanUrls(view) } : {}, log: path14.join(a.root, LOG_FILE), stop: script("stop", a.root) });
|
|
3230
3381
|
}
|
|
3231
3382
|
async function startBackground(a, flags) {
|
|
3232
3383
|
const port = Number(flags.get("port") ?? 0);
|
|
3233
3384
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new CliError("INVALID_PORT", "--port is a number from 0 to 65535.");
|
|
3234
|
-
const logFile =
|
|
3235
|
-
const fd =
|
|
3385
|
+
const logFile = path14.join(a.root, LOG_FILE);
|
|
3386
|
+
const fd = fs12.openSync(logFile, "a");
|
|
3236
3387
|
const child = spawn(process.execPath, [SERVER_SCRIPT, a.root, ...serverArgs(flags)], { detached: true, stdio: ["ignore", "ignore", fd] });
|
|
3237
3388
|
child.unref();
|
|
3238
|
-
|
|
3389
|
+
fs12.closeSync(fd);
|
|
3239
3390
|
let exited = false;
|
|
3240
3391
|
child.once("exit", () => exited = true);
|
|
3241
3392
|
const deadline = Date.now() + 15e3;
|
|
@@ -3245,14 +3396,14 @@ async function startBackground(a, flags) {
|
|
|
3245
3396
|
if (state && state.pid === child.pid) return state;
|
|
3246
3397
|
if (exited) break;
|
|
3247
3398
|
}
|
|
3248
|
-
const tail =
|
|
3399
|
+
const tail = fs12.existsSync(logFile) ? fs12.readFileSync(logFile, "utf8").split("\n").slice(-12).join("\n") : "";
|
|
3249
3400
|
throw new CliError("SERVER_FAILED", `The editor server did not start. Run ${script("serve", a.root)} in the foreground to see why.${tail ? `
|
|
3250
3401
|
${tail}` : ""}`);
|
|
3251
3402
|
}
|
|
3252
3403
|
async function stop(a) {
|
|
3253
3404
|
const state = readState(a.root);
|
|
3254
3405
|
if (!state || !alive(state.pid)) {
|
|
3255
|
-
|
|
3406
|
+
fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
|
|
3256
3407
|
print({ stopped: false, message: "No editor server is running for this folder." });
|
|
3257
3408
|
return;
|
|
3258
3409
|
}
|
|
@@ -3263,7 +3414,7 @@ async function stop(a) {
|
|
|
3263
3414
|
print({ stopped: false, pid: state.pid, message: `The server did not exit within 5 s; end it with: kill ${state.pid}` });
|
|
3264
3415
|
return;
|
|
3265
3416
|
}
|
|
3266
|
-
|
|
3417
|
+
fs12.rmSync(path14.join(a.root, STATE_FILE), { force: true });
|
|
3267
3418
|
print({ stopped: true, pid: state.pid });
|
|
3268
3419
|
}
|
|
3269
3420
|
async function share(a, flags) {
|
|
@@ -3307,7 +3458,7 @@ async function listComments(a, all) {
|
|
|
3307
3458
|
async function artifactTitle(a) {
|
|
3308
3459
|
const info = await readInfo(a.project).catch(() => null);
|
|
3309
3460
|
const title = typeof info?.title === "string" ? info.title.trim() : "";
|
|
3310
|
-
return title && title !== "project" ? title :
|
|
3461
|
+
return title && title !== "project" ? title : path14.basename(a.root);
|
|
3311
3462
|
}
|
|
3312
3463
|
async function waitComments(a, flags) {
|
|
3313
3464
|
const timeout = flags.has("timeout") ? Number(flags.get("timeout")) : Infinity;
|
|
@@ -3403,7 +3554,10 @@ Usage:
|
|
|
3403
3554
|
node scripts/artifact.mjs check <root> validate project/; exit 1 while problems remain
|
|
3404
3555
|
node scripts/artifact.mjs upload <root> <file>${KIND === "design-system" ? " --group G " : " "} store an upload; prints its /_blob/<id> url
|
|
3405
3556
|
node scripts/artifact.mjs list [dir \u2026] find design systems (folders holding project/design-system.json)
|
|
3406
|
-
node scripts/artifact.mjs
|
|
3557
|
+
${KIND === "design-system" ? "" : ` node scripts/artifact.mjs ds-sync <root> [namespace] [--check] an installed design system is a copy: copy its changed files again
|
|
3558
|
+
from the system's folder and stamp copiedAt (--check: list only;
|
|
3559
|
+
--from <folder>: the source, when the record's path is not found)
|
|
3560
|
+
`} node scripts/artifact.mjs comments <root> [--all] the open comments people left in the editor (--all: resolved too)
|
|
3407
3561
|
node scripts/artifact.mjs comments <root> --wait [--timeout s] wait for a comment sent to the agent from the open editor
|
|
3408
3562
|
node scripts/artifact.mjs comment-reply <root> <id> <text|-> reply as the agent (-: text on stdin) [--resolve]
|
|
3409
3563
|
node scripts/artifact.mjs comment-resolve <root> <id> [--reopen] mark a comment resolved (or open again)
|
|
@@ -3434,16 +3588,43 @@ async function main() {
|
|
|
3434
3588
|
}
|
|
3435
3589
|
case "upload": {
|
|
3436
3590
|
const a = resolveArtifact(positional[0], false);
|
|
3437
|
-
if (!
|
|
3591
|
+
if (!fs12.statSync(a.root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
3438
3592
|
throw new CliError("NOT_FOUND", `${a.root} does not exist. Make the artifact folder first.`);
|
|
3439
3593
|
}
|
|
3440
|
-
|
|
3594
|
+
fs12.mkdirSync(a.project, { recursive: true });
|
|
3441
3595
|
print(await upload(a, positional[1], flags.get("group")));
|
|
3442
3596
|
return 0;
|
|
3443
3597
|
}
|
|
3444
3598
|
case "list":
|
|
3445
3599
|
print(await listSystems(positional));
|
|
3446
3600
|
return 0;
|
|
3601
|
+
case "ds-sync": {
|
|
3602
|
+
if (KIND === "design-system") throw new CliError("NOT_APPLICABLE", "A design system has no installed design systems: ds-sync is for decks and canvases.");
|
|
3603
|
+
const a = resolveArtifact(positional[0]);
|
|
3604
|
+
const check2 = flags.has("check");
|
|
3605
|
+
let results;
|
|
3606
|
+
try {
|
|
3607
|
+
results = syncSystems(a.project, a.root, INDEX_FILE, { namespace: positional[1], from: flags.get("from"), check: check2 });
|
|
3608
|
+
} catch (error) {
|
|
3609
|
+
throw new CliError("NO_SUCH_SYSTEM", error.message);
|
|
3610
|
+
}
|
|
3611
|
+
const behind = results.filter((r) => r.changed.length > 0 || r.removed.length > 0);
|
|
3612
|
+
print({
|
|
3613
|
+
root: a.root,
|
|
3614
|
+
systems: results.map((r) => ({
|
|
3615
|
+
namespace: r.namespace,
|
|
3616
|
+
title: r.title,
|
|
3617
|
+
...r.skipped ? { skipped: r.skipped } : {},
|
|
3618
|
+
...r.missing ? { skipped: `project/ds/${r.namespace}/ does not exist: install it first (SKILL.md, Creating, step 4)` } : {},
|
|
3619
|
+
...r.source ? { source: r.source } : {},
|
|
3620
|
+
changed: r.changed,
|
|
3621
|
+
...r.removed.length > 0 ? { goneFromSource: r.removed } : {},
|
|
3622
|
+
...check2 ? {} : { copied: r.copied, ...r.copiedAt ? { copiedAt: r.copiedAt } : {} }
|
|
3623
|
+
})),
|
|
3624
|
+
next: check2 ? behind.length > 0 ? `Behind its source. Run ${script("ds-sync", a.root)} to copy the changed files again.` : "Every copy matches its source (or its source was not found: see skipped)." : results.some((r) => r.copied.length > 0) ? "Copied. The open editor shows the new files at once (a file over 2 MB, or a types file, after a reload). Files the source dropped stay in the copy: remove one, and change the artboards that load it, if the source no longer has it. Files the source gained are not copied: compare its README with project/ds/<folder>/." : "Nothing to copy."
|
|
3625
|
+
});
|
|
3626
|
+
return check2 && behind.some((r) => r.changed.length > 0) ? 1 : 0;
|
|
3627
|
+
}
|
|
3447
3628
|
case "comments": {
|
|
3448
3629
|
const a = resolveArtifact(positional[0]);
|
|
3449
3630
|
print(flags.has("wait") ? await waitComments(a, flags) : await listComments(a, flags.has("all")));
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import os5 from "node:os";
|
|
5
5
|
|
|
6
6
|
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID } from "node:crypto";
|
|
7
|
-
import
|
|
7
|
+
import fs11 from "node:fs";
|
|
8
8
|
import fsp4 from "node:fs/promises";
|
|
9
9
|
import http from "node:http";
|
|
10
|
-
import
|
|
10
|
+
import path11 from "node:path";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
@@ -1495,9 +1495,155 @@ ${sections || '<p class="muted">This copy has no tokens, components or guide.</p
|
|
|
1495
1495
|
`;
|
|
1496
1496
|
}
|
|
1497
1497
|
|
|
1498
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
1499
1498
|
import fs5 from "node:fs";
|
|
1500
1499
|
import path5 from "node:path";
|
|
1500
|
+
var FOLDER2 = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
1501
|
+
var MAX_FILES = 2e3;
|
|
1502
|
+
var MAX_FILE_BYTES2 = 64 * 1024 * 1024;
|
|
1503
|
+
var DESIGN_SYSTEM_FILE = "design-system.json";
|
|
1504
|
+
function readRecords(project, indexFile) {
|
|
1505
|
+
let index;
|
|
1506
|
+
try {
|
|
1507
|
+
index = JSON.parse(fs5.readFileSync(path5.join(project, indexFile), "utf8"));
|
|
1508
|
+
} catch {
|
|
1509
|
+
return [];
|
|
1510
|
+
}
|
|
1511
|
+
const list = index?.designSystems;
|
|
1512
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
1513
|
+
const out = [];
|
|
1514
|
+
for (const raw of records) {
|
|
1515
|
+
const r = raw;
|
|
1516
|
+
if (typeof r?.namespace !== "string" || !FOLDER2.test(r.namespace)) continue;
|
|
1517
|
+
out.push({
|
|
1518
|
+
namespace: r.namespace,
|
|
1519
|
+
title: typeof r.title === "string" ? r.title : r.namespace,
|
|
1520
|
+
artifact: typeof r.artifact === "string" ? r.artifact : ""
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
function plain(p, want) {
|
|
1526
|
+
const stat = fs5.lstatSync(p, { throwIfNoEntry: false });
|
|
1527
|
+
return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
|
|
1528
|
+
}
|
|
1529
|
+
function walk(dir, prefix = "", out = []) {
|
|
1530
|
+
for (const entry of fs5.readdirSync(path5.join(dir, prefix), { withFileTypes: true })) {
|
|
1531
|
+
if (out.length >= MAX_FILES) break;
|
|
1532
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1533
|
+
if (entry.isDirectory()) walk(dir, rel, out);
|
|
1534
|
+
else if (entry.isFile()) out.push(rel);
|
|
1535
|
+
}
|
|
1536
|
+
return out;
|
|
1537
|
+
}
|
|
1538
|
+
var projectOf = (candidate) => path5.basename(candidate) === "project" ? candidate : path5.join(candidate, "project");
|
|
1539
|
+
function sourceProject(artifact, root, from) {
|
|
1540
|
+
const given = from ?? artifact;
|
|
1541
|
+
if (!given) return { reason: "the record names no source folder", kind: "none" };
|
|
1542
|
+
if (!from && /^[a-z][a-z0-9+.-]*:\/\//i.test(given)) return { reason: `its source is ${given}, which is not a folder on this machine`, kind: "remote" };
|
|
1543
|
+
const candidates = path5.isAbsolute(given) ? [given] : [path5.resolve(given), path5.resolve(root, given), path5.resolve(root, "..", given)];
|
|
1544
|
+
for (const candidate of candidates) {
|
|
1545
|
+
const project = projectOf(candidate);
|
|
1546
|
+
if (plain(project, "dir")) return { project };
|
|
1547
|
+
}
|
|
1548
|
+
if (!from && !path5.isAbsolute(given)) {
|
|
1549
|
+
let dir = path5.resolve(root, "..");
|
|
1550
|
+
for (let up = path5.dirname(dir); up !== dir; up = path5.dirname(dir)) {
|
|
1551
|
+
dir = up;
|
|
1552
|
+
const project = projectOf(path5.resolve(dir, given));
|
|
1553
|
+
if (plain(path5.join(project, DESIGN_SYSTEM_FILE), "file")) return { project };
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return { reason: `its source folder ${given} was not found${from ? "" : ` (tried from ${process.cwd()} and the folders above the artifact); pass --from <folder>`}`, kind: "not-found" };
|
|
1557
|
+
}
|
|
1558
|
+
function compareSystem(project, root, record, from) {
|
|
1559
|
+
const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
|
|
1560
|
+
const found = sourceProject(record.artifact, root, from);
|
|
1561
|
+
if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
|
|
1562
|
+
diff.source = path5.dirname(found.project);
|
|
1563
|
+
const copy = path5.join(project, "ds", record.namespace);
|
|
1564
|
+
if (!plain(copy, "dir")) return { ...diff, missing: true };
|
|
1565
|
+
for (const rel of walk(copy)) {
|
|
1566
|
+
const mine = path5.join(copy, rel);
|
|
1567
|
+
const theirs = path5.join(found.project, rel);
|
|
1568
|
+
if (!plain(mine, "file")) continue;
|
|
1569
|
+
if (!plain(theirs, "file")) {
|
|
1570
|
+
diff.removed.push(rel);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const a = fs5.statSync(mine);
|
|
1574
|
+
const b = fs5.statSync(theirs);
|
|
1575
|
+
if (a.size > MAX_FILE_BYTES2 || b.size > MAX_FILE_BYTES2) continue;
|
|
1576
|
+
if (a.size !== b.size || !fs5.readFileSync(mine).equals(fs5.readFileSync(theirs))) diff.changed.push(rel);
|
|
1577
|
+
}
|
|
1578
|
+
return diff;
|
|
1579
|
+
}
|
|
1580
|
+
function systemStatus(project, root, indexFile, namespace) {
|
|
1581
|
+
const record = readRecords(project, indexFile).find((r) => r.namespace === namespace);
|
|
1582
|
+
if (!record) return null;
|
|
1583
|
+
const diff = compareSystem(project, root, record);
|
|
1584
|
+
if (diff.skippedKind) return { state: "unknown", changed: 0, removed: 0, reason: diff.skippedKind };
|
|
1585
|
+
if (diff.missing) return { state: "unknown", changed: 0, removed: 0, reason: "missing" };
|
|
1586
|
+
return { state: diff.changed.length > 0 ? "behind" : "current", changed: diff.changed.length, removed: diff.removed.length };
|
|
1587
|
+
}
|
|
1588
|
+
var DS_SYNC = /^__ds-sync\/([a-z0-9][a-z0-9_-]{0,63})$/;
|
|
1589
|
+
function pick(records, namespace) {
|
|
1590
|
+
if (namespace === void 0) return records;
|
|
1591
|
+
const one = records.filter((r) => r.namespace === namespace);
|
|
1592
|
+
if (one.length === 0) {
|
|
1593
|
+
const names = records.map((r) => r.namespace);
|
|
1594
|
+
throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
|
|
1595
|
+
}
|
|
1596
|
+
return one;
|
|
1597
|
+
}
|
|
1598
|
+
function compareSystems(project, root, indexFile, namespace, from) {
|
|
1599
|
+
return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
|
|
1600
|
+
}
|
|
1601
|
+
function stamp(project, indexFile, namespaces, now) {
|
|
1602
|
+
const file = path5.join(project, indexFile);
|
|
1603
|
+
const text = fs5.readFileSync(file, "utf8");
|
|
1604
|
+
const index = JSON.parse(text);
|
|
1605
|
+
const list = index.designSystems;
|
|
1606
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
1607
|
+
for (const r of records) {
|
|
1608
|
+
const record = r;
|
|
1609
|
+
if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
|
|
1610
|
+
}
|
|
1611
|
+
const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
|
|
1612
|
+
const out = JSON.stringify(index, null, indent);
|
|
1613
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1614
|
+
fs5.writeFileSync(tmp, text.endsWith("\n") ? `${out}
|
|
1615
|
+
` : out);
|
|
1616
|
+
fs5.renameSync(tmp, file);
|
|
1617
|
+
}
|
|
1618
|
+
function syncSystems(project, root, indexFile, opts) {
|
|
1619
|
+
const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
|
|
1620
|
+
const results = [];
|
|
1621
|
+
const synced = /* @__PURE__ */ new Set();
|
|
1622
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1623
|
+
for (const diff of diffs) {
|
|
1624
|
+
const result = { ...diff, copied: [] };
|
|
1625
|
+
if (!opts.check && diff.source && !diff.missing) {
|
|
1626
|
+
for (const rel of diff.changed) {
|
|
1627
|
+
const target = path5.join(project, "ds", diff.namespace, rel);
|
|
1628
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
1629
|
+
fs5.copyFileSync(path5.join(diff.source, "project", rel), tmp);
|
|
1630
|
+
fs5.renameSync(tmp, target);
|
|
1631
|
+
result.copied.push(rel);
|
|
1632
|
+
}
|
|
1633
|
+
if (diff.changed.length > 0) synced.add(diff.namespace);
|
|
1634
|
+
}
|
|
1635
|
+
results.push(result);
|
|
1636
|
+
}
|
|
1637
|
+
if (synced.size > 0) {
|
|
1638
|
+
stamp(project, indexFile, synced, now);
|
|
1639
|
+
for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
|
|
1640
|
+
}
|
|
1641
|
+
return results;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
1645
|
+
import fs6 from "node:fs";
|
|
1646
|
+
import path6 from "node:path";
|
|
1501
1647
|
var HISTORY_DIR = "history";
|
|
1502
1648
|
var INDEX = "index.json";
|
|
1503
1649
|
var OBJECTS = "objects";
|
|
@@ -1512,13 +1658,13 @@ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
|
1512
1658
|
var SHA = /^[0-9a-f]{64}$/;
|
|
1513
1659
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1514
1660
|
function writeAtomic2(file, data) {
|
|
1515
|
-
|
|
1516
|
-
const tmp =
|
|
1661
|
+
fs6.mkdirSync(path6.dirname(file), { recursive: true });
|
|
1662
|
+
const tmp = path6.join(path6.dirname(file), `.${path6.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
1517
1663
|
try {
|
|
1518
|
-
|
|
1519
|
-
|
|
1664
|
+
fs6.writeFileSync(tmp, data);
|
|
1665
|
+
fs6.renameSync(tmp, file);
|
|
1520
1666
|
} catch (error) {
|
|
1521
|
-
|
|
1667
|
+
fs6.rmSync(tmp, { force: true });
|
|
1522
1668
|
throw error;
|
|
1523
1669
|
}
|
|
1524
1670
|
}
|
|
@@ -1599,10 +1745,10 @@ var History = class {
|
|
|
1599
1745
|
/** path → sha: what the files hold after the newest version. */
|
|
1600
1746
|
headState;
|
|
1601
1747
|
constructor(root) {
|
|
1602
|
-
this.dir =
|
|
1748
|
+
this.dir = path6.join(root, HISTORY_DIR);
|
|
1603
1749
|
let raw = null;
|
|
1604
1750
|
try {
|
|
1605
|
-
raw = JSON.parse(
|
|
1751
|
+
raw = JSON.parse(fs6.readFileSync(path6.join(this.dir, INDEX), "utf8"));
|
|
1606
1752
|
} catch {
|
|
1607
1753
|
}
|
|
1608
1754
|
this.index = parseIndex(raw);
|
|
@@ -1646,7 +1792,7 @@ var History = class {
|
|
|
1646
1792
|
read(sha) {
|
|
1647
1793
|
if (!SHA.test(sha)) return null;
|
|
1648
1794
|
try {
|
|
1649
|
-
return
|
|
1795
|
+
return fs6.readFileSync(path6.join(this.dir, OBJECTS, sha));
|
|
1650
1796
|
} catch {
|
|
1651
1797
|
return null;
|
|
1652
1798
|
}
|
|
@@ -1660,7 +1806,7 @@ var History = class {
|
|
|
1660
1806
|
size(sha) {
|
|
1661
1807
|
if (!SHA.test(sha)) return null;
|
|
1662
1808
|
try {
|
|
1663
|
-
return
|
|
1809
|
+
return fs6.statSync(path6.join(this.dir, OBJECTS, sha)).size;
|
|
1664
1810
|
} catch {
|
|
1665
1811
|
return null;
|
|
1666
1812
|
}
|
|
@@ -1794,8 +1940,8 @@ var History = class {
|
|
|
1794
1940
|
return out;
|
|
1795
1941
|
}
|
|
1796
1942
|
putObject(sha, bytes) {
|
|
1797
|
-
const file =
|
|
1798
|
-
if (
|
|
1943
|
+
const file = path6.join(this.dir, OBJECTS, sha);
|
|
1944
|
+
if (fs6.existsSync(file)) return;
|
|
1799
1945
|
writeAtomic2(file, bytes);
|
|
1800
1946
|
}
|
|
1801
1947
|
/** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
|
|
@@ -1832,14 +1978,14 @@ var History = class {
|
|
|
1832
1978
|
}
|
|
1833
1979
|
let names;
|
|
1834
1980
|
try {
|
|
1835
|
-
names =
|
|
1981
|
+
names = fs6.readdirSync(path6.join(this.dir, OBJECTS));
|
|
1836
1982
|
} catch {
|
|
1837
1983
|
return;
|
|
1838
1984
|
}
|
|
1839
|
-
for (const name of names) if (SHA.test(name) && !keep.has(name))
|
|
1985
|
+
for (const name of names) if (SHA.test(name) && !keep.has(name)) fs6.rmSync(path6.join(this.dir, OBJECTS, name), { force: true });
|
|
1840
1986
|
}
|
|
1841
1987
|
save() {
|
|
1842
|
-
writeAtomic2(
|
|
1988
|
+
writeAtomic2(path6.join(this.dir, INDEX), `${JSON.stringify(this.index)}
|
|
1843
1989
|
`);
|
|
1844
1990
|
}
|
|
1845
1991
|
};
|
|
@@ -1909,19 +2055,19 @@ function diffContents(before, after) {
|
|
|
1909
2055
|
}
|
|
1910
2056
|
|
|
1911
2057
|
import { createHash as createHash2 } from "node:crypto";
|
|
1912
|
-
import
|
|
2058
|
+
import fs10 from "node:fs";
|
|
1913
2059
|
import os4 from "node:os";
|
|
1914
|
-
import
|
|
2060
|
+
import path10 from "node:path";
|
|
1915
2061
|
|
|
1916
|
-
import
|
|
2062
|
+
import fs8 from "node:fs";
|
|
1917
2063
|
import fsp2 from "node:fs/promises";
|
|
1918
|
-
import
|
|
2064
|
+
import path8 from "node:path";
|
|
1919
2065
|
import { promisify } from "node:util";
|
|
1920
2066
|
import zlib from "node:zlib";
|
|
1921
2067
|
|
|
1922
2068
|
import crypto from "node:crypto";
|
|
1923
|
-
import
|
|
1924
|
-
import
|
|
2069
|
+
import fs7 from "node:fs";
|
|
2070
|
+
import path7 from "node:path";
|
|
1925
2071
|
var GUEST_ROLES = ["editor", "commenter", "viewer"];
|
|
1926
2072
|
var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
|
|
1927
2073
|
var SHARING_FILE = ".sharing.json";
|
|
@@ -1948,13 +2094,13 @@ var Sharing = class _Sharing {
|
|
|
1948
2094
|
file;
|
|
1949
2095
|
state;
|
|
1950
2096
|
constructor(root) {
|
|
1951
|
-
this.file =
|
|
2097
|
+
this.file = path7.join(root, SHARING_FILE);
|
|
1952
2098
|
this.state = _Sharing.read(this.file);
|
|
1953
2099
|
}
|
|
1954
2100
|
static read(file) {
|
|
1955
2101
|
let raw;
|
|
1956
2102
|
try {
|
|
1957
|
-
raw = JSON.parse(
|
|
2103
|
+
raw = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
1958
2104
|
} catch {
|
|
1959
2105
|
return fresh();
|
|
1960
2106
|
}
|
|
@@ -1986,9 +2132,9 @@ var Sharing = class _Sharing {
|
|
|
1986
2132
|
}
|
|
1987
2133
|
save() {
|
|
1988
2134
|
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
1989
|
-
|
|
2135
|
+
fs7.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
|
|
1990
2136
|
`, { mode: 384 });
|
|
1991
|
-
|
|
2137
|
+
fs7.renameSync(tmp, this.file);
|
|
1992
2138
|
}
|
|
1993
2139
|
/** The role whose link has this token, while that link is on. */
|
|
1994
2140
|
linkRole(tokenGiven) {
|
|
@@ -2070,10 +2216,10 @@ var ArchiveTooLarge = class extends Error {
|
|
|
2070
2216
|
code = "TOO_LARGE";
|
|
2071
2217
|
};
|
|
2072
2218
|
async function archiveEntries(root, skip = []) {
|
|
2073
|
-
const folder =
|
|
2074
|
-
const skipped = new Set(skip.map((file) =>
|
|
2219
|
+
const folder = path8.basename(path8.resolve(root)) || "artifact";
|
|
2220
|
+
const skipped = new Set(skip.map((file) => path8.resolve(file)));
|
|
2075
2221
|
const entries = [];
|
|
2076
|
-
const
|
|
2222
|
+
const walk2 = async (dir, rel) => {
|
|
2077
2223
|
let names;
|
|
2078
2224
|
try {
|
|
2079
2225
|
names = await fsp2.readdir(dir, { withFileTypes: true });
|
|
@@ -2084,17 +2230,17 @@ async function archiveEntries(root, skip = []) {
|
|
|
2084
2230
|
for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
2085
2231
|
const name = dirent.name;
|
|
2086
2232
|
if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
|
|
2087
|
-
const file =
|
|
2233
|
+
const file = path8.join(dir, name);
|
|
2088
2234
|
if (skipped.has(file)) continue;
|
|
2089
2235
|
const inner = rel ? `${rel}/${name}` : name;
|
|
2090
|
-
if (dirent.isDirectory()) await
|
|
2236
|
+
if (dirent.isDirectory()) await walk2(file, inner);
|
|
2091
2237
|
else if (dirent.isFile()) {
|
|
2092
2238
|
const stat = await fsp2.stat(file).catch(() => null);
|
|
2093
2239
|
if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
|
|
2094
2240
|
}
|
|
2095
2241
|
}
|
|
2096
2242
|
};
|
|
2097
|
-
await
|
|
2243
|
+
await walk2(path8.resolve(root), "");
|
|
2098
2244
|
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.`);
|
|
2099
2245
|
const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
|
|
2100
2246
|
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.`);
|
|
@@ -2143,7 +2289,7 @@ async function writeZip(entries, write) {
|
|
|
2143
2289
|
const crc = crc32(data);
|
|
2144
2290
|
let method = 0;
|
|
2145
2291
|
let body = data;
|
|
2146
|
-
if (data.length > 0 && !STORED.has(
|
|
2292
|
+
if (data.length > 0 && !STORED.has(path8.extname(entry.name).toLowerCase())) {
|
|
2147
2293
|
const deflated = await deflateRaw(data);
|
|
2148
2294
|
if (deflated.length < data.length) {
|
|
2149
2295
|
method = 8;
|
|
@@ -2197,12 +2343,12 @@ async function writeZip(entries, write) {
|
|
|
2197
2343
|
}
|
|
2198
2344
|
function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
|
|
2199
2345
|
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2200
|
-
return `${
|
|
2346
|
+
return `${path8.basename(path8.resolve(root)) || "artifact"}-${day}.zip`;
|
|
2201
2347
|
}
|
|
2202
2348
|
async function writeArchive(root, out) {
|
|
2203
|
-
const file =
|
|
2349
|
+
const file = path8.resolve(out);
|
|
2204
2350
|
const entries = await archiveEntries(root, [file]);
|
|
2205
|
-
const tmp =
|
|
2351
|
+
const tmp = path8.join(path8.dirname(file), `.${path8.basename(file)}.${process.pid}.tmp`);
|
|
2206
2352
|
const handle = await fsp2.open(tmp, "w");
|
|
2207
2353
|
let bytes;
|
|
2208
2354
|
try {
|
|
@@ -2220,10 +2366,10 @@ async function writeArchive(root, out) {
|
|
|
2220
2366
|
return { file, files: entries.length, bytes };
|
|
2221
2367
|
}
|
|
2222
2368
|
async function archiveCommand(root, out) {
|
|
2223
|
-
const where = out ?
|
|
2224
|
-
const target =
|
|
2369
|
+
const where = out ? path8.resolve(out) : path8.dirname(path8.resolve(root));
|
|
2370
|
+
const target = fs8.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path8.join(where, archiveFileName(root)) : where;
|
|
2225
2371
|
const { file, files, bytes } = await writeArchive(root, target);
|
|
2226
|
-
return { file, folder:
|
|
2372
|
+
return { file, folder: path8.basename(path8.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
|
|
2227
2373
|
}
|
|
2228
2374
|
function attachment(name) {
|
|
2229
2375
|
const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
|
|
@@ -2271,31 +2417,31 @@ async function sendArchive(request, response, root) {
|
|
|
2271
2417
|
}
|
|
2272
2418
|
}
|
|
2273
2419
|
|
|
2274
|
-
import
|
|
2420
|
+
import fs9 from "node:fs";
|
|
2275
2421
|
import fsp3 from "node:fs/promises";
|
|
2276
2422
|
import os3 from "node:os";
|
|
2277
|
-
import
|
|
2423
|
+
import path9 from "node:path";
|
|
2278
2424
|
var MAX_BYTES2 = 4294967295;
|
|
2279
2425
|
function shown(file) {
|
|
2280
2426
|
const home = os3.homedir();
|
|
2281
|
-
return home && (file === home || file.startsWith(home +
|
|
2427
|
+
return home && (file === home || file.startsWith(home + path9.sep)) ? `~${file.slice(home.length)}` : file;
|
|
2282
2428
|
}
|
|
2283
2429
|
function saved(file, bytes) {
|
|
2284
|
-
return { path: file, name:
|
|
2430
|
+
return { path: file, name: path9.basename(file), folder: path9.dirname(file), shown: shown(file), bytes };
|
|
2285
2431
|
}
|
|
2286
2432
|
function safeFileName(raw) {
|
|
2287
2433
|
let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
|
|
2288
2434
|
if (name.length > 200) {
|
|
2289
|
-
const ext =
|
|
2435
|
+
const ext = path9.extname(name).slice(0, 16);
|
|
2290
2436
|
name = name.slice(0, 200 - ext.length) + ext;
|
|
2291
2437
|
}
|
|
2292
2438
|
return name || "download";
|
|
2293
2439
|
}
|
|
2294
2440
|
async function openFree(dir, name) {
|
|
2295
|
-
const ext =
|
|
2441
|
+
const ext = path9.extname(name);
|
|
2296
2442
|
const stem = name.slice(0, name.length - ext.length);
|
|
2297
2443
|
for (let n = 0; ; n++) {
|
|
2298
|
-
const file =
|
|
2444
|
+
const file = path9.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
|
|
2299
2445
|
try {
|
|
2300
2446
|
return { file, handle: await fsp3.open(file, "wx") };
|
|
2301
2447
|
} catch (error) {
|
|
@@ -2314,7 +2460,7 @@ async function saveDownload(request, response, root) {
|
|
|
2314
2460
|
let handle = null;
|
|
2315
2461
|
let bytes = 0;
|
|
2316
2462
|
try {
|
|
2317
|
-
const dir =
|
|
2463
|
+
const dir = path9.join(root, EXPORTS_DIR);
|
|
2318
2464
|
await fsp3.mkdir(dir, { recursive: true });
|
|
2319
2465
|
({ file, handle } = await openFree(dir, name));
|
|
2320
2466
|
for await (const chunk of request) {
|
|
@@ -2339,7 +2485,7 @@ async function saveArchive(request, response, root) {
|
|
|
2339
2485
|
request.resume();
|
|
2340
2486
|
try {
|
|
2341
2487
|
const { file } = await archiveCommand(root, void 0);
|
|
2342
|
-
sendJson2(response, 200, saved(file,
|
|
2488
|
+
sendJson2(response, 200, saved(file, fs9.statSync(file).size));
|
|
2343
2489
|
} catch (error) {
|
|
2344
2490
|
const tooLarge = error instanceof ArchiveTooLarge;
|
|
2345
2491
|
sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
|
|
@@ -2380,7 +2526,7 @@ function localOwner() {
|
|
|
2380
2526
|
}
|
|
2381
2527
|
return { id: "local", name, email: `${name}@localhost` };
|
|
2382
2528
|
}
|
|
2383
|
-
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(
|
|
2529
|
+
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash2("sha256").update(path10.resolve(root)).digest("hex").slice(0, 12)}`;
|
|
2384
2530
|
var ShareHost = class {
|
|
2385
2531
|
sharing;
|
|
2386
2532
|
options;
|
|
@@ -2545,7 +2691,7 @@ var ShareHost = class {
|
|
|
2545
2691
|
}
|
|
2546
2692
|
if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
|
|
2547
2693
|
if (method === "GET" && p === "/__share/ui.js") {
|
|
2548
|
-
|
|
2694
|
+
fs10.stat(this.options.uiFile, (error, stat) => {
|
|
2549
2695
|
if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
|
|
2550
2696
|
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" });
|
|
2551
2697
|
});
|
|
@@ -2565,7 +2711,7 @@ var ShareHost = class {
|
|
|
2565
2711
|
*/
|
|
2566
2712
|
headTags(_viewer, query = "") {
|
|
2567
2713
|
let tags = `<script src="/__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&")}"></script>`;
|
|
2568
|
-
if (
|
|
2714
|
+
if (fs10.existsSync(this.options.uiFile)) tags += '<script type="module" src="/__share/ui.js"></script>';
|
|
2569
2715
|
return tags;
|
|
2570
2716
|
}
|
|
2571
2717
|
/** `html` with the head tags first in its <head>; null when it has none. */
|
|
@@ -2957,7 +3103,7 @@ function noticeView(n, label, lostAt, gone) {
|
|
|
2957
3103
|
...n.lost.sha === null ? { deleted: true } : {}
|
|
2958
3104
|
};
|
|
2959
3105
|
}
|
|
2960
|
-
var
|
|
3106
|
+
var plain2 = (text) => ({ en: text, zh: text });
|
|
2961
3107
|
|
|
2962
3108
|
var PEER_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
2963
3109
|
function agentWhere(main2, count, label) {
|
|
@@ -3257,7 +3403,7 @@ async function collectFiles(root, limit) {
|
|
|
3257
3403
|
const seen = /* @__PURE__ */ new Set();
|
|
3258
3404
|
let truncated = false;
|
|
3259
3405
|
const visit = async (relDir, descend) => {
|
|
3260
|
-
const dir = relDir === "" ? root :
|
|
3406
|
+
const dir = relDir === "" ? root : path11.join(root, ...relDir.split("/"));
|
|
3261
3407
|
let entries;
|
|
3262
3408
|
try {
|
|
3263
3409
|
entries = await fsp4.readdir(dir, { withFileTypes: true });
|
|
@@ -3288,8 +3434,8 @@ async function collectFiles(root, limit) {
|
|
|
3288
3434
|
return { rels, truncated };
|
|
3289
3435
|
}
|
|
3290
3436
|
async function writeFileAtomic(file, bytes) {
|
|
3291
|
-
await fsp4.mkdir(
|
|
3292
|
-
const tmp =
|
|
3437
|
+
await fsp4.mkdir(path11.dirname(file), { recursive: true });
|
|
3438
|
+
const tmp = path11.join(path11.dirname(file), `.${path11.basename(file)}.${randomUUID()}.tmp`);
|
|
3293
3439
|
try {
|
|
3294
3440
|
await fsp4.writeFile(tmp, bytes);
|
|
3295
3441
|
await fsp4.rename(tmp, file);
|
|
@@ -3345,11 +3491,11 @@ var Store = class {
|
|
|
3345
3491
|
async diskPath(rel) {
|
|
3346
3492
|
const realRoot = await fsp4.realpath(this.root).catch(() => null);
|
|
3347
3493
|
if (realRoot === null) return null;
|
|
3348
|
-
const file =
|
|
3349
|
-
for (let dir =
|
|
3494
|
+
const file = path11.join(realRoot, ...rel.split("/"));
|
|
3495
|
+
for (let dir = path11.dirname(file); ; dir = path11.dirname(dir)) {
|
|
3350
3496
|
const real = await fsp4.realpath(dir).catch(() => null);
|
|
3351
3497
|
if (real === null) {
|
|
3352
|
-
if (
|
|
3498
|
+
if (path11.dirname(dir) === dir) return null;
|
|
3353
3499
|
continue;
|
|
3354
3500
|
}
|
|
3355
3501
|
if (real !== dir) return null;
|
|
@@ -3579,7 +3725,7 @@ var Store = class {
|
|
|
3579
3725
|
}
|
|
3580
3726
|
async findBlobFile(id) {
|
|
3581
3727
|
try {
|
|
3582
|
-
const names = await fsp4.readdir(
|
|
3728
|
+
const names = await fsp4.readdir(path11.join(this.root, LAYOUT.blobDir));
|
|
3583
3729
|
const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
|
|
3584
3730
|
if (name === void 0) return null;
|
|
3585
3731
|
const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
|
|
@@ -3634,13 +3780,13 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3634
3780
|
const directories = /* @__PURE__ */ new Map();
|
|
3635
3781
|
const visit = (rel, descend) => {
|
|
3636
3782
|
if (directories.size >= maxDirectories) return;
|
|
3637
|
-
const dir = rel === "" ? store.root :
|
|
3638
|
-
if (directories.has(dir) || !
|
|
3639
|
-
const info =
|
|
3783
|
+
const dir = rel === "" ? store.root : path11.join(store.root, ...rel.split("/"));
|
|
3784
|
+
if (directories.has(dir) || !fs11.existsSync(dir)) return;
|
|
3785
|
+
const info = fs11.lstatSync(dir);
|
|
3640
3786
|
if (!info.isDirectory()) return;
|
|
3641
3787
|
directories.set(dir, `${info.dev}:${info.ino}`);
|
|
3642
3788
|
if (!descend) return;
|
|
3643
|
-
for (const entry of
|
|
3789
|
+
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
3644
3790
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
3645
3791
|
const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
3646
3792
|
if (LAYOUT.descendInto(child)) visit(child, true);
|
|
@@ -3657,19 +3803,19 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3657
3803
|
if (watchers.has(dir)) continue;
|
|
3658
3804
|
let watcher;
|
|
3659
3805
|
try {
|
|
3660
|
-
watcher =
|
|
3806
|
+
watcher = fs11.watch(dir, (event, name) => {
|
|
3661
3807
|
if (stopped) return;
|
|
3662
3808
|
if (name) {
|
|
3663
|
-
const changed =
|
|
3809
|
+
const changed = path11.join(dir, String(name));
|
|
3664
3810
|
if (event === "rename" && watchers.has(changed)) {
|
|
3665
3811
|
for (const [watchedDir, item] of watchers) {
|
|
3666
|
-
if (watchedDir === changed || watchedDir.startsWith(changed +
|
|
3812
|
+
if (watchedDir === changed || watchedDir.startsWith(changed + path11.sep)) {
|
|
3667
3813
|
item.watcher.close();
|
|
3668
3814
|
watchers.delete(watchedDir);
|
|
3669
3815
|
}
|
|
3670
3816
|
}
|
|
3671
3817
|
}
|
|
3672
|
-
const rel =
|
|
3818
|
+
const rel = path11.relative(store.root, changed).split(path11.sep).join("/");
|
|
3673
3819
|
if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
|
|
3674
3820
|
}
|
|
3675
3821
|
schedule();
|
|
@@ -3705,7 +3851,7 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3705
3851
|
const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
|
|
3706
3852
|
const parts = [];
|
|
3707
3853
|
for (const rel of rels) {
|
|
3708
|
-
const info = await fsp4.lstat(
|
|
3854
|
+
const info = await fsp4.lstat(path11.join(store.root, ...rel.split("/"))).catch(() => null);
|
|
3709
3855
|
parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
|
|
3710
3856
|
}
|
|
3711
3857
|
return parts.join("\n");
|
|
@@ -3736,11 +3882,11 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3736
3882
|
var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
|
|
3737
3883
|
async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1", log = (line) => process.stderr.write(`${line}
|
|
3738
3884
|
`), agentHook, lan = false, portGiven = false }) {
|
|
3739
|
-
const project =
|
|
3740
|
-
const
|
|
3885
|
+
const project = path11.join(root, "project");
|
|
3886
|
+
const stamp2 = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
|
|
3741
3887
|
const projectDir = await fsp4.realpath(project);
|
|
3742
3888
|
const history = new History(root);
|
|
3743
|
-
const store = new Store(projectDir, history,
|
|
3889
|
+
const store = new Store(projectDir, history, stamp2);
|
|
3744
3890
|
const owner = localOwner();
|
|
3745
3891
|
const share = new ShareHost({
|
|
3746
3892
|
root,
|
|
@@ -3748,9 +3894,9 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
|
|
|
3748
3894
|
owner,
|
|
3749
3895
|
noun: NOUNS[KIND] ?? { en: NOUN, zh: NOUN, zhThis: NOUN },
|
|
3750
3896
|
title: () => artifactTitle(root, projectDir),
|
|
3751
|
-
log:
|
|
3897
|
+
log: stamp2,
|
|
3752
3898
|
createServer: () => http.createServer(handle),
|
|
3753
|
-
uiFile:
|
|
3899
|
+
uiFile: path11.join(EDITOR_DIR, "share-ui.js"),
|
|
3754
3900
|
// The History panel's address and what this person may do there; on a version's page (?at=n), which version.
|
|
3755
3901
|
globals: (viewer, query) => {
|
|
3756
3902
|
const at = Number(query.get("at") ?? "");
|
|
@@ -3759,7 +3905,7 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
|
|
|
3759
3905
|
});
|
|
3760
3906
|
const comments = createCommentsService({
|
|
3761
3907
|
root,
|
|
3762
|
-
log:
|
|
3908
|
+
log: stamp2,
|
|
3763
3909
|
locate: commentLocation ? (input) => commentLocation(input, projectDir) : void 0,
|
|
3764
3910
|
agentHook,
|
|
3765
3911
|
userName: owner.name,
|
|
@@ -3798,7 +3944,7 @@ data: ${JSON.stringify(event)}
|
|
|
3798
3944
|
}
|
|
3799
3945
|
return (p) => {
|
|
3800
3946
|
const rel = p.replace(/^project\//, "");
|
|
3801
|
-
return named?.(rel) ??
|
|
3947
|
+
return named?.(rel) ?? plain2(rel);
|
|
3802
3948
|
};
|
|
3803
3949
|
};
|
|
3804
3950
|
const agentWorked = (paths) => {
|
|
@@ -3829,16 +3975,16 @@ data: ${JSON.stringify(event)}
|
|
|
3829
3975
|
// A notice whose overwrite named the version its saver's edits started from: what was lost goes back by line into
|
|
3830
3976
|
// the file as it is now, the lost lines winning where both changed them; a deleted file gets all of it back.
|
|
3831
3977
|
recover: (_path, base, lost, now) => now === null ? lost : mergeTextBytes(base, lost, now),
|
|
3832
|
-
log:
|
|
3978
|
+
log: stamp2
|
|
3833
3979
|
});
|
|
3834
3980
|
const stopWatch = watchArtifact(
|
|
3835
3981
|
store,
|
|
3836
3982
|
(event) => {
|
|
3837
|
-
|
|
3983
|
+
stamp2(`changed on disk: ${event.paths.join(", ")}${clients.size ? "" : " (no page open)"}`);
|
|
3838
3984
|
changed({ ...event, by: authorOf(AGENT) });
|
|
3839
3985
|
agentWorked(event.paths);
|
|
3840
3986
|
},
|
|
3841
|
-
(error) =>
|
|
3987
|
+
(error) => stamp2(`watching failed: ${errorText(error)}`)
|
|
3842
3988
|
);
|
|
3843
3989
|
const events = async (request, response, viewer) => {
|
|
3844
3990
|
const given = new URL(request.url ?? "/", "http://local").searchParams.get("client") ?? "";
|
|
@@ -3901,7 +4047,7 @@ data: ${JSON.stringify(event)}
|
|
|
3901
4047
|
const parts = rest === "" ? ["index.html"] : rest.split("/");
|
|
3902
4048
|
const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
|
|
3903
4049
|
const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
|
|
3904
|
-
const file = realDist === null ? null :
|
|
4050
|
+
const file = realDist === null ? null : path11.join(realDist, ...parts);
|
|
3905
4051
|
const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
|
|
3906
4052
|
const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
|
|
3907
4053
|
if (file === null || real !== file || !info?.isFile()) {
|
|
@@ -3912,7 +4058,7 @@ data: ${JSON.stringify(event)}
|
|
|
3912
4058
|
response.writeHead(404, { "Cache-Control": "no-store" }).end();
|
|
3913
4059
|
return;
|
|
3914
4060
|
}
|
|
3915
|
-
const ext =
|
|
4061
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3916
4062
|
const type = STATIC_TYPES[ext] ?? "application/octet-stream";
|
|
3917
4063
|
if (ext === ".html") {
|
|
3918
4064
|
const key = parts.join("/");
|
|
@@ -3967,7 +4113,7 @@ data: ${JSON.stringify(event)}
|
|
|
3967
4113
|
const outcome = await store.publish(files, whoOf(viewer), over, merged, CLIENT_ID.test(page) ? page : "");
|
|
3968
4114
|
const names = Object.keys(files).map((p) => p.replace(/^project\//, ""));
|
|
3969
4115
|
const made = outcome.version ? ` as version ${outcome.version.n}` : "";
|
|
3970
|
-
|
|
4116
|
+
stamp2(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
|
|
3971
4117
|
sendJson2(response, outcome.status, outcome.body);
|
|
3972
4118
|
if (outcome.status === 200 && clients.size > 0) {
|
|
3973
4119
|
const from = String(request.headers["x-deck-client"] ?? "");
|
|
@@ -3988,7 +4134,7 @@ data: ${JSON.stringify(event)}
|
|
|
3988
4134
|
return;
|
|
3989
4135
|
}
|
|
3990
4136
|
const result = await store.putBlob(bytes, contentType);
|
|
3991
|
-
|
|
4137
|
+
stamp2(`uploaded from the page: ${result.url} (${contentType}, ${bytes.length} bytes)`);
|
|
3992
4138
|
sendJson2(response, 200, result);
|
|
3993
4139
|
};
|
|
3994
4140
|
const session = (viewer) => {
|
|
@@ -4101,6 +4247,24 @@ data: ${JSON.stringify(event)}
|
|
|
4101
4247
|
else await upload(request, response);
|
|
4102
4248
|
return;
|
|
4103
4249
|
}
|
|
4250
|
+
const dsSync = DESIGN_SYSTEMS ? DS_SYNC.exec(rest) : null;
|
|
4251
|
+
if (dsSync) {
|
|
4252
|
+
const folder = dsSync[1];
|
|
4253
|
+
const status = systemStatus(projectDir, root, INDEX_FILE, folder);
|
|
4254
|
+
if (method === "POST") {
|
|
4255
|
+
request.resume();
|
|
4256
|
+
if (!sameOrigin(request)) return publishError(response, 403, "not_writer", "cross-origin request");
|
|
4257
|
+
if (viewer.guest) return publishError(response, 403, "not_owner", "Only the owner of this computer can sync a design system from its source.");
|
|
4258
|
+
if (status === null) return sendJson2(response, 404, { error: "not_found" });
|
|
4259
|
+
const [result] = syncSystems(projectDir, root, INDEX_FILE, { namespace: folder });
|
|
4260
|
+
return sendJson2(response, 200, { copied: result.copied.length, ...systemStatus(projectDir, root, INDEX_FILE, folder), canSync: true });
|
|
4261
|
+
}
|
|
4262
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
4263
|
+
response.writeHead(405, { Allow: "GET, HEAD, POST" }).end();
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
return status === null ? sendJson2(response, 404, { error: "not_found" }, { "Cache-Control": "no-store" }) : sendJson2(response, 200, { ...status, canSync: !viewer.guest }, { "Cache-Control": "no-store" });
|
|
4267
|
+
}
|
|
4104
4268
|
if (method !== "GET" && method !== "HEAD") {
|
|
4105
4269
|
response.writeHead(405, { Allow: "GET, HEAD" }).end();
|
|
4106
4270
|
return;
|
|
@@ -4156,7 +4320,7 @@ data: ${JSON.stringify(event)}
|
|
|
4156
4320
|
if (!response.headersSent) publishError(response, 413, "too_large", "request body is too large");
|
|
4157
4321
|
return;
|
|
4158
4322
|
}
|
|
4159
|
-
|
|
4323
|
+
stamp2(`request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
4160
4324
|
if (!response.headersSent) publishError(response, 500, "upstream_error", "internal error");
|
|
4161
4325
|
else response.destroy();
|
|
4162
4326
|
}
|
|
@@ -4219,17 +4383,17 @@ data: ${JSON.stringify(event)}
|
|
|
4219
4383
|
throw error;
|
|
4220
4384
|
});
|
|
4221
4385
|
const url = `${serverOrigin(host, actualPort)}${editorPath}`;
|
|
4222
|
-
|
|
4386
|
+
stamp2(`serving ${project} at ${url}`);
|
|
4223
4387
|
const lanUrls = share.lanUrls();
|
|
4224
4388
|
return { url, port: actualPort, grant, close, ...lanUrls.length ? { lan: lanUrls } : {} };
|
|
4225
4389
|
}
|
|
4226
4390
|
function artifactTitle(root, projectDir) {
|
|
4227
4391
|
try {
|
|
4228
|
-
const index = JSON.parse(
|
|
4392
|
+
const index = JSON.parse(fs11.readFileSync(path11.join(projectDir, INDEX_FILE), "utf8"));
|
|
4229
4393
|
for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
|
|
4230
4394
|
} catch {
|
|
4231
4395
|
}
|
|
4232
|
-
return
|
|
4396
|
+
return path11.basename(root);
|
|
4233
4397
|
}
|
|
4234
4398
|
var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
|
|
4235
4399
|
async function serveForeground({ root, port, host, agentHook, lan, portGiven }) {
|
|
@@ -4250,18 +4414,18 @@ async function main(argv) {
|
|
|
4250
4414
|
}
|
|
4251
4415
|
if (positional.length !== 1 || flags.has("help")) {
|
|
4252
4416
|
process.stderr.write(
|
|
4253
|
-
`Usage: node ${
|
|
4417
|
+
`Usage: node ${path11.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
|
|
4254
4418
|
<root> holds project/${INDEX_FILE}.
|
|
4255
4419
|
`
|
|
4256
4420
|
);
|
|
4257
4421
|
return 2;
|
|
4258
4422
|
}
|
|
4259
|
-
let root =
|
|
4260
|
-
if (
|
|
4261
|
-
root =
|
|
4423
|
+
let root = path11.resolve(positional[0]);
|
|
4424
|
+
if (path11.basename(root) === "project" && fs11.existsSync(path11.join(root, INDEX_FILE)) && !fs11.existsSync(path11.join(root, "project"))) {
|
|
4425
|
+
root = path11.dirname(root);
|
|
4262
4426
|
}
|
|
4263
|
-
if (!
|
|
4264
|
-
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${
|
|
4427
|
+
if (!fs11.statSync(path11.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
|
|
4428
|
+
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path11.join(root, "project", INDEX_FILE)} does not exist.` })}
|
|
4265
4429
|
`);
|
|
4266
4430
|
return 2;
|
|
4267
4431
|
}
|
|
@@ -4285,7 +4449,7 @@ async function main(argv) {
|
|
|
4285
4449
|
}
|
|
4286
4450
|
function isMain(moduleUrl) {
|
|
4287
4451
|
try {
|
|
4288
|
-
return Boolean(process.argv[1]) && pathToFileURL(
|
|
4452
|
+
return Boolean(process.argv[1]) && pathToFileURL(fs11.realpathSync(process.argv[1])).href === moduleUrl;
|
|
4289
4453
|
} catch {
|
|
4290
4454
|
return false;
|
|
4291
4455
|
}
|
|
@@ -4302,7 +4466,7 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
4302
4466
|
kindLabels = kind.labels;
|
|
4303
4467
|
kindAgentContext = kind.agentContext;
|
|
4304
4468
|
SCRIPT = fileURLToPath(moduleUrl);
|
|
4305
|
-
EDITOR_DIR =
|
|
4469
|
+
EDITOR_DIR = path11.join(path11.dirname(SCRIPT), "editor");
|
|
4306
4470
|
const runIfMain = () => {
|
|
4307
4471
|
if (!isMain(moduleUrl)) return;
|
|
4308
4472
|
main(process.argv.slice(2)).then(
|