@baize-ai/core 0.3.15 → 0.3.17
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/.dockerignore +1 -0
- package/CHANGELOG.md +29 -0
- package/Dockerfile +10 -1
- package/cli/commands/doctor.js +60 -0
- package/cli/commands/init.js +104 -42
- package/cli/commands/runtime.js +9 -1
- package/cli/lib/__tests__/init-base-url.test.js +55 -13
- package/cli/lib/__tests__/runtime-base-url.test.js +3 -1
- package/cli/lib/__tests__/runtime-launch.test.js +4 -2
- package/cli/lib/__tests__/runtime-setup.test.js +53 -6
- package/cli/lib/__tests__/tmux-env.test.js +19 -5
- package/cli/lib/claude-eval.js +2 -1
- package/cli/lib/codex-hooks.js +12 -0
- package/cli/lib/path-bins.js +54 -0
- package/cli/lib/runtime/claude.js +4 -2
- package/cli/lib/runtime/codex.js +19 -0
- package/cli/lib/runtime/tmux-env.js +5 -4
- package/cli/lib/runtime-setup.js +157 -13
- package/docker/entrypoint.sh +4 -2
- package/docs/ops-runbook.md +161 -0
- package/package.json +2 -2
- package/scripts/docker-publish.sh +11 -5
- package/scripts/pack-release.sh +34 -26
- package/skills/activity-monitor/scripts/__tests__/guardian.test.js +51 -0
- package/skills/activity-monitor/scripts/adapters/runtime-components.js +27 -0
- package/skills/activity-monitor/scripts/guardian.js +45 -1
- package/skills/activity-monitor/scripts/monitor-orchestrator.js +8 -1
- package/skills/activity-monitor/scripts/upgrade-check.js +3 -1
- package/skills/web-console/public/app.js +20 -75
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +6 -24
- package/templates/pm2/ecosystem.config.cjs +5 -4
- package/test/model-provider.test.js +150 -40
- package/test/web-console-routes.test.js +11 -13
package/.dockerignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to baize-core will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.17] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- 启动链路 PATH 完整性加固(D54):共享 `cli/lib/path-bins.js`(`completeBinPath`/`knownBinDirs`——静态 `~/.npm-global/bin` + 动态 npm prefix);所有 runtime 二进制 spawn(codex/claude/baize)子进程 env 补完整 PATH——登录 shell PATH 污染(09-13 事故根因:缺 `~/.npm-global/bin`)不再导致 ENOENT
|
|
12
|
+
- `init.js ensureCompletePathProfile`:每次 init 修复 shell profile 完整 bin PATH(npm-global/.local/claude/baize/node),幂等 + 存量缺失自动补;`saveSystemPath` 写显式完整集
|
|
13
|
+
- `ecosystem.config.cjs` `ENHANCED_PATH` 显式含 `~/.npm-global/bin`(PM2 服务不依赖 SYSTEM_PATH/进程 PATH)
|
|
14
|
+
- Guardian 连续失败告警(`ALERT_AFTER_RESTARTS=3` → `~/baize/activity-monitor/guardian-alert.json` + 日志,恢复自动清除);ENOENT 启动失败打修复提示
|
|
15
|
+
- `baize doctor` 新增 PATH 完整性检查(`path_incomplete` issue + path 组)
|
|
16
|
+
- Dockerfile 本地源码构建层序修复:`COPY core.tgz` 移到 npm install RUN 前(tarball 变更正确失效 npm 层缓存)
|
|
17
|
+
- 运维手册 `docs/ops-runbook.md`:PATH 事故排查 + D54 加固说明 + 巡检命令
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `docker/entrypoint.sh` 不再写 `SYSTEM_PATH`(init.js `saveSystemPath` 唯一写者,写完整集)
|
|
21
|
+
- `templates/pm2/ecosystem.config.cjs` 与 `docker-publish.sh`:发布构建临时 context + 空 `core.tgz` 占位(可选 COPY 不破坏 registry 构建)
|
|
22
|
+
|
|
23
|
+
## [0.3.16] - 2026-09-13
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Codex 供应商配置改造(D51):独立 provider 块写入器 + kind/slug store + init/runtime 遗留路径转换 + web-console 只读激活卡;`/models` 探测读取 provider 块 base_url
|
|
27
|
+
- Dockerfile 本地源码构建支持(D51):`BAZE_CORE_TARBALL=/core.tgz` 安装工作区包而非 registry 版本——验证未发布修复(如 D51)进镜像
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- D51 部署缺陷(init):web-console `import ../../../cli/lib/runtime-setup.js` 运行时 `~/.claude/cli/lib/` 不存在 → ERR_MODULE_NOT_FOUND 崩溃——`syncCoreSkills` 补拷 `package/cli` 至 `SKILLS_DIR 上级/cli`,并从包根复制零依赖 `node_modules/smol-toml`
|
|
31
|
+
- D51 配置污染事故根因(4a37368):`globalCodexConfigPath` 优先读 `HOME` env 而非 `os.homedir()`——jest ESM sandbox 曾写穿真实 `~/.codex/config.toml`
|
|
32
|
+
- D51 review 修复:restore 只删 baize-written 块(`name=key && wire_api=responses`);写盘直设 `0600` + 去重用例;激活卡展示 kind
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- A2A 授权治理收敛(D52 2b11171):agent 本地 authz 仅保留 mode 切换(open/allowlist)——allow/block 名单由 admin 控制台统一管控(PUT 忽略名单强制清空 + UI 隐藏名单编辑并提示管控来源;移除死代码 `validateAuthzList`/peerIds 请求)
|
|
36
|
+
|
|
8
37
|
## [0.3.15] - 2026-08-31
|
|
9
38
|
|
|
10
39
|
### Fixed
|
package/Dockerfile
CHANGED
|
@@ -82,8 +82,17 @@ ENV WEB_CONSOLE_BIND=0.0.0.0
|
|
|
82
82
|
# build context) — source stays private, the image matches the npm version
|
|
83
83
|
# exactly. Build with: docker build --build-arg BAZE_CORE_VERSION=0.2.0 .
|
|
84
84
|
ARG BAZE_CORE_VERSION=latest
|
|
85
|
+
# Local-source build: pass BAZE_CORE_TARBALL=/core.tgz (COPY'd into the
|
|
86
|
+
# build context) to install the workspace package instead of the registry
|
|
87
|
+
# version — lets us verify un-released fixes (e.g. D51) in the image.
|
|
88
|
+
ARG BAZE_CORE_TARBALL=
|
|
89
|
+
# Local-source build tarball — COPY'd BEFORE the install RUN so the file is
|
|
90
|
+
# present in that layer AND a tarball change invalidates the install layer.
|
|
91
|
+
# Registry builds provide an empty placeholder via docker-publish.sh's temp
|
|
92
|
+
# context (or omit BAZE_CORE_TARBALL → the COPY still needs a file present).
|
|
93
|
+
COPY core.tgz /tmp/core.tgz
|
|
85
94
|
WORKDIR /home/baize
|
|
86
|
-
RUN npm install -g @baize-ai/core@${BAZE_CORE_VERSION} \
|
|
95
|
+
RUN if [ -n "${BAZE_CORE_TARBALL}" ]; then npm install -g /tmp/${BAZE_CORE_TARBALL}; else npm install -g @baize-ai/core@${BAZE_CORE_VERSION}; fi \
|
|
87
96
|
&& baize --version \
|
|
88
97
|
# ── Build-time skill dependency preinstall (reproducibility) ─────────
|
|
89
98
|
# `baize init` reinstalls skill deps at runtime; doing it here bakes the
|
package/cli/commands/doctor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { loadComponents } from '../lib/components.js';
|
|
|
20
20
|
import { fetchLatestTagAsync, compareSemverDesc } from '../lib/github.js';
|
|
21
21
|
import { getCurrentVersion } from '../lib/self-upgrade.js';
|
|
22
22
|
import { commandExists } from '../lib/shell-utils.js';
|
|
23
|
+
import { completeBinPath } from '../lib/path-bins.js';
|
|
23
24
|
import { parseSkillMd } from '../lib/skill.js';
|
|
24
25
|
import { bold, dim, green, red, yellow, heading } from '../lib/colors.js';
|
|
25
26
|
import { getActiveAdapter } from '../lib/runtime/index.js';
|
|
@@ -280,6 +281,30 @@ function checkTmuxSession() {
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* D54: verify the runtime binaries are resolvable from the CURRENT process
|
|
286
|
+
* PATH (i.e. PM2 services spawned from here can find codex/claude). A
|
|
287
|
+
* login-shell-polluted PATH (missing ~/.npm-global/bin) shows up here as
|
|
288
|
+
* missing binaries even though the files exist.
|
|
289
|
+
*/
|
|
290
|
+
function checkBinResolvable(bin) {
|
|
291
|
+
try {
|
|
292
|
+
execFileSync(bin, ['--version'], {
|
|
293
|
+
stdio: 'pipe', encoding: 'utf8', timeout: 10000,
|
|
294
|
+
});
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function checkPathComplete() {
|
|
301
|
+
const bins = ['baize'];
|
|
302
|
+
if (ACTIVE_RUNTIME === 'codex') bins.push('codex');
|
|
303
|
+
else bins.push('claude');
|
|
304
|
+
const missing = bins.filter((b) => !checkBinResolvable(b));
|
|
305
|
+
return { complete: missing.length === 0, missing };
|
|
306
|
+
}
|
|
307
|
+
|
|
283
308
|
// ── Diagnostics collection ───────────────────────────────────────
|
|
284
309
|
|
|
285
310
|
async function collectDiagnostics(env) {
|
|
@@ -308,11 +333,13 @@ async function collectDiagnostics(env) {
|
|
|
308
333
|
? checkPm2Services()
|
|
309
334
|
: { running: false, total: 0, online: 0, activityMonitor: false, procs: [] };
|
|
310
335
|
const session = tmux.installed ? checkTmuxSession() : false;
|
|
336
|
+
const pathCheck = checkPathComplete();
|
|
311
337
|
|
|
312
338
|
return {
|
|
313
339
|
system: { tmux, pm2, network: net },
|
|
314
340
|
ai: { cli, auth, authStatus, autonomous, networkSkipped: !net.reachable },
|
|
315
341
|
services: { ...services, session },
|
|
342
|
+
path: pathCheck,
|
|
316
343
|
};
|
|
317
344
|
}
|
|
318
345
|
|
|
@@ -366,6 +393,19 @@ function buildDiagnosticJson(diag, coreVersion) {
|
|
|
366
393
|
}
|
|
367
394
|
}
|
|
368
395
|
}
|
|
396
|
+
// D54: PATH completeness — runtime binaries must resolve from the current
|
|
397
|
+
// process PATH (services spawned from here inherit it). A polluted login
|
|
398
|
+
// shell PATH missing ~/.npm-global/bin surfaces here as ENOENT risk.
|
|
399
|
+
// Only relevant when the CLI is actually installed (else cli_missing covers
|
|
400
|
+
// it and a PATH repair hint would be misleading).
|
|
401
|
+
const pathSkipped = !diag.ai.cli.installed;
|
|
402
|
+
if (!pathSkipped && !diag.path.complete) {
|
|
403
|
+
issues.push({
|
|
404
|
+
id: 'path_incomplete',
|
|
405
|
+
label: `PATH 缺失运行时二进制: ${diag.path.missing.join(', ')}`,
|
|
406
|
+
hint: 'Run: baize init(修复 ~/.npm-global/bin 等完整 PATH)',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
369
409
|
|
|
370
410
|
return {
|
|
371
411
|
version: coreVersion.success ? coreVersion.version : null,
|
|
@@ -395,6 +435,10 @@ function buildDiagnosticJson(diag, coreVersion) {
|
|
|
395
435
|
procs: diag.services.procs.map(p => ({ name: p.name, status: p.pm2_env?.status || 'unknown' })),
|
|
396
436
|
session: { active: diag.services.session },
|
|
397
437
|
},
|
|
438
|
+
path: {
|
|
439
|
+
passed: !diag.ai.cli.installed ? null : diag.path.complete,
|
|
440
|
+
checks: { missing: diag.path.missing },
|
|
441
|
+
},
|
|
398
442
|
},
|
|
399
443
|
issues,
|
|
400
444
|
};
|
|
@@ -512,6 +556,18 @@ function displayServiceGroup(diag, jsonGroup) {
|
|
|
512
556
|
logToFile(`check: services — ${jsonGroup.passed ? 'passed' : 'failed'}`);
|
|
513
557
|
}
|
|
514
558
|
|
|
559
|
+
// D54: PATH completeness group — runtime binaries must resolve from the
|
|
560
|
+
// current process PATH (services spawned from here inherit it).
|
|
561
|
+
function displayPathGroup(diag, jsonGroup) {
|
|
562
|
+
if (!diag.path.complete) {
|
|
563
|
+
const checks = diag.path.missing.map((b) => red(`${b} 不可解析(ENOENT 风险)`));
|
|
564
|
+
displayCheckGroup('PATH', 'fail', checks);
|
|
565
|
+
} else {
|
|
566
|
+
displayCheckGroup('PATH', 'pass', [green('运行时二进制可解析(npm-global/.local/claude/baize)')]);
|
|
567
|
+
}
|
|
568
|
+
logToFile(`check: path — ${jsonGroup.passed ? 'passed' : 'failed'}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
515
571
|
// ── Channel discovery ────────────────────────────────────────────
|
|
516
572
|
|
|
517
573
|
function getNetworkIP() {
|
|
@@ -651,6 +707,7 @@ function runClaudeFix(diagnosticJson) {
|
|
|
651
707
|
const proc = spawn('claude', ['-p', prompt, ...permArgs], {
|
|
652
708
|
cwd: BAIZE_DIR,
|
|
653
709
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
710
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
654
711
|
});
|
|
655
712
|
|
|
656
713
|
let stdout = '';
|
|
@@ -772,6 +829,9 @@ export async function doctorCommand(args) {
|
|
|
772
829
|
displaySystemGroup(diag, diagnostic.groups.system);
|
|
773
830
|
displayAiGroup(diag, diagnostic.groups.ai_service);
|
|
774
831
|
displayServiceGroup(diag, diagnostic.groups.services);
|
|
832
|
+
if (diagnostic.groups.path.passed !== null) {
|
|
833
|
+
displayPathGroup(diag, diagnostic.groups.path);
|
|
834
|
+
}
|
|
775
835
|
|
|
776
836
|
// ── Phase 4: Channels ─────────────────────────────────────────
|
|
777
837
|
|
package/cli/commands/init.js
CHANGED
|
@@ -17,6 +17,7 @@ import { generateManifest, saveMergeBaseline } from '../lib/manifest.js';
|
|
|
17
17
|
import { prompt, promptYesNo, promptChoice, promptSecret } from '../lib/prompts.js';
|
|
18
18
|
import { bold, dim, green, red, yellow, cyan, bgGreen, success, error, warn, heading } from '../lib/colors.js';
|
|
19
19
|
import { commandExists } from '../lib/shell-utils.js';
|
|
20
|
+
import { knownBinDirs, completeBinPath } from '../lib/path-bins.js';
|
|
20
21
|
import { getActiveAdapter } from '../lib/runtime/index.js';
|
|
21
22
|
import {
|
|
22
23
|
activateFreshSplitInstructions,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
saveCodexApiKeyToEnv,
|
|
42
43
|
saveCodexBaseUrlToEnv,
|
|
43
44
|
writeCodexConfig,
|
|
45
|
+
applyCodexProviderBlock,
|
|
44
46
|
} from '../lib/runtime-setup.js';
|
|
45
47
|
|
|
46
48
|
// Source directories (shipped with baize package)
|
|
@@ -113,54 +115,68 @@ function installSystemPackage(pkg) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
+
* D54: ensure the user's shell profile carries the COMPLETE bin PATH —
|
|
119
|
+
* every directory where baize/claude/codex binaries actually live:
|
|
120
|
+
* ~/.npm-global/bin (static — codex/baize default install),
|
|
121
|
+
* npm global prefix bin (dynamic), ~/.local/bin (claude default),
|
|
122
|
+
* ~/.claude/bin, ~/baize/bin (components), ~/.local/node/bin.
|
|
123
|
+
* Idempotent (marker-based) and repairs existing profiles that miss any bin
|
|
124
|
+
* (the 09-13 incident root: ~/.npm-global/bin was never added, so login-shell
|
|
125
|
+
* PATH was always incomplete).
|
|
126
|
+
* NOTE: rc files are NOT chattr/chflags-locked — content-marker idempotency
|
|
127
|
+
* already prevents duplicate writes, and a lock would make `baize
|
|
128
|
+
* self-uninstall` / install.sh re-runs abort with EPERM (D54 review P1-3).
|
|
118
129
|
* Returns the profile path if modified, null otherwise.
|
|
119
130
|
*/
|
|
120
|
-
function
|
|
131
|
+
function ensureCompletePathProfile() {
|
|
121
132
|
const homedir = os.homedir();
|
|
122
133
|
const shell = (process.env.SHELL || '').split('/').pop();
|
|
123
|
-
const pathLine = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
124
|
-
|
|
125
|
-
// Map shell to profile file
|
|
126
134
|
const profileMap = {
|
|
127
135
|
zsh: '.zshrc',
|
|
128
136
|
bash: '.bashrc',
|
|
129
|
-
fish: null, // fish uses different
|
|
137
|
+
fish: null, // fish uses a different config mechanism (handled elsewhere)
|
|
130
138
|
sh: '.profile',
|
|
131
139
|
};
|
|
132
|
-
|
|
133
140
|
const profileName = profileMap[shell] || '.profile';
|
|
134
141
|
if (!profileName) return null; // unsupported shell (fish)
|
|
135
|
-
|
|
136
142
|
const profilePath = path.join(homedir, profileName);
|
|
137
143
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
const content = fs.readFileSync(profilePath, 'utf8');
|
|
141
|
-
if (content.includes('.local/bin')) return null; // already there
|
|
142
|
-
} catch {
|
|
143
|
-
// File doesn't exist — we'll create it
|
|
144
|
-
}
|
|
144
|
+
// ── Complete bin set (single source of truth: cli/lib/path-bins.js) ──────
|
|
145
|
+
const bins = new Set(knownBinDirs());
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// Read existing profile content (missing file → create)
|
|
148
|
+
let content = '';
|
|
149
|
+
try { content = fs.readFileSync(profilePath, 'utf8'); } catch { /* absent */ }
|
|
150
|
+
|
|
151
|
+
// ── Idempotent + repair: add any bin not already present ─────────────────
|
|
152
|
+
const toAdd = [];
|
|
153
|
+
for (const bin of bins) {
|
|
154
|
+
// match both absolute (~/...) and $HOME-relative forms already in profile
|
|
155
|
+
const rel = bin.startsWith(homedir) ? bin.slice(homedir.length) : bin;
|
|
156
|
+
if (content.includes(bin) || content.includes(`$HOME${rel}`)) continue;
|
|
157
|
+
const portable = bin.startsWith(homedir) ? `$HOME${rel}` : bin;
|
|
158
|
+
toAdd.push(`export PATH="${portable}:$PATH"`);
|
|
151
159
|
}
|
|
160
|
+
if (toAdd.length === 0) return null; // already complete
|
|
161
|
+
|
|
162
|
+
fs.appendFileSync(profilePath, `\n# Added by baize init (D54 complete PATH)\n${toAdd.join('\n')}\n`);
|
|
163
|
+
return `~/${profileName}`;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
/**
|
|
155
|
-
* Save the
|
|
156
|
-
*
|
|
167
|
+
* Save the COMPLETE bin PATH to .env so PM2 services can use it.
|
|
168
|
+
* D54: no longer captures process.env.PATH (which may be polluted by a login
|
|
169
|
+
* shell) — builds the full known-good set explicitly: npm global prefix bin,
|
|
170
|
+
* ~/.local/bin, ~/.claude/bin, ~/baize/bin, ~/.local/node/bin.
|
|
157
171
|
*/
|
|
158
172
|
function saveSystemPath(envPath) {
|
|
159
|
-
const
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// the
|
|
163
|
-
const
|
|
173
|
+
const homedir = os.homedir();
|
|
174
|
+
// D54: build the full known-good set via path-bins (single source of truth)
|
|
175
|
+
const bins = new Set(knownBinDirs());
|
|
176
|
+
// keep the current PATH too (it may carry user-added entries worth keeping)
|
|
177
|
+
for (const entry of (process.env.PATH || '').split(':').filter(Boolean)) bins.add(entry);
|
|
178
|
+
const fullPath = [...bins].join(':');
|
|
179
|
+
|
|
164
180
|
let content = '';
|
|
165
181
|
try {
|
|
166
182
|
content = fs.readFileSync(envPath, 'utf8');
|
|
@@ -168,7 +184,7 @@ function saveSystemPath(envPath) {
|
|
|
168
184
|
return; // .env doesn't exist yet
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
const line = `SYSTEM_PATH=${
|
|
187
|
+
const line = `SYSTEM_PATH=${fullPath}`;
|
|
172
188
|
if (content.includes('SYSTEM_PATH=')) {
|
|
173
189
|
content = content.replace(/^SYSTEM_PATH=.*$/m, line);
|
|
174
190
|
} else {
|
|
@@ -476,7 +492,7 @@ function verifySetupToken() {
|
|
|
476
492
|
try {
|
|
477
493
|
const result = spawnSync('claude', ['-p', 'hi', '--max-turns', '1'], {
|
|
478
494
|
timeout: 30000,
|
|
479
|
-
env: { ...process.env },
|
|
495
|
+
env: { ...process.env, PATH: completeBinPath() },
|
|
480
496
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
481
497
|
});
|
|
482
498
|
|
|
@@ -839,6 +855,30 @@ function syncCoreSkills() {
|
|
|
839
855
|
const installed = [];
|
|
840
856
|
const updated = [];
|
|
841
857
|
|
|
858
|
+
// D51: web-console imports `../../../cli/lib/runtime-setup.js`, which from
|
|
859
|
+
// `~/.claude/skills/web-console/scripts/` resolves to `~/.claude/cli/lib/`.
|
|
860
|
+
// The npm package ships `package/cli`, but init never deployed it to the
|
|
861
|
+
// runtime tree — web-console crashed with ERR_MODULE_NOT_FOUND in the image.
|
|
862
|
+
// Deploy the shared CLI lib beside SKILLS_DIR so the import path mirrors the
|
|
863
|
+
// package layout (package/cli ↔ <SKILLS_DIR>/../cli).
|
|
864
|
+
const cliSrc = path.join(PACKAGE_ROOT, 'cli');
|
|
865
|
+
const cliDest = path.join(path.dirname(SKILLS_DIR), 'cli');
|
|
866
|
+
if (fs.existsSync(cliSrc)) {
|
|
867
|
+
try {
|
|
868
|
+
copyTree(cliSrc, cliDest);
|
|
869
|
+
// runtime-setup.js imports smol-toml (D51). The cli/ tree is deployed
|
|
870
|
+
// outside any skill's node_modules, so the dependency must ride along —
|
|
871
|
+
// mirror the package layout by copying the package's smol-toml into
|
|
872
|
+
// <cliDest>/node_modules/.
|
|
873
|
+
const tomlSrc = path.join(PACKAGE_ROOT, 'node_modules', 'smol-toml');
|
|
874
|
+
if (fs.existsSync(tomlSrc)) {
|
|
875
|
+
copyTree(tomlSrc, path.join(cliDest, 'node_modules', 'smol-toml'));
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
console.log(` ${warn(`Failed to sync cli/ to ${cliDest}`)}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
842
882
|
const entries = fs.readdirSync(CORE_SKILLS_SRC, { withFileTypes: true });
|
|
843
883
|
for (const entry of entries) {
|
|
844
884
|
if (!entry.isDirectory()) continue;
|
|
@@ -2153,7 +2193,7 @@ export async function initCommand(args) {
|
|
|
2153
2193
|
console.log(`\n ${cyan('Starting Codex device auth...')}`);
|
|
2154
2194
|
console.log(` ${dim('Follow the instructions to authenticate. Press Ctrl+C when done.')}\n`);
|
|
2155
2195
|
try {
|
|
2156
|
-
spawnSync('codex', ['login', '--device-auth'], { stdio: 'inherit' });
|
|
2196
|
+
spawnSync('codex', ['login', '--device-auth'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2157
2197
|
} catch { /* user may Ctrl+C */ }
|
|
2158
2198
|
codexAuthenticated = isCodexAuthenticated();
|
|
2159
2199
|
if (codexAuthenticated) {
|
|
@@ -2167,7 +2207,7 @@ export async function initCommand(args) {
|
|
|
2167
2207
|
console.log(`\n ${cyan('Starting Codex browser login...')}`);
|
|
2168
2208
|
console.log(` ${dim('After login completes, press Ctrl+C to return.')}\n`);
|
|
2169
2209
|
try {
|
|
2170
|
-
spawnSync('codex', ['login'], { stdio: 'inherit' });
|
|
2210
|
+
spawnSync('codex', ['login'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2171
2211
|
} catch { /* user may Ctrl+C */ }
|
|
2172
2212
|
codexAuthenticated = isCodexAuthenticated();
|
|
2173
2213
|
if (codexAuthenticated) {
|
|
@@ -2182,9 +2222,17 @@ export async function initCommand(args) {
|
|
|
2182
2222
|
}
|
|
2183
2223
|
}
|
|
2184
2224
|
}
|
|
2185
|
-
|
|
2186
|
-
//
|
|
2187
|
-
|
|
2225
|
+
// Write ~/.codex/config.toml to suppress interactive prompts on first
|
|
2226
|
+
// launch. A custom --codex-base-url becomes a dedicated provider block
|
|
2227
|
+
// (D51) — never the built-in-OpenAI openai_base_url override.
|
|
2228
|
+
if (pendingCodexBaseUrl) {
|
|
2229
|
+
applyCodexProviderBlock({
|
|
2230
|
+
providerKey: 'baize-init',
|
|
2231
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2232
|
+
token: opts.codexApiKey || undefined,
|
|
2233
|
+
});
|
|
2234
|
+
if (!quiet) console.log(` ${success('Codex startup config written (provider block)')}`);
|
|
2235
|
+
} else if (writeCodexConfig(BAIZE_DIR)) {
|
|
2188
2236
|
if (!quiet) console.log(` ${success('Codex startup config written')}`);
|
|
2189
2237
|
}
|
|
2190
2238
|
}
|
|
@@ -2276,9 +2324,9 @@ export async function initCommand(args) {
|
|
|
2276
2324
|
// /dev/tty redirects. Use `script` to allocate a fresh pseudo-terminal
|
|
2277
2325
|
// that gives Claude full terminal control.
|
|
2278
2326
|
if (process.platform === 'darwin') {
|
|
2279
|
-
spawnSync('script', ['-q', '/dev/null', 'claude'], { stdio: 'inherit' });
|
|
2327
|
+
spawnSync('script', ['-q', '/dev/null', 'claude'], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2280
2328
|
} else {
|
|
2281
|
-
spawnSync('claude', [], { stdio: 'inherit' });
|
|
2329
|
+
spawnSync('claude', [], { stdio: 'inherit', env: { ...process.env, PATH: completeBinPath() } });
|
|
2282
2330
|
}
|
|
2283
2331
|
} catch { /* user may Ctrl+C */ }
|
|
2284
2332
|
process.removeAllListeners('SIGINT');
|
|
@@ -2409,7 +2457,12 @@ export async function initCommand(args) {
|
|
|
2409
2457
|
}
|
|
2410
2458
|
if (pendingCodexBaseUrl) {
|
|
2411
2459
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2412
|
-
|
|
2460
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2461
|
+
applyCodexProviderBlock({
|
|
2462
|
+
providerKey: 'baize-init',
|
|
2463
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2464
|
+
token: opts.codexApiKey || undefined,
|
|
2465
|
+
});
|
|
2413
2466
|
}
|
|
2414
2467
|
|
|
2415
2468
|
// Timezone: use resolved value or show current
|
|
@@ -2518,7 +2571,12 @@ export async function initCommand(args) {
|
|
|
2518
2571
|
}
|
|
2519
2572
|
if (pendingCodexBaseUrl) {
|
|
2520
2573
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2521
|
-
|
|
2574
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2575
|
+
applyCodexProviderBlock({
|
|
2576
|
+
providerKey: 'baize-init',
|
|
2577
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2578
|
+
token: opts.codexApiKey || undefined,
|
|
2579
|
+
});
|
|
2522
2580
|
}
|
|
2523
2581
|
// Step 8: Configure timezone
|
|
2524
2582
|
if (!quiet) console.log(`\n${heading('Timezone configuration...')}`);
|
|
@@ -2580,9 +2638,13 @@ export async function initCommand(args) {
|
|
|
2580
2638
|
|
|
2581
2639
|
printWebConsoleInfo();
|
|
2582
2640
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2641
|
+
// D54: EVERY init run repairs the complete bin PATH in the shell profile
|
|
2642
|
+
// (npm-global/.local/claude/baize/node bins) — idempotent, no-op when
|
|
2643
|
+
// complete. Runs unconditionally: a claude-runtime deployment re-running
|
|
2644
|
+
// init with --yes/--quiet must also get the missing bins repaired.
|
|
2645
|
+
const profile = ensureCompletePathProfile();
|
|
2646
|
+
if (profile && !quiet) {
|
|
2647
|
+
console.log(`${green(` ✔ PATH 已写入 ${profile}(含 npm-global/.local/claude/baize/node bin)`)}`);
|
|
2586
2648
|
}
|
|
2587
2649
|
|
|
2588
2650
|
if (!quiet) {
|
package/cli/commands/runtime.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
saveSetupTokenToEnv,
|
|
30
30
|
saveCodexApiKey,
|
|
31
31
|
saveCodexBaseUrlToEnv,
|
|
32
|
+
applyCodexProviderBlock,
|
|
32
33
|
saveCodexApiKeyToEnv,
|
|
33
34
|
writeCodexConfig,
|
|
34
35
|
} from '../lib/runtime-setup.js';
|
|
@@ -123,7 +124,14 @@ export function applyBaseUrl(target, baseUrl) {
|
|
|
123
124
|
}
|
|
124
125
|
if (target === 'codex') {
|
|
125
126
|
if (!saveCodexBaseUrlToEnv(baseUrl)) return false;
|
|
126
|
-
|
|
127
|
+
// D51: external endpoints get a dedicated provider block (DeepSeek-safe,
|
|
128
|
+
// local compaction) — never the built-in-openai openai_base_url override.
|
|
129
|
+
try {
|
|
130
|
+
applyCodexProviderBlock({ providerKey: 'baize-runtime', baseUrl, projectDir: BAIZE_DIR });
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
return false;
|
|
129
137
|
}
|
|
@@ -72,7 +72,7 @@ describe('base URL support', () => {
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('writeCodexConfig writes openai_base_url
|
|
75
|
+
test('writeCodexConfig never writes openai_base_url — env override is dropped (D51)', async () => {
|
|
76
76
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-base-url-'));
|
|
77
77
|
const originalHome = process.env.HOME;
|
|
78
78
|
const originalOpenAiBaseUrl = process.env.OPENAI_BASE_URL;
|
|
@@ -87,7 +87,7 @@ describe('base URL support', () => {
|
|
|
87
87
|
|
|
88
88
|
const configPath = path.join(tmpRoot, '.codex', 'config.toml');
|
|
89
89
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
90
|
-
assert.
|
|
90
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
91
91
|
} finally {
|
|
92
92
|
if (originalHome === undefined) delete process.env.HOME;
|
|
93
93
|
else process.env.HOME = originalHome;
|
|
@@ -99,31 +99,73 @@ describe('base URL support', () => {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test('writeCodexConfig
|
|
102
|
+
test('writeCodexConfig strips a stale legacy openai_base_url from existing config (D51)', async () => {
|
|
103
103
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-base-url-opt-'));
|
|
104
104
|
const originalHome = process.env.HOME;
|
|
105
|
-
const originalOpenAiBaseUrl = process.env.OPENAI_BASE_URL;
|
|
106
105
|
|
|
107
106
|
process.env.HOME = tmpRoot;
|
|
108
|
-
delete process.env.OPENAI_BASE_URL;
|
|
109
107
|
|
|
110
108
|
try {
|
|
111
109
|
const { writeCodexConfig } = await import('../runtime-setup.js');
|
|
112
110
|
|
|
113
|
-
assert.equal(
|
|
114
|
-
writeCodexConfig(path.join(tmpRoot, 'baize-project'), { openaiBaseUrl: 'https://explicit-proxy.example.com/v1' }),
|
|
115
|
-
true
|
|
116
|
-
);
|
|
117
|
-
|
|
118
111
|
const configPath = path.join(tmpRoot, '.codex', 'config.toml');
|
|
112
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
113
|
+
fs.writeFileSync(configPath, 'openai_base_url = "https://explicit-proxy.example.com/v1"\nmodel = "old"\n');
|
|
114
|
+
|
|
115
|
+
assert.equal(writeCodexConfig(path.join(tmpRoot, 'baize-project'), { openaiBaseUrl: 'https://explicit-proxy.example.com/v1' }), true);
|
|
116
|
+
|
|
119
117
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
120
|
-
assert.
|
|
118
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
119
|
+
assert.match(config, /^model = "old"$/m, 'unrelated keys preserved');
|
|
121
120
|
} finally {
|
|
122
121
|
if (originalHome === undefined) delete process.env.HOME;
|
|
123
122
|
else process.env.HOME = originalHome;
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('--codex-base-url path writes a baize-init provider block (D51)', async () => {
|
|
129
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-init-provider-'));
|
|
130
|
+
const originalHome = process.env.HOME;
|
|
131
|
+
const originalBaizeDir = process.env.BAIZE_DIR;
|
|
132
|
+
|
|
133
|
+
process.env.HOME = tmpRoot;
|
|
134
|
+
process.env.BAIZE_DIR = path.join(tmpRoot, 'baize');
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { applyCodexProviderBlock } = await import('../runtime-setup.js');
|
|
138
|
+
|
|
139
|
+
// This mirrors exactly what init.js does when --codex-base-url is given:
|
|
140
|
+
// pendingCodexBaseUrl → applyCodexProviderBlock (providerKey 'baize-init').
|
|
141
|
+
// projectDir passed explicitly: the module-level BAIZE_DIR fallback is
|
|
142
|
+
// captured at first import and may not match this test's env override.
|
|
143
|
+
applyCodexProviderBlock({
|
|
144
|
+
providerKey: 'baize-init',
|
|
145
|
+
baseUrl: 'https://proxy.example.com/v1',
|
|
146
|
+
token: 'sk-init-key',
|
|
147
|
+
projectDir: process.env.BAIZE_DIR,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const config = fs.readFileSync(path.join(tmpRoot, '.codex', 'config.toml'), 'utf8');
|
|
151
|
+
assert.match(config, /\[model_providers\.baize-init\]/);
|
|
152
|
+
assert.match(config, /^model_provider = "baize-init"$/m);
|
|
153
|
+
assert.match(config, /^name = "baize-init"$/m);
|
|
154
|
+
assert.match(config, /^base_url = "https:\/\/proxy\.example\.com\/v1"$/m);
|
|
155
|
+
assert.match(config, /^wire_api = "responses"$/m);
|
|
156
|
+
assert.match(config, /^experimental_bearer_token = "sk-init-key"$/m);
|
|
157
|
+
assert.doesNotMatch(config, /openai_base_url/);
|
|
158
|
+
|
|
159
|
+
// Project-level: no provider block, no override.
|
|
160
|
+
const projectConfig = fs.readFileSync(path.join(tmpRoot, 'baize', '.codex', 'config.toml'), 'utf8');
|
|
161
|
+
assert.doesNotMatch(projectConfig, /model_provider/);
|
|
162
|
+
assert.doesNotMatch(projectConfig, /\[model_providers\./);
|
|
163
|
+
assert.doesNotMatch(projectConfig, /openai_base_url/);
|
|
164
|
+
} finally {
|
|
165
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
166
|
+
else process.env.HOME = originalHome;
|
|
167
|
+
if (originalBaizeDir === undefined) delete process.env.BAIZE_DIR;
|
|
168
|
+
else process.env.BAIZE_DIR = originalBaizeDir;
|
|
127
169
|
|
|
128
170
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
129
171
|
}
|
|
@@ -126,6 +126,8 @@ describe('runtime base URL support', () => {
|
|
|
126
126
|
assert.equal(claudeSettings.env.ANTHROPIC_BASE_URL, 'https://claude-proxy.example.com');
|
|
127
127
|
|
|
128
128
|
const codexConfig = fs.readFileSync(path.join(tmpRoot, '.codex', 'config.toml'), 'utf8');
|
|
129
|
-
assert.match(codexConfig, /
|
|
129
|
+
assert.match(codexConfig, /model_provider = "baize-runtime"/);
|
|
130
|
+
assert.match(codexConfig, /\[model_providers\.baize-runtime\][\s\S]*?base_url = "https:\/\/codex-proxy\.example\.com\/v1"/);
|
|
131
|
+
assert.doesNotMatch(codexConfig, /openai_base_url/);
|
|
130
132
|
});
|
|
131
133
|
});
|
|
@@ -278,12 +278,14 @@ describe('Claude launch — compat mode PATH dedupe', () => {
|
|
|
278
278
|
await makeAdapter(ClaudeAdapter).launch({ bypassPermissions: false });
|
|
279
279
|
const env = readSpecEnv();
|
|
280
280
|
assert.ok(env, 'spec should be written');
|
|
281
|
-
|
|
281
|
+
// D54: known bin dirs first + deduplicated (npm-global static first)
|
|
282
|
+
const expected = `${fakeHome}/.npm-global/bin:${fakeHome}/.local/bin:${fakeHome}/.claude/bin:${fakeHome}/baize/bin:${fakeHome}/.local/node/bin:/a:/b:/c`;
|
|
283
|
+
assert.equal(env.PATH, expected, 'PATH must carry known bins first and be deduplicated in compat mode');
|
|
282
284
|
|
|
283
285
|
const tmux = findTmuxNewSession();
|
|
284
286
|
const pathArg = tmux.args.find(a => a.startsWith('PATH='));
|
|
285
287
|
assert.ok(pathArg, 'tmux args should contain PATH= env');
|
|
286
|
-
assert.equal(pathArg,
|
|
288
|
+
assert.equal(pathArg, `PATH=${expected}`, 'tmux -e PATH must also carry known bins + dedupe');
|
|
287
289
|
} finally {
|
|
288
290
|
process.env.PATH = origPath;
|
|
289
291
|
// Restore default (clean env is now the default)
|