@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/cli-commands.js
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import net from 'net';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import containerManager from './containerManager.js';
|
|
12
|
+
import registryStore from './store/registryStore.js';
|
|
13
|
+
import agentStore from './store/agentStore.js';
|
|
14
|
+
import { docker, formatContainer } from './docker-actions/containers.js';
|
|
15
|
+
import { loadSession, isSessionValid, clearSession } from './auth.js';
|
|
16
|
+
import packageJson from './package.json' with { type: 'json' };
|
|
17
|
+
import {
|
|
18
|
+
formatSize,
|
|
19
|
+
formatCreatedTime,
|
|
20
|
+
formatUptime,
|
|
21
|
+
} from './helper-functions.js';
|
|
22
|
+
import { ensureEnvironmentFiles } from './utils/envSetup.js';
|
|
23
|
+
import dotenv from 'dotenv';
|
|
24
|
+
dotenv.config();
|
|
25
|
+
|
|
26
|
+
const WS_PORT = Number(process.env.WS_PORT) || 3001;
|
|
27
|
+
|
|
28
|
+
// ES module helpers
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = dirname(__filename);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if agent is actually running by trying to connect to its port
|
|
34
|
+
*/
|
|
35
|
+
function checkAgentRunning(port = WS_PORT) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const socket = new net.Socket();
|
|
38
|
+
|
|
39
|
+
socket.setTimeout(1000);
|
|
40
|
+
|
|
41
|
+
socket.on('connect', () => {
|
|
42
|
+
socket.destroy();
|
|
43
|
+
resolve(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
socket.on('timeout', () => {
|
|
47
|
+
socket.destroy();
|
|
48
|
+
resolve(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
socket.on('error', () => {
|
|
52
|
+
resolve(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
socket.connect(port, 'localhost');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setupCLICommands(program, startServerFunction) {
|
|
60
|
+
// Login (create session)
|
|
61
|
+
program
|
|
62
|
+
.command('login')
|
|
63
|
+
.description('authenticate Fenwave agent to backstage')
|
|
64
|
+
.option('-p, --port <port>', 'Port to listen on', String(WS_PORT))
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
console.log(chalk.blue('🚀 Initializing Fenwave Agent...'));
|
|
67
|
+
|
|
68
|
+
// Show environment file status during login
|
|
69
|
+
ensureEnvironmentFiles(__dirname, true);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await startServerFunction(parseInt(options.port));
|
|
73
|
+
|
|
74
|
+
// Store server instances for graceful shutdown in global scope
|
|
75
|
+
global.serverInstances = result;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(chalk.red('❌ Failed to start Fenwave Agent:'), error.message);
|
|
78
|
+
if (
|
|
79
|
+
error.message.includes('ECONNREFUSED') ||
|
|
80
|
+
error.message.includes('fetch') ||
|
|
81
|
+
error.message.includes('ENOTFOUND')
|
|
82
|
+
) {
|
|
83
|
+
// Check if we have a stored session
|
|
84
|
+
const existingSession = loadSession();
|
|
85
|
+
if (existingSession && isSessionValid(existingSession)) {
|
|
86
|
+
// We have a valid session but Backstage is offline - this should be fine for independent operation
|
|
87
|
+
console.error(
|
|
88
|
+
chalk.yellow('⚠️ Warning: Cannot connect to Backstage backend')
|
|
89
|
+
);
|
|
90
|
+
// Don't exit, let it continue with the cached session
|
|
91
|
+
} else {
|
|
92
|
+
// No valid session and Backstage offline - authentication required but impossible
|
|
93
|
+
console.error(chalk.red('❌ Cannot connect to Backstage backend'));
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.yellow(
|
|
96
|
+
'💡 Please ensure Backstage is running and you are properly authenticated, then try again.'
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Logout (clear session)
|
|
108
|
+
program
|
|
109
|
+
.command('logout')
|
|
110
|
+
.description('clear stored session and logout')
|
|
111
|
+
.action(() => {
|
|
112
|
+
const session = loadSession();
|
|
113
|
+
if (session) {
|
|
114
|
+
clearSession();
|
|
115
|
+
console.log(chalk.green('✅ Agent disconnected successfully'));
|
|
116
|
+
process.exit(0);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(chalk.yellow('ℹ️ No active session found'));
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Check Auth session and device registration status
|
|
124
|
+
program
|
|
125
|
+
.command('status')
|
|
126
|
+
.description('check current session status and device registration')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
try {
|
|
129
|
+
const { loadDeviceCredential, isDeviceRegistered } = await import('./store/deviceCredentialStore.js');
|
|
130
|
+
const { hasNpmToken } = await import('./store/npmTokenStore.js');
|
|
131
|
+
|
|
132
|
+
console.log(chalk.bold('\n📊 Fenwave Agent Status\n'));
|
|
133
|
+
|
|
134
|
+
// Session status
|
|
135
|
+
const session = loadSession();
|
|
136
|
+
if (session && isSessionValid(session)) {
|
|
137
|
+
console.log(chalk.green('✅ Session: Active'));
|
|
138
|
+
console.log(chalk.gray(` User: ${session.userEntityRef || 'N/A'}`));
|
|
139
|
+
console.log(chalk.gray(` Expires: ${new Date(session.expiresAt).toLocaleString()}`));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.yellow('⚠️ Session: Inactive'));
|
|
142
|
+
console.log(chalk.gray(' Run "fenwave login" to authenticate'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
// Device registration status
|
|
148
|
+
if (isDeviceRegistered()) {
|
|
149
|
+
const deviceCred = loadDeviceCredential();
|
|
150
|
+
console.log(chalk.green('✅ Device: Registered'));
|
|
151
|
+
console.log(chalk.gray(` Device ID: ${deviceCred.deviceId}`));
|
|
152
|
+
console.log(chalk.gray(` Device Name: ${deviceCred.deviceName}`));
|
|
153
|
+
console.log(chalk.gray(` Platform: ${deviceCred.platform}`));
|
|
154
|
+
|
|
155
|
+
let deviceUser;
|
|
156
|
+
if (deviceCred.userEntityRef && deviceCred.userEntityRef !== 'unknown') {
|
|
157
|
+
deviceUser = deviceCred.userEntityRef;
|
|
158
|
+
} else {
|
|
159
|
+
deviceUser = session?.userEntityRef || 'N/A';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.gray(` User: ${deviceUser}`));
|
|
163
|
+
console.log(chalk.gray(` Registered: ${new Date(deviceCred.registeredAt).toLocaleString()}`));
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.yellow('⚠️ Device: Not Registered'));
|
|
166
|
+
console.log(chalk.gray(' Run "fenwave init" or "fenwave register" to register your device'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
// NPM token status
|
|
172
|
+
if (hasNpmToken()) {
|
|
173
|
+
console.log(chalk.green('✅ NPM Token: Configured'));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.yellow('⚠️ NPM Token: Not Configured'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('');
|
|
179
|
+
|
|
180
|
+
// Agent running status
|
|
181
|
+
const isRunning = await checkAgentRunning(WS_PORT);
|
|
182
|
+
if (isRunning) {
|
|
183
|
+
console.log(chalk.green(`✅ Agent: Running (port ${WS_PORT})`));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(chalk.yellow('⚠️ Agent: Not Running'));
|
|
186
|
+
console.log(chalk.gray(' Run "fenwave login" to start the agent'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('');
|
|
190
|
+
process.exit(0);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(chalk.red('❌ Error checking status:'), error.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Interactive Setup Wizard
|
|
198
|
+
program
|
|
199
|
+
.command('init')
|
|
200
|
+
.description('interactive setup wizard for Fenwave agent')
|
|
201
|
+
.option('-t, --token <token>', 'Registration token from Backstage')
|
|
202
|
+
.option('--skip-prerequisites', 'Skip prerequisites check')
|
|
203
|
+
.option('--backend-url <url>', 'Backstage backend URL', 'http://localhost:7007')
|
|
204
|
+
.option('--frontend-url <url>', 'Backstage frontend URL', 'http://localhost:3000')
|
|
205
|
+
.option('--aws-region <region>', 'AWS region for ECR', 'eu-west-1')
|
|
206
|
+
.option('--aws-account-id <id>', 'AWS account ID for ECR')
|
|
207
|
+
.action(async (options) => {
|
|
208
|
+
try {
|
|
209
|
+
const { runSetupWizard } = await import('./setup/setupWizard.js');
|
|
210
|
+
await runSetupWizard({
|
|
211
|
+
token: options.token,
|
|
212
|
+
skipPrerequisites: options.skipPrerequisites,
|
|
213
|
+
backendUrl: options.backendUrl,
|
|
214
|
+
frontendUrl: options.frontendUrl,
|
|
215
|
+
awsRegion: options.awsRegion,
|
|
216
|
+
awsAccountId: options.awsAccountId,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(chalk.red('Setup failed:'), error.message);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Register Device
|
|
225
|
+
program
|
|
226
|
+
.command('register')
|
|
227
|
+
.description('register device with Backstage')
|
|
228
|
+
.option('-t, --token <token>', 'Registration token from Backstage')
|
|
229
|
+
.option('--backend-url <url>', 'Backstage backend URL', 'http://localhost:7007')
|
|
230
|
+
.action(async (options) => {
|
|
231
|
+
const spinner = ora('Registering device...').start();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const { getDeviceMetadata } = await import('./utils/deviceInfo.js');
|
|
235
|
+
const { saveDeviceCredential, isDeviceRegistered } = await import('./store/deviceCredentialStore.js');
|
|
236
|
+
const { saveNpmToken } = await import('./store/npmTokenStore.js');
|
|
237
|
+
|
|
238
|
+
// Check if already registered
|
|
239
|
+
if (isDeviceRegistered()) {
|
|
240
|
+
spinner.warn('Device is already registered');
|
|
241
|
+
const inquirer = (await import('inquirer')).default;
|
|
242
|
+
const { confirmed } = await inquirer.prompt([
|
|
243
|
+
{
|
|
244
|
+
type: 'confirm',
|
|
245
|
+
name: 'confirmed',
|
|
246
|
+
message: 'Re-register (this will replace existing credentials)?',
|
|
247
|
+
default: false,
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
if (!confirmed) {
|
|
252
|
+
console.log(chalk.yellow('Registration cancelled'));
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Get registration token
|
|
258
|
+
let token = options.token;
|
|
259
|
+
if (!token) {
|
|
260
|
+
const inquirer = (await import('inquirer')).default;
|
|
261
|
+
const answers = await inquirer.prompt([
|
|
262
|
+
{
|
|
263
|
+
type: 'password',
|
|
264
|
+
name: 'token',
|
|
265
|
+
message: 'Enter registration token:',
|
|
266
|
+
mask: '*',
|
|
267
|
+
validate: (input) => input && input.length >= 32 ? true : 'Invalid token format',
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
token = answers.token;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Collect device info
|
|
274
|
+
const deviceMetadata = await getDeviceMetadata();
|
|
275
|
+
|
|
276
|
+
// Register with backend
|
|
277
|
+
const response = await axios.post(
|
|
278
|
+
`${options.backendUrl}/api/agent-cli/register`,
|
|
279
|
+
{
|
|
280
|
+
installToken: token,
|
|
281
|
+
deviceInfo: {
|
|
282
|
+
deviceName: deviceMetadata.deviceName,
|
|
283
|
+
platform: deviceMetadata.platform,
|
|
284
|
+
osVersion: deviceMetadata.osVersion,
|
|
285
|
+
agentVersion: deviceMetadata.agentVersion,
|
|
286
|
+
metadata: deviceMetadata.metadata,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{ timeout: 10000 }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Save credentials
|
|
293
|
+
saveDeviceCredential({
|
|
294
|
+
deviceId: response.data.deviceId,
|
|
295
|
+
deviceCredential: response.data.deviceCredential,
|
|
296
|
+
userEntityRef: response.data.userEntityRef || 'unknown',
|
|
297
|
+
deviceName: deviceMetadata.deviceName,
|
|
298
|
+
platform: deviceMetadata.platform,
|
|
299
|
+
agentVersion: deviceMetadata.agentVersion,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Save NPM token if provided
|
|
303
|
+
if (response.data.npmToken) {
|
|
304
|
+
saveNpmToken(response.data.npmToken);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
spinner.succeed('Device registered successfully');
|
|
308
|
+
console.log(chalk.green('\n✅ Registration Complete'));
|
|
309
|
+
console.log(chalk.gray(` Device ID: ${response.data.deviceId}`));
|
|
310
|
+
console.log(chalk.gray(` Device Name: ${deviceMetadata.deviceName}\n`));
|
|
311
|
+
|
|
312
|
+
} catch (error) {
|
|
313
|
+
spinner.fail('Registration failed');
|
|
314
|
+
if (error.response?.status === 401) {
|
|
315
|
+
console.error(chalk.red('❌ Invalid or expired registration token'));
|
|
316
|
+
console.log(chalk.yellow('💡 Get a new token from Backstage at /agent-installer'));
|
|
317
|
+
} else if (error.response?.status === 429) {
|
|
318
|
+
console.error(chalk.red('❌ Rate limit exceeded'));
|
|
319
|
+
console.log(chalk.yellow('💡 Too many attempts. Please wait and try again.'));
|
|
320
|
+
} else {
|
|
321
|
+
console.error(chalk.red('❌ Error:'), error.message);
|
|
322
|
+
}
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Rotate Device Credentials
|
|
328
|
+
program
|
|
329
|
+
.command('rotate-credentials')
|
|
330
|
+
.description('rotate device credentials')
|
|
331
|
+
.option('--backend-url <url>', 'Backstage backend URL', 'http://localhost:7007')
|
|
332
|
+
.action(async (options) => {
|
|
333
|
+
const spinner = ora('Rotating credentials...').start();
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const { loadDeviceCredential, saveDeviceCredential, isDeviceRegistered } = await import('./store/deviceCredentialStore.js');
|
|
337
|
+
|
|
338
|
+
if (!isDeviceRegistered()) {
|
|
339
|
+
spinner.fail('Device is not registered');
|
|
340
|
+
console.log(chalk.yellow('💡 Run "fenwave register" first'));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const deviceCred = loadDeviceCredential();
|
|
345
|
+
|
|
346
|
+
// Rotate credentials with backend
|
|
347
|
+
const response = await axios.post(
|
|
348
|
+
`${options.backendUrl}/api/agent-cli/rotate-credentials`,
|
|
349
|
+
{
|
|
350
|
+
deviceId: deviceCred.deviceId,
|
|
351
|
+
deviceCredential: deviceCred.deviceCredential,
|
|
352
|
+
},
|
|
353
|
+
{ timeout: 10000 }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Update stored credentials
|
|
357
|
+
saveDeviceCredential({
|
|
358
|
+
...deviceCred,
|
|
359
|
+
deviceCredential: response.data.deviceCredential,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
spinner.succeed('Credentials rotated successfully');
|
|
363
|
+
console.log(chalk.green('✅ New credentials saved securely\n'));
|
|
364
|
+
|
|
365
|
+
} catch (error) {
|
|
366
|
+
spinner.fail('Credential rotation failed');
|
|
367
|
+
if (error.response?.status === 401) {
|
|
368
|
+
console.error(chalk.red('❌ Invalid current credentials'));
|
|
369
|
+
console.log(chalk.yellow('💡 Your device may have been revoked. Run "fenwave register" to re-register.'));
|
|
370
|
+
} else if (error.response?.status === 429) {
|
|
371
|
+
console.error(chalk.red('❌ Rate limit exceeded'));
|
|
372
|
+
console.log(chalk.yellow('💡 Too many rotation attempts. Please wait and try again.'));
|
|
373
|
+
} else {
|
|
374
|
+
console.error(chalk.red('❌ Error:'), error.message);
|
|
375
|
+
}
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Uninstall Agent
|
|
381
|
+
program
|
|
382
|
+
.command('uninstall')
|
|
383
|
+
.description('uninstall Fenwave agent and clean up')
|
|
384
|
+
.option('--keep-data', 'Keep configuration and data files')
|
|
385
|
+
.action(async (options) => {
|
|
386
|
+
try {
|
|
387
|
+
const inquirer = (await import('inquirer')).default;
|
|
388
|
+
|
|
389
|
+
// Confirmation
|
|
390
|
+
const { confirmed } = await inquirer.prompt([
|
|
391
|
+
{
|
|
392
|
+
type: 'confirm',
|
|
393
|
+
name: 'confirmed',
|
|
394
|
+
message: 'Are you sure you want to uninstall the Fenwave agent?',
|
|
395
|
+
default: false,
|
|
396
|
+
},
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
if (!confirmed) {
|
|
400
|
+
console.log(chalk.yellow('Uninstall cancelled'));
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const spinner = ora('Uninstalling Fenwave agent...').start();
|
|
405
|
+
|
|
406
|
+
// Stop agent if running
|
|
407
|
+
try {
|
|
408
|
+
const isRunning = await checkAgentRunning(WS_PORT);
|
|
409
|
+
if (isRunning) {
|
|
410
|
+
spinner.text = 'Stopping agent...';
|
|
411
|
+
// Agent will be stopped when process exits
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
// Ignore
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Clear credentials unless --keep-data
|
|
418
|
+
if (!options.keepData) {
|
|
419
|
+
spinner.text = 'Clearing credentials...';
|
|
420
|
+
const { clearDeviceCredential } = await import('./store/deviceCredentialStore.js');
|
|
421
|
+
const { clearNpmToken } = await import('./store/npmTokenStore.js');
|
|
422
|
+
const { clearSetupState } = await import('./store/setupState.js');
|
|
423
|
+
|
|
424
|
+
clearDeviceCredential();
|
|
425
|
+
clearNpmToken();
|
|
426
|
+
clearSetupState();
|
|
427
|
+
clearSession();
|
|
428
|
+
const fwDir = path.join(os.homedir(), '.fenwave');
|
|
429
|
+
|
|
430
|
+
if (fs.existsSync(fwDir)) {
|
|
431
|
+
fs.rmSync(fwDir, { recursive: true, force: true });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
spinner.succeed('Agent uninstalled');
|
|
436
|
+
console.log(chalk.green('\n✅ Fenwave agent uninstalled successfully\n'));
|
|
437
|
+
|
|
438
|
+
if (!options.keepData) {
|
|
439
|
+
console.log(chalk.gray('All configuration and data files have been removed.'));
|
|
440
|
+
} else {
|
|
441
|
+
console.log(chalk.gray('Configuration and data files have been preserved.'));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
console.log(chalk.gray('\nTo reinstall: npm install -g @fenwave/dev-agent\n'));
|
|
445
|
+
|
|
446
|
+
process.exit(0);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error(chalk.red('❌ Uninstall error:'), error.message);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Container commands
|
|
454
|
+
program
|
|
455
|
+
.command('containers')
|
|
456
|
+
.alias('ps')
|
|
457
|
+
.description('list containers')
|
|
458
|
+
.option('-a, --all', 'Show all containers (default shows just running)')
|
|
459
|
+
.action(async (options) => {
|
|
460
|
+
const spinner = ora('Fetching containers...').start();
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const containers = await docker.listContainers({ all: options.all });
|
|
464
|
+
|
|
465
|
+
if (containers.length === 0) {
|
|
466
|
+
spinner.succeed('No containers found');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const containerPromises = containers.map((container) =>
|
|
471
|
+
formatContainer(docker.getContainer(container.Id))
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const formattedContainers = await Promise.all(containerPromises);
|
|
475
|
+
|
|
476
|
+
const containerText =
|
|
477
|
+
containers.length === 1 ? 'container' : 'containers';
|
|
478
|
+
spinner.succeed(`Found ${containers.length} ${containerText}`);
|
|
479
|
+
|
|
480
|
+
// Create a table for display
|
|
481
|
+
const table = new Table({
|
|
482
|
+
head: [
|
|
483
|
+
chalk.blue('ID'),
|
|
484
|
+
chalk.blue('Name'),
|
|
485
|
+
chalk.blue('Image'),
|
|
486
|
+
chalk.blue('Status'),
|
|
487
|
+
chalk.blue('Ports'),
|
|
488
|
+
chalk.blue('CPU %'),
|
|
489
|
+
chalk.blue('MEM %'),
|
|
490
|
+
],
|
|
491
|
+
colWidths: [15, 25, 30, 10, 20, 10, 10],
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
formattedContainers.forEach((container) => {
|
|
495
|
+
const status =
|
|
496
|
+
container.status === 'running'
|
|
497
|
+
? chalk.green(container.status)
|
|
498
|
+
: chalk.red(container.status);
|
|
499
|
+
|
|
500
|
+
table.push([
|
|
501
|
+
container.id.substring(0, 12),
|
|
502
|
+
container.name,
|
|
503
|
+
container.image,
|
|
504
|
+
status,
|
|
505
|
+
container.ports.join(', '),
|
|
506
|
+
`${container.cpu}%`,
|
|
507
|
+
`${container.memory}%`,
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
console.log(table.toString());
|
|
512
|
+
process.exit(0);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
spinner.fail(`Failed to fetch containers: ${error.message}`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Start container(s) - supports multiple containers
|
|
520
|
+
program
|
|
521
|
+
.command('start <containerId(s)...>')
|
|
522
|
+
.description('start one or more containers')
|
|
523
|
+
.action(async (containerIds) => {
|
|
524
|
+
const spinner = ora(`Starting ${containerIds.length} ${containerIds.length === 1 ? 'container' : 'containers'}...`).start();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const results = [];
|
|
528
|
+
for (const containerId of containerIds) {
|
|
529
|
+
try {
|
|
530
|
+
const container = docker.getContainer(containerId);
|
|
531
|
+
await container.start();
|
|
532
|
+
results.push({ id: containerId, success: true });
|
|
533
|
+
} catch (error) {
|
|
534
|
+
results.push({ id: containerId, success: false, error: error.message });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const succeeded = results.filter(r => r.success);
|
|
539
|
+
const failed = results.filter(r => !r.success);
|
|
540
|
+
|
|
541
|
+
if (failed.length === 0) {
|
|
542
|
+
spinner.succeed(`Successfully started ${succeeded.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
543
|
+
} else if (succeeded.length === 0) {
|
|
544
|
+
spinner.fail(`Failed to start all containers`);
|
|
545
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
546
|
+
} else {
|
|
547
|
+
spinner.warn(`Started ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
548
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
549
|
+
}
|
|
550
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
spinner.fail(`Failed to start containers: ${error.message}`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Stop container(s) - supports multiple containers
|
|
558
|
+
program
|
|
559
|
+
.command('stop <containerId(s)...>')
|
|
560
|
+
.description('stop one or more containers')
|
|
561
|
+
.action(async (containerIds) => {
|
|
562
|
+
const spinner = ora(`Stopping ${containerIds.length} ${containerIds.length === 1 ? 'container' : 'containers'}...`).start();
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const results = [];
|
|
566
|
+
for (const containerId of containerIds) {
|
|
567
|
+
try {
|
|
568
|
+
const container = docker.getContainer(containerId);
|
|
569
|
+
await container.stop();
|
|
570
|
+
results.push({ id: containerId, success: true });
|
|
571
|
+
} catch (error) {
|
|
572
|
+
results.push({ id: containerId, success: false, error: error.message });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const succeeded = results.filter(r => r.success);
|
|
577
|
+
const failed = results.filter(r => !r.success);
|
|
578
|
+
|
|
579
|
+
if (failed.length === 0) {
|
|
580
|
+
spinner.succeed(`Successfully stopped ${succeeded.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
581
|
+
} else if (succeeded.length === 0) {
|
|
582
|
+
spinner.fail(`Failed to stop all containers`);
|
|
583
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
584
|
+
} else {
|
|
585
|
+
spinner.warn(`Stopped ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
586
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
587
|
+
}
|
|
588
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
spinner.fail(`Failed to stop containers: ${error.message}`);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Restart container(s) - supports multiple containers
|
|
596
|
+
program
|
|
597
|
+
.command('restart <containerId(s)...>')
|
|
598
|
+
.description('restart one or more containers')
|
|
599
|
+
.action(async (containerIds) => {
|
|
600
|
+
const spinner = ora(`Restarting ${containerIds.length} ${containerIds.length === 1 ? 'container' : 'containers'}...`).start();
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const results = [];
|
|
604
|
+
for (const containerId of containerIds) {
|
|
605
|
+
try {
|
|
606
|
+
const container = docker.getContainer(containerId);
|
|
607
|
+
await container.restart();
|
|
608
|
+
results.push({ id: containerId, success: true });
|
|
609
|
+
} catch (error) {
|
|
610
|
+
results.push({ id: containerId, success: false, error: error.message });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const succeeded = results.filter(r => r.success);
|
|
615
|
+
const failed = results.filter(r => !r.success);
|
|
616
|
+
|
|
617
|
+
if (failed.length === 0) {
|
|
618
|
+
spinner.succeed(`Successfully restarted ${succeeded.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
619
|
+
} else if (succeeded.length === 0) {
|
|
620
|
+
spinner.fail(`Failed to restart all containers`);
|
|
621
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
622
|
+
} else {
|
|
623
|
+
spinner.warn(`Restarted ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
624
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
625
|
+
}
|
|
626
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
spinner.fail(`Failed to restart containers: ${error.message}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Remove container(s) - supports multiple containers
|
|
634
|
+
program
|
|
635
|
+
.command('rm <containerId(s)...>')
|
|
636
|
+
.description('remove one or more containers')
|
|
637
|
+
.option('-f, --force', 'Force remove the container(s)')
|
|
638
|
+
.action(async (containerIds, options) => {
|
|
639
|
+
const spinner = ora(`Removing ${containerIds.length} ${containerIds.length === 1 ? 'container' : 'containers'}...`).start();
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const results = [];
|
|
643
|
+
for (const containerId of containerIds) {
|
|
644
|
+
try {
|
|
645
|
+
const container = docker.getContainer(containerId);
|
|
646
|
+
await container.remove({ force: options.force });
|
|
647
|
+
results.push({ id: containerId, success: true });
|
|
648
|
+
} catch (error) {
|
|
649
|
+
results.push({ id: containerId, success: false, error: error.message });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const succeeded = results.filter(r => r.success);
|
|
654
|
+
const failed = results.filter(r => !r.success);
|
|
655
|
+
|
|
656
|
+
if (failed.length === 0) {
|
|
657
|
+
spinner.succeed(`Successfully removed ${succeeded.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
658
|
+
} else if (succeeded.length === 0) {
|
|
659
|
+
spinner.fail(`Failed to remove all containers`);
|
|
660
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
661
|
+
} else {
|
|
662
|
+
spinner.warn(`Removed ${succeeded.length}/${containerIds.length} ${succeeded.length === 1 ? 'container' : 'containers'}`);
|
|
663
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
664
|
+
}
|
|
665
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
spinner.fail(`Failed to remove containers: ${error.message}`);
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Image commands
|
|
673
|
+
program
|
|
674
|
+
.command('images')
|
|
675
|
+
.description('list images')
|
|
676
|
+
.action(async () => {
|
|
677
|
+
const spinner = ora('Fetching images...').start();
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const images = await docker.listImages();
|
|
681
|
+
|
|
682
|
+
if (images.length === 0) {
|
|
683
|
+
spinner.succeed('No images found');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
spinner.succeed(`Found ${images.length} ${images.length === 1 ? 'image' : 'images'}`);
|
|
688
|
+
|
|
689
|
+
// Create a table for display
|
|
690
|
+
const table = new Table({
|
|
691
|
+
head: [
|
|
692
|
+
chalk.blue('ID'),
|
|
693
|
+
chalk.blue('Repository'),
|
|
694
|
+
chalk.blue('Tag'),
|
|
695
|
+
chalk.blue('Size'),
|
|
696
|
+
chalk.blue('Created'),
|
|
697
|
+
],
|
|
698
|
+
colWidths: [15, 30, 15, 15, 15],
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
images.forEach((image) => {
|
|
702
|
+
// Extract repository and tag
|
|
703
|
+
let name = '<none>';
|
|
704
|
+
let tag = '<none>';
|
|
705
|
+
|
|
706
|
+
if (image.RepoTags && image.RepoTags.length > 0) {
|
|
707
|
+
const [repoTag] = image.RepoTags;
|
|
708
|
+
const parts = repoTag.split(':');
|
|
709
|
+
name = parts[0];
|
|
710
|
+
tag = parts.length > 1 ? parts[1] : 'latest';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
table.push([
|
|
714
|
+
image.Id.substring(7, 19),
|
|
715
|
+
name,
|
|
716
|
+
tag,
|
|
717
|
+
formatSize(image.Size),
|
|
718
|
+
formatCreatedTime(image.Created),
|
|
719
|
+
]);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
console.log(table.toString());
|
|
723
|
+
process.exit(0);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
spinner.fail(`Failed to fetch images: ${error.message}`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Pull image(s) - supports multiple images
|
|
731
|
+
program
|
|
732
|
+
.command('pull <imageTags...>')
|
|
733
|
+
.description('pull one or more images')
|
|
734
|
+
.action(async (imageTags) => {
|
|
735
|
+
const spinner = ora(`Pulling ${imageTags.length} ${imageTags.length === 1 ? 'image' : 'images'}...`).start();
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const results = [];
|
|
739
|
+
for (const imageTag of imageTags) {
|
|
740
|
+
try {
|
|
741
|
+
// Split image tag into name and tag
|
|
742
|
+
const [name, tag = 'latest'] = imageTag.split(':');
|
|
743
|
+
spinner.text = `Pulling ${imageTag}...`;
|
|
744
|
+
|
|
745
|
+
// Pull the image
|
|
746
|
+
const stream = await docker.pull(`${name}:${tag}`);
|
|
747
|
+
|
|
748
|
+
// Track progress
|
|
749
|
+
await new Promise((resolve, reject) => {
|
|
750
|
+
docker.modem.followProgress(
|
|
751
|
+
stream,
|
|
752
|
+
(err, output) => {
|
|
753
|
+
if (err) {
|
|
754
|
+
reject(err);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
resolve(output);
|
|
758
|
+
},
|
|
759
|
+
(event) => {
|
|
760
|
+
if (event.progress) {
|
|
761
|
+
spinner.text = `Pulling ${imageTag}: ${event.progress}`;
|
|
762
|
+
} else if (event.status) {
|
|
763
|
+
spinner.text = `Pulling ${imageTag}: ${event.status}`;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
results.push({ tag: imageTag, success: true });
|
|
770
|
+
} catch (error) {
|
|
771
|
+
results.push({ tag: imageTag, success: false, error: error.message });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const succeeded = results.filter(r => r.success);
|
|
776
|
+
const failed = results.filter(r => !r.success);
|
|
777
|
+
|
|
778
|
+
if (failed.length === 0) {
|
|
779
|
+
spinner.succeed(`Successfully pulled ${succeeded.length} ${succeeded.length === 1 ? 'image' : 'images'}`);
|
|
780
|
+
} else if (succeeded.length === 0) {
|
|
781
|
+
spinner.fail(`Failed to pull all images`);
|
|
782
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.tag}: ${r.error}`)));
|
|
783
|
+
} else {
|
|
784
|
+
spinner.warn(`Pulled ${succeeded.length}/${imageTags.length} ${succeeded.length === 1 ? 'image' : 'images'}`);
|
|
785
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.tag}: ${r.error}`)));
|
|
786
|
+
}
|
|
787
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
spinner.fail(`Failed to pull images: ${error.message}`);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Remove image(s) - supports multiple images
|
|
795
|
+
program
|
|
796
|
+
.command('rmi <imageIds...>')
|
|
797
|
+
.description('remove one or more images')
|
|
798
|
+
.option('-f, --force', 'Force remove the image(s)')
|
|
799
|
+
.action(async (imageIds, options) => {
|
|
800
|
+
const spinner = ora(`Removing ${imageIds.length} ${imageIds.length === 1 ? 'image' : 'images'}...`).start();
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
const results = [];
|
|
804
|
+
for (const imageId of imageIds) {
|
|
805
|
+
try {
|
|
806
|
+
const image = docker.getImage(imageId);
|
|
807
|
+
await image.remove({ force: options.force });
|
|
808
|
+
results.push({ id: imageId, success: true });
|
|
809
|
+
} catch (error) {
|
|
810
|
+
results.push({ id: imageId, success: false, error: error.message });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const succeeded = results.filter(r => r.success);
|
|
815
|
+
const failed = results.filter(r => !r.success);
|
|
816
|
+
|
|
817
|
+
if (failed.length === 0) {
|
|
818
|
+
spinner.succeed(`Successfully removed ${succeeded.length} ${succeeded.length === 1 ? 'image' : 'images'}`);
|
|
819
|
+
} else if (succeeded.length === 0) {
|
|
820
|
+
spinner.fail(`Failed to remove all images`);
|
|
821
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
822
|
+
} else {
|
|
823
|
+
spinner.warn(`Removed ${succeeded.length}/${imageIds.length} ${succeeded.length === 1 ? 'image' : 'images'}`);
|
|
824
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.id}: ${r.error}`)));
|
|
825
|
+
}
|
|
826
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
spinner.fail(`Failed to remove images: ${error.message}`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Volume commands
|
|
834
|
+
program
|
|
835
|
+
.command('volumes')
|
|
836
|
+
.description('list volumes')
|
|
837
|
+
.action(async () => {
|
|
838
|
+
const spinner = ora('Fetching volumes...').start();
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
const { Volumes } = await docker.listVolumes();
|
|
842
|
+
|
|
843
|
+
if (Volumes.length === 0) {
|
|
844
|
+
spinner.succeed('No volumes found');
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
spinner.succeed(`Found ${Volumes.length} ${Volumes.length === 1 ? 'volume' : 'volumes'}`);
|
|
849
|
+
|
|
850
|
+
// Create a table for display
|
|
851
|
+
const table = new Table({
|
|
852
|
+
head: [
|
|
853
|
+
chalk.blue('Name'),
|
|
854
|
+
chalk.blue('Driver'),
|
|
855
|
+
chalk.blue('Mountpoint'),
|
|
856
|
+
],
|
|
857
|
+
colWidths: [30, 15, 50],
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
Volumes.forEach((volume) => {
|
|
861
|
+
table.push([volume.Name, volume.Driver, volume.Mountpoint]);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
console.log(table.toString());
|
|
865
|
+
process.exit(0);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
spinner.fail(`Failed to fetch volumes: ${error.message}`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Create volume(s) - supports multiple volumes
|
|
873
|
+
program
|
|
874
|
+
.command('volume-create <names...>')
|
|
875
|
+
.description('create one or more volumes')
|
|
876
|
+
.option('-d, --driver <driver>', 'Volume driver', 'local')
|
|
877
|
+
.action(async (names, options) => {
|
|
878
|
+
const spinner = ora(`Creating ${names.length} ${names.length === 1 ? 'volume' : 'volumes'}...`).start();
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
const results = [];
|
|
882
|
+
for (const name of names) {
|
|
883
|
+
try {
|
|
884
|
+
await docker.createVolume({
|
|
885
|
+
Name: name,
|
|
886
|
+
Driver: options.driver,
|
|
887
|
+
});
|
|
888
|
+
results.push({ name, success: true });
|
|
889
|
+
} catch (error) {
|
|
890
|
+
results.push({ name, success: false, error: error.message });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const succeeded = results.filter(r => r.success);
|
|
895
|
+
const failed = results.filter(r => !r.success);
|
|
896
|
+
|
|
897
|
+
if (failed.length === 0) {
|
|
898
|
+
spinner.succeed(`Successfully created ${succeeded.length} ${succeeded.length === 1 ? 'volume' : 'volumes'}`);
|
|
899
|
+
} else if (succeeded.length === 0) {
|
|
900
|
+
spinner.fail(`Failed to create all volumes`);
|
|
901
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.name}: ${r.error}`)));
|
|
902
|
+
} else {
|
|
903
|
+
spinner.warn(`Created ${succeeded.length}/${names.length} ${succeeded.length === 1 ? 'volume' : 'volumes'}`);
|
|
904
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.name}: ${r.error}`)));
|
|
905
|
+
}
|
|
906
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
spinner.fail(`Failed to create volumes: ${error.message}`);
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Remove volume(s) - supports multiple volumes
|
|
914
|
+
program
|
|
915
|
+
.command('volume-rm <names...>')
|
|
916
|
+
.description('remove one or more volumes')
|
|
917
|
+
.action(async (names) => {
|
|
918
|
+
const spinner = ora(`Removing ${names.length} ${names.length === 1 ? 'volume' : 'volumes'}...`).start();
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
const results = [];
|
|
922
|
+
for (const name of names) {
|
|
923
|
+
try {
|
|
924
|
+
const volume = docker.getVolume(name);
|
|
925
|
+
await volume.remove();
|
|
926
|
+
results.push({ name, success: true });
|
|
927
|
+
} catch (error) {
|
|
928
|
+
results.push({ name, success: false, error: error.message });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const succeeded = results.filter(r => r.success);
|
|
933
|
+
const failed = results.filter(r => !r.success);
|
|
934
|
+
|
|
935
|
+
if (failed.length === 0) {
|
|
936
|
+
spinner.succeed(`Successfully removed ${succeeded.length} ${succeeded.length === 1 ? 'volume' : 'volumes'}`);
|
|
937
|
+
} else if (succeeded.length === 0) {
|
|
938
|
+
spinner.fail(`Failed to remove all volumes`);
|
|
939
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.name}: ${r.error}`)));
|
|
940
|
+
} else {
|
|
941
|
+
spinner.warn(`Removed ${succeeded.length}/${names.length} ${succeeded.length === 1 ? 'volume' : 'volumes'}`);
|
|
942
|
+
failed.forEach(r => console.log(chalk.red(` ✗ ${r.name}: ${r.error}`)));
|
|
943
|
+
}
|
|
944
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
spinner.fail(`Failed to remove volumes: ${error.message}`);
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Logs command
|
|
952
|
+
program
|
|
953
|
+
.command('logs <containerId>')
|
|
954
|
+
.description('fetch container logs')
|
|
955
|
+
.option('-f, --follow', 'Follow log output')
|
|
956
|
+
.option('-t, --tail <lines>', 'Number of lines to show from the end', '100')
|
|
957
|
+
.action(async (containerId, options) => {
|
|
958
|
+
try {
|
|
959
|
+
const container = docker.getContainer(containerId);
|
|
960
|
+
|
|
961
|
+
if (options.follow) {
|
|
962
|
+
console.log(
|
|
963
|
+
chalk.blue(`Following logs for container ${containerId}...`)
|
|
964
|
+
);
|
|
965
|
+
console.log(chalk.blue('Press Ctrl+C to exit'));
|
|
966
|
+
|
|
967
|
+
const logStream = await container.logs({
|
|
968
|
+
follow: true,
|
|
969
|
+
stdout: true,
|
|
970
|
+
stderr: true,
|
|
971
|
+
tail: Number.parseInt(options.tail, 10),
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
logStream.on('data', (chunk) => {
|
|
975
|
+
process.stdout.write(chunk.toString());
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Handle Ctrl+C
|
|
979
|
+
process.on('SIGINT', () => {
|
|
980
|
+
console.log(chalk.blue('\nStopping log stream...'));
|
|
981
|
+
process.exit(0);
|
|
982
|
+
});
|
|
983
|
+
} else {
|
|
984
|
+
const spinner = ora(
|
|
985
|
+
`Fetching logs for container ${containerId}...`
|
|
986
|
+
).start();
|
|
987
|
+
|
|
988
|
+
const logs = await container.logs({
|
|
989
|
+
stdout: true,
|
|
990
|
+
stderr: true,
|
|
991
|
+
tail: Number.parseInt(options.tail, 10),
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
spinner.stop();
|
|
995
|
+
console.log(logs.toString());
|
|
996
|
+
process.exit(0);
|
|
997
|
+
}
|
|
998
|
+
} catch (error) {
|
|
999
|
+
console.error(chalk.red(`Failed to fetch logs: ${error.message}`));
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// Local-env app commands
|
|
1005
|
+
program
|
|
1006
|
+
.command('local-env')
|
|
1007
|
+
.description('manage local-env app container')
|
|
1008
|
+
.option('--start', 'start the local-env app')
|
|
1009
|
+
.option('--stop', 'stop the local-env app')
|
|
1010
|
+
.option('--status', 'show local-env app status')
|
|
1011
|
+
.option('--logs', 'show local-env app logs')
|
|
1012
|
+
.option('--logs-follow', 'follow local-env app logs')
|
|
1013
|
+
.action(async (options) => {
|
|
1014
|
+
try {
|
|
1015
|
+
if (options.start) {
|
|
1016
|
+
const spinner = ora('Starting local-env app...').start();
|
|
1017
|
+
try {
|
|
1018
|
+
await containerManager.startContainer();
|
|
1019
|
+
spinner.succeed('Local-env app started successfully');
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
spinner.fail(`Failed to start local-env app: ${error.message}`);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
} else if (options.stop) {
|
|
1025
|
+
const spinner = ora('Stopping local-env app...').start();
|
|
1026
|
+
try {
|
|
1027
|
+
await containerManager.stopContainerGracefully();
|
|
1028
|
+
spinner.succeed('Local-env app stopped successfully');
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
spinner.fail(`Failed to stop local-env app: ${error.message}`);
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
} else if (options.status) {
|
|
1034
|
+
const status = await containerManager.getStatus();
|
|
1035
|
+
console.log(chalk.bold('\nLocal-env App Status:'));
|
|
1036
|
+
console.log(chalk.blue('Container:'), status.containerName);
|
|
1037
|
+
console.log(
|
|
1038
|
+
chalk.blue('Running:'),
|
|
1039
|
+
status.isRunning ? chalk.green('Yes') : chalk.red('No')
|
|
1040
|
+
);
|
|
1041
|
+
console.log(chalk.blue('Port:'), status.port);
|
|
1042
|
+
console.log(chalk.blue('Data Directory:'), status.dataDirectory);
|
|
1043
|
+
if (status.isRunning) {
|
|
1044
|
+
console.log(
|
|
1045
|
+
chalk.blue('URL:'),
|
|
1046
|
+
chalk.underline(`http://localhost:${status.port}`)
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
} else if (options.logs) {
|
|
1050
|
+
console.log(chalk.blue('📋 Showing local-env app logs...'));
|
|
1051
|
+
containerManager.showLogs(false);
|
|
1052
|
+
} else if (options.logsFollow) {
|
|
1053
|
+
console.log(
|
|
1054
|
+
chalk.blue(
|
|
1055
|
+
'📋 Following local-env app logs... (Press Ctrl+C to stop)'
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
containerManager.showLogs(true);
|
|
1059
|
+
} else {
|
|
1060
|
+
console.log(
|
|
1061
|
+
chalk.yellow(
|
|
1062
|
+
'Please specify an action: --start, --stop, --status, --logs, or --logs-follow'
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.error(chalk.red('❌ Error:'), error.message);
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
// Registry commands
|
|
1074
|
+
program
|
|
1075
|
+
.command('registries')
|
|
1076
|
+
.description('list registries')
|
|
1077
|
+
.action(async () => {
|
|
1078
|
+
const spinner = ora('Fetching registries...').start();
|
|
1079
|
+
|
|
1080
|
+
try {
|
|
1081
|
+
await registryStore.initialize();
|
|
1082
|
+
const registries = await registryStore.getAllRegistries();
|
|
1083
|
+
|
|
1084
|
+
if (registries.length === 0) {
|
|
1085
|
+
spinner.succeed('No registries found');
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const registryText =
|
|
1090
|
+
registries.length === 1 ? 'registry' : 'registries';
|
|
1091
|
+
spinner.succeed(`Found ${registries.length} ${registryText}`);
|
|
1092
|
+
|
|
1093
|
+
const table = new Table({
|
|
1094
|
+
head: ['ID', 'Name', 'Type', 'URL', 'Connected'].map((h) =>
|
|
1095
|
+
chalk.cyan(h)
|
|
1096
|
+
),
|
|
1097
|
+
style: { head: [], border: [] },
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
for (const registry of registries) {
|
|
1101
|
+
table.push([
|
|
1102
|
+
registry.id.substring(0, 12) + '...',
|
|
1103
|
+
registry.name,
|
|
1104
|
+
registry.type,
|
|
1105
|
+
registry.url,
|
|
1106
|
+
registry.connected ? chalk.green('✓') : chalk.red('✗'),
|
|
1107
|
+
]);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
console.log(chalk.bold('\nConnected Registries:'));
|
|
1111
|
+
console.log(table.toString());
|
|
1112
|
+
process.exit(0);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
spinner.fail(`Failed to fetch registries: ${error.message}`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Agent info command
|
|
1120
|
+
program
|
|
1121
|
+
.command('info')
|
|
1122
|
+
.description('display agent informations')
|
|
1123
|
+
.action(async () => {
|
|
1124
|
+
const spinner = ora('Fetching agent informations...').start();
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
// Get Docker version
|
|
1128
|
+
const dockerVersion = await docker.version();
|
|
1129
|
+
|
|
1130
|
+
// Display agent informations
|
|
1131
|
+
spinner.stop();
|
|
1132
|
+
const { version } = packageJson;
|
|
1133
|
+
|
|
1134
|
+
console.log(chalk.bold('\nFenwave Agent Informations:'));
|
|
1135
|
+
console.log(chalk.blue('Version:'), version);
|
|
1136
|
+
console.log(chalk.blue('Hostname:'), os.hostname());
|
|
1137
|
+
console.log(chalk.blue('Platform:'), os.platform());
|
|
1138
|
+
console.log(chalk.blue('Architecture:'), os.arch());
|
|
1139
|
+
console.log(chalk.blue('Node.js Version:'), process.version);
|
|
1140
|
+
console.log(chalk.blue('Docker Version:'), dockerVersion.Version);
|
|
1141
|
+
console.log(chalk.blue('CPU Cores:'), os.cpus().length);
|
|
1142
|
+
console.log(
|
|
1143
|
+
chalk.blue('Memory:'),
|
|
1144
|
+
`${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// Check if agent is actually running
|
|
1148
|
+
const isAgentRunning = await checkAgentRunning(WS_PORT);
|
|
1149
|
+
|
|
1150
|
+
if (isAgentRunning) {
|
|
1151
|
+
// Agent is running, try to get uptime from stored data
|
|
1152
|
+
try {
|
|
1153
|
+
const agentStartTime = await agentStore.loadAgentStartTime();
|
|
1154
|
+
if (agentStartTime) {
|
|
1155
|
+
const currentTime = Date.now();
|
|
1156
|
+
const agentUptimeMs = currentTime - agentStartTime.getTime();
|
|
1157
|
+
console.log(chalk.blue('Uptime:'), formatUptime(agentUptimeMs));
|
|
1158
|
+
} else {
|
|
1159
|
+
console.log(chalk.blue('Uptime:'), '0 seconds');
|
|
1160
|
+
}
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
console.log(chalk.blue('Uptime:'), '0 seconds');
|
|
1163
|
+
}
|
|
1164
|
+
} else {
|
|
1165
|
+
// Agent is not running
|
|
1166
|
+
console.log(chalk.blue('Status:'), chalk.red('Agent Disconnected'));
|
|
1167
|
+
|
|
1168
|
+
// Clean up stale agent info file if it exists
|
|
1169
|
+
try {
|
|
1170
|
+
await agentStore.clearAgentInfo();
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
// Ignore cleanup errors
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
process.exit(0);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
spinner.fail(`Failed to fetch agent informations: ${error.message}`);
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export {
|
|
1184
|
+
setupCLICommands,
|
|
1185
|
+
};
|