genbox 1.0.14 → 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.
- package/dist/commands/connect.js +7 -2
- package/dist/commands/create.js +51 -115
- package/dist/commands/init.js +360 -105
- package/dist/commands/profiles.js +49 -13
- package/dist/commands/scan.js +64 -2
- package/dist/commands/ssh-setup.js +34 -0
- package/dist/config-explainer.js +2 -1
- package/dist/config-loader.js +131 -41
- package/dist/index.js +3 -1
- package/dist/profile-resolver.js +35 -18
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
196
|
-
const spinner = (0, ora_1.default)('Scanning project...').start();
|
|
196
|
+
let scan;
|
|
197
197
|
const scanner = new scanner_1.ProjectScanner();
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
275
|
-
const
|
|
307
|
+
// Convert to v4 format (declarative-first architecture)
|
|
308
|
+
const v4Config = convertV2ToV4(generated.config, scan);
|
|
276
309
|
// Update project name
|
|
277
|
-
|
|
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
|
-
|
|
288
|
-
? createDefaultProfilesSync(scan,
|
|
289
|
-
: await createDefaultProfiles(scan,
|
|
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 (!
|
|
305
|
-
|
|
337
|
+
if (!v4Config.defaults) {
|
|
338
|
+
v4Config.defaults = {};
|
|
306
339
|
}
|
|
307
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
+
v4Config.repos = {};
|
|
368
483
|
for (const repo of appGitRepos) {
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
542
|
+
const envConfig = await setupEnvironments(scan, v4Config, isMultiRepo);
|
|
430
543
|
if (envConfig) {
|
|
431
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
491
|
-
for (const [envName, envConfig] of Object.entries(
|
|
492
|
-
|
|
493
|
-
|
|
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,
|
|
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 (
|
|
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(
|
|
519
|
-
|
|
520
|
-
|
|
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(' •
|
|
535
|
-
console.log(chalk_1.default.dim(' •
|
|
536
|
-
console.log(chalk_1.default.dim(' • local/no
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
833
|
+
urls[app.name] = url;
|
|
727
834
|
}
|
|
728
835
|
}
|
|
729
|
-
if (Object.keys(
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
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
|
-
|
|
897
|
+
prodUrls[app.name] = url;
|
|
786
898
|
}
|
|
787
899
|
}
|
|
788
|
-
if (Object.keys(
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
|
1150
|
+
* Convert GenboxConfigV2 to GenboxConfigV4 format
|
|
1035
1151
|
*/
|
|
1036
|
-
function
|
|
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
|
-
//
|
|
1165
|
+
// Convert requires to connects_to (v4 format)
|
|
1050
1166
|
if (service.dependsOn?.length) {
|
|
1051
|
-
appConfig.
|
|
1052
|
-
acc[dep] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1214
|
+
provides[queue.container || queue.type] = {
|
|
1096
1215
|
type: 'queue',
|
|
1097
1216
|
image: `${queue.type}:latest`,
|
|
1098
1217
|
port: queue.port,
|
|
1099
|
-
|
|
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
|
-
//
|
|
1114
|
-
const
|
|
1115
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|