@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
@@ -9,234 +9,32 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const { exec } = require('child_process');
13
- const { promisify } = require('util');
14
12
  const path = require('path');
15
13
  const fs = require('fs');
16
- const handlebars = require('handlebars');
17
- const secrets = require('../core/secrets');
18
14
  const config = require('../core/config');
19
15
  const devConfig = require('../utils/dev-config');
20
16
  const logger = require('../utils/logger');
21
- const containerUtils = require('../utils/infra-containers');
22
17
  const dockerUtils = require('../utils/docker');
23
18
  const paths = require('../utils/paths');
24
19
  const statusHelpers = require('../utils/infra-status');
25
-
26
- // Register Handlebars helper for equality check
27
- // Handles both strict equality and numeric string comparisons
28
- // Treats null/undefined as equivalent to "0" (default infrastructure)
29
- handlebars.registerHelper('eq', (a, b) => {
30
- // Handle null/undefined - treat as "0" for default infrastructure
31
- if (a === null || a === undefined) a = '0';
32
- if (b === null || b === undefined) b = '0';
33
-
34
- // If both are numeric strings or one is number and other is numeric string, compare as numbers
35
- const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
36
- const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
37
- // Use numeric comparison if both are numbers, otherwise strict equality
38
- if (typeof aNum === 'number' && typeof bNum === 'number') {
39
- return aNum === bNum;
40
- }
41
- return a === b;
42
- });
43
- const execAsync = promisify(exec);
44
-
45
- /**
46
- * Gets infrastructure directory name based on developer ID
47
- * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
48
- * @param {number|string} devId - Developer ID
49
- * @returns {string} Infrastructure directory name
50
- */
51
- function getInfraDirName(devId) {
52
- const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
53
- return idNum === 0 ? 'infra' : `infra-dev${devId}`;
54
- }
55
-
56
- /**
57
- * Gets Docker Compose project name based on developer ID
58
- * Dev 0: infra (no dev-0 suffix), Dev > 0: infra-dev{id}
59
- * @param {number|string} devId - Developer ID
60
- * @returns {string} Docker Compose project name
61
- */
62
- function getInfraProjectName(devId) {
63
- const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
64
- return idNum === 0 ? 'infra' : `infra-dev${devId}`;
65
- }
66
-
67
- // Wrapper to support cwd option
68
- function execAsyncWithCwd(command, options = {}) {
69
- return new Promise((resolve, reject) => {
70
- const { cwd, ...execOptions } = options;
71
- exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
72
- if (error) {
73
- reject(error);
74
- } else {
75
- resolve({ stdout, stderr });
76
- }
77
- });
78
- });
79
- }
80
-
81
- /**
82
- * Starts local infrastructure services
83
- * Launches Postgres, Redis, Keycloak, and Controller in Docker containers
84
- *
85
- * @async
86
- * @function startInfra
87
- * @returns {Promise<void>} Resolves when infrastructure is started
88
- * @throws {Error} If Docker is not running or compose fails
89
- *
90
- * @example
91
- * await startInfra();
92
- * // Infrastructure services are now running
93
- */
94
- async function checkDockerAvailability() {
95
- try {
96
- await dockerUtils.ensureDockerAndCompose();
97
- } catch (error) {
98
- throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
99
- }
100
- }
101
-
102
- async function ensureAdminSecrets() {
103
- const adminSecretsPath = path.join(paths.getAifabrixHome(), 'admin-secrets.env');
104
- if (!fs.existsSync(adminSecretsPath)) {
105
- logger.log('Generating admin-secrets.env...');
106
- await secrets.generateAdminSecretsEnv();
107
- }
108
- return adminSecretsPath;
109
- }
110
-
111
- /**
112
- * Generates pgAdmin4 configuration files (servers.json and pgpass)
113
- * @param {string} infraDir - Infrastructure directory path
114
- * @param {string} postgresPassword - PostgreSQL password
115
- */
116
- function generatePgAdminConfig(infraDir, postgresPassword) {
117
- const serversJsonTemplatePath = path.join(__dirname, '..', 'templates', 'infra', 'servers.json.hbs');
118
- if (!fs.existsSync(serversJsonTemplatePath)) {
119
- return;
120
- }
121
-
122
- const serversJsonTemplateContent = fs.readFileSync(serversJsonTemplatePath, 'utf8');
123
- const serversJsonTemplate = handlebars.compile(serversJsonTemplateContent);
124
- const serversJsonContent = serversJsonTemplate({ postgresPassword });
125
- const serversJsonPath = path.join(infraDir, 'servers.json');
126
- fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
127
-
128
- const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
129
- const pgpassPath = path.join(infraDir, 'pgpass');
130
- fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
131
- }
132
-
133
- /**
134
- * Prepare infrastructure directory and extract postgres password
135
- * @param {string} devId - Developer ID
136
- * @param {string} adminSecretsPath - Path to admin secrets file
137
- * @returns {Object} Object with infraDir and postgresPassword
138
- */
139
- function prepareInfraDirectory(devId, adminSecretsPath) {
140
- const aifabrixDir = paths.getAifabrixHome();
141
- const infraDirName = getInfraDirName(devId);
142
- const infraDir = path.join(aifabrixDir, infraDirName);
143
- if (!fs.existsSync(infraDir)) {
144
- fs.mkdirSync(infraDir, { recursive: true });
145
- }
146
-
147
- const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
148
- const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
149
- const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
150
- generatePgAdminConfig(infraDir, postgresPassword);
151
-
152
- return { infraDir, postgresPassword };
153
- }
154
-
155
- /**
156
- * Register Handlebars helper for equality comparison
157
- */
158
- function registerHandlebarsHelper() {
159
- handlebars.registerHelper('eq', (a, b) => {
160
- if (a === null || a === undefined) a = '0';
161
- if (b === null || b === undefined) b = '0';
162
- const aNum = typeof a === 'string' && /^\d+$/.test(a) ? parseInt(a, 10) : a;
163
- const bNum = typeof b === 'string' && /^\d+$/.test(b) ? parseInt(b, 10) : b;
164
- if (typeof aNum === 'number' && typeof bNum === 'number') {
165
- return aNum === bNum;
166
- }
167
- return a === b;
168
- });
169
- }
170
-
171
- /**
172
- * Generate docker-compose file from template
173
- * @param {string} templatePath - Path to compose template
174
- * @param {string} devId - Developer ID
175
- * @param {number} idNum - Developer ID number
176
- * @param {Object} ports - Port configuration
177
- * @param {string} infraDir - Infrastructure directory
178
- * @returns {string} Path to generated compose file
179
- */
180
- function generateComposeFile(templatePath, devId, idNum, ports, infraDir) {
181
- const templateContent = fs.readFileSync(templatePath, 'utf8');
182
- const template = handlebars.compile(templateContent);
183
- const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
184
- const serversJsonPath = path.join(infraDir, 'servers.json');
185
- const pgpassPath = path.join(infraDir, 'pgpass');
186
- const composeContent = template({
187
- devId: devId,
188
- postgresPort: ports.postgres,
189
- redisPort: ports.redis,
190
- pgadminPort: ports.pgadmin,
191
- redisCommanderPort: ports.redisCommander,
192
- networkName: networkName,
193
- serversJsonPath: serversJsonPath,
194
- pgpassPath: pgpassPath,
195
- infraDir: infraDir
196
- });
197
- const composePath = path.join(infraDir, 'compose.yaml');
198
- fs.writeFileSync(composePath, composeContent);
199
- return composePath;
200
- }
201
-
202
- /**
203
- * Start Docker services using docker-compose
204
- * @async
205
- * @param {string} composePath - Path to compose file
206
- * @param {string} projectName - Docker project name
207
- * @param {string} adminSecretsPath - Path to admin secrets file
208
- * @param {string} infraDir - Infrastructure directory
209
- */
210
- async function startDockerServices(composePath, projectName, adminSecretsPath, infraDir) {
211
- logger.log(`Using compose file: ${composePath}`);
212
- logger.log('Starting infrastructure services...');
213
- const composeCmd = await dockerUtils.getComposeCommand();
214
- await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
215
- logger.log('Infrastructure services started successfully');
216
- }
217
-
218
- /**
219
- * Copy pgAdmin4 configuration files into container
220
- * @async
221
- * @param {string} pgadminContainerName - pgAdmin container name
222
- * @param {string} serversJsonPath - Path to servers.json file
223
- * @param {string} pgpassPath - Path to pgpass file
224
- */
225
- async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath) {
226
- try {
227
- await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
228
- if (fs.existsSync(serversJsonPath)) {
229
- await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
230
- }
231
- if (fs.existsSync(pgpassPath)) {
232
- await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
233
- await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
234
- }
235
- } catch (error) {
236
- // Ignore copy errors - files might already be there or container not ready
237
- logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
238
- }
239
- }
20
+ const {
21
+ getInfraDirName,
22
+ getInfraProjectName,
23
+ checkDockerAvailability,
24
+ ensureAdminSecrets,
25
+ prepareInfraDirectory,
26
+ registerHandlebarsHelper
27
+ } = require('./helpers');
28
+ const {
29
+ buildTraefikConfig,
30
+ validateTraefikConfig,
31
+ generateComposeFile
32
+ } = require('./compose');
33
+ const {
34
+ execAsyncWithCwd,
35
+ startDockerServicesAndConfigure,
36
+ checkInfraHealth
37
+ } = require('./services');
240
38
 
241
39
  /**
242
40
  * Prepares infrastructure environment
@@ -254,7 +52,7 @@ async function prepareInfrastructureEnvironment(developerId) {
254
52
  const ports = devConfig.getDevPorts(devIdNum);
255
53
  const idNum = devIdNum;
256
54
 
257
- const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
55
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'compose.yaml.hbs');
258
56
  if (!fs.existsSync(templatePath)) {
259
57
  throw new Error(`Compose template not found: ${templatePath}`);
260
58
  }
@@ -266,39 +64,35 @@ async function prepareInfrastructureEnvironment(developerId) {
266
64
  }
267
65
 
268
66
  /**
269
- * Starts Docker services and configures pgAdmin
67
+ * Starts local infrastructure services
68
+ * Launches Postgres, Redis, pgAdmin, and Redis Commander in Docker containers
69
+ *
270
70
  * @async
271
- * @function startDockerServicesAndConfigure
272
- * @param {string} composePath - Compose file path
273
- * @param {string} devId - Developer ID
274
- * @param {number} idNum - Developer ID number
275
- * @param {string} adminSecretsPath - Admin secrets path
276
- * @param {string} infraDir - Infrastructure directory
71
+ * @function startInfra
72
+ * @param {number|string|null} developerId - Developer ID (null = use current)
73
+ * @param {Object} [options] - Infrastructure options
74
+ * @param {boolean} [options.traefik=false] - Include Traefik service
75
+ * @returns {Promise<void>} Resolves when infrastructure is started
76
+ * @throws {Error} If Docker is not running or compose fails
77
+ *
78
+ * @example
79
+ * await startInfra(null, { traefik: true });
80
+ * // Infrastructure services are now running
277
81
  */
278
- async function startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir) {
279
- // Start Docker services
280
- const projectName = getInfraProjectName(devId);
281
- await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
282
-
283
- // Copy pgAdmin4 config files
284
- const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
285
- const serversJsonPath = path.join(infraDir, 'servers.json');
286
- const pgpassPath = path.join(infraDir, 'pgpass');
287
- await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
288
-
289
- // Wait for services to be healthy
290
- await waitForServices(devId);
291
- logger.log('All services are healthy and ready');
292
- }
293
-
294
- async function startInfra(developerId = null) {
82
+ async function startInfra(developerId = null, options = {}) {
295
83
  const { devId, idNum, ports, templatePath, infraDir, adminSecretsPath } = await prepareInfrastructureEnvironment(developerId);
84
+ const { traefik = false } = options;
85
+ const traefikConfig = buildTraefikConfig(traefik);
86
+ const validation = validateTraefikConfig(traefikConfig);
87
+ if (!validation.valid) {
88
+ throw new Error(validation.errors.join('\n'));
89
+ }
296
90
 
297
91
  // Register Handlebars helper
298
92
  registerHandlebarsHelper();
299
93
 
300
94
  // Generate compose file
301
- const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir);
95
+ const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir, { traefik: traefikConfig });
302
96
 
303
97
  try {
304
98
  await startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir);
@@ -381,42 +175,6 @@ async function stopInfraWithVolumes() {
381
175
  }
382
176
  }
383
177
 
384
- /**
385
- * Checks if infrastructure services are running
386
- * Validates that all required services are healthy and accessible
387
- *
388
- * @async
389
- * @function checkInfraHealth
390
- * @returns {Promise<Object>} Health status of each service
391
- * @throws {Error} If health check fails
392
- *
393
- * @example
394
- * const health = await checkInfraHealth();
395
- * // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
396
- */
397
- async function checkInfraHealth(devId = null) {
398
- const developerId = devId || await config.getDeveloperId();
399
- const servicesWithHealthCheck = ['postgres', 'redis'];
400
- const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
401
- const health = {};
402
-
403
- // Check health status for services with health checks
404
- for (const service of servicesWithHealthCheck) {
405
- health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
406
- }
407
-
408
- // Check if services without health checks are running
409
- for (const service of servicesWithoutHealthCheck) {
410
- health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
411
- }
412
-
413
- return health;
414
- }
415
-
416
- // Re-export status helper functions
417
- const getInfraStatus = statusHelpers.getInfraStatus;
418
- const getAppStatus = statusHelpers.getAppStatus;
419
-
420
178
  /**
421
179
  * Restarts a specific infrastructure service
422
180
  * Useful for applying configuration changes
@@ -436,7 +194,7 @@ async function restartService(serviceName) {
436
194
  throw new Error('Service name is required and must be a string');
437
195
  }
438
196
 
439
- const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
197
+ const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander', 'traefik'];
440
198
  if (!validServices.includes(serviceName)) {
441
199
  throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
442
200
  }
@@ -463,34 +221,17 @@ async function restartService(serviceName) {
463
221
  }
464
222
  }
465
223
 
466
- /**
467
- * Waits for services to be healthy
468
- * @private
469
- * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
470
- */
471
- async function waitForServices(devId = null) {
472
- const maxAttempts = 30;
473
- const delay = 2000; // 2 seconds
474
-
475
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
476
- const health = await checkInfraHealth(devId);
477
- const allHealthy = Object.values(health).every(status => status === 'healthy');
478
-
479
- if (allHealthy) {
480
- return;
481
- }
482
-
483
- // Debug logging
484
-
485
- if (attempt < maxAttempts) {
486
- logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
487
- await new Promise(resolve => setTimeout(resolve, delay));
488
- }
489
- }
224
+ // Re-export status helper functions
225
+ const getInfraStatus = statusHelpers.getInfraStatus;
226
+ const getAppStatus = statusHelpers.getAppStatus;
490
227
 
491
- throw new Error('Services failed to become healthy within timeout period');
492
- }
493
228
  module.exports = {
494
- startInfra, stopInfra, stopInfraWithVolumes, checkInfraHealth,
495
- getInfraStatus, getAppStatus, restartService, ensureAdminSecrets
229
+ startInfra,
230
+ stopInfra,
231
+ stopInfraWithVolumes,
232
+ checkInfraHealth,
233
+ getInfraStatus,
234
+ getAppStatus,
235
+ restartService,
236
+ ensureAdminSecrets
496
237
  };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * AI Fabrix Builder Infrastructure Docker Services
3
+ *
4
+ * Handles Docker service operations including starting, stopping, and configuring services.
5
+ *
6
+ * @fileoverview Docker service management for infrastructure
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const { exec } = require('child_process');
12
+ const { promisify } = require('util');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const logger = require('../utils/logger');
16
+ const containerUtils = require('../utils/infra-containers');
17
+ const dockerUtils = require('../utils/docker');
18
+ const config = require('../core/config');
19
+ const { getInfraProjectName } = require('./helpers');
20
+
21
+ const execAsync = promisify(exec);
22
+
23
+ // Wrapper to support cwd option
24
+ function execAsyncWithCwd(command, options = {}) {
25
+ return new Promise((resolve, reject) => {
26
+ const { cwd, ...execOptions } = options;
27
+ exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
28
+ if (error) {
29
+ reject(error);
30
+ } else {
31
+ resolve({ stdout, stderr });
32
+ }
33
+ });
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Start Docker services using docker-compose
39
+ * @async
40
+ * @param {string} composePath - Path to compose file
41
+ * @param {string} projectName - Docker project name
42
+ * @param {string} adminSecretsPath - Path to admin secrets file
43
+ * @param {string} infraDir - Infrastructure directory
44
+ */
45
+ async function startDockerServices(composePath, projectName, adminSecretsPath, infraDir) {
46
+ logger.log(`Using compose file: ${composePath}`);
47
+ logger.log('Starting infrastructure services...');
48
+ const composeCmd = await dockerUtils.getComposeCommand();
49
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
50
+ logger.log('Infrastructure services started successfully');
51
+ }
52
+
53
+ /**
54
+ * Copy pgAdmin4 configuration files into container
55
+ * @async
56
+ * @param {string} pgadminContainerName - pgAdmin container name
57
+ * @param {string} serversJsonPath - Path to servers.json file
58
+ * @param {string} pgpassPath - Path to pgpass file
59
+ */
60
+ async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath) {
61
+ try {
62
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
63
+ if (fs.existsSync(serversJsonPath)) {
64
+ await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
65
+ }
66
+ if (fs.existsSync(pgpassPath)) {
67
+ await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
68
+ await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
69
+ }
70
+ } catch (error) {
71
+ // Ignore copy errors - files might already be there or container not ready
72
+ logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Starts Docker services and configures pgAdmin
78
+ * @async
79
+ * @function startDockerServicesAndConfigure
80
+ * @param {string} composePath - Compose file path
81
+ * @param {string} devId - Developer ID
82
+ * @param {number} idNum - Developer ID number
83
+ * @param {string} adminSecretsPath - Admin secrets path
84
+ * @param {string} infraDir - Infrastructure directory
85
+ */
86
+ async function startDockerServicesAndConfigure(composePath, devId, idNum, adminSecretsPath, infraDir) {
87
+ // Start Docker services
88
+ const projectName = getInfraProjectName(devId);
89
+ await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
90
+
91
+ // Copy pgAdmin4 config files
92
+ const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
93
+ const serversJsonPath = path.join(infraDir, 'servers.json');
94
+ const pgpassPath = path.join(infraDir, 'pgpass');
95
+ await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
96
+
97
+ // Wait for services to be healthy
98
+ await waitForServices(devId);
99
+ logger.log('All services are healthy and ready');
100
+ }
101
+
102
+ /**
103
+ * Waits for services to be healthy
104
+ * @private
105
+ * @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
106
+ */
107
+ async function waitForServices(devId = null) {
108
+ const maxAttempts = 30;
109
+ const delay = 2000; // 2 seconds
110
+
111
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
112
+ const health = await checkInfraHealth(devId);
113
+ const allHealthy = Object.values(health).every(status => status === 'healthy');
114
+
115
+ if (allHealthy) {
116
+ return;
117
+ }
118
+
119
+ if (attempt < maxAttempts) {
120
+ logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
121
+ await new Promise(resolve => setTimeout(resolve, delay));
122
+ }
123
+ }
124
+
125
+ throw new Error('Services failed to become healthy within timeout period');
126
+ }
127
+
128
+ /**
129
+ * Checks if infrastructure services are running
130
+ * Validates that all required services are healthy and accessible
131
+ *
132
+ * @async
133
+ * @function checkInfraHealth
134
+ * @param {number|string|null} [devId] - Developer ID (null = use current)
135
+ * @returns {Promise<Object>} Health status of each service
136
+ * @throws {Error} If health check fails
137
+ *
138
+ * @example
139
+ * const health = await checkInfraHealth();
140
+ * // Returns: { postgres: 'healthy', redis: 'healthy', pgadmin: 'healthy', redis-commander: 'healthy' }
141
+ */
142
+ async function checkInfraHealth(devId = null) {
143
+ const developerId = devId || await config.getDeveloperId();
144
+ const servicesWithHealthCheck = ['postgres', 'redis'];
145
+ const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
146
+ const health = {};
147
+
148
+ // Check health status for services with health checks
149
+ for (const service of servicesWithHealthCheck) {
150
+ health[service] = await containerUtils.checkServiceWithHealthCheck(service, developerId);
151
+ }
152
+
153
+ // Check if services without health checks are running
154
+ for (const service of servicesWithoutHealthCheck) {
155
+ health[service] = await containerUtils.checkServiceWithoutHealthCheck(service, developerId);
156
+ }
157
+
158
+ return health;
159
+ }
160
+
161
+ module.exports = {
162
+ execAsyncWithCwd,
163
+ startDockerServices,
164
+ copyPgAdminConfig,
165
+ startDockerServicesAndConfigure,
166
+ waitForServices,
167
+ checkInfraHealth
168
+ };
@@ -457,20 +457,20 @@
457
457
  },
458
458
  "frontDoorRouting": {
459
459
  "type": "object",
460
- "description": "Front Door routing configuration",
460
+ "description": "Front Door routing configuration for Azure deployments and Traefik ingress for local development",
461
461
  "properties": {
462
462
  "pattern": {
463
463
  "type": "string",
464
- "description": "URL pattern for routing (e.g., '/app/*')",
464
+ "description": "URL pattern for routing (e.g., '/app/*', '/api/v1/*'). Used for both Front Door and Traefik path routing",
465
465
  "pattern": "^/.+"
466
466
  },
467
467
  "requiresRuleSet": {
468
468
  "type": "boolean",
469
- "description": "Whether URL rewriting rule set is required"
469
+ "description": "Whether URL rewriting rule set is required (Azure Front Door only)"
470
470
  },
471
471
  "ruleSetConditions": {
472
472
  "type": "array",
473
- "description": "Rule set conditions for URL rewriting",
473
+ "description": "Rule set conditions for URL rewriting (Azure Front Door only)",
474
474
  "items": {
475
475
  "type": "object",
476
476
  "properties": {
@@ -490,6 +490,25 @@
490
490
  }
491
491
  }
492
492
  }
493
+ },
494
+ "enabled": {
495
+ "type": "boolean",
496
+ "description": "Enable Traefik ingress labels for local development (docker-compose)"
497
+ },
498
+ "host": {
499
+ "type": "string",
500
+ "description": "Hostname for Traefik routing. Supports ${DEV_USERNAME} variable interpolation (e.g., '${DEV_USERNAME}.aifabrix.dev'). Required if enabled is true.",
501
+ "pattern": "^[a-z0-9.$\\{\\}-]+$"
502
+ },
503
+ "tls": {
504
+ "type": "boolean",
505
+ "description": "Enable TLS/HTTPS for Traefik (local development)",
506
+ "default": true
507
+ },
508
+ "certStore": {
509
+ "type": "string",
510
+ "description": "Certificate store name for wildcard certificates. Optional - only needed when using a pre-configured certificate store in Traefik.",
511
+ "pattern": "^[a-z0-9-]+$"
493
512
  }
494
513
  },
495
514
  "additionalProperties": false
@@ -176,8 +176,8 @@
176
176
  "properties":{
177
177
  "expression":{
178
178
  "type":"string",
179
- "description":"Pipe-based DSL expression: '{{raw.path}} | toLower | trim'.",
180
- "pattern":"^\\s*\\{\\{[^}]+\\}\\}(\\s*\\|\\s*[a-zA-Z0-9_]+(\\([^)]*\\))?)*\\s*$"
179
+ "description":"Pipe-based DSL expression: '{{raw.path}} | toLower | trim' or record reference: 'record_ref:customer'.",
180
+ "pattern":"^\\s*((\\{\\{[^}]+\\}\\}(\\s*\\|\\s*[a-zA-Z0-9_]+(\\([^)]*\\))?)*)|(record_ref:[a-z0-9-]+))\\s*$"
181
181
  },
182
182
  "type":{
183
183
  "type":"string",