worktree-bay 2.3.2 → 2.3.4
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/commands/ls.js +5 -5
- package/dist/engine.js +15 -13
- package/dist/git.js +3 -2
- package/dist/proc.js +41 -3
- package/package.json +1 -1
package/dist/commands/ls.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { scanOccupancy, readLabels } from '../slots.js';
|
|
2
2
|
import { portOf } from '../ports.js';
|
|
3
3
|
import { log } from '../util/log.js';
|
|
4
|
-
import { recordedFor,
|
|
4
|
+
import { recordedFor, pidOnPort } from '../proc.js';
|
|
5
5
|
import { t } from '../i18n.js';
|
|
6
|
-
//
|
|
7
|
-
function running(cfg, dir) {
|
|
6
|
+
// 该服务的约定端口是否有进程在监听(按端口判,不受 shell/pnpm 让记录 pid 漂移的影响)
|
|
7
|
+
function running(cfg, dir, port) { return !!recordedFor(cfg.workspaceRoot, dir) && !!pidOnPort(port); }
|
|
8
8
|
export function renderSlots(cfg) {
|
|
9
9
|
const occ = scanOccupancy(cfg);
|
|
10
10
|
const labels = readLabels(cfg);
|
|
11
11
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
12
12
|
const lines = [];
|
|
13
13
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
14
|
-
const svc = (occ.get(n) ?? []).map((o) =>
|
|
14
|
+
const svc = (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); return `${o.service}@${p}${running(cfg, o.dir, p) ? ' ▸run' : ''}`; });
|
|
15
15
|
lines.push(`slot ${n} ${labels[String(n)] ?? t('(未命名)', '(unnamed)')} [${svc.join(', ') || t('无 worktree', 'no worktree')}]`);
|
|
16
16
|
}
|
|
17
17
|
return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
|
|
@@ -22,7 +22,7 @@ export function slotsData(cfg) {
|
|
|
22
22
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
23
23
|
return [...slots].sort((a, b) => a - b).map((n) => ({
|
|
24
24
|
slot: n, feature: labels[String(n)] ?? null,
|
|
25
|
-
services: (occ.get(n) ?? []).map((o) =>
|
|
25
|
+
services: (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); return { service: o.service, port: p, dir: o.dir, running: running(cfg, o.dir, p) }; }),
|
|
26
26
|
}));
|
|
27
27
|
}
|
|
28
28
|
export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
|
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,
|
|
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,30 @@ 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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// 等它在【约定端口】上监听(最多 ~25s,给 vite 冷启动 + 偶发 restart 留足时间)。
|
|
92
|
+
// 起来后按端口查出真实 pid 回填(shell/pnpm 会让记录 pid 漂移)。
|
|
93
|
+
const up = await waitForListen(port, 25000);
|
|
94
|
+
if (up) {
|
|
95
|
+
const real = pidOnPort(port);
|
|
96
|
+
if (real && real > 0)
|
|
97
|
+
setPid(ws, dir, real);
|
|
98
|
+
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
99
|
}
|
|
96
100
|
else {
|
|
97
|
-
|
|
101
|
+
// 超时不等于失败:dev server 可能仍在启动/重启。给中性提示 + 日志末尾,让用户稍后 ls 复查或排查。
|
|
98
102
|
const tail = readLogTail(r.log);
|
|
99
|
-
warn(t(`
|
|
103
|
+
warn(t(` • ${service} dev server 已在后台启动,但 25s 内端口 ${port} 还没就绪——可能仍在启动/重启。\n 稍后用 \`worktree-bay ls\` 看是否 ▸run;若起不来,多半是 start 命令不对或端口被占退避(vite 建议加 --strictPort)。\n 命令: ${cmd} 日志: ${r.log}\n ${tail ? '日志末尾:\n' + tail : ''}`, ` • ${service} dev server launched, but port ${port} isn't ready within 25s — it may still be starting/restarting.\n Check \`worktree-bay ls\` shortly for ▸run; if it never comes up, the start command is likely wrong or it fell back to another port (add --strictPort for vite).\n command: ${cmd} log: ${r.log}\n ${tail ? 'log tail:\n' + tail : ''}`));
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
|
-
//
|
|
103
|
-
async function
|
|
106
|
+
// 轮询直到约定端口被监听(true),或超时(false)
|
|
107
|
+
async function waitForListen(port, ms) {
|
|
104
108
|
const end = Date.now() + ms;
|
|
105
109
|
for (;;) {
|
|
106
|
-
if (!pidAlive(pid))
|
|
107
|
-
return false;
|
|
108
110
|
if (!(await isPortFree(port)))
|
|
109
111
|
return true;
|
|
110
112
|
if (Date.now() >= end)
|
|
111
|
-
return
|
|
112
|
-
await new Promise((r) => setTimeout(r,
|
|
113
|
+
return false;
|
|
114
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
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) {
|
|
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
|
-
|
|
68
|
-
|
|
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.
|
|
3
|
+
"version": "2.3.4",
|
|
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",
|