genbox 1.0.2 → 1.0.4
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 +369 -130
- package/dist/commands/db-sync.js +364 -0
- package/dist/commands/destroy.js +5 -10
- package/dist/commands/init.js +669 -402
- package/dist/commands/profiles.js +333 -0
- package/dist/commands/push.js +140 -47
- package/dist/config-loader.js +529 -0
- package/dist/genbox-selector.js +5 -8
- package/dist/index.js +5 -1
- package/dist/profile-resolver.js +547 -0
- package/dist/scanner/compose-parser.js +441 -0
- package/dist/scanner/config-generator.js +620 -0
- package/dist/scanner/env-analyzer.js +503 -0
- package/dist/scanner/framework-detector.js +621 -0
- package/dist/scanner/index.js +424 -0
- package/dist/scanner/runtime-detector.js +330 -0
- package/dist/scanner/structure-detector.js +412 -0
- package/dist/scanner/types.js +7 -0
- package/dist/schema-v3.js +12 -0
- package/package.json +4 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates genbox.yaml configuration from scan results
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ConfigGenerator = void 0;
|
|
9
|
+
class ConfigGenerator {
|
|
10
|
+
/**
|
|
11
|
+
* Generate configuration from scan results
|
|
12
|
+
*/
|
|
13
|
+
generate(scan) {
|
|
14
|
+
const config = {
|
|
15
|
+
version: '2.0',
|
|
16
|
+
project: {
|
|
17
|
+
name: scan.projectName,
|
|
18
|
+
structure: scan.structure.type,
|
|
19
|
+
},
|
|
20
|
+
services: this.generateServices(scan),
|
|
21
|
+
system: {
|
|
22
|
+
size: this.inferServerSize(scan),
|
|
23
|
+
runtimes: scan.runtimes.map(r => ({
|
|
24
|
+
language: r.language,
|
|
25
|
+
version: r.version || 'latest',
|
|
26
|
+
packageManager: r.packageManager,
|
|
27
|
+
})),
|
|
28
|
+
},
|
|
29
|
+
repos: this.generateRepos(scan),
|
|
30
|
+
};
|
|
31
|
+
// Add workspace config for monorepos
|
|
32
|
+
if (scan.structure.type.startsWith('monorepo')) {
|
|
33
|
+
config.workspace = this.generateWorkspaceConfig(scan);
|
|
34
|
+
}
|
|
35
|
+
// Add infrastructure from docker-compose
|
|
36
|
+
if (scan.compose) {
|
|
37
|
+
config.infrastructure = this.generateInfrastructure(scan);
|
|
38
|
+
}
|
|
39
|
+
// Generate hooks
|
|
40
|
+
config.hooks = this.generateHooks(scan);
|
|
41
|
+
// Generate scripts
|
|
42
|
+
if (scan.scripts.length > 0) {
|
|
43
|
+
config.scripts = this.generateScripts(scan);
|
|
44
|
+
}
|
|
45
|
+
// Generate networking config
|
|
46
|
+
config.networking = this.generateNetworking(scan, config.services);
|
|
47
|
+
const warnings = this.collectWarnings(scan);
|
|
48
|
+
return {
|
|
49
|
+
config,
|
|
50
|
+
envTemplate: this.generateEnvTemplate(scan),
|
|
51
|
+
warnings,
|
|
52
|
+
scripts: this.generateSetupScripts(scan, config),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
generateServices(scan) {
|
|
56
|
+
const services = {};
|
|
57
|
+
// For hybrid structure, prioritize discovered apps over docker-compose
|
|
58
|
+
if (scan.structure.type === 'hybrid' && scan.apps.length > 0) {
|
|
59
|
+
for (const app of scan.apps) {
|
|
60
|
+
if (app.type === 'library')
|
|
61
|
+
continue;
|
|
62
|
+
const framework = scan.frameworks.find(f => f.name === app.framework);
|
|
63
|
+
const port = app.port || framework?.defaultPort || this.getDefaultPort(app.type, app.framework);
|
|
64
|
+
const serviceConfig = {
|
|
65
|
+
type: app.type,
|
|
66
|
+
framework: app.framework,
|
|
67
|
+
path: app.path,
|
|
68
|
+
port,
|
|
69
|
+
start: {
|
|
70
|
+
command: framework?.startCommand || 'npm run dev',
|
|
71
|
+
dev: framework?.devCommand || 'npm run dev',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (framework?.buildCommand) {
|
|
75
|
+
serviceConfig.build = {
|
|
76
|
+
command: framework.buildCommand,
|
|
77
|
+
output: framework?.outputDir,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
services[app.name] = serviceConfig;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// From docker-compose applications (microservices structure)
|
|
84
|
+
else if (scan.compose && scan.structure.type === 'microservices') {
|
|
85
|
+
for (const app of scan.compose.applications) {
|
|
86
|
+
const serviceName = this.normalizeServiceName(app.name);
|
|
87
|
+
const port = app.ports[0]?.host || 3000;
|
|
88
|
+
// Try to find matching framework
|
|
89
|
+
const framework = scan.frameworks.find(f => app.build?.context && app.build.context.includes(f.name));
|
|
90
|
+
services[serviceName] = {
|
|
91
|
+
type: this.inferServiceType(app.name),
|
|
92
|
+
framework: framework?.name,
|
|
93
|
+
port,
|
|
94
|
+
healthcheck: app.healthcheck?.test ? this.extractHealthPath(app.healthcheck.test) : undefined,
|
|
95
|
+
dependsOn: app.dependsOn.map(d => this.normalizeServiceName(d)),
|
|
96
|
+
env: Object.keys(app.environment).filter(k => !k.startsWith('_')),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// From discovered apps (monorepo)
|
|
101
|
+
else if (scan.apps.length > 0) {
|
|
102
|
+
for (const app of scan.apps) {
|
|
103
|
+
if (app.type === 'library')
|
|
104
|
+
continue;
|
|
105
|
+
const framework = scan.frameworks.find(f => f.name === app.framework);
|
|
106
|
+
const port = app.port || framework?.defaultPort || 3000;
|
|
107
|
+
const serviceConfig = {
|
|
108
|
+
type: app.type,
|
|
109
|
+
framework: app.framework,
|
|
110
|
+
path: app.path !== scan.root ? app.path : undefined,
|
|
111
|
+
port,
|
|
112
|
+
start: {
|
|
113
|
+
command: framework?.startCommand || 'npm start',
|
|
114
|
+
dev: framework?.devCommand,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
if (framework?.buildCommand) {
|
|
118
|
+
serviceConfig.build = {
|
|
119
|
+
command: this.wrapForMonorepo(framework.buildCommand, app.name, scan),
|
|
120
|
+
output: framework?.outputDir,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
services[app.name] = serviceConfig;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Single app fallback
|
|
127
|
+
if (Object.keys(services).length === 0 && scan.frameworks.length > 0) {
|
|
128
|
+
const framework = scan.frameworks[0];
|
|
129
|
+
const serviceConfig = {
|
|
130
|
+
type: framework.type === 'fullstack' ? 'frontend' : framework.type,
|
|
131
|
+
framework: framework.name,
|
|
132
|
+
port: framework.defaultPort || 3000,
|
|
133
|
+
start: {
|
|
134
|
+
command: framework.startCommand || 'npm start',
|
|
135
|
+
dev: framework.devCommand,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
if (framework.buildCommand) {
|
|
139
|
+
serviceConfig.build = {
|
|
140
|
+
command: framework.buildCommand,
|
|
141
|
+
output: framework.outputDir,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
services[scan.projectName] = serviceConfig;
|
|
145
|
+
}
|
|
146
|
+
return services;
|
|
147
|
+
}
|
|
148
|
+
getDefaultPort(type, framework) {
|
|
149
|
+
// Framework-specific defaults
|
|
150
|
+
if (framework === 'nextjs')
|
|
151
|
+
return 3000;
|
|
152
|
+
if (framework === 'react' || framework === 'react-admin')
|
|
153
|
+
return 5173;
|
|
154
|
+
if (framework === 'nestjs')
|
|
155
|
+
return 3050;
|
|
156
|
+
if (framework === 'express')
|
|
157
|
+
return 3000;
|
|
158
|
+
// Type-based defaults
|
|
159
|
+
if (type === 'frontend')
|
|
160
|
+
return 3000;
|
|
161
|
+
if (type === 'backend' || type === 'api')
|
|
162
|
+
return 3050;
|
|
163
|
+
return 3000;
|
|
164
|
+
}
|
|
165
|
+
generateWorkspaceConfig(scan) {
|
|
166
|
+
const typeMap = {
|
|
167
|
+
'monorepo-npm': 'npm',
|
|
168
|
+
'monorepo-pnpm': 'pnpm',
|
|
169
|
+
'monorepo-yarn': 'yarn',
|
|
170
|
+
'monorepo-nx': 'nx',
|
|
171
|
+
'monorepo-turborepo': 'turborepo',
|
|
172
|
+
'monorepo-lerna': 'lerna',
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
type: typeMap[scan.structure.type] || 'npm',
|
|
176
|
+
root: `/home/dev/${scan.projectName}`,
|
|
177
|
+
apps: scan.apps
|
|
178
|
+
.filter(a => a.type !== 'library')
|
|
179
|
+
.map(a => ({
|
|
180
|
+
name: a.name,
|
|
181
|
+
path: a.path,
|
|
182
|
+
framework: a.framework,
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
generateInfrastructure(scan) {
|
|
187
|
+
if (!scan.compose)
|
|
188
|
+
return undefined;
|
|
189
|
+
const infrastructure = {};
|
|
190
|
+
// Databases
|
|
191
|
+
if (scan.compose.databases.length > 0) {
|
|
192
|
+
infrastructure.databases = scan.compose.databases.map(db => {
|
|
193
|
+
const type = this.inferDatabaseType(db.image || db.name);
|
|
194
|
+
return {
|
|
195
|
+
type,
|
|
196
|
+
container: db.name,
|
|
197
|
+
name: scan.projectName,
|
|
198
|
+
port: db.ports[0]?.container || this.getDefaultDatabasePort(type),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Caches
|
|
203
|
+
if (scan.compose.caches.length > 0) {
|
|
204
|
+
infrastructure.caches = scan.compose.caches.map(cache => ({
|
|
205
|
+
type: this.inferCacheType(cache.image || cache.name),
|
|
206
|
+
container: cache.name,
|
|
207
|
+
port: cache.ports[0]?.container || 6379,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
// Queues
|
|
211
|
+
if (scan.compose.queues.length > 0) {
|
|
212
|
+
infrastructure.queues = scan.compose.queues.map(queue => {
|
|
213
|
+
const type = this.inferQueueType(queue.image || queue.name);
|
|
214
|
+
return {
|
|
215
|
+
type,
|
|
216
|
+
container: queue.name,
|
|
217
|
+
port: queue.ports[0]?.container || this.getDefaultQueuePort(type),
|
|
218
|
+
managementPort: this.getManagementPort(type, queue),
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return Object.keys(infrastructure).length > 0 ? infrastructure : undefined;
|
|
223
|
+
}
|
|
224
|
+
generateRepos(scan) {
|
|
225
|
+
if (!scan.git)
|
|
226
|
+
return {};
|
|
227
|
+
// Extract repo name from URL
|
|
228
|
+
let repoName = scan.projectName;
|
|
229
|
+
const match = scan.git.remote.match(/[:/]([^/]+\/)?([^/.]+)(\.git)?$/);
|
|
230
|
+
if (match) {
|
|
231
|
+
repoName = match[2];
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
[repoName]: {
|
|
235
|
+
url: scan.git.remote,
|
|
236
|
+
path: `/home/dev/${repoName}`,
|
|
237
|
+
branch: scan.git.branch !== 'main' && scan.git.branch !== 'master'
|
|
238
|
+
? scan.git.branch
|
|
239
|
+
: undefined,
|
|
240
|
+
auth: scan.git.type === 'https' ? 'token' : 'ssh',
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
generateHooks(scan) {
|
|
245
|
+
const hooks = {
|
|
246
|
+
postCheckout: [],
|
|
247
|
+
postStart: [],
|
|
248
|
+
};
|
|
249
|
+
// Install dependencies based on package managers
|
|
250
|
+
for (const runtime of scan.runtimes) {
|
|
251
|
+
const installCmd = this.getInstallCommand(runtime.language, runtime.packageManager);
|
|
252
|
+
if (installCmd) {
|
|
253
|
+
hooks.postCheckout.push({
|
|
254
|
+
command: installCmd,
|
|
255
|
+
workdir: `/home/dev/${scan.projectName}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Add docker compose up if docker is present
|
|
260
|
+
if (scan.compose) {
|
|
261
|
+
hooks.postStart.push({
|
|
262
|
+
command: 'docker compose up -d',
|
|
263
|
+
workdir: `/home/dev/${scan.projectName}`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Clean up empty arrays
|
|
267
|
+
if (hooks.postCheckout.length === 0)
|
|
268
|
+
delete hooks.postCheckout;
|
|
269
|
+
if (hooks.postStart.length === 0)
|
|
270
|
+
delete hooks.postStart;
|
|
271
|
+
return Object.keys(hooks).length > 0 ? hooks : {};
|
|
272
|
+
}
|
|
273
|
+
generateScripts(scan) {
|
|
274
|
+
return scan.scripts.map(script => ({
|
|
275
|
+
name: script.name,
|
|
276
|
+
path: script.path,
|
|
277
|
+
stage: script.stage,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
generateNetworking(scan, services) {
|
|
281
|
+
const expose = [];
|
|
282
|
+
// Expose all application services
|
|
283
|
+
for (const [name, config] of Object.entries(services)) {
|
|
284
|
+
// Frontend services get root subdomain
|
|
285
|
+
if (config.type === 'frontend') {
|
|
286
|
+
expose.push({
|
|
287
|
+
service: name,
|
|
288
|
+
port: config.port,
|
|
289
|
+
subdomain: '',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else if (config.type === 'gateway') {
|
|
293
|
+
expose.push({
|
|
294
|
+
service: name,
|
|
295
|
+
port: config.port,
|
|
296
|
+
subdomain: 'api',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (config.type === 'backend' || config.type === 'api') {
|
|
300
|
+
expose.push({
|
|
301
|
+
service: name,
|
|
302
|
+
port: config.port,
|
|
303
|
+
subdomain: name.replace(/-?(api|service|backend)$/i, '') || 'api',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Sort: frontends first, then by name
|
|
308
|
+
expose.sort((a, b) => {
|
|
309
|
+
if (a.subdomain === '' && b.subdomain !== '')
|
|
310
|
+
return -1;
|
|
311
|
+
if (a.subdomain !== '' && b.subdomain === '')
|
|
312
|
+
return 1;
|
|
313
|
+
return a.service.localeCompare(b.service);
|
|
314
|
+
});
|
|
315
|
+
return expose.length > 0 ? { expose } : undefined;
|
|
316
|
+
}
|
|
317
|
+
generateEnvTemplate(scan) {
|
|
318
|
+
const sections = [];
|
|
319
|
+
const warnings = [];
|
|
320
|
+
// Group variables by service
|
|
321
|
+
const serviceGroups = new Map();
|
|
322
|
+
for (const variable of [...scan.envAnalysis.required, ...scan.envAnalysis.optional]) {
|
|
323
|
+
if (variable.usedBy.length === 0 || variable.usedBy.length > 1) {
|
|
324
|
+
const global = serviceGroups.get('_global') || [];
|
|
325
|
+
global.push(variable);
|
|
326
|
+
serviceGroups.set('_global', global);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const service = variable.usedBy[0];
|
|
330
|
+
const group = serviceGroups.get(service) || [];
|
|
331
|
+
group.push(variable);
|
|
332
|
+
serviceGroups.set(service, group);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Global section
|
|
336
|
+
const globalVars = serviceGroups.get('_global') || [];
|
|
337
|
+
if (globalVars.length > 0) {
|
|
338
|
+
sections.push({
|
|
339
|
+
name: 'GLOBAL',
|
|
340
|
+
description: 'Shared configuration',
|
|
341
|
+
variables: globalVars.map(v => ({
|
|
342
|
+
name: v.name,
|
|
343
|
+
value: v.value || this.generateDefaultValue(v),
|
|
344
|
+
comment: v.description,
|
|
345
|
+
required: this.isRequired(v),
|
|
346
|
+
sensitive: false,
|
|
347
|
+
})),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Service sections
|
|
351
|
+
for (const [service, vars] of serviceGroups) {
|
|
352
|
+
if (service === '_global')
|
|
353
|
+
continue;
|
|
354
|
+
sections.push({
|
|
355
|
+
name: service.toUpperCase(),
|
|
356
|
+
description: `Configuration for ${service}`,
|
|
357
|
+
variables: vars.map(v => ({
|
|
358
|
+
name: v.name,
|
|
359
|
+
value: v.value || this.generateDefaultValue(v),
|
|
360
|
+
comment: v.description,
|
|
361
|
+
required: this.isRequired(v),
|
|
362
|
+
sensitive: false,
|
|
363
|
+
})),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
// Secrets section
|
|
367
|
+
if (scan.envAnalysis.secrets.length > 0) {
|
|
368
|
+
sections.push({
|
|
369
|
+
name: 'SECRETS',
|
|
370
|
+
description: 'Sensitive values - DO NOT COMMIT',
|
|
371
|
+
variables: scan.envAnalysis.secrets.map(v => ({
|
|
372
|
+
name: v.name,
|
|
373
|
+
value: v.value || 'your-secret-here',
|
|
374
|
+
comment: 'SENSITIVE - Replace with actual value',
|
|
375
|
+
required: true,
|
|
376
|
+
sensitive: true,
|
|
377
|
+
})),
|
|
378
|
+
});
|
|
379
|
+
warnings.push('Secrets detected - ensure .env.genbox is in .gitignore');
|
|
380
|
+
}
|
|
381
|
+
return { sections, warnings };
|
|
382
|
+
}
|
|
383
|
+
generateSetupScripts(scan, config) {
|
|
384
|
+
const scripts = [];
|
|
385
|
+
// Generate setup script
|
|
386
|
+
const setupContent = this.generateSetupScript(scan, config);
|
|
387
|
+
scripts.push({
|
|
388
|
+
path: 'scripts/setup-genbox.sh',
|
|
389
|
+
content: setupContent,
|
|
390
|
+
});
|
|
391
|
+
return scripts;
|
|
392
|
+
}
|
|
393
|
+
generateSetupScript(scan, config) {
|
|
394
|
+
const lines = [
|
|
395
|
+
'#!/bin/bash',
|
|
396
|
+
'# Generated by genbox init',
|
|
397
|
+
'set -e',
|
|
398
|
+
'',
|
|
399
|
+
`cd /home/dev/${scan.projectName} || exit 1`,
|
|
400
|
+
'',
|
|
401
|
+
'echo "Setting up development environment..."',
|
|
402
|
+
'',
|
|
403
|
+
];
|
|
404
|
+
// Install dependencies
|
|
405
|
+
for (const runtime of scan.runtimes) {
|
|
406
|
+
const installCmd = this.getInstallCommand(runtime.language, runtime.packageManager);
|
|
407
|
+
if (installCmd) {
|
|
408
|
+
lines.push(`echo "Installing ${runtime.language} dependencies..."`);
|
|
409
|
+
lines.push(installCmd);
|
|
410
|
+
lines.push('');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Start docker services
|
|
414
|
+
if (scan.compose) {
|
|
415
|
+
lines.push('echo "Starting Docker services..."');
|
|
416
|
+
// Check for different compose file variants
|
|
417
|
+
lines.push('if [ -f "docker-compose.secure.yml" ]; then');
|
|
418
|
+
lines.push(' docker compose -f docker-compose.secure.yml up -d');
|
|
419
|
+
lines.push('elif [ -f "docker-compose.yml" ]; then');
|
|
420
|
+
lines.push(' docker compose up -d');
|
|
421
|
+
lines.push('elif [ -f "compose.yml" ]; then');
|
|
422
|
+
lines.push(' docker compose up -d');
|
|
423
|
+
lines.push('fi');
|
|
424
|
+
lines.push('');
|
|
425
|
+
}
|
|
426
|
+
lines.push('echo "Setup complete!"');
|
|
427
|
+
return lines.join('\n');
|
|
428
|
+
}
|
|
429
|
+
inferServerSize(scan) {
|
|
430
|
+
// Count total services
|
|
431
|
+
let serviceCount = scan.apps.filter(a => a.type !== 'library').length;
|
|
432
|
+
if (scan.compose) {
|
|
433
|
+
serviceCount += scan.compose.applications.length;
|
|
434
|
+
serviceCount += scan.compose.databases.length;
|
|
435
|
+
serviceCount += scan.compose.caches.length;
|
|
436
|
+
serviceCount += scan.compose.queues.length;
|
|
437
|
+
}
|
|
438
|
+
// Check for heavy services
|
|
439
|
+
const hasHeavyService = scan.compose?.infrastructure.some(s => /elasticsearch|kafka|spark|hadoop/i.test(s.image || ''));
|
|
440
|
+
if (hasHeavyService || serviceCount > 10)
|
|
441
|
+
return 'xl';
|
|
442
|
+
if (serviceCount > 5)
|
|
443
|
+
return 'large';
|
|
444
|
+
if (serviceCount > 2)
|
|
445
|
+
return 'medium';
|
|
446
|
+
return 'small';
|
|
447
|
+
}
|
|
448
|
+
normalizeServiceName(name) {
|
|
449
|
+
return name
|
|
450
|
+
.replace(/[-_]service$/i, '')
|
|
451
|
+
.replace(/[-_]app$/i, '')
|
|
452
|
+
.toLowerCase()
|
|
453
|
+
.replace(/[^a-z0-9-]/g, '-');
|
|
454
|
+
}
|
|
455
|
+
inferServiceType(name) {
|
|
456
|
+
const lowerName = name.toLowerCase();
|
|
457
|
+
if (/gateway|proxy|ingress/.test(lowerName))
|
|
458
|
+
return 'gateway';
|
|
459
|
+
if (/frontend|web|ui|client/.test(lowerName))
|
|
460
|
+
return 'frontend';
|
|
461
|
+
if (/worker|queue|consumer|job/.test(lowerName))
|
|
462
|
+
return 'worker';
|
|
463
|
+
if (/api/.test(lowerName))
|
|
464
|
+
return 'api';
|
|
465
|
+
return 'backend';
|
|
466
|
+
}
|
|
467
|
+
extractHealthPath(healthcheck) {
|
|
468
|
+
// Extract path from curl commands like "curl -f http://localhost:3000/health"
|
|
469
|
+
const match = healthcheck.match(/https?:\/\/[^/]+(\S+)/);
|
|
470
|
+
return match ? match[1] : undefined;
|
|
471
|
+
}
|
|
472
|
+
wrapForMonorepo(command, appName, scan) {
|
|
473
|
+
const pm = scan.runtimes[0]?.packageManager;
|
|
474
|
+
switch (pm) {
|
|
475
|
+
case 'pnpm':
|
|
476
|
+
return `pnpm --filter ${appName} run ${command.split(' ')[0]}`;
|
|
477
|
+
case 'yarn':
|
|
478
|
+
return `yarn workspace ${appName} ${command}`;
|
|
479
|
+
case 'npm':
|
|
480
|
+
return `npm run ${command} --workspace=${appName}`;
|
|
481
|
+
default:
|
|
482
|
+
return command;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
getInstallCommand(language, packageManager) {
|
|
486
|
+
const commands = {
|
|
487
|
+
node: {
|
|
488
|
+
pnpm: 'pnpm install --frozen-lockfile',
|
|
489
|
+
yarn: 'yarn install --frozen-lockfile',
|
|
490
|
+
npm: 'npm ci',
|
|
491
|
+
bun: 'bun install --frozen-lockfile',
|
|
492
|
+
},
|
|
493
|
+
python: {
|
|
494
|
+
poetry: 'poetry install',
|
|
495
|
+
pipenv: 'pipenv install',
|
|
496
|
+
pip: 'pip install -r requirements.txt',
|
|
497
|
+
uv: 'uv sync',
|
|
498
|
+
},
|
|
499
|
+
go: {
|
|
500
|
+
go: 'go mod download',
|
|
501
|
+
},
|
|
502
|
+
ruby: {
|
|
503
|
+
bundler: 'bundle install',
|
|
504
|
+
},
|
|
505
|
+
rust: {
|
|
506
|
+
cargo: 'cargo fetch',
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
const langCommands = commands[language];
|
|
510
|
+
if (!langCommands)
|
|
511
|
+
return null;
|
|
512
|
+
return langCommands[packageManager || Object.keys(langCommands)[0]] || null;
|
|
513
|
+
}
|
|
514
|
+
inferDatabaseType(imageOrName) {
|
|
515
|
+
const lower = imageOrName.toLowerCase();
|
|
516
|
+
if (/mongo/i.test(lower))
|
|
517
|
+
return 'mongodb';
|
|
518
|
+
if (/postgres/i.test(lower))
|
|
519
|
+
return 'postgres';
|
|
520
|
+
if (/mysql|mariadb/i.test(lower))
|
|
521
|
+
return 'mysql';
|
|
522
|
+
return 'redis';
|
|
523
|
+
}
|
|
524
|
+
inferCacheType(imageOrName) {
|
|
525
|
+
if (/memcache/i.test(imageOrName))
|
|
526
|
+
return 'memcached';
|
|
527
|
+
return 'redis';
|
|
528
|
+
}
|
|
529
|
+
inferQueueType(imageOrName) {
|
|
530
|
+
const lower = imageOrName.toLowerCase();
|
|
531
|
+
if (/kafka/i.test(lower))
|
|
532
|
+
return 'kafka';
|
|
533
|
+
if (/redis/i.test(lower))
|
|
534
|
+
return 'redis';
|
|
535
|
+
return 'rabbitmq';
|
|
536
|
+
}
|
|
537
|
+
getDefaultDatabasePort(type) {
|
|
538
|
+
const ports = {
|
|
539
|
+
mongodb: 27017,
|
|
540
|
+
postgres: 5432,
|
|
541
|
+
mysql: 3306,
|
|
542
|
+
redis: 6379,
|
|
543
|
+
};
|
|
544
|
+
return ports[type] || 3000;
|
|
545
|
+
}
|
|
546
|
+
getDefaultQueuePort(type) {
|
|
547
|
+
const ports = {
|
|
548
|
+
rabbitmq: 5672,
|
|
549
|
+
kafka: 9092,
|
|
550
|
+
redis: 6379,
|
|
551
|
+
};
|
|
552
|
+
return ports[type] || 5672;
|
|
553
|
+
}
|
|
554
|
+
getManagementPort(type, service) {
|
|
555
|
+
if (type === 'rabbitmq') {
|
|
556
|
+
// Look for management port (15672)
|
|
557
|
+
const mgmtPort = service.ports.find(p => p.container === 15672);
|
|
558
|
+
return mgmtPort?.host || 15672;
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
generateDefaultValue(variable) {
|
|
563
|
+
if (variable.name.includes('MONGO'))
|
|
564
|
+
return 'mongodb://localhost:27017/myapp';
|
|
565
|
+
if (variable.name.includes('POSTGRES'))
|
|
566
|
+
return 'postgresql://localhost:5432/myapp';
|
|
567
|
+
if (variable.name.includes('REDIS'))
|
|
568
|
+
return 'redis://localhost:6379';
|
|
569
|
+
if (variable.name.includes('RABBIT'))
|
|
570
|
+
return 'amqp://localhost:5672';
|
|
571
|
+
if (variable.name.includes('PORT'))
|
|
572
|
+
return '3000';
|
|
573
|
+
if (variable.type === 'boolean')
|
|
574
|
+
return 'false';
|
|
575
|
+
if (variable.type === 'number')
|
|
576
|
+
return '0';
|
|
577
|
+
return '';
|
|
578
|
+
}
|
|
579
|
+
isRequired(variable) {
|
|
580
|
+
const requiredPatterns = [
|
|
581
|
+
/^DATABASE/i,
|
|
582
|
+
/^MONGO/i,
|
|
583
|
+
/^POSTGRES/i,
|
|
584
|
+
/^REDIS/i,
|
|
585
|
+
/^JWT_SECRET$/i,
|
|
586
|
+
/^NODE_ENV$/i,
|
|
587
|
+
];
|
|
588
|
+
return requiredPatterns.some(p => p.test(variable.name));
|
|
589
|
+
}
|
|
590
|
+
collectWarnings(scan) {
|
|
591
|
+
const warnings = [];
|
|
592
|
+
// Check for missing versions
|
|
593
|
+
for (const runtime of scan.runtimes) {
|
|
594
|
+
if (!runtime.version) {
|
|
595
|
+
warnings.push(`No version detected for ${runtime.language} - using 'latest'`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Check for port conflicts
|
|
599
|
+
if (scan.compose) {
|
|
600
|
+
const portUsage = new Map();
|
|
601
|
+
for (const [port, service] of scan.compose.portMap) {
|
|
602
|
+
const existing = portUsage.get(port) || [];
|
|
603
|
+
existing.push(service);
|
|
604
|
+
portUsage.set(port, existing);
|
|
605
|
+
}
|
|
606
|
+
for (const [port, services] of portUsage) {
|
|
607
|
+
if (services.length > 1) {
|
|
608
|
+
warnings.push(`Port ${port} used by multiple services: ${services.join(', ')}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Check for missing env variables
|
|
613
|
+
const missingSecrets = scan.envAnalysis.secrets.filter(s => !s.value);
|
|
614
|
+
if (missingSecrets.length > 0) {
|
|
615
|
+
warnings.push(`${missingSecrets.length} secret(s) need values: ${missingSecrets.map(s => s.name).join(', ')}`);
|
|
616
|
+
}
|
|
617
|
+
return warnings;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
exports.ConfigGenerator = ConfigGenerator;
|