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.
@@ -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 successfully!`));
175
- // Add SSH config
176
- if (genbox.ipAddress) {
177
- const sshAdded = (0, ssh_config_1.addSshConfigEntry)({
178
- name,
179
- ipAddress: genbox.ipAddress,
180
- });
181
- if (sshAdded) {
182
- console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
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 (genbox.ipAddress) {
469
- (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress: genbox.ipAddress });
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
- if (genbox.ipAddress) {
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) {
@@ -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. Add secrets to ${ENV_FILENAME}`));
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
- console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
746
- return;
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) {
@@ -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 {
@@ -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
- return envConfig.api.url || envConfig.api.gateway;
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
- // Add API URL based on resolution
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {