genbox 1.0.91 → 1.0.93

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.
@@ -796,11 +796,15 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
796
796
  const appPath = config.apps[app.name]?.path || app.name;
797
797
  const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
798
798
  (resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
799
+ // Get runner type to determine infrastructure URL prefix (docker vs host)
800
+ const runner = config.apps[app.name]?.runner || 'pm2';
801
+ const infraUrlMap = (0, utils_1.buildInfraUrlMap)(envVarsFromFile, runner);
802
+ const combinedUrlMap = { ...serviceUrlMap, ...infraUrlMap };
799
803
  const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
800
804
  if (servicesSections.length > 0) {
801
805
  for (const serviceSectionName of servicesSections) {
802
806
  const serviceName = serviceSectionName.split('/')[1];
803
- const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
807
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, combinedUrlMap);
804
808
  const stagingName = `${app.name}-${serviceName}.env`;
805
809
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
806
810
  files.push({
@@ -812,7 +816,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
812
816
  }
813
817
  }
814
818
  else {
815
- const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
819
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, combinedUrlMap);
816
820
  files.push({
817
821
  path: `/home/dev/.env-staging/${app.name}.env`,
818
822
  content: appEnvContent,
@@ -934,10 +938,39 @@ function generateSetupScript(resolved, config, envFilesToMove = []) {
934
938
  lines.push(' \' "$file" | grep -v "^$" | head -n -1');
935
939
  lines.push(' }');
936
940
  lines.push('');
941
+ lines.push(' # Function to expand infrastructure URL placeholders based on runner type');
942
+ lines.push(' # Docker apps use DOCKER_* values, PM2/host apps use HOST_* values');
943
+ lines.push(' expand_infra_urls() {');
944
+ lines.push(' local envfile="$1"');
945
+ lines.push(' local runner="$2"');
946
+ lines.push(' if [ "$runner" = "docker" ]; then');
947
+ lines.push(' # For docker apps, replace ${VAR} with DOCKER_VAR value');
948
+ lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
949
+ lines.push(' docker_val=$(grep "^DOCKER_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
950
+ lines.push(' if [ -n "$docker_val" ]; then');
951
+ lines.push(' sed -i "s|\\${${var}}|${docker_val}|g" "$envfile"');
952
+ lines.push(' fi');
953
+ lines.push(' done');
954
+ lines.push(' else');
955
+ lines.push(' # For PM2/host apps, replace ${VAR} with HOST_VAR value');
956
+ lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
957
+ lines.push(' host_val=$(grep "^HOST_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
958
+ lines.push(' if [ -n "$host_val" ]; then');
959
+ lines.push(' sed -i "s|\\${${var}}|${host_val}|g" "$envfile"');
960
+ lines.push(' fi');
961
+ lines.push(' done');
962
+ lines.push(' fi');
963
+ lines.push(' }');
964
+ lines.push('');
937
965
  for (const { stagingName, targetPath } of envFilesToMove) {
938
- lines.push(` # Create .env for ${stagingName}`);
966
+ // Determine runner type for this app
967
+ // stagingName could be "appName" or "appName/serviceName" for service sections
968
+ const appName = stagingName.includes('/') ? stagingName.split('/')[0] : stagingName;
969
+ const runner = config.apps[appName]?.runner || 'pm2';
970
+ lines.push(` # Create .env for ${stagingName} (runner: ${runner})`);
939
971
  lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
940
972
  lines.push(` extract_section "${stagingName}" /home/dev/.env.genbox > "${targetPath}"`);
973
+ lines.push(` expand_infra_urls "${targetPath}" "${runner}"`);
941
974
  lines.push(` echo " Created ${targetPath}"`);
942
975
  }
943
976
  lines.push('');
@@ -151,9 +151,32 @@ function convertScanToDetected(scan, root) {
151
151
  })),
152
152
  apps: {},
153
153
  };
154
- // Convert apps
154
+ // Collect docker app build contexts first (docker takes precedence over package.json)
155
+ const dockerBuildContexts = new Set();
156
+ if (scan.compose?.applications) {
157
+ for (const dockerApp of scan.compose.applications) {
158
+ if (dockerApp.build?.context) {
159
+ // Normalize the path for comparison
160
+ const ctx = dockerApp.build.context.replace(/^\.?\/?/, '').replace(/\/$/, '');
161
+ dockerBuildContexts.add(ctx);
162
+ }
163
+ }
164
+ }
165
+ // Map to store git info for directories with docker apps (to preserve repo info)
166
+ const dockerDirGitInfo = new Map();
167
+ // Convert apps from package.json (skip if docker app builds from same directory)
155
168
  const isMultiRepo = scan.structure.type === 'hybrid';
156
169
  for (const app of scan.apps) {
170
+ // Skip if there's a docker app building from this directory
171
+ const normalizedPath = app.path.replace(/^\.?\/?/, '').replace(/\/$/, '');
172
+ if (dockerBuildContexts.has(normalizedPath)) {
173
+ // Save git info for this directory so docker apps can use it
174
+ const gitInfo = detectGitForDirectory(path_1.default.join(root, app.path));
175
+ if (gitInfo) {
176
+ dockerDirGitInfo.set(normalizedPath, gitInfo);
177
+ }
178
+ continue; // Docker app takes precedence
179
+ }
157
180
  const mappedType = mapAppType(app.type);
158
181
  let appGit = undefined;
159
182
  if (isMultiRepo) {
@@ -179,7 +202,9 @@ function convertScanToDetected(scan, root) {
179
202
  git: appGit,
180
203
  };
181
204
  }
182
- // Convert infrastructure
205
+ // Store dockerDirGitInfo for use when adding docker apps
206
+ const _dockerDirGitInfo = dockerDirGitInfo;
207
+ // Convert infrastructure (keep all, no deduplication - user selects by source file)
183
208
  if (scan.compose) {
184
209
  detected.infrastructure = [];
185
210
  for (const db of scan.compose.databases || []) {
@@ -188,7 +213,7 @@ function convertScanToDetected(scan, root) {
188
213
  type: 'database',
189
214
  image: db.image || 'unknown',
190
215
  port: db.ports?.[0]?.host || 0,
191
- source: 'docker-compose.yml',
216
+ source: db.sourceFile || 'docker-compose.yml',
192
217
  });
193
218
  }
194
219
  for (const cache of scan.compose.caches || []) {
@@ -197,7 +222,7 @@ function convertScanToDetected(scan, root) {
197
222
  type: 'cache',
198
223
  image: cache.image || 'unknown',
199
224
  port: cache.ports?.[0]?.host || 0,
200
- source: 'docker-compose.yml',
225
+ source: cache.sourceFile || 'docker-compose.yml',
201
226
  });
202
227
  }
203
228
  for (const queue of scan.compose.queues || []) {
@@ -206,21 +231,56 @@ function convertScanToDetected(scan, root) {
206
231
  type: 'queue',
207
232
  image: queue.image || 'unknown',
208
233
  port: queue.ports?.[0]?.host || 0,
209
- source: 'docker-compose.yml',
234
+ source: queue.sourceFile || 'docker-compose.yml',
235
+ });
236
+ }
237
+ // Add other infrastructure (vector DBs, search engines, etc.)
238
+ for (const infra of scan.compose.infrastructure || []) {
239
+ detected.infrastructure.push({
240
+ name: infra.name,
241
+ type: 'other',
242
+ image: infra.image || 'unknown',
243
+ port: infra.ports?.[0]?.host || 0,
244
+ source: infra.sourceFile || 'docker-compose.yml',
210
245
  });
211
246
  }
212
247
  // Add Docker application services as apps with runner: 'docker'
213
248
  if (scan.compose.applications && scan.compose.applications.length > 0) {
249
+ // Collect infrastructure service keys (name@source) to exclude from apps
250
+ // Only exclude if same name AND same source file
251
+ const infraKeys = new Set([
252
+ ...(scan.compose.databases || []).map(d => `${d.name}@${d.sourceFile || ''}`),
253
+ ...(scan.compose.caches || []).map(c => `${c.name}@${c.sourceFile || ''}`),
254
+ ...(scan.compose.queues || []).map(q => `${q.name}@${q.sourceFile || ''}`),
255
+ ...(scan.compose.infrastructure || []).map(i => `${i.name}@${i.sourceFile || ''}`),
256
+ ]);
214
257
  for (const dockerApp of scan.compose.applications) {
215
- if (detected.apps[dockerApp.name])
216
- continue;
258
+ const infraCheckKey = `${dockerApp.name}@${dockerApp.sourceFile || ''}`;
259
+ if (infraKeys.has(infraCheckKey))
260
+ continue; // Skip infrastructure services from same file
217
261
  const { type: mappedType, reason: typeReason } = inferDockerAppType(dockerApp.name, dockerApp.build?.context, root);
218
- detected.apps[dockerApp.name] = {
262
+ // Use unique key if app name already exists (from different source)
263
+ let detectedAppKey = dockerApp.name;
264
+ if (detected.apps[detectedAppKey]) {
265
+ // Check if it's from a different source - if so, use source in key
266
+ const existingSource = detected.apps[detectedAppKey].source;
267
+ const newSource = dockerApp.sourceFile || 'docker-compose';
268
+ if (existingSource !== newSource) {
269
+ detectedAppKey = `${dockerApp.name}@${newSource}`;
270
+ }
271
+ else {
272
+ continue; // Same source, skip duplicate
273
+ }
274
+ }
275
+ // Look up git info for this docker app's build context
276
+ const buildContext = dockerApp.build?.context?.replace(/^\.?\/?/, '').replace(/\/$/, '') || '';
277
+ const dockerAppGit = _dockerDirGitInfo.get(buildContext);
278
+ detected.apps[detectedAppKey] = {
219
279
  path: dockerApp.build?.context || '.',
220
280
  type: mappedType,
221
281
  type_reason: typeReason,
222
282
  runner: 'docker',
223
- runner_reason: 'defined in docker-compose.yml',
283
+ runner_reason: `defined in ${dockerApp.sourceFile || 'docker-compose.yml'}`,
224
284
  docker: {
225
285
  service: dockerApp.name,
226
286
  build_context: dockerApp.build?.context,
@@ -228,7 +288,9 @@ function convertScanToDetected(scan, root) {
228
288
  image: dockerApp.image,
229
289
  },
230
290
  port: dockerApp.ports?.[0]?.host,
231
- port_source: 'docker-compose.yml ports',
291
+ port_source: `${dockerApp.sourceFile || 'docker-compose.yml'} ports`,
292
+ source: dockerApp.sourceFile || 'docker-compose',
293
+ git: dockerAppGit,
232
294
  };
233
295
  }
234
296
  }
@@ -293,74 +355,216 @@ function saveDetectedConfig(rootDir, detected) {
293
355
  // App Configuration
294
356
  // =============================================================================
295
357
  /**
296
- * Interactive app selection
358
+ * Get app source group for grouping purposes
359
+ */
360
+ function getAppSourceGroup(name, app) {
361
+ // Docker apps - group by docker-compose source file
362
+ if (app.runner === 'docker' && app.source) {
363
+ return app.source;
364
+ }
365
+ // Use app path for grouping
366
+ if (app.path && app.path !== '.') {
367
+ return app.path;
368
+ }
369
+ return 'root';
370
+ }
371
+ /**
372
+ * Interactive app selection (grouped by source)
297
373
  */
298
374
  async function selectApps(detected) {
299
375
  const appEntries = Object.entries(detected.apps);
300
376
  if (appEntries.length === 0)
301
377
  return detected;
378
+ // Group apps by source
379
+ const appsBySource = new Map();
380
+ for (const [name, app] of appEntries) {
381
+ const source = getAppSourceGroup(name, app);
382
+ if (!appsBySource.has(source)) {
383
+ appsBySource.set(source, []);
384
+ }
385
+ appsBySource.get(source).push([name, app]);
386
+ }
302
387
  console.log('');
303
388
  console.log(chalk_1.default.blue('=== Detected Apps ==='));
304
389
  console.log('');
305
- for (const [name, app] of appEntries) {
306
- const parts = [
307
- chalk_1.default.cyan(name),
308
- app.type ? `(${app.type})` : '',
309
- app.framework ? `[${app.framework}]` : '',
310
- app.port ? `port:${app.port}` : '',
311
- app.runner ? chalk_1.default.dim(`runner:${app.runner}`) : '',
312
- ].filter(Boolean);
313
- console.log(` ${parts.join(' ')}`);
314
- if (app.git) {
315
- console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
390
+ // Display grouped by source
391
+ for (const [source, apps] of appsBySource) {
392
+ console.log(chalk_1.default.dim(` ${source}:`));
393
+ for (const [name, app] of apps) {
394
+ const parts = [
395
+ chalk_1.default.cyan(name),
396
+ app.type ? `(${app.type})` : '',
397
+ app.framework ? `[${app.framework}]` : '',
398
+ app.port ? `port:${app.port}` : '',
399
+ app.runner ? chalk_1.default.dim(`runner:${app.runner}`) : '',
400
+ ].filter(Boolean);
401
+ console.log(` ${parts.join(' ')}`);
316
402
  }
317
403
  }
318
404
  console.log('');
319
- const appChoices = appEntries.map(([name, app]) => ({
320
- name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
321
- value: name,
322
- checked: app.type !== 'library',
323
- }));
324
- const selectedApps = await prompts.checkbox({
325
- message: 'Select apps to include:',
326
- choices: appChoices,
405
+ // If only one source group, select individual apps
406
+ if (appsBySource.size === 1) {
407
+ const appChoices = appEntries.map(([name, app]) => ({
408
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
409
+ value: name,
410
+ checked: app.type !== 'library',
411
+ }));
412
+ const selectedApps = await prompts.checkbox({
413
+ message: 'Select apps to include:',
414
+ choices: appChoices,
415
+ });
416
+ const filteredApps = {};
417
+ for (const appName of selectedApps) {
418
+ filteredApps[appName] = detected.apps[appName];
419
+ }
420
+ return { ...detected, apps: filteredApps };
421
+ }
422
+ // Multiple source groups - let user select by group first
423
+ const sourceChoices = Array.from(appsBySource.entries()).map(([source, apps]) => {
424
+ const hasLibraryOnly = apps.every(([, a]) => a.type === 'library');
425
+ const appNames = apps.map(([n]) => n).join(', ');
426
+ return {
427
+ name: `${source} (${appNames})`,
428
+ value: source,
429
+ checked: !hasLibraryOnly, // Uncheck if all are libraries
430
+ };
431
+ });
432
+ const selectedSources = await prompts.checkbox({
433
+ message: 'Select app groups to include:',
434
+ choices: sourceChoices,
327
435
  });
436
+ // Collect all apps from selected sources (excluding libraries by default)
328
437
  const filteredApps = {};
329
- for (const appName of selectedApps) {
330
- filteredApps[appName] = detected.apps[appName];
438
+ for (const source of selectedSources) {
439
+ const apps = appsBySource.get(source);
440
+ if (apps) {
441
+ for (const [name, app] of apps) {
442
+ // Include non-libraries by default from selected groups
443
+ if (app.type !== 'library') {
444
+ filteredApps[name] = app;
445
+ }
446
+ }
447
+ }
448
+ }
449
+ // If user selected groups, ask if they want to fine-tune individual apps
450
+ if (selectedSources.length > 0 && Object.keys(filteredApps).length > 0) {
451
+ const fineTune = await prompts.confirm({
452
+ message: `${Object.keys(filteredApps).length} apps selected. Fine-tune individual apps?`,
453
+ default: false,
454
+ });
455
+ if (fineTune) {
456
+ // Show all apps from selected groups for fine-tuning
457
+ const allAppsInGroups = [];
458
+ for (const source of selectedSources) {
459
+ const apps = appsBySource.get(source);
460
+ if (apps)
461
+ allAppsInGroups.push(...apps);
462
+ }
463
+ const appChoices = allAppsInGroups.map(([name, app]) => ({
464
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
465
+ value: name,
466
+ checked: app.type !== 'library',
467
+ }));
468
+ const selectedApps = await prompts.checkbox({
469
+ message: 'Select individual apps:',
470
+ choices: appChoices,
471
+ });
472
+ const finalApps = {};
473
+ for (const appName of selectedApps) {
474
+ finalApps[appName] = detected.apps[appName];
475
+ }
476
+ return { ...detected, apps: finalApps };
477
+ }
331
478
  }
332
479
  return { ...detected, apps: filteredApps };
333
480
  }
334
481
  /**
335
- * Interactive infrastructure selection
482
+ * Interactive infrastructure selection (grouped by source file)
336
483
  */
337
484
  async function selectInfrastructure(detected) {
338
485
  if (!detected.infrastructure || detected.infrastructure.length === 0) {
339
486
  return detected;
340
487
  }
488
+ // Group infrastructure by source file
489
+ const infraBySource = new Map();
490
+ for (const infra of detected.infrastructure) {
491
+ const source = infra.source || 'unknown';
492
+ if (!infraBySource.has(source)) {
493
+ infraBySource.set(source, []);
494
+ }
495
+ infraBySource.get(source).push(infra);
496
+ }
341
497
  console.log('');
342
498
  console.log(chalk_1.default.blue('=== Detected Infrastructure ==='));
343
499
  console.log('');
344
- for (const infra of detected.infrastructure) {
345
- const parts = [
346
- chalk_1.default.cyan(infra.name),
347
- `(${infra.type})`,
348
- infra.image ? `[${infra.image}]` : '',
349
- infra.port ? `port:${infra.port}` : '',
350
- ].filter(Boolean);
351
- console.log(` ${parts.join(' ')}`);
500
+ // Display grouped by source
501
+ for (const [source, infraList] of infraBySource) {
502
+ console.log(chalk_1.default.dim(` ${source}:`));
503
+ for (const infra of infraList) {
504
+ const parts = [
505
+ chalk_1.default.cyan(infra.name),
506
+ `(${infra.type})`,
507
+ infra.image && infra.image !== 'unknown' ? `[${infra.image}]` : '',
508
+ infra.port ? `port:${infra.port}` : '',
509
+ ].filter(Boolean);
510
+ console.log(` ${parts.join(' ')}`);
511
+ }
352
512
  }
353
513
  console.log('');
354
- const infraChoices = detected.infrastructure.map(infra => ({
355
- name: `${infra.name} (${infra.type}${infra.image ? `, ${infra.image}` : ''})`,
356
- value: infra.name,
357
- checked: true, // Infrastructure is typically required, so default to selected
358
- }));
359
- const selectedInfra = await prompts.checkbox({
360
- message: 'Select infrastructure to include:',
361
- choices: infraChoices,
514
+ // If only one source file, select individual services
515
+ if (infraBySource.size === 1) {
516
+ const infraChoices = detected.infrastructure.map(infra => ({
517
+ name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''})`,
518
+ value: infra.name,
519
+ checked: true,
520
+ }));
521
+ const selectedInfra = await prompts.checkbox({
522
+ message: 'Select infrastructure to include:',
523
+ choices: infraChoices,
524
+ });
525
+ const filteredInfra = detected.infrastructure.filter(infra => selectedInfra.includes(infra.name));
526
+ return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
527
+ }
528
+ // Multiple source files - let user select by file first
529
+ const sourceChoices = Array.from(infraBySource.entries()).map(([source, infraList]) => {
530
+ const serviceNames = infraList.map(i => i.name).join(', ');
531
+ return {
532
+ name: `${source} (${serviceNames})`,
533
+ value: source,
534
+ checked: true,
535
+ };
362
536
  });
363
- const filteredInfra = detected.infrastructure.filter(infra => selectedInfra.includes(infra.name));
537
+ const selectedSources = await prompts.checkbox({
538
+ message: 'Select infrastructure sources to include:',
539
+ choices: sourceChoices,
540
+ });
541
+ // Collect all infrastructure from selected sources
542
+ let filteredInfra = [];
543
+ for (const source of selectedSources) {
544
+ const infraList = infraBySource.get(source);
545
+ if (infraList) {
546
+ filteredInfra.push(...infraList);
547
+ }
548
+ }
549
+ // If user selected sources, ask if they want to fine-tune individual services
550
+ if (selectedSources.length > 0 && filteredInfra.length > 0) {
551
+ const fineTune = await prompts.confirm({
552
+ message: `${filteredInfra.length} infrastructure services selected. Fine-tune individual services?`,
553
+ default: false,
554
+ });
555
+ if (fineTune) {
556
+ const infraChoices = filteredInfra.map(infra => ({
557
+ name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''}) - ${infra.source}`,
558
+ value: `${infra.name}@${infra.source}`, // Unique key
559
+ checked: true,
560
+ }));
561
+ const selectedInfraKeys = await prompts.checkbox({
562
+ message: 'Select individual infrastructure services:',
563
+ choices: infraChoices,
564
+ });
565
+ filteredInfra = filteredInfra.filter(infra => selectedInfraKeys.includes(`${infra.name}@${infra.source}`));
566
+ }
567
+ }
364
568
  return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
365
569
  }
366
570
  /**
@@ -1217,6 +1421,36 @@ function generateEnvFile(projectName, detected, envVars, serviceUrlMappings) {
1217
1421
  }
1218
1422
  }
1219
1423
  }
1424
+ // Add infrastructure URL mappings (HOST_ vs DOCKER_ prefixes)
1425
+ // These are used to differentiate between PM2 apps (use HOST_*) and Docker apps (use DOCKER_*)
1426
+ if (detected.infrastructure && detected.infrastructure.length > 0) {
1427
+ content += `\n# Infrastructure URL Configuration\n`;
1428
+ content += `# HOST_* = for PM2/host-based apps (uses localhost)\n`;
1429
+ content += `# DOCKER_* = for Docker apps (uses docker network hostnames)\n\n`;
1430
+ for (const infra of detected.infrastructure) {
1431
+ const name = infra.name.toLowerCase();
1432
+ const port = infra.port;
1433
+ if (infra.type === 'database' && infra.image?.includes('mongo')) {
1434
+ // MongoDB - use project name as database name
1435
+ const dbName = projectName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
1436
+ content += `HOST_MONGODB_URI=mongodb://localhost:${port}/${dbName}\n`;
1437
+ content += `DOCKER_MONGODB_URI=mongodb://${name}:27017/${dbName}\n`;
1438
+ content += `MONGODB_URI=\${MONGODB_URI}\n\n`;
1439
+ }
1440
+ else if (infra.type === 'cache' && infra.image?.includes('redis')) {
1441
+ // Redis
1442
+ content += `HOST_REDIS_URL=redis://localhost:${port}\n`;
1443
+ content += `DOCKER_REDIS_URL=redis://${name}:6379\n`;
1444
+ content += `REDIS_URL=\${REDIS_URL}\n\n`;
1445
+ }
1446
+ else if (infra.type === 'queue' && infra.image?.includes('rabbitmq')) {
1447
+ // RabbitMQ
1448
+ content += `HOST_RABBITMQ_URL=amqp://localhost:${port}\n`;
1449
+ content += `DOCKER_RABBITMQ_URL=amqp://${name}:5672\n`;
1450
+ content += `RABBITMQ_URL=\${RABBITMQ_URL}\n\n`;
1451
+ }
1452
+ }
1453
+ }
1220
1454
  // Add GIT_TOKEN placeholder if not present
1221
1455
  if (!envVars['GIT_TOKEN']) {
1222
1456
  content += `\n# Git authentication\n# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
@@ -93,6 +93,18 @@ const INFRA_PATTERNS = [
93
93
  /^zipkin/i,
94
94
  /^vault/i,
95
95
  /^consul/i,
96
+ // Vector databases
97
+ /^qdrant/i,
98
+ /^weaviate/i,
99
+ /^semitechnologies\/weaviate/i,
100
+ /^pinecone/i,
101
+ /^milvus/i,
102
+ /^chroma/i,
103
+ // Logging
104
+ /^loki/i,
105
+ /^promtail/i,
106
+ /^fluentd/i,
107
+ /^logstash/i,
96
108
  ];
97
109
  class ComposeParser {
98
110
  /**
@@ -107,9 +119,13 @@ class ComposeParser {
107
119
  for (const file of composeFiles) {
108
120
  try {
109
121
  const content = yaml.load(fs.readFileSync(file, 'utf8'));
122
+ // Get relative path for cleaner display
123
+ const relativeFile = path.relative(root, file);
124
+ // Get compose file's directory for resolving relative build contexts
125
+ const composeDir = path.dirname(file);
110
126
  if (content && content.services) {
111
127
  for (const [name, config] of Object.entries(content.services)) {
112
- const service = this.normalizeService(name, config, root);
128
+ const service = this.normalizeService(name, config, root, composeDir, relativeFile);
113
129
  allServices.push(service);
114
130
  }
115
131
  }
@@ -177,11 +193,11 @@ class ComposeParser {
177
193
  findRecursively(root);
178
194
  return files;
179
195
  }
180
- normalizeService(name, config, root) {
196
+ normalizeService(name, config, root, composeDir, sourceFile) {
181
197
  return {
182
198
  name,
183
199
  image: config.image,
184
- build: this.normalizeBuild(config.build, root),
200
+ build: this.normalizeBuild(config.build, root, composeDir),
185
201
  ports: this.normalizePorts(config.ports),
186
202
  environment: this.normalizeEnvironment(config.environment),
187
203
  envFile: this.normalizeEnvFile(config.env_file),
@@ -190,20 +206,25 @@ class ComposeParser {
190
206
  healthcheck: this.normalizeHealthcheck(config.healthcheck),
191
207
  command: this.normalizeCommand(config.command),
192
208
  labels: this.normalizeLabels(config.labels),
209
+ sourceFile,
193
210
  };
194
211
  }
195
- normalizeBuild(build, root) {
212
+ normalizeBuild(build, root, composeDir) {
196
213
  if (!build)
197
214
  return undefined;
198
215
  if (typeof build === 'string') {
216
+ // Resolve relative to compose file directory, then make relative to root
217
+ const absoluteContext = path.resolve(composeDir, build);
199
218
  return {
200
- context: path.resolve(root, build),
219
+ context: path.relative(root, absoluteContext),
201
220
  };
202
221
  }
203
222
  if (typeof build === 'object') {
204
223
  const b = build;
224
+ // Resolve relative to compose file directory, then make relative to root
225
+ const absoluteContext = path.resolve(composeDir, b.context || '.');
205
226
  return {
206
- context: path.resolve(root, b.context || '.'),
227
+ context: path.relative(root, absoluteContext),
207
228
  dockerfile: b.dockerfile,
208
229
  target: b.target,
209
230
  };
@@ -323,8 +323,8 @@ class EnvAnalyzer {
323
323
  return type;
324
324
  }
325
325
  }
326
- // Check value format
327
- if (value) {
326
+ // Check value format (ensure value is a string)
327
+ if (value && typeof value === 'string') {
328
328
  if (/^(true|false)$/i.test(value))
329
329
  return 'boolean';
330
330
  if (/^\d+$/.test(value))
@@ -133,6 +133,9 @@ class ProjectScanner {
133
133
  type: 'backend', // Will be refined by framework detection
134
134
  scripts,
135
135
  });
136
+ // Also scan first-level subdirectories for additional apps
137
+ const subApps = await this.discoverMultiRepoApps(root, exclude);
138
+ apps.push(...subApps);
136
139
  }
137
140
  return apps;
138
141
  }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseEnvGenboxSections = parseEnvGenboxSections;
8
8
  exports.buildServiceUrlMap = buildServiceUrlMap;
9
9
  exports.buildAppEnvContent = buildAppEnvContent;
10
+ exports.buildInfraUrlMap = buildInfraUrlMap;
10
11
  exports.parseEnvVarsFromSection = parseEnvVarsFromSection;
11
12
  /**
12
13
  * Parse .env.genbox file into segregated sections
@@ -106,6 +107,61 @@ function buildAppEnvContent(sections, appName, serviceUrlMap) {
106
107
  .trim();
107
108
  return envContent;
108
109
  }
110
+ /**
111
+ * Infrastructure variable patterns that need HOST_ vs DOCKER_ prefixes
112
+ * These are database/cache/queue connection strings that differ between
113
+ * host-based apps (PM2) and containerized apps (Docker)
114
+ */
115
+ const INFRA_VAR_PATTERNS = [
116
+ /^(MONGODB|MONGO)_URI$/i,
117
+ /^(MONGODB|MONGO)_URL$/i,
118
+ /^(REDIS)_URL$/i,
119
+ /^(REDIS)_URI$/i,
120
+ /^(RABBITMQ|RABBIT|AMQP)_URL$/i,
121
+ /^(RABBITMQ|RABBIT|AMQP)_URI$/i,
122
+ /^(POSTGRES|POSTGRESQL|PG)_URL$/i,
123
+ /^(POSTGRES|POSTGRESQL|PG)_URI$/i,
124
+ /^DATABASE_URL$/i,
125
+ /^DATABASE_URI$/i,
126
+ ];
127
+ /**
128
+ * Build a map of infrastructure URL variables based on runner type
129
+ * For docker apps: use DOCKER_* prefixed values (e.g., mongodb://mongodb:27017)
130
+ * For host/PM2 apps: use HOST_* prefixed values (e.g., mongodb://localhost:27018)
131
+ *
132
+ * Example .env.genbox:
133
+ * HOST_MONGODB_URI=mongodb://localhost:27018/myapp
134
+ * DOCKER_MONGODB_URI=mongodb://mongodb:27017/myapp
135
+ * MONGODB_URI=${MONGODB_URI} # Placeholder that gets expanded
136
+ */
137
+ function buildInfraUrlMap(envVarsFromFile, runner) {
138
+ const urlMap = {};
139
+ const useDocker = runner === 'docker';
140
+ const prefix = useDocker ? 'DOCKER_' : 'HOST_';
141
+ // Find all infrastructure variables that have HOST_ or DOCKER_ prefixes
142
+ const infraVarNames = new Set();
143
+ for (const key of Object.keys(envVarsFromFile)) {
144
+ const match = key.match(/^(HOST|DOCKER)_(.+)$/);
145
+ if (match) {
146
+ const varName = match[2];
147
+ // Check if it's an infrastructure variable
148
+ if (INFRA_VAR_PATTERNS.some(pattern => pattern.test(varName))) {
149
+ infraVarNames.add(varName);
150
+ }
151
+ }
152
+ }
153
+ // Build mapping: VARNAME → value from appropriate prefix
154
+ for (const varName of infraVarNames) {
155
+ const prefixedKey = `${prefix}${varName}`;
156
+ const fallbackKey = useDocker ? `HOST_${varName}` : `DOCKER_${varName}`;
157
+ // Use prefixed value if available, otherwise fall back
158
+ const value = envVarsFromFile[prefixedKey] || envVarsFromFile[fallbackKey];
159
+ if (value) {
160
+ urlMap[varName] = value;
161
+ }
162
+ }
163
+ return urlMap;
164
+ }
109
165
  /**
110
166
  * Parse env vars from a section content string
111
167
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.91",
3
+ "version": "1.0.93",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {