paintress 0.3.1 → 0.5.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,11 @@
1
1
  #!/usr/bin/env node
2
- import { a as isDaemonRunning, c as pidPath, d as sanitizeProjectName, f as socketPath, i as attachPersistent, l as resolveProjectDir, m as encodeLine, n as runStdioServer, o as sendRequest, p as LineDecoder, r as TOOLS, s as lockPath, u as safeJoin } from "./server-C4TC8Bw3.js";
3
- import { Wt as VERSION, t as renderDocument, w as loadDoc } from "./render-CbYe9rII.js";
2
+ import { a as isDaemonRunning, c as pidPath, d as sanitizeProjectName, f as socketPath, i as attachPersistent, l as resolveProjectDir, m as encodeLine, n as runStdioServer, o as sendRequest, p as LineDecoder, r as TOOLS, s as lockPath, u as safeJoin } from "./server-vPylVJZf.js";
3
+ import { T as loadDoc, qt as VERSION, t as renderDocument } from "./render-CzpO1cOe.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";
7
7
  import { fileURLToPath } from "node:url";
8
- import { dirname, extname, join, relative, resolve } from "node:path";
8
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
9
9
  import { createServer } from "node:net";
10
10
  import { createServer as createServer$1 } from "node:http";
11
11
  import { readFile } from "node:fs/promises";
@@ -21,7 +21,7 @@ const DESIGN_EXT = /* @__PURE__ */ new Set([".ptr", ".pen"]);
21
21
  var Registry = class {
22
22
  projects = /* @__PURE__ */ new Map();
23
23
  /** 注册(或复用)项目目录;同名不同目录视为冲突。depth 默认 3、上限 6 */
24
- watch(projectNameRaw, dir, depth) {
24
+ watch(projectNameRaw, dir, depth, opts = {}) {
25
25
  const name = sanitizeProjectName(projectNameRaw);
26
26
  const { root, rootReal } = resolveProjectDir(dir);
27
27
  const wanted = clampDepth(depth);
@@ -30,6 +30,7 @@ var Registry = class {
30
30
  if (existing.rootReal !== rootReal) throw new Error(`项目名 "${name}" 已被 ${existing.root} 占用,请换一个名字(--name)`);
31
31
  if (depth !== void 0) existing.depth = wanted;
32
32
  existing.lastAccess = Date.now();
33
+ if (opts.owner === "manual") existing.owner = "manual";
33
34
  return existing;
34
35
  }
35
36
  const entry = {
@@ -39,6 +40,7 @@ var Registry = class {
39
40
  depth: wanted,
40
41
  agents: /* @__PURE__ */ new Set(),
41
42
  browsers: /* @__PURE__ */ new Set(),
43
+ owner: opts.owner ?? "manual",
42
44
  lastAccess: Date.now(),
43
45
  createdAt: Date.now()
44
46
  };
@@ -217,8 +219,37 @@ var ChangesStore = class ChangesStore {
217
219
  }
218
220
  };
219
221
  // src/web/watcher.ts
222
+ /**
223
+ * 按需文件监听:正在查看的文件有浏览器 SSE 连接时才 watch(引用计数)。
224
+ * 注册表另有 design 目录监听:chokidar 按目录排除规则与深度过滤,
225
+ * 不把 node_modules / .git 等无关目录纳入 watch,避免耗尽 inotify 配额。
226
+ */
227
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
228
+ "node_modules",
229
+ ".git",
230
+ "dist",
231
+ "build",
232
+ ".next",
233
+ "target",
234
+ ".venv",
235
+ "venv",
236
+ "__pycache__"
237
+ ]);
238
+ /** 与 Registry.listDesigns 同一套列举边界:隐藏项、构建目录、深度、扩展名 */
239
+ function designIgnored(root, depth, path, stats) {
240
+ if (path === root) return false;
241
+ const parts = relative(root, path).split(sep).filter(Boolean);
242
+ if (parts.length === 0) return false;
243
+ if (parts.some((part) => part.startsWith(".") || IGNORED_DIRS.has(part))) return true;
244
+ const maxParts = stats?.isDirectory() === true ? depth : depth + 1;
245
+ if (parts.length > maxParts) return true;
246
+ if (stats?.isDirectory() === true) return false;
247
+ if (stats === void 0 && extname(path) === "") return false;
248
+ return !DESIGN_EXT.has(extname(path).toLowerCase());
249
+ }
220
250
  var WatchManager = class {
221
251
  entries = /* @__PURE__ */ new Map();
252
+ designs = /* @__PURE__ */ new Map();
222
253
  /** 取得对某文件的监听;返回释放函数 */
223
254
  acquire(absPath, onChange) {
224
255
  let entry = this.entries.get(absPath);
@@ -265,10 +296,100 @@ var WatchManager = class {
265
296
  size() {
266
297
  return this.entries.size;
267
298
  }
299
+ /**
300
+ * 项目设计稿目录监听:仅 .ptr / .pen 的增删会触发,且遵守项目扫描深度。
301
+ * 相同 root + depth 重复调用复用同一 watcher;depth 变化时重建。
302
+ */
303
+ watchDesigns(root, depth, onChange) {
304
+ const existing = this.designs.get(root);
305
+ if (existing?.depth === depth) return existing;
306
+ if (existing) existing.close();
307
+ const watcher = chokidar.watch(root, {
308
+ ignoreInitial: true,
309
+ ignored: (path, stats) => designIgnored(root, depth, path, stats),
310
+ awaitWriteFinish: {
311
+ stabilityThreshold: 80,
312
+ pollInterval: 20
313
+ },
314
+ persistent: true
315
+ });
316
+ const handle = {
317
+ depth,
318
+ ready: new Promise((resolve) => watcher.once("ready", () => resolve())),
319
+ close: async () => {
320
+ if (this.designs.get(root) === handle) this.designs.delete(root);
321
+ await watcher.close();
322
+ }
323
+ };
324
+ const onDesignChange = (file) => {
325
+ if (DESIGN_EXT.has(extname(file).toLowerCase())) onChange(file);
326
+ };
327
+ this.designs.set(root, handle);
328
+ watcher.on("add", onDesignChange);
329
+ watcher.on("unlink", onDesignChange);
330
+ watcher.on("unlinkDir", onChange);
331
+ watcher.on("error", () => void handle.close());
332
+ return handle;
333
+ }
334
+ unwatchDesigns(root) {
335
+ this.designs.get(root)?.close();
336
+ }
268
337
  async closeAll() {
269
338
  const all = [...this.entries.values()];
270
339
  this.entries.clear();
271
340
  await Promise.all(all.map((e) => e.watcher.close()));
341
+ await Promise.all([...this.designs.values()].map((d) => d.close()));
342
+ }
343
+ };
344
+ // src/web/update-check.ts
345
+ const OK_TTL_MS = 216e5;
346
+ const FAIL_TTL_MS = 6e5;
347
+ const TIMEOUT_MS = 3e3;
348
+ const REGISTRY_URL = "https://registry.npmjs.org/paintress/latest";
349
+ /** 当前缓存是否已经过期(没有缓存也算过期) */
350
+ function shouldRefresh(state, now = Date.now()) {
351
+ if (state === null) return true;
352
+ const ttl = state.version === null ? FAIL_TTL_MS : OK_TTL_MS;
353
+ return now - state.checkedAt >= ttl;
354
+ }
355
+ /** 查一次 npm;任何失败(离线 / 超时 / 404 / 坏 JSON)都返回 null,不抛 */
356
+ async function fetchLatestVersion(fetchImpl = fetch, timeoutMs = TIMEOUT_MS) {
357
+ try {
358
+ const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(timeoutMs) });
359
+ if (!res.ok) return null;
360
+ const body = await res.json();
361
+ return typeof body.version === "string" && body.version !== "" ? body.version : null;
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+ var VersionChecker = class {
367
+ fetcher;
368
+ state = null;
369
+ inFlight = null;
370
+ constructor(fetcher = () => fetchLatestVersion()) {
371
+ this.fetcher = fetcher;
372
+ }
373
+ /** 已完成探测的最新版号(没查到为 null) */
374
+ get latest() {
375
+ return this.state?.version ?? null;
376
+ }
377
+ /** 读当前值并在后台刷新(不阻塞调用方) */
378
+ peek() {
379
+ if (shouldRefresh(this.state) && this.inFlight === null) {
380
+ const task = this.refresh().finally(() => {
381
+ this.inFlight = null;
382
+ });
383
+ this.inFlight = task;
384
+ }
385
+ return this.latest;
386
+ }
387
+ async refresh() {
388
+ const version = await this.fetcher();
389
+ this.state = {
390
+ version,
391
+ checkedAt: Date.now()
392
+ };
272
393
  }
273
394
  };
274
395
  // src/web/daemon.ts
@@ -374,26 +495,113 @@ function layout(title, body) {
374
495
  function esc(s) {
375
496
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
376
497
  }
498
+ /**
499
+ * 破坏性写接口的 CSRF 门槛:要求自定义头 `x-paintress: 1`。
500
+ * 跨站 fetch 带自定义头会触发 CORS 预检,而 daemon 从不应答预检 —— 跳站页面发不出该请求。
501
+ * 不做 Origin 白名单:手机经隧道/反代访问时 Origin 是本机看不到的域名,白名单会误伤正常使用。
502
+ */
503
+ function isTrustedWrite(req) {
504
+ return req.headers["x-paintress"] === "1";
505
+ }
377
506
  async function startDaemon(opts = {}) {
378
507
  const releaseLock = acquireLock();
379
508
  const registry = new Registry();
380
509
  const marks = new MarksStore();
381
510
  const changes = new ChangesStore();
382
511
  const watchers = new WatchManager();
512
+ const versions = new VersionChecker();
383
513
  const startedAt = Date.now();
384
514
  const idleTimeoutMs = opts.idleTimeoutMs ?? 6e4;
385
515
  const idleCheckMs = opts.idleCheckMs ?? 15e3;
386
516
  let idleSince = null;
387
517
  let closing = false;
518
+ /**
519
+ * 注册表订阅者(浏览器开着的查看器窗口)。
520
+ * 项目列表变化时推 `projects` 事件,避免用户按 F5 才看到新项目。
521
+ */
522
+ const registryClients = /* @__PURE__ */ new Set();
523
+ /** 控制面长连接:shutdown 时必须主动结束,否则 server.close() 会一直等 agent attach */
524
+ const controlClients = /* @__PURE__ */ new Set();
525
+ /** 将被关闭的长连接数:agent attach + 设计稿 SSE + 项目列表 SSE */
526
+ const connectionCount = () => registryClients.size + registry.all().reduce((n, p) => n + p.agents.size + p.browsers.size, 0);
527
+ const notifyRegistry = () => {
528
+ const payload = JSON.stringify({
529
+ projects: registry.all().length,
530
+ at: Date.now()
531
+ });
532
+ for (const res of registryClients) try {
533
+ res.write(`event: projects\ndata: ${payload}\n\n`);
534
+ } catch {
535
+ registryClients.delete(res);
536
+ }
537
+ };
538
+ /**
539
+ * 目录监听:项目目录里新增/删除设计稿时也推 projects 事件,
540
+ * 让「agent 刚创建的文件」在已开着的窗口里自动出现(不靠 F5)。
541
+ * 合并 150ms:一次批量建多个文件只推一次。
542
+ */
543
+ let registryTimer = null;
544
+ const scheduleRegistryNotify = () => {
545
+ if (registryTimer !== null) return;
546
+ registryTimer = setTimeout(() => {
547
+ registryTimer = null;
548
+ notifyRegistry();
549
+ }, 150);
550
+ registryTimer.unref?.();
551
+ };
552
+ /**
553
+ * agent 自动注册的项目退场:连接归零且无浏览器在看的宽限期后 unwatch。
554
+ * - 宽限期跨过 agent 崩溃重启的窗口(新进程会重新 watch/attach,定时器随即撤销)
555
+ * - 浏览器 SSE 开着时永不退场
556
+ * - 人手动 watch 的项目(owner=manual)不参与自动退场
557
+ */
558
+ const autoDetachMs = opts.autoDetachMs ?? 45e3;
559
+ const autoDetachTimers = /* @__PURE__ */ new Map();
560
+ function cancelAutoDetach(name) {
561
+ const timer = autoDetachTimers.get(name);
562
+ if (timer === void 0) return;
563
+ clearTimeout(timer);
564
+ autoDetachTimers.delete(name);
565
+ }
566
+ /** 移除项目并释放目录监听(与 unwatch 同一套收尾),返回是否真的移除了 */
567
+ function detachProject(entry, reason) {
568
+ cancelAutoDetach(entry.name);
569
+ if (!registry.remove(entry.name)) return false;
570
+ if (!registry.all().some((p) => p.rootReal === entry.rootReal)) watchers.unwatchDesigns(entry.rootReal);
571
+ if (!opts.quiet) process.stderr.write(`paintress web: 解绑项目 ${entry.name}(${reason})\n`);
572
+ notifyRegistry();
573
+ return true;
574
+ }
575
+ function maybeScheduleAutoDetach(name) {
576
+ if (closing) return;
577
+ const entry = registry.get(name);
578
+ if (entry === void 0 || entry.owner !== "agent" || !registry.isIdle(entry)) {
579
+ cancelAutoDetach(name);
580
+ return;
581
+ }
582
+ if (autoDetachTimers.has(name)) return;
583
+ const timer = setTimeout(() => {
584
+ autoDetachTimers.delete(name);
585
+ const current = registry.get(name);
586
+ if (current === void 0 || current.owner !== "agent" || !registry.isIdle(current)) return;
587
+ detachProject(current, "agent 已退出");
588
+ }, autoDetachMs);
589
+ timer.unref?.();
590
+ autoDetachTimers.set(name, timer);
591
+ }
388
592
  /** 控制面:一个连接可同时作为 agent 存活信号 */
389
593
  const handleClient = (socket) => {
594
+ controlClients.add(socket);
595
+ socket.on("close", () => controlClients.delete(socket));
390
596
  const decoder = new LineDecoder();
391
597
  let attachToken = null;
392
598
  const cleanup = () => {
393
599
  if (!attachToken) return;
394
- const entry = registry.get(attachToken.project);
600
+ const gone = attachToken.project;
601
+ const entry = registry.get(gone);
395
602
  if (entry) entry.agents.delete(attachToken.token);
396
603
  attachToken = null;
604
+ maybeScheduleAutoDetach(gone);
397
605
  };
398
606
  socket.on("close", cleanup);
399
607
  socket.on("error", cleanup);
@@ -432,22 +640,31 @@ async function startDaemon(opts = {}) {
432
640
  version: VERSION
433
641
  });
434
642
  break;
435
- case "watch":
643
+ case "watch": {
644
+ const entry = registry.watch(req.project, req.dir, req.depth, req.owner === "agent" ? { owner: "agent" } : {});
645
+ cancelAutoDetach(entry.name);
646
+ watchers.watchDesigns(entry.rootReal, entry.depth, scheduleRegistryNotify);
436
647
  reply({
437
648
  ok: true,
438
649
  watched: true,
439
- project: registry.info(registry.watch(req.project, req.dir, req.depth))
650
+ project: registry.info(entry)
440
651
  });
652
+ notifyRegistry();
441
653
  break;
442
- case "unwatch":
654
+ }
655
+ case "unwatch": {
656
+ const before = registry.get(req.project);
657
+ const removed = before === void 0 ? false : detachProject(before, "unwatch");
443
658
  reply({
444
659
  ok: true,
445
- unwatched: registry.remove(req.project)
660
+ unwatched: removed
446
661
  });
447
662
  break;
663
+ }
448
664
  case "attach": {
449
665
  const entry = registry.get(req.project);
450
666
  if (!entry) throw new Error(`项目未注册:${req.project}`);
667
+ cancelAutoDetach(req.project);
451
668
  const token = Symbol(`agent:${req.project}`);
452
669
  entry.agents.add(token);
453
670
  attachToken = {
@@ -560,10 +777,38 @@ async function startDaemon(opts = {}) {
560
777
  pid: process.pid,
561
778
  uptimeMs: Date.now() - startedAt,
562
779
  version: VERSION,
780
+ latest: opts.updateCheck === true ? versions.peek() : null,
781
+ connections: connectionCount(),
563
782
  projects: registry.all().map((p) => registry.info(p))
564
783
  });
565
784
  return;
566
785
  }
786
+ if (segments[1] === "version" && segments.length === 2) {
787
+ sendJson(200, {
788
+ ok: true,
789
+ pid: process.pid,
790
+ version: VERSION,
791
+ latest: opts.updateCheck === true ? versions.peek() : null,
792
+ connections: connectionCount()
793
+ });
794
+ return;
795
+ }
796
+ if (segments[1] === "projects" && segments.length === 3 && req.method === "DELETE") {
797
+ if (!isTrustedWrite(req)) {
798
+ sendJson(403, {
799
+ ok: false,
800
+ error: "缺少 x-paintress 头"
801
+ });
802
+ return;
803
+ }
804
+ const name = decodeURIComponent(segments[2] ?? "");
805
+ const entry = registry.get(name);
806
+ sendJson(200, {
807
+ ok: true,
808
+ unwatched: entry === void 0 ? false : detachProject(entry, "web 界面解绑")
809
+ });
810
+ return;
811
+ }
567
812
  if (segments[1] === "projects" && segments[3] === "files") {
568
813
  const name = decodeURIComponent(segments[2] ?? "");
569
814
  const entry = registry.get(name);
@@ -583,6 +828,24 @@ async function startDaemon(opts = {}) {
583
828
  });
584
829
  return;
585
830
  }
831
+ if (segments[1] === "shutdown" && req.method === "POST") {
832
+ if (!isTrustedWrite(req)) {
833
+ sendJson(403, {
834
+ ok: false,
835
+ error: "缺少 x-paintress 头"
836
+ });
837
+ return;
838
+ }
839
+ res.once("finish", () => {
840
+ setTimeout(() => void shutdown("web 界面关闭"), 0).unref?.();
841
+ });
842
+ sendJson(200, {
843
+ ok: true,
844
+ shutdown: true,
845
+ connections: connectionCount()
846
+ });
847
+ return;
848
+ }
586
849
  if (segments[1] === "changes") {
587
850
  const qProject = url.searchParams.get("project") ?? "";
588
851
  const qFile = url.searchParams.get("path") ?? "";
@@ -666,6 +929,21 @@ async function startDaemon(opts = {}) {
666
929
  if (segments[0] === "events") {
667
930
  const project = url.searchParams.get("project") ?? "";
668
931
  const relPath = url.searchParams.get("path") ?? "";
932
+ if (project === "") {
933
+ res.writeHead(200, {
934
+ "content-type": "text/event-stream",
935
+ "cache-control": "no-cache",
936
+ connection: "keep-alive"
937
+ });
938
+ res.write(": connected\n\n");
939
+ registryClients.add(res);
940
+ const registryKeepAlive = setInterval(() => res.write(": ping\n\n"), 2e4);
941
+ req.on("close", () => {
942
+ clearInterval(registryKeepAlive);
943
+ registryClients.delete(res);
944
+ });
945
+ return;
946
+ }
669
947
  const entry = registry.get(project);
670
948
  if (!entry) {
671
949
  send(404, "project not found", "text/plain; charset=utf-8");
@@ -684,6 +962,7 @@ async function startDaemon(opts = {}) {
684
962
  res.write(": connected\n\n");
685
963
  const token = Symbol("browser");
686
964
  entry.browsers.add(token);
965
+ cancelAutoDetach(entry.name);
687
966
  const release = watchers.acquire(abs, () => {
688
967
  res.write(`event: change\ndata: ${JSON.stringify({
689
968
  path: relPath,
@@ -695,6 +974,7 @@ async function startDaemon(opts = {}) {
695
974
  clearInterval(keepAlive);
696
975
  release();
697
976
  entry.browsers.delete(token);
977
+ maybeScheduleAutoDetach(entry.name);
698
978
  };
699
979
  req.on("close", done);
700
980
  return;
@@ -832,9 +1112,20 @@ async function startDaemon(opts = {}) {
832
1112
  closing = true;
833
1113
  if (!opts.quiet) process.stderr.write(`paintress web: 退出(${reason})\n`);
834
1114
  clearInterval(idleTimer);
1115
+ if (registryTimer !== null) clearTimeout(registryTimer);
1116
+ for (const timer of autoDetachTimers.values()) clearTimeout(timer);
1117
+ autoDetachTimers.clear();
1118
+ for (const client of registryClients) try {
1119
+ client.end();
1120
+ } catch {}
1121
+ registryClients.clear();
1122
+ for (const client of controlClients) client.end();
1123
+ controlClients.clear();
835
1124
  await watchers.closeAll();
836
1125
  await new Promise((resolve) => sockServer.close(() => resolve()));
837
- await new Promise((resolve) => httpServer.close(() => resolve()));
1126
+ const httpClosed = new Promise((resolve) => httpServer.close(() => resolve()));
1127
+ httpServer.closeAllConnections();
1128
+ await httpClosed;
838
1129
  releaseLock();
839
1130
  }
840
1131
  return {
@@ -1145,13 +1436,22 @@ const FLAG_KEYS = /* @__PURE__ */ new Set([
1145
1436
  "idle",
1146
1437
  "daemon",
1147
1438
  "dry-run",
1148
- "opencode-layout"
1439
+ "opencode-layout",
1440
+ "width",
1441
+ "height",
1442
+ "fill",
1443
+ "layout",
1444
+ "padding",
1445
+ "gap",
1446
+ "force",
1447
+ "board"
1149
1448
  ]);
1150
1449
  const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
1151
1450
  "regex",
1152
1451
  "reusable",
1153
1452
  "daemon",
1154
- "dry-run"
1453
+ "dry-run",
1454
+ "force"
1155
1455
  ]);
1156
1456
  /** 纯函数:argv → 命令 + 位置参数 + 标志(可单测) */
1157
1457
  function parseArgv(argv) {
@@ -1219,7 +1519,7 @@ async function main(argv) {
1219
1519
  }
1220
1520
  const parsed = parseArgv(argv);
1221
1521
  if (!parsed) {
1222
- console.error("用法:paintress <mcp|web|ping|status|watch|unwatch|attach|stop|info|query|get|update|insert|delete|move|batch|render|shot|validate|install|doctor|mcp-config> ...(版本:paintress --version)");
1522
+ 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)");
1223
1523
  return 2;
1224
1524
  }
1225
1525
  const { cmd, positionals, flags } = parsed;
@@ -1401,7 +1701,8 @@ async function main(argv) {
1401
1701
  }
1402
1702
  const handle = await startDaemon({
1403
1703
  ...port !== void 0 ? { port } : {},
1404
- ...idle !== void 0 ? { idleTimeoutMs: idle } : {}
1704
+ ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
1705
+ updateCheck: true
1405
1706
  });
1406
1707
  console.log(JSON.stringify({
1407
1708
  ok: true,
@@ -1418,6 +1719,27 @@ async function main(argv) {
1418
1719
  if (!file) throw new Error("缺少 <file> 参数");
1419
1720
  let out;
1420
1721
  switch (cmd) {
1722
+ case "new": {
1723
+ const name = flagStr(flags, "name");
1724
+ const width = flagNum(flags, "width");
1725
+ const height = flagNum(flags, "height");
1726
+ const fill = flagStr(flags, "fill");
1727
+ const layout = flagStr(flags, "layout");
1728
+ const padding = flagNum(flags, "padding");
1729
+ const gap = flagNum(flags, "gap");
1730
+ out = await call("paintress_new", {
1731
+ path: file,
1732
+ ...name !== void 0 ? { name } : {},
1733
+ ...width !== void 0 ? { width } : {},
1734
+ ...height !== void 0 ? { height } : {},
1735
+ ...fill !== void 0 ? { fill } : {},
1736
+ ...layout !== void 0 ? { layout } : {},
1737
+ ...padding !== void 0 ? { padding } : {},
1738
+ ...gap !== void 0 ? { gap } : {},
1739
+ ...flags["force"] === true ? { overwrite: true } : {}
1740
+ });
1741
+ break;
1742
+ }
1421
1743
  case "info":
1422
1744
  out = await call("paintress_doc_info", { path: file });
1423
1745
  break;
@@ -1488,14 +1810,18 @@ async function main(argv) {
1488
1810
  out: a1
1489
1811
  });
1490
1812
  break;
1491
- case "shot":
1813
+ case "shot": {
1492
1814
  if (!a1) throw new Error("缺少 <out.png> 参数");
1815
+ if (flags["board"] === true) throw new Error("--board 需要画板 id 或 name");
1816
+ const board = flagStr(flags, "board");
1493
1817
  out = await call("paintress_screenshot", {
1494
1818
  path: file,
1495
1819
  out: a1,
1496
- ...flagNum(flags, "scale") !== void 0 ? { scale: flagNum(flags, "scale") } : {}
1820
+ ...flagNum(flags, "scale") !== void 0 ? { scale: flagNum(flags, "scale") } : {},
1821
+ ...board !== void 0 ? { board } : {}
1497
1822
  });
1498
1823
  break;
1824
+ }
1499
1825
  default:
1500
1826
  console.error(`未知子命令:${cmd}`);
1501
1827
  return 2;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { Nt as Affine, tt as BackgroundBlurEffect, _t as BlendMode, nt as BlurEffect, Pt as BoolOrVar, L as BrowserSchema, Ft as Color, It as ColorOrVar, Lt as CornerRadius, vt as CrossAxis, A as DocumentObjectSchema, j as DocumentSchema, rt as EffectEntry, it as Effects, R as EllipseSchema, C as FileChangedError, at as FillEntry, yt as FillRule, ot as Fills, z as FrameSchema, st as GradientFill, bt as GradientType, ct as GraphicsBlock, B as GroupSchema, xt as IconLibrary, V as IconSchema, lt as ImageFill, St as ImageFit, H as KNOWN_NODE_TYPES, U as LayoutBlock, Ct as LayoutDir, wt as MainAxis, ut as MeshGradientFill, W as NodeBase, G as NodeSchema, K as NoteSchema, Rt as NumOrVar, M as PTR_SPEC_VERSION, zt as Padding, q as PathSchema, J as PolygonSchema, Tt as PositionMode, N as PtrDocumentSchema, Y as RectangleSchema, X as RefSchema, Z as ScriptSchema, dt as ShaderEffect, ft as ShaderFill, pt as ShadowEffect, Et as ShadowKind, Bt as Sizing, mt as SolidFill, Vt as StringOrVar, Dt as StrokeAlign, ht as StrokeBlock, Ot as StrokeCap, kt as StrokeJoin, gt as StrokeWidth, At as TextAlign, jt as TextAlignVertical, Mt as TextGrowth, Q as TextSchema, $ as UnknownNodeSchema, Ht as VARIABLE_NAME, P as VariableDef, Ut as VariableRef, F as XPaintress, I as X_PAINTRESS_KEY, h as batch, g as buildIndex, O as collectIds, s as commonToCss, n as computeCanvas, c as cornerRadiusToCss, l as effectsToCss, r as escapeAttr, i as escapeHtml, p as expandRefs, u as fillsToCss, k as genId, _ as get, o as getLucideIcon, v as insert, w as loadDoc, m as makeVarResolver, y as move, b as query, x as remove, t as renderDocument, a as renderTree, T as saveDoc, et as schemaForType, E as serializeDoc, d as strokeToCss, f as textToCss, S as update, D as validateDoc } from "./render-CbYe9rII.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-CzpO1cOe.js";
@@ -1 +1 @@
1
- export { t as createMcpServer, n as runStdioServer } from "../server-C4TC8Bw3.js";
1
+ export { t as createMcpServer, n as runStdioServer } from "../server-vPylVJZf.js";