@fenwave/agent 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -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/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
package/index.js
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from "http";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { program } from "commander";
|
|
10
|
+
import { v4 as uuidv4 } from "uuid";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
import dotenv from "dotenv";
|
|
14
|
+
|
|
15
|
+
// ES module helpers
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
// Ensure environment files exist
|
|
20
|
+
import { ensureEnvironmentFiles } from "./utils/envSetup.js";
|
|
21
|
+
const agentEnvPath = ensureEnvironmentFiles(__dirname);
|
|
22
|
+
|
|
23
|
+
// Load environment variables from .env.agent
|
|
24
|
+
dotenv.config({ path: agentEnvPath });
|
|
25
|
+
|
|
26
|
+
// Load environment variables
|
|
27
|
+
dotenv.config();
|
|
28
|
+
|
|
29
|
+
// Import ES modules
|
|
30
|
+
import { setupWebSocketServer, readWsToken } from "./websocket-server.js";
|
|
31
|
+
import { setupCLICommands } from "./cli-commands.js";
|
|
32
|
+
import containerManager from "./containerManager.js";
|
|
33
|
+
import registryStore from "./store/registryStore.js";
|
|
34
|
+
import agentStore from "./store/agentStore.js";
|
|
35
|
+
import { loadConfig } from "./store/configStore.js";
|
|
36
|
+
import { initializeAgentStartTime } from "./docker-actions/general.js";
|
|
37
|
+
import { checkAppHasBeenRun } from "./docker-actions/apps.js";
|
|
38
|
+
import {
|
|
39
|
+
loadSession,
|
|
40
|
+
isSessionValid,
|
|
41
|
+
handleSessionExpiry,
|
|
42
|
+
createSession,
|
|
43
|
+
saveSession,
|
|
44
|
+
clearSession,
|
|
45
|
+
setupSessionWatcher,
|
|
46
|
+
loadBackendUrl,
|
|
47
|
+
} from "./auth.js";
|
|
48
|
+
|
|
49
|
+
// Version from package.json
|
|
50
|
+
const packageJson = JSON.parse(
|
|
51
|
+
fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
52
|
+
);
|
|
53
|
+
const { version } = packageJson;
|
|
54
|
+
|
|
55
|
+
// Store active connections
|
|
56
|
+
const clients = new Map();
|
|
57
|
+
|
|
58
|
+
// Load configuration from config file (saved during fenwave init) or fall back to env variables/defaults
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
const BACKEND_URL = config.backendUrl;
|
|
61
|
+
const FRONTEND_URL = config.frontendUrl;
|
|
62
|
+
const AUTH_TIMEOUT_MS = config.authTimeoutMs;
|
|
63
|
+
const WS_PORT = config.wsPort;
|
|
64
|
+
const CONTAINER_PORT = config.containerPort;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Acknowledge a version_published event after it has been displayed
|
|
68
|
+
*/
|
|
69
|
+
async function acknowledgeVersionPublishedEvent(
|
|
70
|
+
sessionToken,
|
|
71
|
+
eventId,
|
|
72
|
+
changeId
|
|
73
|
+
) {
|
|
74
|
+
try {
|
|
75
|
+
await axios.post(
|
|
76
|
+
`${BACKEND_URL}/api/agent-cli/acknowledge-event`,
|
|
77
|
+
{
|
|
78
|
+
token: sessionToken,
|
|
79
|
+
eventId: eventId,
|
|
80
|
+
changeId: changeId,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
timeout: 5000,
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.log(
|
|
91
|
+
chalk.yellow(
|
|
92
|
+
`⚠️ Failed to acknowledge event ${changeId}: ${error.message}`
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Poll for app update events and broadcast to connected clients
|
|
100
|
+
*/
|
|
101
|
+
async function pollAppUpdates(sessionToken, wss) {
|
|
102
|
+
try {
|
|
103
|
+
const response = await axios.post(
|
|
104
|
+
`${BACKEND_URL}/api/agent-cli/poll-events`,
|
|
105
|
+
{
|
|
106
|
+
token: sessionToken,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
timeout: 10000,
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
response.data &&
|
|
118
|
+
response.data.events &&
|
|
119
|
+
response.data.events.length > 0
|
|
120
|
+
) {
|
|
121
|
+
const events = response.data.events;
|
|
122
|
+
|
|
123
|
+
// Filter and process events
|
|
124
|
+
let broadcastedCount = 0;
|
|
125
|
+
|
|
126
|
+
for (const event of events) {
|
|
127
|
+
const eventType = event.event_type || "blueprint_changed";
|
|
128
|
+
|
|
129
|
+
// For blueprint_changed events, check if the app has ever been run locally
|
|
130
|
+
// If not, skip broadcasting (app modifications before first run are part of initial setup)
|
|
131
|
+
if (eventType === "blueprint_changed") {
|
|
132
|
+
const hasBeenRun = await checkAppHasBeenRun(event.app_name);
|
|
133
|
+
|
|
134
|
+
if (!hasBeenRun) {
|
|
135
|
+
acknowledgeVersionPublishedEvent(
|
|
136
|
+
sessionToken,
|
|
137
|
+
event.id,
|
|
138
|
+
event.change_id
|
|
139
|
+
);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
broadcastedCount++;
|
|
145
|
+
|
|
146
|
+
const message = JSON.stringify({
|
|
147
|
+
type: eventType,
|
|
148
|
+
appId: event.app_id,
|
|
149
|
+
backstageId: event.app_id,
|
|
150
|
+
appName: event.app_name,
|
|
151
|
+
version: event.version,
|
|
152
|
+
changeId: event.change_id,
|
|
153
|
+
timestamp: event.created_at,
|
|
154
|
+
actor: event.actor,
|
|
155
|
+
changedAt: event.created_at,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
wss.clients.forEach((client) => {
|
|
159
|
+
if (client.readyState === 1) {
|
|
160
|
+
// WebSocket.OPEN
|
|
161
|
+
client.send(message);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Auto-acknowledge version_published events after broadcasting
|
|
166
|
+
// (these are just notifications, not sync requests)
|
|
167
|
+
if (eventType === "version_published") {
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
acknowledgeVersionPublishedEvent(
|
|
170
|
+
sessionToken,
|
|
171
|
+
event.id,
|
|
172
|
+
event.change_id
|
|
173
|
+
);
|
|
174
|
+
}, 2000); // 2 second delay to ensure devapp receives it
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (broadcastedCount > 0) {
|
|
179
|
+
console.log(
|
|
180
|
+
chalk.blue(
|
|
181
|
+
`📢 Received ${broadcastedCount} app update ${
|
|
182
|
+
broadcastedCount === 1 ? "event" : "events"
|
|
183
|
+
}`
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// Silently handle polling errors
|
|
190
|
+
if (error.response?.status !== 401) {
|
|
191
|
+
const isConnRefused =
|
|
192
|
+
(error.message && error.message.includes("ECONNREFUSED")) ||
|
|
193
|
+
error.code === "ECONNREFUSED";
|
|
194
|
+
|
|
195
|
+
if (isConnRefused) {
|
|
196
|
+
console.error(
|
|
197
|
+
chalk.red(
|
|
198
|
+
"❌ App update polling error: Please ensure backstage is running."
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
console.error(
|
|
203
|
+
chalk.red(`❌ App update polling error: ${error.message}`)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Start periodic polling for app updates
|
|
212
|
+
*/
|
|
213
|
+
function startAppUpdatePolling(sessionToken, wss) {
|
|
214
|
+
// Poll every 30 seconds
|
|
215
|
+
const pollInterval = setInterval(() => {
|
|
216
|
+
pollAppUpdates(sessionToken, wss);
|
|
217
|
+
}, 30000);
|
|
218
|
+
|
|
219
|
+
console.log(
|
|
220
|
+
chalk.blue("🔄 Started polling for app updates (every 30 seconds)")
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return pollInterval;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Store server instances for graceful shutdown
|
|
227
|
+
let serverInstances = null;
|
|
228
|
+
|
|
229
|
+
// Graceful shutdown handler
|
|
230
|
+
function gracefulShutdown() {
|
|
231
|
+
console.log(chalk.red("\n🔒 Fenwave Agent shutting down gracefully..."));
|
|
232
|
+
|
|
233
|
+
if (serverInstances) {
|
|
234
|
+
// Clear polling interval
|
|
235
|
+
if (serverInstances.pollInterval) {
|
|
236
|
+
clearInterval(serverInstances.pollInterval);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Close session watcher
|
|
240
|
+
if (serverInstances.sessionWatcher) {
|
|
241
|
+
serverInstances.sessionWatcher.close();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Close WebSocket connections
|
|
245
|
+
if (serverInstances.wss) {
|
|
246
|
+
serverInstances.wss.clients.forEach((client) => {
|
|
247
|
+
client.send(
|
|
248
|
+
JSON.stringify({
|
|
249
|
+
type: "agent_shutdown",
|
|
250
|
+
message: "Agent is shutting down",
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
client.close();
|
|
254
|
+
});
|
|
255
|
+
serverInstances.wss.close();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Close HTTP server
|
|
259
|
+
if (serverInstances.server) {
|
|
260
|
+
serverInstances.server.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Clear agent info from persistent storage
|
|
265
|
+
agentStore.clearAgentInfo().catch((err) => {
|
|
266
|
+
console.error("Failed to clear agent info:", err);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Stop container
|
|
270
|
+
containerManager
|
|
271
|
+
.stopContainerGracefully()
|
|
272
|
+
.catch((error) => console.error("Error stopping container:", error.message))
|
|
273
|
+
.finally(() => process.exit(0));
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle graceful shutdown signals
|
|
278
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
279
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
280
|
+
|
|
281
|
+
// Start the WebSocket server
|
|
282
|
+
async function startServer(port = WS_PORT) {
|
|
283
|
+
return new Promise(async (resolve, reject) => {
|
|
284
|
+
try {
|
|
285
|
+
// Check for existing session first and validate it
|
|
286
|
+
console.log(chalk.blue("🔍 Checking for existing session..."));
|
|
287
|
+
const existingSession = loadSession();
|
|
288
|
+
if (existingSession) {
|
|
289
|
+
if (isSessionValid(existingSession)) {
|
|
290
|
+
console.log(
|
|
291
|
+
chalk.green("✅ Found valid session, using existing credentials")
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Initialize registry store
|
|
295
|
+
await registryStore.initialize();
|
|
296
|
+
|
|
297
|
+
// Start local-env container
|
|
298
|
+
console.log(chalk.blue("🐳 Starting container..."));
|
|
299
|
+
try {
|
|
300
|
+
await containerManager.startContainer();
|
|
301
|
+
console.log(chalk.cyan("✅ Container started successfully"));
|
|
302
|
+
} catch (containerError) {
|
|
303
|
+
console.warn(
|
|
304
|
+
chalk.yellow("⚠️ Failed to start container:"),
|
|
305
|
+
containerError.message
|
|
306
|
+
);
|
|
307
|
+
console.log(chalk.blue("📡 Starting agent without container..."));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Start WebSocket server with existing session
|
|
311
|
+
const agentId = uuidv4();
|
|
312
|
+
|
|
313
|
+
// Try to read existing WebSocket token
|
|
314
|
+
const existingWsToken = readWsToken();
|
|
315
|
+
|
|
316
|
+
const server = http.createServer();
|
|
317
|
+
|
|
318
|
+
server.listen(port, async () => {
|
|
319
|
+
// Initialize agent start time
|
|
320
|
+
await initializeAgentStartTime();
|
|
321
|
+
console.log(chalk.blue("📡 Agent is ready to receive connections"));
|
|
322
|
+
|
|
323
|
+
// Display connection information
|
|
324
|
+
console.log(
|
|
325
|
+
chalk.green(
|
|
326
|
+
"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
console.log(chalk.green("✅ Fenwave Agent Started Successfully"));
|
|
330
|
+
console.log(
|
|
331
|
+
chalk.green(
|
|
332
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
console.log(chalk.white("🌐 Fenwave DevApp Dashboard:"));
|
|
336
|
+
console.log(chalk.cyan(` http://localhost:${CONTAINER_PORT}\n`));
|
|
337
|
+
console.log(chalk.white("🔌 WebSocket Server:"));
|
|
338
|
+
console.log(chalk.cyan(` ws://localhost:${port}\n`));
|
|
339
|
+
console.log(chalk.white("👤 User:"));
|
|
340
|
+
console.log(chalk.cyan(` ${existingSession.userEntityRef}\n`));
|
|
341
|
+
console.log(
|
|
342
|
+
chalk.green(
|
|
343
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const wss = setupWebSocketServer(
|
|
349
|
+
server,
|
|
350
|
+
clients,
|
|
351
|
+
agentId,
|
|
352
|
+
{
|
|
353
|
+
userEntityRef: existingSession.userEntityRef,
|
|
354
|
+
expiresAt: existingSession.expiresAt,
|
|
355
|
+
},
|
|
356
|
+
existingWsToken
|
|
357
|
+
); // Pass the existing token to reuse it
|
|
358
|
+
|
|
359
|
+
// Set up file watcher for session monitoring
|
|
360
|
+
const sessionWatcher = setupSessionWatcher(
|
|
361
|
+
handleSessionExpiry,
|
|
362
|
+
server,
|
|
363
|
+
wss
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Start app update polling
|
|
367
|
+
const pollInterval = startAppUpdatePolling(
|
|
368
|
+
existingSession.token,
|
|
369
|
+
wss
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
resolve({
|
|
373
|
+
server,
|
|
374
|
+
wss,
|
|
375
|
+
sessionToken: existingSession.token,
|
|
376
|
+
userEntityRef: existingSession.userEntityRef,
|
|
377
|
+
agentId,
|
|
378
|
+
sessionWatcher,
|
|
379
|
+
pollInterval,
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error(
|
|
383
|
+
chalk.red("❌ Failed to setup WebSocket server:"),
|
|
384
|
+
error.message
|
|
385
|
+
);
|
|
386
|
+
reject(error);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return;
|
|
391
|
+
} else {
|
|
392
|
+
// Session expired, clear it immediately
|
|
393
|
+
clearSession();
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
console.log(
|
|
397
|
+
chalk.blue(
|
|
398
|
+
"🔑 No existing session found, proceeding with new authentication"
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Pre-check: Test Backstage connectivity before opening browser
|
|
404
|
+
try {
|
|
405
|
+
await axios.get(`${BACKEND_URL}`, {
|
|
406
|
+
timeout: 5000,
|
|
407
|
+
validateStatus: () => true, // Accept any HTTP status code (404, 401, etc. - they all mean server is up)
|
|
408
|
+
});
|
|
409
|
+
} catch (connectivityError) {
|
|
410
|
+
console.error(chalk.red("❌ Cannot connect to Backstage backend"));
|
|
411
|
+
console.log(
|
|
412
|
+
chalk.yellow(
|
|
413
|
+
"💡 Please ensure Backstage is running and you are properly authenticated, then try again."
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
return reject(new Error("Backstage backend not available"));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(
|
|
420
|
+
chalk.blue("🌐 Redirecting to the browser for authentication...")
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Set up authentication timeout (1 minute)
|
|
424
|
+
const authTimeout = setTimeout(() => {
|
|
425
|
+
console.log(chalk.red("❌ Authorization timed out"));
|
|
426
|
+
console.log(
|
|
427
|
+
chalk.yellow(
|
|
428
|
+
"💡 Please ensure Backstage is running and you are properly authenticated, then try again."
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}, AUTH_TIMEOUT_MS);
|
|
433
|
+
|
|
434
|
+
// Step 1: Start a local HTTP server to handle the loopback redirect on port 3005
|
|
435
|
+
const loopbackServer = http.createServer(async (req, res) => {
|
|
436
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
437
|
+
if (url.pathname === "/verify-auth") {
|
|
438
|
+
if (req.method === "POST") {
|
|
439
|
+
// Handle POST request with JSON body
|
|
440
|
+
let body = "";
|
|
441
|
+
req.on("data", (chunk) => {
|
|
442
|
+
body += chunk.toString();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
req.on("end", async () => {
|
|
446
|
+
try {
|
|
447
|
+
const { jwt, entityRef } = JSON.parse(body);
|
|
448
|
+
|
|
449
|
+
if (!jwt || !entityRef) {
|
|
450
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
451
|
+
res.end(
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
error: "Authentication failed. Missing JWT or user info.",
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
loopbackServer.close();
|
|
458
|
+
clearTimeout(authTimeout);
|
|
459
|
+
}, 2000);
|
|
460
|
+
return reject(new Error("Authentication failed."));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Clear the authentication timeout since we got a successful response
|
|
464
|
+
clearTimeout(authTimeout);
|
|
465
|
+
console.log(chalk.green("✅ Authentication successful!"));
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
// Step 2: Create session with Backstage backend
|
|
469
|
+
console.log(
|
|
470
|
+
chalk.blue("🔑 Creating session with Backstage backend...")
|
|
471
|
+
);
|
|
472
|
+
const sessionData = await createSession(jwt, BACKEND_URL);
|
|
473
|
+
|
|
474
|
+
// Step 3: Save session locally
|
|
475
|
+
saveSession(
|
|
476
|
+
sessionData.token,
|
|
477
|
+
sessionData.expiresAt,
|
|
478
|
+
entityRef
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// Initialize registry store
|
|
482
|
+
await registryStore.initialize();
|
|
483
|
+
|
|
484
|
+
// Start local-env container
|
|
485
|
+
console.log(chalk.cyan("🐳 Starting container..."));
|
|
486
|
+
try {
|
|
487
|
+
await containerManager.startContainer();
|
|
488
|
+
console.log(
|
|
489
|
+
chalk.cyan("✅ Container started successfully")
|
|
490
|
+
);
|
|
491
|
+
} catch (containerError) {
|
|
492
|
+
console.warn(
|
|
493
|
+
chalk.yellow("⚠️ Failed to start container:"),
|
|
494
|
+
containerError.message
|
|
495
|
+
);
|
|
496
|
+
console.log(
|
|
497
|
+
chalk.blue("📡 Starting agent without container...")
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Generate WebSocket token NOW (before sending response)
|
|
502
|
+
// Import crypto to generate token
|
|
503
|
+
const crypto = await import("crypto");
|
|
504
|
+
const wsToken = crypto.randomBytes(32).toString("hex");
|
|
505
|
+
|
|
506
|
+
// Save token to file for later use
|
|
507
|
+
const wsTokenPath = path.join(
|
|
508
|
+
os.homedir(),
|
|
509
|
+
process.env.AGENT_ROOT_DIR || ".fenwave",
|
|
510
|
+
"ws-token"
|
|
511
|
+
);
|
|
512
|
+
try {
|
|
513
|
+
fs.writeFileSync(wsTokenPath, wsToken, { mode: 0o600 });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn("⚠️ Could not save ws-token:", error.message);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Send success response with WebSocket token
|
|
519
|
+
res.writeHead(200, {
|
|
520
|
+
"Content-Type": "application/json",
|
|
521
|
+
"Access-Control-Allow-Origin": "*",
|
|
522
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
523
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
524
|
+
});
|
|
525
|
+
res.end(
|
|
526
|
+
JSON.stringify({
|
|
527
|
+
message: "Authentication successful",
|
|
528
|
+
wsToken: wsToken,
|
|
529
|
+
wsPort: port,
|
|
530
|
+
containerPort: CONTAINER_PORT,
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Close the loopback server after a short delay to allow the success page redirect
|
|
535
|
+
setTimeout(() => {
|
|
536
|
+
loopbackServer.close();
|
|
537
|
+
}, 2000);
|
|
538
|
+
|
|
539
|
+
// Step 4: Start WebSocket server on the requested port
|
|
540
|
+
const agentId = uuidv4();
|
|
541
|
+
const wsServer = http.createServer();
|
|
542
|
+
|
|
543
|
+
wsServer.listen(port, async () => {
|
|
544
|
+
// Initialize agent start time
|
|
545
|
+
await initializeAgentStartTime();
|
|
546
|
+
|
|
547
|
+
console.log(
|
|
548
|
+
chalk.blue("📡 Agent is ready to receive connections")
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Display connection information
|
|
552
|
+
console.log(
|
|
553
|
+
chalk.green(
|
|
554
|
+
"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
console.log(
|
|
558
|
+
chalk.green("✅ Fenwave Agent Started Successfully")
|
|
559
|
+
);
|
|
560
|
+
console.log(
|
|
561
|
+
chalk.green(
|
|
562
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
console.log(chalk.white("🌐 Fenwave DevApp Dashboard:"));
|
|
566
|
+
console.log(
|
|
567
|
+
chalk.cyan(` http://localhost:${CONTAINER_PORT}\n`)
|
|
568
|
+
);
|
|
569
|
+
console.log(chalk.white("🔌 WebSocket Server:"));
|
|
570
|
+
console.log(chalk.cyan(` ws://localhost:${port}\n`));
|
|
571
|
+
console.log(chalk.white("👤 User:"));
|
|
572
|
+
console.log(chalk.cyan(` ${entityRef}\n`));
|
|
573
|
+
console.log(
|
|
574
|
+
chalk.green(
|
|
575
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const wss = setupWebSocketServer(
|
|
581
|
+
wsServer,
|
|
582
|
+
clients,
|
|
583
|
+
agentId,
|
|
584
|
+
{
|
|
585
|
+
userEntityRef: entityRef,
|
|
586
|
+
expiresAt: sessionData.expiresAt,
|
|
587
|
+
},
|
|
588
|
+
wsToken // Pass the token we generated earlier
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// Set up file watcher for session monitoring
|
|
592
|
+
const sessionWatcher = setupSessionWatcher(
|
|
593
|
+
handleSessionExpiry,
|
|
594
|
+
wsServer,
|
|
595
|
+
wss
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Start app update polling
|
|
599
|
+
startAppUpdatePolling(sessionData.token, wss);
|
|
600
|
+
resolve({
|
|
601
|
+
server: wsServer,
|
|
602
|
+
wss,
|
|
603
|
+
sessionToken: sessionData.token,
|
|
604
|
+
userEntityRef: entityRef,
|
|
605
|
+
agentId,
|
|
606
|
+
sessionWatcher,
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(
|
|
610
|
+
chalk.red("❌ Failed to setup WebSocket server:"),
|
|
611
|
+
error.message
|
|
612
|
+
);
|
|
613
|
+
reject(error);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
} catch (sessionError) {
|
|
617
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
618
|
+
res.end(
|
|
619
|
+
JSON.stringify({
|
|
620
|
+
error: "Failed to create session with Backstage backend.",
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
setTimeout(() => {
|
|
624
|
+
loopbackServer.close();
|
|
625
|
+
clearTimeout(authTimeout);
|
|
626
|
+
}, 2000);
|
|
627
|
+
reject(sessionError);
|
|
628
|
+
}
|
|
629
|
+
} catch (parseError) {
|
|
630
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
631
|
+
res.end(
|
|
632
|
+
JSON.stringify({ error: "Invalid JSON in request body." })
|
|
633
|
+
);
|
|
634
|
+
setTimeout(() => {
|
|
635
|
+
loopbackServer.close();
|
|
636
|
+
clearTimeout(authTimeout);
|
|
637
|
+
}, 2000);
|
|
638
|
+
reject(parseError);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
} else if (req.method === "OPTIONS") {
|
|
642
|
+
// Handle CORS preflight request
|
|
643
|
+
res.writeHead(200, {
|
|
644
|
+
"Access-Control-Allow-Origin": "*",
|
|
645
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
646
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
647
|
+
});
|
|
648
|
+
res.end();
|
|
649
|
+
} else {
|
|
650
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
651
|
+
res.end(JSON.stringify({ error: "Method not allowed. Use POST." }));
|
|
652
|
+
}
|
|
653
|
+
} else if (url.pathname === "/auth-success") {
|
|
654
|
+
// Serve the authentication success page
|
|
655
|
+
const filePath = path.join(process.cwd(), "index.html");
|
|
656
|
+
fs.readFile(filePath, (err, data) => {
|
|
657
|
+
if (err) {
|
|
658
|
+
console.error("Failed to read HTML file:", err);
|
|
659
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
660
|
+
res.end("Internal Server Error");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
664
|
+
res.end(data);
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
668
|
+
res.end("Not Found");
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Use port 3005 for loopback to avoid conflict with WebSocket server
|
|
673
|
+
loopbackServer.listen(3005, async () => {
|
|
674
|
+
const open = (await import("open")).default;
|
|
675
|
+
// Open the Backstage frontend in the default browser
|
|
676
|
+
const authUrl = `${FRONTEND_URL}/agent-cli`;
|
|
677
|
+
open(authUrl);
|
|
678
|
+
});
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.error(chalk.red("Failed to start server:"), error);
|
|
681
|
+
reject(error);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Initialize the CLI
|
|
687
|
+
program
|
|
688
|
+
.name("fenwave")
|
|
689
|
+
.description("Fenwave - Developer Platform CLI")
|
|
690
|
+
.version(version);
|
|
691
|
+
|
|
692
|
+
// Setup CLI commands
|
|
693
|
+
setupCLICommands(program, startServer);
|
|
694
|
+
|
|
695
|
+
// Custom unknown command handler
|
|
696
|
+
program.on("command:*", ([cmd]) => {
|
|
697
|
+
console.error(`error: unknown command '${cmd}'`);
|
|
698
|
+
|
|
699
|
+
const available = program.commands.map((c) => c.name());
|
|
700
|
+
|
|
701
|
+
// First, try prefix matches
|
|
702
|
+
let matches = available.filter((c) => c.startsWith(cmd));
|
|
703
|
+
|
|
704
|
+
// If no prefix matches, try bidirectional substring matches
|
|
705
|
+
if (matches.length === 0) {
|
|
706
|
+
matches = available.filter((c) => cmd.includes(c) || c.includes(cmd));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (matches.length > 0) {
|
|
710
|
+
if (matches.length === 1) {
|
|
711
|
+
console.error(`(Did you mean '${matches[0]}'?)`);
|
|
712
|
+
} else {
|
|
713
|
+
console.error(`(Did you mean one of ${matches.join(", ")}?)`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
process.exit(1);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Pre-process arguments to handle incomplete options before Commander.js
|
|
721
|
+
function preprocessArguments() {
|
|
722
|
+
const args = process.argv.slice(2);
|
|
723
|
+
|
|
724
|
+
if (args.length >= 2) {
|
|
725
|
+
const commandName = args[0];
|
|
726
|
+
const lastArg = args[args.length - 1];
|
|
727
|
+
|
|
728
|
+
// Check if user typed incomplete option (just - or --)
|
|
729
|
+
if (lastArg === "-" || lastArg === "--") {
|
|
730
|
+
// Find the command
|
|
731
|
+
const command = program.commands.find(
|
|
732
|
+
(cmd) =>
|
|
733
|
+
cmd.name() === commandName || cmd.aliases().includes(commandName)
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
if (command) {
|
|
737
|
+
console.error(`error: unknown option '${lastArg}'`);
|
|
738
|
+
|
|
739
|
+
// Get all options for this command
|
|
740
|
+
const options = command.options || [];
|
|
741
|
+
const availableOptions = [];
|
|
742
|
+
|
|
743
|
+
// Add command-specific options
|
|
744
|
+
options.forEach((option) => {
|
|
745
|
+
if (option.short) availableOptions.push(option.short);
|
|
746
|
+
if (option.long) availableOptions.push(option.long);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Always add help option
|
|
750
|
+
if (!availableOptions.includes("-h")) availableOptions.push("-h");
|
|
751
|
+
if (!availableOptions.includes("--help"))
|
|
752
|
+
availableOptions.push("--help");
|
|
753
|
+
|
|
754
|
+
let suggestions = [];
|
|
755
|
+
|
|
756
|
+
if (lastArg === "--") {
|
|
757
|
+
// User typed just '--', suggest all long options
|
|
758
|
+
suggestions = availableOptions.filter((opt) => opt.startsWith("--"));
|
|
759
|
+
} else if (lastArg === "-") {
|
|
760
|
+
// User typed just '-', suggest all short options with their long equivalents
|
|
761
|
+
const shortOpts = availableOptions.filter(
|
|
762
|
+
(opt) => opt.startsWith("-") && !opt.startsWith("--")
|
|
763
|
+
);
|
|
764
|
+
const longOpts = availableOptions.filter((opt) =>
|
|
765
|
+
opt.startsWith("--")
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Pair short and long options
|
|
769
|
+
suggestions = shortOpts.map((shortOpt) => {
|
|
770
|
+
const longEquivalent = longOpts.find(
|
|
771
|
+
(longOpt) =>
|
|
772
|
+
longOpt.substring(2) === shortOpt.substring(1) ||
|
|
773
|
+
(shortOpt === "-h" && longOpt === "--help") ||
|
|
774
|
+
(shortOpt === "-a" && longOpt === "--all") ||
|
|
775
|
+
(shortOpt === "-f" &&
|
|
776
|
+
(longOpt === "--force" || longOpt === "--follow")) ||
|
|
777
|
+
(shortOpt === "-d" && longOpt === "--driver") ||
|
|
778
|
+
(shortOpt === "-p" && longOpt === "--port") ||
|
|
779
|
+
(shortOpt === "-t" && longOpt === "--tail")
|
|
780
|
+
);
|
|
781
|
+
return longEquivalent ? `${shortOpt}, ${longEquivalent}` : shortOpt;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (suggestions.length > 0) {
|
|
786
|
+
const formattedSuggestions = suggestions.map((suggestion) => {
|
|
787
|
+
// Add argument placeholder for options that require arguments
|
|
788
|
+
let displaySuggestion = suggestion;
|
|
789
|
+
|
|
790
|
+
// Find the matching option object to check if it requires an argument
|
|
791
|
+
const option = options.find(
|
|
792
|
+
(opt) =>
|
|
793
|
+
opt.long === suggestion ||
|
|
794
|
+
opt.short === suggestion ||
|
|
795
|
+
(suggestion.includes(opt.long) && opt.long) ||
|
|
796
|
+
(suggestion.includes(opt.short) && opt.short)
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
if (option && option.required) {
|
|
800
|
+
// Use a more descriptive argument name based on the option's own description
|
|
801
|
+
let argName = "<value>";
|
|
802
|
+
if (option.argChoices) {
|
|
803
|
+
argName = `<${option.argChoices.join("|")}>`;
|
|
804
|
+
} else if (option.long) {
|
|
805
|
+
// Map common option names to better argument descriptions
|
|
806
|
+
const optName = option.long.replace("--", "");
|
|
807
|
+
const argNameMap = {
|
|
808
|
+
tail: "lines",
|
|
809
|
+
port: "port",
|
|
810
|
+
driver: "driver",
|
|
811
|
+
"backend-url": "url",
|
|
812
|
+
"frontend-url": "url",
|
|
813
|
+
token: "token",
|
|
814
|
+
"aws-region": "region",
|
|
815
|
+
"aws-account-id": "id",
|
|
816
|
+
};
|
|
817
|
+
argName = `<${argNameMap[optName] || optName}>`;
|
|
818
|
+
}
|
|
819
|
+
displaySuggestion = `${suggestion} ${argName}`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return `'${displaySuggestion}'`;
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
console.error(`(Did you mean ${formattedSuggestions.join(", ")}?)`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Run preprocessing before parsing
|
|
835
|
+
preprocessArguments();
|
|
836
|
+
|
|
837
|
+
// Handle unknown options
|
|
838
|
+
program.exitOverride((err) => {
|
|
839
|
+
if (err.code === "commander.unknownOption") {
|
|
840
|
+
const args = process.argv.slice(2);
|
|
841
|
+
const commandName = args[0];
|
|
842
|
+
const invalidOption = err.message.match(/unknown option '([^']+)'/)?.[1];
|
|
843
|
+
|
|
844
|
+
if (invalidOption) {
|
|
845
|
+
// Find the command
|
|
846
|
+
const command = program.commands.find(
|
|
847
|
+
(cmd) =>
|
|
848
|
+
cmd.name() === commandName || cmd.aliases().includes(commandName)
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
if (command) {
|
|
852
|
+
console.error(`error: unknown option '${invalidOption}'`);
|
|
853
|
+
|
|
854
|
+
// Get all options for this command
|
|
855
|
+
const options = command.options || [];
|
|
856
|
+
const availableOptions = [];
|
|
857
|
+
|
|
858
|
+
// Add command-specific options
|
|
859
|
+
options.forEach((option) => {
|
|
860
|
+
if (option.short) availableOptions.push(option.short);
|
|
861
|
+
if (option.long) availableOptions.push(option.long);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Always add help option
|
|
865
|
+
if (!availableOptions.includes("-h")) availableOptions.push("-h");
|
|
866
|
+
if (!availableOptions.includes("--help"))
|
|
867
|
+
availableOptions.push("--help");
|
|
868
|
+
|
|
869
|
+
// Find suggestions for partial/incorrect options
|
|
870
|
+
const suggestions = availableOptions.filter(
|
|
871
|
+
(opt) =>
|
|
872
|
+
opt.startsWith(invalidOption) ||
|
|
873
|
+
opt.includes(invalidOption.replace(/^-+/, "")) ||
|
|
874
|
+
invalidOption.includes(opt.replace(/^-+/, ""))
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
if (suggestions.length > 0) {
|
|
878
|
+
// Group short and long options together
|
|
879
|
+
const pairedSuggestions = [];
|
|
880
|
+
const processed = new Set();
|
|
881
|
+
|
|
882
|
+
suggestions.forEach((suggestion) => {
|
|
883
|
+
if (processed.has(suggestion)) return;
|
|
884
|
+
|
|
885
|
+
const option = options.find(
|
|
886
|
+
(opt) => opt.long === suggestion || opt.short === suggestion
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
if (option) {
|
|
890
|
+
let displaySuggestion = "";
|
|
891
|
+
|
|
892
|
+
// Check if both short and long forms are in suggestions
|
|
893
|
+
const hasShort =
|
|
894
|
+
option.short && suggestions.includes(option.short);
|
|
895
|
+
const hasLong = option.long && suggestions.includes(option.long);
|
|
896
|
+
|
|
897
|
+
if (hasShort && hasLong) {
|
|
898
|
+
displaySuggestion = `${option.short}, ${option.long}`;
|
|
899
|
+
processed.add(option.short);
|
|
900
|
+
processed.add(option.long);
|
|
901
|
+
} else if (hasShort) {
|
|
902
|
+
// If only short form matched, still pair it with long form if available
|
|
903
|
+
displaySuggestion = option.long
|
|
904
|
+
? `${option.short}, ${option.long}`
|
|
905
|
+
: option.short;
|
|
906
|
+
processed.add(option.short);
|
|
907
|
+
if (option.long) processed.add(option.long);
|
|
908
|
+
} else if (hasLong) {
|
|
909
|
+
// If only long form matched, still pair it with short form if available
|
|
910
|
+
displaySuggestion = option.short
|
|
911
|
+
? `${option.short}, ${option.long}`
|
|
912
|
+
: option.long;
|
|
913
|
+
if (option.short) processed.add(option.short);
|
|
914
|
+
processed.add(option.long);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Add argument placeholder for options that require arguments
|
|
918
|
+
if (option.required) {
|
|
919
|
+
let argName = "<value>";
|
|
920
|
+
if (option.argChoices) {
|
|
921
|
+
argName = `<${option.argChoices.join("|")}>`;
|
|
922
|
+
} else if (option.long) {
|
|
923
|
+
const optName = option.long.replace("--", "");
|
|
924
|
+
const argNameMap = {
|
|
925
|
+
tail: "lines",
|
|
926
|
+
port: "port",
|
|
927
|
+
driver: "driver",
|
|
928
|
+
"backend-url": "url",
|
|
929
|
+
"frontend-url": "url",
|
|
930
|
+
token: "token",
|
|
931
|
+
"aws-region": "region",
|
|
932
|
+
"aws-account-id": "id",
|
|
933
|
+
};
|
|
934
|
+
argName = `<${argNameMap[optName] || optName}>`;
|
|
935
|
+
}
|
|
936
|
+
displaySuggestion = `${displaySuggestion} ${argName}`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
pairedSuggestions.push(`'${displaySuggestion}'`);
|
|
940
|
+
} else {
|
|
941
|
+
// Fallback for options without Option object (like help)
|
|
942
|
+
processed.add(suggestion);
|
|
943
|
+
|
|
944
|
+
// Pair -h with --help
|
|
945
|
+
if (suggestion === "-h" && suggestions.includes("--help")) {
|
|
946
|
+
pairedSuggestions.push(`'${suggestion}, --help'`);
|
|
947
|
+
processed.add("--help");
|
|
948
|
+
} else if (
|
|
949
|
+
suggestion === "--help" &&
|
|
950
|
+
suggestions.includes("-h")
|
|
951
|
+
) {
|
|
952
|
+
// Skip, already handled
|
|
953
|
+
} else {
|
|
954
|
+
pairedSuggestions.push(`'${suggestion}'`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
console.error(`(Did you mean ${pairedSuggestions.join(", ")}?)`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// For other errors (like help), exit normally with the error's exit code
|
|
968
|
+
process.exit(err.exitCode || 1);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Parse command line arguments
|
|
972
|
+
program.parse(process.argv);
|
|
973
|
+
|
|
974
|
+
// If no arguments, show help
|
|
975
|
+
if (process.argv.length === 2) {
|
|
976
|
+
program.help();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Process error handling
|
|
980
|
+
process.on("uncaughtException", (error) => {
|
|
981
|
+
console.error("Uncaught exception:", error);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
985
|
+
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
export { startServer, clients };
|