@baize-ai/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.dockerignore +8 -18
  2. package/CHANGELOG.md +19 -1
  3. package/Dockerfile +66 -12
  4. package/README.md +85 -6
  5. package/README.zh-CN.md +63 -3
  6. package/assets/logo.png +0 -0
  7. package/cli/baize.js +6 -0
  8. package/cli/commands/a2a.js +124 -0
  9. package/cli/commands/add.js +126 -86
  10. package/cli/commands/component.js +27 -4
  11. package/cli/commands/doctor.js +2 -2
  12. package/cli/commands/init.js +28 -0
  13. package/cli/commands/self-uninstall.js +1 -1
  14. package/cli/lib/__tests__/instruction-split.test.js +1 -1
  15. package/cli/lib/a2a.js +235 -0
  16. package/cli/lib/lock.js +3 -3
  17. package/cli/lib/self-upgrade.js +1 -1
  18. package/docker-compose.yml +1 -1
  19. package/docs/docker.md +18 -4
  20. package/docs/release.md +150 -0
  21. package/package.json +1 -1
  22. package/scripts/docker-publish.sh +70 -0
  23. package/scripts/install.sh +4 -4
  24. package/scripts/pack-release.sh +361 -0
  25. package/skills/comm-bridge/package.json +7 -3
  26. package/skills/comm-bridge/scripts/c4-receive.js +23 -2
  27. package/skills/scheduler/package.json +2 -2
  28. package/skills/web-console/SKILL.md +34 -0
  29. package/skills/web-console/package.json +2 -2
  30. package/skills/web-console/public/app.js +900 -4
  31. package/skills/web-console/public/index.html +126 -0
  32. package/skills/web-console/public/styles.css +130 -0
  33. package/skills/web-console/scripts/a2a-admin.js +506 -0
  34. package/skills/web-console/scripts/channel-admin.js +99 -16
  35. package/skills/web-console/scripts/server.js +386 -1
  36. package/skills/web-console/scripts/skill-catalog.js +179 -0
  37. package/templates/claude-system.md +22 -0
  38. package/templates/pm2/ecosystem.config.cjs +4 -1
  39. package/test/a2a-cli.test.js +180 -0
  40. package/test/agent-card-api.test.js +261 -0
  41. package/test/channel-admin.test.js +87 -0
  42. package/test/component-lock.test.js +216 -0
  43. package/test/skill-catalog.test.js +118 -0
  44. package/test/web-console-routes.test.js +488 -1
@@ -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. Display component info (terminal only)
332
- if (!jsonOutput) {
333
- const versionInfo = branch ? ` (branch: ${bold(branch)})` : (resolved.version ? `@${bold(resolved.version)}` : '');
334
- console.log(`\n${heading('Component:')} ${bold(resolved.name)}${versionInfo}`);
335
- console.log(`${heading(resolved.sourceHeading)} ${dim(resolved.sourceLabel)}`);
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: 'skill_dir_exists', message: `Skill directory already exists: ${skillDir}. Remove it first or use "baize upgrade".`,
368
- reply: `Cannot install ${resolved.name}: skill directory already exists. Use "upgrade ${resolved.name}" instead.`,
341
+ error: 'component_locked', message: lockResult.error,
342
+ reply: `Error: ${lockResult.error}`,
369
343
  }, null, 2));
370
344
  } else {
371
- console.error(`\n${error(`Skill directory already exists: ${dim(skillDir)}`)}`);
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
- const downloadLabel = branch ? `${resolved.name} (branch: ${branch})` : resolved.name;
378
- if (!jsonOutput) console.log(`\n${cyan('Downloading')} ${bold(downloadLabel)}...`);
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
- let downloadResult;
381
- if (!resolved.source) {
382
- // No release tag found and no --branch specified
383
- const errType = resolved.fetchError ? 'fetch_failed' : 'no_release';
384
- const errMsg = resolved.fetchError
385
- ? `Could not check for releases: ${resolved.fetchError}`
386
- : `No release found for ${resolved.name}.`;
387
- if (jsonOutput) {
388
- console.log(JSON.stringify({
389
- action: 'add', component: resolved.name, success: false,
390
- error: errType, message: `${errMsg} Use --branch to install from a branch.`,
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
- if (!downloadResult.success) {
404
- if (jsonOutput) {
405
- console.log(JSON.stringify({
406
- action: 'add', component: resolved.name, success: false,
407
- error: 'download_failed', message: `Download failed: ${downloadResult.error}`,
408
- reply: `Failed to download ${resolved.name}: ${downloadResult.error}`,
409
- }, null, 2));
410
- } else {
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
- if (!jsonOutput) console.log(` ${success('Download complete.')}`);
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
- // 8. Commit the authoritative install baseline (manifest + originals)
420
- try {
421
- const manifest = generateManifest(skillDir);
422
- saveMergeBaseline(skillDir, skillDir, manifest);
423
- } catch (err) {
424
- if (!jsonOutput) console.log(` ${warn(`Could not save install baseline: ${err.message}`)}`);
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
- // 9. Detect component type and install accordingly
428
- const componentType = detectComponentType(skillDir);
463
+ // 10. Detect component type and install accordingly
464
+ const componentType = detectComponentType(skillDir);
429
465
 
430
- if (componentType === 'declarative') {
431
- await installDeclarative(resolved, skillDir, skipConfirm, jsonOutput, branch);
432
- } else {
433
- installAI(resolved, skillDir, branch);
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('baize-ai/baize-core', 'CHANGELOG.md', `v${check.latest}`);
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('baize-ai/baize-core', 'CHANGELOG.md');
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
- const ok = await handleRemoveFlow(target, { purge: explicitPurge, skipConfirm: skipConfirm || explicitConfirm, force, jsonOutput });
1254
- if (!ok) process.exit(1);
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
  /**
@@ -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/baize-ai/baize-core/issues/202
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: 'baize-ai/baize-core', current: coreVersion.version });
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)) {
@@ -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/baize-ai/baize-core/issues/212
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: '53b8adaa0898c8e460c5a56009b5921060c0d38aa189e7f50dca199070d1098b',
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 upgrades
3
- * Prevents concurrent upgrades of the same component
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 upgraded by PID ${lockData.pid}`,
63
+ error: `Component "${component}" is being operated on by PID ${lockData.pid}`,
64
64
  existingPid: lockData.pid,
65
65
  };
66
66
  }
@@ -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 = 'baize-ai/baize-core';
30
+ const REPO = 'baize01-ai/baize-core';
31
31
 
32
32
  // ---------------------------------------------------------------------------
33
33
  // Version helpers
@@ -12,7 +12,7 @@
12
12
 
13
13
  services:
14
14
  baize:
15
- image: ghcr.io/baize-ai/baize-core:latest
15
+ image: ghcr.io/baize01-ai/baize-core:latest
16
16
  container_name: baize
17
17
  restart: unless-stopped
18
18