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.
- package/dist/commands/create.js +369 -130
- package/dist/commands/db-sync.js +364 -0
- package/dist/commands/destroy.js +5 -10
- 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/genbox-selector.js +5 -8
- 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 +4 -1
|
@@ -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;
|