worktree-bay 2.3.9 → 2.5.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
@@ -45,7 +45,9 @@ worktree-bay down drill-fix
45
45
  worktree-bay gc
46
46
  ```
47
47
 
48
- > 需要更细的控制:`claim <feature>` 单独占槽、`add <feature> <service> [branch]` 单加一个服务(branch 可自定义,省略则用功能名)、`rm <feature> [service]` 拆单个服务。
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
- "teardown": "docker compose -p {project} down -v", // 拆除时执行
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` | 挂入 / 拆除时执行的 shell 命令 |
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 完成并行开发,并内置工作流指导(告诉 AI 何时用 up/ls/run/down/gc)。
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`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_doctor / ls / up / claim / add / path / run / down / gc / init / skill`(`doctor` 列出全部服务名,`ls` JSON 返回各 worktree 路径,`path` 直接给某功能某服务的目录,`down` 可只拆单个服务,`skill` 取完整指南)。
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
@@ -7,6 +7,7 @@ import { lsCommand } from './commands/ls.js';
7
7
  import { addCommand, upCommand } from './commands/add.js';
8
8
  import { runCommand, shCommand, pathCommand } from './commands/passthrough.js';
9
9
  import { rmCommand } from './commands/rm.js';
10
+ import { startCommand, stopCommand, restartCommand } from './commands/lifecycle.js';
10
11
  import { gcCommand } from './commands/gc.js';
11
12
  import { doctorCommand } from './commands/doctor.js';
12
13
  import { complete, completionCommand, installCompletion } from './commands/completion.js';
@@ -82,6 +83,27 @@ program.command('run <feature> <service> <name> [args...]').description(t('在
82
83
  .action((f, s, n, args) => sync((c) => runCommand(c, f, s, n, args ?? [])));
83
84
  program.command('sh <feature> <service>').description(t('进入服务运行体的 shell', 'open a shell inside the service runtime'))
84
85
  .action((f, s) => sync((c) => shCommand(c, f, s)));
86
+ program.command('start <feature> [service]').description(t('启动该功能的 dev server(worktree 已在,只起 start 进程,不动 worktree)', 'start the feature\'s dev server(s) (worktree already exists; runs the start process only)'))
87
+ .action(async (f, s) => { try {
88
+ await startCommand(loadConfig(process.cwd()), f, s);
89
+ }
90
+ catch (e) {
91
+ die(e.message);
92
+ } });
93
+ program.command('stop <feature> [service]').description(t('停止该功能的 dev server(保留 worktree)', 'stop the feature\'s dev server(s) (keeps the worktree)'))
94
+ .action(async (f, s) => { try {
95
+ await stopCommand(loadConfig(process.cwd()), f, s);
96
+ }
97
+ catch (e) {
98
+ die(e.message);
99
+ } });
100
+ program.command('restart <feature> [service]').description(t('重启该功能的 dev server(停掉再起)', 'restart the feature\'s dev server(s) (stop then start)'))
101
+ .action(async (f, s) => { try {
102
+ await restartCommand(loadConfig(process.cwd()), f, s);
103
+ }
104
+ catch (e) {
105
+ die(e.message);
106
+ } });
85
107
  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'))
86
108
  .action(async (f, o) => { try {
87
109
  await rmCommand(loadConfig(process.cwd()), f, undefined, !!o.force);
@@ -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 已在 → 不重建/不重跑 setup,但确保 dev server 在跑
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 ensureStarted(ctx);
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}`)));
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { readLabels } from '../slots.js';
5
5
  import { log } from '../util/log.js';
6
6
  import { t } from '../i18n.js';
7
- const SUBCMDS = ['init', 'doctor', 'claim', 'up', 'add', 'ls', 'path', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp', 'skill', 'version', 'help'];
7
+ const SUBCMDS = ['init', 'doctor', 'claim', 'up', 'add', 'ls', 'path', 'gc', 'down', 'rm', 'run', 'sh', 'start', 'stop', 'restart', 'completion', 'mcp', 'skill', 'version', 'help'];
8
8
  // words = 命令名 + 光标前已输入完的词(不含当前正在补的词)
9
9
  export function complete(cfg, words) {
10
10
  const prev = words.slice(1);
@@ -14,10 +14,10 @@ export function complete(cfg, words) {
14
14
  return []; // 无配置(不在工作区内):子命令已补全,feature/service 无从读取
15
15
  const sub = prev[0];
16
16
  const pos = prev.length;
17
- const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh', 'path'];
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'].includes(sub) && pos === 2)
20
+ if (['add', 'run', 'sh', 'path', 'start', 'stop', 'restart'].includes(sub) && pos === 2)
21
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 命令名
@@ -0,0 +1,67 @@
1
+ import { repoPath } from '../config.js';
2
+ import { withLock } from '../lock.js';
3
+ import { scanOccupancy, slotOfFeature } from '../slots.js';
4
+ import { buildVars, ensureRuntime, stopRuntime } from '../engine.js';
5
+ import { portInUse } from '../ports.js';
6
+ import { log } from '../util/log.js';
7
+ import { color as c } from '../util/color.js';
8
+ import { t } from '../i18n.js';
9
+ // dev server + infra 生命周期:stop/start/restart 同时管 node(managed 进程)与 docker(stop 钩子 + setup 恢复),不动 worktree。
10
+ function occupantsOf(cfg, feature, service) {
11
+ const slot = slotOfFeature(cfg, feature);
12
+ if (slot === undefined)
13
+ throw new Error(t(`功能「${feature}」未占槽。先 \`worktree-bay up ${feature} <服务...>\` 起它。`, `feature "${feature}" hasn't claimed a slot. Run \`worktree-bay up ${feature} <services...>\` first.`));
14
+ const all = scanOccupancy(cfg).get(slot) ?? [];
15
+ return service ? all.filter((o) => o.service === service) : all;
16
+ }
17
+ function ctxOf(cfg, o) {
18
+ 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) };
19
+ return { ...base, vars: buildVars(cfg, base) };
20
+ }
21
+ // 该服务是否有「可停起的运行体」:managed dev server(start) 或可停的 infra(stop 钩子)
22
+ const hasRuntime = (cfg, service) => { const sp = cfg.services[service]; return !!(sp.start || sp.stop); };
23
+ export async function stopCommand(cfg, feature, service) {
24
+ await withLock(cfg.workspaceRoot, async () => {
25
+ let any = false;
26
+ for (const o of occupantsOf(cfg, feature, service)) {
27
+ if (!hasRuntime(cfg, o.service))
28
+ continue;
29
+ any = true;
30
+ log(c.bold(c.cyan(o.service)));
31
+ await stopRuntime(ctxOf(cfg, o));
32
+ }
33
+ if (!any)
34
+ log(t('没有可停止的运行体(相关服务未配置 start/stop)', 'nothing to stop (no start/stop configured for those services)'));
35
+ });
36
+ }
37
+ export async function startCommand(cfg, feature, service) {
38
+ await withLock(cfg.workspaceRoot, async () => {
39
+ let any = false;
40
+ for (const o of occupantsOf(cfg, feature, service)) {
41
+ if (!hasRuntime(cfg, o.service))
42
+ continue;
43
+ any = true;
44
+ log(c.bold(c.cyan(o.service)));
45
+ await ensureRuntime(ctxOf(cfg, o));
46
+ }
47
+ if (!any)
48
+ log(t('没有可启动的运行体(相关服务未配置 start/stop)', 'nothing to start (no start/stop configured for those services)'));
49
+ });
50
+ }
51
+ export async function restartCommand(cfg, feature, service) {
52
+ await withLock(cfg.workspaceRoot, async () => {
53
+ for (const o of occupantsOf(cfg, feature, service)) {
54
+ if (!hasRuntime(cfg, o.service))
55
+ continue;
56
+ const ctx = ctxOf(cfg, o);
57
+ log(c.bold(c.cyan(o.service)) + c.dim(t(' · 重启…', ' · restarting…')));
58
+ await stopRuntime(ctx);
59
+ if (cfg.services[o.service].start) {
60
+ const port = Number(ctx.vars.port);
61
+ for (let i = 0; i < 40 && (await portInUse(port)); i++)
62
+ await new Promise((r) => setTimeout(r, 100));
63
+ } // 等端口释放
64
+ await ensureRuntime(ctx);
65
+ }
66
+ });
67
+ }
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. 摸清工作区(首次或拿不准时):worktree_bay_doctor 列出全部服务及其仓、校验 git/配置/各仓是否就绪——这也是你获知「有哪些服务名可传给 up/add」的途径。
16
- 1. 起新功能:调用 worktree_bay_up,传功能名 + 要改的服务列表(如 ["api","lms"])。它会自动占槽、为每个服务开 worktree、拷依赖、注入端口并起服务。
17
- 2. 定位代码:用 worktree_bay_path 拿某功能某服务的 worktree 绝对路径,进去改代码;或 worktree_bay_ls(JSON,含各 worktree 路径)总览全局。
18
- 3. 在某功能的某服务里跑测试/命令:worktree_bay_run(name 用配置里定义的,如 "test")。
19
- 4. 收尾:分支合并后,先 worktree_bay_gc 看可回收项,再 worktree_bay_down 拆掉该功能(只传 feature=拆整功能;带 service=只拆某个服务)。
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
- - 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/path/run/down。
23
- - 只起这个功能「实际要改」的服务,不要全起。不知道有哪些服务名时先调 worktree_bay_doctor。
24
- - 拿不准当前状态时先调 worktree_bay_ls。
25
- - worktree_bay_gc 默认只读(dry-run 列出建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,安全保守、不会误删未完成的工作。
26
- - worktree_bay_init 可在新工作区生成配置骨架(已存在则不覆盖);worktree_bay_claim 只占槽并打印各服务端口、不建 worktree(一般直接用 up 即可)。
27
- - 要写或修改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节时,先调用 worktree_bay_skill 获取完整的使用与配置指南(含每个配置原语、模板变量、校验规则与完整示例)。`;
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,6 +52,15 @@ 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 ?? [])] },
55
+ { name: 'worktree_bay_start', description: '启动功能的运行体(docker 容器 + node dev server 一起),不动 worktree。service 省略=该功能所有服务。',
56
+ inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature'] },
57
+ toArgs: (a) => ['start', String(a.feature), ...(a.service ? [String(a.service)] : [])] },
58
+ { name: 'worktree_bay_stop', description: '停止功能的运行体(停 docker 容器 + 杀 node dev server),保留 worktree。service 省略=该功能所有服务。',
59
+ inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature'] },
60
+ toArgs: (a) => ['stop', String(a.feature), ...(a.service ? [String(a.service)] : [])] },
61
+ { name: 'worktree_bay_restart', description: '重启功能的运行体(停掉再起,docker + node 一起)。改了配置或端口卡住时用。service 省略=该功能所有服务。',
62
+ inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature'] },
63
+ toArgs: (a) => ['restart', String(a.feature), ...(a.service ? [String(a.service)] : [])] },
49
64
  { name: 'worktree_bay_down', description: '拆除功能的 worktree:省略 service 拆整个功能的所有服务,传 service 只拆该服务(默认查脏/未推保护,force=true 强删)',
50
65
  inputSchema: { type: 'object', properties: { feature: str, service: str, force: { type: 'boolean' } }, required: ['feature'] },
51
66
  toArgs: (a) => ['rm', String(a.feature), ...(a.service ? [String(a.service)] : []), ...(a.force ? ['-f'] : [])] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "2.3.9",
3
+ "version": "2.5.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,6 +35,9 @@ 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> [service]` | 启动功能的运行体(docker 容器 + node dev server 一起),不动 worktree |
39
+ | `worktree-bay stop <feature> [service]` | 停止功能的运行体(停 docker + 杀 node dev server),保留 worktree |
40
+ | `worktree-bay restart <feature> [service]` | 重启运行体(停掉再起;改了配置/端口卡住时用) |
38
41
  | `worktree-bay down <feature> [-f]` | 拆除整个功能的所有服务 worktree(= `rm <feature>`) |
39
42
  | `worktree-bay rm <feature> [service] [-f]` | 拆除某服务或整槽。默认查脏/未推保护,`-f` 强删 |
40
43
  | `worktree-bay gc [--apply]` | 合并感知回收:默认 dry-run 只列建议,`--apply` 才删「已合并且干净」的 |
@@ -81,9 +84,10 @@ worktree-bay gc # 回收已合并的
81
84
  | `copy` | | string[] | 挂入时从主 checkout **递归拷贝**到 worktree 的文件/目录(含依赖目录,如 `vendor`)。含符号链接也安全(会跟随拷目标内容) |
82
85
  | `env` | | object | dotenv 注入:`{ "文件名": { "KEY": "值模板" } }`,把键值**合并**进该文件(保留其它行,文件不存在则建) |
83
86
  | `upstream` | | object | 声明依赖的上游服务:`{ "service": "api", "fallback": "http://localhost:6001" }`,产出 `{upstreamBase}` 变量 |
84
- | `setup` | | string | 挂入后执行的 shell 命令(继承 stdio,会真跑) |
85
- | `teardown` | | string | 拆除时执行的 shell 命令(可只依赖 `{project}`,在 repo 根跑,不绑 worktree) |
86
- | `start` | | string | 长进程命令(如 `pnpm dev`)。**只打印命令、不阻塞**,交你自己起 |
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` 幂等恢复 |
87
91
  | `exec` | | string[] | 透传命令模板(argv 数组),`{cmd...}` 是 argv splice 占位,防 shell 注入。如 `["docker","exec","-i","{project}-app-1","{cmd...}"]` |
88
92
  | `run` | | object | 命名命令:`{ "test": ["composer","run","test"] }`,供 `worktree-bay run <feature> <service> test` 调用 |
89
93
 
@@ -124,6 +128,7 @@ worktree-bay gc # 回收已合并的
124
128
  "copy": [".env", "vendor"],
125
129
  "env": { ".env": { "APP_PORT": "{port}", "CACHE_PREFIX": "dev:{slug}:" } },
126
130
  "setup": "docker compose -p {project} up -d",
131
+ "stop": "docker compose -p {project} stop",
127
132
  "teardown": "docker compose -p {project} down -v",
128
133
  "exec": ["docker", "exec", "-i", "{project}-app-1", "{cmd...}"],
129
134
  "run": { "test": ["composer", "run", "test"], "migrate": ["php", "artisan", "migrate"] }
@@ -143,9 +148,22 @@ worktree-bay gc # 回收已合并的
143
148
 
144
149
  ---
145
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
+
146
163
  ## 工作原理要点
147
164
 
148
165
  - **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
166
+ - **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid;`ls` 标 `▸run`;`stop`/`down` 按端口可靠停。
149
167
  - **并发安全**:`claim/add/up/rm/down/gc` 全程持工作区原子锁。
150
168
  - **前端自接后端**:前端有 `upstream` 且同槽该上游服务的 worktree 已建,则 `{upstreamBase}` = 本槽上游端口;否则用 `fallback`。所以**联调时先 `up`/`add` 后端,再起前端**。
151
169
  - **合并感知回收**:`gc` 先 `git fetch`,用 `merge-base --is-ancestor` 判断是否并入主分支;**只在「已合并 + 工作区干净 + 无未推」时才自动删**,判不准一律保守不删、只标记。
@@ -154,7 +172,7 @@ worktree-bay gc # 回收已合并的
154
172
 
155
173
  ## 给 AI(MCP)
156
174
 
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` 取本指南全文。
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` 取本指南全文。
158
176
 
159
177
  ## 常见坑
160
178