genbox 1.0.10 → 1.0.12
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 +273 -19
- package/dist/commands/init.js +166 -78
- package/dist/commands/status.js +11 -0
- package/dist/profile-resolver.js +19 -4
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -48,6 +48,65 @@ const config_loader_1 = require("../config-loader");
|
|
|
48
48
|
const profile_resolver_1 = require("../profile-resolver");
|
|
49
49
|
const api_1 = require("../api");
|
|
50
50
|
const ssh_config_1 = require("../ssh-config");
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
/**
|
|
53
|
+
* Poll for genbox IP address (servers take a few seconds to get an IP assigned)
|
|
54
|
+
*/
|
|
55
|
+
async function waitForIpAddress(genboxId, maxAttempts = 30, delayMs = 2000) {
|
|
56
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const genbox = await (0, api_1.fetchApi)(`/genboxes/${genboxId}`);
|
|
59
|
+
if (genbox.ipAddress) {
|
|
60
|
+
return genbox.ipAddress;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Ignore errors during polling
|
|
65
|
+
}
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Find SSH private key
|
|
72
|
+
*/
|
|
73
|
+
function findSshKeyPath() {
|
|
74
|
+
const home = os.homedir();
|
|
75
|
+
const keyPaths = [
|
|
76
|
+
path.join(home, '.ssh', 'id_ed25519'),
|
|
77
|
+
path.join(home, '.ssh', 'id_rsa'),
|
|
78
|
+
];
|
|
79
|
+
for (const keyPath of keyPaths) {
|
|
80
|
+
if (fs.existsSync(keyPath)) {
|
|
81
|
+
return keyPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Wait for SSH to be available on the server
|
|
88
|
+
*/
|
|
89
|
+
async function waitForSsh(ipAddress, maxAttempts = 30, delayMs = 5000) {
|
|
90
|
+
const keyPath = findSshKeyPath();
|
|
91
|
+
if (!keyPath)
|
|
92
|
+
return false;
|
|
93
|
+
const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5`;
|
|
94
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
95
|
+
try {
|
|
96
|
+
(0, child_process_1.execSync)(`ssh ${sshOpts} dev@${ipAddress} "echo 'SSH ready'"`, {
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
timeout: 10000,
|
|
99
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// SSH not ready yet
|
|
105
|
+
}
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
51
110
|
async function provisionGenbox(payload) {
|
|
52
111
|
return (0, api_1.fetchApi)('/genboxes', {
|
|
53
112
|
method: 'POST',
|
|
@@ -171,16 +230,38 @@ exports.createCommand = new commander_1.Command('create')
|
|
|
171
230
|
const spinner = (0, ora_1.default)(`Creating Genbox '${name}'...`).start();
|
|
172
231
|
try {
|
|
173
232
|
const genbox = await provisionGenbox(payload);
|
|
174
|
-
spinner.succeed(chalk_1.default.green(`Genbox '${name}' created
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
233
|
+
spinner.succeed(chalk_1.default.green(`Genbox '${name}' created!`));
|
|
234
|
+
// Wait for IP if not immediately available
|
|
235
|
+
let ipAddress = genbox.ipAddress;
|
|
236
|
+
if (!ipAddress && genbox._id) {
|
|
237
|
+
spinner.start('Waiting for IP address...');
|
|
238
|
+
ipAddress = await waitForIpAddress(genbox._id);
|
|
239
|
+
if (ipAddress) {
|
|
240
|
+
spinner.succeed(`IP address assigned: ${ipAddress}`);
|
|
241
|
+
genbox.ipAddress = ipAddress;
|
|
183
242
|
}
|
|
243
|
+
else {
|
|
244
|
+
spinner.fail('Timed out waiting for IP. Run `genbox status` to check later.');
|
|
245
|
+
displayGenboxInfo(genbox, resolved);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Wait for SSH to be available
|
|
250
|
+
spinner.start('Waiting for SSH to be ready...');
|
|
251
|
+
const sshReady = await waitForSsh(ipAddress);
|
|
252
|
+
if (sshReady) {
|
|
253
|
+
spinner.succeed(chalk_1.default.green('SSH is ready!'));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
spinner.warn('SSH not ready yet. Server may still be booting.');
|
|
257
|
+
}
|
|
258
|
+
// Add SSH config
|
|
259
|
+
const sshAdded = (0, ssh_config_1.addSshConfigEntry)({
|
|
260
|
+
name,
|
|
261
|
+
ipAddress,
|
|
262
|
+
});
|
|
263
|
+
if (sshAdded) {
|
|
264
|
+
console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
|
|
184
265
|
}
|
|
185
266
|
// Display results
|
|
186
267
|
displayGenboxInfo(genbox, resolved);
|
|
@@ -250,6 +331,13 @@ function displayResolvedConfig(resolved) {
|
|
|
250
331
|
}
|
|
251
332
|
console.log('');
|
|
252
333
|
console.log(` ${chalk_1.default.bold('Database:')} ${resolved.database.mode}${resolved.database.source ? ` (from ${resolved.database.source})` : ''}`);
|
|
334
|
+
if (Object.keys(resolved.env).length > 0) {
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(` ${chalk_1.default.bold('Environment:')}`);
|
|
337
|
+
for (const [key, value] of Object.entries(resolved.env)) {
|
|
338
|
+
console.log(chalk_1.default.dim(` ${key}=${value}`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
253
341
|
if (resolved.warnings.length > 0) {
|
|
254
342
|
console.log('');
|
|
255
343
|
console.log(chalk_1.default.yellow(' Warnings:'));
|
|
@@ -260,11 +348,69 @@ function displayResolvedConfig(resolved) {
|
|
|
260
348
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
261
349
|
console.log('');
|
|
262
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
|
+
}
|
|
263
409
|
/**
|
|
264
410
|
* Build API payload from resolved config
|
|
265
411
|
*/
|
|
266
412
|
function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
267
|
-
// Load env vars
|
|
413
|
+
// Load env vars from .env.genbox
|
|
268
414
|
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
269
415
|
// Build services map
|
|
270
416
|
const services = {};
|
|
@@ -280,8 +426,82 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
280
426
|
}
|
|
281
427
|
// Build files bundle
|
|
282
428
|
const files = [];
|
|
429
|
+
// Track env files to move in setup script (staging approach to avoid blocking git clone)
|
|
430
|
+
const envFilesToMove = [];
|
|
431
|
+
// Send .env.genbox content to server for each app
|
|
432
|
+
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
433
|
+
if (fs.existsSync(envGenboxPath)) {
|
|
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
|
|
466
|
+
for (const app of resolved.apps) {
|
|
467
|
+
const appPath = config.apps[app.name]?.path || app.name;
|
|
468
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
469
|
+
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
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
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
283
503
|
// Add setup script if generated
|
|
284
|
-
const setupScript = generateSetupScript(resolved, config);
|
|
504
|
+
const setupScript = generateSetupScript(resolved, config, envFilesToMove);
|
|
285
505
|
if (setupScript) {
|
|
286
506
|
files.push({
|
|
287
507
|
path: '/home/dev/setup-genbox.sh',
|
|
@@ -327,13 +547,27 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
327
547
|
/**
|
|
328
548
|
* Generate setup script
|
|
329
549
|
*/
|
|
330
|
-
function generateSetupScript(resolved, config) {
|
|
550
|
+
function generateSetupScript(resolved, config, envFilesToMove = []) {
|
|
331
551
|
const lines = [
|
|
332
552
|
'#!/bin/bash',
|
|
333
553
|
'# Generated by genbox create',
|
|
334
554
|
'set -e',
|
|
335
555
|
'',
|
|
336
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
|
+
}
|
|
337
571
|
// Change to project directory
|
|
338
572
|
if (resolved.repos.length > 0) {
|
|
339
573
|
lines.push(`cd ${resolved.repos[0].path} || exit 1`);
|
|
@@ -465,18 +699,38 @@ async function createLegacy(name, options) {
|
|
|
465
699
|
gitToken: envVars.GIT_TOKEN,
|
|
466
700
|
});
|
|
467
701
|
spinner.succeed(chalk_1.default.green(`Genbox '${name}' created!`));
|
|
468
|
-
if
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
702
|
+
// Wait for IP if not immediately available
|
|
703
|
+
let ipAddress = genbox.ipAddress;
|
|
704
|
+
if (!ipAddress && genbox._id) {
|
|
705
|
+
spinner.start('Waiting for IP address...');
|
|
706
|
+
ipAddress = await waitForIpAddress(genbox._id);
|
|
707
|
+
if (ipAddress) {
|
|
708
|
+
spinner.succeed(`IP address assigned: ${ipAddress}`);
|
|
709
|
+
genbox.ipAddress = ipAddress;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
spinner.fail('Timed out waiting for IP. Run `genbox status` to check later.');
|
|
713
|
+
return;
|
|
472
714
|
}
|
|
473
715
|
}
|
|
716
|
+
// Wait for SSH to be available
|
|
717
|
+
spinner.start('Waiting for SSH to be ready...');
|
|
718
|
+
const sshReady = await waitForSsh(ipAddress);
|
|
719
|
+
if (sshReady) {
|
|
720
|
+
spinner.succeed(chalk_1.default.green('SSH is ready!'));
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
spinner.warn('SSH not ready yet. Server may still be booting.');
|
|
724
|
+
}
|
|
725
|
+
// Add SSH config
|
|
726
|
+
const sshAdded = (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress });
|
|
727
|
+
if (sshAdded) {
|
|
728
|
+
console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
|
|
729
|
+
}
|
|
474
730
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
475
731
|
console.log(` ${chalk_1.default.bold('Environment:')} ${name}`);
|
|
476
732
|
console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.yellow(genbox.status)}`);
|
|
477
|
-
|
|
478
|
-
console.log(` ${chalk_1.default.bold('IP:')} ${genbox.ipAddress}`);
|
|
479
|
-
}
|
|
733
|
+
console.log(` ${chalk_1.default.bold('IP:')} ${ipAddress}`);
|
|
480
734
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
481
735
|
}
|
|
482
736
|
catch (error) {
|
package/dist/commands/init.js
CHANGED
|
@@ -103,13 +103,15 @@ function detectAppGitRepos(apps, rootDir) {
|
|
|
103
103
|
return repos;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* Find .env files in app directories
|
|
106
|
+
* Find .env files in app directories (including nested microservices)
|
|
107
107
|
*/
|
|
108
108
|
function findAppEnvFiles(apps, rootDir) {
|
|
109
109
|
const envFiles = [];
|
|
110
110
|
const envPatterns = ['.env', '.env.local', '.env.development'];
|
|
111
111
|
for (const app of apps) {
|
|
112
112
|
const appDir = path_1.default.join(rootDir, app.path);
|
|
113
|
+
// Check for direct .env file in app directory
|
|
114
|
+
let foundDirectEnv = false;
|
|
113
115
|
for (const pattern of envPatterns) {
|
|
114
116
|
const envPath = path_1.default.join(appDir, pattern);
|
|
115
117
|
if (fs_1.default.existsSync(envPath)) {
|
|
@@ -118,7 +120,35 @@ function findAppEnvFiles(apps, rootDir) {
|
|
|
118
120
|
envFile: pattern,
|
|
119
121
|
fullPath: envPath,
|
|
120
122
|
});
|
|
121
|
-
|
|
123
|
+
foundDirectEnv = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Check for nested microservices (e.g., api/apps/*)
|
|
128
|
+
const appsSubdir = path_1.default.join(appDir, 'apps');
|
|
129
|
+
if (fs_1.default.existsSync(appsSubdir) && fs_1.default.statSync(appsSubdir).isDirectory()) {
|
|
130
|
+
try {
|
|
131
|
+
const services = fs_1.default.readdirSync(appsSubdir);
|
|
132
|
+
for (const service of services) {
|
|
133
|
+
const serviceDir = path_1.default.join(appsSubdir, service);
|
|
134
|
+
if (!fs_1.default.statSync(serviceDir).isDirectory())
|
|
135
|
+
continue;
|
|
136
|
+
for (const pattern of envPatterns) {
|
|
137
|
+
const envPath = path_1.default.join(serviceDir, pattern);
|
|
138
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
139
|
+
envFiles.push({
|
|
140
|
+
appName: `${app.name}/${service}`,
|
|
141
|
+
envFile: pattern,
|
|
142
|
+
fullPath: envPath,
|
|
143
|
+
isService: true,
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Ignore errors reading subdirectories
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
}
|
|
@@ -454,6 +484,20 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
454
484
|
});
|
|
455
485
|
fs_1.default.writeFileSync(configPath, yamlContent);
|
|
456
486
|
console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
|
|
487
|
+
// Add API URLs from environments to envVarsToAdd
|
|
488
|
+
// Always add LOCAL_API_URL for local development
|
|
489
|
+
envVarsToAdd['LOCAL_API_URL'] = 'http://localhost:3050';
|
|
490
|
+
if (v3Config.environments) {
|
|
491
|
+
for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
|
|
492
|
+
const apiUrl = envConfig.api?.api ||
|
|
493
|
+
envConfig.api?.url ||
|
|
494
|
+
envConfig.api?.gateway;
|
|
495
|
+
if (apiUrl) {
|
|
496
|
+
const varName = `${envName.toUpperCase()}_API_URL`;
|
|
497
|
+
envVarsToAdd[varName] = apiUrl;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
457
501
|
// Generate .env.genbox
|
|
458
502
|
await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting);
|
|
459
503
|
// Show warnings
|
|
@@ -464,11 +508,38 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
464
508
|
console.log(chalk_1.default.dim(` - ${warning}`));
|
|
465
509
|
}
|
|
466
510
|
}
|
|
511
|
+
// Show API URL guidance if environments are configured
|
|
512
|
+
if (v3Config.environments && Object.keys(v3Config.environments).length > 0) {
|
|
513
|
+
console.log('');
|
|
514
|
+
console.log(chalk_1.default.blue('=== API URL Configuration ==='));
|
|
515
|
+
console.log(chalk_1.default.dim('The following API URLs were added to .env.genbox:'));
|
|
516
|
+
console.log('');
|
|
517
|
+
console.log(chalk_1.default.dim(' LOCAL_API_URL=http://localhost:3050'));
|
|
518
|
+
for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
|
|
519
|
+
const apiUrl = envConfig.api?.api ||
|
|
520
|
+
envConfig.api?.url ||
|
|
521
|
+
envConfig.api?.gateway;
|
|
522
|
+
if (apiUrl) {
|
|
523
|
+
const varName = `${envName.toUpperCase()}_API_URL`;
|
|
524
|
+
console.log(chalk_1.default.dim(` ${varName}=${apiUrl}`));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
console.log('');
|
|
528
|
+
console.log(chalk_1.default.yellow('To use dynamic API URLs:'));
|
|
529
|
+
console.log(chalk_1.default.dim(' Use ${API_URL} in your app env vars, e.g.:'));
|
|
530
|
+
console.log(chalk_1.default.cyan(' VITE_API_BASE_URL=${API_URL}'));
|
|
531
|
+
console.log(chalk_1.default.cyan(' NEXT_PUBLIC_API_URL=${API_URL}'));
|
|
532
|
+
console.log('');
|
|
533
|
+
console.log(chalk_1.default.dim(' At create time, ${API_URL} expands based on profile:'));
|
|
534
|
+
console.log(chalk_1.default.dim(' • connect_to: staging → uses STAGING_API_URL'));
|
|
535
|
+
console.log(chalk_1.default.dim(' • connect_to: production → uses PRODUCTION_API_URL'));
|
|
536
|
+
console.log(chalk_1.default.dim(' • local/no connect_to → uses LOCAL_API_URL'));
|
|
537
|
+
}
|
|
467
538
|
// Next steps
|
|
468
539
|
console.log('');
|
|
469
540
|
console.log(chalk_1.default.bold('Next steps:'));
|
|
470
541
|
console.log(chalk_1.default.dim(` 1. Review and edit ${CONFIG_FILENAME}`));
|
|
471
|
-
console.log(chalk_1.default.dim(` 2.
|
|
542
|
+
console.log(chalk_1.default.dim(` 2. Update ${ENV_FILENAME} to use API URL variables where needed`));
|
|
472
543
|
console.log(chalk_1.default.dim(` 3. Run 'genbox profiles' to see available profiles`));
|
|
473
544
|
console.log(chalk_1.default.dim(` 4. Run 'genbox create <name> --profile <profile>' to create an environment`));
|
|
474
545
|
}
|
|
@@ -746,7 +817,7 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
746
817
|
return Object.keys(environments).length > 0 ? environments : undefined;
|
|
747
818
|
}
|
|
748
819
|
/**
|
|
749
|
-
* Setup .env.genbox file
|
|
820
|
+
* Setup .env.genbox file with segregated app sections
|
|
750
821
|
*/
|
|
751
822
|
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}, overwriteExisting = false) {
|
|
752
823
|
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
@@ -760,66 +831,84 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
|
|
|
760
831
|
return;
|
|
761
832
|
}
|
|
762
833
|
}
|
|
834
|
+
// Build segregated content with GLOBAL section first
|
|
835
|
+
let segregatedContent = `# Genbox Environment Variables
|
|
836
|
+
# Project: ${projectName}
|
|
837
|
+
# DO NOT COMMIT THIS FILE
|
|
838
|
+
#
|
|
839
|
+
# This file uses segregated sections for each app/service.
|
|
840
|
+
# At 'genbox create' time, only GLOBAL + selected app sections are used.
|
|
841
|
+
# Use \${API_URL} for dynamic API URLs based on profile's connect_to setting.
|
|
842
|
+
|
|
843
|
+
# === GLOBAL ===
|
|
844
|
+
# These variables are always included regardless of which apps are selected
|
|
845
|
+
|
|
846
|
+
`;
|
|
847
|
+
// Add global env vars
|
|
848
|
+
for (const [key, value] of Object.entries(extraEnvVars)) {
|
|
849
|
+
segregatedContent += `${key}=${value}\n`;
|
|
850
|
+
}
|
|
851
|
+
// Add GIT authentication placeholder if not already added
|
|
852
|
+
if (!extraEnvVars['GIT_TOKEN']) {
|
|
853
|
+
segregatedContent += `# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
|
|
854
|
+
}
|
|
855
|
+
segregatedContent += `
|
|
856
|
+
# Database URLs (used by profiles with database mode)
|
|
857
|
+
# STAGING_MONGODB_URL=mongodb+srv://user:password@staging.mongodb.net
|
|
858
|
+
# PROD_MONGODB_URL=mongodb+srv://readonly:password@prod.mongodb.net
|
|
859
|
+
|
|
860
|
+
`;
|
|
763
861
|
// For multi-repo: find env files in app directories
|
|
764
862
|
if (isMultiRepo && scan) {
|
|
765
863
|
const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
|
|
766
864
|
if (appEnvFiles.length > 0 && !nonInteractive) {
|
|
767
865
|
console.log('');
|
|
768
866
|
console.log(chalk_1.default.blue('=== Environment Files ==='));
|
|
769
|
-
|
|
867
|
+
// Group by app type
|
|
868
|
+
const directApps = appEnvFiles.filter(e => !e.isService);
|
|
869
|
+
const serviceApps = appEnvFiles.filter(e => e.isService);
|
|
870
|
+
if (directApps.length > 0) {
|
|
871
|
+
console.log(chalk_1.default.dim(`Found ${directApps.length} app env files`));
|
|
872
|
+
}
|
|
873
|
+
if (serviceApps.length > 0) {
|
|
874
|
+
console.log(chalk_1.default.dim(`Found ${serviceApps.length} microservice env files`));
|
|
875
|
+
}
|
|
770
876
|
const envChoices = appEnvFiles.map(env => ({
|
|
771
|
-
name: `${env.appName}
|
|
877
|
+
name: env.isService ? `${env.appName} (service)` : env.appName,
|
|
772
878
|
value: env.fullPath,
|
|
773
879
|
checked: true,
|
|
774
880
|
}));
|
|
775
881
|
const selectedEnvFiles = await prompts.checkbox({
|
|
776
|
-
message: 'Select .env files to
|
|
882
|
+
message: 'Select .env files to include in .env.genbox:',
|
|
777
883
|
choices: envChoices,
|
|
778
884
|
});
|
|
779
885
|
if (selectedEnvFiles.length > 0) {
|
|
780
|
-
let mergedContent = `# Genbox Environment Variables
|
|
781
|
-
# Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
|
|
782
|
-
# DO NOT COMMIT THIS FILE
|
|
783
|
-
#
|
|
784
|
-
# Add staging/production URLs:
|
|
785
|
-
# STAGING_MONGODB_URL=mongodb+srv://...
|
|
786
|
-
# STAGING_REDIS_URL=redis://...
|
|
787
|
-
# PROD_MONGODB_URL=mongodb+srv://...
|
|
788
|
-
#
|
|
789
|
-
# Git authentication:
|
|
790
|
-
# GIT_TOKEN=ghp_xxxxxxxxxxxx
|
|
791
|
-
|
|
792
|
-
`;
|
|
793
886
|
for (const envFilePath of selectedEnvFiles) {
|
|
794
887
|
const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
888
|
+
if (!appInfo)
|
|
889
|
+
continue;
|
|
890
|
+
const content = fs_1.default.readFileSync(envFilePath, 'utf8').trim();
|
|
891
|
+
// Add section header and content
|
|
892
|
+
segregatedContent += `# === ${appInfo.appName} ===\n`;
|
|
893
|
+
segregatedContent += content;
|
|
894
|
+
segregatedContent += '\n\n';
|
|
799
895
|
}
|
|
800
|
-
fs_1.default.writeFileSync(envPath, mergedContent);
|
|
801
|
-
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
|
|
802
896
|
}
|
|
803
897
|
}
|
|
804
898
|
else if (appEnvFiles.length > 0 && nonInteractive) {
|
|
805
899
|
// Non-interactive: merge all env files
|
|
806
|
-
let mergedContent = `# Genbox Environment Variables
|
|
807
|
-
# Merged from app directories
|
|
808
|
-
# DO NOT COMMIT THIS FILE
|
|
809
|
-
|
|
810
|
-
`;
|
|
811
900
|
for (const envFile of appEnvFiles) {
|
|
812
|
-
const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
901
|
+
const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8').trim();
|
|
902
|
+
segregatedContent += `# === ${envFile.appName} ===\n`;
|
|
903
|
+
segregatedContent += content;
|
|
904
|
+
segregatedContent += '\n\n';
|
|
816
905
|
}
|
|
817
|
-
fs_1.default.writeFileSync(envPath, mergedContent);
|
|
818
|
-
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
|
|
819
906
|
}
|
|
820
907
|
}
|
|
821
|
-
// If no env
|
|
822
|
-
|
|
908
|
+
// If no app env files found, check for root .env
|
|
909
|
+
const hasAppSections = segregatedContent.includes('# === ') &&
|
|
910
|
+
!segregatedContent.endsWith('# === GLOBAL ===\n');
|
|
911
|
+
if (!hasAppSections) {
|
|
823
912
|
const existingEnvFiles = ['.env.local', '.env', '.env.development'];
|
|
824
913
|
let existingEnvPath;
|
|
825
914
|
for (const envFile of existingEnvFiles) {
|
|
@@ -831,51 +920,28 @@ async function setupEnvFile(projectName, config, nonInteractive = false, scan, i
|
|
|
831
920
|
}
|
|
832
921
|
if (existingEnvPath) {
|
|
833
922
|
const copyExisting = nonInteractive ? true : await prompts.confirm({
|
|
834
|
-
message: `Found ${path_1.default.basename(existingEnvPath)}.
|
|
923
|
+
message: `Found ${path_1.default.basename(existingEnvPath)}. Include in ${ENV_FILENAME}?`,
|
|
835
924
|
default: true,
|
|
836
925
|
});
|
|
837
926
|
if (copyExisting) {
|
|
838
|
-
const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
# Add staging/production URLs:
|
|
844
|
-
# STAGING_MONGODB_URL=mongodb+srv://...
|
|
845
|
-
# STAGING_REDIS_URL=redis://...
|
|
846
|
-
# PROD_MONGODB_URL=mongodb+srv://...
|
|
847
|
-
#
|
|
848
|
-
# Git authentication:
|
|
849
|
-
# GIT_TOKEN=ghp_xxxxxxxxxxxx
|
|
850
|
-
|
|
851
|
-
`;
|
|
852
|
-
fs_1.default.writeFileSync(envPath, header + content);
|
|
853
|
-
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
const createEnv = nonInteractive ? true : await prompts.confirm({
|
|
858
|
-
message: `Create ${ENV_FILENAME} template?`,
|
|
859
|
-
default: true,
|
|
860
|
-
});
|
|
861
|
-
if (createEnv) {
|
|
862
|
-
const template = generateEnvTemplate(projectName, config);
|
|
863
|
-
fs_1.default.writeFileSync(envPath, template);
|
|
864
|
-
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
|
|
927
|
+
const content = fs_1.default.readFileSync(existingEnvPath, 'utf8').trim();
|
|
928
|
+
segregatedContent += `# === root ===\n`;
|
|
929
|
+
segregatedContent += `# From ${path_1.default.basename(existingEnvPath)}\n`;
|
|
930
|
+
segregatedContent += content;
|
|
931
|
+
segregatedContent += '\n\n';
|
|
865
932
|
}
|
|
866
933
|
}
|
|
867
934
|
}
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
fs_1.default.writeFileSync(envPath, content.trim() + '\n' + extraSection);
|
|
935
|
+
// Add END marker
|
|
936
|
+
segregatedContent += `# === END ===\n`;
|
|
937
|
+
// Write the file
|
|
938
|
+
fs_1.default.writeFileSync(envPath, segregatedContent);
|
|
939
|
+
const sectionCount = (segregatedContent.match(/# === [^=]+ ===/g) || []).length - 2; // Exclude GLOBAL and END
|
|
940
|
+
if (sectionCount > 0) {
|
|
941
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} with ${sectionCount} app section(s)`));
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME}`));
|
|
879
945
|
}
|
|
880
946
|
// Add to .gitignore
|
|
881
947
|
const gitignorePath = path_1.default.join(process.cwd(), '.gitignore');
|
|
@@ -897,6 +963,19 @@ function generateEnvTemplate(projectName, config) {
|
|
|
897
963
|
'# DO NOT COMMIT THIS FILE',
|
|
898
964
|
'',
|
|
899
965
|
'# ============================================',
|
|
966
|
+
'# API URL CONFIGURATION',
|
|
967
|
+
'# ============================================',
|
|
968
|
+
'# Use ${API_URL} in your app env vars (e.g., VITE_API_BASE_URL=${API_URL})',
|
|
969
|
+
'# At create time, ${API_URL} expands based on profile:',
|
|
970
|
+
'# - connect_to: staging → uses STAGING_API_URL',
|
|
971
|
+
'# - connect_to: production → uses PRODUCTION_API_URL',
|
|
972
|
+
'# - local/no connect_to → uses LOCAL_API_URL',
|
|
973
|
+
'',
|
|
974
|
+
'LOCAL_API_URL=http://localhost:3050',
|
|
975
|
+
'STAGING_API_URL=https://api.staging.example.com',
|
|
976
|
+
'# PRODUCTION_API_URL=https://api.example.com',
|
|
977
|
+
'',
|
|
978
|
+
'# ============================================',
|
|
900
979
|
'# STAGING ENVIRONMENT',
|
|
901
980
|
'# ============================================',
|
|
902
981
|
'',
|
|
@@ -939,6 +1018,15 @@ function generateEnvTemplate(projectName, config) {
|
|
|
939
1018
|
'STRIPE_SECRET_KEY=sk_test_xxx',
|
|
940
1019
|
'STRIPE_WEBHOOK_SECRET=whsec_xxx',
|
|
941
1020
|
'',
|
|
1021
|
+
'# ============================================',
|
|
1022
|
+
'# APPLICATION ENV VARS',
|
|
1023
|
+
'# ============================================',
|
|
1024
|
+
'# Use ${API_URL} for dynamic API URLs',
|
|
1025
|
+
'',
|
|
1026
|
+
'# Example:',
|
|
1027
|
+
'# VITE_API_BASE_URL=${API_URL}',
|
|
1028
|
+
'# NEXT_PUBLIC_API_URL=${API_URL}',
|
|
1029
|
+
'',
|
|
942
1030
|
];
|
|
943
1031
|
return lines.join('\n');
|
|
944
1032
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -42,6 +42,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
42
42
|
const api_1 = require("../api");
|
|
43
43
|
const config_1 = require("../config");
|
|
44
44
|
const genbox_selector_1 = require("../genbox-selector");
|
|
45
|
+
const ssh_config_1 = require("../ssh-config");
|
|
45
46
|
const os = __importStar(require("os"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
47
48
|
const fs = __importStar(require("fs"));
|
|
@@ -101,6 +102,16 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
104
|
const selectedName = target.name;
|
|
105
|
+
// Auto-add SSH config if missing
|
|
106
|
+
if (!(0, ssh_config_1.hasSshConfigEntry)(selectedName)) {
|
|
107
|
+
const sshAdded = (0, ssh_config_1.addSshConfigEntry)({
|
|
108
|
+
name: selectedName,
|
|
109
|
+
ipAddress: target.ipAddress,
|
|
110
|
+
});
|
|
111
|
+
if (sshAdded) {
|
|
112
|
+
console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(selectedName)}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
104
115
|
// 2. Get SSH key
|
|
105
116
|
let keyPath;
|
|
106
117
|
try {
|
package/dist/profile-resolver.js
CHANGED
|
@@ -113,7 +113,7 @@ class ProfileResolver {
|
|
|
113
113
|
infrastructure,
|
|
114
114
|
database,
|
|
115
115
|
repos: this.resolveRepos(config, apps),
|
|
116
|
-
env: this.resolveEnvVars(config, apps, infrastructure, database),
|
|
116
|
+
env: this.resolveEnvVars(config, apps, infrastructure, database, profile.connect_to),
|
|
117
117
|
hooks: config.hooks || {},
|
|
118
118
|
profile: options.profile,
|
|
119
119
|
warnings,
|
|
@@ -293,7 +293,10 @@ class ProfileResolver {
|
|
|
293
293
|
getUrlForDependency(depName, envConfig) {
|
|
294
294
|
// Check if it's an API dependency
|
|
295
295
|
if (depName === 'api' && envConfig.api) {
|
|
296
|
-
|
|
296
|
+
// Check common fields: url, gateway, api, or first string value
|
|
297
|
+
const apiConfig = envConfig.api;
|
|
298
|
+
return apiConfig.url || apiConfig.gateway || apiConfig.api ||
|
|
299
|
+
Object.values(apiConfig).find(v => typeof v === 'string' && v.startsWith('http'));
|
|
297
300
|
}
|
|
298
301
|
// Check infrastructure
|
|
299
302
|
const infraConfig = envConfig[depName];
|
|
@@ -454,9 +457,21 @@ class ProfileResolver {
|
|
|
454
457
|
/**
|
|
455
458
|
* Resolve environment variables
|
|
456
459
|
*/
|
|
457
|
-
resolveEnvVars(config, apps, infrastructure, database) {
|
|
460
|
+
resolveEnvVars(config, apps, infrastructure, database, connectTo) {
|
|
458
461
|
const env = {};
|
|
459
|
-
//
|
|
462
|
+
// If connect_to is set, get API URL from environment config
|
|
463
|
+
if (connectTo && config.environments?.[connectTo]) {
|
|
464
|
+
const envConfig = config.environments[connectTo];
|
|
465
|
+
const apiUrl = this.getUrlForDependency('api', envConfig);
|
|
466
|
+
if (apiUrl) {
|
|
467
|
+
env['API_URL'] = apiUrl;
|
|
468
|
+
env['VITE_API_URL'] = apiUrl;
|
|
469
|
+
env['VITE_API_BASE_URL'] = apiUrl;
|
|
470
|
+
env['NEXT_PUBLIC_API_URL'] = apiUrl;
|
|
471
|
+
env['NEXT_PUBLIC_API_BASE_URL'] = apiUrl;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Add API URL based on app dependency resolution (may override connect_to)
|
|
460
475
|
for (const app of apps) {
|
|
461
476
|
const apiDep = app.dependencies['api'];
|
|
462
477
|
if (apiDep) {
|