genbox 1.0.22 → 1.0.24

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/api.js CHANGED
@@ -7,6 +7,7 @@ exports.AuthenticationError = void 0;
7
7
  exports.handleApiError = handleApiError;
8
8
  exports.isAuthError = isAuthError;
9
9
  exports.fetchApi = fetchApi;
10
+ exports.checkNameAvailability = checkNameAvailability;
10
11
  const chalk_1 = __importDefault(require("chalk"));
11
12
  const config_store_1 = require("./config-store");
12
13
  const API_URL = process.env.GENBOX_API_URL || 'https://api.genbox.dev';
@@ -74,3 +75,9 @@ async function fetchApi(endpoint, options = {}) {
74
75
  }
75
76
  return response.json();
76
77
  }
78
+ /**
79
+ * Check if a genbox name is available in a workspace
80
+ */
81
+ async function checkNameAvailability(name, workspace) {
82
+ return fetchApi(`/genboxes/check-name?name=${encodeURIComponent(name)}&workspace=${encodeURIComponent(workspace)}`);
83
+ }
@@ -50,6 +50,7 @@ const api_1 = require("../api");
50
50
  const ssh_config_1 = require("../ssh-config");
51
51
  const schema_v4_1 = require("../schema-v4");
52
52
  const child_process_1 = require("child_process");
53
+ const random_name_1 = require("../random-name");
53
54
  /**
54
55
  * Spawn a background process to poll for IP and add SSH config
55
56
  * This runs detached so the main process can exit immediately
@@ -99,9 +100,85 @@ function getPrivateSshKey() {
99
100
  }
100
101
  return undefined;
101
102
  }
103
+ /**
104
+ * Prompt user for environment name
105
+ */
106
+ async function promptForName(skipPrompts) {
107
+ if (skipPrompts) {
108
+ return (0, random_name_1.generateRandomName)();
109
+ }
110
+ const suggestions = (0, random_name_1.generateNameSuggestions)(3);
111
+ const nameChoice = await prompts.select({
112
+ message: 'Environment name:',
113
+ choices: [
114
+ {
115
+ name: `${suggestions[0]} (random)`,
116
+ value: suggestions[0],
117
+ },
118
+ {
119
+ name: `${suggestions[1]} (random)`,
120
+ value: suggestions[1],
121
+ },
122
+ {
123
+ name: `${suggestions[2]} (random)`,
124
+ value: suggestions[2],
125
+ },
126
+ {
127
+ name: 'Enter custom name...',
128
+ value: '__custom__',
129
+ },
130
+ ],
131
+ });
132
+ if (nameChoice === '__custom__') {
133
+ const customName = await prompts.input({
134
+ message: 'Enter environment name:',
135
+ validate: (value) => {
136
+ if (!value.trim())
137
+ return 'Name is required';
138
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(value.trim())) {
139
+ return 'Name must be lowercase, start/end with letter or number, and contain only letters, numbers, and hyphens';
140
+ }
141
+ return true;
142
+ },
143
+ });
144
+ return customName.trim();
145
+ }
146
+ return nameChoice;
147
+ }
148
+ /**
149
+ * Prompt user to select a profile
150
+ */
151
+ async function promptForProfile(profiles) {
152
+ const profileNames = Object.keys(profiles);
153
+ // Build choices with profile descriptions
154
+ const choices = profileNames.map(name => {
155
+ const profile = profiles[name];
156
+ const apps = profile.apps ? profile.apps.join(', ') : 'default apps';
157
+ const description = profile.description || `Apps: ${apps}`;
158
+ return {
159
+ name: `${name} - ${chalk_1.default.dim(description)}`,
160
+ value: name,
161
+ };
162
+ });
163
+ // Add custom option
164
+ choices.push({
165
+ name: `${chalk_1.default.cyan('Custom')} - ${chalk_1.default.dim('Configure manually')}`,
166
+ value: '__custom__',
167
+ });
168
+ console.log(chalk_1.default.blue('=== Profile Selection ==='));
169
+ console.log('');
170
+ const selected = await prompts.select({
171
+ message: 'Select a profile:',
172
+ choices,
173
+ });
174
+ if (selected === '__custom__') {
175
+ return undefined; // No profile, will use default behavior
176
+ }
177
+ return selected;
178
+ }
102
179
  exports.createCommand = new commander_1.Command('create')
103
180
  .description('Create a new Genbox environment')
104
- .argument('<name>', 'Name of the Genbox')
181
+ .argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
105
182
  .option('-p, --profile <profile>', 'Use a predefined profile')
106
183
  .option('-a, --apps <apps>', 'Comma-separated list of apps to include')
107
184
  .option('--add-apps <apps>', 'Add apps to the profile')
@@ -114,7 +191,7 @@ exports.createCommand = new commander_1.Command('create')
114
191
  .option('-f, --from-branch <branch>', 'Source branch to create new branch from (defaults to current/default branch)')
115
192
  .option('-y, --yes', 'Skip interactive prompts')
116
193
  .option('--dry-run', 'Show what would be created without actually creating')
117
- .action(async (name, options) => {
194
+ .action(async (nameArg, options) => {
118
195
  try {
119
196
  // Load configuration
120
197
  const configLoader = new config_loader_1.ConfigLoader();
@@ -122,12 +199,53 @@ exports.createCommand = new commander_1.Command('create')
122
199
  const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
123
200
  if (!loadResult.config || configVersion === 'unknown') {
124
201
  // Fall back to legacy v1/v2 handling
202
+ const name = nameArg || await promptForName(options.yes);
125
203
  await createLegacy(name, options);
126
204
  return;
127
205
  }
128
206
  // Support both v3 and v4 configs
129
207
  const config = loadResult.config;
130
208
  const profileResolver = new profile_resolver_1.ProfileResolver(configLoader);
209
+ // Interactive name prompt if not provided
210
+ let name = nameArg;
211
+ if (!name && !options.yes) {
212
+ name = await promptForName(false);
213
+ }
214
+ else if (!name && options.yes) {
215
+ // Auto-generate name in non-interactive mode
216
+ name = (0, random_name_1.generateRandomName)();
217
+ console.log(chalk_1.default.dim(` Auto-generated name: ${name}`));
218
+ }
219
+ // Interactive profile selection if profiles exist and no -p option provided
220
+ let selectedProfile = options.profile;
221
+ if (!selectedProfile && !options.yes && config.profiles && Object.keys(config.profiles).length > 0) {
222
+ selectedProfile = await promptForProfile(config.profiles);
223
+ }
224
+ // Check if name is available in workspace, add suffix if taken
225
+ const workspace = config.project?.name || 'default';
226
+ try {
227
+ let { available } = await (0, api_1.checkNameAvailability)(name, workspace);
228
+ if (!available) {
229
+ // Add random suffix and retry
230
+ const originalName = name;
231
+ const suffix = Math.random().toString(36).substring(2, 6);
232
+ name = `${originalName}-${suffix}`;
233
+ // Verify the new name is available
234
+ const recheck = await (0, api_1.checkNameAvailability)(name, workspace);
235
+ if (!recheck.available) {
236
+ // Extremely unlikely, but fall back to timestamp
237
+ name = `${originalName}-${Date.now().toString(36)}`;
238
+ }
239
+ console.log(chalk_1.default.yellow(` Name '${originalName}' is already in use, using '${name}' instead`));
240
+ }
241
+ }
242
+ catch (error) {
243
+ // If check fails (e.g., not logged in), continue - API will catch it on create
244
+ if (error instanceof api_1.AuthenticationError) {
245
+ throw error;
246
+ }
247
+ // Silently continue for other errors - the create API will validate
248
+ }
131
249
  // Validate branch options
132
250
  if (options.newBranch && !options.fromBranch) {
133
251
  console.error(chalk_1.default.red('Error: --new-branch (-n) requires --from-branch (-f) to specify the source branch'));
@@ -145,7 +263,7 @@ exports.createCommand = new commander_1.Command('create')
145
263
  // Build create options
146
264
  const createOptions = {
147
265
  name,
148
- profile: options.profile,
266
+ profile: selectedProfile,
149
267
  apps: options.apps ? options.apps.split(',').map((a) => a.trim()) : undefined,
150
268
  addApps: options.addApps ? options.addApps.split(',').map((a) => a.trim()) : undefined,
151
269
  api: options.api,
@@ -162,14 +280,15 @@ exports.createCommand = new commander_1.Command('create')
162
280
  console.log(chalk_1.default.blue('Resolving configuration...'));
163
281
  console.log('');
164
282
  let resolved = await profileResolver.resolve(config, createOptions);
165
- // Interactive branch selection if no branch was specified
166
- if (!options.branch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
283
+ // Interactive branch selection if no branch options were specified
284
+ // Skip if: -b (existing branch), -f (new branch from source), -n (explicit new branch name), or -y (skip prompts)
285
+ if (!options.branch && !options.fromBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
167
286
  resolved = await promptForBranchOptions(resolved, config);
168
287
  }
169
288
  // Display resolved configuration
170
289
  displayResolvedConfig(resolved);
171
290
  // Ask to save as profile (if not using one already)
172
- if (!options.profile && !options.yes && !options.dryRun) {
291
+ if (!selectedProfile && !options.yes && !options.dryRun) {
173
292
  await profileResolver.askSaveProfile(config, resolved);
174
293
  }
175
294
  // Dry run mode
@@ -1174,9 +1174,6 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1174
1174
  }
1175
1175
  }
1176
1176
  if (Object.keys(urls).length > 0) {
1177
- // Add database URLs
1178
- urls['mongodb'] = '${STAGING_MONGODB_URL}';
1179
- urls['redis'] = '${STAGING_REDIS_URL}';
1180
1177
  environments.staging = {
1181
1178
  description: 'Staging environment',
1182
1179
  urls,
@@ -1207,8 +1204,6 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1207
1204
  description: 'Staging environment',
1208
1205
  urls: {
1209
1206
  api: stagingApiUrl,
1210
- mongodb: '${STAGING_MONGODB_URL}',
1211
- redis: '${STAGING_REDIS_URL}',
1212
1207
  },
1213
1208
  };
1214
1209
  }
@@ -1238,8 +1233,6 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1238
1233
  description: 'Staging environment',
1239
1234
  urls: {
1240
1235
  api: stagingApiUrl,
1241
- mongodb: '${STAGING_MONGODB_URL}',
1242
- redis: '${STAGING_REDIS_URL}',
1243
1236
  },
1244
1237
  };
1245
1238
  }
@@ -1279,7 +1272,6 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1279
1272
  }
1280
1273
  }
1281
1274
  if (Object.keys(prodUrls).length > 0) {
1282
- prodUrls['mongodb'] = '${PROD_MONGODB_URL}';
1283
1275
  environments.production = {
1284
1276
  description: 'Production (use with caution)',
1285
1277
  urls: prodUrls,
@@ -1314,7 +1306,6 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
1314
1306
  description: 'Production (use with caution)',
1315
1307
  urls: {
1316
1308
  api: prodApiUrl,
1317
- mongodb: '${PROD_MONGODB_URL}',
1318
1309
  },
1319
1310
  safety: {
1320
1311
  read_only: true,
@@ -506,8 +506,9 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
506
506
  console.log(chalk_1.default.blue('Resolving configuration...'));
507
507
  console.log('');
508
508
  let resolved = await profileResolver.resolve(config, createOptions);
509
- // Interactive branch selection only if no branch specified and no stored branch
510
- if (!options.branch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
509
+ // Interactive branch selection only if no branch options specified and no stored branch
510
+ // Skip if: -b, -f, -n, stored branch, or -y
511
+ if (!options.branch && !options.fromBranch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
511
512
  resolved = await promptForBranchOptions(resolved, config);
512
513
  }
513
514
  // Display what will be rebuilt
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Random name generator for Genbox environments
4
+ * Generates memorable names using adjective-noun combinations
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.generateRandomName = generateRandomName;
8
+ exports.generateNameSuggestions = generateNameSuggestions;
9
+ const adjectives = [
10
+ 'swift', 'calm', 'bold', 'keen', 'warm',
11
+ 'cool', 'bright', 'quick', 'smart', 'fresh',
12
+ 'wild', 'free', 'pure', 'soft', 'sharp',
13
+ 'kind', 'brave', 'proud', 'fair', 'wise',
14
+ 'gold', 'silver', 'azure', 'coral', 'amber',
15
+ 'jade', 'ruby', 'pearl', 'ivory', 'onyx',
16
+ 'misty', 'sunny', 'windy', 'rainy', 'snowy',
17
+ 'lucky', 'happy', 'merry', 'jolly', 'lively',
18
+ ];
19
+ const nouns = [
20
+ 'fox', 'owl', 'wolf', 'hawk', 'bear',
21
+ 'deer', 'hare', 'lynx', 'seal', 'dove',
22
+ 'oak', 'pine', 'elm', 'ash', 'birch',
23
+ 'maple', 'cedar', 'willow', 'sage', 'fern',
24
+ 'river', 'lake', 'creek', 'brook', 'pond',
25
+ 'peak', 'vale', 'ridge', 'cliff', 'mesa',
26
+ 'moon', 'star', 'sun', 'cloud', 'wind',
27
+ 'storm', 'frost', 'dawn', 'dusk', 'wave',
28
+ ];
29
+ function randomElement(array) {
30
+ return array[Math.floor(Math.random() * array.length)];
31
+ }
32
+ /**
33
+ * Generate a random environment name
34
+ * Format: adjective-noun (e.g., "swift-fox", "calm-river")
35
+ */
36
+ function generateRandomName() {
37
+ const adjective = randomElement(adjectives);
38
+ const noun = randomElement(nouns);
39
+ return `${adjective}-${noun}`;
40
+ }
41
+ /**
42
+ * Generate multiple random name suggestions
43
+ */
44
+ function generateNameSuggestions(count = 3) {
45
+ const names = new Set();
46
+ while (names.size < count) {
47
+ names.add(generateRandomName());
48
+ }
49
+ return Array.from(names);
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {