paintress 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as attachPersistent, c as lockPath, d as safeJoin, f as sanitizeProjectName, h as encodeLine, i as TOOLS, l as pidPath, m as LineDecoder, n as runStdioServer, o as isDaemonRunning, p as socketPath, s as sendRequest, u as resolveProjectDir } from "./server-ssrAxbTR.js";
3
- import { T as loadDoc, qt as VERSION, t as renderDocument } from "./render-CCxYNL6A.js";
2
+ import { C as isPresetType, D as presetPathFor, E as notePathFor, O as readTextIfExists, S as findNodeIn, T as listPresets, _ as LineDecoder, a as ensureDaemon, b as componentsPathFor, c as isDaemonRunning, d as lockPath, f as pidPath, g as socketPath, h as sanitizeProjectName, k as setPreset, l as queryDaemon, m as safeJoin, n as runStdioServer, o as reuseDaemon, p as resolveProjectDir, r as TOOLS, s as attachPersistent, u as sendRequest, v as encodeLine, w as listComponents, x as ensureProjectScaffold, y as applyProjectPresets } from "./server-DZRNNWac.js";
3
+ import { T as loadDoc, qt as VERSION, t as renderDocument } from "./render-BSf62388.js";
4
4
  import "./index.js";
5
5
  import { chmodSync, closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
@@ -92,7 +92,9 @@ var Registry = class {
92
92
  if (st.isDirectory()) {
93
93
  if (skip.has(name)) continue;
94
94
  walk(abs, relPath, depth + 1);
95
- } else if (DESIGN_EXT.has(extname(name).toLowerCase())) out.push(relPath);
95
+ } else if (DESIGN_EXT.has(extname(name).toLowerCase())) {
96
+ if (!name.startsWith("__")) out.push(relPath);
97
+ }
96
98
  }
97
99
  };
98
100
  walk(entry.rootReal, "", 0);
@@ -272,6 +274,9 @@ var WatchManager = class {
272
274
  watcher.on("change", () => {
273
275
  for (const cb of created.listeners) cb(absPath);
274
276
  });
277
+ watcher.on("add", () => {
278
+ for (const cb of created.listeners) cb(absPath);
279
+ });
275
280
  watcher.on("unlink", () => {
276
281
  for (const cb of created.listeners) cb(absPath);
277
282
  });
@@ -476,7 +481,7 @@ function acquireLock() {
476
481
  } catch {
477
482
  holder = 0;
478
483
  }
479
- if (isAlive(holder)) throw new Error(`paintress web 已在运行(pid ${holder})。如需重启请先 paintress web --stop`);
484
+ if (isAlive(holder)) throw new Error(`paintress web 已在运行(pid ${holder})。如需重启请先 paintress stop`);
480
485
  try {
481
486
  unlinkSync(p);
482
487
  } catch {}
@@ -539,6 +544,8 @@ async function startDaemon(opts = {}) {
539
544
  const idleCheckMs = opts.idleCheckMs ?? 2e3;
540
545
  let idleSince = null;
541
546
  let closing = false;
547
+ /** 进行中的关停 Promise:close() 必须等它,不能立刻返回 */
548
+ let closingPromise = null;
542
549
  /** 最近一次 HTTP 活动:用来判定「浏览器还在不在」而不单靠 SSE 关闭事件 */
543
550
  let lastHttpAt = Date.now();
544
551
  /**
@@ -662,6 +669,7 @@ async function startDaemon(opts = {}) {
662
669
  ok: true,
663
670
  pong: true,
664
671
  pid: process.pid,
672
+ port,
665
673
  uptimeMs: Date.now() - startedAt,
666
674
  projects: registry.all().length
667
675
  });
@@ -680,6 +688,12 @@ async function startDaemon(opts = {}) {
680
688
  const entry = registry.watch(req.project, req.dir, req.depth, req.owner === "agent" ? { owner: "agent" } : {});
681
689
  cancelAutoDetach(entry.name);
682
690
  watchers.watchDesigns(entry.rootReal, entry.depth, scheduleRegistryNotify);
691
+ try {
692
+ const created = ensureProjectScaffold(entry.rootReal).created;
693
+ if (created.length > 0 && !opts.quiet) process.stderr.write(`paintress web: 项目 ${entry.name} 初始化 ${created.map((f) => relative(entry.rootReal, f)).join("、")}\n`);
694
+ } catch (cause) {
695
+ process.stderr.write(`paintress web: 项目脚手架创建失败(${cause instanceof Error ? cause.message : String(cause)})\n`);
696
+ }
683
697
  reply({
684
698
  ok: true,
685
699
  watched: true,
@@ -958,6 +972,190 @@ async function startDaemon(opts = {}) {
958
972
  });
959
973
  return;
960
974
  }
975
+ if (segments[1] === "notes" && req.method === "GET") {
976
+ const name = url.searchParams.get("project") ?? "";
977
+ const relPath = url.searchParams.get("path") ?? "";
978
+ const entry = registry.get(name);
979
+ if (!entry) {
980
+ sendJson(404, {
981
+ ok: false,
982
+ error: `未注册的项目:${name}`
983
+ });
984
+ return;
985
+ }
986
+ const abs = safeJoin(entry.rootReal, relPath);
987
+ const noteAbs = abs === null ? null : notePathFor(abs);
988
+ if (abs === null || noteAbs === null) {
989
+ sendJson(404, {
990
+ ok: false,
991
+ error: "文件不存在"
992
+ });
993
+ return;
994
+ }
995
+ let markdown;
996
+ try {
997
+ markdown = readTextIfExists(noteAbs);
998
+ } catch (cause) {
999
+ sendJson(413, {
1000
+ ok: false,
1001
+ error: cause instanceof Error ? cause.message : String(cause)
1002
+ });
1003
+ return;
1004
+ }
1005
+ if (markdown === null) {
1006
+ sendJson(200, {
1007
+ ok: true,
1008
+ exists: false
1009
+ });
1010
+ return;
1011
+ }
1012
+ sendJson(200, {
1013
+ ok: true,
1014
+ exists: true,
1015
+ path: relative(entry.rootReal, noteAbs).split(sep).join("/"),
1016
+ markdown
1017
+ });
1018
+ return;
1019
+ }
1020
+ if (segments[1] === "presets") {
1021
+ const qProject = url.searchParams.get("project") ?? "";
1022
+ if (req.method === "GET") {
1023
+ const entry = registry.get(qProject);
1024
+ if (!entry) {
1025
+ sendJson(404, {
1026
+ ok: false,
1027
+ error: `未注册的项目:${qProject}`
1028
+ });
1029
+ return;
1030
+ }
1031
+ const { path, items } = listPresets(entry.rootReal);
1032
+ sendJson(200, {
1033
+ ok: true,
1034
+ path,
1035
+ items
1036
+ });
1037
+ return;
1038
+ }
1039
+ if (req.method === "PUT" || req.method === "POST") {
1040
+ if (!isTrustedWrite(req)) {
1041
+ sendJson(403, {
1042
+ ok: false,
1043
+ error: "缺少 x-paintress 头"
1044
+ });
1045
+ return;
1046
+ }
1047
+ const raw = await readBody();
1048
+ let body;
1049
+ try {
1050
+ body = JSON.parse(raw);
1051
+ } catch {
1052
+ sendJson(400, {
1053
+ ok: false,
1054
+ error: "请求体不是合法 JSON"
1055
+ });
1056
+ return;
1057
+ }
1058
+ const name = qProject !== "" ? qProject : typeof body["project"] === "string" ? body["project"] : "";
1059
+ const entry = registry.get(name);
1060
+ if (!entry) {
1061
+ sendJson(404, {
1062
+ ok: false,
1063
+ error: `未注册的项目:${name}`
1064
+ });
1065
+ return;
1066
+ }
1067
+ const key = typeof body["key"] === "string" ? body["key"] : "";
1068
+ const value = body["value"];
1069
+ if (key === "" || typeof value !== "string" && typeof value !== "number") {
1070
+ sendJson(400, {
1071
+ ok: false,
1072
+ error: "缺少 key 或 value"
1073
+ });
1074
+ return;
1075
+ }
1076
+ const existing = listPresets(entry.rootReal).items.find((p) => p.key === key);
1077
+ const typeRaw = body["type"] ?? existing?.type;
1078
+ if (!isPresetType(typeRaw)) {
1079
+ sendJson(400, {
1080
+ ok: false,
1081
+ error: "type 必须是 color 或 radius"
1082
+ });
1083
+ return;
1084
+ }
1085
+ const label = typeof body["name"] === "string" && body["name"] !== "" ? body["name"] : void 0;
1086
+ try {
1087
+ const items = setPreset(entry.rootReal, {
1088
+ key,
1089
+ type: typeRaw,
1090
+ value: String(value),
1091
+ ...label !== void 0 ? { name: label } : {}
1092
+ });
1093
+ notifyRegistry();
1094
+ sendJson(200, {
1095
+ ok: true,
1096
+ path: presetPathFor(entry.rootReal),
1097
+ items
1098
+ });
1099
+ } catch (cause) {
1100
+ sendJson(400, {
1101
+ ok: false,
1102
+ error: cause instanceof Error ? cause.message : String(cause)
1103
+ });
1104
+ }
1105
+ return;
1106
+ }
1107
+ sendJson(405, {
1108
+ ok: false,
1109
+ error: `不支持的方法:${req.method ?? "?"}`
1110
+ });
1111
+ return;
1112
+ }
1113
+ if (segments[1] === "components" && req.method === "GET") {
1114
+ const name = url.searchParams.get("project") ?? "";
1115
+ const entry = registry.get(name);
1116
+ if (!entry) {
1117
+ sendJson(404, {
1118
+ ok: false,
1119
+ error: `未注册的项目:${name}`
1120
+ });
1121
+ return;
1122
+ }
1123
+ const compPath = componentsPathFor(entry.rootReal);
1124
+ if (compPath === null) {
1125
+ sendJson(200, {
1126
+ ok: true,
1127
+ path: null,
1128
+ items: []
1129
+ });
1130
+ return;
1131
+ }
1132
+ const doc = applyProjectPresets(loadDoc(compPath).doc, compPath).doc;
1133
+ sendJson(200, {
1134
+ ok: true,
1135
+ path: compPath,
1136
+ items: listComponents(compPath).map((c) => {
1137
+ const node = findNodeIn(doc.children, c.id);
1138
+ if (node === null) return c;
1139
+ try {
1140
+ const single = renderDocument({
1141
+ ...doc,
1142
+ children: [node]
1143
+ }, {
1144
+ title: c.id,
1145
+ transparent: true
1146
+ });
1147
+ return {
1148
+ ...c,
1149
+ width: Math.round(single.canvas.width),
1150
+ height: Math.round(single.canvas.height)
1151
+ };
1152
+ } catch {
1153
+ return c;
1154
+ }
1155
+ })
1156
+ });
1157
+ return;
1158
+ }
961
1159
  sendJson(404, {
962
1160
  ok: false,
963
1161
  error: `未知 API:${url.pathname}`
@@ -1001,16 +1199,20 @@ async function startDaemon(opts = {}) {
1001
1199
  const token = Symbol("browser");
1002
1200
  entry.browsers.add(token);
1003
1201
  cancelAutoDetach(entry.name);
1004
- const release = watchers.acquire(abs, () => {
1202
+ const notifyChange = () => {
1005
1203
  res.write(`event: change\ndata: ${JSON.stringify({
1006
1204
  path: relPath,
1007
1205
  at: Date.now()
1008
1206
  })}\n\n`);
1009
- });
1207
+ };
1208
+ const releases = [watchers.acquire(abs, notifyChange)];
1209
+ releases.push(watchers.acquire(abs.replace(/\.(ptr|pen)$/i, ".md"), notifyChange));
1210
+ const presetAbs = presetPathFor(entry.rootReal);
1211
+ if (presetAbs !== null) releases.push(watchers.acquire(presetAbs, notifyChange));
1010
1212
  const keepAlive = setInterval(() => res.write(": ping\n\n"), 2e4);
1011
1213
  const done = () => {
1012
1214
  clearInterval(keepAlive);
1013
- release();
1215
+ for (const release of releases) release();
1014
1216
  entry.browsers.delete(token);
1015
1217
  maybeScheduleAutoDetach(entry.name);
1016
1218
  };
@@ -1032,7 +1234,7 @@ async function startDaemon(opts = {}) {
1032
1234
  return;
1033
1235
  }
1034
1236
  const loaded = loadDoc(abs);
1035
- const rendered = renderDocument(loaded.doc, { title: relPath });
1237
+ const rendered = renderDocument(applyProjectPresets(loaded.doc, abs).doc, { title: relPath });
1036
1238
  const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/") + 1) : "";
1037
1239
  const baseHref = `/files/${entry.name.split("/").map(encodeURIComponent).join("/")}/${dir.split("/").filter((s) => s !== "").map(encodeURIComponent).join("/")}${dir ? "/" : ""}`;
1038
1240
  let html = rendered.html.replace("<head>", `<head>\n<base href="${baseHref}">`);
@@ -1043,6 +1245,34 @@ async function startDaemon(opts = {}) {
1043
1245
  send(200, html);
1044
1246
  return;
1045
1247
  }
1248
+ if (segments[0] === "render-component") {
1249
+ const project = url.searchParams.get("project") ?? "";
1250
+ const id = url.searchParams.get("id") ?? "";
1251
+ const entry = registry.get(project);
1252
+ if (!entry) {
1253
+ send(404, "project not found", "text/plain; charset=utf-8");
1254
+ return;
1255
+ }
1256
+ const compPath = componentsPathFor(entry.rootReal);
1257
+ if (compPath === null || id === "") {
1258
+ send(404, "component not found", "text/plain; charset=utf-8");
1259
+ return;
1260
+ }
1261
+ const doc = applyProjectPresets(loadDoc(compPath).doc, compPath).doc;
1262
+ const node = findNodeIn(doc.children, id);
1263
+ if (node === null) {
1264
+ send(404, "component not found", "text/plain; charset=utf-8");
1265
+ return;
1266
+ }
1267
+ send(200, renderDocument({
1268
+ ...doc,
1269
+ children: [node]
1270
+ }, {
1271
+ title: id,
1272
+ transparent: true
1273
+ }).html);
1274
+ return;
1275
+ }
1046
1276
  if (segments[0] === "files" && segments.length >= 3) {
1047
1277
  const hit = resolveProjectPrefix(registry, segments.slice(1));
1048
1278
  if (hit === null) {
@@ -1171,30 +1401,39 @@ async function startDaemon(opts = {}) {
1171
1401
  changes.prune();
1172
1402
  }, idleCheckMs);
1173
1403
  idleTimer.unref();
1174
- async function shutdown(reason) {
1175
- if (closing) return;
1404
+ /**
1405
+ * 关停:幂等且**可等待**。
1406
+ * 已经在关停中时必须返回同一个 Promise —— 否则 `close()` 会立刻 resolve,
1407
+ * 调用方以为锁已释放,紧接着 `startDaemon` 就撞上「已在运行」。
1408
+ * (v0.6.0 的 release 测试里就是这么连环挂的:idle 关停还在飞,close() 先返回了。)
1409
+ */
1410
+ function shutdown(reason) {
1411
+ if (closingPromise !== null) return closingPromise;
1176
1412
  closing = true;
1177
- if (!opts.quiet) process.stderr.write(`paintress web: 退出(${reason})\n`);
1178
- clearInterval(idleTimer);
1179
- if (registryTimer !== null) clearTimeout(registryTimer);
1180
- for (const timer of autoDetachTimers.values()) clearTimeout(timer);
1181
- autoDetachTimers.clear();
1182
- for (const client of registryClients) try {
1183
- client.write(`event: shutdown\ndata: ${JSON.stringify({
1184
- reason,
1185
- at: Date.now()
1186
- })}\n\n`);
1187
- client.end();
1188
- } catch {}
1189
- registryClients.clear();
1190
- for (const client of controlClients) client.end();
1191
- controlClients.clear();
1192
- await watchers.closeAll();
1193
- await new Promise((resolve) => sockServer.close(() => resolve()));
1194
- const httpClosed = new Promise((resolve) => httpServer.close(() => resolve()));
1195
- httpServer.closeAllConnections();
1196
- await httpClosed;
1197
- releaseLock();
1413
+ closingPromise = (async () => {
1414
+ if (!opts.quiet) process.stderr.write(`paintress web: 退出(${reason})\n`);
1415
+ clearInterval(idleTimer);
1416
+ if (registryTimer !== null) clearTimeout(registryTimer);
1417
+ for (const timer of autoDetachTimers.values()) clearTimeout(timer);
1418
+ autoDetachTimers.clear();
1419
+ for (const client of registryClients) try {
1420
+ client.write(`event: shutdown\ndata: ${JSON.stringify({
1421
+ reason,
1422
+ at: Date.now()
1423
+ })}\n\n`);
1424
+ client.end();
1425
+ } catch {}
1426
+ registryClients.clear();
1427
+ for (const client of controlClients) client.end();
1428
+ controlClients.clear();
1429
+ await watchers.closeAll();
1430
+ await new Promise((resolve) => sockServer.close(() => resolve()));
1431
+ const httpClosed = new Promise((resolve) => httpServer.close(() => resolve()));
1432
+ httpServer.closeAllConnections();
1433
+ await httpClosed;
1434
+ releaseLock();
1435
+ })();
1436
+ return closingPromise;
1198
1437
  }
1199
1438
  return {
1200
1439
  port,
@@ -1514,6 +1753,7 @@ const FLAG_KEYS = /* @__PURE__ */ new Set([
1514
1753
  "gap",
1515
1754
  "force",
1516
1755
  "board",
1756
+ "parent",
1517
1757
  NO_OPEN_FLAG
1518
1758
  ]);
1519
1759
  const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
@@ -1747,39 +1987,57 @@ async function main(argv) {
1747
1987
  const port = flagNum(flags, "port");
1748
1988
  const idle = flagNum(flags, "idle");
1749
1989
  const noOpen = flags[NO_OPEN_FLAG] === true || process.env["PAINTRESS_NO_OPEN"] === "1";
1990
+ const url = (p) => `http://localhost:${p}`;
1991
+ const running = await reuseDaemon(port);
1992
+ if (running !== null) {
1993
+ console.log(JSON.stringify({
1994
+ ok: true,
1995
+ daemon: true,
1996
+ reused: true,
1997
+ pid: running.pid,
1998
+ port: running.port,
1999
+ http: url(running.port)
2000
+ }, null, 2));
2001
+ return 0;
2002
+ }
1750
2003
  if (flags["daemon"] === true) {
1751
- const { spawn } = await import("node:child_process");
1752
- const args = [process.argv[1] ?? "", "web"];
1753
- if (port !== void 0) args.push("--port", String(port));
1754
- if (idle !== void 0) args.push("--idle", String(idle));
1755
- if (noOpen) args.push("--no-open");
1756
- const child = spawn(process.execPath, args, {
1757
- detached: true,
1758
- stdio: "ignore"
2004
+ const res = await ensureDaemon({
2005
+ ...port !== void 0 ? { port } : {},
2006
+ ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
2007
+ noOpen
1759
2008
  });
1760
- child.unref();
1761
- const deadline = Date.now() + 5e3;
1762
- while (Date.now() < deadline) {
1763
- if (isDaemonRunning()) {
1764
- const res = await sendRequest({ kind: "ping" });
1765
- console.log(JSON.stringify({
1766
- daemon: true,
1767
- launcherPid: child.pid,
1768
- ...res
1769
- }, null, 2));
1770
- return 0;
1771
- }
1772
- await new Promise((r) => setTimeout(r, 100));
1773
- }
1774
- throw new Error("后台启动超时(5s 内未出现控制面 socket)");
2009
+ console.log(JSON.stringify({
2010
+ ok: true,
2011
+ daemon: true,
2012
+ reused: !res.started,
2013
+ pid: res.pid,
2014
+ port: res.port,
2015
+ http: url(res.port)
2016
+ }, null, 2));
2017
+ return 0;
1775
2018
  }
1776
- const handle = await startDaemon({
1777
- ...port !== void 0 ? { port } : {},
1778
- ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
1779
- emptyGraceMs: 6e4,
1780
- updateCheck: true
1781
- });
1782
- const http = `http://localhost:${handle.port}`;
2019
+ let handle;
2020
+ try {
2021
+ handle = await startDaemon({
2022
+ ...port !== void 0 ? { port } : {},
2023
+ ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
2024
+ emptyGraceMs: 6e4,
2025
+ updateCheck: true
2026
+ });
2027
+ } catch (cause) {
2028
+ const again = await queryDaemon();
2029
+ if (again === null) throw cause;
2030
+ console.log(JSON.stringify({
2031
+ ok: true,
2032
+ daemon: true,
2033
+ reused: true,
2034
+ pid: again.pid,
2035
+ port: again.port,
2036
+ http: url(again.port)
2037
+ }, null, 2));
2038
+ return 0;
2039
+ }
2040
+ const http = url(handle.port);
1783
2041
  console.log(JSON.stringify({
1784
2042
  ok: true,
1785
2043
  pid: handle.pid,
@@ -1787,7 +2045,7 @@ async function main(argv) {
1787
2045
  http
1788
2046
  }, null, 2));
1789
2047
  if (!noOpen) {
1790
- const { openBrowser } = await import("./server-ssrAxbTR.js").then((n) => n.r);
2048
+ const { openBrowser } = await import("./server-DZRNNWac.js").then((n) => n.i);
1791
2049
  openBrowser(`${http}/`);
1792
2050
  }
1793
2051
  await new Promise((resolve) => {
@@ -1796,8 +2054,54 @@ async function main(argv) {
1796
2054
  });
1797
2055
  return 0;
1798
2056
  }
1799
- if (!file) throw new Error("缺少 <file> 参数");
1800
2057
  let out;
2058
+ if (cmd === "preset") {
2059
+ const action = file ?? "list";
2060
+ if (action === "list") out = await call("paintress_preset", {
2061
+ action: "list",
2062
+ ...a1 !== void 0 ? { path: a1 } : {}
2063
+ });
2064
+ else if (action === "set") {
2065
+ if (!a1 || !a2 || !positionals[3] || !positionals[4]) throw new Error("用法:paintress preset set <dir> <key> <color|radius> <value> [--name 名称]");
2066
+ out = await call("paintress_preset", {
2067
+ action: "set",
2068
+ path: a1,
2069
+ key: a2,
2070
+ type: positionals[3],
2071
+ value: positionals[4],
2072
+ ...flagStr(flags, "name") !== void 0 ? { name: flagStr(flags, "name") } : {}
2073
+ });
2074
+ } else if (action === "remove") {
2075
+ if (!a1 || !a2) throw new Error("用法:paintress preset remove <dir> <key>");
2076
+ out = await call("paintress_preset", {
2077
+ action: "remove",
2078
+ path: a1,
2079
+ key: a2
2080
+ });
2081
+ } else throw new Error(`未知 preset 动作:${action}(list / set / remove)`);
2082
+ console.log(JSON.stringify(out, null, 2));
2083
+ return 0;
2084
+ }
2085
+ if (cmd === "component") {
2086
+ const action = file ?? "list";
2087
+ if (action === "list") out = await call("paintress_component", {
2088
+ action: "list",
2089
+ ...a1 !== void 0 ? { path: a1 } : {}
2090
+ });
2091
+ else if (action === "insert") {
2092
+ if (!a1 || !a2) throw new Error("用法:paintress component insert <file> <id> [--parent p] [--index n]");
2093
+ out = await call("paintress_component", {
2094
+ action: "insert",
2095
+ path: a1,
2096
+ id: a2,
2097
+ ...flagStr(flags, "parent") !== void 0 ? { parent: flagStr(flags, "parent") } : {},
2098
+ ...flagNum(flags, "index") !== void 0 ? { index: flagNum(flags, "index") } : {}
2099
+ });
2100
+ } else throw new Error(`未知 component 动作:${action}(list / insert)`);
2101
+ console.log(JSON.stringify(out, null, 2));
2102
+ return 0;
2103
+ }
2104
+ if (!file) throw new Error("缺少 <file> 参数");
1801
2105
  switch (cmd) {
1802
2106
  case "new": {
1803
2107
  const name = flagStr(flags, "name");
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { It as Affine, it as BackgroundBlurEffect, bt as BlendMode, at as BlurEffect, Lt as BoolOrVar, z as BrowserSchema, Rt as Color, zt as ColorOrVar, Bt as CornerRadius, xt as CrossAxis, j as DocumentObjectSchema, M as DocumentSchema, ot as EffectEntry, st as Effects, B as EllipseSchema, w as FileChangedError, ct as FillEntry, St as FillRule, lt as Fills, V as FrameSchema, ut as GradientFill, Ct as GradientType, dt as GraphicsBlock, H as GroupSchema, U as IconFontSchema, wt as IconLibrary, W as IconSchema, ft as ImageFill, Tt as ImageFit, G as KNOWN_NODE_TYPES, K as LayoutBlock, Et as LayoutDir, Dt as MainAxis, pt as MeshGradientFill, q as NodeBase, J as NodeSchema, Y as NoteSchema, Vt as NumOrVar, N as PEN_DOC_VERSION, P as PTR_SPEC_VERSION, Ht as Padding, X as PathSchema, Z as PolygonSchema, Ot as PositionMode, F as PtrDocumentSchema, Q as RectangleSchema, $ as RefSchema, et as ScriptSchema, mt as ShaderEffect, ht as ShaderFill, gt as ShadowEffect, kt as ShadowKind, Ut as Sizing, _t as SolidFill, Wt as StringOrVar, At as StrokeAlign, vt as StrokeBlock, jt as StrokeCap, Mt as StrokeJoin, yt as StrokeWidth, Nt as TextAlign, Pt as TextAlignVertical, Ft as TextGrowth, tt as TextSchema, nt as UnknownNodeSchema, Gt as VARIABLE_NAME, I as VariableDef, Kt as VariableRef, L as XPaintress, R as X_PAINTRESS_KEY, g as batch, n as boardBoxes, _ as buildIndex, k as collectIds, c as commonToCss, r as computeCanvas, l as cornerRadiusToCss, u as effectsToCss, i as escapeAttr, a as escapeHtml, m as expandRefs, d as fillsToCss, A as genId, v as get, s as getLucideIcon, y as insert, T as loadDoc, h as makeVarResolver, b as move, x as query, S as remove, t as renderDocument, o as renderTree, E as saveDoc, rt as schemaForType, D as serializeDoc, f as strokeToCss, p as textToCss, C as update, O as validateDoc } from "./render-CCxYNL6A.js";
1
+ export { It as Affine, it as BackgroundBlurEffect, bt as BlendMode, at as BlurEffect, Lt as BoolOrVar, z as BrowserSchema, Rt as Color, zt as ColorOrVar, Bt as CornerRadius, xt as CrossAxis, j as DocumentObjectSchema, M as DocumentSchema, ot as EffectEntry, st as Effects, B as EllipseSchema, w as FileChangedError, ct as FillEntry, St as FillRule, lt as Fills, V as FrameSchema, ut as GradientFill, Ct as GradientType, dt as GraphicsBlock, H as GroupSchema, U as IconFontSchema, wt as IconLibrary, W as IconSchema, ft as ImageFill, Tt as ImageFit, G as KNOWN_NODE_TYPES, K as LayoutBlock, Et as LayoutDir, Dt as MainAxis, pt as MeshGradientFill, q as NodeBase, J as NodeSchema, Y as NoteSchema, Vt as NumOrVar, N as PEN_DOC_VERSION, P as PTR_SPEC_VERSION, Ht as Padding, X as PathSchema, Z as PolygonSchema, Ot as PositionMode, F as PtrDocumentSchema, Q as RectangleSchema, $ as RefSchema, et as ScriptSchema, mt as ShaderEffect, ht as ShaderFill, gt as ShadowEffect, kt as ShadowKind, Ut as Sizing, _t as SolidFill, Wt as StringOrVar, At as StrokeAlign, vt as StrokeBlock, jt as StrokeCap, Mt as StrokeJoin, yt as StrokeWidth, Nt as TextAlign, Pt as TextAlignVertical, Ft as TextGrowth, tt as TextSchema, nt as UnknownNodeSchema, Gt as VARIABLE_NAME, I as VariableDef, Kt as VariableRef, L as XPaintress, R as X_PAINTRESS_KEY, g as batch, n as boardBoxes, _ as buildIndex, k as collectIds, c as commonToCss, r as computeCanvas, l as cornerRadiusToCss, u as effectsToCss, i as escapeAttr, a as escapeHtml, m as expandRefs, d as fillsToCss, A as genId, v as get, s as getLucideIcon, y as insert, T as loadDoc, h as makeVarResolver, b as move, x as query, S as remove, t as renderDocument, o as renderTree, E as saveDoc, rt as schemaForType, D as serializeDoc, f as strokeToCss, p as textToCss, C as update, O as validateDoc } from "./render-BSf62388.js";
@@ -1 +1 @@
1
- export { t as createMcpServer, n as runStdioServer } from "../server-ssrAxbTR.js";
1
+ export { t as createMcpServer, n as runStdioServer } from "../server-DZRNNWac.js";
@@ -597,7 +597,7 @@ function collectIds(children, out = []) {
597
597
  }
598
598
  // src/doc/validate.ts
599
599
  /** 结构校验(schema)+ 语义校验(重复 id、悬空 ref、未知类型、未解析变量) */
600
- function validateDoc(input) {
600
+ function validateDoc(input, opts = {}) {
601
601
  const errors = [];
602
602
  const warnings = [];
603
603
  const parsed = DocumentSchema.safeParse(input);
@@ -648,7 +648,13 @@ function validateDoc(input) {
648
648
  }
649
649
  };
650
650
  walk(doc.children, "children");
651
- const varNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
651
+ const localNames = new Set(doc.variables ? Object.keys(doc.variables) : []);
652
+ const varNames = /* @__PURE__ */ new Set([...localNames, ...opts.knownVars ?? []]);
653
+ for (const name of opts.presetVars ?? []) if (localNames.has(name)) warnings.push({
654
+ level: "warning",
655
+ path: "<variables>",
656
+ message: `本地变量 $${name} 覆盖了项目预设(在查看器里调预设不会影响这份文件)`
657
+ });
652
658
  const varRefs = /* @__PURE__ */ new Set();
653
659
  const scan = (obj) => {
654
660
  if (typeof obj === "string" && VARIABLE_NAME.test(obj)) varRefs.add(obj.slice(1));
@@ -1816,7 +1822,7 @@ function renderDocument(doc, opts = {}) {
1816
1822
  `<title>${escapeAttr(title)}</title>`,
1817
1823
  "<style>",
1818
1824
  "* { box-sizing: border-box; margin: 0; padding: 0; }",
1819
- "body { margin: 0; background: #ffffff; }",
1825
+ `body { margin: 0; background: ${opts.transparent === true ? "transparent" : "#ffffff"}; }`,
1820
1826
  ".ptr-canvas { position: relative; }",
1821
1827
  "</style>",
1822
1828
  "</head>",