@aifabrix/builder 2.32.2 → 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 (130) 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/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  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 +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. package/templates/typescript/docker-compose.hbs +26 -0
package/lib/app/list.js CHANGED
@@ -126,14 +126,35 @@ async function findDeviceTokenFromConfig(deviceConfig) {
126
126
  return null;
127
127
  }
128
128
 
129
+ /**
130
+ * Format URL and port for display
131
+ * @param {Object} app - Application object
132
+ * @returns {string} Formatted URL and port string
133
+ */
134
+ function formatUrlAndPort(app) {
135
+ const url = app.url || app.dataplaneUrl || app.dataplane?.url || app.configuration?.dataplaneUrl || null;
136
+ const port = app.port || app.configuration?.port || null;
137
+
138
+ const parts = [];
139
+ if (url) {
140
+ parts.push(`URL: ${chalk.blue(url)}`);
141
+ }
142
+ if (port) {
143
+ parts.push(`Port: ${chalk.blue(port)}`);
144
+ }
145
+
146
+ return parts.length > 0 ? ` (${parts.join(', ')})` : '';
147
+ }
148
+
129
149
  /**
130
150
  * Display applications list
131
151
  * @param {Array} applications - Array of application objects
132
152
  * @param {string} environment - Environment name or key
153
+ * @param {string} controllerUrl - Controller URL
133
154
  */
134
- function displayApplications(applications, environment) {
155
+ function displayApplications(applications, environment, controllerUrl) {
135
156
  const environmentName = environment || 'miso';
136
- const header = `Applications in ${environmentName} environment`;
157
+ const header = `Applications in ${environmentName} environment (${controllerUrl})`;
137
158
 
138
159
  if (applications.length === 0) {
139
160
  logger.log(chalk.bold(`\n📱 ${header}:\n`));
@@ -144,7 +165,8 @@ function displayApplications(applications, environment) {
144
165
  logger.log(chalk.bold(`\n📱 ${header}:\n`));
145
166
  applications.forEach((app) => {
146
167
  const hasPipeline = app.configuration?.pipeline?.isActive ? '✓' : '✗';
147
- logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})`);
168
+ const urlAndPort = formatUrlAndPort(app);
169
+ logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
148
170
  });
149
171
  logger.log('');
150
172
  }
@@ -152,23 +174,19 @@ function displayApplications(applications, environment) {
152
174
  /**
153
175
  * Try to get device token from controller URL
154
176
  * @async
155
- * @param {string} controllerUrl - Controller URL
156
- * @returns {Promise<Object|null>} Object with token and controllerUrl, or null
177
+ * @param {string} controllerUrl - Controller URL (explicitly provided by user)
178
+ * @returns {Promise<Object|null>} Object with token and controllerUrl, or null if token not found
179
+ * @throws {Error} If authentication/refresh fails
157
180
  */
158
181
  async function tryGetTokenFromController(controllerUrl) {
159
- try {
160
- const normalizedUrl = normalizeControllerUrl(controllerUrl);
161
- const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
162
- if (deviceToken && deviceToken.token) {
163
- return {
164
- token: deviceToken.token,
165
- actualControllerUrl: deviceToken.controller || normalizedUrl
166
- };
167
- }
168
- } catch (error) {
169
- logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
170
- logger.error(chalk.gray(`Error: ${error.message}`));
171
- process.exit(1);
182
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
183
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
184
+ if (deviceToken && deviceToken.token) {
185
+ // Always use the provided controller URL (normalized) to ensure we use the exact URL the user specified
186
+ return {
187
+ token: deviceToken.token,
188
+ actualControllerUrl: normalizedUrl
189
+ };
172
190
  }
173
191
  return null;
174
192
  }
@@ -215,25 +233,36 @@ function validateAuthToken(token, actualControllerUrl, controllerUrl) {
215
233
  /**
216
234
  * Get authentication token for listing applications
217
235
  * @async
218
- * @param {string} [controllerUrl] - Optional controller URL
236
+ * @param {string} [controllerUrl] - Optional controller URL (if provided, must use this specific URL)
219
237
  * @param {Object} config - Configuration object
220
238
  * @returns {Promise<Object>} Object with token and actualControllerUrl
221
239
  * @throws {Error} If authentication fails
222
240
  */
223
241
  async function getListAuthToken(controllerUrl, config) {
224
- // Try to get token from controller URL first
225
- let authResult = null;
242
+ // If controller URL is explicitly provided, use only that URL (no fallback)
226
243
  if (controllerUrl) {
227
- authResult = await tryGetTokenFromController(controllerUrl);
244
+ const authResult = await tryGetTokenFromController(controllerUrl);
245
+ if (!authResult || !authResult.token) {
246
+ // No token found for explicitly provided controller URL
247
+ logger.error(chalk.red(`❌ No authentication token found for controller: ${controllerUrl}`));
248
+ logger.error(chalk.gray('Please login to this controller using: aifabrix login'));
249
+ process.exit(1);
250
+ // Return to prevent further execution in tests where process.exit is mocked
251
+ return { token: null, actualControllerUrl: null };
252
+ }
253
+ return validateAuthToken(authResult.token, authResult.actualControllerUrl, controllerUrl);
228
254
  }
229
255
 
230
- // If no token yet, try to find device token from config
231
- if (!authResult && config.device) {
232
- authResult = await tryGetTokenFromConfig(config.device);
256
+ // If no controller URL provided, try to find device token from config
257
+ if (config.device) {
258
+ const authResult = await tryGetTokenFromConfig(config.device);
259
+ if (authResult && authResult.token) {
260
+ return validateAuthToken(authResult.token, authResult.actualControllerUrl, null);
261
+ }
233
262
  }
234
263
 
235
- // Validate and return token or exit
236
- return validateAuthToken(authResult?.token || null, authResult?.actualControllerUrl || null, controllerUrl);
264
+ // No token found anywhere
265
+ return validateAuthToken(null, null, null);
237
266
  }
238
267
 
239
268
  /**
@@ -257,26 +286,38 @@ function handleListResponse(response, actualControllerUrl) {
257
286
  }
258
287
 
259
288
  /**
260
- * 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).
261
291
  * @async
262
- * @param {Object} options - Command options
263
- * @param {string} options.environment - Environment ID or key
264
- * @param {string} [options.controller] - Controller URL (optional, uses configured controller if not provided)
292
+ * @param {Object} [_options] - Command options (reserved)
265
293
  * @throws {Error} If listing fails
266
294
  */
267
- async function listApplications(options) {
268
- const config = await getConfig();
295
+ async function listApplications(options = {}) {
296
+ const { resolveControllerUrl } = require('../utils/controller-url');
297
+ const { resolveEnvironment } = require('../core/config');
269
298
 
270
- // Get authentication token
271
- const controllerUrl = options.controller || null;
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
+ }
305
+
306
+ const environment = options.environment || (await resolveEnvironment());
307
+ const config = await getConfig();
272
308
  const { token, actualControllerUrl } = await getListAuthToken(controllerUrl, config);
273
309
 
310
+ // Check if authentication succeeded (may be null after process.exit in tests)
311
+ if (!token || !actualControllerUrl) {
312
+ return;
313
+ }
314
+
274
315
  // Use centralized API client
275
316
  const authConfig = { type: 'bearer', token: token };
276
317
  try {
277
- const response = await listEnvironmentApplications(actualControllerUrl, options.environment, authConfig);
318
+ const response = await listEnvironmentApplications(actualControllerUrl, environment, authConfig);
278
319
  const applications = handleListResponse(response, actualControllerUrl);
279
- displayApplications(applications, options.environment);
320
+ displayApplications(applications, environment, actualControllerUrl);
280
321
  } catch (error) {
281
322
  logger.error(chalk.red(`❌ Failed to list applications from controller: ${actualControllerUrl}`));
282
323
  logger.error(chalk.gray(`Error: ${error.message}`));
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const inquirer = require('inquirer');
12
+ const { getDefaultControllerUrl } = require('../utils/controller-url');
12
13
 
13
14
  /**
14
15
  * Builds basic questions (port, language)
@@ -223,10 +224,11 @@ function buildExternalSystemQuestions(options, appName) {
223
224
 
224
225
  /**
225
226
  * Builds workflow questions (GitHub, Controller)
227
+ * @async
226
228
  * @param {Object} options - Provided options
227
- * @returns {Array} Array of question objects
229
+ * @returns {Promise<Array>} Array of question objects
228
230
  */
229
- function buildWorkflowQuestions(options) {
231
+ async function buildWorkflowQuestions(options) {
230
232
  const questions = [];
231
233
 
232
234
  // GitHub workflows
@@ -255,11 +257,13 @@ function buildWorkflowQuestions(options) {
255
257
  if (!options.controllerUrl && options.controller &&
256
258
  !Object.prototype.hasOwnProperty.call(options, 'controllerUrl')) {
257
259
  const misoHost = process.env.MISO_HOST || 'localhost';
260
+ const defaultControllerUrl = await getDefaultControllerUrl();
261
+ const defaultUrl = defaultControllerUrl.replace('http://localhost:', `http://${misoHost}:`);
258
262
  questions.push({
259
263
  type: 'input',
260
264
  name: 'controllerUrl',
261
265
  message: 'Enter Controller URL:',
262
- default: `http://${misoHost}:3000`,
266
+ default: defaultUrl,
263
267
  when: (answers) => answers.controller === true
264
268
  });
265
269
  }
@@ -424,14 +428,14 @@ async function promptForOptions(appName, options) {
424
428
  // For external type, prompt for external system configuration
425
429
  questions = [
426
430
  ...buildExternalSystemQuestions(options, appName),
427
- ...buildWorkflowQuestions(options)
431
+ ...(await buildWorkflowQuestions(options))
428
432
  ];
429
433
  } else {
430
434
  // For regular apps, use standard prompts
431
435
  questions = [
432
436
  ...buildBasicQuestions(options, appType),
433
437
  ...buildServiceQuestions(options, appType),
434
- ...buildWorkflowQuestions(options)
438
+ ...(await buildWorkflowQuestions(options))
435
439
  ];
436
440
  }
437
441
 
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);
@@ -9,7 +9,8 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const { getConfig, normalizeControllerUrl } = require('../core/config');
12
+ const { getConfig, normalizeControllerUrl, resolveEnvironment } = require('../core/config');
13
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
14
  const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
15
  const { rotateApplicationSecret } = require('../api/applications.api');
15
16
  const { formatApiError } = require('../utils/api-error-handler');
@@ -61,6 +62,22 @@ function validateEnvironment(environment) {
61
62
  }
62
63
  }
63
64
 
65
+ /**
66
+ * Resolve controller URL and environment from config; exit if controller is missing.
67
+ * @async
68
+ * @returns {Promise<{controllerUrl: string, environment: string}>}
69
+ */
70
+ async function resolveControllerAndEnvironment() {
71
+ const controllerUrl = await resolveControllerUrl();
72
+ if (!controllerUrl) {
73
+ logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml'));
74
+ process.exit(1);
75
+ }
76
+ const environment = await resolveEnvironment();
77
+ validateEnvironment(environment);
78
+ return { controllerUrl, environment };
79
+ }
80
+
64
81
  /**
65
82
  * Validate credentials object structure
66
83
  * @param {Object} credentials - Credentials object to validate
@@ -152,7 +169,8 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
152
169
  logger.log(chalk.green('✅ Secret rotated successfully!\n'));
153
170
  logger.log(chalk.bold('📋 Application Details:'));
154
171
  logger.log(` Key: ${appKey}`);
155
- logger.log(` Environment: ${environment}\n`);
172
+ logger.log(` Environment: ${environment}`);
173
+ logger.log(` Controller: ${apiUrl}\n`);
156
174
 
157
175
  logger.log(chalk.bold.yellow('🔑 NEW CREDENTIALS:'));
158
176
  logger.log(chalk.yellow(` Client ID: ${credentials.clientId}`));
@@ -285,46 +303,46 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
285
303
  }
286
304
 
287
305
  /**
288
- * Rotate secret for an application
306
+ * Call rotate API, validate response, save locally, and display results.
307
+ * @async
308
+ * @param {string} appKey - Application key
309
+ * @param {string} actualControllerUrl - Resolved controller URL
310
+ * @param {string} environment - Environment ID or key
311
+ * @param {string} token - Bearer token
312
+ */
313
+ async function executeRotation(appKey, actualControllerUrl, environment, token) {
314
+ const authConfig = { type: 'bearer', token };
315
+ const response = await rotateApplicationSecret(actualControllerUrl, environment, appKey, authConfig);
316
+
317
+ if (!response.success) {
318
+ const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
319
+ logger.error(formattedError);
320
+ logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
321
+ process.exit(1);
322
+ }
323
+
324
+ const { credentials, message } = validateResponse(response);
325
+ await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
326
+ displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
327
+ }
328
+
329
+ /**
330
+ * Rotate secret for an application.
331
+ * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
289
332
  * @async
290
333
  * @param {string} appKey - Application key
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)
334
+ * @param {Object} [_options] - Command options (reserved)
294
335
  * @throws {Error} If rotation fails
295
336
  */
296
- async function rotateSecret(appKey, options) {
337
+ async function rotateSecret(appKey, _options = {}) {
297
338
  logger.log(chalk.yellow('⚠️ This will invalidate the old ClientSecret!\n'));
298
339
 
340
+ const { controllerUrl, environment } = await resolveControllerAndEnvironment();
299
341
  const config = await getConfig();
300
-
301
- // Get authentication token
302
- const controllerUrl = options.controller || null;
303
342
  const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
304
343
 
305
- // Validate environment
306
- validateEnvironment(options.environment);
307
-
308
- // Use centralized API client
309
- const authConfig = { type: 'bearer', token: token };
310
344
  try {
311
- const response = await rotateApplicationSecret(actualControllerUrl, options.environment, appKey, authConfig);
312
-
313
- if (!response.success) {
314
- const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
315
- logger.error(formattedError);
316
- logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
317
- process.exit(1);
318
- }
319
-
320
- // Validate response structure and extract credentials
321
- const { credentials, message } = validateResponse(response);
322
-
323
- // Save credentials locally
324
- await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
325
-
326
- // Display results
327
- displayRotationResults(appKey, options.environment, credentials, actualControllerUrl, message);
345
+ await executeRotation(appKey, actualControllerUrl, environment, token);
328
346
  } catch (error) {
329
347
  logger.error(chalk.red(`❌ Failed to rotate secret via controller: ${actualControllerUrl}`));
330
348
  logger.error(chalk.gray(`Error: ${error.message}`));