worktree-bay 2.3.8 → 2.3.9

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/lock.js CHANGED
@@ -1,25 +1,85 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { t } from './i18n.js';
4
+ import { warn } from './util/log.js';
5
+ // 工作区原子锁(mkdir)。锁目录内写 owner=pid:
6
+ // - 锁主进程已死 / 无主(陈旧锁,多为上次进程被杀残留)→ 秒级清除重试,不再空等 30s;
7
+ // - 锁被另一个在跑的 worktree-bay 持有 → 立刻提示在等谁,再按需等待,超时给明确指引。
8
+ function pidAlive(pid) { if (!pid)
9
+ return false; try {
10
+ process.kill(pid, 0);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ } }
16
+ function ownerPid(lockDir) {
17
+ try {
18
+ const n = Number(fs.readFileSync(path.join(lockDir, 'owner'), 'utf8').trim());
19
+ return n || null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
4
25
  export async function withLock(ws, fn) {
5
- const lockDir = path.join(ws, '.worktree-bay', 'lock');
6
- fs.mkdirSync(path.join(ws, '.worktree-bay'), { recursive: true });
26
+ const baseDir = path.join(ws, '.worktree-bay');
27
+ const lockDir = path.join(baseDir, 'lock');
28
+ fs.mkdirSync(baseDir, { recursive: true });
7
29
  const start = Date.now();
30
+ let notified = false;
31
+ let ownerMissingSince = 0;
8
32
  for (;;) {
9
33
  try {
10
34
  fs.mkdirSync(lockDir);
11
35
  break;
12
- }
36
+ } // 抢到锁
13
37
  catch {
14
- if (Date.now() - start > 30000)
15
- throw new Error(t('获取工作区锁超时(是否有另一个 worktree-bay 在运行?)。若确认没有,删掉 .worktree-bay/lock 目录再试。', 'timed out acquiring the workspace lock (is another worktree-bay running?). If not, delete the .worktree-bay/lock directory and retry.'));
38
+ const pid = ownerPid(lockDir);
39
+ if (pid === null) {
40
+ // 无 owner 文件:可能是陈旧锁(旧版/被杀残留),也可能是别人刚抢到还没写 owner(竞态)。
41
+ // 给 ~600ms 宽限,仍无主就当陈旧锁清掉。
42
+ if (ownerMissingSince === 0)
43
+ ownerMissingSince = Date.now();
44
+ if (Date.now() - ownerMissingSince > 600) {
45
+ try {
46
+ fs.rmSync(lockDir, { recursive: true, force: true });
47
+ }
48
+ catch { /* 让下轮重试 */ }
49
+ ownerMissingSince = 0;
50
+ continue;
51
+ }
52
+ }
53
+ else {
54
+ ownerMissingSince = 0;
55
+ if (!pidAlive(pid)) {
56
+ try {
57
+ fs.rmSync(lockDir, { recursive: true, force: true });
58
+ }
59
+ catch { /* 重试 */ }
60
+ continue;
61
+ } // 锁主已死 → 陈旧锁,清掉
62
+ if (!notified) {
63
+ warn(t(`正在等待另一个 worktree-bay(pid ${pid})释放工作区锁…`, `waiting for another worktree-bay (pid ${pid}) to release the workspace lock…`));
64
+ notified = true;
65
+ }
66
+ if (Date.now() - start > 30000)
67
+ throw new Error(t(`等待工作区锁超时:另一个 worktree-bay(pid ${pid})仍持有。若它已卡死,结束该进程或删掉 ${lockDir} 再试。`, `timed out waiting for the workspace lock: another worktree-bay (pid ${pid}) still holds it. If stuck, kill it or delete ${lockDir}, then retry.`));
68
+ }
16
69
  await new Promise((r) => setTimeout(r, 50));
17
70
  }
18
71
  }
72
+ try {
73
+ fs.writeFileSync(path.join(lockDir, 'owner'), String(process.pid));
74
+ }
75
+ catch { /* 忽略:owner 仅用于陈旧检测 */ }
19
76
  try {
20
77
  return await fn();
21
78
  }
22
79
  finally {
23
- fs.rmdirSync(lockDir);
80
+ try {
81
+ fs.rmSync(lockDir, { recursive: true, force: true });
82
+ }
83
+ catch { /* 已被清理 */ }
24
84
  }
25
85
  }
package/dist/mcp.js CHANGED
@@ -58,7 +58,8 @@ export const TOOLS = [
58
58
  inputSchema: { type: 'object', properties: {} }, toArgs: () => ['skill'] },
59
59
  ];
60
60
  function runCli(args) {
61
- const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
61
+ // 强制非交互:给 AI 的输出不带颜色/spinner 控制符,保留完整日志
62
+ const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', env: { ...process.env, WORKTREE_BAY_NONINTERACTIVE: '1' } });
62
63
  return [r.stdout, r.stderr].filter(Boolean).join('\n').trim() || '(无输出)';
63
64
  }
64
65
  export function handle(msg) {
@@ -1,5 +1,14 @@
1
- // 轻量 ANSI 着色:仅在 TTY 且未设 NO_COLOR 时上色,否则原样返回(管道/CI/重定向不带控制符)。
2
- const enabled = (!!process.stdout.isTTY || !!process.stderr.isTTY) && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
1
+ // 终端是否「类 TTY」(可上色/动画)。Git Bash/mintty Node isTTY 会误报 false,
2
+ // 但其实是交互终端、支持 ANSI(MSYSTEM/TERM_PROGRAM 可识别)。MCP/脚本可设 WORKTREE_BAY_NONINTERACTIVE 强制纯文本。
3
+ export function ttyLike(stream) {
4
+ if (process.env.WORKTREE_BAY_NONINTERACTIVE)
5
+ return false;
6
+ if (stream?.isTTY)
7
+ return true;
8
+ return !!(process.env.MSYSTEM || process.env.TERM_PROGRAM === 'mintty');
9
+ }
10
+ // 轻量 ANSI 着色:仅在类 TTY 且未设 NO_COLOR 时上色,否则原样返回(管道/CI/重定向/MCP 不带控制符)。
11
+ const enabled = (ttyLike(process.stdout) || ttyLike(process.stderr)) && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
3
12
  const paint = (code) => (s) => (enabled ? `\x1b[${code}m${s}\x1b[0m` : s);
4
13
  export const color = {
5
14
  enabled,
package/dist/util/exec.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawnSync, spawn } from 'node:child_process';
2
2
  import { t } from '../i18n.js';
3
- import { color as c } from './color.js';
3
+ import { color as c, ttyLike } from './color.js';
4
4
  export function run(cmd, args, opts = {}) { const r = spawnSync(cmd, args, { cwd: opts.cwd, stdio: 'inherit', shell: false }); return { code: r.status ?? 1 }; }
5
5
  export function runShell(line, opts = {}) { const r = spawnSync(line, { cwd: opts.cwd, stdio: 'inherit', shell: true }); return { code: r.status ?? 1 }; }
6
6
  export function spliceArgv(template, cmd) { const out = []; for (const el of template) {
@@ -14,7 +14,7 @@ const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
14
14
  // 折叠式执行 setup/teardown 这类外部命令:TTY 下把输出收进单行临时进度(spinner + 秒数 + 最后一行),
15
15
  // 成功收成「✓ label(Ns)」,失败才吐完整日志便于排查;非 TTY(CI/管道/MCP)原样透传保留完整日志。
16
16
  export function runShellLive(line, opts, label) {
17
- if (!process.stderr.isTTY) {
17
+ if (!ttyLike(process.stderr)) {
18
18
  const r = spawnSync(line, { cwd: opts.cwd, stdio: 'inherit', shell: true });
19
19
  return Promise.resolve({ code: r.status ?? 1 });
20
20
  }
@@ -1,12 +1,12 @@
1
1
  import { log } from './log.js';
2
- import { color as c } from './color.js';
2
+ import { color as c, ttyLike } from './color.js';
3
3
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
4
  // 给长耗时异步步骤一个「在干活」的反馈:TTY 下转圈 + 秒数(写 stderr,用 \r 原地刷新),
5
5
  // 非 TTY(管道 / CI / MCP)下退化为「开始…」「✓ 完成(Ns)」两行,避免刷屏。
6
6
  export async function withProgress(label, fn) {
7
7
  const t0 = Date.now();
8
8
  const secs = () => ((Date.now() - t0) / 1000).toFixed(1);
9
- if (!process.stderr.isTTY) {
9
+ if (!ttyLike(process.stderr)) {
10
10
  log(` → ${label} …`);
11
11
  const r = await fn();
12
12
  log(` ${c.green('✓')} ${label}${c.dim(`(${secs()}s)`)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "2.3.8",
3
+ "version": "2.3.9",
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",