worktree-bay 2.3.6 → 2.3.8
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/completion.js +2 -0
- 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/util/color.js +13 -0
- package/dist/util/exec.js +6 -5
- package/dist/util/log.js +2 -1
- package/dist/util/progress.js +4 -3
- 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
|
}
|
|
@@ -19,6 +19,8 @@ export function complete(cfg, words) {
|
|
|
19
19
|
return Object.values(readLabels(cfg));
|
|
20
20
|
if (['add', 'run', 'sh', 'path'].includes(sub) && pos === 2)
|
|
21
21
|
return Object.keys(cfg.services);
|
|
22
|
+
if (sub === 'run' && pos === 3)
|
|
23
|
+
return Object.keys(cfg.services[prev[2]]?.run ?? {}); // run <feature> <service> <name>:补该服务的 run 命令名
|
|
22
24
|
if (sub === 'up' && pos >= 2)
|
|
23
25
|
return Object.keys(cfg.services); // up 接变长服务列表
|
|
24
26
|
return [];
|
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)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 轻量 ANSI 着色:仅在 TTY 且未设 NO_COLOR 时上色,否则原样返回(管道/CI/重定向不带控制符)。
|
|
2
|
+
const enabled = (!!process.stdout.isTTY || !!process.stderr.isTTY) && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
|
|
3
|
+
const paint = (code) => (s) => (enabled ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
4
|
+
export const color = {
|
|
5
|
+
enabled,
|
|
6
|
+
green: paint('32'),
|
|
7
|
+
red: paint('31'),
|
|
8
|
+
yellow: paint('33'),
|
|
9
|
+
cyan: paint('36'),
|
|
10
|
+
blue: paint('34'),
|
|
11
|
+
dim: paint('2'),
|
|
12
|
+
bold: paint('1'),
|
|
13
|
+
};
|
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 } 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) {
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import { log } from './log.js';
|
|
2
|
+
import { color as c } from './color.js';
|
|
2
3
|
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
3
4
|
// 给长耗时异步步骤一个「在干活」的反馈:TTY 下转圈 + 秒数(写 stderr,用 \r 原地刷新),
|
|
4
5
|
// 非 TTY(管道 / CI / MCP)下退化为「开始…」「✓ 完成(Ns)」两行,避免刷屏。
|
|
@@ -8,11 +9,11 @@ export async function withProgress(label, fn) {
|
|
|
8
9
|
if (!process.stderr.isTTY) {
|
|
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.8",
|
|
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",
|