genbox 1.0.11 → 1.0.13

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,273 @@
1
+ "use strict";
2
+ /**
3
+ * Config Command
4
+ *
5
+ * Provides configuration inspection and debugging tools:
6
+ * - genbox config show - Show resolved configuration
7
+ * - genbox config show --explain - Show where each value comes from
8
+ * - genbox config diff <a> <b> - Compare two profiles
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.configCommand = void 0;
48
+ const commander_1 = require("commander");
49
+ const chalk_1 = __importDefault(require("chalk"));
50
+ const yaml = __importStar(require("js-yaml"));
51
+ const config_loader_1 = require("../config-loader");
52
+ const config_explainer_1 = require("../config-explainer");
53
+ exports.configCommand = new commander_1.Command('config')
54
+ .description('Inspect and debug configuration')
55
+ .addCommand(createShowCommand())
56
+ .addCommand(createDiffCommand());
57
+ function createShowCommand() {
58
+ return new commander_1.Command('show')
59
+ .description('Show resolved configuration')
60
+ .argument('[path]', 'Specific config path to show (e.g., apps.web, profiles.quick)')
61
+ .option('--explain', 'Show where each value comes from')
62
+ .option('--profile <profile>', 'Show configuration for a specific profile')
63
+ .option('--json', 'Output as JSON')
64
+ .option('--yaml', 'Output as YAML (default)')
65
+ .action(async (configPath, options) => {
66
+ const configLoader = new config_loader_1.ConfigLoader();
67
+ const loadResult = await configLoader.load();
68
+ if (!loadResult.found || !loadResult.config) {
69
+ console.log(chalk_1.default.red('No genbox.yaml found in current directory or parent directories'));
70
+ process.exit(1);
71
+ }
72
+ const config = loadResult.config;
73
+ if (options.explain) {
74
+ await showWithExplain(config, loadResult, configPath, options.profile);
75
+ }
76
+ else {
77
+ await showConfig(config, configPath, options);
78
+ }
79
+ });
80
+ }
81
+ function createDiffCommand() {
82
+ return new commander_1.Command('diff')
83
+ .description('Compare two profiles')
84
+ .argument('<profile1>', 'First profile name')
85
+ .argument('<profile2>', 'Second profile name')
86
+ .action(async (profile1, profile2) => {
87
+ const configLoader = new config_loader_1.ConfigLoader();
88
+ const loadResult = await configLoader.load();
89
+ if (!loadResult.found || !loadResult.config) {
90
+ console.log(chalk_1.default.red('No genbox.yaml found'));
91
+ process.exit(1);
92
+ }
93
+ const config = loadResult.config;
94
+ const p1 = configLoader.getProfile(config, profile1);
95
+ const p2 = configLoader.getProfile(config, profile2);
96
+ if (!p1) {
97
+ console.log(chalk_1.default.red(`Profile '${profile1}' not found`));
98
+ process.exit(1);
99
+ }
100
+ if (!p2) {
101
+ console.log(chalk_1.default.red(`Profile '${profile2}' not found`));
102
+ process.exit(1);
103
+ }
104
+ showProfileDiff(profile1, p1, profile2, p2, config);
105
+ });
106
+ }
107
+ async function showConfig(config, path, options) {
108
+ let data = config;
109
+ // If profile specified, show that profile's resolved config
110
+ if (options.profile) {
111
+ const configLoader = new config_loader_1.ConfigLoader();
112
+ const profile = configLoader.getProfile(config, options.profile);
113
+ if (!profile) {
114
+ console.log(chalk_1.default.red(`Profile '${options.profile}' not found`));
115
+ process.exit(1);
116
+ }
117
+ data = profile;
118
+ }
119
+ // Navigate to specific path if provided
120
+ if (path) {
121
+ const parts = path.split('.');
122
+ let current = data;
123
+ for (const part of parts) {
124
+ if (current && typeof current === 'object' && part in current) {
125
+ current = current[part];
126
+ }
127
+ else {
128
+ console.log(chalk_1.default.red(`Path '${path}' not found in configuration`));
129
+ process.exit(1);
130
+ }
131
+ }
132
+ data = current;
133
+ }
134
+ // Output
135
+ if (options.json) {
136
+ console.log(JSON.stringify(data, null, 2));
137
+ }
138
+ else {
139
+ console.log(yaml.dump(data, { lineWidth: 120, noRefs: true }));
140
+ }
141
+ }
142
+ async function showWithExplain(config, loadResult, path, profileName) {
143
+ const explainer = new config_explainer_1.ConfigExplainer(loadResult);
144
+ console.log(chalk_1.default.bold.cyan('\n📋 Configuration Analysis\n'));
145
+ // Show sources
146
+ console.log(chalk_1.default.bold('Config Sources (priority order):'));
147
+ for (const source of loadResult.sources) {
148
+ const icon = source.type === 'workspace' ? '🏢' : source.type === 'project' ? '📁' : '👤';
149
+ console.log(` ${icon} ${source.type}: ${source.path}`);
150
+ }
151
+ console.log();
152
+ // If path specified, show just that path
153
+ if (path) {
154
+ const explanation = explainer.explain(path, profileName);
155
+ printExplanation(path, explanation);
156
+ return;
157
+ }
158
+ // Show key configuration values with explanations
159
+ console.log(chalk_1.default.bold('Key Values:\n'));
160
+ // Project info
161
+ printExplanation('project.name', explainer.explain('project.name'));
162
+ printExplanation('project.structure', explainer.explain('project.structure'));
163
+ // Apps
164
+ if (config.apps) {
165
+ console.log(chalk_1.default.bold('\nApps:'));
166
+ for (const appName of Object.keys(config.apps)) {
167
+ console.log(chalk_1.default.cyan(`\n ${appName}:`));
168
+ printExplanation(` apps.${appName}.type`, explainer.explain(`apps.${appName}.type`), 4);
169
+ printExplanation(` apps.${appName}.port`, explainer.explain(`apps.${appName}.port`), 4);
170
+ printExplanation(` apps.${appName}.framework`, explainer.explain(`apps.${appName}.framework`), 4);
171
+ }
172
+ }
173
+ // Profiles
174
+ if (config.profiles && profileName) {
175
+ console.log(chalk_1.default.bold(`\nProfile '${profileName}':`));
176
+ printExplanation(' size', explainer.explain('size', profileName), 4);
177
+ printExplanation(' apps', explainer.explain('apps', profileName), 4);
178
+ printExplanation(' connect_to', explainer.explain('connect_to', profileName), 4);
179
+ printExplanation(' database.mode', explainer.explain('database.mode', profileName), 4);
180
+ }
181
+ // Defaults
182
+ if (config.defaults) {
183
+ console.log(chalk_1.default.bold('\nDefaults:'));
184
+ printExplanation(' defaults.size', explainer.explain('defaults.size'), 4);
185
+ printExplanation(' defaults.branch', explainer.explain('defaults.branch'), 4);
186
+ }
187
+ // Detection warnings
188
+ const warnings = explainer.getDetectionWarnings();
189
+ if (warnings.length > 0) {
190
+ console.log(chalk_1.default.bold.yellow('\n⚠️ Detection Warnings:\n'));
191
+ for (const warning of warnings) {
192
+ console.log(chalk_1.default.yellow(` • ${warning}`));
193
+ }
194
+ }
195
+ console.log();
196
+ }
197
+ function printExplanation(path, sources, indent = 0) {
198
+ const pad = ' '.repeat(indent);
199
+ if (sources.length === 0) {
200
+ console.log(`${pad}${chalk_1.default.dim(path)}: ${chalk_1.default.dim('(not set)')}`);
201
+ return;
202
+ }
203
+ const finalSource = sources.find(s => s.used) || sources[0];
204
+ const valueStr = formatValue(finalSource.value);
205
+ console.log(`${pad}${chalk_1.default.white(path)}: ${chalk_1.default.green(valueStr)}`);
206
+ for (const source of sources) {
207
+ const icon = source.used ? chalk_1.default.green('✓') : chalk_1.default.dim('○');
208
+ const sourceLabel = getSourceLabel(source);
209
+ const value = source.value !== undefined ? formatValue(source.value) : chalk_1.default.dim('(not set)');
210
+ if (source.used) {
211
+ console.log(`${pad} ${icon} ${sourceLabel}: ${value}`);
212
+ if (source.detectedFrom) {
213
+ console.log(`${pad} └─ ${chalk_1.default.dim(`detected from: ${source.detectedFrom}`)}`);
214
+ }
215
+ }
216
+ else {
217
+ console.log(`${pad} ${icon} ${chalk_1.default.dim(sourceLabel)}: ${chalk_1.default.dim(String(value))}`);
218
+ }
219
+ }
220
+ }
221
+ function getSourceLabel(source) {
222
+ switch (source.source) {
223
+ case 'cli':
224
+ return 'CLI flag';
225
+ case 'profile':
226
+ return `Profile '${source.profileName || 'unknown'}'`;
227
+ case 'project':
228
+ return 'genbox.yaml (explicit)';
229
+ case 'workspace':
230
+ return 'workspace genbox.yaml';
231
+ case 'detected':
232
+ return 'detected.yaml';
233
+ case 'default':
234
+ return 'schema default';
235
+ case 'inferred':
236
+ return 'inferred';
237
+ default:
238
+ return source.source;
239
+ }
240
+ }
241
+ function formatValue(value) {
242
+ if (value === undefined)
243
+ return '(undefined)';
244
+ if (value === null)
245
+ return '(null)';
246
+ if (Array.isArray(value))
247
+ return `[${value.join(', ')}]`;
248
+ if (typeof value === 'object')
249
+ return JSON.stringify(value);
250
+ return String(value);
251
+ }
252
+ function showProfileDiff(name1, profile1, name2, profile2, config) {
253
+ console.log(chalk_1.default.bold.cyan(`\n📊 Comparing profiles: ${name1} vs ${name2}\n`));
254
+ const allKeys = new Set([
255
+ ...Object.keys(profile1),
256
+ ...Object.keys(profile2),
257
+ ]);
258
+ let hasDiff = false;
259
+ for (const key of allKeys) {
260
+ const v1 = profile1[key];
261
+ const v2 = profile2[key];
262
+ if (JSON.stringify(v1) !== JSON.stringify(v2)) {
263
+ hasDiff = true;
264
+ console.log(chalk_1.default.bold(` ${key}:`));
265
+ console.log(chalk_1.default.red(` - ${name1}: ${formatValue(v1)}`));
266
+ console.log(chalk_1.default.green(` + ${name2}: ${formatValue(v2)}`));
267
+ console.log();
268
+ }
269
+ }
270
+ if (!hasDiff) {
271
+ console.log(chalk_1.default.dim(' No differences found'));
272
+ }
273
+ }
@@ -348,6 +348,64 @@ function displayResolvedConfig(resolved) {
348
348
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
349
349
  console.log('');
350
350
  }
351
+ /**
352
+ * Parse .env.genbox file into segregated sections
353
+ */
354
+ function parseEnvGenboxSections(content) {
355
+ const sections = new Map();
356
+ let currentSection = 'GLOBAL';
357
+ let currentContent = [];
358
+ for (const line of content.split('\n')) {
359
+ const sectionMatch = line.match(/^# === ([^=]+) ===$/);
360
+ if (sectionMatch) {
361
+ // Save previous section
362
+ if (currentContent.length > 0) {
363
+ sections.set(currentSection, currentContent.join('\n').trim());
364
+ }
365
+ currentSection = sectionMatch[1].trim();
366
+ currentContent = [];
367
+ }
368
+ else if (currentSection !== 'END') {
369
+ currentContent.push(line);
370
+ }
371
+ }
372
+ // Save last section
373
+ if (currentContent.length > 0 && currentSection !== 'END') {
374
+ sections.set(currentSection, currentContent.join('\n').trim());
375
+ }
376
+ return sections;
377
+ }
378
+ /**
379
+ * Build env content for a specific app by combining GLOBAL + app-specific sections
380
+ */
381
+ function buildAppEnvContent(sections, appName, apiUrl) {
382
+ const parts = [];
383
+ // Always include GLOBAL section
384
+ const globalSection = sections.get('GLOBAL');
385
+ if (globalSection) {
386
+ parts.push(globalSection);
387
+ }
388
+ // Include app-specific section if exists
389
+ const appSection = sections.get(appName);
390
+ if (appSection) {
391
+ parts.push(appSection);
392
+ }
393
+ let envContent = parts.join('\n\n');
394
+ // Expand ${API_URL} references
395
+ envContent = envContent.replace(/\$\{API_URL\}/g, apiUrl);
396
+ // Keep only actual env vars (filter out pure comment lines but keep var definitions)
397
+ envContent = envContent
398
+ .split('\n')
399
+ .filter(line => {
400
+ const trimmed = line.trim();
401
+ // Keep empty lines, lines with = (even if commented), and non-comment lines
402
+ return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
403
+ })
404
+ .join('\n')
405
+ .replace(/\n{3,}/g, '\n\n')
406
+ .trim();
407
+ return envContent;
408
+ }
351
409
  /**
352
410
  * Build API payload from resolved config
353
411
  */
@@ -368,24 +426,82 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
368
426
  }
369
427
  // Build files bundle
370
428
  const files = [];
429
+ // Track env files to move in setup script (staging approach to avoid blocking git clone)
430
+ const envFilesToMove = [];
371
431
  // Send .env.genbox content to server for each app
372
432
  const envGenboxPath = path.join(process.cwd(), '.env.genbox');
373
433
  if (fs.existsSync(envGenboxPath)) {
374
- const envContent = fs.readFileSync(envGenboxPath, 'utf-8');
375
- // Add env file for each app (user should have already updated URLs manually)
434
+ const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
435
+ // Parse into sections
436
+ const sections = parseEnvGenboxSections(rawEnvContent);
437
+ // Parse GLOBAL section to get API URL values
438
+ const globalSection = sections.get('GLOBAL') || '';
439
+ const envVarsFromFile = {};
440
+ for (const line of globalSection.split('\n')) {
441
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
442
+ if (match) {
443
+ let value = match[2].trim();
444
+ // Remove quotes if present
445
+ if ((value.startsWith('"') && value.endsWith('"')) ||
446
+ (value.startsWith("'") && value.endsWith("'"))) {
447
+ value = value.slice(1, -1);
448
+ }
449
+ envVarsFromFile[match[1]] = value;
450
+ }
451
+ }
452
+ // Determine API_URL based on profile's connect_to setting
453
+ const connectTo = resolved.profile ?
454
+ (config.profiles?.[resolved.profile]?.connect_to) : undefined;
455
+ let apiUrl;
456
+ if (connectTo) {
457
+ // Use the environment-specific API URL (e.g., STAGING_API_URL)
458
+ const envApiVarName = `${connectTo.toUpperCase()}_API_URL`;
459
+ apiUrl = envVarsFromFile[envApiVarName] || resolved.env['API_URL'] || 'http://localhost:3050';
460
+ }
461
+ else {
462
+ // Use local API URL
463
+ apiUrl = envVarsFromFile['LOCAL_API_URL'] || 'http://localhost:3050';
464
+ }
465
+ // Add env file for each app - filtered by selected apps only
376
466
  for (const app of resolved.apps) {
377
467
  const appPath = config.apps[app.name]?.path || app.name;
378
468
  const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
379
469
  (resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
380
- files.push({
381
- path: `${repoPath}/.env`,
382
- content: envContent,
383
- permissions: '0644',
384
- });
470
+ // Check if this app has microservices (sections like api/gateway, api/auth)
471
+ const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
472
+ if (servicesSections.length > 0) {
473
+ // App has microservices - create env file for each service
474
+ for (const serviceSectionName of servicesSections) {
475
+ const serviceName = serviceSectionName.split('/')[1];
476
+ // Build service-specific env content (GLOBAL + service section)
477
+ const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, apiUrl);
478
+ const stagingName = `${app.name}-${serviceName}.env`;
479
+ const targetPath = `${repoPath}/apps/${serviceName}/.env`;
480
+ files.push({
481
+ path: `/home/dev/.env-staging/${stagingName}`,
482
+ content: serviceEnvContent,
483
+ permissions: '0644',
484
+ });
485
+ envFilesToMove.push({ stagingName, targetPath });
486
+ }
487
+ }
488
+ else {
489
+ // Regular app - build app-specific env content (GLOBAL + app section)
490
+ const appEnvContent = buildAppEnvContent(sections, app.name, apiUrl);
491
+ files.push({
492
+ path: `/home/dev/.env-staging/${app.name}.env`,
493
+ content: appEnvContent,
494
+ permissions: '0644',
495
+ });
496
+ envFilesToMove.push({
497
+ stagingName: `${app.name}.env`,
498
+ targetPath: `${repoPath}/.env`,
499
+ });
500
+ }
385
501
  }
386
502
  }
387
503
  // Add setup script if generated
388
- const setupScript = generateSetupScript(resolved, config);
504
+ const setupScript = generateSetupScript(resolved, config, envFilesToMove);
389
505
  if (setupScript) {
390
506
  files.push({
391
507
  path: '/home/dev/setup-genbox.sh',
@@ -431,13 +547,27 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
431
547
  /**
432
548
  * Generate setup script
433
549
  */
434
- function generateSetupScript(resolved, config) {
550
+ function generateSetupScript(resolved, config, envFilesToMove = []) {
435
551
  const lines = [
436
552
  '#!/bin/bash',
437
553
  '# Generated by genbox create',
438
554
  'set -e',
439
555
  '',
440
556
  ];
557
+ // Move .env files from staging to their correct locations
558
+ // This runs after git clone has completed
559
+ if (envFilesToMove.length > 0) {
560
+ lines.push('# Move .env files from staging to app directories');
561
+ for (const { stagingName, targetPath } of envFilesToMove) {
562
+ lines.push(`if [ -f "/home/dev/.env-staging/${stagingName}" ]; then`);
563
+ lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
564
+ lines.push(` mv "/home/dev/.env-staging/${stagingName}" "${targetPath}"`);
565
+ lines.push(` echo "Moved .env to ${targetPath}"`);
566
+ lines.push('fi');
567
+ }
568
+ lines.push('rm -rf /home/dev/.env-staging 2>/dev/null || true');
569
+ lines.push('');
570
+ }
441
571
  // Change to project directory
442
572
  if (resolved.repos.length > 0) {
443
573
  lines.push(`cd ${resolved.repos[0].path} || exit 1`);