genbox 1.0.9 → 1.0.11
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 +142 -15
- package/dist/commands/init.js +38 -5
- 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,17 +230,39 @@ 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;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
spinner.fail('Timed out waiting for IP. Run `genbox status` to check later.');
|
|
245
|
+
displayGenboxInfo(genbox, resolved);
|
|
246
|
+
return;
|
|
183
247
|
}
|
|
184
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)}`));
|
|
265
|
+
}
|
|
185
266
|
// Display results
|
|
186
267
|
displayGenboxInfo(genbox, resolved);
|
|
187
268
|
}
|
|
@@ -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:'));
|
|
@@ -264,7 +352,7 @@ function displayResolvedConfig(resolved) {
|
|
|
264
352
|
* Build API payload from resolved config
|
|
265
353
|
*/
|
|
266
354
|
function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
267
|
-
// Load env vars
|
|
355
|
+
// Load env vars from .env.genbox
|
|
268
356
|
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
269
357
|
// Build services map
|
|
270
358
|
const services = {};
|
|
@@ -280,6 +368,22 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
280
368
|
}
|
|
281
369
|
// Build files bundle
|
|
282
370
|
const files = [];
|
|
371
|
+
// Send .env.genbox content to server for each app
|
|
372
|
+
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
373
|
+
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)
|
|
376
|
+
for (const app of resolved.apps) {
|
|
377
|
+
const appPath = config.apps[app.name]?.path || app.name;
|
|
378
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
379
|
+
(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
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
283
387
|
// Add setup script if generated
|
|
284
388
|
const setupScript = generateSetupScript(resolved, config);
|
|
285
389
|
if (setupScript) {
|
|
@@ -465,15 +569,38 @@ async function createLegacy(name, options) {
|
|
|
465
569
|
gitToken: envVars.GIT_TOKEN,
|
|
466
570
|
});
|
|
467
571
|
spinner.succeed(chalk_1.default.green(`Genbox '${name}' created!`));
|
|
468
|
-
if
|
|
469
|
-
|
|
572
|
+
// Wait for IP if not immediately available
|
|
573
|
+
let ipAddress = genbox.ipAddress;
|
|
574
|
+
if (!ipAddress && genbox._id) {
|
|
575
|
+
spinner.start('Waiting for IP address...');
|
|
576
|
+
ipAddress = await waitForIpAddress(genbox._id);
|
|
577
|
+
if (ipAddress) {
|
|
578
|
+
spinner.succeed(`IP address assigned: ${ipAddress}`);
|
|
579
|
+
genbox.ipAddress = ipAddress;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
spinner.fail('Timed out waiting for IP. Run `genbox status` to check later.');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Wait for SSH to be available
|
|
587
|
+
spinner.start('Waiting for SSH to be ready...');
|
|
588
|
+
const sshReady = await waitForSsh(ipAddress);
|
|
589
|
+
if (sshReady) {
|
|
590
|
+
spinner.succeed(chalk_1.default.green('SSH is ready!'));
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
spinner.warn('SSH not ready yet. Server may still be booting.');
|
|
594
|
+
}
|
|
595
|
+
// Add SSH config
|
|
596
|
+
const sshAdded = (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress });
|
|
597
|
+
if (sshAdded) {
|
|
598
|
+
console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
|
|
470
599
|
}
|
|
471
600
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
472
601
|
console.log(` ${chalk_1.default.bold('Environment:')} ${name}`);
|
|
473
602
|
console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.yellow(genbox.status)}`);
|
|
474
|
-
|
|
475
|
-
console.log(` ${chalk_1.default.bold('IP:')} ${genbox.ipAddress}`);
|
|
476
|
-
}
|
|
603
|
+
console.log(` ${chalk_1.default.bold('IP:')} ${ipAddress}`);
|
|
477
604
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
478
605
|
}
|
|
479
606
|
catch (error) {
|
package/dist/commands/init.js
CHANGED
|
@@ -137,6 +137,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
137
137
|
const configPath = path_1.default.join(process.cwd(), CONFIG_FILENAME);
|
|
138
138
|
const nonInteractive = options.yes || !process.stdin.isTTY;
|
|
139
139
|
// Check for existing config
|
|
140
|
+
let overwriteExisting = options.force || false;
|
|
140
141
|
if (fs_1.default.existsSync(configPath) && !options.force) {
|
|
141
142
|
if (nonInteractive) {
|
|
142
143
|
console.log(chalk_1.default.yellow('genbox.yaml already exists. Use --force to overwrite.'));
|
|
@@ -150,6 +151,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
150
151
|
if (!overwrite) {
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
154
|
+
overwriteExisting = true;
|
|
153
155
|
}
|
|
154
156
|
console.log(chalk_1.default.blue('Initializing Genbox...'));
|
|
155
157
|
console.log('');
|
|
@@ -310,6 +312,13 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
310
312
|
if (hasHttpsRepos) {
|
|
311
313
|
console.log('');
|
|
312
314
|
console.log(chalk_1.default.yellow('Private repositories require a GitHub token for cloning.'));
|
|
315
|
+
console.log('');
|
|
316
|
+
console.log(chalk_1.default.dim(' To create a token:'));
|
|
317
|
+
console.log(chalk_1.default.dim(' 1. Go to https://github.com/settings/tokens'));
|
|
318
|
+
console.log(chalk_1.default.dim(' 2. Click "Generate new token" → "Classic"'));
|
|
319
|
+
console.log(chalk_1.default.dim(' 3. Select scope: "repo" (Full control of private repositories)'));
|
|
320
|
+
console.log(chalk_1.default.dim(' 4. Generate and copy the token'));
|
|
321
|
+
console.log('');
|
|
313
322
|
const gitToken = await prompts.password({
|
|
314
323
|
message: 'GitHub Personal Access Token (leave empty to skip):',
|
|
315
324
|
});
|
|
@@ -446,7 +455,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
446
455
|
fs_1.default.writeFileSync(configPath, yamlContent);
|
|
447
456
|
console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
|
|
448
457
|
// Generate .env.genbox
|
|
449
|
-
await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo, envVarsToAdd);
|
|
458
|
+
await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo, envVarsToAdd, overwriteExisting);
|
|
450
459
|
// Show warnings
|
|
451
460
|
if (generated.warnings.length > 0) {
|
|
452
461
|
console.log('');
|
|
@@ -455,11 +464,29 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
455
464
|
console.log(chalk_1.default.dim(` - ${warning}`));
|
|
456
465
|
}
|
|
457
466
|
}
|
|
467
|
+
// Show API URL guidance if environments are configured
|
|
468
|
+
if (v3Config.environments && Object.keys(v3Config.environments).length > 0) {
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(chalk_1.default.yellow('Important: Update API URLs in .env.genbox for remote environments'));
|
|
471
|
+
console.log('');
|
|
472
|
+
for (const [envName, envConfig] of Object.entries(v3Config.environments)) {
|
|
473
|
+
const apiUrl = envConfig.api?.api ||
|
|
474
|
+
envConfig.api?.url ||
|
|
475
|
+
envConfig.api?.gateway;
|
|
476
|
+
if (apiUrl) {
|
|
477
|
+
console.log(chalk_1.default.dim(` For ${envName} profiles (connect_to: ${envName}):`));
|
|
478
|
+
console.log(chalk_1.default.cyan(` VITE_API_BASE_URL="${apiUrl}"`));
|
|
479
|
+
console.log(chalk_1.default.cyan(` VITE_AUTH_SERVICE_URL="${apiUrl}"`));
|
|
480
|
+
console.log(chalk_1.default.cyan(` NEXT_PUBLIC_API_BASE_URL="${apiUrl}"`));
|
|
481
|
+
console.log('');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
458
485
|
// Next steps
|
|
459
486
|
console.log('');
|
|
460
487
|
console.log(chalk_1.default.bold('Next steps:'));
|
|
461
488
|
console.log(chalk_1.default.dim(` 1. Review and edit ${CONFIG_FILENAME}`));
|
|
462
|
-
console.log(chalk_1.default.dim(` 2.
|
|
489
|
+
console.log(chalk_1.default.dim(` 2. Update API URLs in ${ENV_FILENAME} for staging/production`));
|
|
463
490
|
console.log(chalk_1.default.dim(` 3. Run 'genbox profiles' to see available profiles`));
|
|
464
491
|
console.log(chalk_1.default.dim(` 4. Run 'genbox create <name> --profile <profile>' to create an environment`));
|
|
465
492
|
}
|
|
@@ -739,11 +766,17 @@ async function setupEnvironments(scan, config, isMultiRepo = false) {
|
|
|
739
766
|
/**
|
|
740
767
|
* Setup .env.genbox file
|
|
741
768
|
*/
|
|
742
|
-
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}) {
|
|
769
|
+
async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false, extraEnvVars = {}, overwriteExisting = false) {
|
|
743
770
|
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
771
|
+
// If overwriting, delete existing file
|
|
744
772
|
if (fs_1.default.existsSync(envPath)) {
|
|
745
|
-
|
|
746
|
-
|
|
773
|
+
if (overwriteExisting) {
|
|
774
|
+
fs_1.default.unlinkSync(envPath);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
747
780
|
}
|
|
748
781
|
// For multi-repo: find env files in app directories
|
|
749
782
|
if (isMultiRepo && scan) {
|
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) {
|