genbox 1.0.3 → 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,547 @@
1
+ "use strict";
2
+ /**
3
+ * Profile Resolver
4
+ *
5
+ * Handles:
6
+ * - Profile resolution with inheritance
7
+ * - CLI flag merging
8
+ * - Dependency resolution with prompts
9
+ * - Interactive app selection
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.ProfileResolver = void 0;
49
+ const prompts = __importStar(require("@inquirer/prompts"));
50
+ const chalk_1 = __importDefault(require("chalk"));
51
+ const config_loader_1 = require("./config-loader");
52
+ class ProfileResolver {
53
+ configLoader;
54
+ constructor(configLoader) {
55
+ this.configLoader = configLoader || new config_loader_1.ConfigLoader();
56
+ }
57
+ /**
58
+ * Resolve configuration from options (CLI flags, profile, interactive)
59
+ */
60
+ async resolve(config, options) {
61
+ const warnings = [];
62
+ // Step 1: Get base profile (if specified)
63
+ let profile = {};
64
+ if (options.profile) {
65
+ const foundProfile = this.configLoader.getProfile(config, options.profile);
66
+ if (foundProfile) {
67
+ profile = foundProfile;
68
+ }
69
+ else {
70
+ warnings.push(`Profile '${options.profile}' not found, using defaults`);
71
+ }
72
+ }
73
+ // Step 2: Determine apps to include
74
+ let selectedApps;
75
+ if (options.apps) {
76
+ // CLI --apps flag takes precedence
77
+ selectedApps = options.apps;
78
+ }
79
+ else if (profile.apps && profile.apps.length > 0) {
80
+ // Use profile apps
81
+ selectedApps = profile.apps;
82
+ // Add additional apps if --add-apps specified
83
+ if (options.addApps) {
84
+ selectedApps = [...new Set([...selectedApps, ...options.addApps])];
85
+ }
86
+ }
87
+ else if (!options.yes) {
88
+ // Interactive mode
89
+ selectedApps = await this.selectAppsInteractive(config);
90
+ }
91
+ else {
92
+ // Default: all non-library apps
93
+ selectedApps = Object.entries(config.apps)
94
+ .filter(([_, app]) => app.type !== 'library')
95
+ .map(([name]) => name);
96
+ }
97
+ // Step 3: Resolve dependencies
98
+ const { apps, infrastructure, dependencyWarnings } = await this.resolveDependencies(config, selectedApps, options, profile);
99
+ warnings.push(...dependencyWarnings);
100
+ // Step 4: Determine database mode
101
+ const database = await this.resolveDatabaseMode(config, options, profile);
102
+ // Step 5: Determine size
103
+ const size = options.size || profile.size || this.inferSize(apps, infrastructure);
104
+ // Step 6: Build resolved config
105
+ const resolved = {
106
+ name: options.name,
107
+ size,
108
+ project: {
109
+ name: config.project.name,
110
+ structure: config.project.structure,
111
+ },
112
+ apps,
113
+ infrastructure,
114
+ database,
115
+ repos: this.resolveRepos(config, apps),
116
+ env: this.resolveEnvVars(config, apps, infrastructure, database),
117
+ hooks: config.hooks || {},
118
+ profile: options.profile,
119
+ warnings,
120
+ };
121
+ return resolved;
122
+ }
123
+ /**
124
+ * Interactive app selection
125
+ */
126
+ async selectAppsInteractive(config) {
127
+ const appChoices = Object.entries(config.apps)
128
+ .filter(([_, app]) => app.type !== 'library')
129
+ .map(([name, app]) => ({
130
+ name: `${name} - ${app.description || app.type} (${app.framework || 'custom'})`,
131
+ value: name,
132
+ checked: app.type === 'frontend', // Pre-select frontends
133
+ }));
134
+ if (appChoices.length === 0) {
135
+ console.log(chalk_1.default.yellow('No apps defined in configuration'));
136
+ return [];
137
+ }
138
+ console.log(chalk_1.default.cyan('\nšŸ“¦ Available Apps:\n'));
139
+ const selected = await prompts.checkbox({
140
+ message: 'Select apps to include in your genbox:',
141
+ choices: appChoices,
142
+ });
143
+ return selected;
144
+ }
145
+ /**
146
+ * Resolve dependencies for selected apps
147
+ */
148
+ async resolveDependencies(config, selectedApps, options, profile) {
149
+ const resolvedApps = [];
150
+ const resolvedInfra = [];
151
+ const warnings = [];
152
+ const processedDeps = new Set();
153
+ // Default environment for dependencies
154
+ const defaultEnv = profile.connect_to || options.api;
155
+ for (const appName of selectedApps) {
156
+ const appConfig = config.apps[appName];
157
+ if (!appConfig) {
158
+ warnings.push(`App '${appName}' not found in configuration`);
159
+ continue;
160
+ }
161
+ const resolvedApp = await this.resolveApp(config, appName, appConfig, selectedApps, defaultEnv, options, processedDeps);
162
+ resolvedApps.push(resolvedApp);
163
+ }
164
+ // Collect infrastructure needs
165
+ for (const app of resolvedApps) {
166
+ for (const [depName, depConfig] of Object.entries(app.dependencies)) {
167
+ if (processedDeps.has(depName))
168
+ continue;
169
+ const infraConfig = config.infrastructure?.[depName];
170
+ if (infraConfig) {
171
+ resolvedInfra.push({
172
+ name: depName,
173
+ type: infraConfig.type,
174
+ mode: depConfig.mode,
175
+ image: depConfig.mode === 'local' ? infraConfig.image : undefined,
176
+ port: depConfig.mode === 'local' ? infraConfig.port : undefined,
177
+ environment: depConfig.environment,
178
+ url: depConfig.url,
179
+ });
180
+ processedDeps.add(depName);
181
+ }
182
+ }
183
+ }
184
+ return { apps: resolvedApps, infrastructure: resolvedInfra, dependencyWarnings: warnings };
185
+ }
186
+ /**
187
+ * Resolve a single app with its dependencies
188
+ */
189
+ async resolveApp(config, appName, appConfig, selectedApps, defaultEnv, options, processedDeps) {
190
+ const dependencies = {};
191
+ // Process each dependency
192
+ for (const [depName, requirement] of Object.entries(appConfig.requires || {})) {
193
+ if (requirement === 'none')
194
+ continue;
195
+ // Check if dependency is already in selected apps
196
+ const isLocalApp = selectedApps.includes(depName);
197
+ // Check if it's infrastructure
198
+ const isInfra = config.infrastructure?.[depName];
199
+ if (isLocalApp) {
200
+ // Dependency is included locally
201
+ dependencies[depName] = { mode: 'local' };
202
+ }
203
+ else if (defaultEnv && config.environments?.[defaultEnv]) {
204
+ // Use default environment
205
+ const envConfig = config.environments[defaultEnv];
206
+ const url = this.getUrlForDependency(depName, envConfig);
207
+ dependencies[depName] = {
208
+ mode: 'remote',
209
+ environment: defaultEnv,
210
+ url,
211
+ };
212
+ }
213
+ else if (requirement === 'required' && !options.yes) {
214
+ // Prompt user for resolution
215
+ const resolution = await this.promptDependencyResolution(config, appName, depName, isInfra ? 'infrastructure' : 'app');
216
+ dependencies[depName] = resolution;
217
+ }
218
+ else if (requirement === 'optional') {
219
+ // Skip optional dependencies if not explicitly included
220
+ continue;
221
+ }
222
+ else {
223
+ // Default to local if no other option
224
+ dependencies[depName] = { mode: 'local' };
225
+ }
226
+ processedDeps.add(depName);
227
+ }
228
+ return {
229
+ name: appName,
230
+ path: appConfig.path,
231
+ type: appConfig.type,
232
+ framework: appConfig.framework,
233
+ port: appConfig.port,
234
+ services: appConfig.services,
235
+ commands: appConfig.commands,
236
+ env: appConfig.env,
237
+ dependencies,
238
+ };
239
+ }
240
+ /**
241
+ * Prompt user for dependency resolution
242
+ */
243
+ async promptDependencyResolution(config, appName, depName, depType) {
244
+ console.log(chalk_1.default.yellow(`\n⚠ '${appName}' requires '${depName}'`));
245
+ const choices = [];
246
+ // Local option
247
+ if (depType === 'app') {
248
+ choices.push({
249
+ name: `Include '${depName}' locally`,
250
+ value: 'local',
251
+ });
252
+ }
253
+ else {
254
+ choices.push({
255
+ name: `Run '${depName}' locally (Docker)`,
256
+ value: 'local',
257
+ });
258
+ }
259
+ // Environment options
260
+ for (const [envName, envConfig] of Object.entries(config.environments || {})) {
261
+ choices.push({
262
+ name: `Connect to ${envName} ${envConfig.description ? `(${envConfig.description})` : ''}`,
263
+ value: envName,
264
+ });
265
+ }
266
+ // Skip option
267
+ choices.push({
268
+ name: 'Skip (app may not work correctly)',
269
+ value: 'skip',
270
+ });
271
+ const answer = await prompts.select({
272
+ message: `How should we handle '${depName}'?`,
273
+ choices,
274
+ });
275
+ if (answer === 'local') {
276
+ return { mode: 'local' };
277
+ }
278
+ else if (answer === 'skip') {
279
+ return { mode: 'remote' }; // Effectively skipped
280
+ }
281
+ else {
282
+ const envConfig = config.environments?.[answer];
283
+ return {
284
+ mode: 'remote',
285
+ environment: answer,
286
+ url: envConfig ? this.getUrlForDependency(depName, envConfig) : undefined,
287
+ };
288
+ }
289
+ }
290
+ /**
291
+ * Get URL for a dependency from environment config
292
+ */
293
+ getUrlForDependency(depName, envConfig) {
294
+ // Check if it's an API dependency
295
+ if (depName === 'api' && envConfig.api) {
296
+ return envConfig.api.url || envConfig.api.gateway;
297
+ }
298
+ // Check infrastructure
299
+ const infraConfig = envConfig[depName];
300
+ return infraConfig?.url;
301
+ }
302
+ /**
303
+ * Resolve database mode
304
+ */
305
+ async resolveDatabaseMode(config, options, profile) {
306
+ // CLI flag takes precedence
307
+ if (options.db) {
308
+ return {
309
+ mode: options.db,
310
+ source: options.dbSource || profile.database?.source || 'staging',
311
+ };
312
+ }
313
+ // Profile setting
314
+ if (profile.database) {
315
+ return {
316
+ mode: profile.database.mode,
317
+ source: profile.database.source,
318
+ };
319
+ }
320
+ // If connect_to is set, use remote
321
+ if (profile.connect_to) {
322
+ const envConfig = config.environments?.[profile.connect_to];
323
+ return {
324
+ mode: 'remote',
325
+ source: profile.connect_to,
326
+ url: envConfig?.mongodb?.url,
327
+ };
328
+ }
329
+ // Interactive mode
330
+ if (!options.yes) {
331
+ return await this.selectDatabaseModeInteractive(config);
332
+ }
333
+ // Default
334
+ return {
335
+ mode: config.defaults?.database?.mode || 'local',
336
+ source: config.defaults?.database?.source,
337
+ };
338
+ }
339
+ /**
340
+ * Interactive database mode selection
341
+ */
342
+ async selectDatabaseModeInteractive(config) {
343
+ console.log(chalk_1.default.cyan('\nšŸ—„ļø Database Configuration:\n'));
344
+ const modeChoices = [
345
+ { name: 'None (no database)', value: 'none' },
346
+ { name: 'Local empty database', value: 'local' },
347
+ { name: 'Copy from staging (snapshot)', value: 'copy-staging' },
348
+ ];
349
+ // Add production copy if available
350
+ if (config.environments?.production) {
351
+ modeChoices.push({
352
+ name: 'Copy from production (snapshot)',
353
+ value: 'copy-production',
354
+ });
355
+ }
356
+ // Add remote options
357
+ if (config.environments?.staging) {
358
+ modeChoices.push({
359
+ name: 'Connect to staging (remote)',
360
+ value: 'remote-staging',
361
+ });
362
+ }
363
+ const answer = await prompts.select({
364
+ message: 'Database mode:',
365
+ choices: modeChoices,
366
+ });
367
+ if (answer === 'none') {
368
+ return { mode: 'none' };
369
+ }
370
+ else if (answer === 'local') {
371
+ return { mode: 'local' };
372
+ }
373
+ else if (answer.startsWith('copy-')) {
374
+ const source = answer.replace('copy-', '');
375
+ return { mode: 'copy', source };
376
+ }
377
+ else if (answer.startsWith('remote-')) {
378
+ const source = answer.replace('remote-', '');
379
+ const envConfig = config.environments?.[source];
380
+ return {
381
+ mode: 'remote',
382
+ source,
383
+ url: envConfig?.mongodb?.url,
384
+ };
385
+ }
386
+ return { mode: 'local' };
387
+ }
388
+ /**
389
+ * Infer server size based on selected apps and infrastructure
390
+ */
391
+ inferSize(apps, infrastructure) {
392
+ const localApps = apps.length;
393
+ const localInfra = infrastructure.filter(i => i.mode === 'local').length;
394
+ const total = localApps + localInfra;
395
+ // Check for heavy services
396
+ const hasHeavyService = apps.some(a => a.services && Object.keys(a.services).length > 3);
397
+ if (total > 8 || hasHeavyService)
398
+ return 'xl';
399
+ if (total > 5)
400
+ return 'large';
401
+ if (total > 2)
402
+ return 'medium';
403
+ return 'small';
404
+ }
405
+ /**
406
+ * Resolve repositories to clone
407
+ */
408
+ resolveRepos(config, apps) {
409
+ const repos = [];
410
+ const seen = new Set();
411
+ for (const app of apps) {
412
+ const appConfig = config.apps[app.name];
413
+ // Check if app has specific repo
414
+ if (appConfig?.repo && config.repos?.[appConfig.repo]) {
415
+ const repoConfig = config.repos[appConfig.repo];
416
+ if (!seen.has(repoConfig.url)) {
417
+ repos.push({
418
+ name: appConfig.repo,
419
+ url: repoConfig.url,
420
+ path: repoConfig.path,
421
+ branch: repoConfig.branch,
422
+ });
423
+ seen.add(repoConfig.url);
424
+ }
425
+ }
426
+ }
427
+ // If no specific repos, use main project repo
428
+ if (repos.length === 0 && config.repos) {
429
+ const mainRepo = Object.entries(config.repos)[0];
430
+ if (mainRepo) {
431
+ repos.push({
432
+ name: mainRepo[0],
433
+ url: mainRepo[1].url,
434
+ path: mainRepo[1].path,
435
+ branch: mainRepo[1].branch,
436
+ });
437
+ }
438
+ }
439
+ return repos;
440
+ }
441
+ /**
442
+ * Resolve environment variables
443
+ */
444
+ resolveEnvVars(config, apps, infrastructure, database) {
445
+ const env = {};
446
+ // Add API URL based on resolution
447
+ for (const app of apps) {
448
+ const apiDep = app.dependencies['api'];
449
+ if (apiDep) {
450
+ if (apiDep.mode === 'local') {
451
+ env['API_URL'] = 'http://localhost:3050';
452
+ env['VITE_API_URL'] = 'http://localhost:3050';
453
+ env['NEXT_PUBLIC_API_URL'] = 'http://localhost:3050';
454
+ }
455
+ else if (apiDep.url) {
456
+ env['API_URL'] = apiDep.url;
457
+ env['VITE_API_URL'] = apiDep.url;
458
+ env['NEXT_PUBLIC_API_URL'] = apiDep.url;
459
+ }
460
+ }
461
+ }
462
+ // Add database URL
463
+ if (database.mode === 'local') {
464
+ env['MONGODB_URI'] = `mongodb://localhost:27017/${config.project.name}`;
465
+ }
466
+ else if (database.url) {
467
+ env['MONGODB_URI'] = database.url;
468
+ }
469
+ // Add infrastructure URLs
470
+ for (const infra of infrastructure) {
471
+ if (infra.mode === 'local') {
472
+ switch (infra.type) {
473
+ case 'cache':
474
+ env['REDIS_URL'] = `redis://localhost:${infra.port || 6379}`;
475
+ break;
476
+ case 'queue':
477
+ env['RABBITMQ_URL'] = `amqp://localhost:${infra.port || 5672}`;
478
+ break;
479
+ }
480
+ }
481
+ else if (infra.url) {
482
+ switch (infra.type) {
483
+ case 'cache':
484
+ env['REDIS_URL'] = infra.url;
485
+ break;
486
+ case 'queue':
487
+ env['RABBITMQ_URL'] = infra.url;
488
+ break;
489
+ }
490
+ }
491
+ }
492
+ return env;
493
+ }
494
+ /**
495
+ * Interactive size selection
496
+ */
497
+ async selectSizeInteractive() {
498
+ console.log(chalk_1.default.cyan('\nšŸ“Š Server Size:\n'));
499
+ const answer = await prompts.select({
500
+ message: 'Select server size:',
501
+ choices: [
502
+ { name: 'Small - 2 CPU, 4GB RAM (UI only)', value: 'small' },
503
+ { name: 'Medium - 4 CPU, 8GB RAM (1-2 services)', value: 'medium' },
504
+ { name: 'Large - 8 CPU, 16GB RAM (full stack)', value: 'large' },
505
+ { name: 'XL - 16 CPU, 32GB RAM (heavy workloads)', value: 'xl' },
506
+ ],
507
+ });
508
+ return answer;
509
+ }
510
+ /**
511
+ * Ask if user wants to save as profile
512
+ */
513
+ async askSaveProfile(config, resolved) {
514
+ const save = await prompts.confirm({
515
+ message: 'Save this configuration as a reusable profile?',
516
+ default: false,
517
+ });
518
+ if (!save)
519
+ return null;
520
+ const name = await prompts.input({
521
+ message: 'Profile name:',
522
+ validate: (value) => {
523
+ if (!value.trim())
524
+ return 'Profile name is required';
525
+ if (config.profiles?.[value])
526
+ return 'Profile already exists';
527
+ if (!/^[a-z0-9-]+$/.test(value))
528
+ return 'Use lowercase letters, numbers, and hyphens';
529
+ return true;
530
+ },
531
+ });
532
+ // Build profile
533
+ const profile = {
534
+ description: `Created for ${resolved.name}`,
535
+ size: resolved.size,
536
+ apps: resolved.apps.map(a => a.name),
537
+ database: {
538
+ mode: resolved.database.mode,
539
+ source: resolved.database.source,
540
+ },
541
+ };
542
+ // Save to user profiles
543
+ this.configLoader.saveUserProfile(name, profile);
544
+ return name;
545
+ }
546
+ }
547
+ exports.ProfileResolver = ProfileResolver;