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/README.md +29 -4
- package/README.zh-CN.md +24 -3
- package/dist/cli.js +342 -16
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/{render-CbYe9rII.js → render-CzpO1cOe.js} +65 -8
- package/dist/{server-C4TC8Bw3.js → server-vPylVJZf.js} +163 -18
- package/package.json +3 -2
- package/skills/paintress/SKILL.md +35 -8
- package/ui/dist/assets/index-Dyrrkh92.css +1 -0
- package/ui/dist/assets/index-kPX0SDa0.js +8 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-WQnLS4sD.js +0 -6
- package/ui/dist/assets/index-lyiZ9W-B.css +0 -1
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-
|
|
3
|
-
import {
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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(
|
|
650
|
+
project: registry.info(entry)
|
|
440
651
|
});
|
|
652
|
+
notifyRegistry();
|
|
441
653
|
break;
|
|
442
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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";
|
package/dist/mcp/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as createMcpServer, n as runStdioServer } from "../server-
|
|
1
|
+
export { t as createMcpServer, n as runStdioServer } from "../server-vPylVJZf.js";
|