worktree-bay 1.5.0 → 2.0.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 +6 -8
- package/dist/commands/claim.js +3 -4
- 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/ports.js +2 -2
- package/package.json +7 -3
- package/skill.md +9 -14
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
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`。
|
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
|
}
|
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/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,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
7
|
+
"git-worktree",
|
|
7
8
|
"worktree",
|
|
8
9
|
"monorepo",
|
|
9
10
|
"cli",
|
|
10
11
|
"ports",
|
|
11
12
|
"orchestration",
|
|
12
13
|
"parallel",
|
|
13
|
-
"
|
|
14
|
+
"parallel-development",
|
|
15
|
+
"developer-tools",
|
|
16
|
+
"dev-environment",
|
|
17
|
+
"mcp"
|
|
14
18
|
],
|
|
15
19
|
"type": "module",
|
|
16
20
|
"license": "MIT",
|
package/skill.md
CHANGED
|
@@ -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",
|