worktree-bay 1.0.3 → 1.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 +30 -10
- package/dist/cli.js +31 -6
- package/dist/commands/add.js +9 -3
- package/dist/commands/completion.js +39 -9
- package/dist/mcp.js +56 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -27,12 +27,8 @@ npm i -g worktree-bay
|
|
|
27
27
|
## 快速上手
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
#
|
|
31
|
-
worktree-bay
|
|
32
|
-
|
|
33
|
-
# 把服务挂进这个功能的槽(开 worktree + 跑该服务的步骤)
|
|
34
|
-
worktree-bay add drill-fix api feature/drill-fix # api 起在 6011
|
|
35
|
-
worktree-bay add drill-fix lms feature/drill-ui # 前端自动接 6011
|
|
30
|
+
# 一条命令起整个功能:自动占槽 + 在 api/lms 上开 worktree(分支默认 = 功能名)
|
|
31
|
+
worktree-bay up drill-fix api lms
|
|
36
32
|
|
|
37
33
|
# 看占用
|
|
38
34
|
worktree-bay ls
|
|
@@ -40,11 +36,15 @@ worktree-bay ls
|
|
|
40
36
|
# 在服务运行体里跑命令(透传)
|
|
41
37
|
worktree-bay run drill-fix api test
|
|
42
38
|
|
|
43
|
-
#
|
|
44
|
-
worktree-bay
|
|
45
|
-
|
|
39
|
+
# 拆除整个功能(默认查脏/未推保护,-f 强删)
|
|
40
|
+
worktree-bay down drill-fix
|
|
41
|
+
|
|
42
|
+
# 回收已合并的(默认 dry-run,--apply 实际执行)
|
|
43
|
+
worktree-bay gc
|
|
46
44
|
```
|
|
47
45
|
|
|
46
|
+
> 需要更细的控制:`claim <feature>` 单独占槽、`add <feature> <service> [branch]` 单加一个服务(branch 可自定义,省略则用功能名)、`rm <feature> [service]` 拆单个服务。
|
|
47
|
+
|
|
48
48
|
## 配置
|
|
49
49
|
|
|
50
50
|
在工作区根放一份 `worktree-bay.config.json`,集中声明所有服务。工具运行时自下而上查找它(或用环境变量 `WORKTREE_BAY_CONFIG` 指定)。
|
|
@@ -106,10 +106,30 @@ worktree-bay gc # 合并感知回收(默认 dry-run,--app
|
|
|
106
106
|
|
|
107
107
|
## Shell 补全
|
|
108
108
|
|
|
109
|
+
一条命令装好(自动探测 shell、幂等写入对应 rc;fish 直接写补全目录):
|
|
110
|
+
|
|
109
111
|
```bash
|
|
110
|
-
worktree-bay completion
|
|
112
|
+
worktree-bay completion install
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
执行 `source ~/.bashrc`(或重开终端)即可 tab 补全子命令、功能名、服务名。也可手动:`worktree-bay completion bash`(打印脚本,自行接入)。
|
|
116
|
+
|
|
117
|
+
## MCP(让 AI 直接用)
|
|
118
|
+
|
|
119
|
+
内置一个 MCP 服务,让 AI(Claude Code 等)通过 MCP 调用 worktree-bay 完成并行开发,并内置工作流指导(告诉 AI 何时用 up/ls/run/down/gc)。
|
|
120
|
+
|
|
121
|
+
启动:`worktree-bay mcp`(stdio)。在 Claude Code 里注册:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"worktree-bay": { "command": "worktree-bay", "args": ["mcp"] }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
111
129
|
```
|
|
112
130
|
|
|
131
|
+
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_up / ls / add / run / down / gc`。
|
|
132
|
+
|
|
113
133
|
## 许可证
|
|
114
134
|
|
|
115
135
|
[MIT](./LICENSE)
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,12 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
5
|
import { claimCommand } from './commands/claim.js';
|
|
6
6
|
import { lsCommand } from './commands/ls.js';
|
|
7
|
-
import { addCommand } from './commands/add.js';
|
|
7
|
+
import { addCommand, upCommand } from './commands/add.js';
|
|
8
8
|
import { runCommand, shCommand } from './commands/passthrough.js';
|
|
9
9
|
import { rmCommand } from './commands/rm.js';
|
|
10
10
|
import { gcCommand } from './commands/gc.js';
|
|
11
|
-
import { complete, completionCommand } from './commands/completion.js';
|
|
11
|
+
import { complete, completionCommand, installCompletion } from './commands/completion.js';
|
|
12
|
+
import { startMcp } from './mcp.js';
|
|
12
13
|
import { die } from './util/log.js';
|
|
13
14
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
15
|
const program = new Command();
|
|
@@ -28,7 +29,14 @@ catch (e) {
|
|
|
28
29
|
} });
|
|
29
30
|
program.command('ls').description('列出所有槽位与占用状态')
|
|
30
31
|
.action(() => sync(lsCommand));
|
|
31
|
-
program.command('
|
|
32
|
+
program.command('up <feature> <services...>').description('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)')
|
|
33
|
+
.action(async (f, services) => { try {
|
|
34
|
+
await upCommand(loadConfig(process.cwd()), f, services);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
die(e.message);
|
|
38
|
+
} });
|
|
39
|
+
program.command('add <feature> <service> [branch] [base]').description('为功能在某服务开 worktree(branch 默认 = 功能名)')
|
|
32
40
|
.action(async (f, s, b, base) => { try {
|
|
33
41
|
await addCommand(loadConfig(process.cwd()), f, s, b, base);
|
|
34
42
|
}
|
|
@@ -39,6 +47,13 @@ program.command('run <feature> <service> <name> [args...]').description('在服
|
|
|
39
47
|
.action((f, s, n, args) => sync((c) => runCommand(c, f, s, n, args ?? [])));
|
|
40
48
|
program.command('sh <feature> <service>').description('进入服务运行体的 shell')
|
|
41
49
|
.action((f, s) => sync((c) => shCommand(c, f, s)));
|
|
50
|
+
program.command('down <feature>').description('拆除整个功能的所有服务 worktree(= rm <feature>)').option('-f, --force', '跳过脏/未推检查强制删除')
|
|
51
|
+
.action(async (f, o) => { try {
|
|
52
|
+
await rmCommand(loadConfig(process.cwd()), f, undefined, !!o.force);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
die(e.message);
|
|
56
|
+
} });
|
|
42
57
|
program.command('rm <feature> [service]').description('拆除某服务或整槽的 worktree(默认查脏/未推保护)').option('-f, --force', '跳过脏/未推检查强制删除')
|
|
43
58
|
.action(async (f, s, o) => { try {
|
|
44
59
|
await rmCommand(loadConfig(process.cwd()), f, s, !!o.force);
|
|
@@ -53,9 +68,19 @@ program.command('gc').description('合并感知回收(默认 dry-run)').opti
|
|
|
53
68
|
catch (e) {
|
|
54
69
|
die(e.message);
|
|
55
70
|
} });
|
|
56
|
-
program.command('completion <shell
|
|
57
|
-
.action((
|
|
58
|
-
|
|
71
|
+
program.command('completion <target> [shell]').description('install 一键装进 shell;或 bash|zsh|fish 打印补全脚本')
|
|
72
|
+
.action((target, shell) => { try {
|
|
73
|
+
if (target === 'install')
|
|
74
|
+
installCompletion(shell);
|
|
75
|
+
else
|
|
76
|
+
completionCommand(target);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
die(e.message);
|
|
80
|
+
} });
|
|
81
|
+
program.command('mcp').description('启动 MCP 服务(stdio),供 AI 通过 MCP 调用 worktree-bay 完成并行开发')
|
|
82
|
+
.action(async () => { try {
|
|
83
|
+
await startMcp();
|
|
59
84
|
}
|
|
60
85
|
catch (e) {
|
|
61
86
|
die(e.message);
|
package/dist/commands/add.js
CHANGED
|
@@ -14,12 +14,18 @@ export function resolveAdd(cfg, feature, service, branch) {
|
|
|
14
14
|
return { service, slot, slug, dir: path.join(repoPath(cfg, service), '.worktrees', slug), repo: repoPath(cfg, service) };
|
|
15
15
|
}
|
|
16
16
|
export async function addCommand(cfg, feature, service, branch, base) {
|
|
17
|
+
const br = branch || feature; // 默认分支 = 功能名
|
|
17
18
|
await withLock(cfg.workspaceRoot, async () => {
|
|
18
|
-
const p = resolveAdd(cfg, feature, service,
|
|
19
|
+
const p = resolveAdd(cfg, feature, service, br);
|
|
19
20
|
const sp = cfg.services[service];
|
|
20
21
|
const ctxBase = { cfg, service, sp, slot: p.slot, slug: p.slug, dir: p.dir, repo: p.repo };
|
|
21
22
|
const ctx = { ...ctxBase, vars: buildVars(cfg, ctxBase) };
|
|
22
|
-
await bringUp(ctx, base ?? `origin/${mainBranch(p.repo)}`,
|
|
23
|
-
log(`✓ ${service} 挂入 "${feature}"(槽 ${p.slot},端口 ${ctx.vars.port})`);
|
|
23
|
+
await bringUp(ctx, base ?? `origin/${mainBranch(p.repo)}`, br);
|
|
24
|
+
log(`✓ ${service} 挂入 "${feature}"(槽 ${p.slot},端口 ${ctx.vars.port},分支 ${br})`);
|
|
24
25
|
});
|
|
25
26
|
}
|
|
27
|
+
// up: 一条命令为功能批量起多个服务(claim 自动 + 各服务默认分支)
|
|
28
|
+
export async function upCommand(cfg, feature, services, base) {
|
|
29
|
+
for (const service of services)
|
|
30
|
+
await addCommand(cfg, feature, service, undefined, base);
|
|
31
|
+
}
|
|
@@ -1,25 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { readLabels } from '../slots.js';
|
|
2
5
|
import { log } from '../util/log.js';
|
|
3
|
-
const SUBCMDS = ['claim', 'add', 'ls', 'gc', 'rm', 'run', 'sh', 'completion'];
|
|
6
|
+
const SUBCMDS = ['claim', 'up', 'add', 'ls', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp'];
|
|
7
|
+
// words = 命令名 + 光标前已输入完的词(不含当前正在补的词)
|
|
4
8
|
export function complete(cfg, words) {
|
|
5
|
-
const
|
|
6
|
-
if (
|
|
9
|
+
const prev = words.slice(1);
|
|
10
|
+
if (prev.length === 0)
|
|
7
11
|
return SUBCMDS;
|
|
8
|
-
const sub =
|
|
9
|
-
const pos =
|
|
10
|
-
|
|
12
|
+
const sub = prev[0];
|
|
13
|
+
const pos = prev.length;
|
|
14
|
+
const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh'];
|
|
15
|
+
if (featureSubs.includes(sub) && pos === 1)
|
|
11
16
|
return Object.values(readLabels(cfg));
|
|
12
17
|
if (['add', 'run', 'sh'].includes(sub) && pos === 2)
|
|
13
18
|
return Object.keys(cfg.services);
|
|
19
|
+
if (sub === 'up' && pos >= 2)
|
|
20
|
+
return Object.keys(cfg.services); // up 接变长服务列表
|
|
14
21
|
return [];
|
|
15
22
|
}
|
|
16
23
|
export function completionScript(shell) {
|
|
24
|
+
// 脚本只传"光标前已完成的词",不含当前正在补的词,与 complete() 的模型一致
|
|
17
25
|
if (shell === 'bash')
|
|
18
|
-
return `_worktree_bay(){ COMPREPLY=( $(worktree-bay __complete -- "\${COMP_WORDS[@]}") ); }\ncomplete -F _worktree_bay worktree-bay`;
|
|
26
|
+
return `_worktree_bay(){ COMPREPLY=( $(worktree-bay __complete -- "\${COMP_WORDS[@]:0:\$COMP_CWORD}") ); }\ncomplete -F _worktree_bay worktree-bay`;
|
|
19
27
|
if (shell === 'zsh')
|
|
20
|
-
return `#compdef worktree-bay\n_worktree_bay(){ compadd -- $(worktree-bay __complete -- "\${words[
|
|
28
|
+
return `#compdef worktree-bay\n_worktree_bay(){ compadd -- $(worktree-bay __complete -- "\${(@)words[1,CURRENT-1]}") }\ncompdef _worktree_bay worktree-bay`;
|
|
21
29
|
if (shell === 'fish')
|
|
22
|
-
return `complete -c worktree-bay -a '(worktree-bay __complete -- (commandline -opc))'`;
|
|
30
|
+
return `complete -c worktree-bay -f -a '(worktree-bay __complete -- (commandline -opc))'`;
|
|
23
31
|
throw new Error('unsupported shell: ' + shell);
|
|
24
32
|
}
|
|
25
33
|
export function completionCommand(shell) { log(completionScript(shell)); }
|
|
34
|
+
// 一键把补全装进当前 shell(幂等)。fish 直接写补全目录(零配置生效);bash/zsh 往 rc 加一行 eval。
|
|
35
|
+
export function installCompletion(shell) {
|
|
36
|
+
const sh = shell || path.basename(process.env.SHELL || 'bash');
|
|
37
|
+
if (sh === 'fish') {
|
|
38
|
+
const dir = path.join(os.homedir(), '.config', 'fish', 'completions');
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
const file = path.join(dir, 'worktree-bay.fish');
|
|
41
|
+
fs.writeFileSync(file, completionScript('fish') + '\n');
|
|
42
|
+
log(`✓ fish 补全已写入 ${file}(新开 fish 即生效)`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const isZsh = sh === 'zsh';
|
|
46
|
+
const rc = path.join(os.homedir(), isZsh ? '.zshrc' : '.bashrc');
|
|
47
|
+
const line = `eval "$(worktree-bay completion ${isZsh ? 'zsh' : 'bash'})"`;
|
|
48
|
+
const cur = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
|
|
49
|
+
if (cur.includes(line)) {
|
|
50
|
+
log(`✓ 补全已在 ${rc},无需重复安装`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
fs.appendFileSync(rc, `\n# worktree-bay completion\n${line}\n`);
|
|
54
|
+
log(`✓ 补全已加入 ${rc},执行 'source ${rc}' 或重开终端即生效`);
|
|
55
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
9
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
10
|
+
// 工具实现:直接调底层 worktree-bay CLI 并捕获输出返回,复用全部逻辑
|
|
11
|
+
function runCli(args) {
|
|
12
|
+
const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
|
|
13
|
+
const text = [r.stdout, r.stderr].filter(Boolean).join('\n').trim() || '(无输出)';
|
|
14
|
+
return { content: [{ type: 'text', text }] };
|
|
15
|
+
}
|
|
16
|
+
export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开发编排器。当你需要在一个多服务工作区里并行开发多个功能、又不想让它们的端口/依赖/数据互相干扰时,用本服务的工具来完成开发工作。
|
|
17
|
+
|
|
18
|
+
核心模型:一个功能占一个「槽位」→ 得到一个端口块;该功能用到哪些服务,就在哪些服务上各开一个 git worktree 挂进这个槽,端口自动错开,前端自动连到同槽的后端。
|
|
19
|
+
|
|
20
|
+
推荐工作流:
|
|
21
|
+
1. 起新功能:调用 worktree_bay_up,传功能名 + 要改的服务列表(如 ["api","lms"])。它会自动占槽、为每个服务开 worktree、拷依赖、注入端口并起服务。
|
|
22
|
+
2. 查看在跑的功能:worktree_bay_ls。
|
|
23
|
+
3. 在某功能的某服务里跑测试/命令:worktree_bay_run(name 用配置里定义的,如 "test")。
|
|
24
|
+
4. 收尾:分支合并后,先 worktree_bay_gc 看可回收项,再 worktree_bay_down 拆掉该功能。
|
|
25
|
+
|
|
26
|
+
要点:
|
|
27
|
+
- 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/run/down。
|
|
28
|
+
- 只起这个功能「实际要改」的服务,不要全起。
|
|
29
|
+
- 拿不准当前状态时先调 worktree_bay_ls。
|
|
30
|
+
- worktree_bay_gc 默认只读(dry-run 列出建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,安全保守、不会误删未完成的工作。`;
|
|
31
|
+
export function createServer() {
|
|
32
|
+
const server = new McpServer({ name: 'worktree-bay', version: VERSION }, { instructions: INSTRUCTIONS });
|
|
33
|
+
server.tool('worktree_bay_ls', '列出所有功能槽位与占用(每槽:功能名、端口块、已起服务及端口、是否已并入主分支)', {}, async () => runCli(['ls']));
|
|
34
|
+
server.tool('worktree_bay_up', '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名,前端自动接同槽后端)。并行开发新功能首选。', {
|
|
35
|
+
feature: z.string().describe('功能名(同时作为默认分支名)'),
|
|
36
|
+
services: z.array(z.string()).describe('要起的服务名列表,如 ["api","lms"]'),
|
|
37
|
+
}, async ({ feature, services }) => runCli(['up', feature, ...services]));
|
|
38
|
+
server.tool('worktree_bay_add', '为功能在单个服务上开 worktree(branch 省略则用功能名)', {
|
|
39
|
+
feature: z.string(), service: z.string(), branch: z.string().optional(),
|
|
40
|
+
}, async ({ feature, service, branch }) => runCli(['add', feature, service, ...(branch ? [branch] : [])]));
|
|
41
|
+
server.tool('worktree_bay_run', '在某功能某服务的运行体里跑预设命令(如 test),可透传额外参数', {
|
|
42
|
+
feature: z.string(), service: z.string(),
|
|
43
|
+
name: z.string().describe('配置里 run.<name> 的名字,如 test'),
|
|
44
|
+
args: z.array(z.string()).optional(),
|
|
45
|
+
}, async ({ feature, service, name, args }) => runCli(['run', feature, service, name, ...(args ?? [])]));
|
|
46
|
+
server.tool('worktree_bay_down', '拆除整个功能的所有服务 worktree(默认查脏/未推保护,force=true 强删)', {
|
|
47
|
+
feature: z.string(), force: z.boolean().optional(),
|
|
48
|
+
}, async ({ feature, force }) => runCli(['down', feature, ...(force ? ['-f'] : [])]));
|
|
49
|
+
server.tool('worktree_bay_gc', '合并感知回收:默认 dry-run 只列建议,apply=true 才实际删除「已合并且干净」的功能', {
|
|
50
|
+
apply: z.boolean().optional(),
|
|
51
|
+
}, async ({ apply }) => runCli(['gc', ...(apply ? ['--apply'] : [])]));
|
|
52
|
+
return server;
|
|
53
|
+
}
|
|
54
|
+
export async function startMcp() {
|
|
55
|
+
await createServer().connect(new StdioServerTransport());
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Config-driven git worktree slot + port orchestrator for parallel multi-service development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"registry": "https://registry.npmjs.org"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
47
|
+
"commander": "^12.1.0",
|
|
48
|
+
"zod": "^3.23.8"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
51
|
"@types/node": "^22.0.0",
|