@aifabrix/builder 2.22.1 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/jest.config.coverage.js +37 -0
  2. package/lib/api/pipeline.api.js +10 -9
  3. package/lib/app-deploy.js +36 -14
  4. package/lib/app-list.js +191 -71
  5. package/lib/app-prompts.js +77 -26
  6. package/lib/app-readme.js +123 -5
  7. package/lib/app-rotate-secret.js +101 -57
  8. package/lib/app-run-helpers.js +200 -172
  9. package/lib/app-run.js +137 -68
  10. package/lib/audit-logger.js +8 -7
  11. package/lib/build.js +161 -250
  12. package/lib/cli.js +73 -65
  13. package/lib/commands/login.js +45 -31
  14. package/lib/commands/logout.js +181 -0
  15. package/lib/commands/secrets-set.js +2 -2
  16. package/lib/commands/secure.js +61 -26
  17. package/lib/config.js +79 -45
  18. package/lib/datasource-deploy.js +89 -29
  19. package/lib/deployer.js +164 -129
  20. package/lib/diff.js +63 -21
  21. package/lib/environment-deploy.js +36 -19
  22. package/lib/external-system-deploy.js +134 -66
  23. package/lib/external-system-download.js +244 -171
  24. package/lib/external-system-test.js +199 -164
  25. package/lib/generator-external.js +145 -72
  26. package/lib/generator-helpers.js +49 -17
  27. package/lib/generator-split.js +105 -58
  28. package/lib/infra.js +101 -131
  29. package/lib/schema/application-schema.json +895 -896
  30. package/lib/schema/env-config.yaml +11 -4
  31. package/lib/template-validator.js +13 -4
  32. package/lib/utils/api.js +8 -8
  33. package/lib/utils/app-register-auth.js +36 -18
  34. package/lib/utils/app-run-containers.js +140 -0
  35. package/lib/utils/auth-headers.js +6 -6
  36. package/lib/utils/build-copy.js +60 -2
  37. package/lib/utils/build-helpers.js +94 -0
  38. package/lib/utils/cli-utils.js +177 -76
  39. package/lib/utils/compose-generator.js +12 -2
  40. package/lib/utils/config-tokens.js +151 -9
  41. package/lib/utils/deployment-errors.js +137 -69
  42. package/lib/utils/deployment-validation-helpers.js +103 -0
  43. package/lib/utils/docker-build.js +57 -0
  44. package/lib/utils/dockerfile-utils.js +13 -3
  45. package/lib/utils/env-copy.js +163 -94
  46. package/lib/utils/env-map.js +226 -86
  47. package/lib/utils/environment-checker.js +2 -2
  48. package/lib/utils/error-formatters/network-errors.js +0 -1
  49. package/lib/utils/external-system-display.js +14 -19
  50. package/lib/utils/external-system-env-helpers.js +107 -0
  51. package/lib/utils/external-system-test-helpers.js +144 -0
  52. package/lib/utils/health-check.js +10 -8
  53. package/lib/utils/infra-status.js +123 -0
  54. package/lib/utils/local-secrets.js +3 -2
  55. package/lib/utils/paths.js +228 -49
  56. package/lib/utils/schema-loader.js +125 -57
  57. package/lib/utils/token-manager.js +10 -7
  58. package/lib/utils/yaml-preserve.js +55 -16
  59. package/lib/validate.js +87 -89
  60. package/package.json +4 -4
  61. package/scripts/ci-fix.sh +19 -0
  62. package/scripts/ci-simulate.sh +19 -0
  63. package/templates/applications/miso-controller/test.yaml +1 -0
  64. package/templates/python/Dockerfile.hbs +8 -45
  65. package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/infra.js CHANGED
@@ -21,6 +21,7 @@ const logger = require('./utils/logger');
21
21
  const containerUtils = require('./utils/infra-containers');
22
22
  const dockerUtils = require('./utils/docker');
23
23
  const paths = require('./utils/paths');
24
+ const statusHelpers = require('./utils/infra-status');
24
25
 
25
26
  // Register Handlebars helper for equality check
26
27
  // Handles both strict equality and numeric string comparisons
@@ -129,20 +130,13 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
129
130
  fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
130
131
  }
131
132
 
132
- async function startInfra(developerId = null) {
133
- await checkDockerAvailability();
134
- const adminSecretsPath = await ensureAdminSecrets();
135
-
136
- const devId = developerId || await config.getDeveloperId();
137
- const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
138
- const ports = devConfig.getDevPorts(devIdNum);
139
- const idNum = devIdNum;
140
-
141
- const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
142
- if (!fs.existsSync(templatePath)) {
143
- throw new Error(`Compose template not found: ${templatePath}`);
144
- }
145
-
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) {
146
140
  const aifabrixDir = paths.getAifabrixHome();
147
141
  const infraDirName = getInfraDirName(devId);
148
142
  const infraDir = path.join(aifabrixDir, infraDirName);
@@ -155,6 +149,13 @@ async function startInfra(developerId = null) {
155
149
  const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
156
150
  generatePgAdminConfig(infraDir, postgresPassword);
157
151
 
152
+ return { infraDir, postgresPassword };
153
+ }
154
+
155
+ /**
156
+ * Register Handlebars helper for equality comparison
157
+ */
158
+ function registerHandlebarsHelper() {
158
159
  handlebars.registerHelper('eq', (a, b) => {
159
160
  if (a === null || a === undefined) a = '0';
160
161
  if (b === null || b === undefined) b = '0';
@@ -165,6 +166,18 @@ async function startInfra(developerId = null) {
165
166
  }
166
167
  return a === b;
167
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) {
168
181
  const templateContent = fs.readFileSync(templatePath, 'utf8');
169
182
  const template = handlebars.compile(templateContent);
170
183
  const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
@@ -183,31 +196,83 @@ async function startInfra(developerId = null) {
183
196
  });
184
197
  const composePath = path.join(infraDir, 'compose.yaml');
185
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
+ }
240
+
241
+ async function startInfra(developerId = null) {
242
+ await checkDockerAvailability();
243
+ const adminSecretsPath = await ensureAdminSecrets();
244
+
245
+ const devId = developerId || await config.getDeveloperId();
246
+ const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
247
+ const ports = devConfig.getDevPorts(devIdNum);
248
+ const idNum = devIdNum;
249
+
250
+ const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
251
+ if (!fs.existsSync(templatePath)) {
252
+ throw new Error(`Compose template not found: ${templatePath}`);
253
+ }
254
+
255
+ // Prepare infrastructure directory
256
+ const { infraDir } = prepareInfraDirectory(devId, adminSecretsPath);
257
+
258
+ // Register Handlebars helper
259
+ registerHandlebarsHelper();
260
+
261
+ // Generate compose file
262
+ const composePath = generateComposeFile(templatePath, devId, idNum, ports, infraDir);
186
263
 
187
264
  try {
188
- logger.log(`Using compose file: ${composePath}`);
189
- logger.log(`Starting infrastructure services for developer ${devId}...`);
265
+ // Start Docker services
190
266
  const projectName = getInfraProjectName(devId);
191
- const composeCmd = await dockerUtils.getComposeCommand();
192
- await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
193
- logger.log('Infrastructure services started successfully');
267
+ await startDockerServices(composePath, projectName, adminSecretsPath, infraDir);
194
268
 
195
- // Copy pgAdmin4 config files into container after it starts
269
+ // Copy pgAdmin4 config files
196
270
  const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
197
- try {
198
- await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
199
- if (fs.existsSync(serversJsonPath)) {
200
- await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
201
- }
202
- if (fs.existsSync(pgpassPath)) {
203
- await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
204
- await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
205
- }
206
- } catch (error) {
207
- // Ignore copy errors - files might already be there or container not ready
208
- logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
209
- }
271
+ const serversJsonPath = path.join(infraDir, 'servers.json');
272
+ const pgpassPath = path.join(infraDir, 'pgpass');
273
+ await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath);
210
274
 
275
+ // Wait for services to be healthy
211
276
  await waitForServices(devId);
212
277
  logger.log('All services are healthy and ready');
213
278
  } finally {
@@ -321,62 +386,9 @@ async function checkInfraHealth(devId = null) {
321
386
  return health;
322
387
  }
323
388
 
324
- /**
325
- * Gets the status of infrastructure services
326
- * Returns detailed information about running containers
327
- *
328
- * @async
329
- * @function getInfraStatus
330
- * @returns {Promise<Object>} Status information for each service
331
- *
332
- * @example
333
- * const status = await getInfraStatus();
334
- * // Returns: { postgres: { status: 'running', port: 5432, url: 'localhost:5432' }, ... }
335
- */
336
- async function getInfraStatus() {
337
- const devId = await config.getDeveloperId();
338
- // Convert string developer ID to number for getDevPorts
339
- const devIdNum = parseInt(devId, 10);
340
- const ports = devConfig.getDevPorts(devIdNum);
341
- const services = {
342
- postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
343
- redis: { port: ports.redis, url: `localhost:${ports.redis}` },
344
- pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
345
- 'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` }
346
- };
347
-
348
- const status = {};
349
-
350
- for (const [serviceName, serviceConfig] of Object.entries(services)) {
351
- try {
352
- const containerName = await containerUtils.findContainer(serviceName, devId);
353
- if (containerName) {
354
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
355
- // Normalize status value (trim whitespace and remove quotes)
356
- const normalizedStatus = stdout.trim().replace(/['"]/g, '');
357
- status[serviceName] = {
358
- status: normalizedStatus,
359
- port: serviceConfig.port,
360
- url: serviceConfig.url
361
- };
362
- } else {
363
- status[serviceName] = {
364
- status: 'not running',
365
- port: serviceConfig.port,
366
- url: serviceConfig.url
367
- };
368
- }
369
- } catch (error) {
370
- status[serviceName] = {
371
- status: 'not running',
372
- port: serviceConfig.port,
373
- url: serviceConfig.url
374
- };
375
- }
376
- }
377
-
378
- return status;
379
- }
389
+ // Re-export status helper functions
390
+ const getInfraStatus = statusHelpers.getInfraStatus;
391
+ const getAppStatus = statusHelpers.getAppStatus;
380
392
 
381
393
  /**
382
394
  * Restarts a specific infrastructure service
@@ -451,48 +463,6 @@ async function waitForServices(devId = null) {
451
463
 
452
464
  throw new Error('Services failed to become healthy within timeout period');
453
465
  }
454
-
455
- /**
456
- * Gets status of running application containers
457
- * Finds all containers matching pattern aifabrix-dev{id}-* (excluding infrastructure)
458
- *
459
- * @async
460
- * @function getAppStatus
461
- * @returns {Promise<Array>} Array of application status objects
462
- *
463
- * @example
464
- * const apps = await getAppStatus();
465
- * // Returns: [{ name: 'myapp', container: 'aifabrix-dev1-myapp', port: '3100:3000', status: 'running', url: 'http://localhost:3100' }]
466
- */
467
- async function getAppStatus() {
468
- const devId = await config.getDeveloperId();
469
- const apps = [];
470
-
471
- try {
472
- const filterPattern = devId === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
473
- const { stdout } = await execAsync(`docker ps --filter "name=${filterPattern}" --format "{{.Names}}\t{{.Ports}}\t{{.Status}}"`);
474
- const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
475
- const infraContainers = devId === 0
476
- ? ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander']
477
- : [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
478
- for (const line of lines) {
479
- const [containerName, ports, status] = line.split('\t');
480
- if (infraContainers.includes(containerName)) continue;
481
- const pattern = devId === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
482
- const appNameMatch = containerName.match(pattern);
483
- if (!appNameMatch) continue;
484
- const appName = appNameMatch[1];
485
- const portMatch = ports.match(/:(\d+)->\d+\//);
486
- const hostPort = portMatch ? portMatch[1] : 'unknown';
487
- const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
488
- apps.push({ name: appName, container: containerName, port: ports, status: status.trim(), url: url });
489
- }
490
- } catch (error) {
491
- return [];
492
- }
493
-
494
- return apps;
495
- }
496
466
  module.exports = {
497
467
  startInfra, stopInfra, stopInfraWithVolumes, checkInfraHealth,
498
468
  getInfraStatus, getAppStatus, restartService, ensureAdminSecrets