worktree-bay 1.2.0 → 1.4.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/dist/cli.js CHANGED
@@ -10,7 +10,8 @@ import { rmCommand } from './commands/rm.js';
10
10
  import { gcCommand } from './commands/gc.js';
11
11
  import { complete, completionCommand, installCompletion } from './commands/completion.js';
12
12
  import { startMcp } from './mcp.js';
13
- import { die } from './util/log.js';
13
+ import { readSkill } from './skill.js';
14
+ import { die, log } from './util/log.js';
14
15
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
15
16
  const program = new Command();
16
17
  program.name('worktree-bay').description('worktree 槽位+端口编排器').version(pkg.version);
@@ -78,9 +79,16 @@ program.command('completion <target> [shell]').description('install 一键装进
78
79
  catch (e) {
79
80
  die(e.message);
80
81
  } });
81
- program.command('mcp').description('启动 MCP 服务(stdio),供 AI 通过 MCP 调用 worktree-bay 完成并行开发')
82
- .action(async () => { try {
83
- await startMcp();
82
+ program.command('mcp').description('启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 worktree-bay')
83
+ .action(() => { try {
84
+ startMcp();
85
+ }
86
+ catch (e) {
87
+ die(e.message);
88
+ } });
89
+ program.command('skill').description('打印 worktree-bay 使用与配置完全指南')
90
+ .action(() => { try {
91
+ log(readSkill());
84
92
  }
85
93
  catch (e) {
86
94
  die(e.message);
@@ -3,7 +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
- const SUBCMDS = ['claim', 'up', 'add', 'ls', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp'];
6
+ const SUBCMDS = ['claim', 'up', 'add', 'ls', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp', 'skill'];
7
7
  // words = 命令名 + 光标前已输入完的词(不含当前正在补的词)
8
8
  export function complete(cfg, words) {
9
9
  const prev = words.slice(1);
package/dist/mcp.js CHANGED
@@ -1,18 +1,12 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { z } from 'zod';
4
1
  import { spawnSync } from 'node:child_process';
5
2
  import { fileURLToPath } from 'node:url';
6
3
  import { readFileSync } from 'node:fs';
4
+ import readline from 'node:readline';
7
5
  import path from 'node:path';
6
+ // 轻量脚本式 MCP:手写 JSON-RPC over stdio,零依赖。客户端按需 spawn,stdin 关闭即退出,非常驻守护进程。
8
7
  const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.js');
9
8
  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
- }
9
+ const PROTOCOL_VERSION = '2024-11-05';
16
10
  export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开发编排器。当你需要在一个多服务工作区里并行开发多个功能、又不想让它们的端口/依赖/数据互相干扰时,用本服务的工具来完成开发工作。
17
11
 
18
12
  核心模型:一个功能占一个「槽位」→ 得到一个端口块;该功能用到哪些服务,就在哪些服务上各开一个 git worktree 挂进这个槽,端口自动错开,前端自动连到同槽的后端。
@@ -27,30 +21,69 @@ export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开
27
21
  - 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/run/down。
28
22
  - 只起这个功能「实际要改」的服务,不要全起。
29
23
  - 拿不准当前状态时先调 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;
24
+ - worktree_bay_gc 默认只读(dry-run 列出建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,安全保守、不会误删未完成的工作。
25
+ - 要写或修改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节时,先调用 worktree_bay_skill 获取完整的使用与配置指南(含每个配置原语、模板变量、校验规则与完整示例)。`;
26
+ const str = { type: 'string' };
27
+ export const TOOLS = [
28
+ { name: 'worktree_bay_ls', description: '列出所有功能槽位与占用(功能名、端口块、已起服务及端口、是否已并入主分支)',
29
+ inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls'] },
30
+ { name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。',
31
+ inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature', 'services'] },
32
+ toArgs: (a) => ['up', String(a.feature), ...(a.services ?? [])] },
33
+ { name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)',
34
+ inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str }, required: ['feature', 'service'] },
35
+ toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : [])] },
36
+ { name: 'worktree_bay_run', description: '在某功能某服务的运行体里跑预设命令(如 test),可透传额外参数',
37
+ inputSchema: { type: 'object', properties: { feature: str, service: str, name: str, args: { type: 'array', items: str } }, required: ['feature', 'service', 'name'] },
38
+ toArgs: (a) => ['run', String(a.feature), String(a.service), String(a.name), ...(a.args ?? [])] },
39
+ { name: 'worktree_bay_down', description: '拆除整个功能的所有服务 worktree(默认查脏/未推保护,force=true 强删)',
40
+ inputSchema: { type: 'object', properties: { feature: str, force: { type: 'boolean' } }, required: ['feature'] },
41
+ toArgs: (a) => ['down', String(a.feature), ...(a.force ? ['-f'] : [])] },
42
+ { name: 'worktree_bay_gc', description: '合并感知回收:默认 dry-run 只列建议,apply=true 才实际删除「已合并且干净」的功能',
43
+ inputSchema: { type: 'object', properties: { apply: { type: 'boolean' } } },
44
+ toArgs: (a) => ['gc', ...(a.apply ? ['--apply'] : [])] },
45
+ { name: 'worktree_bay_skill', description: 'worktree-bay 完整使用与配置指南(每个命令、每个配置原语、模板变量、校验规则、完整示例)。写/改 worktree-bay.config.json 或拿不准细节时先调用它。',
46
+ inputSchema: { type: 'object', properties: {} }, toArgs: () => ['skill'] },
47
+ ];
48
+ function runCli(args) {
49
+ const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
50
+ return [r.stdout, r.stderr].filter(Boolean).join('\n').trim() || '(无输出)';
51
+ }
52
+ export function handle(msg) {
53
+ const { id, method, params } = msg;
54
+ if (method === 'initialize')
55
+ return { jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: 'worktree-bay', version: VERSION }, instructions: INSTRUCTIONS } };
56
+ if (method === 'tools/list')
57
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) } };
58
+ if (method === 'tools/call') {
59
+ const tool = TOOLS.find((t) => t.name === params?.name);
60
+ if (!tool)
61
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: 'unknown tool: ' + params?.name } };
62
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: runCli(tool.toArgs(params?.arguments ?? {})) }] } };
63
+ }
64
+ if (method === 'ping')
65
+ return { jsonrpc: '2.0', id, result: {} };
66
+ if (method?.startsWith('notifications/'))
67
+ return null;
68
+ if (id !== undefined)
69
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } };
70
+ return null;
53
71
  }
54
- export async function startMcp() {
55
- await createServer().connect(new StdioServerTransport());
72
+ export function startMcp() {
73
+ const rl = readline.createInterface({ input: process.stdin });
74
+ rl.on('line', (line) => {
75
+ const t = line.trim();
76
+ if (!t)
77
+ return;
78
+ let msg;
79
+ try {
80
+ msg = JSON.parse(t);
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ const res = handle(msg);
86
+ if (res)
87
+ process.stdout.write(JSON.stringify(res) + '\n');
88
+ });
56
89
  }
package/dist/skill.js ADDED
@@ -0,0 +1,5 @@
1
+ import { readFileSync } from 'node:fs';
2
+ // 读取随包发布的 skill.md(位于包根,dist 的上一级)
3
+ export function readSkill() {
4
+ return readFileSync(new URL('../skill.md', import.meta.url), 'utf8');
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Config-driven git worktree slot + port orchestrator for parallel multi-service development",
5
5
  "keywords": [
6
6
  "git",
@@ -27,7 +27,8 @@
27
27
  "worktree-bay": "dist/cli.js"
28
28
  },
29
29
  "files": [
30
- "dist"
30
+ "dist",
31
+ "skill.md"
31
32
  ],
32
33
  "engines": {
33
34
  "node": ">=20"
@@ -43,9 +44,7 @@
43
44
  "registry": "https://registry.npmjs.org"
44
45
  },
45
46
  "dependencies": {
46
- "@modelcontextprotocol/sdk": "^1.12.0",
47
- "commander": "^12.1.0",
48
- "zod": "^3.23.8"
47
+ "commander": "^12.1.0"
49
48
  },
50
49
  "devDependencies": {
51
50
  "@types/node": "^22.0.0",
package/skill.md ADDED
@@ -0,0 +1,161 @@
1
+ # worktree-bay 使用与配置完全指南
2
+
3
+ `worktree-bay` 是配置驱动、与语言/技术栈无关的 **git worktree 槽位 + 端口编排器**,为多服务工作区的并行开发而生。
4
+
5
+ ## 核心模型:功能 = 槽位
6
+
7
+ - 一个**功能**认领一个**槽位 `N`**(1..maxSlots)→ 得到一个**端口块** `portBase + N*slotSpan`。
8
+ - 功能用到哪些**服务**,就在哪些服务各开一个 **git worktree** 挂进这个槽;块内每个服务按自己的 `offset` 取一个端口,互不相撞。
9
+ - 同槽的前端服务自动把 api base 指向同槽的后端端口。
10
+ - 槽位占用从文件系统派生(看 `<repo>/.worktrees/s<N>-*` 是否存在),删了 worktree 槽自动空出。
11
+
12
+ ---
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm i -g worktree-bay # 需要 Node >= 20
18
+ worktree-bay completion install # 一键装 shell 补全(可选)
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 命令大全
24
+
25
+ | 命令 | 作用 |
26
+ |---|---|
27
+ | `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名) |
28
+ | `worktree-bay claim <feature>` | 只占一个槽、打印端口块(不开 worktree) |
29
+ | `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>` |
30
+ | `worktree-bay ls` | 列出所有槽位:功能名、端口块、已起服务及端口、是否已并入主分支 |
31
+ | `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
32
+ | `worktree-bay sh <feature> <service>` | 进入某服务运行体的 shell |
33
+ | `worktree-bay down <feature> [-f]` | 拆除整个功能的所有服务 worktree(= `rm <feature>`) |
34
+ | `worktree-bay rm <feature> [service] [-f]` | 拆除某服务或整槽。默认查脏/未推保护,`-f` 强删 |
35
+ | `worktree-bay gc [--apply]` | 合并感知回收:默认 dry-run 只列建议,`--apply` 才删「已合并且干净」的 |
36
+ | `worktree-bay completion <install\|bash\|zsh\|fish>` | `install` 一键装进 shell;或打印补全脚本 |
37
+ | `worktree-bay mcp` | 启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 |
38
+ | `worktree-bay skill` | 打印本指南 |
39
+
40
+ 典型流程:
41
+
42
+ ```bash
43
+ worktree-bay up drill-fix api lms # 起整个功能(api+前端,分支都叫 drill-fix)
44
+ worktree-bay ls
45
+ worktree-bay run drill-fix api test # 跑测试
46
+ worktree-bay down drill-fix # 拆掉
47
+ worktree-bay gc # 回收已合并的
48
+ ```
49
+
50
+ ---
51
+
52
+ ## 配置详解:`worktree-bay.config.json`
53
+
54
+ 放在工作区根目录,工具自下而上查找(或用环境变量 `WORKTREE_BAY_CONFIG` 指定绝对路径)。
55
+
56
+ ### 顶层字段
57
+
58
+ | 字段 | 类型 | 说明 |
59
+ |---|---|---|
60
+ | `workspaceRoot` | string | 工作区根绝对路径,各服务仓在其下 |
61
+ | `portBase` | number | 端口基址,如 `6000` |
62
+ | `slotSpan` | number | 每个槽位的端口跨度(块大小),如 `10` |
63
+ | `maxSlots` | number | 最大并行功能数,如 `9` |
64
+ | `services` | object | 服务名 → 服务定义(见下) |
65
+
66
+ **端口计算**:`块基址 = portBase + 槽位N * slotSpan`;`某服务端口 = 块基址 + 该服务 offset`。
67
+ 例:portBase=6000, slotSpan=10,槽 1 的块基址 = 6010,offset=1 的服务端口 = 6011。
68
+
69
+ ### 服务定义原语
70
+
71
+ | 原语 | 必填 | 类型 | 说明 |
72
+ |---|---|---|---|
73
+ | `offset` | ✅ | number | 本服务在块内的偏移;`1 ≤ offset < slotSpan`,各服务互不相同 |
74
+ | `repo` | | string | 仓库目录名(相对 workspaceRoot),**默认 = 服务名** |
75
+ | `vars` | | object | 自定义模板变量,值里可引用基础变量,如 `{ "project": "myapi-{slug}" }` |
76
+ | `copy` | | string[] | 挂入时从主 checkout **递归拷贝**到 worktree 的文件/目录(含依赖目录,如 `vendor`)。含符号链接也安全(会跟随拷目标内容) |
77
+ | `env` | | object | dotenv 注入:`{ "文件名": { "KEY": "值模板" } }`,把键值**合并**进该文件(保留其它行,文件不存在则建) |
78
+ | `upstream` | | object | 声明依赖的上游服务:`{ "service": "api", "fallback": "http://localhost:6001" }`,产出 `{upstreamBase}` 变量 |
79
+ | `setup` | | string | 挂入后执行的 shell 命令(继承 stdio,会真跑) |
80
+ | `teardown` | | string | 拆除时执行的 shell 命令(可只依赖 `{project}`,在 repo 根跑,不绑 worktree) |
81
+ | `start` | | string | 长进程命令(如 `pnpm dev`)。**只打印命令、不阻塞**,交你自己起 |
82
+ | `exec` | | string[] | 透传命令模板(argv 数组),`{cmd...}` 是 argv splice 占位,防 shell 注入。如 `["docker","exec","-i","{project}-app-1","{cmd...}"]` |
83
+ | `run` | | object | 命名命令:`{ "test": ["composer","run","test"] }`,供 `worktree-bay run <feature> <service> test` 调用 |
84
+
85
+ ### 模板变量
86
+
87
+ `vars` / `copy` / `env` 的值 / `setup` / `teardown` / `start` / `exec` 里都可用 `{变量}`:
88
+
89
+ | 变量 | 含义 |
90
+ |---|---|
91
+ | `{slot}` | 槽位号 |
92
+ | `{blockBase}` | 块基址 `portBase + slot*slotSpan` |
93
+ | `{port}` | 本服务端口 `blockBase + offset` |
94
+ | `{slug}` | worktree 目录名 `s<slot>-<分支归一化>` |
95
+ | `{worktree}` | worktree 绝对路径 |
96
+ | `{repo}` | 服务仓绝对路径 |
97
+ | `{upstreamBase}` | 上游服务地址(同槽上游已起则为 `http://localhost:<上游端口>`,否则 `upstream.fallback`) |
98
+ | `{cmd...}` | 透传命令的 argv splice(仅 `exec` 数组里用) |
99
+ | 自定义 | `vars` 里声明的,如 `{project}` |
100
+
101
+ ### 加载时强制校验
102
+
103
+ 1. 各服务 `offset` 互不相同;
104
+ 2. `1 ≤ offset < slotSpan`;
105
+ 3. `upstream.service` 必须存在于 `services`;
106
+ 4. 所有模板里引用的 `{var}` 可解析(基础变量或本服务 vars 已声明);
107
+ 5. `repo` 指向的目录存在。
108
+ 任一不满足则报错退出。
109
+
110
+ ### 完整示例(docker 后端 + vite 前端)
111
+
112
+ ```jsonc
113
+ {
114
+ "workspaceRoot": "/path/to/workspace",
115
+ "portBase": 6000,
116
+ "slotSpan": 10,
117
+ "maxSlots": 9,
118
+ "services": {
119
+ "api": {
120
+ "offset": 1,
121
+ "vars": { "project": "myapi-{slug}" },
122
+ "copy": [".env", "vendor"],
123
+ "env": { ".env": { "APP_PORT": "{port}", "CACHE_PREFIX": "dev:{slug}:" } },
124
+ "setup": "docker compose -p {project} up -d",
125
+ "teardown": "docker compose -p {project} down -v",
126
+ "exec": ["docker", "exec", "-i", "{project}-app-1", "{cmd...}"],
127
+ "run": { "test": ["composer", "run", "test"], "migrate": ["php", "artisan", "migrate"] }
128
+ },
129
+ "web": {
130
+ "offset": 2,
131
+ "upstream": { "service": "api", "fallback": "http://localhost:6001" },
132
+ "env": { ".env.local": { "VITE_API_BASE_URL": "{upstreamBase}" } },
133
+ "setup": "pnpm install",
134
+ "start": "pnpm dev --port {port}"
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ > 依赖处理:后端 `copy: ["vendor"]`(拷已装好的,免重装);前端不拷 `node_modules`,用 `setup: "pnpm install"`(暖 store 下近乎瞬时,且躲开符号链接拷贝坑)。两种都支持,纯配置选择。
141
+
142
+ ---
143
+
144
+ ## 工作原理要点
145
+
146
+ - **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
147
+ - **并发安全**:`claim/add/up/rm/down/gc` 全程持工作区原子锁。
148
+ - **前端自接后端**:前端有 `upstream` 且同槽该上游服务的 worktree 已建,则 `{upstreamBase}` = 本槽上游端口;否则用 `fallback`。所以**联调时先 `up`/`add` 后端,再起前端**。
149
+ - **合并感知回收**:`gc` 先 `git fetch`,用 `merge-base --is-ancestor` 判断是否并入主分支;**只在「已合并 + 工作区干净 + 无未推」时才自动删**,判不准一律保守不删、只标记。
150
+
151
+ ---
152
+
153
+ ## 给 AI(MCP)
154
+
155
+ `worktree-bay mcp` 暴露工具:`worktree_bay_up / ls / add / run / down / gc / skill`。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
156
+
157
+ ## 常见坑
158
+
159
+ - 前端 api base 没指对:确认先起了同槽后端,且前端 `upstream.service` 写的是后端服务名。
160
+ - `add` 报 `origin/...` invalid:该仓没有 origin 或主分支,显式传 `base`(如 `HEAD`)。
161
+ - 槽位不够(maxSlots):`worktree-bay gc` 回收已合并的,或 `down` 拆掉用完的。