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.
@@ -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;