@oussema_mili/test-pkg-123 1.1.32

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 (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 +1921 -0
  6. package/containerManager.js +304 -0
  7. package/daemon/agentRunner.js +491 -0
  8. package/daemon/daemonEntry.js +64 -0
  9. package/daemon/daemonManager.js +266 -0
  10. package/daemon/logManager.js +227 -0
  11. package/dist/styles.css +504 -0
  12. package/docker-actions/apps.js +3913 -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 +713 -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 +41 -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,1921 @@
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
+ .action(async () => {
421
+ const spinner = ora("Stopping Fenwave Agent daemon...").start();
422
+
423
+ try {
424
+ const stopped = await stopDaemon({
425
+ timeout: 10000,
426
+ });
427
+
428
+ if (stopped) {
429
+ spinner.succeed("Fenwave Agent daemon stopped");
430
+ } else {
431
+ spinner.warn("Daemon was not running");
432
+ }
433
+ process.exit(0);
434
+ } catch (error) {
435
+ spinner.fail("Failed to stop daemon");
436
+ console.error(chalk.red(` ${error.message}`));
437
+ process.exit(1);
438
+ }
439
+ });
440
+
441
+ // fenwave service restart - restart daemon
442
+ serviceCommand
443
+ .command("restart")
444
+ .description("restart the daemon")
445
+ .option("-p, --port <port>", "WebSocket port to listen on")
446
+ .action(async (options) => {
447
+ const spinner = ora("Restarting Fenwave Agent daemon...").start();
448
+
449
+ try {
450
+ const daemonInfo = await restartDaemon({
451
+ port: options.port ? parseInt(options.port, 10) : undefined,
452
+ });
453
+
454
+ spinner.succeed("Fenwave Agent daemon restarted");
455
+ console.log(chalk.gray(` PID: ${daemonInfo.pid}`));
456
+ console.log(chalk.gray(` Port: ${daemonInfo.port}`));
457
+ console.log(
458
+ chalk.gray(
459
+ ` Started: ${new Date(daemonInfo.startTime).toLocaleString()}`,
460
+ ),
461
+ );
462
+ process.exit(0);
463
+ } catch (error) {
464
+ spinner.fail("Failed to restart daemon");
465
+ console.error(chalk.red(` ${error.message}`));
466
+ process.exit(1);
467
+ }
468
+ });
469
+
470
+ // fenwave service status - show status
471
+ serviceCommand
472
+ .command("status")
473
+ .description("show daemon status")
474
+ .action(async () => {
475
+ const status = getDaemonStatus();
476
+
477
+ console.log(chalk.bold("\nFenwave Agent Daemon Status\n"));
478
+
479
+ if (status.running) {
480
+ console.log(chalk.green("Status: Running"));
481
+ console.log(chalk.white(`PID: ${status.pid}`));
482
+ console.log(chalk.white(`Port: ${status.port}`));
483
+ console.log(chalk.white(`User: ${status.userEntityRef || "N/A"}`));
484
+ console.log(
485
+ chalk.white(
486
+ `Started: ${new Date(status.startTime).toLocaleString()}`,
487
+ ),
488
+ );
489
+ console.log(
490
+ chalk.white(`Uptime: ${formatDaemonUptime(status.uptime)}`),
491
+ );
492
+ } else {
493
+ console.log(chalk.yellow("Status: Not Running"));
494
+ console.log(chalk.gray("\nTo start the agent, run:"));
495
+ console.log(chalk.cyan(" fenwave service start"));
496
+ }
497
+
498
+ console.log("");
499
+ process.exit(0);
500
+ });
501
+
502
+ // fenwave service logs - view logs
503
+ serviceCommand
504
+ .command("logs")
505
+ .description("view daemon logs")
506
+ .option("-f, --follow", "follow log output in real-time")
507
+ .option("-t, --tail <lines>", "number of lines to show", "100")
508
+ .action(async (options) => {
509
+ if (options.follow) {
510
+ // Follow logs in real-time using tail
511
+ const logPath = getLogPath();
512
+
513
+ if (!fs.existsSync(logPath)) {
514
+ console.log(
515
+ chalk.yellow("No log file found. Is the daemon running?"),
516
+ );
517
+ process.exit(0);
518
+ }
519
+
520
+ console.log(chalk.blue(`Following logs from ${logPath}`));
521
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
522
+
523
+ const tailProcess = spawn("tail", ["-f", "-n", options.tail, logPath], {
524
+ stdio: "inherit",
525
+ });
526
+
527
+ process.on("SIGINT", () => {
528
+ tailProcess.kill();
529
+ process.exit(0);
530
+ });
531
+ } else {
532
+ // Show recent logs
533
+ const logs = readLogs(parseInt(options.tail, 10));
534
+ if (logs) {
535
+ console.log(logs);
536
+ } else {
537
+ console.log(chalk.yellow("No logs found."));
538
+ }
539
+ process.exit(0);
540
+ }
541
+ });
542
+
543
+ // Config command - view and manage agent configuration
544
+ const configCommand = program
545
+ .command("config")
546
+ .description("view and manage agent configuration");
547
+
548
+ // fenwave config show - display current configuration
549
+ configCommand
550
+ .command("show")
551
+ .description("display current agent configuration")
552
+ .action(async () => {
553
+ const { loadConfig, getConfigPath, configExists } =
554
+ await import("./store/configStore.js");
555
+
556
+ console.log(chalk.bold("\nFenwave Agent Configuration\n"));
557
+
558
+ if (!configExists()) {
559
+ console.log(chalk.yellow("No configuration file found."));
560
+ console.log(
561
+ chalk.gray(
562
+ "Using default settings. Run 'fenwave init' or 'fenwave config set' to configure.\n",
563
+ ),
564
+ );
565
+ }
566
+
567
+ const currentConfig = loadConfig();
568
+ console.log(chalk.white(" Config file:"), chalk.gray(getConfigPath()));
569
+ console.log("");
570
+
571
+ const formatValue = (v) => {
572
+ if (v === undefined || v === null) return chalk.gray(String(v));
573
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
574
+ return chalk.cyan(String(v));
575
+ };
576
+
577
+ Object.keys(currentConfig).forEach((key) => {
578
+ console.log(chalk.white(` ${key}:`), formatValue(currentConfig[key]));
579
+ });
580
+
581
+ console.log("");
582
+
583
+ process.exit(0);
584
+ });
585
+
586
+ // fenwave config set - update configuration
587
+ configCommand
588
+ .command("set")
589
+ .description("update agent configuration")
590
+ .option("--backend-url <url>", "Fenwave backend URL")
591
+ .option("--frontend-url <url>", "Fenwave frontend URL")
592
+ .option("--ws-port <port>", "WebSocket port")
593
+ .option("--container-port <port>", "Container port")
594
+ .option("--docker-image <image>", "Docker image for dev container")
595
+ .action(async (options) => {
596
+ const { updateConfig, loadConfig } =
597
+ await import("./store/configStore.js");
598
+
599
+ const updates = {};
600
+ if (options.backendUrl) updates.backendUrl = options.backendUrl;
601
+ if (options.frontendUrl) updates.frontendUrl = options.frontendUrl;
602
+ if (options.wsPort) updates.wsPort = parseInt(options.wsPort, 10);
603
+ if (options.containerPort)
604
+ updates.containerPort = parseInt(options.containerPort, 10);
605
+ if (options.dockerImage) updates.dockerImage = options.dockerImage;
606
+
607
+ if (Object.keys(updates).length === 0) {
608
+ console.log(chalk.yellow("No configuration options provided."));
609
+ console.log(chalk.gray("Use --help to see available options."));
610
+ process.exit(1);
611
+ }
612
+
613
+ updateConfig(updates);
614
+ console.log(chalk.green("Configuration updated successfully!\n"));
615
+
616
+ // Show updated configuration
617
+ const newConfig = loadConfig();
618
+ const formatValue2 = (v) => {
619
+ if (v === undefined || v === null) return chalk.gray(String(v));
620
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
621
+ return chalk.cyan(String(v));
622
+ };
623
+ Object.keys(newConfig).forEach((key) => {
624
+ console.log(chalk.white(` ${key}:`), formatValue2(newConfig[key]));
625
+ });
626
+ console.log("");
627
+
628
+ process.exit(0);
629
+ });
630
+
631
+ // fenwave config reset - reset to defaults
632
+ configCommand
633
+ .command("reset")
634
+ .description("reset configuration to defaults")
635
+ .action(async () => {
636
+ const { saveConfig, DEFAULT_CONFIG } =
637
+ await import("./store/configStore.js");
638
+
639
+ saveConfig(DEFAULT_CONFIG);
640
+ console.log(chalk.green("Configuration reset to defaults.\n"));
641
+ // Print full default configuration
642
+ const { loadConfig } = await import("./store/configStore.js");
643
+ const after = loadConfig();
644
+ const fmt = (v) => {
645
+ if (v === undefined || v === null) return chalk.gray(String(v));
646
+ if (typeof v === "object") return chalk.cyan(JSON.stringify(v));
647
+ return chalk.cyan(String(v));
648
+ };
649
+ Object.keys(after).forEach((k) => {
650
+ console.log(chalk.white(` ${k}:`), fmt(after[k]));
651
+ });
652
+ console.log("");
653
+
654
+ process.exit(0);
655
+ });
656
+
657
+ // Logout (clear session)
658
+ program
659
+ .command("logout")
660
+ .description("clear stored session and logout")
661
+ .action(async () => {
662
+ // Check if daemon is running
663
+ const status = getDaemonStatus();
664
+ if (status.running) {
665
+ console.log(chalk.yellow("Stopping running daemon first..."));
666
+ try {
667
+ await stopDaemon({ force: false, timeout: 5000 });
668
+ } catch (e) {
669
+ // Ignore
670
+ }
671
+ }
672
+
673
+ const session = loadSession();
674
+ if (session) {
675
+ clearSession();
676
+ console.log(chalk.green("Logged out successfully"));
677
+ process.exit(0);
678
+ } else {
679
+ console.log(chalk.yellow("No active session found"));
680
+ process.exit(0);
681
+ }
682
+ });
683
+
684
+ // Check Auth session and device registration status
685
+ program
686
+ .command("status")
687
+ .description("check current session status and device registration")
688
+ .action(async () => {
689
+ try {
690
+ const { loadDeviceCredential, isDeviceRegistered } =
691
+ await import("./store/deviceCredentialStore.js");
692
+
693
+ console.log(chalk.bold("\nFenwave Agent Status\n"));
694
+
695
+ // Session status
696
+ const session = loadSession();
697
+ if (session && isSessionValid(session)) {
698
+ console.log(chalk.green("Session: Active"));
699
+ console.log(chalk.gray(` User: ${session.userEntityRef || "N/A"}`));
700
+ console.log(
701
+ chalk.gray(
702
+ ` Expires: ${new Date(session.expiresAt).toLocaleString()}`,
703
+ ),
704
+ );
705
+ } else {
706
+ console.log(chalk.yellow("Session: Inactive"));
707
+ console.log(chalk.gray(' Run "fenwave login" to authenticate'));
708
+ }
709
+
710
+ console.log("");
711
+
712
+ // Device registration status
713
+ if (isDeviceRegistered()) {
714
+ const deviceCred = loadDeviceCredential();
715
+ console.log(chalk.green("Device: Registered"));
716
+ console.log(chalk.gray(` Device ID: ${deviceCred.deviceId}`));
717
+ console.log(chalk.gray(` Device Name: ${deviceCred.deviceName}`));
718
+ console.log(chalk.gray(` Platform: ${deviceCred.platform}`));
719
+
720
+ let deviceUser;
721
+ if (
722
+ deviceCred.userEntityRef &&
723
+ deviceCred.userEntityRef !== "unknown"
724
+ ) {
725
+ deviceUser = deviceCred.userEntityRef;
726
+ } else {
727
+ deviceUser = session?.userEntityRef || "N/A";
728
+ }
729
+
730
+ console.log(chalk.gray(` User: ${deviceUser}`));
731
+ console.log(
732
+ chalk.gray(
733
+ ` Registered: ${new Date(deviceCred.registeredAt).toLocaleString()}`,
734
+ ),
735
+ );
736
+ } else {
737
+ console.log(chalk.yellow("Device: Not Registered"));
738
+ console.log(
739
+ chalk.gray(
740
+ ' Run "fenwave init" or "fenwave register" to register your device',
741
+ ),
742
+ );
743
+ }
744
+
745
+ console.log("");
746
+
747
+ // NPM token status
748
+ if (hasNpmToken()) {
749
+ console.log(chalk.green("NPM Token: Configured"));
750
+ } else {
751
+ console.log(chalk.yellow("NPM Token: Not Configured"));
752
+ }
753
+
754
+ console.log("");
755
+
756
+ // Agent running status - check daemon first, then fallback to port check
757
+ const daemonStatus = getDaemonStatus();
758
+ if (daemonStatus.running) {
759
+ console.log(
760
+ chalk.green(
761
+ `Agent: Running (daemon, PID ${daemonStatus.pid}, port ${daemonStatus.port})`,
762
+ ),
763
+ );
764
+ console.log(
765
+ chalk.gray(` Uptime: ${formatDaemonUptime(daemonStatus.uptime)}`),
766
+ );
767
+ } else {
768
+ const isRunning = await checkAgentRunning(WS_PORT);
769
+ if (isRunning) {
770
+ console.log(chalk.green(`Agent: Running (port ${WS_PORT})`));
771
+ } else {
772
+ console.log(chalk.yellow("Agent: Not Running"));
773
+ console.log(
774
+ chalk.gray(' Run "fenwave service start" to start the agent'),
775
+ );
776
+ }
777
+ }
778
+
779
+ console.log("");
780
+ process.exit(0);
781
+ } catch (error) {
782
+ console.error(chalk.red("Error checking status:"), error.message);
783
+ process.exit(1);
784
+ }
785
+ });
786
+
787
+ // Interactive Setup Wizard
788
+ program
789
+ .command("init")
790
+ .description("interactive setup wizard for Fenwave agent")
791
+ .option("-t, --token <token>", "Registration token from Fenwave")
792
+ .option("--skip-prerequisites", "Skip prerequisites check")
793
+ .option(
794
+ "--backend-url <url>",
795
+ "Fenwave backend URL",
796
+ "http://localhost:7007",
797
+ )
798
+ .option(
799
+ "--frontend-url <url>",
800
+ "Fenwave frontend URL",
801
+ "http://localhost:3000",
802
+ )
803
+ .option("--aws-region <region>", "AWS region for ECR", "eu-west-1")
804
+ .option("--aws-account-id <id>", "AWS account ID for ECR")
805
+ .action(async (options) => {
806
+ try {
807
+ const { runSetupWizard } = await import("./setup/setupWizard.js");
808
+ await runSetupWizard({
809
+ token: options.token,
810
+ skipPrerequisites: options.skipPrerequisites,
811
+ backendUrl: options.backendUrl,
812
+ frontendUrl: options.frontendUrl,
813
+ awsRegion: options.awsRegion,
814
+ awsAccountId: options.awsAccountId,
815
+ });
816
+ } catch (error) {
817
+ console.error(chalk.red("Setup failed:"), error.message);
818
+ process.exit(1);
819
+ }
820
+ });
821
+
822
+ // Register Device
823
+ program
824
+ .command("register")
825
+ .description("register device with Fenwave")
826
+ .option("-t, --token <token>", "Registration token from Fenwave")
827
+ .option(
828
+ "--backend-url <url>",
829
+ "Fenwave backend URL",
830
+ "http://localhost:7007",
831
+ )
832
+ .action(async (options) => {
833
+ const spinner = ora("Registering device...").start();
834
+
835
+ try {
836
+ const { getDeviceMetadata } = await import("./utils/deviceInfo.js");
837
+ const { saveDeviceCredential, isDeviceRegistered } =
838
+ await import("./store/deviceCredentialStore.js");
839
+
840
+ // Check if already registered
841
+ if (isDeviceRegistered()) {
842
+ spinner.warn("Device is already registered");
843
+ const inquirer = (await import("inquirer")).default;
844
+ const { confirmed } = await inquirer.prompt([
845
+ {
846
+ type: "confirm",
847
+ name: "confirmed",
848
+ message: "Re-register (this will replace existing credentials)?",
849
+ default: false,
850
+ },
851
+ ]);
852
+
853
+ if (!confirmed) {
854
+ console.log(chalk.yellow("Registration cancelled"));
855
+ process.exit(0);
856
+ }
857
+ }
858
+
859
+ // Get registration token
860
+ let token = options.token;
861
+ if (!token) {
862
+ const inquirer = (await import("inquirer")).default;
863
+ const answers = await inquirer.prompt([
864
+ {
865
+ type: "password",
866
+ name: "token",
867
+ message: "Enter registration token:",
868
+ mask: "*",
869
+ validate: (input) =>
870
+ input && input.length >= 32 ? true : "Invalid token format",
871
+ },
872
+ ]);
873
+ token = answers.token;
874
+ }
875
+
876
+ // Collect device info
877
+ const deviceMetadata = await getDeviceMetadata();
878
+
879
+ // Register with backend
880
+ const response = await axios.post(
881
+ `${options.backendUrl}/api/agent-cli/register`,
882
+ {
883
+ installToken: token,
884
+ deviceInfo: {
885
+ deviceName: deviceMetadata.deviceName,
886
+ platform: deviceMetadata.platform,
887
+ osVersion: deviceMetadata.osVersion,
888
+ agentVersion: deviceMetadata.agentVersion,
889
+ metadata: deviceMetadata.metadata,
890
+ },
891
+ },
892
+ { timeout: 10000 },
893
+ );
894
+
895
+ // Save credentials
896
+ saveDeviceCredential({
897
+ deviceId: response.data.deviceId,
898
+ deviceCredential: response.data.deviceCredential,
899
+ userEntityRef: response.data.userEntityRef || "unknown",
900
+ deviceName: deviceMetadata.deviceName,
901
+ platform: deviceMetadata.platform,
902
+ agentVersion: deviceMetadata.agentVersion,
903
+ });
904
+
905
+ spinner.succeed("Device registered successfully");
906
+ console.log(chalk.green("\nRegistration Complete"));
907
+ console.log(chalk.gray(` Device ID: ${response.data.deviceId}`));
908
+ console.log(
909
+ chalk.gray(` Device Name: ${deviceMetadata.deviceName}\n`),
910
+ );
911
+ } catch (error) {
912
+ spinner.fail("Registration failed");
913
+ if (error.response?.status === 401) {
914
+ console.error(chalk.red("Invalid or expired registration token"));
915
+ console.log(
916
+ chalk.yellow("Get a new token from Fenwave at /agent-installer"),
917
+ );
918
+ } else if (error.response?.status === 429) {
919
+ console.error(chalk.red("Rate limit exceeded"));
920
+ console.log(
921
+ chalk.yellow("Too many attempts. Please wait and try again."),
922
+ );
923
+ } else {
924
+ console.error(chalk.red("Error:"), error.message);
925
+ }
926
+ process.exit(1);
927
+ }
928
+ });
929
+
930
+ // Rotate Device Credentials
931
+ program
932
+ .command("rotate-credentials")
933
+ .description("rotate device credentials")
934
+ .option(
935
+ "--backend-url <url>",
936
+ "Fenwave backend URL",
937
+ "http://localhost:7007",
938
+ )
939
+ .action(async (options) => {
940
+ const spinner = ora("Rotating credentials...").start();
941
+
942
+ try {
943
+ const {
944
+ loadDeviceCredential,
945
+ saveDeviceCredential,
946
+ isDeviceRegistered,
947
+ } = await import("./store/deviceCredentialStore.js");
948
+
949
+ if (!isDeviceRegistered()) {
950
+ spinner.fail("Device is not registered");
951
+ console.log(chalk.yellow('Run "fenwave register" first'));
952
+ process.exit(1);
953
+ }
954
+
955
+ const deviceCred = loadDeviceCredential();
956
+
957
+ // Rotate credentials with backend
958
+ const response = await axios.post(
959
+ `${options.backendUrl}/api/agent-cli/rotate-credentials`,
960
+ {
961
+ deviceId: deviceCred.deviceId,
962
+ deviceCredential: deviceCred.deviceCredential,
963
+ },
964
+ { timeout: 10000 },
965
+ );
966
+
967
+ // Update stored credentials
968
+ saveDeviceCredential({
969
+ ...deviceCred,
970
+ deviceCredential: response.data.deviceCredential,
971
+ });
972
+
973
+ spinner.succeed("Credentials rotated successfully");
974
+ console.log(chalk.green("New credentials saved securely\n"));
975
+ } catch (error) {
976
+ spinner.fail("Credential rotation failed");
977
+ if (error.response?.status === 401) {
978
+ console.error(chalk.red("Invalid current credentials"));
979
+ console.log(
980
+ chalk.yellow(
981
+ 'Your device may have been revoked. Run "fenwave register" to re-register.',
982
+ ),
983
+ );
984
+ } else if (error.response?.status === 429) {
985
+ console.error(chalk.red("Rate limit exceeded"));
986
+ console.log(
987
+ chalk.yellow(
988
+ "Too many rotation attempts. Please wait and try again.",
989
+ ),
990
+ );
991
+ } else {
992
+ console.error(chalk.red("Error:"), error.message);
993
+ }
994
+ process.exit(1);
995
+ }
996
+ });
997
+
998
+ // Uninstall Agent
999
+ program
1000
+ .command("uninstall")
1001
+ .description("uninstall Fenwave agent and clean up")
1002
+ .option("--keep-data", "Keep configuration and data files")
1003
+ .action(async (options) => {
1004
+ try {
1005
+ const inquirer = (await import("inquirer")).default;
1006
+
1007
+ // Confirmation
1008
+ const { confirmed } = await inquirer.prompt([
1009
+ {
1010
+ type: "confirm",
1011
+ name: "confirmed",
1012
+ message: "Are you sure you want to uninstall the Fenwave agent?",
1013
+ default: false,
1014
+ },
1015
+ ]);
1016
+
1017
+ if (!confirmed) {
1018
+ console.log(chalk.yellow("Uninstall cancelled"));
1019
+ process.exit(0);
1020
+ }
1021
+
1022
+ const spinner = ora("Uninstalling Fenwave agent...").start();
1023
+
1024
+ // Stop daemon if running
1025
+ try {
1026
+ const status = getDaemonStatus();
1027
+ if (status.running) {
1028
+ spinner.text = "Stopping daemon...";
1029
+ await stopDaemon({ force: true, timeout: 5000 });
1030
+ }
1031
+ } catch (error) {
1032
+ // Ignore
1033
+ }
1034
+
1035
+ // Clear credentials unless --keep-data
1036
+ if (!options.keepData) {
1037
+ spinner.text = "Clearing credentials...";
1038
+ const { clearDeviceCredential } =
1039
+ await import("./store/deviceCredentialStore.js");
1040
+ const { clearNpmToken } = await import("./store/npmTokenStore.js");
1041
+ const { clearSetupState } = await import("./store/setupState.js");
1042
+
1043
+ clearDeviceCredential();
1044
+ clearNpmToken();
1045
+ clearSetupState();
1046
+ clearSession();
1047
+ const fwDir = path.join(os.homedir(), ".fenwave");
1048
+
1049
+ if (fs.existsSync(fwDir)) {
1050
+ fs.rmSync(fwDir, { recursive: true, force: true });
1051
+ }
1052
+ }
1053
+
1054
+ spinner.succeed("Agent uninstalled");
1055
+ console.log(chalk.green("\nFenwave agent uninstalled successfully\n"));
1056
+
1057
+ if (!options.keepData) {
1058
+ console.log(
1059
+ chalk.gray("All configuration and data files have been removed."),
1060
+ );
1061
+ } else {
1062
+ console.log(
1063
+ chalk.gray("Configuration and data files have been preserved."),
1064
+ );
1065
+ }
1066
+
1067
+ console.log(
1068
+ chalk.gray("\nTo reinstall: npm install -g @fenwave/agent\n"),
1069
+ );
1070
+
1071
+ process.exit(0);
1072
+ } catch (error) {
1073
+ console.error(chalk.red("Uninstall error:"), error.message);
1074
+ process.exit(1);
1075
+ }
1076
+ });
1077
+
1078
+ // Container commands
1079
+ program
1080
+ .command("containers")
1081
+ .alias("ps")
1082
+ .description("list containers")
1083
+ .option("-a, --all", "Show all containers (default shows just running)")
1084
+ .action(async (options) => {
1085
+ const spinner = ora("Fetching containers...").start();
1086
+
1087
+ try {
1088
+ const containers = await docker.listContainers({ all: options.all });
1089
+
1090
+ if (containers.length === 0) {
1091
+ spinner.succeed("No containers found");
1092
+ return;
1093
+ }
1094
+
1095
+ const containerPromises = containers.map((container) =>
1096
+ formatContainer(docker.getContainer(container.Id)),
1097
+ );
1098
+
1099
+ const formattedContainers = await Promise.all(containerPromises);
1100
+
1101
+ const containerText =
1102
+ containers.length === 1 ? "container" : "containers";
1103
+ spinner.succeed(`Found ${containers.length} ${containerText}`);
1104
+
1105
+ // Create a table for display
1106
+ const table = new Table({
1107
+ head: [
1108
+ chalk.blue("ID"),
1109
+ chalk.blue("Name"),
1110
+ chalk.blue("Image"),
1111
+ chalk.blue("Status"),
1112
+ chalk.blue("Ports"),
1113
+ chalk.blue("CPU %"),
1114
+ chalk.blue("MEM %"),
1115
+ ],
1116
+ colWidths: [15, 25, 30, 10, 20, 10, 10],
1117
+ });
1118
+
1119
+ formattedContainers.forEach((container) => {
1120
+ const status =
1121
+ container.status === "running"
1122
+ ? chalk.green(container.status)
1123
+ : chalk.red(container.status);
1124
+
1125
+ table.push([
1126
+ container.id.substring(0, 12),
1127
+ container.name,
1128
+ container.image,
1129
+ status,
1130
+ container.ports.join(", "),
1131
+ `${container.cpu}%`,
1132
+ `${container.memory}%`,
1133
+ ]);
1134
+ });
1135
+
1136
+ console.log(table.toString());
1137
+ process.exit(0);
1138
+ } catch (error) {
1139
+ spinner.fail(`Failed to fetch containers: ${error.message}`);
1140
+ process.exit(1);
1141
+ }
1142
+ });
1143
+
1144
+ // Start container(s) - supports multiple containers
1145
+ program
1146
+ .command("start <containerId(s)...>")
1147
+ .description("start one or more containers")
1148
+ .action(async (containerIds) => {
1149
+ const spinner = ora(
1150
+ `Starting ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1151
+ ).start();
1152
+
1153
+ try {
1154
+ const results = [];
1155
+ for (const containerId of containerIds) {
1156
+ try {
1157
+ const container = docker.getContainer(containerId);
1158
+ await container.start();
1159
+ results.push({ id: containerId, success: true });
1160
+ } catch (error) {
1161
+ results.push({
1162
+ id: containerId,
1163
+ success: false,
1164
+ error: error.message,
1165
+ });
1166
+ }
1167
+ }
1168
+
1169
+ const succeeded = results.filter((r) => r.success);
1170
+ const failed = results.filter((r) => !r.success);
1171
+
1172
+ if (failed.length === 0) {
1173
+ spinner.succeed(
1174
+ `Successfully started ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1175
+ );
1176
+ } else if (succeeded.length === 0) {
1177
+ spinner.fail(`Failed to start all containers`);
1178
+ failed.forEach((r) =>
1179
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1180
+ );
1181
+ } else {
1182
+ spinner.warn(
1183
+ `Started ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1184
+ );
1185
+ failed.forEach((r) =>
1186
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1187
+ );
1188
+ }
1189
+ process.exit(failed.length > 0 ? 1 : 0);
1190
+ } catch (error) {
1191
+ spinner.fail(`Failed to start containers: ${error.message}`);
1192
+ process.exit(1);
1193
+ }
1194
+ });
1195
+
1196
+ // Stop container(s) - supports multiple containers
1197
+ program
1198
+ .command("stop <containerId(s)...>")
1199
+ .description("stop one or more containers")
1200
+ .action(async (containerIds) => {
1201
+ const spinner = ora(
1202
+ `Stopping ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1203
+ ).start();
1204
+
1205
+ try {
1206
+ const results = [];
1207
+ for (const containerId of containerIds) {
1208
+ try {
1209
+ const container = docker.getContainer(containerId);
1210
+ await container.stop();
1211
+ results.push({ id: containerId, success: true });
1212
+ } catch (error) {
1213
+ results.push({
1214
+ id: containerId,
1215
+ success: false,
1216
+ error: error.message,
1217
+ });
1218
+ }
1219
+ }
1220
+
1221
+ const succeeded = results.filter((r) => r.success);
1222
+ const failed = results.filter((r) => !r.success);
1223
+
1224
+ if (failed.length === 0) {
1225
+ spinner.succeed(
1226
+ `Successfully stopped ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1227
+ );
1228
+ } else if (succeeded.length === 0) {
1229
+ spinner.fail(`Failed to stop all containers`);
1230
+ failed.forEach((r) =>
1231
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1232
+ );
1233
+ } else {
1234
+ spinner.warn(
1235
+ `Stopped ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1236
+ );
1237
+ failed.forEach((r) =>
1238
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1239
+ );
1240
+ }
1241
+ process.exit(failed.length > 0 ? 1 : 0);
1242
+ } catch (error) {
1243
+ spinner.fail(`Failed to stop containers: ${error.message}`);
1244
+ process.exit(1);
1245
+ }
1246
+ });
1247
+
1248
+ // Restart container(s) - supports multiple containers
1249
+ program
1250
+ .command("restart <containerId(s)...>")
1251
+ .description("restart one or more containers")
1252
+ .action(async (containerIds) => {
1253
+ const spinner = ora(
1254
+ `Restarting ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1255
+ ).start();
1256
+
1257
+ try {
1258
+ const results = [];
1259
+ for (const containerId of containerIds) {
1260
+ try {
1261
+ const container = docker.getContainer(containerId);
1262
+ await container.restart();
1263
+ results.push({ id: containerId, success: true });
1264
+ } catch (error) {
1265
+ results.push({
1266
+ id: containerId,
1267
+ success: false,
1268
+ error: error.message,
1269
+ });
1270
+ }
1271
+ }
1272
+
1273
+ const succeeded = results.filter((r) => r.success);
1274
+ const failed = results.filter((r) => !r.success);
1275
+
1276
+ if (failed.length === 0) {
1277
+ spinner.succeed(
1278
+ `Successfully restarted ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1279
+ );
1280
+ } else if (succeeded.length === 0) {
1281
+ spinner.fail(`Failed to restart all containers`);
1282
+ failed.forEach((r) =>
1283
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1284
+ );
1285
+ } else {
1286
+ spinner.warn(
1287
+ `Restarted ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1288
+ );
1289
+ failed.forEach((r) =>
1290
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1291
+ );
1292
+ }
1293
+ process.exit(failed.length > 0 ? 1 : 0);
1294
+ } catch (error) {
1295
+ spinner.fail(`Failed to restart containers: ${error.message}`);
1296
+ process.exit(1);
1297
+ }
1298
+ });
1299
+
1300
+ // Remove container(s) - supports multiple containers
1301
+ program
1302
+ .command("rm <containerId(s)...>")
1303
+ .description("remove one or more containers")
1304
+ .option("-f, --force", "Force remove the container(s)")
1305
+ .action(async (containerIds, options) => {
1306
+ const spinner = ora(
1307
+ `Removing ${containerIds.length} ${containerIds.length === 1 ? "container" : "containers"}...`,
1308
+ ).start();
1309
+
1310
+ try {
1311
+ const results = [];
1312
+ for (const containerId of containerIds) {
1313
+ try {
1314
+ const container = docker.getContainer(containerId);
1315
+ await container.remove({ force: options.force });
1316
+ results.push({ id: containerId, success: true });
1317
+ } catch (error) {
1318
+ results.push({
1319
+ id: containerId,
1320
+ success: false,
1321
+ error: error.message,
1322
+ });
1323
+ }
1324
+ }
1325
+
1326
+ const succeeded = results.filter((r) => r.success);
1327
+ const failed = results.filter((r) => !r.success);
1328
+
1329
+ if (failed.length === 0) {
1330
+ spinner.succeed(
1331
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1332
+ );
1333
+ } else if (succeeded.length === 0) {
1334
+ spinner.fail(`Failed to remove all containers`);
1335
+ failed.forEach((r) =>
1336
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1337
+ );
1338
+ } else {
1339
+ spinner.warn(
1340
+ `Removed ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? "container" : "containers"}`,
1341
+ );
1342
+ failed.forEach((r) =>
1343
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1344
+ );
1345
+ }
1346
+ process.exit(failed.length > 0 ? 1 : 0);
1347
+ } catch (error) {
1348
+ spinner.fail(`Failed to remove containers: ${error.message}`);
1349
+ process.exit(1);
1350
+ }
1351
+ });
1352
+
1353
+ // Image commands
1354
+ program
1355
+ .command("images")
1356
+ .description("list images")
1357
+ .action(async () => {
1358
+ const spinner = ora("Fetching images...").start();
1359
+
1360
+ try {
1361
+ const images = await docker.listImages();
1362
+
1363
+ if (images.length === 0) {
1364
+ spinner.succeed("No images found");
1365
+ return;
1366
+ }
1367
+
1368
+ spinner.succeed(
1369
+ `Found ${images.length} ${images.length === 1 ? "image" : "images"}`,
1370
+ );
1371
+
1372
+ // Create a table for display
1373
+ const table = new Table({
1374
+ head: [
1375
+ chalk.blue("ID"),
1376
+ chalk.blue("Repository"),
1377
+ chalk.blue("Tag"),
1378
+ chalk.blue("Size"),
1379
+ chalk.blue("Created"),
1380
+ ],
1381
+ colWidths: [15, 30, 15, 15, 15],
1382
+ });
1383
+
1384
+ images.forEach((image) => {
1385
+ // Extract repository and tag
1386
+ let name = "<none>";
1387
+ let tag = "<none>";
1388
+
1389
+ if (image.RepoTags && image.RepoTags.length > 0) {
1390
+ const [repoTag] = image.RepoTags;
1391
+ const parts = repoTag.split(":");
1392
+ name = parts[0];
1393
+ tag = parts.length > 1 ? parts[1] : "latest";
1394
+ }
1395
+
1396
+ table.push([
1397
+ image.Id.substring(7, 19),
1398
+ name,
1399
+ tag,
1400
+ formatSize(image.Size),
1401
+ formatCreatedTime(image.Created),
1402
+ ]);
1403
+ });
1404
+
1405
+ console.log(table.toString());
1406
+ process.exit(0);
1407
+ } catch (error) {
1408
+ spinner.fail(`Failed to fetch images: ${error.message}`);
1409
+ process.exit(1);
1410
+ }
1411
+ });
1412
+
1413
+ // Pull image(s) - supports multiple images
1414
+ program
1415
+ .command("pull <imageTags...>")
1416
+ .description("pull one or more images")
1417
+ .action(async (imageTags) => {
1418
+ const spinner = ora(
1419
+ `Pulling ${imageTags.length} ${imageTags.length === 1 ? "image" : "images"}...`,
1420
+ ).start();
1421
+
1422
+ try {
1423
+ const results = [];
1424
+ for (const imageTag of imageTags) {
1425
+ try {
1426
+ // Split image tag into name and tag
1427
+ const [name, tag = "latest"] = imageTag.split(":");
1428
+ spinner.text = `Pulling ${imageTag}...`;
1429
+
1430
+ // Pull the image
1431
+ const stream = await docker.pull(`${name}:${tag}`);
1432
+
1433
+ // Track progress
1434
+ await new Promise((resolve, reject) => {
1435
+ docker.modem.followProgress(
1436
+ stream,
1437
+ (err, output) => {
1438
+ if (err) {
1439
+ reject(err);
1440
+ return;
1441
+ }
1442
+ resolve(output);
1443
+ },
1444
+ (event) => {
1445
+ if (event.progress) {
1446
+ spinner.text = `Pulling ${imageTag}: ${event.progress}`;
1447
+ } else if (event.status) {
1448
+ spinner.text = `Pulling ${imageTag}: ${event.status}`;
1449
+ }
1450
+ },
1451
+ );
1452
+ });
1453
+
1454
+ results.push({ tag: imageTag, success: true });
1455
+ } catch (error) {
1456
+ results.push({
1457
+ tag: imageTag,
1458
+ success: false,
1459
+ error: error.message,
1460
+ });
1461
+ }
1462
+ }
1463
+
1464
+ const succeeded = results.filter((r) => r.success);
1465
+ const failed = results.filter((r) => !r.success);
1466
+
1467
+ if (failed.length === 0) {
1468
+ spinner.succeed(
1469
+ `Successfully pulled ${succeeded.length} ${succeeded.length === 1 ? "image" : "images"}`,
1470
+ );
1471
+ } else if (succeeded.length === 0) {
1472
+ spinner.fail(`Failed to pull all images`);
1473
+ failed.forEach((r) =>
1474
+ console.log(chalk.red(` - ${r.tag}: ${r.error}`)),
1475
+ );
1476
+ } else {
1477
+ spinner.warn(
1478
+ `Pulled ${succeeded.length}/${imageTags.length} ${succeeded.length === 1 ? "image" : "images"}`,
1479
+ );
1480
+ failed.forEach((r) =>
1481
+ console.log(chalk.red(` - ${r.tag}: ${r.error}`)),
1482
+ );
1483
+ }
1484
+ process.exit(failed.length > 0 ? 1 : 0);
1485
+ } catch (error) {
1486
+ spinner.fail(`Failed to pull images: ${error.message}`);
1487
+ process.exit(1);
1488
+ }
1489
+ });
1490
+
1491
+ // Remove image(s) - supports multiple images
1492
+ program
1493
+ .command("rmi <imageIds...>")
1494
+ .description("remove one or more images")
1495
+ .option("-f, --force", "Force remove the image(s)")
1496
+ .action(async (imageIds, options) => {
1497
+ const spinner = ora(
1498
+ `Removing ${imageIds.length} ${imageIds.length === 1 ? "image" : "images"}...`,
1499
+ ).start();
1500
+
1501
+ try {
1502
+ const results = [];
1503
+ for (const imageId of imageIds) {
1504
+ try {
1505
+ const image = docker.getImage(imageId);
1506
+ await image.remove({ force: options.force });
1507
+ results.push({ id: imageId, success: true });
1508
+ } catch (error) {
1509
+ results.push({ id: imageId, success: false, error: error.message });
1510
+ }
1511
+ }
1512
+
1513
+ const succeeded = results.filter((r) => r.success);
1514
+ const failed = results.filter((r) => !r.success);
1515
+
1516
+ if (failed.length === 0) {
1517
+ spinner.succeed(
1518
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "image" : "images"}`,
1519
+ );
1520
+ } else if (succeeded.length === 0) {
1521
+ spinner.fail(`Failed to remove all images`);
1522
+ failed.forEach((r) =>
1523
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1524
+ );
1525
+ } else {
1526
+ spinner.warn(
1527
+ `Removed ${succeeded.length}/${imageIds.length} ${succeeded.length === 1 ? "image" : "images"}`,
1528
+ );
1529
+ failed.forEach((r) =>
1530
+ console.log(chalk.red(` - ${r.id}: ${r.error}`)),
1531
+ );
1532
+ }
1533
+ process.exit(failed.length > 0 ? 1 : 0);
1534
+ } catch (error) {
1535
+ spinner.fail(`Failed to remove images: ${error.message}`);
1536
+ process.exit(1);
1537
+ }
1538
+ });
1539
+
1540
+ // Volume commands
1541
+ program
1542
+ .command("volumes")
1543
+ .description("list volumes")
1544
+ .action(async () => {
1545
+ const spinner = ora("Fetching volumes...").start();
1546
+
1547
+ try {
1548
+ const { Volumes } = await docker.listVolumes();
1549
+
1550
+ if (Volumes.length === 0) {
1551
+ spinner.succeed("No volumes found");
1552
+ return;
1553
+ }
1554
+
1555
+ spinner.succeed(
1556
+ `Found ${Volumes.length} ${Volumes.length === 1 ? "volume" : "volumes"}`,
1557
+ );
1558
+
1559
+ // Create a table for display
1560
+ const table = new Table({
1561
+ head: [
1562
+ chalk.blue("Name"),
1563
+ chalk.blue("Driver"),
1564
+ chalk.blue("Mountpoint"),
1565
+ ],
1566
+ colWidths: [30, 15, 50],
1567
+ });
1568
+
1569
+ Volumes.forEach((volume) => {
1570
+ table.push([volume.Name, volume.Driver, volume.Mountpoint]);
1571
+ });
1572
+
1573
+ console.log(table.toString());
1574
+ process.exit(0);
1575
+ } catch (error) {
1576
+ spinner.fail(`Failed to fetch volumes: ${error.message}`);
1577
+ process.exit(1);
1578
+ }
1579
+ });
1580
+
1581
+ // Create volume(s) - supports multiple volumes
1582
+ program
1583
+ .command("volume-create <names...>")
1584
+ .description("create one or more volumes")
1585
+ .option("-d, --driver <driver>", "Volume driver", "local")
1586
+ .action(async (names, options) => {
1587
+ const spinner = ora(
1588
+ `Creating ${names.length} ${names.length === 1 ? "volume" : "volumes"}...`,
1589
+ ).start();
1590
+
1591
+ try {
1592
+ const results = [];
1593
+ for (const name of names) {
1594
+ try {
1595
+ await docker.createVolume({
1596
+ Name: name,
1597
+ Driver: options.driver,
1598
+ });
1599
+ results.push({ name, success: true });
1600
+ } catch (error) {
1601
+ results.push({ name, success: false, error: error.message });
1602
+ }
1603
+ }
1604
+
1605
+ const succeeded = results.filter((r) => r.success);
1606
+ const failed = results.filter((r) => !r.success);
1607
+
1608
+ if (failed.length === 0) {
1609
+ spinner.succeed(
1610
+ `Successfully created ${succeeded.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1611
+ );
1612
+ } else if (succeeded.length === 0) {
1613
+ spinner.fail(`Failed to create all volumes`);
1614
+ failed.forEach((r) =>
1615
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1616
+ );
1617
+ } else {
1618
+ spinner.warn(
1619
+ `Created ${succeeded.length}/${names.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1620
+ );
1621
+ failed.forEach((r) =>
1622
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1623
+ );
1624
+ }
1625
+ process.exit(failed.length > 0 ? 1 : 0);
1626
+ } catch (error) {
1627
+ spinner.fail(`Failed to create volumes: ${error.message}`);
1628
+ process.exit(1);
1629
+ }
1630
+ });
1631
+
1632
+ // Remove volume(s) - supports multiple volumes
1633
+ program
1634
+ .command("volume-rm <names...>")
1635
+ .description("remove one or more volumes")
1636
+ .action(async (names) => {
1637
+ const spinner = ora(
1638
+ `Removing ${names.length} ${names.length === 1 ? "volume" : "volumes"}...`,
1639
+ ).start();
1640
+
1641
+ try {
1642
+ const results = [];
1643
+ for (const name of names) {
1644
+ try {
1645
+ const volume = docker.getVolume(name);
1646
+ await volume.remove();
1647
+ results.push({ name, success: true });
1648
+ } catch (error) {
1649
+ results.push({ name, success: false, error: error.message });
1650
+ }
1651
+ }
1652
+
1653
+ const succeeded = results.filter((r) => r.success);
1654
+ const failed = results.filter((r) => !r.success);
1655
+
1656
+ if (failed.length === 0) {
1657
+ spinner.succeed(
1658
+ `Successfully removed ${succeeded.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1659
+ );
1660
+ } else if (succeeded.length === 0) {
1661
+ spinner.fail(`Failed to remove all volumes`);
1662
+ failed.forEach((r) =>
1663
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1664
+ );
1665
+ } else {
1666
+ spinner.warn(
1667
+ `Removed ${succeeded.length}/${names.length} ${succeeded.length === 1 ? "volume" : "volumes"}`,
1668
+ );
1669
+ failed.forEach((r) =>
1670
+ console.log(chalk.red(` - ${r.name}: ${r.error}`)),
1671
+ );
1672
+ }
1673
+ process.exit(failed.length > 0 ? 1 : 0);
1674
+ } catch (error) {
1675
+ spinner.fail(`Failed to remove volumes: ${error.message}`);
1676
+ process.exit(1);
1677
+ }
1678
+ });
1679
+
1680
+ // Logs command
1681
+ program
1682
+ .command("logs <containerId>")
1683
+ .description("fetch container logs")
1684
+ .option("-f, --follow", "Follow log output")
1685
+ .option("-t, --tail <lines>", "Number of lines to show from the end", "100")
1686
+ .action(async (containerId, options) => {
1687
+ try {
1688
+ const container = docker.getContainer(containerId);
1689
+
1690
+ if (options.follow) {
1691
+ console.log(
1692
+ chalk.blue(`Following logs for container ${containerId}...`),
1693
+ );
1694
+ console.log(chalk.blue("Press Ctrl+C to exit"));
1695
+
1696
+ const logStream = await container.logs({
1697
+ follow: true,
1698
+ stdout: true,
1699
+ stderr: true,
1700
+ tail: Number.parseInt(options.tail, 10),
1701
+ });
1702
+
1703
+ logStream.on("data", (chunk) => {
1704
+ process.stdout.write(chunk.toString());
1705
+ });
1706
+
1707
+ // Handle Ctrl+C
1708
+ process.on("SIGINT", () => {
1709
+ console.log(chalk.blue("\nStopping log stream..."));
1710
+ process.exit(0);
1711
+ });
1712
+ } else {
1713
+ const spinner = ora(
1714
+ `Fetching logs for container ${containerId}...`,
1715
+ ).start();
1716
+
1717
+ const logs = await container.logs({
1718
+ stdout: true,
1719
+ stderr: true,
1720
+ tail: Number.parseInt(options.tail, 10),
1721
+ });
1722
+
1723
+ spinner.stop();
1724
+ console.log(logs.toString());
1725
+ process.exit(0);
1726
+ }
1727
+ } catch (error) {
1728
+ console.error(chalk.red(`Failed to fetch logs: ${error.message}`));
1729
+ process.exit(1);
1730
+ }
1731
+ });
1732
+
1733
+ // Local-env app commands
1734
+ program
1735
+ .command("local-env")
1736
+ .description("manage local-env app container")
1737
+ .option("--start", "start the local-env app")
1738
+ .option("--stop", "stop the local-env app")
1739
+ .option("--status", "show local-env app status")
1740
+ .option("--logs", "show local-env app logs")
1741
+ .option("--logs-follow", "follow local-env app logs")
1742
+ .action(async (options) => {
1743
+ try {
1744
+ if (options.start) {
1745
+ const spinner = ora("Starting local-env app...").start();
1746
+ try {
1747
+ await containerManager.startContainer();
1748
+ spinner.succeed("Local-env app started successfully");
1749
+ } catch (error) {
1750
+ spinner.fail(`Failed to start local-env app: ${error.message}`);
1751
+ process.exit(1);
1752
+ }
1753
+ } else if (options.stop) {
1754
+ const spinner = ora("Stopping local-env app...").start();
1755
+ try {
1756
+ await containerManager.stopContainerGracefully();
1757
+ spinner.succeed("Local-env app stopped successfully");
1758
+ } catch (error) {
1759
+ spinner.fail(`Failed to stop local-env app: ${error.message}`);
1760
+ process.exit(1);
1761
+ }
1762
+ } else if (options.status) {
1763
+ const status = await containerManager.getStatus();
1764
+ console.log(chalk.bold("\nLocal-env App Status:"));
1765
+ console.log(chalk.blue("Container:"), status.containerName);
1766
+ console.log(
1767
+ chalk.blue("Running:"),
1768
+ status.isRunning ? chalk.green("Yes") : chalk.red("No"),
1769
+ );
1770
+ console.log(chalk.blue("Port:"), status.port);
1771
+ console.log(chalk.blue("Data Directory:"), status.dataDirectory);
1772
+ if (status.isRunning) {
1773
+ console.log(
1774
+ chalk.blue("URL:"),
1775
+ chalk.underline(`http://localhost:${status.port}`),
1776
+ );
1777
+ }
1778
+ } else if (options.logs) {
1779
+ console.log(chalk.blue("Showing local-env app logs..."));
1780
+ containerManager.showLogs(false);
1781
+ } else if (options.logsFollow) {
1782
+ console.log(
1783
+ chalk.blue(
1784
+ "Following local-env app logs... (Press Ctrl+C to stop)",
1785
+ ),
1786
+ );
1787
+ containerManager.showLogs(true);
1788
+ } else {
1789
+ console.log(
1790
+ chalk.yellow(
1791
+ "Please specify an action: --start, --stop, --status, --logs, or --logs-follow",
1792
+ ),
1793
+ );
1794
+ process.exit(1);
1795
+ }
1796
+ } catch (error) {
1797
+ console.error(chalk.red("Error:"), error.message);
1798
+ process.exit(1);
1799
+ }
1800
+ });
1801
+
1802
+ // Registry commands
1803
+ program
1804
+ .command("registries")
1805
+ .description("list registries")
1806
+ .action(async () => {
1807
+ const spinner = ora("Fetching registries...").start();
1808
+
1809
+ try {
1810
+ await registryStore.initialize();
1811
+ const registries = await registryStore.getAllRegistries();
1812
+
1813
+ if (registries.length === 0) {
1814
+ spinner.succeed("No registries found");
1815
+ return;
1816
+ }
1817
+
1818
+ const registryText =
1819
+ registries.length === 1 ? "registry" : "registries";
1820
+ spinner.succeed(`Found ${registries.length} ${registryText}`);
1821
+
1822
+ const table = new Table({
1823
+ head: ["ID", "Name", "Type", "URL", "Connected"].map((h) =>
1824
+ chalk.cyan(h),
1825
+ ),
1826
+ style: { head: [], border: [] },
1827
+ });
1828
+
1829
+ for (const registry of registries) {
1830
+ table.push([
1831
+ registry.id.substring(0, 12) + "...",
1832
+ registry.name,
1833
+ registry.type,
1834
+ registry.url,
1835
+ registry.connected ? chalk.green("Y") : chalk.red("N"),
1836
+ ]);
1837
+ }
1838
+
1839
+ console.log(chalk.bold("\nConnected Registries:"));
1840
+ console.log(table.toString());
1841
+ process.exit(0);
1842
+ } catch (error) {
1843
+ spinner.fail(`Failed to fetch registries: ${error.message}`);
1844
+ process.exit(1);
1845
+ }
1846
+ });
1847
+
1848
+ // Agent info command
1849
+ program
1850
+ .command("info")
1851
+ .description("display agent information")
1852
+ .action(async () => {
1853
+ const spinner = ora("Fetching agent information...").start();
1854
+
1855
+ try {
1856
+ // Get Docker version
1857
+ const dockerVersion = await docker.version();
1858
+
1859
+ // Display agent information
1860
+ spinner.stop();
1861
+ const { version } = packageJson;
1862
+
1863
+ console.log(chalk.bold("\nFenwave Agent Information:"));
1864
+ console.log(chalk.blue("Version:"), version);
1865
+ console.log(chalk.blue("Hostname:"), os.hostname());
1866
+ console.log(chalk.blue("Platform:"), os.platform());
1867
+ console.log(chalk.blue("Architecture:"), os.arch());
1868
+ console.log(chalk.blue("Node.js Version:"), process.version);
1869
+ console.log(chalk.blue("Docker Version:"), dockerVersion.Version);
1870
+ console.log(chalk.blue("CPU Cores:"), os.cpus().length);
1871
+ console.log(
1872
+ chalk.blue("Memory:"),
1873
+ `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`,
1874
+ );
1875
+
1876
+ // Check daemon status
1877
+ const daemonStatus = getDaemonStatus();
1878
+ if (daemonStatus.running) {
1879
+ console.log(
1880
+ chalk.blue("Status:"),
1881
+ chalk.green(`Running (PID: ${daemonStatus.pid})`),
1882
+ );
1883
+ console.log(chalk.blue("Port:"), daemonStatus.port);
1884
+ console.log(
1885
+ chalk.blue("Uptime:"),
1886
+ formatDaemonUptime(daemonStatus.uptime),
1887
+ );
1888
+ } else {
1889
+ // Fallback to checking port
1890
+ const isAgentRunning = await checkAgentRunning(WS_PORT);
1891
+ if (isAgentRunning) {
1892
+ try {
1893
+ const agentStartTime = await agentStore.loadAgentStartTime();
1894
+ if (agentStartTime) {
1895
+ const currentTime = Date.now();
1896
+ const agentUptimeMs = currentTime - agentStartTime.getTime();
1897
+ console.log(chalk.blue("Uptime:"), formatUptime(agentUptimeMs));
1898
+ } else {
1899
+ console.log(chalk.blue("Uptime:"), "0 seconds");
1900
+ }
1901
+ } catch (error) {
1902
+ console.log(chalk.blue("Uptime:"), "0 seconds");
1903
+ }
1904
+ } else {
1905
+ console.log(chalk.blue("Status:"), chalk.red("Agent Not Running"));
1906
+ try {
1907
+ await agentStore.clearAgentInfo();
1908
+ } catch (error) {
1909
+ // Ignore cleanup errors
1910
+ }
1911
+ }
1912
+ }
1913
+ process.exit(0);
1914
+ } catch (error) {
1915
+ spinner.fail(`Failed to fetch agent information: ${error.message}`);
1916
+ process.exit(1);
1917
+ }
1918
+ });
1919
+ }
1920
+
1921
+ export { setupCLICommands };