@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/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fenwave/agent",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Fenwave Docker Agent and CLI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fenwave",
|
|
7
|
+
"fenleap",
|
|
8
|
+
"fenwave-idp"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/Fenleap/Fenwave-plugins#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Fenleap/Fenwave-plugins/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Fenleap/Fenwave-plugins.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
19
|
+
"author": "Fenleap",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"bin": {
|
|
23
|
+
"fenwave": "index.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "node index.js server",
|
|
27
|
+
"agent": "node index.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@aws-sdk/client-ecr": "^3.879.0",
|
|
31
|
+
"@aws-sdk/client-sts": "^3.879.0",
|
|
32
|
+
"axios": "^1.11.0",
|
|
33
|
+
"chalk": "^4.1.2",
|
|
34
|
+
"cli-table3": "^0.6.3",
|
|
35
|
+
"commander": "^11.0.0",
|
|
36
|
+
"dockerode": "^4.0.0",
|
|
37
|
+
"dotenv": "^16.0.3",
|
|
38
|
+
"google-auth-library": "^9.0.0",
|
|
39
|
+
"inquirer": "^8.2.5",
|
|
40
|
+
"js-yaml": "^4.1.0",
|
|
41
|
+
"open": "^11.0.0",
|
|
42
|
+
"ora": "^5.4.1",
|
|
43
|
+
"uuid": "^9.0.1",
|
|
44
|
+
"ws": "^8.15.1"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=14.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import {
|
|
5
|
+
checkPrerequisites,
|
|
6
|
+
displayPrerequisites,
|
|
7
|
+
getMissingPrerequisites,
|
|
8
|
+
} from "../utils/prerequisites.js";
|
|
9
|
+
import {
|
|
10
|
+
promptRegistrationToken,
|
|
11
|
+
promptConfirmation,
|
|
12
|
+
displayProgress,
|
|
13
|
+
displaySuccess,
|
|
14
|
+
displayError,
|
|
15
|
+
displayWarning,
|
|
16
|
+
displayInfo,
|
|
17
|
+
displayHeader,
|
|
18
|
+
displayStep,
|
|
19
|
+
displayKeyValue,
|
|
20
|
+
} from "../utils/prompts.js";
|
|
21
|
+
import {
|
|
22
|
+
handleError,
|
|
23
|
+
createError,
|
|
24
|
+
ErrorCode,
|
|
25
|
+
isUserCancellation,
|
|
26
|
+
} from "../utils/errorHandler.js";
|
|
27
|
+
import { getDeviceMetadata, displayDeviceInfo } from "../utils/deviceInfo.js";
|
|
28
|
+
import {
|
|
29
|
+
saveDeviceCredential,
|
|
30
|
+
isDeviceRegistered,
|
|
31
|
+
} from "../store/deviceCredentialStore.js";
|
|
32
|
+
import { saveNpmToken } from "../store/npmTokenStore.js";
|
|
33
|
+
import {
|
|
34
|
+
saveSetupProgress,
|
|
35
|
+
clearSetupState,
|
|
36
|
+
SetupStep,
|
|
37
|
+
isSetupInProgress,
|
|
38
|
+
} from "../store/setupState.js";
|
|
39
|
+
import { authenticateDockerWithECR } from "../utils/ecrAuth.js";
|
|
40
|
+
import { initializeConfig } from "../store/configStore.js";
|
|
41
|
+
|
|
42
|
+
const TOTAL_STEPS = 7;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Setup Wizard Orchestrator
|
|
46
|
+
* Guides user through complete agent setup
|
|
47
|
+
*/
|
|
48
|
+
export class SetupWizard {
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.backendUrl = options.backendUrl || "http://localhost:7007";
|
|
51
|
+
this.frontendUrl = options.frontendUrl || "http://localhost:3000";
|
|
52
|
+
this.awsRegion = options.awsRegion || "eu-west-1";
|
|
53
|
+
this.awsAccountId = options.awsAccountId || null; // Will be set from Backstage during registration
|
|
54
|
+
this.skipPrerequisites = options.skipPrerequisites || false;
|
|
55
|
+
this.registrationToken = options.token || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run the complete setup wizard
|
|
60
|
+
*/
|
|
61
|
+
async run() {
|
|
62
|
+
try {
|
|
63
|
+
// Welcome message
|
|
64
|
+
this.displayWelcome();
|
|
65
|
+
|
|
66
|
+
// Initialize and save agent configuration
|
|
67
|
+
const agentConfig = {
|
|
68
|
+
backendUrl: this.backendUrl,
|
|
69
|
+
frontendUrl: this.frontendUrl,
|
|
70
|
+
awsRegion: this.awsRegion,
|
|
71
|
+
awsAccountId: this.awsAccountId,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
initializeConfig(agentConfig);
|
|
75
|
+
|
|
76
|
+
displayInfo("Agent configuration initialized:");
|
|
77
|
+
displayKeyValue("Backend URL", this.backendUrl);
|
|
78
|
+
displayKeyValue("Frontend URL", this.frontendUrl);
|
|
79
|
+
displayKeyValue("AWS Region", this.awsRegion);
|
|
80
|
+
if (this.awsAccountId) {
|
|
81
|
+
displayKeyValue("AWS Account ID", this.awsAccountId);
|
|
82
|
+
}
|
|
83
|
+
console.log("");
|
|
84
|
+
|
|
85
|
+
// Check if already registered
|
|
86
|
+
if (isDeviceRegistered()) {
|
|
87
|
+
displayWarning("Device is already registered!");
|
|
88
|
+
const shouldContinue = await promptConfirmation(
|
|
89
|
+
"Do you want to re-register (this will replace existing credentials)?",
|
|
90
|
+
false
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!shouldContinue) {
|
|
94
|
+
displayInfo(
|
|
95
|
+
'Setup cancelled. Use "fenwave status" to view current registration.'
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for interrupted setup
|
|
102
|
+
if (isSetupInProgress()) {
|
|
103
|
+
const resume = await promptConfirmation(
|
|
104
|
+
"Found interrupted setup. Do you want to resume?",
|
|
105
|
+
true
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!resume) {
|
|
109
|
+
clearSetupState();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 1: Prerequisites Check
|
|
114
|
+
await this.checkPrerequisitesStep();
|
|
115
|
+
|
|
116
|
+
// Step 2: Device Registration
|
|
117
|
+
const registrationData = await this.registrationStep();
|
|
118
|
+
|
|
119
|
+
// Step 3: Docker Registry Configuration
|
|
120
|
+
await this.dockerRegistryStep(registrationData);
|
|
121
|
+
|
|
122
|
+
// Step 4: Pull DevApp Image
|
|
123
|
+
await this.pullImageStep(registrationData);
|
|
124
|
+
|
|
125
|
+
// Step 5: NPM Configuration
|
|
126
|
+
await this.npmConfigStep(registrationData);
|
|
127
|
+
|
|
128
|
+
// Step 6: Start Agent (optional)
|
|
129
|
+
await this.startAgentStep();
|
|
130
|
+
|
|
131
|
+
// Step 7: Complete
|
|
132
|
+
this.displayComplete(registrationData);
|
|
133
|
+
|
|
134
|
+
// Clear setup state
|
|
135
|
+
clearSetupState();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isUserCancellation(error)) {
|
|
138
|
+
displayWarning("\nSetup cancelled by user.");
|
|
139
|
+
displayInfo('Run "fenwave init" again to resume setup.\n');
|
|
140
|
+
} else {
|
|
141
|
+
handleError(error, "Setup Wizard");
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Display welcome message
|
|
149
|
+
*/
|
|
150
|
+
displayWelcome() {
|
|
151
|
+
displayHeader("Fenwave Dev Agent Setup Wizard");
|
|
152
|
+
console.log(
|
|
153
|
+
chalk.gray(
|
|
154
|
+
" This wizard will guide you through setting up the Fenwave Dev Agent.\n"
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
console.log(
|
|
158
|
+
chalk.gray(" You can interrupt the setup anytime and resume later.\n")
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Step 1: Check prerequisites
|
|
164
|
+
*/
|
|
165
|
+
async checkPrerequisitesStep() {
|
|
166
|
+
displayStep(1, TOTAL_STEPS, "Checking Prerequisites");
|
|
167
|
+
|
|
168
|
+
if (this.skipPrerequisites) {
|
|
169
|
+
displayInfo("Skipping prerequisites check (--skip-prerequisites flag)");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const checkResults = await checkPrerequisites({
|
|
174
|
+
backendUrl: this.backendUrl,
|
|
175
|
+
});
|
|
176
|
+
const allPassed = displayPrerequisites(checkResults);
|
|
177
|
+
|
|
178
|
+
if (!allPassed) {
|
|
179
|
+
const missing = getMissingPrerequisites(checkResults);
|
|
180
|
+
|
|
181
|
+
console.log(chalk.yellow("\n⚠️ Missing prerequisites:\n"));
|
|
182
|
+
missing.forEach((item) => {
|
|
183
|
+
console.log(chalk.red(` ❌ ${item.name}: ${item.reason}`));
|
|
184
|
+
console.log(chalk.gray(` Action: ${item.action}\n`));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const shouldContinue = await promptConfirmation(
|
|
188
|
+
"Some prerequisites are missing. Continue anyway?",
|
|
189
|
+
false
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!shouldContinue) {
|
|
193
|
+
throw createError(
|
|
194
|
+
ErrorCode.OPERATION_CANCELLED,
|
|
195
|
+
"Prerequisites check failed"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
saveSetupProgress(SetupStep.PREREQUISITES, { allPassed });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Step 2: Device Registration
|
|
205
|
+
*/
|
|
206
|
+
async registrationStep() {
|
|
207
|
+
displayStep(2, TOTAL_STEPS, "Device Registration");
|
|
208
|
+
|
|
209
|
+
// Get registration token
|
|
210
|
+
if (!this.registrationToken) {
|
|
211
|
+
this.registrationToken = await promptRegistrationToken(this.backendUrl);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Collect device information
|
|
215
|
+
const deviceMetadata = await getDeviceMetadata();
|
|
216
|
+
displayInfo("Collected device information:");
|
|
217
|
+
displayDeviceInfo(deviceMetadata);
|
|
218
|
+
|
|
219
|
+
// Register device with backend
|
|
220
|
+
const registrationData = await displayProgress(
|
|
221
|
+
"Registering device with Backstage...",
|
|
222
|
+
this.registerDevice(deviceMetadata)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Save device credentials
|
|
226
|
+
saveDeviceCredential({
|
|
227
|
+
deviceId: registrationData.deviceId,
|
|
228
|
+
deviceCredential: registrationData.deviceCredential,
|
|
229
|
+
userEntityRef: registrationData.userEntityRef,
|
|
230
|
+
deviceName: deviceMetadata.deviceName,
|
|
231
|
+
platform: deviceMetadata.platform,
|
|
232
|
+
agentVersion: deviceMetadata.agentVersion,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
displaySuccess(`Device registered: ${registrationData.deviceId}`);
|
|
236
|
+
console.log(chalk.gray(` User: ${registrationData.userEntityRef}\n`));
|
|
237
|
+
|
|
238
|
+
saveSetupProgress(SetupStep.REGISTRATION, {
|
|
239
|
+
deviceId: registrationData.deviceId,
|
|
240
|
+
deviceName: deviceMetadata.deviceName,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return registrationData;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Register device with backend
|
|
248
|
+
*/
|
|
249
|
+
async registerDevice(deviceMetadata) {
|
|
250
|
+
try {
|
|
251
|
+
const response = await axios.post(
|
|
252
|
+
`${this.backendUrl}/api/agent-cli/register`,
|
|
253
|
+
{
|
|
254
|
+
installToken: this.registrationToken,
|
|
255
|
+
deviceInfo: {
|
|
256
|
+
deviceName: deviceMetadata.deviceName,
|
|
257
|
+
platform: deviceMetadata.platform,
|
|
258
|
+
osVersion: deviceMetadata.osVersion,
|
|
259
|
+
agentVersion: deviceMetadata.agentVersion,
|
|
260
|
+
metadata: deviceMetadata.metadata,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{ timeout: 10000 }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
deviceId: response.data.deviceId,
|
|
268
|
+
deviceCredential: response.data.deviceCredential,
|
|
269
|
+
npmToken: response.data.npmToken,
|
|
270
|
+
registryConfig: response.data.registryConfig,
|
|
271
|
+
userEntityRef: response.data.userEntityRef || "unknown",
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error.response?.status === 401) {
|
|
275
|
+
throw createError(
|
|
276
|
+
ErrorCode.INVALID_TOKEN,
|
|
277
|
+
"Invalid or expired registration token"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (error.response?.status === 429) {
|
|
281
|
+
throw createError(
|
|
282
|
+
ErrorCode.RATE_LIMIT_EXCEEDED,
|
|
283
|
+
"Too many registration attempts"
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Step 3: Docker Registry Configuration
|
|
292
|
+
*/
|
|
293
|
+
async dockerRegistryStep(registrationData) {
|
|
294
|
+
displayStep(3, TOTAL_STEPS, "Docker Registry Configuration");
|
|
295
|
+
|
|
296
|
+
const shouldConfigure = await promptConfirmation(
|
|
297
|
+
"Configure AWS ECR for Fenwave images?",
|
|
298
|
+
true
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!shouldConfigure) {
|
|
302
|
+
displayInfo("Skipping Docker registry configuration");
|
|
303
|
+
saveSetupProgress(SetupStep.DOCKER_REGISTRY, { skipped: true });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { awsRegion, awsAccountId } = registrationData.registryConfig;
|
|
308
|
+
|
|
309
|
+
displayInfo(`Authenticating with AWS ECR (${awsRegion})...`);
|
|
310
|
+
displayInfo(
|
|
311
|
+
"🔐 Using Backstage-controlled credential flow (temporary credentials)"
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Use Backstage-controlled credential flow
|
|
316
|
+
const authResult = await authenticateDockerWithECR(this.backendUrl);
|
|
317
|
+
|
|
318
|
+
displaySuccess("AWS ECR authenticated successfully");
|
|
319
|
+
displayInfo(`✅ Credentials expire at: ${authResult.expiration}`);
|
|
320
|
+
displayInfo(`📦 Registry: ${authResult.registryUri}`);
|
|
321
|
+
|
|
322
|
+
saveSetupProgress(SetupStep.DOCKER_REGISTRY, {
|
|
323
|
+
awsRegion: authResult.region,
|
|
324
|
+
awsAccountId: authResult.accountId,
|
|
325
|
+
registryUri: authResult.registryUri,
|
|
326
|
+
expiration: authResult.expiration,
|
|
327
|
+
});
|
|
328
|
+
} catch (error) {
|
|
329
|
+
displayError("Failed to authenticate with AWS ECR");
|
|
330
|
+
displayError(`Error: ${error.message}`);
|
|
331
|
+
displayWarning(
|
|
332
|
+
"Make sure your device is registered and Backstage is running"
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const shouldContinue = await promptConfirmation(
|
|
336
|
+
"Continue without ECR authentication?",
|
|
337
|
+
true
|
|
338
|
+
);
|
|
339
|
+
if (!shouldContinue) {
|
|
340
|
+
throw createError(
|
|
341
|
+
ErrorCode.ECR_AUTH_FAILED,
|
|
342
|
+
"ECR authentication failed"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
saveSetupProgress(SetupStep.DOCKER_REGISTRY, {
|
|
347
|
+
failed: true,
|
|
348
|
+
error: error.message,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Step 4: Pull Fenwave DevApp Image
|
|
355
|
+
*/
|
|
356
|
+
async pullImageStep(registrationData) {
|
|
357
|
+
displayStep(4, TOTAL_STEPS, "Pull Fenwave DevApp Image");
|
|
358
|
+
|
|
359
|
+
const shouldPull = await promptConfirmation(
|
|
360
|
+
"Pull Fenwave DevApp Docker image?",
|
|
361
|
+
true
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (!shouldPull) {
|
|
365
|
+
displayInfo("Skipping image pull");
|
|
366
|
+
saveSetupProgress(SetupStep.PULL_IMAGE, { skipped: true });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const { dockerImage } = registrationData.registryConfig;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
displayInfo(`Pulling image: ${dockerImage}`);
|
|
374
|
+
displayWarning("This may take a few minutes...");
|
|
375
|
+
|
|
376
|
+
await this.pullDockerImage(dockerImage);
|
|
377
|
+
|
|
378
|
+
displaySuccess("Docker image pulled successfully");
|
|
379
|
+
saveSetupProgress(SetupStep.PULL_IMAGE, { dockerImage });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
displayError("Failed to pull Docker image");
|
|
382
|
+
|
|
383
|
+
const shouldContinue = await promptConfirmation(
|
|
384
|
+
"Continue without pulling image?",
|
|
385
|
+
true
|
|
386
|
+
);
|
|
387
|
+
if (!shouldContinue) {
|
|
388
|
+
throw createError(
|
|
389
|
+
ErrorCode.DOCKER_PULL_FAILED,
|
|
390
|
+
"Docker image pull failed"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
saveSetupProgress(SetupStep.PULL_IMAGE, { failed: true });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Pull Docker image
|
|
400
|
+
*/
|
|
401
|
+
async pullDockerImage(imageName) {
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
try {
|
|
404
|
+
execSync(`docker pull ${imageName}`, {
|
|
405
|
+
stdio: "inherit",
|
|
406
|
+
});
|
|
407
|
+
resolve();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
reject(error);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Step 5: NPM Configuration
|
|
416
|
+
*/
|
|
417
|
+
async npmConfigStep(registrationData) {
|
|
418
|
+
displayStep(5, TOTAL_STEPS, "NPM Configuration");
|
|
419
|
+
|
|
420
|
+
const { npmToken } = registrationData;
|
|
421
|
+
|
|
422
|
+
if (npmToken) {
|
|
423
|
+
saveNpmToken(npmToken);
|
|
424
|
+
displaySuccess("NPM token configured for future updates");
|
|
425
|
+
saveSetupProgress(SetupStep.NPM_CONFIG, { configured: true });
|
|
426
|
+
} else {
|
|
427
|
+
displayWarning("No NPM token provided by backend");
|
|
428
|
+
saveSetupProgress(SetupStep.NPM_CONFIG, { configured: false });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Step 6: Start Agent (optional)
|
|
434
|
+
*/
|
|
435
|
+
async startAgentStep() {
|
|
436
|
+
displayStep(6, TOTAL_STEPS, "Start Agent Service");
|
|
437
|
+
|
|
438
|
+
const shouldStart = await promptConfirmation("Start the agent now?", true);
|
|
439
|
+
|
|
440
|
+
if (!shouldStart) {
|
|
441
|
+
displayInfo("You can start the agent later with: fenwave login");
|
|
442
|
+
saveSetupProgress(SetupStep.START_AGENT, { started: false });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
displayInfo("Starting agent requires authentication...");
|
|
447
|
+
displayInfo("Please run: fenwave login");
|
|
448
|
+
|
|
449
|
+
saveSetupProgress(SetupStep.START_AGENT, { started: false });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Display completion message
|
|
454
|
+
*/
|
|
455
|
+
displayComplete(registrationData) {
|
|
456
|
+
displayStep(7, TOTAL_STEPS, "Setup Complete! 🎉");
|
|
457
|
+
|
|
458
|
+
console.log("");
|
|
459
|
+
displayHeader("Setup Summary");
|
|
460
|
+
|
|
461
|
+
displayKeyValue({
|
|
462
|
+
"Device ID": registrationData.deviceId,
|
|
463
|
+
"Device Name": registrationData.deviceName || "N/A",
|
|
464
|
+
Platform: registrationData.platform || "N/A",
|
|
465
|
+
User: registrationData.userEntityRef || "N/A",
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
console.log("");
|
|
469
|
+
console.log(chalk.green.bold("✅ Setup completed successfully!\n"));
|
|
470
|
+
|
|
471
|
+
console.log(chalk.bold("Next Steps:\n"));
|
|
472
|
+
console.log(chalk.gray(" 1. Start the agent:"));
|
|
473
|
+
console.log(chalk.cyan(" $ fenwave login\n"));
|
|
474
|
+
console.log(chalk.gray(" 2. Check agent status:"));
|
|
475
|
+
console.log(chalk.cyan(" $ fenwave status\n"));
|
|
476
|
+
console.log(chalk.gray(" 3. View agent information:"));
|
|
477
|
+
console.log(chalk.cyan(" $ fenwave info\n"));
|
|
478
|
+
|
|
479
|
+
console.log(chalk.bold("Documentation:\n"));
|
|
480
|
+
console.log(
|
|
481
|
+
chalk.gray(" • View all commands: ") + chalk.cyan("fenwave --help")
|
|
482
|
+
);
|
|
483
|
+
console.log(
|
|
484
|
+
chalk.gray(" • Setup guide: ") + chalk.cyan("fenwave setup-guide")
|
|
485
|
+
);
|
|
486
|
+
console.log("");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Run setup wizard
|
|
492
|
+
*
|
|
493
|
+
* @param {Object} options - Wizard options
|
|
494
|
+
* @returns {Promise<void>}
|
|
495
|
+
*/
|
|
496
|
+
export async function runSetupWizard(options = {}) {
|
|
497
|
+
const wizard = new SetupWizard(options);
|
|
498
|
+
await wizard.run();
|
|
499
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Session Store
|
|
3
|
+
* Stores agent session information in memory
|
|
4
|
+
* This is a separate module to avoid circular dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Store agent session info (will be set when server starts)
|
|
8
|
+
let agentSessionInfo = {
|
|
9
|
+
agentId: null,
|
|
10
|
+
userEntityRef: null,
|
|
11
|
+
sessionExpiresAt: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set agent session info
|
|
16
|
+
* @param {object} sessionInfo - Session information
|
|
17
|
+
*/
|
|
18
|
+
function setAgentSessionInfo(sessionInfo) {
|
|
19
|
+
agentSessionInfo.agentId = sessionInfo.agentId || null;
|
|
20
|
+
agentSessionInfo.userEntityRef = sessionInfo.userEntityRef || null;
|
|
21
|
+
agentSessionInfo.sessionExpiresAt = sessionInfo.sessionExpiresAt || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get agent session info
|
|
26
|
+
* @returns {object} Agent session info
|
|
27
|
+
*/
|
|
28
|
+
function getAgentSessionInfo() {
|
|
29
|
+
return agentSessionInfo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clear agent session info
|
|
34
|
+
*/
|
|
35
|
+
function clearAgentSessionInfo() {
|
|
36
|
+
agentSessionInfo.agentId = null;
|
|
37
|
+
agentSessionInfo.userEntityRef = null;
|
|
38
|
+
agentSessionInfo.sessionExpiresAt = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
setAgentSessionInfo,
|
|
43
|
+
getAgentSessionInfo,
|
|
44
|
+
clearAgentSessionInfo,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
setAgentSessionInfo,
|
|
49
|
+
getAgentSessionInfo,
|
|
50
|
+
clearAgentSessionInfo,
|
|
51
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { loadConfig } from './configStore.js';
|
|
5
|
+
|
|
6
|
+
const fsPromises = fs.promises;
|
|
7
|
+
|
|
8
|
+
// Load configuration
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const AGENT_ROOT_DIR = config.agentRootDir;
|
|
11
|
+
const AGENT_INFO_DIR = 'agent';
|
|
12
|
+
const AGENT_INFO_FILE = 'info.json';
|
|
13
|
+
const AGENT_INFO_PATH = path.join(
|
|
14
|
+
os.homedir(),
|
|
15
|
+
AGENT_ROOT_DIR,
|
|
16
|
+
AGENT_INFO_DIR,
|
|
17
|
+
AGENT_INFO_FILE
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Agent Store for managing persistent agent information
|
|
22
|
+
*/
|
|
23
|
+
class AgentStore {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.agentInfo = null;
|
|
26
|
+
this.initialized = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the store
|
|
31
|
+
*/
|
|
32
|
+
async initialize() {
|
|
33
|
+
if (this.initialized) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Ensure directory exists
|
|
37
|
+
await fsPromises.mkdir(path.dirname(AGENT_INFO_PATH), { recursive: true });
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to initialize agent store:', error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save agent start time
|
|
47
|
+
*/
|
|
48
|
+
async saveAgentStartTime(startTime) {
|
|
49
|
+
await this.initialize();
|
|
50
|
+
|
|
51
|
+
const agentInfo = {
|
|
52
|
+
startTime: startTime.toISOString(),
|
|
53
|
+
lastUpdated: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await fsPromises.writeFile(AGENT_INFO_PATH, JSON.stringify(agentInfo, null, 2));
|
|
58
|
+
this.agentInfo = agentInfo;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to save agent start time:', error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load agent start time
|
|
67
|
+
*/
|
|
68
|
+
async loadAgentStartTime() {
|
|
69
|
+
await this.initialize();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const data = await fsPromises.readFile(AGENT_INFO_PATH, 'utf8');
|
|
73
|
+
const agentInfo = JSON.parse(data);
|
|
74
|
+
this.agentInfo = agentInfo;
|
|
75
|
+
return new Date(agentInfo.startTime);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code === 'ENOENT') {
|
|
78
|
+
// File doesn't exist - agent not running or first time
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
console.error('Failed to load agent start time:', error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear agent info (when agent stops)
|
|
88
|
+
*/
|
|
89
|
+
async clearAgentInfo() {
|
|
90
|
+
await this.initialize();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await fsPromises.unlink(AGENT_INFO_PATH);
|
|
94
|
+
this.agentInfo = null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.code !== 'ENOENT') {
|
|
97
|
+
console.error('Failed to clear agent info:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if agent is currently running based on stored info
|
|
104
|
+
*/
|
|
105
|
+
async isAgentRunning() {
|
|
106
|
+
const startTime = await this.loadAgentStartTime();
|
|
107
|
+
return startTime !== null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Export singleton instance
|
|
112
|
+
const agentStore = new AgentStore();
|
|
113
|
+
export default agentStore;
|