@aifabrix/builder 2.32.3 → 2.33.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.
Files changed (123) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/types/wizard.types.js +176 -38
  40. package/lib/api/wizard.api.js +161 -23
  41. package/lib/app/deploy.js +116 -54
  42. package/lib/app/display.js +6 -5
  43. package/lib/app/dockerfile.js +2 -1
  44. package/lib/app/list.js +17 -10
  45. package/lib/app/readme.js +41 -112
  46. package/lib/app/register.js +44 -9
  47. package/lib/app/rotate-secret.js +48 -31
  48. package/lib/cli.js +219 -70
  49. package/lib/commands/app.js +4 -9
  50. package/lib/commands/auth-config.js +125 -0
  51. package/lib/commands/auth-status.js +7 -8
  52. package/lib/commands/datasource.js +3 -6
  53. package/lib/commands/login-credentials.js +4 -4
  54. package/lib/commands/login-device.js +26 -17
  55. package/lib/commands/login.js +12 -10
  56. package/lib/commands/wizard-config-normalizer.js +92 -0
  57. package/lib/commands/wizard-core.js +515 -0
  58. package/lib/commands/wizard-dataplane.js +122 -0
  59. package/lib/commands/wizard-headless.js +115 -0
  60. package/lib/commands/wizard.js +110 -332
  61. package/lib/core/config.js +46 -0
  62. package/lib/core/secrets.js +3 -22
  63. package/lib/core/templates-env.js +1 -1
  64. package/lib/datasource/deploy.js +29 -21
  65. package/lib/datasource/list.js +8 -6
  66. package/lib/deployment/deployer.js +25 -0
  67. package/lib/deployment/environment.js +10 -13
  68. package/lib/external-system/delete.js +151 -0
  69. package/lib/external-system/deploy.js +53 -378
  70. package/lib/external-system/download-helpers.js +45 -65
  71. package/lib/external-system/download.js +33 -13
  72. package/lib/external-system/generator.js +11 -7
  73. package/lib/external-system/test-auth.js +4 -3
  74. package/lib/generator/builders.js +3 -1
  75. package/lib/generator/external-controller-manifest.js +157 -0
  76. package/lib/generator/external-schema-utils.js +236 -0
  77. package/lib/generator/external.js +55 -3
  78. package/lib/generator/index.js +22 -10
  79. package/lib/generator/wizard-prompts.js +33 -10
  80. package/lib/generator/wizard.js +69 -86
  81. package/lib/infrastructure/compose.js +100 -0
  82. package/lib/infrastructure/helpers.js +139 -0
  83. package/lib/infrastructure/index.js +52 -311
  84. package/lib/infrastructure/services.js +168 -0
  85. package/lib/schema/application-schema.json +23 -4
  86. package/lib/schema/external-datasource.schema.json +2 -2
  87. package/lib/schema/wizard-config.schema.json +234 -0
  88. package/lib/utils/api.js +32 -50
  89. package/lib/utils/app-existence.js +42 -0
  90. package/lib/utils/app-register-config.js +7 -2
  91. package/lib/utils/auth-config-validator.js +92 -0
  92. package/lib/utils/command-header.js +43 -0
  93. package/lib/utils/compose-generator.js +113 -70
  94. package/lib/utils/controller-url.js +65 -17
  95. package/lib/utils/dataplane-health.js +115 -0
  96. package/lib/utils/dataplane-resolver.js +29 -0
  97. package/lib/utils/dev-config.js +6 -2
  98. package/lib/utils/env-copy.js +2 -1
  99. package/lib/utils/env-ports.js +2 -1
  100. package/lib/utils/env-template.js +1 -1
  101. package/lib/utils/error-formatter.js +49 -0
  102. package/lib/utils/external-readme.js +125 -0
  103. package/lib/utils/help-builder.js +190 -0
  104. package/lib/utils/infra-status.js +13 -3
  105. package/lib/utils/paths.js +17 -2
  106. package/lib/utils/port-resolver.js +111 -0
  107. package/lib/utils/secrets-helpers.js +3 -15
  108. package/lib/utils/secrets-utils.js +2 -2
  109. package/lib/utils/token-manager.js +9 -4
  110. package/lib/utils/variable-transformer.js +7 -2
  111. package/lib/validation/external-manifest-validator.js +202 -0
  112. package/lib/validation/validate-display.js +406 -0
  113. package/lib/validation/validate.js +159 -123
  114. package/lib/validation/validator.js +36 -3
  115. package/lib/validation/wizard-config-validator.js +267 -0
  116. package/package.json +4 -2
  117. package/templates/applications/README.md.hbs +18 -16
  118. package/templates/applications/miso-controller/env.template +1 -1
  119. package/templates/applications/miso-controller/rbac.yaml +7 -7
  120. package/templates/external-system/README.md.hbs +99 -0
  121. package/templates/infra/compose.yaml.hbs +35 -0
  122. package/templates/python/docker-compose.hbs +26 -0
  123. package/templates/typescript/docker-compose.hbs +26 -0
@@ -44,6 +44,7 @@ const {
44
44
  } = require('../utils/secrets-utils');
45
45
  const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
46
46
  const pathsUtil = require('../utils/paths');
47
+ const { getContainerPortFromPath } = require('../utils/port-resolver');
47
48
 
48
49
  /**
49
50
  * Generates a canonical secret name from an environment variable key.
@@ -253,26 +254,6 @@ function applyDockerEnvOverride(dockerEnv) {
253
254
  return dockerEnv;
254
255
  }
255
256
 
256
- /**
257
- * Gets container port from variables.yaml
258
- * @function getContainerPortFromVariables
259
- * @param {string} variablesPath - Path to variables.yaml
260
- * @returns {number|null} Container port or null
261
- */
262
- function getContainerPortFromVariables(variablesPath) {
263
- if (!variablesPath || !fs.existsSync(variablesPath)) {
264
- return null;
265
- }
266
- try {
267
- const variablesContent = fs.readFileSync(variablesPath, 'utf8');
268
- const variables = yaml.load(variablesContent);
269
- // Use containerPort if specified, otherwise use base port (no developer-id offset)
270
- return variables?.build?.containerPort || variables?.port || null;
271
- } catch {
272
- return null;
273
- }
274
- }
275
-
276
257
  /**
277
258
  * Gets container port from docker environment config
278
259
  * @function getContainerPortFromDockerEnv
@@ -305,7 +286,7 @@ async function updatePortForDocker(resolved, variablesPath) {
305
286
  dockerEnv = applyDockerEnvOverride(dockerEnv);
306
287
 
307
288
  // Step 3: Get PORT value for container (should be container port, NOT host port)
308
- let containerPort = getContainerPortFromVariables(variablesPath);
289
+ let containerPort = getContainerPortFromPath(variablesPath);
309
290
  if (containerPort === null) {
310
291
  containerPort = getContainerPortFromDockerEnv(dockerEnv);
311
292
  }
@@ -454,7 +435,7 @@ async function generateAdminSecretsEnv(secretsPath) {
454
435
 
455
436
  const adminSecrets = `# Infrastructure Admin Credentials
456
437
  POSTGRES_PASSWORD=${postgresPassword}
457
- PGADMIN_DEFAULT_EMAIL=admin@aifabrix.ai
438
+ PGADMIN_DEFAULT_EMAIL=admin@aifabrix.dev
458
439
  PGADMIN_DEFAULT_PASSWORD=${postgresPassword}
459
440
  REDIS_HOST=local:redis:6379:0:
460
441
  REDIS_COMMANDER_USER=admin
@@ -104,7 +104,7 @@ function buildMonitoringEnv(config) {
104
104
  }
105
105
 
106
106
  return {
107
- 'MISO_CONTROLLER_URL': config.controllerUrl || 'https://controller.aifabrix.ai',
107
+ 'MISO_CONTROLLER_URL': config.controllerUrl || 'https://controller.aifabrix.dev',
108
108
  'MISO_ENVIRONMENT': 'dev',
109
109
  'MISO_CLIENTID': 'kv://miso-controller-client-idKeyVault',
110
110
  'MISO_CLIENTSECRET': 'kv://miso-controller-client-secretKeyVault',
@@ -48,9 +48,10 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
48
48
  application.configuration?.dataplaneUrl;
49
49
 
50
50
  if (!dataplaneUrl) {
51
- logger.error(chalk.red('❌ Dataplane URL not found in application response'));
52
- logger.error(chalk.gray('\nApplication response:'));
53
- logger.error(chalk.gray(JSON.stringify(application, null, 2)));
51
+ const appType = application.configuration?.type || application.type;
52
+ if (appType === 'external') {
53
+ throw new Error('Dataplane URL not found for external system. Provide --dataplane <url>.');
54
+ }
54
55
  throw new Error('Dataplane URL not found in application configuration');
55
56
  }
56
57
 
@@ -64,19 +65,13 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
64
65
  * @param {Object} options - Options
65
66
  * @throws {Error} If validation fails
66
67
  */
67
- function validateDeploymentInputs(appKey, filePath, options) {
68
+ function validateDeploymentInputs(appKey, filePath) {
68
69
  if (!appKey || typeof appKey !== 'string') {
69
70
  throw new Error('Application key is required');
70
71
  }
71
72
  if (!filePath || typeof filePath !== 'string') {
72
73
  throw new Error('File path is required');
73
74
  }
74
- if (!options.controller) {
75
- throw new Error('Controller URL is required (--controller)');
76
- }
77
- if (!options.environment) {
78
- throw new Error('Environment is required (-e, --environment)');
79
- }
80
75
  }
81
76
 
82
77
  /**
@@ -112,15 +107,18 @@ async function validateAndLoadDatasourceFile(filePath) {
112
107
  * @param {string} controllerUrl - Controller URL
113
108
  * @param {string} environment - Environment key
114
109
  * @param {string} appKey - Application key
110
+ * @param {Object} [options] - Options
111
+ * @param {string} [options.dataplane] - Dataplane URL override
115
112
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
116
113
  */
117
114
  async function setupDeploymentAuth(controllerUrl, environment, appKey) {
115
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
118
116
  logger.log(chalk.blue('🔐 Getting authentication...'));
119
117
  const authConfig = await getDeploymentAuth(controllerUrl, environment, appKey);
120
118
  logger.log(chalk.green('✓ Authentication successful'));
121
119
 
122
- logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
123
- const dataplaneUrl = await getDataplaneUrl(controllerUrl, appKey, environment, authConfig);
120
+ logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
121
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
124
122
  logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
125
123
 
126
124
  return { authConfig, dataplaneUrl };
@@ -165,20 +163,30 @@ function displayDeploymentResults(datasourceConfig, systemKey, environment) {
165
163
  }
166
164
 
167
165
  /**
168
- * Deploys datasource to dataplane
166
+ * Deploys datasource to dataplane.
167
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
169
168
  *
170
169
  * @async
171
170
  * @function deployDatasource
172
171
  * @param {string} appKey - Application key
173
172
  * @param {string} filePath - Path to datasource JSON file
174
- * @param {Object} options - Deployment options
175
- * @param {string} options.controller - Controller URL
176
- * @param {string} options.environment - Environment key
173
+ * @param {Object} [_options] - Deployment options (reserved)
177
174
  * @returns {Promise<Object>} Deployment result
178
175
  * @throws {Error} If deployment fails
179
176
  */
180
- async function deployDatasource(appKey, filePath, options) {
181
- validateDeploymentInputs(appKey, filePath, options);
177
+ async function deployDatasource(appKey, filePath, _options) {
178
+ const { resolveControllerUrl } = require('../utils/controller-url');
179
+ const { resolveEnvironment } = require('../core/config');
180
+ const { displayCommandHeader } = require('../utils/command-header');
181
+
182
+ validateDeploymentInputs(appKey, filePath);
183
+
184
+ // Resolve controller and environment from config
185
+ const controllerUrl = await resolveControllerUrl();
186
+ const environment = await resolveEnvironment();
187
+
188
+ // Display command header
189
+ displayCommandHeader(controllerUrl, environment);
182
190
 
183
191
  logger.log(chalk.blue('📋 Deploying datasource...\n'));
184
192
 
@@ -192,19 +200,19 @@ async function deployDatasource(appKey, filePath, options) {
192
200
  }
193
201
 
194
202
  // Setup authentication and get dataplane URL
195
- const { authConfig, dataplaneUrl } = await setupDeploymentAuth(options.controller, options.environment, appKey);
203
+ const { authConfig, dataplaneUrl } = await setupDeploymentAuth(controllerUrl, environment, appKey);
196
204
 
197
205
  // Publish to dataplane
198
206
  await publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig);
199
207
 
200
208
  // Display results
201
- displayDeploymentResults(datasourceConfig, systemKey, options.environment);
209
+ displayDeploymentResults(datasourceConfig, systemKey, environment);
202
210
 
203
211
  return {
204
212
  success: true,
205
213
  datasourceKey: datasourceConfig.key,
206
214
  systemKey: systemKey,
207
- environment: options.environment,
215
+ environment: environment,
208
216
  dataplaneUrl: dataplaneUrl
209
217
  };
210
218
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const { getConfig } = require('../core/config');
12
+ const { getConfig, resolveEnvironment } = require('../core/config');
13
13
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
14
  const { listEnvironmentDatasources } = require('../api/environments.api');
15
15
  const { formatApiError } = require('../utils/api-error-handler');
@@ -130,8 +130,7 @@ function displayDatasources(datasources, environment) {
130
130
  *
131
131
  * @async
132
132
  * @function listDatasources
133
- * @param {Object} options - Command options
134
- * @param {string} options.environment - Environment ID or key
133
+ * @param {Object} _options - Command options (unused, kept for compatibility)
135
134
  * @throws {Error} If listing fails
136
135
  */
137
136
  /**
@@ -190,9 +189,12 @@ function handleDatasourceApiError(response) {
190
189
  process.exit(1);
191
190
  }
192
191
 
193
- async function listDatasources(options) {
192
+ async function listDatasources(_options) {
194
193
  const config = await getConfig();
195
194
 
195
+ // Resolve environment from config.yaml (no flags)
196
+ const environment = await resolveEnvironment();
197
+
196
198
  // Try to get device token
197
199
  const authInfo = await getDeviceTokenFromConfig(config);
198
200
  validateDatasourceListingAuth(authInfo?.token, authInfo?.controllerUrl);
@@ -204,7 +206,7 @@ async function listDatasources(options) {
204
206
  return; // Never reached, but satisfies linter
205
207
  }
206
208
  const authConfig = { type: 'bearer', token: authInfo.token };
207
- const response = await listEnvironmentDatasources(authInfo.controllerUrl, options.environment, authConfig);
209
+ const response = await listEnvironmentDatasources(authInfo.controllerUrl, environment, authConfig);
208
210
 
209
211
  if (!response.success || !response.data) {
210
212
  handleDatasourceApiError(response);
@@ -212,7 +214,7 @@ async function listDatasources(options) {
212
214
  }
213
215
 
214
216
  const datasources = extractDatasources(response);
215
- displayDatasources(datasources, options.environment);
217
+ displayDatasources(datasources, environment);
216
218
  }
217
219
 
218
220
  module.exports = {
@@ -52,6 +52,27 @@ async function buildValidationData(manifest, validatedEnvKey, authConfig, option
52
52
  return { validationData, pipelineAuthConfig };
53
53
  }
54
54
 
55
+ /**
56
+ * Handle authentication errors during validation
57
+ * @param {Error} error - Error object
58
+ * @param {string} appKey - Application key
59
+ * @throws {Error} Enhanced authentication error
60
+ */
61
+ function handleValidationAuthError(error, appKey) {
62
+ if (error.status === 401 || (error.response && error.response.status === 401)) {
63
+ const authError = new Error(
64
+ `Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
65
+ 'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
66
+ '💡 If the application already exists, rotate the secret:\n' +
67
+ ` aifabrix app rotate-secret ${appKey}\n\n` +
68
+ '💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
69
+ );
70
+ authError.status = 401;
71
+ authError.originalError = error;
72
+ throw authError;
73
+ }
74
+ }
75
+
55
76
  /**
56
77
  * Validates deployment configuration via validate endpoint
57
78
  * This is the first step in the deployment process
@@ -79,6 +100,10 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
79
100
  return handleValidationResponse(response);
80
101
  } catch (error) {
81
102
  lastError = error;
103
+
104
+ // Handle authentication errors (401) - credentials are invalid, not missing
105
+ handleValidationAuthError(error, manifest.key);
106
+
82
107
  const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
83
108
  if (shouldRetry) {
84
109
  const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
@@ -12,6 +12,7 @@
12
12
  const chalk = require('chalk');
13
13
  const logger = require('../utils/logger');
14
14
  const config = require('../core/config');
15
+ const { resolveControllerUrl } = require('../utils/controller-url');
15
16
  const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
16
17
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
17
18
  const { getEnvironmentStatus } = require('../api/environments.api');
@@ -266,27 +267,19 @@ function displayDeploymentResults(result) {
266
267
  * @throws {Error} If deployment fails
267
268
  *
268
269
  * @example
269
- * await deployEnvironment('dev', { controller: 'https://controller.aifabrix.ai' });
270
+ * await deployEnvironment('dev', { controller: 'https://controller.aifabrix.dev' });
270
271
  */
271
272
  /**
272
- * Validates deployment input parameters
273
+ * Validates deployment input parameters (environment key only).
274
+ * Controller URL is resolved from config.yaml via resolveControllerUrl().
273
275
  * @function validateDeploymentInput
274
276
  * @param {string} envKey - Environment key
275
- * @param {Object} options - Deployment options
276
- * @returns {string} Controller URL
277
277
  * @throws {Error} If validation fails
278
278
  */
279
- function validateDeploymentInput(envKey, options) {
279
+ function validateDeploymentInput(envKey) {
280
280
  if (!envKey || typeof envKey !== 'string' || envKey.trim().length === 0) {
281
281
  throw new Error('Environment key is required');
282
282
  }
283
-
284
- const controllerUrl = options.controller || options['controller-url'];
285
- if (!controllerUrl) {
286
- throw new Error('Controller URL is required. Use --controller flag');
287
- }
288
-
289
- return controllerUrl;
290
283
  }
291
284
 
292
285
  /**
@@ -365,7 +358,11 @@ async function pollDeploymentStatusIfEnabled(result, validatedControllerUrl, env
365
358
 
366
359
  async function deployEnvironment(envKey, options = {}) {
367
360
  try {
368
- const controllerUrl = validateDeploymentInput(envKey, options);
361
+ validateDeploymentInput(envKey);
362
+ const controllerUrl = await resolveControllerUrl();
363
+ if (!controllerUrl) {
364
+ throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
365
+ }
369
366
  const authConfig = await prepareEnvironmentDeployment(envKey, controllerUrl, options);
370
367
 
371
368
  const validatedControllerUrl = validateControllerUrl(authConfig.controller);
@@ -0,0 +1,151 @@
1
+ /**
2
+ * External System Delete Module
3
+ *
4
+ * Deletes external systems from dataplane and confirms before removal.
5
+ *
6
+ * @fileoverview External system delete functionality for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const chalk = require('chalk');
14
+ const inquirer = require('inquirer');
15
+ const logger = require('../utils/logger');
16
+ const { getDeploymentAuth } = require('../utils/token-manager');
17
+ const { resolveControllerUrl } = require('../utils/controller-url');
18
+ const { getExternalSystemConfig, deleteExternalSystem } = require('../api/external-systems.api');
19
+
20
+ /**
21
+ * Validates system key format
22
+ * @param {string} systemKey - System key to validate
23
+ */
24
+ function validateSystemKey(systemKey) {
25
+ if (!systemKey || typeof systemKey !== 'string') {
26
+ throw new Error('System key is required and must be a string');
27
+ }
28
+ if (!/^[a-z0-9-_]+$/.test(systemKey)) {
29
+ throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Gets dataplane URL and authentication configuration
35
+ * @async
36
+ * @param {string} systemKey - System key
37
+ * @param {Object} options - Command options
38
+ * @returns {Promise<Object>} Auth and dataplane details
39
+ */
40
+ async function getAuthAndDataplane(systemKey, _options) {
41
+ const { resolveEnvironment } = require('../core/config');
42
+ const environment = await resolveEnvironment();
43
+ const controllerUrl = await resolveControllerUrl();
44
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
45
+
46
+ if (!authConfig.token && !authConfig.clientId) {
47
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
48
+ }
49
+
50
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
51
+ logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
52
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
53
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
54
+
55
+ return { authConfig, dataplaneUrl, environment, controllerUrl };
56
+ }
57
+
58
+ /**
59
+ * Fetches external system configuration for warning display
60
+ * @async
61
+ * @param {string} dataplaneUrl - Dataplane URL
62
+ * @param {string} systemKey - System key
63
+ * @param {Object} authConfig - Authentication configuration
64
+ * @returns {Promise<Object>} System config response data
65
+ */
66
+ async function fetchExternalSystemConfig(dataplaneUrl, systemKey, authConfig) {
67
+ const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
68
+ if (!response || response.success === false) {
69
+ throw new Error(response?.error || response?.formattedError || `External system '${systemKey}' not found`);
70
+ }
71
+ return response.data?.data || response.data || {};
72
+ }
73
+
74
+ /**
75
+ * Formats datasources for warning output
76
+ * @param {Array} dataSources - Datasource objects
77
+ * @returns {string[]} Datasource labels
78
+ */
79
+ function formatDatasourceList(dataSources) {
80
+ if (!Array.isArray(dataSources)) {
81
+ return [];
82
+ }
83
+ return dataSources
84
+ .map(ds => ds.key || ds.displayName || 'unknown-datasource')
85
+ .filter(Boolean);
86
+ }
87
+
88
+ /**
89
+ * Prompts for delete confirmation if needed
90
+ * @async
91
+ * @param {string} systemKey - System key
92
+ * @param {string[]} datasources - Datasource keys
93
+ * @param {Object} options - Command options
94
+ * @returns {Promise<boolean>} True if confirmed
95
+ */
96
+ async function confirmDeletion(systemKey, datasources, options) {
97
+ if (options.yes || options.force) {
98
+ return true;
99
+ }
100
+
101
+ logger.log(chalk.yellow(`\n⚠️ Warning: Deleting external system '${systemKey}' will also delete all associated datasources:`));
102
+ if (datasources.length > 0) {
103
+ datasources.forEach(ds => logger.log(chalk.yellow(` - ${ds}`)));
104
+ } else {
105
+ logger.log(chalk.yellow(' - (no datasources found)'));
106
+ }
107
+
108
+ const answer = await inquirer.prompt([{
109
+ type: 'input',
110
+ name: 'confirm',
111
+ message: `Are you sure you want to delete external system '${systemKey}'? (yes/no):`,
112
+ default: 'no'
113
+ }]);
114
+
115
+ return String(answer.confirm).trim().toLowerCase() === 'yes';
116
+ }
117
+
118
+ /**
119
+ * Deletes an external system from dataplane
120
+ * @async
121
+ * @function deleteExternalSystemCommand
122
+ * @param {string} systemKey - System key
123
+ * @param {Object} options - Command options
124
+ * @returns {Promise<void>}
125
+ * @throws {Error} If deletion fails or is cancelled
126
+ */
127
+ async function deleteExternalSystemCommand(systemKey, options = {}) {
128
+ validateSystemKey(systemKey);
129
+
130
+ const { authConfig, dataplaneUrl } = await getAuthAndDataplane(systemKey, options);
131
+ const configData = await fetchExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
132
+ const dataSources = formatDatasourceList(configData.dataSources || []);
133
+
134
+ const confirmed = await confirmDeletion(systemKey, dataSources, options);
135
+ if (!confirmed) {
136
+ logger.log(chalk.yellow('Deletion cancelled.'));
137
+ return;
138
+ }
139
+
140
+ const response = await deleteExternalSystem(dataplaneUrl, systemKey, authConfig);
141
+ if (!response || response.success === false) {
142
+ throw new Error(response?.error || response?.formattedError || `Failed to delete external system '${systemKey}'`);
143
+ }
144
+
145
+ logger.log(chalk.green(`✓ External system '${systemKey}' deleted successfully`));
146
+ logger.log(chalk.green('✓ All associated datasources have been removed'));
147
+ }
148
+
149
+ module.exports = {
150
+ deleteExternalSystem: deleteExternalSystemCommand
151
+ };