worktree-bay 1.4.0 → 1.5.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/dist/cli.js +28 -4
- package/dist/commands/completion.js +1 -1
- package/dist/commands/doctor.js +29 -0
- package/dist/commands/init.js +37 -0
- package/dist/commands/ls.js +10 -1
- package/dist/commands/passthrough.js +10 -2
- package/package.json +1 -1
- package/skill.md +6 -1
package/dist/cli.js
CHANGED
|
@@ -5,22 +5,41 @@ import { loadConfig } from './config.js';
|
|
|
5
5
|
import { claimCommand } from './commands/claim.js';
|
|
6
6
|
import { lsCommand } from './commands/ls.js';
|
|
7
7
|
import { addCommand, upCommand } from './commands/add.js';
|
|
8
|
-
import { runCommand, shCommand } from './commands/passthrough.js';
|
|
8
|
+
import { runCommand, shCommand, pathCommand } from './commands/passthrough.js';
|
|
9
9
|
import { rmCommand } from './commands/rm.js';
|
|
10
10
|
import { gcCommand } from './commands/gc.js';
|
|
11
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
11
12
|
import { complete, completionCommand, installCompletion } from './commands/completion.js';
|
|
12
13
|
import { startMcp } from './mcp.js';
|
|
13
14
|
import { readSkill } from './skill.js';
|
|
15
|
+
import { initCommand } from './commands/init.js';
|
|
14
16
|
import { die, log } from './util/log.js';
|
|
15
17
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
16
18
|
const program = new Command();
|
|
17
|
-
program.name('worktree-bay').description('worktree
|
|
19
|
+
program.name('worktree-bay').description('worktree 槽位 + 端口编排器:多服务并行开发利器').version(pkg.version);
|
|
20
|
+
program.addHelpText('after', `
|
|
21
|
+
示例:
|
|
22
|
+
worktree-bay init 在当前工作区生成配置(扫描子 git 仓预填服务)
|
|
23
|
+
worktree-bay up drill-fix api lms 一条命令为功能起 api+lms(分支默认 = 功能名)
|
|
24
|
+
worktree-bay ls 看所有功能/端口占用
|
|
25
|
+
worktree-bay run drill-fix api test 在 api 跑 run.test
|
|
26
|
+
worktree-bay down drill-fix 拆除整个功能
|
|
27
|
+
worktree-bay gc 回收已合并的功能(默认 dry-run)
|
|
28
|
+
|
|
29
|
+
更多: worktree-bay skill(完整使用与配置指南) · worktree-bay help <命令>(单命令帮助)`);
|
|
18
30
|
const sync = (fn) => { try {
|
|
19
31
|
fn(loadConfig(process.cwd()));
|
|
20
32
|
}
|
|
21
33
|
catch (e) {
|
|
22
34
|
die(e.message);
|
|
23
35
|
} };
|
|
36
|
+
program.command('init').description('在当前工作区生成 worktree-bay.config.json(扫描子 git 仓预填服务)')
|
|
37
|
+
.action(() => { try {
|
|
38
|
+
initCommand(process.cwd());
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
die(e.message);
|
|
42
|
+
} });
|
|
24
43
|
program.command('claim <feature>').description('为功能占一个槽位(端口块)')
|
|
25
44
|
.action(async (f) => { try {
|
|
26
45
|
await claimCommand(loadConfig(process.cwd()), f);
|
|
@@ -28,8 +47,12 @@ program.command('claim <feature>').description('为功能占一个槽位(端
|
|
|
28
47
|
catch (e) {
|
|
29
48
|
die(e.message);
|
|
30
49
|
} });
|
|
31
|
-
program.command('ls').description('列出所有槽位与占用状态')
|
|
32
|
-
.action(() => sync(lsCommand));
|
|
50
|
+
program.command('ls').description('列出所有槽位与占用状态').option('--json', '以 JSON 输出(含 worktree 路径,便于脚本/AI 消费)')
|
|
51
|
+
.action((o) => sync((c) => lsCommand(c, !!o.json)));
|
|
52
|
+
program.command('path <feature> <service>').description('打印某功能某服务的 worktree 绝对路径(可 cd $(...))')
|
|
53
|
+
.action((f, s) => sync((c) => pathCommand(c, f, s)));
|
|
54
|
+
program.command('doctor').description('体检:git/配置/各服务仓是否就绪')
|
|
55
|
+
.action(() => sync(doctorCommand));
|
|
33
56
|
program.command('up <feature> <services...>').description('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)')
|
|
34
57
|
.action(async (f, services) => { try {
|
|
35
58
|
await upCommand(loadConfig(process.cwd()), f, services);
|
|
@@ -93,6 +116,7 @@ program.command('skill').description('打印 worktree-bay 使用与配置完全
|
|
|
93
116
|
catch (e) {
|
|
94
117
|
die(e.message);
|
|
95
118
|
} });
|
|
119
|
+
program.command('version').description('显示版本号').action(() => log(pkg.version));
|
|
96
120
|
program.command('__complete', { hidden: true }).allowUnknownOption().action(() => {
|
|
97
121
|
const words = process.argv.slice(process.argv.indexOf('--') + 1);
|
|
98
122
|
try {
|
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readLabels } from '../slots.js';
|
|
5
5
|
import { log } from '../util/log.js';
|
|
6
|
-
const SUBCMDS = ['claim', 'up', 'add', 'ls', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp', 'skill'];
|
|
6
|
+
const SUBCMDS = ['init', 'doctor', 'claim', 'up', 'add', 'ls', 'path', 'gc', 'down', 'rm', 'run', 'sh', 'completion', 'mcp', 'skill', 'version', 'help'];
|
|
7
7
|
// words = 命令名 + 光标前已输入完的词(不含当前正在补的词)
|
|
8
8
|
export function complete(cfg, words) {
|
|
9
9
|
const prev = words.slice(1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { repoPath } from '../config.js';
|
|
5
|
+
import { log, warn } from '../util/log.js';
|
|
6
|
+
// 体检:git 是否可用、配置是否有效、各服务仓是否存在且是 git 仓。返回问题数。
|
|
7
|
+
export function doctor(cfg) {
|
|
8
|
+
let problems = 0;
|
|
9
|
+
const ok = (m) => log(`✓ ${m}`);
|
|
10
|
+
const bad = (m) => { warn(`✗ ${m}`); problems++; };
|
|
11
|
+
if (spawnSync('git', ['--version'], { encoding: 'utf8' }).status === 0)
|
|
12
|
+
ok('git 可用');
|
|
13
|
+
else
|
|
14
|
+
bad('git 不可用(worktree 依赖 git)');
|
|
15
|
+
ok(`配置已加载并通过校验(${Object.keys(cfg.services).length} 个服务,槽位 1..${cfg.maxSlots},端口块 ${cfg.portBase}+N*${cfg.slotSpan})`);
|
|
16
|
+
for (const name of Object.keys(cfg.services)) {
|
|
17
|
+
const repo = repoPath(cfg, name);
|
|
18
|
+
if (!fs.existsSync(repo))
|
|
19
|
+
bad(`服务 ${name} 仓目录不存在: ${repo}`);
|
|
20
|
+
else if (!fs.existsSync(path.join(repo, '.git')))
|
|
21
|
+
bad(`服务 ${name} 不是 git 仓: ${repo}`);
|
|
22
|
+
else
|
|
23
|
+
ok(`服务 ${name} → ${repo}`);
|
|
24
|
+
}
|
|
25
|
+
log(problems === 0 ? '\n✓ 一切正常' : `\n✗ 发现 ${problems} 个问题`);
|
|
26
|
+
return problems;
|
|
27
|
+
}
|
|
28
|
+
export function doctorCommand(cfg) { if (doctor(cfg) > 0)
|
|
29
|
+
process.exit(1); }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log, warn } from '../util/log.js';
|
|
4
|
+
// 引导生成一份 worktree-bay.config.json:扫描 cwd 子目录里的 git 仓预填为服务
|
|
5
|
+
export function initCommand(cwd) {
|
|
6
|
+
const target = path.join(cwd, 'worktree-bay.config.json');
|
|
7
|
+
if (fs.existsSync(target)) {
|
|
8
|
+
warn(`已存在 ${target},未覆盖`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const repos = [];
|
|
12
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
13
|
+
if (!e.isDirectory() || e.name.startsWith('.'))
|
|
14
|
+
continue;
|
|
15
|
+
if (fs.existsSync(path.join(cwd, e.name, '.git')))
|
|
16
|
+
repos.push(e.name);
|
|
17
|
+
}
|
|
18
|
+
let services;
|
|
19
|
+
if (repos.length) {
|
|
20
|
+
services = {};
|
|
21
|
+
repos.slice(0, 9).forEach((r, i) => { services[r] = { offset: i + 1, setup: 'echo TODO: 起本服务的命令', run: { test: ['echo', 'TODO'] } }; });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
services = {
|
|
25
|
+
api: { offset: 1, copy: ['.env'], env: { '.env': { APP_PORT: '{port}' } }, setup: 'echo TODO: 起后端', run: { test: ['echo', 'TODO'] } },
|
|
26
|
+
web: { offset: 2, upstream: { service: 'api', fallback: 'http://localhost:6001' }, env: { '.env.local': { VITE_API_BASE_URL: '{upstreamBase}' } }, setup: 'pnpm install', start: 'pnpm dev --port {port}' },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const config = { workspaceRoot: cwd, portBase: 6000, slotSpan: 10, maxSlots: 9, services };
|
|
30
|
+
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
|
|
31
|
+
log(`✓ 已生成 ${target}`);
|
|
32
|
+
if (repos.length)
|
|
33
|
+
log(` 识别到服务: ${repos.join(', ')}(按目录预填,offset 自增)`);
|
|
34
|
+
else
|
|
35
|
+
log(` 未识别到子 git 仓,已写入 api/web 示例模板`);
|
|
36
|
+
log(` 下一步:补全各服务的 setup/env/upstream/exec/run。完整配置说明:worktree-bay skill`);
|
|
37
|
+
}
|
package/dist/commands/ls.js
CHANGED
|
@@ -13,4 +13,13 @@ export function renderSlots(cfg) {
|
|
|
13
13
|
}
|
|
14
14
|
return lines.join('\n') || '(no slots in use)';
|
|
15
15
|
}
|
|
16
|
-
export function
|
|
16
|
+
export function slotsData(cfg) {
|
|
17
|
+
const occ = scanOccupancy(cfg);
|
|
18
|
+
const labels = readLabels(cfg);
|
|
19
|
+
const slots = new Set([...occ.keys(), ...Object.keys(labels).map(Number)]);
|
|
20
|
+
return [...slots].sort((a, b) => a - b).map((n) => {
|
|
21
|
+
const base = blockBase(cfg.portBase, cfg.slotSpan, n);
|
|
22
|
+
return { slot: n, feature: labels[String(n)] ?? null, block: base, services: (occ.get(n) ?? []).map((o) => ({ service: o.service, port: base + cfg.services[o.service].offset, dir: o.dir })) };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { repoPath } from '../config.js';
|
|
2
2
|
import { scanOccupancy, slotOfFeature } from '../slots.js';
|
|
3
3
|
import { buildVars, execArgv, run } from '../engine.js';
|
|
4
|
-
|
|
4
|
+
import { log } from '../util/log.js';
|
|
5
|
+
function occupantOf(cfg, feature, service) {
|
|
5
6
|
const slot = slotOfFeature(cfg, feature);
|
|
6
7
|
if (slot === undefined)
|
|
7
8
|
throw new Error('unknown feature: ' + feature);
|
|
8
9
|
const occ = (scanOccupancy(cfg).get(slot) ?? []).find((o) => o.service === service);
|
|
9
10
|
if (!occ)
|
|
10
11
|
throw new Error(`${service} not in ${feature}`);
|
|
12
|
+
return occ;
|
|
13
|
+
}
|
|
14
|
+
function ctxOf(cfg, feature, service) {
|
|
15
|
+
const occ = occupantOf(cfg, feature, service);
|
|
11
16
|
const sp = cfg.services[service];
|
|
12
|
-
return { sp, vars: buildVars(cfg, { cfg, service, sp, slot, slug: occ.slug, dir: occ.dir, repo: repoPath(cfg, service) }) };
|
|
17
|
+
return { sp, vars: buildVars(cfg, { cfg, service, sp, slot: occ.slot, slug: occ.slug, dir: occ.dir, repo: repoPath(cfg, service) }) };
|
|
18
|
+
}
|
|
19
|
+
export function pathCommand(cfg, feature, service) {
|
|
20
|
+
log(occupantOf(cfg, feature, service).dir);
|
|
13
21
|
}
|
|
14
22
|
export function runCommand(cfg, feature, service, name, args) {
|
|
15
23
|
const ctx = ctxOf(cfg, feature, service);
|
package/package.json
CHANGED
package/skill.md
CHANGED
|
@@ -24,10 +24,13 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
24
24
|
|
|
25
25
|
| 命令 | 作用 |
|
|
26
26
|
|---|---|
|
|
27
|
+
| `worktree-bay init` | 在当前工作区生成 `worktree-bay.config.json`(扫描子 git 仓预填服务) |
|
|
28
|
+
| `worktree-bay doctor` | 体检:git 是否可用、配置是否有效、各服务仓是否就绪 |
|
|
27
29
|
| `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名) |
|
|
28
30
|
| `worktree-bay claim <feature>` | 只占一个槽、打印端口块(不开 worktree) |
|
|
29
31
|
| `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>` |
|
|
30
|
-
| `worktree-bay ls` |
|
|
32
|
+
| `worktree-bay ls [--json]` | 列出所有槽位:功能名、端口块、已起服务及端口、是否已并入主分支;`--json` 输出结构化数据(含 worktree 路径) |
|
|
33
|
+
| `worktree-bay path <feature> <service>` | 打印某服务 worktree 的绝对路径(可 `cd $(worktree-bay path f api)`) |
|
|
31
34
|
| `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
|
|
32
35
|
| `worktree-bay sh <feature> <service>` | 进入某服务运行体的 shell |
|
|
33
36
|
| `worktree-bay down <feature> [-f]` | 拆除整个功能的所有服务 worktree(= `rm <feature>`) |
|
|
@@ -36,6 +39,8 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
36
39
|
| `worktree-bay completion <install\|bash\|zsh\|fish>` | `install` 一键装进 shell;或打印补全脚本 |
|
|
37
40
|
| `worktree-bay mcp` | 启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 |
|
|
38
41
|
| `worktree-bay skill` | 打印本指南 |
|
|
42
|
+
| `worktree-bay version` / `--version` | 显示版本号 |
|
|
43
|
+
| `worktree-bay help [命令]` | 帮助;`help <命令>` 看单命令用法 |
|
|
39
44
|
|
|
40
45
|
典型流程:
|
|
41
46
|
|