worktree-bay 1.5.1 → 2.1.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 +8 -10
- package/dist/cli.js +1 -1
- package/dist/commands/claim.js +3 -4
- package/dist/commands/completion.js +2 -2
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +6 -6
- package/dist/commands/ls.js +7 -8
- package/dist/config.js +13 -11
- package/dist/engine.js +3 -3
- package/dist/mcp.js +8 -5
- package/dist/ports.js +2 -2
- package/package.json +1 -1
- package/skill.md +14 -19
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
> 配置驱动、与语言/技术栈无关的 **git worktree 槽位 + 端口编排器**——为多服务并行开发而生。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
一个功能来了,先**占一个槽位 `N`**,用到哪个服务就在哪个服务开一个 worktree 挂进这个槽。每个服务有自己的**端口段**,同槽的各服务都取段里的第 `N` 个端口(`服务基址 + N`)、各自独立进程,前端自动接上同槽的后端。工具替你管好 worktree 路径、端口分配、依赖、`.env` 注入与回收。
|
|
9
9
|
|
|
10
10
|
## 为什么
|
|
11
11
|
|
|
12
12
|
在 monorepo / 多仓工作区里并行开发多个功能时,git worktree 能隔离代码,但隔离不了运行时——端口会撞、依赖要重装、前端连不上你本地起在偏移端口的后端。`worktree-bay` 在 worktree 之上补一层「**功能 = 槽位**」的编排:
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- **端口不撞**:每个服务有自己的端口段,功能占槽 `N` → 各服务用 `自己的基址 + N`,与主 dev(槽 0)和其它槽天然错开。
|
|
15
15
|
- **免重装**:依赖从主 checkout 拷贝(或按服务自定义安装命令),不必每个 worktree 从头装。
|
|
16
16
|
- **前端自接后端**:前端按「同槽上游服务」自动把 api base 指向本槽后端端口。
|
|
17
17
|
- **不泄漏**:槽位占用从文件系统派生;`gc` 合并感知回收(已并入主分支且干净才删,保守不误删)。
|
|
@@ -52,12 +52,10 @@ worktree-bay gc
|
|
|
52
52
|
```jsonc
|
|
53
53
|
{
|
|
54
54
|
"workspaceRoot": "/path/to/workspace",
|
|
55
|
-
"portBase": 6000,
|
|
56
|
-
"slotSpan": 10,
|
|
57
55
|
"maxSlots": 9,
|
|
58
56
|
"services": {
|
|
59
57
|
"api": {
|
|
60
|
-
"
|
|
58
|
+
"port": 6001, // 端口段基址(= 主 dev/槽0),槽 N 用 6001+N
|
|
61
59
|
"vars": { "project": "myapi-{slug}" },
|
|
62
60
|
"copy": [".env", "vendor"], // 从主 checkout 递归拷文件/目录
|
|
63
61
|
"env": { ".env": { "APP_PORT": "{port}" } }, // 合并键值进 dotenv(保留其它键)
|
|
@@ -67,7 +65,7 @@ worktree-bay gc
|
|
|
67
65
|
"run": { "test": ["composer", "run", "test"] } // 命名命令
|
|
68
66
|
},
|
|
69
67
|
"lms": {
|
|
70
|
-
"
|
|
68
|
+
"port": 6011, // 与 api 段不重叠(间距 > maxSlots)
|
|
71
69
|
"upstream": { "service": "api", "fallback": "http://localhost:6001" }, // → {upstreamBase}
|
|
72
70
|
"env": { ".env.dev.local": { "VITE_API_BASE_URL": "{upstreamBase}" } },
|
|
73
71
|
"setup": "pnpm install",
|
|
@@ -81,7 +79,7 @@ worktree-bay gc
|
|
|
81
79
|
|
|
82
80
|
| 原语 | 说明 |
|
|
83
81
|
|---|---|
|
|
84
|
-
| `
|
|
82
|
+
| `port`(必填) | 本服务端口段基址(= 主 dev/槽0);槽 N 端口 = `port + N`;各服务的段 `[port, port+maxSlots]` 互不重叠 |
|
|
85
83
|
| `repo` | 仓库目录名(相对 workspaceRoot),默认 = 服务名 |
|
|
86
84
|
| `vars` | 自定义模板变量 |
|
|
87
85
|
| `copy` | 从主 checkout 递归拷贝的文件/目录(含依赖目录) |
|
|
@@ -94,11 +92,11 @@ worktree-bay gc
|
|
|
94
92
|
|
|
95
93
|
### 模板变量
|
|
96
94
|
|
|
97
|
-
`{slot}` `{
|
|
95
|
+
`{slot}` `{port}` `{slug}` `{worktree}` `{repo}` `{upstreamBase}` `{cmd...}`,以及 `vars` 里自定义的。
|
|
98
96
|
|
|
99
97
|
## 工作原理
|
|
100
98
|
|
|
101
|
-
-
|
|
99
|
+
- **按服务分段**:每个服务有自己的端口段(基址 `port` = 主 dev/槽0),功能占槽 `N`(1..`maxSlots`)→ 该服务用 `port + N`。服务数量无上限,各段不重叠即可。
|
|
102
100
|
- **占用从文件系统派生**:槽是否被占,看各服务 `<repo>/.worktrees/s<N>-*` 目录是否存在;`.worktree-bay-slots.json` 只是「功能名 → 槽号」的标签账本(预约)。删了 worktree,槽自动空出。
|
|
103
101
|
- **并发安全**:`claim/add/rm/gc` 全程持工作区原子锁。
|
|
104
102
|
- **前端自接**:前端有 `upstream` 时,若同槽已起该上游服务的 worktree,就把 api base 指向本槽端口;否则用 `fallback`。
|
|
@@ -128,7 +126,7 @@ worktree-bay completion install
|
|
|
128
126
|
}
|
|
129
127
|
```
|
|
130
128
|
|
|
131
|
-
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_up / ls / add / run / down / gc
|
|
129
|
+
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(或设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_up / ls / add / path / run / down / gc / skill`(`ls` 以 JSON 返回各 worktree 路径,`path` 直接给某功能某服务的目录,`skill` 取完整指南)。
|
|
132
130
|
|
|
133
131
|
## 许可证
|
|
134
132
|
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ program.command('init').description('在当前工作区生成 worktree-bay.confi
|
|
|
40
40
|
catch (e) {
|
|
41
41
|
die(e.message);
|
|
42
42
|
} });
|
|
43
|
-
program.command('claim <feature>').description('
|
|
43
|
+
program.command('claim <feature>').description('为功能占一个槽位(打印各服务在该槽的端口)')
|
|
44
44
|
.action(async (f) => { try {
|
|
45
45
|
await claimCommand(loadConfig(process.cwd()), f);
|
|
46
46
|
}
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { withLock } from '../lock.js';
|
|
2
2
|
import { claim } from '../slots.js';
|
|
3
|
-
import {
|
|
3
|
+
import { portOf } from '../ports.js';
|
|
4
4
|
import { log } from '../util/log.js';
|
|
5
5
|
export async function claimCommand(cfg, feature) {
|
|
6
6
|
const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature));
|
|
7
|
-
|
|
8
|
-
log(`功能 "${feature}" → 槽 ${slot}(块 ${base})`);
|
|
7
|
+
log(`功能 "${feature}" → 槽 ${slot}`);
|
|
9
8
|
for (const [n, sp] of Object.entries(cfg.services))
|
|
10
|
-
log(` ${n.padEnd(8)} ${
|
|
9
|
+
log(` ${n.padEnd(8)} ${portOf(sp.port, slot)}`);
|
|
11
10
|
}
|
|
@@ -11,10 +11,10 @@ export function complete(cfg, words) {
|
|
|
11
11
|
return SUBCMDS;
|
|
12
12
|
const sub = prev[0];
|
|
13
13
|
const pos = prev.length;
|
|
14
|
-
const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh'];
|
|
14
|
+
const featureSubs = ['up', 'add', 'rm', 'down', 'run', 'sh', 'path'];
|
|
15
15
|
if (featureSubs.includes(sub) && pos === 1)
|
|
16
16
|
return Object.values(readLabels(cfg));
|
|
17
|
-
if (['add', 'run', 'sh'].includes(sub) && pos === 2)
|
|
17
|
+
if (['add', 'run', 'sh', 'path'].includes(sub) && pos === 2)
|
|
18
18
|
return Object.keys(cfg.services);
|
|
19
19
|
if (sub === 'up' && pos >= 2)
|
|
20
20
|
return Object.keys(cfg.services); // up 接变长服务列表
|
package/dist/commands/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ export function doctor(cfg) {
|
|
|
12
12
|
ok('git 可用');
|
|
13
13
|
else
|
|
14
14
|
bad('git 不可用(worktree 依赖 git)');
|
|
15
|
-
ok(`配置已加载并通过校验(${Object.keys(cfg.services).length} 个服务,槽位 1..${cfg.maxSlots}
|
|
15
|
+
ok(`配置已加载并通过校验(${Object.keys(cfg.services).length} 个服务,槽位 1..${cfg.maxSlots},端口 = 服务基址 + 槽号)`);
|
|
16
16
|
for (const name of Object.keys(cfg.services)) {
|
|
17
17
|
const repo = repoPath(cfg, name);
|
|
18
18
|
if (!fs.existsSync(repo))
|
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { log, warn } from '../util/log.js';
|
|
4
|
-
// 引导生成一份 worktree-bay.config.json:扫描 cwd 子目录里的 git
|
|
4
|
+
// 引导生成一份 worktree-bay.config.json:扫描 cwd 子目录里的 git 仓预填为服务(按服务分配端口段)
|
|
5
5
|
export function initCommand(cwd) {
|
|
6
6
|
const target = path.join(cwd, 'worktree-bay.config.json');
|
|
7
7
|
if (fs.existsSync(target)) {
|
|
@@ -18,19 +18,19 @@ export function initCommand(cwd) {
|
|
|
18
18
|
let services;
|
|
19
19
|
if (repos.length) {
|
|
20
20
|
services = {};
|
|
21
|
-
repos.
|
|
21
|
+
repos.forEach((r, i) => { services[r] = { port: 6001 + i * 10, setup: 'echo TODO: 起本服务的命令', run: { test: ['echo', 'TODO'] } }; });
|
|
22
22
|
}
|
|
23
23
|
else {
|
|
24
24
|
services = {
|
|
25
|
-
api: {
|
|
26
|
-
web: {
|
|
25
|
+
api: { port: 6001, copy: ['.env'], env: { '.env': { APP_PORT: '{port}' } }, setup: 'echo TODO: 起后端', run: { test: ['echo', 'TODO'] } },
|
|
26
|
+
web: { port: 6011, upstream: { service: 'api', fallback: 'http://localhost:6001' }, env: { '.env.local': { VITE_API_BASE_URL: '{upstreamBase}' } }, setup: 'pnpm install', start: 'pnpm dev --port {port}' },
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
const config = { workspaceRoot: cwd,
|
|
29
|
+
const config = { workspaceRoot: cwd, maxSlots: 9, services };
|
|
30
30
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
|
|
31
31
|
log(`✓ 已生成 ${target}`);
|
|
32
32
|
if (repos.length)
|
|
33
|
-
log(` 识别到服务: ${repos.join(', ')}
|
|
33
|
+
log(` 识别到服务: ${repos.join(', ')}(每个分配一段端口,基址间隔 10)`);
|
|
34
34
|
else
|
|
35
35
|
log(` 未识别到子 git 仓,已写入 api/web 示例模板`);
|
|
36
36
|
log(` 下一步:补全各服务的 setup/env/upstream/exec/run。完整配置说明:worktree-bay skill`);
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scanOccupancy, readLabels } from '../slots.js';
|
|
2
|
-
import {
|
|
2
|
+
import { portOf } from '../ports.js';
|
|
3
3
|
import { log } from '../util/log.js';
|
|
4
4
|
export function renderSlots(cfg) {
|
|
5
5
|
const occ = scanOccupancy(cfg);
|
|
@@ -7,9 +7,8 @@ export function renderSlots(cfg) {
|
|
|
7
7
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
8
8
|
const lines = [];
|
|
9
9
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
lines.push(`slot ${n} ${labels[String(n)] ?? '(unnamed)'} block=${base} [${svc.join(', ') || 'no worktree'}]`);
|
|
10
|
+
const svc = (occ.get(n) ?? []).map((o) => `${o.service}@${portOf(cfg.services[o.service].port, n)}`);
|
|
11
|
+
lines.push(`slot ${n} ${labels[String(n)] ?? '(unnamed)'} [${svc.join(', ') || 'no worktree'}]`);
|
|
13
12
|
}
|
|
14
13
|
return lines.join('\n') || '(no slots in use)';
|
|
15
14
|
}
|
|
@@ -17,9 +16,9 @@ export function slotsData(cfg) {
|
|
|
17
16
|
const occ = scanOccupancy(cfg);
|
|
18
17
|
const labels = readLabels(cfg);
|
|
19
18
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
20
|
-
return [...slots].sort((a, b) => a - b).map((n) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
19
|
+
return [...slots].sort((a, b) => a - b).map((n) => ({
|
|
20
|
+
slot: n, feature: labels[String(n)] ?? null,
|
|
21
|
+
services: (occ.get(n) ?? []).map((o) => ({ service: o.service, port: portOf(cfg.services[o.service].port, n), dir: o.dir })),
|
|
22
|
+
}));
|
|
24
23
|
}
|
|
25
24
|
export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
|
package/dist/config.js
CHANGED
|
@@ -3,33 +3,35 @@ import path from 'node:path';
|
|
|
3
3
|
function refs(tpl) { return [...tpl.matchAll(/\{(\w+)\}/g)].map((m) => m[1]); }
|
|
4
4
|
export function parseConfig(configPath) {
|
|
5
5
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
6
|
-
for (const k of ['workspaceRoot', '
|
|
6
|
+
for (const k of ['workspaceRoot', 'maxSlots'])
|
|
7
7
|
if (raw[k] === undefined)
|
|
8
8
|
throw new Error(`config: ${k} required`);
|
|
9
|
+
const maxSlots = raw.maxSlots;
|
|
9
10
|
const services = raw.services ?? {};
|
|
10
|
-
|
|
11
|
+
// 端口段:每个服务 [port, port+maxSlots](port=主 dev/槽0,槽 1..maxSlots 落在段内)
|
|
11
12
|
for (const [name, sp] of Object.entries(services)) {
|
|
12
|
-
if (typeof sp.
|
|
13
|
-
throw new Error(`config: ${name}.
|
|
14
|
-
if (sp.offset < 1 || sp.offset >= raw.slotSpan)
|
|
15
|
-
throw new Error(`config: ${name}.offset must be 1..${raw.slotSpan - 1}`); // V2
|
|
16
|
-
if (offsets.has(sp.offset))
|
|
17
|
-
throw new Error(`config: duplicate offset ${sp.offset} (${name})`); // V1
|
|
18
|
-
offsets.add(sp.offset);
|
|
13
|
+
if (typeof sp.port !== 'number' || sp.port < 1)
|
|
14
|
+
throw new Error(`config: ${name}.port must be a positive number`); // V2
|
|
19
15
|
const repoDir = path.join(raw.workspaceRoot, sp.repo ?? name);
|
|
20
16
|
if (!fs.existsSync(repoDir))
|
|
21
17
|
throw new Error(`config: ${name}.repo dir missing: ${repoDir}`); // V5
|
|
22
18
|
}
|
|
19
|
+
const entries = Object.entries(services);
|
|
20
|
+
for (let i = 0; i < entries.length; i++)
|
|
21
|
+
for (let j = i + 1; j < entries.length; j++) { // V1: 段不重叠
|
|
22
|
+
if (Math.abs(entries[i][1].port - entries[j][1].port) <= maxSlots)
|
|
23
|
+
throw new Error(`config: 服务 ${entries[i][0]} 与 ${entries[j][0]} 端口段重叠(间距需 > maxSlots=${maxSlots})`);
|
|
24
|
+
}
|
|
23
25
|
for (const [name, sp] of Object.entries(services))
|
|
24
26
|
if (sp.upstream && !services[sp.upstream.service])
|
|
25
27
|
throw new Error(`config: ${name}.upstream.service '${sp.upstream.service}' not found`); // V3
|
|
26
|
-
const known = new Set(['slot', '
|
|
28
|
+
const known = new Set(['slot', 'port', 'slug', 'worktree', 'repo', 'upstreamBase', 'cmd']); // V4
|
|
27
29
|
for (const sp of Object.values(services))
|
|
28
30
|
for (const v of Object.values(sp.vars ?? {}))
|
|
29
31
|
for (const ref of refs(v))
|
|
30
32
|
if (!known.has(ref) && !(sp.vars && ref in sp.vars))
|
|
31
33
|
throw new Error(`config: unknown template var {${ref}}`);
|
|
32
|
-
return { workspaceRoot: raw.workspaceRoot,
|
|
34
|
+
return { workspaceRoot: raw.workspaceRoot, maxSlots, services, configDir: path.dirname(configPath) };
|
|
33
35
|
}
|
|
34
36
|
export function loadConfig(startDir) {
|
|
35
37
|
if (process.env.WORKTREE_BAY_CONFIG)
|
package/dist/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { renderTemplate } from './config.js';
|
|
4
|
-
import {
|
|
4
|
+
import { portOf, isPortFree } from './ports.js';
|
|
5
5
|
import { scanOccupancy } from './slots.js';
|
|
6
6
|
import { addWorktree } from './git.js';
|
|
7
7
|
import { runShell, run, spliceArgv, isTTY } from './util/exec.js';
|
|
@@ -23,13 +23,13 @@ export function mergeEnvText(text, kv) {
|
|
|
23
23
|
return out.join('\n');
|
|
24
24
|
}
|
|
25
25
|
export function resolveUpstreamBase(cfg, slot, up, materialized) {
|
|
26
|
-
return materialized ? `http://localhost:${portOf(cfg.
|
|
26
|
+
return materialized ? `http://localhost:${portOf(cfg.services[up.service].port, slot)}` : up.fallback;
|
|
27
27
|
}
|
|
28
28
|
function upstreamMaterialized(cfg, slot, service) {
|
|
29
29
|
return (scanOccupancy(cfg).get(slot) ?? []).some((o) => o.service === service);
|
|
30
30
|
}
|
|
31
31
|
export function buildVars(cfg, ctx) {
|
|
32
|
-
const base = { slot: ctx.slot,
|
|
32
|
+
const base = { slot: ctx.slot, port: portOf(ctx.sp.port, ctx.slot), slug: ctx.slug, worktree: ctx.dir, repo: ctx.repo };
|
|
33
33
|
if (ctx.sp.upstream)
|
|
34
34
|
base.upstreamBase = resolveUpstreamBase(cfg, ctx.slot, ctx.sp.upstream, upstreamMaterialized(cfg, ctx.slot, ctx.sp.upstream.service));
|
|
35
35
|
for (const [k, v] of Object.entries(ctx.sp.vars ?? {}))
|
package/dist/mcp.js
CHANGED
|
@@ -9,30 +9,33 @@ const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.u
|
|
|
9
9
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
10
10
|
export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开发编排器。当你需要在一个多服务工作区里并行开发多个功能、又不想让它们的端口/依赖/数据互相干扰时,用本服务的工具来完成开发工作。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
核心模型:每个服务有自己的端口段(基址 = 主 dev/槽0);一个功能占一个「槽位 N」,用到哪些服务就在哪些服务上各开一个 git worktree 挂进这个槽,该服务端口 = 基址 + N,自动错开,前端自动连到同槽的后端。
|
|
13
13
|
|
|
14
14
|
推荐工作流:
|
|
15
15
|
1. 起新功能:调用 worktree_bay_up,传功能名 + 要改的服务列表(如 ["api","lms"])。它会自动占槽、为每个服务开 worktree、拷依赖、注入端口并起服务。
|
|
16
|
-
2.
|
|
16
|
+
2. 定位代码:用 worktree_bay_path 拿某功能某服务的 worktree 绝对路径,进去改代码;或 worktree_bay_ls(JSON,含各 worktree 路径)总览全局。
|
|
17
17
|
3. 在某功能的某服务里跑测试/命令:worktree_bay_run(name 用配置里定义的,如 "test")。
|
|
18
18
|
4. 收尾:分支合并后,先 worktree_bay_gc 看可回收项,再 worktree_bay_down 拆掉该功能。
|
|
19
19
|
|
|
20
20
|
要点:
|
|
21
|
-
- 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/run/down。
|
|
21
|
+
- 同一个功能从头到尾用同一个功能名(它同时是默认分支名)贯穿 up/path/run/down。
|
|
22
22
|
- 只起这个功能「实际要改」的服务,不要全起。
|
|
23
23
|
- 拿不准当前状态时先调 worktree_bay_ls。
|
|
24
24
|
- worktree_bay_gc 默认只读(dry-run 列出建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,安全保守、不会误删未完成的工作。
|
|
25
25
|
- 要写或修改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节时,先调用 worktree_bay_skill 获取完整的使用与配置指南(含每个配置原语、模板变量、校验规则与完整示例)。`;
|
|
26
26
|
const str = { type: 'string' };
|
|
27
27
|
export const TOOLS = [
|
|
28
|
-
{ name: 'worktree_bay_ls', description: '
|
|
29
|
-
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls'] },
|
|
28
|
+
{ name: 'worktree_bay_ls', description: '列出所有功能槽位与占用(JSON:每槽的功能名、已起服务及端口、各 worktree 绝对路径),用于总览当前并行开发状态',
|
|
29
|
+
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls', '--json'] },
|
|
30
30
|
{ name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。',
|
|
31
31
|
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature', 'services'] },
|
|
32
32
|
toArgs: (a) => ['up', String(a.feature), ...(a.services ?? [])] },
|
|
33
33
|
{ name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)',
|
|
34
34
|
inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str }, required: ['feature', 'service'] },
|
|
35
35
|
toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : [])] },
|
|
36
|
+
{ name: 'worktree_bay_path', description: '打印某功能某服务的 worktree 绝对路径——up 之后用它定位代码目录,再进去改文件',
|
|
37
|
+
inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature', 'service'] },
|
|
38
|
+
toArgs: (a) => ['path', String(a.feature), String(a.service)] },
|
|
36
39
|
{ name: 'worktree_bay_run', description: '在某功能某服务的运行体里跑预设命令(如 test),可透传额外参数',
|
|
37
40
|
inputSchema: { type: 'object', properties: { feature: str, service: str, name: str, args: { type: 'array', items: str } }, required: ['feature', 'service', 'name'] },
|
|
38
41
|
toArgs: (a) => ['run', String(a.feature), String(a.service), String(a.name), ...(a.args ?? [])] },
|
package/dist/ports.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
|
-
|
|
3
|
-
export function portOf(
|
|
2
|
+
// 按服务分段:某服务在某槽的端口 = 服务基址(主 dev/槽0) + 槽号
|
|
3
|
+
export function portOf(servicePort, slot) { return servicePort + slot; }
|
|
4
4
|
export function isPortFree(port) {
|
|
5
5
|
return new Promise((resolve) => {
|
|
6
6
|
const srv = net.createServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.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
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## 核心模型:功能 = 槽位
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- 每个**服务**有自己的**端口段**:基址 `port` 就是它的主 dev 端口(= 槽 0),段为 `[port, port+maxSlots]`。
|
|
8
|
+
- 一个**功能**认领一个**槽位 `N`**(1..maxSlots);功能用到哪些**服务**,就在那些服务各开一个 **git worktree** 挂进这个槽,该服务在本槽的端口 = `service.port + N`,与主 dev(槽 0)和其它槽天然错开、互不相撞。
|
|
9
9
|
- 同槽的前端服务自动把 api base 指向同槽的后端端口。
|
|
10
10
|
- 槽位占用从文件系统派生(看 `<repo>/.worktrees/s<N>-*` 是否存在),删了 worktree 槽自动空出。
|
|
11
11
|
|
|
@@ -27,9 +27,9 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
27
27
|
| `worktree-bay init` | 在当前工作区生成 `worktree-bay.config.json`(扫描子 git 仓预填服务) |
|
|
28
28
|
| `worktree-bay doctor` | 体检:git 是否可用、配置是否有效、各服务仓是否就绪 |
|
|
29
29
|
| `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名) |
|
|
30
|
-
| `worktree-bay claim <feature>` |
|
|
30
|
+
| `worktree-bay claim <feature>` | 只占一个槽、打印各服务在该槽的端口(不开 worktree) |
|
|
31
31
|
| `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>` |
|
|
32
|
-
| `worktree-bay ls [--json]` |
|
|
32
|
+
| `worktree-bay ls [--json]` | 列出所有槽位:功能名、已起服务及端口;`--json` 输出结构化数据(含 worktree 绝对路径,便于脚本/AI 消费)。合并状态由 `gc` 判定,`ls` 不查(避免每次都 `git fetch`) |
|
|
33
33
|
| `worktree-bay path <feature> <service>` | 打印某服务 worktree 的绝对路径(可 `cd $(worktree-bay path f api)`) |
|
|
34
34
|
| `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
|
|
35
35
|
| `worktree-bay sh <feature> <service>` | 进入某服务运行体的 shell |
|
|
@@ -63,19 +63,17 @@ worktree-bay gc # 回收已合并的
|
|
|
63
63
|
| 字段 | 类型 | 说明 |
|
|
64
64
|
|---|---|---|
|
|
65
65
|
| `workspaceRoot` | string | 工作区根绝对路径,各服务仓在其下 |
|
|
66
|
-
| `
|
|
67
|
-
| `slotSpan` | number | 每个槽位的端口跨度(块大小),如 `10` |
|
|
68
|
-
| `maxSlots` | number | 最大并行功能数,如 `9` |
|
|
66
|
+
| `maxSlots` | number | 最大并行功能数(每个服务预留 `maxSlots` 个端口),如 `9` |
|
|
69
67
|
| `services` | object | 服务名 → 服务定义(见下) |
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
**端口模型(按服务分段)**:每个服务有自己的端口段,基址 `port` 就是它的主 dev 端口(= 槽 0);某服务在某槽 N 的端口 = `service.port + N`(槽 1..maxSlots)。
|
|
70
|
+
例:`api.port=6001` → 槽 1 用 6002、槽 2 用 6003…;`lms.port=6011` → 槽 1 用 6012…。**服务数量无上限**,只要各服务端口段(`[port, port+maxSlots]`)互不重叠即可。
|
|
73
71
|
|
|
74
72
|
### 服务定义原语
|
|
75
73
|
|
|
76
74
|
| 原语 | 必填 | 类型 | 说明 |
|
|
77
75
|
|---|---|---|---|
|
|
78
|
-
| `
|
|
76
|
+
| `port` | ✅ | number | 本服务端口段基址(= 主 dev/槽0 端口);各服务的段 `[port, port+maxSlots]` 互不重叠 |
|
|
79
77
|
| `repo` | | string | 仓库目录名(相对 workspaceRoot),**默认 = 服务名** |
|
|
80
78
|
| `vars` | | object | 自定义模板变量,值里可引用基础变量,如 `{ "project": "myapi-{slug}" }` |
|
|
81
79
|
| `copy` | | string[] | 挂入时从主 checkout **递归拷贝**到 worktree 的文件/目录(含依赖目录,如 `vendor`)。含符号链接也安全(会跟随拷目标内容) |
|
|
@@ -94,8 +92,7 @@ worktree-bay gc # 回收已合并的
|
|
|
94
92
|
| 变量 | 含义 |
|
|
95
93
|
|---|---|
|
|
96
94
|
| `{slot}` | 槽位号 |
|
|
97
|
-
| `{
|
|
98
|
-
| `{port}` | 本服务端口 `blockBase + offset` |
|
|
95
|
+
| `{port}` | 本服务端口 `service.port + slot` |
|
|
99
96
|
| `{slug}` | worktree 目录名 `s<slot>-<分支归一化>` |
|
|
100
97
|
| `{worktree}` | worktree 绝对路径 |
|
|
101
98
|
| `{repo}` | 服务仓绝对路径 |
|
|
@@ -105,8 +102,8 @@ worktree-bay gc # 回收已合并的
|
|
|
105
102
|
|
|
106
103
|
### 加载时强制校验
|
|
107
104
|
|
|
108
|
-
1.
|
|
109
|
-
2. `
|
|
105
|
+
1. 各服务端口段 `[port, port+maxSlots]` 互不重叠(任意两服务 `|portA - portB| > maxSlots`);
|
|
106
|
+
2. `port` 必填且为正数;
|
|
110
107
|
3. `upstream.service` 必须存在于 `services`;
|
|
111
108
|
4. 所有模板里引用的 `{var}` 可解析(基础变量或本服务 vars 已声明);
|
|
112
109
|
5. `repo` 指向的目录存在。
|
|
@@ -117,12 +114,10 @@ worktree-bay gc # 回收已合并的
|
|
|
117
114
|
```jsonc
|
|
118
115
|
{
|
|
119
116
|
"workspaceRoot": "/path/to/workspace",
|
|
120
|
-
"portBase": 6000,
|
|
121
|
-
"slotSpan": 10,
|
|
122
117
|
"maxSlots": 9,
|
|
123
118
|
"services": {
|
|
124
119
|
"api": {
|
|
125
|
-
"
|
|
120
|
+
"port": 6001,
|
|
126
121
|
"vars": { "project": "myapi-{slug}" },
|
|
127
122
|
"copy": [".env", "vendor"],
|
|
128
123
|
"env": { ".env": { "APP_PORT": "{port}", "CACHE_PREFIX": "dev:{slug}:" } },
|
|
@@ -132,7 +127,7 @@ worktree-bay gc # 回收已合并的
|
|
|
132
127
|
"run": { "test": ["composer", "run", "test"], "migrate": ["php", "artisan", "migrate"] }
|
|
133
128
|
},
|
|
134
129
|
"web": {
|
|
135
|
-
"
|
|
130
|
+
"port": 6011,
|
|
136
131
|
"upstream": { "service": "api", "fallback": "http://localhost:6001" },
|
|
137
132
|
"env": { ".env.local": { "VITE_API_BASE_URL": "{upstreamBase}" } },
|
|
138
133
|
"setup": "pnpm install",
|
|
@@ -157,7 +152,7 @@ worktree-bay gc # 回收已合并的
|
|
|
157
152
|
|
|
158
153
|
## 给 AI(MCP)
|
|
159
154
|
|
|
160
|
-
`worktree-bay mcp` 暴露工具:`worktree_bay_up / ls / add / run / down / gc / skill
|
|
155
|
+
`worktree-bay mcp` 暴露工具:`worktree_bay_up / ls / add / path / run / down / gc / skill`。`ls` 以 JSON 返回(含各 worktree 绝对路径),`path` 直接给某功能某服务的 worktree 目录——拿到路径后即可进去改代码。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
|
|
161
156
|
|
|
162
157
|
## 常见坑
|
|
163
158
|
|