worktree-bay 2.4.0 → 3.0.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 +13 -9
- package/dist/cli.js +13 -11
- package/dist/commands/add.js +3 -3
- package/dist/commands/completion.js +4 -4
- package/dist/commands/lifecycle.js +38 -30
- package/dist/commands/rm.js +12 -5
- package/dist/engine.js +22 -1
- package/dist/mcp.js +31 -16
- package/package.json +1 -1
- package/skill.md +24 -9
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ worktree-bay down drill-fix
|
|
|
45
45
|
worktree-bay gc
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
>
|
|
48
|
+
> 运行体随起随停(不动 worktree/代码):`worktree-bay stop drill-fix` 停掉(docker 容器 + dev server 一起)、`start` 起回来、`restart` 重启。
|
|
49
|
+
>
|
|
50
|
+
> 更细的控制:`claim <feature>` 单独占槽、`add <feature> <service> [branch]` 单加一个服务(branch 可自定义,省略则用功能名)、`rm <feature> [service]` 拆单个服务。
|
|
49
51
|
|
|
50
52
|
## 配置
|
|
51
53
|
|
|
@@ -61,8 +63,9 @@ worktree-bay gc
|
|
|
61
63
|
"vars": { "project": "myapi-{slug}" },
|
|
62
64
|
"copy": [".env", "vendor"], // 从主 checkout 递归拷文件/目录
|
|
63
65
|
"env": { ".env": { "APP_PORT": "{port}" } }, // 合并键值进 dotenv(保留其它键)
|
|
64
|
-
"setup": "docker compose -p {project} up -d", //
|
|
65
|
-
"
|
|
66
|
+
"setup": "docker compose -p {project} up -d", // up 时执行(建运行体)
|
|
67
|
+
"stop": "docker compose -p {project} stop", // stop/restart 时执行(停而不毁)
|
|
68
|
+
"teardown": "docker compose -p {project} down -v", // down 时执行(销毁)
|
|
66
69
|
"exec": ["docker", "exec", "-i", "{project}-app-1", "{cmd...}"], // 透传模板(argv)
|
|
67
70
|
"run": { "test": ["composer", "run", "test"] } // 命名命令
|
|
68
71
|
},
|
|
@@ -71,7 +74,7 @@ worktree-bay gc
|
|
|
71
74
|
"upstream": { "service": "api", "fallback": "http://localhost:6001" }, // → {upstreamBase}
|
|
72
75
|
"env": { ".env.dev.local": { "VITE_API_BASE_URL": "{upstreamBase}" } },
|
|
73
76
|
"setup": "pnpm install",
|
|
74
|
-
"start": "pnpm dev --port {port}" //
|
|
77
|
+
"start": "pnpm dev --port {port}" // 长进程 dev server:up 时自动后台启动
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -87,8 +90,9 @@ worktree-bay gc
|
|
|
87
90
|
| `copy` | 从主 checkout 递归拷贝的文件/目录(含依赖目录) |
|
|
88
91
|
| `env` | 按文件合并 dotenv 键值,文件不存在则建 |
|
|
89
92
|
| `upstream` | 声明依赖的上游服务,产出 `{upstreamBase}` |
|
|
90
|
-
| `setup` / `teardown` |
|
|
91
|
-
| `start` |
|
|
93
|
+
| `setup` / `teardown` | 建立 / 销毁运行体的 shell(`up` 时 setup、`down` 时 teardown) |
|
|
94
|
+
| `start` | 长进程 dev server(如 `pnpm dev`),`up` 时自动**后台启动**、日志落 `.worktree-bay/logs/`,由 `start`/`stop`/`restart` 控制 |
|
|
95
|
+
| `stop` | 停止 infra 运行体的 shell(如 `docker compose stop`),供 `stop`/`restart` 用(让 docker 停而不毁) |
|
|
92
96
|
| `exec` | 透传命令模板(argv 数组,`{cmd...}` splice) |
|
|
93
97
|
| `run` | 命名命令(argv 数组),供 `worktree-bay run <feature> <service> <name>` |
|
|
94
98
|
|
|
@@ -116,9 +120,9 @@ worktree-bay completion install
|
|
|
116
120
|
|
|
117
121
|
## MCP(让 AI 直接用)
|
|
118
122
|
|
|
119
|
-
内置一个 MCP 服务,让 AI(Claude Code 等)通过 MCP 调用 worktree-bay
|
|
123
|
+
内置一个 MCP 服务,让 AI(Claude Code 等)通过 MCP 调用 worktree-bay 完成并行开发,并内置工作流指导(三层模型 + 何时用 doctor/up/path/run/start-stop-restart/down/gc)。
|
|
120
124
|
|
|
121
|
-
启动:`worktree-bay mcp`(stdio)。在 Claude Code
|
|
125
|
+
启动:`worktree-bay mcp`(stdio)。在 Claude Code 里注册(项目级 `.mcp.json` 或全局,跨平台、无需写死路径):
|
|
122
126
|
|
|
123
127
|
```json
|
|
124
128
|
{
|
|
@@ -128,7 +132,7 @@ worktree-bay completion install
|
|
|
128
132
|
}
|
|
129
133
|
```
|
|
130
134
|
|
|
131
|
-
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json
|
|
135
|
+
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(cwd 自动向上查找,**无需写死路径**;也可设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`(`doctor` 列出全部服务名,`ls` JSON 返回各 worktree 路径与 `▸run`,`path` 给某功能某服务目录,`start/stop/restart` 控制运行体,`down` 可只拆单个服务,`skill` 取完整指南)。MCP 调用时自动以非交互模式运行(输出不含颜色/进度控制符)。
|
|
132
136
|
|
|
133
137
|
## 许可证
|
|
134
138
|
|
package/dist/cli.js
CHANGED
|
@@ -73,8 +73,10 @@ catch (e) {
|
|
|
73
73
|
die(e.message);
|
|
74
74
|
} });
|
|
75
75
|
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)'))
|
|
76
|
-
.
|
|
77
|
-
|
|
76
|
+
.option('--branch <branch>', t('要创建的分支名(默认 = 功能名)', 'branch to create (default = feature name)'))
|
|
77
|
+
.option('--base <base>', t('分支基点(默认 = origin/<主分支>)', 'base ref for the branch (default = origin/<main>)'))
|
|
78
|
+
.action(async (f, s, b, base, o) => { try {
|
|
79
|
+
await addCommand(loadConfig(process.cwd()), f, s, o.branch ?? b, o.base ?? base);
|
|
78
80
|
}
|
|
79
81
|
catch (e) {
|
|
80
82
|
die(e.message);
|
|
@@ -83,35 +85,35 @@ program.command('run <feature> <service> <name> [args...]').description(t('在
|
|
|
83
85
|
.action((f, s, n, args) => sync((c) => runCommand(c, f, s, n, args ?? [])));
|
|
84
86
|
program.command('sh <feature> <service>').description(t('进入服务运行体的 shell', 'open a shell inside the service runtime'))
|
|
85
87
|
.action((f, s) => sync((c) => shCommand(c, f, s)));
|
|
86
|
-
program.command('start <feature> [
|
|
88
|
+
program.command('start <feature> [services...]').description(t('启动功能的运行体(docker 容器 + dev server);省略 service = 全部,可列多个。不动 worktree', 'start the feature\'s runtime (docker + dev server); omit services = all, or list several. Leaves the worktree untouched'))
|
|
87
89
|
.action(async (f, s) => { try {
|
|
88
|
-
await startCommand(loadConfig(process.cwd()), f, s);
|
|
90
|
+
await startCommand(loadConfig(process.cwd()), f, s ?? []);
|
|
89
91
|
}
|
|
90
92
|
catch (e) {
|
|
91
93
|
die(e.message);
|
|
92
94
|
} });
|
|
93
|
-
program.command('stop <feature> [
|
|
95
|
+
program.command('stop <feature> [services...]').description(t('停止功能的运行体(停 docker + 杀 dev server);省略 = 全部,可列多个。保留 worktree', 'stop the feature\'s runtime (stop docker + kill dev server); omit = all, or list several. Keeps the worktree'))
|
|
94
96
|
.action(async (f, s) => { try {
|
|
95
|
-
await stopCommand(loadConfig(process.cwd()), f, s);
|
|
97
|
+
await stopCommand(loadConfig(process.cwd()), f, s ?? []);
|
|
96
98
|
}
|
|
97
99
|
catch (e) {
|
|
98
100
|
die(e.message);
|
|
99
101
|
} });
|
|
100
|
-
program.command('restart <feature> [
|
|
102
|
+
program.command('restart <feature> [services...]').description(t('重启功能的运行体(停掉再起);省略 = 全部,可列多个', 'restart the feature\'s runtime (stop then start); omit = all, or list several'))
|
|
101
103
|
.action(async (f, s) => { try {
|
|
102
|
-
await restartCommand(loadConfig(process.cwd()), f, s);
|
|
104
|
+
await restartCommand(loadConfig(process.cwd()), f, s ?? []);
|
|
103
105
|
}
|
|
104
106
|
catch (e) {
|
|
105
107
|
die(e.message);
|
|
106
108
|
} });
|
|
107
|
-
program.command('down <feature>').description(t('
|
|
109
|
+
program.command('down <feature>').description(t('拆除整个功能(所有服务的 worktree)', 'tear down the whole feature (all its service worktrees)')).option('-f, --force', t('跳过脏/未推检查强制删除', 'skip dirty/unpushed checks and force-remove'))
|
|
108
110
|
.action(async (f, o) => { try {
|
|
109
|
-
await rmCommand(loadConfig(process.cwd()), f,
|
|
111
|
+
await rmCommand(loadConfig(process.cwd()), f, [], !!o.force);
|
|
110
112
|
}
|
|
111
113
|
catch (e) {
|
|
112
114
|
die(e.message);
|
|
113
115
|
} });
|
|
114
|
-
program.command('rm <feature>
|
|
116
|
+
program.command('rm <feature> <services...>').description(t('拆除功能下指定的一个或多个服务的 worktree(拆整功能用 down)。默认查脏/未推保护', 'remove the worktree(s) of the given service(s) of a feature (use down for the whole feature). Dirty/unpushed protected by default')).option('-f, --force', t('跳过脏/未推检查强制删除', 'skip dirty/unpushed checks and force-remove'))
|
|
115
117
|
.action(async (f, s, o) => { try {
|
|
116
118
|
await rmCommand(loadConfig(process.cwd()), f, s, !!o.force);
|
|
117
119
|
}
|
package/dist/commands/add.js
CHANGED
|
@@ -4,7 +4,7 @@ import { repoPath } from '../config.js';
|
|
|
4
4
|
import { withLock } from '../lock.js';
|
|
5
5
|
import { claim } from '../slots.js';
|
|
6
6
|
import { slugify, worktreeDirName } from '../naming.js';
|
|
7
|
-
import { buildVars, bringUp, ensureStarted } from '../engine.js';
|
|
7
|
+
import { buildVars, bringUp, ensureStarted, ensureRuntime } from '../engine.js';
|
|
8
8
|
import { mainBranch } from '../git.js';
|
|
9
9
|
import { log } from '../util/log.js';
|
|
10
10
|
import { color as c } from '../util/color.js';
|
|
@@ -23,9 +23,9 @@ export async function addCommand(cfg, feature, service, branch, base) {
|
|
|
23
23
|
const sp = cfg.services[service];
|
|
24
24
|
const ctxBase = { cfg, service, sp, slot: p.slot, slug: p.slug, dir: p.dir, repo: p.repo };
|
|
25
25
|
const ctx = { ...ctxBase, vars: buildVars(cfg, ctxBase) };
|
|
26
|
-
if (fs.existsSync(p.dir)) { // 幂等重入:worktree 已在 →
|
|
26
|
+
if (fs.existsSync(p.dir)) { // 幂等重入:worktree 已在 → 不重建,但「恢复运行体」(docker 容器 + dev server 都拉回来)
|
|
27
27
|
log(c.bold(c.cyan(service)) + c.dim(t(` · 已就绪 · 槽 ${p.slot} · 端口 ${ctx.vars.port}`, ` · ready · slot ${p.slot} · port ${ctx.vars.port}`)));
|
|
28
|
-
await
|
|
28
|
+
await ensureRuntime(ctx);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
log(c.bold(c.cyan(service)) + c.dim(t(` · 槽 ${p.slot} · 端口 ${ctx.vars.port} · 分支 ${br}`, ` · slot ${p.slot} · port ${ctx.vars.port} · branch ${br}`)));
|
|
@@ -17,12 +17,12 @@ export function complete(cfg, words) {
|
|
|
17
17
|
const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh', 'path', 'start', 'stop', 'restart'];
|
|
18
18
|
if (featureSubs.includes(sub) && pos === 1)
|
|
19
19
|
return Object.values(readLabels(cfg));
|
|
20
|
-
if (['add', 'run', 'sh', 'path'
|
|
21
|
-
return Object.keys(cfg.services);
|
|
20
|
+
if (['add', 'run', 'sh', 'path'].includes(sub) && pos === 2)
|
|
21
|
+
return Object.keys(cfg.services); // 单服务
|
|
22
22
|
if (sub === 'run' && pos === 3)
|
|
23
23
|
return Object.keys(cfg.services[prev[2]]?.run ?? {}); // run <feature> <service> <name>:补该服务的 run 命令名
|
|
24
|
-
if (
|
|
25
|
-
return Object.keys(cfg.services); //
|
|
24
|
+
if (['up', 'start', 'stop', 'restart', 'rm'].includes(sub) && pos >= 2)
|
|
25
|
+
return Object.keys(cfg.services); // 变长服务列表
|
|
26
26
|
return [];
|
|
27
27
|
}
|
|
28
28
|
export function completionScript(shell) {
|
|
@@ -1,65 +1,73 @@
|
|
|
1
1
|
import { repoPath } from '../config.js';
|
|
2
2
|
import { withLock } from '../lock.js';
|
|
3
3
|
import { scanOccupancy, slotOfFeature } from '../slots.js';
|
|
4
|
-
import { buildVars,
|
|
5
|
-
import { stopManaged } from '../proc.js';
|
|
4
|
+
import { buildVars, ensureRuntime, stopRuntime } from '../engine.js';
|
|
6
5
|
import { portInUse } from '../ports.js';
|
|
7
|
-
import { log
|
|
6
|
+
import { log } from '../util/log.js';
|
|
8
7
|
import { color as c } from '../util/color.js';
|
|
9
8
|
import { t } from '../i18n.js';
|
|
10
|
-
// dev server 生命周期:start/
|
|
11
|
-
|
|
9
|
+
// dev server + infra 生命周期:stop/start/restart 同时管 node(managed 进程)与 docker(stop 钩子 + setup 恢复),不动 worktree。
|
|
10
|
+
// services 为空 = 整功能;否则只这些服务
|
|
11
|
+
function occupantsOf(cfg, feature, services = []) {
|
|
12
12
|
const slot = slotOfFeature(cfg, feature);
|
|
13
13
|
if (slot === undefined)
|
|
14
14
|
throw new Error(t(`功能「${feature}」未占槽。先 \`worktree-bay up ${feature} <服务...>\` 起它。`, `feature "${feature}" hasn't claimed a slot. Run \`worktree-bay up ${feature} <services...>\` first.`));
|
|
15
15
|
const all = scanOccupancy(cfg).get(slot) ?? [];
|
|
16
|
-
|
|
16
|
+
if (!services.length)
|
|
17
|
+
return all;
|
|
18
|
+
for (const s of services)
|
|
19
|
+
if (!all.some((o) => o.service === s))
|
|
20
|
+
throw new Error(t(`服务「${s}」不在功能「${feature}」里。用 \`worktree-bay ls\` 看已起的服务。`, `service "${s}" is not in feature "${feature}". See \`worktree-bay ls\`.`));
|
|
21
|
+
return all.filter((o) => services.includes(o.service));
|
|
17
22
|
}
|
|
18
23
|
function ctxOf(cfg, o) {
|
|
19
|
-
const
|
|
20
|
-
const base = { cfg, service: o.service, sp, slot: o.slot, slug: o.slug, dir: o.dir, repo: repoPath(cfg, o.service) };
|
|
24
|
+
const base = { cfg, service: o.service, sp: cfg.services[o.service], slot: o.slot, slug: o.slug, dir: o.dir, repo: repoPath(cfg, o.service) };
|
|
21
25
|
return { ...base, vars: buildVars(cfg, base) };
|
|
22
26
|
}
|
|
23
|
-
|
|
27
|
+
// 该服务是否有「可停起的运行体」:managed dev server(start) 或可停的 infra(stop 钩子)
|
|
28
|
+
const hasRuntime = (cfg, service) => { const sp = cfg.services[service]; return !!(sp.start || sp.stop); };
|
|
29
|
+
export async function stopCommand(cfg, feature, services = []) {
|
|
24
30
|
await withLock(cfg.workspaceRoot, async () => {
|
|
25
31
|
let any = false;
|
|
26
|
-
for (const o of occupantsOf(cfg, feature,
|
|
27
|
-
if (!cfg
|
|
32
|
+
for (const o of occupantsOf(cfg, feature, services)) {
|
|
33
|
+
if (!hasRuntime(cfg, o.service))
|
|
28
34
|
continue;
|
|
29
35
|
any = true;
|
|
30
36
|
log(c.bold(c.cyan(o.service)));
|
|
31
|
-
await
|
|
37
|
+
await stopRuntime(ctxOf(cfg, o));
|
|
32
38
|
}
|
|
33
39
|
if (!any)
|
|
34
|
-
|
|
40
|
+
log(t('没有可停止的运行体(相关服务未配置 start/stop)', 'nothing to stop (no start/stop configured for those services)'));
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
|
-
export async function
|
|
43
|
+
export async function startCommand(cfg, feature, services = []) {
|
|
38
44
|
await withLock(cfg.workspaceRoot, async () => {
|
|
39
45
|
let any = false;
|
|
40
|
-
for (const o of occupantsOf(cfg, feature,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
for (const o of occupantsOf(cfg, feature, services)) {
|
|
47
|
+
if (!hasRuntime(cfg, o.service))
|
|
48
|
+
continue;
|
|
49
|
+
any = true;
|
|
50
|
+
log(c.bold(c.cyan(o.service)));
|
|
51
|
+
await ensureRuntime(ctxOf(cfg, o));
|
|
46
52
|
}
|
|
47
53
|
if (!any)
|
|
48
|
-
log(t('
|
|
54
|
+
log(t('没有可启动的运行体(相关服务未配置 start/stop)', 'nothing to start (no start/stop configured for those services)'));
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
|
-
export async function restartCommand(cfg, feature,
|
|
57
|
+
export async function restartCommand(cfg, feature, services = []) {
|
|
52
58
|
await withLock(cfg.workspaceRoot, async () => {
|
|
53
|
-
for (const o of occupantsOf(cfg, feature,
|
|
54
|
-
|
|
55
|
-
if (!sp.start)
|
|
59
|
+
for (const o of occupantsOf(cfg, feature, services)) {
|
|
60
|
+
if (!hasRuntime(cfg, o.service))
|
|
56
61
|
continue;
|
|
62
|
+
const ctx = ctxOf(cfg, o);
|
|
57
63
|
log(c.bold(c.cyan(o.service)) + c.dim(t(' · 重启…', ' · restarting…')));
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
await stopRuntime(ctx);
|
|
65
|
+
if (cfg.services[o.service].start) {
|
|
66
|
+
const port = Number(ctx.vars.port);
|
|
67
|
+
for (let i = 0; i < 40 && (await portInUse(port)); i++)
|
|
68
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
69
|
+
} // 等端口释放
|
|
70
|
+
await ensureRuntime(ctx);
|
|
63
71
|
}
|
|
64
72
|
});
|
|
65
73
|
}
|
package/dist/commands/rm.js
CHANGED
|
@@ -9,17 +9,24 @@ import { stopManaged } from '../proc.js';
|
|
|
9
9
|
import { log, warn } from '../util/log.js';
|
|
10
10
|
import { color as c } from '../util/color.js';
|
|
11
11
|
import { t } from '../i18n.js';
|
|
12
|
-
|
|
12
|
+
// services 为空 = 整功能;否则只这些服务(顺带校验服务名确实在该功能里)
|
|
13
|
+
export function resolveRm(cfg, feature, services = []) {
|
|
13
14
|
const slot = slotOfFeature(cfg, feature);
|
|
14
15
|
if (slot === undefined)
|
|
15
16
|
throw new Error(t(`功能「${feature}」未占槽,无需拆除。用 \`worktree-bay ls\` 看在用的功能。`, `feature "${feature}" has no slot — nothing to tear down. See \`worktree-bay ls\`.`));
|
|
16
17
|
const all = scanOccupancy(cfg).get(slot) ?? [];
|
|
17
|
-
|
|
18
|
+
if (!services.length)
|
|
19
|
+
return all;
|
|
20
|
+
for (const s of services)
|
|
21
|
+
if (!all.some((o) => o.service === s))
|
|
22
|
+
throw new Error(t(`服务「${s}」不在功能「${feature}」里。用 \`worktree-bay ls\` 看已起的服务。`, `service "${s}" is not in feature "${feature}". See \`worktree-bay ls\`.`));
|
|
23
|
+
return all.filter((o) => services.includes(o.service));
|
|
18
24
|
}
|
|
19
|
-
export async function rmCommand(cfg, feature,
|
|
25
|
+
export async function rmCommand(cfg, feature, services, force) {
|
|
20
26
|
await withLock(cfg.workspaceRoot, async () => {
|
|
21
27
|
let removed = 0;
|
|
22
|
-
const
|
|
28
|
+
const wholeFeature = services.length === 0;
|
|
29
|
+
const occs = resolveRm(cfg, feature, services);
|
|
23
30
|
for (const o of occs) {
|
|
24
31
|
const repo = repoPath(cfg, o.service);
|
|
25
32
|
const branch = currentBranch(o.dir);
|
|
@@ -41,7 +48,7 @@ export async function rmCommand(cfg, feature, service, force) {
|
|
|
41
48
|
removed++;
|
|
42
49
|
}
|
|
43
50
|
const slot = slotOfFeature(cfg, feature);
|
|
44
|
-
if (
|
|
51
|
+
if (wholeFeature && (scanOccupancy(cfg).get(slot) ?? []).length === 0) {
|
|
45
52
|
removeLabel(cfg, slot);
|
|
46
53
|
if (removed === 0)
|
|
47
54
|
log(`${c.green('✓')} ` + t(`释放空槽预约 "${feature}"(槽 ${slot})`, `released empty slot reservation "${feature}" (slot ${slot})`));
|
package/dist/engine.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runShellLive, run, spliceArgv, isTTY } from './util/exec.js';
|
|
|
8
8
|
import { warn, log } from './util/log.js';
|
|
9
9
|
import { withProgress } from './util/progress.js';
|
|
10
10
|
import { color as cc } from './util/color.js';
|
|
11
|
-
import { startDetached, recordedFor, pidAlive, setPid, pidOnPort, readLogTail } from './proc.js';
|
|
11
|
+
import { startDetached, recordedFor, pidAlive, setPid, pidOnPort, readLogTail, stopManaged } from './proc.js';
|
|
12
12
|
import { t } from './i18n.js';
|
|
13
13
|
export function mergeEnvText(text, kv) {
|
|
14
14
|
const lines = text.split('\n');
|
|
@@ -114,6 +114,27 @@ async function waitForListen(port, ms) {
|
|
|
114
114
|
await new Promise((r) => setTimeout(r, 200));
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
// 「运行体」= docker 容器(infra) + node dev server。up 重入 / start / restart 共用,统一边界。
|
|
118
|
+
// 恢复运行体:有 stop 钩子的 infra 服务重跑 setup(docker compose up -d 幂等恢复)+ 起 managed dev server。
|
|
119
|
+
export async function ensureRuntime(ctx) {
|
|
120
|
+
const { sp, dir, service, vars } = ctx;
|
|
121
|
+
if (sp.stop && sp.setup) {
|
|
122
|
+
const cmd = renderTemplate(sp.setup, vars);
|
|
123
|
+
await runShellLive(cmd, { cwd: dir }, t(`恢复 ${service}:${cmd}`, `resume ${service}: ${cmd}`));
|
|
124
|
+
}
|
|
125
|
+
await ensureStarted(ctx);
|
|
126
|
+
}
|
|
127
|
+
// 停止运行体:杀 managed dev server + 跑 stop 钩子(docker compose stop)。不动 worktree。
|
|
128
|
+
export async function stopRuntime(ctx) {
|
|
129
|
+
const { cfg, sp, dir, service, vars } = ctx;
|
|
130
|
+
const stopped = stopManaged(cfg.workspaceRoot, dir);
|
|
131
|
+
if (stopped)
|
|
132
|
+
log(` ${cc.green('✓')} ` + t(`已停止 dev server(pid ${stopped.pid})`, `stopped dev server (pid ${stopped.pid})`));
|
|
133
|
+
if (sp.stop) {
|
|
134
|
+
const cmd = renderTemplate(sp.stop, vars);
|
|
135
|
+
await runShellLive(cmd, { cwd: dir }, t(`停 ${service}:${cmd}`, `stop ${service}: ${cmd}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
117
138
|
export function execArgv(ctx, cmd) {
|
|
118
139
|
const tpl = (ctx.sp.exec ?? ['sh', '-c', '{cmd...}']).map((el) => el === '{cmd...}' ? el : renderTemplate(el, ctx.vars));
|
|
119
140
|
const spliced = spliceArgv(tpl, cmd);
|
package/dist/mcp.js
CHANGED
|
@@ -7,24 +7,30 @@ import path from 'node:path';
|
|
|
7
7
|
const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
8
8
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
9
9
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
10
|
-
export const INSTRUCTIONS = `worktree-bay 是「功能 =
|
|
10
|
+
export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开发编排器。在一个多服务工作区里并行开发多个功能、又不想让它们的端口/依赖/数据互相干扰时,用本服务的工具完成开发。
|
|
11
11
|
|
|
12
|
-
核心模型:每个服务有自己的端口段(基址 = 主 dev/槽0);一个功能占一个「槽位 N」,用到哪些服务就在哪些服务上各开一个 git worktree 挂进这个槽,该服务端口 = 基址 + N
|
|
12
|
+
核心模型:每个服务有自己的端口段(基址 = 主 dev/槽0);一个功能占一个「槽位 N」,用到哪些服务就在哪些服务上各开一个 git worktree 挂进这个槽,该服务端口 = 基址 + N,自动错开;前端自动连到同槽的后端。
|
|
13
|
+
|
|
14
|
+
三层职责(决定每个工具的边界):
|
|
15
|
+
- worktree + 基础设施:worktree_bay_up/_add 建立(开 worktree、拷依赖、注入 .env、跑 setup 如 docker compose up),worktree_bay_down 销毁(teardown + 删 worktree)。
|
|
16
|
+
- dev server(前端等长进程):up 时按配置自动「后台」拉起;worktree_bay_start/_stop/_restart 单独控制它,不动 worktree。
|
|
17
|
+
- 在运行体里执行命令:worktree_bay_run(如 test/migrate)。
|
|
13
18
|
|
|
14
19
|
推荐工作流:
|
|
15
|
-
0.
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
19
|
-
4.
|
|
20
|
+
0. 摸清工作区(首次/拿不准时):worktree_bay_doctor —— 列出全部服务及其仓、校验就绪;这也是获知「有哪些服务名可传给 up」的途径。
|
|
21
|
+
1. 起新功能:worktree_bay_up,传功能名 + 要改的服务列表(如 ["api","lms"])。自动占槽、开 worktree、拷依赖、注入端口、跑 setup,并把配了 start 的服务(如前端 dev server)后台拉起(日志在 .worktree-bay/logs/)。
|
|
22
|
+
2. 定位代码:worktree_bay_path 拿某功能某服务的 worktree 绝对路径进去改;或 worktree_bay_ls(JSON,含各 worktree 路径,▸run 标记 dev server 是否在跑)总览。
|
|
23
|
+
3. 跑命令/测试:worktree_bay_run(name 用配置里定义的,如 "test")。
|
|
24
|
+
4. 控制 dev server(按需):worktree_bay_restart 重启 / _stop 停 / _start 起——只影响 dev server,worktree 与代码不受影响。
|
|
25
|
+
5. 收尾:分支合并后先 worktree_bay_gc 看可回收项,再 worktree_bay_down 拆掉该功能(省略 service=整功能;带 service=只拆该服务)。
|
|
20
26
|
|
|
21
27
|
要点:
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- worktree_bay_gc 默认只读(dry-run
|
|
26
|
-
- worktree_bay_init
|
|
27
|
-
-
|
|
28
|
+
- 一个功能从头到尾用同一个功能名(= 默认分支名)。
|
|
29
|
+
- 只起「实际要改」的服务,不要全起;不知道有哪些服务名先调 worktree_bay_doctor。
|
|
30
|
+
- 拿不准当前状态先调 worktree_bay_ls。
|
|
31
|
+
- worktree_bay_gc 默认只读(dry-run 列建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,保守不误删。
|
|
32
|
+
- worktree_bay_init 在新工作区生成配置骨架(已存在则不覆盖);worktree_bay_claim 只占槽并打印端口、不建 worktree(一般直接用 up 即可)。
|
|
33
|
+
- 要写/改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节,先调 worktree_bay_skill 取完整指南(每个配置原语、模板变量、校验规则、完整示例)。`;
|
|
28
34
|
const str = { type: 'string' };
|
|
29
35
|
export const TOOLS = [
|
|
30
36
|
{ name: 'worktree_bay_doctor', description: '体检并列出工作区全部服务及其仓目录、校验 git/配置/各仓是否就绪。起步前先调它,也是获知「有哪些服务名可传给 up/add」的途径。',
|
|
@@ -46,9 +52,18 @@ export const TOOLS = [
|
|
|
46
52
|
{ name: 'worktree_bay_run', description: '在某功能某服务的运行体里跑预设命令(如 test),可透传额外参数',
|
|
47
53
|
inputSchema: { type: 'object', properties: { feature: str, service: str, name: str, args: { type: 'array', items: str } }, required: ['feature', 'service', 'name'] },
|
|
48
54
|
toArgs: (a) => ['run', String(a.feature), String(a.service), String(a.name), ...(a.args ?? [])] },
|
|
49
|
-
{ name: '
|
|
50
|
-
inputSchema: { type: 'object', properties: { feature: str,
|
|
51
|
-
toArgs: (a) => ['
|
|
55
|
+
{ name: 'worktree_bay_start', description: '启动功能的运行体(docker 容器 + node dev server 一起),不动 worktree。services 省略=该功能所有服务,也可列多个。',
|
|
56
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature'] },
|
|
57
|
+
toArgs: (a) => ['start', String(a.feature), ...(a.services ?? [])] },
|
|
58
|
+
{ name: 'worktree_bay_stop', description: '停止功能的运行体(停 docker 容器 + 杀 node dev server),保留 worktree。services 省略=全部,也可列多个。',
|
|
59
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature'] },
|
|
60
|
+
toArgs: (a) => ['stop', String(a.feature), ...(a.services ?? [])] },
|
|
61
|
+
{ name: 'worktree_bay_restart', description: '重启功能的运行体(停掉再起,docker + node 一起)。改了配置或端口卡住时用。services 省略=全部,也可列多个。',
|
|
62
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature'] },
|
|
63
|
+
toArgs: (a) => ['restart', String(a.feature), ...(a.services ?? [])] },
|
|
64
|
+
{ name: 'worktree_bay_down', description: '拆除 worktree:省略 services 拆整个功能(所有服务),给 services 只拆这些服务(默认查脏/未推保护,force=true 强删)',
|
|
65
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str }, force: { type: 'boolean' } }, required: ['feature'] },
|
|
66
|
+
toArgs: (a) => { const s = a.services ?? []; const force = a.force ? ['-f'] : []; return s.length ? ['rm', String(a.feature), ...s, ...force] : ['down', String(a.feature), ...force]; } },
|
|
52
67
|
{ name: 'worktree_bay_gc', description: '合并感知回收:默认 dry-run 只列建议,apply=true 才实际删除「已合并且干净」的功能',
|
|
53
68
|
inputSchema: { type: 'object', properties: { apply: { type: 'boolean' } } },
|
|
54
69
|
toArgs: (a) => ['gc', ...(a.apply ? ['--apply'] : [])] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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
|
@@ -35,11 +35,11 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
35
35
|
| `worktree-bay path <feature> <service>` | 打印某服务 worktree 的绝对路径(可 `cd $(worktree-bay path f api)`) |
|
|
36
36
|
| `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
|
|
37
37
|
| `worktree-bay sh <feature> <service>` | 进入某服务运行体的 shell |
|
|
38
|
-
| `worktree-bay start <feature> [
|
|
39
|
-
| `worktree-bay stop <feature> [
|
|
40
|
-
| `worktree-bay restart <feature> [
|
|
41
|
-
| `worktree-bay down <feature> [-f]` |
|
|
42
|
-
| `worktree-bay rm <feature>
|
|
38
|
+
| `worktree-bay start <feature> [services...]` | 启动功能的运行体(docker 容器 + node dev server 一起);**省略 = 全部**,也可列多个。不动 worktree |
|
|
39
|
+
| `worktree-bay stop <feature> [services...]` | 停止功能的运行体(停 docker + 杀 node dev server);省略 = 全部,可列多个。保留 worktree |
|
|
40
|
+
| `worktree-bay restart <feature> [services...]` | 重启运行体(停掉再起);省略 = 全部,可列多个 |
|
|
41
|
+
| `worktree-bay down <feature> [-f]` | 拆除**整个功能**(所有服务的 worktree)。`up` 的反操作 |
|
|
42
|
+
| `worktree-bay rm <feature> <services...> [-f]` | 拆除指定的**一个或多个服务**(拆整功能用 `down`)。`add` 的反操作。默认查脏/未推保护,`-f` 强删 |
|
|
43
43
|
| `worktree-bay gc [--apply]` | 合并感知回收:默认 dry-run 只列建议,`--apply` 才删「已合并且干净」的 |
|
|
44
44
|
| `worktree-bay completion <install\|bash\|zsh\|fish>` | `install` 一键装进 shell;或打印补全脚本 |
|
|
45
45
|
| `worktree-bay mcp` | 启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 |
|
|
@@ -84,9 +84,10 @@ worktree-bay gc # 回收已合并的
|
|
|
84
84
|
| `copy` | | string[] | 挂入时从主 checkout **递归拷贝**到 worktree 的文件/目录(含依赖目录,如 `vendor`)。含符号链接也安全(会跟随拷目标内容) |
|
|
85
85
|
| `env` | | object | dotenv 注入:`{ "文件名": { "KEY": "值模板" } }`,把键值**合并**进该文件(保留其它行,文件不存在则建) |
|
|
86
86
|
| `upstream` | | object | 声明依赖的上游服务:`{ "service": "api", "fallback": "http://localhost:6001" }`,产出 `{upstreamBase}` 变量 |
|
|
87
|
-
| `setup` | | string | 挂入后执行的 shell
|
|
88
|
-
| `teardown` | | string | 拆除时执行的 shell
|
|
89
|
-
| `start` | | string |
|
|
87
|
+
| `setup` | | string | 挂入后执行的 shell 命令(创建/装好运行体,如 `docker compose up -d`、`pnpm install`)。`up` 时跑 |
|
|
88
|
+
| `teardown` | | string | 拆除时执行的 shell 命令(销毁运行体,如 `docker compose down -v`)。`down`/`rm`/`gc` 时跑 |
|
|
89
|
+
| `start` | | string | 长进程 dev server(如 `pnpm dev`)。`up` 时**自动后台启动**(detach + 日志落 `.worktree-bay/logs/`),按端口追踪 pid;由 `start`/`stop`/`restart` 控制,`ls` 标 `▸run` |
|
|
90
|
+
| `stop` | | string | 停止该服务 infra 运行体的 shell(如 `docker compose stop`)。供 `stop`/`restart` 用——让 docker 容器能「停而不毁」;`start` 时对配了它的服务重跑 `setup` 幂等恢复 |
|
|
90
91
|
| `exec` | | string[] | 透传命令模板(argv 数组),`{cmd...}` 是 argv splice 占位,防 shell 注入。如 `["docker","exec","-i","{project}-app-1","{cmd...}"]` |
|
|
91
92
|
| `run` | | object | 命名命令:`{ "test": ["composer","run","test"] }`,供 `worktree-bay run <feature> <service> test` 调用 |
|
|
92
93
|
|
|
@@ -127,6 +128,7 @@ worktree-bay gc # 回收已合并的
|
|
|
127
128
|
"copy": [".env", "vendor"],
|
|
128
129
|
"env": { ".env": { "APP_PORT": "{port}", "CACHE_PREFIX": "dev:{slug}:" } },
|
|
129
130
|
"setup": "docker compose -p {project} up -d",
|
|
131
|
+
"stop": "docker compose -p {project} stop",
|
|
130
132
|
"teardown": "docker compose -p {project} down -v",
|
|
131
133
|
"exec": ["docker", "exec", "-i", "{project}-app-1", "{cmd...}"],
|
|
132
134
|
"run": { "test": ["composer", "run", "test"], "migrate": ["php", "artisan", "migrate"] }
|
|
@@ -146,9 +148,22 @@ worktree-bay gc # 回收已合并的
|
|
|
146
148
|
|
|
147
149
|
---
|
|
148
150
|
|
|
151
|
+
## 命令边界(三层)
|
|
152
|
+
|
|
153
|
+
| 层 | 是什么 | 建立 | 控制运行 | 销毁 |
|
|
154
|
+
|---|---|---|---|---|
|
|
155
|
+
| ① worktree + 基础设施 | git worktree + `copy`/`env` + `setup` 起的东西 | `up` / `add` | — | `down` / `rm` / `gc`(`teardown`) |
|
|
156
|
+
| ② 运行体(runtime) | docker 容器(infra) + node dev server(`start`) | `up` 顺带起 | **`start` / `stop` / `restart`**(docker+node 一起,不动 worktree) | `down`(一并停) |
|
|
157
|
+
| ③ 在运行体里执行 | 跑命令 / 开 shell | — | `run` / `sh` | — |
|
|
158
|
+
|
|
159
|
+
- `up`:建 worktree+infra(首次)并起运行体;**重入 = 恢复运行体**(docker 挂了重跑 `up` 能拉回来,等价 `start`)。
|
|
160
|
+
- `start`/`stop`/`restart`:只管②运行体,省略 service = 整功能、带 service = 单个;不碰 worktree 与代码。
|
|
161
|
+
- `down`(=`rm <feature>`) ↔ `up`(功能级);`rm <feature> <service>` ↔ `add`(单服务级)。
|
|
162
|
+
|
|
149
163
|
## 工作原理要点
|
|
150
164
|
|
|
151
165
|
- **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
|
|
166
|
+
- **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid;`ls` 标 `▸run`;`stop`/`down` 按端口可靠停。
|
|
152
167
|
- **并发安全**:`claim/add/up/rm/down/gc` 全程持工作区原子锁。
|
|
153
168
|
- **前端自接后端**:前端有 `upstream` 且同槽该上游服务的 worktree 已建,则 `{upstreamBase}` = 本槽上游端口;否则用 `fallback`。所以**联调时先 `up`/`add` 后端,再起前端**。
|
|
154
169
|
- **合并感知回收**:`gc` 先 `git fetch`,用 `merge-base --is-ancestor` 判断是否并入主分支;**只在「已合并 + 工作区干净 + 无未推」时才自动删**,判不准一律保守不删、只标记。
|
|
@@ -157,7 +172,7 @@ worktree-bay gc # 回收已合并的
|
|
|
157
172
|
|
|
158
173
|
## 给 AI(MCP)
|
|
159
174
|
|
|
160
|
-
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / down / gc / init / skill`。`doctor` 列出全部服务(AI
|
|
175
|
+
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知服务名);`ls` 以 JSON 返回(含各 worktree 绝对路径、`▸run`);`path` 给某功能某服务的 worktree 目录;`start/stop/restart` 控制运行体(docker+node);`down` 省略 service 拆整功能、带 service 只拆该服务。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
|
|
161
176
|
|
|
162
177
|
## 常见坑
|
|
163
178
|
|