worktree-bay 4.0.0 → 4.0.2
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/dist/commands/add.js +2 -0
- package/dist/commands/ls.js +7 -6
- package/dist/config.js +8 -5
- package/dist/engine.js +14 -0
- package/package.json +1 -1
- package/skill.md +5 -4
package/dist/commands/add.js
CHANGED
|
@@ -35,6 +35,8 @@ export async function addCommand(cfg, feature, service, branch, base) {
|
|
|
35
35
|
}
|
|
36
36
|
catch (e) {
|
|
37
37
|
const m = String(e.message);
|
|
38
|
+
if (/not a valid branch name|cannot be used as a branch name/i.test(m)) // 分支名不合法(先于 base 判,二者都含 "not a valid")
|
|
39
|
+
throw new Error(t(`分支名「${br}」不合法:git 分支名不能以 / 开头或结尾、不能含 // 或 ..。换一个,如 fix/log(去掉前导斜杠)。`, `invalid branch name "${br}": git branch names can't start/end with "/" or contain "//" or "..". Use e.g. fix/log (no leading slash).`));
|
|
38
40
|
if (/invalid reference|unknown revision|ambiguous argument|not a valid|Not a valid object name/i.test(m))
|
|
39
41
|
throw new Error(t(`基分支「${resolvedBase}」无效(该仓可能没有 origin 或对应主分支)。给 add 显式传 base,例如:worktree-bay add ${feature} ${service} ${br} HEAD`, `invalid base ref "${resolvedBase}" (this repo may have no origin or main branch). Pass an explicit base to add, e.g.: worktree-bay add ${feature} ${service} ${br} HEAD`));
|
|
40
42
|
throw e;
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { scanOccupancy, readLabels } from '../slots.js';
|
|
2
2
|
import { portOf } from '../ports.js';
|
|
3
3
|
import { log } from '../util/log.js';
|
|
4
|
-
import {
|
|
4
|
+
import { pidOnPort } from '../proc.js';
|
|
5
5
|
import { color as c } from '../util/color.js';
|
|
6
6
|
import { t } from '../i18n.js';
|
|
7
|
-
//
|
|
8
|
-
|
|
7
|
+
// 「在跑」= 该服务约定端口上有进程监听,覆盖三类:docker(setup) / managed dev server(start) / 外部手起。
|
|
8
|
+
// 只按端口判:不依赖进程账本 dir 精确匹配(dir 相对/绝对形态会漂移),docker 服务也无 managed 记录。
|
|
9
|
+
function running(port) { return !!pidOnPort(port); }
|
|
9
10
|
export function renderSlots(cfg) {
|
|
10
11
|
const occ = scanOccupancy(cfg);
|
|
11
12
|
const labels = readLabels(cfg);
|
|
12
13
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
13
14
|
const lines = [];
|
|
14
15
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
15
|
-
const svc = (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n);
|
|
16
|
-
lines.push(`${c.dim('
|
|
16
|
+
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'))}]`);
|
|
17
18
|
}
|
|
18
19
|
return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
|
|
19
20
|
}
|
|
@@ -23,7 +24,7 @@ export function slotsData(cfg) {
|
|
|
23
24
|
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
24
25
|
return [...slots].sort((a, b) => a - b).map((n) => ({
|
|
25
26
|
slot: n, feature: labels[String(n)] ?? null,
|
|
26
|
-
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(
|
|
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) }; }),
|
|
27
28
|
}));
|
|
28
29
|
}
|
|
29
30
|
export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
|
package/dist/config.js
CHANGED
|
@@ -4,16 +4,19 @@ import { t } from './i18n.js';
|
|
|
4
4
|
function refs(tpl) { return [...tpl.matchAll(/\{(\w+)\}/g)].map((m) => m[1]); }
|
|
5
5
|
export function parseConfig(configPath) {
|
|
6
6
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
if (raw.maxSlots === undefined)
|
|
8
|
+
throw new Error(t('config: 缺少必填字段 maxSlots', 'config: maxSlots required'));
|
|
9
|
+
const configDir = path.dirname(configPath);
|
|
10
|
+
// workspaceRoot 非必选,默认当前目录(= config 所在目录);相对路径相对 config 目录解析,
|
|
11
|
+
// 已是绝对路径时 path.resolve 原样返回(向后兼容,且不再受进程 cwd 影响)
|
|
12
|
+
const workspaceRoot = path.resolve(configDir, raw.workspaceRoot ?? '.');
|
|
10
13
|
const maxSlots = raw.maxSlots;
|
|
11
14
|
const services = raw.services ?? {};
|
|
12
15
|
// 端口段:每个服务 [port, port+maxSlots](port=主 dev/槽0,槽 1..maxSlots 落在段内)
|
|
13
16
|
for (const [name, sp] of Object.entries(services)) {
|
|
14
17
|
if (typeof sp.port !== 'number' || sp.port < 1)
|
|
15
18
|
throw new Error(t(`config: ${name}.port 必须是正整数`, `config: ${name}.port must be a positive number`)); // V2
|
|
16
|
-
const repoDir = path.join(
|
|
19
|
+
const repoDir = path.join(workspaceRoot, sp.repo ?? name);
|
|
17
20
|
if (!fs.existsSync(repoDir))
|
|
18
21
|
throw new Error(t(`config: ${name}.repo 目录不存在: ${repoDir}(检查 workspaceRoot 与 repo 名,或先 git clone)`, `config: ${name}.repo dir missing: ${repoDir} (check workspaceRoot and the repo name, or clone it first)`)); // V5
|
|
19
22
|
}
|
|
@@ -32,7 +35,7 @@ export function parseConfig(configPath) {
|
|
|
32
35
|
for (const ref of refs(v))
|
|
33
36
|
if (!known.has(ref) && !(sp.vars && ref in sp.vars))
|
|
34
37
|
throw new Error(t(`config: 未知模板变量 {${ref}}(只能引用内置变量或本服务 vars 里已声明的)`, `config: unknown template var {${ref}} (only built-in vars or this service's declared vars are allowed)`));
|
|
35
|
-
return { workspaceRoot
|
|
38
|
+
return { workspaceRoot, maxSlots, services, configDir };
|
|
36
39
|
}
|
|
37
40
|
export function loadConfig(startDir) {
|
|
38
41
|
if (process.env.WORKTREE_BAY_CONFIG)
|
package/dist/engine.js
CHANGED
|
@@ -125,14 +125,28 @@ export async function ensureRuntime(ctx) {
|
|
|
125
125
|
await ensureStarted(ctx);
|
|
126
126
|
}
|
|
127
127
|
// 停止运行体:杀 managed dev server + 跑 stop 钩子(docker compose stop)。不动 worktree。
|
|
128
|
+
// 始终给每个服务输出一行状态——哪怕本就没在跑(否则只剩一个裸服务名,看不出结果)。
|
|
129
|
+
// 用端口实判:docker 容器停了端口即释放、dev server 同理,比「有没有 managed 记录」更准,
|
|
130
|
+
// 据此给出诚实状态(本就空闲 / 外部未托管 / 未在运行),不让一个绿勾掩盖「其实没东西在跑」。
|
|
128
131
|
export async function stopRuntime(ctx) {
|
|
129
132
|
const { cfg, sp, dir, service, vars } = ctx;
|
|
133
|
+
const port = Number(vars.port);
|
|
134
|
+
const wasUp = await portInUse(port);
|
|
130
135
|
const stopped = stopManaged(cfg.workspaceRoot, dir);
|
|
131
136
|
if (stopped)
|
|
132
137
|
log(` ${cc.green('✓')} ` + t(`已停止 dev server(pid ${stopped.pid})`, `stopped dev server (pid ${stopped.pid})`));
|
|
133
138
|
if (sp.stop) {
|
|
139
|
+
// stop 钩子始终跑(docker compose stop 幂等,且能收掉 app 端口没监听、但 mysql/redis 等边车还在的情况)。
|
|
134
140
|
const cmd = renderTemplate(sp.stop, vars);
|
|
135
141
|
await runShellLive(cmd, { cwd: dir }, t(`停 ${service}:${cmd}`, `stop ${service}: ${cmd}`));
|
|
142
|
+
if (!wasUp)
|
|
143
|
+
log(` ${cc.dim('•')} ` + t(`(端口 ${port} 此前空闲,${service} 实际并未在对外服务)`, `(port ${port} was idle; ${service} wasn't actually serving)`));
|
|
144
|
+
}
|
|
145
|
+
if (!stopped && !sp.stop) {
|
|
146
|
+
if (wasUp)
|
|
147
|
+
log(` ${cc.yellow('•')} ` + t(`端口 ${port} 仍被占用(外部启动、未托管),请手动停止`, `port ${port} in use (external, unmanaged); stop manually`));
|
|
148
|
+
else
|
|
149
|
+
log(` ${cc.dim('•')} ` + t('未在运行', 'not running'));
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
export function execArgv(ctx, cmd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
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
|
@@ -66,7 +66,7 @@ worktree-bay gc # 回收已合并的
|
|
|
66
66
|
|
|
67
67
|
| 字段 | 类型 | 说明 |
|
|
68
68
|
|---|---|---|
|
|
69
|
-
| `workspaceRoot` | string |
|
|
69
|
+
| `workspaceRoot` | string | **非必选**,工作区根,各服务仓在其下;**默认 = config 文件所在目录**(即省略时等同 `"."`)。可填相对路径(相对 config 目录解析,不受进程 cwd 影响)或绝对路径 |
|
|
70
70
|
| `maxSlots` | number | 最大并行功能数(每个服务预留 `maxSlots` 个端口),如 `9` |
|
|
71
71
|
| `services` | object | 服务名 → 服务定义(见下) |
|
|
72
72
|
|
|
@@ -85,7 +85,7 @@ worktree-bay gc # 回收已合并的
|
|
|
85
85
|
| `upstream` | | object | 声明依赖的上游服务:`{ "service": "api", "fallback": "http://localhost:6001" }`,产出 `{upstreamBase}` 变量 |
|
|
86
86
|
| `setup` | | string | 挂入后执行的 shell 命令(创建/装好运行体,如 `docker compose up -d`、`pnpm install`)。`up` 时跑 |
|
|
87
87
|
| `teardown` | | string | 拆除时执行的 shell 命令(销毁运行体,如 `docker compose down -v`)。`down`/`rm`/`gc` 时跑 |
|
|
88
|
-
| `start` | | string | 长进程 dev server(如 `pnpm dev`)。`up` 时**自动后台启动**(detach + 日志落 `.worktree-bay/logs/`),按端口追踪 pid;由 `start`/`stop`/`restart` 控制,`ls`
|
|
88
|
+
| `start` | | string | 长进程 dev server(如 `pnpm dev`)。`up` 时**自动后台启动**(detach + 日志落 `.worktree-bay/logs/`),按端口追踪 pid;由 `start`/`stop`/`restart` 控制,`ls` 行首 `●` 标在跑(绿)/未跑(灰) |
|
|
89
89
|
| `stop` | | string | 停止该服务 infra 运行体的 shell(如 `docker compose stop`)。供 `stop`/`restart` 用——让 docker 容器能「停而不毁」;`start` 时对配了它的服务重跑 `setup` 幂等恢复 |
|
|
90
90
|
| `exec` | | string[] | 透传命令模板(argv 数组),`{cmd...}` 是 argv splice 占位,防 shell 注入。如 `["docker","exec","-i","{project}-app-1","{cmd...}"]` |
|
|
91
91
|
| `run` | | object | 命名命令:`{ "test": ["composer","run","test"] }`,供 `worktree-bay run <feature> <service> test` 调用 |
|
|
@@ -162,7 +162,8 @@ worktree-bay gc # 回收已合并的
|
|
|
162
162
|
## 工作原理要点
|
|
163
163
|
|
|
164
164
|
- **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 只是「功能名→槽号」标签账本(预约)。
|
|
165
|
-
- **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid;`ls`
|
|
165
|
+
- **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid;`ls` 行首 `●` 标在跑(绿)/未跑(灰);`stop`/`down` 按端口可靠停。
|
|
166
|
+
- **运行状态判断 = 端口**:`ls` 的 `running`、`stop` 的「是否真在跑」一律按约定端口是否被监听判(覆盖 docker / 托管 dev server / 外部手起三类),不依赖 pid 账本(dir 形态会漂移、docker 无账本记录)。`stop` 对每个服务都给状态:已停 / 端口空闲(docker 钩子仍幂等跑一遍)/ 外部未托管需手动停。
|
|
166
167
|
- **并发安全**:`claim/add/up/rm/down/gc` 全程持工作区原子锁。
|
|
167
168
|
- **前端自接后端**:前端有 `upstream` 且同槽该上游服务的 worktree 已建,则 `{upstreamBase}` = 本槽上游端口;否则用 `fallback`。所以**联调时先 `up`/`add` 后端,再起前端**。
|
|
168
169
|
- **合并感知回收**:`gc` 先 `git fetch`,用 `merge-base --is-ancestor` 判断是否并入主分支;**只在「已合并 + 工作区干净 + 无未推」时才自动删**,判不准一律保守不删、只标记。
|
|
@@ -171,7 +172,7 @@ worktree-bay gc # 回收已合并的
|
|
|
171
172
|
|
|
172
173
|
## 给 AI(MCP)
|
|
173
174
|
|
|
174
|
-
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知服务名);`ls` 以 JSON
|
|
175
|
+
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知服务名);`ls` 以 JSON 返回 `[{slot, feature(可 null), services:[{service, port, dir, running}]}]`(含各 worktree 绝对路径、布尔 `running`);`path` 给某功能某服务的 worktree 目录;`start/stop/restart` 控制运行体(docker+node);`down` 省略 services 拆整功能、给 services 只拆这些。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
|
|
175
176
|
|
|
176
177
|
## 常见坑
|
|
177
178
|
|