aether-colony 5.3.2 → 5.4.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/.aether/aether-utils.sh +181 -5
- package/.aether/commands/archaeology.yaml +3 -3
- package/.aether/commands/build.yaml +80 -45
- package/.aether/commands/chaos.yaml +7 -7
- package/.aether/commands/colonize.yaml +17 -17
- package/.aether/commands/continue.yaml +40 -40
- package/.aether/commands/council.yaml +6 -6
- package/.aether/commands/data-clean.yaml +3 -3
- package/.aether/commands/dream.yaml +2 -2
- package/.aether/commands/entomb.yaml +12 -12
- package/.aether/commands/export-signals.yaml +2 -2
- package/.aether/commands/feedback.yaml +6 -6
- package/.aether/commands/flag.yaml +2 -2
- package/.aether/commands/flags.yaml +4 -4
- package/.aether/commands/focus.yaml +6 -6
- package/.aether/commands/help.yaml +1 -1
- package/.aether/commands/history.yaml +1 -1
- package/.aether/commands/import-signals.yaml +2 -2
- package/.aether/commands/init.yaml +44 -27
- package/.aether/commands/insert-phase.yaml +1 -1
- package/.aether/commands/interpret.yaml +2 -2
- package/.aether/commands/lay-eggs.yaml +3 -3
- package/.aether/commands/maturity.yaml +2 -2
- package/.aether/commands/memory-details.yaml +1 -1
- package/.aether/commands/migrate-state.yaml +1 -1
- package/.aether/commands/oracle.yaml +147 -82
- package/.aether/commands/organize.yaml +5 -5
- package/.aether/commands/patrol.yaml +8 -8
- package/.aether/commands/pause-colony.yaml +7 -7
- package/.aether/commands/phase.yaml +1 -1
- package/.aether/commands/pheromones.yaml +1 -1
- package/.aether/commands/plan.yaml +14 -14
- package/.aether/commands/quick.yaml +4 -4
- package/.aether/commands/redirect.yaml +6 -6
- package/.aether/commands/resume-colony.yaml +9 -9
- package/.aether/commands/resume.yaml +5 -38
- package/.aether/commands/run.yaml +10 -10
- package/.aether/commands/seal.yaml +33 -33
- package/.aether/commands/skill-create.yaml +4 -4
- package/.aether/commands/status.yaml +14 -14
- package/.aether/commands/swarm.yaml +14 -14
- package/.aether/commands/tunnels.yaml +7 -7
- package/.aether/commands/update.yaml +1 -1
- package/.aether/commands/verify-castes.yaml +3 -3
- package/.aether/commands/watch.yaml +15 -15
- package/.aether/docs/command-playbooks/build-complete.md +48 -15
- package/.aether/docs/command-playbooks/build-context.md +11 -11
- package/.aether/docs/command-playbooks/build-full.md +76 -76
- package/.aether/docs/command-playbooks/build-prep.md +10 -10
- package/.aether/docs/command-playbooks/build-verify.md +27 -27
- package/.aether/docs/command-playbooks/build-wave.md +38 -38
- package/.aether/docs/command-playbooks/continue-advance.md +60 -27
- package/.aether/docs/command-playbooks/continue-finalize.md +25 -11
- package/.aether/docs/command-playbooks/continue-full.md +60 -46
- package/.aether/docs/command-playbooks/continue-gates.md +18 -18
- package/.aether/docs/command-playbooks/continue-verify.md +10 -10
- package/.aether/docs/source-of-truth-map.md +10 -10
- package/.aether/docs/structural-learning-stack.md +283 -0
- package/.aether/templates/colony-state-template.json +1 -0
- package/.aether/utils/consolidation-seal.sh +196 -0
- package/.aether/utils/consolidation.sh +127 -0
- package/.aether/utils/curation-ants/archivist.sh +97 -0
- package/.aether/utils/curation-ants/critic.sh +214 -0
- package/.aether/utils/curation-ants/herald.sh +102 -0
- package/.aether/utils/curation-ants/janitor.sh +121 -0
- package/.aether/utils/curation-ants/librarian.sh +99 -0
- package/.aether/utils/curation-ants/nurse.sh +153 -0
- package/.aether/utils/curation-ants/orchestrator.sh +181 -0
- package/.aether/utils/curation-ants/scribe.sh +164 -0
- package/.aether/utils/curation-ants/sentinel.sh +119 -0
- package/.aether/utils/event-bus.sh +301 -0
- package/.aether/utils/graph.sh +559 -0
- package/.aether/utils/instinct-store.sh +401 -0
- package/.aether/utils/learning.sh +79 -7
- package/.aether/utils/oracle/oracle-stop-hook.sh +896 -0
- package/.aether/utils/session.sh +13 -0
- package/.aether/utils/state-api.sh +1 -1
- package/.aether/utils/trust-scoring.sh +347 -0
- package/.aether/utils/worktree.sh +97 -0
- package/.claude/commands/ant/archaeology.md +2 -2
- package/.claude/commands/ant/chaos.md +4 -4
- package/.claude/commands/ant/colonize.md +9 -9
- package/.claude/commands/ant/council.md +6 -6
- package/.claude/commands/ant/data-clean.md +3 -3
- package/.claude/commands/ant/dream.md +2 -2
- package/.claude/commands/ant/entomb.md +9 -9
- package/.claude/commands/ant/export-signals.md +2 -2
- package/.claude/commands/ant/feedback.md +4 -4
- package/.claude/commands/ant/flag.md +2 -2
- package/.claude/commands/ant/flags.md +4 -4
- package/.claude/commands/ant/focus.md +4 -4
- package/.claude/commands/ant/help.md +1 -1
- package/.claude/commands/ant/history.md +1 -1
- package/.claude/commands/ant/import-signals.md +2 -2
- package/.claude/commands/ant/init.md +44 -27
- package/.claude/commands/ant/insert-phase.md +1 -1
- package/.claude/commands/ant/interpret.md +2 -2
- package/.claude/commands/ant/lay-eggs.md +2 -2
- package/.claude/commands/ant/maturity.md +2 -2
- package/.claude/commands/ant/memory-details.md +1 -1
- package/.claude/commands/ant/migrate-state.md +1 -1
- package/.claude/commands/ant/oracle.md +78 -42
- package/.claude/commands/ant/organize.md +3 -3
- package/.claude/commands/ant/patrol.md +8 -8
- package/.claude/commands/ant/pause-colony.md +5 -5
- package/.claude/commands/ant/phase.md +1 -1
- package/.claude/commands/ant/pheromones.md +1 -1
- package/.claude/commands/ant/plan.md +8 -8
- package/.claude/commands/ant/quick.md +4 -4
- package/.claude/commands/ant/redirect.md +4 -4
- package/.claude/commands/ant/resume-colony.md +5 -5
- package/.claude/commands/ant/resume.md +17 -29
- package/.claude/commands/ant/run.md +10 -10
- package/.claude/commands/ant/seal.md +25 -25
- package/.claude/commands/ant/skill-create.md +2 -2
- package/.claude/commands/ant/status.md +14 -14
- package/.claude/commands/ant/swarm.md +14 -14
- package/.claude/commands/ant/tunnels.md +4 -4
- package/.claude/commands/ant/update.md +1 -1
- package/.claude/commands/ant/verify-castes.md +2 -2
- package/.claude/commands/ant/watch.md +8 -8
- package/.opencode/commands/ant/archaeology.md +1 -1
- package/.opencode/commands/ant/build.md +80 -45
- package/.opencode/commands/ant/chaos.md +3 -3
- package/.opencode/commands/ant/colonize.md +8 -8
- package/.opencode/commands/ant/continue.md +40 -40
- package/.opencode/commands/ant/council.md +5 -5
- package/.opencode/commands/ant/data-clean.md +2 -2
- package/.opencode/commands/ant/dream.md +1 -1
- package/.opencode/commands/ant/entomb.md +3 -3
- package/.opencode/commands/ant/export-signals.md +1 -1
- package/.opencode/commands/ant/feedback.md +2 -2
- package/.opencode/commands/ant/flag.md +1 -1
- package/.opencode/commands/ant/flags.md +3 -3
- package/.opencode/commands/ant/focus.md +2 -2
- package/.opencode/commands/ant/import-signals.md +1 -1
- package/.opencode/commands/ant/init.md +44 -27
- package/.opencode/commands/ant/insert-phase.md +1 -1
- package/.opencode/commands/ant/interpret.md +1 -1
- package/.opencode/commands/ant/lay-eggs.md +2 -2
- package/.opencode/commands/ant/maturity.md +1 -1
- package/.opencode/commands/ant/memory-details.md +1 -1
- package/.opencode/commands/ant/oracle.md +69 -40
- package/.opencode/commands/ant/organize.md +2 -2
- package/.opencode/commands/ant/patrol.md +8 -8
- package/.opencode/commands/ant/pause-colony.md +2 -2
- package/.opencode/commands/ant/pheromones.md +1 -1
- package/.opencode/commands/ant/plan.md +6 -6
- package/.opencode/commands/ant/quick.md +4 -4
- package/.opencode/commands/ant/redirect.md +2 -2
- package/.opencode/commands/ant/resume-colony.md +4 -4
- package/.opencode/commands/ant/resume.md +5 -17
- package/.opencode/commands/ant/run.md +10 -10
- package/.opencode/commands/ant/seal.md +8 -8
- package/.opencode/commands/ant/skill-create.md +2 -2
- package/.opencode/commands/ant/status.md +10 -10
- package/.opencode/commands/ant/tunnels.md +3 -3
- package/.opencode/commands/ant/verify-castes.md +1 -1
- package/.opencode/commands/ant/watch.md +7 -7
- package/CHANGELOG.md +83 -0
- package/README.md +22 -9
- package/bin/cli.js +118 -3
- package/bin/lib/binary-downloader.js +267 -0
- package/bin/lib/update-transaction.js +27 -3
- package/bin/lib/version-gate.js +179 -0
- package/bin/npx-entry.js +0 -0
- package/package.json +1 -1
- package/.aether/agents/aether-ambassador.md +0 -140
- package/.aether/agents/aether-archaeologist.md +0 -108
- package/.aether/agents/aether-architect.md +0 -133
- package/.aether/agents/aether-auditor.md +0 -144
- package/.aether/agents/aether-builder.md +0 -184
- package/.aether/agents/aether-chaos.md +0 -115
- package/.aether/agents/aether-chronicler.md +0 -122
- package/.aether/agents/aether-gatekeeper.md +0 -116
- package/.aether/agents/aether-includer.md +0 -117
- package/.aether/agents/aether-keeper.md +0 -177
- package/.aether/agents/aether-measurer.md +0 -128
- package/.aether/agents/aether-oracle.md +0 -137
- package/.aether/agents/aether-probe.md +0 -133
- package/.aether/agents/aether-queen.md +0 -286
- package/.aether/agents/aether-route-setter.md +0 -130
- package/.aether/agents/aether-sage.md +0 -106
- package/.aether/agents/aether-scout.md +0 -101
- package/.aether/agents/aether-surveyor-disciplines.md +0 -391
- package/.aether/agents/aether-surveyor-nest.md +0 -329
- package/.aether/agents/aether-surveyor-pathogens.md +0 -264
- package/.aether/agents/aether-surveyor-provisions.md +0 -334
- package/.aether/agents/aether-tracker.md +0 -137
- package/.aether/agents/aether-watcher.md +0 -174
- package/.aether/agents/aether-weaver.md +0 -130
- package/.aether/commands/claude/archaeology.md +0 -334
- package/.aether/commands/claude/build.md +0 -65
- package/.aether/commands/claude/chaos.md +0 -336
- package/.aether/commands/claude/colonize.md +0 -259
- package/.aether/commands/claude/continue.md +0 -60
- package/.aether/commands/claude/council.md +0 -507
- package/.aether/commands/claude/data-clean.md +0 -81
- package/.aether/commands/claude/dream.md +0 -268
- package/.aether/commands/claude/entomb.md +0 -498
- package/.aether/commands/claude/export-signals.md +0 -57
- package/.aether/commands/claude/feedback.md +0 -96
- package/.aether/commands/claude/flag.md +0 -151
- package/.aether/commands/claude/flags.md +0 -169
- package/.aether/commands/claude/focus.md +0 -76
- package/.aether/commands/claude/help.md +0 -154
- package/.aether/commands/claude/history.md +0 -140
- package/.aether/commands/claude/import-signals.md +0 -71
- package/.aether/commands/claude/init.md +0 -505
- package/.aether/commands/claude/insert-phase.md +0 -105
- package/.aether/commands/claude/interpret.md +0 -278
- package/.aether/commands/claude/lay-eggs.md +0 -210
- package/.aether/commands/claude/maturity.md +0 -113
- package/.aether/commands/claude/memory-details.md +0 -77
- package/.aether/commands/claude/migrate-state.md +0 -171
- package/.aether/commands/claude/oracle.md +0 -642
- package/.aether/commands/claude/organize.md +0 -232
- package/.aether/commands/claude/patrol.md +0 -620
- package/.aether/commands/claude/pause-colony.md +0 -233
- package/.aether/commands/claude/phase.md +0 -115
- package/.aether/commands/claude/pheromones.md +0 -156
- package/.aether/commands/claude/plan.md +0 -693
- package/.aether/commands/claude/preferences.md +0 -65
- package/.aether/commands/claude/quick.md +0 -100
- package/.aether/commands/claude/redirect.md +0 -76
- package/.aether/commands/claude/resume-colony.md +0 -197
- package/.aether/commands/claude/resume.md +0 -388
- package/.aether/commands/claude/run.md +0 -231
- package/.aether/commands/claude/seal.md +0 -774
- package/.aether/commands/claude/skill-create.md +0 -286
- package/.aether/commands/claude/status.md +0 -410
- package/.aether/commands/claude/swarm.md +0 -349
- package/.aether/commands/claude/tunnels.md +0 -426
- package/.aether/commands/claude/update.md +0 -132
- package/.aether/commands/claude/verify-castes.md +0 -143
- package/.aether/commands/claude/watch.md +0 -239
- package/.aether/commands/opencode/archaeology.md +0 -331
- package/.aether/commands/opencode/build.md +0 -1168
- package/.aether/commands/opencode/chaos.md +0 -329
- package/.aether/commands/opencode/colonize.md +0 -195
- package/.aether/commands/opencode/continue.md +0 -1436
- package/.aether/commands/opencode/council.md +0 -437
- package/.aether/commands/opencode/data-clean.md +0 -77
- package/.aether/commands/opencode/dream.md +0 -260
- package/.aether/commands/opencode/entomb.md +0 -377
- package/.aether/commands/opencode/export-signals.md +0 -54
- package/.aether/commands/opencode/feedback.md +0 -99
- package/.aether/commands/opencode/flag.md +0 -149
- package/.aether/commands/opencode/flags.md +0 -167
- package/.aether/commands/opencode/focus.md +0 -73
- package/.aether/commands/opencode/help.md +0 -157
- package/.aether/commands/opencode/history.md +0 -136
- package/.aether/commands/opencode/import-signals.md +0 -68
- package/.aether/commands/opencode/init.md +0 -518
- package/.aether/commands/opencode/insert-phase.md +0 -111
- package/.aether/commands/opencode/interpret.md +0 -272
- package/.aether/commands/opencode/lay-eggs.md +0 -213
- package/.aether/commands/opencode/maturity.md +0 -108
- package/.aether/commands/opencode/memory-details.md +0 -83
- package/.aether/commands/opencode/migrate-state.md +0 -165
- package/.aether/commands/opencode/oracle.md +0 -593
- package/.aether/commands/opencode/organize.md +0 -226
- package/.aether/commands/opencode/patrol.md +0 -626
- package/.aether/commands/opencode/pause-colony.md +0 -203
- package/.aether/commands/opencode/phase.md +0 -113
- package/.aether/commands/opencode/pheromones.md +0 -162
- package/.aether/commands/opencode/plan.md +0 -684
- package/.aether/commands/opencode/preferences.md +0 -71
- package/.aether/commands/opencode/quick.md +0 -91
- package/.aether/commands/opencode/redirect.md +0 -84
- package/.aether/commands/opencode/resume-colony.md +0 -190
- package/.aether/commands/opencode/resume.md +0 -394
- package/.aether/commands/opencode/run.md +0 -237
- package/.aether/commands/opencode/seal.md +0 -452
- package/.aether/commands/opencode/skill-create.md +0 -63
- package/.aether/commands/opencode/status.md +0 -307
- package/.aether/commands/opencode/swarm.md +0 -15
- package/.aether/commands/opencode/tunnels.md +0 -400
- package/.aether/commands/opencode/update.md +0 -127
- package/.aether/commands/opencode/verify-castes.md +0 -139
- package/.aether/commands/opencode/watch.md +0 -227
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
7
|
const { program } = require('commander');
|
|
8
8
|
|
|
9
9
|
// Error handling imports
|
|
@@ -1204,6 +1204,71 @@ function setupHub() {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* Refresh the Go binary during update flow.
|
|
1209
|
+
* Downloads if missing or outdated. Non-blocking — failures are logged, never thrown.
|
|
1210
|
+
*
|
|
1211
|
+
* @param {string} version - Target version string
|
|
1212
|
+
* @param {object} [options] - Options
|
|
1213
|
+
* @param {boolean} [options.quiet=false] - Suppress output
|
|
1214
|
+
* @returns {Promise<{refreshed: boolean, reason?: string}>} Result
|
|
1215
|
+
*/
|
|
1216
|
+
async function refreshBinary(version, options = {}) {
|
|
1217
|
+
const { quiet = false } = options;
|
|
1218
|
+
|
|
1219
|
+
// Binary refresh is non-blocking — update always completes
|
|
1220
|
+
try {
|
|
1221
|
+
const binaryPath = path.join(HOME, '.aether', 'bin',
|
|
1222
|
+
process.platform === 'win32' ? 'aether.exe' : 'aether');
|
|
1223
|
+
|
|
1224
|
+
// Check if binary exists
|
|
1225
|
+
if (!fs.existsSync(binaryPath)) {
|
|
1226
|
+
if (!quiet) log(` ${c.colony('Binary:')} missing, downloading v${version}...`);
|
|
1227
|
+
const { downloadBinary } = require('./lib/binary-downloader');
|
|
1228
|
+
const result = await downloadBinary(version);
|
|
1229
|
+
if (result.success) {
|
|
1230
|
+
if (!quiet) log(` ${c.success('Binary:')} installed v${version} -> ${c.dim(result.path)}`);
|
|
1231
|
+
return { refreshed: true };
|
|
1232
|
+
} else {
|
|
1233
|
+
if (!quiet) log(` ${c.warning('Binary:')} download skipped (${result.reason})`);
|
|
1234
|
+
return { refreshed: false, reason: result.reason };
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Binary exists — check version
|
|
1239
|
+
let installedVersion = null;
|
|
1240
|
+
try {
|
|
1241
|
+
const { execFileSync } = require('child_process');
|
|
1242
|
+
const output = execFileSync(binaryPath, ['version'], { encoding: 'utf8', timeout: 5000 });
|
|
1243
|
+
// Parse version from output like "aether v5.3.3" or just "v5.3.3"
|
|
1244
|
+
const match = output.match(/v?(\d+\.\d+\.\d+)/);
|
|
1245
|
+
if (match) installedVersion = match[1];
|
|
1246
|
+
} catch {
|
|
1247
|
+
// Can't determine version — download fresh
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (!installedVersion || installedVersion !== version) {
|
|
1251
|
+
if (!quiet) log(` ${c.colony('Binary:')} updating ${installedVersion || 'unknown'} -> v${version}...`);
|
|
1252
|
+
const { downloadBinary } = require('./lib/binary-downloader');
|
|
1253
|
+
const result = await downloadBinary(version);
|
|
1254
|
+
if (result.success) {
|
|
1255
|
+
if (!quiet) log(` ${c.success('Binary:')} updated to v${version}`);
|
|
1256
|
+
return { refreshed: true };
|
|
1257
|
+
} else {
|
|
1258
|
+
if (!quiet) log(` ${c.warning('Binary:')} update skipped (${result.reason})`);
|
|
1259
|
+
return { refreshed: false, reason: result.reason };
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Binary is up to date
|
|
1264
|
+
if (!quiet) log(` ${c.dim(`Binary: v${version} (up to date)`)}`);
|
|
1265
|
+
return { refreshed: false, reason: 'up to date' };
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
if (!quiet) log(` ${c.warning('Binary:')} refresh failed (${err.message})`);
|
|
1268
|
+
return { refreshed: false, reason: err.message };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1207
1272
|
async function updateRepo(repoPath, sourceVersion, opts) {
|
|
1208
1273
|
opts = opts || {};
|
|
1209
1274
|
const dryRun = opts.dryRun || false;
|
|
@@ -1276,6 +1341,8 @@ async function updateRepo(repoPath, sourceVersion, opts) {
|
|
|
1276
1341
|
removed: systemRemoved + commandsRemoved + agentsRemoved + rulesRemoved + agentsClaudeRemoved,
|
|
1277
1342
|
removedFiles: allRemovedFiles,
|
|
1278
1343
|
stashCreated: !!transaction.checkpoint?.stashRef,
|
|
1344
|
+
stashRestored: result.stash_restored || false,
|
|
1345
|
+
stashConflict: result.stash_conflict || false,
|
|
1279
1346
|
checkpoint_id: result.checkpoint_id,
|
|
1280
1347
|
cleanup: cleanupResult,
|
|
1281
1348
|
};
|
|
@@ -1381,6 +1448,19 @@ async function performGlobalInstall() {
|
|
|
1381
1448
|
log(c.colony('Setting up distribution hub...'));
|
|
1382
1449
|
setupHub();
|
|
1383
1450
|
|
|
1451
|
+
// Download Go binary (non-blocking)
|
|
1452
|
+
try {
|
|
1453
|
+
const { downloadBinary } = require('./lib/binary-downloader');
|
|
1454
|
+
const result = await downloadBinary(VERSION);
|
|
1455
|
+
if (result.success) {
|
|
1456
|
+
log(` ${c.colony('Binary:')} ${c.dim(result.path)}`);
|
|
1457
|
+
} else {
|
|
1458
|
+
log(` ${c.warning('Binary:')} skipped (${result.reason})`);
|
|
1459
|
+
}
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
log(` ${c.warning('Binary:')} download failed (${err.message})`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1384
1464
|
log('');
|
|
1385
1465
|
log(c.success('Install complete.'));
|
|
1386
1466
|
log(` ${c.queen('Claude Code:')} run /ant to get started`);
|
|
@@ -1535,7 +1615,13 @@ program
|
|
|
1535
1615
|
log(` Distribution chain: ${c.success('\u2713')} clean`);
|
|
1536
1616
|
}
|
|
1537
1617
|
if (result.stashCreated) {
|
|
1538
|
-
|
|
1618
|
+
if (result.stashRestored) {
|
|
1619
|
+
log(` Local changes restored automatically`);
|
|
1620
|
+
} else if (result.stashConflict) {
|
|
1621
|
+
log(` ${c.warning('Stash restore had conflicts')} — resolve manually: cd ${repo.path} && git stash pop`);
|
|
1622
|
+
} else {
|
|
1623
|
+
log(` Stash created. Recover with: cd ${repo.path} && git stash pop`);
|
|
1624
|
+
}
|
|
1539
1625
|
}
|
|
1540
1626
|
updated++;
|
|
1541
1627
|
} else {
|
|
@@ -1556,6 +1642,11 @@ program
|
|
|
1556
1642
|
writeJsonSync(HUB_REGISTRY, registry);
|
|
1557
1643
|
}
|
|
1558
1644
|
|
|
1645
|
+
// Binary refresh is non-blocking — update always completes
|
|
1646
|
+
if (!dryRun) {
|
|
1647
|
+
await refreshBinary(sourceVersion, { quiet: false });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1559
1650
|
const label = dryRun ? 'would update' : 'updated';
|
|
1560
1651
|
let summary = `\nSummary: ${updated} ${label}, ${upToDate} up to date, ${pruned} pruned`;
|
|
1561
1652
|
if (dirty > 0) summary += `, ${dirty} dirty (skipped)`;
|
|
@@ -1639,12 +1730,22 @@ program
|
|
|
1639
1730
|
if (result.cleanup && result.cleanup.cleaned.length === 0 && result.cleanup.failed.length === 0) {
|
|
1640
1731
|
console.log(` Distribution chain: ${c.success('\u2713')} clean`);
|
|
1641
1732
|
}
|
|
1642
|
-
if (result.
|
|
1733
|
+
if (result.stashRestored) {
|
|
1734
|
+
console.log(` ${c.success('\u2713')} Local changes restored`);
|
|
1735
|
+
} else if (result.stashConflict) {
|
|
1736
|
+
console.log(` ${c.warning('\u26A0')} Stash restore had conflicts — resolve manually: git stash pop`);
|
|
1737
|
+
} else if (result.stashCreated) {
|
|
1643
1738
|
console.log(' Git stash created. Recover with: git stash pop');
|
|
1644
1739
|
}
|
|
1645
1740
|
if (result.checkpoint_id) {
|
|
1646
1741
|
console.log(` Checkpoint: ${result.checkpoint_id}`);
|
|
1647
1742
|
}
|
|
1743
|
+
|
|
1744
|
+
// Binary refresh is non-blocking — update always completes
|
|
1745
|
+
if (!dryRun) {
|
|
1746
|
+
await refreshBinary(sourceVersion);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1648
1749
|
console.log(' Colony data (.aether/data/) untouched.');
|
|
1649
1750
|
} catch (error) {
|
|
1650
1751
|
// Handle UpdateError with prominent recovery commands (UPDATE-04)
|
|
@@ -2201,6 +2302,7 @@ module.exports = {
|
|
|
2201
2302
|
loadCheckpointMetadata,
|
|
2202
2303
|
saveCheckpointMetadata,
|
|
2203
2304
|
isUserData,
|
|
2305
|
+
refreshBinary,
|
|
2204
2306
|
syncDirWithCleanup,
|
|
2205
2307
|
syncSkillsToHub,
|
|
2206
2308
|
listFilesRecursive,
|
|
@@ -2219,5 +2321,18 @@ function run() {
|
|
|
2219
2321
|
|
|
2220
2322
|
// Parse command line arguments only when run directly (not when required as a module)
|
|
2221
2323
|
if (require.main === module) {
|
|
2324
|
+
// Delegation shim: if Go binary is available and version matches, pass through to it.
|
|
2325
|
+
// Commands that must run in Node.js (install, update, setup) are excluded.
|
|
2326
|
+
const { shouldDelegate, getBinaryPath } = require('./lib/version-gate');
|
|
2327
|
+
if (shouldDelegate(process.argv)) {
|
|
2328
|
+
const { spawnSync } = require('child_process');
|
|
2329
|
+
const binaryPath = getBinaryPath();
|
|
2330
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
2331
|
+
stdio: 'inherit',
|
|
2332
|
+
env: process.env,
|
|
2333
|
+
});
|
|
2334
|
+
process.exit(result.status);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2222
2337
|
run();
|
|
2223
2338
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Binary Downloader for Aether Colony
|
|
4
|
+
*
|
|
5
|
+
* Downloads the correct platform Go binary from GitHub Releases during
|
|
6
|
+
* `npm install -g aether-colony`. Verifies SHA-256 checksum before
|
|
7
|
+
* atomic install to ~/.aether/bin/aether.
|
|
8
|
+
*
|
|
9
|
+
* Uses ONLY Node.js built-in modules. No external dependencies.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const fsPromises = require('fs/promises');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execFile } = require('child_process');
|
|
20
|
+
const { pipeline } = require('stream/promises');
|
|
21
|
+
const { promisify } = require('util');
|
|
22
|
+
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
|
|
25
|
+
// Platform detection maps process.platform + process.arch to goreleaser naming
|
|
26
|
+
const PLATFORM_MAP = {
|
|
27
|
+
darwin: 'darwin',
|
|
28
|
+
linux: 'linux',
|
|
29
|
+
win32: 'windows',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ARCH_MAP = {
|
|
33
|
+
x64: 'amd64',
|
|
34
|
+
arm64: 'arm64',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect platform and architecture, mapped to goreleaser naming.
|
|
39
|
+
* @returns {{os: string, arch: string}|null} Platform info or null if unsupported
|
|
40
|
+
*/
|
|
41
|
+
function getPlatformArch() {
|
|
42
|
+
const goos = PLATFORM_MAP[process.platform];
|
|
43
|
+
const goarch = ARCH_MAP[process.arch];
|
|
44
|
+
if (!goos || !goarch) return null;
|
|
45
|
+
return { os: goos, arch: goarch };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Download a URL following HTTP redirects (GitHub releases always 302).
|
|
50
|
+
* Node.js https.get() does NOT follow redirects automatically.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} url - URL to download
|
|
53
|
+
* @param {number} maxRedirects - Maximum redirect hops (default 5)
|
|
54
|
+
* @returns {Promise<import('http').IncomingMessage>} Response stream on 200
|
|
55
|
+
*/
|
|
56
|
+
function downloadWithRedirects(url, maxRedirects = 5) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
function attempt(currentUrl, redirectsLeft) {
|
|
59
|
+
const client = currentUrl.startsWith('https') ? https : http;
|
|
60
|
+
client.get(currentUrl, (res) => {
|
|
61
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
62
|
+
if (redirectsLeft <= 0) {
|
|
63
|
+
res.resume();
|
|
64
|
+
return reject(new Error('Too many redirects'));
|
|
65
|
+
}
|
|
66
|
+
res.resume(); // Drain response body before next request
|
|
67
|
+
return attempt(res.headers.location, redirectsLeft - 1);
|
|
68
|
+
}
|
|
69
|
+
if (res.statusCode !== 200) {
|
|
70
|
+
res.resume();
|
|
71
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
72
|
+
}
|
|
73
|
+
resolve(res);
|
|
74
|
+
}).on('error', reject);
|
|
75
|
+
}
|
|
76
|
+
attempt(url, maxRedirects);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Download a URL and return the full response body as a string.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} url - URL to download
|
|
84
|
+
* @returns {Promise<string>} Response body text
|
|
85
|
+
*/
|
|
86
|
+
async function downloadText(url) {
|
|
87
|
+
const response = await downloadWithRedirects(url);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
let data = '';
|
|
90
|
+
response.on('data', (chunk) => { data += chunk; });
|
|
91
|
+
response.on('end', () => resolve(data));
|
|
92
|
+
response.on('error', reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse goreleaser checksums.txt to find the hash for a specific filename.
|
|
98
|
+
* Format: <sha256_hex> <filename> (two-space separator)
|
|
99
|
+
*
|
|
100
|
+
* @param {string} checksumsContent - Full checksums.txt content
|
|
101
|
+
* @param {string} filename - Archive filename to find
|
|
102
|
+
* @returns {string|null} SHA-256 hex digest or null if not found
|
|
103
|
+
*/
|
|
104
|
+
function findChecksum(checksumsContent, filename) {
|
|
105
|
+
const lines = checksumsContent.split('\n');
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const parts = line.split(' ');
|
|
108
|
+
if (parts.length >= 2 && parts[1] === filename) {
|
|
109
|
+
return parts[0];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Download an archive to a temp file while computing SHA-256 hash.
|
|
117
|
+
* Hashing happens during the stream -- no extra I/O pass.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} url - Archive URL
|
|
120
|
+
* @param {string} tmpPath - Temp file path for the download
|
|
121
|
+
* @returns {Promise<{hash: string, tmpPath: string}>} Computed hash and temp file path
|
|
122
|
+
*/
|
|
123
|
+
async function downloadAndHash(url, tmpPath) {
|
|
124
|
+
const hash = crypto.createHash('sha256');
|
|
125
|
+
const fileStream = fs.createWriteStream(tmpPath);
|
|
126
|
+
|
|
127
|
+
const response = await downloadWithRedirects(url);
|
|
128
|
+
|
|
129
|
+
// Hash while streaming -- zero extra I/O
|
|
130
|
+
response.on('data', (chunk) => hash.update(chunk));
|
|
131
|
+
|
|
132
|
+
await pipeline(response, fileStream);
|
|
133
|
+
|
|
134
|
+
return { hash: hash.digest('hex'), tmpPath };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extract binary from archive using system tar command.
|
|
139
|
+
* tar is available on macOS, Linux, and Windows 10+.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} archivePath - Path to the archive file
|
|
142
|
+
* @param {string} destDir - Directory to extract into
|
|
143
|
+
* @param {string} goos - Goreleaser OS name (darwin, linux, windows)
|
|
144
|
+
* @returns {Promise<string>} Path to the extracted binary
|
|
145
|
+
*/
|
|
146
|
+
async function extractBinary(archivePath, destDir, goos) {
|
|
147
|
+
const binaryName = goos === 'windows' ? 'aether.exe' : 'aether';
|
|
148
|
+
|
|
149
|
+
await fsPromises.mkdir(destDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
await execFileAsync('tar', [
|
|
152
|
+
'-xf', archivePath,
|
|
153
|
+
'-C', destDir,
|
|
154
|
+
'--strip-components', '1',
|
|
155
|
+
binaryName,
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
return path.join(destDir, binaryName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Atomically install a binary by renaming from temp to final path.
|
|
163
|
+
* fs.rename() is atomic on both POSIX and Windows (Node 14+).
|
|
164
|
+
*
|
|
165
|
+
* @param {string} extractedBinary - Path to the extracted binary
|
|
166
|
+
* @param {string} targetPath - Final install path
|
|
167
|
+
*/
|
|
168
|
+
async function atomicInstall(extractedBinary, targetPath) {
|
|
169
|
+
await fsPromises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
170
|
+
await fsPromises.rename(extractedBinary, targetPath);
|
|
171
|
+
// Set executable permission on Unix
|
|
172
|
+
if (process.platform !== 'win32') {
|
|
173
|
+
await fsPromises.chmod(targetPath, 0o755);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Download and install the correct platform Go binary from GitHub Releases.
|
|
179
|
+
*
|
|
180
|
+
* This function NEVER throws. On any error it returns {success: false, reason: string}.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} version - Version string (e.g. "5.3.3")
|
|
183
|
+
* @param {object} [options] - Download options
|
|
184
|
+
* @param {boolean} [options.quiet=false] - Suppress progress output
|
|
185
|
+
* @param {number} [options.timeout=30000] - Download timeout in milliseconds
|
|
186
|
+
* @returns {Promise<{success: boolean, reason?: string, path?: string}>} Result
|
|
187
|
+
*/
|
|
188
|
+
async function downloadBinary(version, options = {}) {
|
|
189
|
+
const { quiet = false, timeout = 30000 } = options;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// 1. Platform detection
|
|
193
|
+
const platform = getPlatformArch();
|
|
194
|
+
if (!platform) {
|
|
195
|
+
return { success: false, reason: `Unsupported platform: ${process.platform}/${process.arch}` };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Construct URLs
|
|
199
|
+
const archiveExt = platform.os === 'windows' ? '.zip' : '.tar.gz';
|
|
200
|
+
const archiveFilename = `aether_${version}_${platform.os}_${platform.arch}${archiveExt}`;
|
|
201
|
+
const baseUrl = `https://github.com/calcosmic/Aether/releases/download/v${version}`;
|
|
202
|
+
const archiveUrl = `${baseUrl}/${archiveFilename}`;
|
|
203
|
+
const checksumsUrl = `${baseUrl}/checksums.txt`;
|
|
204
|
+
|
|
205
|
+
// 3. Download checksums.txt
|
|
206
|
+
let checksumsContent;
|
|
207
|
+
try {
|
|
208
|
+
checksumsContent = await downloadText(checksumsUrl);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { success: false, reason: `Failed to download checksums: ${err.message}` };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 4. Parse expected hash
|
|
214
|
+
const expectedHash = findChecksum(checksumsContent, archiveFilename);
|
|
215
|
+
|
|
216
|
+
// 5. Download archive and compute hash
|
|
217
|
+
const tmpArchive = path.join(os.tmpdir(), `aether-download-${Date.now()}.tmp`);
|
|
218
|
+
|
|
219
|
+
// Race download against timeout, clearing timer on success
|
|
220
|
+
let timeoutId;
|
|
221
|
+
const downloadPromise = downloadAndHash(archiveUrl, tmpArchive);
|
|
222
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
223
|
+
timeoutId = setTimeout(() => reject(new Error('Download timed out')), timeout);
|
|
224
|
+
});
|
|
225
|
+
const downloadResult = await Promise.race([downloadPromise, timeoutPromise])
|
|
226
|
+
.finally(() => clearTimeout(timeoutId));
|
|
227
|
+
|
|
228
|
+
const { hash: actualHash } = downloadResult;
|
|
229
|
+
|
|
230
|
+
// 6. Verify checksum
|
|
231
|
+
if (expectedHash && actualHash !== expectedHash) {
|
|
232
|
+
await fsPromises.unlink(tmpArchive).catch(() => {});
|
|
233
|
+
return { success: false, reason: `Checksum mismatch for ${archiveFilename}` };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 7. Extract binary from archive
|
|
237
|
+
const tmpBinaryDir = path.join(os.tmpdir(), `aether-extract-${Date.now()}`);
|
|
238
|
+
const extractedBinary = await extractBinary(tmpArchive, tmpBinaryDir, platform.os);
|
|
239
|
+
|
|
240
|
+
// 8. Atomic install
|
|
241
|
+
const targetPath = path.join(
|
|
242
|
+
os.homedir(), '.aether', 'bin',
|
|
243
|
+
process.platform === 'win32' ? 'aether.exe' : 'aether'
|
|
244
|
+
);
|
|
245
|
+
await atomicInstall(extractedBinary, targetPath);
|
|
246
|
+
|
|
247
|
+
// 9. Cleanup temp files (best effort)
|
|
248
|
+
await fsPromises.unlink(tmpArchive).catch(() => {});
|
|
249
|
+
await fsPromises.rm(tmpBinaryDir, { recursive: true }).catch(() => {});
|
|
250
|
+
|
|
251
|
+
return { success: true, path: targetPath };
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return { success: false, reason: err.message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
downloadBinary,
|
|
259
|
+
getPlatformArch,
|
|
260
|
+
// Internal helpers exported for testing (prefixed with _)
|
|
261
|
+
_findChecksum: findChecksum,
|
|
262
|
+
_downloadWithRedirects: downloadWithRedirects,
|
|
263
|
+
_downloadText: downloadText,
|
|
264
|
+
_downloadAndHash: downloadAndHash,
|
|
265
|
+
_extractBinary: extractBinary,
|
|
266
|
+
_atomicInstall: atomicInstall,
|
|
267
|
+
};
|
|
@@ -172,8 +172,9 @@ class UpdateTransaction {
|
|
|
172
172
|
|
|
173
173
|
// Directories to exclude from sync (user data, local state, and separately-synced dirs)
|
|
174
174
|
// v4.0: archive and chambers added — these are private and must not sync to target repos
|
|
175
|
-
// v6.0: oracle, midden
|
|
176
|
-
|
|
175
|
+
// v6.0: oracle, midden added for complete user data protection
|
|
176
|
+
// v7.0: exchange removed from exclusion — .sh scripts must distribute; .xml/.json data excluded by file extension
|
|
177
|
+
this.EXCLUDE_DIRS = ['data', 'dreams', 'oracle', 'midden', 'checkpoints', 'locks', 'temp', 'agents', 'commands', 'rules', 'archive', 'chambers'];
|
|
177
178
|
|
|
178
179
|
// Files to exclude from sync (user wisdom, protected files)
|
|
179
180
|
this.EXCLUDE_FILES = ['QUEEN.md'];
|
|
@@ -192,7 +193,6 @@ class UpdateTransaction {
|
|
|
192
193
|
'temp',
|
|
193
194
|
'archive',
|
|
194
195
|
'chambers',
|
|
195
|
-
'exchange',
|
|
196
196
|
]);
|
|
197
197
|
this.managedPrefixes = [
|
|
198
198
|
'.claude/commands/ant',
|
|
@@ -792,6 +792,11 @@ class UpdateTransaction {
|
|
|
792
792
|
if (this.EXCLUDE_FILES.includes(basename)) {
|
|
793
793
|
return true;
|
|
794
794
|
}
|
|
795
|
+
// Protect exchange data files — only .sh scripts should distribute
|
|
796
|
+
// Only filter files (not directories), so the exchange dir itself is traversed
|
|
797
|
+
if (parts.includes('exchange') && !basename.endsWith('.sh') && !relPath.endsWith('exchange')) {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
795
800
|
return false;
|
|
796
801
|
}
|
|
797
802
|
|
|
@@ -1664,6 +1669,23 @@ class UpdateTransaction {
|
|
|
1664
1669
|
(this.syncResult?.agents?.removed?.length || 0) +
|
|
1665
1670
|
(this.syncResult?.rules?.removed?.length || 0);
|
|
1666
1671
|
|
|
1672
|
+
// Restore stashed files on success
|
|
1673
|
+
let stashRestored = false;
|
|
1674
|
+
let stashConflict = false;
|
|
1675
|
+
if (!dryRun && this.checkpoint?.stashRef) {
|
|
1676
|
+
try {
|
|
1677
|
+
execSync(`git stash pop ${this.checkpoint.stashRef}`, {
|
|
1678
|
+
cwd: this.repoPath,
|
|
1679
|
+
stdio: 'pipe',
|
|
1680
|
+
});
|
|
1681
|
+
stashRestored = true;
|
|
1682
|
+
this.log(` Restored stash ${this.checkpoint.stashRef}`);
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
stashConflict = true;
|
|
1685
|
+
this.log(` Warning: stash pop had conflicts — manual merge needed`);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1667
1689
|
return {
|
|
1668
1690
|
success: true,
|
|
1669
1691
|
status: dryRun ? 'dry-run' : 'updated',
|
|
@@ -1672,6 +1694,8 @@ class UpdateTransaction {
|
|
|
1672
1694
|
files_removed: filesRemoved,
|
|
1673
1695
|
sync_result: this.syncResult,
|
|
1674
1696
|
cleanup_result: this.cleanupResult || { cleaned: [], failed: [] },
|
|
1697
|
+
stash_restored: stashRestored,
|
|
1698
|
+
stash_conflict: stashConflict,
|
|
1675
1699
|
};
|
|
1676
1700
|
|
|
1677
1701
|
} catch (error) {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version Gate Module
|
|
5
|
+
*
|
|
6
|
+
* Checks whether the Go binary at ~/.aether/bin/aether exists, is executable,
|
|
7
|
+
* and reports a version matching the npm package version. Provides delegation
|
|
8
|
+
* logic so that `aether` commands route to the Go binary when the gate passes
|
|
9
|
+
* and fall back to the Node.js CLI when it does not.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: GATE-01, GATE-02, SHM-01, SHM-02
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
19
|
+
const BINARY_PATH = HOME ? path.join(HOME, '.aether', 'bin', 'aether') : null;
|
|
20
|
+
|
|
21
|
+
// Commands that must always run in Node.js regardless of binary availability (SHM-02)
|
|
22
|
+
const NODE_ONLY_COMMANDS = ['install', 'update', 'setup', 'setup-hub'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compare two semver version strings.
|
|
26
|
+
* Handles optional 'v' prefix and compares major.minor.patch numerically.
|
|
27
|
+
* Pre-release tags are ignored for comparison purposes (treated as the base version).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} a - First version string
|
|
30
|
+
* @param {string} b - Second version string
|
|
31
|
+
* @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
|
|
32
|
+
*/
|
|
33
|
+
function compareVersions(a, b) {
|
|
34
|
+
const stripPrefix = (v) => String(v).replace(/^v/, '');
|
|
35
|
+
const parseParts = (v) => {
|
|
36
|
+
const cleaned = stripPrefix(v);
|
|
37
|
+
// Take only the numeric parts (ignore pre-release tags like -alpha.1)
|
|
38
|
+
const numericPart = cleaned.split('-')[0];
|
|
39
|
+
return numericPart.split('.').map((n) => {
|
|
40
|
+
const parsed = parseInt(n, 10);
|
|
41
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const aParts = parseParts(a);
|
|
46
|
+
const bParts = parseParts(b);
|
|
47
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < maxLen; i++) {
|
|
50
|
+
const aVal = aParts[i] || 0;
|
|
51
|
+
const bVal = bParts[i] || 0;
|
|
52
|
+
if (aVal < bVal) return -1;
|
|
53
|
+
if (aVal > bVal) return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the expected path to the Go binary.
|
|
61
|
+
* @returns {string|null} Absolute path to the binary, or null if HOME is unset
|
|
62
|
+
*/
|
|
63
|
+
function getBinaryPath() {
|
|
64
|
+
return BINARY_PATH;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check whether the Go binary is available and its version matches the npm package.
|
|
69
|
+
*
|
|
70
|
+
* Returns an object describing the binary state:
|
|
71
|
+
* - available: boolean - true if binary exists, is executable, and version matches
|
|
72
|
+
* - path: string - the expected binary path
|
|
73
|
+
* - version: string|null - the binary's reported version (null if unavailable)
|
|
74
|
+
* - reason: string|null - why the gate failed, if it did
|
|
75
|
+
*
|
|
76
|
+
* @param {object} [opts] - Options
|
|
77
|
+
* @param {string} [opts.binaryPath] - Override binary path (for testing)
|
|
78
|
+
* @param {string} [opts.packageVersion] - Override package version (for testing)
|
|
79
|
+
* @param {object} [opts.fs] - Override fs module (for testing)
|
|
80
|
+
* @param {object} [opts.childProcess] - Override child_process (for testing)
|
|
81
|
+
* @returns {{ available: boolean, path: string, version: string|null, reason: string|null }}
|
|
82
|
+
*/
|
|
83
|
+
function checkBinary(opts) {
|
|
84
|
+
opts = opts || {};
|
|
85
|
+
const binaryPath = opts.binaryPath || BINARY_PATH;
|
|
86
|
+
const pkgVersion = opts.packageVersion || require('../../package.json').version;
|
|
87
|
+
const fsMod = opts.fs || fs;
|
|
88
|
+
const cp = opts.childProcess || { execSync };
|
|
89
|
+
|
|
90
|
+
if (!binaryPath) {
|
|
91
|
+
return { available: false, path: binaryPath, version: null, reason: 'HOME not set' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check binary exists
|
|
95
|
+
if (!fsMod.existsSync(binaryPath)) {
|
|
96
|
+
return { available: false, path: binaryPath, version: null, reason: 'binary not found' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check executable
|
|
100
|
+
try {
|
|
101
|
+
fsMod.accessSync(binaryPath, fsMod.constants.X_OK);
|
|
102
|
+
} catch {
|
|
103
|
+
return { available: false, path: binaryPath, version: null, reason: 'binary not executable' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get binary version — call `aether version` and strip "aether v" prefix
|
|
107
|
+
let binaryVersion;
|
|
108
|
+
try {
|
|
109
|
+
const output = cp.execSync(`"${binaryPath}" version`, {
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
timeout: 5000,
|
|
112
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
113
|
+
}).trim();
|
|
114
|
+
// Strip "aether " or "aether v" prefix (e.g. "aether v5.3.3" → "5.3.3")
|
|
115
|
+
binaryVersion = output.replace(/^aether\s+v?/, '');
|
|
116
|
+
} catch {
|
|
117
|
+
return { available: false, path: binaryPath, version: null, reason: 'binary version check failed' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Compare versions (compareVersions handles v-prefix on both sides)
|
|
121
|
+
if (compareVersions(binaryVersion, pkgVersion) !== 0) {
|
|
122
|
+
return {
|
|
123
|
+
available: false,
|
|
124
|
+
path: binaryPath,
|
|
125
|
+
version: binaryVersion,
|
|
126
|
+
reason: `version mismatch: binary=${binaryVersion}, package=${pkgVersion}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { available: true, path: binaryPath, version: binaryVersion, reason: null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Determine whether the current command should be delegated to the Go binary.
|
|
135
|
+
*
|
|
136
|
+
* @param {string[]} argv - process.argv (or equivalent)
|
|
137
|
+
* @param {object} [opts] - Options (forwarded to checkBinary)
|
|
138
|
+
* @returns {boolean} True if the command should be delegated to Go
|
|
139
|
+
*/
|
|
140
|
+
function shouldDelegate(argv, opts) {
|
|
141
|
+
opts = opts || {};
|
|
142
|
+
|
|
143
|
+
// Extract command name: skip node and script path
|
|
144
|
+
// argv[0] = node, argv[1] = script path, argv[2] = command name
|
|
145
|
+
const args = argv.slice(2);
|
|
146
|
+
const command = args[0];
|
|
147
|
+
|
|
148
|
+
// No command means help/version — still delegate if possible
|
|
149
|
+
if (!command) {
|
|
150
|
+
const check = checkBinary(opts);
|
|
151
|
+
return check.available;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Strip leading dashes
|
|
155
|
+
const cleanCommand = command.replace(/^--?/, '');
|
|
156
|
+
|
|
157
|
+
// Node-only commands never delegate (SHM-02)
|
|
158
|
+
if (NODE_ONLY_COMMANDS.includes(cleanCommand)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Global flags (--help, --version, --no-color, --quiet) — delegate if binary available
|
|
163
|
+
if (command.startsWith('-')) {
|
|
164
|
+
const check = checkBinary(opts);
|
|
165
|
+
return check.available;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// All other commands: delegate if version gate passes
|
|
169
|
+
const check = checkBinary(opts);
|
|
170
|
+
return check.available;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
compareVersions,
|
|
175
|
+
checkBinary,
|
|
176
|
+
shouldDelegate,
|
|
177
|
+
getBinaryPath,
|
|
178
|
+
NODE_ONLY_COMMANDS,
|
|
179
|
+
};
|
package/bin/npx-entry.js
CHANGED
|
File without changes
|