@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
@@ -15,6 +15,8 @@ const path = require('path');
15
15
  const handlebars = require('handlebars');
16
16
  const config = require('../core/config');
17
17
  const buildCopy = require('./build-copy');
18
+ const { formatMissingDbPasswordError } = require('./error-formatter');
19
+ const { getContainerPort } = require('./port-resolver');
18
20
 
19
21
  // Register commonly used helpers
20
22
  handlebars.registerHelper('eq', (a, b) => a === b);
@@ -150,6 +152,71 @@ function buildHealthCheckConfig(config) {
150
152
  };
151
153
  }
152
154
 
155
+ /**
156
+ * Derives base path from routing pattern by removing trailing wildcards
157
+ * @param {string} pattern - URL pattern (e.g., '/app/*', '/api/v1/*')
158
+ * @returns {string} Base path for routing
159
+ */
160
+ function derivePathFromPattern(pattern) {
161
+ if (!pattern || typeof pattern !== 'string') {
162
+ return '/';
163
+ }
164
+ const trimmed = pattern.trim();
165
+ if (trimmed === '/' || trimmed === '') {
166
+ return '/';
167
+ }
168
+ const withoutWildcards = trimmed.replace(/\*+$/g, '');
169
+ const withoutTrailingSlashes = withoutWildcards.replace(/\/+$/g, '');
170
+ return withoutTrailingSlashes || '/';
171
+ }
172
+
173
+ /**
174
+ * Builds developer username from developer ID
175
+ * @param {string|number} devId - Developer ID
176
+ * @returns {string} Developer username (dev, dev01, dev02, ...)
177
+ */
178
+ function buildDevUsername(devId) {
179
+ if (devId === undefined || devId === null) {
180
+ return 'dev';
181
+ }
182
+ const devIdString = String(devId);
183
+ if (devIdString === '0') {
184
+ return 'dev';
185
+ }
186
+ const paddedId = devIdString.length === 1 ? devIdString.padStart(2, '0') : devIdString;
187
+ return `dev${paddedId}`;
188
+ }
189
+
190
+ /**
191
+ * Builds Traefik ingress configuration from frontDoorRouting
192
+ * Resolves ${DEV_USERNAME} variable interpolation in host field
193
+ * @param {Object} config - Application configuration
194
+ * @param {string|number} devId - Developer ID
195
+ * @returns {Object} Traefik configuration object
196
+ */
197
+ function buildTraefikConfig(config, devId) {
198
+ const frontDoor = config.frontDoorRouting;
199
+ if (!frontDoor || frontDoor.enabled !== true) {
200
+ return { enabled: false };
201
+ }
202
+
203
+ if (!frontDoor.host || typeof frontDoor.host !== 'string') {
204
+ throw new Error('frontDoorRouting.host is required when frontDoorRouting.enabled is true');
205
+ }
206
+
207
+ const devUsername = buildDevUsername(devId);
208
+ const host = frontDoor.host.replace(/\$\{DEV_USERNAME\}/g, devUsername);
209
+ const path = derivePathFromPattern(frontDoor.pattern);
210
+
211
+ return {
212
+ enabled: true,
213
+ host,
214
+ path,
215
+ tls: frontDoor.tls !== false,
216
+ certStore: frontDoor.certStore || null
217
+ };
218
+ }
219
+
153
220
  /**
154
221
  * Builds requires configuration section
155
222
  * @param {Object} config - Application configuration
@@ -169,17 +236,12 @@ function buildRequiresConfig(config) {
169
236
  * @param {string} appName - Application name
170
237
  * @param {Object} config - Application configuration
171
238
  * @param {number} port - Application port
239
+ * @param {string|number} devId - Developer ID
172
240
  * @returns {Object} Service configuration
173
241
  */
174
- function buildServiceConfig(appName, config, port) {
175
- // Container port: build.containerPort > config.port (NEVER use host port parameter)
176
- // Container port should remain unchanged regardless of developer ID
177
- const containerPortValue = config.build?.containerPort || config.port || 3000;
178
-
179
- // Host port: use port parameter (already calculated from CLI --port or config.port in generateDockerCompose)
180
- // Note: build.localPort is ONLY used for .env file PORT variable (for local PC dev), NOT for Docker Compose
242
+ function buildServiceConfig(appName, config, port, devId) {
243
+ const containerPortValue = getContainerPort(config, 3000);
181
244
  const hostPort = port;
182
-
183
245
  return {
184
246
  app: buildAppConfig(appName, config),
185
247
  image: buildImageConfig(config, appName),
@@ -190,6 +252,7 @@ function buildServiceConfig(appName, config, port) {
190
252
  localPort: config.build?.localPort || null // Only used for .env file PORT variable, not for Docker Compose
191
253
  },
192
254
  healthCheck: buildHealthCheckConfig(config),
255
+ traefik: buildTraefikConfig(config, devId),
193
256
  ...buildRequiresConfig(config)
194
257
  };
195
258
  }
@@ -200,11 +263,7 @@ function buildServiceConfig(appName, config, port) {
200
263
  * @returns {Object} Volumes configuration
201
264
  */
202
265
  function buildVolumesConfig(appName) {
203
- // Use forward slashes for Docker paths (works on both Windows and Unix)
204
- const volumePath = path.join(process.cwd(), 'data', appName);
205
- return {
206
- mountVolume: volumePath.replace(/\\/g, '/')
207
- };
266
+ return { mountVolume: path.join(process.cwd(), 'data', appName).replace(/\\/g, '/') };
208
267
  }
209
268
 
210
269
  /**
@@ -213,23 +272,9 @@ function buildVolumesConfig(appName) {
213
272
  * @returns {Object} Networks configuration
214
273
  */
215
274
  function buildNetworksConfig(config) {
216
- // Get databases from requires.databases or top-level databases
217
- const databases = config.requires?.databases || config.databases || [];
218
- return {
219
- databases: databases
220
- };
275
+ return { databases: config.requires?.databases || config.databases || [] };
221
276
  }
222
277
 
223
- /**
224
- * Reads database passwords from .env file
225
- * Requires DB_0_PASSWORD, DB_1_PASSWORD, etc. to be set in .env file
226
- * @async
227
- * @param {string} envPath - Path to .env file
228
- * @param {Array<Object>} databases - Array of database configurations
229
- * @param {string} appKey - Application key (fallback for single database)
230
- * @returns {Promise<Object>} Object with passwords array and lookup map
231
- * @throws {Error} If required password variables are missing
232
- */
233
278
  /**
234
279
  * Reads and parses .env file
235
280
  * @async
@@ -276,17 +321,21 @@ async function readEnvFile(envPath) {
276
321
  * @function extractPassword
277
322
  * @param {Object} envVars - Environment variables
278
323
  * @param {string} passwordKey - Password key to look up
324
+ * @param {Object} [context] - Optional: { appKey, multi } for clearer error messages
279
325
  * @returns {string} Password value
280
326
  * @throws {Error} If password is missing or empty
281
327
  */
282
- function extractPassword(envVars, passwordKey) {
328
+ function extractPassword(envVars, passwordKey, context = {}) {
329
+ const { appKey, multi } = context;
330
+ const appSuffix = appKey ? ` for application '${appKey}'` : '';
331
+
283
332
  if (!(passwordKey in envVars)) {
284
- throw new Error(`Missing required password variable ${passwordKey} in .env file`);
333
+ throw new Error(multi && appKey ? formatMissingDbPasswordError(appKey, { multiDb: true, passwordKey }) : 'Missing required password variable ' + passwordKey + ' in .env file' + appSuffix + '. Add ' + passwordKey + '=your_secret to your .env file.');
285
334
  }
286
335
 
287
336
  const password = envVars[passwordKey].trim();
288
337
  if (!password || password.length === 0) {
289
- throw new Error(`Password variable ${passwordKey} is empty in .env file`);
338
+ throw new Error('Password variable ' + passwordKey + ' is empty in .env file' + appSuffix + '. Set a non-empty value.');
290
339
  }
291
340
 
292
341
  return password;
@@ -303,17 +352,14 @@ function extractPassword(envVars, passwordKey) {
303
352
  function processMultipleDatabases(databases, envVars, appKey) {
304
353
  const passwords = {};
305
354
  const passwordsArray = [];
306
-
307
355
  for (let i = 0; i < databases.length; i++) {
308
356
  const db = databases[i];
309
357
  const dbName = db.name || appKey;
310
358
  const passwordKey = `DB_${i}_PASSWORD`;
311
- const password = extractPassword(envVars, passwordKey);
312
-
359
+ const password = extractPassword(envVars, passwordKey, { appKey, multi: true });
313
360
  passwords[dbName] = password;
314
361
  passwordsArray.push(password);
315
362
  }
316
-
317
363
  return { passwords, passwordsArray };
318
364
  }
319
365
 
@@ -327,47 +373,36 @@ function processMultipleDatabases(databases, envVars, appKey) {
327
373
  function processSingleDatabase(envVars, appKey) {
328
374
  const passwords = {};
329
375
  const passwordsArray = [];
330
-
331
- // Single database case - use DB_0_PASSWORD or DB_PASSWORD
332
376
  const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
333
-
334
377
  if (!(passwordKey in envVars)) {
335
- throw new Error(`Missing required password variable ${passwordKey} in .env file. Add DB_0_PASSWORD or DB_PASSWORD to your .env file.`);
378
+ throw new Error(formatMissingDbPasswordError(appKey));
336
379
  }
337
-
338
- const password = extractPassword(envVars, passwordKey);
380
+ const password = extractPassword(envVars, passwordKey, { appKey });
339
381
  passwords[appKey] = password;
340
382
  passwordsArray.push(password);
341
-
342
383
  return { passwords, passwordsArray };
343
384
  }
344
385
 
386
+ /**
387
+ * Reads database passwords from .env file
388
+ * @async
389
+ * @function readDatabasePasswords
390
+ * @param {string} envPath - Path to .env file
391
+ * @param {Array<Object>} databases - Array of database configurations
392
+ * @param {string} appKey - Application key (fallback for single database)
393
+ * @returns {Promise<Object>} Object with passwords map and array
394
+ * @throws {Error} If required password variables are missing
395
+ */
345
396
  async function readDatabasePasswords(envPath, databases, appKey) {
346
397
  const envVars = await readEnvFile(envPath);
347
-
348
- // Process each database
349
398
  if (databases && databases.length > 0) {
350
399
  const { passwords, passwordsArray } = processMultipleDatabases(databases, envVars, appKey);
351
- return {
352
- map: passwords,
353
- array: passwordsArray
354
- };
400
+ return { map: passwords, array: passwordsArray };
355
401
  }
356
-
357
402
  const { passwords, passwordsArray } = processSingleDatabase(envVars, appKey);
358
- return {
359
- map: passwords,
360
- array: passwordsArray
361
- };
403
+ return { map: passwords, array: passwordsArray };
362
404
  }
363
405
 
364
- /**
365
- * Generates Docker Compose configuration from template
366
- * @param {string} appName - Application name
367
- * @param {Object} appConfig - Application configuration
368
- * @param {Object} options - Run options
369
- * @returns {Promise<string>} Generated compose content
370
- */
371
406
  /**
372
407
  * Gets developer ID and calculates numeric ID
373
408
  * @async
@@ -376,8 +411,7 @@ async function readDatabasePasswords(envPath, databases, appKey) {
376
411
  */
377
412
  async function getDeveloperIdAndNumeric() {
378
413
  const devId = await config.getDeveloperId();
379
- const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
380
- return { devId, idNum };
414
+ return { devId, idNum: typeof devId === 'string' ? parseInt(devId, 10) : devId };
381
415
  }
382
416
 
383
417
  /**
@@ -411,15 +445,22 @@ async function readDatabasePasswordsIfNeeded(requiresDatabase, databases, envFil
411
445
  return { map: {}, array: [] };
412
446
  }
413
447
 
448
+ /**
449
+ * Generates Docker Compose configuration from template
450
+ * @async
451
+ * @function generateDockerCompose
452
+ * @param {string} appName - Application name
453
+ * @param {Object} appConfig - Application configuration
454
+ * @param {Object} options - Run options
455
+ * @returns {Promise<string>} Generated compose content
456
+ */
414
457
  async function generateDockerCompose(appName, appConfig, options) {
415
458
  const language = appConfig.build?.language || appConfig.language || 'typescript';
416
459
  const template = loadDockerComposeTemplate(language);
417
460
  const port = options.port || appConfig.port || 3000;
418
-
419
461
  const { devId, idNum } = await getDeveloperIdAndNumeric();
420
462
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
421
-
422
- const serviceConfig = buildServiceConfig(appName, appConfig, port);
463
+ const serviceConfig = buildServiceConfig(appName, appConfig, port, devId);
423
464
  const volumesConfig = buildVolumesConfig(appName);
424
465
  const networksConfig = buildNetworksConfig(appConfig);
425
466
 
@@ -439,17 +480,19 @@ async function generateDockerCompose(appName, appConfig, options) {
439
480
  ...volumesConfig,
440
481
  ...networksConfig,
441
482
  envFile: envFileAbsolutePath,
442
- databasePasswords: databasePasswords,
483
+ databasePasswords,
443
484
  devId: idNum,
444
- networkName: networkName,
445
- containerName: containerName
485
+ networkName,
486
+ containerName
446
487
  };
447
-
448
488
  return template(templateData);
449
489
  }
450
490
 
451
491
  module.exports = {
452
492
  generateDockerCompose,
453
- getImageName
493
+ getImageName,
494
+ derivePathFromPattern,
495
+ buildTraefikConfig,
496
+ buildDevUsername
454
497
  };
455
498
 
@@ -11,6 +11,7 @@
11
11
 
12
12
  const { getDeveloperIdNumber } = require('./env-map');
13
13
  const devConfig = require('./dev-config');
14
+ const config = require('../core/config');
14
15
 
15
16
  /**
16
17
  * Calculate default controller URL based on developer ID
@@ -28,33 +29,78 @@ async function getDefaultControllerUrl() {
28
29
  return `http://localhost:${ports.app}`;
29
30
  }
30
31
 
32
+ /**
33
+ * Normalize controller URL (remove trailing slashes)
34
+ * @param {string} url - Controller URL to normalize
35
+ * @returns {string} Normalized controller URL
36
+ */
37
+ function normalizeUrl(url) {
38
+ if (!url || typeof url !== 'string') {
39
+ return url;
40
+ }
41
+ return url.trim().replace(/\/+$/, '');
42
+ }
43
+
44
+ /**
45
+ * Get controller URL from logged-in user's device tokens
46
+ * Returns the first available controller URL from device tokens stored in config
47
+ * @async
48
+ * @function getControllerUrlFromLoggedInUser
49
+ * @returns {Promise<string|null>} Controller URL from logged-in user, or null if not found
50
+ */
51
+ async function getControllerUrlFromLoggedInUser() {
52
+ try {
53
+ const userConfig = await config.getConfig();
54
+ if (!userConfig.device || typeof userConfig.device !== 'object') {
55
+ return null;
56
+ }
57
+
58
+ const deviceUrls = Object.keys(userConfig.device);
59
+ if (deviceUrls.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ // Return the first available controller URL (normalized)
64
+ const firstControllerUrl = deviceUrls[0];
65
+ return normalizeUrl(firstControllerUrl);
66
+ } catch (error) {
67
+ // If config doesn't exist or can't be read, return null
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get controller URL from config.yaml
74
+ * @async
75
+ * @function getControllerFromConfig
76
+ * @returns {Promise<string|null>} Controller URL from config or null
77
+ */
78
+ async function getControllerFromConfig() {
79
+ const { getControllerUrl } = require('../core/config');
80
+ return await getControllerUrl();
81
+ }
82
+
31
83
  /**
32
84
  * Resolve controller URL with fallback chain
33
85
  * Priority:
34
- * 1. options.controller (explicit option)
35
- * 2. config.deployment?.controllerUrl (from config)
86
+ * 1. config.controller (from config.yaml)
87
+ * 2. getControllerUrlFromLoggedInUser() (from logged-in device tokens)
36
88
  * 3. getDefaultControllerUrl() (developer ID-based default)
37
89
  * @async
38
90
  * @function resolveControllerUrl
39
- * @param {Object} options - Command options
40
- * @param {string} [options.controller] - Explicit controller URL option
41
- * @param {Object} config - Configuration object
42
- * @param {Object} [config.deployment] - Deployment configuration
43
- * @param {string} [config.deployment.controllerUrl] - Controller URL from config
44
91
  * @returns {Promise<string>} Resolved controller URL
45
92
  */
46
- async function resolveControllerUrl(options, config) {
47
- // Priority 1: Explicit option
48
- if (options && (options.controller || options.url)) {
49
- const explicitUrl = options.controller || options.url;
50
- if (explicitUrl) {
51
- return explicitUrl.replace(/\/$/, '');
52
- }
93
+ async function resolveControllerUrl() {
94
+ // Priority 1: config.controller (from config.yaml)
95
+ const configController = await getControllerFromConfig();
96
+ if (configController) {
97
+ return configController.replace(/\/+$/, '');
53
98
  }
54
99
 
55
- // Priority 2: Config file
56
- if (config?.deployment?.controllerUrl) {
57
- return config.deployment.controllerUrl.replace(/\/$/, '');
100
+ // Priority 2: Logged-in user's device tokens
101
+ const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
102
+ if (loggedInControllerUrl) {
103
+ return loggedInControllerUrl;
58
104
  }
59
105
 
60
106
  // Priority 3: Developer ID-based default
@@ -63,5 +109,7 @@ async function resolveControllerUrl(options, config) {
63
109
 
64
110
  module.exports = {
65
111
  getDefaultControllerUrl,
112
+ getControllerUrlFromLoggedInUser,
113
+ getControllerFromConfig,
66
114
  resolveControllerUrl
67
115
  };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Dataplane Health Check Utilities
3
+ *
4
+ * Provides utilities for checking dataplane URL health and reachability
5
+ *
6
+ * @fileoverview Dataplane health check utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ /**
12
+ * Tests a single endpoint for reachability
13
+ * @async
14
+ * @function testEndpoint
15
+ * @param {string} testUrl - URL to test
16
+ * @param {number} timeoutMs - Timeout in milliseconds
17
+ * @returns {Promise<boolean>} True if endpoint is reachable
18
+ */
19
+ async function testEndpoint(testUrl, timeoutMs) {
20
+ try {
21
+ const controller = new AbortController();
22
+ const abortTimeoutId = setTimeout(() => controller.abort(), timeoutMs);
23
+ let raceTimeoutId;
24
+
25
+ const response = await Promise.race([
26
+ fetch(testUrl, {
27
+ method: 'GET',
28
+ signal: controller.signal,
29
+ headers: { 'Accept': 'application/json' }
30
+ }),
31
+ new Promise((_, reject) => {
32
+ raceTimeoutId = setTimeout(() => reject(new Error('Timeout')), timeoutMs);
33
+ })
34
+ ]).catch(() => null);
35
+
36
+ clearTimeout(abortTimeoutId);
37
+ if (raceTimeoutId) {
38
+ clearTimeout(raceTimeoutId);
39
+ }
40
+
41
+ // If we get any response (even 404 or 401), the service is reachable
42
+ // 401 is OK because it means the API is working, just needs auth
43
+ if (response && (response.ok || response.status === 404 || response.status === 401)) {
44
+ return true;
45
+ }
46
+
47
+ // If we get a 500 or other server error, the service is up but broken
48
+ // Still consider it "reachable" - the wizard will handle the actual error
49
+ if (response && response.status >= 500) {
50
+ return true;
51
+ }
52
+
53
+ // If response is null (timeout/error), endpoint is not reachable
54
+ return false;
55
+ } catch (error) {
56
+ // Timeout or abort means endpoint is not reachable
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Checks if dataplane URL is reachable and API is functional
63
+ * @async
64
+ * @function checkDataplaneHealth
65
+ * @param {string} dataplaneUrl - Dataplane URL to check
66
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 5000)
67
+ * @returns {Promise<boolean>} True if dataplane is reachable and functional
68
+ */
69
+ async function checkDataplaneHealth(dataplaneUrl, timeoutMs = 5000) {
70
+ if (!dataplaneUrl) {
71
+ return false;
72
+ }
73
+
74
+ const baseUrl = dataplaneUrl.replace(/\/+$/, '');
75
+ const endpointsToTest = ['/health', '/api/v1/health', '/api/health', ''];
76
+
77
+ for (const endpoint of endpointsToTest) {
78
+ const testUrl = endpoint ? `${baseUrl}${endpoint}` : baseUrl;
79
+ const isReachable = await testEndpoint(testUrl, timeoutMs);
80
+ if (isReachable) {
81
+ return true;
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Validates dataplane health before running wizard
90
+ * @async
91
+ * @function validateDataplaneHealth
92
+ * @param {string} dataplaneUrl - Dataplane URL to check
93
+ * @returns {Promise<Error|null>} Error if unhealthy, null if healthy
94
+ */
95
+ async function validateDataplaneHealth(dataplaneUrl) {
96
+ try {
97
+ const isHealthy = await checkDataplaneHealth(dataplaneUrl, 5000);
98
+ if (!isHealthy) {
99
+ return new Error(
100
+ `Dataplane is not reachable at ${dataplaneUrl}.\n\n` +
101
+ 'Please ensure the dataplane service is running and accessible, then try again.\n' +
102
+ `You can check dataplane status with: curl ${dataplaneUrl}/health`
103
+ );
104
+ }
105
+ return null;
106
+ } catch (error) {
107
+ return new Error(`Failed to check dataplane health: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ checkDataplaneHealth,
113
+ validateDataplaneHealth,
114
+ testEndpoint
115
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Dataplane URL Resolver
3
+ *
4
+ * Resolves dataplane URL by discovering from controller
5
+ *
6
+ * @fileoverview Dataplane URL resolution utilities
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { discoverDataplaneUrl } = require('../commands/wizard-dataplane');
12
+
13
+ /**
14
+ * Resolve dataplane URL by discovering from controller
15
+ * @async
16
+ * @function resolveDataplaneUrl
17
+ * @param {string} controllerUrl - Controller URL
18
+ * @param {string} environment - Environment key
19
+ * @param {Object} authConfig - Authentication configuration
20
+ * @returns {Promise<string>} Resolved dataplane URL
21
+ * @throws {Error} If dataplane URL cannot be resolved
22
+ */
23
+ async function resolveDataplaneUrl(controllerUrl, environment, authConfig) {
24
+ return await discoverDataplaneUrl(controllerUrl, environment, authConfig);
25
+ }
26
+
27
+ module.exports = {
28
+ resolveDataplaneUrl
29
+ };
@@ -18,7 +18,9 @@ const BASE_PORTS = {
18
18
  postgres: 5432,
19
19
  redis: 6379,
20
20
  pgadmin: 5050,
21
- redisCommander: 8081
21
+ redisCommander: 8081,
22
+ traefikHttp: 80,
23
+ traefikHttps: 443
22
24
  };
23
25
 
24
26
  /**
@@ -65,7 +67,9 @@ function getDevPorts(developerId) {
65
67
  postgres: BASE_PORTS.postgres + offset,
66
68
  redis: BASE_PORTS.redis + offset,
67
69
  pgadmin: BASE_PORTS.pgadmin + offset,
68
- redisCommander: BASE_PORTS.redisCommander + offset
70
+ redisCommander: BASE_PORTS.redisCommander + offset,
71
+ traefikHttp: BASE_PORTS.traefikHttp + offset,
72
+ traefikHttps: BASE_PORTS.traefikHttps + offset
69
73
  };
70
74
  }
71
75
 
@@ -18,6 +18,7 @@ const devConfig = require('../utils/dev-config');
18
18
  const { rewriteInfraEndpoints } = require('./env-endpoints');
19
19
  const { buildEnvVarMap } = require('./env-map');
20
20
  const { interpolateEnvVars } = require('./secrets-helpers');
21
+ const { getLocalPort } = require('./port-resolver');
21
22
 
22
23
  /**
23
24
  * Read developer ID from config file synchronously
@@ -157,7 +158,7 @@ function extractEnvVarsFromContent(envContent, envVars) {
157
158
  * @returns {Promise<string>} Patched env content
158
159
  */
159
160
  async function patchEnvContentForLocal(envContent, variables) {
160
- const baseAppPort = variables.build?.localPort || variables.port || 3000;
161
+ const baseAppPort = getLocalPort(variables, 3000);
161
162
  const appPort = calculateDevAppPort(baseAppPort);
162
163
  const devIdNum = readDeveloperIdFromConfig(config) || 0;
163
164
  const infraPorts = devConfig.getDevPorts(devIdNum);
@@ -11,6 +11,7 @@
11
11
  const fs = require('fs');
12
12
  const yaml = require('js-yaml');
13
13
  const config = require('../core/config');
14
+ const { getLocalPort } = require('./port-resolver');
14
15
 
15
16
  /**
16
17
  * Update PORT in the container's .env file to use variables.port (+offset)
@@ -66,7 +67,7 @@ function updateContainerPortInEnvFile(envPath, variablesPath) {
66
67
  }
67
68
  const variablesContent = fs.readFileSync(variablesPath, 'utf8');
68
69
  const variables = yaml.load(variablesContent);
69
- const basePort = variables?.port || 3000;
70
+ const basePort = getLocalPort(variables, 3000);
70
71
  const devIdNum = getDeveloperIdFromEnvOrConfig();
71
72
  const port = calculatePortWithDevId(basePort, devIdNum);
72
73
  let envContent = fs.readFileSync(envPath, 'utf8');
@@ -20,7 +20,7 @@ const logger = require('../utils/logger');
20
20
  * @param {string} appKey - Application key
21
21
  * @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
22
22
  * @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
23
- * @param {string} _controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
23
+ * @param {string} _controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.dev')
24
24
  * Note: This parameter is accepted for compatibility but the template format http://${MISO_HOST}:${MISO_PORT} is used instead
25
25
  * @returns {Promise<void>} Resolves when template is updated
26
26
  */