genbox 1.0.14 → 1.0.16

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.
@@ -41,6 +41,7 @@ const commander_1 = require("commander");
41
41
  const chalk_1 = __importDefault(require("chalk"));
42
42
  const api_1 = require("../api");
43
43
  const genbox_selector_1 = require("../genbox-selector");
44
+ const ssh_config_1 = require("../ssh-config");
44
45
  const os = __importStar(require("os"));
45
46
  const path = __importStar(require("path"));
46
47
  const fs = __importStar(require("fs"));
@@ -80,9 +81,13 @@ exports.connectCommand = new commander_1.Command('connect')
80
81
  console.error(chalk_1.default.yellow(`Genbox '${target.name}' is still provisioning (no IP). Please wait.`));
81
82
  return;
82
83
  }
83
- // 2. Get Key
84
+ // 2. Ensure SSH config exists (in case background setup hasn't finished)
85
+ if (!(0, ssh_config_1.hasSshConfigEntry)(target.name)) {
86
+ (0, ssh_config_1.addSshConfigEntry)({ name: target.name, ipAddress: target.ipAddress });
87
+ }
88
+ // 3. Get Key
84
89
  const keyPath = getPrivateSshKey();
85
- // 3. Connect
90
+ // 4. Connect
86
91
  console.log(chalk_1.default.dim(`Connecting to ${chalk_1.default.bold(target.name)} (${target.ipAddress})...`));
87
92
  const sshArgs = [
88
93
  '-i', keyPath,
@@ -48,64 +48,22 @@ 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 schema_v4_1 = require("../schema-v4");
51
52
  const child_process_1 = require("child_process");
52
53
  /**
53
- * Poll for genbox IP address (servers take a few seconds to get an IP assigned)
54
+ * Spawn a background process to poll for IP and add SSH config
55
+ * This runs detached so the main process can exit immediately
54
56
  */
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;
57
+ function spawnSshConfigSetup(genboxId, name) {
58
+ // Get the path to the CLI executable
59
+ const cliPath = process.argv[1];
60
+ // Spawn genbox ssh-setup in background
61
+ const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'ssh-setup', genboxId, name], {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ });
65
+ // Allow parent to exit independently
66
+ child.unref();
109
67
  }
110
68
  async function provisionGenbox(payload) {
111
69
  return (0, api_1.fetchApi)('/genboxes', {
@@ -159,11 +117,13 @@ exports.createCommand = new commander_1.Command('create')
159
117
  // Load configuration
160
118
  const configLoader = new config_loader_1.ConfigLoader();
161
119
  const loadResult = await configLoader.load();
162
- if (!loadResult.config || loadResult.config.version !== '3.0') {
120
+ const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
121
+ if (!loadResult.config || configVersion === 'unknown') {
163
122
  // Fall back to legacy v1/v2 handling
164
123
  await createLegacy(name, options);
165
124
  return;
166
125
  }
126
+ // Support both v3 and v4 configs
167
127
  const config = loadResult.config;
168
128
  const profileResolver = new profile_resolver_1.ProfileResolver(configLoader);
169
129
  // Build create options
@@ -210,7 +170,8 @@ exports.createCommand = new commander_1.Command('create')
210
170
  const publicKey = getPublicSshKey();
211
171
  // Check if SSH auth is needed for git
212
172
  let privateKeyContent;
213
- const usesSSH = config.git_auth?.method === 'ssh' ||
173
+ const v3Config = config;
174
+ const usesSSH = v3Config.git_auth?.method === 'ssh' ||
214
175
  Object.values(config.repos || {}).some(r => r.auth === 'ssh');
215
176
  if (usesSSH && !options.yes) {
216
177
  const injectKey = await prompts.confirm({
@@ -231,40 +192,23 @@ exports.createCommand = new commander_1.Command('create')
231
192
  try {
232
193
  const genbox = await provisionGenbox(payload);
233
194
  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;
195
+ // Add SSH config immediately if IP available, otherwise spawn background process
196
+ if (genbox.ipAddress) {
197
+ const sshAdded = (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress: genbox.ipAddress });
198
+ if (sshAdded) {
199
+ console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
247
200
  }
248
201
  }
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)}`));
202
+ else if (genbox._id) {
203
+ // Spawn background process to poll for IP and add SSH config
204
+ spawnSshConfigSetup(genbox._id, name);
205
+ console.log(chalk_1.default.dim(' SSH config will be added once IP is assigned.'));
265
206
  }
266
207
  // Display results
267
208
  displayGenboxInfo(genbox, resolved);
209
+ // Inform user about server provisioning
210
+ console.log('');
211
+ console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
268
212
  }
269
213
  catch (error) {
270
214
  spinner.fail(chalk_1.default.red(`Failed to create Genbox: ${error.message}`));
@@ -375,10 +319,46 @@ function parseEnvGenboxSections(content) {
375
319
  }
376
320
  return sections;
377
321
  }
322
+ /**
323
+ * Build a map of service URL variables based on connection type
324
+ * e.g., if connectTo=staging: GATEWAY_URL → STAGING_GATEWAY_URL value
325
+ */
326
+ function buildServiceUrlMap(envVarsFromFile, connectTo) {
327
+ const urlMap = {};
328
+ const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
329
+ // Find all service URL variables (LOCAL_*_URL and STAGING_*_URL patterns)
330
+ const serviceNames = new Set();
331
+ for (const key of Object.keys(envVarsFromFile)) {
332
+ const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
333
+ if (match) {
334
+ serviceNames.add(match[2]);
335
+ }
336
+ }
337
+ // Build mapping: VARNAME → value from appropriate prefix
338
+ for (const serviceName of serviceNames) {
339
+ const prefixedKey = `${prefix}${serviceName}`;
340
+ const localKey = `LOCAL_${serviceName}`;
341
+ // Use prefixed value if available, otherwise fall back to local
342
+ const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
343
+ if (value) {
344
+ urlMap[serviceName] = value;
345
+ }
346
+ }
347
+ // Also handle legacy API_URL for backwards compatibility
348
+ if (!urlMap['API_URL']) {
349
+ const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
350
+ envVarsFromFile['LOCAL_API_URL'] ||
351
+ envVarsFromFile['STAGING_API_URL'];
352
+ if (apiUrl) {
353
+ urlMap['API_URL'] = apiUrl;
354
+ }
355
+ }
356
+ return urlMap;
357
+ }
378
358
  /**
379
359
  * Build env content for a specific app by combining GLOBAL + app-specific sections
380
360
  */
381
- function buildAppEnvContent(sections, appName, apiUrl) {
361
+ function buildAppEnvContent(sections, appName, serviceUrlMap) {
382
362
  const parts = [];
383
363
  // Always include GLOBAL section
384
364
  const globalSection = sections.get('GLOBAL');
@@ -391,8 +371,11 @@ function buildAppEnvContent(sections, appName, apiUrl) {
391
371
  parts.push(appSection);
392
372
  }
393
373
  let envContent = parts.join('\n\n');
394
- // Expand ${API_URL} references
395
- envContent = envContent.replace(/\$\{API_URL\}/g, apiUrl);
374
+ // Expand all ${VARNAME} references using the service URL map
375
+ for (const [varName, value] of Object.entries(serviceUrlMap)) {
376
+ const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
377
+ envContent = envContent.replace(pattern, value);
378
+ }
396
379
  // Keep only actual env vars (filter out pure comment lines but keep var definitions)
397
380
  envContent = envContent
398
381
  .split('\n')
@@ -449,18 +432,19 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
449
432
  envVarsFromFile[match[1]] = value;
450
433
  }
451
434
  }
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';
435
+ // Determine connection type from profile's connect_to (v3) or default_connection (v4)
436
+ let connectTo;
437
+ if (resolved.profile && config.profiles?.[resolved.profile]) {
438
+ const profile = config.profiles[resolved.profile];
439
+ connectTo = (0, config_loader_1.getProfileConnection)(profile);
460
440
  }
461
- else {
462
- // Use local API URL
463
- apiUrl = envVarsFromFile['LOCAL_API_URL'] || 'http://localhost:3050';
441
+ // Build service URL map for variable expansion
442
+ // This maps GATEWAY_URL → STAGING_GATEWAY_URL value (if connectTo=staging)
443
+ // or GATEWAY_URL LOCAL_GATEWAY_URL value (if local)
444
+ const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
445
+ // Log what's being expanded for debugging
446
+ if (connectTo && Object.keys(serviceUrlMap).length > 0) {
447
+ console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
464
448
  }
465
449
  // Add env file for each app - filtered by selected apps only
466
450
  for (const app of resolved.apps) {
@@ -474,7 +458,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
474
458
  for (const serviceSectionName of servicesSections) {
475
459
  const serviceName = serviceSectionName.split('/')[1];
476
460
  // Build service-specific env content (GLOBAL + service section)
477
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, apiUrl);
461
+ const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
478
462
  const stagingName = `${app.name}-${serviceName}.env`;
479
463
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
480
464
  files.push({
@@ -487,7 +471,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
487
471
  }
488
472
  else {
489
473
  // Regular app - build app-specific env content (GLOBAL + app section)
490
- const appEnvContent = buildAppEnvContent(sections, app.name, apiUrl);
474
+ const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
491
475
  files.push({
492
476
  path: `/home/dev/.env-staging/${app.name}.env`,
493
477
  content: appEnvContent,
@@ -699,39 +683,28 @@ async function createLegacy(name, options) {
699
683
  gitToken: envVars.GIT_TOKEN,
700
684
  });
701
685
  spinner.succeed(chalk_1.default.green(`Genbox '${name}' created!`));
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;
686
+ // Add SSH config immediately if IP available, otherwise spawn background process
687
+ if (genbox.ipAddress) {
688
+ const sshAdded = (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress: genbox.ipAddress });
689
+ if (sshAdded) {
690
+ console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
714
691
  }
715
692
  }
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)}`));
693
+ else if (genbox._id) {
694
+ // Spawn background process to poll for IP and add SSH config
695
+ spawnSshConfigSetup(genbox._id, name);
696
+ console.log(chalk_1.default.dim(' SSH config will be added once IP is assigned.'));
729
697
  }
730
698
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
731
699
  console.log(` ${chalk_1.default.bold('Environment:')} ${name}`);
732
700
  console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.yellow(genbox.status)}`);
733
- console.log(` ${chalk_1.default.bold('IP:')} ${ipAddress}`);
701
+ if (genbox.ipAddress) {
702
+ console.log(` ${chalk_1.default.bold('IP:')} ${genbox.ipAddress}`);
703
+ }
734
704
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
705
+ // Inform user about server provisioning
706
+ console.log('');
707
+ console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
735
708
  }
736
709
  catch (error) {
737
710
  spinner.fail(chalk_1.default.red(`Failed: ${error.message}`));