genbox 1.0.40 → 1.0.42

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.
@@ -771,6 +771,8 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
771
771
  framework: a.framework,
772
772
  runner: a.runner,
773
773
  docker: a.docker,
774
+ healthcheck: a.healthcheck,
775
+ dependsOn: a.dependsOn,
774
776
  commands: a.commands,
775
777
  })),
776
778
  infrastructure: resolved.infrastructure.map(i => ({
@@ -583,8 +583,15 @@ exports.initCommand = new commander_1.Command('init')
583
583
  console.log(` ... and ${scan.apps.length - 5} more`);
584
584
  }
585
585
  }
586
- if (scan.compose) {
587
- console.log(` ${chalk_1.default.dim('Docker:')} ${scan.compose.applications.length} services`);
586
+ // Show Docker app count (apps with runner: 'docker')
587
+ const dockerApps = scan.apps.filter(a => a.runner === 'docker');
588
+ if (dockerApps.length > 0) {
589
+ console.log(` ${chalk_1.default.dim('Docker:')} ${dockerApps.length} app(s) with docker runner`);
590
+ }
591
+ // Show infrastructure services from docker-compose (databases, caches, etc.)
592
+ if (scan.compose && scan.compose.databases.length + scan.compose.caches.length + scan.compose.queues.length > 0) {
593
+ const infraCount = scan.compose.databases.length + scan.compose.caches.length + scan.compose.queues.length;
594
+ console.log(` ${chalk_1.default.dim('Infra:')} ${infraCount} infrastructure service(s)`);
588
595
  }
589
596
  if (scan.git) {
590
597
  console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
@@ -1886,24 +1893,28 @@ function convertDetectedToScan(detected) {
1886
1893
  lockfile: r.lockfile,
1887
1894
  }));
1888
1895
  // Convert infrastructure to compose analysis
1896
+ // Note: docker_services is deprecated - apps with runner: 'docker' are now tracked per-app
1889
1897
  let compose = null;
1890
1898
  const hasInfra = detected.infrastructure && detected.infrastructure.length > 0;
1891
- const hasDockerServices = detected.docker_services && detected.docker_services.length > 0;
1892
- if (hasInfra || hasDockerServices) {
1899
+ // Get docker apps from apps with runner: 'docker' (new approach)
1900
+ const dockerApps = Object.entries(detected.apps || {})
1901
+ .filter(([, app]) => app.runner === 'docker')
1902
+ .map(([name, app]) => ({
1903
+ name,
1904
+ image: app.docker?.service || name,
1905
+ build: app.docker?.build_context ? {
1906
+ context: app.docker.build_context,
1907
+ dockerfile: app.docker.dockerfile,
1908
+ } : undefined,
1909
+ ports: app.port ? [{ host: app.port, container: app.port }] : [],
1910
+ environment: {},
1911
+ dependsOn: app.depends_on || [],
1912
+ volumes: [],
1913
+ }));
1914
+ if (hasInfra || dockerApps.length > 0) {
1893
1915
  compose = {
1894
1916
  files: ['docker-compose.yml'],
1895
- applications: (detected.docker_services || []).map(svc => ({
1896
- name: svc.name,
1897
- image: svc.image,
1898
- build: svc.build_context ? {
1899
- context: svc.build_context,
1900
- dockerfile: svc.dockerfile,
1901
- } : undefined,
1902
- ports: svc.port ? [{ host: svc.port, container: svc.port }] : [],
1903
- environment: {},
1904
- dependsOn: svc.depends_on || [],
1905
- volumes: [],
1906
- })),
1917
+ applications: dockerApps,
1907
1918
  databases: (detected.infrastructure || [])
1908
1919
  .filter(i => i.type === 'database')
1909
1920
  .map(i => ({
@@ -330,15 +330,105 @@ async function interactiveEditMode(detected) {
330
330
  choices: appChoices,
331
331
  });
332
332
  // Edit each selected app
333
+ // Pass the scanned root dir for docker-compose lookup
334
+ const rootDir = detected._meta.scanned_root;
333
335
  for (const appName of appsToEdit) {
334
- result.apps[appName] = await editAppConfig(appName, result.apps[appName]);
336
+ result.apps[appName] = await editAppConfig(appName, result.apps[appName], rootDir);
335
337
  }
336
338
  return result;
337
339
  }
340
+ /**
341
+ * Look up service info from docker-compose.yml
342
+ */
343
+ function getDockerComposeServiceInfo(rootDir, serviceName) {
344
+ const composeFiles = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'];
345
+ for (const filename of composeFiles) {
346
+ const composePath = path.join(rootDir, filename);
347
+ if (fs.existsSync(composePath)) {
348
+ try {
349
+ const content = fs.readFileSync(composePath, 'utf8');
350
+ const compose = yaml.load(content);
351
+ if (compose?.services?.[serviceName]) {
352
+ const service = compose.services[serviceName];
353
+ const info = {};
354
+ // Parse port
355
+ if (service.ports && service.ports.length > 0) {
356
+ const portDef = service.ports[0];
357
+ if (typeof portDef === 'string') {
358
+ const parts = portDef.split(':');
359
+ info.port = parseInt(parts[0], 10);
360
+ }
361
+ else if (typeof portDef === 'number') {
362
+ info.port = portDef;
363
+ }
364
+ else if (portDef.published) {
365
+ info.port = portDef.published;
366
+ }
367
+ }
368
+ // Parse build context and dockerfile
369
+ if (service.build) {
370
+ if (typeof service.build === 'string') {
371
+ info.buildContext = service.build;
372
+ }
373
+ else {
374
+ info.buildContext = service.build.context;
375
+ info.dockerfile = service.build.dockerfile;
376
+ }
377
+ }
378
+ // Parse healthcheck
379
+ if (service.healthcheck?.test) {
380
+ const test = service.healthcheck.test;
381
+ if (Array.isArray(test)) {
382
+ // ["CMD", "curl", "-f", "http://localhost:3000/health"]
383
+ const cmdIndex = test.indexOf('CMD') + 1 || test.indexOf('CMD-SHELL') + 1;
384
+ if (cmdIndex > 0) {
385
+ const healthCmd = test.slice(cmdIndex).join(' ');
386
+ // Extract health endpoint from curl command
387
+ const urlMatch = healthCmd.match(/https?:\/\/[^/]+(\S+)/);
388
+ if (urlMatch) {
389
+ info.healthcheck = urlMatch[1];
390
+ }
391
+ }
392
+ }
393
+ else if (typeof test === 'string') {
394
+ const urlMatch = test.match(/https?:\/\/[^/]+(\S+)/);
395
+ if (urlMatch) {
396
+ info.healthcheck = urlMatch[1];
397
+ }
398
+ }
399
+ }
400
+ // Parse depends_on
401
+ if (service.depends_on) {
402
+ if (Array.isArray(service.depends_on)) {
403
+ info.dependsOn = service.depends_on;
404
+ }
405
+ else if (typeof service.depends_on === 'object') {
406
+ info.dependsOn = Object.keys(service.depends_on);
407
+ }
408
+ }
409
+ // Parse environment variables (names only)
410
+ if (service.environment) {
411
+ if (Array.isArray(service.environment)) {
412
+ info.envVars = service.environment.map((e) => e.split('=')[0]);
413
+ }
414
+ else if (typeof service.environment === 'object') {
415
+ info.envVars = Object.keys(service.environment);
416
+ }
417
+ }
418
+ return info;
419
+ }
420
+ }
421
+ catch {
422
+ // Ignore parse errors
423
+ }
424
+ }
425
+ }
426
+ return undefined;
427
+ }
338
428
  /**
339
429
  * Edit a single app's configuration
340
430
  */
341
- async function editAppConfig(name, app) {
431
+ async function editAppConfig(name, app, rootDir) {
342
432
  console.log('');
343
433
  console.log(chalk_1.default.blue(`=== Editing: ${name} ===`));
344
434
  // Show current values
@@ -383,38 +473,78 @@ async function editAppConfig(name, app) {
383
473
  result.runner = newRunner;
384
474
  result.runner_reason = 'manually set';
385
475
  }
386
- // If runner is docker, ask for docker config
476
+ // If runner is docker, ask for docker config and auto-detect from docker-compose
387
477
  if (newRunner === 'docker') {
478
+ // First ask for service name to look up in docker-compose
388
479
  const dockerService = await prompts.input({
389
480
  message: 'Docker service name:',
390
481
  default: app.docker?.service || name,
391
482
  });
483
+ // Look up service info from docker-compose.yml
484
+ const composeInfo = getDockerComposeServiceInfo(rootDir, dockerService);
485
+ if (composeInfo) {
486
+ console.log(chalk_1.default.blue('\n Detected from docker-compose.yml:'));
487
+ if (composeInfo.buildContext)
488
+ console.log(chalk_1.default.dim(` build context: ${composeInfo.buildContext}`));
489
+ if (composeInfo.dockerfile)
490
+ console.log(chalk_1.default.dim(` dockerfile: ${composeInfo.dockerfile}`));
491
+ if (composeInfo.port)
492
+ console.log(chalk_1.default.dim(` port: ${composeInfo.port}`));
493
+ if (composeInfo.healthcheck)
494
+ console.log(chalk_1.default.dim(` healthcheck: ${composeInfo.healthcheck}`));
495
+ if (composeInfo.dependsOn?.length)
496
+ console.log(chalk_1.default.dim(` depends_on: ${composeInfo.dependsOn.join(', ')}`));
497
+ if (composeInfo.envVars?.length)
498
+ console.log(chalk_1.default.dim(` env vars: ${composeInfo.envVars.slice(0, 5).join(', ')}${composeInfo.envVars.length > 5 ? ` +${composeInfo.envVars.length - 5} more` : ''}`));
499
+ console.log('');
500
+ }
501
+ // Use detected values as defaults
502
+ const defaultContext = composeInfo?.buildContext || app.docker?.build_context || app.path || '.';
392
503
  const dockerContext = await prompts.input({
393
504
  message: 'Docker build context:',
394
- default: app.docker?.build_context || app.path || '.',
505
+ default: defaultContext,
395
506
  });
507
+ const defaultDockerfile = composeInfo?.dockerfile || app.docker?.dockerfile;
508
+ const dockerfile = defaultDockerfile ? await prompts.input({
509
+ message: 'Dockerfile:',
510
+ default: defaultDockerfile,
511
+ }) : undefined;
396
512
  result.docker = {
397
513
  service: dockerService,
398
514
  build_context: dockerContext,
399
- dockerfile: app.docker?.dockerfile,
515
+ dockerfile: dockerfile || app.docker?.dockerfile,
400
516
  };
517
+ // Auto-set values from docker-compose
518
+ if (composeInfo?.port) {
519
+ result.port = composeInfo.port;
520
+ result.port_source = 'docker-compose.yml';
521
+ }
522
+ if (composeInfo?.healthcheck) {
523
+ result.healthcheck = composeInfo.healthcheck;
524
+ }
525
+ if (composeInfo?.dependsOn?.length) {
526
+ result.depends_on = composeInfo.dependsOn;
527
+ }
401
528
  }
402
- // Edit port (only if not a library)
529
+ // Edit port (only if not a library and not already set by docker-compose)
403
530
  if (newRunner !== 'none' && result.type !== 'library') {
531
+ // For docker runner, only ask if port wasn't auto-detected
532
+ const currentPort = result.port;
533
+ const portDefault = currentPort ? String(currentPort) : '';
404
534
  const portInput = await prompts.input({
405
- message: 'Port (leave empty to skip):',
406
- default: app.port ? String(app.port) : '',
535
+ message: currentPort ? `Port (detected: ${currentPort}, press Enter to keep):` : 'Port (leave empty to skip):',
536
+ default: portDefault,
407
537
  });
408
538
  if (portInput) {
409
539
  const portNum = parseInt(portInput, 10);
410
540
  if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
411
- result.port = portNum;
412
- if (portNum !== app.port) {
541
+ if (portNum !== currentPort) {
542
+ result.port = portNum;
413
543
  result.port_source = 'manually set';
414
544
  }
415
545
  }
416
546
  }
417
- else {
547
+ else if (!currentPort) {
418
548
  result.port = undefined;
419
549
  result.port_source = undefined;
420
550
  }
@@ -242,6 +242,8 @@ class ProfileResolver {
242
242
  const v4Config = appConfig;
243
243
  const runner = v4Config.runner;
244
244
  const docker = v4Config.docker;
245
+ const healthcheck = v4Config.healthcheck;
246
+ const dependsOn = v4Config.depends_on;
245
247
  return {
246
248
  name: appName,
247
249
  path: appConfig.path,
@@ -253,6 +255,8 @@ class ProfileResolver {
253
255
  env: appConfig.env,
254
256
  runner,
255
257
  docker,
258
+ healthcheck,
259
+ dependsOn,
256
260
  dependencies,
257
261
  };
258
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {