worktree-bay 4.1.0 → 4.2.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
@@ -30,7 +30,8 @@ npm i -g worktree-bay
30
30
 
31
31
  ```bash
32
32
  # 一条命令起整个功能:自动占槽 + 在 api/lms 上开 worktree(分支默认 = 功能名)
33
- worktree-bay up drill-fix api lms
33
+ # -d 记录这个槽的用途,写进槽账本,ls 会显示、重入时参考(up/add/claim 都支持)
34
+ worktree-bay up drill-fix api lms -d "修演练页 bug"
34
35
 
35
36
  # 看占用
36
37
  worktree-bay ls
package/dist/cli.js CHANGED
@@ -53,9 +53,10 @@ program.command('init').description(t('在当前工作区生成 worktree-bay.con
53
53
  catch (e) {
54
54
  die(e.message);
55
55
  } });
56
- program.command('claim <feature>').description(t('为功能占一个槽位(打印各服务在该槽的端口)', 'claim a slot for a feature (prints each service\'s port in that slot)'))
57
- .action(async (f) => { try {
58
- await claimCommand(loadConfig(process.cwd()), f);
56
+ program.command('claim <feature> [description]').description(t('为功能占一个槽位(打印各服务在该槽的端口);可选 description 记录这个槽是干嘛的,供重入参考', 'claim a slot for a feature (prints each service\'s port in that slot); optional description records what this slot is for, as a re-entry hint'))
57
+ .option('-d, --description <text>', t('这个槽的介绍/用途(也可用位置参数传)', 'what this slot is for (can also be passed as the positional arg)'))
58
+ .action(async (f, desc, o) => { try {
59
+ await claimCommand(loadConfig(process.cwd()), f, o.description ?? desc);
59
60
  }
60
61
  catch (e) {
61
62
  die(e.message);
@@ -67,8 +68,9 @@ program.command('path <feature> <service>').description(t('打印某功能某服
67
68
  program.command('doctor').description(t('体检:git/配置/各服务仓是否就绪', 'health check: git / config / each service repo readiness'))
68
69
  .action(() => sync(doctorCommand));
69
70
  program.command('up <feature> <services...>').description(t('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)', 'bring up multiple services for a feature (auto-claim + branch defaults to feature name)'))
70
- .action(async (f, services) => { try {
71
- await upCommand(loadConfig(process.cwd()), f, services);
71
+ .option('-d, --description <text>', t('这个槽的介绍/用途,写进槽账本供重入参考', 'what this slot is for; recorded in the slot ledger as a re-entry hint'))
72
+ .action(async (f, services, o) => { try {
73
+ await upCommand(loadConfig(process.cwd()), f, services, undefined, o.description);
72
74
  }
73
75
  catch (e) {
74
76
  die(e.message);
@@ -76,8 +78,9 @@ catch (e) {
76
78
  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)'))
77
79
  .option('--branch <branch>', t('要创建的分支名(默认 = 功能名)', 'branch to create (default = feature name)'))
78
80
  .option('--base <base>', t('分支基点(默认 = origin/<主分支>)', 'base ref for the branch (default = origin/<main>)'))
81
+ .option('-d, --description <text>', t('这个槽的介绍/用途,写进槽账本供重入参考', 'what this slot is for; recorded in the slot ledger as a re-entry hint'))
79
82
  .action(async (f, s, b, base, o) => { try {
80
- await addCommand(loadConfig(process.cwd()), f, s, o.branch ?? b, o.base ?? base);
83
+ await addCommand(loadConfig(process.cwd()), f, s, o.branch ?? b, o.base ?? base, o.description);
81
84
  }
82
85
  catch (e) {
83
86
  die(e.message);
@@ -9,17 +9,17 @@ import { mainBranch } from '../git.js';
9
9
  import { log } from '../util/log.js';
10
10
  import { color as c } from '../util/color.js';
11
11
  import { t } from '../i18n.js';
12
- export function resolveAdd(cfg, feature, service, branch) {
12
+ export function resolveAdd(cfg, feature, service, branch, description) {
13
13
  if (!cfg.services[service])
14
14
  throw new Error(t(`未知服务「${service}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${service}". Run \`worktree-bay doctor\` to see configured services.`));
15
- const slot = claim(cfg, feature);
15
+ const slot = claim(cfg, feature, { branch, description });
16
16
  const slug = worktreeDirName(slot, slugify(branch));
17
17
  return { service, slot, slug, dir: path.join(repoPath(cfg, service), '.worktrees', slug), repo: repoPath(cfg, service) };
18
18
  }
19
- export async function addCommand(cfg, feature, service, branch, base) {
19
+ export async function addCommand(cfg, feature, service, branch, base, description) {
20
20
  const br = branch || feature; // 默认分支 = 功能名
21
21
  await withLock(cfg.workspaceRoot, async () => {
22
- const p = resolveAdd(cfg, feature, service, br);
22
+ const p = resolveAdd(cfg, feature, service, br, description);
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) };
@@ -45,7 +45,7 @@ export async function addCommand(cfg, feature, service, branch, base) {
45
45
  });
46
46
  }
47
47
  // up: 一条命令为功能批量起多个服务(claim 自动 + 各服务默认分支)。每个服务由 addCommand 自己打印简洁标题。
48
- export async function upCommand(cfg, feature, services, base) {
48
+ export async function upCommand(cfg, feature, services, base, description) {
49
49
  for (const service of services)
50
- await addCommand(cfg, feature, service, undefined, base);
50
+ await addCommand(cfg, feature, service, undefined, base, description);
51
51
  }
@@ -4,9 +4,11 @@ import { portOf } from '../ports.js';
4
4
  import { log } from '../util/log.js';
5
5
  import { color as c } from '../util/color.js';
6
6
  import { t } from '../i18n.js';
7
- export async function claimCommand(cfg, feature) {
8
- const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature));
7
+ export async function claimCommand(cfg, feature, description) {
8
+ const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature, { description }));
9
9
  log(c.bold(c.cyan(t(`功能 "${feature}" → 槽 ${slot}`, `feature "${feature}" → slot ${slot}`))));
10
+ if (description)
11
+ log(c.dim(' ' + description));
10
12
  for (const [n, sp] of Object.entries(cfg.services))
11
13
  log(` ${n.padEnd(8)} ${portOf(sp.port, slot)}`);
12
14
  }
@@ -1,4 +1,4 @@
1
- import { scanOccupancy, readLabels } from '../slots.js';
1
+ import { scanOccupancy, readSlots } from '../slots.js';
2
2
  import { portOf } from '../ports.js';
3
3
  import { log } from '../util/log.js';
4
4
  import { pidOnPort } from '../proc.js';
@@ -9,22 +9,38 @@ import { t } from '../i18n.js';
9
9
  function running(port) { return !!pidOnPort(port); }
10
10
  export function renderSlots(cfg) {
11
11
  const occ = scanOccupancy(cfg);
12
- const labels = readLabels(cfg);
13
- const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
12
+ const metas = readSlots(cfg);
13
+ const slots = new Set([...occ.keys(), ...Object.keys(metas).map(Number)]);
14
14
  const lines = [];
15
15
  for (const n of [...slots].sort((a, b) => a - b)) {
16
+ const meta = metas[String(n)];
16
17
  const svc = (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); const dot = running(p) ? c.green('●') : c.dim('●'); return `${dot}${o.service}@${p}`; });
17
- lines.push(`${c.bold(c.cyan(String(n)))}${c.dim(':')} ${c.bold(labels[String(n)] ?? t('(未命名)', '(unnamed)'))} [${svc.join(', ') || c.dim(t('无 worktree', 'no worktree'))}]`);
18
+ lines.push(`${c.bold(c.cyan(String(n)))}${c.dim(':')} ${c.bold(meta?.feature ?? t('(未命名)', '(unnamed)'))} [${svc.join(', ') || c.dim(t('无 worktree', 'no worktree'))}]`);
19
+ if (meta) { // 副行:介绍 + 分支(异于功能名时) + 创建日期,给重入/总览更多上下文
20
+ const bits = [];
21
+ if (meta.description)
22
+ bits.push(meta.description);
23
+ if (meta.branch && meta.branch !== meta.feature)
24
+ bits.push(t(`分支 ${meta.branch}`, `branch ${meta.branch}`));
25
+ if (meta.createdAt)
26
+ bits.push(meta.createdAt.slice(0, 10));
27
+ if (bits.length)
28
+ lines.push(' ' + c.dim(bits.join(' · ')));
29
+ }
18
30
  }
19
31
  return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
20
32
  }
21
33
  export function slotsData(cfg) {
22
34
  const occ = scanOccupancy(cfg);
23
- const labels = readLabels(cfg);
24
- const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
25
- return [...slots].sort((a, b) => a - b).map((n) => ({
26
- slot: n, feature: labels[String(n)] ?? null,
27
- services: (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); return { service: o.service, port: p, dir: o.dir, running: running(p) }; }),
28
- }));
35
+ const metas = readSlots(cfg);
36
+ const slots = new Set([...occ.keys(), ...Object.keys(metas).map(Number)]);
37
+ return [...slots].sort((a, b) => a - b).map((n) => {
38
+ const meta = metas[String(n)];
39
+ return {
40
+ slot: n, feature: meta?.feature ?? null, branch: meta?.branch ?? null,
41
+ description: meta?.description ?? null, createdAt: meta?.createdAt ?? null,
42
+ services: (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); return { service: o.service, port: p, dir: o.dir, running: running(p) }; }),
43
+ };
44
+ });
29
45
  }
30
46
  export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
package/dist/mcp.js CHANGED
@@ -26,7 +26,7 @@ export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开
26
26
 
27
27
  要点:
28
28
  - 一个功能从头到尾用同一个功能名(= 默认分支名)。
29
- - 每个新任务都用一个【新的功能名】调 worktree_bay_up,让工具自动占一个空槽——【不要】去 worktree_bay_ls 挑一个现成的槽来复用。ls 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染。唯一例外:你是在【继续同一个功能】之前没跑完的工作(功能名相同),这时 up 会幂等复用它自己的槽。
29
+ - 每个新任务都用一个【新的功能名】调 worktree_bay_up,让工具自动占一个空槽——【不要】去 worktree_bay_ls 挑一个现成的槽来复用。ls 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染。唯一例外:你是在【继续同一个功能】之前没跑完的工作(功能名相同),这时 up 会幂等复用它自己的槽。起 up / claim 时【带上 description】写明这个槽是干嘛的(记进槽账本、ls 会显示、ls --json 含 description/branch/createdAt),方便你之后重入时判断某个槽在做什么。
30
30
  - 只起「实际要改」的服务,不要全起;不知道有哪些服务名先调 worktree_bay_doctor。
31
31
  - 拿不准当前状态先调 worktree_bay_ls;dev server 起不来或报错就调 worktree_bay_logs 看日志尾部排障。
32
32
  - worktree_bay_gc 默认只读(dry-run 列建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,保守不误删。
@@ -38,15 +38,15 @@ export const TOOLS = [
38
38
  inputSchema: { type: 'object', properties: {} }, toArgs: () => ['doctor'] },
39
39
  { name: 'worktree_bay_ls', description: '列出所有功能槽位与占用(JSON:每槽的功能名、已起服务及端口、各 worktree 绝对路径),用于总览当前并行开发状态',
40
40
  inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls', '--json'] },
41
- { name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。',
42
- inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature', 'services'] },
43
- toArgs: (a) => ['up', String(a.feature), ...(a.services ?? [])] },
44
- { name: 'worktree_bay_claim', description: '只为功能占一个槽位并打印各服务在该槽的端口,不开 worktree(一般直接用 up 即可;需要先预览端口/预约槽时用)',
45
- inputSchema: { type: 'object', properties: { feature: str }, required: ['feature'] },
46
- toArgs: (a) => ['claim', String(a.feature)] },
47
- { name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)',
48
- inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str }, required: ['feature', 'service'] },
49
- toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : [])] },
41
+ { name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。【强烈建议带 description】写明这个槽是干嘛的,记进槽账本、`ls` 会显示,供之后重入参考。',
42
+ inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str }, description: str }, required: ['feature', 'services'] },
43
+ toArgs: (a) => ['up', String(a.feature), ...(a.services ?? []), ...(a.description ? ['--description', String(a.description)] : [])] },
44
+ { name: 'worktree_bay_claim', description: '只为功能占一个槽位并打印各服务在该槽的端口,不开 worktree(一般直接用 up 即可;需要先预览端口/预约槽时用)。description 记录这个槽的用途供重入参考。',
45
+ inputSchema: { type: 'object', properties: { feature: str, description: str }, required: ['feature'] },
46
+ toArgs: (a) => ['claim', String(a.feature), ...(a.description ? ['--description', String(a.description)] : [])] },
47
+ { name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)。description 记录这个槽的用途供重入参考。',
48
+ inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str, description: str }, required: ['feature', 'service'] },
49
+ toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : []), ...(a.description ? ['--description', String(a.description)] : [])] },
50
50
  { name: 'worktree_bay_path', description: '打印某功能某服务的 worktree 绝对路径——up 之后用它定位代码目录,再进去改文件',
51
51
  inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature', 'service'] },
52
52
  toArgs: (a) => ['path', String(a.feature), String(a.service)] },
package/dist/slots.js CHANGED
@@ -23,28 +23,66 @@ export function scanOccupancy(cfg) {
23
23
  return map;
24
24
  }
25
25
  function labelPath(cfg) { return path.join(cfg.workspaceRoot, '.worktree-bay-slots.json'); }
26
- export function readLabels(cfg) { const p = labelPath(cfg); return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}; }
27
- function save(cfg, l) { fs.writeFileSync(labelPath(cfg), JSON.stringify(l, null, 2) + '\n'); }
28
- export function writeLabel(cfg, slot, f) { const l = readLabels(cfg); l[String(slot)] = f; save(cfg, l); }
29
- export function removeLabel(cfg, slot) { const l = readLabels(cfg); delete l[String(slot)]; save(cfg, l); }
30
- export function slotOfFeature(cfg, f) { for (const [k, v] of Object.entries(readLabels(cfg)))
31
- if (v === f)
26
+ export function readSlots(cfg) {
27
+ const p = labelPath(cfg);
28
+ if (!fs.existsSync(p))
29
+ return {};
30
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
31
+ const out = {};
32
+ for (const [k, v] of Object.entries(raw))
33
+ out[k] = typeof v === 'string' ? { feature: v } : v;
34
+ return out;
35
+ }
36
+ function save(cfg, store) { fs.writeFileSync(labelPath(cfg), JSON.stringify(store, null, 2) + '\n'); }
37
+ // 兼容旧调用:slot → 功能名(completion/ls 等仍按字符串用)。
38
+ export function readLabels(cfg) { const o = {}; for (const [k, v] of Object.entries(readSlots(cfg)))
39
+ o[k] = v.feature; return o; }
40
+ export function removeLabel(cfg, slot) { const s = readSlots(cfg); delete s[String(slot)]; save(cfg, s); }
41
+ export function slotOfFeature(cfg, f) { for (const [k, v] of Object.entries(readSlots(cfg)))
42
+ if (v.feature === f)
32
43
  return Number(k); return undefined; }
33
44
  export function freeSlot(cfg) {
34
45
  const occ = scanOccupancy(cfg);
35
- const l = readLabels(cfg);
46
+ const s = readSlots(cfg);
36
47
  for (let n = 1; n <= cfg.maxSlots; n++)
37
- if (!occ.has(n) && l[String(n)] === undefined)
48
+ if (!occ.has(n) && s[String(n)] === undefined)
38
49
  return n;
39
50
  throw new Error(t(`没有空闲槽位(1..${cfg.maxSlots} 全部占用)。用 \`worktree-bay gc\` 回收已合并的,或 \`worktree-bay down <功能>\` 拆掉用完的,或调大配置里的 maxSlots。`, `no free slot (1..${cfg.maxSlots} all taken). Reclaim merged ones with \`worktree-bay gc\`, tear down finished ones with \`worktree-bay down <feature>\`, or raise maxSlots in your config.`));
40
51
  }
41
- export function claim(cfg, f) { const e = slotOfFeature(cfg, f); if (e !== undefined)
42
- return e; const n = freeSlot(cfg); writeLabel(cfg, n, f); return n; }
52
+ // 占槽。首次认领写入 { feature, branch?, description?, createdAt };已占同名功能则【补全/更新】非空的
53
+ // branch / description(保留首次的 createdAt)——所以同一功能重入 up/claim 带上新 --description 即可改介绍。
54
+ export function claim(cfg, f, meta = {}) {
55
+ const store = readSlots(cfg);
56
+ const existing = slotOfFeature(cfg, f);
57
+ if (existing !== undefined) {
58
+ const cur = store[String(existing)];
59
+ const next = { ...cur };
60
+ if (meta.branch)
61
+ next.branch = meta.branch;
62
+ if (meta.description)
63
+ next.description = meta.description;
64
+ if (JSON.stringify(next) !== JSON.stringify(cur)) {
65
+ store[String(existing)] = next;
66
+ save(cfg, store);
67
+ }
68
+ return existing;
69
+ }
70
+ const n = freeSlot(cfg);
71
+ const m = { feature: f };
72
+ if (meta.branch)
73
+ m.branch = meta.branch;
74
+ if (meta.description)
75
+ m.description = meta.description;
76
+ m.createdAt = new Date().toISOString();
77
+ store[String(n)] = m;
78
+ save(cfg, store);
79
+ return n;
80
+ }
43
81
  export function pruneEmptyLabels(cfg) {
44
82
  const occ = scanOccupancy(cfg);
45
83
  const removed = [];
46
- for (const [k, v] of Object.entries(readLabels(cfg)))
84
+ for (const [k, v] of Object.entries(readSlots(cfg)))
47
85
  if (!occ.has(Number(k)))
48
- removed.push({ slot: Number(k), feature: v });
86
+ removed.push({ slot: Number(k), feature: v.feature });
49
87
  return removed;
50
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-bay",
3
- "version": "4.1.0",
3
+ "version": "4.2.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
@@ -9,7 +9,7 @@
9
9
  - 同槽的前端服务自动把 api base 指向同槽的后端端口。
10
10
  - 槽位占用从文件系统派生(看 `<repo>/.worktrees/s<N>-*` 是否存在),删了 worktree 槽自动空出。
11
11
 
12
- > **每个新任务都用一个【新功能名】认领新槽**(`up <新功能名> ...`),让工具自动占一个空槽。**不要先 `ls` 再去挑一个现成的槽来复用**——`ls` 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染数据。唯一例外:你在**继续同一个功能**之前没跑完的工作(用同一功能名),此时 `up` 会幂等复用它自己的槽。
12
+ > **每个新任务都用一个【新功能名】认领新槽**(`up <新功能名> ...`),让工具自动占一个空槽。**不要先 `ls` 再去挑一个现成的槽来复用**——`ls` 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染数据。唯一例外:你在**继续同一个功能**之前没跑完的工作(用同一功能名),此时 `up` 会幂等复用它自己的槽。起槽时给 `up`/`claim`/`add` 带 `-d/--description` 写明这个槽是干嘛的——记进槽账本、`ls` 会显示、`ls --json` 含 `description`/`branch`/`createdAt`,方便日后重入判断某个槽在做什么。
13
13
 
14
14
  ---
15
15
 
@@ -30,9 +30,9 @@ worktree-bay completion install # 一键装 shell 补全(可选)
30
30
  |---|---|
31
31
  | `worktree-bay init` | 在当前工作区生成 `worktree-bay.config.json`(扫描子 git 仓预填服务) |
32
32
  | `worktree-bay doctor` | 体检:git 是否可用、配置是否有效、各服务仓是否就绪 |
33
- | `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名) |
34
- | `worktree-bay claim <feature>` | 只占一个槽、打印各服务在该槽的端口(不开 worktree |
35
- | `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>` |
33
+ | `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名)。`-d/--description` 记录这个槽的用途(写进槽账本,`ls` 显示,重入参考) |
34
+ | `worktree-bay claim <feature> [description]` | 只占一个槽、打印各服务在该槽的端口(不开 worktree)。可选 `description`(位置参数或 `-d`)记录槽用途 |
35
+ | `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>`;`-d/--description` 记录槽用途 |
36
36
  | `worktree-bay ls [--json]` | 列出所有槽位:功能名、已起服务及端口;`--json` 输出结构化数据(含 worktree 绝对路径,便于脚本/AI 消费)。合并状态由 `gc` 判定,`ls` 不查(避免每次都 `git fetch`) |
37
37
  | `worktree-bay path <feature> <service>` | 打印某服务 worktree 的绝对路径(可 `cd $(worktree-bay path f api)`) |
38
38
  | `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
@@ -165,7 +165,7 @@ worktree-bay gc # 回收已合并的
165
165
  ## 工作原理要点
166
166
 
167
167
  - **全命令幂等**:`up / down / start / stop / restart` 重复执行都收敛到同一目标态、不报错——`up` 重入=复用 worktree + 恢复运行体;`start`/`stop` 已在目标态则跳过/仍逐服务给状态;`down` 对未占槽或已拆的服务是友好 no-op。仅「未知服务名」(typo,不在 config)才报错。
168
- - **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
168
+ - **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 是槽位元数据账本(每槽记 `feature` / `branch` / `description` / `createdAt`;旧的纯字符串值自动兼容为 `{feature}`),属预约标记,真正占用仍以 worktree 是否存在为准。
169
169
  - **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid。日志**每次启动滚动**(上一轮存一份 `.prev`,当前文件只含本轮 + 一行启动头,排障不被跨会话历史淹没;用 `worktree-bay logs <feature>` 直接看尾部,免拼路径)。`start` 会**阻塞到约定端口被监听才返回**(给 vite 冷启动留 ~25s),所以命令返回即代表就绪、`ls` 行首 `●` 绿即可开工;超时不算失败(可能仍在编译/重启),会提示去看日志。`stop`/`down` 按端口可靠停。
170
170
  - **运行状态判断 = 端口**:`ls`/`start`/`stop` 判「在不在跑」全用 `pidOnPort`(netstat/lsof,与 ls 同源),不用 connect 探测(docker 发布端口两者会不一致)、也不只看 pid 账本(dir 形态会漂移、docker 无账本记录)。`start` 端口已在监听就跳过、不再误报「恢复」。
171
171
  - **stop 严格停「本目录+本端口+本进程」**:① 优先用启动账本(dir 已规范化匹配:相对/绝对/大小写都认);② 账本缺失时**校验后才杀**——Linux/macOS 比对进程 `cwd` 是否为本 worktree,Windows 取不到 cwd 则核对命令行含 `--port <本端口>`(端口按槽唯一,等价确证);都确证不了就**不动它**并如实报告(绝不凭端口盲杀)。每个服务都有状态行:已停 / 端口空闲 / 无法确认未停。