@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.
Files changed (148) 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-ITBO7BKI.mjs +0 -1243
  74. package/dist/chunk-J33O35WX.mjs +0 -532
  75. package/dist/chunk-N5JTPB6U.mjs +0 -820
  76. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  77. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  78. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  79. package/dist/chunk-QO2JONHP.mjs +0 -1131
  80. package/dist/chunk-RVAPILHA.mjs +0 -1242
  81. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  82. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  83. package/dist/chunk-SOFST2PV.mjs +0 -1242
  84. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  85. package/dist/chunk-TMZWETMH.mjs +0 -1242
  86. package/dist/chunk-TYD7NMMI.mjs +0 -581
  87. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  88. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  89. package/dist/chunk-UQZKODNW.mjs +0 -1124
  90. package/dist/chunk-USC6R2ON.mjs +0 -1242
  91. package/dist/chunk-W32EQCVC.mjs +0 -823
  92. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  93. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  94. package/dist/cli-2AIWTL6F.mjs +0 -8
  95. package/dist/cli-2QKJ5UUL.mjs +0 -8
  96. package/dist/cli-4RIS6DQX.mjs +0 -8
  97. package/dist/cli-5RH4VBBL.mjs +0 -7
  98. package/dist/cli-7MK4YGOP.mjs +0 -7
  99. package/dist/cli-B4KH6MZI.mjs +0 -8
  100. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  101. package/dist/cli-CVP26EL2.mjs +0 -8
  102. package/dist/cli-DDRVVNAV.mjs +0 -8
  103. package/dist/cli-E7U56QVQ.mjs +0 -8
  104. package/dist/cli-EQNRMLL3.mjs +0 -8
  105. package/dist/cli-F5RUHHH4.mjs +0 -8
  106. package/dist/cli-LX6FFSEF.mjs +0 -8
  107. package/dist/cli-LY74GWKR.mjs +0 -6
  108. package/dist/cli-MAT3ZJHI.mjs +0 -8
  109. package/dist/cli-NJXXTQYF.mjs +0 -8
  110. package/dist/cli-O4ZGFAZG.mjs +0 -8
  111. package/dist/cli-ORVLI3UQ.mjs +0 -8
  112. package/dist/cli-PV43ZVKA.mjs +0 -8
  113. package/dist/cli-REVD6ISM.mjs +0 -8
  114. package/dist/cli-TBX76KQX.mjs +0 -8
  115. package/dist/cli-THCGF7SQ.mjs +0 -8
  116. package/dist/cli-TLX5ENVM.mjs +0 -8
  117. package/dist/cli-TMNI5ZYE.mjs +0 -8
  118. package/dist/cli-TNJHCBQA.mjs +0 -6
  119. package/dist/cli-TUX22CZP.mjs +0 -8
  120. package/dist/cli-XJVH7EEP.mjs +0 -8
  121. package/dist/cli-XXOW4VXJ.mjs +0 -8
  122. package/dist/cli-XZ5RESNB.mjs +0 -6
  123. package/dist/cli-YCBYZ76Q.mjs +0 -8
  124. package/dist/cli-ZLMQCU7X.mjs +0 -8
  125. package/dist/dist-2VGKJRBH.mjs +0 -6820
  126. package/dist/dist-37BNX4QG.mjs +0 -7081
  127. package/dist/dist-7LTHRYKA.mjs +0 -11569
  128. package/dist/dist-7XJPQW5C.mjs +0 -6950
  129. package/dist/dist-AYMVOW7T.mjs +0 -7123
  130. package/dist/dist-BHUWCDRS.mjs +0 -7132
  131. package/dist/dist-FAXRJMEN.mjs +0 -6812
  132. package/dist/dist-HQGANM3P.mjs +0 -6976
  133. package/dist/dist-KATLOZQV.mjs +0 -7054
  134. package/dist/dist-KLSB6YHV.mjs +0 -6964
  135. package/dist/dist-LKIOZQ42.mjs +0 -17
  136. package/dist/dist-UYA4RJUH.mjs +0 -2792
  137. package/dist/dist-ZYHCBILM.mjs +0 -6993
  138. package/dist/index.d.mts +0 -23
  139. package/dist/index.d.ts +0 -23
  140. package/dist/index.js +0 -25531
  141. package/dist/index.mjs +0 -18
  142. package/dist/src-APP5P3UD.mjs +0 -1386
  143. package/dist/src-D5HMDDVE.mjs +0 -1324
  144. package/dist/src-EK3WD4AU.mjs +0 -1327
  145. package/dist/src-LSZFLMFN.mjs +0 -1400
  146. package/dist/src-T77DFTFP.mjs +0 -1407
  147. package/dist/src-WIOCZRAC.mjs +0 -1397
  148. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,428 @@
1
+ import { p, handleCancel, withSpinner } from '../ui.js';
2
+ import { getMoxxyHome } from './init.js';
3
+ import { execSync, spawn } from 'node:child_process';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir, platform } from 'node:os';
7
+
8
+ const PLIST_LABEL = 'ai.moxxy.gateway';
9
+ const SYSTEMD_UNIT = 'moxxy-gateway.service';
10
+
11
+ function normalizeApiUrl(apiUrl) {
12
+ const raw = (apiUrl || '').trim();
13
+ if (!raw) return 'http://localhost:3000';
14
+ const withoutTrailingSlash = raw.replace(/\/+$/, '');
15
+ const withoutV1Suffix = withoutTrailingSlash.replace(/\/v1$/i, '');
16
+ return withoutV1Suffix || withoutTrailingSlash;
17
+ }
18
+
19
+ function paths() {
20
+ const home = getMoxxyHome();
21
+ const binName = platform() === 'win32' ? 'moxxy-gateway.exe' : 'moxxy-gateway';
22
+ const bin = join(home, 'bin', binName);
23
+ const logDir = join(home, 'logs');
24
+ const logFile = join(logDir, 'gateway.log');
25
+ const pidFile = join(home, 'gateway.pid');
26
+ return { home, bin, logDir, logFile, pidFile };
27
+ }
28
+
29
+ function findBinary() {
30
+ const { bin } = paths();
31
+ if (existsSync(bin)) return bin;
32
+
33
+ // Check PATH
34
+ const cmd = platform() === 'win32' ? 'where moxxy-gateway' : 'which moxxy-gateway';
35
+ try {
36
+ const found = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
37
+ if (found) return found.split('\n')[0]; // `where` on Windows may return multiple lines
38
+ } catch { /* not on PATH */ }
39
+
40
+ return null;
41
+ }
42
+
43
+ // --- Platform-specific service management ---
44
+
45
+ function plistPath() {
46
+ return join(homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
47
+ }
48
+
49
+ function systemdUnitPath() {
50
+ return join(homedir(), '.config', 'systemd', 'user', SYSTEMD_UNIT);
51
+ }
52
+
53
+ function generatePlist(binaryPath) {
54
+ const { logFile, logDir } = paths();
55
+ mkdirSync(logDir, { recursive: true });
56
+
57
+ return `<?xml version="1.0" encoding="UTF-8"?>
58
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
59
+ <plist version="1.0">
60
+ <dict>
61
+ <key>Label</key>
62
+ <string>${PLIST_LABEL}</string>
63
+ <key>ProgramArguments</key>
64
+ <array>
65
+ <string>${binaryPath}</string>
66
+ </array>
67
+ <key>RunAtLoad</key>
68
+ <false/>
69
+ <key>KeepAlive</key>
70
+ <false/>
71
+ <key>StandardOutPath</key>
72
+ <string>${logFile}</string>
73
+ <key>StandardErrorPath</key>
74
+ <string>${logFile}</string>
75
+ <key>EnvironmentVariables</key>
76
+ <dict>
77
+ <key>MOXXY_HOME</key>
78
+ <string>${getMoxxyHome()}</string>
79
+ </dict>
80
+ </dict>
81
+ </plist>`;
82
+ }
83
+
84
+ function generateSystemdUnit(binaryPath) {
85
+ const { logFile, logDir } = paths();
86
+ mkdirSync(logDir, { recursive: true });
87
+
88
+ return `[Unit]
89
+ Description=Moxxy Gateway
90
+ After=network.target
91
+
92
+ [Service]
93
+ Type=simple
94
+ ExecStart=${binaryPath}
95
+ Environment=MOXXY_HOME=${getMoxxyHome()}
96
+ StandardOutput=append:${logFile}
97
+ StandardError=append:${logFile}
98
+ Restart=on-failure
99
+ RestartSec=5
100
+
101
+ [Install]
102
+ WantedBy=default.target`;
103
+ }
104
+
105
+ // --- Actions ---
106
+
107
+ async function startGateway() {
108
+ const binary = findBinary();
109
+ if (!binary) {
110
+ p.log.error('moxxy-gateway binary not found.');
111
+ p.log.info('Install it with: moxxy init');
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+
116
+ const os = platform();
117
+
118
+ if (os === 'darwin') {
119
+ await startLaunchd(binary);
120
+ } else if (os === 'linux') {
121
+ await startSystemd(binary);
122
+ } else {
123
+ await startFallback(binary);
124
+ }
125
+ }
126
+
127
+ async function startLaunchd(binary) {
128
+ const plist = plistPath();
129
+ const dir = join(homedir(), 'Library', 'LaunchAgents');
130
+ mkdirSync(dir, { recursive: true });
131
+ writeFileSync(plist, generatePlist(binary));
132
+
133
+ try {
134
+ // Bootout first in case it's already loaded
135
+ execSync(`launchctl bootout gui/$(id -u) ${plist} 2>/dev/null`, { stdio: 'ignore' });
136
+ } catch { /* not loaded, fine */ }
137
+
138
+ execSync(`launchctl bootstrap gui/$(id -u) ${plist}`);
139
+ execSync(`launchctl kickstart -k gui/$(id -u)/${PLIST_LABEL}`);
140
+
141
+ await verifyStarted();
142
+ }
143
+
144
+ async function startSystemd(binary) {
145
+ const unitPath = systemdUnitPath();
146
+ const dir = join(homedir(), '.config', 'systemd', 'user');
147
+ mkdirSync(dir, { recursive: true });
148
+ writeFileSync(unitPath, generateSystemdUnit(binary));
149
+
150
+ execSync('systemctl --user daemon-reload');
151
+ execSync(`systemctl --user start ${SYSTEMD_UNIT}`);
152
+
153
+ await verifyStarted();
154
+ }
155
+
156
+ async function startFallback(binary) {
157
+ const { logDir, logFile, pidFile } = paths();
158
+ mkdirSync(logDir, { recursive: true });
159
+
160
+ const { openSync } = await import('node:fs');
161
+ const logFd = openSync(logFile, 'a');
162
+
163
+ const env = { ...process.env, MOXXY_HOME: getMoxxyHome() };
164
+ const child = spawn(binary, [], {
165
+ detached: true,
166
+ stdio: ['ignore', logFd, logFd],
167
+ env,
168
+ });
169
+
170
+ writeFileSync(pidFile, String(child.pid));
171
+ child.unref();
172
+
173
+ await verifyStarted();
174
+ }
175
+
176
+ async function stopGateway() {
177
+ const os = platform();
178
+
179
+ if (os === 'darwin') {
180
+ await stopLaunchd();
181
+ } else if (os === 'linux') {
182
+ await stopSystemd();
183
+ } else {
184
+ await stopFallback();
185
+ }
186
+ }
187
+
188
+ async function stopLaunchd() {
189
+ const plist = plistPath();
190
+
191
+ try {
192
+ execSync(`launchctl bootout gui/$(id -u) ${plist}`);
193
+ p.log.success('Gateway stopped.');
194
+ } catch {
195
+ p.log.warn('Gateway service not running or not loaded.');
196
+ }
197
+ }
198
+
199
+ async function stopSystemd() {
200
+ try {
201
+ execSync(`systemctl --user stop ${SYSTEMD_UNIT}`);
202
+ p.log.success('Gateway stopped.');
203
+ } catch {
204
+ p.log.warn('Gateway service not running.');
205
+ }
206
+ }
207
+
208
+ async function stopFallback() {
209
+ const { pidFile } = paths();
210
+
211
+ if (!existsSync(pidFile)) {
212
+ p.log.warn('No PID file found. Gateway may not be running.');
213
+ return;
214
+ }
215
+
216
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
217
+
218
+ try {
219
+ process.kill(pid, 'SIGTERM');
220
+ p.log.success(`Gateway stopped (PID ${pid}).`);
221
+ } catch (err) {
222
+ if (err.code === 'ESRCH') {
223
+ p.log.warn(`Process ${pid} not found. Cleaning up stale PID file.`);
224
+ } else {
225
+ throw err;
226
+ }
227
+ }
228
+
229
+ try { unlinkSync(pidFile); } catch { /* already gone */ }
230
+ }
231
+
232
+ async function restartGateway() {
233
+ p.log.step('Stopping gateway...');
234
+ await stopGateway();
235
+
236
+ // Brief pause to let the port release
237
+ await new Promise(r => setTimeout(r, 1000));
238
+
239
+ p.log.step('Starting gateway...');
240
+ await startGateway();
241
+ }
242
+
243
+ async function probeGatewayHealth(apiUrl, timeoutMs = 2000) {
244
+ const normalizedApiUrl = normalizeApiUrl(apiUrl);
245
+ try {
246
+ const resp = await fetch(`${normalizedApiUrl}/v1/health`, { signal: AbortSignal.timeout(timeoutMs) });
247
+ const text = await resp.text().catch(() => '');
248
+ let payload = null;
249
+ try {
250
+ payload = text ? JSON.parse(text) : null;
251
+ } catch { /* ignore non-json health body */ }
252
+
253
+ const isHealthy = resp.ok && payload?.status === 'healthy';
254
+ if (isHealthy) {
255
+ return { ok: true, status: resp.status, reason: '' };
256
+ }
257
+
258
+ const reason = payload?.message
259
+ || payload?.status
260
+ || (resp.status === 404 ? 'endpoint /v1/health not found' : `HTTP ${resp.status}`);
261
+ return { ok: false, status: resp.status, reason };
262
+ } catch (err) {
263
+ return { ok: false, status: 0, reason: err?.message || 'not reachable' };
264
+ }
265
+ }
266
+
267
+ async function gatewayStatus() {
268
+ const os = platform();
269
+ let serviceRunning = false;
270
+ let pid = null;
271
+
272
+ if (os === 'darwin') {
273
+ try {
274
+ const out = execSync(`launchctl print gui/$(id -u)/${PLIST_LABEL} 2>/dev/null`, { encoding: 'utf-8' });
275
+ const pidMatch = out.match(/pid\s*=\s*(\d+)/);
276
+ if (pidMatch) {
277
+ pid = pidMatch[1];
278
+ serviceRunning = true;
279
+ }
280
+ } catch { /* not loaded */ }
281
+ } else if (os === 'linux') {
282
+ try {
283
+ execSync(`systemctl --user is-active ${SYSTEMD_UNIT}`, { stdio: 'ignore' });
284
+ serviceRunning = true;
285
+ const out = execSync(`systemctl --user show ${SYSTEMD_UNIT} --property=MainPID`, { encoding: 'utf-8' });
286
+ const pidMatch = out.match(/MainPID=(\d+)/);
287
+ if (pidMatch && pidMatch[1] !== '0') pid = pidMatch[1];
288
+ } catch { /* not active */ }
289
+ }
290
+
291
+ // Fallback: check PID file
292
+ if (!serviceRunning) {
293
+ const { pidFile } = paths();
294
+ if (existsSync(pidFile)) {
295
+ pid = readFileSync(pidFile, 'utf-8').trim();
296
+ try {
297
+ process.kill(parseInt(pid, 10), 0);
298
+ serviceRunning = true;
299
+ } catch {
300
+ serviceRunning = false;
301
+ pid = null;
302
+ }
303
+ }
304
+ }
305
+
306
+ const apiUrl = normalizeApiUrl(process.env.MOXXY_API_URL || 'http://localhost:3000');
307
+ const health = await probeGatewayHealth(apiUrl);
308
+
309
+ if (serviceRunning) {
310
+ p.log.success(`Gateway is running${pid ? ` (PID ${pid})` : ''}`);
311
+ } else {
312
+ p.log.warn('Gateway is not running.');
313
+ }
314
+
315
+ if (health.ok) {
316
+ p.log.success(`Health check: reachable at ${apiUrl}`);
317
+ } else {
318
+ p.log.warn(`Health check: not healthy at ${apiUrl}${health.reason ? ` (${health.reason})` : ''}`);
319
+ }
320
+
321
+ const binary = findBinary();
322
+ if (binary) {
323
+ p.log.info(`Binary: ${binary}`);
324
+ } else {
325
+ p.log.warn('Binary: not found');
326
+ }
327
+ }
328
+
329
+ async function gatewayLogs() {
330
+ const { logFile } = paths();
331
+
332
+ if (!existsSync(logFile)) {
333
+ p.log.warn(`No log file found at ${logFile}`);
334
+ return;
335
+ }
336
+
337
+ p.log.info(`Tailing ${logFile} (Ctrl+C to stop)`);
338
+
339
+ let tail;
340
+ if (platform() === 'win32') {
341
+ tail = spawn('powershell', ['-Command', `Get-Content -Path "${logFile}" -Wait -Tail 50`], { stdio: 'inherit' });
342
+ } else {
343
+ tail = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
344
+ }
345
+
346
+ await new Promise((resolve) => {
347
+ process.on('SIGINT', () => {
348
+ tail.kill();
349
+ resolve();
350
+ });
351
+ tail.on('close', resolve);
352
+ });
353
+ }
354
+
355
+ // --- Verify health after start ---
356
+
357
+ async function verifyStarted() {
358
+ const apiUrl = normalizeApiUrl(process.env.MOXXY_API_URL || 'http://localhost:3000');
359
+ let ok = false;
360
+ let lastReason = '';
361
+
362
+ for (let i = 0; i < 10; i++) {
363
+ await new Promise(r => setTimeout(r, 500));
364
+ const health = await probeGatewayHealth(apiUrl, 1000);
365
+ if (health.ok) {
366
+ ok = true;
367
+ break;
368
+ }
369
+ lastReason = health.reason || '';
370
+ }
371
+
372
+ if (ok) {
373
+ p.log.success(`Gateway started and listening at ${apiUrl}`);
374
+ } else {
375
+ p.log.warn('Gateway process started but health check failed.');
376
+ if (lastReason) {
377
+ p.log.warn(`Last health probe error: ${lastReason}`);
378
+ }
379
+ p.log.info('Check logs with: moxxy gateway logs');
380
+ }
381
+ }
382
+
383
+ // --- Command router ---
384
+
385
+ export { startGateway, stopGateway, findBinary, paths };
386
+
387
+ export async function runGateway(client, args) {
388
+ const sub = args[0];
389
+
390
+ switch (sub) {
391
+ case 'start':
392
+ await startGateway();
393
+ break;
394
+ case 'stop':
395
+ await stopGateway();
396
+ break;
397
+ case 'restart':
398
+ await restartGateway();
399
+ break;
400
+ case 'status':
401
+ await gatewayStatus();
402
+ break;
403
+ case 'logs':
404
+ await gatewayLogs();
405
+ break;
406
+ default:
407
+ if (!sub && (await import('../ui.js')).isInteractive()) {
408
+ const action = await p.select({
409
+ message: 'Gateway action',
410
+ options: [
411
+ { value: 'start', label: 'Start', hint: 'start the gateway' },
412
+ { value: 'stop', label: 'Stop', hint: 'stop the gateway' },
413
+ { value: 'restart', label: 'Restart', hint: 'restart the gateway' },
414
+ { value: 'status', label: 'Status', hint: 'show gateway status' },
415
+ { value: 'logs', label: 'Logs', hint: 'tail gateway logs' },
416
+ ],
417
+ });
418
+ handleCancel(action);
419
+ await runGateway(client, [action]);
420
+ } else {
421
+ const { showHelp } = await import('../help.js');
422
+ if (sub) {
423
+ p.log.error(`Unknown gateway action: ${sub}`);
424
+ }
425
+ showHelp('gateway', p);
426
+ }
427
+ }
428
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Heartbeat commands: set/list/disable.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, p } from '../ui.js';
6
+
7
+ export async function runHeartbeat(client, args) {
8
+ let [action, ...rest] = args;
9
+ const flags = parseFlags(rest);
10
+
11
+ // Interactive sub-menu when no valid action
12
+ if (!['set', 'list', 'disable'].includes(action) && isInteractive()) {
13
+ action = await p.select({
14
+ message: 'Heartbeat action',
15
+ options: [
16
+ { value: 'set', label: 'Set heartbeat', hint: 'configure heartbeat rule' },
17
+ { value: 'list', label: 'List heartbeats', hint: 'show heartbeat rules' },
18
+ { value: 'disable', label: 'Disable heartbeat', hint: 'turn off heartbeat' },
19
+ ],
20
+ });
21
+ handleCancel(action);
22
+ }
23
+
24
+ switch (action) {
25
+ case 'set': {
26
+ let agentId = flags.agent;
27
+
28
+ // Interactive wizard when missing agent
29
+ if (!agentId && isInteractive()) {
30
+ agentId = await pickAgent(client, 'Select agent for heartbeat');
31
+
32
+ const intervalInput = handleCancel(await p.text({
33
+ message: 'Interval in minutes',
34
+ placeholder: '5',
35
+ initialValue: flags.interval || '5',
36
+ validate: (val) => { if (!val || isNaN(parseInt(val, 10))) return 'Must be a number'; },
37
+ }));
38
+ const interval = parseInt(intervalInput, 10);
39
+
40
+ const actionType = handleCancel(await p.select({
41
+ message: 'Action type',
42
+ options: [
43
+ { value: 'notify_cli', label: 'Notify CLI', hint: 'send notification to CLI' },
44
+ { value: 'webhook', label: 'Webhook', hint: 'call external webhook' },
45
+ { value: 'restart', label: 'Restart', hint: 'restart the agent' },
46
+ ],
47
+ }));
48
+
49
+ const body = {
50
+ interval_minutes: interval,
51
+ action_type: actionType,
52
+ };
53
+
54
+ if (actionType === 'webhook') {
55
+ const payload = handleCancel(await p.text({
56
+ message: 'Webhook URL or payload',
57
+ placeholder: 'https://...',
58
+ }));
59
+ if (payload) body.action_payload = payload;
60
+ }
61
+
62
+ const result = await withSpinner('Setting heartbeat...', () =>
63
+ client.request(`/v1/agents/${encodeURIComponent(agentId)}/heartbeats`, 'POST', body), 'Heartbeat configured.');
64
+
65
+ showResult('Heartbeat Set', {
66
+ Agent: agentId,
67
+ Interval: `${interval} min`,
68
+ Action: actionType,
69
+ });
70
+
71
+ return result;
72
+ }
73
+
74
+ if (!agentId) throw new Error('Required: --agent');
75
+ const body = {
76
+ interval_minutes: parseInt(flags.interval || '5', 10),
77
+ action_type: flags.action_type || 'notify_cli',
78
+ };
79
+ if (flags.payload) body.action_payload = flags.payload;
80
+ const result = await client.request(`/v1/agents/${encodeURIComponent(agentId)}/heartbeats`, 'POST', body);
81
+ console.log(JSON.stringify(result, null, 2));
82
+ return result;
83
+ }
84
+
85
+ case 'list': {
86
+ const agentId = flags.agent;
87
+ if (!agentId && isInteractive()) {
88
+ const id = await pickAgent(client, 'Select agent to list heartbeats');
89
+ const result = await withSpinner('Fetching heartbeats...', () =>
90
+ client.request(`/v1/agents/${encodeURIComponent(id)}/heartbeats`, 'GET'), 'Heartbeats loaded.');
91
+ console.log(JSON.stringify(result, null, 2));
92
+ return result;
93
+ }
94
+ if (!agentId) throw new Error('Required: --agent');
95
+ const result = await client.request(`/v1/agents/${encodeURIComponent(agentId)}/heartbeats`, 'GET');
96
+ console.log(JSON.stringify(result, null, 2));
97
+ return result;
98
+ }
99
+
100
+ case 'disable': {
101
+ let agentId = flags.agent;
102
+ let heartbeatId = flags.id;
103
+
104
+ if ((!agentId || !heartbeatId) && isInteractive()) {
105
+ if (!agentId) agentId = await pickAgent(client, 'Select agent');
106
+
107
+ if (!heartbeatId) {
108
+ const heartbeats = await withSpinner('Fetching heartbeats...', () =>
109
+ client.request(`/v1/agents/${encodeURIComponent(agentId)}/heartbeats`, 'GET'), 'Loaded.');
110
+
111
+ if (!heartbeats || heartbeats.length === 0) {
112
+ p.log.warn('No heartbeats found for this agent.');
113
+ return;
114
+ }
115
+
116
+ heartbeatId = handleCancel(await p.select({
117
+ message: 'Select heartbeat to disable',
118
+ options: heartbeats.map(h => ({
119
+ value: h.id,
120
+ label: `${h.action_type} every ${h.interval_minutes}m`,
121
+ hint: h.id.slice(0, 12),
122
+ })),
123
+ }));
124
+ }
125
+ }
126
+
127
+ if (!agentId || !heartbeatId) throw new Error('Required: --agent, --id');
128
+
129
+ if (isInteractive()) {
130
+ await withSpinner('Disabling heartbeat...', () =>
131
+ client.disableHeartbeat(agentId, heartbeatId), 'Heartbeat disabled.');
132
+ } else {
133
+ await client.disableHeartbeat(agentId, heartbeatId);
134
+ console.log(`Heartbeat ${heartbeatId} disabled.`);
135
+ }
136
+ break;
137
+ }
138
+
139
+ default: {
140
+ const { showHelp } = await import('../help.js');
141
+ showHelp('heartbeat', p);
142
+ break;
143
+ }
144
+ }
145
+ }