@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,885 @@
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.setupApplicationCommands = setupApplicationCommands;
40
+ const commander_1 = require("commander");
41
+ const inquirer_1 = __importDefault(require("inquirer"));
42
+ const chalk_1 = __importDefault(require("chalk"));
43
+ const child_process_1 = require("child_process");
44
+ const path = __importStar(require("path"));
45
+ const WorkloadDetector_1 = require("../detection/WorkloadDetector");
46
+ const command_wrapper_1 = require("./base/command-wrapper");
47
+ const container_1 = require("../di/container");
48
+ const types_1 = require("../di/types");
49
+ const config_validator_1 = require("./utils/config-validator");
50
+ const output_formatter_1 = require("./utils/output-formatter");
51
+ const input_parser_1 = require("./utils/input-parser");
52
+ const sudo_checker_1 = require("../utils/sudo-checker");
53
+ const docker_compose_parser_1 = require("../utils/docker-compose-parser");
54
+ /**
55
+ * Application command group
56
+ */
57
+ function setupApplicationCommands(program) {
58
+ const appCommand = program
59
+ .command('application')
60
+ .description('Manage applications')
61
+ .alias('app');
62
+ appCommand
63
+ .command('list')
64
+ .description('List all configured applications')
65
+ .option('--json', 'Output as JSON')
66
+ .alias('ls')
67
+ .action((0, command_wrapper_1.wrapCommand)(async (options) => {
68
+ const container = (0, container_1.getContainer)();
69
+ const logger = container.get(types_1.TYPES.Logger);
70
+ const configRepository = container.get(types_1.TYPES.ConfigRepository);
71
+ const applicationService = container.get(types_1.TYPES.ApplicationService);
72
+ (0, config_validator_1.validateConfig)(configRepository, {
73
+ requireAuth: true,
74
+ requireOrganization: true,
75
+ });
76
+ logger.debug('Getting applications');
77
+ const applications = await applicationService.getApplications();
78
+ if (applications.length === 0) {
79
+ console.log(chalk_1.default.yellow('⚠ No applications configured'));
80
+ console.log(chalk_1.default.gray('Use "edgible application create" to create an application'));
81
+ return;
82
+ }
83
+ if (options.json) {
84
+ console.log(JSON.stringify({
85
+ applications: applications.map((app) => ({
86
+ id: app.id,
87
+ name: app.name,
88
+ servingIp: app.servingIp,
89
+ port: app.port,
90
+ protocol: app.protocol,
91
+ status: app.status,
92
+ workloadId: app.workloadId,
93
+ createdAt: app.createdAt,
94
+ description: app.description || '',
95
+ })),
96
+ }, null, 2));
97
+ return;
98
+ }
99
+ console.log((0, output_formatter_1.formatList)(applications, (app, index) => `${index + 1}. ${(0, output_formatter_1.formatApplication)(app)}`, '📱 Configured Applications:', 'No applications configured'));
100
+ }, {
101
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
102
+ requireAuth: true,
103
+ requireOrganization: true,
104
+ }));
105
+ // Create parent command for application creation
106
+ const createCommand = appCommand
107
+ .command('create')
108
+ .description('Create a new application')
109
+ .alias('new');
110
+ // Existing workload subcommand
111
+ const createExistingCommand = new commander_1.Command('existing')
112
+ .description('Create application for an existing port that is open on the computer')
113
+ .addHelpText('after', `
114
+ Examples:
115
+ $ edgible application create existing --name myapp --port 3000 --device-id <serving-device> --gateway-ids <gw1,gw2>
116
+ $ edgible application create existing --name myapp --port 8080 --protocol https --device-id <serving-device> --gateway-ids <gw>
117
+ $ edgible application create existing --name myapp --port 3000 --https-upgrade --device-id <serving-device> --gateway-ids <gw>
118
+ $ edgible application create existing --no-interactive
119
+ `)
120
+ .option('-n, --name <name>', 'Application name')
121
+ .option('-d, --description <description>', 'Application description')
122
+ .option('-p, --port <port>', 'Port number', '3000')
123
+ .option('--protocol <protocol>', 'Protocol (http, https, tcp, udp)', 'http')
124
+ .option('--https-upgrade', 'Upgrade HTTP protocol to HTTPS automatically')
125
+ .option('--device-id <id>', 'Serving device ID')
126
+ .option('--gateway-ids <ids>', 'Comma-separated gateway device IDs')
127
+ .option('--hostnames <hostnames>', 'Comma-separated additional hostnames to route to this app')
128
+ .option('--no-interactive', 'Run in non-interactive mode')
129
+ .action((0, command_wrapper_1.wrapCommand)(async (options) => {
130
+ const container = (0, container_1.getContainer)();
131
+ const logger = container.get(types_1.TYPES.Logger);
132
+ const configRepository = container.get(types_1.TYPES.ConfigRepository);
133
+ const applicationService = container.get(types_1.TYPES.ApplicationService);
134
+ const gatewayService = container.get(types_1.TYPES.GatewayService);
135
+ const edgibleService = container.get(types_1.TYPES.EdgibleService);
136
+ (0, config_validator_1.validateConfig)(configRepository, {
137
+ requireAuth: true,
138
+ requireOrganization: true,
139
+ });
140
+ logger.debug('Creating application', { name: options.name });
141
+ // Name
142
+ let appName = options.name;
143
+ let useManagedGateway = false;
144
+ if (!appName && !options.noInteractive) {
145
+ const { name } = await inquirer_1.default.prompt([
146
+ {
147
+ type: 'input',
148
+ name: 'name',
149
+ message: 'Enter application name:',
150
+ validate: (i) => !!i.trim() || 'Application name is required',
151
+ },
152
+ ]);
153
+ appName = name.trim();
154
+ }
155
+ if (!appName) {
156
+ throw new Error('Application name is required');
157
+ }
158
+ // Description
159
+ let description = options.description || `Application for ${appName}`;
160
+ // Port (may be overridden by workload selection in interactive mode)
161
+ let port = (0, input_parser_1.parsePort)(options.port);
162
+ // Protocol
163
+ let protocolUnion = (0, input_parser_1.parseProtocol)(options.protocol);
164
+ // HTTPS upgrade: if --https-upgrade is set and protocol is http, change to https
165
+ if (options.httpsUpgrade && protocolUnion === 'http') {
166
+ protocolUnion = 'https';
167
+ }
168
+ // Device IDs (serving) and Gateway IDs
169
+ let deviceId = options.deviceId;
170
+ let gatewayIds = (0, input_parser_1.parseGatewayIds)(options.gatewayIds);
171
+ let hostnames = Array.isArray(options.hostnames)
172
+ ? options.hostnames
173
+ : (typeof options.hostnames === 'string' && options.hostnames.trim().length > 0
174
+ ? options.hostnames.split(',').map(s => s.trim()).filter(Boolean)
175
+ : []);
176
+ if (!options.noInteractive) {
177
+ // Prompt for HTTPS upgrade if protocol is http and --https-upgrade wasn't explicitly set
178
+ if (protocolUnion === 'http' && options.httpsUpgrade === undefined) {
179
+ const { upgradeToHttps } = await inquirer_1.default.prompt([
180
+ {
181
+ type: 'confirm',
182
+ name: 'upgradeToHttps',
183
+ message: 'Upgrade protocol from HTTP to HTTPS?',
184
+ default: false,
185
+ },
186
+ ]);
187
+ if (upgradeToHttps) {
188
+ protocolUnion = 'https';
189
+ }
190
+ }
191
+ // Optional: prompt for additional hostnames
192
+ const { addHostnames } = await inquirer_1.default.prompt([
193
+ {
194
+ type: 'input',
195
+ name: 'addHostnames',
196
+ message: 'Additional hostnames (comma-separated, optional):',
197
+ when: () => hostnames.length === 0,
198
+ },
199
+ ]);
200
+ if (addHostnames && typeof addHostnames === 'string') {
201
+ hostnames = addHostnames.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
202
+ }
203
+ // Prompt for managed gateway option
204
+ if (gatewayIds.length === 0) {
205
+ const { useManaged } = await inquirer_1.default.prompt([
206
+ {
207
+ type: 'confirm',
208
+ name: 'useManaged',
209
+ message: 'Use Edgible managed gateway?',
210
+ default: false,
211
+ },
212
+ ]);
213
+ useManagedGateway = useManaged;
214
+ if (!useManagedGateway) {
215
+ // Try to offer a list of gateways (via service), else fallback to manual input
216
+ try {
217
+ const gatewaysResp = await gatewayService.listGateways();
218
+ const gatewayChoices = gatewaysResp.gateways.map((g) => ({
219
+ name: `${g.device.name} (${g.device.id})`,
220
+ value: g.device.id,
221
+ }));
222
+ if (gatewayChoices.length > 0) {
223
+ const { selectedGateways } = await inquirer_1.default.prompt([
224
+ {
225
+ type: 'checkbox',
226
+ name: 'selectedGateways',
227
+ message: 'Select gateway device(s):',
228
+ choices: gatewayChoices,
229
+ validate: (vals) => vals.length > 0 ? true : 'Select at least one gateway',
230
+ },
231
+ ]);
232
+ gatewayIds = selectedGateways;
233
+ }
234
+ }
235
+ catch (error) {
236
+ logger.warn('Failed to list gateways', error);
237
+ }
238
+ if (gatewayIds.length === 0) {
239
+ const ans2 = await inquirer_1.default.prompt([
240
+ {
241
+ type: 'input',
242
+ name: 'gatewayIds',
243
+ message: 'Enter comma-separated gateway device IDs:',
244
+ validate: (v) => v && v.split(',').map((s) => s.trim()).filter(Boolean).length > 0
245
+ ? true
246
+ : 'At least one gateway ID is required',
247
+ },
248
+ ]);
249
+ gatewayIds = (0, input_parser_1.parseGatewayIds)(ans2.gatewayIds);
250
+ }
251
+ }
252
+ }
253
+ // Serving device selection - fetch available serving devices from organization
254
+ if (!deviceId) {
255
+ try {
256
+ const servingDevicesResp = await edgibleService.listServingDevices();
257
+ const servingDeviceChoices = Array.isArray(servingDevicesResp?.devices)
258
+ ? servingDevicesResp.devices.map((d) => ({
259
+ name: `${d.name || d.id}${d.description ? ` - ${d.description}` : ''}`,
260
+ value: d.id,
261
+ }))
262
+ : [];
263
+ if (servingDeviceChoices.length > 0) {
264
+ const { selectedDeviceId } = await inquirer_1.default.prompt([
265
+ {
266
+ type: 'list',
267
+ name: 'selectedDeviceId',
268
+ message: 'Select serving device:',
269
+ choices: servingDeviceChoices,
270
+ },
271
+ ]);
272
+ deviceId = selectedDeviceId;
273
+ }
274
+ else {
275
+ // Fallback to manual input if no serving devices found
276
+ const ans = await inquirer_1.default.prompt([
277
+ {
278
+ type: 'input',
279
+ name: 'deviceId',
280
+ message: 'Enter serving device ID:',
281
+ validate: (v) => v && v.trim().length > 0 ? true : 'Serving device ID is required',
282
+ },
283
+ ]);
284
+ deviceId = String(ans.deviceId).trim();
285
+ }
286
+ }
287
+ catch (error) {
288
+ logger.warn('Failed to list serving devices', error);
289
+ // Fallback to manual input if listing fails
290
+ const ans = await inquirer_1.default.prompt([
291
+ {
292
+ type: 'input',
293
+ name: 'deviceId',
294
+ message: 'Enter serving device ID:',
295
+ validate: (v) => v && v.trim().length > 0 ? true : 'Serving device ID is required',
296
+ },
297
+ ]);
298
+ deviceId = String(ans.deviceId).trim();
299
+ }
300
+ }
301
+ }
302
+ if (!deviceId) {
303
+ throw new Error('--device-id (serving) is required');
304
+ }
305
+ if (!useManagedGateway && gatewayIds.length === 0) {
306
+ throw new Error('--gateway-ids is required (at least one) or use --use-managed-gateway');
307
+ }
308
+ const subtype = 'local-preexisting';
309
+ // If interactive, allow choosing a local workload
310
+ if (!options.noInteractive) {
311
+ try {
312
+ const workloads = await WorkloadDetector_1.WorkloadDetector.detectWorkloads();
313
+ const running = workloads.filter((w) => w.status === 'running');
314
+ if (running.length > 0) {
315
+ const { selectedWorkloadId } = await inquirer_1.default.prompt([
316
+ {
317
+ type: 'list',
318
+ name: 'selectedWorkloadId',
319
+ message: 'Select local workload to associate:',
320
+ choices: running.map((w) => ({ name: `${w.name} (${w.type})`, value: w.id })),
321
+ },
322
+ ]);
323
+ // Attach to description so the backend can persist in configuration via service call
324
+ description = description || '';
325
+ description = `${description} [workloadId=${selectedWorkloadId}]`;
326
+ // If the selected workload exposes ports, allow user to choose one to override the app port
327
+ const selectedWorkload = running.find((w) => w.id === selectedWorkloadId);
328
+ const availablePorts = Array.isArray(selectedWorkload?.ports)
329
+ ? Array.from(new Set((selectedWorkload.ports || [])
330
+ .map((p) => (typeof p.hostPort === 'number' ? p.hostPort : p.containerPort))
331
+ .filter((n) => typeof n === 'number')))
332
+ : [];
333
+ if (availablePorts.length > 0) {
334
+ const portChoices = [
335
+ ...availablePorts.sort((a, b) => a - b).map((p) => ({ name: `${p}`, value: String(p) })),
336
+ { name: 'Enter a custom port', value: 'custom' },
337
+ ];
338
+ const { chosenPort } = await inquirer_1.default.prompt([
339
+ {
340
+ type: 'list',
341
+ name: 'chosenPort',
342
+ message: 'Select port to expose for this application:',
343
+ choices: portChoices,
344
+ default: String(availablePorts[0]),
345
+ },
346
+ ]);
347
+ if (chosenPort === 'custom') {
348
+ const { customPort } = await inquirer_1.default.prompt([
349
+ {
350
+ type: 'input',
351
+ name: 'customPort',
352
+ message: 'Enter custom port:',
353
+ validate: (v) => {
354
+ try {
355
+ (0, input_parser_1.parsePort)(v);
356
+ return true;
357
+ }
358
+ catch (e) {
359
+ return e.message;
360
+ }
361
+ },
362
+ },
363
+ ]);
364
+ port = (0, input_parser_1.parsePort)(customPort);
365
+ }
366
+ else {
367
+ port = (0, input_parser_1.parsePort)(chosenPort);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ catch (error) {
373
+ logger.debug('Failed to detect workloads', error);
374
+ }
375
+ }
376
+ logger.info('Creating application', {
377
+ name: appName,
378
+ port,
379
+ protocol: protocolUnion,
380
+ deviceId,
381
+ gatewayIds,
382
+ subtype,
383
+ });
384
+ console.log(chalk_1.default.blue(`\nCreating application: ${appName}`));
385
+ console.log(chalk_1.default.gray(`Port: ${port}`));
386
+ console.log(chalk_1.default.gray(`Protocol: ${protocolUnion.toUpperCase()}`));
387
+ console.log(chalk_1.default.gray(`Serving device: ${deviceId}`));
388
+ if (useManagedGateway) {
389
+ console.log(chalk_1.default.gray(`Using Edgible managed gateway`));
390
+ }
391
+ else {
392
+ console.log(chalk_1.default.gray(`Gateways: ${gatewayIds.join(', ')}`));
393
+ }
394
+ if (hostnames.length > 0) {
395
+ console.log(chalk_1.default.gray(`Additional hostnames: ${hostnames.join(', ')}`));
396
+ }
397
+ const result = await applicationService.createApplicationProgrammatically({
398
+ name: appName,
399
+ description,
400
+ port,
401
+ protocol: protocolUnion,
402
+ hostnames,
403
+ deviceIds: [deviceId],
404
+ gatewayIds: useManagedGateway ? undefined : gatewayIds,
405
+ useManagedGateway,
406
+ subtype,
407
+ });
408
+ console.log(chalk_1.default.green('\n✓ Application created successfully!'));
409
+ console.log(chalk_1.default.blue('\n📋 Application Details:'));
410
+ if (result && typeof result === 'object' && 'name' in result) {
411
+ const app = result;
412
+ console.log(chalk_1.default.white(` Name: ${app.name}`));
413
+ console.log(chalk_1.default.white(` ID: ${app.id}`));
414
+ if (app.url)
415
+ console.log(chalk_1.default.white(` URL: ${chalk_1.default.cyan.bold(app.url)}`));
416
+ console.log(chalk_1.default.white(` Status: ${app.status}`));
417
+ }
418
+ }, {
419
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
420
+ requireAuth: true,
421
+ requireOrganization: true,
422
+ }));
423
+ // Docker Compose subcommand
424
+ const createDockerComposeCommand = new commander_1.Command('docker-compose')
425
+ .description('Create application from docker-compose file (on device)')
426
+ .addHelpText('after', `
427
+ Examples:
428
+ $ edgible application create docker-compose --name myapp --compose-file ./docker-compose.yml
429
+ $ edgible application create docker-compose --name myapp
430
+ $ edgible application create docker-compose --no-interactive --name myapp --compose-file ./docker-compose.yml
431
+ `)
432
+ .option('-n, --name <name>', 'Application name')
433
+ .option('-d, --description <description>', 'Application description')
434
+ .option('--compose-file <file>', 'Path to docker-compose.yml file')
435
+ .option('--device-id <id>', 'Serving device ID')
436
+ .option('--gateway-ids <ids>', 'Comma-separated gateway device IDs')
437
+ .option('--hostnames <hostnames>', 'Comma-separated additional hostnames to route to this app')
438
+ .option('--no-interactive', 'Run in non-interactive mode')
439
+ .action((0, command_wrapper_1.wrapCommand)(async (options) => {
440
+ const container = (0, container_1.getContainer)();
441
+ const logger = container.get(types_1.TYPES.Logger);
442
+ const configRepository = container.get(types_1.TYPES.ConfigRepository);
443
+ const applicationService = container.get(types_1.TYPES.ApplicationService);
444
+ const gatewayService = container.get(types_1.TYPES.GatewayService);
445
+ const edgibleService = container.get(types_1.TYPES.EdgibleService);
446
+ (0, config_validator_1.validateConfig)(configRepository, {
447
+ requireAuth: true,
448
+ requireOrganization: true,
449
+ });
450
+ // Check sudo permissions
451
+ if (!(0, sudo_checker_1.checkSudoPermissions)()) {
452
+ console.log(chalk_1.default.yellow('⚠ Warning: Not running as sudo. Docker commands may fail without proper permissions.'));
453
+ }
454
+ // Find docker-compose file
455
+ let composeFilePath;
456
+ try {
457
+ composeFilePath = (0, docker_compose_parser_1.findDockerComposeFile)(options.composeFile);
458
+ }
459
+ catch (error) {
460
+ throw new Error(`Failed to find docker-compose file: ${error instanceof Error ? error.message : 'Unknown error'}`);
461
+ }
462
+ if (!composeFilePath) {
463
+ throw new Error('Docker compose file not found. Please provide --compose-file or ensure docker-compose.yml or docker-compose.yaml exists in the current directory.');
464
+ }
465
+ console.log(chalk_1.default.blue(`Using docker-compose file: ${composeFilePath}`));
466
+ console.log(chalk_1.default.gray('Note: Using Docker Compose v2 (docker compose command)'));
467
+ // Parse compose file
468
+ let composeData;
469
+ try {
470
+ composeData = (0, docker_compose_parser_1.parseDockerComposeFile)(composeFilePath);
471
+ }
472
+ catch (error) {
473
+ throw new Error(`Failed to parse docker-compose file: ${error instanceof Error ? error.message : 'Unknown error'}`);
474
+ }
475
+ if (composeData.containers.length === 0) {
476
+ throw new Error('No containers with exposed ports found in docker-compose file');
477
+ }
478
+ // Build port selection choices
479
+ const portChoices = [];
480
+ for (const containerInfo of composeData.containers) {
481
+ for (const portInfo of containerInfo.ports) {
482
+ portChoices.push({
483
+ name: `${containerInfo.name}:${portInfo.host}->${portInfo.container}/${portInfo.protocol}`,
484
+ value: { container: containerInfo.name, port: portInfo.host },
485
+ });
486
+ }
487
+ }
488
+ if (portChoices.length === 0) {
489
+ throw new Error('No ports found in docker-compose file');
490
+ }
491
+ // Get application name
492
+ let appName = options.name;
493
+ if (!appName && !options.noInteractive) {
494
+ const { name } = await inquirer_1.default.prompt([
495
+ {
496
+ type: 'input',
497
+ name: 'name',
498
+ message: 'Enter application name:',
499
+ validate: (i) => !!i.trim() || 'Application name is required',
500
+ },
501
+ ]);
502
+ appName = name.trim();
503
+ }
504
+ if (!appName) {
505
+ throw new Error('Application name is required');
506
+ }
507
+ // Select port
508
+ let selectedPort;
509
+ let selectedContainer;
510
+ if (!options.noInteractive) {
511
+ const { selected } = await inquirer_1.default.prompt([
512
+ {
513
+ type: 'list',
514
+ name: 'selected',
515
+ message: 'Select container and port to expose:',
516
+ choices: portChoices,
517
+ },
518
+ ]);
519
+ selectedPort = selected.port;
520
+ selectedContainer = selected.container;
521
+ }
522
+ else {
523
+ // In non-interactive mode, use first available port
524
+ selectedPort = portChoices[0].value.port;
525
+ selectedContainer = portChoices[0].value.container;
526
+ }
527
+ // Description
528
+ const description = options.description || `Application for ${appName} (docker-compose: ${selectedContainer})`;
529
+ // Device IDs and Gateway IDs
530
+ let deviceId = options.deviceId;
531
+ let gatewayIds = (0, input_parser_1.parseGatewayIds)(options.gatewayIds);
532
+ let hostnames = Array.isArray(options.hostnames)
533
+ ? options.hostnames
534
+ : (typeof options.hostnames === 'string' && options.hostnames.trim().length > 0
535
+ ? options.hostnames.split(',').map(s => s.trim()).filter(Boolean)
536
+ : []);
537
+ let useManagedGateway = false;
538
+ if (!options.noInteractive) {
539
+ // Optional: prompt for additional hostnames
540
+ const { addHostnames } = await inquirer_1.default.prompt([
541
+ {
542
+ type: 'input',
543
+ name: 'addHostnames',
544
+ message: 'Additional hostnames (comma-separated, optional):',
545
+ when: () => hostnames.length === 0,
546
+ },
547
+ ]);
548
+ if (addHostnames && typeof addHostnames === 'string') {
549
+ hostnames = addHostnames.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
550
+ }
551
+ // Prompt for managed gateway option
552
+ if (gatewayIds.length === 0) {
553
+ const { useManaged } = await inquirer_1.default.prompt([
554
+ {
555
+ type: 'confirm',
556
+ name: 'useManaged',
557
+ message: 'Use Edgible managed gateway?',
558
+ default: false,
559
+ },
560
+ ]);
561
+ useManagedGateway = useManaged;
562
+ if (!useManagedGateway) {
563
+ // Try to offer a list of gateways (via service), else fallback to manual input
564
+ try {
565
+ const gatewaysResp = await gatewayService.listGateways();
566
+ const gatewayChoices = gatewaysResp.gateways.map((g) => ({
567
+ name: `${g.device.name} (${g.device.id})`,
568
+ value: g.device.id,
569
+ }));
570
+ if (gatewayChoices.length > 0) {
571
+ const { selectedGateways } = await inquirer_1.default.prompt([
572
+ {
573
+ type: 'checkbox',
574
+ name: 'selectedGateways',
575
+ message: 'Select gateway device(s):',
576
+ choices: gatewayChoices,
577
+ validate: (vals) => vals.length > 0 ? true : 'Select at least one gateway',
578
+ },
579
+ ]);
580
+ gatewayIds = selectedGateways;
581
+ }
582
+ }
583
+ catch (error) {
584
+ logger.warn('Failed to list gateways', error);
585
+ }
586
+ if (gatewayIds.length === 0) {
587
+ const ans2 = await inquirer_1.default.prompt([
588
+ {
589
+ type: 'input',
590
+ name: 'gatewayIds',
591
+ message: 'Enter comma-separated gateway device IDs:',
592
+ validate: (v) => v && v.split(',').map((s) => s.trim()).filter(Boolean).length > 0
593
+ ? true
594
+ : 'At least one gateway ID is required',
595
+ },
596
+ ]);
597
+ gatewayIds = (0, input_parser_1.parseGatewayIds)(ans2.gatewayIds);
598
+ }
599
+ }
600
+ }
601
+ // Serving device selection
602
+ if (!deviceId) {
603
+ try {
604
+ const servingDevicesResp = await edgibleService.listServingDevices();
605
+ const servingDeviceChoices = Array.isArray(servingDevicesResp?.devices)
606
+ ? servingDevicesResp.devices.map((d) => ({
607
+ name: `${d.name || d.id}${d.description ? ` - ${d.description}` : ''}`,
608
+ value: d.id,
609
+ }))
610
+ : [];
611
+ if (servingDeviceChoices.length > 0) {
612
+ const { selectedDeviceId } = await inquirer_1.default.prompt([
613
+ {
614
+ type: 'list',
615
+ name: 'selectedDeviceId',
616
+ message: 'Select serving device:',
617
+ choices: servingDeviceChoices,
618
+ },
619
+ ]);
620
+ deviceId = selectedDeviceId;
621
+ }
622
+ else {
623
+ // Fallback to manual input if no serving devices found
624
+ const ans = await inquirer_1.default.prompt([
625
+ {
626
+ type: 'input',
627
+ name: 'deviceId',
628
+ message: 'Enter serving device ID:',
629
+ validate: (v) => v && v.trim().length > 0 ? true : 'Serving device ID is required',
630
+ },
631
+ ]);
632
+ deviceId = String(ans.deviceId).trim();
633
+ }
634
+ }
635
+ catch (error) {
636
+ logger.warn('Failed to list serving devices', error);
637
+ // Fallback to manual input if listing fails
638
+ const ans = await inquirer_1.default.prompt([
639
+ {
640
+ type: 'input',
641
+ name: 'deviceId',
642
+ message: 'Enter serving device ID:',
643
+ validate: (v) => v && v.trim().length > 0 ? true : 'Serving device ID is required',
644
+ },
645
+ ]);
646
+ deviceId = String(ans.deviceId).trim();
647
+ }
648
+ }
649
+ }
650
+ if (!deviceId) {
651
+ throw new Error('--device-id (serving) is required');
652
+ }
653
+ if (!useManagedGateway && gatewayIds.length === 0) {
654
+ throw new Error('--gateway-ids is required (at least one) or use --use-managed-gateway');
655
+ }
656
+ // Run docker compose up
657
+ console.log(chalk_1.default.blue(`\nRunning docker compose up...`));
658
+ try {
659
+ const composeDir = path.dirname(composeFilePath);
660
+ const composeFileName = path.basename(composeFilePath);
661
+ (0, child_process_1.execSync)(`docker compose -f ${composeFileName} up -d`, {
662
+ cwd: composeDir,
663
+ stdio: 'inherit',
664
+ });
665
+ console.log(chalk_1.default.green('✓ Docker compose up completed successfully'));
666
+ }
667
+ catch (error) {
668
+ console.log(chalk_1.default.red('✗ Docker compose up failed'));
669
+ throw new Error(`Failed to run docker compose up: ${error instanceof Error ? error.message : 'Unknown error'}`);
670
+ }
671
+ // Create application
672
+ logger.info('Creating application', {
673
+ name: appName,
674
+ port: selectedPort,
675
+ protocol: 'https',
676
+ deviceId,
677
+ gatewayIds,
678
+ subtype: 'docker-compose',
679
+ });
680
+ console.log(chalk_1.default.blue(`\nCreating application: ${appName}`));
681
+ console.log(chalk_1.default.gray(`Port: ${selectedPort}`));
682
+ console.log(chalk_1.default.gray(`Protocol: HTTPS`));
683
+ console.log(chalk_1.default.gray(`Container: ${selectedContainer}`));
684
+ console.log(chalk_1.default.gray(`Serving device: ${deviceId}`));
685
+ if (useManagedGateway) {
686
+ console.log(chalk_1.default.gray(`Using Edgible managed gateway`));
687
+ }
688
+ else {
689
+ console.log(chalk_1.default.gray(`Gateways: ${gatewayIds.join(', ')}`));
690
+ }
691
+ if (hostnames.length > 0) {
692
+ console.log(chalk_1.default.gray(`Additional hostnames: ${hostnames.join(', ')}`));
693
+ }
694
+ const result = await applicationService.createApplicationProgrammatically({
695
+ name: appName,
696
+ description,
697
+ port: selectedPort,
698
+ protocol: 'https',
699
+ hostnames,
700
+ deviceIds: [deviceId],
701
+ gatewayIds: useManagedGateway ? undefined : gatewayIds,
702
+ useManagedGateway,
703
+ subtype: 'docker-compose',
704
+ });
705
+ console.log(chalk_1.default.green('\n✓ Application created successfully!'));
706
+ console.log(chalk_1.default.blue('\n📋 Application Details:'));
707
+ if (result && typeof result === 'object' && 'name' in result) {
708
+ const app = result;
709
+ console.log(chalk_1.default.white(` Name: ${app.name}`));
710
+ console.log(chalk_1.default.white(` ID: ${app.id}`));
711
+ if (app.url)
712
+ console.log(chalk_1.default.white(` URL: ${chalk_1.default.cyan.bold(app.url)}`));
713
+ console.log(chalk_1.default.white(` Status: ${app.status}`));
714
+ }
715
+ }, {
716
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
717
+ requireAuth: true,
718
+ requireOrganization: true,
719
+ }));
720
+ // Stub subcommands for unimplemented workload types
721
+ const createDockerCommand = new commander_1.Command('docker')
722
+ .description('Create application from Docker container (not implemented)')
723
+ .action((0, command_wrapper_1.wrapCommand)(async () => {
724
+ console.log(chalk_1.default.yellow('This workload type is currently not implemented.'));
725
+ }, {
726
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
727
+ requireAuth: false,
728
+ requireOrganization: false,
729
+ }));
730
+ const createQemuCommand = new commander_1.Command('qemu')
731
+ .description('Create application from QEMU VM (not implemented)')
732
+ .action((0, command_wrapper_1.wrapCommand)(async () => {
733
+ console.log(chalk_1.default.yellow('This workload type is currently not implemented.'));
734
+ }, {
735
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
736
+ requireAuth: false,
737
+ requireOrganization: false,
738
+ }));
739
+ const createPodmanCommand = new commander_1.Command('podman')
740
+ .description('Create application from Podman container (not implemented)')
741
+ .action((0, command_wrapper_1.wrapCommand)(async () => {
742
+ console.log(chalk_1.default.yellow('This workload type is currently not implemented.'));
743
+ }, {
744
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
745
+ requireAuth: false,
746
+ requireOrganization: false,
747
+ }));
748
+ // Add all subcommands to create command
749
+ createCommand.addCommand(createExistingCommand);
750
+ createCommand.addCommand(createDockerComposeCommand);
751
+ createCommand.addCommand(createDockerCommand);
752
+ createCommand.addCommand(createQemuCommand);
753
+ createCommand.addCommand(createPodmanCommand);
754
+ appCommand
755
+ .command('get')
756
+ .description('Get status of a specific application')
757
+ .option('-i, --app-id <id>', 'Application ID')
758
+ .option('--json', 'Output as JSON')
759
+ .alias('status')
760
+ .alias('info')
761
+ .action((0, command_wrapper_1.wrapCommand)(async (options) => {
762
+ const container = (0, container_1.getContainer)();
763
+ const logger = container.get(types_1.TYPES.Logger);
764
+ const configRepository = container.get(types_1.TYPES.ConfigRepository);
765
+ const applicationService = container.get(types_1.TYPES.ApplicationService);
766
+ (0, config_validator_1.validateConfig)(configRepository, {
767
+ requireAuth: true,
768
+ requireOrganization: true,
769
+ });
770
+ if (!options.appId) {
771
+ throw new Error('--app-id is required. Usage: edgible application get --app-id <id> [--json]');
772
+ }
773
+ logger.debug('Getting application', { appId: options.appId });
774
+ const app = await applicationService.getApplication(options.appId);
775
+ if (options.json) {
776
+ console.log(JSON.stringify({
777
+ id: app.id,
778
+ name: app.name,
779
+ description: app.description || '',
780
+ url: app.url || '',
781
+ servingIp: app.servingIp,
782
+ port: app.port,
783
+ protocol: app.protocol,
784
+ status: app.status,
785
+ workloadId: app.workloadId || '',
786
+ createdAt: app.createdAt,
787
+ }, null, 2));
788
+ }
789
+ else {
790
+ console.log((0, output_formatter_1.formatApplication)(app));
791
+ }
792
+ }, {
793
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
794
+ requireAuth: true,
795
+ requireOrganization: true,
796
+ }));
797
+ appCommand
798
+ .command('update')
799
+ .description('Update an existing application')
800
+ .option('-i, --app-id <id>', 'Application ID')
801
+ .action(async (options) => {
802
+ console.log(chalk_1.default.yellow('Not yet implemented.'));
803
+ });
804
+ appCommand
805
+ .command('delete')
806
+ .description('Delete an application')
807
+ .option('-i, --app-id <id>', 'Application ID')
808
+ .option('-f, --force', 'Skip confirmation prompt')
809
+ .option('--no-interactive', 'Run in non-interactive mode')
810
+ .alias('rm')
811
+ .action((0, command_wrapper_1.wrapCommand)(async (options) => {
812
+ const container = (0, container_1.getContainer)();
813
+ const logger = container.get(types_1.TYPES.Logger);
814
+ const configRepository = container.get(types_1.TYPES.ConfigRepository);
815
+ const applicationService = container.get(types_1.TYPES.ApplicationService);
816
+ (0, config_validator_1.validateConfig)(configRepository, {
817
+ requireAuth: true,
818
+ requireOrganization: true,
819
+ });
820
+ let appId = options.appId;
821
+ // Interactive mode: prompt for application if not provided
822
+ if (!appId && options.interactive !== false) {
823
+ logger.debug('Fetching applications for interactive selection');
824
+ const applications = await applicationService.getApplications();
825
+ if (applications.length === 0) {
826
+ console.log(chalk_1.default.yellow('⚠ No applications configured'));
827
+ console.log(chalk_1.default.gray('Use "edgible application create" to create an application'));
828
+ return;
829
+ }
830
+ const choices = applications.map((app) => ({
831
+ name: `${app.name} (${app.id})${app.description ? ` - ${app.description}` : ''}`,
832
+ value: app.id,
833
+ }));
834
+ const { selectedAppId } = await inquirer_1.default.prompt([
835
+ {
836
+ type: 'list',
837
+ name: 'selectedAppId',
838
+ message: 'Select application to delete:',
839
+ choices,
840
+ },
841
+ ]);
842
+ appId = selectedAppId;
843
+ }
844
+ if (!appId) {
845
+ throw new Error('--app-id is required in non-interactive mode. Usage: edgible application delete --app-id <id>');
846
+ }
847
+ logger.debug('Deleting application', { appId });
848
+ // Get application details for confirmation
849
+ let appName = appId;
850
+ let app;
851
+ try {
852
+ app = await applicationService.getApplication(appId);
853
+ appName = app.name;
854
+ }
855
+ catch (error) {
856
+ logger.debug('Could not fetch application details for confirmation', error);
857
+ }
858
+ // Confirm deletion unless --force is used
859
+ if (!options.force) {
860
+ const confirmMessage = app
861
+ ? `Are you sure you want to delete application "${appName}" (${appId})?`
862
+ : `Are you sure you want to delete application ${appId}?`;
863
+ const { confirm } = await inquirer_1.default.prompt([
864
+ {
865
+ type: 'confirm',
866
+ name: 'confirm',
867
+ message: confirmMessage,
868
+ default: false,
869
+ },
870
+ ]);
871
+ if (!confirm) {
872
+ logger.info('Application deletion cancelled');
873
+ console.log(chalk_1.default.gray('Deletion cancelled'));
874
+ return;
875
+ }
876
+ }
877
+ await applicationService.deleteApplication(appId);
878
+ console.log(chalk_1.default.green('\n✓ Application deleted successfully!'));
879
+ }, {
880
+ configRepository: (0, container_1.getContainer)().get(types_1.TYPES.ConfigRepository),
881
+ requireAuth: true,
882
+ requireOrganization: true,
883
+ }));
884
+ }
885
+ //# sourceMappingURL=application.js.map