@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.
Files changed (149) 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 +349 -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 +954 -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/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. 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
+ }