genbox 1.0.37 → 1.0.39

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.
@@ -1736,6 +1736,13 @@ function convertV2ToV4(v2Config, scan) {
1736
1736
  if (service.env?.length) {
1737
1737
  appConfig.env = service.env;
1738
1738
  }
1739
+ // Set runner and docker config
1740
+ if (service.runner) {
1741
+ appConfig.runner = service.runner;
1742
+ }
1743
+ if (service.docker) {
1744
+ appConfig.docker = service.docker;
1745
+ }
1739
1746
  apps[name] = appConfig;
1740
1747
  }
1741
1748
  // Convert infrastructure to provides (v4 format)
@@ -1838,7 +1845,15 @@ function convertDetectedToScan(detected) {
1838
1845
  'gateway': 'gateway',
1839
1846
  'library': 'library',
1840
1847
  };
1841
- apps.push({
1848
+ // Map runner types
1849
+ const runnerMap = {
1850
+ 'pm2': 'pm2',
1851
+ 'docker': 'docker',
1852
+ 'node': 'node',
1853
+ 'bun': 'bun',
1854
+ 'none': 'none',
1855
+ };
1856
+ const discoveredApp = {
1842
1857
  name,
1843
1858
  path: app.path,
1844
1859
  type: typeMap[app.type || 'library'] || 'library',
@@ -1850,7 +1865,17 @@ function convertDetectedToScan(detected) {
1850
1865
  build: app.commands.build || '',
1851
1866
  start: app.commands.start || '',
1852
1867
  } : {},
1853
- });
1868
+ runner: app.runner ? runnerMap[app.runner] : undefined,
1869
+ };
1870
+ // Add docker config if present
1871
+ if (app.docker) {
1872
+ discoveredApp.docker = {
1873
+ service: app.docker.service,
1874
+ build_context: app.docker.build_context,
1875
+ dockerfile: app.docker.dockerfile,
1876
+ };
1877
+ }
1878
+ apps.push(discoveredApp);
1854
1879
  }
1855
1880
  // Convert runtimes
1856
1881
  const runtimes = detected.runtimes.map(r => ({
@@ -42,7 +42,6 @@ const prompts = __importStar(require("@inquirer/prompts"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
43
  const yaml = __importStar(require("js-yaml"));
44
44
  const fs = __importStar(require("fs"));
45
- const path = __importStar(require("path"));
46
45
  const config_loader_1 = require("../config-loader");
47
46
  const schema_v4_1 = require("../schema-v4");
48
47
  exports.profilesCommand = new commander_1.Command('profiles')
@@ -278,29 +277,15 @@ exports.profilesCommand
278
277
  source: dbMode.includes('staging') ? 'staging' : dbMode.includes('production') ? 'production' : undefined,
279
278
  } : undefined,
280
279
  };
281
- // Where to save?
282
- const saveLocation = await prompts.select({
283
- message: 'Where to save this profile?',
284
- choices: [
285
- { name: 'genbox.yaml (shared with team)', value: 'project' },
286
- { name: '~/.genbox/profiles.yaml (personal)', value: 'user' },
287
- ],
288
- });
289
- if (saveLocation === 'user') {
290
- configLoader.saveUserProfile(profileName, profile);
291
- console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' saved to ~/.genbox/profiles.yaml`));
292
- }
293
- else {
294
- // Add to project config
295
- if (!config.profiles) {
296
- config.profiles = {};
297
- }
298
- config.profiles[profileName] = profile;
299
- const configPath = configLoader.getConfigPath();
300
- const yamlContent = yaml.dump(config, { lineWidth: 120, noRefs: true });
301
- fs.writeFileSync(configPath, yamlContent);
302
- console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' added to genbox.yaml`));
280
+ // Save to project config (user/global profiles removed - they reference project-specific apps)
281
+ if (!config.profiles) {
282
+ config.profiles = {};
303
283
  }
284
+ config.profiles[profileName] = profile;
285
+ const configPath = configLoader.getConfigPath();
286
+ const yamlContent = yaml.dump(config, { lineWidth: 120, noRefs: true });
287
+ fs.writeFileSync(configPath, yamlContent);
288
+ console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' added to genbox.yaml`));
304
289
  console.log(chalk_1.default.dim(`Use: genbox create <name> --profile ${profileName}`));
305
290
  }
306
291
  catch (error) {
@@ -315,8 +300,7 @@ exports.profilesCommand
315
300
  exports.profilesCommand
316
301
  .command('delete <name>')
317
302
  .description('Delete a profile')
318
- .option('--user', 'Delete from user profiles')
319
- .action(async (name, options) => {
303
+ .action(async (name) => {
320
304
  try {
321
305
  const configLoader = new config_loader_1.ConfigLoader();
322
306
  const loadResult = await configLoader.load();
@@ -331,12 +315,9 @@ exports.profilesCommand
331
315
  return;
332
316
  }
333
317
  const config = loadResult.config;
334
- // Check if profile exists
335
- const inProject = config.profiles?.[name];
336
- const userProfiles = configLoader.loadUserProfiles();
337
- const inUser = userProfiles?.profiles?.[name];
338
- if (!inProject && !inUser) {
339
- console.log(chalk_1.default.red(`Profile '${name}' not found`));
318
+ // Check if profile exists in project
319
+ if (!config.profiles?.[name]) {
320
+ console.log(chalk_1.default.red(`Profile '${name}' not found in genbox.yaml`));
340
321
  return;
341
322
  }
342
323
  // Confirm deletion
@@ -348,20 +329,11 @@ exports.profilesCommand
348
329
  console.log(chalk_1.default.dim('Cancelled.'));
349
330
  return;
350
331
  }
351
- if (options.user && inUser) {
352
- // Delete from user profiles
353
- delete userProfiles.profiles[name];
354
- const userProfilesPath = path.join(require('os').homedir(), '.genbox', 'profiles.yaml');
355
- fs.writeFileSync(userProfilesPath, yaml.dump(userProfiles));
356
- console.log(chalk_1.default.green(`✔ Deleted '${name}' from user profiles`));
357
- }
358
- else if (inProject) {
359
- // Delete from project config
360
- delete config.profiles[name];
361
- const configPath = configLoader.getConfigPath();
362
- fs.writeFileSync(configPath, yaml.dump(config, { lineWidth: 120, noRefs: true }));
363
- console.log(chalk_1.default.green(`✔ Deleted '${name}' from genbox.yaml`));
364
- }
332
+ // Delete from project config
333
+ delete config.profiles[name];
334
+ const configPath = configLoader.getConfigPath();
335
+ fs.writeFileSync(configPath, yaml.dump(config, { lineWidth: 120, noRefs: true }));
336
+ console.log(chalk_1.default.green(`✔ Deleted '${name}' from genbox.yaml`));
365
337
  }
366
338
  catch (error) {
367
339
  if (error.name === 'ExitPromptError') {
@@ -112,10 +112,12 @@ exports.scanCommand = new commander_1.Command('scan')
112
112
  .option('--no-infra', 'Skip infrastructure detection (docker-compose)')
113
113
  .option('--no-scripts', 'Skip script detection')
114
114
  .option('-i, --interactive', 'Interactive mode - select apps before writing')
115
+ .option('--edit', 'Edit mode - review and modify detected values for each app')
115
116
  .option('-e, --exclude <patterns>', 'Comma-separated patterns to exclude', '')
116
117
  .action(async (options) => {
117
118
  const cwd = process.cwd();
118
- const isInteractive = options.interactive && !options.stdout && process.stdin.isTTY;
119
+ const isInteractive = (options.interactive || options.edit) && !options.stdout && process.stdin.isTTY;
120
+ const isEditMode = options.edit && !options.stdout && process.stdin.isTTY;
119
121
  console.log(chalk_1.default.cyan('\n🔍 Scanning project...\n'));
120
122
  try {
121
123
  // Run the scanner
@@ -131,6 +133,10 @@ exports.scanCommand = new commander_1.Command('scan')
131
133
  if (isInteractive) {
132
134
  detected = await interactiveSelection(detected);
133
135
  }
136
+ // Edit mode: let user modify detected values for each app
137
+ if (isEditMode) {
138
+ detected = await interactiveEditMode(detected);
139
+ }
134
140
  // Scan env files for service URLs (only for selected frontend apps)
135
141
  const frontendApps = Object.entries(detected.apps)
136
142
  .filter(([, app]) => app.type === 'frontend')
@@ -285,6 +291,176 @@ async function interactiveSelection(detected) {
285
291
  }
286
292
  return result;
287
293
  }
294
+ /**
295
+ * Interactive edit mode - allows modifying detected values for each app
296
+ */
297
+ async function interactiveEditMode(detected) {
298
+ const result = { ...detected, apps: { ...detected.apps } };
299
+ const appEntries = Object.entries(result.apps);
300
+ if (appEntries.length === 0) {
301
+ console.log(chalk_1.default.dim('No apps to edit.'));
302
+ return result;
303
+ }
304
+ console.log('');
305
+ console.log(chalk_1.default.blue('=== Edit Detected Values ==='));
306
+ console.log(chalk_1.default.dim('Review and modify detected values for each app.\n'));
307
+ // Show summary of all apps first
308
+ console.log(chalk_1.default.bold('Detected apps:'));
309
+ for (const [name, app] of appEntries) {
310
+ const runner = app.runner || 'pm2';
311
+ const port = app.port ? `:${app.port}` : '';
312
+ console.log(` ${chalk_1.default.cyan(name)} - ${app.type || 'unknown'} (${runner})${port}`);
313
+ }
314
+ console.log('');
315
+ // Ask if user wants to edit any apps
316
+ const editApps = await prompts.confirm({
317
+ message: 'Do you want to edit any app configurations?',
318
+ default: false,
319
+ });
320
+ if (!editApps) {
321
+ return result;
322
+ }
323
+ // Let user select which apps to edit
324
+ const appChoices = appEntries.map(([name, app]) => ({
325
+ name: `${name} (${app.type || 'unknown'}, ${app.runner || 'pm2'})`,
326
+ value: name,
327
+ }));
328
+ const appsToEdit = await prompts.checkbox({
329
+ message: 'Select apps to edit:',
330
+ choices: appChoices,
331
+ });
332
+ // Edit each selected app
333
+ for (const appName of appsToEdit) {
334
+ result.apps[appName] = await editAppConfig(appName, result.apps[appName]);
335
+ }
336
+ return result;
337
+ }
338
+ /**
339
+ * Edit a single app's configuration
340
+ */
341
+ async function editAppConfig(name, app) {
342
+ console.log('');
343
+ console.log(chalk_1.default.blue(`=== Editing: ${name} ===`));
344
+ // Show current values
345
+ console.log(chalk_1.default.dim('Current values:'));
346
+ console.log(` Type: ${chalk_1.default.cyan(app.type || 'unknown')} ${chalk_1.default.dim(`(${app.type_reason || 'detected'})`)}`);
347
+ console.log(` Runner: ${chalk_1.default.cyan(app.runner || 'pm2')} ${chalk_1.default.dim(`(${app.runner_reason || 'default'})`)}`);
348
+ console.log(` Port: ${app.port ? chalk_1.default.cyan(String(app.port)) : chalk_1.default.dim('not set')} ${app.port_source ? chalk_1.default.dim(`(${app.port_source})`) : ''}`);
349
+ console.log(` Framework: ${app.framework ? chalk_1.default.cyan(app.framework) : chalk_1.default.dim('not detected')}`);
350
+ console.log('');
351
+ const result = { ...app };
352
+ // Edit type
353
+ const typeChoices = [
354
+ { name: `frontend ${app.type === 'frontend' ? chalk_1.default.green('(current)') : ''}`, value: 'frontend' },
355
+ { name: `backend ${app.type === 'backend' ? chalk_1.default.green('(current)') : ''}`, value: 'backend' },
356
+ { name: `worker ${app.type === 'worker' ? chalk_1.default.green('(current)') : ''}`, value: 'worker' },
357
+ { name: `gateway ${app.type === 'gateway' ? chalk_1.default.green('(current)') : ''}`, value: 'gateway' },
358
+ { name: `library ${app.type === 'library' ? chalk_1.default.green('(current)') : ''}`, value: 'library' },
359
+ ];
360
+ const newType = await prompts.select({
361
+ message: 'App type:',
362
+ choices: typeChoices,
363
+ default: app.type || 'backend',
364
+ });
365
+ if (newType !== app.type) {
366
+ result.type = newType;
367
+ result.type_reason = 'manually set';
368
+ }
369
+ // Edit runner
370
+ const runnerChoices = [
371
+ { name: `pm2 - Process manager (recommended for Node.js) ${app.runner === 'pm2' ? chalk_1.default.green('(current)') : ''}`, value: 'pm2' },
372
+ { name: `docker - Docker compose service ${app.runner === 'docker' ? chalk_1.default.green('(current)') : ''}`, value: 'docker' },
373
+ { name: `bun - Bun runtime ${app.runner === 'bun' ? chalk_1.default.green('(current)') : ''}`, value: 'bun' },
374
+ { name: `node - Direct Node.js execution ${app.runner === 'node' ? chalk_1.default.green('(current)') : ''}`, value: 'node' },
375
+ { name: `none - Library/not runnable ${app.runner === 'none' ? chalk_1.default.green('(current)') : ''}`, value: 'none' },
376
+ ];
377
+ const newRunner = await prompts.select({
378
+ message: 'Runner:',
379
+ choices: runnerChoices,
380
+ default: app.runner || 'pm2',
381
+ });
382
+ if (newRunner !== app.runner) {
383
+ result.runner = newRunner;
384
+ result.runner_reason = 'manually set';
385
+ }
386
+ // If runner is docker, ask for docker config
387
+ if (newRunner === 'docker') {
388
+ const dockerService = await prompts.input({
389
+ message: 'Docker service name:',
390
+ default: app.docker?.service || name,
391
+ });
392
+ const dockerContext = await prompts.input({
393
+ message: 'Docker build context:',
394
+ default: app.docker?.build_context || app.path || '.',
395
+ });
396
+ result.docker = {
397
+ service: dockerService,
398
+ build_context: dockerContext,
399
+ dockerfile: app.docker?.dockerfile,
400
+ };
401
+ }
402
+ // Edit port (only if not a library)
403
+ if (newRunner !== 'none' && result.type !== 'library') {
404
+ const portInput = await prompts.input({
405
+ message: 'Port (leave empty to skip):',
406
+ default: app.port ? String(app.port) : '',
407
+ });
408
+ if (portInput) {
409
+ const portNum = parseInt(portInput, 10);
410
+ if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
411
+ result.port = portNum;
412
+ if (portNum !== app.port) {
413
+ result.port_source = 'manually set';
414
+ }
415
+ }
416
+ }
417
+ else {
418
+ result.port = undefined;
419
+ result.port_source = undefined;
420
+ }
421
+ }
422
+ // Edit framework
423
+ const frameworkChoices = [
424
+ { name: `Keep current: ${app.framework || 'none'}`, value: app.framework || '' },
425
+ { name: '---', value: '__separator__', disabled: true },
426
+ { name: 'nextjs', value: 'nextjs' },
427
+ { name: 'react', value: 'react' },
428
+ { name: 'vue', value: 'vue' },
429
+ { name: 'nuxt', value: 'nuxt' },
430
+ { name: 'vite', value: 'vite' },
431
+ { name: 'astro', value: 'astro' },
432
+ { name: 'nestjs', value: 'nestjs' },
433
+ { name: 'express', value: 'express' },
434
+ { name: 'fastify', value: 'fastify' },
435
+ { name: 'hono', value: 'hono' },
436
+ { name: 'Other (type manually)', value: '__other__' },
437
+ { name: 'None / Clear', value: '__none__' },
438
+ ];
439
+ const frameworkChoice = await prompts.select({
440
+ message: 'Framework:',
441
+ choices: frameworkChoices,
442
+ default: app.framework || '',
443
+ });
444
+ if (frameworkChoice === '__other__') {
445
+ const customFramework = await prompts.input({
446
+ message: 'Enter framework name:',
447
+ });
448
+ if (customFramework) {
449
+ result.framework = customFramework;
450
+ result.framework_source = 'manually set';
451
+ }
452
+ }
453
+ else if (frameworkChoice === '__none__') {
454
+ result.framework = undefined;
455
+ result.framework_source = undefined;
456
+ }
457
+ else if (frameworkChoice && frameworkChoice !== app.framework) {
458
+ result.framework = frameworkChoice;
459
+ result.framework_source = 'manually set';
460
+ }
461
+ console.log(chalk_1.default.green(`✓ Updated ${name}`));
462
+ return result;
463
+ }
288
464
  /**
289
465
  * Interactive service URL selection
290
466
  */
@@ -459,10 +635,14 @@ function convertScanToDetected(scan, root) {
459
635
  const appDir = path.join(root, app.path);
460
636
  appGit = detectGitForDirectory(appDir);
461
637
  }
638
+ // Detect runner based on app type and context
639
+ const { runner, runner_reason } = detectRunner(app, scan);
462
640
  detected.apps[app.name] = {
463
641
  path: app.path,
464
642
  type: mappedType,
465
643
  type_reason: inferTypeReason(app),
644
+ runner,
645
+ runner_reason,
466
646
  port: app.port,
467
647
  port_source: app.port ? inferPortSource(app) : undefined,
468
648
  framework: app.framework, // Framework is a string type
@@ -506,17 +686,31 @@ function convertScanToDetected(scan, root) {
506
686
  source: 'docker-compose.yml',
507
687
  });
508
688
  }
509
- // Save Docker application services (services with build context)
689
+ // Add Docker application services as apps with runner: 'docker'
690
+ // (Only add if not already detected as a PM2 app)
510
691
  if (scan.compose.applications && scan.compose.applications.length > 0) {
511
- detected.docker_services = scan.compose.applications.map(app => ({
512
- name: app.name,
513
- build_context: app.build?.context,
514
- dockerfile: app.build?.dockerfile,
515
- image: app.image,
516
- port: app.ports?.[0]?.host,
517
- depends_on: app.dependsOn?.length ? app.dependsOn : undefined,
518
- source: 'docker-compose.yml',
519
- }));
692
+ for (const dockerApp of scan.compose.applications) {
693
+ // Skip if already exists as a PM2 app
694
+ if (detected.apps[dockerApp.name])
695
+ continue;
696
+ // Infer type by checking file structure in build context
697
+ const { type: mappedType, reason: typeReason } = inferDockerAppType(dockerApp.name, dockerApp.build?.context, scan.root);
698
+ detected.apps[dockerApp.name] = {
699
+ path: dockerApp.build?.context || '.',
700
+ type: mappedType,
701
+ type_reason: typeReason,
702
+ runner: 'docker',
703
+ runner_reason: 'defined in docker-compose.yml',
704
+ docker: {
705
+ service: dockerApp.name,
706
+ build_context: dockerApp.build?.context,
707
+ dockerfile: dockerApp.build?.dockerfile,
708
+ image: dockerApp.image,
709
+ },
710
+ port: dockerApp.ports?.[0]?.host,
711
+ port_source: 'docker-compose.yml ports',
712
+ };
713
+ }
520
714
  }
521
715
  }
522
716
  // Git info
@@ -567,6 +761,38 @@ function mapAppType(type) {
567
761
  return undefined; // Unknown type
568
762
  }
569
763
  }
764
+ /**
765
+ * Detect the appropriate runner for an app
766
+ * Priority:
767
+ * 1. Library apps → none
768
+ * 2. Bun lockfile present → bun
769
+ * 3. Has start script or is a typical app → pm2
770
+ * 4. Simple CLI/script → node
771
+ */
772
+ function detectRunner(app, scan) {
773
+ // Libraries don't run
774
+ if (app.type === 'library') {
775
+ return { runner: 'none', runner_reason: 'library apps are not runnable' };
776
+ }
777
+ // Check for Bun
778
+ const hasBunLockfile = scan.runtimes?.some(r => r.lockfile === 'bun.lockb');
779
+ if (hasBunLockfile) {
780
+ return { runner: 'bun', runner_reason: 'bun.lockb detected' };
781
+ }
782
+ // Check for typical app patterns that benefit from PM2
783
+ const hasStartScript = app.scripts?.start || app.scripts?.dev;
784
+ const isTypicalApp = ['frontend', 'backend', 'api', 'worker', 'gateway'].includes(app.type || '');
785
+ if (hasStartScript || isTypicalApp) {
786
+ return { runner: 'pm2', runner_reason: 'typical Node.js app with start script' };
787
+ }
788
+ // CLI tools or simple scripts can use direct node
789
+ const name = (app.name || '').toLowerCase();
790
+ if (name.includes('cli') || name.includes('tool') || name.includes('script')) {
791
+ return { runner: 'node', runner_reason: 'CLI/tool detected' };
792
+ }
793
+ // Default to pm2 for most apps
794
+ return { runner: 'pm2', runner_reason: 'default for Node.js apps' };
795
+ }
570
796
  function inferTypeReason(app) {
571
797
  if (!app.type)
572
798
  return 'unknown';
@@ -582,6 +808,75 @@ function inferTypeReason(app) {
582
808
  }
583
809
  return 'dependency analysis';
584
810
  }
811
+ /**
812
+ * Infer app type by examining file structure in the build context directory
813
+ * Falls back to name-based detection only if file analysis doesn't find anything
814
+ */
815
+ function inferDockerAppType(name, buildContext, rootDir) {
816
+ // If we have a build context, check file structure first
817
+ if (buildContext && rootDir) {
818
+ const contextPath = path.resolve(rootDir, buildContext);
819
+ // Check for frontend indicators
820
+ const frontendConfigs = ['vite.config.ts', 'vite.config.js', 'next.config.js', 'next.config.mjs', 'nuxt.config.ts', 'astro.config.mjs'];
821
+ for (const config of frontendConfigs) {
822
+ if (fs.existsSync(path.join(contextPath, config))) {
823
+ return { type: 'frontend', reason: `config file found: ${config}` };
824
+ }
825
+ }
826
+ // Check for backend indicators
827
+ const backendConfigs = ['nest-cli.json', 'tsconfig.build.json'];
828
+ for (const config of backendConfigs) {
829
+ if (fs.existsSync(path.join(contextPath, config))) {
830
+ return { type: 'backend', reason: `config file found: ${config}` };
831
+ }
832
+ }
833
+ // Check package.json dependencies
834
+ const packageJsonPath = path.join(contextPath, 'package.json');
835
+ if (fs.existsSync(packageJsonPath)) {
836
+ try {
837
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
838
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
839
+ // Frontend dependencies
840
+ const frontendDeps = ['react', 'react-dom', 'vue', '@angular/core', 'svelte', 'next', 'nuxt', 'vite', '@remix-run/react', 'astro'];
841
+ for (const dep of frontendDeps) {
842
+ if (allDeps[dep]) {
843
+ return { type: 'frontend', reason: `package.json dependency: ${dep}` };
844
+ }
845
+ }
846
+ // Backend dependencies
847
+ const backendDeps = ['@nestjs/core', 'express', 'fastify', 'koa', 'hapi', '@hono/node-server'];
848
+ for (const dep of backendDeps) {
849
+ if (allDeps[dep]) {
850
+ return { type: 'backend', reason: `package.json dependency: ${dep}` };
851
+ }
852
+ }
853
+ // Worker dependencies
854
+ const workerDeps = ['bull', 'bullmq', 'agenda', 'bee-queue'];
855
+ for (const dep of workerDeps) {
856
+ if (allDeps[dep]) {
857
+ return { type: 'worker', reason: `package.json dependency: ${dep}` };
858
+ }
859
+ }
860
+ }
861
+ catch {
862
+ // Ignore parse errors
863
+ }
864
+ }
865
+ }
866
+ // Fall back to name-based detection
867
+ const lowerName = name.toLowerCase();
868
+ if (lowerName.includes('web') || lowerName.includes('frontend') || lowerName.includes('ui') || lowerName.includes('client')) {
869
+ return { type: 'frontend', reason: `naming convention ('${name}' contains frontend keyword)` };
870
+ }
871
+ if (lowerName.includes('api') || lowerName.includes('backend') || lowerName.includes('server') || lowerName.includes('gateway')) {
872
+ return { type: 'backend', reason: `naming convention ('${name}' contains backend keyword)` };
873
+ }
874
+ if (lowerName.includes('worker') || lowerName.includes('queue') || lowerName.includes('job')) {
875
+ return { type: 'worker', reason: `naming convention ('${name}' contains worker keyword)` };
876
+ }
877
+ // Default to backend for Docker services
878
+ return { type: 'backend', reason: 'default for Docker services' };
879
+ }
585
880
  function inferPortSource(app) {
586
881
  if (app.scripts?.dev?.includes('--port')) {
587
882
  return 'package.json scripts.dev (--port flag)';
@@ -467,7 +467,8 @@ class ConfigLoader {
467
467
  */
468
468
  listProfiles(config) {
469
469
  const profiles = [];
470
- // Project profiles
470
+ // Project profiles only (user/global profiles removed - they don't make sense
471
+ // because profiles reference project-specific app names)
471
472
  for (const [name, profile] of Object.entries(config.profiles || {})) {
472
473
  const resolved = this.getProfile(config, name);
473
474
  profiles.push({
@@ -479,24 +480,6 @@ class ConfigLoader {
479
480
  connection: resolved ? getProfileConnection(resolved) : undefined,
480
481
  });
481
482
  }
482
- // User profiles
483
- const userProfiles = this.loadUserProfiles();
484
- if (userProfiles) {
485
- for (const [name, profile] of Object.entries(userProfiles.profiles || {})) {
486
- // Skip if already defined in project
487
- if (config.profiles?.[name])
488
- continue;
489
- const resolved = this.getProfile(config, name);
490
- profiles.push({
491
- name,
492
- description: profile.description,
493
- source: 'user',
494
- apps: resolved?.apps || [],
495
- size: resolved?.size,
496
- connection: resolved ? getProfileConnection(resolved) : undefined,
497
- });
498
- }
499
- }
500
483
  return profiles;
501
484
  }
502
485
  /**
package/dist/migration.js CHANGED
@@ -244,7 +244,8 @@ function migrateApp(appName, appConfig, v3Config, changes, warnings) {
244
244
  if (appConfig.env)
245
245
  v4App.env = appConfig.env;
246
246
  if (appConfig.compose_file) {
247
- v4App.compose = { file: appConfig.compose_file };
247
+ v4App.runner = 'docker';
248
+ v4App.docker = { file: appConfig.compose_file };
248
249
  }
249
250
  // Warn about missing explicit type
250
251
  if (!appConfig.type) {
@@ -77,6 +77,18 @@ class ConfigGenerator {
77
77
  output: framework?.outputDir,
78
78
  };
79
79
  }
80
+ // Set runner from app if available
81
+ if (app.runner) {
82
+ serviceConfig.runner = app.runner;
83
+ }
84
+ // Set docker config if present
85
+ if (app.docker) {
86
+ serviceConfig.docker = {
87
+ service: app.docker.service,
88
+ build_context: app.docker.build_context,
89
+ dockerfile: app.docker.dockerfile,
90
+ };
91
+ }
80
92
  services[app.name] = serviceConfig;
81
93
  }
82
94
  }
@@ -120,8 +132,42 @@ class ConfigGenerator {
120
132
  output: framework?.outputDir,
121
133
  };
122
134
  }
135
+ // Set runner from app if available
136
+ if (app.runner) {
137
+ serviceConfig.runner = app.runner;
138
+ }
139
+ // Set docker config if present
140
+ if (app.docker) {
141
+ serviceConfig.docker = {
142
+ service: app.docker.service,
143
+ build_context: app.docker.build_context,
144
+ dockerfile: app.docker.dockerfile,
145
+ };
146
+ }
123
147
  services[app.name] = serviceConfig;
124
148
  }
149
+ // Also include Docker services from compose (if not already added as apps)
150
+ // This is a fallback for services not detected during scan
151
+ if (scan.compose?.applications) {
152
+ for (const dockerApp of scan.compose.applications) {
153
+ // Skip if already exists as an app
154
+ if (services[dockerApp.name])
155
+ continue;
156
+ const port = dockerApp.ports[0]?.host || 3000;
157
+ services[dockerApp.name] = {
158
+ type: this.inferServiceType(dockerApp.name),
159
+ port,
160
+ runner: 'docker',
161
+ docker: {
162
+ service: dockerApp.name,
163
+ build_context: dockerApp.build?.context,
164
+ dockerfile: dockerApp.build?.dockerfile,
165
+ },
166
+ dependsOn: dockerApp.dependsOn.map(d => this.normalizeServiceName(d)),
167
+ env: Object.keys(dockerApp.environment || {}).filter(k => !k.startsWith('_')),
168
+ };
169
+ }
170
+ }
125
171
  }
126
172
  // Single app fallback
127
173
  if (Object.keys(services).length === 0 && scan.frameworks.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {