genbox 1.0.13 → 1.0.15

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.
@@ -162,6 +162,7 @@ exports.initCommand = new commander_1.Command('init')
162
162
  .option('-y, --yes', 'Use defaults without prompting')
163
163
  .option('--exclude <dirs>', 'Comma-separated directories to exclude')
164
164
  .option('--name <name>', 'Project name (for non-interactive mode)')
165
+ .option('--from-scan', 'Initialize from existing .genbox/detected.yaml (created by genbox scan)')
165
166
  .action(async (options) => {
166
167
  try {
167
168
  const configPath = path_1.default.join(process.cwd(), CONFIG_FILENAME);
@@ -192,11 +193,34 @@ exports.initCommand = new commander_1.Command('init')
192
193
  if (options.exclude) {
193
194
  exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
194
195
  }
195
- // Scan project first (skip scripts initially)
196
- const spinner = (0, ora_1.default)('Scanning project...').start();
196
+ let scan;
197
197
  const scanner = new scanner_1.ProjectScanner();
198
- let scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
199
- spinner.succeed('Project scanned');
198
+ // If --from-scan is specified, load from detected.yaml
199
+ if (options.fromScan) {
200
+ const detectedPath = path_1.default.join(process.cwd(), '.genbox', 'detected.yaml');
201
+ if (!fs_1.default.existsSync(detectedPath)) {
202
+ console.log(chalk_1.default.red('No .genbox/detected.yaml found. Run "genbox scan" first.'));
203
+ process.exit(1);
204
+ }
205
+ const spinner = (0, ora_1.default)('Loading detected configuration...').start();
206
+ try {
207
+ const content = fs_1.default.readFileSync(detectedPath, 'utf8');
208
+ const detected = yaml.load(content);
209
+ scan = convertDetectedToScan(detected);
210
+ spinner.succeed('Loaded from detected.yaml');
211
+ }
212
+ catch (err) {
213
+ spinner.fail('Failed to load detected.yaml');
214
+ console.error(chalk_1.default.red(String(err)));
215
+ process.exit(1);
216
+ }
217
+ }
218
+ else {
219
+ // Scan project first (skip scripts initially)
220
+ const spinner = (0, ora_1.default)('Scanning project...').start();
221
+ scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
222
+ spinner.succeed('Project scanned');
223
+ }
200
224
  // Display scan results
201
225
  console.log('');
202
226
  console.log(chalk_1.default.bold('Detected:'));
@@ -213,9 +237,10 @@ exports.initCommand = new commander_1.Command('init')
213
237
  console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
214
238
  }
215
239
  // For multi-repo: show apps and let user select which to include
240
+ // When using --from-scan, skip app selection - use exactly what's in detected.yaml
216
241
  const isMultiRepoStructure = scan.structure.type === 'hybrid';
217
242
  let selectedApps = scan.apps;
218
- if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive) {
243
+ if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive && !options.fromScan) {
219
244
  console.log('');
220
245
  console.log(chalk_1.default.blue('=== Apps Detected ==='));
221
246
  const appChoices = scan.apps.map(app => ({
@@ -232,6 +257,14 @@ exports.initCommand = new commander_1.Command('init')
232
257
  // Update scan with filtered apps
233
258
  scan = { ...scan, apps: selectedApps };
234
259
  }
260
+ else if (options.fromScan && scan.apps.length > 0) {
261
+ // When using --from-scan, show what was loaded and use it directly
262
+ console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} from detected.yaml`);
263
+ for (const app of scan.apps) {
264
+ console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
265
+ }
266
+ console.log(chalk_1.default.dim('\n (Edit .genbox/detected.yaml to change app selection)'));
267
+ }
235
268
  else if (scan.apps.length > 0) {
236
269
  console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
237
270
  for (const app of scan.apps.slice(0, 5)) {
@@ -248,17 +281,17 @@ exports.initCommand = new commander_1.Command('init')
248
281
  console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
249
282
  }
250
283
  console.log('');
251
- // Get project name
252
- const projectName = nonInteractive
284
+ // Get project name (use scan value when --from-scan)
285
+ const projectName = (nonInteractive || options.fromScan)
253
286
  ? (options.name || scan.projectName)
254
287
  : await prompts.input({
255
288
  message: 'Project name:',
256
289
  default: scan.projectName,
257
290
  });
258
- // Determine if workspace or single project
291
+ // Determine if workspace or single project (auto-detect when --from-scan)
259
292
  let isWorkspace = options.workspace;
260
293
  if (!isWorkspace && (scan.structure.type.startsWith('monorepo') || scan.structure.type === 'hybrid')) {
261
- if (nonInteractive) {
294
+ if (nonInteractive || options.fromScan) {
262
295
  isWorkspace = true; // Default to workspace for monorepos
263
296
  }
264
297
  else {
@@ -271,25 +304,25 @@ exports.initCommand = new commander_1.Command('init')
271
304
  // Generate initial config (v2 format)
272
305
  const generator = new config_generator_1.ConfigGenerator();
273
306
  const generated = generator.generate(scan);
274
- // Convert to v3 format
275
- const v3Config = convertV2ToV3(generated.config, scan);
307
+ // Convert to v4 format (declarative-first architecture)
308
+ const v4Config = convertV2ToV4(generated.config, scan);
276
309
  // Update project name
277
- v3Config.project.name = projectName;
278
- // Ask about profiles
310
+ v4Config.project.name = projectName;
311
+ // Ask about profiles (skip prompt when using --from-scan)
279
312
  let createProfiles = true;
280
- if (!nonInteractive) {
313
+ if (!nonInteractive && !options.fromScan) {
281
314
  createProfiles = await prompts.confirm({
282
315
  message: 'Create predefined profiles for common scenarios?',
283
316
  default: true,
284
317
  });
285
318
  }
286
319
  if (createProfiles) {
287
- v3Config.profiles = nonInteractive
288
- ? createDefaultProfilesSync(scan, v3Config)
289
- : await createDefaultProfiles(scan, v3Config);
320
+ v4Config.profiles = (nonInteractive || options.fromScan)
321
+ ? createDefaultProfilesSync(scan, v4Config)
322
+ : await createDefaultProfiles(scan, v4Config);
290
323
  }
291
- // Get server size
292
- const serverSize = nonInteractive
324
+ // Get server size (use defaults when --from-scan)
325
+ const serverSize = (nonInteractive || options.fromScan)
293
326
  ? generated.config.system.size
294
327
  : await prompts.select({
295
328
  message: 'Default server size:',
@@ -301,13 +334,95 @@ exports.initCommand = new commander_1.Command('init')
301
334
  ],
302
335
  default: generated.config.system.size,
303
336
  });
304
- if (!v3Config.defaults) {
305
- v3Config.defaults = {};
337
+ if (!v4Config.defaults) {
338
+ v4Config.defaults = {};
306
339
  }
307
- v3Config.defaults.size = serverSize;
340
+ v4Config.defaults.size = serverSize;
308
341
  // Git repository setup - different handling for multi-repo vs single-repo
342
+ // When using --from-scan, skip git selection and use what's in detected.yaml
309
343
  const isMultiRepo = isMultiRepoStructure;
310
- if (isMultiRepo) {
344
+ if (options.fromScan) {
345
+ // When using --from-scan, extract git repos from detected.yaml apps
346
+ const detectedPath = path_1.default.join(process.cwd(), '.genbox', 'detected.yaml');
347
+ let detectedConfig = null;
348
+ try {
349
+ const content = fs_1.default.readFileSync(detectedPath, 'utf8');
350
+ detectedConfig = yaml.load(content);
351
+ }
352
+ catch { }
353
+ // Check for per-app git repos first (multi-repo workspace)
354
+ const appsWithGit = detectedConfig
355
+ ? Object.entries(detectedConfig.apps).filter(([, app]) => app.git)
356
+ : [];
357
+ if (appsWithGit.length > 0) {
358
+ // Multi-repo: use per-app git repos
359
+ console.log('');
360
+ console.log(chalk_1.default.blue('=== Git Repositories (from detected.yaml) ==='));
361
+ console.log(chalk_1.default.dim(`Found ${appsWithGit.length} repositories`));
362
+ v4Config.repos = {};
363
+ let hasHttpsRepos = false;
364
+ for (const [appName, app] of appsWithGit) {
365
+ const git = app.git;
366
+ v4Config.repos[appName] = {
367
+ url: git.remote,
368
+ path: `/home/dev/${projectName}/${app.path}`,
369
+ branch: git.branch !== 'main' && git.branch !== 'master' ? git.branch : undefined,
370
+ auth: git.type === 'ssh' ? 'ssh' : 'token',
371
+ };
372
+ console.log(` ${chalk_1.default.cyan(appName)}: ${git.remote}`);
373
+ if (git.type === 'https') {
374
+ hasHttpsRepos = true;
375
+ }
376
+ }
377
+ // Prompt for GIT_TOKEN if any HTTPS repos are found
378
+ if (hasHttpsRepos && !nonInteractive) {
379
+ console.log('');
380
+ console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
381
+ console.log('');
382
+ console.log(chalk_1.default.dim(' To create a token:'));
383
+ console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
384
+ console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
385
+ console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
386
+ console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
387
+ console.log('');
388
+ const gitToken = await prompts.password({
389
+ message: 'GitHub Personal Access Token (leave empty to skip):',
390
+ });
391
+ if (gitToken) {
392
+ envVarsToAdd['GIT_TOKEN'] = gitToken;
393
+ console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
394
+ }
395
+ else {
396
+ console.log(chalk_1.default.dim(' Skipped - add GIT_TOKEN to .env.genbox later if needed'));
397
+ }
398
+ }
399
+ }
400
+ else if (scan.git) {
401
+ // Single repo or monorepo with root git
402
+ const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
403
+ v4Config.repos = {
404
+ [repoName]: {
405
+ url: scan.git.remote,
406
+ path: `/home/dev/${projectName}`,
407
+ auth: scan.git.type === 'ssh' ? 'ssh' : 'token',
408
+ },
409
+ };
410
+ console.log(chalk_1.default.dim(` Git: Using ${repoName} from detected.yaml`));
411
+ // Prompt for GIT_TOKEN if HTTPS
412
+ if (scan.git.type === 'https' && !nonInteractive) {
413
+ console.log('');
414
+ console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
415
+ const gitToken = await prompts.password({
416
+ message: 'GitHub Personal Access Token (leave empty to skip):',
417
+ });
418
+ if (gitToken) {
419
+ envVarsToAdd['GIT_TOKEN'] = gitToken;
420
+ console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
421
+ }
422
+ }
423
+ }
424
+ }
425
+ else if (isMultiRepo) {
311
426
  // Multi-repo workspace: detect git repos in app directories
312
427
  const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
313
428
  if (appGitRepos.length > 0 && !nonInteractive) {
@@ -324,11 +439,11 @@ exports.initCommand = new commander_1.Command('init')
324
439
  choices: repoChoices,
325
440
  });
326
441
  if (selectedRepos.length > 0) {
327
- v3Config.repos = {};
442
+ v4Config.repos = {};
328
443
  let hasHttpsRepos = false;
329
444
  for (const repoName of selectedRepos) {
330
445
  const repo = appGitRepos.find(r => r.appName === repoName);
331
- v3Config.repos[repo.appName] = {
446
+ v4Config.repos[repo.appName] = {
332
447
  url: repo.remote,
333
448
  path: `/home/dev/${projectName}/${repo.appPath}`,
334
449
  branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
@@ -364,9 +479,9 @@ exports.initCommand = new commander_1.Command('init')
364
479
  }
365
480
  else if (appGitRepos.length > 0) {
366
481
  // Non-interactive: include all repos
367
- v3Config.repos = {};
482
+ v4Config.repos = {};
368
483
  for (const repo of appGitRepos) {
369
- v3Config.repos[repo.appName] = {
484
+ v4Config.repos[repo.appName] = {
370
485
  url: repo.remote,
371
486
  path: `/home/dev/${projectName}/${repo.appPath}`,
372
487
  auth: repo.type === 'ssh' ? 'ssh' : 'token',
@@ -378,7 +493,7 @@ exports.initCommand = new commander_1.Command('init')
378
493
  // Single repo or monorepo with root git
379
494
  if (nonInteractive) {
380
495
  const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
381
- v3Config.repos = {
496
+ v4Config.repos = {
382
497
  [repoName]: {
383
498
  url: scan.git.remote,
384
499
  path: `/home/dev/${repoName}`,
@@ -389,10 +504,7 @@ exports.initCommand = new commander_1.Command('init')
389
504
  else {
390
505
  const gitConfig = await setupGitAuth(scan.git, projectName);
391
506
  if (gitConfig.repos) {
392
- v3Config.repos = gitConfig.repos;
393
- }
394
- if (gitConfig.git_auth) {
395
- v3Config.git_auth = gitConfig.git_auth;
507
+ v4Config.repos = gitConfig.repos;
396
508
  }
397
509
  }
398
510
  }
@@ -415,7 +527,7 @@ exports.initCommand = new commander_1.Command('init')
415
527
  },
416
528
  });
417
529
  const repoName = path_1.default.basename(repoUrl, '.git');
418
- v3Config.repos = {
530
+ v4Config.repos = {
419
531
  [repoName]: {
420
532
  url: repoUrl,
421
533
  path: `/home/dev/${repoName}`,
@@ -424,15 +536,16 @@ exports.initCommand = new commander_1.Command('init')
424
536
  };
425
537
  }
426
538
  }
427
- // Environment configuration (skip in non-interactive mode)
539
+ // Environment configuration (skip only in non-interactive mode)
540
+ // For --from-scan, we still want to prompt for environments since they're required for genbox to work
428
541
  if (!nonInteractive) {
429
- const envConfig = await setupEnvironments(scan, v3Config, isMultiRepo);
542
+ const envConfig = await setupEnvironments(scan, v4Config, isMultiRepo);
430
543
  if (envConfig) {
431
- v3Config.environments = envConfig;
544
+ v4Config.environments = envConfig;
432
545
  }
433
546
  }
434
- // Script selection - always show multi-select UI (skip in non-interactive mode)
435
- if (!nonInteractive) {
547
+ // Script selection - always show multi-select UI (skip in non-interactive mode and --from-scan)
548
+ if (!nonInteractive && !options.fromScan) {
436
549
  // Scan for scripts
437
550
  const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
438
551
  const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
@@ -463,7 +576,7 @@ exports.initCommand = new commander_1.Command('init')
463
576
  choices: scriptChoices,
464
577
  });
465
578
  if (selectedScripts.length > 0) {
466
- v3Config.scripts = fullScan.scripts
579
+ v4Config.scripts = fullScan.scripts
467
580
  .filter(s => selectedScripts.includes(s.path))
468
581
  .map(s => ({
469
582
  name: s.name,
@@ -477,7 +590,7 @@ exports.initCommand = new commander_1.Command('init')
477
590
  }
478
591
  }
479
592
  // Save configuration
480
- const yamlContent = yaml.dump(v3Config, {
593
+ const yamlContent = yaml.dump(v4Config, {
481
594
  lineWidth: 120,
482
595
  noRefs: true,
483
596
  quotingType: '"',
@@ -487,11 +600,10 @@ exports.initCommand = new commander_1.Command('init')
487
600
  // Add API URLs from environments to envVarsToAdd
488
601
  // Always add LOCAL_API_URL for local development
489
602
  envVarsToAdd['LOCAL_API_URL'] = 'http://localhost:3050';
490
- if (v3Config.environments) {
491
- for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
492
- const apiUrl = envConfig.api?.api ||
493
- envConfig.api?.url ||
494
- envConfig.api?.gateway;
603
+ if (v4Config.environments) {
604
+ for (const [envName, envConfig] of Object.entries(v4Config.environments)) {
605
+ // v4 format: urls.api contains the API URL
606
+ const apiUrl = envConfig.urls?.api || envConfig.urls?.gateway;
495
607
  if (apiUrl) {
496
608
  const varName = `${envName.toUpperCase()}_API_URL`;
497
609
  envVarsToAdd[varName] = apiUrl;
@@ -499,7 +611,7 @@ exports.initCommand = new commander_1.Command('init')
499
611
  }
500
612
  }
501
613
  // Generate .env.genbox
502
- await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting);
614
+ await setupEnvFile(projectName, v4Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting);
503
615
  // Show warnings
504
616
  if (generated.warnings.length > 0) {
505
617
  console.log('');
@@ -509,16 +621,15 @@ exports.initCommand = new commander_1.Command('init')
509
621
  }
510
622
  }
511
623
  // Show API URL guidance if environments are configured
512
- if (v3Config.environments && Object.keys(v3Config.environments).length > 0) {
624
+ if (v4Config.environments && Object.keys(v4Config.environments).length > 0) {
513
625
  console.log('');
514
626
  console.log(chalk_1.default.blue('=== API URL Configuration ==='));
515
627
  console.log(chalk_1.default.dim('The following API URLs were added to .env.genbox:'));
516
628
  console.log('');
517
629
  console.log(chalk_1.default.dim(' LOCAL_API_URL=http://localhost:3050'));
518
- for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
519
- const apiUrl = envConfig.api?.api ||
520
- envConfig.api?.url ||
521
- envConfig.api?.gateway;
630
+ for (const [envName, envConfig] of Object.entries(v4Config.environments)) {
631
+ // v4 format: urls.api contains the API URL
632
+ const apiUrl = envConfig.urls?.api || envConfig.urls?.gateway;
522
633
  if (apiUrl) {
523
634
  const varName = `${envName.toUpperCase()}_API_URL`;
524
635
  console.log(chalk_1.default.dim(` ${varName}=${apiUrl}`));
@@ -531,9 +642,9 @@ exports.initCommand = new commander_1.Command('init')
531
642
  console.log(chalk_1.default.cyan(' NEXT_PUBLIC_API_URL=${API_URL}'));
532
643
  console.log('');
533
644
  console.log(chalk_1.default.dim(' At create time, ${API_URL} expands based on profile:'));
534
- console.log(chalk_1.default.dim(' • connect_to: staging → uses STAGING_API_URL'));
535
- console.log(chalk_1.default.dim(' • connect_to: production → uses PRODUCTION_API_URL'));
536
- console.log(chalk_1.default.dim(' • local/no connect_to → uses LOCAL_API_URL'));
645
+ console.log(chalk_1.default.dim(' • default_connection: staging → uses STAGING_API_URL'));
646
+ console.log(chalk_1.default.dim(' • default_connection: production → uses PRODUCTION_API_URL'));
647
+ console.log(chalk_1.default.dim(' • local/no default_connection → uses LOCAL_API_URL'));
537
648
  }
538
649
  // Next steps
539
650
  console.log('');
@@ -566,13 +677,13 @@ function createProfilesFromScan(scan) {
566
677
  const frontendApps = scan.apps.filter(a => a.type === 'frontend');
567
678
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
568
679
  const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
569
- // Quick UI profiles for each frontend
680
+ // Quick UI profiles for each frontend (v4: use default_connection instead of connect_to)
570
681
  for (const frontend of frontendApps.slice(0, 3)) {
571
682
  profiles[`${frontend.name}-quick`] = {
572
683
  description: `${frontend.name} only, connected to staging`,
573
684
  size: 'small',
574
685
  apps: [frontend.name],
575
- connect_to: 'staging',
686
+ default_connection: 'staging',
576
687
  };
577
688
  }
578
689
  // Full local development
@@ -599,13 +710,13 @@ function createProfilesFromScan(scan) {
599
710
  },
600
711
  };
601
712
  }
602
- // All frontends + staging
713
+ // All frontends + staging (v4: use default_connection)
603
714
  if (frontendApps.length > 1) {
604
715
  profiles['frontends-staging'] = {
605
716
  description: 'All frontends with staging backend',
606
717
  size: 'medium',
607
718
  apps: frontendApps.map(a => a.name),
608
- connect_to: 'staging',
719
+ default_connection: 'staging',
609
720
  };
610
721
  }
611
722
  // Full stack
@@ -639,14 +750,12 @@ async function setupGitAuth(gitInfo, projectName) {
639
750
  default: 'token',
640
751
  });
641
752
  let repoUrl = gitInfo.remote;
642
- let git_auth;
643
753
  if (authMethod === 'token') {
644
754
  // Convert SSH to HTTPS if needed
645
755
  if (gitInfo.type === 'ssh') {
646
756
  repoUrl = (0, scan_1.sshToHttps)(gitInfo.remote);
647
757
  console.log(chalk_1.default.dim(` Will use HTTPS: ${repoUrl}`));
648
758
  }
649
- git_auth = { method: 'token' };
650
759
  // Show token setup instructions
651
760
  console.log('');
652
761
  console.log(chalk_1.default.yellow(' Add your token to .env.genbox:'));
@@ -677,7 +786,6 @@ async function setupGitAuth(gitInfo, projectName) {
677
786
  });
678
787
  }
679
788
  }
680
- git_auth = { method: 'ssh', ssh_key_path: sshKeyPath };
681
789
  console.log('');
682
790
  console.log(chalk_1.default.yellow(' Add your SSH key to .env.genbox:'));
683
791
  console.log(chalk_1.default.white(` GIT_SSH_KEY="$(cat ${sshKeyPath})"`));
@@ -691,11 +799,10 @@ async function setupGitAuth(gitInfo, projectName) {
691
799
  auth: authMethod === 'public' ? undefined : authMethod,
692
800
  },
693
801
  },
694
- git_auth,
695
802
  };
696
803
  }
697
804
  /**
698
- * Setup staging/production environments
805
+ * Setup staging/production environments (v4 format)
699
806
  */
700
807
  async function setupEnvironments(scan, config, isMultiRepo = false) {
701
808
  const setupEnvs = await prompts.confirm({
@@ -716,22 +823,23 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
716
823
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
717
824
  if (backendApps.length > 0) {
718
825
  console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
719
- const stagingApi = {};
826
+ const urls = {};
720
827
  for (const app of backendApps) {
721
828
  const url = await prompts.input({
722
829
  message: ` ${app.name} staging URL (leave empty to skip):`,
723
830
  default: '',
724
831
  });
725
832
  if (url) {
726
- stagingApi[app.name] = url;
833
+ urls[app.name] = url;
727
834
  }
728
835
  }
729
- if (Object.keys(stagingApi).length > 0) {
836
+ if (Object.keys(urls).length > 0) {
837
+ // Add database URLs
838
+ urls['mongodb'] = '${STAGING_MONGODB_URL}';
839
+ urls['redis'] = '${STAGING_REDIS_URL}';
730
840
  environments.staging = {
731
841
  description: 'Staging environment',
732
- api: stagingApi,
733
- mongodb: { url: '${STAGING_MONGODB_URL}' },
734
- redis: { url: '${STAGING_REDIS_URL}' },
842
+ urls,
735
843
  };
736
844
  }
737
845
  }
@@ -744,9 +852,11 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
744
852
  if (stagingApiUrl) {
745
853
  environments.staging = {
746
854
  description: 'Staging environment',
747
- api: { gateway: stagingApiUrl },
748
- mongodb: { url: '${STAGING_MONGODB_URL}' },
749
- redis: { url: '${STAGING_REDIS_URL}' },
855
+ urls: {
856
+ api: stagingApiUrl,
857
+ mongodb: '${STAGING_MONGODB_URL}',
858
+ redis: '${STAGING_REDIS_URL}',
859
+ },
750
860
  };
751
861
  }
752
862
  }
@@ -760,9 +870,11 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
760
870
  if (stagingApiUrl) {
761
871
  environments.staging = {
762
872
  description: 'Staging environment',
763
- api: { gateway: stagingApiUrl },
764
- mongodb: { url: '${STAGING_MONGODB_URL}' },
765
- redis: { url: '${STAGING_REDIS_URL}' },
873
+ urls: {
874
+ api: stagingApiUrl,
875
+ mongodb: '${STAGING_MONGODB_URL}',
876
+ redis: '${STAGING_REDIS_URL}',
877
+ },
766
878
  };
767
879
  }
768
880
  }
@@ -775,23 +887,24 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
775
887
  const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
776
888
  if (backendApps.length > 0) {
777
889
  console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
778
- const prodApi = {};
890
+ const prodUrls = {};
779
891
  for (const app of backendApps) {
780
892
  const url = await prompts.input({
781
893
  message: ` ${app.name} production URL:`,
782
894
  default: '',
783
895
  });
784
896
  if (url) {
785
- prodApi[app.name] = url;
897
+ prodUrls[app.name] = url;
786
898
  }
787
899
  }
788
- if (Object.keys(prodApi).length > 0) {
900
+ if (Object.keys(prodUrls).length > 0) {
901
+ prodUrls['mongodb'] = '${PROD_MONGODB_URL}';
789
902
  environments.production = {
790
903
  description: 'Production (use with caution)',
791
- api: prodApi,
792
- mongodb: {
793
- url: '${PROD_MONGODB_URL}',
904
+ urls: prodUrls,
905
+ safety: {
794
906
  read_only: true,
907
+ require_confirmation: true,
795
908
  },
796
909
  };
797
910
  }
@@ -805,10 +918,13 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
805
918
  if (prodApiUrl) {
806
919
  environments.production = {
807
920
  description: 'Production (use with caution)',
808
- api: { gateway: prodApiUrl },
809
- mongodb: {
810
- url: '${PROD_MONGODB_URL}',
921
+ urls: {
922
+ api: prodApiUrl,
923
+ mongodb: '${PROD_MONGODB_URL}',
924
+ },
925
+ safety: {
811
926
  read_only: true,
927
+ require_confirmation: true,
812
928
  },
813
929
  };
814
930
  }
@@ -1031,10 +1147,10 @@ function generateEnvTemplate(projectName, config) {
1031
1147
  return lines.join('\n');
1032
1148
  }
1033
1149
  /**
1034
- * Convert GenboxConfigV2 to GenboxConfigV3 format
1150
+ * Convert GenboxConfigV2 to GenboxConfigV4 format
1035
1151
  */
1036
- function convertV2ToV3(v2Config, scan) {
1037
- // Convert services to apps
1152
+ function convertV2ToV4(v2Config, scan) {
1153
+ // Convert services to apps (v4 format)
1038
1154
  const apps = {};
1039
1155
  for (const [name, service] of Object.entries(v2Config.services || {})) {
1040
1156
  const appConfig = {
@@ -1046,10 +1162,13 @@ function convertV2ToV3(v2Config, scan) {
1046
1162
  if (service.framework) {
1047
1163
  appConfig.framework = service.framework;
1048
1164
  }
1049
- // Only add requires if there are dependencies
1165
+ // Convert requires to connects_to (v4 format)
1050
1166
  if (service.dependsOn?.length) {
1051
- appConfig.requires = service.dependsOn.reduce((acc, dep) => {
1052
- acc[dep] = 'required';
1167
+ appConfig.connects_to = service.dependsOn.reduce((acc, dep) => {
1168
+ acc[dep] = {
1169
+ mode: 'local',
1170
+ required: true,
1171
+ };
1053
1172
  return acc;
1054
1173
  }, {});
1055
1174
  }
@@ -1070,11 +1189,11 @@ function convertV2ToV3(v2Config, scan) {
1070
1189
  }
1071
1190
  apps[name] = appConfig;
1072
1191
  }
1073
- // Convert infrastructure
1074
- const infrastructure = {};
1192
+ // Convert infrastructure to provides (v4 format)
1193
+ const provides = {};
1075
1194
  if (v2Config.infrastructure?.databases) {
1076
1195
  for (const db of v2Config.infrastructure.databases) {
1077
- infrastructure[db.container || db.type] = {
1196
+ provides[db.container || db.type] = {
1078
1197
  type: 'database',
1079
1198
  image: `${db.type}:latest`,
1080
1199
  port: db.port,
@@ -1083,7 +1202,7 @@ function convertV2ToV3(v2Config, scan) {
1083
1202
  }
1084
1203
  if (v2Config.infrastructure?.caches) {
1085
1204
  for (const cache of v2Config.infrastructure.caches) {
1086
- infrastructure[cache.container || cache.type] = {
1205
+ provides[cache.container || cache.type] = {
1087
1206
  type: 'cache',
1088
1207
  image: `${cache.type}:latest`,
1089
1208
  port: cache.port,
@@ -1092,15 +1211,15 @@ function convertV2ToV3(v2Config, scan) {
1092
1211
  }
1093
1212
  if (v2Config.infrastructure?.queues) {
1094
1213
  for (const queue of v2Config.infrastructure.queues) {
1095
- infrastructure[queue.container || queue.type] = {
1214
+ provides[queue.container || queue.type] = {
1096
1215
  type: 'queue',
1097
1216
  image: `${queue.type}:latest`,
1098
1217
  port: queue.port,
1099
- management_port: queue.managementPort,
1218
+ additional_ports: queue.managementPort ? { management: queue.managementPort } : undefined,
1100
1219
  };
1101
1220
  }
1102
1221
  }
1103
- // Convert repos
1222
+ // Convert repos (v4 format)
1104
1223
  const repos = {};
1105
1224
  for (const [name, repo] of Object.entries(v2Config.repos || {})) {
1106
1225
  repos[name] = {
@@ -1110,18 +1229,25 @@ function convertV2ToV3(v2Config, scan) {
1110
1229
  auth: repo.auth,
1111
1230
  };
1112
1231
  }
1113
- // Build v3 config
1114
- const v3Config = {
1115
- version: '3.0',
1232
+ // Map structure type to v4
1233
+ const structureMap = {
1234
+ 'single-app': 'single-app',
1235
+ 'monorepo-pnpm': 'monorepo',
1236
+ 'monorepo-yarn': 'monorepo',
1237
+ 'monorepo-npm': 'monorepo',
1238
+ 'microservices': 'microservices',
1239
+ 'hybrid': 'hybrid',
1240
+ };
1241
+ // Build v4 config
1242
+ const v4Config = {
1243
+ version: 4,
1116
1244
  project: {
1117
1245
  name: v2Config.project.name,
1118
- structure: v2Config.project.structure === 'single-app' ? 'single-app' :
1119
- v2Config.project.structure.startsWith('monorepo') ? 'monorepo' :
1120
- v2Config.project.structure,
1246
+ structure: structureMap[v2Config.project.structure] || 'single-app',
1121
1247
  description: v2Config.project.description,
1122
1248
  },
1123
1249
  apps,
1124
- infrastructure: Object.keys(infrastructure).length > 0 ? infrastructure : undefined,
1250
+ provides: Object.keys(provides).length > 0 ? provides : undefined,
1125
1251
  repos: Object.keys(repos).length > 0 ? repos : undefined,
1126
1252
  defaults: {
1127
1253
  size: v2Config.system.size,
@@ -1131,6 +1257,135 @@ function convertV2ToV3(v2Config, scan) {
1131
1257
  post_start: v2Config.hooks.postStart,
1132
1258
  pre_start: v2Config.hooks.preStart,
1133
1259
  } : undefined,
1260
+ strict: {
1261
+ enabled: true,
1262
+ allow_detect: true,
1263
+ warnings_as_errors: false,
1264
+ },
1265
+ };
1266
+ return v4Config;
1267
+ }
1268
+ /**
1269
+ * Convert DetectedConfig (from detected.yaml) to ProjectScan format
1270
+ * This allows --from-scan to use the same code paths as fresh scanning
1271
+ */
1272
+ function convertDetectedToScan(detected) {
1273
+ // Convert structure type
1274
+ const structureTypeMap = {
1275
+ 'single-app': 'single-app',
1276
+ 'monorepo': 'monorepo-pnpm',
1277
+ 'workspace': 'hybrid',
1278
+ 'microservices': 'microservices',
1279
+ 'hybrid': 'hybrid',
1280
+ };
1281
+ // Convert apps
1282
+ const apps = [];
1283
+ for (const [name, app] of Object.entries(detected.apps || {})) {
1284
+ // Map detected type to scanner type
1285
+ const typeMap = {
1286
+ 'frontend': 'frontend',
1287
+ 'backend': 'backend',
1288
+ 'worker': 'worker',
1289
+ 'gateway': 'gateway',
1290
+ 'library': 'library',
1291
+ };
1292
+ apps.push({
1293
+ name,
1294
+ path: app.path,
1295
+ type: typeMap[app.type || 'library'] || 'library',
1296
+ framework: app.framework,
1297
+ port: app.port,
1298
+ dependencies: app.dependencies,
1299
+ scripts: app.commands ? {
1300
+ dev: app.commands.dev || '',
1301
+ build: app.commands.build || '',
1302
+ start: app.commands.start || '',
1303
+ } : {},
1304
+ });
1305
+ }
1306
+ // Convert runtimes
1307
+ const runtimes = detected.runtimes.map(r => ({
1308
+ language: r.language,
1309
+ version: r.version,
1310
+ versionSource: r.version_source,
1311
+ packageManager: r.package_manager,
1312
+ lockfile: r.lockfile,
1313
+ }));
1314
+ // Convert infrastructure to compose analysis
1315
+ let compose = null;
1316
+ if (detected.infrastructure && detected.infrastructure.length > 0) {
1317
+ compose = {
1318
+ files: ['docker-compose.yml'],
1319
+ applications: [],
1320
+ databases: detected.infrastructure
1321
+ .filter(i => i.type === 'database')
1322
+ .map(i => ({
1323
+ name: i.name,
1324
+ image: i.image,
1325
+ ports: [{ host: i.port, container: i.port }],
1326
+ environment: {},
1327
+ dependsOn: [],
1328
+ volumes: [],
1329
+ })),
1330
+ caches: detected.infrastructure
1331
+ .filter(i => i.type === 'cache')
1332
+ .map(i => ({
1333
+ name: i.name,
1334
+ image: i.image,
1335
+ ports: [{ host: i.port, container: i.port }],
1336
+ environment: {},
1337
+ dependsOn: [],
1338
+ volumes: [],
1339
+ })),
1340
+ queues: detected.infrastructure
1341
+ .filter(i => i.type === 'queue')
1342
+ .map(i => ({
1343
+ name: i.name,
1344
+ image: i.image,
1345
+ ports: [{ host: i.port, container: i.port }],
1346
+ environment: {},
1347
+ dependsOn: [],
1348
+ volumes: [],
1349
+ })),
1350
+ infrastructure: [],
1351
+ portMap: new Map(),
1352
+ dependencyGraph: new Map(),
1353
+ };
1354
+ }
1355
+ // Convert git
1356
+ const git = detected.git ? {
1357
+ remote: detected.git.remote || '',
1358
+ type: detected.git.type || 'https',
1359
+ provider: (detected.git.provider || 'other'),
1360
+ branch: detected.git.branch || 'main',
1361
+ } : undefined;
1362
+ // Convert scripts
1363
+ const scripts = (detected.scripts || []).map(s => ({
1364
+ name: s.name,
1365
+ path: s.path,
1366
+ stage: s.stage,
1367
+ isExecutable: s.executable,
1368
+ }));
1369
+ return {
1370
+ projectName: path_1.default.basename(detected._meta.scanned_root),
1371
+ root: detected._meta.scanned_root,
1372
+ structure: {
1373
+ type: structureTypeMap[detected.structure.type] || 'single-app',
1374
+ confidence: detected.structure.confidence,
1375
+ indicators: detected.structure.indicators,
1376
+ },
1377
+ runtimes,
1378
+ frameworks: [], // Not stored in detected.yaml
1379
+ compose,
1380
+ apps,
1381
+ envAnalysis: {
1382
+ required: [],
1383
+ optional: [],
1384
+ secrets: [],
1385
+ references: [],
1386
+ sources: [],
1387
+ },
1388
+ git,
1389
+ scripts,
1134
1390
  };
1135
- return v3Config;
1136
1391
  }