@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/build.js CHANGED
@@ -14,75 +14,17 @@ const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const path = require('path');
16
16
  const paths = require('./utils/paths');
17
- const { detectAppType } = require('./utils/paths');
18
- const { exec } = require('child_process');
19
- const { promisify } = require('util');
17
+ const { detectAppType, getProjectRoot } = require('./utils/paths');
20
18
  const chalk = require('chalk');
21
19
  const yaml = require('js-yaml');
22
20
  const secrets = require('./secrets');
23
21
  const config = require('./config');
24
22
  const logger = require('./utils/logger');
25
- const validator = require('./validator');
26
23
  const dockerfileUtils = require('./utils/dockerfile-utils');
27
24
  const dockerBuild = require('./utils/docker-build');
28
25
  const buildCopy = require('./utils/build-copy');
29
26
  const { buildDevImageName } = require('./utils/image-name');
30
-
31
- const execAsync = promisify(exec);
32
-
33
- /**
34
- * Copies application template files to dev directory
35
- * Used when apps directory doesn't exist to ensure build can proceed
36
- * @async
37
- * @param {string} templatePath - Path to template directory
38
- * @param {string} devDir - Target dev directory
39
- * @param {string} _language - Language (typescript/python) - currently unused but kept for future use
40
- * @throws {Error} If copying fails
41
- */
42
- async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
43
- if (!fsSync.existsSync(templatePath)) {
44
- throw new Error(`Template path not found: ${templatePath}`);
45
- }
46
-
47
- const entries = await fs.readdir(templatePath);
48
-
49
- // Copy only application files, skip Dockerfile and docker-compose templates
50
- const appFiles = entries.filter(entry => {
51
- const lowerEntry = entry.toLowerCase();
52
- // Include .gitignore, exclude .hbs files and docker-related files
53
- if (entry === '.gitignore') {
54
- return true;
55
- }
56
- if (lowerEntry.endsWith('.hbs')) {
57
- return false;
58
- }
59
- if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
60
- return false;
61
- }
62
- if (entry.startsWith('.') && entry !== '.gitignore') {
63
- return false;
64
- }
65
- return true;
66
- });
67
-
68
- for (const entry of appFiles) {
69
- const sourcePath = path.join(templatePath, entry);
70
- const targetPath = path.join(devDir, entry);
71
-
72
- // Skip if source file doesn't exist (e.g., .gitignore might not be in template)
73
- try {
74
- const entryStats = await fs.stat(sourcePath);
75
- if (entryStats.isFile()) {
76
- await fs.copyFile(sourcePath, targetPath);
77
- }
78
- } catch (error) {
79
- // Skip files that don't exist (e.g., .gitignore might not be in template)
80
- if (error.code !== 'ENOENT') {
81
- throw error;
82
- }
83
- }
84
- }
85
- }
27
+ const buildHelpers = require('./utils/build-helpers');
86
28
 
87
29
  /**
88
30
  * Loads variables.yaml configuration for an application
@@ -235,96 +177,174 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
235
177
  * @param {boolean} options.forceTemplate - Force template flag
236
178
  * @returns {Promise<string>} Path to Dockerfile
237
179
  */
238
- async function determineDockerfile(appName, options) {
239
- // Use dev directory if provided, otherwise fall back to builder directory
240
- const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
241
-
242
- const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
243
- if (templateDockerfile) {
244
- const relativePath = path.relative(process.cwd(), templateDockerfile);
245
- logger.log(chalk.green(`āœ“ Using existing Dockerfile: ${relativePath}`));
246
- return templateDockerfile;
247
- }
248
180
 
249
- const customDockerfile = dockerfileUtils.checkProjectDockerfile(searchPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
250
- if (customDockerfile) {
251
- logger.log(chalk.green(`āœ“ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
252
- return customDockerfile;
253
- }
181
+ /**
182
+ * Executes Docker build and handles tagging
183
+ * @async
184
+ * @param {string} imageName - Image name
185
+ * @param {string} dockerfilePath - Path to Dockerfile
186
+ * @param {string} contextPath - Build context path
187
+ * @param {string} tag - Image tag
188
+ * @param {Object} options - Build options
189
+ */
254
190
 
255
- // Generate Dockerfile in dev directory if provided
256
- const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig, options.devDir);
257
- const relativePath = path.relative(process.cwd(), dockerfilePath);
258
- logger.log(chalk.green(`āœ“ Generated Dockerfile from template: ${relativePath}`));
259
- return dockerfilePath;
191
+ async function postBuildTasks(appName, buildConfig) {
192
+ try {
193
+ const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
194
+ logger.log(chalk.green(`āœ“ Generated .env file: ${envPath}`));
195
+ // Note: processEnvVariables is already called by generateEnvFile to generate local .env
196
+ // at the envOutputPath, so we don't need to manually copy the docker .env file
197
+ } catch (error) {
198
+ logger.log(chalk.yellow(`āš ļø Warning: Could not generate .env file: ${error.message}`));
199
+ }
260
200
  }
261
201
 
262
202
  /**
263
- * Loads and validates configuration for build
203
+ * Check if app is external type and handle accordingly
264
204
  * @async
265
205
  * @param {string} appName - Application name
266
- * @returns {Promise<Object>} Configuration object with config, imageName, and buildConfig
267
- * @throws {Error} If configuration cannot be loaded or validated
206
+ * @returns {Promise<boolean>} True if external (handled), false if should continue
268
207
  */
269
- async function loadAndValidateConfig(appName) {
208
+ async function checkExternalAppType(appName) {
270
209
  const variables = await loadVariablesYaml(appName);
210
+ if (variables.app && variables.app.type === 'external') {
211
+ const generator = require('./generator');
212
+ const jsonPath = await generator.generateDeployJson(appName);
213
+ logger.log(chalk.green(`āœ“ Generated deployment JSON: ${jsonPath}`));
214
+ return true;
215
+ }
216
+ return false;
217
+ }
271
218
 
272
- // Validate configuration
273
- const validation = await validator.validateVariables(appName);
274
- if (!validation.valid) {
275
- throw new Error(`Configuration validation failed:\n${validation.errors.join('\n')}`);
219
+ /**
220
+ * Prepare dev directory and copy application files
221
+ * @async
222
+ * @param {string} appName - Application name
223
+ * @param {Object} buildConfig - Build configuration
224
+ * @param {Object} options - Build options
225
+ * @returns {Promise<Object>} Object with devDir, effectiveImageName, imageName, appConfig, and developerId
226
+ */
227
+ async function prepareDevDirectory(appName, buildConfig, options) {
228
+ const developerId = await config.getDeveloperId();
229
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
230
+ const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
231
+ logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
232
+ const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
233
+ logger.log(chalk.green(`āœ“ Files copied to: ${devDir}`));
234
+
235
+ const { config: appConfig, imageName } = await buildHelpers.loadAndValidateConfig(appName);
236
+ const effectiveImageName = buildDevImageName(imageName, developerId);
237
+
238
+ // Check if application source files exist, if not copy from templates
239
+ const appsPath = path.join(process.cwd(), 'apps', appName);
240
+ if (fsSync.existsSync(appsPath)) {
241
+ await buildCopy.copyAppSourceFiles(appsPath, devDir);
242
+ logger.log(chalk.green(`āœ“ Copied application source files from apps/${appName}`));
243
+ } else {
244
+ // No apps directory - check if we need to copy template files
245
+ const language = options.language || buildConfig.language || detectLanguage(devDir);
246
+ const packageJsonPath = path.join(devDir, 'package.json');
247
+ const requirementsPath = path.join(devDir, 'requirements.txt');
248
+
249
+ if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
250
+ const projectRoot = getProjectRoot();
251
+ const templatePath = path.join(projectRoot, 'templates', 'typescript');
252
+ await buildCopy.copyTemplateFilesToDevDir(templatePath, devDir, language);
253
+ logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
254
+ } else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
255
+ const projectRoot = getProjectRoot();
256
+ const templatePath = path.join(projectRoot, 'templates', 'python');
257
+ await buildCopy.copyTemplateFilesToDevDir(templatePath, devDir, language);
258
+ logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
259
+ }
276
260
  }
277
261
 
278
- // Extract image name
279
- let imageName;
280
- if (typeof variables.image === 'string') {
281
- imageName = variables.image.split(':')[0];
282
- } else if (variables.image?.name) {
283
- imageName = variables.image.name;
284
- } else if (variables.app?.key) {
285
- imageName = variables.app.key;
262
+ return { devDir, effectiveImageName, imageName, appConfig };
263
+ }
264
+
265
+ /**
266
+ * Prepare build context path
267
+ * @param {Object} buildConfig - Build configuration
268
+ * @param {string} devDir - Developer directory path
269
+ * @returns {string} Resolved context path
270
+ */
271
+ function prepareBuildContext(buildConfig, devDir) {
272
+ let contextPath;
273
+
274
+ // Check if context is using old format (../appName) - these are incompatible with dev directory structure
275
+ if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
276
+ // Old format detected - always use devDir instead
277
+ logger.log(chalk.yellow(`āš ļø Warning: Build context uses old format: ${buildConfig.context}`));
278
+ logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
279
+ contextPath = devDir;
280
+ } else if (buildConfig.context && buildConfig.context !== '../..') {
281
+ // Resolve relative context path from dev directory
282
+ contextPath = path.resolve(devDir, buildConfig.context);
283
+ } else if (buildConfig.context === '../..') {
284
+ // For apps flag, context is relative to devDir
285
+ contextPath = process.cwd();
286
286
  } else {
287
- imageName = appName;
287
+ // No context specified, use dev directory
288
+ contextPath = devDir;
288
289
  }
289
290
 
290
- // Extract build config
291
- const buildConfig = variables.build || {};
291
+ // Ensure context path is absolute and normalized
292
+ contextPath = path.resolve(contextPath);
292
293
 
293
- return {
294
- config: variables,
295
- imageName,
296
- buildConfig
297
- };
294
+ // Validate that context path exists (skip in test environments)
295
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
296
+ process.env.JEST_WORKER_ID !== undefined ||
297
+ typeof jest !== 'undefined';
298
+
299
+ if (!isTestEnv && !fsSync.existsSync(contextPath)) {
300
+ throw new Error(
301
+ `Build context path does not exist: ${contextPath}\n` +
302
+ `Expected dev directory: ${devDir}\n` +
303
+ 'Please ensure files were copied correctly or update the context in variables.yaml.'
304
+ );
305
+ }
306
+
307
+ return contextPath;
298
308
  }
299
309
 
300
310
  /**
301
- * Executes Docker build and handles tagging
311
+ * Handle Dockerfile generation
302
312
  * @async
303
- * @param {string} imageName - Image name
304
- * @param {string} dockerfilePath - Path to Dockerfile
305
- * @param {string} contextPath - Build context path
306
- * @param {string} tag - Image tag
313
+ * @param {string} appName - Application name
314
+ * @param {Object} params - Parameters
315
+ * @param {string} params.devDir - Developer directory
316
+ * @param {Object} params.buildConfig - Build configuration
317
+ * @param {string} params.contextPath - Build context path
318
+ * @param {Object} params.appConfig - Application configuration
307
319
  * @param {Object} options - Build options
320
+ * @returns {Promise<string>} Dockerfile path
308
321
  */
309
- async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
310
- await dockerBuild.executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
311
-
312
- // Tag image if additional tag provided
313
- if (options.tag && options.tag !== 'latest') {
314
- await execAsync(`docker tag ${imageName}:${tag} ${imageName}:latest`);
315
- logger.log(chalk.green(`āœ“ Tagged image: ${imageName}:latest`));
322
+ async function handleDockerfileGeneration(appName, params, options, buildHelpers) {
323
+ const { devDir, buildConfig, contextPath, appConfig } = params;
324
+ const appDockerfilePath = path.join(devDir, 'Dockerfile');
325
+ const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
326
+
327
+ // Determine language (skip if existing Dockerfile found)
328
+ let language = options.language || buildConfig.language;
329
+ if (!language && !hasExistingDockerfile) {
330
+ language = detectLanguage(devDir);
331
+ } else if (!language) {
332
+ // Default language if existing Dockerfile is found (won't be used, but needed for API)
333
+ language = 'typescript';
316
334
  }
317
- }
318
-
319
- async function postBuildTasks(appName, buildConfig) {
320
- try {
321
- const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
322
- logger.log(chalk.green(`āœ“ Generated .env file: ${envPath}`));
323
- // Note: processEnvVariables is already called by generateEnvFile to generate local .env
324
- // at the envOutputPath, so we don't need to manually copy the docker .env file
325
- } catch (error) {
326
- logger.log(chalk.yellow(`āš ļø Warning: Could not generate .env file: ${error.message}`));
335
+ if (!hasExistingDockerfile) {
336
+ logger.log(chalk.green(`āœ“ Detected language: ${language}`));
327
337
  }
338
+
339
+ // Determine Dockerfile (needs context path to generate in correct location)
340
+ return await buildHelpers.determineDockerfile(appName, {
341
+ language,
342
+ config: appConfig,
343
+ buildConfig,
344
+ contextPath,
345
+ forceTemplate: options.forceTemplate,
346
+ devDir: devDir
347
+ }, generateDockerfile);
328
348
  }
329
349
 
330
350
  /**
@@ -347,11 +367,7 @@ async function postBuildTasks(appName, buildConfig) {
347
367
  */
348
368
  async function buildApp(appName, options = {}) {
349
369
  // Check if app type is external - generate JSON files only (not deploy)
350
- const variables = await loadVariablesYaml(appName);
351
- if (variables.app && variables.app.type === 'external') {
352
- const generator = require('./generator');
353
- const jsonPath = await generator.generateDeployJson(appName);
354
- logger.log(chalk.green(`āœ“ Generated deployment JSON: ${jsonPath}`));
370
+ if (await checkExternalAppType(appName)) {
355
371
  return null;
356
372
  }
357
373
 
@@ -359,131 +375,27 @@ async function buildApp(appName, options = {}) {
359
375
  logger.log(chalk.blue(`\nšŸ”Ø Building application: ${appName}`));
360
376
 
361
377
  // 1. Load and validate configuration
362
- const { config: appConfig, imageName, buildConfig } = await loadAndValidateConfig(appName);
363
-
364
- // 2. Get developer ID and copy files to dev-specific directory
365
- const developerId = await config.getDeveloperId();
366
- const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
367
- const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
368
- logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
369
- const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
370
- logger.log(chalk.green(`āœ“ Files copied to: ${devDir}`));
371
- const effectiveImageName = buildDevImageName(imageName, developerId);
372
-
373
- // 2a. Check if application source files exist, if not copy from templates
374
- const appsPath = path.join(process.cwd(), 'apps', appName);
375
- if (fsSync.existsSync(appsPath)) {
376
- // Copy app source files from apps directory
377
- await buildCopy.copyAppSourceFiles(appsPath, devDir);
378
- logger.log(chalk.green(`āœ“ Copied application source files from apps/${appName}`));
379
- } else {
380
- // No apps directory - check if we need to copy template files
381
- const language = options.language || buildConfig.language || detectLanguage(devDir);
382
- const packageJsonPath = path.join(devDir, 'package.json');
383
- const requirementsPath = path.join(devDir, 'requirements.txt');
384
-
385
- if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
386
- // Copy TypeScript template files
387
- const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
388
- await copyTemplateFilesToDevDir(templatePath, devDir, language);
389
- logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
390
- } else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
391
- // Copy Python template files
392
- const templatePath = path.join(__dirname, '..', 'templates', 'python');
393
- await copyTemplateFilesToDevDir(templatePath, devDir, language);
394
- logger.log(chalk.green(`āœ“ Generated application files from ${language} template`));
395
- }
396
- }
397
-
398
- // 3. Prepare build context (use dev-specific directory)
399
- // If buildConfig.context is relative, resolve it relative to devDir
400
- // If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
401
- let contextPath;
402
-
403
- // Check if context is using old format (../appName) - these are incompatible with dev directory structure
404
- if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
405
- // Old format detected - always use devDir instead
406
- logger.log(chalk.yellow(`āš ļø Warning: Build context uses old format: ${buildConfig.context}`));
407
- logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
408
- contextPath = devDir;
409
- } else if (buildConfig.context && buildConfig.context !== '../..') {
410
- // Resolve relative context path from dev directory
411
- contextPath = path.resolve(devDir, buildConfig.context);
412
- } else if (buildConfig.context === '../..') {
413
- // For apps flag, context is relative to devDir (which is ~/.aifabrix/<app>-dev-<id>)
414
- // So '../..' from devDir goes to ~/.aifabrix, then we need to go to project root
415
- // Actually, we need to keep the relative path and let Docker resolve it
416
- // But the build context should be the project root, not devDir
417
- contextPath = process.cwd();
418
- } else {
419
- // No context specified, use dev directory
420
- contextPath = devDir;
421
- }
378
+ const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
422
379
 
423
- // Ensure context path is absolute and normalized
424
- contextPath = path.resolve(contextPath);
380
+ // 2. Prepare dev directory and copy files
381
+ const { devDir, effectiveImageName, imageName, appConfig } = await prepareDevDirectory(appName, buildConfig, options);
425
382
 
426
- // Validate that context path exists (skip in test environments)
427
- const isTestEnv = process.env.NODE_ENV === 'test' ||
428
- process.env.JEST_WORKER_ID !== undefined ||
429
- typeof jest !== 'undefined';
383
+ // 3. Prepare build context
384
+ const contextPath = prepareBuildContext(buildConfig, devDir);
430
385
 
431
- if (!isTestEnv && !fsSync.existsSync(contextPath)) {
432
- throw new Error(
433
- `Build context path does not exist: ${contextPath}\n` +
434
- `Expected dev directory: ${devDir}\n` +
435
- 'Please ensure files were copied correctly or update the context in variables.yaml.'
436
- );
437
- }
438
-
439
- // 4. Check if Dockerfile exists in dev directory
440
- const appDockerfilePath = path.join(devDir, 'Dockerfile');
441
- const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
442
-
443
- // 5. Determine language (skip if existing Dockerfile found)
444
- let language = options.language || buildConfig.language;
445
- if (!language && !hasExistingDockerfile) {
446
- language = detectLanguage(devDir);
447
- } else if (!language) {
448
- // Default language if existing Dockerfile is found (won't be used, but needed for API)
449
- language = 'typescript';
450
- }
451
- if (!hasExistingDockerfile) {
452
- logger.log(chalk.green(`āœ“ Detected language: ${language}`));
453
- }
454
-
455
- // 6. Determine Dockerfile (needs context path to generate in correct location)
456
- // Use dev directory for Dockerfile generation
457
- const dockerfilePath = await determineDockerfile(appName, {
458
- language,
459
- config: appConfig,
386
+ // 4. Handle Dockerfile generation
387
+ const dockerfilePath = await handleDockerfileGeneration(appName, {
388
+ devDir,
460
389
  buildConfig,
461
390
  contextPath,
462
- forceTemplate: options.forceTemplate,
463
- devDir: devDir
464
- });
391
+ appConfig
392
+ }, options, buildHelpers);
465
393
 
466
- // 6. Build Docker image
394
+ // 5. Execute Docker build
467
395
  const tag = options.tag || 'latest';
396
+ await dockerBuild.executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, options);
468
397
 
469
- // Log paths for debugging
470
- logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
471
- logger.log(chalk.blue(`Using build context: ${contextPath}`));
472
-
473
- await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
474
- // Back-compat: also tag the built dev image as the base image name
475
- try {
476
- // Use runtime promisify so tests can capture this call reliably
477
- const { promisify } = require('util');
478
- const { exec } = require('child_process');
479
- const run = promisify(exec);
480
- await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
481
- logger.log(chalk.green(`āœ“ Tagged image: ${imageName}:${tag}`));
482
- } catch (err) {
483
- logger.log(chalk.yellow(`āš ļø Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
484
- }
485
-
486
- // 7. Post-build tasks
398
+ // 6. Post-build tasks
487
399
  await postBuildTasks(appName, buildConfig);
488
400
 
489
401
  logger.log(chalk.green('\nāœ… Build completed successfully!'));
@@ -493,5 +405,4 @@ async function buildApp(appName, options = {}) {
493
405
  throw new Error(`Build failed: ${error.message}`);
494
406
  }
495
407
  }
496
-
497
- module.exports = { loadVariablesYaml, resolveContextPath, executeDockerBuild: dockerBuild.executeDockerBuild, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
408
+ module.exports = { loadVariablesYaml, resolveContextPath, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
package/lib/cli.js CHANGED
@@ -21,6 +21,7 @@ const chalk = require('chalk');
21
21
  const logger = require('./utils/logger');
22
22
  const { validateCommand, handleCommandError } = require('./utils/cli-utils');
23
23
  const { handleLogin } = require('./commands/login');
24
+ const { handleLogout } = require('./commands/logout');
24
25
  const { handleSecure } = require('./commands/secure');
25
26
  const { handleSecretsSet } = require('./commands/secrets-set');
26
27
 
@@ -49,6 +50,20 @@ function setupCommands(program) {
49
50
  }
50
51
  });
51
52
 
53
+ program.command('logout')
54
+ .description('Clear authentication tokens')
55
+ .option('-c, --controller <url>', 'Clear device tokens for specific controller')
56
+ .option('-e, --environment <env>', 'Clear client tokens for specific environment')
57
+ .option('-a, --app <app>', 'Clear client tokens for specific app (requires --environment)')
58
+ .action(async(options) => {
59
+ try {
60
+ await handleLogout(options);
61
+ } catch (error) {
62
+ handleCommandError(error, 'logout');
63
+ process.exit(1);
64
+ }
65
+ });
66
+
52
67
  // Infrastructure commands
53
68
  program.command('up')
54
69
  .description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
@@ -474,14 +489,43 @@ function setupCommands(program) {
474
489
  });
475
490
 
476
491
  // Developer configuration commands
477
- program.command('dev config')
492
+ const dev = program
493
+ .command('dev')
494
+ .description('Developer configuration and isolation');
495
+
496
+ // Helper function to display developer configuration
497
+ async function displayDevConfig(devId) {
498
+ const devIdNum = parseInt(devId, 10);
499
+ const ports = devConfig.getDevPorts(devIdNum);
500
+ const configVars = [
501
+ { key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
502
+ { key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
503
+ { key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() }
504
+ ].filter(v => v.value);
505
+
506
+ logger.log('\nšŸ”§ Developer Configuration\n');
507
+ logger.log(`Developer ID: ${devId}`);
508
+ logger.log('\nPorts:');
509
+ logger.log(` App: ${ports.app}`);
510
+ logger.log(` Postgres: ${ports.postgres}`);
511
+ logger.log(` Redis: ${ports.redis}`);
512
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
513
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
514
+
515
+ if (configVars.length > 0) {
516
+ logger.log('\nConfiguration:');
517
+ configVars.forEach(v => logger.log(` ${v.key}: ${v.value}`));
518
+ }
519
+ logger.log('');
520
+ }
521
+
522
+ // Config subcommand
523
+ dev
524
+ .command('config')
478
525
  .description('Show or set developer configuration')
479
526
  .option('--set-id <id>', 'Set developer ID')
480
- .action(async(cmdName, opts) => {
527
+ .action(async(options) => {
481
528
  try {
482
- // For commands with spaces like 'dev config', Commander.js passes command name as first arg
483
- // Options are passed as second arg, or if only one arg, it might be the command name
484
- const options = typeof cmdName === 'object' && cmdName !== null ? cmdName : (opts || {});
485
529
  // Commander.js converts --set-id to setId in options object
486
530
  const setIdValue = options.setId || options['set-id'];
487
531
  if (setIdValue) {
@@ -493,77 +537,41 @@ function setupCommands(program) {
493
537
  await config.setDeveloperId(setIdValue);
494
538
  process.env.AIFABRIX_DEVELOPERID = setIdValue;
495
539
  logger.log(chalk.green(`āœ“ Developer ID set to ${setIdValue}`));
496
- // Convert to number only for getDevPorts (which requires a number)
497
- const devIdNum = parseInt(setIdValue, 10);
498
540
  // Use the ID we just set instead of reading from file to avoid race conditions
499
- const ports = devConfig.getDevPorts(devIdNum);
500
- logger.log('\nšŸ”§ Developer Configuration\n');
501
- logger.log(`Developer ID: ${setIdValue}`);
502
- logger.log('\nPorts:');
503
- logger.log(` App: ${ports.app}`);
504
- logger.log(` Postgres: ${ports.postgres}`);
505
- logger.log(` Redis: ${ports.redis}`);
506
- logger.log(` pgAdmin: ${ports.pgadmin}`);
507
- logger.log(` Redis Commander: ${ports.redisCommander}`);
508
-
509
- // Display configuration variables if set
510
- const aifabrixHome = await config.getAifabrixHomeOverride();
511
- const aifabrixSecrets = await config.getAifabrixSecretsPath();
512
- const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
513
-
514
- if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
515
- logger.log('\nConfiguration:');
516
- if (aifabrixHome) {
517
- logger.log(` aifabrix-home: ${aifabrixHome}`);
518
- }
519
- if (aifabrixSecrets) {
520
- logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
521
- }
522
- if (aifabrixEnvConfig) {
523
- logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
524
- }
525
- }
526
- logger.log('');
541
+ await displayDevConfig(setIdValue);
527
542
  return;
528
543
  }
529
544
 
530
545
  const devId = await config.getDeveloperId();
531
- // Convert string developer ID to number for getDevPorts
532
- const devIdNum = parseInt(devId, 10);
533
- const ports = devConfig.getDevPorts(devIdNum);
534
- logger.log('\nšŸ”§ Developer Configuration\n');
535
- logger.log(`Developer ID: ${devId}`);
536
- logger.log('\nPorts:');
537
- logger.log(` App: ${ports.app}`);
538
- logger.log(` Postgres: ${ports.postgres}`);
539
- logger.log(` Redis: ${ports.redis}`);
540
- logger.log(` pgAdmin: ${ports.pgadmin}`);
541
- logger.log(` Redis Commander: ${ports.redisCommander}`);
542
-
543
- // Display configuration variables if set
544
- const aifabrixHome = await config.getAifabrixHomeOverride();
545
- const aifabrixSecrets = await config.getAifabrixSecretsPath();
546
- const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
547
-
548
- if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
549
- logger.log('\nConfiguration:');
550
- if (aifabrixHome) {
551
- logger.log(` aifabrix-home: ${aifabrixHome}`);
552
- }
553
- if (aifabrixSecrets) {
554
- logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
555
- }
556
- if (aifabrixEnvConfig) {
557
- logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
558
- }
559
- }
560
- logger.log('');
546
+ await displayDevConfig(devId);
561
547
  } catch (error) {
562
548
  handleCommandError(error, 'dev config');
563
549
  process.exit(1);
564
550
  }
565
551
  });
566
552
 
553
+ // Set-id subcommand
554
+ dev
555
+ .command('set-id <id>')
556
+ .description('Set developer ID (convenience alias for "dev config --set-id")')
557
+ .action(async(id) => {
558
+ try {
559
+ const digitsOnly = /^[0-9]+$/.test(id);
560
+ if (!digitsOnly) {
561
+ throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
562
+ }
563
+ // Preserve the original string value to maintain leading zeros (e.g., "01")
564
+ await config.setDeveloperId(id);
565
+ process.env.AIFABRIX_DEVELOPERID = id;
566
+ logger.log(chalk.green(`āœ“ Developer ID set to ${id}`));
567
+ // Use the ID we just set instead of reading from file to avoid race conditions
568
+ await displayDevConfig(id);
569
+ } catch (error) {
570
+ handleCommandError(error, 'dev set-id');
571
+ process.exit(1);
572
+ }
573
+ });
574
+
567
575
  // Secrets management commands
568
576
  const secretsCmd = program
569
577
  .command('secrets')