genbox 1.0.3 → 1.0.5

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,503 @@
1
+ "use strict";
2
+ /**
3
+ * Environment Variable Analyzer
4
+ *
5
+ * Analyzes environment variables from multiple sources:
6
+ * - .env files
7
+ * - docker-compose files
8
+ * - Source code references
9
+ * - Framework configs
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
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.EnvAnalyzer = void 0;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ // Patterns for detecting variable types
49
+ const ENV_TYPE_PATTERNS = [
50
+ // Database URIs
51
+ [/^(DATABASE|MONGO|POSTGRES|MYSQL|REDIS|RABBIT).*_URI$/i, 'database_uri'],
52
+ [/^(DATABASE|MONGO|POSTGRES|MYSQL|REDIS|RABBIT).*_URL$/i, 'database_uri'],
53
+ [/^(MONGODB|POSTGRESQL|MYSQL)_/i, 'database_uri'],
54
+ // Secrets/Keys
55
+ [/_SECRET$/i, 'secret'],
56
+ [/_KEY$/i, 'secret'],
57
+ [/_TOKEN$/i, 'secret'],
58
+ [/_PASSWORD$/i, 'secret'],
59
+ [/_PRIVATE/i, 'secret'],
60
+ [/^SECRET_/i, 'secret'],
61
+ [/^API_KEY$/i, 'api_key'],
62
+ [/_API_KEY$/i, 'api_key'],
63
+ // URLs
64
+ [/_URL$/i, 'url'],
65
+ [/_URI$/i, 'url'],
66
+ [/_ENDPOINT$/i, 'url'],
67
+ [/^(HTTPS?|WS):\/\//i, 'url'],
68
+ // Booleans
69
+ [/^(ENABLE|DISABLE|DEBUG|VERBOSE|IS_|HAS_|USE_|ALLOW_)/i, 'boolean'],
70
+ // Numbers
71
+ [/_PORT$/i, 'number'],
72
+ [/_COUNT$/i, 'number'],
73
+ [/_SIZE$/i, 'number'],
74
+ [/_TIMEOUT$/i, 'number'],
75
+ [/_LIMIT$/i, 'number'],
76
+ [/_MAX$/i, 'number'],
77
+ [/_MIN$/i, 'number'],
78
+ // Paths
79
+ [/_PATH$/i, 'path'],
80
+ [/_DIR$/i, 'path'],
81
+ [/_FILE$/i, 'path'],
82
+ ];
83
+ // Known secret variable names
84
+ const SECRET_PATTERNS = [
85
+ /PASSWORD/i,
86
+ /SECRET/i,
87
+ /TOKEN/i,
88
+ /API_KEY/i,
89
+ /PRIVATE/i,
90
+ /CREDENTIAL/i,
91
+ /^AWS_/i,
92
+ /^GIT_TOKEN$/i,
93
+ /^GITHUB_TOKEN$/i,
94
+ /^NPM_TOKEN$/i,
95
+ ];
96
+ // Variables that are typically required
97
+ const REQUIRED_PATTERNS = [
98
+ /^DATABASE/i,
99
+ /^MONGO/i,
100
+ /^POSTGRES/i,
101
+ /^MYSQL/i,
102
+ /^REDIS/i,
103
+ /^JWT_SECRET$/i,
104
+ /^NODE_ENV$/i,
105
+ /^PORT$/i,
106
+ ];
107
+ class EnvAnalyzer {
108
+ /**
109
+ * Analyze environment variables from all sources
110
+ */
111
+ async analyze(root, apps, compose) {
112
+ const allVariables = [];
113
+ const sources = [];
114
+ const references = [];
115
+ // 1. Extract from .env files
116
+ const envFiles = [
117
+ '.env.example',
118
+ '.env.sample',
119
+ '.env.template',
120
+ '.env.local',
121
+ '.env.development',
122
+ '.env',
123
+ ];
124
+ for (const envFile of envFiles) {
125
+ const filePath = path.join(root, envFile);
126
+ if (fs.existsSync(filePath)) {
127
+ const vars = this.parseEnvFile(filePath, envFile);
128
+ allVariables.push(...vars);
129
+ sources.push(envFile);
130
+ }
131
+ }
132
+ // 2. Extract from docker-compose
133
+ if (compose) {
134
+ for (const service of [...compose.applications, ...compose.databases, ...compose.caches, ...compose.queues]) {
135
+ // From environment section
136
+ for (const [name, value] of Object.entries(service.environment)) {
137
+ allVariables.push({
138
+ name,
139
+ value: value || undefined,
140
+ type: this.inferType(name, value),
141
+ source: `docker-compose (${service.name})`,
142
+ usedBy: [service.name],
143
+ });
144
+ }
145
+ // From env_file references
146
+ if (service.envFile) {
147
+ for (const envFile of service.envFile) {
148
+ const filePath = path.join(root, envFile);
149
+ if (fs.existsSync(filePath)) {
150
+ const vars = this.parseEnvFile(filePath, envFile);
151
+ for (const v of vars) {
152
+ v.usedBy = [service.name];
153
+ }
154
+ allVariables.push(...vars);
155
+ if (!sources.includes(envFile)) {
156
+ sources.push(envFile);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ // 3. Extract from source code references
164
+ const codeRefs = await this.extractFromCode(root, apps);
165
+ references.push(...codeRefs);
166
+ // Add referenced variables that weren't found in env files
167
+ for (const ref of codeRefs) {
168
+ const exists = allVariables.some(v => v.name === ref.variable);
169
+ if (!exists) {
170
+ allVariables.push({
171
+ name: ref.variable,
172
+ type: this.inferType(ref.variable),
173
+ source: 'code reference',
174
+ usedBy: ref.service ? [ref.service] : [],
175
+ });
176
+ }
177
+ }
178
+ // 4. Deduplicate and merge
179
+ const merged = this.mergeVariables(allVariables);
180
+ // 5. Categorize
181
+ const required = merged.filter(v => this.isRequired(v) && !this.isSecret(v.name));
182
+ const secrets = merged.filter(v => this.isSecret(v.name));
183
+ const optional = merged.filter(v => !this.isRequired(v) && !this.isSecret(v.name));
184
+ return {
185
+ required,
186
+ optional,
187
+ secrets,
188
+ references,
189
+ sources,
190
+ };
191
+ }
192
+ parseEnvFile(filePath, source) {
193
+ const variables = [];
194
+ const content = fs.readFileSync(filePath, 'utf8');
195
+ const lines = content.split('\n');
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i].trim();
198
+ // Skip comments and empty lines
199
+ if (!line || line.startsWith('#'))
200
+ continue;
201
+ // Parse KEY=value
202
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
203
+ if (match) {
204
+ const [, name, rawValue] = match;
205
+ let value = rawValue;
206
+ // Remove quotes if present
207
+ if ((value.startsWith('"') && value.endsWith('"')) ||
208
+ (value.startsWith("'") && value.endsWith("'"))) {
209
+ value = value.slice(1, -1);
210
+ }
211
+ // Handle empty/placeholder values
212
+ const isPlaceholder = !value ||
213
+ value.includes('your-') ||
214
+ value.includes('change-me') ||
215
+ value.includes('xxx') ||
216
+ value === '""' ||
217
+ value === "''";
218
+ variables.push({
219
+ name,
220
+ value: isPlaceholder ? undefined : value,
221
+ type: this.inferType(name, value),
222
+ source,
223
+ usedBy: [],
224
+ });
225
+ }
226
+ }
227
+ return variables;
228
+ }
229
+ async extractFromCode(root, apps) {
230
+ const references = [];
231
+ // Patterns to search for env variable usage
232
+ const patterns = [
233
+ [/process\.env\.([A-Z_][A-Z0-9_]*)/g, 'node'],
234
+ [/process\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g, 'node'],
235
+ [/Bun\.env\.([A-Z_][A-Z0-9_]*)/g, 'node'],
236
+ [/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g, 'node'],
237
+ [/os\.environ\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g, 'python'],
238
+ [/os\.getenv\(['"]([A-Z_][A-Z0-9_]*)['"]\)/g, 'python'],
239
+ [/os\.Getenv\(['"]([A-Z_][A-Z0-9_]*)['"]\)/g, 'go'],
240
+ [/ENV\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g, 'ruby'],
241
+ [/std::env::var\(['"]([A-Z_][A-Z0-9_]*)['"]\)/g, 'rust'],
242
+ ];
243
+ // File extensions to search
244
+ const extensions = {
245
+ node: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
246
+ python: ['.py'],
247
+ go: ['.go'],
248
+ ruby: ['.rb'],
249
+ rust: ['.rs'],
250
+ };
251
+ // Search in app directories
252
+ const searchDirs = apps.length > 0
253
+ ? apps.map(a => path.join(root, a.path))
254
+ : [root];
255
+ for (const dir of searchDirs) {
256
+ const appName = apps.find(a => path.join(root, a.path) === dir)?.name;
257
+ try {
258
+ await this.searchDirectory(dir, (filePath, content) => {
259
+ for (const [pattern, lang] of patterns) {
260
+ const fileExt = path.extname(filePath);
261
+ if (!extensions[lang]?.includes(fileExt)) {
262
+ continue;
263
+ }
264
+ let match;
265
+ while ((match = pattern.exec(content)) !== null) {
266
+ const varName = match[1];
267
+ // Skip common non-env variables
268
+ if (['NODE', 'VITE', 'NEXT', 'REACT'].includes(varName)) {
269
+ continue;
270
+ }
271
+ references.push({
272
+ variable: varName,
273
+ file: path.relative(root, filePath),
274
+ service: appName,
275
+ });
276
+ }
277
+ }
278
+ });
279
+ }
280
+ catch {
281
+ // Directory doesn't exist or can't be read
282
+ }
283
+ }
284
+ // Deduplicate references
285
+ const seen = new Set();
286
+ return references.filter(ref => {
287
+ const key = `${ref.variable}:${ref.service || ''}`;
288
+ if (seen.has(key))
289
+ return false;
290
+ seen.add(key);
291
+ return true;
292
+ });
293
+ }
294
+ async searchDirectory(dir, callback) {
295
+ const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', '.venv', 'vendor', '__pycache__'];
296
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
297
+ for (const entry of entries) {
298
+ const fullPath = path.join(dir, entry.name);
299
+ if (entry.isDirectory()) {
300
+ if (!ignoreDirs.includes(entry.name)) {
301
+ await this.searchDirectory(fullPath, callback);
302
+ }
303
+ }
304
+ else if (entry.isFile()) {
305
+ // Skip large files
306
+ try {
307
+ const stats = fs.statSync(fullPath);
308
+ if (stats.size > 1024 * 1024)
309
+ continue; // Skip files > 1MB
310
+ const content = fs.readFileSync(fullPath, 'utf8');
311
+ callback(fullPath, content);
312
+ }
313
+ catch {
314
+ // Can't read file
315
+ }
316
+ }
317
+ }
318
+ }
319
+ inferType(name, value) {
320
+ // Check patterns based on name
321
+ for (const [pattern, type] of ENV_TYPE_PATTERNS) {
322
+ if (pattern.test(name)) {
323
+ return type;
324
+ }
325
+ }
326
+ // Check value format
327
+ if (value) {
328
+ if (/^(true|false)$/i.test(value))
329
+ return 'boolean';
330
+ if (/^\d+$/.test(value))
331
+ return 'number';
332
+ if (/^https?:\/\//.test(value))
333
+ return 'url';
334
+ if (/^mongodb(\+srv)?:\/\//.test(value))
335
+ return 'database_uri';
336
+ if (/^postgres(ql)?:\/\//.test(value))
337
+ return 'database_uri';
338
+ if (/^mysql:\/\//.test(value))
339
+ return 'database_uri';
340
+ if (/^redis:\/\//.test(value))
341
+ return 'database_uri';
342
+ if (/^amqp:\/\//.test(value))
343
+ return 'database_uri';
344
+ if (value.startsWith('/') || value.startsWith('./'))
345
+ return 'path';
346
+ }
347
+ return 'string';
348
+ }
349
+ isSecret(name) {
350
+ return SECRET_PATTERNS.some(p => p.test(name));
351
+ }
352
+ isRequired(variable) {
353
+ // Check if it matches required patterns
354
+ if (REQUIRED_PATTERNS.some(p => p.test(variable.name))) {
355
+ return true;
356
+ }
357
+ // If it's used by multiple services, it's likely required
358
+ if (variable.usedBy.length > 1) {
359
+ return true;
360
+ }
361
+ // If it's a database URI or similar, it's required
362
+ if (variable.type === 'database_uri') {
363
+ return true;
364
+ }
365
+ return false;
366
+ }
367
+ mergeVariables(variables) {
368
+ const merged = new Map();
369
+ for (const variable of variables) {
370
+ const existing = merged.get(variable.name);
371
+ if (existing) {
372
+ // Merge usedBy
373
+ for (const service of variable.usedBy) {
374
+ if (!existing.usedBy.includes(service)) {
375
+ existing.usedBy.push(service);
376
+ }
377
+ }
378
+ // Keep value if we found one
379
+ if (!existing.value && variable.value) {
380
+ existing.value = variable.value;
381
+ }
382
+ // Keep more specific type
383
+ if (existing.type === 'string' && variable.type !== 'string') {
384
+ existing.type = variable.type;
385
+ }
386
+ }
387
+ else {
388
+ merged.set(variable.name, { ...variable });
389
+ }
390
+ }
391
+ return Array.from(merged.values());
392
+ }
393
+ /**
394
+ * Generate an environment template from analysis
395
+ */
396
+ generateTemplate(analysis) {
397
+ const lines = [
398
+ '# Genbox Environment Configuration',
399
+ '# Generated by genbox init',
400
+ '#',
401
+ '# IMPORTANT: Update values before deploying',
402
+ '',
403
+ ];
404
+ // Group by service
405
+ const serviceGroups = new Map();
406
+ const globalVars = [];
407
+ for (const variable of [...analysis.required, ...analysis.optional, ...analysis.secrets]) {
408
+ if (variable.usedBy.length === 0 || variable.usedBy.length > 1) {
409
+ globalVars.push(variable);
410
+ }
411
+ else {
412
+ const service = variable.usedBy[0];
413
+ const group = serviceGroups.get(service) || [];
414
+ group.push(variable);
415
+ serviceGroups.set(service, group);
416
+ }
417
+ }
418
+ // Global section
419
+ if (globalVars.length > 0) {
420
+ lines.push('# === Global Configuration ===');
421
+ lines.push('');
422
+ for (const v of globalVars) {
423
+ lines.push(this.formatEnvLine(v));
424
+ }
425
+ lines.push('');
426
+ }
427
+ // Service sections
428
+ for (const [service, vars] of serviceGroups) {
429
+ lines.push(`# === ${service.toUpperCase()} ===`);
430
+ lines.push('');
431
+ for (const v of vars) {
432
+ lines.push(this.formatEnvLine(v));
433
+ }
434
+ lines.push('');
435
+ }
436
+ // Secrets section
437
+ const secretVars = analysis.secrets.filter(v => !globalVars.includes(v) && !Array.from(serviceGroups.values()).flat().includes(v));
438
+ if (secretVars.length > 0) {
439
+ lines.push('# === SECRETS (DO NOT COMMIT) ===');
440
+ lines.push('');
441
+ for (const v of secretVars) {
442
+ lines.push(this.formatEnvLine(v));
443
+ }
444
+ lines.push('');
445
+ }
446
+ return lines.join('\n');
447
+ }
448
+ formatEnvLine(variable) {
449
+ let value = variable.value || this.generateDefaultValue(variable);
450
+ // Add comment for required/sensitive
451
+ let comment = '';
452
+ if (this.isSecret(variable.name)) {
453
+ comment = ' # SENSITIVE';
454
+ }
455
+ else if (this.isRequired(variable)) {
456
+ comment = ' # Required';
457
+ }
458
+ // Quote if necessary
459
+ if (value.includes(' ') || value.includes('#')) {
460
+ value = `"${value}"`;
461
+ }
462
+ return `${variable.name}=${value}${comment}`;
463
+ }
464
+ generateDefaultValue(variable) {
465
+ switch (variable.type) {
466
+ case 'database_uri':
467
+ if (variable.name.includes('MONGO')) {
468
+ return 'mongodb://localhost:27017/myapp';
469
+ }
470
+ if (variable.name.includes('POSTGRES')) {
471
+ return 'postgresql://localhost:5432/myapp';
472
+ }
473
+ if (variable.name.includes('MYSQL')) {
474
+ return 'mysql://localhost:3306/myapp';
475
+ }
476
+ if (variable.name.includes('REDIS')) {
477
+ return 'redis://localhost:6379';
478
+ }
479
+ if (variable.name.includes('RABBIT')) {
480
+ return 'amqp://localhost:5672';
481
+ }
482
+ return 'your-database-uri-here';
483
+ case 'url':
484
+ return 'http://localhost:3000';
485
+ case 'secret':
486
+ case 'api_key':
487
+ return 'your-secret-key-here';
488
+ case 'boolean':
489
+ return 'false';
490
+ case 'number':
491
+ if (variable.name.includes('PORT'))
492
+ return '3000';
493
+ if (variable.name.includes('TIMEOUT'))
494
+ return '30000';
495
+ return '0';
496
+ case 'path':
497
+ return './';
498
+ default:
499
+ return '';
500
+ }
501
+ }
502
+ }
503
+ exports.EnvAnalyzer = EnvAnalyzer;