@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
@@ -1,16 +1,23 @@
1
1
  # Environment Configuration
2
2
  # Defines host values for different deployment contexts
3
+ #
4
+ # Public Port Support (Docker Context):
5
+ # For docker context, any *_PORT variable automatically gets a corresponding *_PUBLIC_PORT
6
+ # calculated as: *_PUBLIC_PORT = *_PORT + (developer-id * 100)
7
+ # This enables developer-specific host access ports while maintaining internal container ports.
8
+ # Example: MISO_PORT=3000 (internal) -> MISO_PUBLIC_PORT=3100 (for developer-id 1)
9
+ # The pattern applies to all services (MISO, KEYCLOAK, DB, REDIS, etc.) automatically.
3
10
 
4
11
  environments:
5
12
  docker:
6
13
  DB_HOST: postgres
7
- DB_PORT: 5432
14
+ DB_PORT: 5432 # Internal port (container-to-container). DB_PUBLIC_PORT calculated automatically.
8
15
  REDIS_HOST: redis
9
- REDIS_PORT: 6379
16
+ REDIS_PORT: 6379 # Internal port (container-to-container). REDIS_PUBLIC_PORT calculated automatically.
10
17
  MISO_HOST: miso-controller
11
- MISO_PORT: 3000
18
+ MISO_PORT: 3000 # Internal port (container-to-container). MISO_PUBLIC_PORT calculated automatically.
12
19
  KEYCLOAK_HOST: keycloak
13
- KEYCLOAK_PORT: 8082
20
+ KEYCLOAK_PORT: 8082 # Internal port (container-to-container). KEYCLOAK_PUBLIC_PORT calculated automatically.
14
21
  NODE_ENV: production
15
22
  PYTHONUNBUFFERED: 1
16
23
  PYTHONDONTWRITEBYTECODE: 1
@@ -11,6 +11,7 @@
11
11
  const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
+ const { getProjectRoot } = require('./utils/paths');
14
15
 
15
16
  /**
16
17
  * Validates that a template exists and contains files
@@ -23,7 +24,9 @@ async function validateTemplate(templateName) {
23
24
  throw new Error('Template name is required and must be a string');
24
25
  }
25
26
 
26
- const templatePath = path.join(__dirname, '..', 'templates', 'applications', templateName);
27
+ // Use getProjectRoot to reliably find templates in all environments (including CI)
28
+ const projectRoot = getProjectRoot();
29
+ const templatePath = path.join(projectRoot, 'templates', 'applications', templateName);
27
30
 
28
31
  // Check if template folder exists
29
32
  if (!fsSync.existsSync(templatePath)) {
@@ -63,7 +66,9 @@ async function copyTemplateFiles(templateName, appPath) {
63
66
  // Validate template first
64
67
  await validateTemplate(templateName);
65
68
 
66
- const templatePath = path.join(__dirname, '..', 'templates', 'applications', templateName);
69
+ // Use getProjectRoot to reliably find templates in all environments (including CI)
70
+ const projectRoot = getProjectRoot();
71
+ const templatePath = path.join(projectRoot, 'templates', 'applications', templateName);
67
72
  const copiedFiles = [];
68
73
 
69
74
  async function copyDirectory(sourceDir, targetDir) {
@@ -113,7 +118,9 @@ async function copyAppFiles(language, appPath) {
113
118
  }
114
119
 
115
120
  const normalizedLanguage = language.toLowerCase();
116
- const languageTemplatePath = path.join(__dirname, '..', 'templates', normalizedLanguage);
121
+ // Use getProjectRoot to reliably find templates in all environments (including CI)
122
+ const projectRoot = getProjectRoot();
123
+ const languageTemplatePath = path.join(projectRoot, 'templates', normalizedLanguage);
117
124
 
118
125
  // Check if language template folder exists
119
126
  if (!fsSync.existsSync(languageTemplatePath)) {
@@ -166,7 +173,9 @@ async function copyAppFiles(language, appPath) {
166
173
  * @returns {Promise<string[]>} Array of available template names
167
174
  */
168
175
  async function listAvailableTemplates() {
169
- const templatesDir = path.join(__dirname, '..', 'templates', 'applications');
176
+ // Use getProjectRoot to reliably find templates in all environments (including CI)
177
+ const projectRoot = getProjectRoot();
178
+ const templatesDir = path.join(projectRoot, 'templates', 'applications');
170
179
 
171
180
  if (!fsSync.existsSync(templatesDir)) {
172
181
  return [];
package/lib/utils/api.js CHANGED
@@ -27,14 +27,14 @@ async function logApiPerformance(params) {
27
27
  // Log all API calls (both success and failure) to audit log for troubleshooting
28
28
  // This helps track what API calls were made when errors occur
29
29
  try {
30
- await auditLogger.logApiCall(
31
- params.url,
32
- params.options,
33
- params.statusCode,
34
- params.duration,
35
- params.success,
36
- params.errorInfo || {}
37
- );
30
+ await auditLogger.logApiCall({
31
+ url: params.url,
32
+ options: params.options,
33
+ statusCode: params.statusCode,
34
+ duration: params.duration,
35
+ success: params.success,
36
+ errorInfo: params.errorInfo || {}
37
+ });
38
38
  } catch (logError) {
39
39
  // Don't fail the API call if audit logging fails
40
40
  // Silently continue - audit logging should never break functionality
@@ -47,6 +47,38 @@ function displayAuthenticationError(error = null, controllerUrlOrData = null) {
47
47
  process.exit(1);
48
48
  }
49
49
 
50
+ /**
51
+ * Find device token from config by trying each stored URL
52
+ * @async
53
+ * @param {Object} deviceConfig - Device configuration object
54
+ * @param {Array} attemptedUrls - Array to track attempted URLs
55
+ * @returns {Promise<Object|null>} Token result with token and controllerUrl, or null if not found
56
+ */
57
+ async function findDeviceTokenFromConfig(deviceConfig, attemptedUrls) {
58
+ const deviceUrls = Object.keys(deviceConfig);
59
+ if (deviceUrls.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ for (const storedUrl of deviceUrls) {
64
+ attemptedUrls.push(storedUrl);
65
+ try {
66
+ const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
67
+ const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
68
+ if (deviceToken && deviceToken.token) {
69
+ return {
70
+ token: deviceToken.token,
71
+ controllerUrl: deviceToken.controller || normalizedStoredUrl
72
+ };
73
+ }
74
+ } catch (error) {
75
+ // Continue to next URL
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
50
82
  /**
51
83
  * Check if user is authenticated and get token
52
84
  * @async
@@ -83,24 +115,10 @@ async function checkAuthentication(controllerUrl, environment) {
83
115
 
84
116
  // If no token yet, try to find any device token in config
85
117
  if (!token && config.device) {
86
- const deviceUrls = Object.keys(config.device);
87
- if (deviceUrls.length > 0) {
88
- // Try each device token until we find a valid one
89
- for (const storedUrl of deviceUrls) {
90
- attemptedUrls.push(storedUrl);
91
- try {
92
- const normalizedStoredUrl = normalizeControllerUrl(storedUrl);
93
- const deviceToken = await getOrRefreshDeviceToken(normalizedStoredUrl);
94
- if (deviceToken && deviceToken.token) {
95
- token = deviceToken.token;
96
- finalControllerUrl = deviceToken.controller || normalizedStoredUrl;
97
- break;
98
- }
99
- } catch (error) {
100
- lastError = error;
101
- // Continue to next URL
102
- }
103
- }
118
+ const tokenResult = await findDeviceTokenFromConfig(config.device, attemptedUrls);
119
+ if (tokenResult) {
120
+ token = tokenResult.token;
121
+ finalControllerUrl = tokenResult.controllerUrl;
104
122
  }
105
123
  }
106
124
 
@@ -0,0 +1,140 @@
1
+ /**
2
+ * AI Fabrix Builder - App Run Container Helpers
3
+ *
4
+ * Container-related helper functions for application run workflow.
5
+ * Extracted from app-run-helpers.js to reduce file size.
6
+ *
7
+ * @fileoverview Container helper functions for application run workflow
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const chalk = require('chalk');
15
+ const logger = require('./logger');
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ /**
20
+ * Checks if Docker image exists for the application
21
+ * @param {string} imageName - Image name (can include repository prefix)
22
+ * @param {string} tag - Image tag (default: latest)
23
+ * @param {boolean} [debug=false] - Enable debug logging
24
+ * @returns {Promise<boolean>} True if image exists
25
+ */
26
+ async function checkImageExists(imageName, tag = 'latest', debug = false) {
27
+ try {
28
+ const fullImageName = `${imageName}:${tag}`;
29
+ const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
30
+ if (debug) {
31
+ logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
32
+ }
33
+ const { stdout } = await execAsync(cmd);
34
+ const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
35
+ const exists = lines.some(line => line.trim() === fullImageName);
36
+ if (debug) {
37
+ logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
38
+ }
39
+ return exists;
40
+ } catch (error) {
41
+ if (debug) {
42
+ logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
43
+ }
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Checks if container is already running
50
+ * @param {string} appName - Application name
51
+ * @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
52
+ * @param {boolean} [debug=false] - Enable debug logging
53
+ * @returns {Promise<boolean>} True if container is running
54
+ */
55
+ async function checkContainerRunning(appName, developerId, debug = false) {
56
+ try {
57
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
58
+ const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
59
+ const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
60
+ if (debug) {
61
+ logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
62
+ }
63
+ const { stdout } = await execAsync(cmd);
64
+ const isRunning = stdout.trim() === containerName;
65
+ if (debug) {
66
+ logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
67
+ if (isRunning) {
68
+ const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
69
+ const { stdout: status } = await execAsync(statusCmd);
70
+ const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
71
+ const { stdout: ports } = await execAsync(portsCmd);
72
+ logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
73
+ logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
74
+ }
75
+ }
76
+ return isRunning;
77
+ } catch (error) {
78
+ if (debug) {
79
+ logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
80
+ }
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Stops and removes existing container
87
+ * @param {string} appName - Application name
88
+ * @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
89
+ * @param {boolean} [debug=false] - Enable debug logging
90
+ */
91
+ async function stopAndRemoveContainer(appName, developerId, debug = false) {
92
+ try {
93
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
94
+ const containerName = idNum === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
95
+ logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
96
+ const stopCmd = `docker stop ${containerName}`;
97
+ if (debug) {
98
+ logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
99
+ }
100
+ await execAsync(stopCmd);
101
+ const rmCmd = `docker rm ${containerName}`;
102
+ if (debug) {
103
+ logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
104
+ }
105
+ await execAsync(rmCmd);
106
+ logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
107
+ } catch (error) {
108
+ if (debug) {
109
+ logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
110
+ }
111
+ const idNum2 = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
112
+ const containerName = idNum2 === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
113
+ logger.log(chalk.gray(`Container ${containerName} was not running`));
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Log container status for debugging
119
+ * @async
120
+ * @param {string} containerName - Container name
121
+ * @param {boolean} debug - Enable debug logging
122
+ */
123
+ async function logContainerStatus(containerName, debug) {
124
+ if (debug) {
125
+ const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
126
+ const { stdout: status } = await execAsync(statusCmd);
127
+ const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
128
+ const { stdout: ports } = await execAsync(portsCmd);
129
+ logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
130
+ logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
131
+ }
132
+ }
133
+
134
+ module.exports = {
135
+ checkImageExists,
136
+ checkContainerRunning,
137
+ stopAndRemoveContainer,
138
+ logContainerStatus
139
+ };
140
+
@@ -47,10 +47,10 @@ function createClientCredentialsHeaders(clientId, clientSecret) {
47
47
  * Supports both Bearer token and client credentials authentication
48
48
  *
49
49
  * @param {Object} authConfig - Authentication configuration
50
- * @param {string} authConfig.type - Auth type: 'bearer' or 'credentials'
50
+ * @param {string} authConfig.type - Auth type: 'bearer' or 'client-credentials'
51
51
  * @param {string} [authConfig.token] - Bearer token (for type 'bearer')
52
- * @param {string} [authConfig.clientId] - Client ID (for type 'credentials')
53
- * @param {string} [authConfig.clientSecret] - Client secret (for type 'credentials')
52
+ * @param {string} [authConfig.clientId] - Client ID (for type 'client-credentials')
53
+ * @param {string} [authConfig.clientSecret] - Client secret (for type 'client-credentials')
54
54
  * @returns {Object} Headers object with authentication
55
55
  * @throws {Error} If auth config is invalid
56
56
  */
@@ -66,14 +66,14 @@ function createAuthHeaders(authConfig) {
66
66
  return createBearerTokenHeaders(authConfig.token);
67
67
  }
68
68
 
69
- if (authConfig.type === 'credentials') {
69
+ if (authConfig.type === 'client-credentials') {
70
70
  if (!authConfig.clientId || !authConfig.clientSecret) {
71
- throw new Error('Client ID and Client Secret are required for credentials authentication');
71
+ throw new Error('Client ID and Client Secret are required for client-credentials authentication');
72
72
  }
73
73
  return createClientCredentialsHeaders(authConfig.clientId, authConfig.clientSecret);
74
74
  }
75
75
 
76
- throw new Error(`Invalid authentication type: ${authConfig.type}. Must be 'bearer' or 'credentials'`);
76
+ throw new Error(`Invalid authentication type: ${authConfig.type}. Must be 'bearer' or 'client-credentials'`);
77
77
  }
78
78
 
79
79
  module.exports = {
@@ -31,7 +31,7 @@ const paths = require('./paths');
31
31
  * // Returns: '~/.aifabrix/applications-dev-1'
32
32
  */
33
33
  async function copyBuilderToDevDirectory(appName, developerId) {
34
- const builderPath = path.join(process.cwd(), 'builder', appName);
34
+ const builderPath = paths.getBuilderPath(appName);
35
35
 
36
36
  // Ensure builder directory exists
37
37
  if (!fsSync.existsSync(builderPath)) {
@@ -140,10 +140,68 @@ function devDirectoryExists(appName, developerId) {
140
140
  return fsSync.existsSync(devDir);
141
141
  }
142
142
 
143
+ /**
144
+ * Copies application template files to dev directory
145
+ * Used when apps directory doesn't exist to ensure build can proceed
146
+ * @async
147
+ * @param {string} templatePath - Path to template directory
148
+ * @param {string} devDir - Target dev directory
149
+ * @param {string} _language - Language (typescript/python) - currently unused but kept for future use
150
+ * @throws {Error} If copying fails
151
+ */
152
+ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
153
+ if (!fsSync.existsSync(templatePath)) {
154
+ throw new Error(`Template path not found: ${templatePath}`);
155
+ }
156
+
157
+ // Ensure dev directory exists before copying files
158
+ await fs.mkdir(devDir, { recursive: true });
159
+
160
+ const entries = await fs.readdir(templatePath);
161
+
162
+ // Copy only application files, skip Dockerfile and docker-compose templates
163
+ const appFiles = entries.filter(entry => {
164
+ const lowerEntry = entry.toLowerCase();
165
+ // Include .gitignore, exclude .hbs files and docker-related files
166
+ if (entry === '.gitignore') {
167
+ return true;
168
+ }
169
+ if (lowerEntry.endsWith('.hbs')) {
170
+ return false;
171
+ }
172
+ if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
173
+ return false;
174
+ }
175
+ if (entry.startsWith('.') && entry !== '.gitignore') {
176
+ return false;
177
+ }
178
+ return true;
179
+ });
180
+
181
+ for (const entry of appFiles) {
182
+ const sourcePath = path.join(templatePath, entry);
183
+ const targetPath = path.join(devDir, entry);
184
+
185
+ // Skip if source file doesn't exist (e.g., .gitignore might not be in template)
186
+ try {
187
+ const entryStats = await fs.stat(sourcePath);
188
+ if (entryStats.isFile()) {
189
+ await fs.copyFile(sourcePath, targetPath);
190
+ }
191
+ } catch (error) {
192
+ // Skip files that don't exist (e.g., .gitignore might not be in template)
193
+ if (error.code !== 'ENOENT') {
194
+ throw error;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
143
200
  module.exports = {
144
201
  copyBuilderToDevDirectory,
145
202
  copyAppSourceFiles,
146
203
  getDevDirectory,
147
- devDirectoryExists
204
+ devDirectoryExists,
205
+ copyTemplateFilesToDevDir
148
206
  };
149
207
 
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Build Helper Functions
3
+ *
4
+ * Helper functions for build operations.
5
+ * Separated from build.js to maintain file size limits.
6
+ *
7
+ * @fileoverview Build helper functions
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const path = require('path');
13
+ const dockerfileUtils = require('./dockerfile-utils');
14
+ const logger = require('./logger');
15
+ const chalk = require('chalk');
16
+
17
+ /**
18
+ * Determine Dockerfile path (template, custom, or generate)
19
+ * @async
20
+ * @param {string} appName - Application name
21
+ * @param {Object} options - Options with language, config, buildConfig, contextPath, forceTemplate, devDir
22
+ * @param {Function} generateDockerfileFn - Function to generate Dockerfile
23
+ * @returns {Promise<string>} Path to Dockerfile
24
+ */
25
+ async function determineDockerfile(appName, options, generateDockerfileFn) {
26
+ // Use dev directory if provided, otherwise fall back to builder directory
27
+ const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
28
+
29
+ const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
30
+ if (templateDockerfile) {
31
+ const relativePath = path.relative(process.cwd(), templateDockerfile);
32
+ logger.log(chalk.green(`✓ Using existing Dockerfile: ${relativePath}`));
33
+ return templateDockerfile;
34
+ }
35
+
36
+ const customDockerfile = dockerfileUtils.checkProjectDockerfile(searchPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
37
+ if (customDockerfile) {
38
+ logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
39
+ return customDockerfile;
40
+ }
41
+
42
+ // Generate Dockerfile in dev directory if provided
43
+ const dockerfilePath = await generateDockerfileFn(appName, options.language, options.config, options.buildConfig, options.devDir);
44
+ const relativePath = path.relative(process.cwd(), dockerfilePath);
45
+ logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
46
+ return dockerfilePath;
47
+ }
48
+
49
+ /**
50
+ * Loads and validates configuration for build
51
+ * @async
52
+ * @param {string} appName - Application name
53
+ * @returns {Promise<Object>} Configuration object with config, imageName, and buildConfig
54
+ * @throws {Error} If configuration cannot be loaded or validated
55
+ */
56
+ async function loadAndValidateConfig(appName) {
57
+ const { loadVariablesYaml } = require('../build');
58
+ const validator = require('../validator');
59
+
60
+ const variables = await loadVariablesYaml(appName);
61
+
62
+ // Validate configuration
63
+ const validation = await validator.validateVariables(appName);
64
+ if (!validation.valid) {
65
+ throw new Error(`Configuration validation failed:\n${validation.errors.join('\n')}`);
66
+ }
67
+
68
+ // Extract image name
69
+ let imageName;
70
+ if (typeof variables.image === 'string') {
71
+ imageName = variables.image.split(':')[0];
72
+ } else if (variables.image?.name) {
73
+ imageName = variables.image.name;
74
+ } else if (variables.app?.key) {
75
+ imageName = variables.app.key;
76
+ } else {
77
+ imageName = appName;
78
+ }
79
+
80
+ // Extract build config
81
+ const buildConfig = variables.build || {};
82
+
83
+ return {
84
+ config: variables,
85
+ imageName,
86
+ buildConfig
87
+ };
88
+ }
89
+
90
+ module.exports = {
91
+ determineDockerfile,
92
+ loadAndValidateConfig
93
+ };
94
+