@baize-ai/core 0.2.0 → 0.3.1
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 +8 -18
- package/CHANGELOG.md +24 -1
- package/Dockerfile +66 -12
- package/README.md +85 -6
- package/README.zh-CN.md +63 -3
- package/assets/logo.png +0 -0
- package/cli/baize.js +6 -0
- package/cli/commands/a2a.js +124 -0
- package/cli/commands/add.js +126 -86
- package/cli/commands/component.js +27 -4
- package/cli/commands/doctor.js +2 -2
- package/cli/commands/init.js +28 -0
- package/cli/commands/self-uninstall.js +1 -1
- package/cli/lib/__tests__/instruction-split.test.js +1 -1
- package/cli/lib/a2a.js +235 -0
- package/cli/lib/lock.js +3 -3
- package/cli/lib/self-upgrade.js +1 -1
- package/docker-compose.yml +1 -1
- package/docs/docker.md +18 -4
- package/docs/release.md +150 -0
- package/package.json +1 -1
- package/scripts/docker-publish.sh +70 -0
- package/scripts/install.sh +4 -4
- package/scripts/pack-release.sh +361 -0
- package/skills/comm-bridge/package.json +7 -3
- package/skills/comm-bridge/scripts/c4-receive.js +23 -2
- package/skills/scheduler/package.json +2 -2
- package/skills/web-console/SKILL.md +34 -0
- package/skills/web-console/package.json +2 -2
- package/skills/web-console/public/app.js +967 -4
- package/skills/web-console/public/index.html +141 -0
- package/skills/web-console/public/styles.css +136 -0
- package/skills/web-console/scripts/a2a-admin.js +533 -0
- package/skills/web-console/scripts/channel-admin.js +99 -16
- package/skills/web-console/scripts/server.js +395 -1
- package/skills/web-console/scripts/skill-catalog.js +179 -0
- package/templates/claude-system.md +22 -0
- package/templates/pm2/ecosystem.config.cjs +4 -1
- package/test/a2a-cli.test.js +180 -0
- package/test/agent-card-api.test.js +261 -0
- package/test/channel-admin.test.js +87 -0
- package/test/component-lock.test.js +216 -0
- package/test/skill-catalog.test.js +118 -0
- package/test/web-console-routes.test.js +512 -1
package/cli/commands/add.js
CHANGED
|
@@ -31,6 +31,7 @@ import { promptYesNo, prompt, promptSecret } from '../lib/prompts.js';
|
|
|
31
31
|
import { writeEnvEntries } from '../lib/env.js';
|
|
32
32
|
import { hasConfigureHook, runConfigureHook } from '../lib/configure-hook.js';
|
|
33
33
|
import { registerService } from '../lib/service.js';
|
|
34
|
+
import { acquireLock, releaseLock } from '../lib/lock.js';
|
|
34
35
|
import { bold, dim, green, red, yellow, cyan, success, error, warn, heading } from '../lib/colors.js';
|
|
35
36
|
|
|
36
37
|
function printManualCaddyRoutes(result) {
|
|
@@ -328,109 +329,147 @@ export async function addComponent(args) {
|
|
|
328
329
|
return;
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
// 5.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (resolved.deliveredVia) {
|
|
338
|
-
console.log(`${heading('File:')} ${dim(resolved.deliveredVia.path)}`);
|
|
339
|
-
console.log(`${heading('Checksum:')} ${resolved.deliveredVia.verified ? green('sha256 verified') : warn('NOT VERIFIED (--trust-file)')}`);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (resolved.isThirdParty) {
|
|
343
|
-
console.log(warn('Third-party component — not verified by Baize team.'));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (regInfo.description) console.log(`${heading('Description:')} ${regInfo.description}`);
|
|
347
|
-
if (regInfo.type) console.log(`${heading('Type:')} ${regInfo.type}`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 6. User confirmation (skip in JSON mode — confirmation handled at application layer)
|
|
351
|
-
if (!skipConfirm && !jsonOutput) {
|
|
352
|
-
console.log('');
|
|
353
|
-
const confirmed = await promptYesNo('Proceed with installation? [Y/n]: ', true);
|
|
354
|
-
if (!confirmed) {
|
|
355
|
-
console.log(dim('Installation cancelled.'));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// 7. Download
|
|
361
|
-
const skillDir = path.join(SKILLS_DIR, resolved.name);
|
|
362
|
-
|
|
363
|
-
if (fs.existsSync(skillDir)) {
|
|
332
|
+
// 5. Acquire the component lock — protects every write below (download,
|
|
333
|
+
// manifest baseline, registration). --check preview mode returned above and
|
|
334
|
+
// never holds the lock. Held through confirmation; released in the finally
|
|
335
|
+
// block, and explicitly on process.exit paths (which bypass finally).
|
|
336
|
+
const lockResult = acquireLock(resolved.name);
|
|
337
|
+
if (!lockResult.success) {
|
|
364
338
|
if (jsonOutput) {
|
|
365
339
|
console.log(JSON.stringify({
|
|
366
340
|
action: 'add', component: resolved.name, success: false,
|
|
367
|
-
error: '
|
|
368
|
-
reply: `
|
|
341
|
+
error: 'component_locked', message: lockResult.error,
|
|
342
|
+
reply: `Error: ${lockResult.error}`,
|
|
369
343
|
}, null, 2));
|
|
370
344
|
} else {
|
|
371
|
-
console.error(
|
|
372
|
-
console.error(dim('Remove it first or use "baize upgrade".'));
|
|
345
|
+
console.error(error(lockResult.error));
|
|
373
346
|
}
|
|
374
347
|
process.exit(1);
|
|
375
348
|
}
|
|
376
349
|
|
|
377
|
-
|
|
378
|
-
|
|
350
|
+
try {
|
|
351
|
+
// 6. Display component info (terminal only)
|
|
352
|
+
if (!jsonOutput) {
|
|
353
|
+
const versionInfo = branch ? ` (branch: ${bold(branch)})` : (resolved.version ? `@${bold(resolved.version)}` : '');
|
|
354
|
+
console.log(`\n${heading('Component:')} ${bold(resolved.name)}${versionInfo}`);
|
|
355
|
+
console.log(`${heading(resolved.sourceHeading)} ${dim(resolved.sourceLabel)}`);
|
|
379
356
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
reply: `${errMsg} Use "add ${resolved.name} --branch main" to install from the main branch.`,
|
|
392
|
-
}, null, 2));
|
|
393
|
-
} else {
|
|
394
|
-
console.error(error(errMsg));
|
|
395
|
-
console.log(dim(`Use "baize add ${resolved.name} --branch main" to install from the main branch.`));
|
|
357
|
+
if (resolved.deliveredVia) {
|
|
358
|
+
console.log(`${heading('File:')} ${dim(resolved.deliveredVia.path)}`);
|
|
359
|
+
console.log(`${heading('Checksum:')} ${resolved.deliveredVia.verified ? green('sha256 verified') : warn('NOT VERIFIED (--trust-file)')}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (resolved.isThirdParty) {
|
|
363
|
+
console.log(warn('Third-party component — not verified by Baize team.'));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (regInfo.description) console.log(`${heading('Description:')} ${regInfo.description}`);
|
|
367
|
+
if (regInfo.type) console.log(`${heading('Type:')} ${regInfo.type}`);
|
|
396
368
|
}
|
|
397
|
-
process.exit(1);
|
|
398
|
-
}
|
|
399
|
-
// Identity/transport split: acquisition (when present) says where the bytes
|
|
400
|
-
// come from this once; resolved.source stays the persisted identity.
|
|
401
|
-
downloadResult = acquireSource(resolved.acquisition || resolved.source, skillDir);
|
|
402
369
|
|
|
403
|
-
|
|
404
|
-
if (jsonOutput) {
|
|
405
|
-
console.log(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
console.error(error(`Download failed: ${downloadResult.error}`));
|
|
370
|
+
// 7. User confirmation (skip in JSON mode — confirmation handled at application layer)
|
|
371
|
+
if (!skipConfirm && !jsonOutput) {
|
|
372
|
+
console.log('');
|
|
373
|
+
const confirmed = await promptYesNo('Proceed with installation? [Y/n]: ', true);
|
|
374
|
+
if (!confirmed) {
|
|
375
|
+
console.log(dim('Installation cancelled.'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
412
378
|
}
|
|
413
|
-
cleanup(skillDir);
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
416
379
|
|
|
417
|
-
|
|
380
|
+
// 8. Download
|
|
381
|
+
const skillDir = path.join(SKILLS_DIR, resolved.name);
|
|
382
|
+
|
|
383
|
+
if (fs.existsSync(skillDir)) {
|
|
384
|
+
// Orphan residue: the skill dir exists but the component was never
|
|
385
|
+
// registered in components.json — a previous install was interrupted
|
|
386
|
+
// after extraction (slow download + timeout, crash, killed tab). Remove
|
|
387
|
+
// it and proceed, so a retry succeeds instead of deadlocking on
|
|
388
|
+
// "already exists" (web console had no in-UI recovery path).
|
|
389
|
+
const registered = loadComponents()[resolved.name];
|
|
390
|
+
if (!registered) {
|
|
391
|
+
if (!jsonOutput) {
|
|
392
|
+
console.error(dim(`检测到未完成安装的残留目录,自动清理后继续:${skillDir}`));
|
|
393
|
+
}
|
|
394
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
395
|
+
} else {
|
|
396
|
+
if (jsonOutput) {
|
|
397
|
+
console.log(JSON.stringify({
|
|
398
|
+
action: 'add', component: resolved.name, success: false,
|
|
399
|
+
error: 'skill_dir_exists', message: `Skill directory already exists: ${skillDir}. Remove it first or use "baize upgrade".`,
|
|
400
|
+
reply: `Cannot install ${resolved.name}: skill directory already exists. Use "upgrade ${resolved.name}" instead.`,
|
|
401
|
+
}, null, 2));
|
|
402
|
+
} else {
|
|
403
|
+
console.error(`\n${error(`Skill directory already exists: ${dim(skillDir)}`)}`);
|
|
404
|
+
console.error(dim('Remove it first or use "baize upgrade".'));
|
|
405
|
+
}
|
|
406
|
+
releaseLock(resolved.name);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
418
410
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
411
|
+
const downloadLabel = branch ? `${resolved.name} (branch: ${branch})` : resolved.name;
|
|
412
|
+
if (!jsonOutput) console.log(`\n${cyan('Downloading')} ${bold(downloadLabel)}...`);
|
|
413
|
+
|
|
414
|
+
let downloadResult;
|
|
415
|
+
if (!resolved.source) {
|
|
416
|
+
// No release tag found and no --branch specified
|
|
417
|
+
const errType = resolved.fetchError ? 'fetch_failed' : 'no_release';
|
|
418
|
+
const errMsg = resolved.fetchError
|
|
419
|
+
? `Could not check for releases: ${resolved.fetchError}`
|
|
420
|
+
: `No release found for ${resolved.name}.`;
|
|
421
|
+
if (jsonOutput) {
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
action: 'add', component: resolved.name, success: false,
|
|
424
|
+
error: errType, message: `${errMsg} Use --branch to install from a branch.`,
|
|
425
|
+
reply: `${errMsg} Use "add ${resolved.name} --branch main" to install from the main branch.`,
|
|
426
|
+
}, null, 2));
|
|
427
|
+
} else {
|
|
428
|
+
console.error(error(errMsg));
|
|
429
|
+
console.log(dim(`Use "baize add ${resolved.name} --branch main" to install from the main branch.`));
|
|
430
|
+
}
|
|
431
|
+
releaseLock(resolved.name);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
// Identity/transport split: acquisition (when present) says where the bytes
|
|
435
|
+
// come from this once; resolved.source stays the persisted identity.
|
|
436
|
+
downloadResult = acquireSource(resolved.acquisition || resolved.source, skillDir);
|
|
437
|
+
|
|
438
|
+
if (!downloadResult.success) {
|
|
439
|
+
if (jsonOutput) {
|
|
440
|
+
console.log(JSON.stringify({
|
|
441
|
+
action: 'add', component: resolved.name, success: false,
|
|
442
|
+
error: 'download_failed', message: `Download failed: ${downloadResult.error}`,
|
|
443
|
+
reply: `Failed to download ${resolved.name}: ${downloadResult.error}`,
|
|
444
|
+
}, null, 2));
|
|
445
|
+
} else {
|
|
446
|
+
console.error(error(`Download failed: ${downloadResult.error}`));
|
|
447
|
+
}
|
|
448
|
+
cleanup(skillDir);
|
|
449
|
+
releaseLock(resolved.name);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!jsonOutput) console.log(` ${success('Download complete.')}`);
|
|
454
|
+
|
|
455
|
+
// 9. Commit the authoritative install baseline (manifest + originals)
|
|
456
|
+
try {
|
|
457
|
+
const manifest = generateManifest(skillDir);
|
|
458
|
+
saveMergeBaseline(skillDir, skillDir, manifest);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
if (!jsonOutput) console.log(` ${warn(`Could not save install baseline: ${err.message}`)}`);
|
|
461
|
+
}
|
|
426
462
|
|
|
427
|
-
|
|
428
|
-
|
|
463
|
+
// 10. Detect component type and install accordingly
|
|
464
|
+
const componentType = detectComponentType(skillDir);
|
|
429
465
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
466
|
+
if (componentType === 'declarative') {
|
|
467
|
+
await installDeclarative(resolved, skillDir, skipConfirm, jsonOutput, branch);
|
|
468
|
+
} else {
|
|
469
|
+
installAI(resolved, skillDir, branch);
|
|
470
|
+
}
|
|
471
|
+
} finally {
|
|
472
|
+
releaseLock(resolved.name);
|
|
434
473
|
}
|
|
435
474
|
}
|
|
436
475
|
|
|
@@ -487,6 +526,7 @@ async function installDeclarative(resolved, skillDir, skipConfirm, jsonOutput, b
|
|
|
487
526
|
console.error(` ${error(`npm install failed: ${err.message}`)}`);
|
|
488
527
|
}
|
|
489
528
|
cleanup(skillDir);
|
|
529
|
+
releaseLock(resolved.name);
|
|
490
530
|
process.exit(1);
|
|
491
531
|
}
|
|
492
532
|
}
|
|
@@ -955,11 +955,11 @@ function handleSelfCheckOnly({ jsonOutput, branch, beta = false }) {
|
|
|
955
955
|
} else {
|
|
956
956
|
// Fallback: fetch changelog from remote
|
|
957
957
|
try {
|
|
958
|
-
const rawChangelog = fetchRawFile('
|
|
958
|
+
const rawChangelog = fetchRawFile('baize01-ai/baize-core', 'CHANGELOG.md', `v${check.latest}`);
|
|
959
959
|
changelog = filterChangelog(rawChangelog, check.current);
|
|
960
960
|
} catch {
|
|
961
961
|
try {
|
|
962
|
-
const rawChangelog = fetchRawFile('
|
|
962
|
+
const rawChangelog = fetchRawFile('baize01-ai/baize-core', 'CHANGELOG.md');
|
|
963
963
|
changelog = filterChangelog(rawChangelog, check.current);
|
|
964
964
|
} catch {
|
|
965
965
|
// CHANGELOG.md may not exist
|
|
@@ -1250,8 +1250,31 @@ export async function uninstallComponent(args) {
|
|
|
1250
1250
|
return handleUninstallCheck(target, { jsonOutput });
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1253
|
+
// Acquire the component lock — protects every removal write below (service,
|
|
1254
|
+
// bins, Caddy routes, skill/data dirs, components.json). --check preview
|
|
1255
|
+
// mode returns above and never holds the lock.
|
|
1256
|
+
const lockResult = acquireLock(target);
|
|
1257
|
+
if (!lockResult.success) {
|
|
1258
|
+
if (jsonOutput) {
|
|
1259
|
+
const errOutput = { action: 'uninstall', component: target, success: false, error: lockResult.error };
|
|
1260
|
+
errOutput.reply = formatC4Reply('error', { message: lockResult.error });
|
|
1261
|
+
console.log(JSON.stringify(errOutput, null, 2));
|
|
1262
|
+
} else {
|
|
1263
|
+
console.error(`Error: ${lockResult.error}`);
|
|
1264
|
+
}
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
const ok = await handleRemoveFlow(target, { purge: explicitPurge, skipConfirm: skipConfirm || explicitConfirm, force, jsonOutput });
|
|
1270
|
+
if (!ok) {
|
|
1271
|
+
// Release before exit — process.exit() bypasses the finally block
|
|
1272
|
+
releaseLock(target);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
} finally {
|
|
1276
|
+
releaseLock(target);
|
|
1277
|
+
}
|
|
1255
1278
|
}
|
|
1256
1279
|
|
|
1257
1280
|
/**
|
package/cli/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* baize doctor — diagnose installation health and auto-fix via Claude.
|
|
3
3
|
*
|
|
4
|
-
* Design: https://github.com/
|
|
4
|
+
* Design: https://github.com/baize01-ai/baize-core/issues/202
|
|
5
5
|
*
|
|
6
6
|
* Layer 1: Diagnose all checks. Ensure Claude CLI is reachable.
|
|
7
7
|
* Layer 2: If Claude is available, delegate all fixes to `claude -p`.
|
|
@@ -811,7 +811,7 @@ export async function doctorCommand(args) {
|
|
|
811
811
|
const targets = [];
|
|
812
812
|
|
|
813
813
|
if (coreVersion.success) {
|
|
814
|
-
targets.push({ name: 'baize-core', repo: '
|
|
814
|
+
targets.push({ name: 'baize-core', repo: 'baize01-ai/baize-core', current: coreVersion.version });
|
|
815
815
|
}
|
|
816
816
|
|
|
817
817
|
for (const [name, info] of Object.entries(components)) {
|
package/cli/commands/init.js
CHANGED
|
@@ -867,6 +867,11 @@ function syncCoreSkills() {
|
|
|
867
867
|
|
|
868
868
|
/**
|
|
869
869
|
* Install npm dependencies for all skills that need them.
|
|
870
|
+
* Idempotent: if a skill's dependencies are already installed AND loadable
|
|
871
|
+
* (native modules included), the install is skipped. This is what makes the
|
|
872
|
+
* Docker image reproducible — the image bakes deps at build time (with a
|
|
873
|
+
* local-compile fallback), so runtime init must NOT reinstall them (a reinstall
|
|
874
|
+
* can pull a broken platform prebuild, e.g. better-sqlite3 Mach-O on linux).
|
|
870
875
|
*/
|
|
871
876
|
function installSkillDependencies() {
|
|
872
877
|
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
@@ -880,6 +885,10 @@ function installSkillDependencies() {
|
|
|
880
885
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
881
886
|
if (!pkg.dependencies || Object.keys(pkg.dependencies).length === 0) continue;
|
|
882
887
|
|
|
888
|
+
if (skillDepsUsable(skillDir, pkg.dependencies)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
|
|
883
892
|
console.log(` ${cyan(`Installing ${bold(entry.name)} dependencies...`)}`);
|
|
884
893
|
execSync('npm install --production', {
|
|
885
894
|
cwd: skillDir,
|
|
@@ -892,6 +901,25 @@ function installSkillDependencies() {
|
|
|
892
901
|
}
|
|
893
902
|
}
|
|
894
903
|
|
|
904
|
+
/**
|
|
905
|
+
* True when every dependency of the skill is already installed AND require()-able
|
|
906
|
+
* from the skill directory (catches broken native modules: prebuild downloads of
|
|
907
|
+
* the wrong platform still leave a node_modules entry, but require() fails).
|
|
908
|
+
*/
|
|
909
|
+
function skillDepsUsable(skillDir, dependencies) {
|
|
910
|
+
const names = Object.keys(dependencies);
|
|
911
|
+
if (names.length === 0) return true;
|
|
912
|
+
for (const name of names) {
|
|
913
|
+
const probe = `require(${JSON.stringify(name)});`;
|
|
914
|
+
try {
|
|
915
|
+
execFileSync(process.execPath, ['-e', probe], { cwd: skillDir, stdio: 'ignore' });
|
|
916
|
+
} catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
895
923
|
/**
|
|
896
924
|
* Ensure a web-console password exists in .env.
|
|
897
925
|
* Reads from BAIZE_WEB_PASSWORD (new name), falls back to WEB_CONSOLE_PASSWORD (legacy).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* baize uninstall --self — Remove baize entirely from the system.
|
|
3
3
|
*
|
|
4
|
-
* Design: https://github.com/
|
|
4
|
+
* Design: https://github.com/baize01-ai/baize-core/issues/212
|
|
5
5
|
*
|
|
6
6
|
* Phase 1: Stop services (tmux sessions + PM2 baize services)
|
|
7
7
|
* Phase 2: Uninstall the baize npm package
|
|
@@ -85,7 +85,7 @@ describe('split instruction assembler', () => {
|
|
|
85
85
|
// pins alongside the reviewed content change (issue #722 content redraft).
|
|
86
86
|
const managedHeader = '> **Baize-managed system instructions.** This file is replaced during upgrades. Put all custom instructions in `~/baize/BAIZE.md`.\n\n';
|
|
87
87
|
const expected = {
|
|
88
|
-
claude: '
|
|
88
|
+
claude: '38b978aef11ae181a7fe5bc94e13f25275de6fe85313b836a00b70095c294053',
|
|
89
89
|
codex: '1caab5d34fbe6ec9c858f94953fdc5d1abce27167f91597875ba6b091600c4e1',
|
|
90
90
|
};
|
|
91
91
|
for (const runtime of ['claude', 'codex']) {
|
package/cli/lib/a2a.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A CLI delegation helpers (D19 contract §6).
|
|
3
|
+
*
|
|
4
|
+
* All `baize a2a` subcommands delegate to the @baize-ai/baize-a2a channel's
|
|
5
|
+
* scripts/cli.js, which emits a single JSON object on stdout with --json
|
|
6
|
+
* (success exit 0; failure = error detail on stderr + exit 1).
|
|
7
|
+
*
|
|
8
|
+
* Resolution precedence:
|
|
9
|
+
* BAIZE_A2A_PATH env → project node_modules (@baize-ai/baize-a2a)
|
|
10
|
+
* → global npm package (npm root -g + @baize-ai/baize-a2a)
|
|
11
|
+
* → dev sibling repo (../baize-a2a relative to the core checkout).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ordered candidate paths for the baize-a2a scripts/cli.js entry (excluding
|
|
25
|
+
* the BAIZE_A2A_PATH env override, which takes precedence in a2aCliPath()).
|
|
26
|
+
*/
|
|
27
|
+
export function a2aCliCandidates() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
// a2a installed as a channel (`baize add` → ~/baize/.claude/skills/a2a/)
|
|
30
|
+
path.join(process.env.BAIZE_DIR || path.join(os.homedir(), 'baize'), '.claude', 'skills', 'a2a', 'scripts', 'cli.js'),
|
|
31
|
+
// a2a installed into the same node_modules as this checkout
|
|
32
|
+
path.join(__dirname, '..', '..', 'node_modules', '@baize-ai', 'baize-a2a', 'scripts', 'cli.js'),
|
|
33
|
+
// core installed as a dependency with a2a as a sibling package
|
|
34
|
+
path.join(__dirname, '..', '..', '..', '@baize-ai', 'baize-a2a', 'scripts', 'cli.js'),
|
|
35
|
+
];
|
|
36
|
+
try {
|
|
37
|
+
const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 10000 }).trim();
|
|
38
|
+
candidates.push(path.join(npmRoot, '@baize-ai', 'baize-a2a', 'scripts', 'cli.js'));
|
|
39
|
+
} catch {
|
|
40
|
+
// npm unavailable — skip the global layout
|
|
41
|
+
}
|
|
42
|
+
// Dev-time sibling repo (../baize-a2a next to baize-core)
|
|
43
|
+
candidates.push(path.join(__dirname, '..', '..', '..', 'baize-a2a', 'scripts', 'cli.js'));
|
|
44
|
+
return candidates;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Locate the baize-a2a scripts/cli.js entry.
|
|
49
|
+
* @returns {string|null} absolute path, or null when not found.
|
|
50
|
+
*/
|
|
51
|
+
export function a2aCliPath() {
|
|
52
|
+
const envPath = process.env.BAIZE_A2A_PATH;
|
|
53
|
+
if (envPath) {
|
|
54
|
+
if (fs.existsSync(envPath)) {
|
|
55
|
+
// Accept either the cli.js file itself or the package directory.
|
|
56
|
+
if (fs.statSync(envPath).isDirectory()) {
|
|
57
|
+
const inner = path.join(envPath, 'scripts', 'cli.js');
|
|
58
|
+
if (fs.existsSync(inner)) return inner;
|
|
59
|
+
} else {
|
|
60
|
+
return envPath;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return a2aCliCandidates().find((c) => fs.existsSync(c)) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run a baize-a2a CLI command, always with --json appended.
|
|
69
|
+
*
|
|
70
|
+
* The child gets a piped stdin that is ended immediately: the baize-a2a CLI
|
|
71
|
+
* merges a JSON object from stdin (contract §6) and blocks until stdin EOF,
|
|
72
|
+
* so an 'ignore' stream would hang it forever.
|
|
73
|
+
*
|
|
74
|
+
* @param {string[]} args argv after the subcommand (e.g. ['enable', '--admin', url])
|
|
75
|
+
* @param {object} [opts]
|
|
76
|
+
* @param {string} [opts.cliPath] resolved cli.js path (defaults to a2aCliPath())
|
|
77
|
+
* @param {number} [opts.timeout] exec timeout ms (default 120s; task sync uses 10min+)
|
|
78
|
+
* @param {Function} [opts.spawnFn] injectable spawn for tests
|
|
79
|
+
* @returns {Promise<{success:boolean, json?:object, output?:string, error?:string}>}
|
|
80
|
+
*/
|
|
81
|
+
export function runA2aCli(args, { cliPath = a2aCliPath(), timeout = 120000, spawnFn = spawn } = {}) {
|
|
82
|
+
const cli = typeof cliPath === 'function' ? cliPath() : cliPath;
|
|
83
|
+
if (!cli) {
|
|
84
|
+
return Promise.resolve({
|
|
85
|
+
success: false,
|
|
86
|
+
error: '未找到 baize-a2a CLI(设置 BAIZE_A2A_PATH 或安装 @baize-ai/baize-a2a)',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const argv = args.includes('--json') ? [...args] : [...args, '--json'];
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const child = spawnFn(process.execPath, [cli, ...argv], {
|
|
92
|
+
env: { ...process.env },
|
|
93
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
let stdout = '';
|
|
96
|
+
let stderr = '';
|
|
97
|
+
let settled = false;
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
child.kill('SIGTERM');
|
|
102
|
+
resolve({ success: false, error: `a2a 命令超时(${Math.round(timeout / 1000)}s)`, output: (stdout || stderr).trim() });
|
|
103
|
+
}, timeout);
|
|
104
|
+
child.stdout.on('data', (d) => { stdout += String(d); });
|
|
105
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
106
|
+
child.on('error', (err) => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
resolve({ success: false, error: err.message });
|
|
111
|
+
});
|
|
112
|
+
child.on('close', (code) => {
|
|
113
|
+
if (settled) return;
|
|
114
|
+
settled = true;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
const raw = stdout.trim();
|
|
117
|
+
let json = null;
|
|
118
|
+
try {
|
|
119
|
+
json = raw ? JSON.parse(raw) : null;
|
|
120
|
+
} catch {
|
|
121
|
+
// non-JSON stdout
|
|
122
|
+
}
|
|
123
|
+
if (code === 0) {
|
|
124
|
+
resolve({ success: true, json: json || {}, output: raw });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const detail = (json?.error || stderr.trim() || raw || `exit ${code}`).slice(-1500);
|
|
128
|
+
resolve({ success: false, error: detail, json, output: raw || stderr.trim() });
|
|
129
|
+
});
|
|
130
|
+
child.stdin.end();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Human-readable output ───────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const KEY_LABELS = {
|
|
137
|
+
status: [
|
|
138
|
+
['enabled', '启用'],
|
|
139
|
+
['agentId', 'Agent ID'],
|
|
140
|
+
['registered', '已注册'],
|
|
141
|
+
['registrationStatus', '注册状态'],
|
|
142
|
+
['advertiseUrl', '宣告地址'],
|
|
143
|
+
['endpoint', 'Endpoint'],
|
|
144
|
+
['version', '版本'],
|
|
145
|
+
],
|
|
146
|
+
enable: [
|
|
147
|
+
['agentId', 'Agent ID'],
|
|
148
|
+
['registrationId', '注册 ID'],
|
|
149
|
+
['status', '状态'],
|
|
150
|
+
],
|
|
151
|
+
send: [
|
|
152
|
+
['status', '状态'],
|
|
153
|
+
['requestId', 'Request ID'],
|
|
154
|
+
],
|
|
155
|
+
task: [
|
|
156
|
+
['taskId', '任务 ID'],
|
|
157
|
+
['status', '状态'],
|
|
158
|
+
['result', '结果'],
|
|
159
|
+
['error', '错误'],
|
|
160
|
+
],
|
|
161
|
+
'task-status': [
|
|
162
|
+
['taskId', '任务 ID'],
|
|
163
|
+
['status', '状态'],
|
|
164
|
+
['message', '消息'],
|
|
165
|
+
['result', '结果'],
|
|
166
|
+
['createdAt', '创建时间'],
|
|
167
|
+
['updatedAt', '更新时间'],
|
|
168
|
+
['completedAt', '完成时间'],
|
|
169
|
+
],
|
|
170
|
+
'task-cancel': [
|
|
171
|
+
['taskId', '任务 ID'],
|
|
172
|
+
['status', '状态'],
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function boolText(value) {
|
|
177
|
+
if (value === true || value === 'true') return '是';
|
|
178
|
+
if (value === false || value === 'false') return '否';
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function listBlock(title, rows) {
|
|
183
|
+
if (!rows.length) return [`${title}:(无)`];
|
|
184
|
+
return [`${title}:`, ...rows.map((r) => ` • ${r}`)];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format a delegated JSON result for terminal display.
|
|
189
|
+
* Falls back to pretty-printed JSON for unknown shapes.
|
|
190
|
+
*/
|
|
191
|
+
export function formatA2aOutput(kind, json) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
const pushKV = (k, v) => {
|
|
194
|
+
if (v !== undefined && v !== null && v !== '') lines.push(`${k}: ${v}`);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (kind === 'peer-list') {
|
|
198
|
+
const peers = json.peers || [];
|
|
199
|
+
lines.push(...listBlock('Peer 目录', peers.map((p) => {
|
|
200
|
+
const skills = Array.isArray(p.skills) && p.skills.length
|
|
201
|
+
? `(skills: ${p.skills.map((s) => (typeof s === 'string' ? s : s.id || s.name)).join(', ')})` : '';
|
|
202
|
+
const expiry = p.expiresAt ? `,缓存至 ${p.expiresAt}` : '';
|
|
203
|
+
return `${p.agentId}${skills}${expiry}\n endpoint: ${p.endpoint || '-'}`;
|
|
204
|
+
})));
|
|
205
|
+
} else if (kind === 'search') {
|
|
206
|
+
const agents = json.agents || [];
|
|
207
|
+
lines.push(...listBlock('搜索结果', agents.map((a) => {
|
|
208
|
+
const meta = [a.status, a.team, a.role].filter(Boolean).join(' · ');
|
|
209
|
+
const labels = Array.isArray(a.labels) && a.labels.length ? `,labels: ${a.labels.join(', ')}` : '';
|
|
210
|
+
const reach = a.reachability != null ? `,reachability: ${a.reachability}` : '';
|
|
211
|
+
return `${a.agentId}${a.name ? `(${a.name})` : ''}${meta ? ` — ${meta}` : ''}${labels}${reach}`;
|
|
212
|
+
})));
|
|
213
|
+
} else if (kind === 'disable') {
|
|
214
|
+
return json.ok === true ? 'A2A 已停用' : JSON.stringify(json, null, 2);
|
|
215
|
+
} else if (kind === 'status') {
|
|
216
|
+
const pairs = KEY_LABELS.status;
|
|
217
|
+
for (const [k, label] of pairs) {
|
|
218
|
+
if (k === 'enabled' || k === 'registered') {
|
|
219
|
+
if (json[k] !== undefined) lines.push(`${label}: ${boolText(json[k])}`);
|
|
220
|
+
} else {
|
|
221
|
+
pushKV(label, json[k]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else if (kind === 'task') {
|
|
225
|
+
// async task returns early; sync task may carry result/error
|
|
226
|
+
for (const [k, label] of KEY_LABELS.task) pushKV(label, json[k]);
|
|
227
|
+
if (json.status === 'failed' && json.error) lines.push(`错误: ${json.error}`);
|
|
228
|
+
} else if (KEY_LABELS[kind]) {
|
|
229
|
+
for (const [k, label] of KEY_LABELS[kind]) pushKV(label, json[k]);
|
|
230
|
+
} else {
|
|
231
|
+
return JSON.stringify(json, null, 2);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines.length ? lines.join('\n') : JSON.stringify(json, null, 2);
|
|
235
|
+
}
|
package/cli/lib/lock.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File-based lock utilities for component
|
|
3
|
-
* Prevents concurrent
|
|
2
|
+
* File-based lock utilities for component operations
|
|
3
|
+
* Prevents concurrent operations (add / upgrade / uninstall) on the same component
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from 'node:fs';
|
|
@@ -60,7 +60,7 @@ export function acquireLock(component) {
|
|
|
60
60
|
// Valid lock exists
|
|
61
61
|
return {
|
|
62
62
|
success: false,
|
|
63
|
-
error: `Component "${component}" is being
|
|
63
|
+
error: `Component "${component}" is being operated on by PID ${lockData.pid}`,
|
|
64
64
|
existingPid: lockData.pid,
|
|
65
65
|
};
|
|
66
66
|
}
|
package/cli/lib/self-upgrade.js
CHANGED
|
@@ -27,7 +27,7 @@ import { deployManifestTemplate } from './runtime/tmux-env.js';
|
|
|
27
27
|
import { writeCodexConfig } from './runtime-setup.js';
|
|
28
28
|
import { getCoreEcosystemPath, restartManagedProcess } from './pm2.js';
|
|
29
29
|
|
|
30
|
-
const REPO = '
|
|
30
|
+
const REPO = 'baize01-ai/baize-core';
|
|
31
31
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Version helpers
|