worktree-bay 2.3.7 → 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/commands/add.js +3 -2
- package/dist/commands/claim.js +2 -1
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/ls.js +3 -2
- package/dist/commands/rm.js +5 -4
- package/dist/engine.js +5 -4
- package/dist/lock.js +66 -6
- package/dist/mcp.js +2 -1
- package/dist/util/color.js +22 -0
- package/dist/util/exec.js +7 -6
- package/dist/util/log.js +2 -1
- package/dist/util/progress.js +5 -4
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -7,6 +7,7 @@ import { slugify, worktreeDirName } from '../naming.js';
|
|
|
7
7
|
import { buildVars, bringUp, ensureStarted } from '../engine.js';
|
|
8
8
|
import { mainBranch } from '../git.js';
|
|
9
9
|
import { log } from '../util/log.js';
|
|
10
|
+
import { color as c } from '../util/color.js';
|
|
10
11
|
import { t } from '../i18n.js';
|
|
11
12
|
export function resolveAdd(cfg, feature, service, branch) {
|
|
12
13
|
if (!cfg.services[service])
|
|
@@ -23,11 +24,11 @@ export async function addCommand(cfg, feature, service, branch, base) {
|
|
|
23
24
|
const ctxBase = { cfg, service, sp, slot: p.slot, slug: p.slug, dir: p.dir, repo: p.repo };
|
|
24
25
|
const ctx = { ...ctxBase, vars: buildVars(cfg, ctxBase) };
|
|
25
26
|
if (fs.existsSync(p.dir)) { // 幂等重入:worktree 已在 → 不重建/不重跑 setup,但确保 dev server 在跑
|
|
26
|
-
log(t(
|
|
27
|
+
log(c.bold(c.cyan(service)) + c.dim(t(` · 已就绪 · 槽 ${p.slot} · 端口 ${ctx.vars.port}`, ` · ready · slot ${p.slot} · port ${ctx.vars.port}`)));
|
|
27
28
|
await ensureStarted(ctx);
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
|
-
log(t(
|
|
31
|
+
log(c.bold(c.cyan(service)) + c.dim(t(` · 槽 ${p.slot} · 端口 ${ctx.vars.port} · 分支 ${br}`, ` · slot ${p.slot} · port ${ctx.vars.port} · branch ${br}`)));
|
|
31
32
|
const resolvedBase = base ?? `origin/${mainBranch(p.repo)}`;
|
|
32
33
|
try {
|
|
33
34
|
await bringUp(ctx, resolvedBase, br);
|
package/dist/commands/claim.js
CHANGED
|
@@ -2,10 +2,11 @@ import { withLock } from '../lock.js';
|
|
|
2
2
|
import { claim } from '../slots.js';
|
|
3
3
|
import { portOf } from '../ports.js';
|
|
4
4
|
import { log } from '../util/log.js';
|
|
5
|
+
import { color as c } from '../util/color.js';
|
|
5
6
|
import { t } from '../i18n.js';
|
|
6
7
|
export async function claimCommand(cfg, feature) {
|
|
7
8
|
const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature));
|
|
8
|
-
log(t(`功能 "${feature}" → 槽 ${slot}`, `feature "${feature}" → slot ${slot}`));
|
|
9
|
+
log(c.bold(c.cyan(t(`功能 "${feature}" → 槽 ${slot}`, `feature "${feature}" → slot ${slot}`))));
|
|
9
10
|
for (const [n, sp] of Object.entries(cfg.services))
|
|
10
11
|
log(` ${n.padEnd(8)} ${portOf(sp.port, slot)}`);
|
|
11
12
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,12 +3,13 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { repoPath } from '../config.js';
|
|
5
5
|
import { log, warn } from '../util/log.js';
|
|
6
|
+
import { color as c } from '../util/color.js';
|
|
6
7
|
import { t } from '../i18n.js';
|
|
7
8
|
// 体检:git 是否可用、配置是否有效、各服务仓是否存在且是 git 仓。返回问题数。
|
|
8
9
|
export function doctor(cfg) {
|
|
9
10
|
let problems = 0;
|
|
10
|
-
const ok = (m) => log(
|
|
11
|
-
const bad = (m) => { warn(
|
|
11
|
+
const ok = (m) => log(`${c.green('✓')} ${m}`);
|
|
12
|
+
const bad = (m) => { warn(`${c.red('✗')} ${m}`); problems++; };
|
|
12
13
|
if (spawnSync('git', ['--version'], { encoding: 'utf8' }).status === 0)
|
|
13
14
|
ok(t('git 可用', 'git available'));
|
|
14
15
|
else
|
|
@@ -23,7 +24,7 @@ export function doctor(cfg) {
|
|
|
23
24
|
else
|
|
24
25
|
ok(`${t('服务', 'service')} ${name} → ${repo}`);
|
|
25
26
|
}
|
|
26
|
-
log(problems === 0 ? t('\n✓ 一切正常', '\n✓ all good') : t(`\n✗ 发现 ${problems} 个问题`, `\n✗ found ${problems} problem(s)`));
|
|
27
|
+
log(problems === 0 ? c.green(t('\n✓ 一切正常', '\n✓ all good')) : c.red(t(`\n✗ 发现 ${problems} 个问题`, `\n✗ found ${problems} problem(s)`)));
|
|
27
28
|
return problems;
|
|
28
29
|
}
|
|
29
30
|
export function doctorCommand(cfg) { if (doctor(cfg) > 0)
|
package/dist/commands/ls.js
CHANGED
|
@@ -2,6 +2,7 @@ import { scanOccupancy, readLabels } from '../slots.js';
|
|
|
2
2
|
import { portOf } from '../ports.js';
|
|
3
3
|
import { log } from '../util/log.js';
|
|
4
4
|
import { recordedFor, pidOnPort } from '../proc.js';
|
|
5
|
+
import { color as c } from '../util/color.js';
|
|
5
6
|
import { t } from '../i18n.js';
|
|
6
7
|
// 该服务的约定端口是否有进程在监听(按端口判,不受 shell/pnpm 让记录 pid 漂移的影响)
|
|
7
8
|
function running(cfg, dir, port) { return !!recordedFor(cfg.workspaceRoot, dir) && !!pidOnPort(port); }
|
|
@@ -11,8 +12,8 @@ export function renderSlots(cfg) {
|
|
|
11
12
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
12
13
|
const lines = [];
|
|
13
14
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
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
|
-
lines.push(
|
|
15
|
+
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) ? c.green(' ▸run') : ''}`; });
|
|
16
|
+
lines.push(`${c.dim('slot ' + n)} ${c.bold(c.cyan(labels[String(n)] ?? t('(未命名)', '(unnamed)')))} [${svc.join(', ') || c.dim(t('无 worktree', 'no worktree'))}]`);
|
|
16
17
|
}
|
|
17
18
|
return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
|
|
18
19
|
}
|
package/dist/commands/rm.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runShellLive } from '../util/exec.js';
|
|
|
7
7
|
import { withProgress } from '../util/progress.js';
|
|
8
8
|
import { stopManaged } from '../proc.js';
|
|
9
9
|
import { log, warn } from '../util/log.js';
|
|
10
|
+
import { color as c } from '../util/color.js';
|
|
10
11
|
import { t } from '../i18n.js';
|
|
11
12
|
export function resolveRm(cfg, feature, service) {
|
|
12
13
|
const slot = slotOfFeature(cfg, feature);
|
|
@@ -23,13 +24,13 @@ export async function rmCommand(cfg, feature, service, force) {
|
|
|
23
24
|
const repo = repoPath(cfg, o.service);
|
|
24
25
|
const branch = currentBranch(o.dir);
|
|
25
26
|
if (!force && (isDirty(o.dir) || hasUnpushed(repo, branch))) {
|
|
26
|
-
warn(t(`${o.service} · 跳过:有未提交或未推送的改动。先提交/推送,或加 -f 强删(会丢改动)。`, `${o.service} · skipped: uncommitted or unpushed changes. Commit/push first, or pass -f to force-remove (discards them).`));
|
|
27
|
+
warn(c.yellow(t(`${o.service} · 跳过:有未提交或未推送的改动。先提交/推送,或加 -f 强删(会丢改动)。`, `${o.service} · skipped: uncommitted or unpushed changes. Commit/push first, or pass -f to force-remove (discards them).`)));
|
|
27
28
|
continue;
|
|
28
29
|
}
|
|
29
|
-
log(
|
|
30
|
+
log(c.bold(c.cyan(o.service)) + c.dim(t(' · 拆除…', ' · tearing down…')));
|
|
30
31
|
const stopped = stopManaged(cfg.workspaceRoot, o.dir); // 先停 dev server(释放对 worktree 文件的占用)
|
|
31
32
|
if (stopped)
|
|
32
|
-
log(
|
|
33
|
+
log(` ${c.green('✓')} ` + t(`已停止 dev server(pid ${stopped.pid})`, `stopped dev server (pid ${stopped.pid})`));
|
|
33
34
|
const sp = cfg.services[o.service];
|
|
34
35
|
if (sp.teardown) {
|
|
35
36
|
const vars = buildVars(cfg, { cfg, service: o.service, sp, slot: o.slot, slug: o.slug, dir: o.dir, repo });
|
|
@@ -43,7 +44,7 @@ export async function rmCommand(cfg, feature, service, force) {
|
|
|
43
44
|
if (!service && (scanOccupancy(cfg).get(slot) ?? []).length === 0) {
|
|
44
45
|
removeLabel(cfg, slot);
|
|
45
46
|
if (removed === 0)
|
|
46
|
-
log(t(
|
|
47
|
+
log(`${c.green('✓')} ` + t(`释放空槽预约 "${feature}"(槽 ${slot})`, `released empty slot reservation "${feature}" (slot ${slot})`));
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
}
|
package/dist/engine.js
CHANGED
|
@@ -7,6 +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 { color as cc } from './util/color.js';
|
|
10
11
|
import { startDetached, recordedFor, pidAlive, setPid, pidOnPort, readLogTail } from './proc.js';
|
|
11
12
|
import { t } from './i18n.js';
|
|
12
13
|
export function mergeEnvText(text, kv) {
|
|
@@ -78,11 +79,11 @@ export async function ensureStarted(ctx) {
|
|
|
78
79
|
const ws = cfg.workspaceRoot, port = Number(vars.port);
|
|
79
80
|
const rec = recordedFor(ws, dir);
|
|
80
81
|
if (rec && pidAlive(rec.pid)) {
|
|
81
|
-
log(t(` • ${service} dev server 已在跑(pid ${rec.pid},端口 ${port})`, ` • ${service} dev server already running (pid ${rec.pid}, port ${port})`));
|
|
82
|
+
log(cc.dim(t(` • ${service} dev server 已在跑(pid ${rec.pid},端口 ${port})`, ` • ${service} dev server already running (pid ${rec.pid}, port ${port})`)));
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
if (await portInUse(port)) {
|
|
85
|
-
log(t(` • 端口 ${port} 已在监听,视为 ${service} dev server 在跑,跳过启动`, ` • port ${port} already listening; treating ${service} dev server as up, skip`));
|
|
86
|
+
log(cc.dim(t(` • 端口 ${port} 已在监听,视为 ${service} dev server 在跑,跳过启动`, ` • port ${port} already listening; treating ${service} dev server as up, skip`)));
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
const cmd = renderTemplate(sp.start, vars);
|
|
@@ -94,12 +95,12 @@ export async function ensureStarted(ctx) {
|
|
|
94
95
|
const real = pidOnPort(port);
|
|
95
96
|
if (real && real > 0)
|
|
96
97
|
setPid(ws, dir, real);
|
|
97
|
-
log(
|
|
98
|
+
log(cc.green(' ▸') + t(` 已后台启动 ${service} dev server(pid ${real || r.pid},端口 ${port}) `, ` started ${service} dev server in background (pid ${real || r.pid}, port ${port}) `) + cc.dim(t(`日志: ${r.log}`, `log: ${r.log}`)));
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
100
101
|
// 超时不等于失败:dev server 可能仍在启动/重启。给中性提示 + 日志末尾,让用户稍后 ls 复查或排查。
|
|
101
102
|
const tail = readLogTail(r.log);
|
|
102
|
-
warn(t(` • ${service} dev server 已在后台启动,但 25s 内端口 ${port} 还没就绪——可能仍在启动/重启。\n 稍后用 \`worktree-bay ls\` 看是否 ▸run;若起不来,多半是 start 命令不对或端口被占退避(vite 建议加 --strictPort)。\n 命令: ${cmd} 日志: ${r.log}
|
|
103
|
+
warn(cc.yellow(t(` • ${service} dev server 已在后台启动,但 25s 内端口 ${port} 还没就绪——可能仍在启动/重启。\n 稍后用 \`worktree-bay ls\` 看是否 ▸run;若起不来,多半是 start 命令不对或端口被占退避(vite 建议加 --strictPort)。\n 命令: ${cmd} 日志: ${r.log}`, ` • ${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}`)) + (tail ? '\n' + cc.dim(tail) : ''));
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
// 轮询直到约定端口被监听(true),或超时(false)
|
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
|
|
6
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -0,0 +1,22 @@
|
|
|
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';
|
|
12
|
+
const paint = (code) => (s) => (enabled ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
13
|
+
export const color = {
|
|
14
|
+
enabled,
|
|
15
|
+
green: paint('32'),
|
|
16
|
+
red: paint('31'),
|
|
17
|
+
yellow: paint('33'),
|
|
18
|
+
cyan: paint('36'),
|
|
19
|
+
blue: paint('34'),
|
|
20
|
+
dim: paint('2'),
|
|
21
|
+
bold: paint('1'),
|
|
22
|
+
};
|
package/dist/util/exec.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync, spawn } from 'node:child_process';
|
|
2
2
|
import { t } from '../i18n.js';
|
|
3
|
+
import { color as c, ttyLike } from './color.js';
|
|
3
4
|
export function run(cmd, args, opts = {}) { const r = spawnSync(cmd, args, { cwd: opts.cwd, stdio: 'inherit', shell: false }); return { code: r.status ?? 1 }; }
|
|
4
5
|
export function runShell(line, opts = {}) { const r = spawnSync(line, { cwd: opts.cwd, stdio: 'inherit', shell: true }); return { code: r.status ?? 1 }; }
|
|
5
6
|
export function spliceArgv(template, cmd) { const out = []; for (const el of template) {
|
|
@@ -13,7 +14,7 @@ const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '
|
|
|
13
14
|
// 折叠式执行 setup/teardown 这类外部命令:TTY 下把输出收进单行临时进度(spinner + 秒数 + 最后一行),
|
|
14
15
|
// 成功收成「✓ label(Ns)」,失败才吐完整日志便于排查;非 TTY(CI/管道/MCP)原样透传保留完整日志。
|
|
15
16
|
export function runShellLive(line, opts, label) {
|
|
16
|
-
if (!process.stderr
|
|
17
|
+
if (!ttyLike(process.stderr)) {
|
|
17
18
|
const r = spawnSync(line, { cwd: opts.cwd, stdio: 'inherit', shell: true });
|
|
18
19
|
return Promise.resolve({ code: r.status ?? 1 });
|
|
19
20
|
}
|
|
@@ -25,9 +26,9 @@ export function runShellLive(line, opts, label) {
|
|
|
25
26
|
let fi = 0;
|
|
26
27
|
const render = () => {
|
|
27
28
|
const secs = Math.floor((Date.now() - t0) / 1000);
|
|
28
|
-
const head = ` ${SPIN[fi++ % SPIN.length]} ${label} ${secs}
|
|
29
|
-
const room = Math.max(0, (process.stderr.columns || 80) -
|
|
30
|
-
process.stderr.write('\r\x1b[2K' + head + last.slice(0, room));
|
|
29
|
+
const head = ` ${c.cyan(SPIN[fi++ % SPIN.length])} ${label} ${c.dim(secs + 's')} `;
|
|
30
|
+
const room = Math.max(0, (process.stderr.columns || 80) - (` x ${label} ${secs}s `).length - 1);
|
|
31
|
+
process.stderr.write('\r\x1b[2K' + head + c.dim(last.slice(0, room)));
|
|
31
32
|
};
|
|
32
33
|
const timer = setInterval(render, 120);
|
|
33
34
|
const onData = (d) => {
|
|
@@ -45,9 +46,9 @@ export function runShellLive(line, opts, label) {
|
|
|
45
46
|
const secs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
46
47
|
process.stderr.write('\r\x1b[2K');
|
|
47
48
|
if (code === 0)
|
|
48
|
-
process.stderr.write(
|
|
49
|
+
process.stderr.write(` ${c.green('✓')} ${label}${c.dim(`(${secs}s)`)}\n`);
|
|
49
50
|
else
|
|
50
|
-
process.stderr.write(t(` ✗ ${label} 失败(退出码 ${code},${secs}s
|
|
51
|
+
process.stderr.write(c.red(t(` ✗ ${label} 失败(退出码 ${code},${secs}s)↓`, ` ✗ ${label} failed (exit ${code}, ${secs}s) ↓`)) + '\n' + buf.join(''));
|
|
51
52
|
resolve({ code: code ?? 1 });
|
|
52
53
|
});
|
|
53
54
|
});
|
package/dist/util/log.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { color as c } from './color.js';
|
|
1
2
|
export const log = (...a) => console.log(...a);
|
|
2
3
|
export const warn = (...a) => console.warn(...a);
|
|
3
|
-
export const die = (m) => { console.error('worktree-bay: ' + m); process.exit(1); };
|
|
4
|
+
export const die = (m) => { console.error(c.red('worktree-bay:') + ' ' + m); process.exit(1); };
|
package/dist/util/progress.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { log } from './log.js';
|
|
2
|
+
import { color as c, ttyLike } from './color.js';
|
|
2
3
|
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
3
4
|
// 给长耗时异步步骤一个「在干活」的反馈:TTY 下转圈 + 秒数(写 stderr,用 \r 原地刷新),
|
|
4
5
|
// 非 TTY(管道 / CI / MCP)下退化为「开始…」「✓ 完成(Ns)」两行,避免刷屏。
|
|
5
6
|
export async function withProgress(label, fn) {
|
|
6
7
|
const t0 = Date.now();
|
|
7
8
|
const secs = () => ((Date.now() - t0) / 1000).toFixed(1);
|
|
8
|
-
if (!process.stderr
|
|
9
|
+
if (!ttyLike(process.stderr)) {
|
|
9
10
|
log(` → ${label} …`);
|
|
10
11
|
const r = await fn();
|
|
11
|
-
log(` ✓ ${label}
|
|
12
|
+
log(` ${c.green('✓')} ${label}${c.dim(`(${secs()}s)`)}`);
|
|
12
13
|
return r;
|
|
13
14
|
}
|
|
14
15
|
let i = 0;
|
|
15
|
-
const timer = setInterval(() => { process.stderr.write(`\r ${FRAMES[i++ % FRAMES.length]} ${label} ${secs()}
|
|
16
|
+
const timer = setInterval(() => { process.stderr.write(`\r ${c.cyan(FRAMES[i++ % FRAMES.length])} ${label} ${c.dim(secs() + 's')} `); }, 120);
|
|
16
17
|
if (typeof timer.unref === 'function')
|
|
17
18
|
timer.unref();
|
|
18
19
|
try {
|
|
@@ -20,6 +21,6 @@ export async function withProgress(label, fn) {
|
|
|
20
21
|
}
|
|
21
22
|
finally {
|
|
22
23
|
clearInterval(timer);
|
|
23
|
-
process.stderr.write(`\r ✓ ${label}
|
|
24
|
+
process.stderr.write(`\r ${c.green('✓')} ${label}${c.dim(`(${secs()}s)`)} \n`);
|
|
24
25
|
}
|
|
25
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "2.3.
|
|
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",
|