@hybridlabor-api/aos 4.15.0 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude/hooks/env-file-protection.mjs +19 -1
- package/.claude/hooks/go-gate.mjs +787 -68
- package/.claude/hooks/go-grant.mjs +88 -0
- package/.claude/settings.json +8 -0
- package/.codex-plugin/plugin.json +36 -6
- package/.opencode/commands/bdb-aos-brainstorm.md +5 -0
- package/.opencode/commands/bdb-aos-doctor.md +5 -0
- package/.opencode/commands/bdb-aos-graph.md +5 -0
- package/.opencode/commands/bdb-aos-init.md +5 -0
- package/.opencode/commands/bdb-aos-loop.md +5 -0
- package/.opencode/commands/bdb-aos-mastersession.md +5 -0
- package/.opencode/commands/bdb-aos-memb.md +5 -0
- package/.opencode/commands/bdb-aos-orchestrator.md +5 -0
- package/.opencode/commands/bdb-aos-plan.md +5 -0
- package/.opencode/commands/bdb-aos-playbooks.md +5 -0
- package/.opencode/commands/bdb-aos-setup.md +5 -0
- package/.opencode/commands/bdb-aos-shipping.md +5 -0
- package/.opencode/commands/bdb-aos-startproject.md +5 -0
- package/.opencode/commands/bdb-aos-store.md +5 -0
- package/.opencode/plugins/bdb-aos.js +54 -10
- package/README.md +4 -4
- package/agy-commands/loop.md +5 -0
- package/bin/aos-acp.mjs +7 -3
- package/bin/aos-doctor.mjs +26 -9
- package/bin/aos-uninstall.mjs +25 -1
- package/commands/brainstorm.md +5 -0
- package/commands/doctor.md +5 -0
- package/commands/graph.md +5 -0
- package/commands/init.md +5 -0
- package/commands/loop.md +5 -0
- package/commands/mastersession.md +5 -0
- package/commands/memb.md +5 -0
- package/commands/orchestrator.md +5 -0
- package/commands/plan.md +5 -0
- package/commands/playbooks.md +5 -0
- package/commands/setup.md +5 -0
- package/commands/shipping.md +5 -0
- package/commands/startproject.md +5 -0
- package/commands/store.md +5 -0
- package/docs/codex-agy-setup.md +16 -0
- package/docs/opencode-setup.md +36 -0
- package/docs/plugin-migration.md +61 -0
- package/installer.js +564 -215
- package/lib/plugin-migration.js +462 -0
- package/lib/store-ui/index.html +9 -1
- package/lib/store-ui/server.mjs +2 -0
- package/package.json +7 -2
- package/plugin-commands.json +144 -0
- package/plugin.json +302 -0
- package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/bdb-aos-codex/skills/brainstorm/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/doctor/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/graph/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/init/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/loop/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/mastersession/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/memb/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/orchestrator/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/plan/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/playbooks/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/setup/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/shipping/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/startproject/SKILL.md +6 -0
- package/plugins/bdb-aos-codex/skills/store/SKILL.md +6 -0
- package/scripts/build-plugin-manifest.mjs +202 -3
- package/skills/basic/master-session/SKILL.md +1 -1
- package/skills/bdb-aos/scripts/list-playbooks.mjs +72 -0
- package/skills/global_config/gogate/SKILL.md +123 -0
- package/skills/global_config/loop-templates/SKILL.md +27 -0
- package/skills/global_config/loop-templates/references/ci-until-green.md +27 -0
- package/skills/global_config/loop-templates/references/daily-summary.md +27 -0
- package/skills/global_config/loop-templates/references/pr-to-merge.md +27 -0
- package/skills/global_config/loop-templates/references/review-rounds.md +27 -0
- package/skills/playbooks/pb-bug-fix/SKILL.md +48 -0
- package/skills/playbooks/pb-clip-from-moodboard/SKILL.md +55 -0
- package/skills/playbooks/pb-crew-call-sheet/SKILL.md +40 -0
- package/skills/playbooks/pb-deploy-saas/SKILL.md +44 -0
- package/skills/playbooks/pb-docs-site/SKILL.md +44 -0
- package/skills/playbooks/pb-focus-chunks/SKILL.md +40 -0
- package/skills/playbooks/pb-handover/SKILL.md +41 -0
- package/skills/playbooks/pb-harness-work/SKILL.md +64 -0
- package/skills/playbooks/pb-health-weekly/SKILL.md +43 -0
- package/skills/playbooks/pb-idea-to-launch/SKILL.md +51 -0
- package/skills/playbooks/pb-image-to-3d/SKILL.md +46 -0
- package/skills/playbooks/pb-inbox-zero/SKILL.md +39 -0
- package/skills/playbooks/pb-invoice-check/SKILL.md +41 -0
- package/skills/playbooks/pb-landing-page/SKILL.md +46 -0
- package/skills/playbooks/pb-launch-video/SKILL.md +49 -0
- package/skills/playbooks/pb-machine-setup/SKILL.md +44 -0
- package/skills/playbooks/pb-master/SKILL.md +53 -0
- package/skills/playbooks/pb-newsletter/SKILL.md +43 -0
- package/skills/playbooks/pb-offer/SKILL.md +39 -0
- package/skills/playbooks/pb-open-source/SKILL.md +50 -0
- package/skills/playbooks/pb-pcb-to-case/SKILL.md +46 -0
- package/skills/playbooks/pb-redesign-app/SKILL.md +47 -0
- package/skills/playbooks/pb-release-aos/SKILL.md +50 -0
- package/skills/playbooks/pb-security-sweep/SKILL.md +48 -0
- package/skills/playbooks/pb-ship/SKILL.md +49 -0
- package/skills/playbooks/pb-show-build/SKILL.md +48 -0
- package/skills/playbooks/pb-social-pack/SKILL.md +47 -0
- package/skills/playbooks/pb-todo/SKILL.md +41 -0
- package/skills/playbooks/pb-worktrees-land/SKILL.md +52 -0
- package/.codex-plugin/marketplace.json +0 -11
package/installer.js
CHANGED
|
@@ -23,6 +23,7 @@ const net = require('net');
|
|
|
23
23
|
const readline = require('readline');
|
|
24
24
|
const util = require('util');
|
|
25
25
|
const crypto = require('crypto');
|
|
26
|
+
const pluginMigration = require('./lib/plugin-migration');
|
|
26
27
|
|
|
27
28
|
function verifyDaemonListening(port, name, timeoutMs = 4000) {
|
|
28
29
|
return new Promise((resolve) => {
|
|
@@ -497,8 +498,8 @@ const CORE_MCP = 'memb-mcp';
|
|
|
497
498
|
// compared against: (a) the SOURCE hash (unmodified) and (b) the MANIFEST
|
|
498
499
|
// hash (last-installed value). This gives us three cases:
|
|
499
500
|
// ours + unmodified → source_hash == disk_hash → overwrite + update manifest
|
|
500
|
-
// ours + user-edited → manifest_hash
|
|
501
|
-
//
|
|
501
|
+
// ours + user-edited → disk_hash != manifest_hash → keep user file,
|
|
502
|
+
// write <file>.new
|
|
502
503
|
// foreign → no manifest record AND disk_hash
|
|
503
504
|
// not in any known source hash → skip + warn
|
|
504
505
|
|
|
@@ -560,8 +561,11 @@ function adoptFileIntoManifest(manifest, targetPath, sourceHash) {
|
|
|
560
561
|
};
|
|
561
562
|
}
|
|
562
563
|
|
|
564
|
+
// User-edited files this run left untouched (shipped copy written as <file>.new).
|
|
565
|
+
const keptUserEdits = [];
|
|
566
|
+
|
|
563
567
|
// Manifest-aware file writer implementing the three-way conflict policy.
|
|
564
|
-
// Returns 'wrote' | 'bak' | 'skipped'.
|
|
568
|
+
// Returns 'wrote' | 'bak' | 'kept' | 'skipped'.
|
|
565
569
|
// knownSourceHashes: Set of sha256 hashes of all source files in the current
|
|
566
570
|
// payload (used to identify files we recognise even before manifest entry).
|
|
567
571
|
function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes) {
|
|
@@ -627,15 +631,13 @@ function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes
|
|
|
627
631
|
}
|
|
628
632
|
|
|
629
633
|
// Case: ours + user-edited (manifest recorded our hash, but disk now differs).
|
|
634
|
+
// The user's file wins: the shipped version lands next to it as .new, and
|
|
635
|
+
// the manifest keeps the old hash so the next run still sees the edit.
|
|
630
636
|
if (manifestEntry && diskHash !== manifestEntry.sha256) {
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
636
|
-
manifest[targetPath] = { path: targetPath, sha256: sourceHash, version: pkg.version, installedAt: new Date().toISOString() };
|
|
637
|
-
log.warn(`[manifest] User-edited file backed up to ${bakPath}, new version written.`);
|
|
638
|
-
return 'bak';
|
|
637
|
+
const newPath = `${targetPath}.new`;
|
|
638
|
+
fs.copyFileSync(sourcePath, newPath);
|
|
639
|
+
keptUserEdits.push(targetPath);
|
|
640
|
+
return 'kept';
|
|
639
641
|
}
|
|
640
642
|
|
|
641
643
|
// Default: manifest entry present, disk matches manifest (no change needed) – overwrite.
|
|
@@ -765,6 +767,17 @@ function pruneRemovedSkills(manifest) {
|
|
|
765
767
|
|
|
766
768
|
function flushSessionManifest() {
|
|
767
769
|
if (_sessionManifest) saveInstallManifest(_sessionManifest);
|
|
770
|
+
reportKeptUserEdits();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function reportKeptUserEdits() {
|
|
774
|
+
if (keptUserEdits.length === 0) return;
|
|
775
|
+
log.warn([
|
|
776
|
+
`${keptUserEdits.length} file(s) you edited were KEPT; the shipped version is next to each as <file>.new:`,
|
|
777
|
+
...keptUserEdits.map((p) => ` ${p}`),
|
|
778
|
+
'Merge or delete the .new files when convenient.',
|
|
779
|
+
].join('\n'));
|
|
780
|
+
keptUserEdits.length = 0;
|
|
768
781
|
}
|
|
769
782
|
|
|
770
783
|
// skills/global_legacy/ has not existed in the shipped payload for a long
|
|
@@ -1265,9 +1278,94 @@ function reportFatal(stage, e) {
|
|
|
1265
1278
|
process.exitCode = 1;
|
|
1266
1279
|
}
|
|
1267
1280
|
|
|
1281
|
+
// AOS_PLUGIN_MIGRATION=off skips plugin registration and loose-copy removal; =check only reports.
|
|
1282
|
+
function pluginMigrationMode(argv = process.argv, env = process.env) {
|
|
1283
|
+
const flag = argv.find((a) => a.startsWith('--plugin-migration='));
|
|
1284
|
+
const raw = String(flag ? flag.slice('--plugin-migration='.length) : (env.AOS_PLUGIN_MIGRATION || '')).trim().toLowerCase();
|
|
1285
|
+
if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
|
|
1286
|
+
return raw === 'check' || DRY_RUN ? 'check' : 'on';
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// .agents/plugins holds the Codex marketplace file; it belongs to the repo checkout, not to ~/.agents or a project.
|
|
1290
|
+
const AGENTS_COPY_EXCLUDE = ['plugins'];
|
|
1291
|
+
|
|
1292
|
+
let _pluginMigration = null;
|
|
1293
|
+
function runPluginMigration({ targetHome = homeDir, detected = null, mode = pluginMigrationMode(), registrars } = {}) {
|
|
1294
|
+
if (_pluginMigration && targetHome === homeDir) return _pluginMigration;
|
|
1295
|
+
const keys = detected || detectPlatforms().map((d) => d.key);
|
|
1296
|
+
const ownsManifest = !_sessionManifest;
|
|
1297
|
+
const manifest = _sessionManifest || loadInstallManifest();
|
|
1298
|
+
let result;
|
|
1299
|
+
try {
|
|
1300
|
+
result = pluginMigration.migrate({ home: targetHome, manifest, detected: keys, mode, ...(registrars ? { registrars } : {}) });
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
log.warn(`Plugin migration skipped: ${e.message}`);
|
|
1303
|
+
result = { covered: new Set(), lines: [] };
|
|
1304
|
+
}
|
|
1305
|
+
for (const line of result.lines) log.step(line);
|
|
1306
|
+
if (ownsManifest && mode === 'on') saveInstallManifest(manifest);
|
|
1307
|
+
if (targetHome === homeDir) _pluginMigration = result;
|
|
1308
|
+
return result;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// The plugin migration runs first: when it retires Claude's loose copies, ~/.claude/skills is not
|
|
1312
|
+
// written again, so a rerun neither recreates nor re-backs-up what the plugin now delivers.
|
|
1313
|
+
function installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase }) {
|
|
1314
|
+
const detected = detectPlatforms().map((d) => d.key);
|
|
1315
|
+
if (targets.some((t) => t.value === '2') && !detected.includes('claudecode')) detected.push('claudecode');
|
|
1316
|
+
const pluginCovered = runPluginMigration({ detected }).covered;
|
|
1317
|
+
|
|
1318
|
+
for (const t of targets) {
|
|
1319
|
+
const viaPlugin = t.value === '2' && pluginCovered.has('claudecode');
|
|
1320
|
+
if (mode === 'replace') {
|
|
1321
|
+
if (!viaPlugin) {
|
|
1322
|
+
moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
|
|
1323
|
+
moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
|
|
1324
|
+
}
|
|
1325
|
+
moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
installStep(`create the skill target directories (${t.value})`, () => {
|
|
1329
|
+
if (!viaPlugin) {
|
|
1330
|
+
fs.mkdirSync(t.targetSkillDir, { recursive: true });
|
|
1331
|
+
retireObsoleteLegacyDir(t.targetLegacyDir);
|
|
1332
|
+
}
|
|
1333
|
+
fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
|
|
1334
|
+
}, 'The skill copies below will most likely be skipped as well.');
|
|
1335
|
+
|
|
1336
|
+
if (fs.existsSync(skillsBase)) {
|
|
1337
|
+
installStep(`install the skills (${t.value})`, () => {
|
|
1338
|
+
const rawDirs = fs.readdirSync(skillsBase);
|
|
1339
|
+
const dirs = rawDirs.sort((a, b) => {
|
|
1340
|
+
const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
|
|
1341
|
+
const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
|
|
1342
|
+
if (aIsLeaf && !bIsLeaf) return 1;
|
|
1343
|
+
if (!aIsLeaf && bIsLeaf) return -1;
|
|
1344
|
+
return 0;
|
|
1345
|
+
});
|
|
1346
|
+
for (const dir of dirs) {
|
|
1347
|
+
const fullPath = path.join(skillsBase, dir);
|
|
1348
|
+
if (!fs.statSync(fullPath).isDirectory()) continue;
|
|
1349
|
+
|
|
1350
|
+
if (viaPlugin && dir !== 'workspace_agents') continue;
|
|
1351
|
+
if (dir === 'global_legacy') {
|
|
1352
|
+
copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
|
|
1353
|
+
} else if (dir === 'workspace_agents') {
|
|
1354
|
+
copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
|
|
1355
|
+
} else {
|
|
1356
|
+
syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
|
|
1360
|
+
}, 'The skills are missing or incomplete; the rest of the installation continues.');
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1268
1365
|
function syncSkillsToGlobalHarnesses(excludeSkills = []) {
|
|
1269
1366
|
const skillsBase = path.join(srcDir, 'skills');
|
|
1270
1367
|
if (!fs.existsSync(skillsBase)) return;
|
|
1368
|
+
const pluginCovered = runPluginMigration().covered;
|
|
1271
1369
|
|
|
1272
1370
|
// Mirror only into harnesses that are actually present. This list used to
|
|
1273
1371
|
// be unconditional, which both wrote skills nobody would read and planted
|
|
@@ -1288,7 +1386,7 @@ function syncSkillsToGlobalHarnesses(excludeSkills = []) {
|
|
|
1288
1386
|
{ dir: path.join(homeDir, '.cursor', 'skills'), key: 'cursor' },
|
|
1289
1387
|
{ dir: path.join(homeDir, '.roo', 'skills'), key: 'vscode' },
|
|
1290
1388
|
{ dir: process.platform === 'win32' ? path.join(process.env.APPDATA || homeDir, 'opencode', 'skills') : path.join(homeDir, '.config', 'opencode', 'skills'), key: 'opencode' },
|
|
1291
|
-
].filter((d) => d.key === null || detectedKeys.has(d.key));
|
|
1389
|
+
].filter((d) => d.key === null || (detectedKeys.has(d.key) && !pluginCovered.has(d.key)));
|
|
1292
1390
|
|
|
1293
1391
|
for (const { dir: dest } of extraSkillDestinations) {
|
|
1294
1392
|
try {
|
|
@@ -1537,6 +1635,17 @@ async function promptCredentials(referenceMcpDir) {
|
|
|
1537
1635
|
|
|
1538
1636
|
const DAEMON_LOGON_FALLBACK_EXIT_CODE = 10;
|
|
1539
1637
|
|
|
1638
|
+
// The daemon job points at these scripts for good, so they live in the installer-owned shared store
|
|
1639
|
+
// (~/.agents/skills), which the plugin migration never retires, not in ~/.claude/skills.
|
|
1640
|
+
function stableOpenWikiScripts() {
|
|
1641
|
+
const dest = path.join(homeDir, '.agents', 'skills', 'openwiki-skill');
|
|
1642
|
+
if (!fs.existsSync(path.join(dest, 'scripts', 'install_daemon.sh'))) {
|
|
1643
|
+
const src = path.join(srcDir, 'skills', 'global_config', 'openwiki-skill');
|
|
1644
|
+
if (fs.existsSync(src)) copyDirRecursiveSync(src, dest);
|
|
1645
|
+
}
|
|
1646
|
+
return path.join(dest, 'scripts');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1540
1649
|
async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
|
|
1541
1650
|
const prov = openwikiEnv.provider || "google";
|
|
1542
1651
|
if (!apiKey && !["ollama", "lmstudio"].includes(prov)) {
|
|
@@ -1550,7 +1659,7 @@ async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
|
|
|
1550
1659
|
const s = spinner();
|
|
1551
1660
|
s.start('Installing OpenWiki Daemon...');
|
|
1552
1661
|
|
|
1553
|
-
const scriptBase =
|
|
1662
|
+
const scriptBase = stableOpenWikiScripts();
|
|
1554
1663
|
|
|
1555
1664
|
const daemonEnv = Object.assign({}, process.env, {
|
|
1556
1665
|
OPENWIKI_PROVIDER: prov,
|
|
@@ -2504,118 +2613,283 @@ function aoSupportedHere() {
|
|
|
2504
2613
|
return ['darwin', 'linux', 'win32'].includes(process.platform);
|
|
2505
2614
|
}
|
|
2506
2615
|
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
return false;
|
|
2511
|
-
}
|
|
2616
|
+
const AO_PKG = '@hybridlabor-api/bdb-agent-orchestrator';
|
|
2617
|
+
// `ao service install` restarts the daemon; it can take well over 8s to bind.
|
|
2618
|
+
const AO_DAEMON_WAIT_MS = 30000;
|
|
2512
2619
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2620
|
+
function aoBinTarget() {
|
|
2621
|
+
return process.platform === 'win32'
|
|
2622
|
+
? path.join(process.env.LOCALAPPDATA || homeDir, 'Programs', 'ao', 'ao.exe')
|
|
2623
|
+
: path.join(homeDir, '.local', 'bin', 'ao');
|
|
2624
|
+
}
|
|
2516
2625
|
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2626
|
+
// Go links the `go version -m` text between these two 16-byte markers, so the
|
|
2627
|
+
// revision of a binary can be read from its bytes: no Go toolchain needed, and
|
|
2628
|
+
// the binary (possibly the live daemon) is never executed.
|
|
2629
|
+
const GO_BUILDINFO_START = Buffer.from('3077af0c9274080241e1c107e6d618e6', 'hex');
|
|
2630
|
+
const GO_BUILDINFO_END = Buffer.from('f932433186182072008242104116d8f2', 'hex');
|
|
2631
|
+
|
|
2632
|
+
function readGoBuildInfo(binPath) {
|
|
2633
|
+
let buf;
|
|
2634
|
+
try { buf = fs.readFileSync(binPath); } catch { return null; }
|
|
2635
|
+
const start = buf.indexOf(GO_BUILDINFO_START);
|
|
2636
|
+
const end = start < 0 ? -1 : buf.indexOf(GO_BUILDINFO_END, start);
|
|
2637
|
+
return end < 0 ? null : parseGoBuildInfo(buf.subarray(start + GO_BUILDINFO_START.length, end).toString('utf8'));
|
|
2638
|
+
}
|
|
2525
2639
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2640
|
+
// Accepts the embedded text or `go version -m` output (same lines, tab-indented).
|
|
2641
|
+
function parseGoBuildInfo(text) {
|
|
2642
|
+
const get = (key) => (String(text).match(new RegExp(`^\\s*build\\s+${key}=(\\S*)`, 'm')) || [])[1];
|
|
2643
|
+
const revision = get('vcs\\.revision');
|
|
2644
|
+
if (!revision) return null;
|
|
2645
|
+
return { revision, time: get('vcs\\.time') || null, modified: get('vcs\\.modified') === 'true' };
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Never trade the installed ao for a build that is not provably newer and clean.
|
|
2649
|
+
// allowDirty: the AOS_AO_DEV_BUILD opt-in. daemonRunning/candidateIsRelease
|
|
2650
|
+
// only matter when the installed binary's revision is unreadable: a clean
|
|
2651
|
+
// published release may then replace it, but not while some daemon of unknown
|
|
2652
|
+
// build is running.
|
|
2653
|
+
function decideAoInstall({ installedExists, installed, candidate, allowDirty = false, candidateIsRelease = false, daemonRunning = true }) {
|
|
2654
|
+
if (!installedExists) return { install: true, reason: 'no ao installed yet' };
|
|
2655
|
+
if (!candidate) return { install: false, reason: 'the candidate build has no readable revision' };
|
|
2656
|
+
if (candidate.modified && !allowDirty) return { install: false, reason: `the candidate ${candidate.revision.slice(0, 9)} is a dirty build (vcs.modified=true)` };
|
|
2657
|
+
if (!installed) {
|
|
2658
|
+
if (candidateIsRelease && !candidate.modified && daemonRunning === false) {
|
|
2659
|
+
return { install: true, reason: `the installed binary's revision is unreadable and no daemon is running; installing the clean release ${candidate.revision.slice(0, 9)}` };
|
|
2660
|
+
}
|
|
2661
|
+
return { install: false, reason: 'the installed binary\'s revision is unreadable, so a downgrade cannot be ruled out' };
|
|
2662
|
+
}
|
|
2663
|
+
if (candidate.revision === installed.revision) return { install: false, reason: `already at ${installed.revision.slice(0, 9)}` };
|
|
2664
|
+
const c = Date.parse(candidate.time || '');
|
|
2665
|
+
const i = Date.parse(installed.time || '');
|
|
2666
|
+
if (!(c > i)) return { install: false, reason: `the candidate ${candidate.revision.slice(0, 9)} (${candidate.time || 'no date'}) is not newer than the installed ${installed.revision.slice(0, 9)} (${installed.time || 'no date'})` };
|
|
2667
|
+
return { install: true, reason: `${installed.revision.slice(0, 9)} -> ${candidate.revision.slice(0, 9)}` };
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// A dev checkout is a release source only when clean and HEAD sits on a tag.
|
|
2671
|
+
function checkAoWorkspace(dir, git = (args) => execFileSync('git', ['--no-optional-locks', '-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()) {
|
|
2672
|
+
try {
|
|
2673
|
+
if (git(['status', '--porcelain'])) return { ok: false, reason: 'it has uncommitted or untracked changes' };
|
|
2674
|
+
let tag;
|
|
2675
|
+
try { tag = git(['describe', '--tags', '--exact-match', 'HEAD']); } catch { return { ok: false, reason: 'HEAD is not on a release tag' }; }
|
|
2676
|
+
return { ok: true, tag, head: git(['rev-parse', 'HEAD']) };
|
|
2677
|
+
} catch (e) {
|
|
2678
|
+
return { ok: false, reason: `git could not read it (${String(e.message).split('\n')[0]})` };
|
|
2533
2679
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// Builds into a temp dir: a build left in the checkout's gitignored backend/bin
|
|
2683
|
+
// outlives every later pull and gets picked up as if it were current.
|
|
2684
|
+
function buildAoFromSource(dir) {
|
|
2685
|
+
if (!hasExecutable('go') || !fs.existsSync(path.join(dir, 'backend', 'cmd', 'ao'))) return null;
|
|
2686
|
+
const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aos-ao-build-')), process.platform === 'win32' ? 'ao.exe' : 'ao');
|
|
2687
|
+
const s = spinner();
|
|
2688
|
+
s.start('Compiling AO daemon binary from Go source...');
|
|
2689
|
+
try {
|
|
2690
|
+
execFileSync('go', ['build', '-ldflags=-s -w', '-o', out, './cmd/ao'], { cwd: path.join(dir, 'backend'), stdio: 'ignore' });
|
|
2691
|
+
s.stop('Compiled AO daemon binary successfully.');
|
|
2692
|
+
return out;
|
|
2693
|
+
} catch (e) {
|
|
2694
|
+
s.stop(`Failed to compile AO from source: ${e.message}`);
|
|
2695
|
+
return null;
|
|
2537
2696
|
}
|
|
2697
|
+
}
|
|
2538
2698
|
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2699
|
+
// Installed version = the release version the installer recorded for exactly
|
|
2700
|
+
// the revision on disk. Never the dev workspace's package.json.
|
|
2701
|
+
function trustedAoVersion(build, record) {
|
|
2702
|
+
return build && !build.modified && record && record.version && record.revision === build.revision ? record.version : null;
|
|
2703
|
+
}
|
|
2543
2704
|
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
candidateBinaries.push(
|
|
2570
|
-
path.join(osAgentDir, 'backend', 'ao-daemon'),
|
|
2571
|
-
path.join(osAgentDir, 'backend', 'bin', 'ao'),
|
|
2572
|
-
path.join(osAgentDir, 'backend', 'ao')
|
|
2573
|
-
);
|
|
2705
|
+
function describeAoVersion({ exists, build, record, latest }) {
|
|
2706
|
+
if (!exists) return { text: 'Not installed', updateAvailable: false };
|
|
2707
|
+
if (!build) return { text: 'Installed (build revision unreadable, version unknown)', updateAvailable: false };
|
|
2708
|
+
const rev = build.revision.slice(0, 9);
|
|
2709
|
+
if (build.modified) return { text: `Dev build ${rev} with uncommitted changes (not a release, version not comparable)`, updateAvailable: false };
|
|
2710
|
+
const version = trustedAoVersion(build, record);
|
|
2711
|
+
if (!version) return { text: `Build ${rev} (release version unknown)`, updateAvailable: false };
|
|
2712
|
+
if (latest && isNewerVersion(version, latest)) return { text: `Update available (v${version} ➔ v${latest})`, updateAvailable: true };
|
|
2713
|
+
return { text: `v${version} (Up to date)`, updateAvailable: false };
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// The daemon's running.json (AO backend/internal/runfile: pid, port, startedAt).
|
|
2717
|
+
// pid+startedAt tells a restarted daemon from the old one still on :3101.
|
|
2718
|
+
function readAoDaemonIdentity(file = process.env.AO_RUN_FILE || path.join(homeDir, '.ao', 'running.json')) {
|
|
2719
|
+
const data = readJsonFile(file);
|
|
2720
|
+
if (!data || data.pid == null) return null;
|
|
2721
|
+
return data.startedAt ? `${data.pid}@${data.startedAt}` : String(data.pid);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
async function waitUntil(check, timeoutMs, pollMs = 300) {
|
|
2725
|
+
const deadline = Date.now() + timeoutMs;
|
|
2726
|
+
for (;;) {
|
|
2727
|
+
if (check()) return true;
|
|
2728
|
+
if (Date.now() >= deadline) return false;
|
|
2729
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
2574
2730
|
}
|
|
2575
|
-
|
|
2731
|
+
}
|
|
2576
2732
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2733
|
+
// Back up, swap atomically, register the service, wait for a NEW daemon on
|
|
2734
|
+
// :3101. A failed swap or service install puts the previous binary back.
|
|
2735
|
+
async function installAoBinary({ src, dest, stamp = timestamp, serviceInstall, waitForDaemon, readIdentity = readAoDaemonIdentity, restartWaitMs = AO_DAEMON_WAIT_MS }) {
|
|
2736
|
+
let backup = null;
|
|
2737
|
+
if (src) {
|
|
2738
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2739
|
+
if (fs.existsSync(dest)) {
|
|
2740
|
+
backup = `${dest}.bak-${stamp}`;
|
|
2741
|
+
fs.copyFileSync(dest, backup);
|
|
2742
|
+
}
|
|
2581
2743
|
try {
|
|
2582
|
-
|
|
2583
|
-
fs.mkdirSync(path.dirname(buildTarget), { recursive: true });
|
|
2584
|
-
execFileSync('go', ['build', '-ldflags=-s -w', '-o', buildTarget, './cmd/ao'], {
|
|
2585
|
-
cwd: path.join(osAgentDir, 'backend'),
|
|
2586
|
-
stdio: 'ignore'
|
|
2587
|
-
});
|
|
2588
|
-
daemonBin = buildTarget;
|
|
2589
|
-
s.stop('Compiled AO daemon binary successfully.');
|
|
2744
|
+
installBinaryAtomically(src, dest);
|
|
2590
2745
|
} catch (e) {
|
|
2591
|
-
|
|
2746
|
+
if (backup) installBinaryAtomically(backup, dest);
|
|
2747
|
+
log.warn(`Could not place the ao binary (${e.message})${backup ? '; the previous binary is back in place' : ''}.`);
|
|
2748
|
+
return { ok: false, backup, restored: !!backup };
|
|
2592
2749
|
}
|
|
2750
|
+
log.step(`Installed ao to ${dest}${backup ? ` (previous binary kept at ${backup})` : ''}`);
|
|
2751
|
+
}
|
|
2752
|
+
const before = readIdentity();
|
|
2753
|
+
try {
|
|
2754
|
+
await serviceInstall(dest);
|
|
2755
|
+
} catch (e) {
|
|
2756
|
+
if (backup) {
|
|
2757
|
+
installBinaryAtomically(backup, dest);
|
|
2758
|
+
try { await serviceInstall(dest); } catch (e2) { logDebug(e2, 'ao service install after restore'); }
|
|
2759
|
+
}
|
|
2760
|
+
log.warn(`AO service install failed: ${e.message}${backup ? `. Restored the previous ao from ${backup}` : ''}. Start it by hand with: ao service install`);
|
|
2761
|
+
return { ok: false, backup, restored: !!backup };
|
|
2762
|
+
}
|
|
2763
|
+
// No identity before (file missing/unreadable): the port check is all we have.
|
|
2764
|
+
const restarted = before ? await waitUntil(() => { const now = readIdentity(); return !!now && now !== before; }, restartWaitMs) : true;
|
|
2765
|
+
const listening = await waitForDaemon();
|
|
2766
|
+
const backupNote = backup ? ` The previous binary is at ${backup} (restore: mv "${backup}" "${dest}" && ao service install).` : '';
|
|
2767
|
+
if (listening && restarted) {
|
|
2768
|
+
log.success('AO Orchestrator running on http://localhost:3101');
|
|
2769
|
+
} else if (listening) {
|
|
2770
|
+
log.warn(`AO on :3101 is still the old process (${before}); it was not restarted, probably because it does not run under the AO service. Restart it: ao service install, then check ao service status (or stop it and start it the way it was started).${backupNote}`);
|
|
2771
|
+
} else {
|
|
2772
|
+
const hint = process.platform === 'win32'
|
|
2773
|
+
? 'Run `ao service install` again or log out and back in, then check `ao service status`.'
|
|
2774
|
+
: 'Check `ao service status` and `ao service logs`.';
|
|
2775
|
+
log.warn(`AO did not answer on :3101 within ${Math.round(AO_DAEMON_WAIT_MS / 1000)}s. ${hint}${backupNote}`);
|
|
2593
2776
|
}
|
|
2777
|
+
return { ok: true, backup, listening, restarted };
|
|
2778
|
+
}
|
|
2594
2779
|
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2780
|
+
function aoPlatformBinaries(dir) {
|
|
2781
|
+
const b = (...p) => path.join(dir, 'backend', ...p);
|
|
2782
|
+
if (process.platform === 'win32') return [b('bin', 'ao-windows-amd64.exe'), b('bin', 'ao.exe'), b('ao.exe')];
|
|
2783
|
+
const plat = process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
2784
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
2785
|
+
return [b('bin', `ao-${plat}-${arch}`), b('ao-daemon'), b('bin', 'ao'), b('ao')];
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// Picks the binary this run may install. A local checkout counts only when it
|
|
2789
|
+
// is a release source (clean, HEAD on a tag) or AOS_AO_DEV_BUILD=1 opts in;
|
|
2790
|
+
// otherwise the published release package is used, never the dev checkout.
|
|
2791
|
+
function selectAoCandidate({
|
|
2792
|
+
checkoutDir, isLocalGitRepo, releaseDir,
|
|
2793
|
+
devOptIn = process.env.AOS_AO_DEV_BUILD === '1',
|
|
2794
|
+
dryRun = DRY_RUN,
|
|
2795
|
+
checkWorkspace = checkAoWorkspace,
|
|
2796
|
+
download = (dir) => downloadOrUpdateModule(AO_PKG, dir, 'BDB Agent Orchestrator'),
|
|
2797
|
+
build = buildAoFromSource,
|
|
2798
|
+
candidatesIn = aoPlatformBinaries,
|
|
2799
|
+
}) {
|
|
2800
|
+
if (isLocalGitRepo) {
|
|
2801
|
+
const ws = checkWorkspace(checkoutDir);
|
|
2802
|
+
if (ws.ok) {
|
|
2803
|
+
if (dryRun) return { bin: null, source: 'dry-run' };
|
|
2804
|
+
// A prebuilt binary counts only when it was built clean from this exact HEAD.
|
|
2805
|
+
const prebuilt = candidatesIn(checkoutDir).find((p) => {
|
|
2806
|
+
const info = readGoBuildInfo(p);
|
|
2807
|
+
return info && !info.modified && info.revision === ws.head;
|
|
2808
|
+
});
|
|
2809
|
+
return { bin: prebuilt || build(checkoutDir), source: 'checkout' };
|
|
2810
|
+
}
|
|
2811
|
+
if (devOptIn) {
|
|
2812
|
+
log.warn(`AOS_AO_DEV_BUILD=1: building ao from ${checkoutDir} HEAD although ${ws.reason}; a downgrade is still refused.`);
|
|
2813
|
+
if (dryRun) return { bin: null, source: 'dry-run' };
|
|
2814
|
+
return { bin: build(checkoutDir), source: 'dev', allowDirty: true };
|
|
2815
|
+
}
|
|
2816
|
+
log.warn(`The AO checkout ${checkoutDir} is not a release source (${ws.reason}; Go also counts untracked files as uncommitted), so the published release is used. To build from the checkout instead, rerun with AOS_AO_DEV_BUILD=1.`);
|
|
2817
|
+
}
|
|
2818
|
+
if (fs.existsSync(path.join(releaseDir, '.git'))) {
|
|
2819
|
+
log.warn(`${releaseDir} is a git checkout; not downloading the release over it. Keeping the installed ao.`);
|
|
2820
|
+
return { bin: null, source: 'release-dir-is-checkout' };
|
|
2821
|
+
}
|
|
2822
|
+
if (dryRun) {
|
|
2823
|
+
log.step(`[dry-run] would download ${AO_PKG} to ${releaseDir}`);
|
|
2824
|
+
return { bin: null, source: 'dry-run' };
|
|
2825
|
+
}
|
|
2826
|
+
if (!download(releaseDir)) {
|
|
2827
|
+
log.warn(`Could not download ${AO_PKG}; keeping the installed ao.`);
|
|
2828
|
+
return { bin: null, source: 'download-failed' };
|
|
2602
2829
|
}
|
|
2830
|
+
return { bin: candidatesIn(releaseDir).find((p) => fs.existsSync(p)) || null, source: 'release' };
|
|
2831
|
+
}
|
|
2603
2832
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2833
|
+
async function installOSAgentWorkspace() {
|
|
2834
|
+
if (!aoSupportedHere()) {
|
|
2835
|
+
log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
|
|
2606
2836
|
return false;
|
|
2607
2837
|
}
|
|
2608
2838
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2839
|
+
const releaseDir = path.join(moduleBasePath(), 'bdb-agent-orchestrator');
|
|
2840
|
+
const checkoutDir = [
|
|
2841
|
+
path.join(homeDir, 'dev', 'agents', 'bdb-agent-orchestrator'),
|
|
2842
|
+
path.join(homeDir, 'dev', 'bdb-dev', 'bdb-agent-orchestrator'),
|
|
2843
|
+
].find((d) => fs.existsSync(path.join(d, '.git')));
|
|
2844
|
+
if (checkoutDir) log.step(`Found a local AO checkout: ${checkoutDir}`);
|
|
2845
|
+
|
|
2846
|
+
const pick = selectAoCandidate({ checkoutDir, isLocalGitRepo: !!checkoutDir, releaseDir });
|
|
2847
|
+
if (DRY_RUN) {
|
|
2848
|
+
log.step('[dry-run] would install the ao binary and run `ao service install`');
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const binTarget = aoBinTarget();
|
|
2853
|
+
const installedExists = fs.existsSync(binTarget);
|
|
2854
|
+
let src = null;
|
|
2855
|
+
let candidate = null;
|
|
2856
|
+
if (pick.bin) {
|
|
2857
|
+
candidate = readGoBuildInfo(pick.bin);
|
|
2858
|
+
const installed = installedExists ? readGoBuildInfo(binTarget) : null;
|
|
2859
|
+
const daemonRunning = installedExists && !installed
|
|
2860
|
+
? (readAoDaemonIdentity() !== null || await verifyDaemonListening(3101, 'AO Orchestrator', 1500))
|
|
2861
|
+
: true;
|
|
2862
|
+
const verdict = decideAoInstall({ installedExists, installed, candidate, allowDirty: !!pick.allowDirty, candidateIsRelease: pick.source === 'release', daemonRunning });
|
|
2863
|
+
if (verdict.install) src = pick.bin;
|
|
2864
|
+
else log.step(`Keeping ${binTarget}: ${verdict.reason}.`);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
if (!src && !installedExists) {
|
|
2868
|
+
log.warn(`No ao binary could be installed (${pick.source}).`);
|
|
2869
|
+
return false;
|
|
2615
2870
|
}
|
|
2871
|
+
// Unchanged binary and a live daemon: nothing to restart.
|
|
2872
|
+
if (!src && await verifyDaemonListening(3101, 'AO Orchestrator', 1500)) {
|
|
2873
|
+
log.success('AO Orchestrator running on http://localhost:3101');
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
const result = await installAoBinary({
|
|
2878
|
+
src,
|
|
2879
|
+
dest: binTarget,
|
|
2880
|
+
serviceInstall: (bin) => aoServiceInstall(bin),
|
|
2881
|
+
waitForDaemon: () => verifyDaemonListening(3101, 'AO Orchestrator', AO_DAEMON_WAIT_MS),
|
|
2882
|
+
});
|
|
2883
|
+
if (result.ok && src) {
|
|
2884
|
+
const pkgJson = readJsonFile(path.join(pick.source === 'release' ? releaseDir : checkoutDir, 'package.json')) || {};
|
|
2885
|
+
saveManifest({ ao: { version: pick.source === 'dev' ? null : (pkgJson.version || null), revision: candidate ? candidate.revision : null, modified: !!(candidate && candidate.modified), installedAt: new Date().toISOString() } });
|
|
2886
|
+
}
|
|
2887
|
+
return result.ok ? undefined : false;
|
|
2888
|
+
}
|
|
2616
2889
|
|
|
2890
|
+
function aoServiceInstall(binTarget) {
|
|
2617
2891
|
// macOS only: code-sign ad-hoc to prevent AMFI SIGKILL
|
|
2618
|
-
if (
|
|
2892
|
+
if (process.platform === 'darwin') {
|
|
2619
2893
|
try {
|
|
2620
2894
|
execFileSync('codesign', ['-v', binTarget], { stdio: 'ignore' });
|
|
2621
2895
|
} catch {
|
|
@@ -2625,35 +2899,17 @@ async function installOSAgentWorkspace() {
|
|
|
2625
2899
|
log.step('Signed it ad-hoc.');
|
|
2626
2900
|
} catch (e) { log.warn(`Could not sign it: ${e.message}`); }
|
|
2627
2901
|
}
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
// AO installs its own service, and does it for macOS, Windows and Linux.
|
|
2631
|
-
if (isMac) {
|
|
2632
2902
|
const legacyPlist = path.join(homeDir, 'Library', 'LaunchAgents', 'com.bdb.agent-workspace.plist');
|
|
2633
2903
|
if (fs.existsSync(legacyPlist)) {
|
|
2634
2904
|
try { execFileSync('launchctl', ['unload', legacyPlist], { stdio: 'ignore' }); } catch (e) { logDebug(e, 'unload legacy ao agent'); }
|
|
2635
2905
|
try { fs.unlinkSync(legacyPlist); log.step('Removed the old com.bdb.agent-workspace service.'); } catch (e) { logDebug(e, 'remove legacy plist'); }
|
|
2636
2906
|
}
|
|
2637
2907
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
throw new Error(describeExecError(e));
|
|
2644
|
-
}
|
|
2645
|
-
}, 'Start it by hand with: ao service install');
|
|
2646
|
-
if (!serviceStep.ok) return false;
|
|
2647
|
-
|
|
2648
|
-
// Windows startup entries fire asynchronously — give the service more time
|
|
2649
|
-
const aoTimeout = process.platform === 'win32' ? 20000 : 8000;
|
|
2650
|
-
if (await verifyDaemonListening(3101, 'AO Orchestrator', aoTimeout)) {
|
|
2651
|
-
log.success('AO Orchestrator running on http://localhost:3101');
|
|
2652
|
-
} else {
|
|
2653
|
-
const hint = process.platform === 'win32'
|
|
2654
|
-
? 'Run `ao service install` again or log out and back in, then check `ao service status`.'
|
|
2655
|
-
: 'Check `ao service status` and `ao service logs`.';
|
|
2656
|
-
log.warn(`AO did not answer on :3101 — ${hint}`);
|
|
2908
|
+
// AO installs its own service, and does it for macOS, Windows and Linux.
|
|
2909
|
+
try {
|
|
2910
|
+
execFileSync(binTarget, ['service', 'install'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
2911
|
+
} catch (e) {
|
|
2912
|
+
throw new Error(describeExecError(e));
|
|
2657
2913
|
}
|
|
2658
2914
|
}
|
|
2659
2915
|
async function promptMemBIngestion(mcpCodeTarget) {
|
|
@@ -2715,6 +2971,18 @@ function verifyEcosystemInstallation() {
|
|
|
2715
2971
|
];
|
|
2716
2972
|
|
|
2717
2973
|
for (const mod of modules) {
|
|
2974
|
+
if (mod.pkg === AO_PKG) {
|
|
2975
|
+
const bin = aoBinTarget();
|
|
2976
|
+
const exists = fs.existsSync(bin);
|
|
2977
|
+
const build = exists ? readGoBuildInfo(bin) : null;
|
|
2978
|
+
const record = (readJsonFile(path.join(homeDir, '.agents', '.bdb-manifest.json')) || {}).ao || null;
|
|
2979
|
+
const version = trustedAoVersion(build, record);
|
|
2980
|
+
const latest = version ? (newestDistTag(AO_PKG, version) || {}).version : null;
|
|
2981
|
+
const s = describeAoVersion({ exists, build, record, latest });
|
|
2982
|
+
const mark = !exists ? `${colors.dim}⚪` : s.updateAvailable ? `${colors.yellow}⚠️ ` : build && !build.modified ? `${colors.green}✅` : `${colors.yellow}⚠️ `;
|
|
2983
|
+
console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${mark} ${s.text}${colors.reset}`);
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2718
2986
|
let modulePkgPath = null;
|
|
2719
2987
|
for (const p of mod.paths) {
|
|
2720
2988
|
const candidate = path.join(p, 'package.json');
|
|
@@ -2727,29 +2995,9 @@ function verifyEcosystemInstallation() {
|
|
|
2727
2995
|
if (modulePkgPath) {
|
|
2728
2996
|
try {
|
|
2729
2997
|
const localVer = JSON.parse(fs.readFileSync(modulePkgPath, 'utf8')).version || '1.0.0';
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
2734
|
-
const distTagsJson = process.platform === 'win32'
|
|
2735
|
-
? execSync(`"${npmCmd}" view ${mod.pkg} dist-tags --json`, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 4000 }).trim()
|
|
2736
|
-
: execFileSync(npmCmd, ['view', mod.pkg, 'dist-tags', '--json'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 4000 }).trim();
|
|
2737
|
-
const distTags = JSON.parse(distTagsJson);
|
|
2738
|
-
for (const [tag, ver] of Object.entries(distTags)) {
|
|
2739
|
-
if (isNewerVersion(localVer, ver)) {
|
|
2740
|
-
if (!newerVersion || isNewerVersion(newerVersion, ver)) {
|
|
2741
|
-
newerVersion = ver;
|
|
2742
|
-
newerTag = tag;
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
} catch (e) { logDebug(e, 'operation'); }
|
|
2747
|
-
|
|
2748
|
-
// `npm view <pkg> dist-tags --json` fetches all tags.
|
|
2749
|
-
// We compare every tag's version against localVer with the fixed
|
|
2750
|
-
// isNewerVersion(), and report an update if ANY tag is ahead.
|
|
2751
|
-
if (newerVersion) {
|
|
2752
|
-
console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.yellow}⚠️ Update available (v${localVer} ➔ v${newerVersion} (${newerTag}))${colors.reset}`);
|
|
2998
|
+
const newer = newestDistTag(mod.pkg, localVer);
|
|
2999
|
+
if (newer) {
|
|
3000
|
+
console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.yellow}⚠️ Update available (v${localVer} ➔ v${newer.version} (${newer.tag}))${colors.reset}`);
|
|
2753
3001
|
} else {
|
|
2754
3002
|
console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.green}✅ v${localVer} (Up to date)${colors.reset}`);
|
|
2755
3003
|
}
|
|
@@ -3473,6 +3721,14 @@ function compileClaudeAgents(agents, targetDir, pipelineConfig = null) {
|
|
|
3473
3721
|
}
|
|
3474
3722
|
}
|
|
3475
3723
|
|
|
3724
|
+
// .claude/agents/<slug>.md is shipped and copied to ~/.claude/agents by the
|
|
3725
|
+
// manifest-tracked "copy harness directories" step. Compiling the same slug
|
|
3726
|
+
// there first made the two writers fight: the compiled bytes never matched the
|
|
3727
|
+
// manifest, so every update flagged all 13 personas as user-edited.
|
|
3728
|
+
function agentsNotShippedAsFiles(agents, shippedDir = path.join(srcDir, '.claude', 'agents')) {
|
|
3729
|
+
return agents.filter((a) => !fs.existsSync(path.join(shippedDir, `${a.name.toLowerCase().replace(/_/g, '-')}.md`)));
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3476
3732
|
// Generates .opencode/agents/<name>.md. Emits description, mode: subagent, and model.
|
|
3477
3733
|
function compileOpenCodeAgents(agents, targetDir, pipelineConfig = null) {
|
|
3478
3734
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -3575,8 +3831,12 @@ function injectHarnessRules() {
|
|
|
3575
3831
|
if (sources.ok) {
|
|
3576
3832
|
const { globalRules, startcycleContent, agentsMdContent } = sources.value;
|
|
3577
3833
|
|
|
3834
|
+
// Run from $HOME, these project files ARE the global files that the
|
|
3835
|
+
// manifest-tracked harness copy below writes; a second writer would
|
|
3836
|
+
// make every update see them as user-edited.
|
|
3837
|
+
const cwdIsHome = path.resolve(currentDir) === path.resolve(homeDir);
|
|
3578
3838
|
const cursorRulesDir = path.join(currentDir, '.cursor', 'rules');
|
|
3579
|
-
installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
|
|
3839
|
+
if (!cwdIsHome) installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
|
|
3580
3840
|
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
3581
3841
|
fs.writeFileSync(path.join(cursorRulesDir, '000_global_rules.mdc'), `---\nname: global-rules\ndescription: Global BDB Agent Rules\n---\n\n${globalRules}`);
|
|
3582
3842
|
if (startcycleContent) {
|
|
@@ -3618,7 +3878,7 @@ function injectHarnessRules() {
|
|
|
3618
3878
|
installStep('compile Claude Code subagents', () => {
|
|
3619
3879
|
if (agentsMdContent) {
|
|
3620
3880
|
const pipelineConfig = loadPipelineConfig();
|
|
3621
|
-
const agents = parseAgentsMd(agentsMdContent);
|
|
3881
|
+
const agents = agentsNotShippedAsFiles(parseAgentsMd(agentsMdContent));
|
|
3622
3882
|
const claudeAgentsDir = path.join(homeDir, '.claude', 'agents');
|
|
3623
3883
|
compileClaudeAgents(agents, claudeAgentsDir, pipelineConfig);
|
|
3624
3884
|
log.step(`Compiled AGENTS.md to Claude Code subagents in ${claudeAgentsDir}`);
|
|
@@ -3723,7 +3983,7 @@ function injectHarnessRules() {
|
|
|
3723
3983
|
const targetPath = path.join(homeDir, dir);
|
|
3724
3984
|
// settings.json is merged separately below: a wholesale
|
|
3725
3985
|
// copy would clobber user-owned keys like enabledPlugins.
|
|
3726
|
-
const exclude = dir === '.claude' ? ['settings.json'] : [];
|
|
3986
|
+
const exclude = dir === '.claude' ? ['settings.json'] : dir === '.agents' ? AGENTS_COPY_EXCLUDE : [];
|
|
3727
3987
|
copyDirRecursiveSync(sourcePath, targetPath, exclude);
|
|
3728
3988
|
log.step(`Copied ${dir} to ${targetPath}`);
|
|
3729
3989
|
}
|
|
@@ -3735,13 +3995,109 @@ function injectHarnessRules() {
|
|
|
3735
3995
|
const globalAgentsDir = path.join(os.homedir(), '.agents');
|
|
3736
3996
|
const agentsDirSrc = path.join(srcDir, '.agents');
|
|
3737
3997
|
if (fs.existsSync(agentsDirSrc)) {
|
|
3738
|
-
copyDirRecursiveSync(agentsDirSrc, globalAgentsDir);
|
|
3998
|
+
copyDirRecursiveSync(agentsDirSrc, globalAgentsDir, AGENTS_COPY_EXCLUDE);
|
|
3739
3999
|
log.step(`Synced global .agents/ to ${globalAgentsDir}`);
|
|
3740
4000
|
}
|
|
3741
4001
|
}, 'agents.md and workflows/startcycle.md may be missing globally.');
|
|
3742
4002
|
}
|
|
3743
4003
|
}
|
|
3744
4004
|
|
|
4005
|
+
// Optional third-party OpenCode components. Off unless named in
|
|
4006
|
+
// AOS_OPENCODE_OPTIONAL=ponytail,loop,rtk or --opencode-optional=ponytail,loop,rtk.
|
|
4007
|
+
// Pinned entries only; the foreign installers (opencode-loop's npx installer, rtk init)
|
|
4008
|
+
// are never run because they rewrite the user's opencode config.
|
|
4009
|
+
const OPENCODE_OPTIONAL_PINS = {
|
|
4010
|
+
ponytail: '@dietrichgebert/ponytail@4.10.0',
|
|
4011
|
+
loop: '@bybrawe/opencode-loop@0.6.2',
|
|
4012
|
+
};
|
|
4013
|
+
const OPENCODE_RTK_HINT = 'rtk is not installed by AOS. Run yourself: brew install rtk && rtk init -g --opencode';
|
|
4014
|
+
const OPENCODE_LOOP_SHELL_WARNING = 'opencode-loop: /loop-shell style commands run shell commands as child processes. ' +
|
|
4015
|
+
'tool.execute.before, and so the AOS go-gate, may never see them (unverified). ' +
|
|
4016
|
+
'Do not schedule git push, publish or other gated commands through them.';
|
|
4017
|
+
|
|
4018
|
+
function parseOpencodeOptional(argv = process.argv, env = process.env) {
|
|
4019
|
+
const flag = argv.find((a) => a.startsWith('--opencode-optional='));
|
|
4020
|
+
const raw = [env.AOS_OPENCODE_OPTIONAL, flag && flag.slice('--opencode-optional='.length)].filter(Boolean).join(',');
|
|
4021
|
+
return new Set(raw.split(',').map((x) => x.trim().toLowerCase()).filter(Boolean));
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
const pluginPackageName = (entry) => {
|
|
4025
|
+
const str = typeof entry === 'string' ? entry : (Array.isArray(entry) ? String(entry[0]) : '');
|
|
4026
|
+
return str.replace(/(?<=.)@[^@/]*$/, '');
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
// OpenCode auto-loads plugins/*.js and also loads plugin[] paths, then dedupes by file
|
|
4030
|
+
// URL (verified in the 1.18.30 binary strings, not by running it). Same path = one load;
|
|
4031
|
+
// a second differing spelling would load the gate and loop keeper twice.
|
|
4032
|
+
function warnOnDuplicateAosPlugin(plugins, canonical) {
|
|
4033
|
+
const norm = (v) => String(Array.isArray(v) ? v[0] : v).replace(/\\/g, '/');
|
|
4034
|
+
const others = plugins.filter((p) => /bdb-aos\.js$/.test(norm(p)) && norm(p) !== canonical);
|
|
4035
|
+
if (others.length > 0) {
|
|
4036
|
+
log.warn(`opencode.jsonc lists bdb-aos.js under another path (${others.map(norm).join(', ')}). ` +
|
|
4037
|
+
`plugins/bdb-aos.js is auto-loaded too; remove the extra entry to avoid a double gate.`);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
function appendOptionalOpencodePlugins(data, configPath, optional) {
|
|
4042
|
+
const wanted = optional instanceof Set ? optional : new Set(optional || []);
|
|
4043
|
+
for (const name of wanted) {
|
|
4044
|
+
if (!(name in OPENCODE_OPTIONAL_PINS) && name !== 'rtk') log.warn(`Unknown OpenCode optional component "${name}" ignored.`);
|
|
4045
|
+
}
|
|
4046
|
+
if (wanted.has('rtk')) log.message(OPENCODE_RTK_HINT);
|
|
4047
|
+
let backedUp = false;
|
|
4048
|
+
for (const name of Object.keys(OPENCODE_OPTIONAL_PINS)) {
|
|
4049
|
+
if (!wanted.has(name) || DRY_RUN) continue;
|
|
4050
|
+
const pin = OPENCODE_OPTIONAL_PINS[name];
|
|
4051
|
+
if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(pin))) continue;
|
|
4052
|
+
if (!backedUp && configPath && fs.existsSync(configPath)) {
|
|
4053
|
+
const bak = `${configPath}.${timestamp}.bak`;
|
|
4054
|
+
try { fs.copyFileSync(configPath, bak); backedUp = true; } catch (e) {
|
|
4055
|
+
log.warn(`Could not back up ${configPath}, skipping optional ${name}: ${e.message}`);
|
|
4056
|
+
continue;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
data.plugin.push(pin);
|
|
4060
|
+
log.step(`Added opt-in OpenCode plugin ${pin}`);
|
|
4061
|
+
}
|
|
4062
|
+
if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(OPENCODE_OPTIONAL_PINS.loop))) {
|
|
4063
|
+
log.warn(OPENCODE_LOOP_SHELL_WARNING);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
// Idempotent and edit-safe: a file is overwritten only when it is absent, already
|
|
4068
|
+
// identical, or still byte-equal to what this installer last recorded. Anything else
|
|
4069
|
+
// (user edit, or an untracked file with the same name) is kept and the shipped copy
|
|
4070
|
+
// lands as <file>.new. Files the installer did not create are never removed.
|
|
4071
|
+
function installOpencodeCommands(srcDirPath, destDirPath) {
|
|
4072
|
+
if (DRY_RUN) {
|
|
4073
|
+
log.message(`[dry-run] install OpenCode commands: ${srcDirPath} -> ${destDirPath}`);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const ownsManifest = !_sessionManifest;
|
|
4077
|
+
const manifest = _sessionManifest || loadInstallManifest();
|
|
4078
|
+
fs.mkdirSync(destDirPath, { recursive: true });
|
|
4079
|
+
let wrote = 0;
|
|
4080
|
+
for (const file of fs.readdirSync(srcDirPath)) {
|
|
4081
|
+
const src = path.join(srcDirPath, file);
|
|
4082
|
+
if (!fs.statSync(src).isFile()) continue;
|
|
4083
|
+
const dest = path.join(destDirPath, file);
|
|
4084
|
+
const srcHash = computeFileHash(src);
|
|
4085
|
+
const diskHash = fs.existsSync(dest) ? computeFileHash(dest) : null;
|
|
4086
|
+
if (diskHash === null || diskHash === srcHash || diskHash === manifest[dest]?.sha256) {
|
|
4087
|
+
if (diskHash !== srcHash) { fs.copyFileSync(src, dest); wrote++; }
|
|
4088
|
+
manifest[dest] = { path: dest, sha256: srcHash, version: pkg.version, installedAt: new Date().toISOString() };
|
|
4089
|
+
} else {
|
|
4090
|
+
fs.copyFileSync(src, `${dest}.new`);
|
|
4091
|
+
keptUserEdits.push(dest);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
if (ownsManifest) {
|
|
4095
|
+
saveInstallManifest(manifest);
|
|
4096
|
+
reportKeptUserEdits();
|
|
4097
|
+
}
|
|
4098
|
+
log.step(`OpenCode commands in ${destDirPath}: ${wrote} written`);
|
|
4099
|
+
}
|
|
4100
|
+
|
|
3745
4101
|
// Copy the OpenCode plugin + command payload and register both in
|
|
3746
4102
|
// opencode.jsonc. This is the single copy site: the Quick Update path
|
|
3747
4103
|
// (injectHarnessRules -> installGlobalHooks) and the fresh-install path
|
|
@@ -3752,7 +4108,7 @@ function injectHarnessRules() {
|
|
|
3752
4108
|
// Sync merges MCP servers in the same pass and writes once). When `data` is
|
|
3753
4109
|
// omitted the function loads and saves the config itself, which is what lets the
|
|
3754
4110
|
// Quick Update path register the plugin as well as copy it.
|
|
3755
|
-
function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null } = {}) {
|
|
4111
|
+
function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null, optional = parseOpencodeOptional() } = {}) {
|
|
3756
4112
|
const opencodeDir = configPath
|
|
3757
4113
|
? path.dirname(configPath)
|
|
3758
4114
|
: (process.platform === 'win32'
|
|
@@ -3789,13 +4145,11 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
|
|
|
3789
4145
|
try { copyDirRecursiveSync(trailLibSrc, path.join(opencodeDir, 'plugins', 'lib')); } catch (e) { log.warn(`Could not install OpenCode plugin lib: ${e.message}`); }
|
|
3790
4146
|
}
|
|
3791
4147
|
|
|
3792
|
-
// Slash-command payloads
|
|
3793
|
-
// native resolution and only survives as a raw-text match in the plugin.
|
|
4148
|
+
// Slash-command payloads (/startcycle-graph and the generated /bdb-aos-<cmd> files).
|
|
3794
4149
|
const commandsSrc = path.join(srcDir, '.opencode', 'commands');
|
|
3795
4150
|
if (fs.existsSync(commandsSrc)) {
|
|
3796
4151
|
try {
|
|
3797
|
-
|
|
3798
|
-
log.step(`Installed OpenCode commands to ${path.join(opencodeDir, 'commands')}`);
|
|
4152
|
+
installOpencodeCommands(commandsSrc, path.join(opencodeDir, 'commands'));
|
|
3799
4153
|
} catch (e) {
|
|
3800
4154
|
log.warn(`Could not install OpenCode commands: ${e.message}`);
|
|
3801
4155
|
}
|
|
@@ -3822,6 +4176,8 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
|
|
|
3822
4176
|
return str.includes('bdb-aos');
|
|
3823
4177
|
});
|
|
3824
4178
|
if (!alreadyRegistered) data.plugin.push(pluginPathNormalized);
|
|
4179
|
+
warnOnDuplicateAosPlugin(data.plugin, pluginPathNormalized);
|
|
4180
|
+
appendOptionalOpencodePlugins(data, configPath, optional);
|
|
3825
4181
|
|
|
3826
4182
|
// Skill paths. `.agents/skills` stays project-relative on purpose: it is the
|
|
3827
4183
|
// per-project contract, and OpenCode resolves it against each project, so it
|
|
@@ -3987,7 +4343,7 @@ function installGlobalBinaries() {
|
|
|
3987
4343
|
// merged result goes to a .bdb-new.json sidecar -- the same recovery pattern
|
|
3988
4344
|
// the MCP config merge in installMcpsForTarget uses.
|
|
3989
4345
|
function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
|
|
3990
|
-
const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
|
|
4346
|
+
const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'go-grant.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
|
|
3991
4347
|
// memb-inject reads the machine-global memB store under $HOME and is
|
|
3992
4348
|
// installed once per machine, so it stays $HOME-anchored even inside a
|
|
3993
4349
|
// project harness -- unlike the two gate hooks, which are per-checkout by
|
|
@@ -4095,9 +4451,12 @@ function selfCheckAgyHooks(hooksPath, spawn = spawnSync) {
|
|
|
4095
4451
|
const failures = [];
|
|
4096
4452
|
for (const command of [...new Set(commands)]) {
|
|
4097
4453
|
// AGENTTRAIL_PORT=1: the no-op payload must not show up on a live map.
|
|
4098
|
-
const r = spawn(shell, [flag, command], { cwd: path.dirname(hooksPath), input: '{}', encoding: 'utf8', timeout:
|
|
4454
|
+
const r = spawn(shell, [flag, command], { cwd: path.dirname(hooksPath), input: '{}', encoding: 'utf8', timeout: 20000, windowsHide: true, env: { ...process.env, AGENTTRAIL_PORT: '1' } });
|
|
4099
4455
|
const stderr = String((r && r.stderr) || '');
|
|
4100
4456
|
let reason = null;
|
|
4457
|
+
// A slow machine (memb-inject reads the store) can exceed the limit without the hook being broken:
|
|
4458
|
+
// the failures this check exists for (bad path, missing module, non-zero exit) are immediate.
|
|
4459
|
+
if (r && r.error && r.error.code === 'ETIMEDOUT') { log.warn(`agy hook self-check timed out, not treated as a failure: ${command}`); continue; }
|
|
4101
4460
|
if (!r || r.error) reason = `spawn failed: ${r && r.error ? r.error.message : 'no result'}`;
|
|
4102
4461
|
else if (r.status !== 0) reason = `exit ${r.status}\n ${stderr.trim().slice(0, 500)}`;
|
|
4103
4462
|
else if (/MODULE_NOT_FOUND|Cannot find module/.test(stderr)) reason = `module not found\n ${stderr.trim().slice(0, 500)}`;
|
|
@@ -4391,7 +4750,7 @@ function installProjectHarness() {
|
|
|
4391
4750
|
installStep('copy .agents/ contract into project', () => {
|
|
4392
4751
|
const agentsSrc = path.join(srcDir, '.agents');
|
|
4393
4752
|
if (!fs.existsSync(agentsSrc)) throw new Error(`missing payload: ${agentsSrc}`);
|
|
4394
|
-
copyDirRecursiveSync(agentsSrc, projectAgentsDir);
|
|
4753
|
+
copyDirRecursiveSync(agentsSrc, projectAgentsDir, AGENTS_COPY_EXCLUDE);
|
|
4395
4754
|
log.step(`Copied .agents/ contract to ${projectAgentsDir}`);
|
|
4396
4755
|
}, 'graph.md / state.schema.json may be missing in the project.');
|
|
4397
4756
|
|
|
@@ -5420,20 +5779,22 @@ async function runQuickUpdate(installState) {
|
|
|
5420
5779
|
|
|
5421
5780
|
|
|
5422
5781
|
function buildAoAnnouncementBanner() {
|
|
5782
|
+
const W = 78;
|
|
5783
|
+
const row = (text, style = '') => `\x1b[36m│\x1b[0m${style}${text.padEnd(W)}\x1b[0m\x1b[36m│\x1b[0m`;
|
|
5423
5784
|
return [
|
|
5424
|
-
|
|
5425
|
-
'
|
|
5426
|
-
'
|
|
5427
|
-
'
|
|
5428
|
-
'
|
|
5429
|
-
'
|
|
5430
|
-
'
|
|
5431
|
-
'
|
|
5432
|
-
'
|
|
5433
|
-
'
|
|
5434
|
-
'
|
|
5435
|
-
'
|
|
5436
|
-
|
|
5785
|
+
`\x1b[36m╭${'─'.repeat(W)}╮\x1b[0m`,
|
|
5786
|
+
row(''),
|
|
5787
|
+
row(' BDB AGENT ORCHESTRATOR APP — FINAL BETA NOW AVAILABLE', '\x1b[1m\x1b[35m'),
|
|
5788
|
+
row(''),
|
|
5789
|
+
row(' The next generation of cross-harness multi-agent orchestration'),
|
|
5790
|
+
row(' is now open to every user as a final beta.'),
|
|
5791
|
+
row(''),
|
|
5792
|
+
row(' • Dashboard & WebUI: http://localhost:3101'),
|
|
5793
|
+
row(' • Service command: ao service install (enables the background service)'),
|
|
5794
|
+
row(' • Quick launch: ao open or ao service status'),
|
|
5795
|
+
row(' • Features: session telemetry, live AgentTrail, multi-workspaces'),
|
|
5796
|
+
row(''),
|
|
5797
|
+
`\x1b[36m╰${'─'.repeat(W)}╯\x1b[0m\n`
|
|
5437
5798
|
].join('\n');
|
|
5438
5799
|
}
|
|
5439
5800
|
|
|
@@ -5487,11 +5848,11 @@ function installBinaryAtomically(src, dest) {
|
|
|
5487
5848
|
|
|
5488
5849
|
async function main() {
|
|
5489
5850
|
const args = process.argv.slice(2);
|
|
5490
|
-
if (args.includes('--version') || args.includes('-V')) {
|
|
5851
|
+
if (args.includes('--version') || args.includes('-V') || args[0] === 'version') {
|
|
5491
5852
|
console.log(pkg.version);
|
|
5492
5853
|
return;
|
|
5493
5854
|
}
|
|
5494
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
5855
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
5495
5856
|
console.log(`Usage: aos [command] [options]
|
|
5496
5857
|
|
|
5497
5858
|
Commands:
|
|
@@ -5501,6 +5862,15 @@ Commands:
|
|
|
5501
5862
|
Options:
|
|
5502
5863
|
-y, --yes Non-interactive install (implied without a TTY)
|
|
5503
5864
|
--dry-run Show what would change
|
|
5865
|
+
--opencode-optional=LIST
|
|
5866
|
+
Opt in to OpenCode extras (comma list: ponytail, loop, rtk), or set
|
|
5867
|
+
AOS_OPENCODE_OPTIONAL. Pinned plugin[] entries are appended after a
|
|
5868
|
+
config backup; rtk only prints a brew hint. Off by default. AOS never
|
|
5869
|
+
runs foreign installers and never touches OpenCode's mcp set.
|
|
5870
|
+
--plugin-migration=off|check
|
|
5871
|
+
Skip (off) or only report (check) the bdb-aos plugin registration and
|
|
5872
|
+
removal of AOS's own loose skill copies; same as AOS_PLUGIN_MIGRATION.
|
|
5873
|
+
--dry-run implies check. Default: on.
|
|
5504
5874
|
--verbose, -v Verbose output
|
|
5505
5875
|
-V, --version Print the version and exit
|
|
5506
5876
|
-h, --help Print this help and exit`);
|
|
@@ -5868,45 +6238,7 @@ Options:
|
|
|
5868
6238
|
const s = spinner();
|
|
5869
6239
|
s.start(`Installing optimized skills${tier === '2' ? ' [Basic Tier]' : ''} to ${targets.length} target(s)...`);
|
|
5870
6240
|
|
|
5871
|
-
|
|
5872
|
-
if (mode === 'replace') {
|
|
5873
|
-
moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
|
|
5874
|
-
moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
|
|
5875
|
-
moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
|
|
5876
|
-
}
|
|
5877
|
-
|
|
5878
|
-
installStep(`create the skill target directories (${t.value})`, () => {
|
|
5879
|
-
fs.mkdirSync(t.targetSkillDir, { recursive: true });
|
|
5880
|
-
retireObsoleteLegacyDir(t.targetLegacyDir);
|
|
5881
|
-
fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
|
|
5882
|
-
}, 'The skill copies below will most likely be skipped as well.');
|
|
5883
|
-
|
|
5884
|
-
if (fs.existsSync(skillsBase)) {
|
|
5885
|
-
installStep(`install the skills (${t.value})`, () => {
|
|
5886
|
-
const rawDirs = fs.readdirSync(skillsBase);
|
|
5887
|
-
const dirs = rawDirs.sort((a, b) => {
|
|
5888
|
-
const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
|
|
5889
|
-
const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
|
|
5890
|
-
if (aIsLeaf && !bIsLeaf) return 1;
|
|
5891
|
-
if (!aIsLeaf && bIsLeaf) return -1;
|
|
5892
|
-
return 0;
|
|
5893
|
-
});
|
|
5894
|
-
for (const dir of dirs) {
|
|
5895
|
-
const fullPath = path.join(skillsBase, dir);
|
|
5896
|
-
if (!fs.statSync(fullPath).isDirectory()) continue;
|
|
5897
|
-
|
|
5898
|
-
if (dir === 'global_legacy') {
|
|
5899
|
-
copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
|
|
5900
|
-
} else if (dir === 'workspace_agents') {
|
|
5901
|
-
copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
|
|
5902
|
-
} else {
|
|
5903
|
-
syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
|
|
5904
|
-
}
|
|
5905
|
-
}
|
|
5906
|
-
log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
|
|
5907
|
-
}, 'The skills are missing or incomplete; the rest of the installation continues.');
|
|
5908
|
-
}
|
|
5909
|
-
}
|
|
6241
|
+
installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase });
|
|
5910
6242
|
|
|
5911
6243
|
syncSkillsToGlobalHarnesses(excludeSkills);
|
|
5912
6244
|
pruneRemovedSkills(_sessionManifest);
|
|
@@ -5961,6 +6293,16 @@ if (require.main === module) {
|
|
|
5961
6293
|
module.exports = {
|
|
5962
6294
|
installBinaryAtomically,
|
|
5963
6295
|
installOSAgentWorkspace,
|
|
6296
|
+
readGoBuildInfo,
|
|
6297
|
+
parseGoBuildInfo,
|
|
6298
|
+
decideAoInstall,
|
|
6299
|
+
checkAoWorkspace,
|
|
6300
|
+
installAoBinary,
|
|
6301
|
+
readAoDaemonIdentity,
|
|
6302
|
+
selectAoCandidate,
|
|
6303
|
+
describeAoVersion,
|
|
6304
|
+
buildAoAnnouncementBanner,
|
|
6305
|
+
agentsNotShippedAsFiles,
|
|
5964
6306
|
downloadOrUpdateModule,
|
|
5965
6307
|
detectPlatforms,
|
|
5966
6308
|
markPlatformsExplicit,
|
|
@@ -5976,6 +6318,10 @@ module.exports = {
|
|
|
5976
6318
|
mergeCodexTomlMcpServers,
|
|
5977
6319
|
installGlobalHooks,
|
|
5978
6320
|
installOpencodePlugin,
|
|
6321
|
+
installOpencodeCommands,
|
|
6322
|
+
parseOpencodeOptional,
|
|
6323
|
+
pluginMigrationMode,
|
|
6324
|
+
runPluginMigration,
|
|
5979
6325
|
installProjectHarness,
|
|
5980
6326
|
promptMcpSelection,
|
|
5981
6327
|
mirrorMcpServersTo,
|
|
@@ -5988,6 +6334,9 @@ module.exports = {
|
|
|
5988
6334
|
resolveFileConflict,
|
|
5989
6335
|
buildKnownSourceHashes,
|
|
5990
6336
|
initSessionManifest,
|
|
6337
|
+
installTargetSkills,
|
|
6338
|
+
stableOpenWikiScripts,
|
|
6339
|
+
AGENTS_COPY_EXCLUDE,
|
|
5991
6340
|
flushSessionManifest,
|
|
5992
6341
|
copyDirRecursiveSync,
|
|
5993
6342
|
INSTALL_MANIFEST_PATH,
|