paintress 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { A as presetPathFor, C as ensureProjectScaffold, D as listPresetGroups, E as listComponents, F as setProjectRoots, M as projectFontRegistry, N as readTextIfExists, O as listPresets, P as setPreset, S as componentsPathFor, T as findProjectRoot, _ as LineDecoder, a as ensureDaemon, b as screenshotPen, c as isDaemonRunning, d as lockPath, f as pidPath, g as socketPath, h as sanitizeProjectName, j as projectFilesInfo, k as notePathFor, 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 findNodeIn, x as applyProjectPresets, y as findBrowser } from "./server-DBRmwE_v.js";
3
+ import { P as planFonts, T as loadDoc, rn as VERSION, t as renderDocument, z as isPresetType } from "./render-DWs1iQ0v.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
- import { homedir } from "node:os";
6
+ import { homedir, tmpdir } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { dirname, extname, join, relative, resolve, sep } from "node:path";
9
+ import { randomUUID } from "node:crypto";
9
10
  import { createServer } from "node:net";
10
11
  import { createServer as createServer$1 } from "node:http";
11
12
  import { readFile } from "node:fs/promises";
@@ -121,18 +122,24 @@ var Registry = class {
121
122
  var MarksStore = class {
122
123
  byProject = /* @__PURE__ */ new Map();
123
124
  seq = 0;
124
- /** 同一项目内同一 nodeId 复用既有短号;否则分配新号 */
125
+ /** 短号按「文件 + nodeId」去重:节点 id 只在文件内唯一,跨文件同 id 不能复用 */
126
+ key(project, nodeId, file) {
127
+ return `${project}\u0000${file ?? ""}\u0000${nodeId}`;
128
+ }
129
+ /** 同一项目内同一文件同一 nodeId 复用既有短号;否则分配新号 */
125
130
  add(input) {
126
131
  let bucket = this.byProject.get(input.project);
127
132
  if (!bucket) {
128
133
  bucket = /* @__PURE__ */ new Map();
129
134
  this.byProject.set(input.project, bucket);
130
135
  }
131
- const existing = bucket.get(input.nodeId);
136
+ const key = this.key(input.project, input.nodeId, input.file);
137
+ const existing = bucket.get(key);
132
138
  if (existing) {
133
139
  if (input.name !== void 0) existing.name = input.name;
134
140
  if (input.type !== void 0) existing.type = input.type;
135
141
  if (input.path !== void 0) existing.path = input.path;
142
+ if (input.file !== void 0) existing.file = input.file;
136
143
  existing.at = Date.now();
137
144
  return existing;
138
145
  }
@@ -140,21 +147,22 @@ var MarksStore = class {
140
147
  const mark = {
141
148
  shortId: this.seq,
142
149
  project: input.project,
150
+ ...input.file !== void 0 ? { file: input.file } : {},
143
151
  nodeId: input.nodeId,
144
152
  name: input.name,
145
153
  type: input.type,
146
154
  path: input.path ?? input.nodeId,
147
155
  at: Date.now()
148
156
  };
149
- bucket.set(input.nodeId, mark);
157
+ bucket.set(key, mark);
150
158
  return mark;
151
159
  }
152
160
  list(project) {
153
161
  const bucket = this.byProject.get(project);
154
162
  return bucket ? [...bucket.values()].sort((a, b) => a.shortId - b.shortId) : [];
155
163
  }
156
- get(project, nodeId) {
157
- return this.byProject.get(project)?.get(nodeId);
164
+ get(project, nodeId, file) {
165
+ return this.byProject.get(project)?.get(this.key(project, nodeId, file));
158
166
  }
159
167
  clear(project) {
160
168
  const bucket = this.byProject.get(project);
@@ -163,8 +171,16 @@ var MarksStore = class {
163
171
  this.byProject.delete(project);
164
172
  return n;
165
173
  }
166
- remove(project, nodeId) {
167
- return this.byProject.get(project)?.delete(nodeId) ?? false;
174
+ remove(project, nodeId, file) {
175
+ const bucket = this.byProject.get(project);
176
+ if (bucket === void 0) return false;
177
+ if (file !== void 0) return bucket.delete(this.key(project, nodeId, file));
178
+ let removed = false;
179
+ for (const [key, mark] of bucket) if (mark.nodeId === nodeId) {
180
+ bucket.delete(key);
181
+ removed = true;
182
+ }
183
+ return removed;
168
184
  }
169
185
  };
170
186
  // src/web/changes.ts
@@ -423,6 +439,10 @@ const MIME = {
423
439
  function mimeOf(filePath) {
424
440
  return MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream";
425
441
  }
442
+ /** 项目内资源 → /files URL(项目名可能带斜杠,按段编码) */
443
+ function filesUrl(project, relPath) {
444
+ return `/files/${project.split("/").map(encodeURIComponent).join("/")}/${relPath.split("/").filter((seg) => seg !== "").map(encodeURIComponent).join("/")}`;
445
+ }
426
446
  /** 定位前端产物目录:源码运行 / 构建后 / cwd 三种候选,找不到则回退精简视图 */
427
447
  function findUiDist() {
428
448
  const here = dirname(fileURLToPath(import.meta.url));
@@ -527,10 +547,50 @@ function esc(s) {
527
547
  function isTrustedWrite(req) {
528
548
  return req.headers["x-paintress"] === "1";
529
549
  }
550
+ /**
551
+ * 属性面板用的变量表:项目预设打底、文档本地同名优先。
552
+ * themed 列表取第一个可显示的值(面板只做展示,不替人挑主题)。
553
+ */
554
+ function panelVars(fileAbs) {
555
+ const out = {};
556
+ const collect = (doc) => {
557
+ for (const [key, def] of Object.entries(doc?.variables ?? {})) {
558
+ const raw = def["value"];
559
+ const first = Array.isArray(raw) ? raw.map((e) => e !== null && typeof e === "object" ? e["value"] : void 0).find((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean") : raw;
560
+ if (typeof first === "string" || typeof first === "number" || typeof first === "boolean") out[key] = String(first);
561
+ }
562
+ };
563
+ try {
564
+ const p = presetPathFor(fileAbs);
565
+ if (p !== null) collect(loadDoc(p).doc);
566
+ collect(loadDoc(fileAbs).doc);
567
+ } catch {}
568
+ return out;
569
+ }
570
+ var FontReportStore = class {
571
+ byFile = /* @__PURE__ */ new Map();
572
+ set(project, path, report) {
573
+ this.byFile.set(`${project}\u0000${path}`, {
574
+ ...report,
575
+ project,
576
+ path,
577
+ at: Date.now()
578
+ });
579
+ }
580
+ get(project, path) {
581
+ return this.byFile.get(`${project}\u0000${path}`);
582
+ }
583
+ list(project) {
584
+ return [...this.byFile.values()].filter((r) => r.project === project);
585
+ }
586
+ };
530
587
  async function startDaemon(opts = {}) {
531
588
  const releaseLock = acquireLock();
532
589
  const registry = new Registry();
533
590
  const marks = new MarksStore();
591
+ /** daemon 自己也是 doc 层的一个调用方:注册根就是它的项目表 */
592
+ const syncProjectRoots = () => setProjectRoots(registry.all().map((p) => p.root));
593
+ const fontReports = new FontReportStore();
534
594
  const changes = new ChangesStore();
535
595
  const watchers = new WatchManager();
536
596
  const versions = new VersionChecker();
@@ -683,6 +743,12 @@ async function startDaemon(opts = {}) {
683
743
  version: VERSION
684
744
  });
685
745
  break;
746
+ case "fonts":
747
+ reply({
748
+ ok: true,
749
+ reports: fontReports.list(req.project)
750
+ });
751
+ break;
686
752
  case "watch": {
687
753
  everHadProjects = true;
688
754
  const entry = registry.watch(req.project, req.dir, req.depth, req.owner === "agent" ? { owner: "agent" } : {});
@@ -694,6 +760,7 @@ async function startDaemon(opts = {}) {
694
760
  } catch (cause) {
695
761
  process.stderr.write(`paintress web: 项目脚手架创建失败(${cause instanceof Error ? cause.message : String(cause)})\n`);
696
762
  }
763
+ syncProjectRoots();
697
764
  reply({
698
765
  ok: true,
699
766
  watched: true,
@@ -705,6 +772,7 @@ async function startDaemon(opts = {}) {
705
772
  case "unwatch": {
706
773
  const before = registry.get(req.project);
707
774
  const removed = before === void 0 ? false : detachProject(before, "unwatch");
775
+ syncProjectRoots();
708
776
  reply({
709
777
  ok: true,
710
778
  unwatched: removed
@@ -898,6 +966,61 @@ async function startDaemon(opts = {}) {
898
966
  });
899
967
  return;
900
968
  }
969
+ if (segments[1] === "fonts" && segments.length === 2) {
970
+ const qProject = url.searchParams.get("project") ?? "";
971
+ const qFile = url.searchParams.get("path") ?? "";
972
+ if (req.method === "POST") {
973
+ const raw = await readBody();
974
+ let body;
975
+ try {
976
+ body = JSON.parse(raw);
977
+ } catch {
978
+ sendJson(400, {
979
+ ok: false,
980
+ error: "请求体不是合法 JSON"
981
+ });
982
+ return;
983
+ }
984
+ const project = typeof body["project"] === "string" ? body["project"] : qProject;
985
+ const path = typeof body["path"] === "string" ? body["path"] : qFile;
986
+ if (project === "" || path === "") {
987
+ sendJson(400, {
988
+ ok: false,
989
+ error: "缺少 project / path"
990
+ });
991
+ return;
992
+ }
993
+ if (!registry.get(project)) {
994
+ sendJson(404, {
995
+ ok: false,
996
+ error: `未注册的项目:${project}`
997
+ });
998
+ return;
999
+ }
1000
+ const checked = Array.isArray(body["checked"]) ? body["checked"].map(String) : [];
1001
+ const missing = Array.isArray(body["missing"]) ? body["missing"].map(String) : [];
1002
+ fontReports.set(project, path, {
1003
+ checked,
1004
+ missing,
1005
+ substituted: [],
1006
+ source: "viewer"
1007
+ });
1008
+ sendJson(200, { ok: true });
1009
+ return;
1010
+ }
1011
+ if (req.method !== "GET") {
1012
+ sendJson(405, {
1013
+ ok: false,
1014
+ error: "不支持的方法"
1015
+ });
1016
+ return;
1017
+ }
1018
+ sendJson(200, {
1019
+ ok: true,
1020
+ reports: fontReports.list(qProject)
1021
+ });
1022
+ return;
1023
+ }
901
1024
  if (segments[1] === "changes") {
902
1025
  const qProject = url.searchParams.get("project") ?? "";
903
1026
  const qFile = url.searchParams.get("path") ?? "";
@@ -909,6 +1032,132 @@ async function startDaemon(opts = {}) {
909
1032
  });
910
1033
  return;
911
1034
  }
1035
+ if (segments[1] === "export" && req.method === "GET") {
1036
+ const qProject = url.searchParams.get("project") ?? "";
1037
+ const qFile = url.searchParams.get("path") ?? "";
1038
+ const qBoard = url.searchParams.get("board") ?? "";
1039
+ const qScale = Number(url.searchParams.get("scale") ?? "1");
1040
+ if (qProject === "" || qFile === "" || qBoard === "") {
1041
+ sendJson(400, {
1042
+ ok: false,
1043
+ error: "缺少 project / path / board"
1044
+ });
1045
+ return;
1046
+ }
1047
+ if (qScale !== .5 && qScale !== 1 && qScale !== 2) {
1048
+ sendJson(400, {
1049
+ ok: false,
1050
+ error: "scale 只支持 0.5 / 1 / 2"
1051
+ });
1052
+ return;
1053
+ }
1054
+ const entry = registry.get(qProject);
1055
+ if (!entry) {
1056
+ sendJson(404, {
1057
+ ok: false,
1058
+ error: `未注册的项目:${qProject}`
1059
+ });
1060
+ return;
1061
+ }
1062
+ const abs = safeJoin(entry.rootReal, qFile);
1063
+ if (!abs) {
1064
+ sendJson(400, {
1065
+ ok: false,
1066
+ error: `路径不在项目内:${qFile}`
1067
+ });
1068
+ return;
1069
+ }
1070
+ if (findBrowser() === null) {
1071
+ sendJson(501, {
1072
+ ok: false,
1073
+ error: "本机没有 Chromium/Chrome,无法导出 PNG(可用 PTR_BROWSER 指定)"
1074
+ });
1075
+ return;
1076
+ }
1077
+ const tmpPng = join(tmpdir(), `paintress-export-${randomUUID()}.png`);
1078
+ try {
1079
+ const shot = await screenshotPen(abs, tmpPng, {
1080
+ board: qBoard,
1081
+ scale: qScale
1082
+ });
1083
+ const buf = await readFile(tmpPng);
1084
+ const raw = `${(qFile.split("/").pop() ?? qFile).replace(/\.[^.]+$/, "")}-${shot.board?.name !== void 0 && shot.board.name !== "" ? shot.board.name : shot.board?.id ?? qBoard}@${qScale}x.png`;
1085
+ const ascii = raw.replace(/[^A-Za-z0-9._@-]+/g, "_");
1086
+ res.writeHead(200, {
1087
+ "content-type": "image/png",
1088
+ "content-disposition": `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(raw)}`,
1089
+ "cache-control": "no-store",
1090
+ "x-paintress-scale": String(shot.scale)
1091
+ });
1092
+ res.end(buf);
1093
+ return;
1094
+ } catch (cause) {
1095
+ sendJson(400, {
1096
+ ok: false,
1097
+ error: cause instanceof Error ? cause.message : String(cause)
1098
+ });
1099
+ return;
1100
+ } finally {
1101
+ try {
1102
+ unlinkSync(tmpPng);
1103
+ } catch {}
1104
+ }
1105
+ }
1106
+ if (segments[1] === "node" && req.method === "GET") {
1107
+ const qProject = url.searchParams.get("project") ?? "";
1108
+ const qFile = url.searchParams.get("path") ?? "";
1109
+ const qId = url.searchParams.get("id") ?? "";
1110
+ if (qProject === "" || qFile === "" || qId === "") {
1111
+ sendJson(400, {
1112
+ ok: false,
1113
+ error: "缺少 project / path / id"
1114
+ });
1115
+ return;
1116
+ }
1117
+ const entry = registry.get(qProject);
1118
+ if (!entry) {
1119
+ sendJson(404, {
1120
+ ok: false,
1121
+ error: `未注册的项目:${qProject}`
1122
+ });
1123
+ return;
1124
+ }
1125
+ const abs = safeJoin(entry.rootReal, qFile);
1126
+ if (!abs) {
1127
+ sendJson(400, {
1128
+ ok: false,
1129
+ error: `路径不在项目内:${qFile}`
1130
+ });
1131
+ return;
1132
+ }
1133
+ let doc;
1134
+ try {
1135
+ doc = loadDoc(abs).doc;
1136
+ } catch (cause) {
1137
+ sendJson(404, {
1138
+ ok: false,
1139
+ error: cause instanceof Error ? cause.message : String(cause)
1140
+ });
1141
+ return;
1142
+ }
1143
+ const node = findNodeIn(doc.children, qId);
1144
+ if (node === null) {
1145
+ sendJson(404, {
1146
+ ok: false,
1147
+ error: `节点不存在:${qId}`
1148
+ });
1149
+ return;
1150
+ }
1151
+ entry.lastAccess = Date.now();
1152
+ const { children, ...fields } = node;
1153
+ sendJson(200, {
1154
+ ok: true,
1155
+ node: fields,
1156
+ childrenCount: Array.isArray(children) ? children.length : 0,
1157
+ vars: panelVars(abs)
1158
+ });
1159
+ return;
1160
+ }
912
1161
  if (segments[1] === "marks") {
913
1162
  const qProject = url.searchParams.get("project") ?? "";
914
1163
  if (req.method === "GET") {
@@ -951,6 +1200,7 @@ async function startDaemon(opts = {}) {
951
1200
  mark: marks.add({
952
1201
  project: projectIn,
953
1202
  nodeId,
1203
+ ...typeof body["file"] === "string" && body["file"] !== "" ? { file: body["file"] } : {},
954
1204
  ...typeof body["name"] === "string" ? { name: body["name"] } : {},
955
1205
  ...typeof body["type"] === "string" ? { type: body["type"] } : {},
956
1206
  ...typeof body["path"] === "string" ? { path: body["path"] } : {}
@@ -960,9 +1210,10 @@ async function startDaemon(opts = {}) {
960
1210
  }
961
1211
  if (req.method === "DELETE") {
962
1212
  const nodeId = url.searchParams.get("nodeId");
1213
+ const file = url.searchParams.get("file") ?? void 0;
963
1214
  sendJson(200, {
964
1215
  ok: true,
965
- cleared: nodeId ? marks.remove(qProject, nodeId) ? 1 : 0 : marks.clear(qProject)
1216
+ cleared: nodeId ? marks.remove(qProject, nodeId, file) ? 1 : 0 : marks.clear(qProject)
966
1217
  });
967
1218
  return;
968
1219
  }
@@ -1032,7 +1283,8 @@ async function startDaemon(opts = {}) {
1032
1283
  sendJson(200, {
1033
1284
  ok: true,
1034
1285
  path,
1035
- items
1286
+ items,
1287
+ groups: listPresetGroups(entry.rootReal).groups
1036
1288
  });
1037
1289
  return;
1038
1290
  }
@@ -1078,7 +1330,7 @@ async function startDaemon(opts = {}) {
1078
1330
  if (!isPresetType(typeRaw)) {
1079
1331
  sendJson(400, {
1080
1332
  ok: false,
1081
- error: "type 必须是 color 或 radius"
1333
+ error: "type 必须是 color / radius / spacing / fontSize / font"
1082
1334
  });
1083
1335
  return;
1084
1336
  }
@@ -1094,7 +1346,8 @@ async function startDaemon(opts = {}) {
1094
1346
  sendJson(200, {
1095
1347
  ok: true,
1096
1348
  path: presetPathFor(entry.rootReal),
1097
- items
1349
+ items,
1350
+ groups: listPresetGroups(entry.rootReal).groups
1098
1351
  });
1099
1352
  } catch (cause) {
1100
1353
  sendJson(400, {
@@ -1234,10 +1487,21 @@ async function startDaemon(opts = {}) {
1234
1487
  return;
1235
1488
  }
1236
1489
  const loaded = loadDoc(abs);
1237
- const rendered = renderDocument(applyProjectPresets(loaded.doc, abs).doc, { title: relPath });
1490
+ const merged = applyProjectPresets(loaded.doc, abs).doc;
1491
+ const { registry: fontRegistry } = projectFontRegistry(abs);
1492
+ const fontPlan = planFonts(fontRegistry, (source) => source.target === void 0 ? null : filesUrl(entry.name, source.target));
1493
+ const rendered = renderDocument(merged, {
1494
+ title: relPath,
1495
+ fonts: fontPlan,
1496
+ probeFonts: url.searchParams.get("embed") === "1"
1497
+ });
1238
1498
  const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/") + 1) : "";
1239
1499
  const baseHref = `/files/${entry.name.split("/").map(encodeURIComponent).join("/")}/${dir.split("/").filter((s) => s !== "").map(encodeURIComponent).join("/")}${dir ? "/" : ""}`;
1240
1500
  let html = rendered.html.replace("<head>", `<head>\n<base href="${baseHref}">`);
1501
+ if (url.searchParams.get("embed") === "1") {
1502
+ const fontApi = `/api/fonts?project=${encodeURIComponent(entry.name)}&path=${encodeURIComponent(relPath)}`;
1503
+ html = html.replace("</head>", `<script>window.PAINTRESS_FONT_URL=${JSON.stringify(fontApi)}<\/script>\n</head>`);
1504
+ }
1241
1505
  if (url.searchParams.get("embed") !== "1") {
1242
1506
  const liveScript = `<script>\n(function(){\n var es = new EventSource('/events?project=${encodeURIComponent(entry.name)}&path=${encodeURIComponent(relPath)}');\n es.addEventListener('change', function(){ location.reload(); });\n})();\n<\/script>`;
1243
1507
  html = html.replace("</body>", `${liveScript}</body>`);
@@ -1264,12 +1528,16 @@ async function startDaemon(opts = {}) {
1264
1528
  send(404, "component not found", "text/plain; charset=utf-8");
1265
1529
  return;
1266
1530
  }
1267
- send(200, renderDocument({
1531
+ const single = {
1268
1532
  ...doc,
1269
1533
  children: [node]
1270
- }, {
1534
+ };
1535
+ const { registry: fontRegistry } = projectFontRegistry(compPath);
1536
+ const fontPlan = planFonts(fontRegistry, (source) => source.target === void 0 ? null : filesUrl(entry.name, source.target));
1537
+ send(200, renderDocument(single, {
1271
1538
  title: id,
1272
- transparent: true
1539
+ transparent: true,
1540
+ fonts: fontPlan
1273
1541
  }).html);
1274
1542
  return;
1275
1543
  }
@@ -1286,10 +1554,27 @@ async function startDaemon(opts = {}) {
1286
1554
  send(404, "file not found", "text/plain; charset=utf-8");
1287
1555
  return;
1288
1556
  }
1557
+ const ext = extname(abs).toLowerCase();
1558
+ const isFont = ext === ".woff" || ext === ".woff2" || ext === ".ttf" || ext === ".otf";
1559
+ if (isFont) try {
1560
+ if (statSync(abs).size > 33554432) {
1561
+ sendJson(413, {
1562
+ ok: false,
1563
+ error: "字体文件超过 32 MB,拒绝加载"
1564
+ });
1565
+ return;
1566
+ }
1567
+ } catch {
1568
+ sendJson(404, {
1569
+ ok: false,
1570
+ error: "file not found"
1571
+ });
1572
+ return;
1573
+ }
1289
1574
  const buf = await readFile(abs);
1290
1575
  res.writeHead(200, {
1291
1576
  "content-type": mimeOf(abs),
1292
- "cache-control": "no-cache"
1577
+ "cache-control": isFont ? "public, max-age=31536000, immutable" : "no-cache"
1293
1578
  });
1294
1579
  res.end(buf);
1295
1580
  return;
@@ -1742,6 +2027,7 @@ const FLAG_KEYS = /* @__PURE__ */ new Set([
1742
2027
  "scale",
1743
2028
  "port",
1744
2029
  "idle",
2030
+ "foreground",
1745
2031
  "daemon",
1746
2032
  "dry-run",
1747
2033
  "opencode-layout",
@@ -1754,14 +2040,28 @@ const FLAG_KEYS = /* @__PURE__ */ new Set([
1754
2040
  "force",
1755
2041
  "board",
1756
2042
  "parent",
2043
+ "apply",
2044
+ "min-uses",
2045
+ "key",
2046
+ "group",
2047
+ "prop",
2048
+ "value",
2049
+ "preview",
2050
+ "note",
2051
+ "metrics",
2052
+ "embed-fonts",
1757
2053
  NO_OPEN_FLAG
1758
2054
  ]);
2055
+ /** `daemon` 只是历史别名:默认行为已经是后台,留着不报错 */
1759
2056
  const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
1760
2057
  "regex",
1761
2058
  "reusable",
2059
+ "foreground",
1762
2060
  "daemon",
1763
2061
  "dry-run",
1764
2062
  "force",
2063
+ "apply",
2064
+ "embed-fonts",
1765
2065
  NO_OPEN_FLAG
1766
2066
  ]);
1767
2067
  /** 纯函数:argv → 命令 + 位置参数 + 标志(可单测) */
@@ -1810,6 +2110,14 @@ function flagNum(f, key) {
1810
2110
  if (!Number.isInteger(n) || n < 0) throw new Error(`--${key} 需要非负整数,收到:${v}`);
1811
2111
  return n;
1812
2112
  }
2113
+ /** 连续数值参数(截图倍率 0.2–2);与整数参数分开,不能把 0.8 当成非法 */
2114
+ function flagDecimal(f, key) {
2115
+ const v = flagStr(f, key);
2116
+ if (v === void 0) return void 0;
2117
+ const n = Number(v);
2118
+ if (!Number.isFinite(n)) throw new Error(`--${key} 需要数字,收到:${v}`);
2119
+ return n;
2120
+ }
1813
2121
  function parseJson(text, what) {
1814
2122
  try {
1815
2123
  return JSON.parse(text);
@@ -1823,6 +2131,23 @@ async function call(tool, args) {
1823
2131
  return await def.run(args);
1824
2132
  }
1825
2133
  /** 入口:返回退出码(可单测) */
2134
+ /** 当前目录所在项目里两个关键文件的现状(只报告,不创建) */
2135
+ function cwdDesignSystem() {
2136
+ try {
2137
+ const root = findProjectRoot(process.cwd());
2138
+ return root === null ? null : projectFilesInfo(root);
2139
+ } catch {
2140
+ return null;
2141
+ }
2142
+ }
2143
+ /** CLI 进程也要用「注册根」解析项目文件:从 daemon 拉一次项目表即可 */
2144
+ async function syncProjectRoots() {
2145
+ if (!isDaemonRunning()) return;
2146
+ try {
2147
+ const res = await sendRequest({ kind: "status" }, 2e3);
2148
+ if (res.ok && "projects" in res && Array.isArray(res.projects)) setProjectRoots(res.projects.map((p) => p.root));
2149
+ } catch {}
2150
+ }
1826
2151
  async function main(argv) {
1827
2152
  if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
1828
2153
  console.log(`paintress ${VERSION}`);
@@ -1830,10 +2155,11 @@ async function main(argv) {
1830
2155
  }
1831
2156
  const parsed = parseArgv(argv);
1832
2157
  if (!parsed) {
1833
- console.error("用法:paintress <mcp|web|ping|status|watch|unwatch|attach|stop|new|info|query|get|update|insert|delete|move|batch|render|shot|validate|install|doctor|mcp-config> ...(版本:paintress --version)");
2158
+ console.error("用法:paintress <mcp|web|ping|status|watch|unwatch|attach|stop|new|info|query|get|update|insert|delete|move|batch|render|shot|validate|audit|establish|adjust|install|doctor|mcp-config> ...(版本:paintress --version)");
1834
2159
  return 2;
1835
2160
  }
1836
2161
  const { cmd, positionals, flags } = parsed;
2162
+ await syncProjectRoots();
1837
2163
  const [file, a1, a2] = positionals;
1838
2164
  try {
1839
2165
  if (cmd === "mcp") {
@@ -1845,7 +2171,7 @@ async function main(argv) {
1845
2171
  console.log(JSON.stringify({
1846
2172
  ok: false,
1847
2173
  running: false,
1848
- hint: "未运行:paintress web --daemon"
2174
+ hint: "未运行:paintress web"
1849
2175
  }));
1850
2176
  return 1;
1851
2177
  }
@@ -1996,11 +2322,12 @@ async function main(argv) {
1996
2322
  reused: true,
1997
2323
  pid: running.pid,
1998
2324
  port: running.port,
1999
- http: url(running.port)
2325
+ http: url(running.port),
2326
+ designSystem: cwdDesignSystem()
2000
2327
  }, null, 2));
2001
2328
  return 0;
2002
2329
  }
2003
- if (flags["daemon"] === true) {
2330
+ if (flags["foreground"] !== true) {
2004
2331
  const res = await ensureDaemon({
2005
2332
  ...port !== void 0 ? { port } : {},
2006
2333
  ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
@@ -2012,7 +2339,8 @@ async function main(argv) {
2012
2339
  reused: !res.started,
2013
2340
  pid: res.pid,
2014
2341
  port: res.port,
2015
- http: url(res.port)
2342
+ http: url(res.port),
2343
+ designSystem: cwdDesignSystem()
2016
2344
  }, null, 2));
2017
2345
  return 0;
2018
2346
  }
@@ -2045,7 +2373,7 @@ async function main(argv) {
2045
2373
  http
2046
2374
  }, null, 2));
2047
2375
  if (!noOpen) {
2048
- const { openBrowser } = await import("./server-DZRNNWac.js").then((n) => n.i);
2376
+ const { openBrowser } = await import("./server-DBRmwE_v.js").then((n) => n.i);
2049
2377
  openBrowser(`${http}/`);
2050
2378
  }
2051
2379
  await new Promise((resolve) => {
@@ -2061,8 +2389,45 @@ async function main(argv) {
2061
2389
  action: "list",
2062
2390
  ...a1 !== void 0 ? { path: a1 } : {}
2063
2391
  });
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 名称]");
2392
+ else if (action === "groups") {
2393
+ const full = await call("paintress_preset", {
2394
+ action: "list",
2395
+ ...a1 !== void 0 ? { path: a1 } : {}
2396
+ });
2397
+ out = {
2398
+ ok: true,
2399
+ path: full.path ?? null,
2400
+ groups: full.groups ?? {}
2401
+ };
2402
+ } else if (action === "seed") out = await call("paintress_preset", {
2403
+ action: "seed",
2404
+ ...a1 !== void 0 ? { path: a1 } : {},
2405
+ ...flags["apply"] === true ? { apply: true } : {}
2406
+ });
2407
+ else if (action === "set-group") {
2408
+ if (!a1 || !a2 || !positionals[3] || !positionals[4]) throw new Error("用法:paintress preset set-group <dir> <组名> <target> '<props-json>'");
2409
+ let props;
2410
+ try {
2411
+ props = JSON.parse(positionals[4]);
2412
+ } catch {
2413
+ throw new Error(`props 不是合法 JSON:${positionals[4]}`);
2414
+ }
2415
+ out = await call("paintress_preset", {
2416
+ action: "set-group",
2417
+ path: a1,
2418
+ group: a2,
2419
+ target: positionals[3],
2420
+ props
2421
+ });
2422
+ } else if (action === "remove-group") {
2423
+ if (!a1 || !a2) throw new Error("用法:paintress preset remove-group <dir> <组名>");
2424
+ out = await call("paintress_preset", {
2425
+ action: "remove-group",
2426
+ path: a1,
2427
+ group: a2
2428
+ });
2429
+ } else if (action === "set") {
2430
+ if (!a1 || !a2 || !positionals[3] || !positionals[4]) throw new Error("用法:paintress preset set <dir> <key> <color|radius|spacing|fontSize|font> <value> [--name 名称]");
2066
2431
  out = await call("paintress_preset", {
2067
2432
  action: "set",
2068
2433
  path: a1,
@@ -2078,7 +2443,7 @@ async function main(argv) {
2078
2443
  path: a1,
2079
2444
  key: a2
2080
2445
  });
2081
- } else throw new Error(`未知 preset 动作:${action}(list / set / remove)`);
2446
+ } else throw new Error(`未知 preset 动作:${action}(list / set / remove / seed / set-group / remove-group)`);
2082
2447
  console.log(JSON.stringify(out, null, 2));
2083
2448
  return 0;
2084
2449
  }
@@ -2097,7 +2462,76 @@ async function main(argv) {
2097
2462
  ...flagStr(flags, "parent") !== void 0 ? { parent: flagStr(flags, "parent") } : {},
2098
2463
  ...flagNum(flags, "index") !== void 0 ? { index: flagNum(flags, "index") } : {}
2099
2464
  });
2100
- } else throw new Error(`未知 component 动作:${action}(list / insert)`);
2465
+ } else if (action === "add") {
2466
+ if (!a1 || !a2) throw new Error("用法:paintress component add <file> <id> [--name 名称] [--parent p] [--index n]");
2467
+ out = await call("paintress_component", {
2468
+ action: "add",
2469
+ path: a1,
2470
+ id: a2,
2471
+ ...flagStr(flags, "name") !== void 0 ? { name: flagStr(flags, "name") } : {},
2472
+ ...flagStr(flags, "parent") !== void 0 ? { parent: flagStr(flags, "parent") } : {},
2473
+ ...flagNum(flags, "index") !== void 0 ? { index: flagNum(flags, "index") } : {}
2474
+ });
2475
+ } else throw new Error(`未知 component 动作:${action}(list / insert / add)`);
2476
+ console.log(JSON.stringify(out, null, 2));
2477
+ return 0;
2478
+ }
2479
+ if (cmd === "font") {
2480
+ const action = file ?? "list";
2481
+ if (action === "list") out = await call("paintress_font", {
2482
+ action: "list",
2483
+ ...a1 !== void 0 ? { path: a1 } : {}
2484
+ });
2485
+ else if (action === "remove") {
2486
+ if (!a1 || !a2) throw new Error("用法:paintress font remove <dir> <family>");
2487
+ out = await call("paintress_font", {
2488
+ action: "remove",
2489
+ path: a1,
2490
+ family: a2
2491
+ });
2492
+ } else if (action === "default") {
2493
+ if (!a1 || !a2) throw new Error("用法:paintress font default <dir> <token-or-family>");
2494
+ out = await call("paintress_font", {
2495
+ action: "default",
2496
+ path: a1,
2497
+ value: a2
2498
+ });
2499
+ } else if (action === "add") {
2500
+ if (!a1 || !a2 || !positionals[3]) throw new Error("用法:paintress font add <dir> <family> <web|css|file|system|bundle> [source] [--preview x] [--note n] [--metrics '{\"sizeAdjust\":\"96%\"}']");
2501
+ const metricsRaw = flagStr(flags, "metrics");
2502
+ let metrics;
2503
+ if (metricsRaw !== void 0) try {
2504
+ metrics = JSON.parse(metricsRaw);
2505
+ } catch {
2506
+ throw new Error(`metrics 不是合法 JSON:${metricsRaw}`);
2507
+ }
2508
+ const preview = flagStr(flags, "preview");
2509
+ const note = flagStr(flags, "note");
2510
+ out = await call("paintress_font", {
2511
+ action: "add",
2512
+ path: a1,
2513
+ family: a2,
2514
+ kind: positionals[3],
2515
+ ...positionals[4] !== void 0 ? { source: positionals[4] } : {},
2516
+ ...preview !== void 0 ? { preview } : {},
2517
+ ...note !== void 0 ? { note } : {},
2518
+ ...metrics !== void 0 ? { metrics } : {}
2519
+ });
2520
+ } else throw new Error(`未知 font 动作:${action}(list / add / remove / default)`);
2521
+ console.log(JSON.stringify(out, null, 2));
2522
+ return 0;
2523
+ }
2524
+ if (cmd === "project") {
2525
+ out = await call("paintress_project", { ...file !== void 0 ? { path: file } : {} });
2526
+ console.log(JSON.stringify(out, null, 2));
2527
+ return 0;
2528
+ }
2529
+ if (cmd === "convert") {
2530
+ out = await call("paintress_convert", {
2531
+ path: file,
2532
+ ...a1 !== void 0 ? { out: a1 } : {},
2533
+ ...flags["force"] === true ? { overwrite: true } : {}
2534
+ });
2101
2535
  console.log(JSON.stringify(out, null, 2));
2102
2536
  return 0;
2103
2537
  }
@@ -2191,9 +2625,36 @@ async function main(argv) {
2191
2625
  if (!a1) throw new Error("缺少 <out.html> 参数");
2192
2626
  out = await call("paintress_render", {
2193
2627
  path: file,
2194
- out: a1
2628
+ out: a1,
2629
+ ...flags["embed-fonts"] === true ? { embedFonts: true } : {}
2195
2630
  });
2196
2631
  break;
2632
+ case "audit":
2633
+ out = await call("paintress_audit", { path: file });
2634
+ break;
2635
+ case "establish":
2636
+ out = await call("paintress_establish", {
2637
+ path: file,
2638
+ ...flags["apply"] === true ? { apply: true } : {},
2639
+ ...flagNum(flags, "min-uses") !== void 0 ? { minUses: flagNum(flags, "min-uses") } : {}
2640
+ });
2641
+ break;
2642
+ case "adjust": {
2643
+ const key = flagStr(flags, "key");
2644
+ const group = flagStr(flags, "group");
2645
+ const prop = flagStr(flags, "prop");
2646
+ const value = flagStr(flags, "value");
2647
+ if (value === void 0) throw new Error("adjust 需要 --value");
2648
+ out = await call("paintress_adjust", {
2649
+ path: file,
2650
+ value,
2651
+ ...key !== void 0 ? { key } : {},
2652
+ ...group !== void 0 ? { group } : {},
2653
+ ...prop !== void 0 ? { prop } : {},
2654
+ ...flags["apply"] === true ? { apply: true } : {}
2655
+ });
2656
+ break;
2657
+ }
2197
2658
  case "shot": {
2198
2659
  if (!a1) throw new Error("缺少 <out.png> 参数");
2199
2660
  if (flags["board"] === true) throw new Error("--board 需要画板 id 或 name");
@@ -2201,7 +2662,7 @@ async function main(argv) {
2201
2662
  out = await call("paintress_screenshot", {
2202
2663
  path: file,
2203
2664
  out: a1,
2204
- ...flagNum(flags, "scale") !== void 0 ? { scale: flagNum(flags, "scale") } : {},
2665
+ ...flagDecimal(flags, "scale") !== void 0 ? { scale: flagDecimal(flags, "scale") } : {},
2205
2666
  ...board !== void 0 ? { board } : {}
2206
2667
  });
2207
2668
  break;