worktree-bay 2.1.1 → 2.2.0
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/README.md +3 -1
- package/dist/cli.js +40 -20
- package/dist/commands/add.js +19 -4
- package/dist/commands/claim.js +2 -1
- package/dist/commands/completion.js +4 -3
- package/dist/commands/doctor.js +8 -7
- package/dist/commands/gc.js +6 -5
- package/dist/commands/init.js +6 -5
- package/dist/commands/ls.js +3 -2
- package/dist/commands/passthrough.js +4 -3
- package/dist/commands/rm.js +5 -4
- package/dist/config.js +9 -8
- package/dist/engine.js +5 -4
- package/dist/git.js +10 -1
- package/dist/i18n.js +23 -0
- package/dist/lock.js +2 -1
- package/dist/mcp.js +14 -5
- package/dist/slots.js +2 -1
- package/dist/util/clierr.js +19 -0
- package/package.json +1 -1
- package/skill.md +3 -1
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ npm i -g worktree-bay
|
|
|
24
24
|
|
|
25
25
|
需要 Node ≥ 20。
|
|
26
26
|
|
|
27
|
+
> 输出语言按系统区域自动切换中/英(识别不出时默认中文)。可用环境变量 `WORKTREE_BAY_LANG=zh|en` 强制指定。
|
|
28
|
+
|
|
27
29
|
## 快速上手
|
|
28
30
|
|
|
29
31
|
```bash
|
|
@@ -126,7 +128,7 @@ worktree-bay completion install
|
|
|
126
128
|
}
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`
|
|
131
|
+
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_doctor / ls / up / claim / add / path / run / down / gc / init / skill`(`doctor` 列出全部服务名,`ls` 以 JSON 返回各 worktree 路径,`path` 直接给某功能某服务的目录,`down` 可只拆单个服务,`skill` 取完整指南)。
|
|
130
132
|
|
|
131
133
|
## 许可证
|
|
132
134
|
|
package/dist/cli.js
CHANGED
|
@@ -14,10 +14,12 @@ import { startMcp } from './mcp.js';
|
|
|
14
14
|
import { readSkill } from './skill.js';
|
|
15
15
|
import { initCommand } from './commands/init.js';
|
|
16
16
|
import { die, log } from './util/log.js';
|
|
17
|
+
import { t } from './i18n.js';
|
|
18
|
+
import { friendlyParseError } from './util/clierr.js';
|
|
17
19
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
18
20
|
const program = new Command();
|
|
19
|
-
program.name('worktree-bay').description('worktree 槽位 + 端口编排器:多服务并行开发利器').version(pkg.version);
|
|
20
|
-
program.addHelpText('after', `
|
|
21
|
+
program.name('worktree-bay').description(t('worktree 槽位 + 端口编排器:多服务并行开发利器', 'Per-feature git worktree + port slot orchestrator for parallel multi-service development')).version(pkg.version);
|
|
22
|
+
program.addHelpText('after', t(`
|
|
21
23
|
示例:
|
|
22
24
|
worktree-bay init 在当前工作区生成配置(扫描子 git 仓预填服务)
|
|
23
25
|
worktree-bay up drill-fix api lms 一条命令为功能起 api+lms(分支默认 = 功能名)
|
|
@@ -26,73 +28,82 @@ program.addHelpText('after', `
|
|
|
26
28
|
worktree-bay down drill-fix 拆除整个功能
|
|
27
29
|
worktree-bay gc 回收已合并的功能(默认 dry-run)
|
|
28
30
|
|
|
29
|
-
更多: worktree-bay skill(完整使用与配置指南) · worktree-bay help
|
|
31
|
+
更多: worktree-bay skill(完整使用与配置指南) · worktree-bay help <命令>(单命令帮助)`, `
|
|
32
|
+
Examples:
|
|
33
|
+
worktree-bay init scaffold config here (scans child git repos)
|
|
34
|
+
worktree-bay up drill-fix api lms bring up api+lms for a feature (branch defaults to feature name)
|
|
35
|
+
worktree-bay ls list all features / port usage
|
|
36
|
+
worktree-bay run drill-fix api test run run.test inside api
|
|
37
|
+
worktree-bay down drill-fix tear down the whole feature
|
|
38
|
+
worktree-bay gc reclaim merged features (dry-run by default)
|
|
39
|
+
|
|
40
|
+
More: worktree-bay skill (full usage & config guide) · worktree-bay help <command>`));
|
|
30
41
|
const sync = (fn) => { try {
|
|
31
42
|
fn(loadConfig(process.cwd()));
|
|
32
43
|
}
|
|
33
44
|
catch (e) {
|
|
34
45
|
die(e.message);
|
|
35
46
|
} };
|
|
36
|
-
program.command('init').description('在当前工作区生成 worktree-bay.config.json(扫描子 git 仓预填服务)')
|
|
47
|
+
program.command('init').description(t('在当前工作区生成 worktree-bay.config.json(扫描子 git 仓预填服务)', 'scaffold worktree-bay.config.json here (scans child git repos to prefill services)'))
|
|
37
48
|
.action(() => { try {
|
|
38
49
|
initCommand(process.cwd());
|
|
39
50
|
}
|
|
40
51
|
catch (e) {
|
|
41
52
|
die(e.message);
|
|
42
53
|
} });
|
|
43
|
-
program.command('claim <feature>').description('为功能占一个槽位(打印各服务在该槽的端口)')
|
|
54
|
+
program.command('claim <feature>').description(t('为功能占一个槽位(打印各服务在该槽的端口)', 'claim a slot for a feature (prints each service\'s port in that slot)'))
|
|
44
55
|
.action(async (f) => { try {
|
|
45
56
|
await claimCommand(loadConfig(process.cwd()), f);
|
|
46
57
|
}
|
|
47
58
|
catch (e) {
|
|
48
59
|
die(e.message);
|
|
49
60
|
} });
|
|
50
|
-
program.command('ls').description('列出所有槽位与占用状态').option('--json', '以 JSON 输出(含 worktree 路径,便于脚本/AI 消费)')
|
|
61
|
+
program.command('ls').description(t('列出所有槽位与占用状态', 'list all slots and their occupancy')).option('--json', t('以 JSON 输出(含 worktree 路径,便于脚本/AI 消费)', 'output JSON (includes worktree paths, for scripts/AI)'))
|
|
51
62
|
.action((o) => sync((c) => lsCommand(c, !!o.json)));
|
|
52
|
-
program.command('path <feature> <service>').description('打印某功能某服务的 worktree 绝对路径(可 cd $(...))')
|
|
63
|
+
program.command('path <feature> <service>').description(t('打印某功能某服务的 worktree 绝对路径(可 cd $(...))', 'print the absolute worktree path for a feature\'s service (cd $(...))'))
|
|
53
64
|
.action((f, s) => sync((c) => pathCommand(c, f, s)));
|
|
54
|
-
program.command('doctor').description('体检:git/配置/各服务仓是否就绪')
|
|
65
|
+
program.command('doctor').description(t('体检:git/配置/各服务仓是否就绪', 'health check: git / config / each service repo readiness'))
|
|
55
66
|
.action(() => sync(doctorCommand));
|
|
56
|
-
program.command('up <feature> <services...>').description('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)')
|
|
67
|
+
program.command('up <feature> <services...>').description(t('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)', 'bring up multiple services for a feature (auto-claim + branch defaults to feature name)'))
|
|
57
68
|
.action(async (f, services) => { try {
|
|
58
69
|
await upCommand(loadConfig(process.cwd()), f, services);
|
|
59
70
|
}
|
|
60
71
|
catch (e) {
|
|
61
72
|
die(e.message);
|
|
62
73
|
} });
|
|
63
|
-
program.command('add <feature> <service> [branch] [base]').description('为功能在某服务开 worktree(branch 默认 = 功能名)')
|
|
74
|
+
program.command('add <feature> <service> [branch] [base]').description(t('为功能在某服务开 worktree(branch 默认 = 功能名)', 'open a worktree for a feature on one service (branch defaults to feature name)'))
|
|
64
75
|
.action(async (f, s, b, base) => { try {
|
|
65
76
|
await addCommand(loadConfig(process.cwd()), f, s, b, base);
|
|
66
77
|
}
|
|
67
78
|
catch (e) {
|
|
68
79
|
die(e.message);
|
|
69
80
|
} });
|
|
70
|
-
program.command('run <feature> <service> <name> [args...]').description('在服务运行体里跑 run.<name> 命令(透传 args)')
|
|
81
|
+
program.command('run <feature> <service> <name> [args...]').description(t('在服务运行体里跑 run.<name> 命令(透传 args)', 'run the configured run.<name> command inside a service (passes args through)'))
|
|
71
82
|
.action((f, s, n, args) => sync((c) => runCommand(c, f, s, n, args ?? [])));
|
|
72
|
-
program.command('sh <feature> <service>').description('进入服务运行体的 shell')
|
|
83
|
+
program.command('sh <feature> <service>').description(t('进入服务运行体的 shell', 'open a shell inside the service runtime'))
|
|
73
84
|
.action((f, s) => sync((c) => shCommand(c, f, s)));
|
|
74
|
-
program.command('down <feature>').description('拆除整个功能的所有服务 worktree(= rm <feature>)').option('-f, --force', '跳过脏/未推检查强制删除')
|
|
85
|
+
program.command('down <feature>').description(t('拆除整个功能的所有服务 worktree(= rm <feature>)', 'tear down all of a feature\'s service worktrees (= rm <feature>)')).option('-f, --force', t('跳过脏/未推检查强制删除', 'skip dirty/unpushed checks and force-remove'))
|
|
75
86
|
.action(async (f, o) => { try {
|
|
76
87
|
await rmCommand(loadConfig(process.cwd()), f, undefined, !!o.force);
|
|
77
88
|
}
|
|
78
89
|
catch (e) {
|
|
79
90
|
die(e.message);
|
|
80
91
|
} });
|
|
81
|
-
program.command('rm <feature> [service]').description('拆除某服务或整槽的 worktree(默认查脏/未推保护)').option('-f, --force', '跳过脏/未推检查强制删除')
|
|
92
|
+
program.command('rm <feature> [service]').description(t('拆除某服务或整槽的 worktree(默认查脏/未推保护)', 'remove one service\'s or the whole slot\'s worktree (dirty/unpushed protected by default)')).option('-f, --force', t('跳过脏/未推检查强制删除', 'skip dirty/unpushed checks and force-remove'))
|
|
82
93
|
.action(async (f, s, o) => { try {
|
|
83
94
|
await rmCommand(loadConfig(process.cwd()), f, s, !!o.force);
|
|
84
95
|
}
|
|
85
96
|
catch (e) {
|
|
86
97
|
die(e.message);
|
|
87
98
|
} });
|
|
88
|
-
program.command('gc').description('合并感知回收(默认 dry-run)').option('--apply', '实际执行回收')
|
|
99
|
+
program.command('gc').description(t('合并感知回收(默认 dry-run)', 'merge-aware reclaim (dry-run by default)')).option('--apply', t('实际执行回收', 'actually perform the reclaim'))
|
|
89
100
|
.action(async (o) => { try {
|
|
90
101
|
await gcCommand(loadConfig(process.cwd()), !!o.apply);
|
|
91
102
|
}
|
|
92
103
|
catch (e) {
|
|
93
104
|
die(e.message);
|
|
94
105
|
} });
|
|
95
|
-
program.command('completion <target> [shell]').description('install 一键装进 shell;或 bash|zsh|fish 打印补全脚本')
|
|
106
|
+
program.command('completion <target> [shell]').description(t('install 一键装进 shell;或 bash|zsh|fish 打印补全脚本', 'install: set up shell completion; or bash|zsh|fish: print the completion script'))
|
|
96
107
|
.action((target, shell) => { try {
|
|
97
108
|
if (target === 'install')
|
|
98
109
|
installCompletion(shell);
|
|
@@ -102,21 +113,21 @@ program.command('completion <target> [shell]').description('install 一键装进
|
|
|
102
113
|
catch (e) {
|
|
103
114
|
die(e.message);
|
|
104
115
|
} });
|
|
105
|
-
program.command('mcp').description('启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 worktree-bay')
|
|
116
|
+
program.command('mcp').description(t('启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 worktree-bay', 'start the MCP server (stdio, lightweight, spawned on demand) for AI agents'))
|
|
106
117
|
.action(() => { try {
|
|
107
118
|
startMcp();
|
|
108
119
|
}
|
|
109
120
|
catch (e) {
|
|
110
121
|
die(e.message);
|
|
111
122
|
} });
|
|
112
|
-
program.command('skill').description('打印 worktree-bay 使用与配置完全指南')
|
|
123
|
+
program.command('skill').description(t('打印 worktree-bay 使用与配置完全指南', 'print the full worktree-bay usage & config guide'))
|
|
113
124
|
.action(() => { try {
|
|
114
125
|
log(readSkill());
|
|
115
126
|
}
|
|
116
127
|
catch (e) {
|
|
117
128
|
die(e.message);
|
|
118
129
|
} });
|
|
119
|
-
program.command('version').description('显示版本号').action(() => log(pkg.version));
|
|
130
|
+
program.command('version').description(t('显示版本号', 'show the version number')).action(() => log(pkg.version));
|
|
120
131
|
program.command('__complete', { hidden: true }).allowUnknownOption().action(() => {
|
|
121
132
|
const words = process.argv.slice(process.argv.indexOf('--') + 1);
|
|
122
133
|
let cfg = null;
|
|
@@ -129,4 +140,13 @@ program.command('__complete', { hidden: true }).allowUnknownOption().action(() =
|
|
|
129
140
|
}
|
|
130
141
|
catch { /* 静默 */ }
|
|
131
142
|
});
|
|
132
|
-
|
|
143
|
+
// 把 commander 的英文解析错误(missing required argument 等)重写为自然语言(中/英)+ 用法 + 建议。
|
|
144
|
+
// 走 configureOutput.writeErr(会被子命令继承)而非 exitOverride+catch(子命令上不可靠)。
|
|
145
|
+
program.showSuggestionAfterError(false);
|
|
146
|
+
program.configureOutput({
|
|
147
|
+
writeErr: (str) => {
|
|
148
|
+
const sub = program.commands.find((c) => c.name() === process.argv[2]);
|
|
149
|
+
process.stderr.write('worktree-bay: ' + friendlyParseError(str, sub ? { name: sub.name(), usage: sub.usage(), description: sub.description() } : undefined) + '\n');
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
program.parseAsync(process.argv).catch((e) => die(e.message));
|
package/dist/commands/add.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { repoPath } from '../config.js';
|
|
3
4
|
import { withLock } from '../lock.js';
|
|
@@ -5,10 +6,11 @@ import { claim } from '../slots.js';
|
|
|
5
6
|
import { slugify, worktreeDirName } from '../naming.js';
|
|
6
7
|
import { buildVars, bringUp } from '../engine.js';
|
|
7
8
|
import { mainBranch } from '../git.js';
|
|
8
|
-
import { log } from '../util/log.js';
|
|
9
|
+
import { log, warn } from '../util/log.js';
|
|
10
|
+
import { t } from '../i18n.js';
|
|
9
11
|
export function resolveAdd(cfg, feature, service, branch) {
|
|
10
12
|
if (!cfg.services[service])
|
|
11
|
-
throw new Error(
|
|
13
|
+
throw new Error(t(`未知服务「${service}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${service}". Run \`worktree-bay doctor\` to see configured services.`));
|
|
12
14
|
const slot = claim(cfg, feature);
|
|
13
15
|
const slug = worktreeDirName(slot, slugify(branch));
|
|
14
16
|
return { service, slot, slug, dir: path.join(repoPath(cfg, service), '.worktrees', slug), repo: repoPath(cfg, service) };
|
|
@@ -18,10 +20,23 @@ export async function addCommand(cfg, feature, service, branch, base) {
|
|
|
18
20
|
await withLock(cfg.workspaceRoot, async () => {
|
|
19
21
|
const p = resolveAdd(cfg, feature, service, br);
|
|
20
22
|
const sp = cfg.services[service];
|
|
23
|
+
if (fs.existsSync(p.dir)) { // 幂等:该服务已在本功能下开过 worktree → 跳过(让 up 可安全重跑)
|
|
24
|
+
warn(t(`• ${service} 已在功能 "${feature}"(槽 ${p.slot}),跳过。要重建先 \`worktree-bay rm ${feature} ${service}\`。`, `• ${service} already in "${feature}" (slot ${p.slot}), skipping. To recreate: \`worktree-bay rm ${feature} ${service}\`.`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
21
27
|
const ctxBase = { cfg, service, sp, slot: p.slot, slug: p.slug, dir: p.dir, repo: p.repo };
|
|
22
28
|
const ctx = { ...ctxBase, vars: buildVars(cfg, ctxBase) };
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
const resolvedBase = base ?? `origin/${mainBranch(p.repo)}`;
|
|
30
|
+
try {
|
|
31
|
+
await bringUp(ctx, resolvedBase, br);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const m = String(e.message);
|
|
35
|
+
if (/invalid reference|unknown revision|ambiguous argument|not a valid|Not a valid object name/i.test(m))
|
|
36
|
+
throw new Error(t(`基分支「${resolvedBase}」无效(该仓可能没有 origin 或对应主分支)。给 add 显式传 base,例如:worktree-bay add ${feature} ${service} ${br} HEAD`, `invalid base ref "${resolvedBase}" (this repo may have no origin or main branch). Pass an explicit base to add, e.g.: worktree-bay add ${feature} ${service} ${br} HEAD`));
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
log(t(`✓ ${service} 挂入 "${feature}"(槽 ${p.slot},端口 ${ctx.vars.port},分支 ${br})`, `✓ ${service} added to "${feature}" (slot ${p.slot}, port ${ctx.vars.port}, branch ${br})`));
|
|
25
40
|
});
|
|
26
41
|
}
|
|
27
42
|
// up: 一条命令为功能批量起多个服务(claim 自动 + 各服务默认分支)
|
package/dist/commands/claim.js
CHANGED
|
@@ -2,9 +2,10 @@ 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 { t } from '../i18n.js';
|
|
5
6
|
export async function claimCommand(cfg, feature) {
|
|
6
7
|
const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature));
|
|
7
|
-
log(`功能 "${feature}" → 槽 ${slot}`);
|
|
8
|
+
log(t(`功能 "${feature}" → 槽 ${slot}`, `feature "${feature}" → slot ${slot}`));
|
|
8
9
|
for (const [n, sp] of Object.entries(cfg.services))
|
|
9
10
|
log(` ${n.padEnd(8)} ${portOf(sp.port, slot)}`);
|
|
10
11
|
}
|
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readLabels } from '../slots.js';
|
|
5
5
|
import { log } from '../util/log.js';
|
|
6
|
+
import { t } from '../i18n.js';
|
|
6
7
|
const SUBCMDS = ['init', 'doctor', 'claim', 'up', 'add', 'ls', 'path', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp', 'skill', 'version', 'help'];
|
|
7
8
|
// words = 命令名 + 光标前已输入完的词(不含当前正在补的词)
|
|
8
9
|
export function complete(cfg, words) {
|
|
@@ -42,7 +43,7 @@ export function installCompletion(shell) {
|
|
|
42
43
|
fs.mkdirSync(dir, { recursive: true });
|
|
43
44
|
const file = path.join(dir, 'worktree-bay.fish');
|
|
44
45
|
fs.writeFileSync(file, completionScript('fish') + '\n');
|
|
45
|
-
log(`✓ fish 补全已写入 ${file}(新开 fish
|
|
46
|
+
log(t(`✓ fish 补全已写入 ${file}(新开 fish 即生效)`, `✓ fish completion written to ${file} (effective in a new fish session)`));
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
const isZsh = sh === 'zsh';
|
|
@@ -50,9 +51,9 @@ export function installCompletion(shell) {
|
|
|
50
51
|
const line = `eval "$(worktree-bay completion ${isZsh ? 'zsh' : 'bash'})"`;
|
|
51
52
|
const cur = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
|
|
52
53
|
if (cur.includes(line)) {
|
|
53
|
-
log(`✓ 补全已在 ${rc}
|
|
54
|
+
log(t(`✓ 补全已在 ${rc},无需重复安装`, `✓ completion already in ${rc}, nothing to do`));
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
fs.appendFileSync(rc, `\n# worktree-bay completion\n${line}\n`);
|
|
57
|
-
log(`✓ 补全已加入 ${rc},执行 'source ${rc}'
|
|
58
|
+
log(t(`✓ 补全已加入 ${rc},执行 'source ${rc}' 或重开终端即生效`, `✓ completion added to ${rc}; run 'source ${rc}' or reopen your terminal to enable it`));
|
|
58
59
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,26 +3,27 @@ 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 { t } from '../i18n.js';
|
|
6
7
|
// 体检:git 是否可用、配置是否有效、各服务仓是否存在且是 git 仓。返回问题数。
|
|
7
8
|
export function doctor(cfg) {
|
|
8
9
|
let problems = 0;
|
|
9
10
|
const ok = (m) => log(`✓ ${m}`);
|
|
10
11
|
const bad = (m) => { warn(`✗ ${m}`); problems++; };
|
|
11
12
|
if (spawnSync('git', ['--version'], { encoding: 'utf8' }).status === 0)
|
|
12
|
-
ok('git 可用');
|
|
13
|
+
ok(t('git 可用', 'git available'));
|
|
13
14
|
else
|
|
14
|
-
bad('git 不可用(worktree 依赖 git)');
|
|
15
|
-
ok(`配置已加载并通过校验(${Object.keys(cfg.services).length} 个服务,槽位 1..${cfg.maxSlots},端口 = 服务基址 +
|
|
15
|
+
bad(t('git 不可用(worktree 依赖 git,请先安装 git)', 'git not available (worktree needs git — install it first)'));
|
|
16
|
+
ok(t(`配置已加载并通过校验(${Object.keys(cfg.services).length} 个服务,槽位 1..${cfg.maxSlots},端口 = 服务基址 + 槽号)`, `config loaded and valid (${Object.keys(cfg.services).length} services, slots 1..${cfg.maxSlots}, port = service base + slot)`));
|
|
16
17
|
for (const name of Object.keys(cfg.services)) {
|
|
17
18
|
const repo = repoPath(cfg, name);
|
|
18
19
|
if (!fs.existsSync(repo))
|
|
19
|
-
bad(`服务 ${name} 仓目录不存在: ${repo}`);
|
|
20
|
+
bad(t(`服务 ${name} 仓目录不存在: ${repo}(检查配置里的 workspaceRoot / repo,或先 git clone)`, `service ${name} repo dir missing: ${repo} (check workspaceRoot/repo in config, or clone it)`));
|
|
20
21
|
else if (!fs.existsSync(path.join(repo, '.git')))
|
|
21
|
-
bad(`服务 ${name} 不是 git 仓: ${repo}`);
|
|
22
|
+
bad(t(`服务 ${name} 不是 git 仓: ${repo}(在该目录 git init 或 clone)`, `service ${name} is not a git repo: ${repo} (git init or clone there)`));
|
|
22
23
|
else
|
|
23
|
-
ok(
|
|
24
|
+
ok(`${t('服务', 'service')} ${name} → ${repo}`);
|
|
24
25
|
}
|
|
25
|
-
log(problems === 0 ? '\n✓ 一切正常' : `\n✗ 发现 ${problems}
|
|
26
|
+
log(problems === 0 ? t('\n✓ 一切正常', '\n✓ all good') : t(`\n✗ 发现 ${problems} 个问题`, `\n✗ found ${problems} problem(s)`));
|
|
26
27
|
return problems;
|
|
27
28
|
}
|
|
28
29
|
export function doctorCommand(cfg) { if (doctor(cfg) > 0)
|
package/dist/commands/gc.js
CHANGED
|
@@ -5,6 +5,7 @@ import { buildVars } from '../engine.js';
|
|
|
5
5
|
import { currentBranch, isDirty, hasUnpushed, isMergedToMain, remoteBranchGone, removeWorktree } from '../git.js';
|
|
6
6
|
import { runShell } from '../util/exec.js';
|
|
7
7
|
import { log, warn } from '../util/log.js';
|
|
8
|
+
import { t } from '../i18n.js';
|
|
8
9
|
export function classifyForGc(s) {
|
|
9
10
|
if (s.merged && !s.dirty && !s.unpushed)
|
|
10
11
|
return 'auto-remove';
|
|
@@ -20,7 +21,7 @@ export async function gcCommand(cfg, apply) {
|
|
|
20
21
|
const branch = currentBranch(o.dir);
|
|
21
22
|
const v = classifyForGc({ merged: isMergedToMain(repo, branch), dirty: isDirty(o.dir), unpushed: hasUnpushed(repo, branch) });
|
|
22
23
|
if (v === 'auto-remove') {
|
|
23
|
-
log(`[gc] ${o.service} (
|
|
24
|
+
log(t(`[gc] ${o.service} (槽 ${slot}) 已合并且干净 → 移除`, `[gc] ${o.service} (slot ${slot}) merged & clean → remove`));
|
|
24
25
|
if (apply) {
|
|
25
26
|
const sp = cfg.services[o.service];
|
|
26
27
|
if (sp.teardown) {
|
|
@@ -31,16 +32,16 @@ export async function gcCommand(cfg, apply) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
else if (v === 'flag')
|
|
34
|
-
warn(`[gc] ${o.service} (slot ${slot})
|
|
35
|
+
warn(t(`[gc] ${o.service} (槽 ${slot}) 已合并但有脏/未推改动 → 跳过;确认无误后用 \`worktree-bay rm ${o.slug.replace(/^s\d+-/, '')} ${o.service} -f\` 删`, `[gc] ${o.service} (slot ${slot}) merged but dirty/unpushed → skipped; once sure, remove with \`worktree-bay rm <feature> ${o.service} -f\``));
|
|
35
36
|
else if (remoteBranchGone(repo, branch))
|
|
36
|
-
warn(`[gc] ${o.service} (
|
|
37
|
+
warn(t(`[gc] ${o.service} (槽 ${slot}) 远端分支已删(疑似 squash 合并)→ 确认后用 \`worktree-bay down <功能>\` 拆`, `[gc] ${o.service} (slot ${slot}) remote branch gone (likely squash-merged) → once sure, tear down with \`worktree-bay down <feature>\``));
|
|
37
38
|
}
|
|
38
39
|
for (const { slot, feature } of pruneEmptyLabels(cfg)) {
|
|
39
|
-
log(`[gc] 槽 ${slot}(${feature}
|
|
40
|
+
log(t(`[gc] 槽 ${slot}(${feature})是空预约(无 worktree)`, `[gc] slot ${slot} (${feature}) is an empty reservation (no worktree)`));
|
|
40
41
|
if (apply)
|
|
41
42
|
removeLabel(cfg, slot);
|
|
42
43
|
}
|
|
43
44
|
if (!apply)
|
|
44
|
-
log('(dry-run;加 --apply
|
|
45
|
+
log(t('(dry-run;加 --apply 才真正执行 auto-remove 与空预约清理)', '(dry-run; pass --apply to actually perform auto-remove and clear empty reservations)'));
|
|
45
46
|
});
|
|
46
47
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { log, warn } from '../util/log.js';
|
|
4
|
+
import { t } from '../i18n.js';
|
|
4
5
|
// 引导生成一份 worktree-bay.config.json:扫描 cwd 子目录里的 git 仓预填为服务(按服务分配端口段)
|
|
5
6
|
export function initCommand(cwd) {
|
|
6
7
|
const target = path.join(cwd, 'worktree-bay.config.json');
|
|
7
8
|
if (fs.existsSync(target)) {
|
|
8
|
-
warn(`已存在 ${target}
|
|
9
|
+
warn(t(`已存在 ${target},未覆盖`, `${target} already exists, not overwriting`));
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
const repos = [];
|
|
@@ -28,10 +29,10 @@ export function initCommand(cwd) {
|
|
|
28
29
|
}
|
|
29
30
|
const config = { workspaceRoot: cwd, maxSlots: 9, services };
|
|
30
31
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
|
|
31
|
-
log(`✓ 已生成 ${target}`);
|
|
32
|
+
log(t(`✓ 已生成 ${target}`, `✓ wrote ${target}`));
|
|
32
33
|
if (repos.length)
|
|
33
|
-
log(` 识别到服务: ${repos.join(', ')}(每个分配一段端口,基址间隔 10
|
|
34
|
+
log(t(` 识别到服务: ${repos.join(', ')}(每个分配一段端口,基址间隔 10)`, ` detected services: ${repos.join(', ')} (each gets a port segment, base ports 10 apart)`));
|
|
34
35
|
else
|
|
35
|
-
log(` 未识别到子 git 仓,已写入 api/web
|
|
36
|
-
log(` 下一步:补全各服务的 setup/env/upstream/exec/run。完整配置说明:worktree-bay skill`);
|
|
36
|
+
log(t(` 未识别到子 git 仓,已写入 api/web 示例模板`, ` no child git repos found; wrote an api/web example template`));
|
|
37
|
+
log(t(` 下一步:补全各服务的 setup/env/upstream/exec/run。完整配置说明:worktree-bay skill`, ` next: fill in each service's setup/env/upstream/exec/run. Full config guide: worktree-bay skill`));
|
|
37
38
|
}
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { t } from '../i18n.js';
|
|
4
5
|
export function renderSlots(cfg) {
|
|
5
6
|
const occ = scanOccupancy(cfg);
|
|
6
7
|
const labels = readLabels(cfg);
|
|
@@ -8,9 +9,9 @@ export function renderSlots(cfg) {
|
|
|
8
9
|
const lines = [];
|
|
9
10
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
10
11
|
const svc = (occ.get(n) ?? []).map((o) => `${o.service}@${portOf(cfg.services[o.service].port, n)}`);
|
|
11
|
-
lines.push(`slot ${n} ${labels[String(n)] ?? '(unnamed)'} [${svc.join(', ') || 'no worktree'}]`);
|
|
12
|
+
lines.push(`slot ${n} ${labels[String(n)] ?? t('(未命名)', '(unnamed)')} [${svc.join(', ') || t('无 worktree', 'no worktree')}]`);
|
|
12
13
|
}
|
|
13
|
-
return lines.join('\n') || '(no slots in use)';
|
|
14
|
+
return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
|
|
14
15
|
}
|
|
15
16
|
export function slotsData(cfg) {
|
|
16
17
|
const occ = scanOccupancy(cfg);
|
|
@@ -2,13 +2,14 @@ import { repoPath } from '../config.js';
|
|
|
2
2
|
import { scanOccupancy, slotOfFeature } from '../slots.js';
|
|
3
3
|
import { buildVars, execArgv, run } from '../engine.js';
|
|
4
4
|
import { log } from '../util/log.js';
|
|
5
|
+
import { t } from '../i18n.js';
|
|
5
6
|
function occupantOf(cfg, feature, service) {
|
|
6
7
|
const slot = slotOfFeature(cfg, feature);
|
|
7
8
|
if (slot === undefined)
|
|
8
|
-
throw new Error(
|
|
9
|
+
throw new Error(t(`功能「${feature}」未占槽。先 \`worktree-bay up ${feature} <服务...>\` 起它,再用 \`worktree-bay ls\` 确认。`, `feature "${feature}" hasn't claimed a slot. Run \`worktree-bay up ${feature} <services...>\` first, then check \`worktree-bay ls\`.`));
|
|
9
10
|
const occ = (scanOccupancy(cfg).get(slot) ?? []).find((o) => o.service === service);
|
|
10
11
|
if (!occ)
|
|
11
|
-
throw new Error(
|
|
12
|
+
throw new Error(t(`服务「${service}」不在功能「${feature}」里。先 \`worktree-bay add ${feature} ${service}\`,或用 \`worktree-bay ls\` 看已起的服务。`, `service "${service}" is not in feature "${feature}". Run \`worktree-bay add ${feature} ${service}\` first, or see \`worktree-bay ls\`.`));
|
|
12
13
|
return occ;
|
|
13
14
|
}
|
|
14
15
|
function ctxOf(cfg, feature, service) {
|
|
@@ -23,7 +24,7 @@ export function runCommand(cfg, feature, service, name, args) {
|
|
|
23
24
|
const ctx = ctxOf(cfg, feature, service);
|
|
24
25
|
const named = ctx.sp.run?.[name];
|
|
25
26
|
if (!named)
|
|
26
|
-
throw new Error(
|
|
27
|
+
throw new Error(t(`服务 ${service} 没有定义 run.${name}。在 worktree-bay.config.json 里该服务的 "run" 下加一条,如 "run": { "${name}": ["echo", "hi"] }。`, `service ${service} has no run.${name}. Add it under that service's "run" in worktree-bay.config.json, e.g. "run": { "${name}": ["echo", "hi"] }.`));
|
|
27
28
|
const argv = execArgv(ctx, [...named, ...args]);
|
|
28
29
|
process.exit(run(argv[0], argv.slice(1)).code);
|
|
29
30
|
}
|
package/dist/commands/rm.js
CHANGED
|
@@ -5,10 +5,11 @@ import { buildVars } from '../engine.js';
|
|
|
5
5
|
import { isDirty, hasUnpushed, currentBranch, removeWorktree } from '../git.js';
|
|
6
6
|
import { runShell } from '../util/exec.js';
|
|
7
7
|
import { log, warn } from '../util/log.js';
|
|
8
|
+
import { t } from '../i18n.js';
|
|
8
9
|
export function resolveRm(cfg, feature, service) {
|
|
9
10
|
const slot = slotOfFeature(cfg, feature);
|
|
10
11
|
if (slot === undefined)
|
|
11
|
-
throw new Error(
|
|
12
|
+
throw new Error(t(`功能「${feature}」未占槽,无需拆除。用 \`worktree-bay ls\` 看在用的功能。`, `feature "${feature}" has no slot — nothing to tear down. See \`worktree-bay ls\`.`));
|
|
12
13
|
const all = scanOccupancy(cfg).get(slot) ?? [];
|
|
13
14
|
return service ? all.filter((o) => o.service === service) : all;
|
|
14
15
|
}
|
|
@@ -19,7 +20,7 @@ export async function rmCommand(cfg, feature, service, force) {
|
|
|
19
20
|
const repo = repoPath(cfg, o.service);
|
|
20
21
|
const branch = currentBranch(o.dir);
|
|
21
22
|
if (!force && (isDirty(o.dir) || hasUnpushed(repo, branch))) {
|
|
22
|
-
warn(`跳过 ${o.service}
|
|
23
|
+
warn(t(`跳过 ${o.service}:有未提交或未推送的改动。先提交/推送,或加 -f 强制删除(会丢这些改动)。`, `skipped ${o.service}: it has uncommitted or unpushed changes. Commit/push first, or pass -f to force-remove (discards them).`));
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
25
26
|
const sp = cfg.services[o.service];
|
|
@@ -28,14 +29,14 @@ export async function rmCommand(cfg, feature, service, force) {
|
|
|
28
29
|
runShell(renderTemplate(sp.teardown, vars), { cwd: repo });
|
|
29
30
|
}
|
|
30
31
|
removeWorktree(repo, o.dir, force);
|
|
31
|
-
log(`✓ 移除 ${o.service}`);
|
|
32
|
+
log(t(`✓ 移除 ${o.service}`, `✓ removed ${o.service}`));
|
|
32
33
|
removed++;
|
|
33
34
|
}
|
|
34
35
|
const slot = slotOfFeature(cfg, feature);
|
|
35
36
|
if (!service && (scanOccupancy(cfg).get(slot) ?? []).length === 0) {
|
|
36
37
|
removeLabel(cfg, slot);
|
|
37
38
|
if (removed === 0)
|
|
38
|
-
log(`✓ 释放空槽预约 "${feature}"(槽 ${slot}
|
|
39
|
+
log(t(`✓ 释放空槽预约 "${feature}"(槽 ${slot})`, `✓ released empty slot reservation "${feature}" (slot ${slot})`));
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { t } from './i18n.js';
|
|
3
4
|
function refs(tpl) { return [...tpl.matchAll(/\{(\w+)\}/g)].map((m) => m[1]); }
|
|
4
5
|
export function parseConfig(configPath) {
|
|
5
6
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
6
7
|
for (const k of ['workspaceRoot', 'maxSlots'])
|
|
7
8
|
if (raw[k] === undefined)
|
|
8
|
-
throw new Error(`config: ${k} required`);
|
|
9
|
+
throw new Error(t(`config: 缺少必填字段 ${k}`, `config: ${k} required`));
|
|
9
10
|
const maxSlots = raw.maxSlots;
|
|
10
11
|
const services = raw.services ?? {};
|
|
11
12
|
// 端口段:每个服务 [port, port+maxSlots](port=主 dev/槽0,槽 1..maxSlots 落在段内)
|
|
12
13
|
for (const [name, sp] of Object.entries(services)) {
|
|
13
14
|
if (typeof sp.port !== 'number' || sp.port < 1)
|
|
14
|
-
throw new Error(`config: ${name}.port must be a positive number`); // V2
|
|
15
|
+
throw new Error(t(`config: ${name}.port 必须是正整数`, `config: ${name}.port must be a positive number`)); // V2
|
|
15
16
|
const repoDir = path.join(raw.workspaceRoot, sp.repo ?? name);
|
|
16
17
|
if (!fs.existsSync(repoDir))
|
|
17
|
-
throw new Error(`config: ${name}.repo dir missing: ${repoDir}`); // V5
|
|
18
|
+
throw new Error(t(`config: ${name}.repo 目录不存在: ${repoDir}(检查 workspaceRoot 与 repo 名,或先 git clone)`, `config: ${name}.repo dir missing: ${repoDir} (check workspaceRoot and the repo name, or clone it first)`)); // V5
|
|
18
19
|
}
|
|
19
20
|
const entries = Object.entries(services);
|
|
20
21
|
for (let i = 0; i < entries.length; i++)
|
|
21
22
|
for (let j = i + 1; j < entries.length; j++) { // V1: 段不重叠
|
|
22
23
|
if (Math.abs(entries[i][1].port - entries[j][1].port) <= maxSlots)
|
|
23
|
-
throw new Error(`config: 服务 ${entries[i][0]} 与 ${entries[j][0]}
|
|
24
|
+
throw new Error(t(`config: 服务 ${entries[i][0]} 与 ${entries[j][0]} 端口段重叠(两服务 port 间距需 > maxSlots=${maxSlots},请拉开端口基址)`, `config: services ${entries[i][0]} and ${entries[j][0]} have overlapping port segments (|portA-portB| must be > maxSlots=${maxSlots}; spread the base ports apart)`));
|
|
24
25
|
}
|
|
25
26
|
for (const [name, sp] of Object.entries(services))
|
|
26
27
|
if (sp.upstream && !services[sp.upstream.service])
|
|
27
|
-
throw new Error(`config: ${name}.upstream.service '${sp.upstream.service}' not found`); // V3
|
|
28
|
+
throw new Error(t(`config: ${name}.upstream.service '${sp.upstream.service}' 不存在于 services(写成已声明的服务名)`, `config: ${name}.upstream.service '${sp.upstream.service}' not found in services (use a declared service name)`)); // V3
|
|
28
29
|
const known = new Set(['slot', 'port', 'slug', 'worktree', 'repo', 'upstreamBase', 'cmd']); // V4
|
|
29
30
|
for (const sp of Object.values(services))
|
|
30
31
|
for (const v of Object.values(sp.vars ?? {}))
|
|
31
32
|
for (const ref of refs(v))
|
|
32
33
|
if (!known.has(ref) && !(sp.vars && ref in sp.vars))
|
|
33
|
-
throw new Error(`config: unknown template var {${ref}}`);
|
|
34
|
+
throw new Error(t(`config: 未知模板变量 {${ref}}(只能引用内置变量或本服务 vars 里已声明的)`, `config: unknown template var {${ref}} (only built-in vars or this service's declared vars are allowed)`));
|
|
34
35
|
return { workspaceRoot: raw.workspaceRoot, maxSlots, services, configDir: path.dirname(configPath) };
|
|
35
36
|
}
|
|
36
37
|
export function loadConfig(startDir) {
|
|
@@ -43,14 +44,14 @@ export function loadConfig(startDir) {
|
|
|
43
44
|
return parseConfig(p);
|
|
44
45
|
const parent = path.dirname(dir);
|
|
45
46
|
if (parent === dir)
|
|
46
|
-
throw new Error('worktree-bay.config.json not found');
|
|
47
|
+
throw new Error(t('未找到 worktree-bay.config.json。请在工作区根目录运行 `worktree-bay init` 生成,或用环境变量 WORKTREE_BAY_CONFIG 指定其绝对路径。', 'worktree-bay.config.json not found. Run `worktree-bay init` in your workspace root to create one, or set WORKTREE_BAY_CONFIG to its absolute path.'));
|
|
47
48
|
dir = parent;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export function repoPath(cfg, service) {
|
|
51
52
|
const sp = cfg.services[service];
|
|
52
53
|
if (!sp)
|
|
53
|
-
throw new Error(
|
|
54
|
+
throw new Error(t(`未知服务「${service}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${service}". Run \`worktree-bay doctor\` to see configured services.`));
|
|
54
55
|
return path.join(cfg.workspaceRoot, sp.repo ?? service);
|
|
55
56
|
}
|
|
56
57
|
export function renderTemplate(tpl, vars) {
|
package/dist/engine.js
CHANGED
|
@@ -6,6 +6,7 @@ import { scanOccupancy } from './slots.js';
|
|
|
6
6
|
import { addWorktree } from './git.js';
|
|
7
7
|
import { runShell, run, spliceArgv, isTTY } from './util/exec.js';
|
|
8
8
|
import { warn, log } from './util/log.js';
|
|
9
|
+
import { t } from './i18n.js';
|
|
9
10
|
export function mergeEnvText(text, kv) {
|
|
10
11
|
const lines = text.split('\n');
|
|
11
12
|
const seen = new Set();
|
|
@@ -39,7 +40,7 @@ export function buildVars(cfg, ctx) {
|
|
|
39
40
|
export async function bringUp(ctx, base, branch) {
|
|
40
41
|
const { sp, dir, repo, vars } = ctx;
|
|
41
42
|
if (!(await isPortFree(Number(vars.port))))
|
|
42
|
-
throw new Error(`port ${vars.port}
|
|
43
|
+
throw new Error(t(`端口 ${vars.port} 已被占用。先停掉占用它的进程,或用 \`worktree-bay gc\`/\`worktree-bay down <功能>\` 释放其它槽后重试。`, `port ${vars.port} is already in use. Stop whatever is using it, or free a slot with \`worktree-bay gc\`/\`worktree-bay down <feature>\`, then retry.`));
|
|
43
44
|
addWorktree(repo, dir, branch, base);
|
|
44
45
|
for (const rel of sp.copy ?? []) {
|
|
45
46
|
// dereference: vendor/node_modules 含符号链接,Windows 下原样复制符号链接会失败,跟随并拷目标内容
|
|
@@ -47,7 +48,7 @@ export async function bringUp(ctx, base, branch) {
|
|
|
47
48
|
for (const lock of ['composer.lock', 'pnpm-lock.yaml', 'package-lock.json']) {
|
|
48
49
|
const a = path.join(repo, lock), b = path.join(dir, lock);
|
|
49
50
|
if (fs.existsSync(a) && fs.existsSync(b) && fs.readFileSync(a, 'utf8') !== fs.readFileSync(b, 'utf8'))
|
|
50
|
-
warn(`⚠ ${lock} 与主 checkout
|
|
51
|
+
warn(t(`⚠ ${lock} 与主 checkout 不一致,拷来的依赖可能版本错位;建议把该服务的 copy 去掉、改用 setup 跑安装命令。`, `⚠ ${lock} differs from the main checkout; copied dependencies may be the wrong version. Consider dropping copy for this service and installing via setup instead.`));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
for (const [file, kv] of Object.entries(sp.env ?? {})) {
|
|
@@ -61,10 +62,10 @@ export async function bringUp(ctx, base, branch) {
|
|
|
61
62
|
if (sp.setup) {
|
|
62
63
|
const r = runShell(renderTemplate(sp.setup, vars), { cwd: dir });
|
|
63
64
|
if (r.code !== 0)
|
|
64
|
-
throw new Error(
|
|
65
|
+
throw new Error(t(`setup 命令失败(退出码 ${r.code})。查看上面的输出排查;修好后可重跑 add(已建的 worktree 会被复用,不会重复创建)。`, `setup command failed (exit code ${r.code}). Check the output above; after fixing, re-run add (the existing worktree is reused, not recreated).`));
|
|
65
66
|
}
|
|
66
67
|
if (sp.start)
|
|
67
|
-
log(` 启动: (cd ${dir} && ${renderTemplate(sp.start, vars)})`);
|
|
68
|
+
log(t(` 启动: (cd ${dir} && ${renderTemplate(sp.start, vars)})`, ` start: (cd ${dir} && ${renderTemplate(sp.start, vars)})`));
|
|
68
69
|
}
|
|
69
70
|
export function execArgv(ctx, cmd) {
|
|
70
71
|
const tpl = (ctx.sp.exec ?? ['sh', '-c', '{cmd...}']).map((el) => el === '{cmd...}' ? el : renderTemplate(el, ctx.vars));
|
package/dist/git.js
CHANGED
|
@@ -3,7 +3,16 @@ import fs from 'node:fs';
|
|
|
3
3
|
function git(repo, ...a) { return spawnSync('git', ['-C', repo, ...a], { encoding: 'utf8' }); }
|
|
4
4
|
function ok(repo, ...a) { const r = git(repo, ...a); if (r.status !== 0)
|
|
5
5
|
throw new Error(`git ${a.join(' ')}: ${r.stderr || r.stdout}`); return r.stdout; }
|
|
6
|
-
export function
|
|
6
|
+
export function branchExists(repo, branch) {
|
|
7
|
+
return git(repo, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`).status === 0;
|
|
8
|
+
}
|
|
9
|
+
export function addWorktree(repo, dir, branch, base) {
|
|
10
|
+
// 分支已存在(如上次 worktree 删了但分支留着)→ 直接挂出复用,不用 -b 重建(否则 git 会报 "branch already exists")
|
|
11
|
+
if (branchExists(repo, branch))
|
|
12
|
+
ok(repo, 'worktree', 'add', dir, branch);
|
|
13
|
+
else
|
|
14
|
+
ok(repo, 'worktree', 'add', '-b', branch, dir, base);
|
|
15
|
+
}
|
|
7
16
|
export function removeWorktree(repo, dir, force) {
|
|
8
17
|
const a = ['worktree', 'remove', dir];
|
|
9
18
|
if (force)
|
package/dist/i18n.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 识别顺序:WORKTREE_BAY_LANG 覆盖 → POSIX locale 环境变量 → Intl(OS 区域) → 回落中文。
|
|
2
|
+
// 规则:命中 zh* → zh;有明确的非中文 locale → en;完全识别不出 → zh(用户选定的回落)。
|
|
3
|
+
export function detectLang() {
|
|
4
|
+
const envLocale = process.env.WORKTREE_BAY_LANG || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
|
|
5
|
+
if (/^zh/i.test(envLocale))
|
|
6
|
+
return 'zh';
|
|
7
|
+
if (envLocale.trim())
|
|
8
|
+
return 'en';
|
|
9
|
+
let osLocale = '';
|
|
10
|
+
try {
|
|
11
|
+
osLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
12
|
+
}
|
|
13
|
+
catch { /* 某些精简运行时无 Intl */ }
|
|
14
|
+
if (/^zh/i.test(osLocale))
|
|
15
|
+
return 'zh';
|
|
16
|
+
if (osLocale.trim())
|
|
17
|
+
return 'en';
|
|
18
|
+
return 'zh';
|
|
19
|
+
}
|
|
20
|
+
// 取中/英文案。两种语言都就地给出,运行时按 detectLang() 选。
|
|
21
|
+
export function t(zh, en) {
|
|
22
|
+
return detectLang() === 'zh' ? zh : en;
|
|
23
|
+
}
|
package/dist/lock.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { t } from './i18n.js';
|
|
3
4
|
export async function withLock(ws, fn) {
|
|
4
5
|
const lockDir = path.join(ws, '.worktree-bay', 'lock');
|
|
5
6
|
fs.mkdirSync(path.join(ws, '.worktree-bay'), { recursive: true });
|
|
@@ -11,7 +12,7 @@ export async function withLock(ws, fn) {
|
|
|
11
12
|
}
|
|
12
13
|
catch {
|
|
13
14
|
if (Date.now() - start > 30000)
|
|
14
|
-
throw new Error('worktree-bay
|
|
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.'));
|
|
15
16
|
await new Promise((r) => setTimeout(r, 50));
|
|
16
17
|
}
|
|
17
18
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -12,24 +12,31 @@ export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开
|
|
|
12
12
|
核心模型:每个服务有自己的端口段(基址 = 主 dev/槽0);一个功能占一个「槽位 N」,用到哪些服务就在哪些服务上各开一个 git worktree 挂进这个槽,该服务端口 = 基址 + N,自动错开,前端自动连到同槽的后端。
|
|
13
13
|
|
|
14
14
|
推荐工作流:
|
|
15
|
+
0. 摸清工作区(首次或拿不准时):worktree_bay_doctor 列出全部服务及其仓、校验 git/配置/各仓是否就绪——这也是你获知「有哪些服务名可传给 up/add」的途径。
|
|
15
16
|
1. 起新功能:调用 worktree_bay_up,传功能名 + 要改的服务列表(如 ["api","lms"])。它会自动占槽、为每个服务开 worktree、拷依赖、注入端口并起服务。
|
|
16
17
|
2. 定位代码:用 worktree_bay_path 拿某功能某服务的 worktree 绝对路径,进去改代码;或 worktree_bay_ls(JSON,含各 worktree 路径)总览全局。
|
|
17
18
|
3. 在某功能的某服务里跑测试/命令:worktree_bay_run(name 用配置里定义的,如 "test")。
|
|
18
|
-
4. 收尾:分支合并后,先 worktree_bay_gc 看可回收项,再 worktree_bay_down
|
|
19
|
+
4. 收尾:分支合并后,先 worktree_bay_gc 看可回收项,再 worktree_bay_down 拆掉该功能(只传 feature=拆整功能;带 service=只拆某个服务)。
|
|
19
20
|
|
|
20
21
|
要点:
|
|
21
22
|
- 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/path/run/down。
|
|
22
|
-
-
|
|
23
|
+
- 只起这个功能「实际要改」的服务,不要全起。不知道有哪些服务名时先调 worktree_bay_doctor。
|
|
23
24
|
- 拿不准当前状态时先调 worktree_bay_ls。
|
|
24
25
|
- worktree_bay_gc 默认只读(dry-run 列出建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,安全保守、不会误删未完成的工作。
|
|
26
|
+
- worktree_bay_init 可在新工作区生成配置骨架(已存在则不覆盖);worktree_bay_claim 只占槽并打印各服务端口、不建 worktree(一般直接用 up 即可)。
|
|
25
27
|
- 要写或修改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节时,先调用 worktree_bay_skill 获取完整的使用与配置指南(含每个配置原语、模板变量、校验规则与完整示例)。`;
|
|
26
28
|
const str = { type: 'string' };
|
|
27
29
|
export const TOOLS = [
|
|
30
|
+
{ name: 'worktree_bay_doctor', description: '体检并列出工作区全部服务及其仓目录、校验 git/配置/各仓是否就绪。起步前先调它,也是获知「有哪些服务名可传给 up/add」的途径。',
|
|
31
|
+
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['doctor'] },
|
|
28
32
|
{ name: 'worktree_bay_ls', description: '列出所有功能槽位与占用(JSON:每槽的功能名、已起服务及端口、各 worktree 绝对路径),用于总览当前并行开发状态',
|
|
29
33
|
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls', '--json'] },
|
|
30
34
|
{ name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。',
|
|
31
35
|
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature', 'services'] },
|
|
32
36
|
toArgs: (a) => ['up', String(a.feature), ...(a.services ?? [])] },
|
|
37
|
+
{ name: 'worktree_bay_claim', description: '只为功能占一个槽位并打印各服务在该槽的端口,不开 worktree(一般直接用 up 即可;需要先预览端口/预约槽时用)',
|
|
38
|
+
inputSchema: { type: 'object', properties: { feature: str }, required: ['feature'] },
|
|
39
|
+
toArgs: (a) => ['claim', String(a.feature)] },
|
|
33
40
|
{ name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)',
|
|
34
41
|
inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str }, required: ['feature', 'service'] },
|
|
35
42
|
toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : [])] },
|
|
@@ -39,12 +46,14 @@ export const TOOLS = [
|
|
|
39
46
|
{ name: 'worktree_bay_run', description: '在某功能某服务的运行体里跑预设命令(如 test),可透传额外参数',
|
|
40
47
|
inputSchema: { type: 'object', properties: { feature: str, service: str, name: str, args: { type: 'array', items: str } }, required: ['feature', 'service', 'name'] },
|
|
41
48
|
toArgs: (a) => ['run', String(a.feature), String(a.service), String(a.name), ...(a.args ?? [])] },
|
|
42
|
-
{ name: 'worktree_bay_down', description: '
|
|
43
|
-
inputSchema: { type: 'object', properties: { feature: str, force: { type: 'boolean' } }, required: ['feature'] },
|
|
44
|
-
toArgs: (a) => ['
|
|
49
|
+
{ name: 'worktree_bay_down', description: '拆除功能的 worktree:省略 service 拆整个功能的所有服务,传 service 只拆该服务(默认查脏/未推保护,force=true 强删)',
|
|
50
|
+
inputSchema: { type: 'object', properties: { feature: str, service: str, force: { type: 'boolean' } }, required: ['feature'] },
|
|
51
|
+
toArgs: (a) => ['rm', String(a.feature), ...(a.service ? [String(a.service)] : []), ...(a.force ? ['-f'] : [])] },
|
|
45
52
|
{ name: 'worktree_bay_gc', description: '合并感知回收:默认 dry-run 只列建议,apply=true 才实际删除「已合并且干净」的功能',
|
|
46
53
|
inputSchema: { type: 'object', properties: { apply: { type: 'boolean' } } },
|
|
47
54
|
toArgs: (a) => ['gc', ...(a.apply ? ['--apply'] : [])] },
|
|
55
|
+
{ name: 'worktree_bay_init', description: '在当前工作区生成 worktree-bay.config.json 骨架(扫描子 git 仓预填服务);已存在则不覆盖。新工作区首次落地配置时用。',
|
|
56
|
+
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['init'] },
|
|
48
57
|
{ name: 'worktree_bay_skill', description: 'worktree-bay 完整使用与配置指南(每个命令、每个配置原语、模板变量、校验规则、完整示例)。写/改 worktree-bay.config.json 或拿不准细节时先调用它。',
|
|
49
58
|
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['skill'] },
|
|
50
59
|
];
|
package/dist/slots.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { repoPath } from './config.js';
|
|
4
4
|
import { parseWorktreeDir } from './naming.js';
|
|
5
|
+
import { t } from './i18n.js';
|
|
5
6
|
export function scanOccupancy(cfg) {
|
|
6
7
|
const map = new Map();
|
|
7
8
|
for (const service of Object.keys(cfg.services)) {
|
|
@@ -35,7 +36,7 @@ export function freeSlot(cfg) {
|
|
|
35
36
|
for (let n = 1; n <= cfg.maxSlots; n++)
|
|
36
37
|
if (!occ.has(n) && l[String(n)] === undefined)
|
|
37
38
|
return n;
|
|
38
|
-
throw new Error(`no free slot (1..${cfg.maxSlots} all taken)
|
|
39
|
+
throw new Error(t(`没有空闲槽位(1..${cfg.maxSlots} 全部占用)。用 \`worktree-bay gc\` 回收已合并的,或 \`worktree-bay down <功能>\` 拆掉用完的,或调大配置里的 maxSlots。`, `no free slot (1..${cfg.maxSlots} all taken). Reclaim merged ones with \`worktree-bay gc\`, tear down finished ones with \`worktree-bay down <feature>\`, or raise maxSlots in your config.`));
|
|
39
40
|
}
|
|
40
41
|
export function claim(cfg, f) { const e = slotOfFeature(cfg, f); if (e !== undefined)
|
|
41
42
|
return e; const n = freeSlot(cfg); writeLabel(cfg, n, f); return n; }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { t } from '../i18n.js';
|
|
2
|
+
export function friendlyParseError(rawMessage, cmd) {
|
|
3
|
+
const m = (rawMessage || '').replace(/\r?\n$/, '');
|
|
4
|
+
const grab = (re) => { const x = re.exec(m); return x ? x[1] : ''; };
|
|
5
|
+
const hint = cmd
|
|
6
|
+
? t(`\n用法: worktree-bay ${cmd.name} ${cmd.usage}\n说明: ${cmd.description}\n查看完整帮助: worktree-bay help ${cmd.name}`, `\nUsage: worktree-bay ${cmd.name} ${cmd.usage}\n${cmd.description}\nFull help: worktree-bay help ${cmd.name}`)
|
|
7
|
+
: t('\n运行 worktree-bay help 查看所有命令与用法', '\nRun: worktree-bay help');
|
|
8
|
+
if (/missing required argument/i.test(m))
|
|
9
|
+
return t(`缺少必填参数「${grab(/argument '(.+?)'/)}」。把它补在命令后面再试。${hint}`, `Missing required argument "${grab(/argument '(.+?)'/)}". Append it to the command and retry.${hint}`);
|
|
10
|
+
if (/argument missing/i.test(m))
|
|
11
|
+
return t(`选项「${grab(/option '(.+?)'/)}」缺少取值。${hint}`, `Option "${grab(/option '(.+?)'/)}" needs a value.${hint}`);
|
|
12
|
+
if (/too many arguments/i.test(m))
|
|
13
|
+
return t(`参数过多,多余的请删掉。${hint}`, `Too many arguments — remove the extra ones.${hint}`);
|
|
14
|
+
if (/unknown option/i.test(m))
|
|
15
|
+
return t(`未知选项「${grab(/unknown option '?(.+?)'?$/)}」。检查拼写或去掉它。${hint}`, `Unknown option "${grab(/unknown option '?(.+?)'?$/)}" — check spelling or drop it.${hint}`);
|
|
16
|
+
if (/unknown command/i.test(m))
|
|
17
|
+
return t(`未知命令「${grab(/unknown command '(.+?)'/)}」。运行 worktree-bay help 查看所有命令。`, `Unknown command "${grab(/unknown command '(.+?)'/)}". Run: worktree-bay help`);
|
|
18
|
+
return m.replace(/^error:\s*/i, '');
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
package/skill.md
CHANGED
|
@@ -18,6 +18,8 @@ npm i -g worktree-bay # 需要 Node >= 20
|
|
|
18
18
|
worktree-bay completion install # 一键装 shell 补全(可选)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
> **输出语言**:按系统区域(locale / `LANG` / `LC_*`)自动中英切换,识别不出时默认中文。可用 `WORKTREE_BAY_LANG=zh|en` 强制。错误提示均为自然语言并附「应该怎么做」的建议。
|
|
22
|
+
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
## 命令大全
|
|
@@ -152,7 +154,7 @@ worktree-bay gc # 回收已合并的
|
|
|
152
154
|
|
|
153
155
|
## 给 AI(MCP)
|
|
154
156
|
|
|
155
|
-
`worktree-bay mcp` 暴露工具:`
|
|
157
|
+
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / down / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知有哪些服务名可用);`ls` 以 JSON 返回(含各 worktree 绝对路径);`path` 直接给某功能某服务的 worktree 目录;`down` 省略 service 拆整功能、带 service 只拆该服务。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
|
|
156
158
|
|
|
157
159
|
## 常见坑
|
|
158
160
|
|