@moxxy/cli 0.0.12 → 0.1.1
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 +349 -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 +954 -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/settings.js +224 -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 +392 -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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall command: remove all Moxxy data from the system.
|
|
3
|
+
* Removes ~/.moxxy (database, agents, config) but does NOT remove the CLI package.
|
|
4
|
+
*/
|
|
5
|
+
import { p, handleCancel } from '../ui.js';
|
|
6
|
+
import { LOGO } from '../cli.js';
|
|
7
|
+
import { getMoxxyHome } from './init.js';
|
|
8
|
+
import { shellUnsetInstruction, shellProfileName } from '../platform.js';
|
|
9
|
+
import { existsSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import { platform } from 'node:os';
|
|
13
|
+
|
|
14
|
+
export async function runUninstall(client, args) {
|
|
15
|
+
console.log(LOGO);
|
|
16
|
+
p.intro('Uninstall Moxxy');
|
|
17
|
+
|
|
18
|
+
const moxxyHome = getMoxxyHome();
|
|
19
|
+
|
|
20
|
+
// ── 1. Show what will be removed ──
|
|
21
|
+
|
|
22
|
+
const items = [];
|
|
23
|
+
|
|
24
|
+
if (existsSync(moxxyHome)) {
|
|
25
|
+
const size = getDirSize(moxxyHome);
|
|
26
|
+
items.push(`${moxxyHome} (${formatBytes(size)})`);
|
|
27
|
+
|
|
28
|
+
// List contents for visibility
|
|
29
|
+
if (existsSync(join(moxxyHome, 'moxxy.db'))) {
|
|
30
|
+
items.push(' - Database (moxxy.db)');
|
|
31
|
+
}
|
|
32
|
+
if (existsSync(join(moxxyHome, 'agents'))) {
|
|
33
|
+
const agents = readdirSync(join(moxxyHome, 'agents'));
|
|
34
|
+
items.push(` - ${agents.length} agent workspace(s)`);
|
|
35
|
+
}
|
|
36
|
+
if (existsSync(join(moxxyHome, 'config'))) {
|
|
37
|
+
items.push(' - Configuration files');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
p.log.info(`Moxxy home not found at ${moxxyHome}. Nothing to remove.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for running gateway
|
|
44
|
+
let gatewayRunning = false;
|
|
45
|
+
try {
|
|
46
|
+
const resp = await fetch(`${client.baseUrl}/v1/providers`);
|
|
47
|
+
if (resp) gatewayRunning = true;
|
|
48
|
+
} catch {
|
|
49
|
+
// Not running
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (gatewayRunning) {
|
|
53
|
+
p.log.warn('Gateway is currently running. It should be stopped first.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (items.length === 0 && !gatewayRunning) {
|
|
57
|
+
p.outro('Nothing to uninstall.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── 2. Show removal summary ──
|
|
62
|
+
|
|
63
|
+
if (items.length > 0) {
|
|
64
|
+
p.note(items.join('\n'), 'The following will be permanently deleted');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 3. Confirm ──
|
|
68
|
+
|
|
69
|
+
const confirmed = await p.confirm({
|
|
70
|
+
message: 'Are you sure you want to remove all Moxxy data? This cannot be undone.',
|
|
71
|
+
initialValue: false,
|
|
72
|
+
});
|
|
73
|
+
handleCancel(confirmed);
|
|
74
|
+
|
|
75
|
+
if (!confirmed) {
|
|
76
|
+
p.outro('Uninstall cancelled.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Double confirmation for safety
|
|
81
|
+
const reallyConfirmed = await p.confirm({
|
|
82
|
+
message: 'Really delete everything? Type Yes to confirm.',
|
|
83
|
+
initialValue: false,
|
|
84
|
+
});
|
|
85
|
+
handleCancel(reallyConfirmed);
|
|
86
|
+
|
|
87
|
+
if (!reallyConfirmed) {
|
|
88
|
+
p.outro('Uninstall cancelled.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 4. Stop gateway if running ──
|
|
93
|
+
|
|
94
|
+
if (gatewayRunning) {
|
|
95
|
+
p.log.step('Stopping gateway...');
|
|
96
|
+
try {
|
|
97
|
+
if (platform() === 'win32') {
|
|
98
|
+
// Windows: use netstat + taskkill
|
|
99
|
+
const out = execSync('netstat -ano | findstr :3000', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
100
|
+
const pids = new Set();
|
|
101
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
102
|
+
const parts = line.trim().split(/\s+/);
|
|
103
|
+
const pid = parts[parts.length - 1];
|
|
104
|
+
if (pid && pid !== '0') pids.add(pid);
|
|
105
|
+
}
|
|
106
|
+
for (const pid of pids) {
|
|
107
|
+
try { execSync(`taskkill /PID ${pid} /F`, { stdio: 'pipe' }); } catch { /* already dead */ }
|
|
108
|
+
}
|
|
109
|
+
if (pids.size > 0) p.log.success('Gateway stopped.');
|
|
110
|
+
} else {
|
|
111
|
+
// Unix: use lsof
|
|
112
|
+
const pids = execSync("lsof -ti:3000 2>/dev/null || true", { encoding: 'utf-8' }).trim();
|
|
113
|
+
if (pids) {
|
|
114
|
+
for (const pid of pids.split('\n').filter(Boolean)) {
|
|
115
|
+
try { process.kill(parseInt(pid), 'SIGTERM'); } catch { /* already dead */ }
|
|
116
|
+
}
|
|
117
|
+
p.log.success('Gateway stopped.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
p.log.warn('Could not stop gateway automatically. Please stop it manually.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 4b. Stop running plugins ──
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { pluginPaths, readRegistry, isProcessAlive } = await import('../lib/plugin-registry.js');
|
|
129
|
+
const { registryFile } = pluginPaths();
|
|
130
|
+
if (existsSync(registryFile)) {
|
|
131
|
+
const registry = readRegistry();
|
|
132
|
+
for (const plug of Object.values(registry.plugins)) {
|
|
133
|
+
if (plug.pid && isProcessAlive(plug.pid)) {
|
|
134
|
+
try {
|
|
135
|
+
process.kill(plug.pid, 'SIGTERM');
|
|
136
|
+
p.log.info(`Stopped plugin: ${plug.name} (PID ${plug.pid})`);
|
|
137
|
+
} catch { /* already dead */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch { /* no plugins */ }
|
|
142
|
+
|
|
143
|
+
// ── 5. Remove ~/.moxxy ──
|
|
144
|
+
|
|
145
|
+
if (existsSync(moxxyHome)) {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(moxxyHome, { recursive: true, force: true });
|
|
148
|
+
p.log.success(`Removed ${moxxyHome}`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
p.log.error(`Failed to remove ${moxxyHome}: ${err.message}`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── 6. Clean environment reminder ──
|
|
157
|
+
|
|
158
|
+
const envVars = ['MOXXY_TOKEN', 'MOXXY_API_URL', 'MOXXY_HOME'].filter(v => process.env[v]);
|
|
159
|
+
|
|
160
|
+
const instructions = [];
|
|
161
|
+
if (envVars.length > 0) {
|
|
162
|
+
instructions.push(`Remove these from ${shellProfileName()}:`);
|
|
163
|
+
for (const v of envVars) {
|
|
164
|
+
instructions.push(` ${shellUnsetInstruction(v)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
instructions.push('');
|
|
168
|
+
instructions.push('To remove the CLI itself:');
|
|
169
|
+
instructions.push(' npm remove -g moxxy-cli');
|
|
170
|
+
|
|
171
|
+
p.note(instructions.join('\n'), 'Manual cleanup');
|
|
172
|
+
|
|
173
|
+
p.outro('Moxxy has been uninstalled. Goodbye!');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getDirSize(dirPath) {
|
|
177
|
+
let size = 0;
|
|
178
|
+
try {
|
|
179
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const fullPath = join(dirPath, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
size += getDirSize(fullPath);
|
|
184
|
+
} else {
|
|
185
|
+
try { size += statSync(fullPath).size; } catch { /* skip */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch { /* skip */ }
|
|
189
|
+
return size;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatBytes(bytes) {
|
|
193
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
194
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
195
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
196
|
+
}
|
|
@@ -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
|
+
}
|