@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.
- package/LICENSE +29 -0
- package/README.md +220 -0
- package/auth-callback.html +97 -0
- package/auth.js +276 -0
- package/cli-commands.js +1923 -0
- package/containerManager.js +304 -0
- package/daemon/agentRunner.js +429 -0
- package/daemon/daemonEntry.js +64 -0
- package/daemon/daemonManager.js +271 -0
- package/daemon/logManager.js +227 -0
- package/dist/styles.css +504 -0
- package/docker-actions/apps.js +3938 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +355 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +224 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/setup-tasks.js +859 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +83 -0
- package/index.js +341 -0
- package/package.json +82 -0
- package/postcss.config.mjs +5 -0
- package/scripts/release.sh +212 -0
- package/setup/setupWizard.js +403 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +171 -0
- package/store/daemonStore.js +217 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/styles.css +1 -0
- package/utils/appLogger.js +223 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +44 -0
- package/utils/errorHandler.js +327 -0
- package/utils/portUtils.js +59 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/utils/ssl-certificates.js +256 -0
- package/websocket-server.js +415 -0
package/cli-commands.js
ADDED
|
@@ -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 };
|