@aifabrix/builder 2.2.0 → 2.3.2

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.
package/lib/app-run.js CHANGED
@@ -9,390 +9,13 @@
9
9
  * @version 2.0.0
10
10
  */
11
11
 
12
- const fs = require('fs').promises;
13
- const fsSync = require('fs');
14
- const path = require('path');
15
12
  const chalk = require('chalk');
16
- const yaml = require('js-yaml');
17
- const { exec } = require('child_process');
18
- const { promisify } = require('util');
19
- const validator = require('./validator');
20
- const infra = require('./infra');
21
- const secrets = require('./secrets');
22
13
  const config = require('./config');
23
- const buildCopy = require('./utils/build-copy');
24
14
  const logger = require('./utils/logger');
25
- const { waitForHealthCheck, checkPortAvailable } = require('./utils/health-check');
15
+ const { checkPortAvailable, waitForHealthCheck } = require('./utils/health-check');
26
16
  const composeGenerator = require('./utils/compose-generator');
27
-
28
- const execAsync = promisify(exec);
29
-
30
- /**
31
- * Checks if Docker image exists for the application
32
- * @param {string} imageName - Image name (can include repository prefix)
33
- * @param {string} tag - Image tag (default: latest)
34
- * @param {boolean} [debug=false] - Enable debug logging
35
- * @returns {Promise<boolean>} True if image exists
36
- */
37
- async function checkImageExists(imageName, tag = 'latest', debug = false) {
38
- try {
39
- const fullImageName = `${imageName}:${tag}`;
40
- const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
41
- if (debug) {
42
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
43
- }
44
- // Use Docker's native filtering for cross-platform compatibility (Windows-safe)
45
- const { stdout } = await execAsync(cmd);
46
- const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
47
- const exists = lines.some(line => line.trim() === fullImageName);
48
- if (debug) {
49
- logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
50
- }
51
- return exists;
52
- } catch (error) {
53
- if (debug) {
54
- logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
55
- }
56
- return false;
57
- }
58
- }
59
-
60
- /**
61
- * Checks if container is already running
62
- * @param {string} appName - Application name
63
- * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
64
- * @param {boolean} [debug=false] - Enable debug logging
65
- * @returns {Promise<boolean>} True if container is running
66
- */
67
- async function checkContainerRunning(appName, developerId, debug = false) {
68
- try {
69
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
70
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
71
- const cmd = `docker ps --filter "name=${containerName}" --format "{{.Names}}"`;
72
- if (debug) {
73
- logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
74
- }
75
- const { stdout } = await execAsync(cmd);
76
- const isRunning = stdout.trim() === containerName;
77
- if (debug) {
78
- logger.log(chalk.gray(`[DEBUG] Container ${containerName} running: ${isRunning}`));
79
- if (isRunning) {
80
- // Get container status details
81
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
82
- const { stdout: status } = await execAsync(statusCmd);
83
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
84
- const { stdout: ports } = await execAsync(portsCmd);
85
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
86
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
87
- }
88
- }
89
- return isRunning;
90
- } catch (error) {
91
- if (debug) {
92
- logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
93
- }
94
- return false;
95
- }
96
- }
97
-
98
- /**
99
- * Stops and removes existing container
100
- * @param {string} appName - Application name
101
- * @param {number} developerId - Developer ID (0 = default infra, > 0 = developer-specific)
102
- * @param {boolean} [debug=false] - Enable debug logging
103
- */
104
- async function stopAndRemoveContainer(appName, developerId, debug = false) {
105
- try {
106
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
107
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
108
- logger.log(chalk.yellow(`Stopping existing container ${containerName}...`));
109
- const stopCmd = `docker stop ${containerName}`;
110
- if (debug) {
111
- logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
112
- }
113
- await execAsync(stopCmd);
114
- const rmCmd = `docker rm ${containerName}`;
115
- if (debug) {
116
- logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
117
- }
118
- await execAsync(rmCmd);
119
- logger.log(chalk.green(`✓ Container ${containerName} stopped and removed`));
120
- } catch (error) {
121
- if (debug) {
122
- logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
123
- }
124
- // Container might not exist, which is fine
125
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
126
- const containerName = developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${developerId}-${appName}`;
127
- logger.log(chalk.gray(`Container ${containerName} was not running`));
128
- }
129
- }
130
-
131
- /**
132
- * Validates app name and loads configuration
133
- * @async
134
- * @param {string} appName - Application name
135
- * @returns {Promise<Object>} Application configuration
136
- * @throws {Error} If validation fails
137
- */
138
- async function validateAppConfiguration(appName) {
139
- // Validate app name
140
- if (!appName || typeof appName !== 'string') {
141
- throw new Error('Application name is required');
142
- }
143
-
144
- // Check if we're running from inside the builder directory
145
- const currentDir = process.cwd();
146
- const normalizedPath = currentDir.replace(/\\/g, '/');
147
- const expectedBuilderPath = `builder/${appName}`;
148
-
149
- // If inside builder/{appName}, suggest moving to project root
150
- if (normalizedPath.endsWith(expectedBuilderPath)) {
151
- const projectRoot = path.resolve(currentDir, '../..');
152
- throw new Error(
153
- 'You\'re running from inside the builder directory.\n' +
154
- `Current directory: ${currentDir}\n` +
155
- 'Please change to the project root and try again:\n' +
156
- ` cd ${projectRoot}\n` +
157
- ` aifabrix run ${appName}`
158
- );
159
- }
160
-
161
- // Load and validate app configuration
162
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
163
- if (!fsSync.existsSync(configPath)) {
164
- const expectedDir = path.join(currentDir, 'builder', appName);
165
- throw new Error(
166
- `Application configuration not found: ${configPath}\n` +
167
- `Current directory: ${currentDir}\n` +
168
- `Expected location: ${expectedDir}\n` +
169
- 'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
170
- `Run 'aifabrix create ${appName}' first if configuration doesn't exist`
171
- );
172
- }
173
-
174
- const configContent = fsSync.readFileSync(configPath, 'utf8');
175
- const config = yaml.load(configContent);
176
-
177
- // Validate configuration
178
- const validation = await validator.validateApplication(appName);
179
- if (!validation.valid) {
180
- const allErrors = [];
181
-
182
- if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
183
- allErrors.push('variables.yaml:');
184
- allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
185
- }
186
-
187
- if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
188
- allErrors.push('rbac.yaml:');
189
- allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
190
- }
191
-
192
- if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
193
- allErrors.push('env.template:');
194
- allErrors.push(...validation.env.errors.map(err => ` ${err}`));
195
- }
196
-
197
- if (allErrors.length === 0) {
198
- throw new Error('Configuration validation failed');
199
- }
200
-
201
- throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
202
- }
203
-
204
- return config;
205
- }
206
-
207
- /**
208
- * Checks prerequisites: Docker image and infrastructure
209
- * @async
210
- * @param {string} appName - Application name
211
- * @param {Object} config - Application configuration
212
- * @param {boolean} [debug=false] - Enable debug logging
213
- * @throws {Error} If prerequisites are not met
214
- */
215
- async function checkPrerequisites(appName, config, debug = false) {
216
- // Extract image name from configuration (same logic as build process)
217
- const imageName = composeGenerator.getImageName(config, appName);
218
- const imageTag = config.image?.tag || 'latest';
219
- const fullImageName = `${imageName}:${imageTag}`;
220
-
221
- if (debug) {
222
- logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
223
- }
224
-
225
- // Check if Docker image exists
226
- logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
227
- const imageExists = await checkImageExists(imageName, imageTag, debug);
228
- if (!imageExists) {
229
- throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
230
- }
231
- logger.log(chalk.green(`✓ Image ${fullImageName} found`));
232
-
233
- // Check infrastructure health
234
- logger.log(chalk.blue('Checking infrastructure health...'));
235
- const infraHealth = await infra.checkInfraHealth();
236
- if (debug) {
237
- logger.log(chalk.gray(`[DEBUG] Infrastructure health: ${JSON.stringify(infraHealth, null, 2)}`));
238
- }
239
- const unhealthyServices = Object.entries(infraHealth)
240
- .filter(([_, status]) => status !== 'healthy')
241
- .map(([service, _]) => service);
242
-
243
- if (unhealthyServices.length > 0) {
244
- throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
245
- }
246
- logger.log(chalk.green('✓ Infrastructure is running'));
247
- }
248
- /**
249
- * Prepares environment: ensures .env file and generates Docker Compose
250
- * @async
251
- * @param {string} appName - Application name
252
- * @param {Object} appConfig - Application configuration
253
- * @param {Object} options - Run options
254
- * @returns {Promise<string>} Path to generated compose file
255
- */
256
- async function prepareEnvironment(appName, appConfig, options) {
257
- // Get developer ID and dev-specific directory
258
- const developerId = await config.getDeveloperId();
259
- const devDir = buildCopy.getDevDirectory(appName, developerId);
260
-
261
- // Ensure dev directory exists (should exist from build, but check anyway)
262
- if (!fsSync.existsSync(devDir)) {
263
- await buildCopy.copyBuilderToDevDirectory(appName, developerId);
264
- }
265
-
266
- // Ensure .env file exists with 'docker' environment context (for running in Docker)
267
- // Generate in builder directory first, then copy to dev directory
268
- const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
269
- if (!fsSync.existsSync(builderEnvPath)) {
270
- logger.log(chalk.yellow('Generating .env file from template...'));
271
- await secrets.generateEnvFile(appName, null, 'docker');
272
- } else {
273
- // Re-generate with 'docker' context to ensure correct hostnames for Docker
274
- logger.log(chalk.blue('Updating .env file for Docker environment...'));
275
- await secrets.generateEnvFile(appName, null, 'docker');
276
- }
277
-
278
- // Copy .env to dev directory
279
- const devEnvPath = path.join(devDir, '.env');
280
- if (fsSync.existsSync(builderEnvPath)) {
281
- await fs.copyFile(builderEnvPath, devEnvPath);
282
- }
283
-
284
- // Also ensure .env file in apps/ directory is updated (for Docker build context)
285
- const variablesPath = path.join(devDir, 'variables.yaml');
286
- if (fsSync.existsSync(variablesPath)) {
287
- const variablesContent = fsSync.readFileSync(variablesPath, 'utf8');
288
- const variables = yaml.load(variablesContent);
289
-
290
- if (variables?.build?.envOutputPath && variables.build.envOutputPath !== null) {
291
- // The generateEnvFile already copies to apps/, but ensure it's using docker context
292
- logger.log(chalk.blue('Ensuring .env file in apps/ directory is updated for Docker...'));
293
- await secrets.generateEnvFile(appName, null, 'docker');
294
- // Copy .env to dev directory again after regeneration
295
- if (fsSync.existsSync(builderEnvPath)) {
296
- await fs.copyFile(builderEnvPath, devEnvPath);
297
- }
298
- }
299
- }
300
-
301
- // Generate Docker Compose configuration
302
- logger.log(chalk.blue('Generating Docker Compose configuration...'));
303
- const composeOptions = { ...options };
304
- if (!composeOptions.port) {
305
- const basePort = appConfig.port || 3000;
306
- composeOptions.port = developerId === 0 ? basePort : basePort + (developerId * 100);
307
- }
308
- const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
309
-
310
- // Write compose file to dev-specific directory (devDir already defined above)
311
- // Ensure dev directory exists (should exist from build, but check anyway)
312
- if (!fsSync.existsSync(devDir)) {
313
- await buildCopy.copyBuilderToDevDirectory(appName, developerId);
314
- }
315
- const tempComposePath = path.join(devDir, 'docker-compose.yaml');
316
- await fs.writeFile(tempComposePath, composeContent);
317
-
318
- return tempComposePath;
319
- }
320
-
321
- /**
322
- * Starts the container and waits for health check
323
- * @async
324
- * @param {string} appName - Application name
325
- * @param {string} composePath - Path to Docker Compose file
326
- * @param {number} port - Application port
327
- * @param {Object} config - Application configuration
328
- * @param {boolean} [debug=false] - Enable debug logging
329
- * @throws {Error} If container fails to start or become healthy
330
- */
331
- async function startContainer(appName, composePath, port, config = null, debug = false) {
332
- logger.log(chalk.blue(`Starting ${appName}...`));
333
-
334
- // Ensure ADMIN_SECRETS_PATH is set for db-init service
335
- const adminSecretsPath = await infra.ensureAdminSecrets();
336
- if (debug) {
337
- logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
338
- }
339
-
340
- // Load POSTGRES_PASSWORD from admin-secrets.env
341
- const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
342
- const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
343
- const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
344
-
345
- // Set environment variables for docker-compose
346
- const env = {
347
- ...process.env,
348
- ADMIN_SECRETS_PATH: adminSecretsPath,
349
- POSTGRES_PASSWORD: postgresPassword
350
- };
351
-
352
- if (debug) {
353
- logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
354
- }
355
-
356
- const composeCmd = `docker-compose -f "${composePath}" up -d`;
357
- if (debug) {
358
- logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
359
- logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
360
- }
361
- await execAsync(composeCmd, { env });
362
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
363
- const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
364
- logger.log(chalk.green(`✓ Container ${containerName} started`));
365
- if (debug) {
366
- // Get container status after start
367
- const statusCmd = `docker ps --filter "name=${containerName}" --format "{{.Status}}"`;
368
- const { stdout: status } = await execAsync(statusCmd);
369
- const portsCmd = `docker ps --filter "name=${containerName}" --format "{{.Ports}}"`;
370
- const { stdout: ports } = await execAsync(portsCmd);
371
- logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
372
- logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
373
- }
374
-
375
- // Wait for health check using host port (CLI --port or dev-specific port, NOT localPort)
376
- const healthCheckPath = config?.healthCheck?.path || '/health';
377
- logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
378
- await waitForHealthCheck(appName, 90, port, config, debug);
379
- }
380
- /**
381
- * Displays run status after successful start
382
- * @param {string} appName - Application name
383
- * @param {number} port - Application port
384
- * @param {Object} config - Application configuration (with developerId property)
385
- */
386
- async function displayRunStatus(appName, port, config) {
387
- // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
388
- const containerName = config.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${config.developerId}-${appName}`;
389
- const healthCheckPath = config?.healthCheck?.path || '/health';
390
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
391
-
392
- logger.log(chalk.green(`\n✓ App running at http://localhost:${port}`));
393
- logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
394
- logger.log(chalk.gray(`Container: ${containerName}`));
395
- }
17
+ // Helper functions extracted to reduce file size and complexity
18
+ const helpers = require('./app-run-helpers');
396
19
 
397
20
  /**
398
21
  * Runs the application locally using Docker
@@ -421,7 +44,7 @@ async function runApp(appName, options = {}) {
421
44
 
422
45
  try {
423
46
  // Validate app name and load configuration
424
- const appConfig = await validateAppConfiguration(appName);
47
+ const appConfig = await helpers.validateAppConfiguration(appName);
425
48
 
426
49
  // Load developer ID once from config module - it's now cached and available as config.developerId
427
50
  // Developer ID: 0 = default infra, > 0 = developer-specific
@@ -433,21 +56,23 @@ async function runApp(appName, options = {}) {
433
56
  }
434
57
 
435
58
  // Check prerequisites: image and infrastructure
436
- await checkPrerequisites(appName, appConfig, debug);
59
+ await helpers.checkPrerequisites(appName, appConfig, debug);
437
60
 
438
61
  // Check if container is already running
439
- const containerRunning = await checkContainerRunning(appName, appConfig.developerId, debug);
62
+ const containerRunning = await helpers.checkContainerRunning(appName, appConfig.developerId, debug);
440
63
  if (containerRunning) {
441
64
  // Dev 0: aifabrix-{appName} (no dev-0 suffix), Dev > 0: aifabrix-dev{id}-{appName}
442
- const containerName = appConfig.developerId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
65
+ const idNum2 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
66
+ const containerName = idNum2 === 0 ? `aifabrix-${appName}` : `aifabrix-dev${appConfig.developerId}-${appName}`;
443
67
  logger.log(chalk.yellow(`Container ${containerName} is already running`));
444
- await stopAndRemoveContainer(appName, appConfig.developerId, debug);
68
+ await helpers.stopAndRemoveContainer(appName, appConfig.developerId, debug);
445
69
  }
446
70
 
447
71
  // Calculate host port: use dev-specific port offset if not overridden
448
72
  // IMPORTANT: Container port (for Dockerfile) stays unchanged from appConfig.port
449
73
  const basePort = appConfig.port || 3000;
450
- const hostPort = options.port || (appConfig.developerId === 0 ? basePort : basePort + (appConfig.developerId * 100));
74
+ const idNum3 = typeof appConfig.developerId === 'string' ? parseInt(appConfig.developerId, 10) : appConfig.developerId;
75
+ const hostPort = options.port || (idNum3 === 0 ? basePort : basePort + (idNum3 * 100));
451
76
  if (debug) {
452
77
  logger.log(chalk.gray(`[DEBUG] Host port: ${hostPort} (${options.port ? 'CLI override' : 'dev-specific'}), Container port: ${appConfig.build?.containerPort || appConfig.port || 3000} (unchanged)`));
453
78
  }
@@ -460,17 +85,17 @@ async function runApp(appName, options = {}) {
460
85
  }
461
86
 
462
87
  // Prepare environment: ensure .env file and generate Docker Compose
463
- const tempComposePath = await prepareEnvironment(appName, appConfig, options);
88
+ const tempComposePath = await helpers.prepareEnvironment(appName, appConfig, options);
464
89
  if (debug) {
465
90
  logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
466
91
  }
467
92
 
468
93
  try {
469
94
  // Start container and wait for health check
470
- await startContainer(appName, tempComposePath, hostPort, appConfig, debug);
95
+ await helpers.startContainer(appName, tempComposePath, hostPort, appConfig, debug);
471
96
 
472
97
  // Display success message
473
- await displayRunStatus(appName, hostPort, appConfig);
98
+ await helpers.displayRunStatus(appName, hostPort, appConfig);
474
99
 
475
100
  } catch (error) {
476
101
  // Keep the compose file for debugging - don't delete on error
@@ -491,9 +116,9 @@ async function runApp(appName, options = {}) {
491
116
  }
492
117
  module.exports = {
493
118
  runApp,
494
- checkImageExists,
495
- checkContainerRunning,
496
- stopAndRemoveContainer,
119
+ checkImageExists: helpers.checkImageExists,
120
+ checkContainerRunning: helpers.checkContainerRunning,
121
+ stopAndRemoveContainer: helpers.stopAndRemoveContainer,
497
122
  checkPortAvailable,
498
123
  generateDockerCompose: composeGenerator.generateDockerCompose,
499
124
  waitForHealthCheck
package/lib/build.js CHANGED
@@ -28,6 +28,52 @@ const buildCopy = require('./utils/build-copy');
28
28
 
29
29
  const execAsync = promisify(exec);
30
30
 
31
+ /**
32
+ * Copies application template files to dev directory
33
+ * Used when apps directory doesn't exist to ensure build can proceed
34
+ * @async
35
+ * @param {string} templatePath - Path to template directory
36
+ * @param {string} devDir - Target dev directory
37
+ * @param {string} _language - Language (typescript/python) - currently unused but kept for future use
38
+ * @throws {Error} If copying fails
39
+ */
40
+ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
41
+ if (!fsSync.existsSync(templatePath)) {
42
+ throw new Error(`Template path not found: ${templatePath}`);
43
+ }
44
+
45
+ const entries = await fs.readdir(templatePath);
46
+
47
+ // Copy only application files, skip Dockerfile and docker-compose templates
48
+ const appFiles = entries.filter(entry => {
49
+ const lowerEntry = entry.toLowerCase();
50
+ // Include .gitignore, exclude .hbs files and docker-related files
51
+ if (entry === '.gitignore') {
52
+ return true;
53
+ }
54
+ if (lowerEntry.endsWith('.hbs')) {
55
+ return false;
56
+ }
57
+ if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
58
+ return false;
59
+ }
60
+ if (entry.startsWith('.') && entry !== '.gitignore') {
61
+ return false;
62
+ }
63
+ return true;
64
+ });
65
+
66
+ for (const entry of appFiles) {
67
+ const sourcePath = path.join(templatePath, entry);
68
+ const targetPath = path.join(devDir, entry);
69
+
70
+ const entryStats = await fs.stat(sourcePath);
71
+ if (entryStats.isFile()) {
72
+ await fs.copyFile(sourcePath, targetPath);
73
+ }
74
+ }
75
+ }
76
+
31
77
  /**
32
78
  * Loads variables.yaml configuration for an application
33
79
  * @param {string} appName - Application name
@@ -91,7 +137,6 @@ function detectLanguage(appPath) {
91
137
  const packageJsonPath = path.join(appPath, 'package.json');
92
138
  const requirementsPath = path.join(appPath, 'requirements.txt');
93
139
  const pyprojectPath = path.join(appPath, 'pyproject.toml');
94
- const dockerfilePath = path.join(appPath, 'Dockerfile');
95
140
 
96
141
  // Check for package.json (TypeScript/Node.js)
97
142
  if (fsSync.existsSync(packageJsonPath)) {
@@ -103,11 +148,6 @@ function detectLanguage(appPath) {
103
148
  return 'python';
104
149
  }
105
150
 
106
- // Check for custom Dockerfile
107
- if (fsSync.existsSync(dockerfilePath)) {
108
- throw new Error('Custom Dockerfile found. Use --force-template to regenerate from template.');
109
- }
110
-
111
151
  // Default to typescript if no indicators found
112
152
  return 'typescript';
113
153
  }
@@ -310,16 +350,49 @@ async function buildApp(appName, options = {}) {
310
350
 
311
351
  // 2. Get developer ID and copy files to dev-specific directory
312
352
  const developerId = await config.getDeveloperId();
313
- const directoryName = developerId === 0 ? 'applications' : `dev-${developerId}`;
353
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
354
+ const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
314
355
  logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
315
356
  const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
316
357
  logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
317
358
 
359
+ // 2a. Check if application source files exist, if not copy from templates
360
+ const appsPath = path.join(process.cwd(), 'apps', appName);
361
+ if (fsSync.existsSync(appsPath)) {
362
+ // Copy app source files from apps directory
363
+ await buildCopy.copyAppSourceFiles(appsPath, devDir);
364
+ logger.log(chalk.green(`✓ Copied application source files from apps/${appName}`));
365
+ } else {
366
+ // No apps directory - check if we need to copy template files
367
+ const language = options.language || buildConfig.language || detectLanguage(devDir);
368
+ const packageJsonPath = path.join(devDir, 'package.json');
369
+ const requirementsPath = path.join(devDir, 'requirements.txt');
370
+
371
+ if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
372
+ // Copy TypeScript template files
373
+ const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
374
+ await copyTemplateFilesToDevDir(templatePath, devDir, language);
375
+ logger.log(chalk.green(`✓ Generated application files from ${language} template`));
376
+ } else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
377
+ // Copy Python template files
378
+ const templatePath = path.join(__dirname, '..', 'templates', 'python');
379
+ await copyTemplateFilesToDevDir(templatePath, devDir, language);
380
+ logger.log(chalk.green(`✓ Generated application files from ${language} template`));
381
+ }
382
+ }
383
+
318
384
  // 3. Prepare build context (use dev-specific directory)
319
385
  // If buildConfig.context is relative, resolve it relative to devDir
320
386
  // If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
321
387
  let contextPath;
322
- if (buildConfig.context && buildConfig.context !== '../..') {
388
+
389
+ // Check if context is using old format (../appName) - these are incompatible with dev directory structure
390
+ if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
391
+ // Old format detected - always use devDir instead
392
+ logger.log(chalk.yellow(`⚠️ Warning: Build context uses old format: ${buildConfig.context}`));
393
+ logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
394
+ contextPath = devDir;
395
+ } else if (buildConfig.context && buildConfig.context !== '../..') {
323
396
  // Resolve relative context path from dev directory
324
397
  contextPath = path.resolve(devDir, buildConfig.context);
325
398
  } else if (buildConfig.context === '../..') {
@@ -333,6 +406,22 @@ async function buildApp(appName, options = {}) {
333
406
  contextPath = devDir;
334
407
  }
335
408
 
409
+ // Ensure context path is absolute and normalized
410
+ contextPath = path.resolve(contextPath);
411
+
412
+ // Validate that context path exists (skip in test environments)
413
+ const isTestEnv = process.env.NODE_ENV === 'test' ||
414
+ process.env.JEST_WORKER_ID !== undefined ||
415
+ typeof jest !== 'undefined';
416
+
417
+ if (!isTestEnv && !fsSync.existsSync(contextPath)) {
418
+ throw new Error(
419
+ `Build context path does not exist: ${contextPath}\n` +
420
+ `Expected dev directory: ${devDir}\n` +
421
+ 'Please ensure files were copied correctly or update the context in variables.yaml.'
422
+ );
423
+ }
424
+
336
425
  // 4. Check if Dockerfile exists in dev directory
337
426
  const appDockerfilePath = path.join(devDir, 'Dockerfile');
338
427
  const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
@@ -362,6 +451,11 @@ async function buildApp(appName, options = {}) {
362
451
 
363
452
  // 6. Build Docker image
364
453
  const tag = options.tag || 'latest';
454
+
455
+ // Log paths for debugging
456
+ logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
457
+ logger.log(chalk.blue(`Using build context: ${contextPath}`));
458
+
365
459
  await executeBuild(imageName, dockerfilePath, contextPath, tag, options);
366
460
 
367
461
  // 7. Post-build tasks