@oussema_mili/test-pkg-123 1.1.22

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.

Potentially problematic release.


This version of @oussema_mili/test-pkg-123 might be problematic. Click here for more details.

Files changed (49) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +220 -0
  3. package/auth-callback.html +97 -0
  4. package/auth.js +276 -0
  5. package/cli-commands.js +1923 -0
  6. package/containerManager.js +304 -0
  7. package/daemon/agentRunner.js +429 -0
  8. package/daemon/daemonEntry.js +64 -0
  9. package/daemon/daemonManager.js +271 -0
  10. package/daemon/logManager.js +227 -0
  11. package/dist/styles.css +504 -0
  12. package/docker-actions/apps.js +3938 -0
  13. package/docker-actions/config-transformer.js +380 -0
  14. package/docker-actions/containers.js +355 -0
  15. package/docker-actions/general.js +171 -0
  16. package/docker-actions/images.js +1128 -0
  17. package/docker-actions/logs.js +224 -0
  18. package/docker-actions/metrics.js +270 -0
  19. package/docker-actions/registry.js +1100 -0
  20. package/docker-actions/setup-tasks.js +859 -0
  21. package/docker-actions/terminal.js +247 -0
  22. package/docker-actions/volumes.js +696 -0
  23. package/helper-functions.js +193 -0
  24. package/index.html +83 -0
  25. package/index.js +341 -0
  26. package/package.json +82 -0
  27. package/postcss.config.mjs +5 -0
  28. package/scripts/release.sh +212 -0
  29. package/setup/setupWizard.js +403 -0
  30. package/store/agentSessionStore.js +51 -0
  31. package/store/agentStore.js +113 -0
  32. package/store/configStore.js +171 -0
  33. package/store/daemonStore.js +217 -0
  34. package/store/deviceCredentialStore.js +107 -0
  35. package/store/npmTokenStore.js +65 -0
  36. package/store/registryStore.js +329 -0
  37. package/store/setupState.js +147 -0
  38. package/styles.css +1 -0
  39. package/utils/appLogger.js +223 -0
  40. package/utils/deviceInfo.js +98 -0
  41. package/utils/ecrAuth.js +225 -0
  42. package/utils/encryption.js +112 -0
  43. package/utils/envSetup.js +44 -0
  44. package/utils/errorHandler.js +327 -0
  45. package/utils/portUtils.js +59 -0
  46. package/utils/prerequisites.js +323 -0
  47. package/utils/prompts.js +318 -0
  48. package/utils/ssl-certificates.js +256 -0
  49. package/websocket-server.js +415 -0
@@ -0,0 +1,1923 @@
1
+ import os from "os";
2
+ import http from "http";
3
+ import Table from "cli-table3";
4
+ import net from "net";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import axios from "axios";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import crypto from "crypto";
11
+ import { fileURLToPath } from "url";
12
+ import { dirname } from "path";
13
+ import { spawn } from "child_process";
14
+ import containerManager from "./containerManager.js";
15
+ import registryStore from "./store/registryStore.js";
16
+ import agentStore from "./store/agentStore.js";
17
+ import { docker, formatContainer } from "./docker-actions/containers.js";
18
+ import {
19
+ loadSession,
20
+ isSessionValid,
21
+ clearSession,
22
+ createSession,
23
+ saveSession,
24
+ } from "./auth.js";
25
+ import { hasNpmToken } from "./store/npmTokenStore.js";
26
+ import packageJson from "./package.json" with { type: "json" };
27
+ import {
28
+ formatSize,
29
+ formatCreatedTime,
30
+ formatUptime,
31
+ } from "./helper-functions.js";
32
+ import { ensureEnvironmentFiles } from "./utils/envSetup.js";
33
+ import { loadConfig } from "./store/configStore.js";
34
+ import { runAgent, setupSignalHandlers } from "./daemon/agentRunner.js";
35
+ import {
36
+ startDaemon,
37
+ stopDaemon,
38
+ restartDaemon,
39
+ getDaemonStatus,
40
+ formatUptime as formatDaemonUptime,
41
+ } from "./daemon/daemonManager.js";
42
+ import { readLogs, getLogPath } from "./daemon/logManager.js";
43
+ import { getDaemonState } from "./store/daemonStore.js";
44
+ import dotenv from "dotenv";
45
+ dotenv.config();
46
+
47
+ // Load configuration
48
+ const config = loadConfig();
49
+ const WS_PORT = config.wsPort;
50
+ const BACKEND_URL = config.backendUrl;
51
+ const FRONTEND_URL = config.frontendUrl;
52
+ const AUTH_TIMEOUT_MS = config.authTimeoutMs;
53
+
54
+ // ES module helpers
55
+ const __filename = fileURLToPath(import.meta.url);
56
+ const __dirname = dirname(__filename);
57
+
58
+ /**
59
+ * Check if agent is actually running by trying to connect to its port
60
+ */
61
+ function checkAgentRunning(port = WS_PORT) {
62
+ return new Promise((resolve) => {
63
+ const socket = new net.Socket();
64
+
65
+ socket.setTimeout(1000);
66
+
67
+ socket.on("connect", () => {
68
+ socket.destroy();
69
+ resolve(true);
70
+ });
71
+
72
+ socket.on("timeout", () => {
73
+ socket.destroy();
74
+ resolve(false);
75
+ });
76
+
77
+ socket.on("error", () => {
78
+ resolve(false);
79
+ });
80
+
81
+ socket.connect(port, "localhost");
82
+ });
83
+ }
84
+
85
+ function setupCLICommands(program, startServerFunction) {
86
+ // Login (authentication only - does NOT start the agent)
87
+ program
88
+ .command("login")
89
+ .description("authenticate with Fenwave (does not start the agent)")
90
+ .option("--backend-url <url>", "Fenwave backend URL (overrides config)")
91
+ .option("--frontend-url <url>", "Fenwave frontend URL (overrides config)")
92
+ .option("--save", "Save the provided URLs to agent configuration")
93
+ .action(async (options) => {
94
+ // Determine URLs: CLI options > saved config > defaults
95
+ let backendUrl = options.backendUrl || BACKEND_URL;
96
+ let frontendUrl = options.frontendUrl || FRONTEND_URL;
97
+
98
+ // If --save flag is provided, persist the URLs to config
99
+ if (options.save && (options.backendUrl || options.frontendUrl)) {
100
+ const { updateConfig } = await import("./store/configStore.js");
101
+ const updates = {};
102
+ if (options.backendUrl) updates.backendUrl = options.backendUrl;
103
+ if (options.frontendUrl) updates.frontendUrl = options.frontendUrl;
104
+ updateConfig(updates);
105
+ console.log(chalk.green("Configuration saved!"));
106
+ }
107
+ console.log(chalk.blue("Authenticating with Fenwave..."));
108
+ console.log(chalk.gray(` Backend URL: ${backendUrl}`));
109
+ console.log(chalk.gray(` Frontend URL: ${frontendUrl}`));
110
+
111
+ // Show environment file status during login
112
+ ensureEnvironmentFiles(__dirname, true);
113
+
114
+ // Check for existing valid session
115
+ const existingSession = loadSession();
116
+ if (existingSession && isSessionValid(existingSession)) {
117
+ console.log(chalk.green("Already authenticated!"));
118
+ console.log(chalk.gray(` User: ${existingSession.userEntityRef}`));
119
+ console.log(
120
+ chalk.gray(
121
+ ` Expires: ${new Date(existingSession.expiresAt).toLocaleString()}`,
122
+ ),
123
+ );
124
+ console.log(chalk.yellow("\nTo start the agent, run:"));
125
+ console.log(chalk.cyan(" fenwave service start # Background mode"));
126
+ console.log(chalk.cyan(" fenwave service run # Foreground mode"));
127
+ process.exit(0);
128
+ return;
129
+ }
130
+
131
+ // Pre-check: Test Fenwave connectivity before opening browser
132
+ try {
133
+ await axios.get(`${backendUrl}`, {
134
+ timeout: 5000,
135
+ validateStatus: () => true,
136
+ });
137
+ } catch (connectivityError) {
138
+ console.error(
139
+ chalk.red(`Cannot connect to Fenwave backend at ${backendUrl}`),
140
+ );
141
+ console.log(
142
+ chalk.yellow("Please check the URL and ensure Fenwave is running."),
143
+ );
144
+ console.log(
145
+ chalk.yellow(
146
+ "You can specify a different URL with: fenwave login --backend-url <url> --frontend-url <url>",
147
+ ),
148
+ );
149
+ process.exit(1);
150
+ }
151
+
152
+ console.log(
153
+ chalk.blue("Redirecting to the browser for authentication..."),
154
+ );
155
+
156
+ // Set up authentication timeout
157
+ const authTimeout = setTimeout(() => {
158
+ console.log(chalk.red("Authorization timed out"));
159
+ console.log(
160
+ chalk.yellow(
161
+ "Please ensure Fenwave is running and you are properly authenticated, then try again.",
162
+ ),
163
+ );
164
+ process.exit(1);
165
+ }, AUTH_TIMEOUT_MS);
166
+
167
+ // Start a local HTTP server to handle the loopback redirect on a random available port
168
+ const loopbackServer = http.createServer(async (req, res) => {
169
+ const url = new URL(req.url, `http://${req.headers.host}`);
170
+
171
+ if (url.pathname === "/auth-callback" && req.method === "GET") {
172
+ // Serve the auth callback HTML page that reads credentials from URL fragment
173
+ const htmlPath = path.join(__dirname, "auth-callback.html");
174
+ fs.readFile(htmlPath, "utf8", (err, htmlContent) => {
175
+ if (err) {
176
+ res.writeHead(500, { "Content-Type": "text/plain" });
177
+ res.end("Error loading authentication page");
178
+ return;
179
+ }
180
+ res.writeHead(200, { "Content-Type": "text/html" });
181
+ res.end(htmlContent);
182
+ });
183
+ } else if (url.pathname === "/auth-complete" && req.method === "POST") {
184
+ // Handle POST with credentials from fragment reader page
185
+ let body = "";
186
+ req.on("data", (chunk) => {
187
+ body += chunk;
188
+ });
189
+ req.on("end", async () => {
190
+ try {
191
+ const { jwt, entityRef } = JSON.parse(body);
192
+
193
+ if (!jwt || !entityRef) {
194
+ res.writeHead(400, { "Content-Type": "application/json" });
195
+ res.end(
196
+ JSON.stringify({
197
+ success: false,
198
+ error: "Missing JWT or user information",
199
+ }),
200
+ );
201
+ return;
202
+ }
203
+
204
+ clearTimeout(authTimeout);
205
+ console.log(chalk.green("Authentication successful!"));
206
+
207
+ // Create session with Fenwave backend
208
+ console.log(
209
+ chalk.blue("Creating session with Fenwave backend..."),
210
+ );
211
+ const sessionData = await createSession(jwt, backendUrl);
212
+
213
+ // Save session locally
214
+ saveSession(sessionData.token, sessionData.expiresAt, entityRef);
215
+
216
+ // Generate and save WebSocket token for later use
217
+ const wsToken = crypto.randomBytes(32).toString("hex");
218
+ const wsTokenPath = path.join(
219
+ os.homedir(),
220
+ config.agentRootDir,
221
+ "ws-token",
222
+ );
223
+ const wsTokenDir = path.dirname(wsTokenPath);
224
+ if (!fs.existsSync(wsTokenDir)) {
225
+ fs.mkdirSync(wsTokenDir, { recursive: true, mode: 0o700 });
226
+ }
227
+ fs.writeFileSync(wsTokenPath, wsToken, { mode: 0o600 });
228
+
229
+ // Return success with redirect URL
230
+ const successParams = new URLSearchParams({
231
+ wsToken,
232
+ wsPort: String(config.wsPort),
233
+ containerPort: String(config.containerPort),
234
+ });
235
+
236
+ res.writeHead(200, { "Content-Type": "application/json" });
237
+ res.end(
238
+ JSON.stringify({
239
+ success: true,
240
+ redirectUrl: `/auth-success?${successParams.toString()}`,
241
+ }),
242
+ );
243
+
244
+ // Schedule cleanup after redirect completes
245
+ setTimeout(() => {
246
+ loopbackServer.close();
247
+
248
+ console.log(chalk.green("\n" + "=".repeat(50)));
249
+ console.log(chalk.green(" Authentication Complete"));
250
+ console.log(chalk.green("=".repeat(50) + "\n"));
251
+ console.log(chalk.white(" User:"), chalk.cyan(entityRef));
252
+ console.log(
253
+ chalk.white(" Expires:"),
254
+ chalk.cyan(new Date(sessionData.expiresAt).toLocaleString()),
255
+ );
256
+ console.log(chalk.green("\n" + "=".repeat(50) + "\n"));
257
+ console.log(chalk.yellow("To start the agent, run one of:"));
258
+ console.log(
259
+ chalk.cyan(
260
+ " fenwave service start # Background daemon mode",
261
+ ),
262
+ );
263
+ console.log(
264
+ chalk.cyan(
265
+ " fenwave service run # Foreground mode (for development)\n",
266
+ ),
267
+ );
268
+
269
+ process.exit(0);
270
+ }, 3000);
271
+ } catch (sessionError) {
272
+ console.error(
273
+ chalk.red("Failed to create session:"),
274
+ sessionError.message,
275
+ );
276
+ res.writeHead(500, { "Content-Type": "application/json" });
277
+ res.end(
278
+ JSON.stringify({
279
+ success: false,
280
+ error: `Session creation failed: ${sessionError.message}`,
281
+ }),
282
+ );
283
+ setTimeout(() => {
284
+ loopbackServer.close();
285
+ clearTimeout(authTimeout);
286
+ process.exit(1);
287
+ }, 3000);
288
+ }
289
+ });
290
+ } else if (url.pathname === "/auth-success") {
291
+ // Serve the authentication success page (index.html handles localStorage via query params)
292
+ const htmlPath = path.join(__dirname, "index.html");
293
+ fs.readFile(htmlPath, "utf8", (err, htmlContent) => {
294
+ if (err) {
295
+ res.writeHead(500, { "Content-Type": "text/plain" });
296
+ res.end("Error loading success page");
297
+ return;
298
+ }
299
+ res.writeHead(200, { "Content-Type": "text/html" });
300
+ res.end(htmlContent);
301
+ });
302
+ } else if (url.pathname === "/dist/styles.css") {
303
+ // Serve the compiled Tailwind CSS file
304
+ const cssPath = path.join(__dirname, "dist", "styles.css");
305
+ fs.readFile(cssPath, (err, data) => {
306
+ if (err) {
307
+ res.writeHead(404, { "Content-Type": "text/plain" });
308
+ res.end("CSS file not found");
309
+ return;
310
+ }
311
+ res.writeHead(200, { "Content-Type": "text/css" });
312
+ res.end(data);
313
+ });
314
+ } else {
315
+ res.writeHead(404, { "Content-Type": "text/plain" });
316
+ res.end("Not Found");
317
+ }
318
+ });
319
+
320
+ // Find an available port dynamically (starting from 49152 - ephemeral port range)
321
+ const { findAvailablePort } = await import("./utils/portUtils.js");
322
+ const loopbackPort = await findAvailablePort(49152, 100);
323
+
324
+ loopbackServer.listen(loopbackPort, async () => {
325
+ const open = (await import("open")).default;
326
+ // Pass redirect_uri to the frontend so it knows where to redirect after auth
327
+ const redirectUri = encodeURIComponent(
328
+ `http://localhost:${loopbackPort}/auth-callback`,
329
+ );
330
+ const authUrl = `${frontendUrl}/agent-cli?redirect_uri=${redirectUri}`;
331
+ console.log(chalk.gray(` Loopback server on port: ${loopbackPort}`));
332
+ console.log(chalk.gray(` Opening: ${authUrl}`));
333
+ open(authUrl);
334
+ });
335
+ });
336
+
337
+ // Service subcommand
338
+ const serviceCommand = program
339
+ .command("service")
340
+ .description("manage the Fenwave agent service");
341
+
342
+ // fenwave service run - foreground mode
343
+ serviceCommand
344
+ .command("run")
345
+ .description("run the agent in foreground mode (for development)")
346
+ .option("-p, --port <port>", "WebSocket port to listen on", String(WS_PORT))
347
+ .action(async (options) => {
348
+ console.log(chalk.blue("Starting Fenwave Agent in foreground mode..."));
349
+
350
+ // Show environment file status
351
+ ensureEnvironmentFiles(__dirname, true);
352
+
353
+ // Setup signal handlers for graceful shutdown
354
+ setupSignalHandlers();
355
+
356
+ try {
357
+ await runAgent({
358
+ preferredPort: parseInt(options.port, 10),
359
+ isDaemon: false,
360
+ });
361
+ } catch (error) {
362
+ console.error(chalk.red(`Failed to start agent: ${error.message}`));
363
+ process.exit(1);
364
+ }
365
+ });
366
+
367
+ // fenwave service start - background daemon mode
368
+ serviceCommand
369
+ .command("start")
370
+ .description("start the agent as a background daemon")
371
+ .option("-p, --port <port>", "WebSocket port to listen on", String(WS_PORT))
372
+ .action(async (options) => {
373
+ const spinner = ora("Starting Fenwave Agent daemon...").start();
374
+
375
+ try {
376
+ // Check for valid session first
377
+ const session = loadSession();
378
+ if (!session || !isSessionValid(session)) {
379
+ spinner.fail("No valid session found");
380
+ console.log(
381
+ chalk.yellow(
382
+ '\nPlease run "fenwave login" first to authenticate.\n',
383
+ ),
384
+ );
385
+ process.exit(1);
386
+ }
387
+
388
+ const daemonInfo = await startDaemon({
389
+ port: options.port ? parseInt(options.port, 10) : undefined,
390
+ });
391
+
392
+ spinner.succeed("Fenwave Agent daemon started");
393
+ console.log(chalk.gray(` PID: ${daemonInfo.pid}`));
394
+ console.log(chalk.gray(` Port: ${daemonInfo.port}`));
395
+ console.log(
396
+ chalk.gray(
397
+ ` Started: ${new Date(daemonInfo.startTime).toLocaleString()}`,
398
+ ),
399
+ );
400
+ console.log(
401
+ chalk.yellow(
402
+ '\nUse "fenwave service status" to check the daemon status.',
403
+ ),
404
+ );
405
+ console.log(
406
+ chalk.yellow('Use "fenwave service logs -f" to follow daemon logs.'),
407
+ );
408
+ process.exit(0);
409
+ } catch (error) {
410
+ spinner.fail("Failed to start daemon");
411
+ console.error(chalk.red(` ${error.message}`));
412
+ process.exit(1);
413
+ }
414
+ });
415
+
416
+ // fenwave service stop - stop daemon
417
+ serviceCommand
418
+ .command("stop")
419
+ .description("stop the running daemon")
420
+ .option("-f, --force", "force kill if graceful shutdown fails")
421
+ .action(async (options) => {
422
+ const spinner = ora("Stopping Fenwave Agent daemon...").start();
423
+
424
+ try {
425
+ const stopped = await stopDaemon({
426
+ force: options.force,
427
+ timeout: 10000,
428
+ });
429
+
430
+ if (stopped) {
431
+ spinner.succeed("Fenwave Agent daemon stopped");
432
+ } else {
433
+ spinner.warn("Daemon was not running");
434
+ }
435
+ process.exit(0);
436
+ } catch (error) {
437
+ spinner.fail("Failed to stop daemon");
438
+ console.error(chalk.red(` ${error.message}`));
439
+ process.exit(1);
440
+ }
441
+ });
442
+
443
+ // fenwave service restart - restart daemon
444
+ serviceCommand
445
+ .command("restart")
446
+ .description("restart the daemon")
447
+ .option("-p, --port <port>", "WebSocket port to listen on")
448
+ .action(async (options) => {
449
+ const spinner = ora("Restarting Fenwave Agent daemon...").start();
450
+
451
+ try {
452
+ const daemonInfo = await restartDaemon({
453
+ port: options.port ? parseInt(options.port, 10) : undefined,
454
+ });
455
+
456
+ spinner.succeed("Fenwave Agent daemon restarted");
457
+ console.log(chalk.gray(` PID: ${daemonInfo.pid}`));
458
+ console.log(chalk.gray(` Port: ${daemonInfo.port}`));
459
+ console.log(
460
+ chalk.gray(
461
+ ` Started: ${new Date(daemonInfo.startTime).toLocaleString()}`,
462
+ ),
463
+ );
464
+ process.exit(0);
465
+ } catch (error) {
466
+ spinner.fail("Failed to restart daemon");
467
+ console.error(chalk.red(` ${error.message}`));
468
+ process.exit(1);
469
+ }
470
+ });
471
+
472
+ // fenwave service status - show status
473
+ serviceCommand
474
+ .command("status")
475
+ .description("show daemon status")
476
+ .action(async () => {
477
+ const status = getDaemonStatus();
478
+
479
+ console.log(chalk.bold("\nFenwave Agent Daemon Status\n"));
480
+
481
+ if (status.running) {
482
+ console.log(chalk.green("Status: Running"));
483
+ console.log(chalk.white(`PID: ${status.pid}`));
484
+ console.log(chalk.white(`Port: ${status.port}`));
485
+ console.log(chalk.white(`User: ${status.userEntityRef || "N/A"}`));
486
+ console.log(
487
+ chalk.white(
488
+ `Started: ${new Date(status.startTime).toLocaleString()}`,
489
+ ),
490
+ );
491
+ console.log(
492
+ chalk.white(`Uptime: ${formatDaemonUptime(status.uptime)}`),
493
+ );
494
+ } else {
495
+ console.log(chalk.yellow("Status: Not Running"));
496
+ console.log(chalk.gray("\nTo start the agent, run:"));
497
+ console.log(chalk.cyan(" fenwave service start"));
498
+ }
499
+
500
+ console.log("");
501
+ process.exit(0);
502
+ });
503
+
504
+ // fenwave service logs - view logs
505
+ serviceCommand
506
+ .command("logs")
507
+ .description("view daemon logs")
508
+ .option("-f, --follow", "follow log output in real-time")
509
+ .option("-t, --tail <lines>", "number of lines to show", "100")
510
+ .action(async (options) => {
511
+ if (options.follow) {
512
+ // Follow logs in real-time using tail
513
+ const logPath = getLogPath();
514
+
515
+ if (!fs.existsSync(logPath)) {
516
+ console.log(
517
+ chalk.yellow("No log file found. Is the daemon running?"),
518
+ );
519
+ process.exit(0);
520
+ }
521
+
522
+ console.log(chalk.blue(`Following logs from ${logPath}`));
523
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
524
+
525
+ const tailProcess = spawn("tail", ["-f", "-n", options.tail, logPath], {
526
+ stdio: "inherit",
527
+ });
528
+
529
+ process.on("SIGINT", () => {
530
+ tailProcess.kill();
531
+ process.exit(0);
532
+ });
533
+ } else {
534
+ // Show recent logs
535
+ const logs = readLogs(parseInt(options.tail, 10));
536
+ if (logs) {
537
+ console.log(logs);
538
+ } else {
539
+ console.log(chalk.yellow("No logs found."));
540
+ }
541
+ process.exit(0);
542
+ }
543
+ });
544
+
545
+ // Config command - view and manage agent configuration
546
+ const configCommand = program
547
+ .command("config")
548
+ .description("view and manage agent configuration");
549
+
550
+ // fenwave config show - display current configuration
551
+ configCommand
552
+ .command("show")
553
+ .description("display current agent configuration")
554
+ .action(async () => {
555
+ const { loadConfig, getConfigPath, configExists } =
556
+ await import("./store/configStore.js");
557
+
558
+ console.log(chalk.bold("\nFenwave Agent Configuration\n"));
559
+
560
+ if (!configExists()) {
561
+ console.log(chalk.yellow("No configuration file found."));
562
+ console.log(
563
+ chalk.gray(
564
+ "Using default settings. Run 'fenwave init' or 'fenwave config set' to configure.\n",
565
+ ),
566
+ );
567
+ }
568
+
569
+ const currentConfig = loadConfig();
570
+ console.log(chalk.white(" Config file:"), chalk.gray(getConfigPath()));
571
+ console.log("");
572
+
573
+ const formatValue = (v) => {
574
+ if (v === undefined || v === null) return chalk.gray(String(v));
575
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
576
+ return chalk.cyan(String(v));
577
+ };
578
+
579
+ Object.keys(currentConfig).forEach((key) => {
580
+ console.log(chalk.white(` ${key}:`), formatValue(currentConfig[key]));
581
+ });
582
+
583
+ console.log("");
584
+
585
+ process.exit(0);
586
+ });
587
+
588
+ // fenwave config set - update configuration
589
+ configCommand
590
+ .command("set")
591
+ .description("update agent configuration")
592
+ .option("--backend-url <url>", "Fenwave backend URL")
593
+ .option("--frontend-url <url>", "Fenwave frontend URL")
594
+ .option("--ws-port <port>", "WebSocket port")
595
+ .option("--container-port <port>", "Container port")
596
+ .option("--docker-image <image>", "Docker image for dev container")
597
+ .action(async (options) => {
598
+ const { updateConfig, loadConfig } =
599
+ await import("./store/configStore.js");
600
+
601
+ const updates = {};
602
+ if (options.backendUrl) updates.backendUrl = options.backendUrl;
603
+ if (options.frontendUrl) updates.frontendUrl = options.frontendUrl;
604
+ if (options.wsPort) updates.wsPort = parseInt(options.wsPort, 10);
605
+ if (options.containerPort)
606
+ updates.containerPort = parseInt(options.containerPort, 10);
607
+ if (options.dockerImage) updates.dockerImage = options.dockerImage;
608
+
609
+ if (Object.keys(updates).length === 0) {
610
+ console.log(chalk.yellow("No configuration options provided."));
611
+ console.log(chalk.gray("Use --help to see available options."));
612
+ process.exit(1);
613
+ }
614
+
615
+ updateConfig(updates);
616
+ console.log(chalk.green("Configuration updated successfully!\n"));
617
+
618
+ // Show updated configuration
619
+ const newConfig = loadConfig();
620
+ const formatValue2 = (v) => {
621
+ if (v === undefined || v === null) return chalk.gray(String(v));
622
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
623
+ return chalk.cyan(String(v));
624
+ };
625
+ Object.keys(newConfig).forEach((key) => {
626
+ console.log(chalk.white(` ${key}:`), formatValue2(newConfig[key]));
627
+ });
628
+ console.log("");
629
+
630
+ process.exit(0);
631
+ });
632
+
633
+ // fenwave config reset - reset to defaults
634
+ configCommand
635
+ .command("reset")
636
+ .description("reset configuration to defaults")
637
+ .action(async () => {
638
+ const { saveConfig, DEFAULT_CONFIG } =
639
+ await import("./store/configStore.js");
640
+
641
+ saveConfig(DEFAULT_CONFIG);
642
+ console.log(chalk.green("Configuration reset to defaults.\n"));
643
+ // Print full default configuration
644
+ const { loadConfig } = await import("./store/configStore.js");
645
+ const after = loadConfig();
646
+ const fmt = (v) => {
647
+ if (v === undefined || v === null) return chalk.gray(String(v));
648
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
649
+ return chalk.cyan(String(v));
650
+ };
651
+ Object.keys(after).forEach((k) => {
652
+ console.log(chalk.white(` ${k}:`), fmt(after[k]));
653
+ });
654
+ console.log("");
655
+
656
+ process.exit(0);
657
+ });
658
+
659
+ // Logout (clear session)
660
+ program
661
+ .command("logout")
662
+ .description("clear stored session and logout")
663
+ .action(async () => {
664
+ // Check if daemon is running
665
+ const status = getDaemonStatus();
666
+ if (status.running) {
667
+ console.log(chalk.yellow("Stopping running daemon first..."));
668
+ try {
669
+ await stopDaemon({ force: false, timeout: 5000 });
670
+ } catch (e) {
671
+ // Ignore
672
+ }
673
+ }
674
+
675
+ const session = loadSession();
676
+ if (session) {
677
+ clearSession();
678
+ console.log(chalk.green("Logged out successfully"));
679
+ process.exit(0);
680
+ } else {
681
+ console.log(chalk.yellow("No active session found"));
682
+ process.exit(0);
683
+ }
684
+ });
685
+
686
+ // Check Auth session and device registration status
687
+ program
688
+ .command("status")
689
+ .description("check current session status and device registration")
690
+ .action(async () => {
691
+ try {
692
+ const { loadDeviceCredential, isDeviceRegistered } =
693
+ await import("./store/deviceCredentialStore.js");
694
+
695
+ console.log(chalk.bold("\nFenwave Agent Status\n"));
696
+
697
+ // Session status
698
+ const session = loadSession();
699
+ if (session && isSessionValid(session)) {
700
+ console.log(chalk.green("Session: Active"));
701
+ console.log(chalk.gray(` User: ${session.userEntityRef || "N/A"}`));
702
+ console.log(
703
+ chalk.gray(
704
+ ` Expires: ${new Date(session.expiresAt).toLocaleString()}`,
705
+ ),
706
+ );
707
+ } else {
708
+ console.log(chalk.yellow("Session: Inactive"));
709
+ console.log(chalk.gray(' Run "fenwave login" to authenticate'));
710
+ }
711
+
712
+ console.log("");
713
+
714
+ // Device registration status
715
+ if (isDeviceRegistered()) {
716
+ const deviceCred = loadDeviceCredential();
717
+ console.log(chalk.green("Device: Registered"));
718
+ console.log(chalk.gray(` Device ID: ${deviceCred.deviceId}`));
719
+ console.log(chalk.gray(` Device Name: ${deviceCred.deviceName}`));
720
+ console.log(chalk.gray(` Platform: ${deviceCred.platform}`));
721
+
722
+ let deviceUser;
723
+ if (
724
+ deviceCred.userEntityRef &&
725
+ deviceCred.userEntityRef !== "unknown"
726
+ ) {
727
+ deviceUser = deviceCred.userEntityRef;
728
+ } else {
729
+ deviceUser = session?.userEntityRef || "N/A";
730
+ }
731
+
732
+ console.log(chalk.gray(` User: ${deviceUser}`));
733
+ console.log(
734
+ chalk.gray(
735
+ ` Registered: ${new Date(deviceCred.registeredAt).toLocaleString()}`,
736
+ ),
737
+ );
738
+ } else {
739
+ console.log(chalk.yellow("Device: Not Registered"));
740
+ console.log(
741
+ chalk.gray(
742
+ ' Run "fenwave init" or "fenwave register" to register your device',
743
+ ),
744
+ );
745
+ }
746
+
747
+ console.log("");
748
+
749
+ // NPM token status
750
+ if (hasNpmToken()) {
751
+ console.log(chalk.green("NPM Token: Configured"));
752
+ } else {
753
+ console.log(chalk.yellow("NPM Token: Not Configured"));
754
+ }
755
+
756
+ console.log("");
757
+
758
+ // Agent running status - check daemon first, then fallback to port check
759
+ const daemonStatus = getDaemonStatus();
760
+ if (daemonStatus.running) {
761
+ console.log(
762
+ chalk.green(
763
+ `Agent: Running (daemon, PID ${daemonStatus.pid}, port ${daemonStatus.port})`,
764
+ ),
765
+ );
766
+ console.log(
767
+ chalk.gray(` Uptime: ${formatDaemonUptime(daemonStatus.uptime)}`),
768
+ );
769
+ } else {
770
+ const isRunning = await checkAgentRunning(WS_PORT);
771
+ if (isRunning) {
772
+ console.log(chalk.green(`Agent: Running (port ${WS_PORT})`));
773
+ } else {
774
+ console.log(chalk.yellow("Agent: Not Running"));
775
+ console.log(
776
+ chalk.gray(' Run "fenwave service start" to start the agent'),
777
+ );
778
+ }
779
+ }
780
+
781
+ console.log("");
782
+ process.exit(0);
783
+ } catch (error) {
784
+ console.error(chalk.red("Error checking status:"), error.message);
785
+ process.exit(1);
786
+ }
787
+ });
788
+
789
+ // Interactive Setup Wizard
790
+ program
791
+ .command("init")
792
+ .description("interactive setup wizard for Fenwave agent")
793
+ .option("-t, --token <token>", "Registration token from Fenwave")
794
+ .option("--skip-prerequisites", "Skip prerequisites check")
795
+ .option(
796
+ "--backend-url <url>",
797
+ "Fenwave backend URL",
798
+ "http://localhost:7007",
799
+ )
800
+ .option(
801
+ "--frontend-url <url>",
802
+ "Fenwave frontend URL",
803
+ "http://localhost:3000",
804
+ )
805
+ .option("--aws-region <region>", "AWS region for ECR", "eu-west-1")
806
+ .option("--aws-account-id <id>", "AWS account ID for ECR")
807
+ .action(async (options) => {
808
+ try {
809
+ const { runSetupWizard } = await import("./setup/setupWizard.js");
810
+ await runSetupWizard({
811
+ token: options.token,
812
+ skipPrerequisites: options.skipPrerequisites,
813
+ backendUrl: options.backendUrl,
814
+ frontendUrl: options.frontendUrl,
815
+ awsRegion: options.awsRegion,
816
+ awsAccountId: options.awsAccountId,
817
+ });
818
+ } catch (error) {
819
+ console.error(chalk.red("Setup failed:"), error.message);
820
+ process.exit(1);
821
+ }
822
+ });
823
+
824
+ // Register Device
825
+ program
826
+ .command("register")
827
+ .description("register device with Fenwave")
828
+ .option("-t, --token <token>", "Registration token from Fenwave")
829
+ .option(
830
+ "--backend-url <url>",
831
+ "Fenwave backend URL",
832
+ "http://localhost:7007",
833
+ )
834
+ .action(async (options) => {
835
+ const spinner = ora("Registering device...").start();
836
+
837
+ try {
838
+ const { getDeviceMetadata } = await import("./utils/deviceInfo.js");
839
+ const { saveDeviceCredential, isDeviceRegistered } =
840
+ await import("./store/deviceCredentialStore.js");
841
+
842
+ // Check if already registered
843
+ if (isDeviceRegistered()) {
844
+ spinner.warn("Device is already registered");
845
+ const inquirer = (await import("inquirer")).default;
846
+ const { confirmed } = await inquirer.prompt([
847
+ {
848
+ type: "confirm",
849
+ name: "confirmed",
850
+ message: "Re-register (this will replace existing credentials)?",
851
+ default: false,
852
+ },
853
+ ]);
854
+
855
+ if (!confirmed) {
856
+ console.log(chalk.yellow("Registration cancelled"));
857
+ process.exit(0);
858
+ }
859
+ }
860
+
861
+ // Get registration token
862
+ let token = options.token;
863
+ if (!token) {
864
+ const inquirer = (await import("inquirer")).default;
865
+ const answers = await inquirer.prompt([
866
+ {
867
+ type: "password",
868
+ name: "token",
869
+ message: "Enter registration token:",
870
+ mask: "*",
871
+ validate: (input) =>
872
+ input && input.length >= 32 ? true : "Invalid token format",
873
+ },
874
+ ]);
875
+ token = answers.token;
876
+ }
877
+
878
+ // Collect device info
879
+ const deviceMetadata = await getDeviceMetadata();
880
+
881
+ // Register with backend
882
+ const response = await axios.post(
883
+ `${options.backendUrl}/api/agent-cli/register`,
884
+ {
885
+ installToken: token,
886
+ deviceInfo: {
887
+ deviceName: deviceMetadata.deviceName,
888
+ platform: deviceMetadata.platform,
889
+ osVersion: deviceMetadata.osVersion,
890
+ agentVersion: deviceMetadata.agentVersion,
891
+ metadata: deviceMetadata.metadata,
892
+ },
893
+ },
894
+ { timeout: 10000 },
895
+ );
896
+
897
+ // Save credentials
898
+ saveDeviceCredential({
899
+ deviceId: response.data.deviceId,
900
+ deviceCredential: response.data.deviceCredential,
901
+ userEntityRef: response.data.userEntityRef || "unknown",
902
+ deviceName: deviceMetadata.deviceName,
903
+ platform: deviceMetadata.platform,
904
+ agentVersion: deviceMetadata.agentVersion,
905
+ });
906
+
907
+ spinner.succeed("Device registered successfully");
908
+ console.log(chalk.green("\nRegistration Complete"));
909
+ console.log(chalk.gray(` Device ID: ${response.data.deviceId}`));
910
+ console.log(
911
+ chalk.gray(` Device Name: ${deviceMetadata.deviceName}\n`),
912
+ );
913
+ } catch (error) {
914
+ spinner.fail("Registration failed");
915
+ if (error.response?.status === 401) {
916
+ console.error(chalk.red("Invalid or expired registration token"));
917
+ console.log(
918
+ chalk.yellow("Get a new token from Fenwave at /agent-installer"),
919
+ );
920
+ } else if (error.response?.status === 429) {
921
+ console.error(chalk.red("Rate limit exceeded"));
922
+ console.log(
923
+ chalk.yellow("Too many attempts. Please wait and try again."),
924
+ );
925
+ } else {
926
+ console.error(chalk.red("Error:"), error.message);
927
+ }
928
+ process.exit(1);
929
+ }
930
+ });
931
+
932
+ // Rotate Device Credentials
933
+ program
934
+ .command("rotate-credentials")
935
+ .description("rotate device credentials")
936
+ .option(
937
+ "--backend-url <url>",
938
+ "Fenwave backend URL",
939
+ "http://localhost:7007",
940
+ )
941
+ .action(async (options) => {
942
+ const spinner = ora("Rotating credentials...").start();
943
+
944
+ try {
945
+ const {
946
+ loadDeviceCredential,
947
+ saveDeviceCredential,
948
+ isDeviceRegistered,
949
+ } = await import("./store/deviceCredentialStore.js");
950
+
951
+ if (!isDeviceRegistered()) {
952
+ spinner.fail("Device is not registered");
953
+ console.log(chalk.yellow('Run "fenwave register" first'));
954
+ process.exit(1);
955
+ }
956
+
957
+ const deviceCred = loadDeviceCredential();
958
+
959
+ // Rotate credentials with backend
960
+ const response = await axios.post(
961
+ `${options.backendUrl}/api/agent-cli/rotate-credentials`,
962
+ {
963
+ deviceId: deviceCred.deviceId,
964
+ deviceCredential: deviceCred.deviceCredential,
965
+ },
966
+ { timeout: 10000 },
967
+ );
968
+
969
+ // Update stored credentials
970
+ saveDeviceCredential({
971
+ ...deviceCred,
972
+ deviceCredential: response.data.deviceCredential,
973
+ });
974
+
975
+ spinner.succeed("Credentials rotated successfully");
976
+ console.log(chalk.green("New credentials saved securely\n"));
977
+ } catch (error) {
978
+ spinner.fail("Credential rotation failed");
979
+ if (error.response?.status === 401) {
980
+ console.error(chalk.red("Invalid current credentials"));
981
+ console.log(
982
+ chalk.yellow(
983
+ 'Your device may have been revoked. Run "fenwave register" to re-register.',
984
+ ),
985
+ );
986
+ } else if (error.response?.status === 429) {
987
+ console.error(chalk.red("Rate limit exceeded"));
988
+ console.log(
989
+ chalk.yellow(
990
+ "Too many rotation attempts. Please wait and try again.",
991
+ ),
992
+ );
993
+ } else {
994
+ console.error(chalk.red("Error:"), error.message);
995
+ }
996
+ process.exit(1);
997
+ }
998
+ });
999
+
1000
+ // Uninstall Agent
1001
+ program
1002
+ .command("uninstall")
1003
+ .description("uninstall Fenwave agent and clean up")
1004
+ .option("--keep-data", "Keep configuration and data files")
1005
+ .action(async (options) => {
1006
+ try {
1007
+ const inquirer = (await import("inquirer")).default;
1008
+
1009
+ // Confirmation
1010
+ const { confirmed } = await inquirer.prompt([
1011
+ {
1012
+ type: "confirm",
1013
+ name: "confirmed",
1014
+ message: "Are you sure you want to uninstall the Fenwave agent?",
1015
+ default: false,
1016
+ },
1017
+ ]);
1018
+
1019
+ if (!confirmed) {
1020
+ console.log(chalk.yellow("Uninstall cancelled"));
1021
+ process.exit(0);
1022
+ }
1023
+
1024
+ const spinner = ora("Uninstalling Fenwave agent...").start();
1025
+
1026
+ // Stop daemon if running
1027
+ try {
1028
+ const status = getDaemonStatus();
1029
+ if (status.running) {
1030
+ spinner.text = "Stopping daemon...";
1031
+ await stopDaemon({ force: true, timeout: 5000 });
1032
+ }
1033
+ } catch (error) {
1034
+ // Ignore
1035
+ }
1036
+
1037
+ // Clear credentials unless --keep-data
1038
+ if (!options.keepData) {
1039
+ spinner.text = "Clearing credentials...";
1040
+ const { clearDeviceCredential } =
1041
+ await import("./store/deviceCredentialStore.js");
1042
+ const { clearNpmToken } = await import("./store/npmTokenStore.js");
1043
+ const { clearSetupState } = await import("./store/setupState.js");
1044
+
1045
+ clearDeviceCredential();
1046
+ clearNpmToken();
1047
+ clearSetupState();
1048
+ clearSession();
1049
+ const fwDir = path.join(os.homedir(), ".fenwave");
1050
+
1051
+ if (fs.existsSync(fwDir)) {
1052
+ fs.rmSync(fwDir, { recursive: true, force: true });
1053
+ }
1054
+ }
1055
+
1056
+ spinner.succeed("Agent uninstalled");
1057
+ console.log(chalk.green("\nFenwave agent uninstalled successfully\n"));
1058
+
1059
+ if (!options.keepData) {
1060
+ console.log(
1061
+ chalk.gray("All configuration and data files have been removed."),
1062
+ );
1063
+ } else {
1064
+ console.log(
1065
+ chalk.gray("Configuration and data files have been preserved."),
1066
+ );
1067
+ }
1068
+
1069
+ console.log(
1070
+ chalk.gray("\nTo reinstall: npm install -g @fenwave/agent\n"),
1071
+ );
1072
+
1073
+ process.exit(0);
1074
+ } catch (error) {
1075
+ console.error(chalk.red("Uninstall error:"), error.message);
1076
+ process.exit(1);
1077
+ }
1078
+ });
1079
+
1080
+ // Container commands
1081
+ program
1082
+ .command("containers")
1083
+ .alias("ps")
1084
+ .description("list containers")
1085
+ .option("-a, --all", "Show all containers (default shows just running)")
1086
+ .action(async (options) => {
1087
+ const spinner = ora("Fetching containers...").start();
1088
+
1089
+ try {
1090
+ const containers = await docker.listContainers({ all: options.all });
1091
+
1092
+ if (containers.length === 0) {
1093
+ spinner.succeed("No containers found");
1094
+ return;
1095
+ }
1096
+
1097
+ const containerPromises = containers.map((container) =>
1098
+ formatContainer(docker.getContainer(container.Id)),
1099
+ );
1100
+
1101
+ const formattedContainers = await Promise.all(containerPromises);
1102
+
1103
+ const containerText =
1104
+ containers.length === 1 ? "container" : "containers";
1105
+ spinner.succeed(`Found ${containers.length} ${containerText}`);
1106
+
1107
+ // Create a table for display
1108
+ const table = new Table({
1109
+ head: [
1110
+ chalk.blue("ID"),
1111
+ chalk.blue("Name"),
1112
+ chalk.blue("Image"),
1113
+ chalk.blue("Status"),
1114
+ chalk.blue("Ports"),
1115
+ chalk.blue("CPU %"),
1116
+ chalk.blue("MEM %"),
1117
+ ],
1118
+ colWidths: [15, 25, 30, 10, 20, 10, 10],
1119
+ });
1120
+
1121
+ formattedContainers.forEach((container) => {
1122
+ const status =
1123
+ container.status === "running"
1124
+ ? chalk.green(container.status)
1125
+ : chalk.red(container.status);
1126
+
1127
+ table.push([
1128
+ container.id.substring(0, 12),
1129
+ container.name,
1130
+ container.image,
1131
+ status,
1132
+ container.ports.join(", "),
1133
+ `${container.cpu}%`,
1134
+ `${container.memory}%`,
1135
+ ]);
1136
+ });
1137
+
1138
+ console.log(table.toString());
1139
+ process.exit(0);
1140
+ } catch (error) {
1141
+ spinner.fail(`Failed to fetch containers: ${error.message}`);
1142
+ process.exit(1);
1143
+ }
1144
+ });
1145
+
1146
+ // Start container(s) - supports multiple containers
1147
+ program
1148
+ .command("start <containerId(s)...>")
1149
+ .description("start one or more containers")
1150
+ .action(async (containerIds) => {
1151
+ const spinner = ora(
1152
+ `Starting ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1153
+ ).start();
1154
+
1155
+ try {
1156
+ const results = [];
1157
+ for (const containerId of containerIds) {
1158
+ try {
1159
+ const container = docker.getContainer(containerId);
1160
+ await container.start();
1161
+ results.push({ id: containerId, success: true });
1162
+ } catch (error) {
1163
+ results.push({
1164
+ id: containerId,
1165
+ success: false,
1166
+ error: error.message,
1167
+ });
1168
+ }
1169
+ }
1170
+
1171
+ const succeeded = results.filter((r) => r.success);
1172
+ const failed = results.filter((r) => !r.success);
1173
+
1174
+ if (failed.length === 0) {
1175
+ spinner.succeed(
1176
+ `Successfully started ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1177
+ );
1178
+ } else if (succeeded.length === 0) {
1179
+ spinner.fail(`Failed to start all containers`);
1180
+ failed.forEach((r) =>
1181
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1182
+ );
1183
+ } else {
1184
+ spinner.warn(
1185
+ `Started ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1186
+ );
1187
+ failed.forEach((r) =>
1188
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1189
+ );
1190
+ }
1191
+ process.exit(failed.length > 0 ? 1 : 0);
1192
+ } catch (error) {
1193
+ spinner.fail(`Failed to start containers: ${error.message}`);
1194
+ process.exit(1);
1195
+ }
1196
+ });
1197
+
1198
+ // Stop container(s) - supports multiple containers
1199
+ program
1200
+ .command("stop <containerId(s)...>")
1201
+ .description("stop one or more containers")
1202
+ .action(async (containerIds) => {
1203
+ const spinner = ora(
1204
+ `Stopping ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1205
+ ).start();
1206
+
1207
+ try {
1208
+ const results = [];
1209
+ for (const containerId of containerIds) {
1210
+ try {
1211
+ const container = docker.getContainer(containerId);
1212
+ await container.stop();
1213
+ results.push({ id: containerId, success: true });
1214
+ } catch (error) {
1215
+ results.push({
1216
+ id: containerId,
1217
+ success: false,
1218
+ error: error.message,
1219
+ });
1220
+ }
1221
+ }
1222
+
1223
+ const succeeded = results.filter((r) => r.success);
1224
+ const failed = results.filter((r) => !r.success);
1225
+
1226
+ if (failed.length === 0) {
1227
+ spinner.succeed(
1228
+ `Successfully stopped ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1229
+ );
1230
+ } else if (succeeded.length === 0) {
1231
+ spinner.fail(`Failed to stop all containers`);
1232
+ failed.forEach((r) =>
1233
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1234
+ );
1235
+ } else {
1236
+ spinner.warn(
1237
+ `Stopped ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1238
+ );
1239
+ failed.forEach((r) =>
1240
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1241
+ );
1242
+ }
1243
+ process.exit(failed.length > 0 ? 1 : 0);
1244
+ } catch (error) {
1245
+ spinner.fail(`Failed to stop containers: ${error.message}`);
1246
+ process.exit(1);
1247
+ }
1248
+ });
1249
+
1250
+ // Restart container(s) - supports multiple containers
1251
+ program
1252
+ .command("restart <containerId(s)...>")
1253
+ .description("restart one or more containers")
1254
+ .action(async (containerIds) => {
1255
+ const spinner = ora(
1256
+ `Restarting ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1257
+ ).start();
1258
+
1259
+ try {
1260
+ const results = [];
1261
+ for (const containerId of containerIds) {
1262
+ try {
1263
+ const container = docker.getContainer(containerId);
1264
+ await container.restart();
1265
+ results.push({ id: containerId, success: true });
1266
+ } catch (error) {
1267
+ results.push({
1268
+ id: containerId,
1269
+ success: false,
1270
+ error: error.message,
1271
+ });
1272
+ }
1273
+ }
1274
+
1275
+ const succeeded = results.filter((r) => r.success);
1276
+ const failed = results.filter((r) => !r.success);
1277
+
1278
+ if (failed.length === 0) {
1279
+ spinner.succeed(
1280
+ `Successfully restarted ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1281
+ );
1282
+ } else if (succeeded.length === 0) {
1283
+ spinner.fail(`Failed to restart all containers`);
1284
+ failed.forEach((r) =>
1285
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1286
+ );
1287
+ } else {
1288
+ spinner.warn(
1289
+ `Restarted ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1290
+ );
1291
+ failed.forEach((r) =>
1292
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1293
+ );
1294
+ }
1295
+ process.exit(failed.length > 0 ? 1 : 0);
1296
+ } catch (error) {
1297
+ spinner.fail(`Failed to restart containers: ${error.message}`);
1298
+ process.exit(1);
1299
+ }
1300
+ });
1301
+
1302
+ // Remove container(s) - supports multiple containers
1303
+ program
1304
+ .command("rm <containerId(s)...>")
1305
+ .description("remove one or more containers")
1306
+ .option("-f, --force", "Force remove the container(s)")
1307
+ .action(async (containerIds, options) => {
1308
+ const spinner = ora(
1309
+ `Removing ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1310
+ ).start();
1311
+
1312
+ try {
1313
+ const results = [];
1314
+ for (const containerId of containerIds) {
1315
+ try {
1316
+ const container = docker.getContainer(containerId);
1317
+ await container.remove({ force: options.force });
1318
+ results.push({ id: containerId, success: true });
1319
+ } catch (error) {
1320
+ results.push({
1321
+ id: containerId,
1322
+ success: false,
1323
+ error: error.message,
1324
+ });
1325
+ }
1326
+ }
1327
+
1328
+ const succeeded = results.filter((r) => r.success);
1329
+ const failed = results.filter((r) => !r.success);
1330
+
1331
+ if (failed.length === 0) {
1332
+ spinner.succeed(
1333
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1334
+ );
1335
+ } else if (succeeded.length === 0) {
1336
+ spinner.fail(`Failed to remove all containers`);
1337
+ failed.forEach((r) =>
1338
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1339
+ );
1340
+ } else {
1341
+ spinner.warn(
1342
+ `Removed ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1343
+ );
1344
+ failed.forEach((r) =>
1345
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1346
+ );
1347
+ }
1348
+ process.exit(failed.length > 0 ? 1 : 0);
1349
+ } catch (error) {
1350
+ spinner.fail(`Failed to remove containers: ${error.message}`);
1351
+ process.exit(1);
1352
+ }
1353
+ });
1354
+
1355
+ // Image commands
1356
+ program
1357
+ .command("images")
1358
+ .description("list images")
1359
+ .action(async () => {
1360
+ const spinner = ora("Fetching images...").start();
1361
+
1362
+ try {
1363
+ const images = await docker.listImages();
1364
+
1365
+ if (images.length === 0) {
1366
+ spinner.succeed("No images found");
1367
+ return;
1368
+ }
1369
+
1370
+ spinner.succeed(
1371
+ `Found ${images.length} ${images.length === 1 ? "image" : "images"}`,
1372
+ );
1373
+
1374
+ // Create a table for display
1375
+ const table = new Table({
1376
+ head: [
1377
+ chalk.blue("ID"),
1378
+ chalk.blue("Repository"),
1379
+ chalk.blue("Tag"),
1380
+ chalk.blue("Size"),
1381
+ chalk.blue("Created"),
1382
+ ],
1383
+ colWidths: [15, 30, 15, 15, 15],
1384
+ });
1385
+
1386
+ images.forEach((image) => {
1387
+ // Extract repository and tag
1388
+ let name = "<none>";
1389
+ let tag = "<none>";
1390
+
1391
+ if (image.RepoTags && image.RepoTags.length > 0) {
1392
+ const [repoTag] = image.RepoTags;
1393
+ const parts = repoTag.split(":");
1394
+ name = parts[0];
1395
+ tag = parts.length > 1 ? parts[1] : "latest";
1396
+ }
1397
+
1398
+ table.push([
1399
+ image.Id.substring(7, 19),
1400
+ name,
1401
+ tag,
1402
+ formatSize(image.Size),
1403
+ formatCreatedTime(image.Created),
1404
+ ]);
1405
+ });
1406
+
1407
+ console.log(table.toString());
1408
+ process.exit(0);
1409
+ } catch (error) {
1410
+ spinner.fail(`Failed to fetch images: ${error.message}`);
1411
+ process.exit(1);
1412
+ }
1413
+ });
1414
+
1415
+ // Pull image(s) - supports multiple images
1416
+ program
1417
+ .command("pull <imageTags...>")
1418
+ .description("pull one or more images")
1419
+ .action(async (imageTags) => {
1420
+ const spinner = ora(
1421
+ `Pulling ${imageTags.length} ${imageTags.length === 1 ? "image" : "images"}...`,
1422
+ ).start();
1423
+
1424
+ try {
1425
+ const results = [];
1426
+ for (const imageTag of imageTags) {
1427
+ try {
1428
+ // Split image tag into name and tag
1429
+ const [name, tag = "latest"] = imageTag.split(":");
1430
+ spinner.text = `Pulling ${imageTag}...`;
1431
+
1432
+ // Pull the image
1433
+ const stream = await docker.pull(`${name}:${tag}`);
1434
+
1435
+ // Track progress
1436
+ await new Promise((resolve, reject) => {
1437
+ docker.modem.followProgress(
1438
+ stream,
1439
+ (err, output) => {
1440
+ if (err) {
1441
+ reject(err);
1442
+ return;
1443
+ }
1444
+ resolve(output);
1445
+ },
1446
+ (event) => {
1447
+ if (event.progress) {
1448
+ spinner.text = `Pulling ${imageTag}: ${event.progress}`;
1449
+ } else if (event.status) {
1450
+ spinner.text = `Pulling ${imageTag}: ${event.status}`;
1451
+ }
1452
+ },
1453
+ );
1454
+ });
1455
+
1456
+ results.push({ tag: imageTag, success: true });
1457
+ } catch (error) {
1458
+ results.push({
1459
+ tag: imageTag,
1460
+ success: false,
1461
+ error: error.message,
1462
+ });
1463
+ }
1464
+ }
1465
+
1466
+ const succeeded = results.filter((r) => r.success);
1467
+ const failed = results.filter((r) => !r.success);
1468
+
1469
+ if (failed.length === 0) {
1470
+ spinner.succeed(
1471
+ `Successfully pulled ${succeeded.length} ${succeeded.length === 1 ? "image" : "images"}`,
1472
+ );
1473
+ } else if (succeeded.length === 0) {
1474
+ spinner.fail(`Failed to pull all images`);
1475
+ failed.forEach((r) =>
1476
+ console.log(chalk.red(` - ${r.tag}: ${r.error}`)),
1477
+ );
1478
+ } else {
1479
+ spinner.warn(
1480
+ `Pulled ${succeeded.length}/${imageTags.length} ${succeeded.length === 1 ? "image" : "images"}`,
1481
+ );
1482
+ failed.forEach((r) =>
1483
+ console.log(chalk.red(` - ${r.tag}: ${r.error}`)),
1484
+ );
1485
+ }
1486
+ process.exit(failed.length > 0 ? 1 : 0);
1487
+ } catch (error) {
1488
+ spinner.fail(`Failed to pull images: ${error.message}`);
1489
+ process.exit(1);
1490
+ }
1491
+ });
1492
+
1493
+ // Remove image(s) - supports multiple images
1494
+ program
1495
+ .command("rmi <imageIds...>")
1496
+ .description("remove one or more images")
1497
+ .option("-f, --force", "Force remove the image(s)")
1498
+ .action(async (imageIds, options) => {
1499
+ const spinner = ora(
1500
+ `Removing ${imageIds.length} ${imageIds.length === 1 ? "image" : "images"}...`,
1501
+ ).start();
1502
+
1503
+ try {
1504
+ const results = [];
1505
+ for (const imageId of imageIds) {
1506
+ try {
1507
+ const image = docker.getImage(imageId);
1508
+ await image.remove({ force: options.force });
1509
+ results.push({ id: imageId, success: true });
1510
+ } catch (error) {
1511
+ results.push({ id: imageId, success: false, error: error.message });
1512
+ }
1513
+ }
1514
+
1515
+ const succeeded = results.filter((r) => r.success);
1516
+ const failed = results.filter((r) => !r.success);
1517
+
1518
+ if (failed.length === 0) {
1519
+ spinner.succeed(
1520
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "image" : "images"}`,
1521
+ );
1522
+ } else if (succeeded.length === 0) {
1523
+ spinner.fail(`Failed to remove all images`);
1524
+ failed.forEach((r) =>
1525
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1526
+ );
1527
+ } else {
1528
+ spinner.warn(
1529
+ `Removed ${succeeded.length}/${imageIds.length} ${succeeded.length === 1 ? "image" : "images"}`,
1530
+ );
1531
+ failed.forEach((r) =>
1532
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1533
+ );
1534
+ }
1535
+ process.exit(failed.length > 0 ? 1 : 0);
1536
+ } catch (error) {
1537
+ spinner.fail(`Failed to remove images: ${error.message}`);
1538
+ process.exit(1);
1539
+ }
1540
+ });
1541
+
1542
+ // Volume commands
1543
+ program
1544
+ .command("volumes")
1545
+ .description("list volumes")
1546
+ .action(async () => {
1547
+ const spinner = ora("Fetching volumes...").start();
1548
+
1549
+ try {
1550
+ const { Volumes } = await docker.listVolumes();
1551
+
1552
+ if (Volumes.length === 0) {
1553
+ spinner.succeed("No volumes found");
1554
+ return;
1555
+ }
1556
+
1557
+ spinner.succeed(
1558
+ `Found ${Volumes.length} ${Volumes.length === 1 ? "volume" : "volumes"}`,
1559
+ );
1560
+
1561
+ // Create a table for display
1562
+ const table = new Table({
1563
+ head: [
1564
+ chalk.blue("Name"),
1565
+ chalk.blue("Driver"),
1566
+ chalk.blue("Mountpoint"),
1567
+ ],
1568
+ colWidths: [30, 15, 50],
1569
+ });
1570
+
1571
+ Volumes.forEach((volume) => {
1572
+ table.push([volume.Name, volume.Driver, volume.Mountpoint]);
1573
+ });
1574
+
1575
+ console.log(table.toString());
1576
+ process.exit(0);
1577
+ } catch (error) {
1578
+ spinner.fail(`Failed to fetch volumes: ${error.message}`);
1579
+ process.exit(1);
1580
+ }
1581
+ });
1582
+
1583
+ // Create volume(s) - supports multiple volumes
1584
+ program
1585
+ .command("volume-create <names...>")
1586
+ .description("create one or more volumes")
1587
+ .option("-d, --driver <driver>", "Volume driver", "local")
1588
+ .action(async (names, options) => {
1589
+ const spinner = ora(
1590
+ `Creating ${names.length} ${names.length === 1 ? "volume" : "volumes"}...`,
1591
+ ).start();
1592
+
1593
+ try {
1594
+ const results = [];
1595
+ for (const name of names) {
1596
+ try {
1597
+ await docker.createVolume({
1598
+ Name: name,
1599
+ Driver: options.driver,
1600
+ });
1601
+ results.push({ name, success: true });
1602
+ } catch (error) {
1603
+ results.push({ name, success: false, error: error.message });
1604
+ }
1605
+ }
1606
+
1607
+ const succeeded = results.filter((r) => r.success);
1608
+ const failed = results.filter((r) => !r.success);
1609
+
1610
+ if (failed.length === 0) {
1611
+ spinner.succeed(
1612
+ `Successfully created ${succeeded.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1613
+ );
1614
+ } else if (succeeded.length === 0) {
1615
+ spinner.fail(`Failed to create all volumes`);
1616
+ failed.forEach((r) =>
1617
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1618
+ );
1619
+ } else {
1620
+ spinner.warn(
1621
+ `Created ${succeeded.length}/${names.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1622
+ );
1623
+ failed.forEach((r) =>
1624
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1625
+ );
1626
+ }
1627
+ process.exit(failed.length > 0 ? 1 : 0);
1628
+ } catch (error) {
1629
+ spinner.fail(`Failed to create volumes: ${error.message}`);
1630
+ process.exit(1);
1631
+ }
1632
+ });
1633
+
1634
+ // Remove volume(s) - supports multiple volumes
1635
+ program
1636
+ .command("volume-rm <names...>")
1637
+ .description("remove one or more volumes")
1638
+ .action(async (names) => {
1639
+ const spinner = ora(
1640
+ `Removing ${names.length} ${names.length === 1 ? "volume" : "volumes"}...`,
1641
+ ).start();
1642
+
1643
+ try {
1644
+ const results = [];
1645
+ for (const name of names) {
1646
+ try {
1647
+ const volume = docker.getVolume(name);
1648
+ await volume.remove();
1649
+ results.push({ name, success: true });
1650
+ } catch (error) {
1651
+ results.push({ name, success: false, error: error.message });
1652
+ }
1653
+ }
1654
+
1655
+ const succeeded = results.filter((r) => r.success);
1656
+ const failed = results.filter((r) => !r.success);
1657
+
1658
+ if (failed.length === 0) {
1659
+ spinner.succeed(
1660
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1661
+ );
1662
+ } else if (succeeded.length === 0) {
1663
+ spinner.fail(`Failed to remove all volumes`);
1664
+ failed.forEach((r) =>
1665
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1666
+ );
1667
+ } else {
1668
+ spinner.warn(
1669
+ `Removed ${succeeded.length}/${names.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1670
+ );
1671
+ failed.forEach((r) =>
1672
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1673
+ );
1674
+ }
1675
+ process.exit(failed.length > 0 ? 1 : 0);
1676
+ } catch (error) {
1677
+ spinner.fail(`Failed to remove volumes: ${error.message}`);
1678
+ process.exit(1);
1679
+ }
1680
+ });
1681
+
1682
+ // Logs command
1683
+ program
1684
+ .command("logs <containerId>")
1685
+ .description("fetch container logs")
1686
+ .option("-f, --follow", "Follow log output")
1687
+ .option("-t, --tail <lines>", "Number of lines to show from the end", "100")
1688
+ .action(async (containerId, options) => {
1689
+ try {
1690
+ const container = docker.getContainer(containerId);
1691
+
1692
+ if (options.follow) {
1693
+ console.log(
1694
+ chalk.blue(`Following logs for container ${containerId}...`),
1695
+ );
1696
+ console.log(chalk.blue("Press Ctrl+C to exit"));
1697
+
1698
+ const logStream = await container.logs({
1699
+ follow: true,
1700
+ stdout: true,
1701
+ stderr: true,
1702
+ tail: Number.parseInt(options.tail, 10),
1703
+ });
1704
+
1705
+ logStream.on("data", (chunk) => {
1706
+ process.stdout.write(chunk.toString());
1707
+ });
1708
+
1709
+ // Handle Ctrl+C
1710
+ process.on("SIGINT", () => {
1711
+ console.log(chalk.blue("\nStopping log stream..."));
1712
+ process.exit(0);
1713
+ });
1714
+ } else {
1715
+ const spinner = ora(
1716
+ `Fetching logs for container ${containerId}...`,
1717
+ ).start();
1718
+
1719
+ const logs = await container.logs({
1720
+ stdout: true,
1721
+ stderr: true,
1722
+ tail: Number.parseInt(options.tail, 10),
1723
+ });
1724
+
1725
+ spinner.stop();
1726
+ console.log(logs.toString());
1727
+ process.exit(0);
1728
+ }
1729
+ } catch (error) {
1730
+ console.error(chalk.red(`Failed to fetch logs: ${error.message}`));
1731
+ process.exit(1);
1732
+ }
1733
+ });
1734
+
1735
+ // Local-env app commands
1736
+ program
1737
+ .command("local-env")
1738
+ .description("manage local-env app container")
1739
+ .option("--start", "start the local-env app")
1740
+ .option("--stop", "stop the local-env app")
1741
+ .option("--status", "show local-env app status")
1742
+ .option("--logs", "show local-env app logs")
1743
+ .option("--logs-follow", "follow local-env app logs")
1744
+ .action(async (options) => {
1745
+ try {
1746
+ if (options.start) {
1747
+ const spinner = ora("Starting local-env app...").start();
1748
+ try {
1749
+ await containerManager.startContainer();
1750
+ spinner.succeed("Local-env app started successfully");
1751
+ } catch (error) {
1752
+ spinner.fail(`Failed to start local-env app: ${error.message}`);
1753
+ process.exit(1);
1754
+ }
1755
+ } else if (options.stop) {
1756
+ const spinner = ora("Stopping local-env app...").start();
1757
+ try {
1758
+ await containerManager.stopContainerGracefully();
1759
+ spinner.succeed("Local-env app stopped successfully");
1760
+ } catch (error) {
1761
+ spinner.fail(`Failed to stop local-env app: ${error.message}`);
1762
+ process.exit(1);
1763
+ }
1764
+ } else if (options.status) {
1765
+ const status = await containerManager.getStatus();
1766
+ console.log(chalk.bold("\nLocal-env App Status:"));
1767
+ console.log(chalk.blue("Container:"), status.containerName);
1768
+ console.log(
1769
+ chalk.blue("Running:"),
1770
+ status.isRunning ? chalk.green("Yes") : chalk.red("No"),
1771
+ );
1772
+ console.log(chalk.blue("Port:"), status.port);
1773
+ console.log(chalk.blue("Data Directory:"), status.dataDirectory);
1774
+ if (status.isRunning) {
1775
+ console.log(
1776
+ chalk.blue("URL:"),
1777
+ chalk.underline(`http://localhost:${status.port}`),
1778
+ );
1779
+ }
1780
+ } else if (options.logs) {
1781
+ console.log(chalk.blue("Showing local-env app logs..."));
1782
+ containerManager.showLogs(false);
1783
+ } else if (options.logsFollow) {
1784
+ console.log(
1785
+ chalk.blue(
1786
+ "Following local-env app logs... (Press Ctrl+C to stop)",
1787
+ ),
1788
+ );
1789
+ containerManager.showLogs(true);
1790
+ } else {
1791
+ console.log(
1792
+ chalk.yellow(
1793
+ "Please specify an action: --start, --stop, --status, --logs, or --logs-follow",
1794
+ ),
1795
+ );
1796
+ process.exit(1);
1797
+ }
1798
+ } catch (error) {
1799
+ console.error(chalk.red("Error:"), error.message);
1800
+ process.exit(1);
1801
+ }
1802
+ });
1803
+
1804
+ // Registry commands
1805
+ program
1806
+ .command("registries")
1807
+ .description("list registries")
1808
+ .action(async () => {
1809
+ const spinner = ora("Fetching registries...").start();
1810
+
1811
+ try {
1812
+ await registryStore.initialize();
1813
+ const registries = await registryStore.getAllRegistries();
1814
+
1815
+ if (registries.length === 0) {
1816
+ spinner.succeed("No registries found");
1817
+ return;
1818
+ }
1819
+
1820
+ const registryText =
1821
+ registries.length === 1 ? "registry" : "registries";
1822
+ spinner.succeed(`Found ${registries.length} ${registryText}`);
1823
+
1824
+ const table = new Table({
1825
+ head: ["ID", "Name", "Type", "URL", "Connected"].map((h) =>
1826
+ chalk.cyan(h),
1827
+ ),
1828
+ style: { head: [], border: [] },
1829
+ });
1830
+
1831
+ for (const registry of registries) {
1832
+ table.push([
1833
+ registry.id.substring(0, 12) + "...",
1834
+ registry.name,
1835
+ registry.type,
1836
+ registry.url,
1837
+ registry.connected ? chalk.green("Y") : chalk.red("N"),
1838
+ ]);
1839
+ }
1840
+
1841
+ console.log(chalk.bold("\nConnected Registries:"));
1842
+ console.log(table.toString());
1843
+ process.exit(0);
1844
+ } catch (error) {
1845
+ spinner.fail(`Failed to fetch registries: ${error.message}`);
1846
+ process.exit(1);
1847
+ }
1848
+ });
1849
+
1850
+ // Agent info command
1851
+ program
1852
+ .command("info")
1853
+ .description("display agent information")
1854
+ .action(async () => {
1855
+ const spinner = ora("Fetching agent information...").start();
1856
+
1857
+ try {
1858
+ // Get Docker version
1859
+ const dockerVersion = await docker.version();
1860
+
1861
+ // Display agent information
1862
+ spinner.stop();
1863
+ const { version } = packageJson;
1864
+
1865
+ console.log(chalk.bold("\nFenwave Agent Information:"));
1866
+ console.log(chalk.blue("Version:"), version);
1867
+ console.log(chalk.blue("Hostname:"), os.hostname());
1868
+ console.log(chalk.blue("Platform:"), os.platform());
1869
+ console.log(chalk.blue("Architecture:"), os.arch());
1870
+ console.log(chalk.blue("Node.js Version:"), process.version);
1871
+ console.log(chalk.blue("Docker Version:"), dockerVersion.Version);
1872
+ console.log(chalk.blue("CPU Cores:"), os.cpus().length);
1873
+ console.log(
1874
+ chalk.blue("Memory:"),
1875
+ `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
1876
+ );
1877
+
1878
+ // Check daemon status
1879
+ const daemonStatus = getDaemonStatus();
1880
+ if (daemonStatus.running) {
1881
+ console.log(
1882
+ chalk.blue("Status:"),
1883
+ chalk.green(`Running (PID: ${daemonStatus.pid})`),
1884
+ );
1885
+ console.log(chalk.blue("Port:"), daemonStatus.port);
1886
+ console.log(
1887
+ chalk.blue("Uptime:"),
1888
+ formatDaemonUptime(daemonStatus.uptime),
1889
+ );
1890
+ } else {
1891
+ // Fallback to checking port
1892
+ const isAgentRunning = await checkAgentRunning(WS_PORT);
1893
+ if (isAgentRunning) {
1894
+ try {
1895
+ const agentStartTime = await agentStore.loadAgentStartTime();
1896
+ if (agentStartTime) {
1897
+ const currentTime = Date.now();
1898
+ const agentUptimeMs = currentTime - agentStartTime.getTime();
1899
+ console.log(chalk.blue("Uptime:"), formatUptime(agentUptimeMs));
1900
+ } else {
1901
+ console.log(chalk.blue("Uptime:"), "0 seconds");
1902
+ }
1903
+ } catch (error) {
1904
+ console.log(chalk.blue("Uptime:"), "0 seconds");
1905
+ }
1906
+ } else {
1907
+ console.log(chalk.blue("Status:"), chalk.red("Agent Not Running"));
1908
+ try {
1909
+ await agentStore.clearAgentInfo();
1910
+ } catch (error) {
1911
+ // Ignore cleanup errors
1912
+ }
1913
+ }
1914
+ }
1915
+ process.exit(0);
1916
+ } catch (error) {
1917
+ spinner.fail(`Failed to fetch agent information: ${error.message}`);
1918
+ process.exit(1);
1919
+ }
1920
+ });
1921
+ }
1922
+
1923
+ export { setupCLICommands };