@moxxy/cli 0.0.12 → 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.
- package/README.md +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +341 -0
- package/src/commands/agent.js +413 -0
- package/src/commands/auth.js +326 -0
- package/src/commands/channel.js +285 -0
- package/src/commands/doctor.js +261 -0
- package/src/commands/events.js +80 -0
- package/src/commands/gateway.js +428 -0
- package/src/commands/heartbeat.js +145 -0
- package/src/commands/init.js +767 -0
- package/src/commands/mcp.js +278 -0
- package/src/commands/plugin.js +583 -0
- package/src/commands/provider.js +1934 -0
- package/src/commands/skill.js +125 -0
- package/src/commands/template.js +237 -0
- package/src/commands/uninstall.js +196 -0
- package/src/commands/update.js +406 -0
- package/src/commands/vault.js +219 -0
- package/src/help.js +368 -0
- package/src/lib/plugin-registry.js +98 -0
- package/src/platform.js +40 -0
- package/src/sse-client.js +79 -0
- package/src/tui/action-wizards.js +130 -0
- package/src/tui/app.jsx +859 -0
- package/src/tui/components/action-picker.jsx +86 -0
- package/src/tui/components/chat-panel.jsx +120 -0
- package/src/tui/components/footer.jsx +13 -0
- package/src/tui/components/header.jsx +45 -0
- package/src/tui/components/input-area.jsx +384 -0
- package/src/tui/components/messages/ask-message.jsx +13 -0
- package/src/tui/components/messages/assistant-message.jsx +165 -0
- package/src/tui/components/messages/channel-message.jsx +18 -0
- package/src/tui/components/messages/event-message.jsx +22 -0
- package/src/tui/components/messages/hive-status.jsx +34 -0
- package/src/tui/components/messages/skill-message.jsx +31 -0
- package/src/tui/components/messages/system-message.jsx +12 -0
- package/src/tui/components/messages/thinking.jsx +25 -0
- package/src/tui/components/messages/tool-group.jsx +62 -0
- package/src/tui/components/messages/tool-message.jsx +66 -0
- package/src/tui/components/messages/user-message.jsx +12 -0
- package/src/tui/components/model-picker.jsx +138 -0
- package/src/tui/components/multiline-input.jsx +72 -0
- package/src/tui/events-handler.js +730 -0
- package/src/tui/helpers.js +59 -0
- package/src/tui/hooks/use-command-handler.js +451 -0
- package/src/tui/index.jsx +55 -0
- package/src/tui/input-utils.js +26 -0
- package/src/tui/markdown-renderer.js +66 -0
- package/src/tui/mcp-wizard.js +136 -0
- package/src/tui/model-picker.js +174 -0
- package/src/tui/slash-commands.js +26 -0
- package/src/tui/store.js +12 -0
- package/src/tui/theme.js +17 -0
- package/src/ui.js +109 -0
- package/bin/moxxy.js +0 -2
- package/dist/chunk-23LZYKQ6.mjs +0 -1131
- package/dist/chunk-2FZEA3NG.mjs +0 -457
- package/dist/chunk-3KDPLS22.mjs +0 -1131
- package/dist/chunk-3QRJTRBT.mjs +0 -1102
- package/dist/chunk-6DZX6EAA.mjs +0 -37
- package/dist/chunk-A4WRDUNY.mjs +0 -1242
- package/dist/chunk-C46NSEKG.mjs +0 -211
- package/dist/chunk-CAUXONEF.mjs +0 -1131
- package/dist/chunk-CPL5V56X.mjs +0 -1131
- package/dist/chunk-CTBVTTBG.mjs +0 -440
- package/dist/chunk-FHHLXTEZ.mjs +0 -1121
- package/dist/chunk-FXY3GPVA.mjs +0 -1126
- package/dist/chunk-GSNMMI3H.mjs +0 -530
- package/dist/chunk-HHOAOGUS.mjs +0 -1242
- package/dist/chunk-ITBO7BKI.mjs +0 -1243
- package/dist/chunk-J33O35WX.mjs +0 -532
- package/dist/chunk-N5JTPB6U.mjs +0 -820
- package/dist/chunk-NGVL4Q5C.mjs +0 -1102
- package/dist/chunk-Q2OCMNYI.mjs +0 -1131
- package/dist/chunk-QDVRLN6D.mjs +0 -1121
- package/dist/chunk-QO2JONHP.mjs +0 -1131
- package/dist/chunk-RVAPILHA.mjs +0 -1242
- package/dist/chunk-S7YBOV7E.mjs +0 -1131
- package/dist/chunk-SHIG6Y5L.mjs +0 -1074
- package/dist/chunk-SOFST2PV.mjs +0 -1242
- package/dist/chunk-SUNUYS6G.mjs +0 -1243
- package/dist/chunk-TMZWETMH.mjs +0 -1242
- package/dist/chunk-TYD7NMMI.mjs +0 -581
- package/dist/chunk-TYQ3YS42.mjs +0 -1068
- package/dist/chunk-UALWCJ7F.mjs +0 -1131
- package/dist/chunk-UQZKODNW.mjs +0 -1124
- package/dist/chunk-USC6R2ON.mjs +0 -1242
- package/dist/chunk-W32EQCVC.mjs +0 -823
- package/dist/chunk-WMB5ENMC.mjs +0 -1242
- package/dist/chunk-WNHA5JAP.mjs +0 -1242
- package/dist/cli-2AIWTL6F.mjs +0 -8
- package/dist/cli-2QKJ5UUL.mjs +0 -8
- package/dist/cli-4RIS6DQX.mjs +0 -8
- package/dist/cli-5RH4VBBL.mjs +0 -7
- package/dist/cli-7MK4YGOP.mjs +0 -7
- package/dist/cli-B4KH6MZI.mjs +0 -8
- package/dist/cli-CGO2LZ6Z.mjs +0 -8
- package/dist/cli-CVP26EL2.mjs +0 -8
- package/dist/cli-DDRVVNAV.mjs +0 -8
- package/dist/cli-E7U56QVQ.mjs +0 -8
- package/dist/cli-EQNRMLL3.mjs +0 -8
- package/dist/cli-F5RUHHH4.mjs +0 -8
- package/dist/cli-LX6FFSEF.mjs +0 -8
- package/dist/cli-LY74GWKR.mjs +0 -6
- package/dist/cli-MAT3ZJHI.mjs +0 -8
- package/dist/cli-NJXXTQYF.mjs +0 -8
- package/dist/cli-O4ZGFAZG.mjs +0 -8
- package/dist/cli-ORVLI3UQ.mjs +0 -8
- package/dist/cli-PV43ZVKA.mjs +0 -8
- package/dist/cli-REVD6ISM.mjs +0 -8
- package/dist/cli-TBX76KQX.mjs +0 -8
- package/dist/cli-THCGF7SQ.mjs +0 -8
- package/dist/cli-TLX5ENVM.mjs +0 -8
- package/dist/cli-TMNI5ZYE.mjs +0 -8
- package/dist/cli-TNJHCBQA.mjs +0 -6
- package/dist/cli-TUX22CZP.mjs +0 -8
- package/dist/cli-XJVH7EEP.mjs +0 -8
- package/dist/cli-XXOW4VXJ.mjs +0 -8
- package/dist/cli-XZ5RESNB.mjs +0 -6
- package/dist/cli-YCBYZ76Q.mjs +0 -8
- package/dist/cli-ZLMQCU7X.mjs +0 -8
- package/dist/dist-2VGKJRBH.mjs +0 -6820
- package/dist/dist-37BNX4QG.mjs +0 -7081
- package/dist/dist-7LTHRYKA.mjs +0 -11569
- package/dist/dist-7XJPQW5C.mjs +0 -6950
- package/dist/dist-AYMVOW7T.mjs +0 -7123
- package/dist/dist-BHUWCDRS.mjs +0 -7132
- package/dist/dist-FAXRJMEN.mjs +0 -6812
- package/dist/dist-HQGANM3P.mjs +0 -6976
- package/dist/dist-KATLOZQV.mjs +0 -7054
- package/dist/dist-KLSB6YHV.mjs +0 -6964
- package/dist/dist-LKIOZQ42.mjs +0 -17
- package/dist/dist-UYA4RJUH.mjs +0 -2792
- package/dist/dist-ZYHCBILM.mjs +0 -6993
- package/dist/index.d.mts +0 -23
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -25531
- package/dist/index.mjs +0 -18
- package/dist/src-APP5P3UD.mjs +0 -1386
- package/dist/src-D5HMDDVE.mjs +0 -1324
- package/dist/src-EK3WD4AU.mjs +0 -1327
- package/dist/src-LSZFLMFN.mjs +0 -1400
- package/dist/src-T77DFTFP.mjs +0 -1407
- package/dist/src-WIOCZRAC.mjs +0 -1397
- 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
|
+
}
|