genbox 1.0.13 → 1.0.15

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}`));
@@ -449,9 +393,12 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
449
393
  envVarsFromFile[match[1]] = value;
450
394
  }
451
395
  }
452
- // Determine API_URL based on profile's connect_to setting
453
- const connectTo = resolved.profile ?
454
- (config.profiles?.[resolved.profile]?.connect_to) : undefined;
396
+ // Determine API_URL based on profile's connect_to (v3) or default_connection (v4)
397
+ let connectTo;
398
+ if (resolved.profile && config.profiles?.[resolved.profile]) {
399
+ const profile = config.profiles[resolved.profile];
400
+ connectTo = (0, config_loader_1.getProfileConnection)(profile);
401
+ }
455
402
  let apiUrl;
456
403
  if (connectTo) {
457
404
  // Use the environment-specific API URL (e.g., STAGING_API_URL)
@@ -699,39 +646,28 @@ async function createLegacy(name, options) {
699
646
  gitToken: envVars.GIT_TOKEN,
700
647
  });
701
648
  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;
649
+ // Add SSH config immediately if IP available, otherwise spawn background process
650
+ if (genbox.ipAddress) {
651
+ const sshAdded = (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress: genbox.ipAddress });
652
+ if (sshAdded) {
653
+ console.log(chalk_1.default.dim(` SSH config added: ssh ${(0, ssh_config_1.getSshAlias)(name)}`));
714
654
  }
715
655
  }
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)}`));
656
+ else if (genbox._id) {
657
+ // Spawn background process to poll for IP and add SSH config
658
+ spawnSshConfigSetup(genbox._id, name);
659
+ console.log(chalk_1.default.dim(' SSH config will be added once IP is assigned.'));
729
660
  }
730
661
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
731
662
  console.log(` ${chalk_1.default.bold('Environment:')} ${name}`);
732
663
  console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.yellow(genbox.status)}`);
733
- console.log(` ${chalk_1.default.bold('IP:')} ${ipAddress}`);
664
+ if (genbox.ipAddress) {
665
+ console.log(` ${chalk_1.default.bold('IP:')} ${genbox.ipAddress}`);
666
+ }
734
667
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
668
+ // Inform user about server provisioning
669
+ console.log('');
670
+ console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
735
671
  }
736
672
  catch (error) {
737
673
  spinner.fail(chalk_1.default.red(`Failed: ${error.message}`));
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -7,21 +40,152 @@ exports.destroyCommand = void 0;
7
40
  const commander_1 = require("commander");
8
41
  const chalk_1 = __importDefault(require("chalk"));
9
42
  const confirm_1 = __importDefault(require("@inquirer/confirm"));
43
+ const select_1 = __importDefault(require("@inquirer/select"));
44
+ const prompts = __importStar(require("@inquirer/prompts"));
10
45
  const ora_1 = __importDefault(require("ora"));
11
46
  const api_1 = require("../api");
12
47
  const genbox_selector_1 = require("../genbox-selector");
13
48
  const ssh_config_1 = require("../ssh-config");
49
+ /**
50
+ * Format genbox for display in selection list
51
+ */
52
+ function formatGenboxChoice(g) {
53
+ const statusColor = g.status === 'running' ? chalk_1.default.green :
54
+ g.status === 'terminated' ? chalk_1.default.red : chalk_1.default.yellow;
55
+ const projectInfo = g.project ? chalk_1.default.dim(` [${g.project}]`) : '';
56
+ return {
57
+ name: `${g.name} ${statusColor(`(${g.status})`)} ${chalk_1.default.dim(g.ipAddress || 'No IP')}${projectInfo}`,
58
+ value: g,
59
+ };
60
+ }
61
+ /**
62
+ * Handle bulk delete flow when --all flag is used without a name
63
+ */
64
+ async function handleBulkDelete(options) {
65
+ const projectName = (0, genbox_selector_1.getProjectContext)();
66
+ // Fetch both project and global genboxes for counts
67
+ const allGenboxes = await (0, genbox_selector_1.getGenboxes)({ all: true });
68
+ const projectGenboxes = projectName
69
+ ? allGenboxes.filter(g => g.project === projectName || g.workspace === projectName)
70
+ : [];
71
+ if (allGenboxes.length === 0) {
72
+ console.log(chalk_1.default.yellow('No genboxes found.'));
73
+ return;
74
+ }
75
+ let targetGenboxes;
76
+ let scopeLabel;
77
+ // If in project context, ask user to choose scope
78
+ if (projectName && projectGenboxes.length > 0) {
79
+ const scopeChoices = [
80
+ {
81
+ name: `Delete from ${chalk_1.default.cyan(projectName)} project ${chalk_1.default.dim(`(${projectGenboxes.length} genbox${projectGenboxes.length === 1 ? '' : 'es'})`)}`,
82
+ value: 'project',
83
+ },
84
+ {
85
+ name: `Delete globally ${chalk_1.default.dim(`(${allGenboxes.length} genbox${allGenboxes.length === 1 ? '' : 'es'})`)}`,
86
+ value: 'global',
87
+ },
88
+ ];
89
+ const scope = await (0, select_1.default)({
90
+ message: 'Select deletion scope:',
91
+ choices: scopeChoices,
92
+ });
93
+ if (scope === 'project') {
94
+ targetGenboxes = projectGenboxes;
95
+ scopeLabel = `project '${projectName}'`;
96
+ }
97
+ else {
98
+ targetGenboxes = allGenboxes;
99
+ scopeLabel = 'all projects';
100
+ }
101
+ }
102
+ else {
103
+ // Not in project context or no project genboxes - show all
104
+ targetGenboxes = allGenboxes;
105
+ scopeLabel = 'all projects';
106
+ console.log(chalk_1.default.dim(`Found ${allGenboxes.length} genbox${allGenboxes.length === 1 ? '' : 'es'} globally.\n`));
107
+ }
108
+ if (targetGenboxes.length === 0) {
109
+ console.log(chalk_1.default.yellow(`No genboxes found in ${scopeLabel}.`));
110
+ return;
111
+ }
112
+ // Show checkbox selection for genboxes
113
+ const choices = targetGenboxes.map(g => ({
114
+ ...formatGenboxChoice(g),
115
+ checked: false, // Default to unchecked for safety
116
+ }));
117
+ console.log(''); // Add spacing
118
+ const selectedGenboxes = await prompts.checkbox({
119
+ message: `Select genboxes to destroy from ${scopeLabel}:`,
120
+ choices,
121
+ required: true,
122
+ });
123
+ if (selectedGenboxes.length === 0) {
124
+ console.log(chalk_1.default.yellow('No genboxes selected.'));
125
+ return;
126
+ }
127
+ // Show summary and confirm
128
+ console.log('');
129
+ console.log(chalk_1.default.yellow.bold('⚠️ The following genboxes will be PERMANENTLY destroyed:'));
130
+ console.log('');
131
+ selectedGenboxes.forEach(g => {
132
+ const statusColor = g.status === 'running' ? chalk_1.default.green : chalk_1.default.yellow;
133
+ console.log(` ${chalk_1.default.red('•')} ${g.name} ${statusColor(`(${g.status})`)} ${chalk_1.default.dim(g.ipAddress || 'No IP')}`);
134
+ });
135
+ console.log('');
136
+ let confirmed = options.yes;
137
+ if (!confirmed) {
138
+ confirmed = await (0, confirm_1.default)({
139
+ message: `Are you sure you want to destroy ${chalk_1.default.red(selectedGenboxes.length)} genbox${selectedGenboxes.length === 1 ? '' : 'es'}?`,
140
+ default: false,
141
+ });
142
+ }
143
+ if (!confirmed) {
144
+ console.log(chalk_1.default.dim('Operation cancelled.'));
145
+ return;
146
+ }
147
+ // Delete each genbox
148
+ console.log('');
149
+ let successCount = 0;
150
+ let failCount = 0;
151
+ for (const genbox of selectedGenboxes) {
152
+ const spinner = (0, ora_1.default)(`Destroying ${genbox.name}...`).start();
153
+ try {
154
+ await (0, api_1.fetchApi)(`/genboxes/${genbox._id}`, { method: 'DELETE' });
155
+ (0, ssh_config_1.removeSshConfigEntry)(genbox.name);
156
+ spinner.succeed(chalk_1.default.green(`Destroyed '${genbox.name}'`));
157
+ successCount++;
158
+ }
159
+ catch (error) {
160
+ spinner.fail(chalk_1.default.red(`Failed to destroy '${genbox.name}': ${error.message}`));
161
+ failCount++;
162
+ }
163
+ }
164
+ // Summary
165
+ console.log('');
166
+ if (failCount === 0) {
167
+ console.log(chalk_1.default.green.bold(`✓ Successfully destroyed ${successCount} genbox${successCount === 1 ? '' : 'es'}.`));
168
+ }
169
+ else {
170
+ console.log(chalk_1.default.yellow(`Completed: ${successCount} destroyed, ${failCount} failed.`));
171
+ }
172
+ }
14
173
  exports.destroyCommand = new commander_1.Command('destroy')
15
174
  .alias('delete')
16
- .description('Destroy a Genbox')
175
+ .description('Destroy one or more Genboxes')
17
176
  .argument('[name]', 'Name of the Genbox to destroy (optional - will prompt if not provided)')
18
177
  .option('-y, --yes', 'Skip confirmation')
19
- .option('-a, --all', 'Select from all genboxes (not just current project)')
178
+ .option('-a, --all', 'Bulk delete mode - select multiple genboxes to destroy')
20
179
  .action(async (name, options) => {
21
180
  try {
22
- // 1. Select Genbox (interactive if no name provided)
181
+ // Bulk delete mode when --all is used without a specific name
182
+ if (options.all && !name) {
183
+ await handleBulkDelete(options);
184
+ return;
185
+ }
186
+ // Single genbox deletion flow
23
187
  const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
24
- all: options.all,
188
+ all: options.all, // If --all with name, search in all genboxes
25
189
  selectMessage: 'Select a genbox to destroy:',
26
190
  });
27
191
  if (cancelled) {
@@ -31,7 +195,7 @@ exports.destroyCommand = new commander_1.Command('destroy')
31
195
  if (!target) {
32
196
  return;
33
197
  }
34
- // 2. Confirm
198
+ // Confirm
35
199
  let confirmed = options.yes;
36
200
  if (!confirmed) {
37
201
  confirmed = await (0, confirm_1.default)({
@@ -43,7 +207,7 @@ exports.destroyCommand = new commander_1.Command('destroy')
43
207
  console.log('Operation cancelled.');
44
208
  return;
45
209
  }
46
- // 3. Delete
210
+ // Delete
47
211
  const spinner = (0, ora_1.default)(`Destroying ${target.name}...`).start();
48
212
  await (0, api_1.fetchApi)(`/genboxes/${target._id}`, {
49
213
  method: 'DELETE',