aether-colony 1.1.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/CONTEXT.md +160 -0
- package/.aether/QUEEN.md +84 -0
- package/.aether/aether-utils.sh +7749 -0
- package/.aether/docs/QUEEN-SYSTEM.md +211 -0
- package/.aether/docs/README.md +68 -0
- package/.aether/docs/caste-system.md +48 -0
- package/.aether/docs/disciplines/DISCIPLINES.md +93 -0
- package/.aether/docs/disciplines/coding-standards.md +197 -0
- package/.aether/docs/disciplines/debugging.md +207 -0
- package/.aether/docs/disciplines/learning.md +254 -0
- package/.aether/docs/disciplines/tdd.md +257 -0
- package/.aether/docs/disciplines/verification-loop.md +167 -0
- package/.aether/docs/disciplines/verification.md +116 -0
- package/.aether/docs/error-codes.md +268 -0
- package/.aether/docs/known-issues.md +233 -0
- package/.aether/docs/pheromones.md +205 -0
- package/.aether/docs/queen-commands.md +97 -0
- package/.aether/exchange/colony-registry.xml +11 -0
- package/.aether/exchange/pheromone-xml.sh +575 -0
- package/.aether/exchange/pheromones.xml +87 -0
- package/.aether/exchange/queen-wisdom.xml +14 -0
- package/.aether/exchange/registry-xml.sh +273 -0
- package/.aether/exchange/wisdom-xml.sh +319 -0
- package/.aether/midden/approach-changes.md +5 -0
- package/.aether/midden/build-failures.md +5 -0
- package/.aether/midden/test-failures.md +5 -0
- package/.aether/model-profiles.yaml +100 -0
- package/.aether/rules/aether-colony.md +134 -0
- package/.aether/schemas/aether-types.xsd +255 -0
- package/.aether/schemas/colony-registry.xsd +309 -0
- package/.aether/schemas/example-prompt-builder.xml +234 -0
- package/.aether/schemas/pheromone.xsd +163 -0
- package/.aether/schemas/prompt.xsd +416 -0
- package/.aether/schemas/queen-wisdom.xsd +325 -0
- package/.aether/schemas/worker-priming.xsd +276 -0
- package/.aether/templates/QUEEN.md.template +79 -0
- package/.aether/templates/colony-state-reset.jq.template +22 -0
- package/.aether/templates/colony-state.template.json +35 -0
- package/.aether/templates/constraints.template.json +9 -0
- package/.aether/templates/crowned-anthill.template.md +36 -0
- package/.aether/templates/handoff-build-error.template.md +30 -0
- package/.aether/templates/handoff-build-success.template.md +39 -0
- package/.aether/templates/handoff.template.md +40 -0
- package/.aether/templates/learning-observations.template.json +6 -0
- package/.aether/templates/midden.template.json +7 -0
- package/.aether/templates/pheromones.template.json +6 -0
- package/.aether/templates/session.template.json +9 -0
- package/.aether/utils/atomic-write.sh +219 -0
- package/.aether/utils/chamber-compare.sh +193 -0
- package/.aether/utils/chamber-utils.sh +297 -0
- package/.aether/utils/colorize-log.sh +132 -0
- package/.aether/utils/error-handler.sh +212 -0
- package/.aether/utils/file-lock.sh +158 -0
- package/.aether/utils/queen-to-md.xsl +395 -0
- package/.aether/utils/semantic-cli.sh +413 -0
- package/.aether/utils/spawn-tree.sh +428 -0
- package/.aether/utils/spawn-with-model.sh +56 -0
- package/.aether/utils/state-loader.sh +215 -0
- package/.aether/utils/swarm-display.sh +268 -0
- package/.aether/utils/watch-spawn-tree.sh +253 -0
- package/.aether/utils/xml-compose.sh +253 -0
- package/.aether/utils/xml-convert.sh +273 -0
- package/.aether/utils/xml-core.sh +186 -0
- package/.aether/utils/xml-query.sh +201 -0
- package/.aether/utils/xml-utils.sh +110 -0
- package/.aether/workers.md +765 -0
- package/.claude/agents/ant/aether-ambassador.md +264 -0
- package/.claude/agents/ant/aether-archaeologist.md +322 -0
- package/.claude/agents/ant/aether-auditor.md +266 -0
- package/.claude/agents/ant/aether-builder.md +187 -0
- package/.claude/agents/ant/aether-chaos.md +268 -0
- package/.claude/agents/ant/aether-chronicler.md +304 -0
- package/.claude/agents/ant/aether-gatekeeper.md +325 -0
- package/.claude/agents/ant/aether-includer.md +373 -0
- package/.claude/agents/ant/aether-keeper.md +271 -0
- package/.claude/agents/ant/aether-measurer.md +317 -0
- package/.claude/agents/ant/aether-probe.md +210 -0
- package/.claude/agents/ant/aether-queen.md +325 -0
- package/.claude/agents/ant/aether-route-setter.md +173 -0
- package/.claude/agents/ant/aether-sage.md +353 -0
- package/.claude/agents/ant/aether-scout.md +142 -0
- package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
- package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
- package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
- package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
- package/.claude/agents/ant/aether-tracker.md +265 -0
- package/.claude/agents/ant/aether-watcher.md +244 -0
- package/.claude/agents/ant/aether-weaver.md +247 -0
- package/.claude/commands/ant/archaeology.md +341 -0
- package/.claude/commands/ant/build.md +1160 -0
- package/.claude/commands/ant/chaos.md +349 -0
- package/.claude/commands/ant/colonize.md +270 -0
- package/.claude/commands/ant/continue.md +1070 -0
- package/.claude/commands/ant/council.md +309 -0
- package/.claude/commands/ant/dream.md +265 -0
- package/.claude/commands/ant/entomb.md +487 -0
- package/.claude/commands/ant/feedback.md +78 -0
- package/.claude/commands/ant/flag.md +139 -0
- package/.claude/commands/ant/flags.md +155 -0
- package/.claude/commands/ant/focus.md +58 -0
- package/.claude/commands/ant/help.md +122 -0
- package/.claude/commands/ant/history.md +137 -0
- package/.claude/commands/ant/init.md +409 -0
- package/.claude/commands/ant/interpret.md +267 -0
- package/.claude/commands/ant/lay-eggs.md +201 -0
- package/.claude/commands/ant/maturity.md +102 -0
- package/.claude/commands/ant/memory-details.md +77 -0
- package/.claude/commands/ant/migrate-state.md +165 -0
- package/.claude/commands/ant/oracle.md +387 -0
- package/.claude/commands/ant/organize.md +227 -0
- package/.claude/commands/ant/pause-colony.md +247 -0
- package/.claude/commands/ant/phase.md +126 -0
- package/.claude/commands/ant/plan.md +544 -0
- package/.claude/commands/ant/redirect.md +58 -0
- package/.claude/commands/ant/resume-colony.md +182 -0
- package/.claude/commands/ant/resume.md +363 -0
- package/.claude/commands/ant/seal.md +306 -0
- package/.claude/commands/ant/status.md +272 -0
- package/.claude/commands/ant/swarm.md +361 -0
- package/.claude/commands/ant/tunnels.md +425 -0
- package/.claude/commands/ant/update.md +209 -0
- package/.claude/commands/ant/verify-castes.md +95 -0
- package/.claude/commands/ant/watch.md +238 -0
- package/.opencode/agents/aether-ambassador.md +140 -0
- package/.opencode/agents/aether-archaeologist.md +108 -0
- package/.opencode/agents/aether-auditor.md +144 -0
- package/.opencode/agents/aether-builder.md +184 -0
- package/.opencode/agents/aether-chaos.md +115 -0
- package/.opencode/agents/aether-chronicler.md +122 -0
- package/.opencode/agents/aether-gatekeeper.md +116 -0
- package/.opencode/agents/aether-includer.md +117 -0
- package/.opencode/agents/aether-keeper.md +177 -0
- package/.opencode/agents/aether-measurer.md +128 -0
- package/.opencode/agents/aether-probe.md +133 -0
- package/.opencode/agents/aether-queen.md +286 -0
- package/.opencode/agents/aether-route-setter.md +130 -0
- package/.opencode/agents/aether-sage.md +106 -0
- package/.opencode/agents/aether-scout.md +101 -0
- package/.opencode/agents/aether-surveyor-disciplines.md +386 -0
- package/.opencode/agents/aether-surveyor-nest.md +324 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +259 -0
- package/.opencode/agents/aether-surveyor-provisions.md +329 -0
- package/.opencode/agents/aether-tracker.md +137 -0
- package/.opencode/agents/aether-watcher.md +174 -0
- package/.opencode/agents/aether-weaver.md +130 -0
- package/.opencode/commands/ant/archaeology.md +338 -0
- package/.opencode/commands/ant/build.md +1200 -0
- package/.opencode/commands/ant/chaos.md +346 -0
- package/.opencode/commands/ant/colonize.md +202 -0
- package/.opencode/commands/ant/continue.md +938 -0
- package/.opencode/commands/ant/council.md +305 -0
- package/.opencode/commands/ant/dream.md +262 -0
- package/.opencode/commands/ant/entomb.md +367 -0
- package/.opencode/commands/ant/feedback.md +80 -0
- package/.opencode/commands/ant/flag.md +137 -0
- package/.opencode/commands/ant/flags.md +153 -0
- package/.opencode/commands/ant/focus.md +56 -0
- package/.opencode/commands/ant/help.md +124 -0
- package/.opencode/commands/ant/history.md +127 -0
- package/.opencode/commands/ant/init.md +337 -0
- package/.opencode/commands/ant/interpret.md +256 -0
- package/.opencode/commands/ant/lay-eggs.md +141 -0
- package/.opencode/commands/ant/maturity.md +92 -0
- package/.opencode/commands/ant/memory-details.md +77 -0
- package/.opencode/commands/ant/migrate-state.md +153 -0
- package/.opencode/commands/ant/oracle.md +338 -0
- package/.opencode/commands/ant/organize.md +224 -0
- package/.opencode/commands/ant/pause-colony.md +220 -0
- package/.opencode/commands/ant/phase.md +123 -0
- package/.opencode/commands/ant/plan.md +531 -0
- package/.opencode/commands/ant/redirect.md +67 -0
- package/.opencode/commands/ant/resume-colony.md +178 -0
- package/.opencode/commands/ant/resume.md +363 -0
- package/.opencode/commands/ant/seal.md +247 -0
- package/.opencode/commands/ant/status.md +272 -0
- package/.opencode/commands/ant/swarm.md +357 -0
- package/.opencode/commands/ant/tunnels.md +406 -0
- package/.opencode/commands/ant/update.md +191 -0
- package/.opencode/commands/ant/verify-castes.md +85 -0
- package/.opencode/commands/ant/watch.md +220 -0
- package/.opencode/opencode.json +3 -0
- package/CHANGELOG.md +325 -0
- package/DISCLAIMER.md +74 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/bin/cli.js +2436 -0
- package/bin/generate-commands.sh +291 -0
- package/bin/lib/caste-colors.js +57 -0
- package/bin/lib/colors.js +76 -0
- package/bin/lib/errors.js +255 -0
- package/bin/lib/event-types.js +190 -0
- package/bin/lib/file-lock.js +695 -0
- package/bin/lib/init.js +454 -0
- package/bin/lib/logger.js +242 -0
- package/bin/lib/model-profiles.js +445 -0
- package/bin/lib/model-verify.js +288 -0
- package/bin/lib/nestmate-loader.js +130 -0
- package/bin/lib/proxy-health.js +253 -0
- package/bin/lib/spawn-logger.js +266 -0
- package/bin/lib/state-guard.js +602 -0
- package/bin/lib/state-sync.js +516 -0
- package/bin/lib/telemetry.js +441 -0
- package/bin/lib/update-transaction.js +1454 -0
- package/bin/npx-install.js +178 -0
- package/bin/sync-to-runtime.sh +6 -0
- package/bin/validate-package.sh +88 -0
- package/package.json +70 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,2436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { program } = require('commander');
|
|
8
|
+
|
|
9
|
+
// Error handling imports
|
|
10
|
+
const {
|
|
11
|
+
AetherError,
|
|
12
|
+
HubError,
|
|
13
|
+
RepoError,
|
|
14
|
+
GitError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
FileSystemError,
|
|
17
|
+
ConfigurationError,
|
|
18
|
+
getExitCode,
|
|
19
|
+
wrapError,
|
|
20
|
+
} = require('./lib/errors');
|
|
21
|
+
const { logError, logActivity } = require('./lib/logger');
|
|
22
|
+
const { UpdateTransaction, UpdateError, UpdateErrorCodes } = require('./lib/update-transaction');
|
|
23
|
+
const { initializeRepo, isInitialized } = require('./lib/init');
|
|
24
|
+
const { syncStateFromPlanning, reconcileStates } = require('./lib/state-sync');
|
|
25
|
+
const { createVerificationReport } = require('./lib/model-verify');
|
|
26
|
+
const {
|
|
27
|
+
loadModelProfiles,
|
|
28
|
+
getAllAssignments,
|
|
29
|
+
getProviderForModel,
|
|
30
|
+
validateCaste,
|
|
31
|
+
validateModel,
|
|
32
|
+
setModelOverride,
|
|
33
|
+
resetModelOverride,
|
|
34
|
+
getEffectiveModel,
|
|
35
|
+
getUserOverrides,
|
|
36
|
+
getModelMetadata,
|
|
37
|
+
getProxyConfig,
|
|
38
|
+
} = require('./lib/model-profiles');
|
|
39
|
+
const {
|
|
40
|
+
checkProxyHealth,
|
|
41
|
+
verifyModelRouting,
|
|
42
|
+
formatProxyStatusColored,
|
|
43
|
+
} = require('./lib/proxy-health');
|
|
44
|
+
const { findNestmates, formatNestmates, loadNestmateTodos } = require('./lib/nestmate-loader');
|
|
45
|
+
const { logSpawn, formatSpawnTree } = require('./lib/spawn-logger');
|
|
46
|
+
const {
|
|
47
|
+
getTelemetrySummary,
|
|
48
|
+
getModelPerformance,
|
|
49
|
+
} = require('./lib/telemetry');
|
|
50
|
+
|
|
51
|
+
// Color palette
|
|
52
|
+
const c = require('./lib/colors');
|
|
53
|
+
|
|
54
|
+
const VERSION = require('../package.json').version;
|
|
55
|
+
const PACKAGE_DIR = path.resolve(__dirname, '..');
|
|
56
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
57
|
+
if (!HOME) {
|
|
58
|
+
const error = new ConfigurationError(
|
|
59
|
+
'HOME environment variable is not set',
|
|
60
|
+
{ env: Object.keys(process.env).filter(k => k.includes('HOME') || k.includes('USER')) },
|
|
61
|
+
'Please ensure HOME or USERPROFILE is defined'
|
|
62
|
+
);
|
|
63
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
64
|
+
process.exit(getExitCode(error.code));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Claude Code paths (global)
|
|
68
|
+
const COMMANDS_SRC = path.join(PACKAGE_DIR, 'commands', 'ant');
|
|
69
|
+
const COMMANDS_DEST = path.join(HOME, '.claude', 'commands', 'ant');
|
|
70
|
+
const AGENTS_DEST = path.join(HOME, '.claude', 'agents', 'ant');
|
|
71
|
+
|
|
72
|
+
// OpenCode paths (global)
|
|
73
|
+
const OPENCODE_COMMANDS_DEST = path.join(HOME, '.opencode', 'command');
|
|
74
|
+
const OPENCODE_AGENTS_DEST = path.join(HOME, '.opencode', 'agent');
|
|
75
|
+
|
|
76
|
+
// Hub paths
|
|
77
|
+
const HUB_DIR = path.join(HOME, '.aether');
|
|
78
|
+
const HUB_SYSTEM_DIR = path.join(HUB_DIR, 'system');
|
|
79
|
+
const HUB_COMMANDS_CLAUDE = path.join(HUB_SYSTEM_DIR, 'commands', 'claude');
|
|
80
|
+
const HUB_COMMANDS_OPENCODE = path.join(HUB_SYSTEM_DIR, 'commands', 'opencode');
|
|
81
|
+
const HUB_AGENTS = path.join(HUB_SYSTEM_DIR, 'agents');
|
|
82
|
+
const HUB_AGENTS_CLAUDE = path.join(HUB_SYSTEM_DIR, 'agents-claude');
|
|
83
|
+
const HUB_RULES = path.join(HUB_SYSTEM_DIR, 'rules');
|
|
84
|
+
const HUB_REGISTRY = path.join(HUB_DIR, 'registry.json');
|
|
85
|
+
const HUB_VERSION = path.join(HUB_DIR, 'version.json');
|
|
86
|
+
|
|
87
|
+
// Global quiet flag (set by --quiet option)
|
|
88
|
+
let globalQuiet = false;
|
|
89
|
+
|
|
90
|
+
// Global error handlers
|
|
91
|
+
process.on('uncaughtException', (error) => {
|
|
92
|
+
const structuredError = wrapError(error);
|
|
93
|
+
structuredError.code = 'E_UNCAUGHT_EXCEPTION';
|
|
94
|
+
structuredError.recovery = 'Please report this issue with the error details';
|
|
95
|
+
|
|
96
|
+
// Log to activity.log
|
|
97
|
+
logError(structuredError);
|
|
98
|
+
|
|
99
|
+
// Output structured JSON to stderr
|
|
100
|
+
console.error(JSON.stringify(structuredError.toJSON(), null, 2));
|
|
101
|
+
|
|
102
|
+
// Exit with appropriate code
|
|
103
|
+
process.exit(getExitCode(structuredError.code));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
107
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
108
|
+
const details = reason instanceof Error ? { stack: reason.stack, name: reason.name } : {};
|
|
109
|
+
|
|
110
|
+
const error = new AetherError(
|
|
111
|
+
'E_UNHANDLED_REJECTION',
|
|
112
|
+
message,
|
|
113
|
+
{ ...details, promise: String(promise) },
|
|
114
|
+
'Please report this issue with the error details'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Log to activity.log
|
|
118
|
+
logError(error);
|
|
119
|
+
|
|
120
|
+
// Output structured JSON to stderr
|
|
121
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
122
|
+
|
|
123
|
+
// Exit with appropriate code
|
|
124
|
+
process.exit(getExitCode(error.code));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Feature Flags class for graceful degradation
|
|
129
|
+
* Tracks which features are available vs degraded
|
|
130
|
+
*/
|
|
131
|
+
class FeatureFlags {
|
|
132
|
+
constructor() {
|
|
133
|
+
this.features = {
|
|
134
|
+
activityLog: true,
|
|
135
|
+
progressDisplay: true,
|
|
136
|
+
gitIntegration: true,
|
|
137
|
+
hashComparison: true,
|
|
138
|
+
manifestTracking: true,
|
|
139
|
+
};
|
|
140
|
+
this.degradedFeatures = new Set();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Disable a feature with a reason
|
|
145
|
+
* @param {string} feature - Feature name
|
|
146
|
+
* @param {string} reason - Why the feature was disabled
|
|
147
|
+
*/
|
|
148
|
+
disable(feature, reason) {
|
|
149
|
+
if (this.features.hasOwnProperty(feature)) {
|
|
150
|
+
this.features[feature] = false;
|
|
151
|
+
this.degradedFeatures.add({ feature, reason, timestamp: new Date().toISOString() });
|
|
152
|
+
|
|
153
|
+
// Log degradation warning
|
|
154
|
+
console.warn(JSON.stringify({
|
|
155
|
+
warning: {
|
|
156
|
+
type: 'FEATURE_DEGRADED',
|
|
157
|
+
feature,
|
|
158
|
+
reason,
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
},
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a feature is enabled
|
|
167
|
+
* @param {string} feature - Feature name
|
|
168
|
+
* @returns {boolean} True if enabled
|
|
169
|
+
*/
|
|
170
|
+
isEnabled(feature) {
|
|
171
|
+
return this.features[feature] || false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get list of degraded features
|
|
176
|
+
* @returns {Array} Array of degraded feature objects
|
|
177
|
+
*/
|
|
178
|
+
getDegradedFeatures() {
|
|
179
|
+
return Array.from(this.degradedFeatures);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Global feature flags instance
|
|
184
|
+
const features = new FeatureFlags();
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Wrap a command function with error handling
|
|
188
|
+
* @param {Function} commandFn - Async command function to wrap
|
|
189
|
+
* @param {object} options - Options for error handling
|
|
190
|
+
* @param {boolean} options.logActivity - Whether to log activity (default: true)
|
|
191
|
+
* @returns {Function} Wrapped function
|
|
192
|
+
*/
|
|
193
|
+
function wrapCommand(commandFn, options = {}) {
|
|
194
|
+
const { logActivity: shouldLog = true } = options;
|
|
195
|
+
|
|
196
|
+
return async (...args) => {
|
|
197
|
+
try {
|
|
198
|
+
return await commandFn(...args);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
let structuredError;
|
|
201
|
+
|
|
202
|
+
if (error instanceof AetherError) {
|
|
203
|
+
structuredError = error;
|
|
204
|
+
} else {
|
|
205
|
+
structuredError = wrapError(error);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Log to activity.log
|
|
209
|
+
if (shouldLog) {
|
|
210
|
+
logError(structuredError);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Output structured JSON to stderr
|
|
214
|
+
console.error(JSON.stringify(structuredError.toJSON(), null, 2));
|
|
215
|
+
|
|
216
|
+
// Exit with appropriate code
|
|
217
|
+
process.exit(getExitCode(structuredError.code));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function log(msg) {
|
|
223
|
+
if (!globalQuiet) console.log(msg);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Format UpdateError with prominent recovery commands display
|
|
228
|
+
* @param {UpdateError} error - The update error to format
|
|
229
|
+
* @returns {string} Formatted error message with recovery box
|
|
230
|
+
*/
|
|
231
|
+
function formatUpdateError(error) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
|
|
234
|
+
// Header box
|
|
235
|
+
const headerWidth = 62;
|
|
236
|
+
lines.push(c.error('╔' + '═'.repeat(headerWidth) + '╗'));
|
|
237
|
+
lines.push(c.error('║') + ' UPDATE FAILED'.padEnd(headerWidth) + c.error('║'));
|
|
238
|
+
lines.push(c.error('╚' + '═'.repeat(headerWidth) + '╝'));
|
|
239
|
+
lines.push('');
|
|
240
|
+
|
|
241
|
+
// Error code and message
|
|
242
|
+
lines.push(`Error: ${c.bold(error.code)} - ${error.message}`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
|
|
245
|
+
// Details section
|
|
246
|
+
if (error.details && Object.keys(error.details).length > 0) {
|
|
247
|
+
lines.push('Details:');
|
|
248
|
+
|
|
249
|
+
// Handle specific error types with formatted details
|
|
250
|
+
switch (error.code) {
|
|
251
|
+
case UpdateErrorCodes.E_REPO_DIRTY:
|
|
252
|
+
if (error.details.trackedCount > 0) {
|
|
253
|
+
lines.push(` Modified files: ${error.details.trackedCount}`);
|
|
254
|
+
}
|
|
255
|
+
if (error.details.untrackedCount > 0) {
|
|
256
|
+
lines.push(` Untracked files: ${error.details.untrackedCount}`);
|
|
257
|
+
}
|
|
258
|
+
if (error.details.stagedCount > 0) {
|
|
259
|
+
lines.push(` Staged files: ${error.details.stagedCount}`);
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case UpdateErrorCodes.E_HUB_INACCESSIBLE:
|
|
264
|
+
if (error.details.errors) {
|
|
265
|
+
for (const err of error.details.errors.slice(0, 3)) {
|
|
266
|
+
lines.push(` - ${err}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case UpdateErrorCodes.E_PARTIAL_UPDATE:
|
|
272
|
+
lines.push(` Missing files: ${error.details.missingCount || 0}`);
|
|
273
|
+
lines.push(` Corrupted files: ${error.details.corruptedCount || 0}`);
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case UpdateErrorCodes.E_NETWORK_ERROR:
|
|
277
|
+
lines.push(` Hub directory: ${error.details.hubDir || 'unknown'}`);
|
|
278
|
+
if (error.details.errorCode) {
|
|
279
|
+
lines.push(` Error code: ${error.details.errorCode}`);
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
default:
|
|
284
|
+
// Generic details display
|
|
285
|
+
for (const [key, value] of Object.entries(error.details)) {
|
|
286
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
287
|
+
lines.push(` ${key}: ${value}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
lines.push('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Recovery commands box
|
|
295
|
+
if (error.recoveryCommands && error.recoveryCommands.length > 0) {
|
|
296
|
+
const maxCmdLength = Math.max(...error.recoveryCommands.map(cmd => cmd.length));
|
|
297
|
+
const boxWidth = Math.max(maxCmdLength + 4, 40);
|
|
298
|
+
|
|
299
|
+
lines.push(c.warning('╔' + '═'.repeat(boxWidth) + '╗'));
|
|
300
|
+
lines.push(c.warning('║') + ' RECOVERY COMMANDS'.padEnd(boxWidth) + c.warning('║'));
|
|
301
|
+
lines.push(c.warning('║') + ' '.repeat(boxWidth) + c.warning('║'));
|
|
302
|
+
|
|
303
|
+
for (const cmd of error.recoveryCommands) {
|
|
304
|
+
lines.push(c.warning('║') + ' ' + c.bold(cmd).padEnd(boxWidth - 2) + c.warning('║'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
lines.push(c.warning('║') + ' '.repeat(boxWidth) + c.warning('║'));
|
|
308
|
+
|
|
309
|
+
// Add specific guidance based on error type
|
|
310
|
+
switch (error.code) {
|
|
311
|
+
case UpdateErrorCodes.E_REPO_DIRTY:
|
|
312
|
+
lines.push(c.warning('║') + ' Or to discard changes (DANGER):'.padEnd(boxWidth) + c.warning('║'));
|
|
313
|
+
lines.push(c.warning('║') + ' ' + c.bold('git checkout -- .').padEnd(boxWidth - 2) + c.warning('║'));
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case UpdateErrorCodes.E_HUB_INACCESSIBLE:
|
|
317
|
+
lines.push(c.warning('║') + ' Or to reinstall hub:'.padEnd(boxWidth) + c.warning('║'));
|
|
318
|
+
lines.push(c.warning('║') + ' ' + c.bold('aether install').padEnd(boxWidth - 2) + c.warning('║'));
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case UpdateErrorCodes.E_PARTIAL_UPDATE:
|
|
322
|
+
case UpdateErrorCodes.E_NETWORK_ERROR:
|
|
323
|
+
lines.push(c.warning('║') + ' Then retry:'.padEnd(boxWidth) + c.warning('║'));
|
|
324
|
+
lines.push(c.warning('║') + ' ' + c.bold('aether update').padEnd(boxWidth - 2) + c.warning('║'));
|
|
325
|
+
break;
|
|
326
|
+
|
|
327
|
+
case UpdateErrorCodes.E_VERIFY_FAILED:
|
|
328
|
+
lines.push(c.warning('║') + ' Or restore checkpoint:'.padEnd(boxWidth) + c.warning('║'));
|
|
329
|
+
if (error.details?.checkpoint_id) {
|
|
330
|
+
lines.push(c.warning('║') + ` ${c.bold(`aether checkpoint restore ${error.details.checkpoint_id}`)}`.padEnd(boxWidth) + c.warning('║'));
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
lines.push(c.warning('╚' + '═'.repeat(boxWidth) + '╝'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Checkpoint ID if available
|
|
339
|
+
if (error.details?.checkpoint_id) {
|
|
340
|
+
lines.push('');
|
|
341
|
+
lines.push(`Checkpoint ID: ${c.dim(error.details.checkpoint_id)} (for manual restore if needed)`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return lines.join('\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function copyDirSync(src, dest) {
|
|
348
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
349
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
350
|
+
let count = 0;
|
|
351
|
+
for (const entry of entries) {
|
|
352
|
+
const srcPath = path.join(src, entry.name);
|
|
353
|
+
const destPath = path.join(dest, entry.name);
|
|
354
|
+
if (entry.isDirectory()) {
|
|
355
|
+
count += copyDirSync(srcPath, destPath);
|
|
356
|
+
} else {
|
|
357
|
+
fs.copyFileSync(srcPath, destPath);
|
|
358
|
+
// Preserve executable bit for shell scripts
|
|
359
|
+
if (entry.name.endsWith('.sh')) {
|
|
360
|
+
fs.chmodSync(destPath, 0o755);
|
|
361
|
+
}
|
|
362
|
+
count++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return count;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function removeDirSync(dir) {
|
|
369
|
+
if (!fs.existsSync(dir)) return 0;
|
|
370
|
+
let count = 0;
|
|
371
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const fullPath = path.join(dir, entry.name);
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
count += removeDirSync(fullPath);
|
|
376
|
+
} else {
|
|
377
|
+
fs.unlinkSync(fullPath);
|
|
378
|
+
count++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
fs.rmdirSync(dir);
|
|
382
|
+
return count;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Remove only files from dest that exist in source (safe for shared directories)
|
|
386
|
+
function removeFilesFromSource(sourceDir, destDir) {
|
|
387
|
+
if (!fs.existsSync(sourceDir) || !fs.existsSync(destDir)) return 0;
|
|
388
|
+
let count = 0;
|
|
389
|
+
const sourceFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
390
|
+
for (const file of sourceFiles) {
|
|
391
|
+
const destPath = path.join(destDir, file);
|
|
392
|
+
if (fs.existsSync(destPath)) {
|
|
393
|
+
fs.unlinkSync(destPath);
|
|
394
|
+
count++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return count;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readJsonSafe(filePath) {
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function writeJsonSync(filePath, data) {
|
|
409
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
410
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function hashFileSync(filePath) {
|
|
414
|
+
try {
|
|
415
|
+
const content = fs.readFileSync(filePath);
|
|
416
|
+
return 'sha256:' + crypto.createHash('sha256').update(content).digest('hex');
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error(`Warning: could not hash ${filePath}: ${err.message}`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function validateManifest(manifest) {
|
|
424
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
425
|
+
return { valid: false, error: 'Manifest must be an object' };
|
|
426
|
+
}
|
|
427
|
+
if (!manifest.generated_at || typeof manifest.generated_at !== 'string') {
|
|
428
|
+
return { valid: false, error: 'Manifest missing required field: generated_at' };
|
|
429
|
+
}
|
|
430
|
+
if (!manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) {
|
|
431
|
+
return { valid: false, error: 'Manifest missing required field: files' };
|
|
432
|
+
}
|
|
433
|
+
return { valid: true };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function listFilesRecursive(dir, base) {
|
|
437
|
+
base = base || dir;
|
|
438
|
+
const results = [];
|
|
439
|
+
if (!fs.existsSync(dir)) return results;
|
|
440
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
if (entry.name.startsWith('.')) continue;
|
|
443
|
+
const fullPath = path.join(dir, entry.name);
|
|
444
|
+
if (entry.isDirectory()) {
|
|
445
|
+
results.push(...listFilesRecursive(fullPath, base));
|
|
446
|
+
} else {
|
|
447
|
+
results.push(path.relative(base, fullPath));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return results;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function cleanEmptyDirs(dir) {
|
|
454
|
+
if (!fs.existsSync(dir)) return;
|
|
455
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
456
|
+
for (const entry of entries) {
|
|
457
|
+
if (entry.isDirectory()) {
|
|
458
|
+
cleanEmptyDirs(path.join(dir, entry.name));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Re-read after recursive cleanup
|
|
462
|
+
const remaining = fs.readdirSync(dir);
|
|
463
|
+
if (remaining.length === 0) {
|
|
464
|
+
fs.rmdirSync(dir);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function generateManifest(hubDir) {
|
|
469
|
+
const files = {};
|
|
470
|
+
const allFiles = listFilesRecursive(hubDir);
|
|
471
|
+
for (const relPath of allFiles) {
|
|
472
|
+
// Skip registry, version, and manifest metadata files
|
|
473
|
+
if (relPath === 'registry.json' || relPath === 'version.json' || relPath === 'manifest.json') continue;
|
|
474
|
+
const fullPath = path.join(hubDir, relPath);
|
|
475
|
+
const hash = hashFileSync(fullPath);
|
|
476
|
+
// Skip files that couldn't be hashed (permission issues, etc.)
|
|
477
|
+
if (hash) {
|
|
478
|
+
files[relPath] = hash;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { generated_at: new Date().toISOString(), files };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function syncDirWithCleanup(src, dest, opts) {
|
|
485
|
+
opts = opts || {};
|
|
486
|
+
const dryRun = opts.dryRun || false;
|
|
487
|
+
try {
|
|
488
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
489
|
+
} catch (err) {
|
|
490
|
+
if (err.code !== 'EEXIST') {
|
|
491
|
+
console.error(`Warning: could not create directory ${dest}: ${err.message}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Copy phase with hash comparison
|
|
496
|
+
let copied = 0;
|
|
497
|
+
let skipped = 0;
|
|
498
|
+
const srcFiles = listFilesRecursive(src);
|
|
499
|
+
if (!dryRun) {
|
|
500
|
+
for (const relPath of srcFiles) {
|
|
501
|
+
const srcPath = path.join(src, relPath);
|
|
502
|
+
const destPath = path.join(dest, relPath);
|
|
503
|
+
try {
|
|
504
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
505
|
+
|
|
506
|
+
// Hash comparison: only copy if file doesn't exist or hash differs
|
|
507
|
+
let shouldCopy = true;
|
|
508
|
+
if (fs.existsSync(destPath)) {
|
|
509
|
+
const srcHash = hashFileSync(srcPath);
|
|
510
|
+
const destHash = hashFileSync(destPath);
|
|
511
|
+
if (srcHash === destHash) {
|
|
512
|
+
shouldCopy = false;
|
|
513
|
+
skipped++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (shouldCopy) {
|
|
518
|
+
fs.copyFileSync(srcPath, destPath);
|
|
519
|
+
if (relPath.endsWith('.sh')) {
|
|
520
|
+
fs.chmodSync(destPath, 0o755);
|
|
521
|
+
}
|
|
522
|
+
copied++;
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.error(`Warning: could not copy ${relPath}: ${err.message}`);
|
|
526
|
+
skipped++;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
copied = srcFiles.length;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Cleanup phase — remove files in dest that aren't in src
|
|
534
|
+
const destFiles = listFilesRecursive(dest);
|
|
535
|
+
const srcSet = new Set(srcFiles);
|
|
536
|
+
const removed = [];
|
|
537
|
+
for (const relPath of destFiles) {
|
|
538
|
+
if (!srcSet.has(relPath)) {
|
|
539
|
+
removed.push(relPath);
|
|
540
|
+
if (!dryRun) {
|
|
541
|
+
try {
|
|
542
|
+
fs.unlinkSync(path.join(dest, relPath));
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.error(`Warning: could not remove ${relPath}: ${err.message}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!dryRun && removed.length > 0) {
|
|
551
|
+
try {
|
|
552
|
+
cleanEmptyDirs(dest);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error(`Warning: could not clean directories: ${err.message}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return { copied, removed, skipped };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function computeFileHash(filePath) {
|
|
562
|
+
try {
|
|
563
|
+
const content = fs.readFileSync(filePath);
|
|
564
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
565
|
+
} catch {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Checkpoint allowlist - only these files are captured in checkpoints
|
|
571
|
+
// NEVER include: data/, dreams/, oracle/, TO-DOs.md (user data)
|
|
572
|
+
// Note: runtime/ was removed in v4.0 — .aether/ is published directly
|
|
573
|
+
const CHECKPOINT_ALLOWLIST = [
|
|
574
|
+
'.aether/*.md', // All .md files directly in .aether/
|
|
575
|
+
'.claude/commands/ant/**', // All files in .claude/commands/ant/ recursively
|
|
576
|
+
'.claude/agents/ant/**', // All files in .claude/agents/ant/ recursively
|
|
577
|
+
'.opencode/commands/ant/**', // All files in .opencode/commands/ant/ recursively
|
|
578
|
+
'.opencode/agents/**', // All files in .opencode/agents/ recursively
|
|
579
|
+
'bin/cli.js', // Specific file: bin/cli.js
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
// Forbidden user data patterns - these are NEVER checkpointed
|
|
583
|
+
const USER_DATA_PATTERNS = [
|
|
584
|
+
'data/',
|
|
585
|
+
'dreams/',
|
|
586
|
+
'oracle/',
|
|
587
|
+
'TO-DOs.md',
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Check if a file path matches user data patterns
|
|
592
|
+
* @param {string} filePath - File path to check
|
|
593
|
+
* @returns {boolean} True if file is user data
|
|
594
|
+
*/
|
|
595
|
+
function isUserData(filePath) {
|
|
596
|
+
for (const pattern of USER_DATA_PATTERNS) {
|
|
597
|
+
if (filePath.includes(pattern)) {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Check if a file is tracked by git
|
|
606
|
+
* @param {string} repoPath - Repository root path
|
|
607
|
+
* @param {string} filePath - File path relative to repo root
|
|
608
|
+
* @returns {boolean} True if file is tracked by git
|
|
609
|
+
*/
|
|
610
|
+
function isGitTracked(repoPath, filePath) {
|
|
611
|
+
try {
|
|
612
|
+
execSync(`git ls-files --error-unmatch "${filePath}"`, {
|
|
613
|
+
cwd: repoPath,
|
|
614
|
+
stdio: 'pipe'
|
|
615
|
+
});
|
|
616
|
+
return true;
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Get files matching the checkpoint allowlist
|
|
624
|
+
* @param {string} repoPath - Repository root path
|
|
625
|
+
* @returns {string[]} Array of file paths relative to repo root
|
|
626
|
+
*/
|
|
627
|
+
function getAllowlistedFiles(repoPath) {
|
|
628
|
+
const files = [];
|
|
629
|
+
|
|
630
|
+
for (const pattern of CHECKPOINT_ALLOWLIST) {
|
|
631
|
+
if (pattern === 'bin/cli.js') {
|
|
632
|
+
// Specific file - must be tracked by git for stash to work
|
|
633
|
+
const fullPath = path.join(repoPath, pattern);
|
|
634
|
+
if (fs.existsSync(fullPath) && isGitTracked(repoPath, pattern)) {
|
|
635
|
+
files.push(pattern);
|
|
636
|
+
}
|
|
637
|
+
} else if (pattern === '.aether/*.md') {
|
|
638
|
+
// .md files directly in .aether/ (not subdirs)
|
|
639
|
+
const aetherDir = path.join(repoPath, '.aether');
|
|
640
|
+
if (fs.existsSync(aetherDir)) {
|
|
641
|
+
const entries = fs.readdirSync(aetherDir, { withFileTypes: true });
|
|
642
|
+
for (const entry of entries) {
|
|
643
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
644
|
+
const filePath = path.join('.aether', entry.name);
|
|
645
|
+
if (!isUserData(filePath) && isGitTracked(repoPath, filePath)) {
|
|
646
|
+
files.push(filePath);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} else if (pattern.endsWith('/**')) {
|
|
652
|
+
// Recursive directory pattern
|
|
653
|
+
const dirPath = pattern.slice(0, -3); // Remove '/**'
|
|
654
|
+
const fullDir = path.join(repoPath, dirPath);
|
|
655
|
+
if (fs.existsSync(fullDir)) {
|
|
656
|
+
const dirFiles = listFilesRecursive(fullDir);
|
|
657
|
+
for (const relFile of dirFiles) {
|
|
658
|
+
const filePath = path.join(dirPath, relFile);
|
|
659
|
+
if (!isUserData(filePath) && isGitTracked(repoPath, filePath)) {
|
|
660
|
+
files.push(filePath);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return files;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Generate checkpoint metadata with file hashes
|
|
672
|
+
* @param {string} repoPath - Repository root path
|
|
673
|
+
* @param {string} message - Optional checkpoint message
|
|
674
|
+
* @returns {object} Checkpoint metadata object
|
|
675
|
+
*/
|
|
676
|
+
function generateCheckpointMetadata(repoPath, message) {
|
|
677
|
+
const now = new Date();
|
|
678
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
679
|
+
const checkpointId = `chk_${now.toISOString().slice(0, 10).replace(/-/g, '')}_${now.toTimeString().slice(0, 8).replace(/:/g, '')}`;
|
|
680
|
+
|
|
681
|
+
const allowlistedFiles = getAllowlistedFiles(repoPath);
|
|
682
|
+
const files = {};
|
|
683
|
+
const excluded = [];
|
|
684
|
+
|
|
685
|
+
for (const filePath of allowlistedFiles) {
|
|
686
|
+
// Double-check for user data (safety)
|
|
687
|
+
if (isUserData(filePath)) {
|
|
688
|
+
excluded.push(filePath);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const fullPath = path.join(repoPath, filePath);
|
|
693
|
+
const hash = hashFileSync(fullPath);
|
|
694
|
+
if (hash) {
|
|
695
|
+
files[filePath] = hash;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
checkpoint_id: checkpointId,
|
|
701
|
+
created_at: now.toISOString(),
|
|
702
|
+
message: message || 'Checkpoint created',
|
|
703
|
+
files,
|
|
704
|
+
excluded: excluded.length > 0 ? excluded : undefined,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Save checkpoint metadata to .aether/checkpoints/
|
|
710
|
+
* @param {string} repoPath - Repository root path
|
|
711
|
+
* @param {object} metadata - Checkpoint metadata object
|
|
712
|
+
*/
|
|
713
|
+
function saveCheckpointMetadata(repoPath, metadata) {
|
|
714
|
+
const checkpointsDir = path.join(repoPath, '.aether', 'checkpoints');
|
|
715
|
+
fs.mkdirSync(checkpointsDir, { recursive: true });
|
|
716
|
+
const metadataPath = path.join(checkpointsDir, `${metadata.checkpoint_id}.json`);
|
|
717
|
+
writeJsonSync(metadataPath, metadata);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Load checkpoint metadata by ID
|
|
722
|
+
* @param {string} repoPath - Repository root path
|
|
723
|
+
* @param {string} checkpointId - Checkpoint ID
|
|
724
|
+
* @returns {object|null} Checkpoint metadata or null if not found
|
|
725
|
+
*/
|
|
726
|
+
function loadCheckpointMetadata(repoPath, checkpointId) {
|
|
727
|
+
const metadataPath = path.join(repoPath, '.aether', 'checkpoints', `${checkpointId}.json`);
|
|
728
|
+
return readJsonSafe(metadataPath);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isGitRepo(repoPath) {
|
|
732
|
+
try {
|
|
733
|
+
execSync('git rev-parse --git-dir', { cwd: repoPath, stdio: 'pipe' });
|
|
734
|
+
return true;
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function getGitDirtyFiles(repoPath, targetDirs) {
|
|
741
|
+
try {
|
|
742
|
+
const args = targetDirs.filter(d => fs.existsSync(path.join(repoPath, d)));
|
|
743
|
+
if (args.length === 0) return [];
|
|
744
|
+
const result = execSync(`git status --porcelain -- ${args.map(d => `"${d}"`).join(' ')}`, {
|
|
745
|
+
cwd: repoPath,
|
|
746
|
+
stdio: 'pipe',
|
|
747
|
+
encoding: 'utf8',
|
|
748
|
+
});
|
|
749
|
+
return result.trim().split('\n').filter(Boolean).map(line => line.slice(3));
|
|
750
|
+
} catch {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function gitStashFiles(repoPath, files) {
|
|
756
|
+
try {
|
|
757
|
+
const fileArgs = files.map(f => `"${f}"`).join(' ');
|
|
758
|
+
execSync(`git stash push -m "aether-update-backup" -- ${fileArgs}`, {
|
|
759
|
+
cwd: repoPath,
|
|
760
|
+
stdio: 'pipe',
|
|
761
|
+
});
|
|
762
|
+
return true;
|
|
763
|
+
} catch (err) {
|
|
764
|
+
log(` Warning: git stash failed (${err.message}). Proceeding without stash.`);
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Directories to exclude from hub sync (user data, local state)
|
|
770
|
+
// 'rules' is excluded here because it is synced via a dedicated step (rulesSrc below)
|
|
771
|
+
const HUB_EXCLUDE_DIRS = ['data', 'dreams', 'checkpoints', 'locks', 'temp', 'rules'];
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Check if a path should be excluded from hub sync
|
|
775
|
+
* @param {string} relPath - Relative path from .aether/
|
|
776
|
+
* @returns {boolean} True if should be excluded
|
|
777
|
+
*/
|
|
778
|
+
function shouldExcludeFromHub(relPath) {
|
|
779
|
+
const parts = relPath.split(path.sep);
|
|
780
|
+
// Exclude if any part of the path is in the exclude list
|
|
781
|
+
return parts.some(part => HUB_EXCLUDE_DIRS.includes(part));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Sync .aether/ directory to hub, excluding user data directories
|
|
786
|
+
* @param {string} srcDir - Source .aether/ directory
|
|
787
|
+
* @param {string} destDir - Destination hub directory
|
|
788
|
+
* @returns {object} Sync result with copied, removed, skipped counts
|
|
789
|
+
*/
|
|
790
|
+
function syncAetherToHub(srcDir, destDir) {
|
|
791
|
+
if (!fs.existsSync(srcDir)) {
|
|
792
|
+
return { copied: 0, removed: 0, skipped: 0 };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
796
|
+
|
|
797
|
+
// Get all files in source, filtering out excluded directories
|
|
798
|
+
const srcFiles = [];
|
|
799
|
+
function collectFiles(dir, base) {
|
|
800
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
801
|
+
for (const entry of entries) {
|
|
802
|
+
if (entry.name.startsWith('.')) continue;
|
|
803
|
+
const fullPath = path.join(dir, entry.name);
|
|
804
|
+
const relPath = path.relative(base, fullPath);
|
|
805
|
+
|
|
806
|
+
if (shouldExcludeFromHub(relPath)) continue;
|
|
807
|
+
|
|
808
|
+
if (entry.isDirectory()) {
|
|
809
|
+
collectFiles(fullPath, base);
|
|
810
|
+
} else {
|
|
811
|
+
srcFiles.push(relPath);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
collectFiles(srcDir, srcDir);
|
|
816
|
+
|
|
817
|
+
// Copy files with hash comparison
|
|
818
|
+
let copied = 0;
|
|
819
|
+
let skipped = 0;
|
|
820
|
+
for (const relPath of srcFiles) {
|
|
821
|
+
const srcPath = path.join(srcDir, relPath);
|
|
822
|
+
const destPath = path.join(destDir, relPath);
|
|
823
|
+
|
|
824
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
825
|
+
|
|
826
|
+
// Hash comparison
|
|
827
|
+
let shouldCopy = true;
|
|
828
|
+
if (fs.existsSync(destPath)) {
|
|
829
|
+
const srcHash = hashFileSync(srcPath);
|
|
830
|
+
const destHash = hashFileSync(destPath);
|
|
831
|
+
if (srcHash === destHash) {
|
|
832
|
+
shouldCopy = false;
|
|
833
|
+
skipped++;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (shouldCopy) {
|
|
838
|
+
fs.copyFileSync(srcPath, destPath);
|
|
839
|
+
if (relPath.endsWith('.sh')) {
|
|
840
|
+
fs.chmodSync(destPath, 0o755);
|
|
841
|
+
}
|
|
842
|
+
copied++;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Cleanup: remove files in dest that aren't in source (and aren't excluded)
|
|
847
|
+
const destFiles = [];
|
|
848
|
+
function collectDestFiles(dir, base) {
|
|
849
|
+
if (!fs.existsSync(dir)) return;
|
|
850
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
851
|
+
for (const entry of entries) {
|
|
852
|
+
if (entry.name.startsWith('.') || entry.name === 'registry.json' || entry.name === 'version.json' || entry.name === 'manifest.json') continue;
|
|
853
|
+
const fullPath = path.join(dir, entry.name);
|
|
854
|
+
const relPath = path.relative(base, fullPath);
|
|
855
|
+
|
|
856
|
+
if (shouldExcludeFromHub(relPath)) continue;
|
|
857
|
+
|
|
858
|
+
if (entry.isDirectory()) {
|
|
859
|
+
collectDestFiles(fullPath, base);
|
|
860
|
+
} else {
|
|
861
|
+
destFiles.push(relPath);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
collectDestFiles(destDir, destDir);
|
|
866
|
+
|
|
867
|
+
const srcSet = new Set(srcFiles);
|
|
868
|
+
const removed = [];
|
|
869
|
+
for (const relPath of destFiles) {
|
|
870
|
+
if (!srcSet.has(relPath)) {
|
|
871
|
+
removed.push(relPath);
|
|
872
|
+
try {
|
|
873
|
+
fs.unlinkSync(path.join(destDir, relPath));
|
|
874
|
+
} catch (err) {
|
|
875
|
+
// Ignore cleanup errors
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Clean up empty directories
|
|
881
|
+
cleanEmptyDirs(destDir);
|
|
882
|
+
|
|
883
|
+
return { copied, removed, skipped };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function setupHub() {
|
|
887
|
+
// Create ~/.aether/ directory structure and populate from package
|
|
888
|
+
try {
|
|
889
|
+
fs.mkdirSync(HUB_DIR, { recursive: true });
|
|
890
|
+
|
|
891
|
+
// MIGRATION: Check for old structure and migrate to system/
|
|
892
|
+
const oldStructureFiles = [
|
|
893
|
+
path.join(HUB_DIR, 'aether-utils.sh'),
|
|
894
|
+
path.join(HUB_DIR, 'workers.md'),
|
|
895
|
+
];
|
|
896
|
+
const hasOldStructure = oldStructureFiles.some(f => fs.existsSync(f));
|
|
897
|
+
const hasNewStructure = fs.existsSync(HUB_SYSTEM_DIR);
|
|
898
|
+
|
|
899
|
+
if (hasOldStructure && !hasNewStructure) {
|
|
900
|
+
log(' Migrating hub to new structure...');
|
|
901
|
+
fs.mkdirSync(HUB_SYSTEM_DIR, { recursive: true });
|
|
902
|
+
|
|
903
|
+
// Move system files to system/
|
|
904
|
+
const systemFiles = ['aether-utils.sh', 'workers.md', 'CONTEXT.md', 'model-profiles.yaml'];
|
|
905
|
+
const systemDirs = ['docs', 'utils', 'commands', 'agents', 'schemas', 'exchange', 'templates', 'lib'];
|
|
906
|
+
|
|
907
|
+
for (const file of systemFiles) {
|
|
908
|
+
const oldPath = path.join(HUB_DIR, file);
|
|
909
|
+
if (fs.existsSync(oldPath)) {
|
|
910
|
+
fs.renameSync(oldPath, path.join(HUB_SYSTEM_DIR, file));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const dir of systemDirs) {
|
|
915
|
+
const oldPath = path.join(HUB_DIR, dir);
|
|
916
|
+
if (fs.existsSync(oldPath)) {
|
|
917
|
+
fs.renameSync(oldPath, path.join(HUB_SYSTEM_DIR, dir));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
log(' Migration complete: system files moved to ~/.aether/system/');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Create system/ directory structure
|
|
925
|
+
fs.mkdirSync(HUB_SYSTEM_DIR, { recursive: true });
|
|
926
|
+
fs.mkdirSync(path.join(HUB_SYSTEM_DIR, 'commands', 'claude'), { recursive: true });
|
|
927
|
+
fs.mkdirSync(path.join(HUB_SYSTEM_DIR, 'commands', 'opencode'), { recursive: true });
|
|
928
|
+
fs.mkdirSync(path.join(HUB_SYSTEM_DIR, 'agents'), { recursive: true });
|
|
929
|
+
fs.mkdirSync(path.join(HUB_SYSTEM_DIR, 'agents-claude'), { recursive: true });
|
|
930
|
+
fs.mkdirSync(path.join(HUB_SYSTEM_DIR, 'rules'), { recursive: true });
|
|
931
|
+
|
|
932
|
+
// Read previous manifest for delta reporting
|
|
933
|
+
const prevManifestRaw = readJsonSafe(path.join(HUB_DIR, 'manifest.json'));
|
|
934
|
+
const prevManifest = prevManifestRaw && validateManifest(prevManifestRaw).valid ? prevManifestRaw : null;
|
|
935
|
+
if (prevManifestRaw && !prevManifest) {
|
|
936
|
+
log(` Warning: previous manifest is invalid, regenerating`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Sync .aether/ -> ~/.aether/system/ (direct packaging, no staging)
|
|
940
|
+
// v4.0: .aether/ is published directly — runtime/ staging removed
|
|
941
|
+
const aetherSrc = path.join(PACKAGE_DIR, '.aether');
|
|
942
|
+
if (fs.existsSync(aetherSrc)) {
|
|
943
|
+
const result = syncAetherToHub(aetherSrc, HUB_SYSTEM_DIR);
|
|
944
|
+
log(` Hub system: ${result.copied} files, ${result.skipped} unchanged -> ${HUB_SYSTEM_DIR}`);
|
|
945
|
+
if (result.removed.length > 0) {
|
|
946
|
+
log(` Hub system: removed ${result.removed.length} stale files`);
|
|
947
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Migration message for users upgrading from pre-4.0 (runtime/ era)
|
|
952
|
+
const prevManifestForMigration = readJsonSafe(path.join(HUB_DIR, 'manifest.json'));
|
|
953
|
+
if (prevManifestForMigration && prevManifestForMigration.version && prevManifestForMigration.version.startsWith('3.')) {
|
|
954
|
+
log('');
|
|
955
|
+
log(' Distribution pipeline simplified (v4.0 change):');
|
|
956
|
+
log(' - runtime/ staging directory has been removed');
|
|
957
|
+
log(' - .aether/ is now published directly (private dirs excluded)');
|
|
958
|
+
log(' - Your colony state and data are unaffected');
|
|
959
|
+
log(' - See CHANGELOG.md for details');
|
|
960
|
+
log('');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Clean up legacy directories from very old hub structure (pre-system/)
|
|
964
|
+
const legacyDirs = [
|
|
965
|
+
path.join(HUB_DIR, '.aether'),
|
|
966
|
+
path.join(HUB_DIR, 'visualizations'),
|
|
967
|
+
];
|
|
968
|
+
for (const legacyDir of legacyDirs) {
|
|
969
|
+
if (fs.existsSync(legacyDir)) {
|
|
970
|
+
try {
|
|
971
|
+
removeDirSync(legacyDir);
|
|
972
|
+
log(` Cleaned up legacy: ${path.basename(legacyDir)}/`);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
// Ignore cleanup errors
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Sync .claude/commands/ant/ -> ~/.aether/system/commands/claude/
|
|
980
|
+
const claudeCmdSrc = fs.existsSync(COMMANDS_SRC)
|
|
981
|
+
? COMMANDS_SRC
|
|
982
|
+
: path.join(PACKAGE_DIR, '.claude', 'commands', 'ant');
|
|
983
|
+
if (fs.existsSync(claudeCmdSrc)) {
|
|
984
|
+
const result = syncDirWithCleanup(claudeCmdSrc, HUB_COMMANDS_CLAUDE);
|
|
985
|
+
log(` Hub commands (claude): ${result.copied} files -> ${HUB_COMMANDS_CLAUDE}`);
|
|
986
|
+
if (result.removed.length > 0) {
|
|
987
|
+
log(` Hub commands (claude): removed ${result.removed.length} stale files`);
|
|
988
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Sync .opencode/commands/ant/ -> ~/.aether/system/commands/opencode/
|
|
993
|
+
const opencodeCmdSrc = path.join(PACKAGE_DIR, '.opencode', 'commands', 'ant');
|
|
994
|
+
if (fs.existsSync(opencodeCmdSrc)) {
|
|
995
|
+
const result = syncDirWithCleanup(opencodeCmdSrc, HUB_COMMANDS_OPENCODE);
|
|
996
|
+
log(` Hub commands (opencode): ${result.copied} files -> ${HUB_COMMANDS_OPENCODE}`);
|
|
997
|
+
if (result.removed.length > 0) {
|
|
998
|
+
log(` Hub commands (opencode): removed ${result.removed.length} stale files`);
|
|
999
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Sync .opencode/agents/ -> ~/.aether/system/agents/
|
|
1004
|
+
const agentsSrc = path.join(PACKAGE_DIR, '.opencode', 'agents');
|
|
1005
|
+
if (fs.existsSync(agentsSrc)) {
|
|
1006
|
+
const result = syncDirWithCleanup(agentsSrc, HUB_AGENTS);
|
|
1007
|
+
log(` Hub agents: ${result.copied} files -> ${HUB_AGENTS}`);
|
|
1008
|
+
if (result.removed.length > 0) {
|
|
1009
|
+
log(` Hub agents: removed ${result.removed.length} stale files`);
|
|
1010
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Sync .claude/agents/ant/ -> ~/.aether/system/agents-claude/
|
|
1015
|
+
const claudeAgentsSrc = path.join(PACKAGE_DIR, '.claude', 'agents', 'ant');
|
|
1016
|
+
if (fs.existsSync(claudeAgentsSrc)) {
|
|
1017
|
+
const result = syncDirWithCleanup(claudeAgentsSrc, HUB_AGENTS_CLAUDE);
|
|
1018
|
+
log(` Hub agents (claude): ${result.copied} files, ${result.skipped} unchanged -> ${HUB_AGENTS_CLAUDE}`);
|
|
1019
|
+
if (result.removed.length > 0) {
|
|
1020
|
+
log(` Hub agents (claude): removed ${result.removed.length} stale files`);
|
|
1021
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Sync rules/ from .aether/ -> ~/.aether/system/rules/
|
|
1026
|
+
// v4.0: source is .aether/rules/ directly (no runtime/ staging)
|
|
1027
|
+
const rulesSrc = path.join(PACKAGE_DIR, '.aether', 'rules');
|
|
1028
|
+
if (fs.existsSync(rulesSrc)) {
|
|
1029
|
+
const result = syncDirWithCleanup(rulesSrc, HUB_RULES);
|
|
1030
|
+
log(` Hub rules: ${result.copied} files -> ${HUB_RULES}`);
|
|
1031
|
+
if (result.removed.length > 0) {
|
|
1032
|
+
log(` Hub rules: removed ${result.removed.length} stale files`);
|
|
1033
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Create/preserve registry.json (at root, not in system/)
|
|
1038
|
+
if (!fs.existsSync(HUB_REGISTRY)) {
|
|
1039
|
+
writeJsonSync(HUB_REGISTRY, { schema_version: 1, repos: [] });
|
|
1040
|
+
log(` Registry: initialized ${HUB_REGISTRY}`);
|
|
1041
|
+
} else {
|
|
1042
|
+
log(` Registry: preserved existing ${HUB_REGISTRY}`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Generate and write manifest (at root, tracks everything)
|
|
1046
|
+
const manifest = generateManifest(HUB_DIR);
|
|
1047
|
+
const manifestPath = path.join(HUB_DIR, 'manifest.json');
|
|
1048
|
+
writeJsonSync(manifestPath, manifest);
|
|
1049
|
+
const fileCount = Object.keys(manifest.files).length;
|
|
1050
|
+
log(` Manifest: ${fileCount} files tracked`);
|
|
1051
|
+
|
|
1052
|
+
// Report manifest delta
|
|
1053
|
+
if (prevManifest && prevManifest.files) {
|
|
1054
|
+
const prevKeys = new Set(Object.keys(prevManifest.files));
|
|
1055
|
+
const currKeys = new Set(Object.keys(manifest.files));
|
|
1056
|
+
const added = [...currKeys].filter(k => !prevKeys.has(k));
|
|
1057
|
+
const removed = [...prevKeys].filter(k => !currKeys.has(k));
|
|
1058
|
+
const changed = [...currKeys].filter(k => prevKeys.has(k) && prevManifest.files[k] !== manifest.files[k]);
|
|
1059
|
+
if (added.length || removed.length || changed.length) {
|
|
1060
|
+
log(` Manifest delta: +${added.length} added, -${removed.length} removed, ~${changed.length} changed`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Write version.json (at root)
|
|
1065
|
+
writeJsonSync(HUB_VERSION, { version: VERSION, updated_at: new Date().toISOString() });
|
|
1066
|
+
log(` Hub version: ${VERSION}`);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
// Hub setup failure doesn't block install
|
|
1069
|
+
log(` Hub setup warning: ${err.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function updateRepo(repoPath, sourceVersion, opts) {
|
|
1074
|
+
opts = opts || {};
|
|
1075
|
+
const dryRun = opts.dryRun || false;
|
|
1076
|
+
const force = opts.force || false;
|
|
1077
|
+
const quiet = opts.quiet || false;
|
|
1078
|
+
|
|
1079
|
+
const repoAether = path.join(repoPath, '.aether');
|
|
1080
|
+
const repoVersionFile = path.join(repoAether, 'version.json');
|
|
1081
|
+
|
|
1082
|
+
if (!fs.existsSync(repoAether)) {
|
|
1083
|
+
return { status: 'skipped', reason: 'no .aether directory' };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const currentVersion = readJsonSafe(repoVersionFile);
|
|
1087
|
+
const currentVer = currentVersion ? currentVersion.version : 'unknown';
|
|
1088
|
+
|
|
1089
|
+
// Target directories for git safety checks
|
|
1090
|
+
const targetDirs = ['.aether', '.claude/commands/ant', '.claude/agents/ant', '.claude/rules', '.opencode/commands/ant', '.opencode/agents'];
|
|
1091
|
+
|
|
1092
|
+
// Git safety: check for dirty files in target directories (skip in dry-run mode)
|
|
1093
|
+
let dirtyFiles = [];
|
|
1094
|
+
if (isGitRepo(repoPath)) {
|
|
1095
|
+
dirtyFiles = getGitDirtyFiles(repoPath, targetDirs);
|
|
1096
|
+
if (dirtyFiles.length > 0 && !force) {
|
|
1097
|
+
return { status: 'dirty', files: dirtyFiles };
|
|
1098
|
+
}
|
|
1099
|
+
// Note: --force handling is now done via checkpoint stash in UpdateTransaction
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Use UpdateTransaction for two-phase commit with automatic rollback
|
|
1103
|
+
const transaction = new UpdateTransaction(repoPath, { sourceVersion, quiet, force });
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
const result = await transaction.execute(sourceVersion, { dryRun });
|
|
1107
|
+
|
|
1108
|
+
// Calculate file counts from sync result
|
|
1109
|
+
const systemCopied = result.sync_result?.system?.copied || 0;
|
|
1110
|
+
const commandsCopied = (result.sync_result?.commands?.copied || 0);
|
|
1111
|
+
const agentsCopied = result.sync_result?.agents?.copied || 0;
|
|
1112
|
+
const rulesCopied = result.sync_result?.rules?.copied || 0;
|
|
1113
|
+
const agentsClaudeCopied = result.sync_result?.agents_claude?.copied || 0;
|
|
1114
|
+
|
|
1115
|
+
const systemRemoved = result.sync_result?.system?.removed?.length || 0;
|
|
1116
|
+
const commandsRemoved = result.sync_result?.commands?.removed?.length || 0;
|
|
1117
|
+
const agentsRemoved = result.sync_result?.agents?.removed?.length || 0;
|
|
1118
|
+
const rulesRemoved = result.sync_result?.rules?.removed?.length || 0;
|
|
1119
|
+
const agentsClaudeRemoved = result.sync_result?.agents_claude?.removed?.length || 0;
|
|
1120
|
+
|
|
1121
|
+
const allRemovedFiles = [
|
|
1122
|
+
...(result.sync_result?.system?.removed || []),
|
|
1123
|
+
...(result.sync_result?.commands?.removed || []).map(f => `.claude/commands/ant/${f}`),
|
|
1124
|
+
...(result.sync_result?.agents?.removed || []).map(f => `.opencode/agents/${f}`),
|
|
1125
|
+
...(result.sync_result?.rules?.removed || []).map(f => `.claude/rules/${f}`),
|
|
1126
|
+
...(result.sync_result?.agents_claude?.removed || []).map(f => `.claude/agents/ant/${f}`),
|
|
1127
|
+
];
|
|
1128
|
+
|
|
1129
|
+
const cleanupResult = result.cleanup_result || { cleaned: [], failed: [] };
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
status: result.status,
|
|
1133
|
+
from: currentVer,
|
|
1134
|
+
to: sourceVersion,
|
|
1135
|
+
system: systemCopied,
|
|
1136
|
+
commands: commandsCopied,
|
|
1137
|
+
agents: agentsCopied,
|
|
1138
|
+
rules: rulesCopied,
|
|
1139
|
+
agentsClaude: agentsClaudeCopied,
|
|
1140
|
+
removed: systemRemoved + commandsRemoved + agentsRemoved + rulesRemoved + agentsClaudeRemoved,
|
|
1141
|
+
removedFiles: allRemovedFiles,
|
|
1142
|
+
stashCreated: !!transaction.checkpoint?.stashRef,
|
|
1143
|
+
checkpoint_id: result.checkpoint_id,
|
|
1144
|
+
cleanup: cleanupResult,
|
|
1145
|
+
};
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
// Handle UpdateError with recovery commands
|
|
1148
|
+
if (error instanceof UpdateError) {
|
|
1149
|
+
// Re-throw with additional context
|
|
1150
|
+
error.details = {
|
|
1151
|
+
...error.details,
|
|
1152
|
+
repoPath,
|
|
1153
|
+
sourceVersion,
|
|
1154
|
+
from: currentVer,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
throw error;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Commander.js program setup
|
|
1162
|
+
program
|
|
1163
|
+
.name('aether')
|
|
1164
|
+
.description('Aether Colony - Multi-agent system using ant colony intelligence')
|
|
1165
|
+
.version(VERSION, '-v, --version', 'show version')
|
|
1166
|
+
.option('--no-color', 'disable colored output')
|
|
1167
|
+
.option('-q, --quiet', 'suppress output')
|
|
1168
|
+
.helpOption('-h, --help', 'show help');
|
|
1169
|
+
|
|
1170
|
+
// Handle --no-color globally
|
|
1171
|
+
program.on('option:no-color', () => {
|
|
1172
|
+
process.env.NO_COLOR = '1';
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// Handle --quiet globally
|
|
1176
|
+
program.on('option:quiet', () => {
|
|
1177
|
+
globalQuiet = true;
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// Install command
|
|
1181
|
+
program
|
|
1182
|
+
.command('install')
|
|
1183
|
+
.description('Install commands and agents to ~/.claude/ and set up distribution hub')
|
|
1184
|
+
.action(wrapCommand(async () => {
|
|
1185
|
+
log(c.header(`aether-colony v${VERSION} — installing...`));
|
|
1186
|
+
|
|
1187
|
+
// Sync commands to ~/.claude/commands/ant/ (with orphan cleanup)
|
|
1188
|
+
if (!fs.existsSync(COMMANDS_SRC)) {
|
|
1189
|
+
// Running from source repo — commands are in .claude/commands/ant/
|
|
1190
|
+
const repoCommands = path.join(PACKAGE_DIR, '.claude', 'commands', 'ant');
|
|
1191
|
+
if (fs.existsSync(repoCommands)) {
|
|
1192
|
+
const result = syncDirWithCleanup(repoCommands, COMMANDS_DEST);
|
|
1193
|
+
log(` Commands: ${result.copied} files -> ${COMMANDS_DEST}`);
|
|
1194
|
+
if (result.removed.length > 0) {
|
|
1195
|
+
log(` Commands: removed ${result.removed.length} stale files`);
|
|
1196
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
console.error(' Commands source not found. Skipping.');
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
const result = syncDirWithCleanup(COMMANDS_SRC, COMMANDS_DEST);
|
|
1203
|
+
log(` Commands: ${result.copied} files -> ${COMMANDS_DEST}`);
|
|
1204
|
+
if (result.removed.length > 0) {
|
|
1205
|
+
log(` Commands: removed ${result.removed.length} stale files`);
|
|
1206
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Sync agents to ~/.claude/agents/ant/ (with orphan cleanup)
|
|
1211
|
+
const repoAgents = path.join(PACKAGE_DIR, '.claude', 'agents', 'ant');
|
|
1212
|
+
if (fs.existsSync(repoAgents)) {
|
|
1213
|
+
const result = syncDirWithCleanup(repoAgents, AGENTS_DEST);
|
|
1214
|
+
log(` Agents (claude): ${result.copied} files -> ${AGENTS_DEST}`);
|
|
1215
|
+
if (result.removed.length > 0) {
|
|
1216
|
+
log(` Agents (claude): removed ${result.removed.length} stale files`);
|
|
1217
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Sync OpenCode commands to ~/.opencode/command/ (with orphan cleanup)
|
|
1222
|
+
const opencodeCmdsSrc = path.join(PACKAGE_DIR, '.opencode', 'commands', 'ant');
|
|
1223
|
+
if (fs.existsSync(opencodeCmdsSrc)) {
|
|
1224
|
+
const result = syncDirWithCleanup(opencodeCmdsSrc, OPENCODE_COMMANDS_DEST);
|
|
1225
|
+
log(` Commands (opencode): ${result.copied} files -> ${OPENCODE_COMMANDS_DEST}`);
|
|
1226
|
+
if (result.removed.length > 0) {
|
|
1227
|
+
log(` Commands (opencode): removed ${result.removed.length} stale files`);
|
|
1228
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Sync OpenCode agents to ~/.opencode/agent/ (with orphan cleanup)
|
|
1233
|
+
const opencodeAgentsSrc = path.join(PACKAGE_DIR, '.opencode', 'agents');
|
|
1234
|
+
if (fs.existsSync(opencodeAgentsSrc)) {
|
|
1235
|
+
const result = syncDirWithCleanup(opencodeAgentsSrc, OPENCODE_AGENTS_DEST);
|
|
1236
|
+
log(` Agents (opencode): ${result.copied} files -> ${OPENCODE_AGENTS_DEST}`);
|
|
1237
|
+
if (result.removed.length > 0) {
|
|
1238
|
+
log(` Agents (opencode): removed ${result.removed.length} stale files`);
|
|
1239
|
+
for (const f of result.removed) log(` - ${f}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Set up distribution hub at ~/.aether/
|
|
1244
|
+
log('');
|
|
1245
|
+
log(c.colony('Setting up distribution hub...'));
|
|
1246
|
+
setupHub();
|
|
1247
|
+
|
|
1248
|
+
log('');
|
|
1249
|
+
log(c.success('Install complete.'));
|
|
1250
|
+
log(` ${c.queen('Claude Code:')} run /ant to get started`);
|
|
1251
|
+
log(` ${c.colony('OpenCode:')} run /ant to get started`);
|
|
1252
|
+
log(` ${c.colony('Hub:')} ${c.dim('~/.aether/')} (for coordinated updates across repos)`);
|
|
1253
|
+
}));
|
|
1254
|
+
|
|
1255
|
+
// Update command
|
|
1256
|
+
program
|
|
1257
|
+
.command('update')
|
|
1258
|
+
.description('Update current repo from hub (use --all to update all registered repos)')
|
|
1259
|
+
.option('-f, --force', 'stash dirty files and force update')
|
|
1260
|
+
.option('-a, --all', 'update all registered repos')
|
|
1261
|
+
.option('-l, --list', 'show registered repos and versions')
|
|
1262
|
+
.option('-d, --dry-run', 'preview what would change without modifying files')
|
|
1263
|
+
.action(wrapCommand(async (options) => {
|
|
1264
|
+
const forceFlag = options.force || false;
|
|
1265
|
+
const allFlag = options.all || false;
|
|
1266
|
+
const listFlag = options.list || false;
|
|
1267
|
+
const dryRun = options.dryRun || false;
|
|
1268
|
+
|
|
1269
|
+
// Check hub exists
|
|
1270
|
+
if (!fs.existsSync(HUB_VERSION)) {
|
|
1271
|
+
const error = new HubError(
|
|
1272
|
+
'No distribution hub found at ~/.aether/',
|
|
1273
|
+
{ path: HUB_DIR }
|
|
1274
|
+
);
|
|
1275
|
+
logError(error);
|
|
1276
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
1277
|
+
process.exit(getExitCode(error.code));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const hubVersion = readJsonSafe(HUB_VERSION);
|
|
1281
|
+
const sourceVersion = hubVersion ? hubVersion.version : VERSION;
|
|
1282
|
+
|
|
1283
|
+
if (listFlag) {
|
|
1284
|
+
// Show registered repos
|
|
1285
|
+
const registry = readJsonSafe(HUB_REGISTRY);
|
|
1286
|
+
if (!registry || registry.repos.length === 0) {
|
|
1287
|
+
console.log(c.info('No repos registered. Run the Claude Code slash command /ant:init in a repo to register it.'));
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.log(c.header(`Registered repos (hub v${sourceVersion}):\n`));
|
|
1291
|
+
for (const repo of registry.repos) {
|
|
1292
|
+
const exists = fs.existsSync(repo.path);
|
|
1293
|
+
const status = exists ? `v${repo.version}` : 'NOT FOUND';
|
|
1294
|
+
const marker = exists ? (repo.version === sourceVersion ? ' ' : '* ') : 'x ';
|
|
1295
|
+
console.log(`${marker}${repo.path} (${status})`);
|
|
1296
|
+
}
|
|
1297
|
+
console.log('');
|
|
1298
|
+
console.log(c.dim('* = update available, x = path no longer exists'));
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (allFlag) {
|
|
1303
|
+
// Update all registered repos
|
|
1304
|
+
const registry = readJsonSafe(HUB_REGISTRY);
|
|
1305
|
+
if (!registry || registry.repos.length === 0) {
|
|
1306
|
+
console.log(c.info('No repos registered. Run the Claude Code slash command /ant:init in a repo to register it.'));
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
let updated = 0;
|
|
1311
|
+
let upToDate = 0;
|
|
1312
|
+
let pruned = 0;
|
|
1313
|
+
let dirty = 0;
|
|
1314
|
+
let totalRemoved = 0;
|
|
1315
|
+
const survivingRepos = [];
|
|
1316
|
+
|
|
1317
|
+
if (dryRun) {
|
|
1318
|
+
console.log(c.warning('Dry run — no files will be modified.\n'));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
for (const repo of registry.repos) {
|
|
1322
|
+
if (!fs.existsSync(repo.path)) {
|
|
1323
|
+
log(` ${c.warning('Pruned:')} ${repo.path} (no longer exists)`);
|
|
1324
|
+
pruned++;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
survivingRepos.push(repo);
|
|
1329
|
+
|
|
1330
|
+
if (!forceFlag && !dryRun && repo.version === sourceVersion) {
|
|
1331
|
+
log(` Up-to-date: ${repo.path} (v${repo.version})`);
|
|
1332
|
+
upToDate++;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
const result = await updateRepo(repo.path, sourceVersion, { dryRun, force: forceFlag, quiet: true });
|
|
1338
|
+
if (result.status === 'dirty') {
|
|
1339
|
+
console.error(` ${c.error('Dirty:')} ${repo.path} — uncommitted changes in managed files:`);
|
|
1340
|
+
for (const f of result.files) console.error(` ${f}`);
|
|
1341
|
+
console.error(` Skipping. Use --force to stash and update.`);
|
|
1342
|
+
dirty++;
|
|
1343
|
+
} else if (result.status === 'dry-run') {
|
|
1344
|
+
log(` Would update: ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents, ${result.agentsClaude} claude agents]`);
|
|
1345
|
+
if (result.removed > 0) {
|
|
1346
|
+
log(` Would remove ${result.removed} stale files:`);
|
|
1347
|
+
for (const f of result.removedFiles) log(` - ${f}`);
|
|
1348
|
+
}
|
|
1349
|
+
updated++;
|
|
1350
|
+
} else if (result.status === 'updated') {
|
|
1351
|
+
log(` ${c.success('Updated:')} ${repo.path} (${result.from} -> ${result.to}) [${result.system} system, ${result.commands} commands, ${result.agents} agents, ${result.agentsClaude} claude agents]`);
|
|
1352
|
+
if (result.removed > 0) {
|
|
1353
|
+
log(` Removed ${result.removed} stale files:`);
|
|
1354
|
+
for (const f of result.removedFiles) log(` - ${f}`);
|
|
1355
|
+
totalRemoved += result.removed;
|
|
1356
|
+
}
|
|
1357
|
+
// Distribution chain cleanup reporting
|
|
1358
|
+
if (result.cleanup && result.cleanup.cleaned.length > 0) {
|
|
1359
|
+
for (const label of result.cleanup.cleaned) {
|
|
1360
|
+
log(` ${c.success('\u2713')} Removed ${label}`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
for (const failure of (result.cleanup?.failed || [])) {
|
|
1364
|
+
log(` ${c.error('\u2717')} Failed to remove ${failure.label}: ${failure.error}`);
|
|
1365
|
+
}
|
|
1366
|
+
if (result.cleanup && result.cleanup.cleaned.length === 0 && result.cleanup.failed.length === 0) {
|
|
1367
|
+
log(` Distribution chain: ${c.success('\u2713')} clean`);
|
|
1368
|
+
}
|
|
1369
|
+
if (result.stashCreated) {
|
|
1370
|
+
log(` Stash created. Recover with: cd ${repo.path} && git stash pop`);
|
|
1371
|
+
}
|
|
1372
|
+
updated++;
|
|
1373
|
+
} else {
|
|
1374
|
+
log(` Skipped: ${repo.path} (${result.reason})`);
|
|
1375
|
+
}
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
// Handle UpdateError with formatted recovery commands
|
|
1378
|
+
if (error instanceof UpdateError) {
|
|
1379
|
+
console.error(formatUpdateError(error));
|
|
1380
|
+
}
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Save pruned registry
|
|
1386
|
+
if (pruned > 0 && !dryRun) {
|
|
1387
|
+
registry.repos = survivingRepos;
|
|
1388
|
+
writeJsonSync(HUB_REGISTRY, registry);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const label = dryRun ? 'would update' : 'updated';
|
|
1392
|
+
let summary = `\nSummary: ${updated} ${label}, ${upToDate} up to date, ${pruned} pruned`;
|
|
1393
|
+
if (dirty > 0) summary += `, ${dirty} dirty (skipped)`;
|
|
1394
|
+
if (totalRemoved > 0) summary += `, ${totalRemoved} stale files removed`;
|
|
1395
|
+
console.log(summary);
|
|
1396
|
+
} else {
|
|
1397
|
+
// Update current repo
|
|
1398
|
+
const repoPath = process.cwd();
|
|
1399
|
+
const repoAether = path.join(repoPath, '.aether');
|
|
1400
|
+
|
|
1401
|
+
if (!fs.existsSync(repoAether)) {
|
|
1402
|
+
const error = new RepoError(
|
|
1403
|
+
'No .aether/ directory found in current repo.',
|
|
1404
|
+
{ path: repoPath }
|
|
1405
|
+
);
|
|
1406
|
+
logError(error);
|
|
1407
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
1408
|
+
process.exit(getExitCode(error.code));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const pendingPath = path.join(repoAether, '.update-pending');
|
|
1412
|
+
const hasPending = fs.existsSync(pendingPath);
|
|
1413
|
+
|
|
1414
|
+
if (hasPending) {
|
|
1415
|
+
console.log('Detected incomplete update, re-syncing...');
|
|
1416
|
+
try { fs.unlinkSync(pendingPath); } catch { /* ignore */ }
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const currentVersion = readJsonSafe(path.join(repoAether, 'version.json'));
|
|
1420
|
+
const currentVer = currentVersion ? currentVersion.version : 'unknown';
|
|
1421
|
+
|
|
1422
|
+
if (!hasPending && !forceFlag && !dryRun && currentVer === sourceVersion) {
|
|
1423
|
+
console.log(c.info(`Already up to date (v${sourceVersion}).`));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (dryRun) {
|
|
1428
|
+
console.log(c.warning('Dry run — no files will be modified.\n'));
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
try {
|
|
1432
|
+
const result = await updateRepo(repoPath, sourceVersion, { dryRun, force: forceFlag });
|
|
1433
|
+
|
|
1434
|
+
if (result.status === 'dirty') {
|
|
1435
|
+
const error = new GitError(
|
|
1436
|
+
'Uncommitted changes in managed files',
|
|
1437
|
+
{ files: result.files, repo: repoPath }
|
|
1438
|
+
);
|
|
1439
|
+
logError(error);
|
|
1440
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
1441
|
+
console.error('\nUse --force to stash changes and update, or commit/stash manually first.');
|
|
1442
|
+
process.exit(getExitCode(error.code));
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (result.status === 'dry-run') {
|
|
1446
|
+
console.log(`Would update: ${result.from} -> ${result.to}`);
|
|
1447
|
+
console.log(` ${result.system} system files, ${result.commands} command files, ${result.agents} agent files, ${result.agentsClaude} claude agent files`);
|
|
1448
|
+
if (result.removed > 0) {
|
|
1449
|
+
console.log(` Would remove ${result.removed} stale files:`);
|
|
1450
|
+
for (const f of result.removedFiles) console.log(` - ${f}`);
|
|
1451
|
+
}
|
|
1452
|
+
console.log(' Colony data (.aether/data/) untouched.');
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
console.log(c.success(`Updated: ${result.from} -> ${result.to}`));
|
|
1457
|
+
console.log(` ${result.system} system files, ${result.commands} command files, ${result.agents} agent files, ${result.agentsClaude} claude agent files`);
|
|
1458
|
+
if (result.removed > 0) {
|
|
1459
|
+
console.log(` Removed ${result.removed} stale files:`);
|
|
1460
|
+
for (const f of result.removedFiles) console.log(` - ${f}`);
|
|
1461
|
+
}
|
|
1462
|
+
// Distribution chain cleanup reporting
|
|
1463
|
+
if (result.cleanup && result.cleanup.cleaned.length > 0) {
|
|
1464
|
+
for (const label of result.cleanup.cleaned) {
|
|
1465
|
+
console.log(` ${c.success('\u2713')} Removed ${label}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const failure of (result.cleanup?.failed || [])) {
|
|
1469
|
+
console.log(` ${c.error('\u2717')} Failed to remove ${failure.label}: ${failure.error}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (result.cleanup && result.cleanup.cleaned.length === 0 && result.cleanup.failed.length === 0) {
|
|
1472
|
+
console.log(` Distribution chain: ${c.success('\u2713')} clean`);
|
|
1473
|
+
}
|
|
1474
|
+
if (result.stashCreated) {
|
|
1475
|
+
console.log(' Git stash created. Recover with: git stash pop');
|
|
1476
|
+
}
|
|
1477
|
+
if (result.checkpoint_id) {
|
|
1478
|
+
console.log(` Checkpoint: ${result.checkpoint_id}`);
|
|
1479
|
+
}
|
|
1480
|
+
console.log(' Colony data (.aether/data/) untouched.');
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
// Handle UpdateError with prominent recovery commands (UPDATE-04)
|
|
1483
|
+
if (error instanceof UpdateError) {
|
|
1484
|
+
console.error(`\n${c.error('========================================')}`);
|
|
1485
|
+
console.error(c.error('UPDATE FAILED - RECOVERY REQUIRED'));
|
|
1486
|
+
console.error(c.error('========================================'));
|
|
1487
|
+
console.error(`\n${c.error('Error:')} ${error.message}`);
|
|
1488
|
+
if (error.details?.checkpoint_id) {
|
|
1489
|
+
console.error(`\nCheckpoint ID: ${error.details.checkpoint_id}`);
|
|
1490
|
+
}
|
|
1491
|
+
console.error(`\n${c.warning('To recover your workspace:')}`);
|
|
1492
|
+
for (const cmd of error.recoveryCommands) {
|
|
1493
|
+
console.error(` ${cmd}`);
|
|
1494
|
+
}
|
|
1495
|
+
console.error(c.error('========================================'));
|
|
1496
|
+
|
|
1497
|
+
// Log to activity log
|
|
1498
|
+
logError(error);
|
|
1499
|
+
|
|
1500
|
+
// Output structured JSON to stderr
|
|
1501
|
+
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
1502
|
+
process.exit(getExitCode(error.code) || 1);
|
|
1503
|
+
}
|
|
1504
|
+
throw error;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}));
|
|
1508
|
+
|
|
1509
|
+
// Version command
|
|
1510
|
+
program
|
|
1511
|
+
.command('version')
|
|
1512
|
+
.description('Show installed version and hub status')
|
|
1513
|
+
.action(() => {
|
|
1514
|
+
console.log(c.header(`aether-colony v${VERSION}`));
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// Uninstall command
|
|
1518
|
+
program
|
|
1519
|
+
.command('uninstall')
|
|
1520
|
+
.description('Remove slash-commands from ~/.claude/commands/ant/ and ~/.opencode/ (preserves project state and hub)')
|
|
1521
|
+
.action(wrapCommand(async () => {
|
|
1522
|
+
log(c.header(`aether-colony v${VERSION} — uninstalling...`));
|
|
1523
|
+
|
|
1524
|
+
// Remove Claude Code commands
|
|
1525
|
+
if (fs.existsSync(COMMANDS_DEST)) {
|
|
1526
|
+
const n = removeDirSync(COMMANDS_DEST);
|
|
1527
|
+
log(` Removed: ${n} command files from ${COMMANDS_DEST}`);
|
|
1528
|
+
} else {
|
|
1529
|
+
log(' Claude Code commands already removed.');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Remove Claude Code agents
|
|
1533
|
+
if (fs.existsSync(AGENTS_DEST)) {
|
|
1534
|
+
const n = removeDirSync(AGENTS_DEST);
|
|
1535
|
+
log(` Removed: ${n} agent files from ${AGENTS_DEST}`);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Remove OpenCode commands (only our files, preserve others)
|
|
1539
|
+
const opencodeCmdsSrc = path.join(PACKAGE_DIR, '.opencode', 'commands', 'ant');
|
|
1540
|
+
if (fs.existsSync(OPENCODE_COMMANDS_DEST) && fs.existsSync(opencodeCmdsSrc)) {
|
|
1541
|
+
const n = removeFilesFromSource(opencodeCmdsSrc, OPENCODE_COMMANDS_DEST);
|
|
1542
|
+
log(` Removed: ${n} command files from ${OPENCODE_COMMANDS_DEST}`);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Remove OpenCode agents (only our files, preserve others)
|
|
1546
|
+
const opencodeAgentsSrc = path.join(PACKAGE_DIR, '.opencode', 'agents');
|
|
1547
|
+
if (fs.existsSync(OPENCODE_AGENTS_DEST) && fs.existsSync(opencodeAgentsSrc)) {
|
|
1548
|
+
const n = removeFilesFromSource(opencodeAgentsSrc, OPENCODE_AGENTS_DEST);
|
|
1549
|
+
log(` Removed: ${n} agent files from ${OPENCODE_AGENTS_DEST}`);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
log('');
|
|
1553
|
+
log(c.success('Uninstall complete. Per-project .aether/data/ directories are untouched.'));
|
|
1554
|
+
log(` ${c.colony('Hub:')} ${c.dim('~/.aether/')} preserved (remove manually if desired).`);
|
|
1555
|
+
}));
|
|
1556
|
+
|
|
1557
|
+
// Checkpoint command
|
|
1558
|
+
program
|
|
1559
|
+
.command('checkpoint')
|
|
1560
|
+
.description('Manage Aether checkpoints (safe snapshots of system files)')
|
|
1561
|
+
.addCommand(
|
|
1562
|
+
program.createCommand('create')
|
|
1563
|
+
.description('Create a new checkpoint of Aether system files')
|
|
1564
|
+
.argument('[message]', 'optional message describing the checkpoint')
|
|
1565
|
+
.action(wrapCommand(async (message) => {
|
|
1566
|
+
const repoPath = process.cwd();
|
|
1567
|
+
|
|
1568
|
+
// 1. Check if in git repo
|
|
1569
|
+
if (!isGitRepo(repoPath)) {
|
|
1570
|
+
console.error(c.error('Error: Not in a git repository'));
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// 2. Get allowlisted files using CHECKPOINT_ALLOWLIST
|
|
1575
|
+
const allowlistedFiles = getAllowlistedFiles(repoPath);
|
|
1576
|
+
if (allowlistedFiles.length === 0) {
|
|
1577
|
+
console.log(c.warning('No allowlisted files found to checkpoint'));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// 3. Verify no user data in allowlist (safety check)
|
|
1582
|
+
const userDataFiles = allowlistedFiles.filter(f => isUserData(f));
|
|
1583
|
+
if (userDataFiles.length > 0) {
|
|
1584
|
+
console.error(c.error('Safety check failed: user data detected in allowlist:'));
|
|
1585
|
+
for (const f of userDataFiles) console.error(` - ${f}`);
|
|
1586
|
+
process.exit(1);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// 4. Generate checkpoint metadata with hashes
|
|
1590
|
+
const metadata = generateCheckpointMetadata(repoPath, message);
|
|
1591
|
+
|
|
1592
|
+
// 5. Create git stash with allowlisted files
|
|
1593
|
+
// Command format: git stash push -m "aether-checkpoint-{timestamp}" -- {files}
|
|
1594
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1595
|
+
const stashMessage = `aether-checkpoint-${timestamp}`;
|
|
1596
|
+
const fileArgs = allowlistedFiles.map(f => `"${f}"`).join(' ');
|
|
1597
|
+
|
|
1598
|
+
try {
|
|
1599
|
+
execSync(`git stash push -m "${stashMessage}" -- ${fileArgs}`, {
|
|
1600
|
+
cwd: repoPath,
|
|
1601
|
+
stdio: 'pipe'
|
|
1602
|
+
});
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
console.error(c.error(`Failed to create git stash: ${err.message}`));
|
|
1605
|
+
process.exit(1);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// 6. Save metadata to .aether/checkpoints/
|
|
1609
|
+
saveCheckpointMetadata(repoPath, metadata);
|
|
1610
|
+
|
|
1611
|
+
// 7. Output success with checkpoint ID
|
|
1612
|
+
console.log(c.success(`Checkpoint created: ${metadata.checkpoint_id}`));
|
|
1613
|
+
console.log(` Files: ${Object.keys(metadata.files).length}`);
|
|
1614
|
+
console.log(` Stash: ${stashMessage}`);
|
|
1615
|
+
if (message) console.log(` Message: ${message}`);
|
|
1616
|
+
}))
|
|
1617
|
+
)
|
|
1618
|
+
.addCommand(
|
|
1619
|
+
program.createCommand('list')
|
|
1620
|
+
.description('List all checkpoints')
|
|
1621
|
+
.action(wrapCommand(async () => {
|
|
1622
|
+
const repoPath = process.cwd();
|
|
1623
|
+
const checkpointsDir = path.join(repoPath, '.aether', 'checkpoints');
|
|
1624
|
+
|
|
1625
|
+
if (!fs.existsSync(checkpointsDir)) {
|
|
1626
|
+
console.log(c.info('No checkpoints found'));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const files = fs.readdirSync(checkpointsDir)
|
|
1631
|
+
.filter(f => f.endsWith('.json'))
|
|
1632
|
+
.sort();
|
|
1633
|
+
|
|
1634
|
+
if (files.length === 0) {
|
|
1635
|
+
console.log(c.info('No checkpoints found'));
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
console.log(c.header('Checkpoints:'));
|
|
1640
|
+
for (const file of files) {
|
|
1641
|
+
const metadata = loadCheckpointMetadata(repoPath, file.replace('.json', ''));
|
|
1642
|
+
if (metadata) {
|
|
1643
|
+
const fileCount = Object.keys(metadata.files).length;
|
|
1644
|
+
const date = new Date(metadata.created_at).toLocaleString();
|
|
1645
|
+
console.log(` ${metadata.checkpoint_id} ${date} ${fileCount} files ${metadata.message || ''}`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}))
|
|
1649
|
+
)
|
|
1650
|
+
.addCommand(
|
|
1651
|
+
program.createCommand('restore')
|
|
1652
|
+
.description('Restore Aether files from a checkpoint')
|
|
1653
|
+
.argument('<checkpoint-id>', 'checkpoint ID to restore from')
|
|
1654
|
+
.action(wrapCommand(async (checkpointId) => {
|
|
1655
|
+
const repoPath = process.cwd();
|
|
1656
|
+
|
|
1657
|
+
// 1. Load checkpoint metadata
|
|
1658
|
+
const metadata = loadCheckpointMetadata(repoPath, checkpointId);
|
|
1659
|
+
if (!metadata) {
|
|
1660
|
+
console.error(c.error(`Checkpoint not found: ${checkpointId}`));
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// 2. Verify metadata integrity (hashes match current files if they exist)
|
|
1665
|
+
let integrityCheck = true;
|
|
1666
|
+
for (const [filePath, storedHash] of Object.entries(metadata.files)) {
|
|
1667
|
+
const fullPath = path.join(repoPath, filePath);
|
|
1668
|
+
if (fs.existsSync(fullPath)) {
|
|
1669
|
+
const currentHash = hashFileSync(fullPath);
|
|
1670
|
+
if (currentHash !== storedHash) {
|
|
1671
|
+
console.warn(c.warning(`File changed since checkpoint: ${filePath}`));
|
|
1672
|
+
integrityCheck = false;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// 3. Use git stash to restore files
|
|
1678
|
+
// Find stash by message pattern
|
|
1679
|
+
try {
|
|
1680
|
+
const stashList = execSync('git stash list', { cwd: repoPath, encoding: 'utf8' });
|
|
1681
|
+
const stashMatch = stashList.match(/stash@\{([^}]+)\}.*aether-checkpoint-/);
|
|
1682
|
+
if (stashMatch) {
|
|
1683
|
+
execSync(`git stash pop stash@{${stashMatch[1]}}`, {
|
|
1684
|
+
cwd: repoPath,
|
|
1685
|
+
stdio: 'pipe'
|
|
1686
|
+
});
|
|
1687
|
+
console.log(c.success(`Restored from checkpoint: ${checkpointId}`));
|
|
1688
|
+
console.log(` Files restored: ${Object.keys(metadata.files).length}`);
|
|
1689
|
+
} else {
|
|
1690
|
+
console.error(c.error('Could not find matching git stash'));
|
|
1691
|
+
process.exit(1);
|
|
1692
|
+
}
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
console.error(c.error(`Failed to restore checkpoint: ${err.message}`));
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
}))
|
|
1698
|
+
)
|
|
1699
|
+
.addCommand(
|
|
1700
|
+
program.createCommand('verify')
|
|
1701
|
+
.description('Verify checkpoint integrity')
|
|
1702
|
+
.argument('<checkpoint-id>', 'checkpoint ID to verify')
|
|
1703
|
+
.action(wrapCommand(async (checkpointId) => {
|
|
1704
|
+
const repoPath = process.cwd();
|
|
1705
|
+
|
|
1706
|
+
// 1. Load checkpoint metadata
|
|
1707
|
+
const metadata = loadCheckpointMetadata(repoPath, checkpointId);
|
|
1708
|
+
if (!metadata) {
|
|
1709
|
+
console.error(c.error(`Checkpoint not found: ${checkpointId}`));
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// 2. Re-compute hashes for all files in metadata
|
|
1714
|
+
// 3. Compare with stored hashes
|
|
1715
|
+
let passed = 0;
|
|
1716
|
+
let failed = 0;
|
|
1717
|
+
let missing = 0;
|
|
1718
|
+
|
|
1719
|
+
for (const [filePath, storedHash] of Object.entries(metadata.files)) {
|
|
1720
|
+
const fullPath = path.join(repoPath, filePath);
|
|
1721
|
+
if (!fs.existsSync(fullPath)) {
|
|
1722
|
+
console.log(c.error(` MISSING: ${filePath}`));
|
|
1723
|
+
missing++;
|
|
1724
|
+
} else {
|
|
1725
|
+
const currentHash = hashFileSync(fullPath);
|
|
1726
|
+
if (currentHash === storedHash) {
|
|
1727
|
+
console.log(c.success(` OK: ${filePath}`));
|
|
1728
|
+
passed++;
|
|
1729
|
+
} else {
|
|
1730
|
+
console.log(c.error(` MISMATCH: ${filePath}`));
|
|
1731
|
+
failed++;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// 4. Report any mismatches
|
|
1737
|
+
console.log('');
|
|
1738
|
+
if (failed === 0 && missing === 0) {
|
|
1739
|
+
console.log(c.success(`All ${passed} files verified successfully`));
|
|
1740
|
+
} else {
|
|
1741
|
+
console.log(c.warning(`Verification complete: ${passed} passed, ${failed} mismatched, ${missing} missing`));
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
}))
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
// Sync-state command - Synchronize COLONY_STATE.json with .planning/STATE.md
|
|
1748
|
+
program
|
|
1749
|
+
.command('sync-state')
|
|
1750
|
+
.description('Synchronize COLONY_STATE.json with .planning/STATE.md')
|
|
1751
|
+
.option('-d, --dry-run', 'Show what would change without applying')
|
|
1752
|
+
.action(wrapCommand(async (options) => {
|
|
1753
|
+
const repoPath = process.cwd();
|
|
1754
|
+
|
|
1755
|
+
if (!isInitialized(repoPath)) {
|
|
1756
|
+
console.error('Aether not initialized. Run: aether init');
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Check for mismatches
|
|
1761
|
+
const reconciliation = reconcileStates(repoPath);
|
|
1762
|
+
if (!reconciliation.consistent) {
|
|
1763
|
+
console.log('State mismatch detected:');
|
|
1764
|
+
reconciliation.mismatches.forEach(m => console.log(` - ${m}`));
|
|
1765
|
+
console.log('');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (options.dryRun) {
|
|
1769
|
+
console.log('Dry run - no changes made');
|
|
1770
|
+
console.log(`Resolution: ${reconciliation.resolution}`);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Perform sync
|
|
1775
|
+
const result = syncStateFromPlanning(repoPath);
|
|
1776
|
+
if (result.synced) {
|
|
1777
|
+
if (result.changed) {
|
|
1778
|
+
console.log('State synchronized successfully');
|
|
1779
|
+
console.log('Updates:', result.updates.join(', '));
|
|
1780
|
+
} else {
|
|
1781
|
+
console.log('State already synchronized - no changes needed');
|
|
1782
|
+
}
|
|
1783
|
+
} else {
|
|
1784
|
+
console.error(`Sync failed: ${result.error}`);
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
}));
|
|
1788
|
+
|
|
1789
|
+
// Caste emoji mapping for display
|
|
1790
|
+
const CASTE_EMOJIS = {
|
|
1791
|
+
builder: '🔨',
|
|
1792
|
+
watcher: '👁️',
|
|
1793
|
+
scout: '🔍',
|
|
1794
|
+
chaos: '🎲',
|
|
1795
|
+
oracle: '🔮',
|
|
1796
|
+
architect: '🏗️',
|
|
1797
|
+
prime: '🏛️',
|
|
1798
|
+
colonizer: '🌱',
|
|
1799
|
+
route_setter: '🧭',
|
|
1800
|
+
archaeologist: '📜',
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Format context window for display
|
|
1805
|
+
* @param {number} contextWindow - Context window size
|
|
1806
|
+
* @returns {string} Formatted string (e.g., "256K")
|
|
1807
|
+
*/
|
|
1808
|
+
function formatContextWindow(contextWindow) {
|
|
1809
|
+
if (!contextWindow) return '-';
|
|
1810
|
+
if (contextWindow >= 1000) {
|
|
1811
|
+
return `${Math.round(contextWindow / 1000)}K`;
|
|
1812
|
+
}
|
|
1813
|
+
return String(contextWindow);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Caste-models command - Manage caste-to-model assignments
|
|
1817
|
+
const casteModelsCmd = program
|
|
1818
|
+
.command('caste-models')
|
|
1819
|
+
.description('Manage caste-to-model assignments');
|
|
1820
|
+
|
|
1821
|
+
// list subcommand
|
|
1822
|
+
casteModelsCmd
|
|
1823
|
+
.command('list')
|
|
1824
|
+
.description('List current model assignments per caste')
|
|
1825
|
+
.option('--verify', 'Verify model availability on proxy')
|
|
1826
|
+
.action(wrapCommand(async (options) => {
|
|
1827
|
+
const repoPath = process.cwd();
|
|
1828
|
+
const profiles = loadModelProfiles(repoPath);
|
|
1829
|
+
const overrides = getUserOverrides(profiles);
|
|
1830
|
+
const proxyConfig = getProxyConfig(profiles);
|
|
1831
|
+
|
|
1832
|
+
// Check proxy health
|
|
1833
|
+
let proxyHealth = null;
|
|
1834
|
+
let proxyModels = null;
|
|
1835
|
+
if (proxyConfig?.endpoint) {
|
|
1836
|
+
proxyHealth = await checkProxyHealth(proxyConfig.endpoint);
|
|
1837
|
+
if (proxyHealth.healthy && proxyHealth.models) {
|
|
1838
|
+
proxyModels = proxyHealth.models;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
console.log(c.header('Caste Model Assignments\n'));
|
|
1843
|
+
|
|
1844
|
+
// Display proxy status
|
|
1845
|
+
if (proxyConfig?.endpoint) {
|
|
1846
|
+
const proxyStatus = formatProxyStatusColored(proxyHealth, c) + c.dim(` @ ${proxyConfig.endpoint}`);
|
|
1847
|
+
console.log(`Proxy: ${proxyStatus}`);
|
|
1848
|
+
if (!proxyHealth?.healthy) {
|
|
1849
|
+
console.log(c.warning('Warning: Using default model (kimi-k2.5) for all castes'));
|
|
1850
|
+
}
|
|
1851
|
+
console.log('');
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Table header - add Verify column if --verify flag
|
|
1855
|
+
const verifyFlag = options.verify;
|
|
1856
|
+
const header = verifyFlag
|
|
1857
|
+
? `${'Caste'.padEnd(14)} ${'Model'.padEnd(14)} ${'Provider'.padEnd(10)} ${'Context'.padEnd(8)} Verify Status`
|
|
1858
|
+
: `${'Caste'.padEnd(14)} ${'Model'.padEnd(14)} ${'Provider'.padEnd(10)} ${'Context'.padEnd(8)} Status`;
|
|
1859
|
+
console.log(header);
|
|
1860
|
+
console.log(verifyFlag ? '─'.repeat(70) : '─'.repeat(60));
|
|
1861
|
+
|
|
1862
|
+
// Get all assignments
|
|
1863
|
+
const assignments = getAllAssignments(profiles);
|
|
1864
|
+
|
|
1865
|
+
for (const assignment of assignments) {
|
|
1866
|
+
const emoji = CASTE_EMOJIS[assignment.caste] || '•';
|
|
1867
|
+
const casteName = assignment.caste.charAt(0).toUpperCase() + assignment.caste.slice(1);
|
|
1868
|
+
const casteDisplay = `${emoji} ${casteName}`;
|
|
1869
|
+
|
|
1870
|
+
// Check for override
|
|
1871
|
+
const hasOverride = overrides[assignment.caste] !== undefined;
|
|
1872
|
+
const effectiveModel = getEffectiveModel(profiles, assignment.caste);
|
|
1873
|
+
const modelDisplay = effectiveModel.model + (hasOverride ? ' (override)' : '');
|
|
1874
|
+
|
|
1875
|
+
// Get model metadata
|
|
1876
|
+
const metadata = getModelMetadata(profiles, effectiveModel.model);
|
|
1877
|
+
const provider = metadata?.provider || assignment.provider || '-';
|
|
1878
|
+
const contextWindow = formatContextWindow(metadata?.context_window);
|
|
1879
|
+
|
|
1880
|
+
// Status indicator based on proxy health
|
|
1881
|
+
const status = proxyHealth?.healthy ? '✓' : '⚠';
|
|
1882
|
+
|
|
1883
|
+
// Verify flag - check if model is available on proxy
|
|
1884
|
+
let verifyStatus = '';
|
|
1885
|
+
if (verifyFlag) {
|
|
1886
|
+
if (proxyModels) {
|
|
1887
|
+
const isAvailable = proxyModels.includes(effectiveModel.model);
|
|
1888
|
+
verifyStatus = isAvailable ? '✓' : '✗';
|
|
1889
|
+
} else {
|
|
1890
|
+
verifyStatus = '?';
|
|
1891
|
+
}
|
|
1892
|
+
console.log(
|
|
1893
|
+
`${casteDisplay.padEnd(14)} ${modelDisplay.padEnd(14)} ${provider.padEnd(10)} ${contextWindow.padEnd(8)} ${verifyStatus.padEnd(7)} ${status}`
|
|
1894
|
+
);
|
|
1895
|
+
} else {
|
|
1896
|
+
console.log(
|
|
1897
|
+
`${casteDisplay.padEnd(14)} ${modelDisplay.padEnd(14)} ${provider.padEnd(10)} ${contextWindow.padEnd(8)} ${status}`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Show overrides summary if any exist
|
|
1903
|
+
const overrideCount = Object.keys(overrides).length;
|
|
1904
|
+
if (overrideCount > 0) {
|
|
1905
|
+
console.log('');
|
|
1906
|
+
console.log(c.info(`Active overrides: ${overrideCount}`));
|
|
1907
|
+
for (const [caste, model] of Object.entries(overrides)) {
|
|
1908
|
+
console.log(` ${caste}: ${model}`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}));
|
|
1912
|
+
|
|
1913
|
+
// set subcommand
|
|
1914
|
+
casteModelsCmd
|
|
1915
|
+
.command('set')
|
|
1916
|
+
.description('Set model override for a caste')
|
|
1917
|
+
.argument('<assignment>', 'caste=model (e.g., builder=glm-5)')
|
|
1918
|
+
.action(wrapCommand(async (assignment) => {
|
|
1919
|
+
// Parse caste=model format
|
|
1920
|
+
const match = assignment.match(/^([^=]+)=(.+)$/);
|
|
1921
|
+
if (!match) {
|
|
1922
|
+
const error = new ValidationError(
|
|
1923
|
+
`Invalid assignment format: '${assignment}'`,
|
|
1924
|
+
{ received: assignment },
|
|
1925
|
+
'Use format: caste=model (e.g., builder=glm-5)'
|
|
1926
|
+
);
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const [, caste, model] = match;
|
|
1931
|
+
|
|
1932
|
+
const repoPath = process.cwd();
|
|
1933
|
+
|
|
1934
|
+
// Validate and set
|
|
1935
|
+
try {
|
|
1936
|
+
const result = setModelOverride(repoPath, caste, model);
|
|
1937
|
+
|
|
1938
|
+
if (result.previous) {
|
|
1939
|
+
console.log(c.success(`Updated ${caste}: ${result.previous} → ${model}`));
|
|
1940
|
+
} else {
|
|
1941
|
+
console.log(c.success(`Set ${caste} to ${model}`));
|
|
1942
|
+
}
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
if (error.name === 'ValidationError') {
|
|
1945
|
+
// Add helpful suggestions
|
|
1946
|
+
if (error.details?.validCastes) {
|
|
1947
|
+
console.error(c.error(`Error: ${error.message}`));
|
|
1948
|
+
console.error('\nValid castes:');
|
|
1949
|
+
for (const casteName of error.details.validCastes) {
|
|
1950
|
+
const emoji = CASTE_EMOJIS[casteName] || '•';
|
|
1951
|
+
console.error(` ${emoji} ${casteName}`);
|
|
1952
|
+
}
|
|
1953
|
+
} else if (error.details?.validModels) {
|
|
1954
|
+
console.error(c.error(`Error: ${error.message}`));
|
|
1955
|
+
console.error('\nValid models:');
|
|
1956
|
+
for (const modelName of error.details.validModels) {
|
|
1957
|
+
console.error(` • ${modelName}`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
throw error;
|
|
1963
|
+
}
|
|
1964
|
+
}));
|
|
1965
|
+
|
|
1966
|
+
// reset subcommand
|
|
1967
|
+
casteModelsCmd
|
|
1968
|
+
.command('reset')
|
|
1969
|
+
.description('Reset caste to default model (remove override)')
|
|
1970
|
+
.argument('<caste>', 'caste name (e.g., builder)')
|
|
1971
|
+
.action(wrapCommand(async (caste) => {
|
|
1972
|
+
const repoPath = process.cwd();
|
|
1973
|
+
|
|
1974
|
+
try {
|
|
1975
|
+
const result = resetModelOverride(repoPath, caste);
|
|
1976
|
+
|
|
1977
|
+
if (result.hadOverride) {
|
|
1978
|
+
console.log(c.success(`Reset ${caste} to default model`));
|
|
1979
|
+
} else {
|
|
1980
|
+
console.log(c.info(`${caste} was already using default model`));
|
|
1981
|
+
}
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
if (error.name === 'ValidationError' && error.details?.validCastes) {
|
|
1984
|
+
console.error(c.error(`Error: ${error.message}`));
|
|
1985
|
+
console.error('\nValid castes:');
|
|
1986
|
+
for (const casteName of error.details.validCastes) {
|
|
1987
|
+
const emoji = CASTE_EMOJIS[casteName] || '•';
|
|
1988
|
+
console.error(` ${emoji} ${casteName}`);
|
|
1989
|
+
}
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
throw error;
|
|
1993
|
+
}
|
|
1994
|
+
}));
|
|
1995
|
+
|
|
1996
|
+
// Verify-models command - Verify model routing configuration
|
|
1997
|
+
program
|
|
1998
|
+
.command('verify-models')
|
|
1999
|
+
.description('Verify model routing configuration is active')
|
|
2000
|
+
.action(wrapCommand(async () => {
|
|
2001
|
+
const repoPath = process.cwd();
|
|
2002
|
+
const report = await createVerificationReport(repoPath);
|
|
2003
|
+
|
|
2004
|
+
console.log('=== Model Routing Verification ===\n');
|
|
2005
|
+
|
|
2006
|
+
// Proxy status
|
|
2007
|
+
console.log(`LiteLLM Proxy: ${report.proxy.running ? '✓ Running' : '✗ Not running'}`);
|
|
2008
|
+
if (report.proxy.running) {
|
|
2009
|
+
console.log(` Latency: ${report.proxy.latency}ms`);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Environment
|
|
2013
|
+
console.log(`\nEnvironment:`);
|
|
2014
|
+
console.log(` ANTHROPIC_MODEL: ${report.env.model || '(not set)'}`);
|
|
2015
|
+
console.log(` ANTHROPIC_BASE_URL: ${report.env.baseUrl || '(not set)'}`);
|
|
2016
|
+
|
|
2017
|
+
// Caste assignments
|
|
2018
|
+
console.log(`\nCaste Model Assignments:`);
|
|
2019
|
+
for (const [caste, info] of Object.entries(report.castes)) {
|
|
2020
|
+
const status = info.assigned ? '✓' : '✗';
|
|
2021
|
+
console.log(` ${status} ${caste}: ${info.model || 'default'}`);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Model profiles file
|
|
2025
|
+
console.log(`\nModel Profiles File:`);
|
|
2026
|
+
if (report.profilesFile.exists) {
|
|
2027
|
+
console.log(` ✓ Found: ${report.profilesFile.path}`);
|
|
2028
|
+
const profileCount = Object.keys(report.profilesFile.profiles).length;
|
|
2029
|
+
console.log(` Profiles: ${profileCount} castes configured`);
|
|
2030
|
+
} else {
|
|
2031
|
+
console.log(` ✗ Not found: ${report.profilesFile.path}`);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Issues
|
|
2035
|
+
if (report.issues.length > 0) {
|
|
2036
|
+
console.log(`\nIssues Found:`);
|
|
2037
|
+
report.issues.forEach(issue => console.log(` ⚠ ${issue}`));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Recommendation
|
|
2041
|
+
console.log(`\n${report.recommendation}`);
|
|
2042
|
+
}));
|
|
2043
|
+
|
|
2044
|
+
// Spawn-log command - Log a worker spawn event
|
|
2045
|
+
program
|
|
2046
|
+
.command('spawn-log')
|
|
2047
|
+
.description('Log a worker spawn event')
|
|
2048
|
+
.requiredOption('-p, --parent <parent>', 'Parent ant name')
|
|
2049
|
+
.requiredOption('-c, --caste <caste>', 'Worker caste')
|
|
2050
|
+
.requiredOption('-n, --name <name>', 'Child ant name')
|
|
2051
|
+
.requiredOption('-t, --task <task>', 'Task description')
|
|
2052
|
+
.requiredOption('-m, --model <model>', 'Model used')
|
|
2053
|
+
.option('-s, --status <status>', 'Spawn status', 'spawned')
|
|
2054
|
+
.action(wrapCommand(async (options) => {
|
|
2055
|
+
const repoPath = process.cwd();
|
|
2056
|
+
await logSpawn(repoPath, {
|
|
2057
|
+
parent: options.parent,
|
|
2058
|
+
caste: options.caste,
|
|
2059
|
+
child: options.name,
|
|
2060
|
+
task: options.task,
|
|
2061
|
+
model: options.model,
|
|
2062
|
+
status: options.status
|
|
2063
|
+
});
|
|
2064
|
+
console.log(`Logged spawn: ${options.parent} → ${options.name} (${options.caste}) [${options.model}]`);
|
|
2065
|
+
}));
|
|
2066
|
+
|
|
2067
|
+
// Spawn-tree command - Display worker spawn tree
|
|
2068
|
+
program
|
|
2069
|
+
.command('spawn-tree')
|
|
2070
|
+
.description('Display worker spawn tree with model information')
|
|
2071
|
+
.action(wrapCommand(async () => {
|
|
2072
|
+
const repoPath = process.cwd();
|
|
2073
|
+
const tree = formatSpawnTree(repoPath);
|
|
2074
|
+
console.log(tree);
|
|
2075
|
+
}));
|
|
2076
|
+
|
|
2077
|
+
// Status command - Show colony status
|
|
2078
|
+
program
|
|
2079
|
+
.command('status')
|
|
2080
|
+
.description('Show colony status')
|
|
2081
|
+
.option('-j, --json', 'Output as JSON')
|
|
2082
|
+
.action(wrapCommand(async (options) => {
|
|
2083
|
+
const repoPath = process.cwd();
|
|
2084
|
+
const colonyStatePath = path.join(repoPath, '.aether', 'data', 'COLONY_STATE.json');
|
|
2085
|
+
|
|
2086
|
+
// Check if colony exists
|
|
2087
|
+
if (!fs.existsSync(colonyStatePath)) {
|
|
2088
|
+
console.log(c.warning('No colony found in current directory.'));
|
|
2089
|
+
console.log(c.dim('Run /ant:init to create a new colony, or cd to a project with an existing colony.'));
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Load colony state
|
|
2094
|
+
let state;
|
|
2095
|
+
try {
|
|
2096
|
+
state = JSON.parse(fs.readFileSync(colonyStatePath, 'utf8'));
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
console.log(c.error('Could not read colony state.'));
|
|
2099
|
+
console.log(c.dim(`Error: ${err.message}`));
|
|
2100
|
+
console.log(c.dim('The colony state file may be corrupted. Consider running /ant:init to reinitialize.'));
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// JSON output
|
|
2105
|
+
if (options.json) {
|
|
2106
|
+
console.log(JSON.stringify(state, null, 2));
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// Dashboard output
|
|
2111
|
+
console.log(c.header('Colony Status\n'));
|
|
2112
|
+
|
|
2113
|
+
// Goal
|
|
2114
|
+
if (state.goal) {
|
|
2115
|
+
console.log(`${c.queen('Goal:')} ${state.goal}`);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// State
|
|
2119
|
+
if (state.state) {
|
|
2120
|
+
const stateDisplay = state.state === 'BUILDING' ? c.success(state.state) :
|
|
2121
|
+
state.state === 'PLANNING' ? c.warning(state.state) :
|
|
2122
|
+
state.state === 'COMPLETED' ? c.success(state.state) :
|
|
2123
|
+
state.state;
|
|
2124
|
+
console.log(`${c.colony('State:')} ${stateDisplay}`);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// Phase
|
|
2128
|
+
if (state.current_phase !== undefined) {
|
|
2129
|
+
console.log(`${c.info('Phase:')} ${state.current_phase}`);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Version
|
|
2133
|
+
if (state.version) {
|
|
2134
|
+
console.log(`${c.dim('Version:')} ${state.version}`);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Last updated
|
|
2138
|
+
if (state.last_updated) {
|
|
2139
|
+
const lastUpdated = new Date(state.last_updated);
|
|
2140
|
+
const now = new Date();
|
|
2141
|
+
const hoursAgo = Math.round((now - lastUpdated) / (1000 * 60 * 60));
|
|
2142
|
+
const timeDisplay = hoursAgo < 1 ? 'just now' :
|
|
2143
|
+
hoursAgo < 24 ? `${hoursAgo} hours ago` :
|
|
2144
|
+
`${Math.round(hoursAgo / 24)} days ago`;
|
|
2145
|
+
console.log(`${c.dim('Last updated:')} ${timeDisplay}`);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Event count
|
|
2149
|
+
if (state.events && state.events.length > 0) {
|
|
2150
|
+
console.log(`${c.dim('Events:')} ${state.events.length}`);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
console.log('');
|
|
2154
|
+
}));
|
|
2155
|
+
|
|
2156
|
+
// Nestmates command - List sibling colonies
|
|
2157
|
+
program
|
|
2158
|
+
.command('nestmates')
|
|
2159
|
+
.description('List sibling colonies (nestmates)')
|
|
2160
|
+
.action(wrapCommand(async () => {
|
|
2161
|
+
const repoPath = process.cwd();
|
|
2162
|
+
const nestmates = findNestmates(repoPath);
|
|
2163
|
+
|
|
2164
|
+
if (nestmates.length === 0) {
|
|
2165
|
+
console.log('No nestmates found (sibling directories with .aether/).');
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
console.log(`Found ${nestmates.length} nestmate(s):\n`);
|
|
2170
|
+
console.log(formatNestmates(nestmates));
|
|
2171
|
+
}));
|
|
2172
|
+
|
|
2173
|
+
// Telemetry command - View model performance telemetry
|
|
2174
|
+
const telemetryCmd = program
|
|
2175
|
+
.command('telemetry')
|
|
2176
|
+
.description('View model performance telemetry')
|
|
2177
|
+
.action(wrapCommand(async () => {
|
|
2178
|
+
// Default action: show summary
|
|
2179
|
+
const repoPath = process.cwd();
|
|
2180
|
+
const summary = getTelemetrySummary(repoPath);
|
|
2181
|
+
|
|
2182
|
+
console.log(c.header('Model Performance Telemetry\n'));
|
|
2183
|
+
console.log(`Total Spawns: ${summary.total_spawns}`);
|
|
2184
|
+
console.log(`Models Used: ${summary.total_models}\n`);
|
|
2185
|
+
|
|
2186
|
+
if (summary.total_spawns === 0) {
|
|
2187
|
+
console.log(c.info('No telemetry data yet. Run some builds to collect data.'));
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
console.log('Model Performance:');
|
|
2192
|
+
console.log('─'.repeat(60));
|
|
2193
|
+
for (const [model, stats] of Object.entries(summary.models)) {
|
|
2194
|
+
const rate = (stats.success_rate * 100).toFixed(1);
|
|
2195
|
+
const rateColor = stats.success_rate >= 0.9 ? c.success :
|
|
2196
|
+
stats.success_rate >= 0.7 ? c.warning : c.error;
|
|
2197
|
+
console.log(` ${model.padEnd(15)} ${String(stats.total_spawns).padStart(4)} spawns ${rateColor(rate + '%')} success`);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (summary.recent_decisions.length > 0) {
|
|
2201
|
+
console.log('\nRecent Routing Decisions:');
|
|
2202
|
+
console.log('─'.repeat(60));
|
|
2203
|
+
for (const decision of summary.recent_decisions.slice(-5)) {
|
|
2204
|
+
console.log(` ${decision.caste.padEnd(10)} → ${decision.selected_model.padEnd(12)} (${decision.source})`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}));
|
|
2208
|
+
|
|
2209
|
+
// summary subcommand (explicit)
|
|
2210
|
+
telemetryCmd
|
|
2211
|
+
.command('summary')
|
|
2212
|
+
.description('Show overall telemetry summary')
|
|
2213
|
+
.action(wrapCommand(async () => {
|
|
2214
|
+
const repoPath = process.cwd();
|
|
2215
|
+
const summary = getTelemetrySummary(repoPath);
|
|
2216
|
+
|
|
2217
|
+
console.log(c.header('Model Performance Telemetry\n'));
|
|
2218
|
+
console.log(`Total Spawns: ${summary.total_spawns}`);
|
|
2219
|
+
console.log(`Models Used: ${summary.total_models}\n`);
|
|
2220
|
+
|
|
2221
|
+
if (summary.total_spawns === 0) {
|
|
2222
|
+
console.log(c.info('No telemetry data yet. Run some builds to collect data.'));
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
console.log('Model Performance:');
|
|
2227
|
+
console.log('─'.repeat(60));
|
|
2228
|
+
for (const [model, stats] of Object.entries(summary.models)) {
|
|
2229
|
+
const rate = (stats.success_rate * 100).toFixed(1);
|
|
2230
|
+
const rateColor = stats.success_rate >= 0.9 ? c.success :
|
|
2231
|
+
stats.success_rate >= 0.7 ? c.warning : c.error;
|
|
2232
|
+
console.log(` ${model.padEnd(15)} ${String(stats.total_spawns).padStart(4)} spawns ${rateColor(rate + '%')} success`);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (summary.recent_decisions.length > 0) {
|
|
2236
|
+
console.log('\nRecent Routing Decisions:');
|
|
2237
|
+
console.log('─'.repeat(60));
|
|
2238
|
+
for (const decision of summary.recent_decisions.slice(-5)) {
|
|
2239
|
+
console.log(` ${decision.caste.padEnd(10)} → ${decision.selected_model.padEnd(12)} (${decision.source})`);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}));
|
|
2243
|
+
|
|
2244
|
+
// model subcommand
|
|
2245
|
+
telemetryCmd
|
|
2246
|
+
.command('model <model-name>')
|
|
2247
|
+
.description('Show detailed performance for a specific model')
|
|
2248
|
+
.action(wrapCommand(async (modelName) => {
|
|
2249
|
+
const repoPath = process.cwd();
|
|
2250
|
+
const performance = getModelPerformance(repoPath, modelName);
|
|
2251
|
+
|
|
2252
|
+
if (!performance) {
|
|
2253
|
+
console.log(c.warning(`No data for model: ${modelName}`));
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
console.log(c.header(`Model Performance: ${modelName}\n`));
|
|
2258
|
+
console.log(`Total Spawns: ${performance.total_spawns}`);
|
|
2259
|
+
console.log(`Success Rate: ${(performance.success_rate * 100).toFixed(1)}%`);
|
|
2260
|
+
console.log(` ✓ Completed: ${performance.successful_completions}`);
|
|
2261
|
+
console.log(` ✗ Failed: ${performance.failed_completions}`);
|
|
2262
|
+
console.log(` 🚫 Blocked: ${performance.blocked}`);
|
|
2263
|
+
|
|
2264
|
+
if (Object.keys(performance.by_caste).length > 0) {
|
|
2265
|
+
console.log('\nPerformance by Caste:');
|
|
2266
|
+
console.log('─'.repeat(50));
|
|
2267
|
+
for (const [caste, stats] of Object.entries(performance.by_caste)) {
|
|
2268
|
+
const casteRate = stats.spawns > 0 ? (stats.success / stats.spawns * 100).toFixed(1) : '0.0';
|
|
2269
|
+
console.log(` ${caste.padEnd(12)} ${String(stats.spawns).padStart(4)} spawns ${casteRate}% success`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}));
|
|
2273
|
+
|
|
2274
|
+
// performance subcommand
|
|
2275
|
+
telemetryCmd
|
|
2276
|
+
.command('performance')
|
|
2277
|
+
.description('Show models ranked by performance')
|
|
2278
|
+
.action(wrapCommand(async () => {
|
|
2279
|
+
const repoPath = process.cwd();
|
|
2280
|
+
const summary = getTelemetrySummary(repoPath);
|
|
2281
|
+
|
|
2282
|
+
console.log(c.header('Model Performance Ranking\n'));
|
|
2283
|
+
|
|
2284
|
+
if (summary.total_spawns === 0) {
|
|
2285
|
+
console.log(c.info('No telemetry data yet. Run some builds to collect data.'));
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// Sort models by success rate
|
|
2290
|
+
const ranked = Object.entries(summary.models)
|
|
2291
|
+
.map(([model, stats]) => ({ model, ...stats }))
|
|
2292
|
+
.sort((a, b) => b.success_rate - a.success_rate);
|
|
2293
|
+
|
|
2294
|
+
console.log(`${'Rank'.padEnd(6)} ${'Model'.padEnd(15)} ${'Spawns'.padStart(6)} ${'Success'.padStart(8)} ${'Rate'.padStart(6)}`);
|
|
2295
|
+
console.log('─'.repeat(60));
|
|
2296
|
+
|
|
2297
|
+
ranked.forEach((m, i) => {
|
|
2298
|
+
const rank = `${i + 1}.`.padEnd(6);
|
|
2299
|
+
const rate = (m.success_rate * 100).toFixed(1);
|
|
2300
|
+
const rateColor = m.success_rate >= 0.9 ? c.success :
|
|
2301
|
+
m.success_rate >= 0.7 ? c.warning : c.error;
|
|
2302
|
+
console.log(`${rank} ${m.model.padEnd(15)} ${String(m.total_spawns).padStart(6)} ${String(m.successful_completions || 0).padStart(8)} ${rateColor(rate.padStart(5) + '%')}`);
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
console.log('\n' + c.dim('Tip: Use "aether telemetry model <name>" for detailed stats'));
|
|
2306
|
+
}));
|
|
2307
|
+
|
|
2308
|
+
// Context command - Show auto-loaded context
|
|
2309
|
+
program
|
|
2310
|
+
.command('context')
|
|
2311
|
+
.description('Show auto-loaded context including nestmates')
|
|
2312
|
+
.action(wrapCommand(async () => {
|
|
2313
|
+
const repoPath = process.cwd();
|
|
2314
|
+
|
|
2315
|
+
// Load nestmates
|
|
2316
|
+
const nestmates = findNestmates(repoPath);
|
|
2317
|
+
console.log('=== Auto-Loaded Context ===\n');
|
|
2318
|
+
|
|
2319
|
+
// Nestmates
|
|
2320
|
+
console.log(`Nestmates: ${nestmates.length} found`);
|
|
2321
|
+
if (nestmates.length > 0) {
|
|
2322
|
+
console.log(formatNestmates(nestmates));
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// TO-DOs from nestmates
|
|
2326
|
+
console.log('\nCross-Project TO-DOs:');
|
|
2327
|
+
let hasTodos = false;
|
|
2328
|
+
for (const nestmate of nestmates) {
|
|
2329
|
+
const todos = loadNestmateTodos(nestmate.path);
|
|
2330
|
+
if (todos.length > 0) {
|
|
2331
|
+
hasTodos = true;
|
|
2332
|
+
console.log(`\n${nestmate.name}:`);
|
|
2333
|
+
for (const todo of todos) {
|
|
2334
|
+
console.log(` ${todo.file}:`);
|
|
2335
|
+
for (const item of todo.items.slice(0, 5)) {
|
|
2336
|
+
console.log(` ${item}`);
|
|
2337
|
+
}
|
|
2338
|
+
if (todo.items.length > 5) {
|
|
2339
|
+
console.log(` ... and ${todo.items.length - 5} more`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
if (!hasTodos) {
|
|
2345
|
+
console.log(' No cross-project TO-DOs found.');
|
|
2346
|
+
}
|
|
2347
|
+
}));
|
|
2348
|
+
|
|
2349
|
+
// Init command - Initialize Aether in current repository
|
|
2350
|
+
program
|
|
2351
|
+
.command('init')
|
|
2352
|
+
.description('Initialize Aether in current repository')
|
|
2353
|
+
.option('-g, --goal <goal>', 'Initial colony goal', 'Aether colony')
|
|
2354
|
+
.option('-f, --force', 'Reinitialize even if already initialized')
|
|
2355
|
+
.action(wrapCommand(async (options) => {
|
|
2356
|
+
const repoPath = process.cwd();
|
|
2357
|
+
|
|
2358
|
+
// Check if already initialized
|
|
2359
|
+
if (isInitialized(repoPath) && !options.force) {
|
|
2360
|
+
console.log('Aether is already initialized in this repository.');
|
|
2361
|
+
console.log('Use --force to reinitialize (WARNING: may overwrite state).');
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// Initialize
|
|
2366
|
+
const result = await initializeRepo(repoPath, {
|
|
2367
|
+
goal: options.goal,
|
|
2368
|
+
skipIfExists: !options.force
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
if (result.success) {
|
|
2372
|
+
console.log('Aether initialized successfully!');
|
|
2373
|
+
console.log(`State file: ${result.stateFile}`);
|
|
2374
|
+
console.log('');
|
|
2375
|
+
console.log('Next steps:');
|
|
2376
|
+
console.log(' 1. Define your colony goal in .aether/data/COLONY_STATE.json');
|
|
2377
|
+
console.log(' 2. Run: aether sync-state');
|
|
2378
|
+
console.log(' 3. Run: aether verify-models');
|
|
2379
|
+
console.log(' 4. Start building: /ant:init');
|
|
2380
|
+
}
|
|
2381
|
+
}));
|
|
2382
|
+
|
|
2383
|
+
// Custom help handler to show CLI vs Slash command distinction
|
|
2384
|
+
program.on('--help', () => {
|
|
2385
|
+
console.log('');
|
|
2386
|
+
console.log(c.bold('CLI Commands (Terminal):'));
|
|
2387
|
+
console.log(' init Initialize Aether in current repository');
|
|
2388
|
+
console.log(' install Install slash-commands and set up distribution hub');
|
|
2389
|
+
console.log(' update Update current repo from hub');
|
|
2390
|
+
console.log(' sync-state Synchronize COLONY_STATE.json with .planning/STATE.md');
|
|
2391
|
+
console.log(' verify-models Verify model routing configuration');
|
|
2392
|
+
console.log(' version Show installed version');
|
|
2393
|
+
console.log(' uninstall Remove slash-commands (preserves project state and hub)');
|
|
2394
|
+
console.log('');
|
|
2395
|
+
console.log(c.bold('Slash Commands (Claude Code):'));
|
|
2396
|
+
console.log(' /ant:init <goal> Initialize colony in current repo');
|
|
2397
|
+
console.log(' /ant:status Show colony status');
|
|
2398
|
+
console.log(' /ant:plan Generate project plan');
|
|
2399
|
+
console.log(' /ant:build <n> Build phase N');
|
|
2400
|
+
console.log('');
|
|
2401
|
+
console.log(c.dim('Run these in Claude Code after installing with "aether install"'));
|
|
2402
|
+
console.log('');
|
|
2403
|
+
console.log(c.bold('Examples:'));
|
|
2404
|
+
console.log(' $ aether init --goal "My project" # Initialize Aether in current repo');
|
|
2405
|
+
console.log(' $ aether install # Install slash commands');
|
|
2406
|
+
console.log(' $ aether update --list # Show registered repos');
|
|
2407
|
+
console.log(' $ aether update --all --force # Force update all repos');
|
|
2408
|
+
console.log(' $ aether --no-color version # Show version without colors');
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// Configure error output to use colors
|
|
2412
|
+
program.configureOutput({
|
|
2413
|
+
outputError: (str, write) => write(c.error(str))
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
// Export functions for testing
|
|
2417
|
+
module.exports = {
|
|
2418
|
+
hashFileSync,
|
|
2419
|
+
validateManifest,
|
|
2420
|
+
generateManifest,
|
|
2421
|
+
computeFileHash,
|
|
2422
|
+
isGitTracked,
|
|
2423
|
+
getAllowlistedFiles,
|
|
2424
|
+
generateCheckpointMetadata,
|
|
2425
|
+
loadCheckpointMetadata,
|
|
2426
|
+
saveCheckpointMetadata,
|
|
2427
|
+
isUserData,
|
|
2428
|
+
syncDirWithCleanup,
|
|
2429
|
+
listFilesRecursive,
|
|
2430
|
+
cleanEmptyDirs
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
// Parse command line arguments only when run directly (not when required as a module)
|
|
2434
|
+
if (require.main === module) {
|
|
2435
|
+
program.parse();
|
|
2436
|
+
}
|