genbox 1.0.91 → 1.0.93
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/create.js +36 -3
- package/dist/commands/init.js +284 -50
- package/dist/scanner/compose-parser.js +27 -6
- package/dist/scanner/env-analyzer.js +2 -2
- package/dist/scanner/index.js +3 -0
- package/dist/utils/env-parser.js +56 -0
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -796,11 +796,15 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
796
796
|
const appPath = config.apps[app.name]?.path || app.name;
|
|
797
797
|
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
798
798
|
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
799
|
+
// Get runner type to determine infrastructure URL prefix (docker vs host)
|
|
800
|
+
const runner = config.apps[app.name]?.runner || 'pm2';
|
|
801
|
+
const infraUrlMap = (0, utils_1.buildInfraUrlMap)(envVarsFromFile, runner);
|
|
802
|
+
const combinedUrlMap = { ...serviceUrlMap, ...infraUrlMap };
|
|
799
803
|
const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
|
|
800
804
|
if (servicesSections.length > 0) {
|
|
801
805
|
for (const serviceSectionName of servicesSections) {
|
|
802
806
|
const serviceName = serviceSectionName.split('/')[1];
|
|
803
|
-
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName,
|
|
807
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, combinedUrlMap);
|
|
804
808
|
const stagingName = `${app.name}-${serviceName}.env`;
|
|
805
809
|
const targetPath = `${repoPath}/apps/${serviceName}/.env`;
|
|
806
810
|
files.push({
|
|
@@ -812,7 +816,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
812
816
|
}
|
|
813
817
|
}
|
|
814
818
|
else {
|
|
815
|
-
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name,
|
|
819
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, combinedUrlMap);
|
|
816
820
|
files.push({
|
|
817
821
|
path: `/home/dev/.env-staging/${app.name}.env`,
|
|
818
822
|
content: appEnvContent,
|
|
@@ -934,10 +938,39 @@ function generateSetupScript(resolved, config, envFilesToMove = []) {
|
|
|
934
938
|
lines.push(' \' "$file" | grep -v "^$" | head -n -1');
|
|
935
939
|
lines.push(' }');
|
|
936
940
|
lines.push('');
|
|
941
|
+
lines.push(' # Function to expand infrastructure URL placeholders based on runner type');
|
|
942
|
+
lines.push(' # Docker apps use DOCKER_* values, PM2/host apps use HOST_* values');
|
|
943
|
+
lines.push(' expand_infra_urls() {');
|
|
944
|
+
lines.push(' local envfile="$1"');
|
|
945
|
+
lines.push(' local runner="$2"');
|
|
946
|
+
lines.push(' if [ "$runner" = "docker" ]; then');
|
|
947
|
+
lines.push(' # For docker apps, replace ${VAR} with DOCKER_VAR value');
|
|
948
|
+
lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
|
|
949
|
+
lines.push(' docker_val=$(grep "^DOCKER_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
|
|
950
|
+
lines.push(' if [ -n "$docker_val" ]; then');
|
|
951
|
+
lines.push(' sed -i "s|\\${${var}}|${docker_val}|g" "$envfile"');
|
|
952
|
+
lines.push(' fi');
|
|
953
|
+
lines.push(' done');
|
|
954
|
+
lines.push(' else');
|
|
955
|
+
lines.push(' # For PM2/host apps, replace ${VAR} with HOST_VAR value');
|
|
956
|
+
lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
|
|
957
|
+
lines.push(' host_val=$(grep "^HOST_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
|
|
958
|
+
lines.push(' if [ -n "$host_val" ]; then');
|
|
959
|
+
lines.push(' sed -i "s|\\${${var}}|${host_val}|g" "$envfile"');
|
|
960
|
+
lines.push(' fi');
|
|
961
|
+
lines.push(' done');
|
|
962
|
+
lines.push(' fi');
|
|
963
|
+
lines.push(' }');
|
|
964
|
+
lines.push('');
|
|
937
965
|
for (const { stagingName, targetPath } of envFilesToMove) {
|
|
938
|
-
|
|
966
|
+
// Determine runner type for this app
|
|
967
|
+
// stagingName could be "appName" or "appName/serviceName" for service sections
|
|
968
|
+
const appName = stagingName.includes('/') ? stagingName.split('/')[0] : stagingName;
|
|
969
|
+
const runner = config.apps[appName]?.runner || 'pm2';
|
|
970
|
+
lines.push(` # Create .env for ${stagingName} (runner: ${runner})`);
|
|
939
971
|
lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
|
|
940
972
|
lines.push(` extract_section "${stagingName}" /home/dev/.env.genbox > "${targetPath}"`);
|
|
973
|
+
lines.push(` expand_infra_urls "${targetPath}" "${runner}"`);
|
|
941
974
|
lines.push(` echo " Created ${targetPath}"`);
|
|
942
975
|
}
|
|
943
976
|
lines.push('');
|
package/dist/commands/init.js
CHANGED
|
@@ -151,9 +151,32 @@ function convertScanToDetected(scan, root) {
|
|
|
151
151
|
})),
|
|
152
152
|
apps: {},
|
|
153
153
|
};
|
|
154
|
-
//
|
|
154
|
+
// Collect docker app build contexts first (docker takes precedence over package.json)
|
|
155
|
+
const dockerBuildContexts = new Set();
|
|
156
|
+
if (scan.compose?.applications) {
|
|
157
|
+
for (const dockerApp of scan.compose.applications) {
|
|
158
|
+
if (dockerApp.build?.context) {
|
|
159
|
+
// Normalize the path for comparison
|
|
160
|
+
const ctx = dockerApp.build.context.replace(/^\.?\/?/, '').replace(/\/$/, '');
|
|
161
|
+
dockerBuildContexts.add(ctx);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Map to store git info for directories with docker apps (to preserve repo info)
|
|
166
|
+
const dockerDirGitInfo = new Map();
|
|
167
|
+
// Convert apps from package.json (skip if docker app builds from same directory)
|
|
155
168
|
const isMultiRepo = scan.structure.type === 'hybrid';
|
|
156
169
|
for (const app of scan.apps) {
|
|
170
|
+
// Skip if there's a docker app building from this directory
|
|
171
|
+
const normalizedPath = app.path.replace(/^\.?\/?/, '').replace(/\/$/, '');
|
|
172
|
+
if (dockerBuildContexts.has(normalizedPath)) {
|
|
173
|
+
// Save git info for this directory so docker apps can use it
|
|
174
|
+
const gitInfo = detectGitForDirectory(path_1.default.join(root, app.path));
|
|
175
|
+
if (gitInfo) {
|
|
176
|
+
dockerDirGitInfo.set(normalizedPath, gitInfo);
|
|
177
|
+
}
|
|
178
|
+
continue; // Docker app takes precedence
|
|
179
|
+
}
|
|
157
180
|
const mappedType = mapAppType(app.type);
|
|
158
181
|
let appGit = undefined;
|
|
159
182
|
if (isMultiRepo) {
|
|
@@ -179,7 +202,9 @@ function convertScanToDetected(scan, root) {
|
|
|
179
202
|
git: appGit,
|
|
180
203
|
};
|
|
181
204
|
}
|
|
182
|
-
//
|
|
205
|
+
// Store dockerDirGitInfo for use when adding docker apps
|
|
206
|
+
const _dockerDirGitInfo = dockerDirGitInfo;
|
|
207
|
+
// Convert infrastructure (keep all, no deduplication - user selects by source file)
|
|
183
208
|
if (scan.compose) {
|
|
184
209
|
detected.infrastructure = [];
|
|
185
210
|
for (const db of scan.compose.databases || []) {
|
|
@@ -188,7 +213,7 @@ function convertScanToDetected(scan, root) {
|
|
|
188
213
|
type: 'database',
|
|
189
214
|
image: db.image || 'unknown',
|
|
190
215
|
port: db.ports?.[0]?.host || 0,
|
|
191
|
-
source: 'docker-compose.yml',
|
|
216
|
+
source: db.sourceFile || 'docker-compose.yml',
|
|
192
217
|
});
|
|
193
218
|
}
|
|
194
219
|
for (const cache of scan.compose.caches || []) {
|
|
@@ -197,7 +222,7 @@ function convertScanToDetected(scan, root) {
|
|
|
197
222
|
type: 'cache',
|
|
198
223
|
image: cache.image || 'unknown',
|
|
199
224
|
port: cache.ports?.[0]?.host || 0,
|
|
200
|
-
source: 'docker-compose.yml',
|
|
225
|
+
source: cache.sourceFile || 'docker-compose.yml',
|
|
201
226
|
});
|
|
202
227
|
}
|
|
203
228
|
for (const queue of scan.compose.queues || []) {
|
|
@@ -206,21 +231,56 @@ function convertScanToDetected(scan, root) {
|
|
|
206
231
|
type: 'queue',
|
|
207
232
|
image: queue.image || 'unknown',
|
|
208
233
|
port: queue.ports?.[0]?.host || 0,
|
|
209
|
-
source: 'docker-compose.yml',
|
|
234
|
+
source: queue.sourceFile || 'docker-compose.yml',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Add other infrastructure (vector DBs, search engines, etc.)
|
|
238
|
+
for (const infra of scan.compose.infrastructure || []) {
|
|
239
|
+
detected.infrastructure.push({
|
|
240
|
+
name: infra.name,
|
|
241
|
+
type: 'other',
|
|
242
|
+
image: infra.image || 'unknown',
|
|
243
|
+
port: infra.ports?.[0]?.host || 0,
|
|
244
|
+
source: infra.sourceFile || 'docker-compose.yml',
|
|
210
245
|
});
|
|
211
246
|
}
|
|
212
247
|
// Add Docker application services as apps with runner: 'docker'
|
|
213
248
|
if (scan.compose.applications && scan.compose.applications.length > 0) {
|
|
249
|
+
// Collect infrastructure service keys (name@source) to exclude from apps
|
|
250
|
+
// Only exclude if same name AND same source file
|
|
251
|
+
const infraKeys = new Set([
|
|
252
|
+
...(scan.compose.databases || []).map(d => `${d.name}@${d.sourceFile || ''}`),
|
|
253
|
+
...(scan.compose.caches || []).map(c => `${c.name}@${c.sourceFile || ''}`),
|
|
254
|
+
...(scan.compose.queues || []).map(q => `${q.name}@${q.sourceFile || ''}`),
|
|
255
|
+
...(scan.compose.infrastructure || []).map(i => `${i.name}@${i.sourceFile || ''}`),
|
|
256
|
+
]);
|
|
214
257
|
for (const dockerApp of scan.compose.applications) {
|
|
215
|
-
|
|
216
|
-
|
|
258
|
+
const infraCheckKey = `${dockerApp.name}@${dockerApp.sourceFile || ''}`;
|
|
259
|
+
if (infraKeys.has(infraCheckKey))
|
|
260
|
+
continue; // Skip infrastructure services from same file
|
|
217
261
|
const { type: mappedType, reason: typeReason } = inferDockerAppType(dockerApp.name, dockerApp.build?.context, root);
|
|
218
|
-
|
|
262
|
+
// Use unique key if app name already exists (from different source)
|
|
263
|
+
let detectedAppKey = dockerApp.name;
|
|
264
|
+
if (detected.apps[detectedAppKey]) {
|
|
265
|
+
// Check if it's from a different source - if so, use source in key
|
|
266
|
+
const existingSource = detected.apps[detectedAppKey].source;
|
|
267
|
+
const newSource = dockerApp.sourceFile || 'docker-compose';
|
|
268
|
+
if (existingSource !== newSource) {
|
|
269
|
+
detectedAppKey = `${dockerApp.name}@${newSource}`;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
continue; // Same source, skip duplicate
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Look up git info for this docker app's build context
|
|
276
|
+
const buildContext = dockerApp.build?.context?.replace(/^\.?\/?/, '').replace(/\/$/, '') || '';
|
|
277
|
+
const dockerAppGit = _dockerDirGitInfo.get(buildContext);
|
|
278
|
+
detected.apps[detectedAppKey] = {
|
|
219
279
|
path: dockerApp.build?.context || '.',
|
|
220
280
|
type: mappedType,
|
|
221
281
|
type_reason: typeReason,
|
|
222
282
|
runner: 'docker',
|
|
223
|
-
runner_reason:
|
|
283
|
+
runner_reason: `defined in ${dockerApp.sourceFile || 'docker-compose.yml'}`,
|
|
224
284
|
docker: {
|
|
225
285
|
service: dockerApp.name,
|
|
226
286
|
build_context: dockerApp.build?.context,
|
|
@@ -228,7 +288,9 @@ function convertScanToDetected(scan, root) {
|
|
|
228
288
|
image: dockerApp.image,
|
|
229
289
|
},
|
|
230
290
|
port: dockerApp.ports?.[0]?.host,
|
|
231
|
-
port_source: 'docker-compose.yml ports
|
|
291
|
+
port_source: `${dockerApp.sourceFile || 'docker-compose.yml'} ports`,
|
|
292
|
+
source: dockerApp.sourceFile || 'docker-compose',
|
|
293
|
+
git: dockerAppGit,
|
|
232
294
|
};
|
|
233
295
|
}
|
|
234
296
|
}
|
|
@@ -293,74 +355,216 @@ function saveDetectedConfig(rootDir, detected) {
|
|
|
293
355
|
// App Configuration
|
|
294
356
|
// =============================================================================
|
|
295
357
|
/**
|
|
296
|
-
*
|
|
358
|
+
* Get app source group for grouping purposes
|
|
359
|
+
*/
|
|
360
|
+
function getAppSourceGroup(name, app) {
|
|
361
|
+
// Docker apps - group by docker-compose source file
|
|
362
|
+
if (app.runner === 'docker' && app.source) {
|
|
363
|
+
return app.source;
|
|
364
|
+
}
|
|
365
|
+
// Use app path for grouping
|
|
366
|
+
if (app.path && app.path !== '.') {
|
|
367
|
+
return app.path;
|
|
368
|
+
}
|
|
369
|
+
return 'root';
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Interactive app selection (grouped by source)
|
|
297
373
|
*/
|
|
298
374
|
async function selectApps(detected) {
|
|
299
375
|
const appEntries = Object.entries(detected.apps);
|
|
300
376
|
if (appEntries.length === 0)
|
|
301
377
|
return detected;
|
|
378
|
+
// Group apps by source
|
|
379
|
+
const appsBySource = new Map();
|
|
380
|
+
for (const [name, app] of appEntries) {
|
|
381
|
+
const source = getAppSourceGroup(name, app);
|
|
382
|
+
if (!appsBySource.has(source)) {
|
|
383
|
+
appsBySource.set(source, []);
|
|
384
|
+
}
|
|
385
|
+
appsBySource.get(source).push([name, app]);
|
|
386
|
+
}
|
|
302
387
|
console.log('');
|
|
303
388
|
console.log(chalk_1.default.blue('=== Detected Apps ==='));
|
|
304
389
|
console.log('');
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
390
|
+
// Display grouped by source
|
|
391
|
+
for (const [source, apps] of appsBySource) {
|
|
392
|
+
console.log(chalk_1.default.dim(` ${source}:`));
|
|
393
|
+
for (const [name, app] of apps) {
|
|
394
|
+
const parts = [
|
|
395
|
+
chalk_1.default.cyan(name),
|
|
396
|
+
app.type ? `(${app.type})` : '',
|
|
397
|
+
app.framework ? `[${app.framework}]` : '',
|
|
398
|
+
app.port ? `port:${app.port}` : '',
|
|
399
|
+
app.runner ? chalk_1.default.dim(`runner:${app.runner}`) : '',
|
|
400
|
+
].filter(Boolean);
|
|
401
|
+
console.log(` ${parts.join(' ')}`);
|
|
316
402
|
}
|
|
317
403
|
}
|
|
318
404
|
console.log('');
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
405
|
+
// If only one source group, select individual apps
|
|
406
|
+
if (appsBySource.size === 1) {
|
|
407
|
+
const appChoices = appEntries.map(([name, app]) => ({
|
|
408
|
+
name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
|
|
409
|
+
value: name,
|
|
410
|
+
checked: app.type !== 'library',
|
|
411
|
+
}));
|
|
412
|
+
const selectedApps = await prompts.checkbox({
|
|
413
|
+
message: 'Select apps to include:',
|
|
414
|
+
choices: appChoices,
|
|
415
|
+
});
|
|
416
|
+
const filteredApps = {};
|
|
417
|
+
for (const appName of selectedApps) {
|
|
418
|
+
filteredApps[appName] = detected.apps[appName];
|
|
419
|
+
}
|
|
420
|
+
return { ...detected, apps: filteredApps };
|
|
421
|
+
}
|
|
422
|
+
// Multiple source groups - let user select by group first
|
|
423
|
+
const sourceChoices = Array.from(appsBySource.entries()).map(([source, apps]) => {
|
|
424
|
+
const hasLibraryOnly = apps.every(([, a]) => a.type === 'library');
|
|
425
|
+
const appNames = apps.map(([n]) => n).join(', ');
|
|
426
|
+
return {
|
|
427
|
+
name: `${source} (${appNames})`,
|
|
428
|
+
value: source,
|
|
429
|
+
checked: !hasLibraryOnly, // Uncheck if all are libraries
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
const selectedSources = await prompts.checkbox({
|
|
433
|
+
message: 'Select app groups to include:',
|
|
434
|
+
choices: sourceChoices,
|
|
327
435
|
});
|
|
436
|
+
// Collect all apps from selected sources (excluding libraries by default)
|
|
328
437
|
const filteredApps = {};
|
|
329
|
-
for (const
|
|
330
|
-
|
|
438
|
+
for (const source of selectedSources) {
|
|
439
|
+
const apps = appsBySource.get(source);
|
|
440
|
+
if (apps) {
|
|
441
|
+
for (const [name, app] of apps) {
|
|
442
|
+
// Include non-libraries by default from selected groups
|
|
443
|
+
if (app.type !== 'library') {
|
|
444
|
+
filteredApps[name] = app;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// If user selected groups, ask if they want to fine-tune individual apps
|
|
450
|
+
if (selectedSources.length > 0 && Object.keys(filteredApps).length > 0) {
|
|
451
|
+
const fineTune = await prompts.confirm({
|
|
452
|
+
message: `${Object.keys(filteredApps).length} apps selected. Fine-tune individual apps?`,
|
|
453
|
+
default: false,
|
|
454
|
+
});
|
|
455
|
+
if (fineTune) {
|
|
456
|
+
// Show all apps from selected groups for fine-tuning
|
|
457
|
+
const allAppsInGroups = [];
|
|
458
|
+
for (const source of selectedSources) {
|
|
459
|
+
const apps = appsBySource.get(source);
|
|
460
|
+
if (apps)
|
|
461
|
+
allAppsInGroups.push(...apps);
|
|
462
|
+
}
|
|
463
|
+
const appChoices = allAppsInGroups.map(([name, app]) => ({
|
|
464
|
+
name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
|
|
465
|
+
value: name,
|
|
466
|
+
checked: app.type !== 'library',
|
|
467
|
+
}));
|
|
468
|
+
const selectedApps = await prompts.checkbox({
|
|
469
|
+
message: 'Select individual apps:',
|
|
470
|
+
choices: appChoices,
|
|
471
|
+
});
|
|
472
|
+
const finalApps = {};
|
|
473
|
+
for (const appName of selectedApps) {
|
|
474
|
+
finalApps[appName] = detected.apps[appName];
|
|
475
|
+
}
|
|
476
|
+
return { ...detected, apps: finalApps };
|
|
477
|
+
}
|
|
331
478
|
}
|
|
332
479
|
return { ...detected, apps: filteredApps };
|
|
333
480
|
}
|
|
334
481
|
/**
|
|
335
|
-
* Interactive infrastructure selection
|
|
482
|
+
* Interactive infrastructure selection (grouped by source file)
|
|
336
483
|
*/
|
|
337
484
|
async function selectInfrastructure(detected) {
|
|
338
485
|
if (!detected.infrastructure || detected.infrastructure.length === 0) {
|
|
339
486
|
return detected;
|
|
340
487
|
}
|
|
488
|
+
// Group infrastructure by source file
|
|
489
|
+
const infraBySource = new Map();
|
|
490
|
+
for (const infra of detected.infrastructure) {
|
|
491
|
+
const source = infra.source || 'unknown';
|
|
492
|
+
if (!infraBySource.has(source)) {
|
|
493
|
+
infraBySource.set(source, []);
|
|
494
|
+
}
|
|
495
|
+
infraBySource.get(source).push(infra);
|
|
496
|
+
}
|
|
341
497
|
console.log('');
|
|
342
498
|
console.log(chalk_1.default.blue('=== Detected Infrastructure ==='));
|
|
343
499
|
console.log('');
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
500
|
+
// Display grouped by source
|
|
501
|
+
for (const [source, infraList] of infraBySource) {
|
|
502
|
+
console.log(chalk_1.default.dim(` ${source}:`));
|
|
503
|
+
for (const infra of infraList) {
|
|
504
|
+
const parts = [
|
|
505
|
+
chalk_1.default.cyan(infra.name),
|
|
506
|
+
`(${infra.type})`,
|
|
507
|
+
infra.image && infra.image !== 'unknown' ? `[${infra.image}]` : '',
|
|
508
|
+
infra.port ? `port:${infra.port}` : '',
|
|
509
|
+
].filter(Boolean);
|
|
510
|
+
console.log(` ${parts.join(' ')}`);
|
|
511
|
+
}
|
|
352
512
|
}
|
|
353
513
|
console.log('');
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
514
|
+
// If only one source file, select individual services
|
|
515
|
+
if (infraBySource.size === 1) {
|
|
516
|
+
const infraChoices = detected.infrastructure.map(infra => ({
|
|
517
|
+
name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''})`,
|
|
518
|
+
value: infra.name,
|
|
519
|
+
checked: true,
|
|
520
|
+
}));
|
|
521
|
+
const selectedInfra = await prompts.checkbox({
|
|
522
|
+
message: 'Select infrastructure to include:',
|
|
523
|
+
choices: infraChoices,
|
|
524
|
+
});
|
|
525
|
+
const filteredInfra = detected.infrastructure.filter(infra => selectedInfra.includes(infra.name));
|
|
526
|
+
return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
|
|
527
|
+
}
|
|
528
|
+
// Multiple source files - let user select by file first
|
|
529
|
+
const sourceChoices = Array.from(infraBySource.entries()).map(([source, infraList]) => {
|
|
530
|
+
const serviceNames = infraList.map(i => i.name).join(', ');
|
|
531
|
+
return {
|
|
532
|
+
name: `${source} (${serviceNames})`,
|
|
533
|
+
value: source,
|
|
534
|
+
checked: true,
|
|
535
|
+
};
|
|
362
536
|
});
|
|
363
|
-
const
|
|
537
|
+
const selectedSources = await prompts.checkbox({
|
|
538
|
+
message: 'Select infrastructure sources to include:',
|
|
539
|
+
choices: sourceChoices,
|
|
540
|
+
});
|
|
541
|
+
// Collect all infrastructure from selected sources
|
|
542
|
+
let filteredInfra = [];
|
|
543
|
+
for (const source of selectedSources) {
|
|
544
|
+
const infraList = infraBySource.get(source);
|
|
545
|
+
if (infraList) {
|
|
546
|
+
filteredInfra.push(...infraList);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// If user selected sources, ask if they want to fine-tune individual services
|
|
550
|
+
if (selectedSources.length > 0 && filteredInfra.length > 0) {
|
|
551
|
+
const fineTune = await prompts.confirm({
|
|
552
|
+
message: `${filteredInfra.length} infrastructure services selected. Fine-tune individual services?`,
|
|
553
|
+
default: false,
|
|
554
|
+
});
|
|
555
|
+
if (fineTune) {
|
|
556
|
+
const infraChoices = filteredInfra.map(infra => ({
|
|
557
|
+
name: `${infra.name} (${infra.type}${infra.image && infra.image !== 'unknown' ? `, ${infra.image}` : ''}) - ${infra.source}`,
|
|
558
|
+
value: `${infra.name}@${infra.source}`, // Unique key
|
|
559
|
+
checked: true,
|
|
560
|
+
}));
|
|
561
|
+
const selectedInfraKeys = await prompts.checkbox({
|
|
562
|
+
message: 'Select individual infrastructure services:',
|
|
563
|
+
choices: infraChoices,
|
|
564
|
+
});
|
|
565
|
+
filteredInfra = filteredInfra.filter(infra => selectedInfraKeys.includes(`${infra.name}@${infra.source}`));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
364
568
|
return { ...detected, infrastructure: filteredInfra.length > 0 ? filteredInfra : undefined };
|
|
365
569
|
}
|
|
366
570
|
/**
|
|
@@ -1217,6 +1421,36 @@ function generateEnvFile(projectName, detected, envVars, serviceUrlMappings) {
|
|
|
1217
1421
|
}
|
|
1218
1422
|
}
|
|
1219
1423
|
}
|
|
1424
|
+
// Add infrastructure URL mappings (HOST_ vs DOCKER_ prefixes)
|
|
1425
|
+
// These are used to differentiate between PM2 apps (use HOST_*) and Docker apps (use DOCKER_*)
|
|
1426
|
+
if (detected.infrastructure && detected.infrastructure.length > 0) {
|
|
1427
|
+
content += `\n# Infrastructure URL Configuration\n`;
|
|
1428
|
+
content += `# HOST_* = for PM2/host-based apps (uses localhost)\n`;
|
|
1429
|
+
content += `# DOCKER_* = for Docker apps (uses docker network hostnames)\n\n`;
|
|
1430
|
+
for (const infra of detected.infrastructure) {
|
|
1431
|
+
const name = infra.name.toLowerCase();
|
|
1432
|
+
const port = infra.port;
|
|
1433
|
+
if (infra.type === 'database' && infra.image?.includes('mongo')) {
|
|
1434
|
+
// MongoDB - use project name as database name
|
|
1435
|
+
const dbName = projectName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
1436
|
+
content += `HOST_MONGODB_URI=mongodb://localhost:${port}/${dbName}\n`;
|
|
1437
|
+
content += `DOCKER_MONGODB_URI=mongodb://${name}:27017/${dbName}\n`;
|
|
1438
|
+
content += `MONGODB_URI=\${MONGODB_URI}\n\n`;
|
|
1439
|
+
}
|
|
1440
|
+
else if (infra.type === 'cache' && infra.image?.includes('redis')) {
|
|
1441
|
+
// Redis
|
|
1442
|
+
content += `HOST_REDIS_URL=redis://localhost:${port}\n`;
|
|
1443
|
+
content += `DOCKER_REDIS_URL=redis://${name}:6379\n`;
|
|
1444
|
+
content += `REDIS_URL=\${REDIS_URL}\n\n`;
|
|
1445
|
+
}
|
|
1446
|
+
else if (infra.type === 'queue' && infra.image?.includes('rabbitmq')) {
|
|
1447
|
+
// RabbitMQ
|
|
1448
|
+
content += `HOST_RABBITMQ_URL=amqp://localhost:${port}\n`;
|
|
1449
|
+
content += `DOCKER_RABBITMQ_URL=amqp://${name}:5672\n`;
|
|
1450
|
+
content += `RABBITMQ_URL=\${RABBITMQ_URL}\n\n`;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1220
1454
|
// Add GIT_TOKEN placeholder if not present
|
|
1221
1455
|
if (!envVars['GIT_TOKEN']) {
|
|
1222
1456
|
content += `\n# Git authentication\n# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
|
|
@@ -93,6 +93,18 @@ const INFRA_PATTERNS = [
|
|
|
93
93
|
/^zipkin/i,
|
|
94
94
|
/^vault/i,
|
|
95
95
|
/^consul/i,
|
|
96
|
+
// Vector databases
|
|
97
|
+
/^qdrant/i,
|
|
98
|
+
/^weaviate/i,
|
|
99
|
+
/^semitechnologies\/weaviate/i,
|
|
100
|
+
/^pinecone/i,
|
|
101
|
+
/^milvus/i,
|
|
102
|
+
/^chroma/i,
|
|
103
|
+
// Logging
|
|
104
|
+
/^loki/i,
|
|
105
|
+
/^promtail/i,
|
|
106
|
+
/^fluentd/i,
|
|
107
|
+
/^logstash/i,
|
|
96
108
|
];
|
|
97
109
|
class ComposeParser {
|
|
98
110
|
/**
|
|
@@ -107,9 +119,13 @@ class ComposeParser {
|
|
|
107
119
|
for (const file of composeFiles) {
|
|
108
120
|
try {
|
|
109
121
|
const content = yaml.load(fs.readFileSync(file, 'utf8'));
|
|
122
|
+
// Get relative path for cleaner display
|
|
123
|
+
const relativeFile = path.relative(root, file);
|
|
124
|
+
// Get compose file's directory for resolving relative build contexts
|
|
125
|
+
const composeDir = path.dirname(file);
|
|
110
126
|
if (content && content.services) {
|
|
111
127
|
for (const [name, config] of Object.entries(content.services)) {
|
|
112
|
-
const service = this.normalizeService(name, config, root);
|
|
128
|
+
const service = this.normalizeService(name, config, root, composeDir, relativeFile);
|
|
113
129
|
allServices.push(service);
|
|
114
130
|
}
|
|
115
131
|
}
|
|
@@ -177,11 +193,11 @@ class ComposeParser {
|
|
|
177
193
|
findRecursively(root);
|
|
178
194
|
return files;
|
|
179
195
|
}
|
|
180
|
-
normalizeService(name, config, root) {
|
|
196
|
+
normalizeService(name, config, root, composeDir, sourceFile) {
|
|
181
197
|
return {
|
|
182
198
|
name,
|
|
183
199
|
image: config.image,
|
|
184
|
-
build: this.normalizeBuild(config.build, root),
|
|
200
|
+
build: this.normalizeBuild(config.build, root, composeDir),
|
|
185
201
|
ports: this.normalizePorts(config.ports),
|
|
186
202
|
environment: this.normalizeEnvironment(config.environment),
|
|
187
203
|
envFile: this.normalizeEnvFile(config.env_file),
|
|
@@ -190,20 +206,25 @@ class ComposeParser {
|
|
|
190
206
|
healthcheck: this.normalizeHealthcheck(config.healthcheck),
|
|
191
207
|
command: this.normalizeCommand(config.command),
|
|
192
208
|
labels: this.normalizeLabels(config.labels),
|
|
209
|
+
sourceFile,
|
|
193
210
|
};
|
|
194
211
|
}
|
|
195
|
-
normalizeBuild(build, root) {
|
|
212
|
+
normalizeBuild(build, root, composeDir) {
|
|
196
213
|
if (!build)
|
|
197
214
|
return undefined;
|
|
198
215
|
if (typeof build === 'string') {
|
|
216
|
+
// Resolve relative to compose file directory, then make relative to root
|
|
217
|
+
const absoluteContext = path.resolve(composeDir, build);
|
|
199
218
|
return {
|
|
200
|
-
context: path.
|
|
219
|
+
context: path.relative(root, absoluteContext),
|
|
201
220
|
};
|
|
202
221
|
}
|
|
203
222
|
if (typeof build === 'object') {
|
|
204
223
|
const b = build;
|
|
224
|
+
// Resolve relative to compose file directory, then make relative to root
|
|
225
|
+
const absoluteContext = path.resolve(composeDir, b.context || '.');
|
|
205
226
|
return {
|
|
206
|
-
context: path.
|
|
227
|
+
context: path.relative(root, absoluteContext),
|
|
207
228
|
dockerfile: b.dockerfile,
|
|
208
229
|
target: b.target,
|
|
209
230
|
};
|
|
@@ -323,8 +323,8 @@ class EnvAnalyzer {
|
|
|
323
323
|
return type;
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
-
// Check value format
|
|
327
|
-
if (value) {
|
|
326
|
+
// Check value format (ensure value is a string)
|
|
327
|
+
if (value && typeof value === 'string') {
|
|
328
328
|
if (/^(true|false)$/i.test(value))
|
|
329
329
|
return 'boolean';
|
|
330
330
|
if (/^\d+$/.test(value))
|
package/dist/scanner/index.js
CHANGED
|
@@ -133,6 +133,9 @@ class ProjectScanner {
|
|
|
133
133
|
type: 'backend', // Will be refined by framework detection
|
|
134
134
|
scripts,
|
|
135
135
|
});
|
|
136
|
+
// Also scan first-level subdirectories for additional apps
|
|
137
|
+
const subApps = await this.discoverMultiRepoApps(root, exclude);
|
|
138
|
+
apps.push(...subApps);
|
|
136
139
|
}
|
|
137
140
|
return apps;
|
|
138
141
|
}
|
package/dist/utils/env-parser.js
CHANGED
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.parseEnvGenboxSections = parseEnvGenboxSections;
|
|
8
8
|
exports.buildServiceUrlMap = buildServiceUrlMap;
|
|
9
9
|
exports.buildAppEnvContent = buildAppEnvContent;
|
|
10
|
+
exports.buildInfraUrlMap = buildInfraUrlMap;
|
|
10
11
|
exports.parseEnvVarsFromSection = parseEnvVarsFromSection;
|
|
11
12
|
/**
|
|
12
13
|
* Parse .env.genbox file into segregated sections
|
|
@@ -106,6 +107,61 @@ function buildAppEnvContent(sections, appName, serviceUrlMap) {
|
|
|
106
107
|
.trim();
|
|
107
108
|
return envContent;
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Infrastructure variable patterns that need HOST_ vs DOCKER_ prefixes
|
|
112
|
+
* These are database/cache/queue connection strings that differ between
|
|
113
|
+
* host-based apps (PM2) and containerized apps (Docker)
|
|
114
|
+
*/
|
|
115
|
+
const INFRA_VAR_PATTERNS = [
|
|
116
|
+
/^(MONGODB|MONGO)_URI$/i,
|
|
117
|
+
/^(MONGODB|MONGO)_URL$/i,
|
|
118
|
+
/^(REDIS)_URL$/i,
|
|
119
|
+
/^(REDIS)_URI$/i,
|
|
120
|
+
/^(RABBITMQ|RABBIT|AMQP)_URL$/i,
|
|
121
|
+
/^(RABBITMQ|RABBIT|AMQP)_URI$/i,
|
|
122
|
+
/^(POSTGRES|POSTGRESQL|PG)_URL$/i,
|
|
123
|
+
/^(POSTGRES|POSTGRESQL|PG)_URI$/i,
|
|
124
|
+
/^DATABASE_URL$/i,
|
|
125
|
+
/^DATABASE_URI$/i,
|
|
126
|
+
];
|
|
127
|
+
/**
|
|
128
|
+
* Build a map of infrastructure URL variables based on runner type
|
|
129
|
+
* For docker apps: use DOCKER_* prefixed values (e.g., mongodb://mongodb:27017)
|
|
130
|
+
* For host/PM2 apps: use HOST_* prefixed values (e.g., mongodb://localhost:27018)
|
|
131
|
+
*
|
|
132
|
+
* Example .env.genbox:
|
|
133
|
+
* HOST_MONGODB_URI=mongodb://localhost:27018/myapp
|
|
134
|
+
* DOCKER_MONGODB_URI=mongodb://mongodb:27017/myapp
|
|
135
|
+
* MONGODB_URI=${MONGODB_URI} # Placeholder that gets expanded
|
|
136
|
+
*/
|
|
137
|
+
function buildInfraUrlMap(envVarsFromFile, runner) {
|
|
138
|
+
const urlMap = {};
|
|
139
|
+
const useDocker = runner === 'docker';
|
|
140
|
+
const prefix = useDocker ? 'DOCKER_' : 'HOST_';
|
|
141
|
+
// Find all infrastructure variables that have HOST_ or DOCKER_ prefixes
|
|
142
|
+
const infraVarNames = new Set();
|
|
143
|
+
for (const key of Object.keys(envVarsFromFile)) {
|
|
144
|
+
const match = key.match(/^(HOST|DOCKER)_(.+)$/);
|
|
145
|
+
if (match) {
|
|
146
|
+
const varName = match[2];
|
|
147
|
+
// Check if it's an infrastructure variable
|
|
148
|
+
if (INFRA_VAR_PATTERNS.some(pattern => pattern.test(varName))) {
|
|
149
|
+
infraVarNames.add(varName);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Build mapping: VARNAME → value from appropriate prefix
|
|
154
|
+
for (const varName of infraVarNames) {
|
|
155
|
+
const prefixedKey = `${prefix}${varName}`;
|
|
156
|
+
const fallbackKey = useDocker ? `HOST_${varName}` : `DOCKER_${varName}`;
|
|
157
|
+
// Use prefixed value if available, otherwise fall back
|
|
158
|
+
const value = envVarsFromFile[prefixedKey] || envVarsFromFile[fallbackKey];
|
|
159
|
+
if (value) {
|
|
160
|
+
urlMap[varName] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return urlMap;
|
|
164
|
+
}
|
|
109
165
|
/**
|
|
110
166
|
* Parse env vars from a section content string
|
|
111
167
|
*/
|