@aifabrix/builder 2.22.1 → 2.31.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 (65) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secrets-set.js +2 -2
  16. package/lib/commands/secure.js +61 -26
  17. package/lib/config.js +79 -45
  18. package/lib/datasource-deploy.js +89 -29
  19. package/lib/deployer.js +164 -129
  20. package/lib/diff.js +63 -21
  21. package/lib/environment-deploy.js +36 -19
  22. package/lib/external-system-deploy.js +134 -66
  23. package/lib/external-system-download.js +244 -171
  24. package/lib/external-system-test.js +199 -164
  25. package/lib/generator-external.js +145 -72
  26. package/lib/generator-helpers.js +49 -17
  27. package/lib/generator-split.js +105 -58
  28. package/lib/infra.js +101 -131
  29. package/lib/schema/application-schema.json +895 -896
  30. package/lib/schema/env-config.yaml +11 -4
  31. package/lib/template-validator.js +13 -4
  32. package/lib/utils/api.js +8 -8
  33. package/lib/utils/app-register-auth.js +36 -18
  34. package/lib/utils/app-run-containers.js +140 -0
  35. package/lib/utils/auth-headers.js +6 -6
  36. package/lib/utils/build-copy.js +60 -2
  37. package/lib/utils/build-helpers.js +94 -0
  38. package/lib/utils/cli-utils.js +177 -76
  39. package/lib/utils/compose-generator.js +12 -2
  40. package/lib/utils/config-tokens.js +151 -9
  41. package/lib/utils/deployment-errors.js +137 -69
  42. package/lib/utils/deployment-validation-helpers.js +103 -0
  43. package/lib/utils/docker-build.js +57 -0
  44. package/lib/utils/dockerfile-utils.js +13 -3
  45. package/lib/utils/env-copy.js +163 -94
  46. package/lib/utils/env-map.js +226 -86
  47. package/lib/utils/environment-checker.js +2 -2
  48. package/lib/utils/error-formatters/network-errors.js +0 -1
  49. package/lib/utils/external-system-display.js +14 -19
  50. package/lib/utils/external-system-env-helpers.js +107 -0
  51. package/lib/utils/external-system-test-helpers.js +144 -0
  52. package/lib/utils/health-check.js +10 -8
  53. package/lib/utils/infra-status.js +123 -0
  54. package/lib/utils/local-secrets.js +3 -2
  55. package/lib/utils/paths.js +228 -49
  56. package/lib/utils/schema-loader.js +125 -57
  57. package/lib/utils/token-manager.js +10 -7
  58. package/lib/utils/yaml-preserve.js +55 -16
  59. package/lib/validate.js +87 -89
  60. package/package.json +4 -4
  61. package/scripts/ci-fix.sh +19 -0
  62. package/scripts/ci-simulate.sh +19 -0
  63. package/templates/applications/miso-controller/test.yaml +1 -0
  64. package/templates/python/Dockerfile.hbs +8 -45
  65. package/templates/typescript/Dockerfile.hbs +8 -42
@@ -126,6 +126,39 @@ async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, opti
126
126
  }
127
127
  }
128
128
 
129
+ /**
130
+ * Process environment status response
131
+ * @param {Object} response - API response
132
+ * @param {string} validatedEnvKey - Validated environment key
133
+ * @returns {Object|null} Status result if ready, null if needs to continue polling
134
+ * @throws {Error} If deployment failed
135
+ */
136
+ function processEnvironmentStatusResponse(response, validatedEnvKey) {
137
+ if (!response.success || !response.data) {
138
+ return null;
139
+ }
140
+
141
+ const responseData = response.data.data || response.data;
142
+ const status = responseData.status || responseData.ready;
143
+ const isReady = status === 'ready' || status === 'completed' || responseData.ready === true;
144
+
145
+ if (isReady) {
146
+ return {
147
+ success: true,
148
+ environment: validatedEnvKey,
149
+ status: 'ready',
150
+ message: 'Environment is ready for application deployments'
151
+ };
152
+ }
153
+
154
+ // Check for terminal failure states
155
+ if (status === 'failed' || status === 'error') {
156
+ throw new Error(`Environment deployment failed: ${responseData.message || 'Unknown error'}`);
157
+ }
158
+
159
+ return null;
160
+ }
161
+
129
162
  /**
130
163
  * Polls environment deployment status
131
164
  * @async
@@ -153,25 +186,9 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
153
186
  await new Promise(resolve => setTimeout(resolve, pollInterval));
154
187
 
155
188
  const response = await getEnvironmentStatus(validatedUrl, validatedEnvKey, apiAuthConfig);
156
-
157
- if (response.success && response.data) {
158
- const responseData = response.data.data || response.data;
159
- const status = responseData.status || responseData.ready;
160
- const isReady = status === 'ready' || status === 'completed' || responseData.ready === true;
161
-
162
- if (isReady) {
163
- return {
164
- success: true,
165
- environment: validatedEnvKey,
166
- status: 'ready',
167
- message: 'Environment is ready for application deployments'
168
- };
169
- }
170
-
171
- // Check for terminal failure states
172
- if (status === 'failed' || status === 'error') {
173
- throw new Error(`Environment deployment failed: ${responseData.message || 'Unknown error'}`);
174
- }
189
+ const statusResult = processEnvironmentStatusResponse(response, validatedEnvKey);
190
+ if (statusResult) {
191
+ return statusResult;
175
192
  }
176
193
  } catch (error) {
177
194
  // If it's a terminal error (not a timeout), throw it
@@ -194,6 +194,132 @@ async function buildExternalSystem(appName, options = {}) {
194
194
  }
195
195
  }
196
196
 
197
+ /**
198
+ * Validate deployment prerequisites
199
+ * @async
200
+ * @param {string} appName - Application name
201
+ * @returns {Promise<Object>} Validation result with systemFiles, datasourceFiles, and systemKey
202
+ */
203
+ async function validateDeploymentPrerequisites(appName) {
204
+ const { systemFiles: _systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
205
+ return { systemFiles: _systemFiles, datasourceFiles, systemKey };
206
+ }
207
+
208
+ /**
209
+ * Prepare deployment files and get authentication
210
+ * @async
211
+ * @param {string} appName - Application name
212
+ * @param {Object} options - Deployment options
213
+ * @returns {Promise<Object>} Object with applicationSchema, authConfig, controllerUrl, environment, and systemKey
214
+ */
215
+ async function prepareDeploymentFiles(appName, options) {
216
+ logger.log(chalk.blue('šŸ“‹ Generating application schema...'));
217
+ const applicationSchema = await generateExternalSystemApplicationSchema(appName);
218
+ logger.log(chalk.green('āœ“ Application schema generated'));
219
+
220
+ const config = await getConfig();
221
+ const environment = options.environment || 'dev';
222
+ const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
223
+ const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
224
+
225
+ if (!authConfig.token && !authConfig.clientId) {
226
+ throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
227
+ }
228
+
229
+ const { systemKey } = await validateDeploymentPrerequisites(appName);
230
+
231
+ return { applicationSchema, authConfig, controllerUrl, environment, systemKey };
232
+ }
233
+
234
+ /**
235
+ * Upload application and get upload ID
236
+ * @async
237
+ * @param {string} dataplaneUrl - Dataplane URL
238
+ * @param {Object} authConfig - Authentication configuration
239
+ * @param {Object} applicationSchema - Application schema
240
+ * @returns {Promise<string>} Upload ID
241
+ * @throws {Error} If upload fails
242
+ */
243
+ async function uploadApplication(dataplaneUrl, authConfig, applicationSchema) {
244
+ logger.log(chalk.blue('šŸ“¤ Uploading application configuration...'));
245
+ const uploadResponse = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicationSchema);
246
+
247
+ if (!uploadResponse.success || !uploadResponse.data) {
248
+ throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
249
+ }
250
+
251
+ const uploadData = uploadResponse.data.data || uploadResponse.data;
252
+ const uploadId = uploadData.uploadId || uploadData.id;
253
+
254
+ if (!uploadId) {
255
+ throw new Error('Upload ID not found in upload response');
256
+ }
257
+
258
+ logger.log(chalk.green(`āœ“ Upload successful (ID: ${uploadId})`));
259
+ return uploadId;
260
+ }
261
+
262
+ /**
263
+ * Validate upload and display changes
264
+ * @async
265
+ * @param {string} dataplaneUrl - Dataplane URL
266
+ * @param {string} uploadId - Upload ID
267
+ * @param {Object} authConfig - Authentication configuration
268
+ * @returns {Promise<void>}
269
+ * @throws {Error} If validation fails
270
+ */
271
+ async function validateUpload(dataplaneUrl, uploadId, authConfig) {
272
+ logger.log(chalk.blue('šŸ” Validating upload...'));
273
+ const validateResponse = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
274
+
275
+ if (!validateResponse.success || !validateResponse.data) {
276
+ throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
277
+ }
278
+
279
+ const validateData = validateResponse.data.data || validateResponse.data;
280
+
281
+ // Display changes
282
+ if (validateData.changes && validateData.changes.length > 0) {
283
+ logger.log(chalk.blue('\nšŸ“‹ Changes to be published:'));
284
+ for (const change of validateData.changes) {
285
+ const changeType = change.type || 'unknown';
286
+ const changeEntity = change.entity || change.key || 'unknown';
287
+ const emoji = changeType === 'new' ? 'āž•' : changeType === 'modified' ? 'āœļø' : 'šŸ—‘ļø';
288
+ logger.log(chalk.gray(` ${emoji} ${changeType}: ${changeEntity}`));
289
+ }
290
+ }
291
+
292
+ if (validateData.summary) {
293
+ logger.log(chalk.blue(`\nšŸ“Š Summary: ${validateData.summary}`));
294
+ }
295
+
296
+ logger.log(chalk.green('āœ“ Validation successful'));
297
+ }
298
+
299
+ /**
300
+ * Publish application
301
+ * @async
302
+ * @param {string} dataplaneUrl - Dataplane URL
303
+ * @param {string} uploadId - Upload ID
304
+ * @param {Object} authConfig - Authentication configuration
305
+ * @param {Object} options - Publish options
306
+ * @param {boolean} [options.generateMcpContract] - Generate MCP contract (default: true)
307
+ * @returns {Promise<Object>} Publish response data
308
+ * @throws {Error} If publish fails
309
+ */
310
+ async function publishApplication(dataplaneUrl, uploadId, authConfig, options) {
311
+ const generateMcpContract = options.generateMcpContract !== false; // Default to true
312
+ logger.log(chalk.blue(`šŸ“¢ Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
313
+
314
+ const publishResponse = await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig, { generateMcpContract });
315
+
316
+ if (!publishResponse.success || !publishResponse.data) {
317
+ throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
318
+ }
319
+
320
+ return publishResponse.data.data || publishResponse.data;
321
+ }
322
+
197
323
  /**
198
324
  * Publishes external system to dataplane using application-level workflow
199
325
  * Uses upload → validate → publish workflow for atomic deployment
@@ -212,23 +338,11 @@ async function deployExternalSystem(appName, options = {}) {
212
338
  try {
213
339
  logger.log(chalk.blue(`\nšŸš€ Publishing external system: ${appName}`));
214
340
 
215
- // Validate files
216
- const { systemFiles: _systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
217
-
218
- // Generate application-schema.json structure
219
- logger.log(chalk.blue('šŸ“‹ Generating application schema...'));
220
- const applicationSchema = await generateExternalSystemApplicationSchema(appName);
221
- logger.log(chalk.green('āœ“ Application schema generated'));
341
+ // Validate prerequisites
342
+ const { datasourceFiles } = await validateDeploymentPrerequisites(appName);
222
343
 
223
- // Get authentication
224
- const config = await getConfig();
225
- const environment = options.environment || 'dev';
226
- const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
227
- const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
228
-
229
- if (!authConfig.token && !authConfig.clientId) {
230
- throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
231
- }
344
+ // Prepare deployment files and get authentication
345
+ const { applicationSchema, authConfig, controllerUrl, environment, systemKey } = await prepareDeploymentFiles(appName, options);
232
346
 
233
347
  // Get dataplane URL from controller
234
348
  logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
@@ -236,65 +350,19 @@ async function deployExternalSystem(appName, options = {}) {
236
350
  logger.log(chalk.green(`āœ“ Dataplane URL: ${dataplaneUrl}`));
237
351
 
238
352
  // Step 1: Upload application
239
- logger.log(chalk.blue('šŸ“¤ Uploading application configuration...'));
240
- const uploadResponse = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicationSchema);
241
-
242
- if (!uploadResponse.success || !uploadResponse.data) {
243
- throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
244
- }
245
-
246
- const uploadData = uploadResponse.data.data || uploadResponse.data;
247
- const uploadId = uploadData.uploadId || uploadData.id;
248
-
249
- if (!uploadId) {
250
- throw new Error('Upload ID not found in upload response');
251
- }
252
-
253
- logger.log(chalk.green(`āœ“ Upload successful (ID: ${uploadId})`));
353
+ const uploadId = await uploadApplication(dataplaneUrl, authConfig, applicationSchema);
254
354
 
255
355
  // Step 2: Validate upload (optional, can be skipped)
256
356
  if (!options.skipValidation) {
257
- logger.log(chalk.blue('šŸ” Validating upload...'));
258
- const validateResponse = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
259
-
260
- if (!validateResponse.success || !validateResponse.data) {
261
- throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
262
- }
263
-
264
- const validateData = validateResponse.data.data || validateResponse.data;
265
-
266
- // Display changes
267
- if (validateData.changes && validateData.changes.length > 0) {
268
- logger.log(chalk.blue('\nšŸ“‹ Changes to be published:'));
269
- for (const change of validateData.changes) {
270
- const changeType = change.type || 'unknown';
271
- const changeEntity = change.entity || change.key || 'unknown';
272
- const emoji = changeType === 'new' ? 'āž•' : changeType === 'modified' ? 'āœļø' : 'šŸ—‘ļø';
273
- logger.log(chalk.gray(` ${emoji} ${changeType}: ${changeEntity}`));
274
- }
275
- }
276
-
277
- if (validateData.summary) {
278
- logger.log(chalk.blue(`\nšŸ“Š Summary: ${validateData.summary}`));
279
- }
280
-
281
- logger.log(chalk.green('āœ“ Validation successful'));
357
+ await validateUpload(dataplaneUrl, uploadId, authConfig);
282
358
  } else {
283
359
  logger.log(chalk.yellow('⚠ Skipping validation step'));
284
360
  }
285
361
 
286
362
  // Step 3: Publish application
287
- const generateMcpContract = options.generateMcpContract !== false; // Default to true
288
- logger.log(chalk.blue(`šŸ“¢ Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
289
-
290
- const publishResponse = await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig, { generateMcpContract });
291
-
292
- if (!publishResponse.success || !publishResponse.data) {
293
- throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
294
- }
295
-
296
- const publishData = publishResponse.data.data || publishResponse.data;
363
+ const publishData = await publishApplication(dataplaneUrl, uploadId, authConfig, options);
297
364
 
365
+ // Display success summary
298
366
  logger.log(chalk.green('\nāœ… External system published successfully!'));
299
367
  logger.log(chalk.blue(`System: ${systemKey}`));
300
368
  if (publishData.systems && publishData.systems.length > 0) {