@edgible-team/cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +136 -0
  2. package/README.md +450 -0
  3. package/dist/client/api-client.js +1057 -0
  4. package/dist/client/index.js +21 -0
  5. package/dist/commands/agent.js +1280 -0
  6. package/dist/commands/ai.js +608 -0
  7. package/dist/commands/application.js +885 -0
  8. package/dist/commands/auth.js +570 -0
  9. package/dist/commands/base/BaseCommand.js +93 -0
  10. package/dist/commands/base/CommandHandler.js +7 -0
  11. package/dist/commands/base/command-wrapper.js +58 -0
  12. package/dist/commands/base/middleware.js +77 -0
  13. package/dist/commands/config.js +116 -0
  14. package/dist/commands/connectivity.js +59 -0
  15. package/dist/commands/debug.js +98 -0
  16. package/dist/commands/discover.js +144 -0
  17. package/dist/commands/examples/migrated-command-example.js +180 -0
  18. package/dist/commands/gateway.js +494 -0
  19. package/dist/commands/managedGateway.js +787 -0
  20. package/dist/commands/utils/config-validator.js +76 -0
  21. package/dist/commands/utils/gateway-prompt.js +79 -0
  22. package/dist/commands/utils/input-parser.js +120 -0
  23. package/dist/commands/utils/output-formatter.js +109 -0
  24. package/dist/config/app-config.js +99 -0
  25. package/dist/detection/SystemCapabilityDetector.js +1244 -0
  26. package/dist/detection/ToolDetector.js +305 -0
  27. package/dist/detection/WorkloadDetector.js +314 -0
  28. package/dist/di/bindings.js +99 -0
  29. package/dist/di/container.js +88 -0
  30. package/dist/di/types.js +32 -0
  31. package/dist/index.js +52 -0
  32. package/dist/interfaces/IDaemonManager.js +3 -0
  33. package/dist/repositories/config-repository.js +62 -0
  34. package/dist/repositories/gateway-repository.js +35 -0
  35. package/dist/scripts/postinstall.js +101 -0
  36. package/dist/services/AgentStatusManager.js +299 -0
  37. package/dist/services/ConnectivityTester.js +271 -0
  38. package/dist/services/DependencyInstaller.js +475 -0
  39. package/dist/services/LocalAgentManager.js +2216 -0
  40. package/dist/services/application/ApplicationService.js +299 -0
  41. package/dist/services/auth/AuthService.js +214 -0
  42. package/dist/services/aws.js +644 -0
  43. package/dist/services/daemon/DaemonManagerFactory.js +65 -0
  44. package/dist/services/daemon/DockerDaemonManager.js +395 -0
  45. package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
  46. package/dist/services/daemon/PodmanDaemonManager.js +369 -0
  47. package/dist/services/daemon/SystemdDaemonManager.js +221 -0
  48. package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
  49. package/dist/services/daemon/index.js +16 -0
  50. package/dist/services/edgible.js +3060 -0
  51. package/dist/services/gateway/GatewayService.js +334 -0
  52. package/dist/state/config.js +146 -0
  53. package/dist/types/AgentConfig.js +5 -0
  54. package/dist/types/AgentStatus.js +5 -0
  55. package/dist/types/ApiClient.js +5 -0
  56. package/dist/types/ApiRequests.js +5 -0
  57. package/dist/types/ApiResponses.js +5 -0
  58. package/dist/types/Application.js +5 -0
  59. package/dist/types/CaddyJson.js +5 -0
  60. package/dist/types/UnifiedAgentStatus.js +56 -0
  61. package/dist/types/WireGuard.js +5 -0
  62. package/dist/types/Workload.js +5 -0
  63. package/dist/types/agent.js +5 -0
  64. package/dist/types/command-options.js +5 -0
  65. package/dist/types/connectivity.js +5 -0
  66. package/dist/types/errors.js +250 -0
  67. package/dist/types/gateway-types.js +5 -0
  68. package/dist/types/index.js +48 -0
  69. package/dist/types/models/ApplicationData.js +5 -0
  70. package/dist/types/models/CertificateData.js +5 -0
  71. package/dist/types/models/DeviceData.js +5 -0
  72. package/dist/types/models/DevicePoolData.js +5 -0
  73. package/dist/types/models/OrganizationData.js +5 -0
  74. package/dist/types/models/OrganizationInviteData.js +5 -0
  75. package/dist/types/models/ProviderConfiguration.js +5 -0
  76. package/dist/types/models/ResourceData.js +5 -0
  77. package/dist/types/models/ServiceResourceData.js +5 -0
  78. package/dist/types/models/UserData.js +5 -0
  79. package/dist/types/route.js +5 -0
  80. package/dist/types/validation/schemas.js +218 -0
  81. package/dist/types/validation.js +5 -0
  82. package/dist/utils/FileIntegrityManager.js +256 -0
  83. package/dist/utils/PathMigration.js +219 -0
  84. package/dist/utils/PathResolver.js +235 -0
  85. package/dist/utils/PlatformDetector.js +277 -0
  86. package/dist/utils/console-logger.js +130 -0
  87. package/dist/utils/docker-compose-parser.js +179 -0
  88. package/dist/utils/errors.js +130 -0
  89. package/dist/utils/health-checker.js +155 -0
  90. package/dist/utils/json-logger.js +72 -0
  91. package/dist/utils/log-formatter.js +293 -0
  92. package/dist/utils/logger.js +59 -0
  93. package/dist/utils/network-utils.js +217 -0
  94. package/dist/utils/output.js +182 -0
  95. package/dist/utils/passwordValidation.js +91 -0
  96. package/dist/utils/progress.js +167 -0
  97. package/dist/utils/sudo-checker.js +22 -0
  98. package/dist/utils/urls.js +32 -0
  99. package/dist/utils/validation.js +31 -0
  100. package/dist/validation/schemas.js +175 -0
  101. package/dist/validation/validator.js +67 -0
  102. package/package.json +83 -0
@@ -0,0 +1,644 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.AWSService = void 0;
40
+ const client_ec2_1 = require("@aws-sdk/client-ec2");
41
+ const client_s3_1 = require("@aws-sdk/client-s3");
42
+ const client_ssm_1 = require("@aws-sdk/client-ssm");
43
+ const credential_provider_node_1 = require("@aws-sdk/credential-provider-node");
44
+ const ssh2_1 = require("ssh2");
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const os = __importStar(require("os"));
48
+ const chalk_1 = __importDefault(require("chalk"));
49
+ class AWSService {
50
+ constructor(profile, region = 'us-east-1') {
51
+ this.profile = profile || 'default';
52
+ this.region = region;
53
+ const credentials = (0, credential_provider_node_1.defaultProvider)({
54
+ profile: this.profile
55
+ });
56
+ this.ec2Client = new client_ec2_1.EC2Client({
57
+ region: this.region,
58
+ credentials
59
+ });
60
+ this.s3Client = new client_s3_1.S3Client({
61
+ region: this.region,
62
+ credentials
63
+ });
64
+ this.ssmClient = new client_ssm_1.SSMClient({
65
+ region: this.region,
66
+ credentials
67
+ });
68
+ }
69
+ /**
70
+ * Check if AWS CLI is available and get available profiles
71
+ */
72
+ async checkAWSCLI() {
73
+ try {
74
+ const { execSync } = require('child_process');
75
+ const output = execSync('aws configure list-profiles', { encoding: 'utf8', timeout: 5000 });
76
+ const profiles = output.trim().split('\n').filter((p) => p.trim());
77
+ return { available: true, profiles };
78
+ }
79
+ catch (error) {
80
+ return { available: false, profiles: [] };
81
+ }
82
+ }
83
+ /**
84
+ * Validate AWS credentials for a profile
85
+ */
86
+ async validateCredentials(profile) {
87
+ try {
88
+ const testClient = new client_ec2_1.EC2Client({
89
+ region: this.region,
90
+ credentials: (0, credential_provider_node_1.defaultProvider)({
91
+ profile: profile || this.profile
92
+ })
93
+ });
94
+ await testClient.send(new client_ec2_1.DescribeInstancesCommand({ MaxResults: 5 }));
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ console.error(chalk_1.default.red('AWS credentials validation failed:'), error);
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * Create a key pair for EC2 instance
104
+ */
105
+ async createKeyPair(keyName) {
106
+ try {
107
+ const command = new client_ec2_1.CreateKeyPairCommand({
108
+ KeyName: keyName,
109
+ TagSpecifications: [{
110
+ ResourceType: 'key-pair',
111
+ Tags: [{
112
+ Key: 'Purpose',
113
+ Value: 'Edgible-Gateway'
114
+ }]
115
+ }]
116
+ });
117
+ const response = await this.ec2Client.send(command);
118
+ if (!response.KeyMaterial) {
119
+ throw new Error('Key material not returned from AWS');
120
+ }
121
+ return {
122
+ keyName: response.KeyName,
123
+ keyFingerprint: response.KeyFingerprint,
124
+ privateKey: response.KeyMaterial,
125
+ publicKey: '' // Will be generated from private key if needed
126
+ };
127
+ }
128
+ catch (error) {
129
+ console.error(chalk_1.default.red('Error creating key pair:'), error);
130
+ throw error;
131
+ }
132
+ }
133
+ /**
134
+ * Check if key pair exists
135
+ */
136
+ async keyPairExists(keyName) {
137
+ try {
138
+ const command = new client_ec2_1.DescribeKeyPairsCommand({
139
+ KeyNames: [keyName]
140
+ });
141
+ await this.ec2Client.send(command);
142
+ return true;
143
+ }
144
+ catch (error) {
145
+ return false;
146
+ }
147
+ }
148
+ /**
149
+ * Create EC2 instance for gateway
150
+ */
151
+ async createEC2Instance(config) {
152
+ try {
153
+ const userData = config.userData || this.generateUserData();
154
+ // Get or create security group with SSH access
155
+ const securityGroupId = await this.getOrCreateSSHSecurityGroup();
156
+ const command = new client_ec2_1.RunInstancesCommand({
157
+ ImageId: 'ami-0c462b53550d4fca8', // Amazon Linux 2 AMI for ap-southeast-2
158
+ MinCount: 1,
159
+ MaxCount: 1,
160
+ InstanceType: (config.instanceType || 't3.micro'),
161
+ KeyName: config.keyPairName,
162
+ SecurityGroupIds: config.securityGroupIds || [securityGroupId],
163
+ SubnetId: config.subnetId,
164
+ UserData: Buffer.from(userData).toString('base64'),
165
+ TagSpecifications: [{
166
+ ResourceType: 'instance',
167
+ Tags: [{
168
+ Key: 'Name',
169
+ Value: config.name
170
+ }, {
171
+ Key: 'Purpose',
172
+ Value: 'Edgible-Gateway'
173
+ }]
174
+ }]
175
+ });
176
+ const response = await this.ec2Client.send(command);
177
+ const instance = response.Instances?.[0];
178
+ if (!instance?.InstanceId) {
179
+ throw new Error('Failed to create EC2 instance');
180
+ }
181
+ // Wait for instance to be running
182
+ await this.waitForInstanceRunning(instance.InstanceId);
183
+ // Get instance details
184
+ const instanceDetails = await this.getInstanceDetails(instance.InstanceId);
185
+ return {
186
+ instanceId: instance.InstanceId,
187
+ publicIp: instanceDetails.publicIp,
188
+ privateIp: instanceDetails.privateIp,
189
+ state: instanceDetails.state,
190
+ keyPairName: config.keyPairName,
191
+ region: this.region,
192
+ launchTime: instance.LaunchTime || new Date()
193
+ };
194
+ }
195
+ catch (error) {
196
+ console.error(chalk_1.default.red('Error creating EC2 instance:'), error);
197
+ throw error;
198
+ }
199
+ }
200
+ /**
201
+ * Get instance details
202
+ */
203
+ async getInstanceDetails(instanceId) {
204
+ const command = new client_ec2_1.DescribeInstancesCommand({
205
+ InstanceIds: [instanceId]
206
+ });
207
+ const response = await this.ec2Client.send(command);
208
+ const instance = response.Reservations?.[0]?.Instances?.[0];
209
+ if (!instance) {
210
+ throw new Error('Instance not found');
211
+ }
212
+ return {
213
+ publicIp: instance.PublicIpAddress || '',
214
+ privateIp: instance.PrivateIpAddress || '',
215
+ state: instance.State?.Name || 'unknown'
216
+ };
217
+ }
218
+ /**
219
+ * Wait for instance to be running
220
+ */
221
+ async waitForInstanceRunning(instanceId, maxWaitTime = 300000) {
222
+ const startTime = Date.now();
223
+ while (Date.now() - startTime < maxWaitTime) {
224
+ try {
225
+ const details = await this.getInstanceDetails(instanceId);
226
+ if (details.state === 'running') {
227
+ return;
228
+ }
229
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
230
+ }
231
+ catch (error) {
232
+ await new Promise(resolve => setTimeout(resolve, 10000));
233
+ }
234
+ }
235
+ throw new Error('Instance did not start within expected time');
236
+ }
237
+ /**
238
+ * Terminate EC2 instance
239
+ */
240
+ async terminateInstance(instanceId) {
241
+ try {
242
+ const command = new client_ec2_1.TerminateInstancesCommand({
243
+ InstanceIds: [instanceId]
244
+ });
245
+ await this.ec2Client.send(command);
246
+ }
247
+ catch (error) {
248
+ console.error(chalk_1.default.red('Error terminating instance:'), error);
249
+ throw error;
250
+ }
251
+ }
252
+ /**
253
+ * Check if instance is ready for SSH connections
254
+ */
255
+ async waitForSSHReady(instanceId, maxWaitTime = 300) {
256
+ console.log(chalk_1.default.gray('Waiting for instance to be ready for SSH...'));
257
+ const startTime = Date.now();
258
+ while (Date.now() - startTime < maxWaitTime * 1000) {
259
+ try {
260
+ const instanceDetails = await this.getInstanceDetails(instanceId);
261
+ if (instanceDetails.state === 'running' && instanceDetails.publicIp) {
262
+ console.log(chalk_1.default.green(`✓ Instance ready: ${instanceDetails.publicIp}`));
263
+ return true;
264
+ }
265
+ }
266
+ catch (error) {
267
+ // Continue waiting
268
+ }
269
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
270
+ }
271
+ return false;
272
+ }
273
+ /**
274
+ * Create or get security group with SSH access
275
+ */
276
+ async getOrCreateSSHSecurityGroup() {
277
+ const groupName = 'edgible-gateway-ssh';
278
+ try {
279
+ // Check if security group already exists
280
+ const describeCommand = new client_ec2_1.DescribeSecurityGroupsCommand({
281
+ GroupNames: [groupName]
282
+ });
283
+ const describeResponse = await this.ec2Client.send(describeCommand);
284
+ if (describeResponse.SecurityGroups && describeResponse.SecurityGroups.length > 0) {
285
+ const groupId = describeResponse.SecurityGroups[0].GroupId;
286
+ console.log(chalk_1.default.gray(`Using existing security group: ${groupId}`));
287
+ return groupId;
288
+ }
289
+ }
290
+ catch (error) {
291
+ // Security group doesn't exist, create it
292
+ }
293
+ // Create security group
294
+ console.log(chalk_1.default.gray('Creating security group with SSH access...'));
295
+ const createCommand = new client_ec2_1.CreateSecurityGroupCommand({
296
+ GroupName: groupName,
297
+ Description: 'Security group for Edgible Gateway with SSH access'
298
+ });
299
+ const createResponse = await this.ec2Client.send(createCommand);
300
+ const groupId = createResponse.GroupId;
301
+ if (!groupId) {
302
+ throw new Error('Failed to create security group');
303
+ }
304
+ // Add SSH rule
305
+ const authorizeCommand = new client_ec2_1.AuthorizeSecurityGroupIngressCommand({
306
+ GroupId: groupId,
307
+ IpPermissions: [{
308
+ IpProtocol: 'tcp',
309
+ FromPort: 22,
310
+ ToPort: 22,
311
+ IpRanges: [{ CidrIp: '0.0.0.0/0' }]
312
+ }]
313
+ });
314
+ await this.ec2Client.send(authorizeCommand);
315
+ console.log(chalk_1.default.green(`✓ Security group created: ${groupId}`));
316
+ return groupId;
317
+ }
318
+ /**
319
+ * Delete key pair
320
+ */
321
+ async deleteKeyPair(keyName) {
322
+ try {
323
+ const command = new client_ec2_1.DeleteKeyPairCommand({
324
+ KeyName: keyName
325
+ });
326
+ await this.ec2Client.send(command);
327
+ }
328
+ catch (error) {
329
+ console.error(chalk_1.default.red('Error deleting key pair:'), error);
330
+ throw error;
331
+ }
332
+ }
333
+ /**
334
+ * Execute SSH command on instance
335
+ */
336
+ async executeSSHCommand(connection, command, timeout = 600000) {
337
+ return new Promise((resolve, reject) => {
338
+ const conn = new ssh2_1.Client();
339
+ let commandExecuted = false;
340
+ // Set overall timeout for command execution (default 10 minutes)
341
+ const timeoutId = setTimeout(() => {
342
+ if (!commandExecuted) {
343
+ conn.end();
344
+ reject(new Error(`Command execution timeout after ${timeout / 1000} seconds`));
345
+ }
346
+ }, timeout);
347
+ conn.on('ready', () => {
348
+ conn.exec(command, (err, stream) => {
349
+ if (err) {
350
+ clearTimeout(timeoutId);
351
+ conn.end();
352
+ reject(err);
353
+ return;
354
+ }
355
+ let stdout = '';
356
+ let stderr = '';
357
+ stream.on('close', (code) => {
358
+ commandExecuted = true;
359
+ clearTimeout(timeoutId);
360
+ conn.end();
361
+ resolve({ stdout, stderr, exitCode: code });
362
+ });
363
+ stream.on('data', (data) => {
364
+ stdout += data.toString();
365
+ });
366
+ stream.stderr.on('data', (data) => {
367
+ stderr += data.toString();
368
+ });
369
+ });
370
+ });
371
+ conn.on('error', (err) => {
372
+ clearTimeout(timeoutId);
373
+ reject(err);
374
+ });
375
+ conn.connect({
376
+ host: connection.host,
377
+ port: connection.port,
378
+ username: connection.username,
379
+ privateKey: connection.privateKey,
380
+ readyTimeout: 30000, // 30 seconds
381
+ keepaliveInterval: 10000, // 10 seconds
382
+ keepaliveCountMax: 3
383
+ });
384
+ });
385
+ }
386
+ /**
387
+ * Upload file via SSH
388
+ */
389
+ async uploadFile(connection, localPath, remotePath) {
390
+ return new Promise((resolve, reject) => {
391
+ // Verify local file exists before attempting upload
392
+ if (!require('fs').existsSync(localPath)) {
393
+ reject(new Error(`Local file does not exist: ${localPath}`));
394
+ return;
395
+ }
396
+ const fs = require('fs');
397
+ const stats = fs.statSync(localPath);
398
+ const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
399
+ const conn = new ssh2_1.Client();
400
+ let sftpConnected = false;
401
+ let uploadStarted = false;
402
+ conn.on('ready', () => {
403
+ conn.sftp((err, sftp) => {
404
+ if (err) {
405
+ conn.end();
406
+ reject(new Error(`SFTP connection failed: ${err.message || String(err)}`));
407
+ return;
408
+ }
409
+ sftpConnected = true;
410
+ // Check if remote directory exists and is writable
411
+ const remoteDir = require('path').dirname(remotePath);
412
+ sftp.stat(remoteDir, (statErr) => {
413
+ if (statErr) {
414
+ conn.end();
415
+ reject(new Error(`Remote directory does not exist or is not accessible: ${remoteDir} (Error: ${statErr.message || String(statErr)}, Code: ${statErr.code || 'unknown'})`));
416
+ return;
417
+ }
418
+ uploadStarted = true;
419
+ // Attempt the upload with detailed error handling
420
+ sftp.fastPut(localPath, remotePath, (uploadErr) => {
421
+ conn.end();
422
+ if (uploadErr) {
423
+ const errorMessage = `SFTP upload failed: ${uploadErr.message || String(uploadErr)}`;
424
+ const errorCode = uploadErr.code ? ` (Code: ${uploadErr.code})` : '';
425
+ const contextInfo = `\n Local: ${localPath} (${fileSizeMB} MB)\n Remote: ${remotePath}\n User: ${connection.username}\n Host: ${connection.host}`;
426
+ reject(new Error(`${errorMessage}${errorCode}${contextInfo}`));
427
+ }
428
+ else {
429
+ resolve();
430
+ }
431
+ });
432
+ });
433
+ });
434
+ });
435
+ conn.on('error', (err) => {
436
+ reject(new Error(`SSH connection error: ${err.message || String(err)}`));
437
+ });
438
+ // Set a timeout for the entire operation
439
+ const timeout = setTimeout(() => {
440
+ if (!sftpConnected) {
441
+ conn.end();
442
+ reject(new Error(`SFTP connection timeout after 30 seconds. Upload may be too large (${fileSizeMB} MB)`));
443
+ }
444
+ else if (!uploadStarted) {
445
+ conn.end();
446
+ reject(new Error(`SFTP upload start timeout. Remote directory may be inaccessible: ${require('path').dirname(remotePath)}`));
447
+ }
448
+ }, 60000); // 60 second timeout
449
+ conn.on('close', () => {
450
+ clearTimeout(timeout);
451
+ });
452
+ conn.connect({
453
+ host: connection.host,
454
+ port: connection.port,
455
+ username: connection.username,
456
+ privateKey: connection.privateKey,
457
+ readyTimeout: 30000, // 30 seconds
458
+ keepaliveInterval: 10000, // 10 seconds
459
+ keepaliveCountMax: 3
460
+ });
461
+ });
462
+ }
463
+ /**
464
+ * Generate user data script for EC2 instance
465
+ */
466
+ generateUserData() {
467
+ return `#!/bin/bash
468
+ # Update system
469
+ yum update -y
470
+
471
+ # Install Node.js
472
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
473
+ export NVM_DIR="$HOME/.nvm"
474
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
475
+ nvm install 18
476
+ nvm use 18
477
+
478
+ # Create system-wide symlink for Node.js (so root can access it)
479
+ NODE_VERSION=$(nvm list | grep -oE 'v18\.[0-9]+\.[0-9]+' | head -1)
480
+ if [ -n "$NODE_VERSION" ]; then
481
+ sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/node" /usr/local/bin/node
482
+ sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/npm" /usr/local/bin/npm
483
+ fi
484
+
485
+ # Install PM2 for process management (install globally for root as well)
486
+ npm install -g pm2
487
+ sudo npm install -g pm2 || true
488
+
489
+ # Create directory for agent (owned by root)
490
+ sudo mkdir -p /opt/edgible-agent
491
+ cd /opt/edgible-agent
492
+ sudo chown root:root /opt/edgible-agent
493
+
494
+ # Note: Agent code will be uploaded by CLI
495
+ echo "Agent directory created. Waiting for agent code upload..."
496
+
497
+ # Set up systemd service for agent (running as root)
498
+ cat > /tmp/edgible-agent.service << 'EOF'
499
+ [Unit]
500
+ Description=Edgible Agent
501
+ After=network.target
502
+
503
+ [Service]
504
+ Type=simple
505
+ User=root
506
+ WorkingDirectory=/opt/edgible-agent
507
+ ExecStart=/usr/local/bin/node /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
508
+ Restart=always
509
+ RestartSec=10
510
+ Environment=NODE_ENV=production
511
+ Environment=EDGIBLE_CONFIG_PATH=/opt/edgible-agent/.edgible/agent
512
+ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
513
+
514
+ [Install]
515
+ WantedBy=multi-user.target
516
+ EOF
517
+ sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service
518
+
519
+ # Enable service (but don't start until agent is uploaded)
520
+ systemctl enable edgible-agent.service
521
+
522
+ echo "Edgible Agent setup complete"
523
+ `;
524
+ }
525
+ /**
526
+ * Save SSH key to file
527
+ */
528
+ saveSSHKey(keyPair, keyName) {
529
+ const sshDir = path.join(os.homedir(), '.ssh');
530
+ const keyPath = path.join(sshDir, `${keyName}.pem`);
531
+ // Ensure .ssh directory exists
532
+ if (!fs.existsSync(sshDir)) {
533
+ fs.mkdirSync(sshDir, { mode: 0o700 });
534
+ }
535
+ // Save private key
536
+ fs.writeFileSync(keyPath, keyPair.privateKey, { mode: 0o600 });
537
+ return keyPath;
538
+ }
539
+ /**
540
+ * Load SSH key from file
541
+ */
542
+ loadSSHKey(keyPath) {
543
+ if (!fs.existsSync(keyPath)) {
544
+ throw new Error(`SSH key file not found: ${keyPath}`);
545
+ }
546
+ return fs.readFileSync(keyPath, 'utf8');
547
+ }
548
+ /**
549
+ * Download agent from S3 bucket
550
+ * @param bucketName S3 bucket name
551
+ * @param keyPath S3 object key (path within bucket)
552
+ * @param destinationPath Local file path to save the downloaded file
553
+ * @param useCloudFront Whether to use CloudFront URL instead of direct S3
554
+ * @param cloudFrontUrl CloudFront distribution URL (e.g., https://distribution.edgible.com)
555
+ */
556
+ async downloadAgentFromS3(bucketName, keyPath, destinationPath, useCloudFront = false, cloudFrontUrl) {
557
+ try {
558
+ if (useCloudFront && cloudFrontUrl) {
559
+ // Use CloudFront URL for download (public access)
560
+ const url = `${cloudFrontUrl}/${keyPath}`;
561
+ console.log(chalk_1.default.gray(`Downloading from CloudFront: ${url}`));
562
+ const response = await fetch(url);
563
+ if (!response.ok) {
564
+ // Provide specific error messages for common issues
565
+ let errorMessage = `Failed to download from CloudFront: ${response.status} ${response.statusText}`;
566
+ if (response.status === 404) {
567
+ errorMessage = `Agent version not found at ${url}\n\n` +
568
+ `The agent may not have been deployed to S3 yet.\n` +
569
+ `To deploy the agent:\n` +
570
+ ` 1. Navigate to agent-v2 directory\n` +
571
+ ` 2. Run: npm run build\n` +
572
+ ` 3. Run: ./scripts/deploy-to-s3.sh --env production\n\n` +
573
+ `Expected S3 path: s3://${bucketName}/${keyPath}\n` +
574
+ `Current environment: ${keyPath.split('/')[0] || 'unknown'}`;
575
+ }
576
+ else if (response.status === 403) {
577
+ errorMessage = `Access denied to ${url}\n\n` +
578
+ `This usually means one of:\n` +
579
+ ` 1. Agent not deployed: The file doesn't exist at s3://${bucketName}/${keyPath}\n` +
580
+ ` → Deploy agent: cd agent-v2 && ./scripts/deploy-to-s3.sh --env production\n\n` +
581
+ ` 2. CloudFront misconfiguration: OAC or bucket policy issue\n` +
582
+ ` → Check backend Pulumi outputs for distribution status\n\n` +
583
+ ` 3. Wrong bucket: Using bucket "${bucketName}" but file may be in different bucket\n` +
584
+ ` → Check Pulumi output: AgentDistributionBucket\n\n` +
585
+ `To verify file exists:\n` +
586
+ ` aws s3 ls s3://${bucketName}/${keyPath} --profile edgible`;
587
+ }
588
+ throw new Error(errorMessage);
589
+ }
590
+ const buffer = await response.arrayBuffer();
591
+ fs.writeFileSync(destinationPath, Buffer.from(buffer));
592
+ console.log(chalk_1.default.green(`✓ Downloaded to ${destinationPath}`));
593
+ }
594
+ else {
595
+ // Use S3 SDK for direct download
596
+ console.log(chalk_1.default.gray(`Downloading from S3: s3://${bucketName}/${keyPath}`));
597
+ const command = new client_s3_1.GetObjectCommand({
598
+ Bucket: bucketName,
599
+ Key: keyPath
600
+ });
601
+ const response = await this.s3Client.send(command);
602
+ if (!response.Body) {
603
+ throw new Error('No data returned from S3');
604
+ }
605
+ // Ensure destination directory exists
606
+ const destDir = path.dirname(destinationPath);
607
+ if (!fs.existsSync(destDir)) {
608
+ fs.mkdirSync(destDir, { recursive: true });
609
+ }
610
+ // Convert stream to buffer and write to file
611
+ const chunks = [];
612
+ for await (const chunk of response.Body) {
613
+ chunks.push(chunk);
614
+ }
615
+ const buffer = Buffer.concat(chunks);
616
+ fs.writeFileSync(destinationPath, buffer);
617
+ console.log(chalk_1.default.green(`✓ Downloaded to ${destinationPath}`));
618
+ }
619
+ }
620
+ catch (error) {
621
+ console.error(chalk_1.default.red('Error downloading from S3:'), error);
622
+ throw error;
623
+ }
624
+ }
625
+ /**
626
+ * Get S3 download URL (for use in SSH commands on remote server)
627
+ * @param bucketName S3 bucket name
628
+ * @param keyPath S3 object key
629
+ * @param useCloudFront Whether to use CloudFront URL
630
+ * @param cloudFrontUrl CloudFront distribution URL
631
+ * @returns URL string for downloading
632
+ */
633
+ getS3DownloadUrl(bucketName, keyPath, useCloudFront = false, cloudFrontUrl) {
634
+ if (useCloudFront && cloudFrontUrl) {
635
+ return `${cloudFrontUrl}/${keyPath}`;
636
+ }
637
+ else {
638
+ // Return S3 public URL (requires bucket to be public or presigned URL)
639
+ return `https://${bucketName}.s3.${this.region}.amazonaws.com/${keyPath}`;
640
+ }
641
+ }
642
+ }
643
+ exports.AWSService = AWSService;
644
+ //# sourceMappingURL=aws.js.map