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 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
@@ -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
- .action(async (f, s, b, base) => { try {
77
- await addCommand(loadConfig(process.cwd()), f, s, b, base);
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> [service]').description(t('启动该功能的 dev server(worktree 已在,只起 start 进程,不动 worktree', 'start the feature\'s dev server(s) (worktree already exists; runs the start process only)'))
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> [service]').description(t('停止该功能的 dev server(保留 worktree', 'stop the feature\'s dev server(s) (keeps the worktree)'))
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> [service]').description(t('重启该功能的 dev server(停掉再起)', 'restart the feature\'s dev server(s) (stop then start)'))
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('拆除整个功能的所有服务 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'))
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, undefined, !!o.force);
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> [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'))
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
  }
@@ -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}`)));
@@ -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', 'start', 'stop', 'restart'].includes(sub) && pos === 2)
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 (sub === 'up' && pos >= 2)
25
- return Object.keys(cfg.services); // up 接变长服务列表
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, ensureStarted } from '../engine.js';
5
- import { stopManaged } from '../proc.js';
4
+ import { buildVars, ensureRuntime, stopRuntime } from '../engine.js';
6
5
  import { portInUse } from '../ports.js';
7
- import { log, warn } from '../util/log.js';
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/stop/restart(只管 start 配置起的进程,不动 worktree
11
- function occupantsOf(cfg, feature, service) {
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
- return service ? all.filter((o) => o.service === service) : all;
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 sp = cfg.services[o.service];
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
- export async function startCommand(cfg, feature, service) {
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, service)) {
27
- if (!cfg.services[o.service].start)
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 ensureStarted(ctxOf(cfg, o));
37
+ await stopRuntime(ctxOf(cfg, o));
32
38
  }
33
39
  if (!any)
34
- warn(t('没有可启动的 dev server(相关服务未配置 start)', 'nothing to start (those services have no start configured)'));
40
+ log(t('没有可停止的运行体(相关服务未配置 start/stop)', 'nothing to stop (no start/stop configured for those services)'));
35
41
  });
36
42
  }
37
- export async function stopCommand(cfg, feature, service) {
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, service)) {
41
- const stopped = stopManaged(cfg.workspaceRoot, o.dir);
42
- if (stopped) {
43
- any = true;
44
- log(`${c.green('✓')} ` + t(`已停止 ${o.service} dev server(pid ${stopped.pid})`, `stopped ${o.service} dev server (pid ${stopped.pid})`));
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('没有在跑的 dev server', 'no running dev server'));
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, service) {
57
+ export async function restartCommand(cfg, feature, services = []) {
52
58
  await withLock(cfg.workspaceRoot, async () => {
53
- for (const o of occupantsOf(cfg, feature, service)) {
54
- const sp = cfg.services[o.service];
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
- stopManaged(cfg.workspaceRoot, o.dir);
59
- const port = Number(ctxOf(cfg, o).vars.port);
60
- for (let i = 0; i < 40 && (await portInUse(port)); i++)
61
- await new Promise((r) => setTimeout(r, 100)); // 等端口释放(最多 ~4s)
62
- await ensureStarted(ctxOf(cfg, o));
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
  }
@@ -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
- export function resolveRm(cfg, feature, service) {
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
- return service ? all.filter((o) => o.service === service) : all;
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, service, force) {
25
+ export async function rmCommand(cfg, feature, services, force) {
20
26
  await withLock(cfg.workspaceRoot, async () => {
21
27
  let removed = 0;
22
- const occs = resolveRm(cfg, feature, service);
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 (!service && (scanOccupancy(cfg).get(slot) ?? []).length === 0) {
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. 摸清工作区(首次或拿不准时):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,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: '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'] : [])] },
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": "2.4.0",
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> [service]` | 启动该功能的 dev server(worktree 已在,只起 `start` 进程,不动 worktree |
39
- | `worktree-bay stop <feature> [service]` | 停止该功能的 dev server(保留 worktree |
40
- | `worktree-bay restart <feature> [service]` | 重启 dev server(停掉再起;改了配置/端口卡住时用) |
41
- | `worktree-bay down <feature> [-f]` | 拆除整个功能的所有服务 worktree(= `rm <feature>`) |
42
- | `worktree-bay rm <feature> [service] [-f]` | 拆除某服务或整槽。默认查脏/未推保护,`-f` 强删 |
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 命令(继承 stdio,会真跑) |
88
- | `teardown` | | string | 拆除时执行的 shell 命令(可只依赖 `{project}`,在 repo 根跑,不绑 worktree) |
89
- | `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` 幂等恢复 |
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 借此得知有哪些服务名可用);`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` 取本指南全文。
161
176
 
162
177
  ## 常见坑
163
178