genbox 1.0.37 → 1.0.39
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 +27 -2
- package/dist/commands/profiles.js +17 -45
- package/dist/commands/scan.js +306 -11
- package/dist/config-loader.js +2 -19
- package/dist/migration.js +2 -1
- package/dist/scanner/config-generator.js +46 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1736,6 +1736,13 @@ function convertV2ToV4(v2Config, scan) {
|
|
|
1736
1736
|
if (service.env?.length) {
|
|
1737
1737
|
appConfig.env = service.env;
|
|
1738
1738
|
}
|
|
1739
|
+
// Set runner and docker config
|
|
1740
|
+
if (service.runner) {
|
|
1741
|
+
appConfig.runner = service.runner;
|
|
1742
|
+
}
|
|
1743
|
+
if (service.docker) {
|
|
1744
|
+
appConfig.docker = service.docker;
|
|
1745
|
+
}
|
|
1739
1746
|
apps[name] = appConfig;
|
|
1740
1747
|
}
|
|
1741
1748
|
// Convert infrastructure to provides (v4 format)
|
|
@@ -1838,7 +1845,15 @@ function convertDetectedToScan(detected) {
|
|
|
1838
1845
|
'gateway': 'gateway',
|
|
1839
1846
|
'library': 'library',
|
|
1840
1847
|
};
|
|
1841
|
-
|
|
1848
|
+
// Map runner types
|
|
1849
|
+
const runnerMap = {
|
|
1850
|
+
'pm2': 'pm2',
|
|
1851
|
+
'docker': 'docker',
|
|
1852
|
+
'node': 'node',
|
|
1853
|
+
'bun': 'bun',
|
|
1854
|
+
'none': 'none',
|
|
1855
|
+
};
|
|
1856
|
+
const discoveredApp = {
|
|
1842
1857
|
name,
|
|
1843
1858
|
path: app.path,
|
|
1844
1859
|
type: typeMap[app.type || 'library'] || 'library',
|
|
@@ -1850,7 +1865,17 @@ function convertDetectedToScan(detected) {
|
|
|
1850
1865
|
build: app.commands.build || '',
|
|
1851
1866
|
start: app.commands.start || '',
|
|
1852
1867
|
} : {},
|
|
1853
|
-
|
|
1868
|
+
runner: app.runner ? runnerMap[app.runner] : undefined,
|
|
1869
|
+
};
|
|
1870
|
+
// Add docker config if present
|
|
1871
|
+
if (app.docker) {
|
|
1872
|
+
discoveredApp.docker = {
|
|
1873
|
+
service: app.docker.service,
|
|
1874
|
+
build_context: app.docker.build_context,
|
|
1875
|
+
dockerfile: app.docker.dockerfile,
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
apps.push(discoveredApp);
|
|
1854
1879
|
}
|
|
1855
1880
|
// Convert runtimes
|
|
1856
1881
|
const runtimes = detected.runtimes.map(r => ({
|
|
@@ -42,7 +42,6 @@ const prompts = __importStar(require("@inquirer/prompts"));
|
|
|
42
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
43
|
const yaml = __importStar(require("js-yaml"));
|
|
44
44
|
const fs = __importStar(require("fs"));
|
|
45
|
-
const path = __importStar(require("path"));
|
|
46
45
|
const config_loader_1 = require("../config-loader");
|
|
47
46
|
const schema_v4_1 = require("../schema-v4");
|
|
48
47
|
exports.profilesCommand = new commander_1.Command('profiles')
|
|
@@ -278,29 +277,15 @@ exports.profilesCommand
|
|
|
278
277
|
source: dbMode.includes('staging') ? 'staging' : dbMode.includes('production') ? 'production' : undefined,
|
|
279
278
|
} : undefined,
|
|
280
279
|
};
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
choices: [
|
|
285
|
-
{ name: 'genbox.yaml (shared with team)', value: 'project' },
|
|
286
|
-
{ name: '~/.genbox/profiles.yaml (personal)', value: 'user' },
|
|
287
|
-
],
|
|
288
|
-
});
|
|
289
|
-
if (saveLocation === 'user') {
|
|
290
|
-
configLoader.saveUserProfile(profileName, profile);
|
|
291
|
-
console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' saved to ~/.genbox/profiles.yaml`));
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
// Add to project config
|
|
295
|
-
if (!config.profiles) {
|
|
296
|
-
config.profiles = {};
|
|
297
|
-
}
|
|
298
|
-
config.profiles[profileName] = profile;
|
|
299
|
-
const configPath = configLoader.getConfigPath();
|
|
300
|
-
const yamlContent = yaml.dump(config, { lineWidth: 120, noRefs: true });
|
|
301
|
-
fs.writeFileSync(configPath, yamlContent);
|
|
302
|
-
console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' added to genbox.yaml`));
|
|
280
|
+
// Save to project config (user/global profiles removed - they reference project-specific apps)
|
|
281
|
+
if (!config.profiles) {
|
|
282
|
+
config.profiles = {};
|
|
303
283
|
}
|
|
284
|
+
config.profiles[profileName] = profile;
|
|
285
|
+
const configPath = configLoader.getConfigPath();
|
|
286
|
+
const yamlContent = yaml.dump(config, { lineWidth: 120, noRefs: true });
|
|
287
|
+
fs.writeFileSync(configPath, yamlContent);
|
|
288
|
+
console.log(chalk_1.default.green(`\n✔ Profile '${profileName}' added to genbox.yaml`));
|
|
304
289
|
console.log(chalk_1.default.dim(`Use: genbox create <name> --profile ${profileName}`));
|
|
305
290
|
}
|
|
306
291
|
catch (error) {
|
|
@@ -315,8 +300,7 @@ exports.profilesCommand
|
|
|
315
300
|
exports.profilesCommand
|
|
316
301
|
.command('delete <name>')
|
|
317
302
|
.description('Delete a profile')
|
|
318
|
-
.
|
|
319
|
-
.action(async (name, options) => {
|
|
303
|
+
.action(async (name) => {
|
|
320
304
|
try {
|
|
321
305
|
const configLoader = new config_loader_1.ConfigLoader();
|
|
322
306
|
const loadResult = await configLoader.load();
|
|
@@ -331,12 +315,9 @@ exports.profilesCommand
|
|
|
331
315
|
return;
|
|
332
316
|
}
|
|
333
317
|
const config = loadResult.config;
|
|
334
|
-
// Check if profile exists
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const inUser = userProfiles?.profiles?.[name];
|
|
338
|
-
if (!inProject && !inUser) {
|
|
339
|
-
console.log(chalk_1.default.red(`Profile '${name}' not found`));
|
|
318
|
+
// Check if profile exists in project
|
|
319
|
+
if (!config.profiles?.[name]) {
|
|
320
|
+
console.log(chalk_1.default.red(`Profile '${name}' not found in genbox.yaml`));
|
|
340
321
|
return;
|
|
341
322
|
}
|
|
342
323
|
// Confirm deletion
|
|
@@ -348,20 +329,11 @@ exports.profilesCommand
|
|
|
348
329
|
console.log(chalk_1.default.dim('Cancelled.'));
|
|
349
330
|
return;
|
|
350
331
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
console.log(chalk_1.default.green(`✔ Deleted '${name}' from user profiles`));
|
|
357
|
-
}
|
|
358
|
-
else if (inProject) {
|
|
359
|
-
// Delete from project config
|
|
360
|
-
delete config.profiles[name];
|
|
361
|
-
const configPath = configLoader.getConfigPath();
|
|
362
|
-
fs.writeFileSync(configPath, yaml.dump(config, { lineWidth: 120, noRefs: true }));
|
|
363
|
-
console.log(chalk_1.default.green(`✔ Deleted '${name}' from genbox.yaml`));
|
|
364
|
-
}
|
|
332
|
+
// Delete from project config
|
|
333
|
+
delete config.profiles[name];
|
|
334
|
+
const configPath = configLoader.getConfigPath();
|
|
335
|
+
fs.writeFileSync(configPath, yaml.dump(config, { lineWidth: 120, noRefs: true }));
|
|
336
|
+
console.log(chalk_1.default.green(`✔ Deleted '${name}' from genbox.yaml`));
|
|
365
337
|
}
|
|
366
338
|
catch (error) {
|
|
367
339
|
if (error.name === 'ExitPromptError') {
|
package/dist/commands/scan.js
CHANGED
|
@@ -112,10 +112,12 @@ exports.scanCommand = new commander_1.Command('scan')
|
|
|
112
112
|
.option('--no-infra', 'Skip infrastructure detection (docker-compose)')
|
|
113
113
|
.option('--no-scripts', 'Skip script detection')
|
|
114
114
|
.option('-i, --interactive', 'Interactive mode - select apps before writing')
|
|
115
|
+
.option('--edit', 'Edit mode - review and modify detected values for each app')
|
|
115
116
|
.option('-e, --exclude <patterns>', 'Comma-separated patterns to exclude', '')
|
|
116
117
|
.action(async (options) => {
|
|
117
118
|
const cwd = process.cwd();
|
|
118
|
-
const isInteractive = options.interactive && !options.stdout && process.stdin.isTTY;
|
|
119
|
+
const isInteractive = (options.interactive || options.edit) && !options.stdout && process.stdin.isTTY;
|
|
120
|
+
const isEditMode = options.edit && !options.stdout && process.stdin.isTTY;
|
|
119
121
|
console.log(chalk_1.default.cyan('\n🔍 Scanning project...\n'));
|
|
120
122
|
try {
|
|
121
123
|
// Run the scanner
|
|
@@ -131,6 +133,10 @@ exports.scanCommand = new commander_1.Command('scan')
|
|
|
131
133
|
if (isInteractive) {
|
|
132
134
|
detected = await interactiveSelection(detected);
|
|
133
135
|
}
|
|
136
|
+
// Edit mode: let user modify detected values for each app
|
|
137
|
+
if (isEditMode) {
|
|
138
|
+
detected = await interactiveEditMode(detected);
|
|
139
|
+
}
|
|
134
140
|
// Scan env files for service URLs (only for selected frontend apps)
|
|
135
141
|
const frontendApps = Object.entries(detected.apps)
|
|
136
142
|
.filter(([, app]) => app.type === 'frontend')
|
|
@@ -285,6 +291,176 @@ async function interactiveSelection(detected) {
|
|
|
285
291
|
}
|
|
286
292
|
return result;
|
|
287
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Interactive edit mode - allows modifying detected values for each app
|
|
296
|
+
*/
|
|
297
|
+
async function interactiveEditMode(detected) {
|
|
298
|
+
const result = { ...detected, apps: { ...detected.apps } };
|
|
299
|
+
const appEntries = Object.entries(result.apps);
|
|
300
|
+
if (appEntries.length === 0) {
|
|
301
|
+
console.log(chalk_1.default.dim('No apps to edit.'));
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(chalk_1.default.blue('=== Edit Detected Values ==='));
|
|
306
|
+
console.log(chalk_1.default.dim('Review and modify detected values for each app.\n'));
|
|
307
|
+
// Show summary of all apps first
|
|
308
|
+
console.log(chalk_1.default.bold('Detected apps:'));
|
|
309
|
+
for (const [name, app] of appEntries) {
|
|
310
|
+
const runner = app.runner || 'pm2';
|
|
311
|
+
const port = app.port ? `:${app.port}` : '';
|
|
312
|
+
console.log(` ${chalk_1.default.cyan(name)} - ${app.type || 'unknown'} (${runner})${port}`);
|
|
313
|
+
}
|
|
314
|
+
console.log('');
|
|
315
|
+
// Ask if user wants to edit any apps
|
|
316
|
+
const editApps = await prompts.confirm({
|
|
317
|
+
message: 'Do you want to edit any app configurations?',
|
|
318
|
+
default: false,
|
|
319
|
+
});
|
|
320
|
+
if (!editApps) {
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
// Let user select which apps to edit
|
|
324
|
+
const appChoices = appEntries.map(([name, app]) => ({
|
|
325
|
+
name: `${name} (${app.type || 'unknown'}, ${app.runner || 'pm2'})`,
|
|
326
|
+
value: name,
|
|
327
|
+
}));
|
|
328
|
+
const appsToEdit = await prompts.checkbox({
|
|
329
|
+
message: 'Select apps to edit:',
|
|
330
|
+
choices: appChoices,
|
|
331
|
+
});
|
|
332
|
+
// Edit each selected app
|
|
333
|
+
for (const appName of appsToEdit) {
|
|
334
|
+
result.apps[appName] = await editAppConfig(appName, result.apps[appName]);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Edit a single app's configuration
|
|
340
|
+
*/
|
|
341
|
+
async function editAppConfig(name, app) {
|
|
342
|
+
console.log('');
|
|
343
|
+
console.log(chalk_1.default.blue(`=== Editing: ${name} ===`));
|
|
344
|
+
// Show current values
|
|
345
|
+
console.log(chalk_1.default.dim('Current values:'));
|
|
346
|
+
console.log(` Type: ${chalk_1.default.cyan(app.type || 'unknown')} ${chalk_1.default.dim(`(${app.type_reason || 'detected'})`)}`);
|
|
347
|
+
console.log(` Runner: ${chalk_1.default.cyan(app.runner || 'pm2')} ${chalk_1.default.dim(`(${app.runner_reason || 'default'})`)}`);
|
|
348
|
+
console.log(` Port: ${app.port ? chalk_1.default.cyan(String(app.port)) : chalk_1.default.dim('not set')} ${app.port_source ? chalk_1.default.dim(`(${app.port_source})`) : ''}`);
|
|
349
|
+
console.log(` Framework: ${app.framework ? chalk_1.default.cyan(app.framework) : chalk_1.default.dim('not detected')}`);
|
|
350
|
+
console.log('');
|
|
351
|
+
const result = { ...app };
|
|
352
|
+
// Edit type
|
|
353
|
+
const typeChoices = [
|
|
354
|
+
{ name: `frontend ${app.type === 'frontend' ? chalk_1.default.green('(current)') : ''}`, value: 'frontend' },
|
|
355
|
+
{ name: `backend ${app.type === 'backend' ? chalk_1.default.green('(current)') : ''}`, value: 'backend' },
|
|
356
|
+
{ name: `worker ${app.type === 'worker' ? chalk_1.default.green('(current)') : ''}`, value: 'worker' },
|
|
357
|
+
{ name: `gateway ${app.type === 'gateway' ? chalk_1.default.green('(current)') : ''}`, value: 'gateway' },
|
|
358
|
+
{ name: `library ${app.type === 'library' ? chalk_1.default.green('(current)') : ''}`, value: 'library' },
|
|
359
|
+
];
|
|
360
|
+
const newType = await prompts.select({
|
|
361
|
+
message: 'App type:',
|
|
362
|
+
choices: typeChoices,
|
|
363
|
+
default: app.type || 'backend',
|
|
364
|
+
});
|
|
365
|
+
if (newType !== app.type) {
|
|
366
|
+
result.type = newType;
|
|
367
|
+
result.type_reason = 'manually set';
|
|
368
|
+
}
|
|
369
|
+
// Edit runner
|
|
370
|
+
const runnerChoices = [
|
|
371
|
+
{ name: `pm2 - Process manager (recommended for Node.js) ${app.runner === 'pm2' ? chalk_1.default.green('(current)') : ''}`, value: 'pm2' },
|
|
372
|
+
{ name: `docker - Docker compose service ${app.runner === 'docker' ? chalk_1.default.green('(current)') : ''}`, value: 'docker' },
|
|
373
|
+
{ name: `bun - Bun runtime ${app.runner === 'bun' ? chalk_1.default.green('(current)') : ''}`, value: 'bun' },
|
|
374
|
+
{ name: `node - Direct Node.js execution ${app.runner === 'node' ? chalk_1.default.green('(current)') : ''}`, value: 'node' },
|
|
375
|
+
{ name: `none - Library/not runnable ${app.runner === 'none' ? chalk_1.default.green('(current)') : ''}`, value: 'none' },
|
|
376
|
+
];
|
|
377
|
+
const newRunner = await prompts.select({
|
|
378
|
+
message: 'Runner:',
|
|
379
|
+
choices: runnerChoices,
|
|
380
|
+
default: app.runner || 'pm2',
|
|
381
|
+
});
|
|
382
|
+
if (newRunner !== app.runner) {
|
|
383
|
+
result.runner = newRunner;
|
|
384
|
+
result.runner_reason = 'manually set';
|
|
385
|
+
}
|
|
386
|
+
// If runner is docker, ask for docker config
|
|
387
|
+
if (newRunner === 'docker') {
|
|
388
|
+
const dockerService = await prompts.input({
|
|
389
|
+
message: 'Docker service name:',
|
|
390
|
+
default: app.docker?.service || name,
|
|
391
|
+
});
|
|
392
|
+
const dockerContext = await prompts.input({
|
|
393
|
+
message: 'Docker build context:',
|
|
394
|
+
default: app.docker?.build_context || app.path || '.',
|
|
395
|
+
});
|
|
396
|
+
result.docker = {
|
|
397
|
+
service: dockerService,
|
|
398
|
+
build_context: dockerContext,
|
|
399
|
+
dockerfile: app.docker?.dockerfile,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Edit port (only if not a library)
|
|
403
|
+
if (newRunner !== 'none' && result.type !== 'library') {
|
|
404
|
+
const portInput = await prompts.input({
|
|
405
|
+
message: 'Port (leave empty to skip):',
|
|
406
|
+
default: app.port ? String(app.port) : '',
|
|
407
|
+
});
|
|
408
|
+
if (portInput) {
|
|
409
|
+
const portNum = parseInt(portInput, 10);
|
|
410
|
+
if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
|
|
411
|
+
result.port = portNum;
|
|
412
|
+
if (portNum !== app.port) {
|
|
413
|
+
result.port_source = 'manually set';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
result.port = undefined;
|
|
419
|
+
result.port_source = undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Edit framework
|
|
423
|
+
const frameworkChoices = [
|
|
424
|
+
{ name: `Keep current: ${app.framework || 'none'}`, value: app.framework || '' },
|
|
425
|
+
{ name: '---', value: '__separator__', disabled: true },
|
|
426
|
+
{ name: 'nextjs', value: 'nextjs' },
|
|
427
|
+
{ name: 'react', value: 'react' },
|
|
428
|
+
{ name: 'vue', value: 'vue' },
|
|
429
|
+
{ name: 'nuxt', value: 'nuxt' },
|
|
430
|
+
{ name: 'vite', value: 'vite' },
|
|
431
|
+
{ name: 'astro', value: 'astro' },
|
|
432
|
+
{ name: 'nestjs', value: 'nestjs' },
|
|
433
|
+
{ name: 'express', value: 'express' },
|
|
434
|
+
{ name: 'fastify', value: 'fastify' },
|
|
435
|
+
{ name: 'hono', value: 'hono' },
|
|
436
|
+
{ name: 'Other (type manually)', value: '__other__' },
|
|
437
|
+
{ name: 'None / Clear', value: '__none__' },
|
|
438
|
+
];
|
|
439
|
+
const frameworkChoice = await prompts.select({
|
|
440
|
+
message: 'Framework:',
|
|
441
|
+
choices: frameworkChoices,
|
|
442
|
+
default: app.framework || '',
|
|
443
|
+
});
|
|
444
|
+
if (frameworkChoice === '__other__') {
|
|
445
|
+
const customFramework = await prompts.input({
|
|
446
|
+
message: 'Enter framework name:',
|
|
447
|
+
});
|
|
448
|
+
if (customFramework) {
|
|
449
|
+
result.framework = customFramework;
|
|
450
|
+
result.framework_source = 'manually set';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (frameworkChoice === '__none__') {
|
|
454
|
+
result.framework = undefined;
|
|
455
|
+
result.framework_source = undefined;
|
|
456
|
+
}
|
|
457
|
+
else if (frameworkChoice && frameworkChoice !== app.framework) {
|
|
458
|
+
result.framework = frameworkChoice;
|
|
459
|
+
result.framework_source = 'manually set';
|
|
460
|
+
}
|
|
461
|
+
console.log(chalk_1.default.green(`✓ Updated ${name}`));
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
288
464
|
/**
|
|
289
465
|
* Interactive service URL selection
|
|
290
466
|
*/
|
|
@@ -459,10 +635,14 @@ function convertScanToDetected(scan, root) {
|
|
|
459
635
|
const appDir = path.join(root, app.path);
|
|
460
636
|
appGit = detectGitForDirectory(appDir);
|
|
461
637
|
}
|
|
638
|
+
// Detect runner based on app type and context
|
|
639
|
+
const { runner, runner_reason } = detectRunner(app, scan);
|
|
462
640
|
detected.apps[app.name] = {
|
|
463
641
|
path: app.path,
|
|
464
642
|
type: mappedType,
|
|
465
643
|
type_reason: inferTypeReason(app),
|
|
644
|
+
runner,
|
|
645
|
+
runner_reason,
|
|
466
646
|
port: app.port,
|
|
467
647
|
port_source: app.port ? inferPortSource(app) : undefined,
|
|
468
648
|
framework: app.framework, // Framework is a string type
|
|
@@ -506,17 +686,31 @@ function convertScanToDetected(scan, root) {
|
|
|
506
686
|
source: 'docker-compose.yml',
|
|
507
687
|
});
|
|
508
688
|
}
|
|
509
|
-
//
|
|
689
|
+
// Add Docker application services as apps with runner: 'docker'
|
|
690
|
+
// (Only add if not already detected as a PM2 app)
|
|
510
691
|
if (scan.compose.applications && scan.compose.applications.length > 0) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
692
|
+
for (const dockerApp of scan.compose.applications) {
|
|
693
|
+
// Skip if already exists as a PM2 app
|
|
694
|
+
if (detected.apps[dockerApp.name])
|
|
695
|
+
continue;
|
|
696
|
+
// Infer type by checking file structure in build context
|
|
697
|
+
const { type: mappedType, reason: typeReason } = inferDockerAppType(dockerApp.name, dockerApp.build?.context, scan.root);
|
|
698
|
+
detected.apps[dockerApp.name] = {
|
|
699
|
+
path: dockerApp.build?.context || '.',
|
|
700
|
+
type: mappedType,
|
|
701
|
+
type_reason: typeReason,
|
|
702
|
+
runner: 'docker',
|
|
703
|
+
runner_reason: 'defined in docker-compose.yml',
|
|
704
|
+
docker: {
|
|
705
|
+
service: dockerApp.name,
|
|
706
|
+
build_context: dockerApp.build?.context,
|
|
707
|
+
dockerfile: dockerApp.build?.dockerfile,
|
|
708
|
+
image: dockerApp.image,
|
|
709
|
+
},
|
|
710
|
+
port: dockerApp.ports?.[0]?.host,
|
|
711
|
+
port_source: 'docker-compose.yml ports',
|
|
712
|
+
};
|
|
713
|
+
}
|
|
520
714
|
}
|
|
521
715
|
}
|
|
522
716
|
// Git info
|
|
@@ -567,6 +761,38 @@ function mapAppType(type) {
|
|
|
567
761
|
return undefined; // Unknown type
|
|
568
762
|
}
|
|
569
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Detect the appropriate runner for an app
|
|
766
|
+
* Priority:
|
|
767
|
+
* 1. Library apps → none
|
|
768
|
+
* 2. Bun lockfile present → bun
|
|
769
|
+
* 3. Has start script or is a typical app → pm2
|
|
770
|
+
* 4. Simple CLI/script → node
|
|
771
|
+
*/
|
|
772
|
+
function detectRunner(app, scan) {
|
|
773
|
+
// Libraries don't run
|
|
774
|
+
if (app.type === 'library') {
|
|
775
|
+
return { runner: 'none', runner_reason: 'library apps are not runnable' };
|
|
776
|
+
}
|
|
777
|
+
// Check for Bun
|
|
778
|
+
const hasBunLockfile = scan.runtimes?.some(r => r.lockfile === 'bun.lockb');
|
|
779
|
+
if (hasBunLockfile) {
|
|
780
|
+
return { runner: 'bun', runner_reason: 'bun.lockb detected' };
|
|
781
|
+
}
|
|
782
|
+
// Check for typical app patterns that benefit from PM2
|
|
783
|
+
const hasStartScript = app.scripts?.start || app.scripts?.dev;
|
|
784
|
+
const isTypicalApp = ['frontend', 'backend', 'api', 'worker', 'gateway'].includes(app.type || '');
|
|
785
|
+
if (hasStartScript || isTypicalApp) {
|
|
786
|
+
return { runner: 'pm2', runner_reason: 'typical Node.js app with start script' };
|
|
787
|
+
}
|
|
788
|
+
// CLI tools or simple scripts can use direct node
|
|
789
|
+
const name = (app.name || '').toLowerCase();
|
|
790
|
+
if (name.includes('cli') || name.includes('tool') || name.includes('script')) {
|
|
791
|
+
return { runner: 'node', runner_reason: 'CLI/tool detected' };
|
|
792
|
+
}
|
|
793
|
+
// Default to pm2 for most apps
|
|
794
|
+
return { runner: 'pm2', runner_reason: 'default for Node.js apps' };
|
|
795
|
+
}
|
|
570
796
|
function inferTypeReason(app) {
|
|
571
797
|
if (!app.type)
|
|
572
798
|
return 'unknown';
|
|
@@ -582,6 +808,75 @@ function inferTypeReason(app) {
|
|
|
582
808
|
}
|
|
583
809
|
return 'dependency analysis';
|
|
584
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Infer app type by examining file structure in the build context directory
|
|
813
|
+
* Falls back to name-based detection only if file analysis doesn't find anything
|
|
814
|
+
*/
|
|
815
|
+
function inferDockerAppType(name, buildContext, rootDir) {
|
|
816
|
+
// If we have a build context, check file structure first
|
|
817
|
+
if (buildContext && rootDir) {
|
|
818
|
+
const contextPath = path.resolve(rootDir, buildContext);
|
|
819
|
+
// Check for frontend indicators
|
|
820
|
+
const frontendConfigs = ['vite.config.ts', 'vite.config.js', 'next.config.js', 'next.config.mjs', 'nuxt.config.ts', 'astro.config.mjs'];
|
|
821
|
+
for (const config of frontendConfigs) {
|
|
822
|
+
if (fs.existsSync(path.join(contextPath, config))) {
|
|
823
|
+
return { type: 'frontend', reason: `config file found: ${config}` };
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Check for backend indicators
|
|
827
|
+
const backendConfigs = ['nest-cli.json', 'tsconfig.build.json'];
|
|
828
|
+
for (const config of backendConfigs) {
|
|
829
|
+
if (fs.existsSync(path.join(contextPath, config))) {
|
|
830
|
+
return { type: 'backend', reason: `config file found: ${config}` };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Check package.json dependencies
|
|
834
|
+
const packageJsonPath = path.join(contextPath, 'package.json');
|
|
835
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
836
|
+
try {
|
|
837
|
+
const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
838
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
839
|
+
// Frontend dependencies
|
|
840
|
+
const frontendDeps = ['react', 'react-dom', 'vue', '@angular/core', 'svelte', 'next', 'nuxt', 'vite', '@remix-run/react', 'astro'];
|
|
841
|
+
for (const dep of frontendDeps) {
|
|
842
|
+
if (allDeps[dep]) {
|
|
843
|
+
return { type: 'frontend', reason: `package.json dependency: ${dep}` };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Backend dependencies
|
|
847
|
+
const backendDeps = ['@nestjs/core', 'express', 'fastify', 'koa', 'hapi', '@hono/node-server'];
|
|
848
|
+
for (const dep of backendDeps) {
|
|
849
|
+
if (allDeps[dep]) {
|
|
850
|
+
return { type: 'backend', reason: `package.json dependency: ${dep}` };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// Worker dependencies
|
|
854
|
+
const workerDeps = ['bull', 'bullmq', 'agenda', 'bee-queue'];
|
|
855
|
+
for (const dep of workerDeps) {
|
|
856
|
+
if (allDeps[dep]) {
|
|
857
|
+
return { type: 'worker', reason: `package.json dependency: ${dep}` };
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Ignore parse errors
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// Fall back to name-based detection
|
|
867
|
+
const lowerName = name.toLowerCase();
|
|
868
|
+
if (lowerName.includes('web') || lowerName.includes('frontend') || lowerName.includes('ui') || lowerName.includes('client')) {
|
|
869
|
+
return { type: 'frontend', reason: `naming convention ('${name}' contains frontend keyword)` };
|
|
870
|
+
}
|
|
871
|
+
if (lowerName.includes('api') || lowerName.includes('backend') || lowerName.includes('server') || lowerName.includes('gateway')) {
|
|
872
|
+
return { type: 'backend', reason: `naming convention ('${name}' contains backend keyword)` };
|
|
873
|
+
}
|
|
874
|
+
if (lowerName.includes('worker') || lowerName.includes('queue') || lowerName.includes('job')) {
|
|
875
|
+
return { type: 'worker', reason: `naming convention ('${name}' contains worker keyword)` };
|
|
876
|
+
}
|
|
877
|
+
// Default to backend for Docker services
|
|
878
|
+
return { type: 'backend', reason: 'default for Docker services' };
|
|
879
|
+
}
|
|
585
880
|
function inferPortSource(app) {
|
|
586
881
|
if (app.scripts?.dev?.includes('--port')) {
|
|
587
882
|
return 'package.json scripts.dev (--port flag)';
|
package/dist/config-loader.js
CHANGED
|
@@ -467,7 +467,8 @@ class ConfigLoader {
|
|
|
467
467
|
*/
|
|
468
468
|
listProfiles(config) {
|
|
469
469
|
const profiles = [];
|
|
470
|
-
// Project profiles
|
|
470
|
+
// Project profiles only (user/global profiles removed - they don't make sense
|
|
471
|
+
// because profiles reference project-specific app names)
|
|
471
472
|
for (const [name, profile] of Object.entries(config.profiles || {})) {
|
|
472
473
|
const resolved = this.getProfile(config, name);
|
|
473
474
|
profiles.push({
|
|
@@ -479,24 +480,6 @@ class ConfigLoader {
|
|
|
479
480
|
connection: resolved ? getProfileConnection(resolved) : undefined,
|
|
480
481
|
});
|
|
481
482
|
}
|
|
482
|
-
// User profiles
|
|
483
|
-
const userProfiles = this.loadUserProfiles();
|
|
484
|
-
if (userProfiles) {
|
|
485
|
-
for (const [name, profile] of Object.entries(userProfiles.profiles || {})) {
|
|
486
|
-
// Skip if already defined in project
|
|
487
|
-
if (config.profiles?.[name])
|
|
488
|
-
continue;
|
|
489
|
-
const resolved = this.getProfile(config, name);
|
|
490
|
-
profiles.push({
|
|
491
|
-
name,
|
|
492
|
-
description: profile.description,
|
|
493
|
-
source: 'user',
|
|
494
|
-
apps: resolved?.apps || [],
|
|
495
|
-
size: resolved?.size,
|
|
496
|
-
connection: resolved ? getProfileConnection(resolved) : undefined,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
483
|
return profiles;
|
|
501
484
|
}
|
|
502
485
|
/**
|
package/dist/migration.js
CHANGED
|
@@ -244,7 +244,8 @@ function migrateApp(appName, appConfig, v3Config, changes, warnings) {
|
|
|
244
244
|
if (appConfig.env)
|
|
245
245
|
v4App.env = appConfig.env;
|
|
246
246
|
if (appConfig.compose_file) {
|
|
247
|
-
v4App.
|
|
247
|
+
v4App.runner = 'docker';
|
|
248
|
+
v4App.docker = { file: appConfig.compose_file };
|
|
248
249
|
}
|
|
249
250
|
// Warn about missing explicit type
|
|
250
251
|
if (!appConfig.type) {
|
|
@@ -77,6 +77,18 @@ class ConfigGenerator {
|
|
|
77
77
|
output: framework?.outputDir,
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
// Set runner from app if available
|
|
81
|
+
if (app.runner) {
|
|
82
|
+
serviceConfig.runner = app.runner;
|
|
83
|
+
}
|
|
84
|
+
// Set docker config if present
|
|
85
|
+
if (app.docker) {
|
|
86
|
+
serviceConfig.docker = {
|
|
87
|
+
service: app.docker.service,
|
|
88
|
+
build_context: app.docker.build_context,
|
|
89
|
+
dockerfile: app.docker.dockerfile,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
80
92
|
services[app.name] = serviceConfig;
|
|
81
93
|
}
|
|
82
94
|
}
|
|
@@ -120,8 +132,42 @@ class ConfigGenerator {
|
|
|
120
132
|
output: framework?.outputDir,
|
|
121
133
|
};
|
|
122
134
|
}
|
|
135
|
+
// Set runner from app if available
|
|
136
|
+
if (app.runner) {
|
|
137
|
+
serviceConfig.runner = app.runner;
|
|
138
|
+
}
|
|
139
|
+
// Set docker config if present
|
|
140
|
+
if (app.docker) {
|
|
141
|
+
serviceConfig.docker = {
|
|
142
|
+
service: app.docker.service,
|
|
143
|
+
build_context: app.docker.build_context,
|
|
144
|
+
dockerfile: app.docker.dockerfile,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
123
147
|
services[app.name] = serviceConfig;
|
|
124
148
|
}
|
|
149
|
+
// Also include Docker services from compose (if not already added as apps)
|
|
150
|
+
// This is a fallback for services not detected during scan
|
|
151
|
+
if (scan.compose?.applications) {
|
|
152
|
+
for (const dockerApp of scan.compose.applications) {
|
|
153
|
+
// Skip if already exists as an app
|
|
154
|
+
if (services[dockerApp.name])
|
|
155
|
+
continue;
|
|
156
|
+
const port = dockerApp.ports[0]?.host || 3000;
|
|
157
|
+
services[dockerApp.name] = {
|
|
158
|
+
type: this.inferServiceType(dockerApp.name),
|
|
159
|
+
port,
|
|
160
|
+
runner: 'docker',
|
|
161
|
+
docker: {
|
|
162
|
+
service: dockerApp.name,
|
|
163
|
+
build_context: dockerApp.build?.context,
|
|
164
|
+
dockerfile: dockerApp.build?.dockerfile,
|
|
165
|
+
},
|
|
166
|
+
dependsOn: dockerApp.dependsOn.map(d => this.normalizeServiceName(d)),
|
|
167
|
+
env: Object.keys(dockerApp.environment || {}).filter(k => !k.startsWith('_')),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
125
171
|
}
|
|
126
172
|
// Single app fallback
|
|
127
173
|
if (Object.keys(services).length === 0 && scan.frameworks.length > 0) {
|