@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
package/lib/app/deploy.js CHANGED
@@ -18,6 +18,8 @@ const logger = require('../utils/logger');
18
18
  const config = require('../core/config');
19
19
  const { getDeploymentAuth } = require('../utils/token-manager');
20
20
  const { detectAppType } = require('../utils/paths');
21
+ const { resolveControllerUrl } = require('../utils/controller-url');
22
+ const { checkApplicationExists } = require('../utils/app-existence');
21
23
 
22
24
  /**
23
25
  * Validate application name format
@@ -178,15 +180,26 @@ async function loadVariablesFile(variablesPath) {
178
180
  }
179
181
 
180
182
  /**
181
- * Extracts deployment configuration from options and variables
182
- * @param {Object} options - CLI options
183
- * @param {Object} variables - Variables from variables.yaml
184
- * @returns {Object} Extracted configuration
183
+ * Extracts deployment configuration from config.yaml
184
+ * Resolves controller URL using fallback chain: config.controller → logged-in user → developer ID default
185
+ * Resolves environment using fallback chain: config.environment → default 'dev'
186
+ * @async
187
+ * @param {Object} options - CLI options (for poll settings only)
188
+ * @param {Object} _variables - Variables from variables.yaml (unused, kept for compatibility)
189
+ * @returns {Promise<Object>} Extracted configuration with resolved controller URL
185
190
  */
186
- function extractDeploymentConfig(options, variables) {
191
+ async function extractDeploymentConfig(options, _variables) {
192
+ const { resolveEnvironment } = require('../core/config');
193
+
194
+ // Resolve controller URL from config.yaml (no flags, no options)
195
+ const controllerUrl = await resolveControllerUrl();
196
+
197
+ // Resolve environment from config.yaml (no flags, no options)
198
+ const envKey = await resolveEnvironment();
199
+
187
200
  return {
188
- controllerUrl: options.controller || variables.deployment?.controllerUrl,
189
- envKey: options.environment || variables.deployment?.environment,
201
+ controllerUrl,
202
+ envKey,
190
203
  poll: options.poll !== false,
191
204
  pollInterval: options.pollInterval || 5000,
192
205
  pollMaxAttempts: options.pollMaxAttempts || 60
@@ -200,7 +213,7 @@ function extractDeploymentConfig(options, variables) {
200
213
  */
201
214
  function validateDeploymentConfig(deploymentConfig) {
202
215
  if (!deploymentConfig.controllerUrl) {
203
- throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
216
+ throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
204
217
  }
205
218
  if (!deploymentConfig.auth) {
206
219
  throw new Error('Authentication is required. Run "aifabrix login" first or ensure credentials are in secrets.local.yaml');
@@ -208,19 +221,15 @@ function validateDeploymentConfig(deploymentConfig) {
208
221
  }
209
222
 
210
223
  /**
211
- * Configure deployment environment settings
224
+ * Configure deployment environment settings from config.yaml
212
225
  * @async
213
- * @param {Object} options - CLI options
226
+ * @param {Object} _options - CLI options (unused, kept for compatibility)
214
227
  * @param {Object} deploymentConfig - Deployment configuration to update
215
228
  * @returns {Promise<void>}
216
229
  */
217
- async function configureDeploymentEnvironment(options, deploymentConfig) {
218
- // Update root-level environment if provided
219
- if (options.environment) {
220
- await config.setCurrentEnvironment(options.environment);
221
- }
222
-
223
- // Get current environment from root-level config
230
+ async function configureDeploymentEnvironment(_options, deploymentConfig) {
231
+ // Get current environment from root-level config (already resolved in extractDeploymentConfig)
232
+ // This function is kept for compatibility but no longer updates environment from options
224
233
  const currentEnvironment = await config.getCurrentEnvironment();
225
234
  deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
226
235
  }
@@ -234,9 +243,9 @@ async function configureDeploymentEnvironment(options, deploymentConfig) {
234
243
  * @throws {Error} If authentication fails
235
244
  */
236
245
  async function refreshDeploymentToken(appName, deploymentConfig) {
237
- // Get controller URL
246
+ // Get controller URL (should already be resolved by extractDeploymentConfig)
238
247
  if (!deploymentConfig.controllerUrl) {
239
- throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
248
+ throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
240
249
  }
241
250
 
242
251
  // Get deployment authentication (device token → client token → credentials)
@@ -275,7 +284,7 @@ async function loadDeploymentConfig(appName, options) {
275
284
  const variablesPath = path.join(appPath, 'variables.yaml');
276
285
  const variables = await loadVariablesFile(variablesPath);
277
286
 
278
- const deploymentConfig = extractDeploymentConfig(options, variables);
287
+ const deploymentConfig = await extractDeploymentConfig(options, variables);
279
288
 
280
289
  await configureDeploymentEnvironment(options, deploymentConfig);
281
290
  await refreshDeploymentToken(appName, deploymentConfig);
@@ -360,6 +369,80 @@ function displayDeploymentResults(result) {
360
369
  }
361
370
  }
362
371
 
372
+ /**
373
+ * Check if app is external and handle external deployment
374
+ * @async
375
+ * @function handleExternalDeployment
376
+ * @param {string} appName - Application name
377
+ * @param {Object} options - Deployment options
378
+ * @returns {Promise<Object|null>} Deployment result if external, null otherwise
379
+ */
380
+ async function handleExternalDeployment(appName, options) {
381
+ const { isExternal } = await detectAppType(appName);
382
+ if (isExternal) {
383
+ const externalDeploy = require('../external-system/deploy');
384
+ await externalDeploy.deployExternalSystem(appName, options);
385
+ return { success: true, type: 'external' };
386
+ }
387
+ return null;
388
+ }
389
+
390
+ /**
391
+ * Handle deployment errors
392
+ * @async
393
+ * @function handleDeploymentError
394
+ * @param {Error} error - Error that occurred
395
+ * @param {string} appName - Application name
396
+ * @param {string} controllerUrl - Controller URL (from config, or null if not yet resolved)
397
+ * @param {boolean} usedExternalDeploy - Whether external deployment was used
398
+ */
399
+ async function handleDeploymentError(error, appName, controllerUrl, usedExternalDeploy) {
400
+ if (usedExternalDeploy) {
401
+ throw error;
402
+ }
403
+ const alreadyLogged = error._logged === true;
404
+ const url = controllerUrl || 'unknown';
405
+ const deployer = require('../deployment/deployer');
406
+ await deployer.handleDeploymentErrors(error, appName, url, alreadyLogged);
407
+ }
408
+
409
+ /**
410
+ * Execute standard application deployment flow
411
+ * @async
412
+ * @function executeStandardDeployment
413
+ * @param {string} appName - Application name
414
+ * @param {Object} options - Deployment options
415
+ * @returns {Promise<Object>} Deployment result
416
+ */
417
+ async function executeStandardDeployment(appName, options) {
418
+ const config = await loadDeploymentConfig(appName, options);
419
+ const controllerUrl = config.controllerUrl || 'unknown';
420
+
421
+ // Check if application exists before deployment
422
+ const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
423
+
424
+ const { manifest, manifestPath } = await generateAndValidateManifest(appName);
425
+ displayDeploymentInfo(manifest, manifestPath);
426
+
427
+ try {
428
+ const result = await executeDeployment(manifest, config);
429
+ displayDeploymentResults(result);
430
+ return { result, controllerUrl, appExists };
431
+ } catch (error) {
432
+ // Enhance error if app exists and credentials are invalid
433
+ if (appExists && error.status === 401 && !error.message.includes('rotate-secret')) {
434
+ const enhancedError = new Error(
435
+ `${error.message}\n\n💡 The application '${appName}' exists in environment '${config.envKey}'. ` +
436
+ `To fix invalid credentials, rotate the application secret:\n aifabrix app rotate-secret ${appName}`
437
+ );
438
+ enhancedError.status = 401;
439
+ enhancedError.formatted = error.formatted || enhancedError.message;
440
+ throw enhancedError;
441
+ }
442
+ throw error;
443
+ }
444
+ }
445
+
363
446
  /**
364
447
  * Deploys application to Miso Controller
365
448
  * Orchestrates manifest generation, key creation, and deployment
@@ -368,59 +451,38 @@ function displayDeploymentResults(result) {
368
451
  * @function deployApp
369
452
  * @param {string} appName - Name of the application to deploy
370
453
  * @param {Object} options - Deployment options
371
- * @param {string} options.controller - Controller URL (required)
372
- * @param {string} [options.environment] - Target environment (miso/dev/tst/pro)
373
454
  * @param {boolean} [options.poll] - Poll for deployment status
374
455
  * @param {number} [options.pollInterval] - Polling interval in milliseconds
456
+ * @param {number} [options.pollMaxAttempts] - Max polling attempts
375
457
  * @returns {Promise<Object>} Deployment result
376
458
  * @throws {Error} If deployment fails
377
459
  *
460
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
461
+ *
378
462
  * @example
379
- * await deployApp('myapp', { controller: 'https://controller.aifabrix.ai', environment: 'dev' });
463
+ * await deployApp('myapp', { poll: true });
380
464
  */
381
465
  async function deployApp(appName, options = {}) {
382
466
  let controllerUrl = null;
383
- let config = null;
467
+ let usedExternalDeploy = false;
384
468
 
385
469
  try {
386
- // 1. Input validation
387
470
  if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
388
471
  throw new Error('App name is required');
389
472
  }
390
-
391
473
  validateAppName(appName);
392
474
 
393
- // 2. Check if app type is external - use normal deployment flow with application-schema.json
394
- // External systems now deploy via miso controller as normal application (full application file)
395
- // The json command generates application-schema.json which is used for deployment
396
-
397
- // 2. Load deployment configuration
398
- config = await loadDeploymentConfig(appName, options);
399
- controllerUrl = config.controllerUrl || options.controller || 'unknown';
400
-
401
- // 3. Generate and validate manifest
402
- const { manifest, manifestPath } = await generateAndValidateManifest(appName);
403
-
404
- // 4. Display deployment info
405
- displayDeploymentInfo(manifest, manifestPath);
406
-
407
- // 5. Execute deployment
408
- const result = await executeDeployment(manifest, config);
409
-
410
- // 6. Display results
411
- displayDeploymentResults(result);
475
+ const externalResult = await handleExternalDeployment(appName, options);
476
+ if (externalResult) {
477
+ return externalResult;
478
+ }
479
+ usedExternalDeploy = false;
412
480
 
481
+ const { result, controllerUrl: url } = await executeStandardDeployment(appName, options);
482
+ controllerUrl = url;
413
483
  return result;
414
-
415
484
  } catch (error) {
416
- // Use unified error handler from deployer
417
- // Check if error was already logged (from deployer.js)
418
- const alreadyLogged = error._logged === true;
419
- const url = controllerUrl || options.controller || 'unknown';
420
-
421
- const deployer = require('../deployment/deployer');
422
- // handleDeploymentErrors will log, format, and throw the error
423
- await deployer.handleDeploymentErrors(error, appName, url, alreadyLogged);
485
+ await handleDeploymentError(error, appName, controllerUrl, usedExternalDeploy);
424
486
  }
425
487
  }
426
488
 
@@ -25,9 +25,9 @@ function displayExternalSystemSuccess(appName, config, location) {
25
25
  logger.log(chalk.blue(`System Key: ${config.systemKey || appName}`));
26
26
  logger.log(chalk.green('\nNext steps:'));
27
27
  logger.log(chalk.white('1. Edit external system JSON files in ' + location));
28
- logger.log(chalk.white('2. Run: aifabrix app register ' + appName + ' --environment dev'));
29
- logger.log(chalk.white('3. Run: aifabrix build ' + appName + ' (deploys to dataplane)'));
30
- logger.log(chalk.white('4. Run: aifabrix deploy ' + appName + ' (publishes to dataplane)'));
28
+ logger.log(chalk.white('2. Run: aifabrix validate ' + appName + ' --type external'));
29
+ logger.log(chalk.white('3. Run: aifabrix login'));
30
+ logger.log(chalk.white('4. Run: aifabrix deploy ' + appName));
31
31
  }
32
32
 
33
33
  /**
@@ -50,8 +50,9 @@ function displayWebappSuccess(appName, config, envConversionMessage) {
50
50
 
51
51
  logger.log(chalk.green('\nNext steps:'));
52
52
  logger.log(chalk.white('1. Copy env.template to .env and fill in your values'));
53
- logger.log(chalk.white('2. Run: aifabrix build ' + appName));
54
- logger.log(chalk.white('3. Run: aifabrix run ' + appName));
53
+ logger.log(chalk.white('2. Run: aifabrix up'));
54
+ logger.log(chalk.white('3. Run: aifabrix build ' + appName));
55
+ logger.log(chalk.white('4. Run: aifabrix run ' + appName));
55
56
  }
56
57
 
57
58
  /**
@@ -15,6 +15,7 @@ const yaml = require('js-yaml');
15
15
  const build = require('../build');
16
16
  const { validateAppName } = require('./push');
17
17
  const logger = require('../utils/logger');
18
+ const { getContainerPort } = require('../utils/port-resolver');
18
19
 
19
20
  /**
20
21
  * Checks if Dockerfile exists and validates overwrite permission
@@ -52,7 +53,7 @@ async function loadAppConfig(configPath, options) {
52
53
  const variables = yaml.load(yamlContent);
53
54
  return {
54
55
  language: options.language || variables.build?.language || 'typescript',
55
- port: variables.build?.port || variables.port || 3000,
56
+ port: getContainerPort(variables, 3000),
56
57
  ...variables
57
58
  };
58
59
  } catch {
package/lib/app/list.js CHANGED
@@ -286,18 +286,25 @@ function handleListResponse(response, actualControllerUrl) {
286
286
  }
287
287
 
288
288
  /**
289
- * List applications in an environment
289
+ * List applications in an environment.
290
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
290
291
  * @async
291
- * @param {Object} options - Command options
292
- * @param {string} options.environment - Environment ID or key
293
- * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
292
+ * @param {Object} [_options] - Command options (reserved)
294
293
  * @throws {Error} If listing fails
295
294
  */
296
- async function listApplications(options) {
297
- const config = await getConfig();
295
+ async function listApplications(options = {}) {
296
+ const { resolveControllerUrl } = require('../utils/controller-url');
297
+ const { resolveEnvironment } = require('../core/config');
298
+
299
+ const controllerUrl = options.controller || (await resolveControllerUrl());
300
+ if (!controllerUrl) {
301
+ logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml'));
302
+ process.exit(1);
303
+ return;
304
+ }
298
305
 
299
- // Get authentication token
300
- const controllerUrl = options.controller || null;
306
+ const environment = options.environment || (await resolveEnvironment());
307
+ const config = await getConfig();
301
308
  const { token, actualControllerUrl } = await getListAuthToken(controllerUrl, config);
302
309
 
303
310
  // Check if authentication succeeded (may be null after process.exit in tests)
@@ -308,9 +315,9 @@ async function listApplications(options) {
308
315
  // Use centralized API client
309
316
  const authConfig = { type: 'bearer', token: token };
310
317
  try {
311
- const response = await listEnvironmentApplications(actualControllerUrl, options.environment, authConfig);
318
+ const response = await listEnvironmentApplications(actualControllerUrl, environment, authConfig);
312
319
  const applications = handleListResponse(response, actualControllerUrl);
313
- displayApplications(applications, options.environment, actualControllerUrl);
320
+ displayApplications(applications, environment, actualControllerUrl);
314
321
  } catch (error) {
315
322
  logger.error(chalk.red(`❌ Failed to list applications from controller: ${actualControllerUrl}`));
316
323
  logger.error(chalk.gray(`Error: ${error.message}`));
package/lib/app/readme.js CHANGED
@@ -12,6 +12,7 @@ const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
14
  const handlebars = require('handlebars');
15
+ const { generateExternalReadmeContent } = require('../utils/external-readme');
15
16
 
16
17
  /**
17
18
  * Checks if a file exists
@@ -86,6 +87,27 @@ function extractServiceFlags(config) {
86
87
  };
87
88
  }
88
89
 
90
+ /**
91
+ * Builds placeholder datasources for external README generation
92
+ * @function buildExternalDatasourcePlaceholders
93
+ * @param {number} datasourceCount - Datasource count
94
+ * @returns {Array<Object>} Datasource placeholders
95
+ */
96
+ function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
97
+ const normalizedCount = Number.isInteger(datasourceCount)
98
+ ? datasourceCount
99
+ : parseInt(datasourceCount, 10);
100
+ const total = Number.isFinite(normalizedCount) && normalizedCount > 0 ? normalizedCount : 0;
101
+ return Array.from({ length: total }, (_value, index) => {
102
+ const entityType = `entity${index + 1}`;
103
+ return {
104
+ entityType,
105
+ displayName: `Datasource ${index + 1}`,
106
+ fileName: `${systemKey}-datasource-${entityType}.json`
107
+ };
108
+ });
109
+ }
110
+
89
111
  /**
90
112
  * Builds template context for README generation
91
113
  * @function buildReadmeContext
@@ -95,8 +117,11 @@ function extractServiceFlags(config) {
95
117
  */
96
118
  function buildReadmeContext(appName, config) {
97
119
  const displayName = formatAppDisplayName(appName);
98
- const imageName = `aifabrix/${appName}`;
99
- const port = config.port || 3000;
120
+ const port = config.port ?? 3000;
121
+ const localPort = (typeof config.build?.localPort === 'number' && config.build.localPort > 0)
122
+ ? config.build.localPort
123
+ : port;
124
+ const imageName = config.image?.name || `aifabrix/${appName}`;
100
125
  // Extract registry from nested structure (config.image.registry) or flattened (config.registry)
101
126
  const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
102
127
 
@@ -108,6 +133,7 @@ function buildReadmeContext(appName, config) {
108
133
  displayName,
109
134
  imageName,
110
135
  port,
136
+ localPort,
111
137
  registry,
112
138
  ...serviceFlags,
113
139
  hasAnyService
@@ -115,117 +141,20 @@ function buildReadmeContext(appName, config) {
115
141
  }
116
142
 
117
143
  function generateReadmeMd(appName, config) {
118
- const context = buildReadmeContext(appName, config);
119
- // Always generate comprehensive README programmatically to ensure consistency
120
- // regardless of template file content
121
- return generateComprehensiveReadme(context);
122
- }
123
-
124
- /**
125
- * Generates comprehensive README.md content programmatically
126
- * @param {Object} context - Template context
127
- * @returns {string} Comprehensive README.md content
128
- */
129
- function generateComprehensiveReadme(context) {
130
- const { appName, displayName, imageName, port, registry, hasDatabase, hasRedis, hasStorage, hasAuthentication, hasAnyService } = context;
131
-
132
- let prerequisites = 'Before running this application, ensure the following prerequisites are met:\n';
133
- prerequisites += '- `@aifabrix/builder` installed globally\n';
134
- prerequisites += '- Docker Desktop running\n';
135
-
136
- if (hasAnyService) {
137
- if (hasDatabase) {
138
- prerequisites += '- PostgreSQL database\n';
139
- }
140
- if (hasRedis) {
141
- prerequisites += '- Redis\n';
142
- }
143
- if (hasStorage) {
144
- prerequisites += '- File storage configured\n';
145
- }
146
- if (hasAuthentication) {
147
- prerequisites += '- Authentication/RBAC configured\n';
148
- }
149
- } else {
150
- prerequisites += '- Infrastructure running\n';
144
+ if (config.type === 'external') {
145
+ const systemKey = config.systemKey || appName;
146
+ const datasources = buildExternalDatasourcePlaceholders(systemKey, config.datasourceCount);
147
+ return generateExternalReadmeContent({
148
+ appName,
149
+ systemKey,
150
+ systemType: config.systemType,
151
+ displayName: config.systemDisplayName,
152
+ description: config.systemDescription,
153
+ datasources
154
+ });
151
155
  }
152
-
153
- let troubleshooting = '';
154
- if (hasDatabase) {
155
- troubleshooting = `### Database Connection Issues
156
-
157
- If you encounter database connection errors, ensure:
158
- - PostgreSQL is running and accessible
159
- - Database credentials are correctly configured in your \`.env\` file
160
- - The database name matches your configuration
161
- - Verify infrastructure is running and PostgreSQL is accessible`;
162
- } else {
163
- troubleshooting = 'Verify infrastructure is running.';
164
- }
165
-
166
- return `# ${displayName} Builder
167
-
168
- Build, run, and deploy ${displayName}.
169
-
170
- ## Prerequisites
171
-
172
- ${prerequisites}
173
-
174
- ## Quick Start
175
-
176
- ### 1. Install
177
-
178
- Install the AI Fabrix Builder CLI if you haven't already.
179
-
180
- ### 2. Configure
181
-
182
- Configure your application settings in \`variables.yaml\`.
183
-
184
- ### 3. Build & Run Locally
185
-
186
- Build the application:
187
- \`\`\`bash
188
- aifabrix build ${appName}
189
- \`\`\`
190
-
191
- Run the application:
192
- \`\`\`bash
193
- aifabrix run ${appName}
194
- \`\`\`
195
-
196
- The application will be available at http://localhost:${port} (default: ${port}).
197
-
198
- ### 4. Deploy to Azure
199
-
200
- Push to registry:
201
- \`\`\`bash
202
- aifabrix push ${appName} --registry ${registry} --tag "v1.0.0,latest"
203
- \`\`\`
204
-
205
- ## Configuration
206
-
207
- - **Port**: ${port} (default: 3000)
208
- - **Image**: ${imageName}:latest
209
- - **Registry**: ${registry}
210
-
211
- ## Docker Commands
212
-
213
- View logs:
214
- \`\`\`bash
215
- docker logs aifabrix-${appName} -f
216
- \`\`\`
217
-
218
- Stop the application:
219
- \`\`\`bash
220
- aifabrix down ${appName}
221
- \`\`\`
222
-
223
- ## Troubleshooting
224
-
225
- ${troubleshooting}
226
-
227
- For more information, see the [AI Fabrix Builder documentation](https://docs.aifabrix.com).
228
- `;
156
+ const context = buildReadmeContext(appName, config);
157
+ return _loadReadmeTemplate()(context);
229
158
  }
230
159
 
231
160
  /**
@@ -43,7 +43,7 @@ function buildRegistrationData(appConfig, options) {
43
43
 
44
44
  // Handle external type vs non-external types differently
45
45
  if (appConfig.appType === 'external') {
46
- // For external type: include externalIntegration, exclude registryMode/port/image
46
+ // For external type: include externalIntegration, exclude registryMode/port/image/url
47
47
  if (appConfig.externalIntegration) {
48
48
  registrationData.externalIntegration = appConfig.externalIntegration;
49
49
  }
@@ -60,6 +60,12 @@ function buildRegistrationData(appConfig, options) {
60
60
  if (appConfig.image) {
61
61
  registrationData.image = appConfig.image;
62
62
  }
63
+
64
+ // URL: always set when we have port so controller DB has it. Precedence: --url, variables (app.url, deployment.dataplaneUrl, deployment.appUrl), else http://localhost:{localPort|port}
65
+ const portForUrl = appConfig.localPort ?? appConfig.port;
66
+ if (portForUrl) {
67
+ registrationData.url = options.url || appConfig.url || `http://localhost:${portForUrl}`;
68
+ }
63
69
  }
64
70
 
65
71
  return registrationData;
@@ -103,20 +109,48 @@ async function saveLocalCredentials(responseData, apiUrl) {
103
109
  }
104
110
 
105
111
  /**
106
- * Register an application
112
+ * For localhost controller: apply developer-id offset to port and URL fallback so the
113
+ * controller can reach the app on the correct Docker/exposed host port.
114
+ * @async
115
+ * @param {Object} appConfig - App config (mutated: port, url)
116
+ * @param {string} apiUrl - Controller API URL
117
+ * @param {Object} options - CLI options (url override)
118
+ */
119
+ async function applyLocalhostPortAdjustment(appConfig, apiUrl, options) {
120
+ if (!isLocalhost(apiUrl) || appConfig.port === null || appConfig.port === undefined) {
121
+ return;
122
+ }
123
+ const { getDeveloperId } = require('../core/config');
124
+ const devId = await getDeveloperId();
125
+ const devIdNum = (devId !== null && devId !== undefined && devId !== '') ? parseInt(devId, 10) : 0;
126
+ if (Number.isNaN(devIdNum) || devIdNum <= 0) {
127
+ return;
128
+ }
129
+ const adjusted = appConfig.port + devIdNum * 100;
130
+ appConfig.port = adjusted;
131
+ if (!options.url && !appConfig.url) {
132
+ appConfig.url = `http://localhost:${adjusted}`;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Register an application.
138
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
107
139
  * @async
108
140
  * @param {string} appKey - Application key
109
141
  * @param {Object} options - Registration options
110
- * @param {string} options.environment - Environment ID or key
111
- * @param {string} [options.controller] - Controller URL (overrides variables.yaml)
112
142
  * @param {number} [options.port] - Application port
143
+ * @param {string} [options.url] - Application URL (overrides variables; see app register --help for fallback when omitted)
113
144
  * @param {string} [options.name] - Override display name
114
145
  * @param {string} [options.description] - Override description
115
146
  * @throws {Error} If registration fails
116
147
  */
117
- async function registerApplication(appKey, options) {
148
+ async function registerApplication(appKey, options = {}) {
118
149
  logger.log(chalk.blue('📋 Registering application...\n'));
119
150
 
151
+ const { resolveControllerUrl } = require('../utils/controller-url');
152
+ const { resolveEnvironment } = require('../core/config');
153
+
120
154
  // Load variables.yaml
121
155
  const { variables, created } = await loadVariablesYaml(appKey);
122
156
  const finalVariables = created
@@ -127,10 +161,11 @@ async function registerApplication(appKey, options) {
127
161
  const appConfig = await extractAppConfiguration(finalVariables, appKey, options);
128
162
  await validateAppRegistrationData(appConfig, appKey);
129
163
 
130
- // Get controller URL with priority: options.controller > variables.yaml > device tokens
131
- const controllerUrl = options.controller || finalVariables?.deployment?.controllerUrl;
132
- const authConfig = await checkAuthentication(controllerUrl, options.environment);
133
- const environment = registerApplicationSchema.environmentId(options.environment);
164
+ const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
165
+ const authConfig = await checkAuthentication(controllerUrl, environmentKey);
166
+ const environment = registerApplicationSchema.environmentId(environmentKey);
167
+
168
+ await applyLocalhostPortAdjustment(appConfig, authConfig.apiUrl, options);
134
169
 
135
170
  // Register application
136
171
  const registrationData = buildRegistrationData(appConfig, options);