@baize-ai/core 0.3.14 → 0.3.16
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/CHANGELOG.md +26 -0
- package/Dockerfile +10 -1
- package/cli/commands/add.js +3 -0
- package/cli/commands/component.js +289 -40
- package/cli/commands/init.js +91 -5
- 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-setup.test.js +53 -6
- package/cli/lib/runtime/codex.js +17 -0
- package/cli/lib/runtime-setup.js +153 -12
- package/cli/lib/upgrade.js +55 -15
- package/docker/entrypoint.sh +0 -20
- package/docker-compose.yml +20 -2
- package/package.json +2 -2
- package/scripts/docker-publish.sh +0 -4
- package/scripts/pack-release.sh +34 -26
- package/skills/web-console/public/app.js +44 -82
- package/skills/web-console/public/index.html +1 -1
- package/skills/web-console/scripts/a2a-admin.js +225 -26
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +22 -32
- package/templates/pm2/ecosystem.config.cjs +6 -0
- package/test/channel-admin.test.js +7 -5
- package/test/helpers/run-upgrade-file-driver.mjs +15 -0
- package/test/model-provider.test.js +150 -40
- package/test/upgrade-file.test.js +229 -0
- package/test/upgrade-local-version.test.js +70 -0
- package/test/upgrade-restart-hook.test.js +129 -0
- package/test/web-console-routes.test.js +94 -17
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ 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.16] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Codex 供应商配置改造(D51):独立 provider 块写入器 + kind/slug store + init/runtime 遗留路径转换 + web-console 只读激活卡;`/models` 探测读取 provider 块 base_url
|
|
12
|
+
- Dockerfile 本地源码构建支持(D51):`BAZE_CORE_TARBALL=/core.tgz` 安装工作区包而非 registry 版本——验证未发布修复(如 D51)进镜像
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- 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`
|
|
16
|
+
- D51 配置污染事故根因(4a37368):`globalCodexConfigPath` 优先读 `HOME` env 而非 `os.homedir()`——jest ESM sandbox 曾写穿真实 `~/.codex/config.toml`
|
|
17
|
+
- D51 review 修复:restore 只删 baize-written 块(`name=key && wire_api=responses`);写盘直设 `0600` + 去重用例;激活卡展示 kind
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- A2A 授权治理收敛(D52 2b11171):agent 本地 authz 仅保留 mode 切换(open/allowlist)——allow/block 名单由 admin 控制台统一管控(PUT 忽略名单强制清空 + UI 隐藏名单编辑并提示管控来源;移除死代码 `validateAuthzList`/peerIds 请求)
|
|
21
|
+
|
|
22
|
+
## [0.3.15] - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- A2A daemon 监控自愈统一收编 `baize init`(D49):init 幂等 bootstrap(enabled → detached `cli.js start --background` → daemon-ctl pm2+waitReady → pm2 save),结果落 `components/a2a/bootstrap-state.json` 并在 web 状态卡展示;Docker 与裸机同一路径,entrypoint 的 Docker 专属 Step 3b(D48)删除
|
|
26
|
+
- core pm2 ecosystem 不再纳管 baize-a2a(D49):其生命周期唯一归属 daemon-ctl——消除三条启动路径并存导致的 8443 双实例竞态
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `baize upgrade <name> --file <tgz>`(D49):商业组件离线升级——manifest/版本闸(新 > 旧)、备份回滚、既有 9 步管线;step8 重启钩子对 baize-a2a 优先走其 `cli.js restart`(覆盖 nohup daemon,pm2-only 路径的盲区修复)
|
|
30
|
+
- getLocalVersion 优先读组件 package.json(SKILL.md frontmatter 曾恒 0.1.0 → 升级判定失真)
|
|
31
|
+
- Web 控制台 A2A 上传:已安装 = 升级(版本闸 409 version_not_higher + 备份回滚 + 自动重启 daemon),安装/升级双语义
|
|
32
|
+
- docker-compose healthcheck:A2A enabled 时追加 TCP 8443 就绪探测(pm2 list 无法发现静默空转的 daemon)
|
|
33
|
+
|
|
8
34
|
## [0.3.14] - 2026-08-30
|
|
9
35
|
|
|
10
36
|
### Fixed
|
package/Dockerfile
CHANGED
|
@@ -82,8 +82,12 @@ 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=
|
|
85
89
|
WORKDIR /home/baize
|
|
86
|
-
RUN npm install -g @baize-ai/core@${BAZE_CORE_VERSION} \
|
|
90
|
+
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
91
|
&& baize --version \
|
|
88
92
|
# ── Build-time skill dependency preinstall (reproducibility) ─────────
|
|
89
93
|
# `baize init` reinstalls skill deps at runtime; doing it here bakes the
|
|
@@ -134,6 +138,11 @@ RUN mkdir -p \
|
|
|
134
138
|
# ── Copy PM2 ecosystem config ─────────────────────────────────────────────────
|
|
135
139
|
COPY --chown=baize:baize templates/pm2/ecosystem.config.cjs /home/baize/baize/pm2/ecosystem.config.cjs
|
|
136
140
|
|
|
141
|
+
# ── Local-source build context (optional) ────────────────────────────────────
|
|
142
|
+
# When BAZE_CORE_TARBALL is set, the tarball must live at ./core.tgz in the
|
|
143
|
+
# build context; it is staged at /tmp so the install RUN above can consume it.
|
|
144
|
+
COPY core.tgz /tmp/core.tgz
|
|
145
|
+
|
|
137
146
|
# ── Copy entrypoint ───────────────────────────────────────────────────────────
|
|
138
147
|
COPY --chown=baize:baize docker/entrypoint.sh /entrypoint.sh
|
|
139
148
|
RUN chmod +x /entrypoint.sh
|
package/cli/commands/add.js
CHANGED
|
@@ -714,6 +714,9 @@ async function installDeclarative(resolved, skillDir, skipConfirm, jsonOutput, b
|
|
|
714
714
|
console.log(` ${success(`${bold(service.name || ('baize-' + resolved.name))} started`)}`);
|
|
715
715
|
} else {
|
|
716
716
|
console.log(` ${error(`Failed to start service: ${svcResult.error}`)}`);
|
|
717
|
+
if (name === 'a2a') {
|
|
718
|
+
console.log(` ${dim('Run `baize a2a start` (or \`baize init\`) to bring the A2A daemon up.')}`);
|
|
719
|
+
}
|
|
717
720
|
console.log(` ${dim('You can start it manually later.')}`);
|
|
718
721
|
}
|
|
719
722
|
}
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { execFileSync } from 'node:child_process';
|
|
8
9
|
import { BAIZE_DIR, SKILLS_DIR, COMPONENTS_DIR, getBaizeConfig } from '../lib/config.js';
|
|
9
10
|
import { bold, dim, green, red, yellow, cyan, success, error, warn, heading } from '../lib/colors.js';
|
|
10
11
|
import { loadRegistry } from '../lib/registry.js';
|
|
11
|
-
import { searchNpmPackages, npmRegistryUrl } from '../lib/download.js';
|
|
12
|
+
import { searchNpmPackages, npmRegistryUrl, acquireSource } from '../lib/download.js';
|
|
12
13
|
import { loadComponents, saveComponents } from '../lib/components.js';
|
|
13
|
-
import { checkForUpdates, getLocalSourceUpgradeError, getRepo, runUpgrade, downloadToTemp, readChangelog, filterChangelog, cleanupTemp } from '../lib/upgrade.js';
|
|
14
|
+
import { checkForUpdates, getLocalVersion, getLocalSourceUpgradeError, getRepo, runUpgrade, downloadToTemp, readChangelog, filterChangelog, cleanupTemp } from '../lib/upgrade.js';
|
|
14
15
|
import {
|
|
15
16
|
checkForCoreUpdates, runSelfUpgrade,
|
|
16
17
|
downloadCoreToTemp, readChangelog as readCoreChangelog,
|
|
@@ -21,7 +22,7 @@ import { parseSkillMd } from '../lib/skill.js';
|
|
|
21
22
|
import { linkBins, unlinkBins } from '../lib/bin.js';
|
|
22
23
|
import { removeCaddyRoutes } from '../lib/caddy.js';
|
|
23
24
|
import { acquireLock, releaseLock } from '../lib/lock.js';
|
|
24
|
-
import { fetchRawFile } from '../lib/github.js';
|
|
25
|
+
import { fetchRawFile, compareSemverDesc } from '../lib/github.js';
|
|
25
26
|
import { promptYesNo } from '../lib/prompts.js';
|
|
26
27
|
import { evaluateUpgrade } from '../lib/claude-eval.js';
|
|
27
28
|
|
|
@@ -251,6 +252,8 @@ export async function upgradeComponent(args) {
|
|
|
251
252
|
const skipEval = args.includes('--skip-eval');
|
|
252
253
|
const beta = args.includes('--beta');
|
|
253
254
|
const hasTempDirFlag = args.includes('--temp-dir');
|
|
255
|
+
const fileIndex = args.indexOf('--file');
|
|
256
|
+
const upgradeFile = fileIndex !== -1 ? args[fileIndex + 1] : null;
|
|
254
257
|
|
|
255
258
|
if (hasTempDirFlag) {
|
|
256
259
|
const msg = '--temp-dir is not supported.';
|
|
@@ -272,6 +275,17 @@ export async function upgradeComponent(args) {
|
|
|
272
275
|
process.exit(1);
|
|
273
276
|
}
|
|
274
277
|
|
|
278
|
+
if (fileIndex !== -1 && (!upgradeFile || upgradeFile.startsWith('-'))) {
|
|
279
|
+
console.error('Error: --file requires a path to a .tgz archive.');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --file is a fully offline source: remote/branch resolution flags make no sense.
|
|
284
|
+
if (upgradeFile && (branch || beta)) {
|
|
285
|
+
console.error('Error: --file cannot be combined with --branch or --beta.');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
275
289
|
// --beta and --branch are mutually exclusive
|
|
276
290
|
if (beta && branch) {
|
|
277
291
|
console.error('Error: --beta and --branch are mutually exclusive.');
|
|
@@ -291,7 +305,7 @@ export async function upgradeComponent(args) {
|
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
// Get target component (filter out flags and flag values)
|
|
294
|
-
const flagsWithValues = new Set(['--branch', '--mode']);
|
|
308
|
+
const flagsWithValues = new Set(['--branch', '--mode', '--file']);
|
|
295
309
|
const target = args.find((a, i) => {
|
|
296
310
|
if (a.startsWith('-')) return false;
|
|
297
311
|
if (a === 'confirm') return false;
|
|
@@ -300,6 +314,12 @@ export async function upgradeComponent(args) {
|
|
|
300
314
|
return true;
|
|
301
315
|
});
|
|
302
316
|
|
|
317
|
+
// --file upgrades one installed component from a local tarball.
|
|
318
|
+
if (upgradeFile && (upgradeAll || upgradeSelf)) {
|
|
319
|
+
console.error('Error: --file upgrades a single installed component; it cannot be combined with --all or --self.');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
303
323
|
// Handle --self: upgrade baize-core itself
|
|
304
324
|
if (upgradeSelf) {
|
|
305
325
|
if (checkOnly) {
|
|
@@ -326,6 +346,7 @@ export async function upgradeComponent(args) {
|
|
|
326
346
|
console.log(' --yes, -y Skip confirmation');
|
|
327
347
|
console.log(' --skip-eval Skip upgrade analysis of local changes');
|
|
328
348
|
console.log(' --beta Include prerelease (beta) versions');
|
|
349
|
+
console.log(' --file <tgz> Upgrade from a local .tar.gz archive (offline delivery)');
|
|
329
350
|
console.log(' --branch <b> Upgrade from a specific branch (e.g. feat/xxx)');
|
|
330
351
|
console.log(' --mode <m> Merge mode: "merge" (default, smart three-way) or "overwrite"');
|
|
331
352
|
console.log('\nExamples:');
|
|
@@ -356,20 +377,25 @@ export async function upgradeComponent(args) {
|
|
|
356
377
|
process.exit(1);
|
|
357
378
|
}
|
|
358
379
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
if (upgradeFile) {
|
|
381
|
+
// The tarball IS the local source for this upgrade — a local-source
|
|
382
|
+
// registration does not block it.
|
|
383
|
+
} else {
|
|
384
|
+
const localSourceError = getLocalSourceUpgradeError(target, components[target]);
|
|
385
|
+
if (localSourceError) {
|
|
386
|
+
const result = {
|
|
387
|
+
action: checkOnly ? 'check' : 'upgrade',
|
|
388
|
+
component: target,
|
|
389
|
+
...localSourceError,
|
|
390
|
+
reply: localSourceError.message,
|
|
391
|
+
};
|
|
392
|
+
if (jsonOutput) {
|
|
393
|
+
console.log(JSON.stringify(result, null, 2));
|
|
394
|
+
} else {
|
|
395
|
+
console.error(`Error: ${localSourceError.message}`);
|
|
396
|
+
}
|
|
397
|
+
process.exit(1);
|
|
371
398
|
}
|
|
372
|
-
process.exit(1);
|
|
373
399
|
}
|
|
374
400
|
|
|
375
401
|
if (!fs.existsSync(skillDir)) {
|
|
@@ -388,6 +414,17 @@ export async function upgradeComponent(args) {
|
|
|
388
414
|
process.exit(1);
|
|
389
415
|
}
|
|
390
416
|
|
|
417
|
+
// Mode 0: offline upgrade from a local tgz (K4 commercial delivery —
|
|
418
|
+
// baize-a2a never ships to npm; --file and web upload are the only channels).
|
|
419
|
+
if (upgradeFile) {
|
|
420
|
+
if (checkOnly) {
|
|
421
|
+
return handleFileCheckOnly(target, upgradeFile, { jsonOutput });
|
|
422
|
+
}
|
|
423
|
+
const ok = await handleFileUpgradeFlow(target, upgradeFile, { jsonOutput, skipConfirm: skipConfirm || explicitConfirm, mode });
|
|
424
|
+
if (!ok) process.exit(1);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
391
428
|
// Mode 1: Check only (--check) — no lock, downloads to temp for file comparison
|
|
392
429
|
if (checkOnly) {
|
|
393
430
|
return handleCheckOnly(target, { jsonOutput, branch, beta });
|
|
@@ -717,29 +754,7 @@ async function handleUpgradeFlow(component, { jsonOutput, skipConfirm, skipEval,
|
|
|
717
754
|
});
|
|
718
755
|
|
|
719
756
|
if (result.success) {
|
|
720
|
-
|
|
721
|
-
// Update components.json
|
|
722
|
-
const components = loadComponents();
|
|
723
|
-
if (components[component]) {
|
|
724
|
-
components[component].version = result.to || components[component].version;
|
|
725
|
-
components[component].upgradedAt = new Date().toISOString();
|
|
726
|
-
|
|
727
|
-
// Update bin symlinks (remove old, create new)
|
|
728
|
-
const oldBin = components[component].bin;
|
|
729
|
-
if (oldBin) unlinkBins(oldBin);
|
|
730
|
-
const updatedSkill = parseSkillMd(skillDir);
|
|
731
|
-
const newBin = linkBins(skillDir, updatedSkill?.frontmatter?.bin);
|
|
732
|
-
if (newBin) {
|
|
733
|
-
components[component].bin = newBin;
|
|
734
|
-
} else {
|
|
735
|
-
delete components[component].bin;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
saveComponents(components);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Clean old backups (keep only the latest)
|
|
742
|
-
cleanOldBackups(skillDir);
|
|
757
|
+
applyUpgradeBookkeeping(component, skillDir, result);
|
|
743
758
|
}
|
|
744
759
|
|
|
745
760
|
// Output result
|
|
@@ -777,6 +792,240 @@ async function handleUpgradeFlow(component, { jsonOutput, skipConfirm, skipEval,
|
|
|
777
792
|
}
|
|
778
793
|
}
|
|
779
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Post-success bookkeeping shared by every upgrade path: bump components.json,
|
|
797
|
+
* refresh bin symlinks, prune old backups.
|
|
798
|
+
*/
|
|
799
|
+
function applyUpgradeBookkeeping(component, skillDir, result) {
|
|
800
|
+
const components = loadComponents();
|
|
801
|
+
if (components[component]) {
|
|
802
|
+
components[component].version = result.to || components[component].version;
|
|
803
|
+
components[component].upgradedAt = new Date().toISOString();
|
|
804
|
+
|
|
805
|
+
// Update bin symlinks (remove old, create new)
|
|
806
|
+
const oldBin = components[component].bin;
|
|
807
|
+
if (oldBin) unlinkBins(oldBin);
|
|
808
|
+
const updatedSkill = parseSkillMd(skillDir);
|
|
809
|
+
const newBin = linkBins(skillDir, updatedSkill?.frontmatter?.bin);
|
|
810
|
+
if (newBin) {
|
|
811
|
+
components[component].bin = newBin;
|
|
812
|
+
} else {
|
|
813
|
+
delete components[component].bin;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
saveComponents(components);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Clean old backups (keep only the latest)
|
|
820
|
+
cleanOldBackups(skillDir);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Resolve a --file path (~ expansion + absolutize).
|
|
825
|
+
*/
|
|
826
|
+
function resolveTgzPath(filePath) {
|
|
827
|
+
const expanded = filePath === '~'
|
|
828
|
+
? os.homedir()
|
|
829
|
+
: filePath.startsWith('~/')
|
|
830
|
+
? path.join(os.homedir(), filePath.slice(2))
|
|
831
|
+
: filePath;
|
|
832
|
+
return path.resolve(expanded);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Extract a local .tgz into a fresh temp dir and read its package manifest.
|
|
837
|
+
* Safety vetting (path traversal / links) is delegated to the local-tarball
|
|
838
|
+
* source resolver in cli/lib/download.js — the same gate `baize add` uses.
|
|
839
|
+
*
|
|
840
|
+
* @returns {{ ok: true, tempDir: string, version: string, name: string|null }
|
|
841
|
+
* | { ok: false, error: string }}
|
|
842
|
+
*/
|
|
843
|
+
function extractTarballArtifact(tgzPath) {
|
|
844
|
+
if (!/\.(?:tar\.gz|tgz)$/i.test(tgzPath)) {
|
|
845
|
+
return { ok: false, error: `--file must be a .tgz archive: ${tgzPath}` };
|
|
846
|
+
}
|
|
847
|
+
if (!fs.existsSync(tgzPath)) {
|
|
848
|
+
return { ok: false, error: `File not found: ${tgzPath}` };
|
|
849
|
+
}
|
|
850
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'baize-upgrade-file-'));
|
|
851
|
+
const extract = acquireSource({ type: 'local-tarball', path: tgzPath }, tempDir);
|
|
852
|
+
if (!extract.success) {
|
|
853
|
+
cleanupTemp(tempDir);
|
|
854
|
+
return { ok: false, error: extract.error || 'Failed to extract tarball' };
|
|
855
|
+
}
|
|
856
|
+
let version = null;
|
|
857
|
+
let name = null;
|
|
858
|
+
try {
|
|
859
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(tempDir, 'package.json'), 'utf8'));
|
|
860
|
+
if (typeof pkg.version === 'string' && pkg.version.trim()) version = pkg.version.trim();
|
|
861
|
+
if (typeof pkg.name === 'string' && pkg.name.trim()) name = pkg.name.trim();
|
|
862
|
+
} catch {
|
|
863
|
+
// missing/invalid package.json → version stays null, rejected by caller
|
|
864
|
+
}
|
|
865
|
+
if (!version) {
|
|
866
|
+
cleanupTemp(tempDir);
|
|
867
|
+
return { ok: false, error: 'Tarball has no package.json version' };
|
|
868
|
+
}
|
|
869
|
+
return { ok: true, tempDir, version, name };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* K4: offline upgrade from a local .tgz (commercial delivery channel —
|
|
874
|
+
* baize-a2a never ships to npm, so --file and web upload are the only
|
|
875
|
+
* upgrade paths). Skips checkForUpdates/downloadToTemp: the tarball IS the
|
|
876
|
+
* source. Version gate: incoming semver must be strictly higher than the
|
|
877
|
+
* locally installed one (getLocalVersion reads package.json first — K6).
|
|
878
|
+
* Everything downstream reuses the existing 9-step runUpgrade pipeline
|
|
879
|
+
* (backup → smart merge → npm install → baseline → restart), including
|
|
880
|
+
* failure rollback.
|
|
881
|
+
*
|
|
882
|
+
* Returns true on success, false on failure. No process.exit() here.
|
|
883
|
+
*/
|
|
884
|
+
async function handleFileUpgradeFlow(component, filePath, { jsonOutput, skipConfirm, mode = 'merge' }) {
|
|
885
|
+
const skillDir = path.join(SKILLS_DIR, component);
|
|
886
|
+
let tempDir = null;
|
|
887
|
+
|
|
888
|
+
const failWith = (payload) => {
|
|
889
|
+
if (jsonOutput) {
|
|
890
|
+
const errOutput = { action: 'upgrade', component, success: false, ...payload };
|
|
891
|
+
errOutput.reply = formatC4Reply('error', { message: payload.message || payload.error });
|
|
892
|
+
console.log(JSON.stringify(errOutput, null, 2));
|
|
893
|
+
} else {
|
|
894
|
+
console.error(`Error: ${payload.message || payload.error}`);
|
|
895
|
+
}
|
|
896
|
+
return false;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const artifact = extractTarballArtifact(resolveTgzPath(filePath));
|
|
900
|
+
if (!artifact.ok) {
|
|
901
|
+
return failWith({ error: artifact.error });
|
|
902
|
+
}
|
|
903
|
+
tempDir = artifact.tempDir;
|
|
904
|
+
|
|
905
|
+
const lockResult = acquireLock(component);
|
|
906
|
+
if (!lockResult.success) {
|
|
907
|
+
return failWith({ error: lockResult.error });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
// Manifest sanity: the artifact must be the same package as installed.
|
|
912
|
+
let installedName = null;
|
|
913
|
+
try {
|
|
914
|
+
const localPkg = JSON.parse(fs.readFileSync(path.join(skillDir, 'package.json'), 'utf8'));
|
|
915
|
+
if (typeof localPkg.name === 'string') installedName = localPkg.name;
|
|
916
|
+
} catch {
|
|
917
|
+
// skill package.json is optional for the name check
|
|
918
|
+
}
|
|
919
|
+
if (installedName && artifact.name && artifact.name !== installedName) {
|
|
920
|
+
return failWith({ error: `manifest name mismatch: installed ${installedName}, tarball ${artifact.name}` });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const local = getLocalVersion(skillDir);
|
|
924
|
+
if (!local.success) {
|
|
925
|
+
return failWith({ error: `Cannot read current version: ${local.error}` });
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (compareSemverDesc(local.version, artifact.version) <= 0) {
|
|
929
|
+
return failWith({
|
|
930
|
+
error: 'version_not_higher',
|
|
931
|
+
message: `Incoming version ${artifact.version} is not higher than installed ${local.version} — nothing to upgrade.`,
|
|
932
|
+
local: local.version,
|
|
933
|
+
incoming: artifact.version,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (!jsonOutput) {
|
|
938
|
+
console.log(`\n${bold(component)}: ${dim(local.version)} → ${bold(artifact.version)}`);
|
|
939
|
+
console.log(dim(`Source: local tarball (${filePath})`));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Confirmation (no download/eval steps — the artifact is already local)
|
|
943
|
+
if (!skipConfirm) {
|
|
944
|
+
const confirmed = await promptYesNo('Proceed with upgrade? [y/N]: ');
|
|
945
|
+
if (!confirmed) {
|
|
946
|
+
console.log('Upgrade cancelled.');
|
|
947
|
+
return true; // Not an error — user chose to cancel
|
|
948
|
+
}
|
|
949
|
+
} else if (!jsonOutput) {
|
|
950
|
+
console.log(`Upgrading ${bold(component)}...`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const result = runUpgrade(component, {
|
|
954
|
+
tempDir,
|
|
955
|
+
newVersion: artifact.version,
|
|
956
|
+
mode,
|
|
957
|
+
jsonOutput,
|
|
958
|
+
onStep: !jsonOutput ? printStep : undefined,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
if (result.success) {
|
|
962
|
+
applyUpgradeBookkeeping(component, skillDir, result);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (jsonOutput) {
|
|
966
|
+
const output = { ...result, source: { type: 'local-tarball', path: resolveTgzPath(filePath) } };
|
|
967
|
+
output.reply = formatC4Reply('upgrade', { component, ...result });
|
|
968
|
+
console.log(JSON.stringify(output, null, 2));
|
|
969
|
+
} else if (result.success) {
|
|
970
|
+
console.log(`\n${success(`${bold(component)} upgraded: ${dim(result.from)} → ${bold(result.to)}`)}`);
|
|
971
|
+
} else {
|
|
972
|
+
console.log(`\n${error(`Upgrade failed (step ${result.failedStep}): ${result.error}`)}`);
|
|
973
|
+
if (result.rollback?.performed) {
|
|
974
|
+
console.log(`\n${bold('Auto-rollback performed:')}`);
|
|
975
|
+
for (const r of result.rollback.steps) {
|
|
976
|
+
if (r.success) console.log(` ${success(r.action)}`);
|
|
977
|
+
else console.log(` ${error(r.action)}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return result.success;
|
|
983
|
+
} finally {
|
|
984
|
+
cleanupTemp(tempDir);
|
|
985
|
+
releaseLock(component);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* K4 --check variant: compare a local tgz against the installed version
|
|
991
|
+
* without touching the installation (no lock needed).
|
|
992
|
+
*/
|
|
993
|
+
function handleFileCheckOnly(component, filePath, { jsonOutput }) {
|
|
994
|
+
const skillDir = path.join(SKILLS_DIR, component);
|
|
995
|
+
const artifact = extractTarballArtifact(resolveTgzPath(filePath));
|
|
996
|
+
if (!artifact.ok) {
|
|
997
|
+
if (jsonOutput) {
|
|
998
|
+
console.log(JSON.stringify({ action: 'check', component, success: false, error: artifact.error }, null, 2));
|
|
999
|
+
} else {
|
|
1000
|
+
console.error(`Error: ${artifact.error}`);
|
|
1001
|
+
}
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const local = getLocalVersion(skillDir);
|
|
1006
|
+
const hasUpdate = local.success && compareSemverDesc(local.version, artifact.version) > 0;
|
|
1007
|
+
const result = {
|
|
1008
|
+
action: 'check',
|
|
1009
|
+
component,
|
|
1010
|
+
success: local.success,
|
|
1011
|
+
hasUpdate,
|
|
1012
|
+
current: local.success ? local.version : null,
|
|
1013
|
+
latest: artifact.version,
|
|
1014
|
+
source: { type: 'local-tarball', path: resolveTgzPath(filePath) },
|
|
1015
|
+
...(local.success ? {} : { error: local.error }),
|
|
1016
|
+
};
|
|
1017
|
+
if (jsonOutput) {
|
|
1018
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1019
|
+
} else if (hasUpdate) {
|
|
1020
|
+
console.log(`${bold(component)}: ${dim(result.current)} → ${bold(result.latest)} (local tarball)`);
|
|
1021
|
+
} else if (local.success) {
|
|
1022
|
+
console.log(success(`${bold(component)} is up to date (v${result.current}); tarball v${artifact.version} is not higher.`));
|
|
1023
|
+
} else {
|
|
1024
|
+
console.error(`Error: ${local.error}`);
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
780
1029
|
/**
|
|
781
1030
|
* Clean old .backup/ directories, keeping only the latest.
|
|
782
1031
|
*/
|
package/cli/commands/init.js
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
saveCodexApiKeyToEnv,
|
|
42
42
|
saveCodexBaseUrlToEnv,
|
|
43
43
|
writeCodexConfig,
|
|
44
|
+
applyCodexProviderBlock,
|
|
44
45
|
} from '../lib/runtime-setup.js';
|
|
45
46
|
|
|
46
47
|
// Source directories (shipped with baize package)
|
|
@@ -839,6 +840,30 @@ function syncCoreSkills() {
|
|
|
839
840
|
const installed = [];
|
|
840
841
|
const updated = [];
|
|
841
842
|
|
|
843
|
+
// D51: web-console imports `../../../cli/lib/runtime-setup.js`, which from
|
|
844
|
+
// `~/.claude/skills/web-console/scripts/` resolves to `~/.claude/cli/lib/`.
|
|
845
|
+
// The npm package ships `package/cli`, but init never deployed it to the
|
|
846
|
+
// runtime tree — web-console crashed with ERR_MODULE_NOT_FOUND in the image.
|
|
847
|
+
// Deploy the shared CLI lib beside SKILLS_DIR so the import path mirrors the
|
|
848
|
+
// package layout (package/cli ↔ <SKILLS_DIR>/../cli).
|
|
849
|
+
const cliSrc = path.join(PACKAGE_ROOT, 'cli');
|
|
850
|
+
const cliDest = path.join(path.dirname(SKILLS_DIR), 'cli');
|
|
851
|
+
if (fs.existsSync(cliSrc)) {
|
|
852
|
+
try {
|
|
853
|
+
copyTree(cliSrc, cliDest);
|
|
854
|
+
// runtime-setup.js imports smol-toml (D51). The cli/ tree is deployed
|
|
855
|
+
// outside any skill's node_modules, so the dependency must ride along —
|
|
856
|
+
// mirror the package layout by copying the package's smol-toml into
|
|
857
|
+
// <cliDest>/node_modules/.
|
|
858
|
+
const tomlSrc = path.join(PACKAGE_ROOT, 'node_modules', 'smol-toml');
|
|
859
|
+
if (fs.existsSync(tomlSrc)) {
|
|
860
|
+
copyTree(tomlSrc, path.join(cliDest, 'node_modules', 'smol-toml'));
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
console.log(` ${warn(`Failed to sync cli/ to ${cliDest}`)}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
842
867
|
const entries = fs.readdirSync(CORE_SKILLS_SRC, { withFileTypes: true });
|
|
843
868
|
for (const entry of entries) {
|
|
844
869
|
if (!entry.isDirectory()) continue;
|
|
@@ -1145,6 +1170,47 @@ function startCoreServices(webPassword = null) {
|
|
|
1145
1170
|
}
|
|
1146
1171
|
}
|
|
1147
1172
|
|
|
1173
|
+
// ── A2A daemon bootstrap (D49) ───────────────────────────────────
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Fire-and-forget A2A daemon bootstrap. When the commercial baize-a2a
|
|
1177
|
+
* component is installed AND enabled, bring its daemon up through the
|
|
1178
|
+
* component's own CLI (`start --background`), which:
|
|
1179
|
+
* - prefers pm2 (autorestart + `pm2 save`/resurrect survive reboots),
|
|
1180
|
+
* - waits for TCP + card-identity readiness (30s),
|
|
1181
|
+
* - falls back to a nohup daemon when pm2 is unavailable,
|
|
1182
|
+
* - records the outcome in components/a2a/bootstrap-state.json.
|
|
1183
|
+
*
|
|
1184
|
+
* CRITICAL: this must NEVER block init. The child runs detached; init does
|
|
1185
|
+
* not wait and cannot fail because of it. Docker and bare metal take the
|
|
1186
|
+
* exact same path — the Docker-only entrypoint self-heal (D48 Step 3b) was
|
|
1187
|
+
* removed in favor of this unified bootstrap (user decision, 2026-08-31).
|
|
1188
|
+
*/
|
|
1189
|
+
function bootstrapA2aDaemon({ quiet = false } = {}) {
|
|
1190
|
+
try {
|
|
1191
|
+
const cli = path.join(SKILLS_DIR, 'a2a', 'scripts', 'cli.js');
|
|
1192
|
+
const a2aConfigPath = path.join(COMPONENTS_DIR, 'a2a', 'config.json');
|
|
1193
|
+
if (!fs.existsSync(cli) || !fs.existsSync(a2aConfigPath)) return;
|
|
1194
|
+
let enabled = false;
|
|
1195
|
+
try {
|
|
1196
|
+
enabled = JSON.parse(fs.readFileSync(a2aConfigPath, 'utf8')).enabled === true;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (!enabled) return;
|
|
1201
|
+
fs.mkdirSync(path.join(COMPONENTS_DIR, 'a2a', 'logs'), { recursive: true });
|
|
1202
|
+
const child = spawn(process.execPath, [cli, 'start', '--background'], {
|
|
1203
|
+
detached: true,
|
|
1204
|
+
stdio: 'ignore',
|
|
1205
|
+
env: process.env,
|
|
1206
|
+
});
|
|
1207
|
+
child.unref();
|
|
1208
|
+
if (!quiet) console.log(` ${dim('A2A daemon bootstrap launched (background) — see components/a2a/bootstrap-state.json.')}`);
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
if (!quiet) console.log(` ${warn(`A2A bootstrap failed to launch: ${err.message}`)}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1148
1214
|
// ── PM2 boot auto-start ──────────────────────────────────────────
|
|
1149
1215
|
|
|
1150
1216
|
/**
|
|
@@ -2141,9 +2207,17 @@ export async function initCommand(args) {
|
|
|
2141
2207
|
}
|
|
2142
2208
|
}
|
|
2143
2209
|
}
|
|
2144
|
-
|
|
2145
|
-
//
|
|
2146
|
-
|
|
2210
|
+
// Write ~/.codex/config.toml to suppress interactive prompts on first
|
|
2211
|
+
// launch. A custom --codex-base-url becomes a dedicated provider block
|
|
2212
|
+
// (D51) — never the built-in-OpenAI openai_base_url override.
|
|
2213
|
+
if (pendingCodexBaseUrl) {
|
|
2214
|
+
applyCodexProviderBlock({
|
|
2215
|
+
providerKey: 'baize-init',
|
|
2216
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2217
|
+
token: opts.codexApiKey || undefined,
|
|
2218
|
+
});
|
|
2219
|
+
if (!quiet) console.log(` ${success('Codex startup config written (provider block)')}`);
|
|
2220
|
+
} else if (writeCodexConfig(BAIZE_DIR)) {
|
|
2147
2221
|
if (!quiet) console.log(` ${success('Codex startup config written')}`);
|
|
2148
2222
|
}
|
|
2149
2223
|
}
|
|
@@ -2368,7 +2442,12 @@ export async function initCommand(args) {
|
|
|
2368
2442
|
}
|
|
2369
2443
|
if (pendingCodexBaseUrl) {
|
|
2370
2444
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2371
|
-
|
|
2445
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2446
|
+
applyCodexProviderBlock({
|
|
2447
|
+
providerKey: 'baize-init',
|
|
2448
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2449
|
+
token: opts.codexApiKey || undefined,
|
|
2450
|
+
});
|
|
2372
2451
|
}
|
|
2373
2452
|
|
|
2374
2453
|
// Timezone: use resolved value or show current
|
|
@@ -2394,6 +2473,7 @@ export async function initCommand(args) {
|
|
|
2394
2473
|
}
|
|
2395
2474
|
if (!quiet) console.log(heading('Starting services...'));
|
|
2396
2475
|
const servicesStarted = startCoreServices(opts.webPassword);
|
|
2476
|
+
bootstrapA2aDaemon({ quiet });
|
|
2397
2477
|
if (servicesStarted > 0) {
|
|
2398
2478
|
setupPm2Startup();
|
|
2399
2479
|
if (!quiet) console.log(`\n${green(`${servicesStarted} service(s) started.`)} ${dim('Run "baize status" to check.')}`);
|
|
@@ -2476,7 +2556,12 @@ export async function initCommand(args) {
|
|
|
2476
2556
|
}
|
|
2477
2557
|
if (pendingCodexBaseUrl) {
|
|
2478
2558
|
saveCodexBaseUrlToEnv(pendingCodexBaseUrl);
|
|
2479
|
-
|
|
2559
|
+
// D51: custom endpoint → dedicated provider block, not the built-in-OpenAI override.
|
|
2560
|
+
applyCodexProviderBlock({
|
|
2561
|
+
providerKey: 'baize-init',
|
|
2562
|
+
baseUrl: pendingCodexBaseUrl,
|
|
2563
|
+
token: opts.codexApiKey || undefined,
|
|
2564
|
+
});
|
|
2480
2565
|
}
|
|
2481
2566
|
// Step 8: Configure timezone
|
|
2482
2567
|
if (!quiet) console.log(`\n${heading('Timezone configuration...')}`);
|
|
@@ -2516,6 +2601,7 @@ export async function initCommand(args) {
|
|
|
2516
2601
|
// Step 11: Start services
|
|
2517
2602
|
if (!quiet) console.log(`\n${heading('Starting services...')}`);
|
|
2518
2603
|
const servicesStarted = startCoreServices(opts.webPassword);
|
|
2604
|
+
bootstrapA2aDaemon({ quiet });
|
|
2519
2605
|
|
|
2520
2606
|
if (servicesStarted > 0) {
|
|
2521
2607
|
setupPm2Startup();
|
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
|
}
|