@aifabrix/builder 2.32.3 → 2.33.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 (127) 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 +12 -11
  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/index.js +6 -2
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +161 -23
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +17 -10
  46. package/lib/app/readme.js +41 -112
  47. package/lib/app/register.js +44 -9
  48. package/lib/app/rotate-secret.js +48 -31
  49. package/lib/cli.js +219 -70
  50. package/lib/commands/app.js +4 -9
  51. package/lib/commands/auth-config.js +125 -0
  52. package/lib/commands/auth-status.js +7 -8
  53. package/lib/commands/datasource.js +3 -6
  54. package/lib/commands/login-credentials.js +4 -4
  55. package/lib/commands/login-device.js +26 -17
  56. package/lib/commands/login.js +12 -10
  57. package/lib/commands/wizard-config-normalizer.js +92 -0
  58. package/lib/commands/wizard-core.js +515 -0
  59. package/lib/commands/wizard-dataplane.js +122 -0
  60. package/lib/commands/wizard-headless.js +115 -0
  61. package/lib/commands/wizard.js +110 -332
  62. package/lib/core/config.js +46 -0
  63. package/lib/core/secrets.js +3 -22
  64. package/lib/core/templates-env.js +1 -1
  65. package/lib/datasource/deploy.js +59 -23
  66. package/lib/datasource/list.js +108 -19
  67. package/lib/deployment/deployer.js +25 -0
  68. package/lib/deployment/environment.js +10 -13
  69. package/lib/external-system/delete.js +151 -0
  70. package/lib/external-system/deploy.js +53 -378
  71. package/lib/external-system/download-helpers.js +45 -65
  72. package/lib/external-system/download.js +33 -13
  73. package/lib/external-system/generator.js +11 -7
  74. package/lib/external-system/test-auth.js +4 -3
  75. package/lib/generator/builders.js +3 -1
  76. package/lib/generator/external-controller-manifest.js +157 -0
  77. package/lib/generator/external-schema-utils.js +236 -0
  78. package/lib/generator/external.js +55 -3
  79. package/lib/generator/index.js +22 -10
  80. package/lib/generator/wizard-prompts.js +33 -10
  81. package/lib/generator/wizard.js +69 -86
  82. package/lib/infrastructure/compose.js +100 -0
  83. package/lib/infrastructure/helpers.js +139 -0
  84. package/lib/infrastructure/index.js +52 -311
  85. package/lib/infrastructure/services.js +168 -0
  86. package/lib/schema/application-schema.json +23 -4
  87. package/lib/schema/external-datasource.schema.json +2 -2
  88. package/lib/schema/wizard-config.schema.json +234 -0
  89. package/lib/utils/api.js +102 -52
  90. package/lib/utils/app-existence.js +42 -0
  91. package/lib/utils/app-register-config.js +7 -2
  92. package/lib/utils/auth-config-validator.js +92 -0
  93. package/lib/utils/command-header.js +43 -0
  94. package/lib/utils/compose-generator.js +113 -70
  95. package/lib/utils/controller-url.js +65 -17
  96. package/lib/utils/dataplane-health.js +115 -0
  97. package/lib/utils/dataplane-resolver.js +29 -0
  98. package/lib/utils/dev-config.js +6 -2
  99. package/lib/utils/env-copy.js +2 -1
  100. package/lib/utils/env-ports.js +2 -1
  101. package/lib/utils/env-template.js +1 -1
  102. package/lib/utils/error-formatter.js +49 -0
  103. package/lib/utils/error-formatters/network-errors.js +13 -3
  104. package/lib/utils/external-readme.js +125 -0
  105. package/lib/utils/help-builder.js +190 -0
  106. package/lib/utils/infra-status.js +13 -3
  107. package/lib/utils/paths.js +17 -2
  108. package/lib/utils/port-resolver.js +111 -0
  109. package/lib/utils/secrets-helpers.js +3 -15
  110. package/lib/utils/secrets-utils.js +2 -2
  111. package/lib/utils/token-manager.js +9 -4
  112. package/lib/utils/variable-transformer.js +7 -2
  113. package/lib/validation/external-manifest-validator.js +202 -0
  114. package/lib/validation/validate-display.js +406 -0
  115. package/lib/validation/validate.js +159 -123
  116. package/lib/validation/validator.js +36 -3
  117. package/lib/validation/wizard-config-validator.js +267 -0
  118. package/package.json +4 -2
  119. package/templates/applications/README.md.hbs +18 -16
  120. package/templates/applications/miso-controller/env.template +1 -1
  121. package/templates/applications/miso-controller/rbac.yaml +7 -7
  122. package/templates/external-system/README.md.hbs +99 -0
  123. package/templates/github/ci.yaml.hbs +44 -1
  124. package/templates/github/release.yaml.hbs +44 -0
  125. package/templates/infra/compose.yaml.hbs +35 -0
  126. package/templates/python/docker-compose.hbs +26 -0
  127. 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,18 +107,38 @@ 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);
124
- logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
120
+ logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
121
+ let dataplaneUrl;
122
+ try {
123
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
124
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
125
+ } catch (error) {
126
+ logger.error(chalk.red('❌ Failed to resolve dataplane URL:'), error.message);
127
+ logger.error(chalk.gray('\nThe dataplane URL is automatically discovered from the controller.'));
128
+ logger.error(chalk.gray('If discovery fails, ensure you are logged in and the controller is accessible:'));
129
+ logger.error(chalk.gray(' aifabrix login'));
130
+ throw error;
131
+ }
132
+
133
+ // Validate dataplane URL
134
+ if (!dataplaneUrl || !dataplaneUrl.trim()) {
135
+ logger.error(chalk.red('❌ Dataplane URL is empty.'));
136
+ logger.error(chalk.gray('The dataplane URL could not be discovered from the controller.'));
137
+ logger.error(chalk.gray('Ensure the dataplane service is registered in the controller.'));
138
+ throw new Error('Dataplane URL is empty');
139
+ }
125
140
 
126
- return { authConfig, dataplaneUrl };
141
+ return { authConfig, dataplaneUrl: dataplaneUrl.trim() };
127
142
  }
128
143
 
129
144
  /**
@@ -145,6 +160,17 @@ async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig,
145
160
  const formattedError = publishResponse.formattedError || formatApiError(publishResponse);
146
161
  logger.error(chalk.red('❌ Publish failed:'));
147
162
  logger.error(formattedError);
163
+
164
+ // Show dataplane URL and endpoint information
165
+ if (publishResponse.errorData && publishResponse.errorData.endpointUrl) {
166
+ logger.error(chalk.gray(`\nEndpoint URL: ${publishResponse.errorData.endpointUrl}`));
167
+ } else if (dataplaneUrl) {
168
+ logger.error(chalk.gray(`\nDataplane URL: ${dataplaneUrl}`));
169
+ logger.error(chalk.gray(`System Key: ${systemKey}`));
170
+ }
171
+
172
+ logger.error(chalk.gray('\nFull response for debugging:'));
173
+ logger.error(chalk.gray(JSON.stringify(publishResponse, null, 2)));
148
174
  throw new Error(`Dataplane publish failed: ${formattedError}`);
149
175
  }
150
176
 
@@ -165,20 +191,30 @@ function displayDeploymentResults(datasourceConfig, systemKey, environment) {
165
191
  }
166
192
 
167
193
  /**
168
- * Deploys datasource to dataplane
194
+ * Deploys datasource to dataplane.
195
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
169
196
  *
170
197
  * @async
171
198
  * @function deployDatasource
172
199
  * @param {string} appKey - Application key
173
200
  * @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
201
+ * @param {Object} [_options] - Deployment options (reserved)
177
202
  * @returns {Promise<Object>} Deployment result
178
203
  * @throws {Error} If deployment fails
179
204
  */
180
- async function deployDatasource(appKey, filePath, options) {
181
- validateDeploymentInputs(appKey, filePath, options);
205
+ async function deployDatasource(appKey, filePath, _options) {
206
+ const { resolveControllerUrl } = require('../utils/controller-url');
207
+ const { resolveEnvironment } = require('../core/config');
208
+ const { displayCommandHeader } = require('../utils/command-header');
209
+
210
+ validateDeploymentInputs(appKey, filePath);
211
+
212
+ // Resolve controller and environment from config
213
+ const controllerUrl = await resolveControllerUrl();
214
+ const environment = await resolveEnvironment();
215
+
216
+ // Display command header
217
+ displayCommandHeader(controllerUrl, environment);
182
218
 
183
219
  logger.log(chalk.blue('📋 Deploying datasource...\n'));
184
220
 
@@ -192,19 +228,19 @@ async function deployDatasource(appKey, filePath, options) {
192
228
  }
193
229
 
194
230
  // Setup authentication and get dataplane URL
195
- const { authConfig, dataplaneUrl } = await setupDeploymentAuth(options.controller, options.environment, appKey);
231
+ const { authConfig, dataplaneUrl } = await setupDeploymentAuth(controllerUrl, environment, appKey);
196
232
 
197
233
  // Publish to dataplane
198
234
  await publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig);
199
235
 
200
236
  // Display results
201
- displayDeploymentResults(datasourceConfig, systemKey, options.environment);
237
+ displayDeploymentResults(datasourceConfig, systemKey, environment);
202
238
 
203
239
  return {
204
240
  success: true,
205
241
  datasourceKey: datasourceConfig.key,
206
242
  systemKey: systemKey,
207
- environment: options.environment,
243
+ environment: environment,
208
244
  dataplaneUrl: dataplaneUrl
209
245
  };
210
246
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Datasource List Command
3
3
  *
4
- * Lists datasources from an environment via controller API.
4
+ * Lists datasources from an environment via dataplane API.
5
+ * Gets dataplane URL from controller, then lists datasources from dataplane.
5
6
  *
6
7
  * @fileoverview Datasource listing for AI Fabrix Builder
7
8
  * @author AI Fabrix Team
@@ -9,9 +10,10 @@
9
10
  */
10
11
 
11
12
  const chalk = require('chalk');
12
- const { getConfig } = require('../core/config');
13
+ const { getConfig, resolveEnvironment } = require('../core/config');
13
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
- const { listEnvironmentDatasources } = require('../api/environments.api');
15
+ const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources-core.api');
16
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
17
  const { formatApiError } = require('../utils/api-error-handler');
16
18
  const logger = require('../utils/logger');
17
19
 
@@ -103,14 +105,19 @@ function extractDatasources(response) {
103
105
  * @function displayDatasources
104
106
  * @param {Array} datasources - Array of datasource objects
105
107
  * @param {string} environment - Environment key
108
+ * @param {string} dataplaneUrl - Dataplane URL for header display
106
109
  */
107
- function displayDatasources(datasources, environment) {
110
+ function displayDatasources(datasources, environment, dataplaneUrl) {
111
+ const environmentName = environment || 'dev';
112
+ const header = `Datasources in ${environmentName} environment${dataplaneUrl ? ` (${dataplaneUrl})` : ''}`;
113
+
108
114
  if (datasources.length === 0) {
109
- logger.log(chalk.yellow(`\nNo datasources found in environment: ${environment}`));
115
+ logger.log(chalk.bold(`\n📋 ${header}:\n`));
116
+ logger.log(chalk.gray(' No datasources found in this environment.\n'));
110
117
  return;
111
118
  }
112
119
 
113
- logger.log(chalk.blue(`\n📋 Datasources in environment: ${environment}\n`));
120
+ logger.log(chalk.bold(`\n📋 ${header}:\n`));
114
121
  logger.log(chalk.gray('Key'.padEnd(30) + 'Display Name'.padEnd(30) + 'System Key'.padEnd(20) + 'Version'.padEnd(15) + 'Status'));
115
122
  logger.log(chalk.gray('-'.repeat(120)));
116
123
 
@@ -130,8 +137,7 @@ function displayDatasources(datasources, environment) {
130
137
  *
131
138
  * @async
132
139
  * @function listDatasources
133
- * @param {Object} options - Command options
134
- * @param {string} options.environment - Environment ID or key
140
+ * @param {Object} _options - Command options (unused, kept for compatibility)
135
141
  * @throws {Error} If listing fails
136
142
  */
137
143
  /**
@@ -170,7 +176,7 @@ async function getDeviceTokenFromConfig(config) {
170
176
  * @param {string|null} controllerUrl - Controller URL
171
177
  */
172
178
  function validateDatasourceListingAuth(token, controllerUrl) {
173
- if (!token || !controllerUrl) {
179
+ if (!token || !controllerUrl || (typeof controllerUrl === 'string' && !controllerUrl.trim())) {
174
180
  logger.error(chalk.red('❌ Not logged in. Run: aifabrix login'));
175
181
  logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
176
182
  process.exit(1);
@@ -182,37 +188,120 @@ function validateDatasourceListingAuth(token, controllerUrl) {
182
188
  * @function handleDatasourceApiError
183
189
  * @param {Object} response - API response
184
190
  */
185
- function handleDatasourceApiError(response) {
191
+ function handleDatasourceApiError(response, dataplaneUrl = null) {
186
192
  const formattedError = response.formattedError || formatApiError(response);
187
193
  logger.error(formattedError);
194
+
195
+ // Show endpoint URL from error data if available (more specific than dataplane URL)
196
+ if (response.errorData && response.errorData.endpointUrl) {
197
+ logger.error(chalk.gray(`\nEndpoint URL: ${response.errorData.endpointUrl}`));
198
+ } else if (response.errorData && response.errorData.controllerUrl) {
199
+ logger.error(chalk.gray(`\nDataplane URL: ${response.errorData.controllerUrl}`));
200
+ } else if (dataplaneUrl) {
201
+ logger.error(chalk.gray(`\nDataplane URL: ${dataplaneUrl}`));
202
+ }
203
+
188
204
  logger.error(chalk.gray('\nFull response for debugging:'));
189
205
  logger.error(chalk.gray(JSON.stringify(response, null, 2)));
190
206
  process.exit(1);
191
207
  }
192
208
 
193
- async function listDatasources(options) {
209
+ /**
210
+ * Validates and trims controller URL
211
+ * @function validateControllerUrl
212
+ * @param {string} controllerUrl - Controller URL to validate
213
+ * @returns {string} Trimmed controller URL
214
+ */
215
+ function validateControllerUrl(controllerUrl) {
216
+ const trimmed = controllerUrl.trim();
217
+ if (!trimmed) {
218
+ logger.error(chalk.red('❌ Controller URL is empty.'));
219
+ logger.error(chalk.gray(` Controller URL from config: ${JSON.stringify(controllerUrl)}`));
220
+ logger.error(chalk.gray(' Run: aifabrix login --method device --controller <url>'));
221
+ process.exit(1);
222
+ }
223
+ return trimmed;
224
+ }
225
+
226
+ /**
227
+ * Resolves and validates dataplane URL from controller
228
+ * @async
229
+ * @function resolveAndValidateDataplaneUrl
230
+ * @param {string} controllerUrl - Controller URL
231
+ * @param {string} environment - Environment key
232
+ * @param {Object} authConfig - Authentication configuration
233
+ * @returns {Promise<string>} Validated dataplane URL
234
+ */
235
+ async function resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig) {
236
+ let dataplaneUrl;
237
+ try {
238
+ // discoverDataplaneUrl already logs progress and success messages
239
+ dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
240
+ } catch (error) {
241
+ logger.error(chalk.red('❌ Failed to resolve dataplane URL:'), error.message);
242
+ logger.error(chalk.gray('\nThe dataplane URL is automatically discovered from the controller.'));
243
+ logger.error(chalk.gray('If discovery fails, ensure you are logged in and the controller is accessible:'));
244
+ logger.error(chalk.gray(' aifabrix login'));
245
+ process.exit(1);
246
+ // eslint-disable-next-line no-unreachable
247
+ throw error; // Never reached in production, but needed for tests when process.exit is mocked
248
+ }
249
+
250
+ if (!dataplaneUrl || typeof dataplaneUrl !== 'string' || !dataplaneUrl.trim()) {
251
+ logger.error(chalk.red('❌ Dataplane URL is empty.'));
252
+ logger.error(chalk.gray('The dataplane URL could not be discovered from the controller.'));
253
+ logger.error(chalk.gray('Ensure the dataplane service is registered in the controller.'));
254
+ process.exit(1);
255
+ // eslint-disable-next-line no-unreachable
256
+ throw new Error('Dataplane URL is empty'); // Never reached in production, but needed for tests
257
+ }
258
+
259
+ return dataplaneUrl.trim();
260
+ }
261
+
262
+ /**
263
+ * Sets up authentication configuration for dataplane API calls
264
+ * @function setupAuthConfig
265
+ * @param {string} token - Authentication token
266
+ * @param {string} controllerUrl - Controller URL
267
+ * @returns {Object} Authentication configuration
268
+ */
269
+ function setupAuthConfig(token, controllerUrl) {
270
+ return {
271
+ type: 'bearer',
272
+ token: token,
273
+ controller: controllerUrl
274
+ };
275
+ }
276
+
277
+ async function listDatasources(_options) {
194
278
  const config = await getConfig();
195
279
 
280
+ // Resolve environment from config.yaml (no flags)
281
+ const environment = await resolveEnvironment();
282
+
196
283
  // Try to get device token
197
284
  const authInfo = await getDeviceTokenFromConfig(config);
198
285
  validateDatasourceListingAuth(authInfo?.token, authInfo?.controllerUrl);
199
286
 
200
- // Call controller API using centralized API client
201
- // Note: validateDatasourceListingAuth will exit if auth is missing, so this check is defensive
202
287
  if (!authInfo || !authInfo.token || !authInfo.controllerUrl) {
203
- validateDatasourceListingAuth(null, null); // This will exit
204
- return; // Never reached, but satisfies linter
288
+ validateDatasourceListingAuth(null, null);
289
+ return;
205
290
  }
206
- const authConfig = { type: 'bearer', token: authInfo.token };
207
- const response = await listEnvironmentDatasources(authInfo.controllerUrl, options.environment, authConfig);
291
+
292
+ const controllerUrl = validateControllerUrl(authInfo.controllerUrl);
293
+ const authConfig = setupAuthConfig(authInfo.token, controllerUrl);
294
+ const dataplaneUrl = await resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig);
295
+
296
+ const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig);
208
297
 
209
298
  if (!response.success || !response.data) {
210
- handleDatasourceApiError(response);
299
+ handleDatasourceApiError(response, dataplaneUrl);
211
300
  return; // Ensure we don't continue after exit
212
301
  }
213
302
 
214
303
  const datasources = extractDatasources(response);
215
- displayDatasources(datasources, options.environment);
304
+ displayDatasources(datasources, environment, dataplaneUrl);
216
305
  }
217
306
 
218
307
  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
+ };