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/README.md +53 -14
- package/README.zh-CN.md +54 -13
- package/dist/cli.js +492 -31
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/{render-BSf62388.js → render-DWs1iQ0v.js} +878 -56
- package/dist/server-DBRmwE_v.js +3462 -0
- package/package.json +1 -1
- package/skills/paintress/SKILL.md +37 -24
- package/ui/dist/assets/index-CSUM68ms.js +8 -0
- package/ui/dist/assets/{index-BFcvu7wr.css → index-CXBgFia_.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/dist/server-DZRNNWac.js +0 -1599
- package/ui/dist/assets/index-CEhK1JXE.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as
|
|
3
|
-
import { T as loadDoc,
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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["
|
|
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-
|
|
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 === "
|
|
2065
|
-
|
|
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
|
|
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
|
-
...
|
|
2665
|
+
...flagDecimal(flags, "scale") !== void 0 ? { scale: flagDecimal(flags, "scale") } : {},
|
|
2205
2666
|
...board !== void 0 ? { board } : {}
|
|
2206
2667
|
});
|
|
2207
2668
|
break;
|