worktree-bay 4.0.3 → 4.0.4

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.
@@ -8,17 +8,17 @@ import { color as c } from '../util/color.js';
8
8
  import { t } from '../i18n.js';
9
9
  // dev server + infra 生命周期:stop/start/restart 同时管 node(managed 进程)与 docker(stop 钩子 + setup 恢复),不动 worktree。
10
10
  // services 为空 = 整功能;否则只这些服务
11
+ // 幂等:未占槽 / 指定服务当前未占用 → 返回空,交调用方按 no-op 处理(重复执行不报错)。
12
+ // 仅对「未知服务名」(typo,根本不在 config)报错。
11
13
  function occupantsOf(cfg, feature, services = []) {
14
+ for (const s of services)
15
+ if (!cfg.services[s])
16
+ throw new Error(t(`未知服务「${s}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${s}". Run \`worktree-bay doctor\` to see configured services.`));
12
17
  const slot = slotOfFeature(cfg, feature);
13
18
  if (slot === undefined)
14
- throw new Error(t(`功能「${feature}」未占槽。先 \`worktree-bay up ${feature} <服务...>\` 起它。`, `feature "${feature}" hasn't claimed a slot. Run \`worktree-bay up ${feature} <services...>\` first.`));
19
+ return [];
15
20
  const all = scanOccupancy(cfg).get(slot) ?? [];
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));
21
+ return services.length ? all.filter((o) => services.includes(o.service)) : all;
22
22
  }
23
23
  function ctxOf(cfg, o) {
24
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) };
@@ -37,7 +37,7 @@ export async function stopCommand(cfg, feature, services = []) {
37
37
  await stopRuntime(ctxOf(cfg, o));
38
38
  }
39
39
  if (!any)
40
- log(t('没有可停止的运行体(相关服务未配置 start/stop)', 'nothing to stop (no start/stop configured for those services)'));
40
+ log(c.dim(t('没有可停止的运行体(功能未占槽,或相关服务未配置 start/stop)', 'nothing to stop (feature has no slot, or those services have no start/stop)')));
41
41
  });
42
42
  }
43
43
  export async function startCommand(cfg, feature, services = []) {
@@ -51,14 +51,16 @@ export async function startCommand(cfg, feature, services = []) {
51
51
  await ensureRuntime(ctxOf(cfg, o));
52
52
  }
53
53
  if (!any)
54
- log(t('没有可启动的运行体(相关服务未配置 start/stop)', 'nothing to start (no start/stop configured for those services)'));
54
+ log(c.dim(t(`没有可启动的运行体(功能未占槽——先 \`worktree-bay up ${feature} <服务...>\`,或相关服务未配 start/stop)`, `nothing to start (feature has no slot run \`worktree-bay up ${feature} <services...>\` first, or those services have no start/stop)`)));
55
55
  });
56
56
  }
57
57
  export async function restartCommand(cfg, feature, services = []) {
58
58
  await withLock(cfg.workspaceRoot, async () => {
59
+ let any = false;
59
60
  for (const o of occupantsOf(cfg, feature, services)) {
60
61
  if (!hasRuntime(cfg, o.service))
61
62
  continue;
63
+ any = true;
62
64
  const ctx = ctxOf(cfg, o);
63
65
  log(c.bold(c.cyan(o.service)) + c.dim(t(' · 重启…', ' · restarting…')));
64
66
  await stopRuntime(ctx);
@@ -69,5 +71,7 @@ export async function restartCommand(cfg, feature, services = []) {
69
71
  } // 等端口释放
70
72
  await ensureRuntime(ctx);
71
73
  }
74
+ if (!any)
75
+ log(c.dim(t('没有可重启的运行体(功能未占槽,或相关服务未配置 start/stop)', 'nothing to restart (feature has no slot, or those services have no start/stop)')));
72
76
  });
73
77
  }
@@ -9,24 +9,32 @@ 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
- // services 为空 = 整功能;否则只这些服务(顺带校验服务名确实在该功能里)
12
+ // services 为空 = 整功能;否则只这些服务。幂等:未占槽 / 指定服务当前未占用 → 返回空(no-op),
13
+ // 仅对「未知服务名」(typo,根本不在 config)报错。
13
14
  export function resolveRm(cfg, feature, services = []) {
15
+ for (const s of services)
16
+ if (!cfg.services[s])
17
+ throw new Error(t(`未知服务「${s}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${s}". Run \`worktree-bay doctor\` to see configured services.`));
14
18
  const slot = slotOfFeature(cfg, feature);
15
19
  if (slot === undefined)
16
- throw new Error(t(`功能「${feature}」未占槽,无需拆除。用 \`worktree-bay ls\` 看在用的功能。`, `feature "${feature}" has no slot — nothing to tear down. See \`worktree-bay ls\`.`));
20
+ return [];
17
21
  const all = scanOccupancy(cfg).get(slot) ?? [];
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));
22
+ return services.length ? all.filter((o) => services.includes(o.service)) : all;
24
23
  }
25
24
  export async function rmCommand(cfg, feature, services, force) {
26
25
  await withLock(cfg.workspaceRoot, async () => {
26
+ const slot = slotOfFeature(cfg, feature);
27
+ if (slot === undefined) {
28
+ log(c.green('✓') + ' ' + t(`功能「${feature}」未占槽,无需拆除(已是目标状态)`, `feature "${feature}" has no slot — nothing to tear down (already in target state)`));
29
+ return;
30
+ }
27
31
  let removed = 0;
28
32
  const wholeFeature = services.length === 0;
29
33
  const occs = resolveRm(cfg, feature, services);
34
+ if (occs.length === 0) {
35
+ log(c.dim(t('指定服务当前未占用,无需拆除(已是目标状态)', 'those services aren\'t occupied — nothing to tear down (already in target state)')));
36
+ return;
37
+ }
30
38
  for (const o of occs) {
31
39
  const repo = repoPath(cfg, o.service);
32
40
  const branch = currentBranch(o.dir);
@@ -47,7 +55,6 @@ export async function rmCommand(cfg, feature, services, force) {
47
55
  await withProgress(t(`移除 ${o.service} 的 worktree`, `removing ${o.service} worktree`), () => removeWorktree(repo, o.dir, force));
48
56
  removed++;
49
57
  }
50
- const slot = slotOfFeature(cfg, feature);
51
58
  if (wholeFeature && (scanOccupancy(cfg).get(slot) ?? []).length === 0) {
52
59
  removeLabel(cfg, slot);
53
60
  if (removed === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
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
@@ -161,6 +161,7 @@ worktree-bay gc # 回收已合并的
161
161
 
162
162
  ## 工作原理要点
163
163
 
164
+ - **全命令幂等**:`up / down / start / stop / restart` 重复执行都收敛到同一目标态、不报错——`up` 重入=复用 worktree + 恢复运行体;`start`/`stop` 已在目标态则跳过/仍逐服务给状态;`down` 对未占槽或已拆的服务是友好 no-op。仅「未知服务名」(typo,不在 config)才报错。
164
165
  - **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
165
166
  - **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid;`ls` 行首 `●` 标在跑(绿)/未跑(灰);`stop`/`down` 按端口可靠停。
166
167
  - **运行状态判断 = 端口**:`ls`/`start`/`stop` 判「在不在跑」全用 `pidOnPort`(netstat/lsof,与 ls 同源),不用 connect 探测(docker 发布端口两者会不一致)、也不只看 pid 账本(dir 形态会漂移、docker 无账本记录)。`start` 端口已在监听就跳过、不再误报「恢复」。