worktree-bay 2.1.0 → 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 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`)。暴露的工具:`worktree_bay_up / ls / add / path / run / down / gc / skill`(`ls` 以 JSON 返回各 worktree 路径,`path` 直接给某功能某服务的目录,`skill` 取完整指南)。
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,26 +113,40 @@ 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);
133
+ let cfg = null;
134
+ try {
135
+ cfg = loadConfig(process.cwd());
136
+ }
137
+ catch { /* 无配置:仍补全子命令,只是补不了 feature/service */ }
122
138
  try {
123
- console.log(complete(loadConfig(process.cwd()), words).join('\n'));
139
+ console.log(complete(cfg, words).join('\n'));
124
140
  }
125
141
  catch { /* 静默 */ }
126
142
  });
127
- program.parseAsync(process.argv);
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));
@@ -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('unknown service: ' + service);
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
- await bringUp(ctx, base ?? `origin/${mainBranch(p.repo)}`, br);
24
- log(`✓ ${service} 挂入 "${feature}"(槽 ${p.slot},端口 ${ctx.vars.port},分支 ${br})`);
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 自动 + 各服务默认分支)
@@ -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,12 +3,15 @@ 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) {
9
10
  const prev = words.slice(1);
10
11
  if (prev.length === 0)
11
12
  return SUBCMDS;
13
+ if (!cfg)
14
+ return []; // 无配置(不在工作区内):子命令已补全,feature/service 无从读取
12
15
  const sub = prev[0];
13
16
  const pos = prev.length;
14
17
  const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh', 'path'];
@@ -24,8 +27,9 @@ export function completionScript(shell) {
24
27
  // 脚本只传"光标前已完成的词",不含当前正在补的词,与 complete() 的模型一致
25
28
  if (shell === 'bash')
26
29
  return `_worktree_bay(){ COMPREPLY=( $(worktree-bay __complete -- "\${COMP_WORDS[@]:0:\$COMP_CWORD}") ); }\ncomplete -F _worktree_bay worktree-bay`;
30
+ // zsh 默认不对 $(...) 做单词分割,必须用 ${(f)...} 按行拆成多个候选,否则多行输出会变成单个候选
27
31
  if (shell === 'zsh')
28
- return `#compdef worktree-bay\n_worktree_bay(){ compadd -- $(worktree-bay __complete -- "\${(@)words[1,CURRENT-1]}") }\ncompdef _worktree_bay worktree-bay`;
32
+ return `#compdef worktree-bay\n_worktree_bay(){ compadd -- \${(f)"$(worktree-bay __complete -- "\${(@)words[1,CURRENT-1]}")"} }\ncompdef _worktree_bay worktree-bay`;
29
33
  if (shell === 'fish')
30
34
  return `complete -c worktree-bay -f -a '(worktree-bay __complete -- (commandline -opc))'`;
31
35
  throw new Error('unsupported shell: ' + shell);
@@ -39,7 +43,7 @@ export function installCompletion(shell) {
39
43
  fs.mkdirSync(dir, { recursive: true });
40
44
  const file = path.join(dir, 'worktree-bay.fish');
41
45
  fs.writeFileSync(file, completionScript('fish') + '\n');
42
- log(`✓ fish 补全已写入 ${file}(新开 fish 即生效)`);
46
+ log(t(`✓ fish 补全已写入 ${file}(新开 fish 即生效)`, `✓ fish completion written to ${file} (effective in a new fish session)`));
43
47
  return;
44
48
  }
45
49
  const isZsh = sh === 'zsh';
@@ -47,9 +51,9 @@ export function installCompletion(shell) {
47
51
  const line = `eval "$(worktree-bay completion ${isZsh ? 'zsh' : 'bash'})"`;
48
52
  const cur = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
49
53
  if (cur.includes(line)) {
50
- log(`✓ 补全已在 ${rc},无需重复安装`);
54
+ log(t(`✓ 补全已在 ${rc},无需重复安装`, `✓ completion already in ${rc}, nothing to do`));
51
55
  return;
52
56
  }
53
57
  fs.appendFileSync(rc, `\n# worktree-bay completion\n${line}\n`);
54
- log(`✓ 补全已加入 ${rc},执行 'source ${rc}' 或重开终端即生效`);
58
+ log(t(`✓ 补全已加入 ${rc},执行 'source ${rc}' 或重开终端即生效`, `✓ completion added to ${rc}; run 'source ${rc}' or reopen your terminal to enable it`));
55
59
  }
@@ -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(`服务 ${name} → ${repo}`);
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)
@@ -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} (slot ${slot}) 已合并且干净 → 移除`);
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} (slot ${slot}) 远端分支已删(疑似 squash)→ 确认后 bay rm`);
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})空预约(无 worktree)`);
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 执行 auto-remove 与空预约清理)');
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
  }
@@ -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
  }
@@ -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('unknown feature: ' + feature);
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(`${service} not in ${feature}`);
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(`run.${name} 未定义于 ${service}`);
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
  }
@@ -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('unknown feature: ' + feature);
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}:有未提交/未推改动(-f 强删)`);
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]} 端口段重叠(间距需 > maxSlots=${maxSlots})`);
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('unknown service: ' + service);
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} 被占用(Codex#11)`);
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 不一致,拷来依赖可能版本错位,建议改跑安装(Codex#18)`);
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('setup 失败');
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
@@ -1,11 +1,29 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  function git(repo, ...a) { return spawnSync('git', ['-C', repo, ...a], { encoding: 'utf8' }); }
3
4
  function ok(repo, ...a) { const r = git(repo, ...a); if (r.status !== 0)
4
5
  throw new Error(`git ${a.join(' ')}: ${r.stderr || r.stdout}`); return r.stdout; }
5
- export function addWorktree(repo, dir, branch, base) { ok(repo, 'worktree', 'add', '-b', branch, dir, base); }
6
- export function removeWorktree(repo, dir, force) { const a = ['worktree', 'remove', dir]; if (force)
7
- a.push('--force'); const r = git(repo, ...a); if (r.status !== 0)
8
- throw new Error('worktree remove: ' + (r.stderr || r.stdout)); git(repo, 'worktree', 'prune'); }
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
+ }
16
+ export function removeWorktree(repo, dir, force) {
17
+ const a = ['worktree', 'remove', dir];
18
+ if (force)
19
+ a.push('--force');
20
+ git(repo, ...a); // 尽力移除;残留交给下面兜底(调用方已先过脏/未推保护,到这里删除是安全的)
21
+ // git worktree remove 不会删被忽略的文件(前端 node_modules 等)→ 目录残留报 "Directory not empty";
22
+ // 它还可能先摘了登记再删目录失败,留下「git 不认但磁盘还在」的孤儿。统一兜底:物理删目录 + prune 同步元数据。
23
+ if (fs.existsSync(dir))
24
+ fs.rmSync(dir, { recursive: true, force: true });
25
+ git(repo, 'worktree', 'prune');
26
+ }
9
27
  export function isDirty(dir) { return ok(dir, 'status', '--porcelain').trim().length > 0; }
10
28
  export function currentBranch(dir) { return ok(dir, 'rev-parse', '--abbrev-ref', 'HEAD').trim(); }
11
29
  export function mainBranch(repo) {
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: lock timeout (另一个 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: '拆除整个功能的所有服务 worktree(默认查脏/未推保护,force=true 强删)',
43
- inputSchema: { type: 'object', properties: { feature: str, force: { type: 'boolean' } }, required: ['feature'] },
44
- toArgs: (a) => ['down', String(a.feature), ...(a.force ? ['-f'] : [])] },
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.1.0",
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",
@@ -54,6 +54,6 @@
54
54
  "@types/node": "^22.0.0",
55
55
  "tsx": "^4.19.0",
56
56
  "typescript": "^5.6.0",
57
- "vitest": "^2.1.0"
57
+ "vitest": "^4.1.0"
58
58
  }
59
59
  }
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` 暴露工具:`worktree_bay_up / ls / add / path / run / down / gc / skill`。`ls` 以 JSON 返回(含各 worktree 绝对路径),`path` 直接给某功能某服务的 worktree 目录——拿到路径后即可进去改代码。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
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