worktree-bay 2.3.2 → 2.3.3

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/engine.js CHANGED
@@ -7,7 +7,7 @@ import { addWorktree } from './git.js';
7
7
  import { runShellLive, run, spliceArgv, isTTY } from './util/exec.js';
8
8
  import { warn, log } from './util/log.js';
9
9
  import { withProgress } from './util/progress.js';
10
- import { startDetached, recordedFor, pidAlive, stopManaged, readLogTail } from './proc.js';
10
+ import { startDetached, recordedFor, pidAlive, setPid, pidOnPort, readLogTail } from './proc.js';
11
11
  import { t } from './i18n.js';
12
12
  export function mergeEnvText(text, kv) {
13
13
  const lines = text.split('\n');
@@ -88,28 +88,28 @@ export async function ensureStarted(ctx) {
88
88
  }
89
89
  const cmd = renderTemplate(sp.start, vars);
90
90
  const r = startDetached(ws, dir, service, slug, port, cmd);
91
- // 验证没立刻挂:轮询 ~3s,端口起来=成功,pid 没了=启动失败(命令多半不对)
92
- const alive = await waitStartup(r.pid, port, 3000);
93
- if (alive) {
94
- log(t(` ▸ 已后台启动 ${service} dev server(pid ${r.pid},端口 ${port}) 日志: ${r.log}`, ` ▸ started ${service} dev server in background (pid ${r.pid}, port ${port}) log: ${r.log}`));
91
+ // 等它在【约定端口】上监听(最多 ~12s)。起来后按端口查出真实 pid 回填(shell/pnpm 会让记录 pid 漂移)。
92
+ const up = await waitForListen(port, 12000);
93
+ if (up) {
94
+ const real = pidOnPort(port);
95
+ if (real && real > 0)
96
+ setPid(ws, dir, real);
97
+ log(t(` ▸ 已后台启动 ${service} dev server(pid ${real || r.pid},端口 ${port}) 日志: ${r.log}`, ` ▸ started ${service} dev server in background (pid ${real || r.pid}, port ${port}) log: ${r.log}`));
95
98
  }
96
99
  else {
97
- stopManaged(ws, dir); // 进程已死,清掉登记,别留假状态
98
100
  const tail = readLogTail(r.log);
99
- warn(t(` ✗ ${service} dev server 启动后立即退出——start 命令多半不对。\n 命令: ${cmd}\n 请在 worktree 里手动跑一遍排查: cd ${dir} && ${cmd}\n 日志(${r.log}):\n${tail || '(空)'}`, ` ✗ ${service} dev server exited immediately the start command is likely wrong.\n command: ${cmd}\n reproduce in the worktree: cd ${dir} && ${cmd}\n log (${r.log}):\n${tail || '(empty)'}`));
101
+ warn(t(` ✗ ${service} dev server 未在约定端口 ${port} 上监听(12s 内)。多半是 start 命令不对,或端口被占后退避了——建议给 vite 加 --strictPort。\n 命令: ${cmd}\n 手动排查: cd ${dir} && ${cmd}\n 日志(${r.log}):\n${tail || '(空)'}`, ` ✗ ${service} dev server is not listening on the expected port ${port} (within 12s). The start command may be wrong, or it fell back to another port — add --strictPort for vite.\n command: ${cmd}\n reproduce: cd ${dir} && ${cmd}\n log (${r.log}):\n${tail || '(empty)'}`));
100
102
  }
101
103
  }
102
- // 端口起来→true(成功);pid 死了→false(崩了);超时仍存活→true(还在启动,如 vite 冷启动较慢)
103
- async function waitStartup(pid, port, ms) {
104
+ // 轮询直到约定端口被监听(true),或超时(false
105
+ async function waitForListen(port, ms) {
104
106
  const end = Date.now() + ms;
105
107
  for (;;) {
106
- if (!pidAlive(pid))
107
- return false;
108
108
  if (!(await isPortFree(port)))
109
109
  return true;
110
110
  if (Date.now() >= end)
111
- return pidAlive(pid);
112
- await new Promise((r) => setTimeout(r, 150));
111
+ return false;
112
+ await new Promise((r) => setTimeout(r, 200));
113
113
  }
114
114
  }
115
115
  export function execArgv(ctx, cmd) {
package/dist/git.js CHANGED
@@ -20,9 +20,10 @@ export async function removeWorktree(repo, dir, force) {
20
20
  git(repo, ...a); // 尽力移除;残留交给下面兜底(调用方已先过脏/未推保护,到这里删除是安全的)
21
21
  // git worktree remove 不会删被忽略的文件(前端 node_modules 等)→ 目录残留报 "Directory not empty";
22
22
  // 它还可能先摘了登记再删目录失败,留下「git 不认但磁盘还在」的孤儿。统一兜底:物理删目录 + prune 同步元数据。
23
- // 用异步 rm,让删 node_modules(可能数秒)时上层 spinner 能转。
23
+ // 用异步 rm,让删 node_modules(可能数秒)时上层 spinner 能转;
24
+ // maxRetries 兜底 Windows 上刚杀进程、句柄尚未释放导致的 EBUSY/ENOTEMPTY。
24
25
  if (fs.existsSync(dir))
25
- await fs.promises.rm(dir, { recursive: true, force: true });
26
+ await fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 300 });
26
27
  git(repo, 'worktree', 'prune');
27
28
  }
28
29
  export function isDirty(dir) { return ok(dir, 'status', '--porcelain').trim().length > 0; }
package/dist/proc.js CHANGED
@@ -9,7 +9,8 @@ catch {
9
9
  return [];
10
10
  } }
11
11
  function writeProcs(ws, recs) { fs.mkdirSync(path.join(ws, '.worktree-bay'), { recursive: true }); fs.writeFileSync(regPath(ws), JSON.stringify(recs, null, 2) + '\n'); }
12
- export function pidAlive(pid) { try {
12
+ export function pidAlive(pid) { if (!pid || pid < 1)
13
+ return false; try {
13
14
  process.kill(pid, 0);
14
15
  return true;
15
16
  }
@@ -17,6 +18,10 @@ catch {
17
18
  return false;
18
19
  } }
19
20
  export function recordedFor(ws, dir) { return readProcs(ws).find((r) => r.dir === dir); }
21
+ export function setPid(ws, dir, pid) { const recs = readProcs(ws); const r = recs.find((x) => x.dir === dir); if (r) {
22
+ r.pid = pid;
23
+ writeProcs(ws, recs);
24
+ } }
20
25
  export function readLogTail(file, lines = 15) {
21
26
  try {
22
27
  return fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean).slice(-lines).join('\n');
@@ -25,6 +30,31 @@ export function readLogTail(file, lines = 15) {
25
30
  return '';
26
31
  }
27
32
  }
33
+ // 找出监听某端口的进程 pid(shell/pnpm 等中间层会让记录的 pid 漂移,按端口查最可靠)。
34
+ export function pidOnPort(port) {
35
+ if (process.platform === 'win32') {
36
+ const r = spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' });
37
+ for (const line of (r.stdout || '').split(/\r?\n/)) {
38
+ const m = new RegExp(`[:.]${port}\\s+\\S+\\s+LISTENING\\s+(\\d+)`, 'i').exec(line);
39
+ if (m)
40
+ return Number(m[1]);
41
+ }
42
+ return undefined;
43
+ }
44
+ let r = spawnSync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' });
45
+ if (r.status === 0 && r.stdout.trim()) {
46
+ const pid = parseInt(r.stdout.trim().split(/\s+/)[0], 10);
47
+ if (pid)
48
+ return pid;
49
+ }
50
+ r = spawnSync('ss', ['-ltnp'], { encoding: 'utf8' });
51
+ if (r.status === 0) {
52
+ const m = new RegExp(`:${port}\\s.*pid=(\\d+)`).exec(r.stdout || '');
53
+ if (m)
54
+ return Number(m[1]);
55
+ }
56
+ return undefined;
57
+ }
28
58
  export function startDetached(ws, dir, service, slug, port, cmd) {
29
59
  const logDir = path.join(ws, '.worktree-bay', 'logs');
30
60
  fs.mkdirSync(logDir, { recursive: true });
@@ -59,13 +89,21 @@ function killTree(pid) {
59
89
  }
60
90
  }
61
91
  // 停掉某 worktree 的托管进程(含进程树),并从账本移除。返回被停的记录(无则 undefined)。
92
+ // 同时按「记录 pid」和「当前端口占用 pid」双杀——shell/pnpm 中间层会让记录 pid 漂移,按端口兜底最稳。
62
93
  export function stopManaged(ws, dir) {
63
94
  const recs = readProcs(ws);
64
95
  const rec = recs.find((r) => r.dir === dir);
65
96
  if (!rec)
66
97
  return undefined;
67
- if (pidAlive(rec.pid))
68
- killTree(rec.pid);
98
+ const targets = new Set();
99
+ if (rec.pid > 0)
100
+ targets.add(rec.pid);
101
+ const onPort = pidOnPort(rec.port);
102
+ if (onPort)
103
+ targets.add(onPort);
104
+ for (const pid of targets)
105
+ if (pidAlive(pid))
106
+ killTree(pid);
69
107
  writeProcs(ws, recs.filter((r) => r.dir !== dir));
70
108
  return rec;
71
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "Per-feature git worktree + port slots for parallel multi-service development: auto deps, env wiring, frontend-to-backend, merge-aware reclaim, plus an MCP server for AI agents.",
5
5
  "keywords": [
6
6
  "git",