genbox 1.0.4 → 1.0.6
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/init.js +371 -126
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -51,6 +51,79 @@ const config_generator_1 = require("../scanner/config-generator");
|
|
|
51
51
|
const scan_1 = require("../scan");
|
|
52
52
|
const CONFIG_FILENAME = 'genbox.yaml';
|
|
53
53
|
const ENV_FILENAME = '.env.genbox';
|
|
54
|
+
/**
|
|
55
|
+
* Detect git repositories in app directories (for multi-repo workspaces)
|
|
56
|
+
*/
|
|
57
|
+
function detectAppGitRepos(apps, rootDir) {
|
|
58
|
+
const { execSync } = require('child_process');
|
|
59
|
+
const repos = [];
|
|
60
|
+
for (const app of apps) {
|
|
61
|
+
const appDir = path_1.default.join(rootDir, app.path);
|
|
62
|
+
const gitDir = path_1.default.join(appDir, '.git');
|
|
63
|
+
if (!fs_1.default.existsSync(gitDir))
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
const remote = execSync('git remote get-url origin', {
|
|
67
|
+
cwd: appDir,
|
|
68
|
+
stdio: 'pipe',
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
}).trim();
|
|
71
|
+
if (!remote)
|
|
72
|
+
continue;
|
|
73
|
+
const isSSH = remote.startsWith('git@') || remote.startsWith('ssh://');
|
|
74
|
+
let provider = 'other';
|
|
75
|
+
if (remote.includes('github.com'))
|
|
76
|
+
provider = 'github';
|
|
77
|
+
else if (remote.includes('gitlab.com'))
|
|
78
|
+
provider = 'gitlab';
|
|
79
|
+
else if (remote.includes('bitbucket.org'))
|
|
80
|
+
provider = 'bitbucket';
|
|
81
|
+
let branch = 'main';
|
|
82
|
+
try {
|
|
83
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
84
|
+
cwd: appDir,
|
|
85
|
+
stdio: 'pipe',
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
}).trim();
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
repos.push({
|
|
91
|
+
appName: app.name,
|
|
92
|
+
appPath: app.path,
|
|
93
|
+
remote,
|
|
94
|
+
type: isSSH ? 'ssh' : 'https',
|
|
95
|
+
provider,
|
|
96
|
+
branch,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// No git remote in this directory
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return repos;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Find .env files in app directories
|
|
107
|
+
*/
|
|
108
|
+
function findAppEnvFiles(apps, rootDir) {
|
|
109
|
+
const envFiles = [];
|
|
110
|
+
const envPatterns = ['.env', '.env.local', '.env.development'];
|
|
111
|
+
for (const app of apps) {
|
|
112
|
+
const appDir = path_1.default.join(rootDir, app.path);
|
|
113
|
+
for (const pattern of envPatterns) {
|
|
114
|
+
const envPath = path_1.default.join(appDir, pattern);
|
|
115
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
116
|
+
envFiles.push({
|
|
117
|
+
appName: app.name,
|
|
118
|
+
envFile: pattern,
|
|
119
|
+
fullPath: envPath,
|
|
120
|
+
});
|
|
121
|
+
break; // Only take the first match per app
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return envFiles;
|
|
126
|
+
}
|
|
54
127
|
exports.initCommand = new commander_1.Command('init')
|
|
55
128
|
.description('Initialize a new Genbox configuration')
|
|
56
129
|
.option('--v2', 'Use legacy v2 format (single-app only)')
|
|
@@ -80,24 +153,15 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
80
153
|
}
|
|
81
154
|
console.log(chalk_1.default.blue('Initializing Genbox...'));
|
|
82
155
|
console.log('');
|
|
83
|
-
// Get
|
|
156
|
+
// Get initial exclusions from CLI options only
|
|
84
157
|
let exclude = [];
|
|
85
158
|
if (options.exclude) {
|
|
86
159
|
exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
|
|
87
160
|
}
|
|
88
|
-
|
|
89
|
-
const excludeDirs = await prompts.input({
|
|
90
|
-
message: 'Directories to exclude (comma-separated, or empty to skip):',
|
|
91
|
-
default: '',
|
|
92
|
-
});
|
|
93
|
-
exclude = excludeDirs
|
|
94
|
-
? excludeDirs.split(',').map((d) => d.trim()).filter(Boolean)
|
|
95
|
-
: [];
|
|
96
|
-
}
|
|
97
|
-
// Scan project (skip scripts initially - we'll ask about them later)
|
|
161
|
+
// Scan project first (skip scripts initially)
|
|
98
162
|
const spinner = (0, ora_1.default)('Scanning project...').start();
|
|
99
163
|
const scanner = new scanner_1.ProjectScanner();
|
|
100
|
-
|
|
164
|
+
let scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
|
|
101
165
|
spinner.succeed('Project scanned');
|
|
102
166
|
// Display scan results
|
|
103
167
|
console.log('');
|
|
@@ -114,7 +178,27 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
114
178
|
const frameworkStr = scan.frameworks.map(f => f.name).join(', ');
|
|
115
179
|
console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
|
|
116
180
|
}
|
|
117
|
-
|
|
181
|
+
// For multi-repo: show apps and let user select which to include
|
|
182
|
+
const isMultiRepoStructure = scan.structure.type === 'hybrid';
|
|
183
|
+
let selectedApps = scan.apps;
|
|
184
|
+
if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive) {
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(chalk_1.default.blue('=== Apps Detected ==='));
|
|
187
|
+
const appChoices = scan.apps.map(app => ({
|
|
188
|
+
name: `${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`,
|
|
189
|
+
value: app.name,
|
|
190
|
+
checked: app.type !== 'library', // Default select non-libraries
|
|
191
|
+
}));
|
|
192
|
+
const selectedAppNames = await prompts.checkbox({
|
|
193
|
+
message: 'Select apps to include:',
|
|
194
|
+
choices: appChoices,
|
|
195
|
+
});
|
|
196
|
+
// Filter scan.apps to only selected ones
|
|
197
|
+
selectedApps = scan.apps.filter(a => selectedAppNames.includes(a.name));
|
|
198
|
+
// Update scan with filtered apps
|
|
199
|
+
scan = { ...scan, apps: selectedApps };
|
|
200
|
+
}
|
|
201
|
+
else if (scan.apps.length > 0) {
|
|
118
202
|
console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
|
|
119
203
|
for (const app of scan.apps.slice(0, 5)) {
|
|
120
204
|
console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
|
|
@@ -187,10 +271,52 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
187
271
|
v3Config.defaults = {};
|
|
188
272
|
}
|
|
189
273
|
v3Config.defaults.size = serverSize;
|
|
190
|
-
// Git repository setup
|
|
191
|
-
|
|
274
|
+
// Git repository setup - different handling for multi-repo vs single-repo
|
|
275
|
+
const isMultiRepo = isMultiRepoStructure;
|
|
276
|
+
if (isMultiRepo) {
|
|
277
|
+
// Multi-repo workspace: detect git repos in app directories
|
|
278
|
+
const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
|
|
279
|
+
if (appGitRepos.length > 0 && !nonInteractive) {
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log(chalk_1.default.blue('=== Git Repositories ==='));
|
|
282
|
+
console.log(chalk_1.default.dim(`Found ${appGitRepos.length} git repositories in app directories`));
|
|
283
|
+
const repoChoices = appGitRepos.map(repo => ({
|
|
284
|
+
name: `${repo.appName} - ${repo.remote}`,
|
|
285
|
+
value: repo.appName,
|
|
286
|
+
checked: true, // Default to include all
|
|
287
|
+
}));
|
|
288
|
+
const selectedRepos = await prompts.checkbox({
|
|
289
|
+
message: 'Select repositories to include:',
|
|
290
|
+
choices: repoChoices,
|
|
291
|
+
});
|
|
292
|
+
if (selectedRepos.length > 0) {
|
|
293
|
+
v3Config.repos = {};
|
|
294
|
+
for (const repoName of selectedRepos) {
|
|
295
|
+
const repo = appGitRepos.find(r => r.appName === repoName);
|
|
296
|
+
v3Config.repos[repo.appName] = {
|
|
297
|
+
url: repo.remote,
|
|
298
|
+
path: `/home/dev/${projectName}/${repo.appPath}`,
|
|
299
|
+
branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
|
|
300
|
+
auth: repo.type === 'ssh' ? 'ssh' : 'token',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (appGitRepos.length > 0) {
|
|
306
|
+
// Non-interactive: include all repos
|
|
307
|
+
v3Config.repos = {};
|
|
308
|
+
for (const repo of appGitRepos) {
|
|
309
|
+
v3Config.repos[repo.appName] = {
|
|
310
|
+
url: repo.remote,
|
|
311
|
+
path: `/home/dev/${projectName}/${repo.appPath}`,
|
|
312
|
+
auth: repo.type === 'ssh' ? 'ssh' : 'token',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (scan.git) {
|
|
318
|
+
// Single repo or monorepo with root git
|
|
192
319
|
if (nonInteractive) {
|
|
193
|
-
// Use detected git config with defaults
|
|
194
320
|
const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
|
|
195
321
|
v3Config.repos = {
|
|
196
322
|
[repoName]: {
|
|
@@ -210,7 +336,8 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
210
336
|
}
|
|
211
337
|
}
|
|
212
338
|
}
|
|
213
|
-
else if (!nonInteractive) {
|
|
339
|
+
else if (!nonInteractive && !isMultiRepo) {
|
|
340
|
+
// Only ask to add repo for non-multi-repo projects
|
|
214
341
|
const addRepo = await prompts.confirm({
|
|
215
342
|
message: 'No git remote detected. Add a repository?',
|
|
216
343
|
default: false,
|
|
@@ -239,66 +366,55 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
239
366
|
}
|
|
240
367
|
// Environment configuration (skip in non-interactive mode)
|
|
241
368
|
if (!nonInteractive) {
|
|
242
|
-
const envConfig = await setupEnvironments(scan, v3Config);
|
|
369
|
+
const envConfig = await setupEnvironments(scan, v3Config, isMultiRepo);
|
|
243
370
|
if (envConfig) {
|
|
244
371
|
v3Config.environments = envConfig;
|
|
245
372
|
}
|
|
246
373
|
}
|
|
247
|
-
// Script selection (skip in non-interactive mode)
|
|
374
|
+
// Script selection - always show multi-select UI (skip in non-interactive mode)
|
|
248
375
|
if (!nonInteractive) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
|
|
264
|
-
const existing = scriptsByDir.get(dir) || [];
|
|
265
|
-
existing.push(script);
|
|
266
|
-
scriptsByDir.set(dir, existing);
|
|
267
|
-
}
|
|
268
|
-
// Show grouped scripts
|
|
269
|
-
for (const [dir, scripts] of scriptsByDir) {
|
|
270
|
-
console.log(chalk_1.default.dim(` ${dir}/`));
|
|
271
|
-
for (const s of scripts.slice(0, 5)) {
|
|
272
|
-
console.log(chalk_1.default.dim(` - ${s.name}`));
|
|
273
|
-
}
|
|
274
|
-
if (scripts.length > 5) {
|
|
275
|
-
console.log(chalk_1.default.dim(` ... and ${scripts.length - 5} more`));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Let user select scripts
|
|
279
|
-
const scriptChoices = fullScan.scripts.map(s => ({
|
|
280
|
-
name: `${s.path} (${s.stage})`,
|
|
281
|
-
value: s.path,
|
|
282
|
-
checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
|
|
283
|
-
}));
|
|
284
|
-
const selectedScripts = await prompts.checkbox({
|
|
285
|
-
message: 'Select scripts to include:',
|
|
286
|
-
choices: scriptChoices,
|
|
287
|
-
});
|
|
288
|
-
if (selectedScripts.length > 0) {
|
|
289
|
-
v3Config.scripts = fullScan.scripts
|
|
290
|
-
.filter(s => selectedScripts.includes(s.path))
|
|
291
|
-
.map(s => ({
|
|
292
|
-
name: s.name,
|
|
293
|
-
path: s.path,
|
|
294
|
-
stage: s.stage,
|
|
295
|
-
}));
|
|
296
|
-
}
|
|
376
|
+
// Scan for scripts
|
|
377
|
+
const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
|
|
378
|
+
const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
|
|
379
|
+
scriptsSpinner.stop();
|
|
380
|
+
if (fullScan.scripts.length > 0) {
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log(chalk_1.default.blue('=== Setup Scripts ==='));
|
|
383
|
+
// Group scripts by directory for display
|
|
384
|
+
const scriptsByDir = new Map();
|
|
385
|
+
for (const script of fullScan.scripts) {
|
|
386
|
+
const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
|
|
387
|
+
const existing = scriptsByDir.get(dir) || [];
|
|
388
|
+
existing.push(script);
|
|
389
|
+
scriptsByDir.set(dir, existing);
|
|
297
390
|
}
|
|
298
|
-
|
|
299
|
-
|
|
391
|
+
// Show grouped scripts
|
|
392
|
+
for (const [dir, scripts] of scriptsByDir) {
|
|
393
|
+
console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
|
|
394
|
+
}
|
|
395
|
+
// Let user select scripts with multi-select
|
|
396
|
+
const scriptChoices = fullScan.scripts.map(s => ({
|
|
397
|
+
name: `${s.path} (${s.stage})`,
|
|
398
|
+
value: s.path,
|
|
399
|
+
checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
|
|
400
|
+
}));
|
|
401
|
+
const selectedScripts = await prompts.checkbox({
|
|
402
|
+
message: 'Select scripts to include (space to toggle, enter to confirm):',
|
|
403
|
+
choices: scriptChoices,
|
|
404
|
+
});
|
|
405
|
+
if (selectedScripts.length > 0) {
|
|
406
|
+
v3Config.scripts = fullScan.scripts
|
|
407
|
+
.filter(s => selectedScripts.includes(s.path))
|
|
408
|
+
.map(s => ({
|
|
409
|
+
name: s.name,
|
|
410
|
+
path: s.path,
|
|
411
|
+
stage: s.stage,
|
|
412
|
+
}));
|
|
300
413
|
}
|
|
301
414
|
}
|
|
415
|
+
else {
|
|
416
|
+
console.log(chalk_1.default.dim('No scripts found.'));
|
|
417
|
+
}
|
|
302
418
|
}
|
|
303
419
|
// Save configuration
|
|
304
420
|
const yamlContent = yaml.dump(v3Config, {
|
|
@@ -309,7 +425,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
309
425
|
fs_1.default.writeFileSync(configPath, yamlContent);
|
|
310
426
|
console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
|
|
311
427
|
// Generate .env.genbox
|
|
312
|
-
await setupEnvFile(projectName, v3Config, nonInteractive);
|
|
428
|
+
await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo);
|
|
313
429
|
// Show warnings
|
|
314
430
|
if (generated.warnings.length > 0) {
|
|
315
431
|
console.log('');
|
|
@@ -480,7 +596,7 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
480
596
|
/**
|
|
481
597
|
* Setup staging/production environments
|
|
482
598
|
*/
|
|
483
|
-
async function setupEnvironments(scan, config) {
|
|
599
|
+
async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
484
600
|
const setupEnvs = await prompts.confirm({
|
|
485
601
|
message: 'Configure staging/production environments?',
|
|
486
602
|
default: true,
|
|
@@ -493,37 +609,108 @@ async function setupEnvironments(scan, config) {
|
|
|
493
609
|
console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
|
|
494
610
|
console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
|
|
495
611
|
console.log('');
|
|
496
|
-
const stagingApiUrl = await prompts.input({
|
|
497
|
-
message: 'Staging API URL (leave empty to skip):',
|
|
498
|
-
default: '',
|
|
499
|
-
});
|
|
500
612
|
const environments = {};
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
613
|
+
if (isMultiRepo) {
|
|
614
|
+
// For multi-repo: configure API URLs per backend app
|
|
615
|
+
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
616
|
+
if (backendApps.length > 0) {
|
|
617
|
+
console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
|
|
618
|
+
const stagingApi = {};
|
|
619
|
+
for (const app of backendApps) {
|
|
620
|
+
const url = await prompts.input({
|
|
621
|
+
message: ` ${app.name} staging URL (leave empty to skip):`,
|
|
622
|
+
default: '',
|
|
623
|
+
});
|
|
624
|
+
if (url) {
|
|
625
|
+
stagingApi[app.name] = url;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (Object.keys(stagingApi).length > 0) {
|
|
629
|
+
environments.staging = {
|
|
630
|
+
description: 'Staging environment',
|
|
631
|
+
api: stagingApi,
|
|
632
|
+
mongodb: { url: '${STAGING_MONGODB_URL}' },
|
|
633
|
+
redis: { url: '${STAGING_REDIS_URL}' },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
// No backend apps, just ask for a single URL
|
|
639
|
+
const stagingApiUrl = await prompts.input({
|
|
640
|
+
message: 'Staging API URL (leave empty to skip):',
|
|
641
|
+
default: '',
|
|
642
|
+
});
|
|
643
|
+
if (stagingApiUrl) {
|
|
644
|
+
environments.staging = {
|
|
645
|
+
description: 'Staging environment',
|
|
646
|
+
api: { gateway: stagingApiUrl },
|
|
647
|
+
mongodb: { url: '${STAGING_MONGODB_URL}' },
|
|
648
|
+
redis: { url: '${STAGING_REDIS_URL}' },
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Single repo: simple single URL
|
|
655
|
+
const stagingApiUrl = await prompts.input({
|
|
656
|
+
message: 'Staging API URL (leave empty to skip):',
|
|
657
|
+
default: '',
|
|
658
|
+
});
|
|
659
|
+
if (stagingApiUrl) {
|
|
660
|
+
environments.staging = {
|
|
661
|
+
description: 'Staging environment',
|
|
662
|
+
api: { gateway: stagingApiUrl },
|
|
663
|
+
mongodb: { url: '${STAGING_MONGODB_URL}' },
|
|
664
|
+
redis: { url: '${STAGING_REDIS_URL}' },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
508
667
|
}
|
|
509
668
|
const setupProd = await prompts.confirm({
|
|
510
669
|
message: 'Also configure production environment?',
|
|
511
670
|
default: false,
|
|
512
671
|
});
|
|
513
672
|
if (setupProd) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
673
|
+
if (isMultiRepo) {
|
|
674
|
+
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
675
|
+
if (backendApps.length > 0) {
|
|
676
|
+
console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
|
|
677
|
+
const prodApi = {};
|
|
678
|
+
for (const app of backendApps) {
|
|
679
|
+
const url = await prompts.input({
|
|
680
|
+
message: ` ${app.name} production URL:`,
|
|
681
|
+
default: '',
|
|
682
|
+
});
|
|
683
|
+
if (url) {
|
|
684
|
+
prodApi[app.name] = url;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (Object.keys(prodApi).length > 0) {
|
|
688
|
+
environments.production = {
|
|
689
|
+
description: 'Production (use with caution)',
|
|
690
|
+
api: prodApi,
|
|
691
|
+
mongodb: {
|
|
692
|
+
url: '${PROD_MONGODB_URL}',
|
|
693
|
+
read_only: true,
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
const prodApiUrl = await prompts.input({
|
|
701
|
+
message: 'Production API URL:',
|
|
702
|
+
default: '',
|
|
703
|
+
});
|
|
704
|
+
if (prodApiUrl) {
|
|
705
|
+
environments.production = {
|
|
706
|
+
description: 'Production (use with caution)',
|
|
707
|
+
api: { gateway: prodApiUrl },
|
|
708
|
+
mongodb: {
|
|
709
|
+
url: '${PROD_MONGODB_URL}',
|
|
710
|
+
read_only: true,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
527
714
|
}
|
|
528
715
|
}
|
|
529
716
|
return Object.keys(environments).length > 0 ? environments : undefined;
|
|
@@ -531,31 +718,89 @@ async function setupEnvironments(scan, config) {
|
|
|
531
718
|
/**
|
|
532
719
|
* Setup .env.genbox file
|
|
533
720
|
*/
|
|
534
|
-
async function setupEnvFile(projectName, config, nonInteractive = false) {
|
|
721
|
+
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false) {
|
|
535
722
|
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
536
723
|
if (fs_1.default.existsSync(envPath)) {
|
|
537
724
|
console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
|
|
538
725
|
return;
|
|
539
726
|
}
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
727
|
+
// For multi-repo: find env files in app directories
|
|
728
|
+
if (isMultiRepo && scan) {
|
|
729
|
+
const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
|
|
730
|
+
if (appEnvFiles.length > 0 && !nonInteractive) {
|
|
731
|
+
console.log('');
|
|
732
|
+
console.log(chalk_1.default.blue('=== Environment Files ==='));
|
|
733
|
+
console.log(chalk_1.default.dim(`Found .env files in ${appEnvFiles.length} app directories`));
|
|
734
|
+
const envChoices = appEnvFiles.map(env => ({
|
|
735
|
+
name: `${env.appName}/${env.envFile}`,
|
|
736
|
+
value: env.fullPath,
|
|
737
|
+
checked: true,
|
|
738
|
+
}));
|
|
739
|
+
const selectedEnvFiles = await prompts.checkbox({
|
|
740
|
+
message: 'Select .env files to merge into .env.genbox:',
|
|
741
|
+
choices: envChoices,
|
|
742
|
+
});
|
|
743
|
+
if (selectedEnvFiles.length > 0) {
|
|
744
|
+
let mergedContent = `# Genbox Environment Variables
|
|
745
|
+
# Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
|
|
746
|
+
# DO NOT COMMIT THIS FILE
|
|
747
|
+
#
|
|
748
|
+
# Add staging/production URLs:
|
|
749
|
+
# STAGING_MONGODB_URL=mongodb+srv://...
|
|
750
|
+
# STAGING_REDIS_URL=redis://...
|
|
751
|
+
# PROD_MONGODB_URL=mongodb+srv://...
|
|
752
|
+
#
|
|
753
|
+
# Git authentication:
|
|
754
|
+
# GIT_TOKEN=ghp_xxxxxxxxxxxx
|
|
755
|
+
|
|
756
|
+
`;
|
|
757
|
+
for (const envFilePath of selectedEnvFiles) {
|
|
758
|
+
const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
|
|
759
|
+
const content = fs_1.default.readFileSync(envFilePath, 'utf8');
|
|
760
|
+
mergedContent += `\n# === ${appInfo?.appName || path_1.default.dirname(envFilePath)} ===\n`;
|
|
761
|
+
mergedContent += content;
|
|
762
|
+
mergedContent += '\n';
|
|
763
|
+
}
|
|
764
|
+
fs_1.default.writeFileSync(envPath, mergedContent);
|
|
765
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
else if (appEnvFiles.length > 0 && nonInteractive) {
|
|
769
|
+
// Non-interactive: merge all env files
|
|
770
|
+
let mergedContent = `# Genbox Environment Variables
|
|
771
|
+
# Merged from app directories
|
|
772
|
+
# DO NOT COMMIT THIS FILE
|
|
773
|
+
|
|
774
|
+
`;
|
|
775
|
+
for (const envFile of appEnvFiles) {
|
|
776
|
+
const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
|
|
777
|
+
mergedContent += `\n# === ${envFile.appName} ===\n`;
|
|
778
|
+
mergedContent += content;
|
|
779
|
+
mergedContent += '\n';
|
|
780
|
+
}
|
|
781
|
+
fs_1.default.writeFileSync(envPath, mergedContent);
|
|
782
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
|
|
548
783
|
}
|
|
549
784
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
785
|
+
// If no env file created yet, check for root .env
|
|
786
|
+
if (!fs_1.default.existsSync(envPath)) {
|
|
787
|
+
const existingEnvFiles = ['.env.local', '.env', '.env.development'];
|
|
788
|
+
let existingEnvPath;
|
|
789
|
+
for (const envFile of existingEnvFiles) {
|
|
790
|
+
const fullPath = path_1.default.join(process.cwd(), envFile);
|
|
791
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
792
|
+
existingEnvPath = fullPath;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (existingEnvPath) {
|
|
797
|
+
const copyExisting = nonInteractive ? true : await prompts.confirm({
|
|
798
|
+
message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
|
|
799
|
+
default: true,
|
|
800
|
+
});
|
|
801
|
+
if (copyExisting) {
|
|
802
|
+
const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
|
|
803
|
+
const header = `# Genbox Environment Variables
|
|
559
804
|
# Generated from ${path_1.default.basename(existingEnvPath)}
|
|
560
805
|
# DO NOT COMMIT THIS FILE
|
|
561
806
|
#
|
|
@@ -568,20 +813,20 @@ async function setupEnvFile(projectName, config, nonInteractive = false) {
|
|
|
568
813
|
# GIT_TOKEN=ghp_xxxxxxxxxxxx
|
|
569
814
|
|
|
570
815
|
`;
|
|
571
|
-
|
|
572
|
-
|
|
816
|
+
fs_1.default.writeFileSync(envPath, header + content);
|
|
817
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
|
|
818
|
+
}
|
|
573
819
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
|
|
820
|
+
else {
|
|
821
|
+
const createEnv = nonInteractive ? true : await prompts.confirm({
|
|
822
|
+
message: `Create ${ENV_FILENAME} template?`,
|
|
823
|
+
default: true,
|
|
824
|
+
});
|
|
825
|
+
if (createEnv) {
|
|
826
|
+
const template = generateEnvTemplate(projectName, config);
|
|
827
|
+
fs_1.default.writeFileSync(envPath, template);
|
|
828
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
|
|
829
|
+
}
|
|
585
830
|
}
|
|
586
831
|
}
|
|
587
832
|
// Add to .gitignore
|