paintress 0.5.0 → 0.5.2

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 CHANGED
@@ -73,16 +73,18 @@ paintress mcp-config <client> print the config snippet
73
73
  ```bash
74
74
  paintress web --daemon --idle 3600000
75
75
  paintress watch demo ./demo
76
- # open http://localhost:3316/?project=demo&file=hello.ptr
76
+ # open http://localhost:3316/demo?file=hello.ptr
77
77
  ```
78
78
 
79
79
  Canvas navigation: wheel zooms at the pointer, space / middle button / empty area drags to pan, `Ctrl/Cmd+0` returns to 100%, `Shift+1` fits the window. Files with more than one board get a board strip. Edits made through the CLI or MCP are highlighted in the viewer, and clicks assign short ids that the agent can read back with `paintress_marks`.
80
80
 
81
- Project rows have an **×** on hover that unbinds the project (design files are untouched; an agent re-registers on its next start), and **Stop server** in the sidebar footer shuts the viewer process down — both ask for confirmation first.
81
+ Projects are named after their directory path (`c-thun/raybend`), so the URL reads like the path: `http://localhost:3316/c-thun/raybend?file=design/editor.pen`. Project rows have an **×** on hover that unbinds the project (design files are untouched; an agent re-registers on its next start), and **Stop server** in the sidebar footer shuts the viewer process down — both ask for confirmation first.
82
+
83
+ The viewer follows the agent instead of asking you to manage it. When an agent starts the viewer it opens the browser on its own project; projects appear and leave with a short animation as agents come and go; when the last project is gone a 10-second countdown closes the window and the process. A browser that nobody is looking at no longer keeps the process alive.
82
84
 
83
85
  When npm has a newer release than the running one, the version badge in the header grows a second line in rust red; clicking it opens the upgrade prompt (one copyable sentence) and the GitHub link. The check is client-optional on the daemon side: if npm is unreachable, the badge simply stays as it is.
84
86
 
85
- The toolbar's right side has two viewer-only controls. **Canvas background** switches Light / Gray / Dark for the design surface without touching the rest of the UI (click a stop or drag the knob). **Language** switches the interface between 中文 and EN; on first load it follows the browser, and anything that is not Chinese gets English. Both are per-browser and remembered locally. Registering a project, or dropping a new `.ptr` / `.pen` into a watched directory, updates every open viewer without a reload.
87
+ The toolbar's right side has two viewer-only controls. **Canvas background** switches Light / Gray / Dark for the design surface without touching the rest of the UI. **Language** switches the interface between 中文 and EN; on first load it follows the browser, and anything that is not Chinese gets English. Both are per-browser and remembered locally. Registering a project, or dropping a new `.ptr` / `.pen` into a watched directory, updates every open viewer without a reload.
86
88
 
87
89
  ## MCP clients
88
90
 
@@ -116,18 +118,19 @@ paintress mcp-config cursor # print the exact snippet instead of editing files
116
118
 
117
119
  ## Release (maintainers)
118
120
 
119
- Publishing is tag-driven: a `v*` tag makes `.github/workflows/publish.yml` build, test and `npm publish --provenance` through OIDC trusted publishing. Do not publish from a laptop.
121
+ Publishing is tag-driven: a `v*` tag makes `.github/workflows/publish.yml` first check that **ci passed for the same commit**, then build, verify the package contents and run `npm publish --provenance` through OIDC trusted publishing. Tests run once, in `ci.yml`; publishing does not re-run them. Do not publish from a laptop.
120
122
 
121
123
  ```bash
122
124
  pnpm release patch # 0.3.1 → 0.3.2
123
125
  pnpm release minor # 0.3.1 → 0.4.0
124
126
  pnpm release major # 0.3.1 → 1.0.0
125
127
  pnpm release patch --dry-run # print the plan only
128
+ pnpm release --retag # after a failed release: move the unpublished tag and re-publish
126
129
  ```
127
130
 
128
131
  The script refuses to run on a dirty worktree or a non-default branch, verifies the tag is free, runs typecheck (server + ui), the test suite, the build and the package-contents check, then commits `chore(release): vX.Y.Z`, tags it, pushes branch + tag, and finally waits until the version is actually on npm (a green run that published nothing is reported as a failure, not a success).
129
132
 
130
- `--prepare-only` stops after the local commit and tag; `--no-wait` returns right after the push; `--local` publishes from this machine as an emergency path and skips the CI checks.
133
+ `--prepare-only` stops after the local commit and tag; `--no-wait` returns right after the push; `--local` publishes from this machine as an emergency path and skips the CI checks. `--retag` is the recovery path after a failed release: once it confirms the version is not on npm yet, it moves the existing tag to the current commit and re-publishes under the same version number; an already published version is refused.
131
134
 
132
135
  ## License
133
136
 
package/README.zh-CN.md CHANGED
@@ -73,7 +73,7 @@ paintress mcp-config <client> 打印单个客户端的
73
73
  ```bash
74
74
  paintress web --daemon --idle 3600000
75
75
  paintress watch demo ./demo
76
- # 打开 http://localhost:3316/?project=demo&file=hello.ptr
76
+ # 打开 http://localhost:3316/demo?file=hello.ptr
77
77
  ```
78
78
 
79
79
  画布操作:滚轮以指针为锚点缩放,空格 / 中键 / 空白处拖拽平移,`Ctrl/Cmd+0` 回到 100%,`Shift+1` 适应窗口。多画板文件会显示画板条。通过 CLI 或 MCP 做的修改会在查看器里高亮,点击会让节点获得短号,agent 用 `paintress_marks` 读回。
@@ -112,18 +112,19 @@ paintress mcp-config cursor # 只打印片段,不改文件
112
112
 
113
113
  ## 发版(维护者)
114
114
 
115
- 发布由 tag 驱动:`v*` tag 会让 `.github/workflows/publish.yml` 构建、测试并通过 OIDC trusted publishing 执行 `npm publish --provenance`。不要在笔记本上直接发布。
115
+ 发布由 tag 驱动:`v*` tag 会让 `.github/workflows/publish.yml` 先校验**同一提交的 ci 已通过**,再构建、自检包内容并通过 OIDC trusted publishing 执行 `npm publish --provenance`。测试只在 `ci.yml` 跑一次,发布不重跑。不要在笔记本上直接发布。
116
116
 
117
117
  ```bash
118
118
  pnpm release patch # 0.3.1 → 0.3.2
119
119
  pnpm release minor # 0.3.1 → 0.4.0
120
120
  pnpm release major # 0.3.1 → 1.0.0
121
121
  pnpm release patch --dry-run # 只打印计划
122
+ pnpm release --retag # 发版失败后:把未发布的 tag 移到当前提交重发
122
123
  ```
123
124
 
124
125
  脚本会拒绝在脏工作区或非默认分支上发版,确认 tag 未被占用,跑类型检查(服务端 + ui)、测试、构建与包内容自检,然后提交 `chore(release): vX.Y.Z`、打 tag、推分支与 tag,最后等到版本真的出现在 npm 上才算完(CI 绿了但什么都没发出去,会如实报成失败)。
125
126
 
126
- `--prepare-only` 停在本地提交与 tag;`--no-wait` 推完就返回;`--local` 本机直接发布,是应急路径,会跳过 CI 的检查。
127
+ `--prepare-only` 停在本地提交与 tag;`--no-wait` 推完就返回;`--local` 本机直接发布,是应急路径,会跳过 CI 的检查。`--retag` 是发版失败后的恢复路径:确认该版本还没上 npm 后,把已有 tag 移到当前提交重发(版本号不变);已发布的版本会被拒绝。
127
128
 
128
129
  ## 许可
129
130
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
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-vPylVJZf.js";
3
- import { T as loadDoc, qt as VERSION, t as renderDocument } from "./render-CzpO1cOe.js";
2
+ import { a as attachPersistent, c as lockPath, d as safeJoin, f as sanitizeProjectName, h as encodeLine, i as TOOLS, l as pidPath, m as LineDecoder, n as runStdioServer, o as isDaemonRunning, p as socketPath, s as sendRequest, u as resolveProjectDir } from "./server-ssrAxbTR.js";
3
+ import { T as loadDoc, qt as VERSION, t as renderDocument } from "./render-CCxYNL6A.js";
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";
@@ -35,6 +35,7 @@ var Registry = class {
35
35
  }
36
36
  const entry = {
37
37
  name,
38
+ label: name,
38
39
  root,
39
40
  rootReal,
40
41
  depth: wanted,
@@ -100,6 +101,7 @@ var Registry = class {
100
101
  info(entry) {
101
102
  return {
102
103
  name: entry.name,
104
+ label: entry.label,
103
105
  root: entry.root,
104
106
  depth: entry.depth,
105
107
  fileCount: this.listDesigns(entry).length,
@@ -427,6 +429,23 @@ function findUiDist() {
427
429
  for (const c of candidates) if (existsSync(join(c, "index.html"))) return c;
428
430
  return null;
429
431
  }
432
+ /**
433
+ * 从 URL 路径段里按最长前缀解析项目。
434
+ * 项目 key 现在是目录层级本身(`c-thun/raybend`),所以 `/c-thun/raybend`、
435
+ * `/render/c-thun/raybend/design/editor.pen` 都能在不需要 %2F 转义的情况下命中。
436
+ * 单段名(`paintress watch myname dir`)同样适用。
437
+ */
438
+ function resolveProjectPrefix(registry, segments, maxDepth = 3) {
439
+ for (let n = Math.min(maxDepth, segments.length); n >= 1; n--) {
440
+ const key = segments.slice(0, n).map((s) => decodeURIComponent(s)).join("/");
441
+ const entry = registry.get(key);
442
+ if (entry) return {
443
+ entry,
444
+ rest: segments.slice(n)
445
+ };
446
+ }
447
+ return null;
448
+ }
430
449
  function isAlive(pid) {
431
450
  if (!Number.isInteger(pid) || pid <= 0) return false;
432
451
  try {
@@ -512,9 +531,16 @@ async function startDaemon(opts = {}) {
512
531
  const versions = new VersionChecker();
513
532
  const startedAt = Date.now();
514
533
  const idleTimeoutMs = opts.idleTimeoutMs ?? 6e4;
515
- const idleCheckMs = opts.idleCheckMs ?? 15e3;
534
+ const emptyGraceMs = opts.emptyGraceMs ?? Math.min(idleTimeoutMs, 3e3);
535
+ const emptyBrowserMs = opts.emptyBrowserMs ?? 3e4;
536
+ const manualEmptyMs = opts.manualEmptyMs ?? 6e5;
537
+ /** 本次启动有没有过项目:决定「空仓 + 浏览器开着」是等接入还是收工 */
538
+ let everHadProjects = false;
539
+ const idleCheckMs = opts.idleCheckMs ?? 2e3;
516
540
  let idleSince = null;
517
541
  let closing = false;
542
+ /** 最近一次 HTTP 活动:用来判定「浏览器还在不在」而不单靠 SSE 关闭事件 */
543
+ let lastHttpAt = Date.now();
518
544
  /**
519
545
  * 注册表订阅者(浏览器开着的查看器窗口)。
520
546
  * 项目列表变化时推 `projects` 事件,避免用户按 F5 才看到新项目。
@@ -522,11 +548,20 @@ async function startDaemon(opts = {}) {
522
548
  const registryClients = /* @__PURE__ */ new Set();
523
549
  /** 控制面长连接:shutdown 时必须主动结束,否则 server.close() 会一直等 agent attach */
524
550
  const controlClients = /* @__PURE__ */ new Set();
551
+ /** agent attach 总数 / 浏览器连接总数(注册表 SSE + 设计稿 SSE) */
552
+ const agentCount = () => registry.all().reduce((n, p) => n + p.agents.size, 0);
553
+ const browserCount = () => registryClients.size + registry.all().reduce((n, p) => n + p.browsers.size, 0);
525
554
  /** 将被关闭的长连接数:agent attach + 设计稿 SSE + 项目列表 SSE */
526
555
  const connectionCount = () => registryClients.size + registry.all().reduce((n, p) => n + p.agents.size + p.browsers.size, 0);
556
+ /**
557
+ * 推注册表快照。事件带全量项目信息:前端不用再拉一次接口,项目增删能同一帧完成,
558
+ * 动画也因此不会晚一步。列表很小(每个项目只扫一次 shallow 目录),频繁推不心疼。
559
+ */
527
560
  const notifyRegistry = () => {
528
561
  const payload = JSON.stringify({
529
- projects: registry.all().length,
562
+ projects: registry.all().map((p) => registry.info(p)),
563
+ agents: agentCount(),
564
+ browsers: browserCount(),
530
565
  at: Date.now()
531
566
  });
532
567
  for (const res of registryClients) try {
@@ -555,7 +590,7 @@ async function startDaemon(opts = {}) {
555
590
  * - 浏览器 SSE 开着时永不退场
556
591
  * - 人手动 watch 的项目(owner=manual)不参与自动退场
557
592
  */
558
- const autoDetachMs = opts.autoDetachMs ?? 45e3;
593
+ const autoDetachMs = opts.autoDetachMs ?? 5e3;
559
594
  const autoDetachTimers = /* @__PURE__ */ new Map();
560
595
  function cancelAutoDetach(name) {
561
596
  const timer = autoDetachTimers.get(name);
@@ -641,6 +676,7 @@ async function startDaemon(opts = {}) {
641
676
  });
642
677
  break;
643
678
  case "watch": {
679
+ everHadProjects = true;
644
680
  const entry = registry.watch(req.project, req.dir, req.depth, req.owner === "agent" ? { owner: "agent" } : {});
645
681
  cancelAutoDetach(entry.name);
646
682
  watchers.watchDesigns(entry.rootReal, entry.depth, scheduleRegistryNotify);
@@ -676,6 +712,7 @@ async function startDaemon(opts = {}) {
676
712
  attached: true,
677
713
  project: req.project
678
714
  });
715
+ notifyRegistry();
679
716
  break;
680
717
  }
681
718
  case "detach":
@@ -747,6 +784,7 @@ async function startDaemon(opts = {}) {
747
784
  const sockServer = createServer(handleClient);
748
785
  const uiDist = findUiDist();
749
786
  const httpServer = createServer$1(async (req, res) => {
787
+ lastHttpAt = Date.now();
750
788
  const url = new URL(req.url ?? "/", "http://localhost");
751
789
  const segments = url.pathname.split("/").filter((s) => s !== "");
752
790
  const send = (code, body, type = "text/html; charset=utf-8") => {
@@ -793,7 +831,7 @@ async function startDaemon(opts = {}) {
793
831
  });
794
832
  return;
795
833
  }
796
- if (segments[1] === "projects" && segments.length === 3 && req.method === "DELETE") {
834
+ if (segments[1] === "projects" && req.method === "DELETE") {
797
835
  if (!isTrustedWrite(req)) {
798
836
  sendJson(403, {
799
837
  ok: false,
@@ -980,14 +1018,14 @@ async function startDaemon(opts = {}) {
980
1018
  return;
981
1019
  }
982
1020
  if (segments[0] === "render" && segments.length >= 3) {
983
- const name = decodeURIComponent(segments[1] ?? "");
984
- const entry = registry.get(name);
985
- if (!entry) {
1021
+ const hit = resolveProjectPrefix(registry, segments.slice(1));
1022
+ if (hit === null || hit.rest.length === 0) {
986
1023
  send(404, "project not found", "text/plain; charset=utf-8");
987
1024
  return;
988
1025
  }
1026
+ const { entry } = hit;
989
1027
  entry.lastAccess = Date.now();
990
- const relPath = segments.slice(2).map(decodeURIComponent).join("/");
1028
+ const relPath = hit.rest.map(decodeURIComponent).join("/");
991
1029
  const abs = safeJoin(entry.rootReal, relPath);
992
1030
  if (!abs) {
993
1031
  send(404, "file not found", "text/plain; charset=utf-8");
@@ -996,7 +1034,7 @@ async function startDaemon(opts = {}) {
996
1034
  const loaded = loadDoc(abs);
997
1035
  const rendered = renderDocument(loaded.doc, { title: relPath });
998
1036
  const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/") + 1) : "";
999
- const baseHref = `/files/${encodeURIComponent(entry.name)}/${dir.split("/").filter((s) => s !== "").map(encodeURIComponent).join("/")}${dir ? "/" : ""}`;
1037
+ const baseHref = `/files/${entry.name.split("/").map(encodeURIComponent).join("/")}/${dir.split("/").filter((s) => s !== "").map(encodeURIComponent).join("/")}${dir ? "/" : ""}`;
1000
1038
  let html = rendered.html.replace("<head>", `<head>\n<base href="${baseHref}">`);
1001
1039
  if (url.searchParams.get("embed") !== "1") {
1002
1040
  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>`;
@@ -1006,13 +1044,13 @@ async function startDaemon(opts = {}) {
1006
1044
  return;
1007
1045
  }
1008
1046
  if (segments[0] === "files" && segments.length >= 3) {
1009
- const name = decodeURIComponent(segments[1] ?? "");
1010
- const entry = registry.get(name);
1011
- if (!entry) {
1047
+ const hit = resolveProjectPrefix(registry, segments.slice(1));
1048
+ if (hit === null) {
1012
1049
  send(404, "project not found", "text/plain; charset=utf-8");
1013
1050
  return;
1014
1051
  }
1015
- const relPath = segments.slice(2).map(decodeURIComponent).join("/");
1052
+ const { entry } = hit;
1053
+ const relPath = hit.rest.map(decodeURIComponent).join("/");
1016
1054
  const abs = safeJoin(entry.rootReal, relPath);
1017
1055
  if (!abs) {
1018
1056
  send(404, "file not found", "text/plain; charset=utf-8");
@@ -1053,21 +1091,21 @@ async function startDaemon(opts = {}) {
1053
1091
  if (segments.length === 0) {
1054
1092
  const items = registry.all().map((p) => {
1055
1093
  const info = registry.info(p);
1056
- return `<div class="card"><h2 style="margin:0 0 4px"><a href="/${encodeURIComponent(p.name)}">${esc(p.name)}</a></h2><div class="meta">${esc(info.root)} · ${info.fileCount} 个设计稿 · agent ${info.agentConns} · 浏览器 ${info.browserConns}</div></div>`;
1094
+ return `<div class="card"><h2 style="margin:0 0 4px"><a href="${`/${p.name.split("/").map(encodeURIComponent).join("/")}`}">${esc(p.label)}</a></h2><div class="meta">${esc(info.root)} · ${info.fileCount} 个设计稿 · agent ${info.agentConns} · 浏览器 ${info.browserConns}</div></div>`;
1057
1095
  }).join("");
1058
1096
  send(200, layout("项目", `<h1>paintress</h1><div class="meta">pid ${process.pid} · 端口 ${opts.port ?? DEFAULT_PORT} · 运行 ${Math.round((Date.now() - startedAt) / 1e3)}s</div><p class="meta">前端未构建(<code>ui/dist</code> 不存在),当前为精简视图。</p>` + (items || "<p class=\"empty\">还没有注册任何项目。用 <code>paintress watch &lt;name&gt; &lt;dir&gt;</code> 注册。</p>")));
1059
1097
  return;
1060
1098
  }
1061
- const projectName = decodeURIComponent(segments[0] ?? "");
1062
- const entry = registry.get(projectName);
1063
- if (!entry) {
1064
- send(404, layout("未找到", `<h1>404</h1><p>未注册的项目:<code>${esc(projectName)}</code></p><p><a href="/">返回</a></p>`));
1099
+ const hit = resolveProjectPrefix(registry, segments);
1100
+ if (hit === null) {
1101
+ send(404, layout("未找到", `<h1>404</h1><p>未注册的项目:<code>${esc(segments.join("/"))}</code></p><p><a href="/">返回</a></p>`));
1065
1102
  return;
1066
1103
  }
1104
+ const { entry } = hit;
1067
1105
  entry.lastAccess = Date.now();
1068
- if (segments.length === 1) {
1106
+ if (hit.rest.length === 0) {
1069
1107
  const files = registry.listDesigns(entry);
1070
- const list = files.map((f) => `<li><a href="/render/${encodeURIComponent(entry.name)}/${f.split("/").map(encodeURIComponent).join("/")}">${esc(f)}</a></li>`).join("");
1108
+ const list = files.map((f) => `<li><a href="/render/${entry.name.split("/").map(encodeURIComponent).join("/")}/${f.split("/").map(encodeURIComponent).join("/")}">${esc(f)}</a></li>`).join("");
1071
1109
  send(200, layout(entry.name, `<h1>${esc(entry.name)}</h1><div class="meta">${esc(entry.root)} · depth ${entry.depth}</div><h2>设计稿(${files.length})</h2>` + (files.length ? `<ul>${list}</ul>` : "<p class=\"empty\">目录下没有 .ptr / .pen 文件</p>") + "<p><a href=\"/\">← 项目列表</a></p>"));
1072
1110
  return;
1073
1111
  }
@@ -1096,14 +1134,40 @@ async function startDaemon(opts = {}) {
1096
1134
  resolve();
1097
1135
  });
1098
1136
  });
1137
+ /**
1138
+ * 生命周期策略:控制权在 agent 侧,人不需要管进程在不在。
1139
+ *
1140
+ * - 有 agent attach → 永远活着(agent 正在干活)。
1141
+ * - 没有 agent,但浏览器开着:
1142
+ * · 还有项目 → 活着;项目归零 → 给前端 10s 倒计时自己走(服务端 emptyBrowserMs 兜底)。
1143
+ * - 没有 agent 也没有浏览器:
1144
+ * · 还有手动 watch 的项目 → idleTimeoutMs 后关;
1145
+ * · 一个项目都没有 → 立刻(emptyGraceMs)关。
1146
+ *
1147
+ * 修的是旧逻辑的真 bug:旧代码只在「项目全部 idle」时计数,自动拉起的 daemon
1148
+ * 又写死 `--idle 1h`,于是浏览器早就关了、进程还能挂一小时。
1149
+ */
1099
1150
  const idleTimer = setInterval(() => {
1100
- if (registry.all().length === 0) idleSince = idleSince ?? Date.now();
1101
- else if (registry.all().every((p) => registry.isIdle(p))) idleSince = idleSince ?? Date.now();
1102
- else {
1151
+ const now = Date.now();
1152
+ const projects = registry.all().length;
1153
+ const agents = agentCount();
1154
+ const browsing = browserCount() > 0 && now - lastHttpAt < 3e4;
1155
+ let threshold;
1156
+ if (agents > 0) {
1103
1157
  idleSince = null;
1158
+ changes.prune();
1104
1159
  return;
1105
1160
  }
1106
- if (idleSince !== null && Date.now() - idleSince >= idleTimeoutMs) shutdown("idle");
1161
+ if (browsing) {
1162
+ if (projects > 0) {
1163
+ idleSince = null;
1164
+ changes.prune();
1165
+ return;
1166
+ }
1167
+ threshold = everHadProjects ? emptyBrowserMs : manualEmptyMs;
1168
+ } else threshold = projects > 0 ? idleTimeoutMs : emptyGraceMs;
1169
+ idleSince = idleSince ?? now;
1170
+ if (now - idleSince >= threshold) shutdown(projects > 0 ? "idle" : "无项目");
1107
1171
  changes.prune();
1108
1172
  }, idleCheckMs);
1109
1173
  idleTimer.unref();
@@ -1116,6 +1180,10 @@ async function startDaemon(opts = {}) {
1116
1180
  for (const timer of autoDetachTimers.values()) clearTimeout(timer);
1117
1181
  autoDetachTimers.clear();
1118
1182
  for (const client of registryClients) try {
1183
+ client.write(`event: shutdown\ndata: ${JSON.stringify({
1184
+ reason,
1185
+ at: Date.now()
1186
+ })}\n\n`);
1119
1187
  client.end();
1120
1188
  } catch {}
1121
1189
  registryClients.clear();
@@ -1423,6 +1491,7 @@ function readConfig(path) {
1423
1491
  return existsSync(path) ? readFileSync(path, "utf8") : null;
1424
1492
  }
1425
1493
  // src/cli.ts
1494
+ const NO_OPEN_FLAG = "no-open";
1426
1495
  const FLAG_KEYS = /* @__PURE__ */ new Set([
1427
1496
  "type",
1428
1497
  "name",
@@ -1444,14 +1513,16 @@ const FLAG_KEYS = /* @__PURE__ */ new Set([
1444
1513
  "padding",
1445
1514
  "gap",
1446
1515
  "force",
1447
- "board"
1516
+ "board",
1517
+ NO_OPEN_FLAG
1448
1518
  ]);
1449
1519
  const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
1450
1520
  "regex",
1451
1521
  "reusable",
1452
1522
  "daemon",
1453
1523
  "dry-run",
1454
- "force"
1524
+ "force",
1525
+ NO_OPEN_FLAG
1455
1526
  ]);
1456
1527
  /** 纯函数:argv → 命令 + 位置参数 + 标志(可单测) */
1457
1528
  function parseArgv(argv) {
@@ -1556,10 +1627,11 @@ async function main(argv) {
1556
1627
  if (cmd === "watch") {
1557
1628
  if (!file || !a1) throw new Error("用法:paintress watch <name> <dir> [--depth 1..6]");
1558
1629
  const depth = flagNum(flags, "depth");
1630
+ const dir = resolve(a1);
1559
1631
  console.log(JSON.stringify(await sendRequest({
1560
1632
  kind: "watch",
1561
1633
  project: file,
1562
- dir: a1,
1634
+ dir,
1563
1635
  ...depth !== void 0 ? { depth } : {}
1564
1636
  }, 8e3), null, 2));
1565
1637
  return 0;
@@ -1674,11 +1746,13 @@ async function main(argv) {
1674
1746
  if (cmd === "web") {
1675
1747
  const port = flagNum(flags, "port");
1676
1748
  const idle = flagNum(flags, "idle");
1749
+ const noOpen = flags[NO_OPEN_FLAG] === true || process.env["PAINTRESS_NO_OPEN"] === "1";
1677
1750
  if (flags["daemon"] === true) {
1678
1751
  const { spawn } = await import("node:child_process");
1679
1752
  const args = [process.argv[1] ?? "", "web"];
1680
1753
  if (port !== void 0) args.push("--port", String(port));
1681
1754
  if (idle !== void 0) args.push("--idle", String(idle));
1755
+ if (noOpen) args.push("--no-open");
1682
1756
  const child = spawn(process.execPath, args, {
1683
1757
  detached: true,
1684
1758
  stdio: "ignore"
@@ -1702,14 +1776,20 @@ async function main(argv) {
1702
1776
  const handle = await startDaemon({
1703
1777
  ...port !== void 0 ? { port } : {},
1704
1778
  ...idle !== void 0 ? { idleTimeoutMs: idle } : {},
1779
+ emptyGraceMs: 6e4,
1705
1780
  updateCheck: true
1706
1781
  });
1782
+ const http = `http://localhost:${handle.port}`;
1707
1783
  console.log(JSON.stringify({
1708
1784
  ok: true,
1709
1785
  pid: handle.pid,
1710
1786
  port: handle.port,
1711
- http: `http://localhost:${handle.port}`
1787
+ http
1712
1788
  }, null, 2));
1789
+ if (!noOpen) {
1790
+ const { openBrowser } = await import("./server-ssrAxbTR.js").then((n) => n.r);
1791
+ openBrowser(`${http}/`);
1792
+ }
1713
1793
  await new Promise((resolve) => {
1714
1794
  process.once("SIGINT", () => void handle.close().then(() => resolve()));
1715
1795
  process.once("SIGTERM", () => void handle.close().then(() => resolve()));
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { It as Affine, it as BackgroundBlurEffect, bt as BlendMode, at as BlurEffect, Lt as BoolOrVar, z as BrowserSchema, Rt as Color, zt as ColorOrVar, Bt as CornerRadius, xt as CrossAxis, j as DocumentObjectSchema, M as DocumentSchema, ot as EffectEntry, st as Effects, B as EllipseSchema, w as FileChangedError, ct as FillEntry, St as FillRule, lt as Fills, V as FrameSchema, ut as GradientFill, Ct as GradientType, dt as GraphicsBlock, H as GroupSchema, U as IconFontSchema, wt as IconLibrary, W as IconSchema, ft as ImageFill, Tt as ImageFit, G as KNOWN_NODE_TYPES, K as LayoutBlock, Et as LayoutDir, Dt as MainAxis, pt as MeshGradientFill, q as NodeBase, J as NodeSchema, Y as NoteSchema, Vt as NumOrVar, N as PEN_DOC_VERSION, P as PTR_SPEC_VERSION, Ht as Padding, X as PathSchema, Z as PolygonSchema, Ot as PositionMode, F as PtrDocumentSchema, Q as RectangleSchema, $ as RefSchema, et as ScriptSchema, mt as ShaderEffect, ht as ShaderFill, gt as ShadowEffect, kt as ShadowKind, Ut as Sizing, _t as SolidFill, Wt as StringOrVar, At as StrokeAlign, vt as StrokeBlock, jt as StrokeCap, Mt as StrokeJoin, yt as StrokeWidth, Nt as TextAlign, Pt as TextAlignVertical, Ft as TextGrowth, tt as TextSchema, nt as UnknownNodeSchema, Gt as VARIABLE_NAME, I as VariableDef, Kt as VariableRef, L as XPaintress, R as X_PAINTRESS_KEY, g as batch, n as boardBoxes, _ as buildIndex, k as collectIds, c as commonToCss, r as computeCanvas, l as cornerRadiusToCss, u as effectsToCss, i as escapeAttr, a as escapeHtml, m as expandRefs, d as fillsToCss, A as genId, v as get, s as getLucideIcon, y as insert, T as loadDoc, h as makeVarResolver, b as move, x as query, S as remove, t as renderDocument, o as renderTree, E as saveDoc, rt as schemaForType, D as serializeDoc, f as strokeToCss, p as textToCss, C as update, O as validateDoc } from "./render-CzpO1cOe.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-CCxYNL6A.js";
@@ -1 +1 @@
1
- export { t as createMcpServer, n as runStdioServer } from "../server-vPylVJZf.js";
1
+ export { t as createMcpServer, n as runStdioServer } from "../server-ssrAxbTR.js";