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.
- package/dist/commands/create.js +369 -130
- package/dist/commands/db-sync.js +364 -0
- 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/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 +2 -1
|
@@ -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;
|