@moxxy/cli 0.0.11 → 0.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.
Files changed (141) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +341 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +767 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/skill.js +125 -0
  18. package/src/commands/template.js +237 -0
  19. package/src/commands/uninstall.js +196 -0
  20. package/src/commands/update.js +406 -0
  21. package/src/commands/vault.js +219 -0
  22. package/src/help.js +368 -0
  23. package/src/lib/plugin-registry.js +98 -0
  24. package/src/platform.js +40 -0
  25. package/src/sse-client.js +79 -0
  26. package/src/tui/action-wizards.js +130 -0
  27. package/src/tui/app.jsx +859 -0
  28. package/src/tui/components/action-picker.jsx +86 -0
  29. package/src/tui/components/chat-panel.jsx +120 -0
  30. package/src/tui/components/footer.jsx +13 -0
  31. package/src/tui/components/header.jsx +45 -0
  32. package/src/tui/components/input-area.jsx +384 -0
  33. package/src/tui/components/messages/ask-message.jsx +13 -0
  34. package/src/tui/components/messages/assistant-message.jsx +165 -0
  35. package/src/tui/components/messages/channel-message.jsx +18 -0
  36. package/src/tui/components/messages/event-message.jsx +22 -0
  37. package/src/tui/components/messages/hive-status.jsx +34 -0
  38. package/src/tui/components/messages/skill-message.jsx +31 -0
  39. package/src/tui/components/messages/system-message.jsx +12 -0
  40. package/src/tui/components/messages/thinking.jsx +25 -0
  41. package/src/tui/components/messages/tool-group.jsx +62 -0
  42. package/src/tui/components/messages/tool-message.jsx +66 -0
  43. package/src/tui/components/messages/user-message.jsx +12 -0
  44. package/src/tui/components/model-picker.jsx +138 -0
  45. package/src/tui/components/multiline-input.jsx +72 -0
  46. package/src/tui/events-handler.js +730 -0
  47. package/src/tui/helpers.js +59 -0
  48. package/src/tui/hooks/use-command-handler.js +451 -0
  49. package/src/tui/index.jsx +55 -0
  50. package/src/tui/input-utils.js +26 -0
  51. package/src/tui/markdown-renderer.js +66 -0
  52. package/src/tui/mcp-wizard.js +136 -0
  53. package/src/tui/model-picker.js +174 -0
  54. package/src/tui/slash-commands.js +26 -0
  55. package/src/tui/store.js +12 -0
  56. package/src/tui/theme.js +17 -0
  57. package/src/ui.js +109 -0
  58. package/bin/moxxy.js +0 -2
  59. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  60. package/dist/chunk-2FZEA3NG.mjs +0 -457
  61. package/dist/chunk-3KDPLS22.mjs +0 -1131
  62. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  63. package/dist/chunk-6DZX6EAA.mjs +0 -37
  64. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  65. package/dist/chunk-C46NSEKG.mjs +0 -211
  66. package/dist/chunk-CAUXONEF.mjs +0 -1131
  67. package/dist/chunk-CPL5V56X.mjs +0 -1131
  68. package/dist/chunk-CTBVTTBG.mjs +0 -440
  69. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  70. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  71. package/dist/chunk-GSNMMI3H.mjs +0 -530
  72. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  73. package/dist/chunk-N5JTPB6U.mjs +0 -820
  74. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  75. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  76. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  77. package/dist/chunk-QO2JONHP.mjs +0 -1131
  78. package/dist/chunk-RVAPILHA.mjs +0 -1242
  79. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  80. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  81. package/dist/chunk-SOFST2PV.mjs +0 -1242
  82. package/dist/chunk-TMZWETMH.mjs +0 -1242
  83. package/dist/chunk-TYD7NMMI.mjs +0 -581
  84. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  85. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  86. package/dist/chunk-UQZKODNW.mjs +0 -1124
  87. package/dist/chunk-USC6R2ON.mjs +0 -1242
  88. package/dist/chunk-W32EQCVC.mjs +0 -823
  89. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  90. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  91. package/dist/cli-2AIWTL6F.mjs +0 -8
  92. package/dist/cli-2QKJ5UUL.mjs +0 -8
  93. package/dist/cli-4RIS6DQX.mjs +0 -8
  94. package/dist/cli-5RH4VBBL.mjs +0 -7
  95. package/dist/cli-7MK4YGOP.mjs +0 -7
  96. package/dist/cli-B4KH6MZI.mjs +0 -8
  97. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  98. package/dist/cli-CVP26EL2.mjs +0 -8
  99. package/dist/cli-DDRVVNAV.mjs +0 -8
  100. package/dist/cli-E7U56QVQ.mjs +0 -8
  101. package/dist/cli-EQNRMLL3.mjs +0 -8
  102. package/dist/cli-F5RUHHH4.mjs +0 -8
  103. package/dist/cli-LX6FFSEF.mjs +0 -8
  104. package/dist/cli-LY74GWKR.mjs +0 -6
  105. package/dist/cli-MAT3ZJHI.mjs +0 -8
  106. package/dist/cli-NJXXTQYF.mjs +0 -8
  107. package/dist/cli-O4ZGFAZG.mjs +0 -8
  108. package/dist/cli-ORVLI3UQ.mjs +0 -8
  109. package/dist/cli-PV43ZVKA.mjs +0 -8
  110. package/dist/cli-REVD6ISM.mjs +0 -8
  111. package/dist/cli-TBX76KQX.mjs +0 -8
  112. package/dist/cli-TLX5ENVM.mjs +0 -8
  113. package/dist/cli-TNJHCBQA.mjs +0 -6
  114. package/dist/cli-TUX22CZP.mjs +0 -8
  115. package/dist/cli-XJVH7EEP.mjs +0 -8
  116. package/dist/cli-XXOW4VXJ.mjs +0 -8
  117. package/dist/cli-XZ5RESNB.mjs +0 -6
  118. package/dist/cli-YCBYZ76Q.mjs +0 -8
  119. package/dist/cli-ZLMQCU7X.mjs +0 -8
  120. package/dist/dist-2VGKJRBH.mjs +0 -6820
  121. package/dist/dist-37BNX4QG.mjs +0 -7081
  122. package/dist/dist-7LTHRYKA.mjs +0 -11569
  123. package/dist/dist-7XJPQW5C.mjs +0 -6950
  124. package/dist/dist-AYMVOW7T.mjs +0 -7123
  125. package/dist/dist-FAXRJMEN.mjs +0 -6812
  126. package/dist/dist-HQGANM3P.mjs +0 -6976
  127. package/dist/dist-KATLOZQV.mjs +0 -7054
  128. package/dist/dist-KLSB6YHV.mjs +0 -6964
  129. package/dist/dist-LKIOZQ42.mjs +0 -17
  130. package/dist/dist-UYA4RJUH.mjs +0 -2792
  131. package/dist/dist-ZYHCBILM.mjs +0 -6993
  132. package/dist/index.d.mts +0 -23
  133. package/dist/index.d.ts +0 -23
  134. package/dist/index.js +0 -25512
  135. package/dist/index.mjs +0 -18
  136. package/dist/src-APP5P3UD.mjs +0 -1386
  137. package/dist/src-D5HMDDVE.mjs +0 -1324
  138. package/dist/src-EK3WD4AU.mjs +0 -1327
  139. package/dist/src-LSZFLMFN.mjs +0 -1400
  140. package/dist/src-WIOCZRAC.mjs +0 -1397
  141. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,406 @@
1
+ import { p } from '../ui.js';
2
+ import { startGateway, stopGateway, findBinary, paths } from './gateway.js';
3
+ import { execSync } from 'node:child_process';
4
+ import {
5
+ existsSync, copyFileSync, renameSync, chmodSync, unlinkSync,
6
+ createReadStream, createWriteStream, readFileSync,
7
+ } from 'node:fs';
8
+ import { pipeline } from 'node:stream/promises';
9
+ import { createHash } from 'node:crypto';
10
+ import { platform, arch } from 'node:os';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { dirname, join } from 'node:path';
13
+
14
+ const GITHUB_REPO = process.env.MOXXY_GITHUB_REPO || 'moxxy-ai/moxxy';
15
+ const GITHUB_API = 'https://api.github.com';
16
+
17
+ // --- Pure functions (exported for testing) ---
18
+
19
+ export function detectPlatform() {
20
+ const osMap = { darwin: 'darwin', linux: 'linux' };
21
+ const archMap = { arm64: 'arm64', x64: 'x86_64' };
22
+ const os = osMap[platform()] || platform();
23
+ const cpuArch = archMap[arch()] || arch();
24
+ const binaryName = `moxxy-gateway-${os}-${cpuArch}`;
25
+ return { os, arch: cpuArch, binaryName };
26
+ }
27
+
28
+ export function parseUpdateFlags(args) {
29
+ const flags = { check: false, rollback: false, force: false, json: false };
30
+ for (const arg of args) {
31
+ if (arg === '--check') flags.check = true;
32
+ else if (arg === '--rollback') flags.rollback = true;
33
+ else if (arg === '--force') flags.force = true;
34
+ else if (arg === '--json') flags.json = true;
35
+ }
36
+ return flags;
37
+ }
38
+
39
+ export function compareVersions(current, latest) {
40
+ const normalize = (v) => v.replace(/^v/, '').split('.').map(Number);
41
+ const c = normalize(current);
42
+ const l = normalize(latest);
43
+
44
+ // Pad to equal length
45
+ while (c.length < 3) c.push(0);
46
+ while (l.length < 3) l.push(0);
47
+
48
+ for (let i = 0; i < 3; i++) {
49
+ if (l[i] > c[i]) return 'update-available';
50
+ if (l[i] < c[i]) return 'newer';
51
+ }
52
+ return 'up-to-date';
53
+ }
54
+
55
+ export function parseChecksumFile(content) {
56
+ const result = {};
57
+ if (!content) return result;
58
+ for (const line of content.split('\n')) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed) continue;
61
+ // Format: <64-hex-hash> <filename> (two spaces between)
62
+ const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/);
63
+ if (match) {
64
+ result[match[2]] = match[1];
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+
70
+ export function findAssetUrl(assets, binaryName) {
71
+ const asset = assets.find(a => a.name === binaryName);
72
+ return asset ? asset.browser_download_url : null;
73
+ }
74
+
75
+ export function findChecksumUrl(assets) {
76
+ const asset = assets.find(a => a.name === 'checksums.sha256');
77
+ return asset ? asset.browser_download_url : null;
78
+ }
79
+
80
+ // --- Internal helpers ---
81
+
82
+ async function getCurrentGatewayVersion(binPath) {
83
+ // Try health endpoint first
84
+ const apiUrl = process.env.MOXXY_API_URL || 'http://localhost:3000';
85
+ try {
86
+ const resp = await fetch(`${apiUrl}/v1/health`, { signal: AbortSignal.timeout(2000) });
87
+ if (resp.ok) {
88
+ const data = await resp.json();
89
+ if (data.version) return data.version;
90
+ }
91
+ } catch { /* gateway not running, fallback */ }
92
+
93
+ // Fallback to binary --version
94
+ if (binPath && existsSync(binPath)) {
95
+ try {
96
+ const out = execSync(`"${binPath}" --version`, { encoding: 'utf-8', timeout: 5000 });
97
+ const match = out.trim().match(/moxxy-gateway\s+(.+)/);
98
+ if (match) return match[1];
99
+ } catch { /* binary not executable or other error */ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ function getCurrentCliVersion() {
106
+ try {
107
+ const __filename = fileURLToPath(import.meta.url);
108
+ const __dirname = dirname(__filename);
109
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
110
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
111
+ return pkg.version;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ async function fetchLatestRelease() {
118
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
119
+ const headers = { 'Accept': 'application/vnd.github+json', 'User-Agent': 'moxxy-cli' };
120
+ if (token) headers['Authorization'] = `Bearer ${token}`;
121
+
122
+ const url = `${GITHUB_API}/repos/${GITHUB_REPO}/releases/latest`;
123
+ const resp = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
124
+
125
+ if (resp.status === 403) {
126
+ const remaining = resp.headers.get('x-ratelimit-remaining');
127
+ if (remaining === '0') {
128
+ throw new Error('GitHub API rate limit exceeded. Set GITHUB_TOKEN env var to increase the limit.');
129
+ }
130
+ throw new Error(`GitHub API returned 403: ${resp.statusText}`);
131
+ }
132
+ if (resp.status === 404) {
133
+ throw new Error('No releases found. The project may not have published any releases yet.');
134
+ }
135
+ if (!resp.ok) {
136
+ throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
137
+ }
138
+
139
+ return resp.json();
140
+ }
141
+
142
+ async function downloadFile(url, destPath) {
143
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
144
+ const headers = { 'User-Agent': 'moxxy-cli', 'Accept': 'application/octet-stream' };
145
+ if (token) headers['Authorization'] = `Bearer ${token}`;
146
+
147
+ const resp = await fetch(url, { headers, signal: AbortSignal.timeout(120000) });
148
+ if (!resp.ok) throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
149
+
150
+ const fileStream = createWriteStream(destPath);
151
+ await pipeline(resp.body, fileStream);
152
+ }
153
+
154
+ async function downloadText(url) {
155
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
156
+ const headers = { 'User-Agent': 'moxxy-cli', 'Accept': 'application/octet-stream' };
157
+ if (token) headers['Authorization'] = `Bearer ${token}`;
158
+
159
+ const resp = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
160
+ if (!resp.ok) throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
161
+ return resp.text();
162
+ }
163
+
164
+ async function verifyChecksum(filePath, expectedHash) {
165
+ return new Promise((resolve, reject) => {
166
+ const hash = createHash('sha256');
167
+ const stream = createReadStream(filePath);
168
+ stream.on('data', (chunk) => hash.update(chunk));
169
+ stream.on('end', () => {
170
+ const actual = hash.digest('hex');
171
+ resolve(actual === expectedHash);
172
+ });
173
+ stream.on('error', reject);
174
+ });
175
+ }
176
+
177
+ async function healthCheckAfterUpdate() {
178
+ const apiUrl = process.env.MOXXY_API_URL || 'http://localhost:3000';
179
+ for (let i = 0; i < 10; i++) {
180
+ await new Promise(r => setTimeout(r, 500));
181
+ try {
182
+ const resp = await fetch(`${apiUrl}/v1/health`, { signal: AbortSignal.timeout(1000) });
183
+ if (resp.ok) {
184
+ const data = await resp.json();
185
+ return { healthy: true, version: data.version || null };
186
+ }
187
+ } catch { /* retry */ }
188
+ }
189
+ return { healthy: false, version: null };
190
+ }
191
+
192
+ async function isGatewayRunning() {
193
+ const apiUrl = process.env.MOXXY_API_URL || 'http://localhost:3000';
194
+ try {
195
+ const resp = await fetch(`${apiUrl}/v1/health`, { signal: AbortSignal.timeout(2000) });
196
+ return resp.ok;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ // --- Rollback ---
203
+
204
+ async function runRollback(flags) {
205
+ const { bin: binPath } = paths();
206
+ const bakPath = binPath + '.bak';
207
+
208
+ if (!existsSync(bakPath)) {
209
+ p.log.error('No backup found. Cannot rollback (no .bak file exists).');
210
+ process.exitCode = 1;
211
+ return;
212
+ }
213
+
214
+ const wasRunning = await isGatewayRunning();
215
+
216
+ if (wasRunning) {
217
+ p.log.step('Stopping gateway...');
218
+ try { await stopGateway(); } catch (e) { p.log.warn(`Failed to stop gateway: ${e.message}`); }
219
+ await new Promise(r => setTimeout(r, 1000));
220
+ }
221
+
222
+ copyFileSync(bakPath, binPath);
223
+ chmodSync(binPath, 0o755);
224
+ p.log.success('Restored gateway binary from backup.');
225
+
226
+ if (wasRunning) {
227
+ p.log.step('Starting gateway...');
228
+ try { await startGateway(); } catch (e) { p.log.warn(`Failed to start gateway: ${e.message}`); }
229
+ }
230
+
231
+ const health = await healthCheckAfterUpdate();
232
+ if (health.healthy) {
233
+ p.log.success(`Gateway healthy${health.version ? ` (v${health.version})` : ''}.`);
234
+ } else if (wasRunning) {
235
+ p.log.warn('Gateway health check failed after rollback.');
236
+ }
237
+
238
+ if (flags.json) {
239
+ console.log(JSON.stringify({ rolled_back: true, healthy: health.healthy, version: health.version }));
240
+ }
241
+ }
242
+
243
+ // --- Main update flow ---
244
+
245
+ export async function runUpdate(client, args) {
246
+ const flags = parseUpdateFlags(args);
247
+
248
+ if (flags.rollback) {
249
+ await runRollback(flags);
250
+ return;
251
+ }
252
+
253
+ const { binaryName } = detectPlatform();
254
+ const binPath = findBinary();
255
+
256
+ // Current versions
257
+ const currentGateway = await getCurrentGatewayVersion(binPath);
258
+ const currentCli = getCurrentCliVersion();
259
+
260
+ // Fetch latest release
261
+ let release;
262
+ try {
263
+ release = await fetchLatestRelease();
264
+ } catch (err) {
265
+ p.log.error(err.message);
266
+ process.exitCode = 1;
267
+ return;
268
+ }
269
+
270
+ const latestVersion = release.tag_name.replace(/^v/, '');
271
+
272
+ // Compare
273
+ const gatewayComparison = currentGateway
274
+ ? compareVersions(currentGateway, latestVersion)
275
+ : 'update-available';
276
+
277
+ const cliComparison = currentCli
278
+ ? compareVersions(currentCli, latestVersion)
279
+ : 'update-available';
280
+
281
+ // --check: just report
282
+ if (flags.check) {
283
+ if (flags.json) {
284
+ console.log(JSON.stringify({
285
+ current: { gateway: currentGateway, cli: currentCli },
286
+ latest: latestVersion,
287
+ gateway_status: gatewayComparison,
288
+ cli_status: cliComparison,
289
+ }));
290
+ } else {
291
+ p.log.info(`Current gateway: ${currentGateway || 'unknown'}`);
292
+ p.log.info(`Current CLI: ${currentCli || 'unknown'}`);
293
+ p.log.info(`Latest release: ${latestVersion}`);
294
+ if (gatewayComparison === 'update-available' || cliComparison === 'update-available') {
295
+ p.log.info('Update available! Run `moxxy update` to install.');
296
+ } else {
297
+ p.log.success('Everything is up to date.');
298
+ }
299
+ }
300
+ return;
301
+ }
302
+
303
+ // Up-to-date check
304
+ if (gatewayComparison === 'up-to-date' && cliComparison === 'up-to-date' && !flags.force) {
305
+ p.log.success(`Already up to date (v${latestVersion}).`);
306
+ return;
307
+ }
308
+
309
+ // --- Update gateway binary ---
310
+ if (gatewayComparison === 'update-available' || flags.force) {
311
+ const assetUrl = findAssetUrl(release.assets, binaryName);
312
+ if (!assetUrl) {
313
+ p.log.error(`No binary found for platform: ${binaryName}`);
314
+ p.log.info('Available assets: ' + release.assets.map(a => a.name).join(', '));
315
+ process.exitCode = 1;
316
+ return;
317
+ }
318
+
319
+ // Checksum
320
+ const checksumUrl = findChecksumUrl(release.assets);
321
+ let expectedHash = null;
322
+ if (checksumUrl) {
323
+ try {
324
+ const checksumContent = await downloadText(checksumUrl);
325
+ const checksums = parseChecksumFile(checksumContent);
326
+ expectedHash = checksums[binaryName] || null;
327
+ } catch (e) {
328
+ p.log.warn(`Could not download checksums: ${e.message}`);
329
+ }
330
+ } else {
331
+ p.log.warn('No checksum file found in release. Skipping verification.');
332
+ }
333
+
334
+ const { bin: installPath } = paths();
335
+ const tmpPath = installPath + '.download';
336
+
337
+ // Download
338
+ p.log.step(`Downloading ${binaryName}...`);
339
+ try {
340
+ await downloadFile(assetUrl, tmpPath);
341
+ } catch (err) {
342
+ try { unlinkSync(tmpPath); } catch { /* cleanup */ }
343
+ p.log.error(`Download failed: ${err.message}`);
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+
348
+ // Verify checksum
349
+ if (expectedHash) {
350
+ const valid = await verifyChecksum(tmpPath, expectedHash);
351
+ if (!valid) {
352
+ try { unlinkSync(tmpPath); } catch { /* cleanup */ }
353
+ p.log.error('Checksum verification failed. The download may be corrupted.');
354
+ process.exitCode = 1;
355
+ return;
356
+ }
357
+ p.log.success('Checksum verified.');
358
+ }
359
+
360
+ // Stop gateway if running
361
+ const wasRunning = await isGatewayRunning();
362
+ if (wasRunning) {
363
+ p.log.step('Stopping gateway...');
364
+ try { await stopGateway(); } catch (e) { p.log.warn(`Failed to stop gateway: ${e.message}`); }
365
+ await new Promise(r => setTimeout(r, 1000));
366
+ }
367
+
368
+ // Backup + install
369
+ if (existsSync(installPath)) {
370
+ copyFileSync(installPath, installPath + '.bak');
371
+ }
372
+ renameSync(tmpPath, installPath);
373
+ chmodSync(installPath, 0o755);
374
+ p.log.success(`Gateway updated to v${latestVersion}.`);
375
+
376
+ // Restart if it was running
377
+ if (wasRunning) {
378
+ p.log.step('Starting gateway...');
379
+ try { await startGateway(); } catch (e) { p.log.warn(`Failed to start gateway: ${e.message}`); }
380
+
381
+ const health = await healthCheckAfterUpdate();
382
+ if (health.healthy) {
383
+ p.log.success('Gateway is healthy.');
384
+ } else {
385
+ p.log.warn('Gateway health check failed. Run `moxxy update --rollback` to restore the previous version.');
386
+ }
387
+ }
388
+ } else if (gatewayComparison !== 'update-available') {
389
+ p.log.info(`Gateway already at v${currentGateway}.`);
390
+ }
391
+
392
+ // --- Update CLI ---
393
+ if (cliComparison === 'update-available' || flags.force) {
394
+ p.log.step('Updating CLI...');
395
+ try {
396
+ execSync(`npm install -g @moxxy/cli@${latestVersion}`, { stdio: 'pipe', timeout: 60000 });
397
+ p.log.success(`CLI updated to v${latestVersion}.`);
398
+ } catch (err) {
399
+ p.log.warn(`CLI update failed. Install manually: npm install -g @moxxy/cli@${latestVersion}`);
400
+ }
401
+ } else if (cliComparison !== 'update-available') {
402
+ p.log.info(`CLI already at v${currentCli}.`);
403
+ }
404
+
405
+ p.log.success('Update complete.');
406
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Vault commands: add/grant/revoke/list.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, p } from '../ui.js';
6
+
7
+ export async function runVault(client, args) {
8
+ let [action, ...rest] = args;
9
+ const flags = parseFlags(rest);
10
+
11
+ // Interactive sub-menu when no valid action
12
+ if (!['add', 'remove', 'grant', 'revoke', 'list'].includes(action) && isInteractive()) {
13
+ action = await p.select({
14
+ message: 'Vault action',
15
+ options: [
16
+ { value: 'add', label: 'Add secret', hint: 'register a new secret' },
17
+ { value: 'remove', label: 'Remove secret', hint: 'delete a secret from the vault' },
18
+ { value: 'grant', label: 'Grant access', hint: 'grant agent access to a secret' },
19
+ { value: 'revoke', label: 'Revoke access', hint: 'revoke agent secret access' },
20
+ { value: 'list', label: 'List secrets', hint: 'show all secrets' },
21
+ ],
22
+ });
23
+ handleCancel(action);
24
+ }
25
+
26
+ switch (action) {
27
+ case 'add': {
28
+ // Interactive wizard when missing required fields
29
+ if ((!flags.key && !flags.name) && isInteractive()) {
30
+ const keyName = handleCancel(await p.text({
31
+ message: 'Secret key name',
32
+ placeholder: 'OPENAI_API_KEY',
33
+ validate: (val) => { if (!val) return 'Key name is required'; },
34
+ }));
35
+
36
+ const backendKey = handleCancel(await p.text({
37
+ message: 'Backend key reference',
38
+ placeholder: 'env:OPENAI_API_KEY',
39
+ validate: (val) => { if (!val) return 'Backend key is required'; },
40
+ }));
41
+
42
+ const policyLabel = handleCancel(await p.text({
43
+ message: 'Policy label',
44
+ placeholder: 'optional',
45
+ }));
46
+
47
+ const body = {
48
+ key_name: keyName,
49
+ backend_key: backendKey,
50
+ };
51
+ if (policyLabel) body.policy_label = policyLabel;
52
+
53
+ const result = await withSpinner('Adding secret...', () =>
54
+ client.request('/v1/vault/secrets', 'POST', body), 'Secret added.');
55
+
56
+ showResult('Secret Added', {
57
+ ID: result.id || result.secret_ref_id,
58
+ Key: keyName,
59
+ Backend: backendKey,
60
+ });
61
+
62
+ const grantNow = await p.confirm({
63
+ message: 'Grant to an agent?',
64
+ initialValue: false,
65
+ });
66
+ handleCancel(grantNow);
67
+
68
+ if (grantNow) {
69
+ const agentId = await pickAgent(client, 'Select agent to grant');
70
+ const secretId = result.id || result.secret_ref_id;
71
+ if (secretId) {
72
+ await withSpinner('Granting access...', () =>
73
+ client.request('/v1/vault/grants', 'POST', {
74
+ agent_id: agentId,
75
+ secret_ref_id: secretId,
76
+ }), 'Access granted.');
77
+ } else {
78
+ p.log.warn('Could not determine secret ID for grant.');
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ const body = {
86
+ key_name: flags.key || flags.name,
87
+ backend_key: flags.backend,
88
+ };
89
+ if (flags.label) body.policy_label = flags.label;
90
+ if (!body.key_name || !body.backend_key) {
91
+ throw new Error('Required: --key, --backend');
92
+ }
93
+ const result = await client.request('/v1/vault/secrets', 'POST', body);
94
+ console.log(JSON.stringify(result, null, 2));
95
+ return result;
96
+ }
97
+
98
+ case 'remove': {
99
+ let secretId = flags.id || flags.secret;
100
+
101
+ if (!secretId && isInteractive()) {
102
+ const secrets = await withSpinner('Fetching secrets...', () =>
103
+ client.listSecrets(), 'Secrets loaded.');
104
+
105
+ if (!Array.isArray(secrets) || secrets.length === 0) {
106
+ p.log.warn('No secrets to remove.');
107
+ return;
108
+ }
109
+
110
+ secretId = handleCancel(await p.select({
111
+ message: 'Select secret to remove',
112
+ options: secrets.map(s => ({
113
+ value: s.id,
114
+ label: s.key_name,
115
+ hint: `${s.id.slice(0, 12)} backend=${s.backend_key}`,
116
+ })),
117
+ }));
118
+ }
119
+
120
+ if (!secretId) throw new Error('Required: --id');
121
+
122
+ if (isInteractive()) {
123
+ await withSpinner('Removing secret...', () =>
124
+ client.deleteSecret(secretId), 'Secret removed.');
125
+ } else {
126
+ await client.deleteSecret(secretId);
127
+ console.log(`Secret ${secretId} removed.`);
128
+ }
129
+ break;
130
+ }
131
+
132
+ case 'grant': {
133
+ if (!flags.agent || !flags.secret) {
134
+ throw new Error('Required: --agent, --secret');
135
+ }
136
+ const body = {
137
+ agent_id: flags.agent,
138
+ secret_ref_id: flags.secret,
139
+ };
140
+ const result = await client.request('/v1/vault/grants', 'POST', body);
141
+ console.log(`Grant created for agent ${flags.agent}.`);
142
+ return result;
143
+ }
144
+
145
+ case 'list': {
146
+ if (isInteractive()) {
147
+ const secrets = await withSpinner('Fetching secrets...', () =>
148
+ client.listSecrets(), 'Secrets loaded.');
149
+ const grants = await withSpinner('Fetching grants...', () =>
150
+ client.listGrants(), 'Grants loaded.');
151
+
152
+ if (Array.isArray(secrets) && secrets.length > 0) {
153
+ p.log.info('\u2500\u2500 Secrets \u2500\u2500');
154
+ for (const s of secrets) {
155
+ p.log.info(` ${s.key_name} (${s.id.slice(0, 12)}) backend=${s.backend_key}`);
156
+ }
157
+ } else {
158
+ p.log.warn('No secrets found.');
159
+ }
160
+
161
+ if (Array.isArray(grants) && grants.length > 0) {
162
+ p.log.info('\u2500\u2500 Grants \u2500\u2500');
163
+ for (const g of grants) {
164
+ const status = g.revoked_at ? 'revoked' : 'active';
165
+ p.log.info(` agent=${g.agent_id.slice(0, 12)} secret=${g.secret_ref_id.slice(0, 12)} [${status}]`);
166
+ }
167
+ } else {
168
+ p.log.info('No grants found.');
169
+ }
170
+ } else {
171
+ const secrets = await client.listSecrets();
172
+ const grants = await client.listGrants();
173
+ console.log(JSON.stringify({ secrets, grants }, null, 2));
174
+ }
175
+ break;
176
+ }
177
+
178
+ case 'revoke': {
179
+ let grantId = flags.id || flags.grant;
180
+
181
+ if (!grantId && isInteractive()) {
182
+ const grants = await withSpinner('Fetching grants...', () =>
183
+ client.listGrants(), 'Grants loaded.');
184
+
185
+ const active = (grants || []).filter(g => !g.revoked_at);
186
+ if (active.length === 0) {
187
+ p.log.warn('No active grants to revoke.');
188
+ return;
189
+ }
190
+
191
+ grantId = handleCancel(await p.select({
192
+ message: 'Select grant to revoke',
193
+ options: active.map(g => ({
194
+ value: g.id,
195
+ label: `agent=${g.agent_id.slice(0, 12)} secret=${g.secret_ref_id.slice(0, 12)}`,
196
+ hint: g.id.slice(0, 12),
197
+ })),
198
+ }));
199
+ }
200
+
201
+ if (!grantId) throw new Error('Required: --id');
202
+
203
+ if (isInteractive()) {
204
+ await withSpinner('Revoking grant...', () =>
205
+ client.revokeGrant(grantId), 'Grant revoked.');
206
+ } else {
207
+ await client.revokeGrant(grantId);
208
+ console.log(`Grant ${grantId} revoked.`);
209
+ }
210
+ break;
211
+ }
212
+
213
+ default: {
214
+ const { showHelp } = await import('../help.js');
215
+ showHelp('vault', p);
216
+ break;
217
+ }
218
+ }
219
+ }