genbox 1.0.14 → 1.0.16
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 +101 -128
- package/dist/commands/init.js +636 -107
- package/dist/commands/profiles.js +49 -13
- package/dist/commands/scan.js +238 -3
- 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
|
@@ -51,6 +51,217 @@ 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
|
+
* Extract unique HTTP URLs from frontend app sections in .env.genbox content
|
|
56
|
+
* Catches both localhost URLs and Docker internal service references
|
|
57
|
+
*/
|
|
58
|
+
function extractFrontendHttpUrls(envContent, frontendApps) {
|
|
59
|
+
const serviceUrls = new Map();
|
|
60
|
+
// Parse sections
|
|
61
|
+
const sections = new Map();
|
|
62
|
+
let currentSection = 'GLOBAL';
|
|
63
|
+
let currentContent = [];
|
|
64
|
+
for (const line of envContent.split('\n')) {
|
|
65
|
+
const sectionMatch = line.match(/^# === ([^=]+) ===$/);
|
|
66
|
+
if (sectionMatch) {
|
|
67
|
+
if (currentContent.length > 0) {
|
|
68
|
+
sections.set(currentSection, currentContent.join('\n'));
|
|
69
|
+
}
|
|
70
|
+
currentSection = sectionMatch[1].trim();
|
|
71
|
+
currentContent = [];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
currentContent.push(line);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (currentContent.length > 0) {
|
|
78
|
+
sections.set(currentSection, currentContent.join('\n'));
|
|
79
|
+
}
|
|
80
|
+
// Only process frontend app sections
|
|
81
|
+
for (const appName of frontendApps) {
|
|
82
|
+
const appSection = sections.get(appName);
|
|
83
|
+
if (!appSection)
|
|
84
|
+
continue;
|
|
85
|
+
// Find all HTTP URLs
|
|
86
|
+
// Match: VAR_NAME="http://host:port/path" or VAR_NAME=http://host:port/path
|
|
87
|
+
const urlRegex = /^([A-Z_][A-Z0-9_]*)=["']?(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?[^"'\s]*)["']?/gm;
|
|
88
|
+
let match;
|
|
89
|
+
while ((match = urlRegex.exec(appSection)) !== null) {
|
|
90
|
+
const varName = match[1];
|
|
91
|
+
const fullUrl = match[2];
|
|
92
|
+
// Extract hostname from URL
|
|
93
|
+
const hostMatch = fullUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)/);
|
|
94
|
+
if (!hostMatch)
|
|
95
|
+
continue;
|
|
96
|
+
const hostname = hostMatch[1];
|
|
97
|
+
// Only include URLs that look like local/development URLs:
|
|
98
|
+
// - localhost
|
|
99
|
+
// - Single-word hosts (Docker internal names like "auth", "gateway", "redis")
|
|
100
|
+
// - IP addresses
|
|
101
|
+
const isLocalUrl = hostname === 'localhost' ||
|
|
102
|
+
!hostname.includes('.') || // No dots = likely Docker internal name
|
|
103
|
+
/^\d+\.\d+\.\d+\.\d+$/.test(hostname); // IP address
|
|
104
|
+
if (!isLocalUrl) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Extract base URL (protocol + host + port)
|
|
108
|
+
const baseMatch = fullUrl.match(/^(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?)/);
|
|
109
|
+
if (!baseMatch)
|
|
110
|
+
continue;
|
|
111
|
+
const baseUrl = baseMatch[1];
|
|
112
|
+
// Group by base URL
|
|
113
|
+
if (!serviceUrls.has(baseUrl)) {
|
|
114
|
+
serviceUrls.set(baseUrl, { urls: new Set(), vars: [] });
|
|
115
|
+
}
|
|
116
|
+
serviceUrls.get(baseUrl).urls.add(fullUrl);
|
|
117
|
+
if (!serviceUrls.get(baseUrl).vars.includes(varName)) {
|
|
118
|
+
serviceUrls.get(baseUrl).vars.push(varName);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return serviceUrls;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Determine service name from URL (hostname or port)
|
|
126
|
+
* Generates variable prefix from hostname or port number
|
|
127
|
+
*/
|
|
128
|
+
function getServiceNameFromUrl(baseUrl) {
|
|
129
|
+
// Parse the URL
|
|
130
|
+
const urlMatch = baseUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)(?::(\d+))?/);
|
|
131
|
+
if (!urlMatch) {
|
|
132
|
+
return { name: 'unknown', varPrefix: 'UNKNOWN', description: 'Unknown Service' };
|
|
133
|
+
}
|
|
134
|
+
const hostname = urlMatch[1];
|
|
135
|
+
const port = urlMatch[2] ? parseInt(urlMatch[2]) : undefined;
|
|
136
|
+
// Generate from hostname if not localhost
|
|
137
|
+
if (hostname !== 'localhost') {
|
|
138
|
+
const varPrefix = hostname.toUpperCase().replace(/-/g, '_');
|
|
139
|
+
return {
|
|
140
|
+
name: hostname,
|
|
141
|
+
varPrefix: `${varPrefix}`,
|
|
142
|
+
description: `${hostname} service`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Generate from port for localhost URLs
|
|
146
|
+
if (port) {
|
|
147
|
+
return {
|
|
148
|
+
name: `port-${port}`,
|
|
149
|
+
varPrefix: `PORT_${port}`,
|
|
150
|
+
description: `localhost:${port}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { name: 'unknown', varPrefix: 'UNKNOWN', description: 'Unknown Service' };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Prompt user for staging URLs for each unique service URL found in frontend apps
|
|
157
|
+
*/
|
|
158
|
+
async function promptForStagingUrls(serviceUrls, existingStagingApiUrl) {
|
|
159
|
+
const mappings = [];
|
|
160
|
+
if (serviceUrls.size === 0) {
|
|
161
|
+
return mappings;
|
|
162
|
+
}
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log(chalk_1.default.blue('=== Frontend Service URL Configuration ==='));
|
|
165
|
+
console.log(chalk_1.default.dim('Frontend apps have service URLs that need staging equivalents.'));
|
|
166
|
+
console.log(chalk_1.default.dim('This enables profiles like admin-quick to connect to staging backend.'));
|
|
167
|
+
console.log('');
|
|
168
|
+
// Sort by port number for consistent ordering
|
|
169
|
+
const sortedServices = Array.from(serviceUrls.entries()).sort((a, b) => {
|
|
170
|
+
const portA = parseInt(a[0].match(/:(\d+)/)?.[1] || '0');
|
|
171
|
+
const portB = parseInt(b[0].match(/:(\d+)/)?.[1] || '0');
|
|
172
|
+
return portA - portB;
|
|
173
|
+
});
|
|
174
|
+
for (const [baseUrl, { urls, vars }] of sortedServices) {
|
|
175
|
+
const serviceInfo = getServiceNameFromUrl(baseUrl);
|
|
176
|
+
// Show what URLs use this service
|
|
177
|
+
console.log(chalk_1.default.cyan(`${serviceInfo.description} (${baseUrl})`));
|
|
178
|
+
console.log(chalk_1.default.dim(` Used by: ${vars.slice(0, 3).join(', ')}${vars.length > 3 ? ` +${vars.length - 3} more` : ''}`));
|
|
179
|
+
// For gateway, suggest existing staging URL if available
|
|
180
|
+
let defaultUrl = '';
|
|
181
|
+
if ((serviceInfo.name === 'gateway' || baseUrl.includes(':3050')) && existingStagingApiUrl) {
|
|
182
|
+
defaultUrl = existingStagingApiUrl;
|
|
183
|
+
}
|
|
184
|
+
const stagingUrl = await prompts.input({
|
|
185
|
+
message: ` Staging URL (leave empty to skip):`,
|
|
186
|
+
default: defaultUrl,
|
|
187
|
+
});
|
|
188
|
+
mappings.push({
|
|
189
|
+
varName: `${serviceInfo.varPrefix}_URL`,
|
|
190
|
+
localUrl: baseUrl,
|
|
191
|
+
stagingUrl: stagingUrl || undefined,
|
|
192
|
+
description: serviceInfo.description,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return mappings;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Transform .env.genbox content to use expandable variables
|
|
199
|
+
*/
|
|
200
|
+
function transformEnvWithVariables(envContent, mappings, frontendApps) {
|
|
201
|
+
if (mappings.length === 0) {
|
|
202
|
+
return envContent;
|
|
203
|
+
}
|
|
204
|
+
let result = envContent;
|
|
205
|
+
// Build GLOBAL section additions
|
|
206
|
+
const globalAdditions = [
|
|
207
|
+
'',
|
|
208
|
+
'# Service URL Configuration',
|
|
209
|
+
'# These expand based on profile: ${GATEWAY_URL} → LOCAL or STAGING value',
|
|
210
|
+
];
|
|
211
|
+
for (const mapping of mappings) {
|
|
212
|
+
globalAdditions.push(`LOCAL_${mapping.varName}=${mapping.localUrl}`);
|
|
213
|
+
if (mapping.stagingUrl) {
|
|
214
|
+
globalAdditions.push(`STAGING_${mapping.varName}=${mapping.stagingUrl}`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
globalAdditions.push(`# STAGING_${mapping.varName}=https://your-staging-url.com`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
globalAdditions.push('');
|
|
221
|
+
// Insert after GLOBAL section header and existing content
|
|
222
|
+
const globalInsertPoint = result.indexOf('# === GLOBAL ===');
|
|
223
|
+
if (globalInsertPoint !== -1) {
|
|
224
|
+
// Find the next section or end of GLOBAL
|
|
225
|
+
const nextSectionMatch = result.substring(globalInsertPoint + 20).match(/\n# === [^=]+ ===/);
|
|
226
|
+
const insertAt = nextSectionMatch
|
|
227
|
+
? globalInsertPoint + 20 + nextSectionMatch.index
|
|
228
|
+
: result.length;
|
|
229
|
+
result = result.substring(0, insertAt) + globalAdditions.join('\n') + result.substring(insertAt);
|
|
230
|
+
}
|
|
231
|
+
// Replace localhost URLs with variable syntax in frontend app sections
|
|
232
|
+
// Parse sections again after modification
|
|
233
|
+
const lines = result.split('\n');
|
|
234
|
+
const transformedLines = [];
|
|
235
|
+
let currentSection = 'GLOBAL';
|
|
236
|
+
let inFrontendSection = false;
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
const sectionMatch = line.match(/^# === ([^=]+) ===$/);
|
|
239
|
+
if (sectionMatch) {
|
|
240
|
+
currentSection = sectionMatch[1].trim();
|
|
241
|
+
inFrontendSection = frontendApps.includes(currentSection);
|
|
242
|
+
transformedLines.push(line);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (inFrontendSection && line.includes('http://localhost:')) {
|
|
246
|
+
// Check if this line matches any of our mappings
|
|
247
|
+
let transformedLine = line;
|
|
248
|
+
for (const mapping of mappings) {
|
|
249
|
+
if (line.includes(mapping.localUrl)) {
|
|
250
|
+
// Replace the full URL, preserving any path suffix
|
|
251
|
+
const urlPattern = new RegExp(mapping.localUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(/[^"\'\\s]*)?', 'g');
|
|
252
|
+
transformedLine = transformedLine.replace(urlPattern, (match, pathSuffix) => {
|
|
253
|
+
return `\${${mapping.varName}}${pathSuffix || ''}`;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
transformedLines.push(transformedLine);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
transformedLines.push(line);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return transformedLines.join('\n');
|
|
264
|
+
}
|
|
54
265
|
/**
|
|
55
266
|
* Detect git repositories in app directories (for multi-repo workspaces)
|
|
56
267
|
*/
|
|
@@ -162,6 +373,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
162
373
|
.option('-y, --yes', 'Use defaults without prompting')
|
|
163
374
|
.option('--exclude <dirs>', 'Comma-separated directories to exclude')
|
|
164
375
|
.option('--name <name>', 'Project name (for non-interactive mode)')
|
|
376
|
+
.option('--from-scan', 'Initialize from existing .genbox/detected.yaml (created by genbox scan)')
|
|
165
377
|
.action(async (options) => {
|
|
166
378
|
try {
|
|
167
379
|
const configPath = path_1.default.join(process.cwd(), CONFIG_FILENAME);
|
|
@@ -192,11 +404,34 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
192
404
|
if (options.exclude) {
|
|
193
405
|
exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
|
|
194
406
|
}
|
|
195
|
-
|
|
196
|
-
const spinner = (0, ora_1.default)('Scanning project...').start();
|
|
407
|
+
let scan;
|
|
197
408
|
const scanner = new scanner_1.ProjectScanner();
|
|
198
|
-
|
|
199
|
-
|
|
409
|
+
// If --from-scan is specified, load from detected.yaml
|
|
410
|
+
if (options.fromScan) {
|
|
411
|
+
const detectedPath = path_1.default.join(process.cwd(), '.genbox', 'detected.yaml');
|
|
412
|
+
if (!fs_1.default.existsSync(detectedPath)) {
|
|
413
|
+
console.log(chalk_1.default.red('No .genbox/detected.yaml found. Run "genbox scan" first.'));
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
const spinner = (0, ora_1.default)('Loading detected configuration...').start();
|
|
417
|
+
try {
|
|
418
|
+
const content = fs_1.default.readFileSync(detectedPath, 'utf8');
|
|
419
|
+
const detected = yaml.load(content);
|
|
420
|
+
scan = convertDetectedToScan(detected);
|
|
421
|
+
spinner.succeed('Loaded from detected.yaml');
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
spinner.fail('Failed to load detected.yaml');
|
|
425
|
+
console.error(chalk_1.default.red(String(err)));
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// Scan project first (skip scripts initially)
|
|
431
|
+
const spinner = (0, ora_1.default)('Scanning project...').start();
|
|
432
|
+
scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
|
|
433
|
+
spinner.succeed('Project scanned');
|
|
434
|
+
}
|
|
200
435
|
// Display scan results
|
|
201
436
|
console.log('');
|
|
202
437
|
console.log(chalk_1.default.bold('Detected:'));
|
|
@@ -213,9 +448,10 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
213
448
|
console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
|
|
214
449
|
}
|
|
215
450
|
// For multi-repo: show apps and let user select which to include
|
|
451
|
+
// When using --from-scan, skip app selection - use exactly what's in detected.yaml
|
|
216
452
|
const isMultiRepoStructure = scan.structure.type === 'hybrid';
|
|
217
453
|
let selectedApps = scan.apps;
|
|
218
|
-
if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive) {
|
|
454
|
+
if (isMultiRepoStructure && scan.apps.length > 0 && !nonInteractive && !options.fromScan) {
|
|
219
455
|
console.log('');
|
|
220
456
|
console.log(chalk_1.default.blue('=== Apps Detected ==='));
|
|
221
457
|
const appChoices = scan.apps.map(app => ({
|
|
@@ -232,6 +468,14 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
232
468
|
// Update scan with filtered apps
|
|
233
469
|
scan = { ...scan, apps: selectedApps };
|
|
234
470
|
}
|
|
471
|
+
else if (options.fromScan && scan.apps.length > 0) {
|
|
472
|
+
// When using --from-scan, show what was loaded and use it directly
|
|
473
|
+
console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} from detected.yaml`);
|
|
474
|
+
for (const app of scan.apps) {
|
|
475
|
+
console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
|
|
476
|
+
}
|
|
477
|
+
console.log(chalk_1.default.dim('\n (Edit .genbox/detected.yaml to change app selection)'));
|
|
478
|
+
}
|
|
235
479
|
else if (scan.apps.length > 0) {
|
|
236
480
|
console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
|
|
237
481
|
for (const app of scan.apps.slice(0, 5)) {
|
|
@@ -248,17 +492,17 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
248
492
|
console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
|
|
249
493
|
}
|
|
250
494
|
console.log('');
|
|
251
|
-
// Get project name
|
|
252
|
-
const projectName = nonInteractive
|
|
495
|
+
// Get project name (use scan value when --from-scan)
|
|
496
|
+
const projectName = (nonInteractive || options.fromScan)
|
|
253
497
|
? (options.name || scan.projectName)
|
|
254
498
|
: await prompts.input({
|
|
255
499
|
message: 'Project name:',
|
|
256
500
|
default: scan.projectName,
|
|
257
501
|
});
|
|
258
|
-
// Determine if workspace or single project
|
|
502
|
+
// Determine if workspace or single project (auto-detect when --from-scan)
|
|
259
503
|
let isWorkspace = options.workspace;
|
|
260
504
|
if (!isWorkspace && (scan.structure.type.startsWith('monorepo') || scan.structure.type === 'hybrid')) {
|
|
261
|
-
if (nonInteractive) {
|
|
505
|
+
if (nonInteractive || options.fromScan) {
|
|
262
506
|
isWorkspace = true; // Default to workspace for monorepos
|
|
263
507
|
}
|
|
264
508
|
else {
|
|
@@ -271,25 +515,25 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
271
515
|
// Generate initial config (v2 format)
|
|
272
516
|
const generator = new config_generator_1.ConfigGenerator();
|
|
273
517
|
const generated = generator.generate(scan);
|
|
274
|
-
// Convert to
|
|
275
|
-
const
|
|
518
|
+
// Convert to v4 format (declarative-first architecture)
|
|
519
|
+
const v4Config = convertV2ToV4(generated.config, scan);
|
|
276
520
|
// Update project name
|
|
277
|
-
|
|
278
|
-
// Ask about profiles
|
|
521
|
+
v4Config.project.name = projectName;
|
|
522
|
+
// Ask about profiles (skip prompt when using --from-scan)
|
|
279
523
|
let createProfiles = true;
|
|
280
|
-
if (!nonInteractive) {
|
|
524
|
+
if (!nonInteractive && !options.fromScan) {
|
|
281
525
|
createProfiles = await prompts.confirm({
|
|
282
526
|
message: 'Create predefined profiles for common scenarios?',
|
|
283
527
|
default: true,
|
|
284
528
|
});
|
|
285
529
|
}
|
|
286
530
|
if (createProfiles) {
|
|
287
|
-
|
|
288
|
-
? createDefaultProfilesSync(scan,
|
|
289
|
-
: await createDefaultProfiles(scan,
|
|
531
|
+
v4Config.profiles = (nonInteractive || options.fromScan)
|
|
532
|
+
? createDefaultProfilesSync(scan, v4Config)
|
|
533
|
+
: await createDefaultProfiles(scan, v4Config);
|
|
290
534
|
}
|
|
291
|
-
// Get server size
|
|
292
|
-
const serverSize = nonInteractive
|
|
535
|
+
// Get server size (use defaults when --from-scan)
|
|
536
|
+
const serverSize = (nonInteractive || options.fromScan)
|
|
293
537
|
? generated.config.system.size
|
|
294
538
|
: await prompts.select({
|
|
295
539
|
message: 'Default server size:',
|
|
@@ -301,13 +545,99 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
301
545
|
],
|
|
302
546
|
default: generated.config.system.size,
|
|
303
547
|
});
|
|
304
|
-
if (!
|
|
305
|
-
|
|
548
|
+
if (!v4Config.defaults) {
|
|
549
|
+
v4Config.defaults = {};
|
|
306
550
|
}
|
|
307
|
-
|
|
551
|
+
v4Config.defaults.size = serverSize;
|
|
308
552
|
// Git repository setup - different handling for multi-repo vs single-repo
|
|
553
|
+
// When using --from-scan, skip git selection and use what's in detected.yaml
|
|
309
554
|
const isMultiRepo = isMultiRepoStructure;
|
|
310
|
-
if (
|
|
555
|
+
if (options.fromScan) {
|
|
556
|
+
// When using --from-scan, extract git repos from detected.yaml apps
|
|
557
|
+
const detectedPath = path_1.default.join(process.cwd(), '.genbox', 'detected.yaml');
|
|
558
|
+
let detectedConfig = null;
|
|
559
|
+
try {
|
|
560
|
+
const content = fs_1.default.readFileSync(detectedPath, 'utf8');
|
|
561
|
+
detectedConfig = yaml.load(content);
|
|
562
|
+
}
|
|
563
|
+
catch { }
|
|
564
|
+
// Check for per-app git repos first (multi-repo workspace)
|
|
565
|
+
const appsWithGit = detectedConfig
|
|
566
|
+
? Object.entries(detectedConfig.apps).filter(([, app]) => app.git)
|
|
567
|
+
: [];
|
|
568
|
+
if (appsWithGit.length > 0) {
|
|
569
|
+
// Multi-repo: use per-app git repos
|
|
570
|
+
console.log('');
|
|
571
|
+
console.log(chalk_1.default.blue('=== Git Repositories (from detected.yaml) ==='));
|
|
572
|
+
console.log(chalk_1.default.dim(`Found ${appsWithGit.length} repositories`));
|
|
573
|
+
v4Config.repos = {};
|
|
574
|
+
let hasHttpsRepos = false;
|
|
575
|
+
for (const [appName, app] of appsWithGit) {
|
|
576
|
+
const git = app.git;
|
|
577
|
+
v4Config.repos[appName] = {
|
|
578
|
+
url: git.remote,
|
|
579
|
+
path: `/home/dev/${projectName}/${app.path}`,
|
|
580
|
+
branch: git.branch !== 'main' && git.branch !== 'master' ? git.branch : undefined,
|
|
581
|
+
auth: git.type === 'ssh' ? 'ssh' : 'token',
|
|
582
|
+
};
|
|
583
|
+
console.log(` ${chalk_1.default.cyan(appName)}: ${git.remote}`);
|
|
584
|
+
if (git.type === 'https') {
|
|
585
|
+
hasHttpsRepos = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Prompt for GIT_TOKEN if any HTTPS repos are found
|
|
589
|
+
if (hasHttpsRepos && !nonInteractive) {
|
|
590
|
+
console.log('');
|
|
591
|
+
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
592
|
+
console.log('');
|
|
593
|
+
console.log(chalk_1.default.dim(' To create a token:'));
|
|
594
|
+
console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
|
|
595
|
+
console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
|
|
596
|
+
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
597
|
+
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
598
|
+
console.log('');
|
|
599
|
+
let gitToken = await prompts.password({
|
|
600
|
+
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
601
|
+
});
|
|
602
|
+
if (gitToken) {
|
|
603
|
+
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
604
|
+
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
605
|
+
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
606
|
+
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.log(chalk_1.default.dim(' Skipped - add GIT_TOKEN to .env.genbox later if needed'));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if (scan.git) {
|
|
614
|
+
// Single repo or monorepo with root git
|
|
615
|
+
const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
|
|
616
|
+
v4Config.repos = {
|
|
617
|
+
[repoName]: {
|
|
618
|
+
url: scan.git.remote,
|
|
619
|
+
path: `/home/dev/${projectName}`,
|
|
620
|
+
auth: scan.git.type === 'ssh' ? 'ssh' : 'token',
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
console.log(chalk_1.default.dim(` Git: Using ${repoName} from detected.yaml`));
|
|
624
|
+
// Prompt for GIT_TOKEN if HTTPS
|
|
625
|
+
if (scan.git.type === 'https' && !nonInteractive) {
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
628
|
+
let gitToken = await prompts.password({
|
|
629
|
+
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
630
|
+
});
|
|
631
|
+
if (gitToken) {
|
|
632
|
+
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
633
|
+
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
634
|
+
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
635
|
+
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else if (isMultiRepo) {
|
|
311
641
|
// Multi-repo workspace: detect git repos in app directories
|
|
312
642
|
const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
|
|
313
643
|
if (appGitRepos.length > 0 && !nonInteractive) {
|
|
@@ -324,11 +654,11 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
324
654
|
choices: repoChoices,
|
|
325
655
|
});
|
|
326
656
|
if (selectedRepos.length > 0) {
|
|
327
|
-
|
|
657
|
+
v4Config.repos = {};
|
|
328
658
|
let hasHttpsRepos = false;
|
|
329
659
|
for (const repoName of selectedRepos) {
|
|
330
660
|
const repo = appGitRepos.find(r => r.appName === repoName);
|
|
331
|
-
|
|
661
|
+
v4Config.repos[repo.appName] = {
|
|
332
662
|
url: repo.remote,
|
|
333
663
|
path: `/home/dev/${projectName}/${repo.appPath}`,
|
|
334
664
|
branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
|
|
@@ -349,10 +679,12 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
349
679
|
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
350
680
|
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
351
681
|
console.log('');
|
|
352
|
-
|
|
682
|
+
let gitToken = await prompts.password({
|
|
353
683
|
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
354
684
|
});
|
|
355
685
|
if (gitToken) {
|
|
686
|
+
// Strip any "GIT_TOKEN=" prefix if user pasted the whole line
|
|
687
|
+
gitToken = gitToken.replace(/^GIT_TOKEN=/i, '');
|
|
356
688
|
envVarsToAdd['GIT_TOKEN'] = gitToken;
|
|
357
689
|
console.log(chalk_1.default.green('✓ GIT_TOKEN will be added to .env.genbox'));
|
|
358
690
|
}
|
|
@@ -364,9 +696,9 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
364
696
|
}
|
|
365
697
|
else if (appGitRepos.length > 0) {
|
|
366
698
|
// Non-interactive: include all repos
|
|
367
|
-
|
|
699
|
+
v4Config.repos = {};
|
|
368
700
|
for (const repo of appGitRepos) {
|
|
369
|
-
|
|
701
|
+
v4Config.repos[repo.appName] = {
|
|
370
702
|
url: repo.remote,
|
|
371
703
|
path: `/home/dev/${projectName}/${repo.appPath}`,
|
|
372
704
|
auth: repo.type === 'ssh' ? 'ssh' : 'token',
|
|
@@ -378,7 +710,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
378
710
|
// Single repo or monorepo with root git
|
|
379
711
|
if (nonInteractive) {
|
|
380
712
|
const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
|
|
381
|
-
|
|
713
|
+
v4Config.repos = {
|
|
382
714
|
[repoName]: {
|
|
383
715
|
url: scan.git.remote,
|
|
384
716
|
path: `/home/dev/${repoName}`,
|
|
@@ -389,10 +721,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
389
721
|
else {
|
|
390
722
|
const gitConfig = await setupGitAuth(scan.git, projectName);
|
|
391
723
|
if (gitConfig.repos) {
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
if (gitConfig.git_auth) {
|
|
395
|
-
v3Config.git_auth = gitConfig.git_auth;
|
|
724
|
+
v4Config.repos = gitConfig.repos;
|
|
396
725
|
}
|
|
397
726
|
}
|
|
398
727
|
}
|
|
@@ -415,7 +744,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
415
744
|
},
|
|
416
745
|
});
|
|
417
746
|
const repoName = path_1.default.basename(repoUrl, '.git');
|
|
418
|
-
|
|
747
|
+
v4Config.repos = {
|
|
419
748
|
[repoName]: {
|
|
420
749
|
url: repoUrl,
|
|
421
750
|
path: `/home/dev/${repoName}`,
|
|
@@ -424,15 +753,16 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
424
753
|
};
|
|
425
754
|
}
|
|
426
755
|
}
|
|
427
|
-
// Environment configuration (skip in non-interactive mode)
|
|
756
|
+
// Environment configuration (skip only in non-interactive mode)
|
|
757
|
+
// For --from-scan, we still want to prompt for environments since they're required for genbox to work
|
|
428
758
|
if (!nonInteractive) {
|
|
429
|
-
const envConfig = await setupEnvironments(scan,
|
|
759
|
+
const envConfig = await setupEnvironments(scan, v4Config, isMultiRepo);
|
|
430
760
|
if (envConfig) {
|
|
431
|
-
|
|
761
|
+
v4Config.environments = envConfig;
|
|
432
762
|
}
|
|
433
763
|
}
|
|
434
|
-
// Script selection - always show multi-select UI (skip in non-interactive mode)
|
|
435
|
-
if (!nonInteractive) {
|
|
764
|
+
// Script selection - always show multi-select UI (skip in non-interactive mode and --from-scan)
|
|
765
|
+
if (!nonInteractive && !options.fromScan) {
|
|
436
766
|
// Scan for scripts
|
|
437
767
|
const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
|
|
438
768
|
const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
|
|
@@ -463,7 +793,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
463
793
|
choices: scriptChoices,
|
|
464
794
|
});
|
|
465
795
|
if (selectedScripts.length > 0) {
|
|
466
|
-
|
|
796
|
+
v4Config.scripts = fullScan.scripts
|
|
467
797
|
.filter(s => selectedScripts.includes(s.path))
|
|
468
798
|
.map(s => ({
|
|
469
799
|
name: s.name,
|
|
@@ -477,7 +807,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
477
807
|
}
|
|
478
808
|
}
|
|
479
809
|
// Save configuration
|
|
480
|
-
const yamlContent = yaml.dump(
|
|
810
|
+
const yamlContent = yaml.dump(v4Config, {
|
|
481
811
|
lineWidth: 120,
|
|
482
812
|
noRefs: true,
|
|
483
813
|
quotingType: '"',
|
|
@@ -487,19 +817,29 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
487
817
|
// Add API URLs from environments to envVarsToAdd
|
|
488
818
|
// Always add LOCAL_API_URL for local development
|
|
489
819
|
envVarsToAdd['LOCAL_API_URL'] = 'http://localhost:3050';
|
|
490
|
-
if (
|
|
491
|
-
for (const [envName, envConfig] of Object.entries(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
envConfig.api?.gateway;
|
|
820
|
+
if (v4Config.environments) {
|
|
821
|
+
for (const [envName, envConfig] of Object.entries(v4Config.environments)) {
|
|
822
|
+
// v4 format: urls.api contains the API URL
|
|
823
|
+
const apiUrl = envConfig.urls?.api || envConfig.urls?.gateway;
|
|
495
824
|
if (apiUrl) {
|
|
496
825
|
const varName = `${envName.toUpperCase()}_API_URL`;
|
|
497
826
|
envVarsToAdd[varName] = apiUrl;
|
|
498
827
|
}
|
|
499
828
|
}
|
|
500
829
|
}
|
|
830
|
+
// Load detected service URLs if using --from-scan
|
|
831
|
+
let detectedServiceUrls;
|
|
832
|
+
if (options.fromScan) {
|
|
833
|
+
const detectedPath = path_1.default.join(process.cwd(), '.genbox', 'detected.yaml');
|
|
834
|
+
try {
|
|
835
|
+
const content = fs_1.default.readFileSync(detectedPath, 'utf8');
|
|
836
|
+
const detectedConfig = yaml.load(content);
|
|
837
|
+
detectedServiceUrls = detectedConfig.service_urls;
|
|
838
|
+
}
|
|
839
|
+
catch { }
|
|
840
|
+
}
|
|
501
841
|
// Generate .env.genbox
|
|
502
|
-
await setupEnvFile(projectName,
|
|
842
|
+
await setupEnvFile(projectName, v4Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting, detectedServiceUrls);
|
|
503
843
|
// Show warnings
|
|
504
844
|
if (generated.warnings.length > 0) {
|
|
505
845
|
console.log('');
|
|
@@ -509,16 +849,15 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
509
849
|
}
|
|
510
850
|
}
|
|
511
851
|
// Show API URL guidance if environments are configured
|
|
512
|
-
if (
|
|
852
|
+
if (v4Config.environments && Object.keys(v4Config.environments).length > 0) {
|
|
513
853
|
console.log('');
|
|
514
854
|
console.log(chalk_1.default.blue('=== API URL Configuration ==='));
|
|
515
855
|
console.log(chalk_1.default.dim('The following API URLs were added to .env.genbox:'));
|
|
516
856
|
console.log('');
|
|
517
857
|
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;
|
|
858
|
+
for (const [envName, envConfig] of Object.entries(v4Config.environments)) {
|
|
859
|
+
// v4 format: urls.api contains the API URL
|
|
860
|
+
const apiUrl = envConfig.urls?.api || envConfig.urls?.gateway;
|
|
522
861
|
if (apiUrl) {
|
|
523
862
|
const varName = `${envName.toUpperCase()}_API_URL`;
|
|
524
863
|
console.log(chalk_1.default.dim(` ${varName}=${apiUrl}`));
|
|
@@ -531,9 +870,9 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
531
870
|
console.log(chalk_1.default.cyan(' NEXT_PUBLIC_API_URL=${API_URL}'));
|
|
532
871
|
console.log('');
|
|
533
872
|
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
|
|
873
|
+
console.log(chalk_1.default.dim(' • default_connection: staging → uses STAGING_API_URL'));
|
|
874
|
+
console.log(chalk_1.default.dim(' • default_connection: production → uses PRODUCTION_API_URL'));
|
|
875
|
+
console.log(chalk_1.default.dim(' • local/no default_connection → uses LOCAL_API_URL'));
|
|
537
876
|
}
|
|
538
877
|
// Next steps
|
|
539
878
|
console.log('');
|
|
@@ -566,13 +905,13 @@ function createProfilesFromScan(scan) {
|
|
|
566
905
|
const frontendApps = scan.apps.filter(a => a.type === 'frontend');
|
|
567
906
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
568
907
|
const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
|
|
569
|
-
// Quick UI profiles for each frontend
|
|
908
|
+
// Quick UI profiles for each frontend (v4: use default_connection instead of connect_to)
|
|
570
909
|
for (const frontend of frontendApps.slice(0, 3)) {
|
|
571
910
|
profiles[`${frontend.name}-quick`] = {
|
|
572
911
|
description: `${frontend.name} only, connected to staging`,
|
|
573
912
|
size: 'small',
|
|
574
913
|
apps: [frontend.name],
|
|
575
|
-
|
|
914
|
+
default_connection: 'staging',
|
|
576
915
|
};
|
|
577
916
|
}
|
|
578
917
|
// Full local development
|
|
@@ -599,13 +938,13 @@ function createProfilesFromScan(scan) {
|
|
|
599
938
|
},
|
|
600
939
|
};
|
|
601
940
|
}
|
|
602
|
-
// All frontends + staging
|
|
941
|
+
// All frontends + staging (v4: use default_connection)
|
|
603
942
|
if (frontendApps.length > 1) {
|
|
604
943
|
profiles['frontends-staging'] = {
|
|
605
944
|
description: 'All frontends with staging backend',
|
|
606
945
|
size: 'medium',
|
|
607
946
|
apps: frontendApps.map(a => a.name),
|
|
608
|
-
|
|
947
|
+
default_connection: 'staging',
|
|
609
948
|
};
|
|
610
949
|
}
|
|
611
950
|
// Full stack
|
|
@@ -639,14 +978,12 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
639
978
|
default: 'token',
|
|
640
979
|
});
|
|
641
980
|
let repoUrl = gitInfo.remote;
|
|
642
|
-
let git_auth;
|
|
643
981
|
if (authMethod === 'token') {
|
|
644
982
|
// Convert SSH to HTTPS if needed
|
|
645
983
|
if (gitInfo.type === 'ssh') {
|
|
646
984
|
repoUrl = (0, scan_1.sshToHttps)(gitInfo.remote);
|
|
647
985
|
console.log(chalk_1.default.dim(` Will use HTTPS: ${repoUrl}`));
|
|
648
986
|
}
|
|
649
|
-
git_auth = { method: 'token' };
|
|
650
987
|
// Show token setup instructions
|
|
651
988
|
console.log('');
|
|
652
989
|
console.log(chalk_1.default.yellow(' Add your token to .env.genbox:'));
|
|
@@ -677,7 +1014,6 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
677
1014
|
});
|
|
678
1015
|
}
|
|
679
1016
|
}
|
|
680
|
-
git_auth = { method: 'ssh', ssh_key_path: sshKeyPath };
|
|
681
1017
|
console.log('');
|
|
682
1018
|
console.log(chalk_1.default.yellow(' Add your SSH key to .env.genbox:'));
|
|
683
1019
|
console.log(chalk_1.default.white(` GIT_SSH_KEY="$(cat ${sshKeyPath})"`));
|
|
@@ -691,11 +1027,10 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
691
1027
|
auth: authMethod === 'public' ? undefined : authMethod,
|
|
692
1028
|
},
|
|
693
1029
|
},
|
|
694
|
-
git_auth,
|
|
695
1030
|
};
|
|
696
1031
|
}
|
|
697
1032
|
/**
|
|
698
|
-
* Setup staging/production environments
|
|
1033
|
+
* Setup staging/production environments (v4 format)
|
|
699
1034
|
*/
|
|
700
1035
|
async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
701
1036
|
const setupEnvs = await prompts.confirm({
|
|
@@ -716,22 +1051,23 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
716
1051
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
717
1052
|
if (backendApps.length > 0) {
|
|
718
1053
|
console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
|
|
719
|
-
const
|
|
1054
|
+
const urls = {};
|
|
720
1055
|
for (const app of backendApps) {
|
|
721
1056
|
const url = await prompts.input({
|
|
722
1057
|
message: ` ${app.name} staging URL (leave empty to skip):`,
|
|
723
1058
|
default: '',
|
|
724
1059
|
});
|
|
725
1060
|
if (url) {
|
|
726
|
-
|
|
1061
|
+
urls[app.name] = url;
|
|
727
1062
|
}
|
|
728
1063
|
}
|
|
729
|
-
if (Object.keys(
|
|
1064
|
+
if (Object.keys(urls).length > 0) {
|
|
1065
|
+
// Add database URLs
|
|
1066
|
+
urls['mongodb'] = '${STAGING_MONGODB_URL}';
|
|
1067
|
+
urls['redis'] = '${STAGING_REDIS_URL}';
|
|
730
1068
|
environments.staging = {
|
|
731
1069
|
description: 'Staging environment',
|
|
732
|
-
|
|
733
|
-
mongodb: { url: '${STAGING_MONGODB_URL}' },
|
|
734
|
-
redis: { url: '${STAGING_REDIS_URL}' },
|
|
1070
|
+
urls,
|
|
735
1071
|
};
|
|
736
1072
|
}
|
|
737
1073
|
}
|
|
@@ -744,9 +1080,11 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
744
1080
|
if (stagingApiUrl) {
|
|
745
1081
|
environments.staging = {
|
|
746
1082
|
description: 'Staging environment',
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1083
|
+
urls: {
|
|
1084
|
+
api: stagingApiUrl,
|
|
1085
|
+
mongodb: '${STAGING_MONGODB_URL}',
|
|
1086
|
+
redis: '${STAGING_REDIS_URL}',
|
|
1087
|
+
},
|
|
750
1088
|
};
|
|
751
1089
|
}
|
|
752
1090
|
}
|
|
@@ -760,9 +1098,11 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
760
1098
|
if (stagingApiUrl) {
|
|
761
1099
|
environments.staging = {
|
|
762
1100
|
description: 'Staging environment',
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1101
|
+
urls: {
|
|
1102
|
+
api: stagingApiUrl,
|
|
1103
|
+
mongodb: '${STAGING_MONGODB_URL}',
|
|
1104
|
+
redis: '${STAGING_REDIS_URL}',
|
|
1105
|
+
},
|
|
766
1106
|
};
|
|
767
1107
|
}
|
|
768
1108
|
}
|
|
@@ -775,23 +1115,24 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
775
1115
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
776
1116
|
if (backendApps.length > 0) {
|
|
777
1117
|
console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
|
|
778
|
-
const
|
|
1118
|
+
const prodUrls = {};
|
|
779
1119
|
for (const app of backendApps) {
|
|
780
1120
|
const url = await prompts.input({
|
|
781
1121
|
message: ` ${app.name} production URL:`,
|
|
782
1122
|
default: '',
|
|
783
1123
|
});
|
|
784
1124
|
if (url) {
|
|
785
|
-
|
|
1125
|
+
prodUrls[app.name] = url;
|
|
786
1126
|
}
|
|
787
1127
|
}
|
|
788
|
-
if (Object.keys(
|
|
1128
|
+
if (Object.keys(prodUrls).length > 0) {
|
|
1129
|
+
prodUrls['mongodb'] = '${PROD_MONGODB_URL}';
|
|
789
1130
|
environments.production = {
|
|
790
1131
|
description: 'Production (use with caution)',
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
url: '${PROD_MONGODB_URL}',
|
|
1132
|
+
urls: prodUrls,
|
|
1133
|
+
safety: {
|
|
794
1134
|
read_only: true,
|
|
1135
|
+
require_confirmation: true,
|
|
795
1136
|
},
|
|
796
1137
|
};
|
|
797
1138
|
}
|
|
@@ -805,10 +1146,13 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
805
1146
|
if (prodApiUrl) {
|
|
806
1147
|
environments.production = {
|
|
807
1148
|
description: 'Production (use with caution)',
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1149
|
+
urls: {
|
|
1150
|
+
api: prodApiUrl,
|
|
1151
|
+
mongodb: '${PROD_MONGODB_URL}',
|
|
1152
|
+
},
|
|
1153
|
+
safety: {
|
|
811
1154
|
read_only: true,
|
|
1155
|
+
require_confirmation: true,
|
|
812
1156
|
},
|
|
813
1157
|
};
|
|
814
1158
|
}
|
|
@@ -819,7 +1163,7 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
819
1163
|
/**
|
|
820
1164
|
* Setup .env.genbox file with segregated app sections
|
|
821
1165
|
*/
|
|
822
|
-
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}, overwriteExisting = false) {
|
|
1166
|
+
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}, overwriteExisting = false, detectedServiceUrls) {
|
|
823
1167
|
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
824
1168
|
// If overwriting, delete existing file
|
|
825
1169
|
if (fs_1.default.existsSync(envPath)) {
|
|
@@ -932,6 +1276,52 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
|
|
|
932
1276
|
}
|
|
933
1277
|
}
|
|
934
1278
|
}
|
|
1279
|
+
// Identify frontend apps for URL transformation
|
|
1280
|
+
const frontendApps = scan?.apps
|
|
1281
|
+
.filter(a => a.type === 'frontend')
|
|
1282
|
+
.map(a => a.name) || [];
|
|
1283
|
+
// Also get frontend apps from config if available
|
|
1284
|
+
if (config.apps) {
|
|
1285
|
+
for (const [name, app] of Object.entries(config.apps)) {
|
|
1286
|
+
if (app.type === 'frontend' && !frontendApps.includes(name)) {
|
|
1287
|
+
frontendApps.push(name);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (frontendApps.length > 0 && !nonInteractive) {
|
|
1292
|
+
// Use service URLs from detected.yaml if available (preferred)
|
|
1293
|
+
// Otherwise fall back to scanning the collected env content
|
|
1294
|
+
let serviceUrls;
|
|
1295
|
+
if (detectedServiceUrls && detectedServiceUrls.length > 0) {
|
|
1296
|
+
// Convert detected service URLs to the Map format
|
|
1297
|
+
serviceUrls = new Map();
|
|
1298
|
+
for (const svc of detectedServiceUrls) {
|
|
1299
|
+
serviceUrls.set(svc.base_url, {
|
|
1300
|
+
urls: new Set([svc.base_url]),
|
|
1301
|
+
vars: svc.used_by,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
console.log('');
|
|
1305
|
+
console.log(chalk_1.default.dim(`Found ${detectedServiceUrls.length} service URL(s) from detected.yaml`));
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
// Fall back to extracting from collected env content
|
|
1309
|
+
serviceUrls = extractFrontendHttpUrls(segregatedContent, frontendApps);
|
|
1310
|
+
}
|
|
1311
|
+
if (serviceUrls.size > 0) {
|
|
1312
|
+
// Get existing staging API URL if configured
|
|
1313
|
+
const existingStagingApiUrl = extraEnvVars['STAGING_API_URL'] ||
|
|
1314
|
+
(config.environments?.staging?.urls?.api);
|
|
1315
|
+
// Prompt for staging equivalents
|
|
1316
|
+
const urlMappings = await promptForStagingUrls(serviceUrls, existingStagingApiUrl);
|
|
1317
|
+
// Transform content with expandable variables
|
|
1318
|
+
if (urlMappings.length > 0) {
|
|
1319
|
+
segregatedContent = transformEnvWithVariables(segregatedContent, urlMappings, frontendApps);
|
|
1320
|
+
console.log('');
|
|
1321
|
+
console.log(chalk_1.default.green(`✓ Configured ${urlMappings.length} service URL(s) for staging support`));
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
935
1325
|
// Add END marker
|
|
936
1326
|
segregatedContent += `# === END ===\n`;
|
|
937
1327
|
// Write the file
|
|
@@ -1031,10 +1421,10 @@ function generateEnvTemplate(projectName, config) {
|
|
|
1031
1421
|
return lines.join('\n');
|
|
1032
1422
|
}
|
|
1033
1423
|
/**
|
|
1034
|
-
* Convert GenboxConfigV2 to
|
|
1424
|
+
* Convert GenboxConfigV2 to GenboxConfigV4 format
|
|
1035
1425
|
*/
|
|
1036
|
-
function
|
|
1037
|
-
// Convert services to apps
|
|
1426
|
+
function convertV2ToV4(v2Config, scan) {
|
|
1427
|
+
// Convert services to apps (v4 format)
|
|
1038
1428
|
const apps = {};
|
|
1039
1429
|
for (const [name, service] of Object.entries(v2Config.services || {})) {
|
|
1040
1430
|
const appConfig = {
|
|
@@ -1046,10 +1436,13 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1046
1436
|
if (service.framework) {
|
|
1047
1437
|
appConfig.framework = service.framework;
|
|
1048
1438
|
}
|
|
1049
|
-
//
|
|
1439
|
+
// Convert requires to connects_to (v4 format)
|
|
1050
1440
|
if (service.dependsOn?.length) {
|
|
1051
|
-
appConfig.
|
|
1052
|
-
acc[dep] =
|
|
1441
|
+
appConfig.connects_to = service.dependsOn.reduce((acc, dep) => {
|
|
1442
|
+
acc[dep] = {
|
|
1443
|
+
mode: 'local',
|
|
1444
|
+
required: true,
|
|
1445
|
+
};
|
|
1053
1446
|
return acc;
|
|
1054
1447
|
}, {});
|
|
1055
1448
|
}
|
|
@@ -1070,11 +1463,11 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1070
1463
|
}
|
|
1071
1464
|
apps[name] = appConfig;
|
|
1072
1465
|
}
|
|
1073
|
-
// Convert infrastructure
|
|
1074
|
-
const
|
|
1466
|
+
// Convert infrastructure to provides (v4 format)
|
|
1467
|
+
const provides = {};
|
|
1075
1468
|
if (v2Config.infrastructure?.databases) {
|
|
1076
1469
|
for (const db of v2Config.infrastructure.databases) {
|
|
1077
|
-
|
|
1470
|
+
provides[db.container || db.type] = {
|
|
1078
1471
|
type: 'database',
|
|
1079
1472
|
image: `${db.type}:latest`,
|
|
1080
1473
|
port: db.port,
|
|
@@ -1083,7 +1476,7 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1083
1476
|
}
|
|
1084
1477
|
if (v2Config.infrastructure?.caches) {
|
|
1085
1478
|
for (const cache of v2Config.infrastructure.caches) {
|
|
1086
|
-
|
|
1479
|
+
provides[cache.container || cache.type] = {
|
|
1087
1480
|
type: 'cache',
|
|
1088
1481
|
image: `${cache.type}:latest`,
|
|
1089
1482
|
port: cache.port,
|
|
@@ -1092,15 +1485,15 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1092
1485
|
}
|
|
1093
1486
|
if (v2Config.infrastructure?.queues) {
|
|
1094
1487
|
for (const queue of v2Config.infrastructure.queues) {
|
|
1095
|
-
|
|
1488
|
+
provides[queue.container || queue.type] = {
|
|
1096
1489
|
type: 'queue',
|
|
1097
1490
|
image: `${queue.type}:latest`,
|
|
1098
1491
|
port: queue.port,
|
|
1099
|
-
|
|
1492
|
+
additional_ports: queue.managementPort ? { management: queue.managementPort } : undefined,
|
|
1100
1493
|
};
|
|
1101
1494
|
}
|
|
1102
1495
|
}
|
|
1103
|
-
// Convert repos
|
|
1496
|
+
// Convert repos (v4 format)
|
|
1104
1497
|
const repos = {};
|
|
1105
1498
|
for (const [name, repo] of Object.entries(v2Config.repos || {})) {
|
|
1106
1499
|
repos[name] = {
|
|
@@ -1110,18 +1503,25 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1110
1503
|
auth: repo.auth,
|
|
1111
1504
|
};
|
|
1112
1505
|
}
|
|
1113
|
-
//
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1506
|
+
// Map structure type to v4
|
|
1507
|
+
const structureMap = {
|
|
1508
|
+
'single-app': 'single-app',
|
|
1509
|
+
'monorepo-pnpm': 'monorepo',
|
|
1510
|
+
'monorepo-yarn': 'monorepo',
|
|
1511
|
+
'monorepo-npm': 'monorepo',
|
|
1512
|
+
'microservices': 'microservices',
|
|
1513
|
+
'hybrid': 'hybrid',
|
|
1514
|
+
};
|
|
1515
|
+
// Build v4 config
|
|
1516
|
+
const v4Config = {
|
|
1517
|
+
version: 4,
|
|
1116
1518
|
project: {
|
|
1117
1519
|
name: v2Config.project.name,
|
|
1118
|
-
structure: v2Config.project.structure
|
|
1119
|
-
v2Config.project.structure.startsWith('monorepo') ? 'monorepo' :
|
|
1120
|
-
v2Config.project.structure,
|
|
1520
|
+
structure: structureMap[v2Config.project.structure] || 'single-app',
|
|
1121
1521
|
description: v2Config.project.description,
|
|
1122
1522
|
},
|
|
1123
1523
|
apps,
|
|
1124
|
-
|
|
1524
|
+
provides: Object.keys(provides).length > 0 ? provides : undefined,
|
|
1125
1525
|
repos: Object.keys(repos).length > 0 ? repos : undefined,
|
|
1126
1526
|
defaults: {
|
|
1127
1527
|
size: v2Config.system.size,
|
|
@@ -1131,6 +1531,135 @@ function convertV2ToV3(v2Config, scan) {
|
|
|
1131
1531
|
post_start: v2Config.hooks.postStart,
|
|
1132
1532
|
pre_start: v2Config.hooks.preStart,
|
|
1133
1533
|
} : undefined,
|
|
1534
|
+
strict: {
|
|
1535
|
+
enabled: true,
|
|
1536
|
+
allow_detect: true,
|
|
1537
|
+
warnings_as_errors: false,
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
return v4Config;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Convert DetectedConfig (from detected.yaml) to ProjectScan format
|
|
1544
|
+
* This allows --from-scan to use the same code paths as fresh scanning
|
|
1545
|
+
*/
|
|
1546
|
+
function convertDetectedToScan(detected) {
|
|
1547
|
+
// Convert structure type
|
|
1548
|
+
const structureTypeMap = {
|
|
1549
|
+
'single-app': 'single-app',
|
|
1550
|
+
'monorepo': 'monorepo-pnpm',
|
|
1551
|
+
'workspace': 'hybrid',
|
|
1552
|
+
'microservices': 'microservices',
|
|
1553
|
+
'hybrid': 'hybrid',
|
|
1554
|
+
};
|
|
1555
|
+
// Convert apps
|
|
1556
|
+
const apps = [];
|
|
1557
|
+
for (const [name, app] of Object.entries(detected.apps || {})) {
|
|
1558
|
+
// Map detected type to scanner type
|
|
1559
|
+
const typeMap = {
|
|
1560
|
+
'frontend': 'frontend',
|
|
1561
|
+
'backend': 'backend',
|
|
1562
|
+
'worker': 'worker',
|
|
1563
|
+
'gateway': 'gateway',
|
|
1564
|
+
'library': 'library',
|
|
1565
|
+
};
|
|
1566
|
+
apps.push({
|
|
1567
|
+
name,
|
|
1568
|
+
path: app.path,
|
|
1569
|
+
type: typeMap[app.type || 'library'] || 'library',
|
|
1570
|
+
framework: app.framework,
|
|
1571
|
+
port: app.port,
|
|
1572
|
+
dependencies: app.dependencies,
|
|
1573
|
+
scripts: app.commands ? {
|
|
1574
|
+
dev: app.commands.dev || '',
|
|
1575
|
+
build: app.commands.build || '',
|
|
1576
|
+
start: app.commands.start || '',
|
|
1577
|
+
} : {},
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
// Convert runtimes
|
|
1581
|
+
const runtimes = detected.runtimes.map(r => ({
|
|
1582
|
+
language: r.language,
|
|
1583
|
+
version: r.version,
|
|
1584
|
+
versionSource: r.version_source,
|
|
1585
|
+
packageManager: r.package_manager,
|
|
1586
|
+
lockfile: r.lockfile,
|
|
1587
|
+
}));
|
|
1588
|
+
// Convert infrastructure to compose analysis
|
|
1589
|
+
let compose = null;
|
|
1590
|
+
if (detected.infrastructure && detected.infrastructure.length > 0) {
|
|
1591
|
+
compose = {
|
|
1592
|
+
files: ['docker-compose.yml'],
|
|
1593
|
+
applications: [],
|
|
1594
|
+
databases: detected.infrastructure
|
|
1595
|
+
.filter(i => i.type === 'database')
|
|
1596
|
+
.map(i => ({
|
|
1597
|
+
name: i.name,
|
|
1598
|
+
image: i.image,
|
|
1599
|
+
ports: [{ host: i.port, container: i.port }],
|
|
1600
|
+
environment: {},
|
|
1601
|
+
dependsOn: [],
|
|
1602
|
+
volumes: [],
|
|
1603
|
+
})),
|
|
1604
|
+
caches: detected.infrastructure
|
|
1605
|
+
.filter(i => i.type === 'cache')
|
|
1606
|
+
.map(i => ({
|
|
1607
|
+
name: i.name,
|
|
1608
|
+
image: i.image,
|
|
1609
|
+
ports: [{ host: i.port, container: i.port }],
|
|
1610
|
+
environment: {},
|
|
1611
|
+
dependsOn: [],
|
|
1612
|
+
volumes: [],
|
|
1613
|
+
})),
|
|
1614
|
+
queues: detected.infrastructure
|
|
1615
|
+
.filter(i => i.type === 'queue')
|
|
1616
|
+
.map(i => ({
|
|
1617
|
+
name: i.name,
|
|
1618
|
+
image: i.image,
|
|
1619
|
+
ports: [{ host: i.port, container: i.port }],
|
|
1620
|
+
environment: {},
|
|
1621
|
+
dependsOn: [],
|
|
1622
|
+
volumes: [],
|
|
1623
|
+
})),
|
|
1624
|
+
infrastructure: [],
|
|
1625
|
+
portMap: new Map(),
|
|
1626
|
+
dependencyGraph: new Map(),
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
// Convert git
|
|
1630
|
+
const git = detected.git ? {
|
|
1631
|
+
remote: detected.git.remote || '',
|
|
1632
|
+
type: detected.git.type || 'https',
|
|
1633
|
+
provider: (detected.git.provider || 'other'),
|
|
1634
|
+
branch: detected.git.branch || 'main',
|
|
1635
|
+
} : undefined;
|
|
1636
|
+
// Convert scripts
|
|
1637
|
+
const scripts = (detected.scripts || []).map(s => ({
|
|
1638
|
+
name: s.name,
|
|
1639
|
+
path: s.path,
|
|
1640
|
+
stage: s.stage,
|
|
1641
|
+
isExecutable: s.executable,
|
|
1642
|
+
}));
|
|
1643
|
+
return {
|
|
1644
|
+
projectName: path_1.default.basename(detected._meta.scanned_root),
|
|
1645
|
+
root: detected._meta.scanned_root,
|
|
1646
|
+
structure: {
|
|
1647
|
+
type: structureTypeMap[detected.structure.type] || 'single-app',
|
|
1648
|
+
confidence: detected.structure.confidence,
|
|
1649
|
+
indicators: detected.structure.indicators,
|
|
1650
|
+
},
|
|
1651
|
+
runtimes,
|
|
1652
|
+
frameworks: [], // Not stored in detected.yaml
|
|
1653
|
+
compose,
|
|
1654
|
+
apps,
|
|
1655
|
+
envAnalysis: {
|
|
1656
|
+
required: [],
|
|
1657
|
+
optional: [],
|
|
1658
|
+
secrets: [],
|
|
1659
|
+
references: [],
|
|
1660
|
+
sources: [],
|
|
1661
|
+
},
|
|
1662
|
+
git,
|
|
1663
|
+
scripts,
|
|
1134
1664
|
};
|
|
1135
|
-
return v3Config;
|
|
1136
1665
|
}
|