genbox 1.0.3 → 1.0.4

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.
@@ -38,451 +38,718 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.initCommand = void 0;
40
40
  const commander_1 = require("commander");
41
- const inquirer_1 = __importDefault(require("inquirer"));
41
+ const prompts = __importStar(require("@inquirer/prompts"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
+ const ora_1 = __importDefault(require("ora"));
43
44
  const path_1 = __importDefault(require("path"));
44
45
  const fs_1 = __importDefault(require("fs"));
45
- const config_1 = require("../config");
46
- const scan_1 = require("../scan");
46
+ const yaml = __importStar(require("js-yaml"));
47
47
  const process = __importStar(require("process"));
48
48
  const os = __importStar(require("os"));
49
+ const scanner_1 = require("../scanner");
50
+ const config_generator_1 = require("../scanner/config-generator");
51
+ const scan_1 = require("../scan");
52
+ const CONFIG_FILENAME = 'genbox.yaml';
53
+ const ENV_FILENAME = '.env.genbox';
49
54
  exports.initCommand = new commander_1.Command('init')
50
55
  .description('Initialize a new Genbox configuration')
51
- .action(async () => {
56
+ .option('--v2', 'Use legacy v2 format (single-app only)')
57
+ .option('--workspace', 'Initialize as workspace config (for multi-repo projects)')
58
+ .option('--force', 'Overwrite existing configuration')
59
+ .option('-y, --yes', 'Use defaults without prompting')
60
+ .option('--exclude <dirs>', 'Comma-separated directories to exclude')
61
+ .option('--name <name>', 'Project name (for non-interactive mode)')
62
+ .action(async (options) => {
52
63
  try {
53
- if ((0, config_1.hasConfig)()) {
64
+ const configPath = path_1.default.join(process.cwd(), CONFIG_FILENAME);
65
+ const nonInteractive = options.yes || !process.stdin.isTTY;
66
+ // Check for existing config
67
+ if (fs_1.default.existsSync(configPath) && !options.force) {
68
+ if (nonInteractive) {
69
+ console.log(chalk_1.default.yellow('genbox.yaml already exists. Use --force to overwrite.'));
70
+ return;
71
+ }
54
72
  console.log(chalk_1.default.yellow('genbox.yaml already exists.'));
55
- const { overwrite } = await inquirer_1.default.prompt([
56
- {
57
- type: 'confirm',
58
- name: 'overwrite',
59
- message: 'Do you want to overwrite it?',
60
- default: false,
61
- },
62
- ]);
73
+ const overwrite = await prompts.confirm({
74
+ message: 'Do you want to overwrite it?',
75
+ default: false,
76
+ });
63
77
  if (!overwrite) {
64
78
  return;
65
79
  }
66
80
  }
67
81
  console.log(chalk_1.default.blue('Initializing Genbox...'));
68
- console.log(chalk_1.default.dim('Scanning project...'));
69
- const info = (0, scan_1.scanProject)(process.cwd());
70
- console.log(chalk_1.default.green(`Detected: ${info.name}`));
71
- if (info.hasDocker)
72
- console.log(chalk_1.default.dim(' - Docker detected'));
73
- // 1. Project Name
74
- const { project_name } = await inquirer_1.default.prompt([
75
- {
76
- type: 'input',
77
- name: 'project_name',
78
- message: 'Project Name:',
79
- default: info.name,
82
+ console.log('');
83
+ // Get directory exclusions
84
+ let exclude = [];
85
+ if (options.exclude) {
86
+ exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
87
+ }
88
+ else if (!nonInteractive) {
89
+ const excludeDirs = await prompts.input({
90
+ message: 'Directories to exclude (comma-separated, or empty to skip):',
91
+ default: '',
92
+ });
93
+ exclude = excludeDirs
94
+ ? excludeDirs.split(',').map((d) => d.trim()).filter(Boolean)
95
+ : [];
96
+ }
97
+ // Scan project (skip scripts initially - we'll ask about them later)
98
+ const spinner = (0, ora_1.default)('Scanning project...').start();
99
+ const scanner = new scanner_1.ProjectScanner();
100
+ const scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
101
+ spinner.succeed('Project scanned');
102
+ // Display scan results
103
+ console.log('');
104
+ console.log(chalk_1.default.bold('Detected:'));
105
+ console.log(` ${chalk_1.default.dim('Project:')} ${scan.projectName}`);
106
+ console.log(` ${chalk_1.default.dim('Structure:')} ${scan.structure.type} (${scan.structure.confidence} confidence)`);
107
+ if (scan.runtimes.length > 0) {
108
+ const runtimeStr = scan.runtimes
109
+ .map(r => `${r.language}${r.version ? ` ${r.version}` : ''}`)
110
+ .join(', ');
111
+ console.log(` ${chalk_1.default.dim('Runtimes:')} ${runtimeStr}`);
112
+ }
113
+ if (scan.frameworks.length > 0) {
114
+ const frameworkStr = scan.frameworks.map(f => f.name).join(', ');
115
+ console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
116
+ }
117
+ if (scan.apps.length > 0) {
118
+ console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
119
+ for (const app of scan.apps.slice(0, 5)) {
120
+ console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
80
121
  }
81
- ]);
82
- // 2. Scripts Selection
83
- let selectedScripts = [];
84
- if (info.foundScripts.length > 0) {
85
- const scriptAnswers = await inquirer_1.default.prompt([
86
- {
87
- type: 'checkbox',
88
- name: 'scripts',
89
- message: 'Select setup scripts to run:',
90
- choices: info.foundScripts,
91
- default: info.foundScripts,
92
- },
93
- ]);
94
- selectedScripts = scriptAnswers.scripts;
95
- }
96
- // 3. Server Size
97
- const { server_size } = await inquirer_1.default.prompt([
98
- {
99
- type: 'select',
100
- name: 'server_size',
101
- message: 'Default Server Size:',
102
- choices: ['small', 'medium', 'large', 'xl'],
103
- default: 'small',
122
+ if (scan.apps.length > 5) {
123
+ console.log(` ... and ${scan.apps.length - 5} more`);
104
124
  }
105
- ]);
106
- // 4. Git Repository Setup
107
- let gitAuth;
108
- let repoUrl = info.gitRemote;
109
- let repoAuthMethod;
110
- if (info.gitRemote) {
111
- console.log('');
112
- console.log(chalk_1.default.blue('=== Git Repository Setup ==='));
113
- console.log(chalk_1.default.dim(`Detected remote: ${info.gitRemote}`));
114
- // Ask about authentication method - default to PAT (token)
115
- const { authMethod } = await inquirer_1.default.prompt([
116
- {
117
- type: 'select',
118
- name: 'authMethod',
119
- message: 'How should the genbox access this repository?',
120
- choices: [
121
- { name: 'Personal Access Token (PAT) - recommended', value: 'token' },
122
- { name: 'SSH Key - use existing key', value: 'ssh' },
123
- { name: 'Public - no authentication needed', value: 'public' },
124
- ],
125
- default: 'token',
126
- }
127
- ]);
128
- if (authMethod === 'token') {
129
- repoAuthMethod = 'token';
130
- // Convert SSH URL to HTTPS if needed
131
- if (info.gitRemoteType === 'ssh') {
132
- const httpsUrl = (0, scan_1.sshToHttps)(info.gitRemote);
133
- console.log(chalk_1.default.dim(` Will use HTTPS URL: ${httpsUrl}`));
134
- repoUrl = httpsUrl;
135
- }
136
- gitAuth = { method: 'token' };
137
- // Detect git provider for specific instructions
138
- const isGitHub = info.gitRemote.includes('github.com');
139
- const isGitLab = info.gitRemote.includes('gitlab.com');
140
- const isBitbucket = info.gitRemote.includes('bitbucket.org');
141
- console.log('');
142
- console.log(chalk_1.default.yellow(' Personal Access Token Setup:'));
143
- console.log('');
144
- if (isGitHub) {
145
- console.log(chalk_1.default.bold(' GitHub Fine-grained Token Setup:'));
146
- console.log(chalk_1.default.dim(' 1. Go to: https://github.com/settings/tokens?type=beta'));
147
- console.log(chalk_1.default.dim(' 2. Click "Generate new token"'));
148
- console.log(chalk_1.default.dim(' 3. Token name: "genbox-' + project_name + '"'));
149
- console.log(chalk_1.default.dim(' 4. Set expiration (90 days or custom)'));
150
- console.log(chalk_1.default.dim(' 5. Repository access: Select "Only select repositories"'));
151
- console.log(chalk_1.default.dim(' Then choose your repository from the dropdown'));
152
- console.log(chalk_1.default.dim(' 6. Scroll down to "Repository permissions" (not Account permissions!)'));
153
- console.log(chalk_1.default.dim(' - Contents: Read and write'));
154
- console.log(chalk_1.default.dim(' - Pull requests: Read and write (optional, for PR creation)'));
155
- console.log(chalk_1.default.dim(' 7. Click "Generate token" at the bottom and copy it'));
156
- }
157
- else if (isGitLab) {
158
- console.log(chalk_1.default.bold(' GitLab Token Setup:'));
159
- console.log(chalk_1.default.dim(' 1. Go to: https://gitlab.com/-/profile/personal_access_tokens'));
160
- console.log(chalk_1.default.dim(' 2. Token name: "genbox-' + project_name + '"'));
161
- console.log(chalk_1.default.dim(' 3. Select scopes: "read_repository", "write_repository"'));
162
- console.log(chalk_1.default.dim(' 4. Click "Create personal access token"'));
163
- }
164
- else if (isBitbucket) {
165
- console.log(chalk_1.default.bold(' Bitbucket App Password Setup:'));
166
- console.log(chalk_1.default.dim(' 1. Go to: https://bitbucket.org/account/settings/app-passwords/'));
167
- console.log(chalk_1.default.dim(' 2. Click "Create app password"'));
168
- console.log(chalk_1.default.dim(' 3. Label: "genbox-' + project_name + '"'));
169
- console.log(chalk_1.default.dim(' 4. Select: Repositories (Read, Write)'));
170
- }
171
- else {
172
- console.log(chalk_1.default.dim(' Create a personal access token with repo read/write permissions'));
173
- }
174
- console.log('');
175
- console.log(chalk_1.default.cyan(' Add the token to .env.genbox:'));
176
- console.log(chalk_1.default.white(' GIT_TOKEN=ghp_xxxxxxxxxxxx'));
177
- console.log('');
125
+ }
126
+ if (scan.compose) {
127
+ console.log(` ${chalk_1.default.dim('Docker:')} ${scan.compose.applications.length} services`);
128
+ }
129
+ if (scan.git) {
130
+ console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
131
+ }
132
+ console.log('');
133
+ // Get project name
134
+ const projectName = nonInteractive
135
+ ? (options.name || scan.projectName)
136
+ : await prompts.input({
137
+ message: 'Project name:',
138
+ default: scan.projectName,
139
+ });
140
+ // Determine if workspace or single project
141
+ let isWorkspace = options.workspace;
142
+ if (!isWorkspace && (scan.structure.type.startsWith('monorepo') || scan.structure.type === 'hybrid')) {
143
+ if (nonInteractive) {
144
+ isWorkspace = true; // Default to workspace for monorepos
178
145
  }
179
- else if (authMethod === 'ssh') {
180
- repoAuthMethod = 'ssh';
181
- // Convert HTTPS URL to SSH if needed
182
- if (info.gitRemoteType === 'https') {
183
- const sshUrl = (0, scan_1.httpsToSsh)(info.gitRemote);
184
- console.log(chalk_1.default.dim(` Will use SSH URL: ${sshUrl}`));
185
- repoUrl = sshUrl;
186
- }
187
- // Ask consent before scanning SSH keys
188
- const { scanSshKeys } = await inquirer_1.default.prompt([
189
- {
190
- type: 'confirm',
191
- name: 'scanSshKeys',
192
- message: 'May we scan ~/.ssh/ for available SSH keys?',
193
- default: true,
194
- }
195
- ]);
196
- if (scanSshKeys) {
197
- const sshKeys = (0, scan_1.detectSshKeys)();
198
- if (sshKeys.length > 0) {
199
- console.log(chalk_1.default.green(` Found ${sshKeys.length} SSH key(s)`));
200
- const keyChoices = sshKeys.map((key) => ({
201
- name: `${key.name} (${key.type})`,
202
- value: key.path,
203
- }));
204
- keyChoices.push({ name: 'Enter custom path...', value: 'custom' });
205
- const { selectedKey } = await inquirer_1.default.prompt([
206
- {
207
- type: 'select',
208
- name: 'selectedKey',
209
- message: 'Select SSH key to use:',
210
- choices: keyChoices,
211
- }
212
- ]);
213
- if (selectedKey === 'custom') {
214
- const { customKeyPath } = await inquirer_1.default.prompt([
215
- {
216
- type: 'input',
217
- name: 'customKeyPath',
218
- message: 'Enter path to SSH private key:',
219
- default: path_1.default.join(os.homedir(), '.ssh', 'id_rsa'),
220
- }
221
- ]);
222
- gitAuth = { method: 'ssh', ssh_key_path: customKeyPath };
223
- }
224
- else {
225
- gitAuth = { method: 'ssh', ssh_key_path: selectedKey };
226
- }
227
- }
228
- else {
229
- console.log(chalk_1.default.yellow(' No SSH keys found'));
230
- const { keyPath } = await inquirer_1.default.prompt([
231
- {
232
- type: 'input',
233
- name: 'keyPath',
234
- message: 'Enter path to SSH private key:',
235
- default: path_1.default.join(os.homedir(), '.ssh', 'id_ed25519'),
236
- }
237
- ]);
238
- gitAuth = { method: 'ssh', ssh_key_path: keyPath };
239
- }
146
+ else {
147
+ isWorkspace = await prompts.confirm({
148
+ message: 'Detected monorepo/workspace structure. Configure as workspace?',
149
+ default: true,
150
+ });
151
+ }
152
+ }
153
+ // Generate initial config (v2 format)
154
+ const generator = new config_generator_1.ConfigGenerator();
155
+ const generated = generator.generate(scan);
156
+ // Convert to v3 format
157
+ const v3Config = convertV2ToV3(generated.config, scan);
158
+ // Update project name
159
+ v3Config.project.name = projectName;
160
+ // Ask about profiles
161
+ let createProfiles = true;
162
+ if (!nonInteractive) {
163
+ createProfiles = await prompts.confirm({
164
+ message: 'Create predefined profiles for common scenarios?',
165
+ default: true,
166
+ });
167
+ }
168
+ if (createProfiles) {
169
+ v3Config.profiles = nonInteractive
170
+ ? createDefaultProfilesSync(scan, v3Config)
171
+ : await createDefaultProfiles(scan, v3Config);
172
+ }
173
+ // Get server size
174
+ const serverSize = nonInteractive
175
+ ? generated.config.system.size
176
+ : await prompts.select({
177
+ message: 'Default server size:',
178
+ choices: [
179
+ { name: 'Small - 2 CPU, 4GB RAM', value: 'small' },
180
+ { name: 'Medium - 4 CPU, 8GB RAM', value: 'medium' },
181
+ { name: 'Large - 8 CPU, 16GB RAM', value: 'large' },
182
+ { name: 'XL - 16 CPU, 32GB RAM', value: 'xl' },
183
+ ],
184
+ default: generated.config.system.size,
185
+ });
186
+ if (!v3Config.defaults) {
187
+ v3Config.defaults = {};
188
+ }
189
+ v3Config.defaults.size = serverSize;
190
+ // Git repository setup
191
+ if (scan.git) {
192
+ if (nonInteractive) {
193
+ // Use detected git config with defaults
194
+ const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
195
+ v3Config.repos = {
196
+ [repoName]: {
197
+ url: scan.git.remote,
198
+ path: `/home/dev/${repoName}`,
199
+ auth: scan.git.type === 'ssh' ? 'ssh' : 'token',
200
+ },
201
+ };
202
+ }
203
+ else {
204
+ const gitConfig = await setupGitAuth(scan.git, projectName);
205
+ if (gitConfig.repos) {
206
+ v3Config.repos = gitConfig.repos;
240
207
  }
241
- else {
242
- // User declined scanning, ask for path directly
243
- const { keyPath } = await inquirer_1.default.prompt([
244
- {
245
- type: 'input',
246
- name: 'keyPath',
247
- message: 'Enter path to SSH private key:',
248
- default: path_1.default.join(os.homedir(), '.ssh', 'id_ed25519'),
249
- }
250
- ]);
251
- gitAuth = { method: 'ssh', ssh_key_path: keyPath };
208
+ if (gitConfig.git_auth) {
209
+ v3Config.git_auth = gitConfig.git_auth;
252
210
  }
253
- console.log('');
254
- console.log(chalk_1.default.cyan(' Add your SSH private key to .env.genbox:'));
255
- console.log(chalk_1.default.white(` GIT_SSH_KEY="$(cat ${gitAuth?.ssh_key_path})"`));
256
- console.log('');
257
- console.log(chalk_1.default.dim(' Or manually paste the key content'));
258
211
  }
259
- // If 'public', no auth needed
260
212
  }
261
- else {
262
- // No git remote detected
263
- console.log('');
264
- console.log(chalk_1.default.dim(' No git remote detected.'));
265
- const { addRepo } = await inquirer_1.default.prompt([
266
- {
267
- type: 'confirm',
268
- name: 'addRepo',
269
- message: 'Would you like to add a git repository?',
270
- default: false,
271
- }
272
- ]);
213
+ else if (!nonInteractive) {
214
+ const addRepo = await prompts.confirm({
215
+ message: 'No git remote detected. Add a repository?',
216
+ default: false,
217
+ });
273
218
  if (addRepo) {
274
- const { repoUrlInput } = await inquirer_1.default.prompt([
275
- {
276
- type: 'input',
277
- name: 'repoUrlInput',
278
- message: 'Enter repository URL (HTTPS recommended):',
279
- validate: (input) => {
280
- if (!input)
281
- return 'Repository URL is required';
282
- if (!input.startsWith('git@') && !input.startsWith('https://') && !input.startsWith('http://')) {
283
- return 'Please enter a valid git URL (https://... or git@...)';
284
- }
285
- return true;
219
+ const repoUrl = await prompts.input({
220
+ message: 'Repository URL (HTTPS recommended):',
221
+ validate: (value) => {
222
+ if (!value)
223
+ return 'Repository URL is required';
224
+ if (!value.startsWith('git@') && !value.startsWith('https://')) {
225
+ return 'Enter a valid git URL';
286
226
  }
227
+ return true;
228
+ },
229
+ });
230
+ const repoName = path_1.default.basename(repoUrl, '.git');
231
+ v3Config.repos = {
232
+ [repoName]: {
233
+ url: repoUrl,
234
+ path: `/home/dev/${repoName}`,
235
+ auth: repoUrl.startsWith('git@') ? 'ssh' : 'token',
236
+ },
237
+ };
238
+ }
239
+ }
240
+ // Environment configuration (skip in non-interactive mode)
241
+ if (!nonInteractive) {
242
+ const envConfig = await setupEnvironments(scan, v3Config);
243
+ if (envConfig) {
244
+ v3Config.environments = envConfig;
245
+ }
246
+ }
247
+ // Script selection (skip in non-interactive mode)
248
+ if (!nonInteractive) {
249
+ const includeScripts = await prompts.confirm({
250
+ message: 'Include setup scripts in configuration?',
251
+ default: false,
252
+ });
253
+ if (includeScripts) {
254
+ // Scan for scripts now
255
+ const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
256
+ const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
257
+ scriptsSpinner.stop();
258
+ if (fullScan.scripts.length > 0) {
259
+ console.log(chalk_1.default.dim(`\nFound ${fullScan.scripts.length} scripts:`));
260
+ // Group scripts by directory
261
+ const scriptsByDir = new Map();
262
+ for (const script of fullScan.scripts) {
263
+ const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
264
+ const existing = scriptsByDir.get(dir) || [];
265
+ existing.push(script);
266
+ scriptsByDir.set(dir, existing);
287
267
  }
288
- ]);
289
- repoUrl = repoUrlInput;
290
- const urlType = repoUrlInput.startsWith('git@') || repoUrlInput.startsWith('ssh://') ? 'ssh' : 'https';
291
- if (urlType === 'https') {
292
- gitAuth = { method: 'token' };
293
- repoAuthMethod = 'token';
294
- console.log(chalk_1.default.cyan(' Add GIT_TOKEN to .env.genbox for authentication'));
295
- }
296
- else {
297
- // SSH URL entered, ask about key with consent
298
- const { scanSshKeys } = await inquirer_1.default.prompt([
299
- {
300
- type: 'confirm',
301
- name: 'scanSshKeys',
302
- message: 'May we scan ~/.ssh/ for available SSH keys?',
303
- default: true,
268
+ // Show grouped scripts
269
+ for (const [dir, scripts] of scriptsByDir) {
270
+ console.log(chalk_1.default.dim(` ${dir}/`));
271
+ for (const s of scripts.slice(0, 5)) {
272
+ console.log(chalk_1.default.dim(` - ${s.name}`));
304
273
  }
305
- ]);
306
- if (scanSshKeys) {
307
- const sshKeys = (0, scan_1.detectSshKeys)();
308
- if (sshKeys.length > 0) {
309
- const { selectedKey } = await inquirer_1.default.prompt([
310
- {
311
- type: 'select',
312
- name: 'selectedKey',
313
- message: 'Select SSH key:',
314
- choices: sshKeys.map((k) => ({ name: k.name, value: k.path })),
315
- }
316
- ]);
317
- gitAuth = { method: 'ssh', ssh_key_path: selectedKey };
318
- }
319
- else {
320
- gitAuth = { method: 'ssh', ssh_key_path: path_1.default.join(os.homedir(), '.ssh', 'id_ed25519') };
274
+ if (scripts.length > 5) {
275
+ console.log(chalk_1.default.dim(` ... and ${scripts.length - 5} more`));
321
276
  }
322
277
  }
323
- else {
324
- const { keyPath } = await inquirer_1.default.prompt([
325
- {
326
- type: 'input',
327
- name: 'keyPath',
328
- message: 'Enter path to SSH private key:',
329
- default: path_1.default.join(os.homedir(), '.ssh', 'id_ed25519'),
330
- }
331
- ]);
332
- gitAuth = { method: 'ssh', ssh_key_path: keyPath };
278
+ // Let user select scripts
279
+ const scriptChoices = fullScan.scripts.map(s => ({
280
+ name: `${s.path} (${s.stage})`,
281
+ value: s.path,
282
+ checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
283
+ }));
284
+ const selectedScripts = await prompts.checkbox({
285
+ message: 'Select scripts to include:',
286
+ choices: scriptChoices,
287
+ });
288
+ if (selectedScripts.length > 0) {
289
+ v3Config.scripts = fullScan.scripts
290
+ .filter(s => selectedScripts.includes(s.path))
291
+ .map(s => ({
292
+ name: s.name,
293
+ path: s.path,
294
+ stage: s.stage,
295
+ }));
333
296
  }
334
- repoAuthMethod = 'ssh';
335
- }
336
- }
337
- }
338
- // Smart Suggestion: Create setup script if none selected but Docker exists
339
- let extraScripts = [];
340
- if (selectedScripts.length === 0 && info.hasDocker) {
341
- const { createSetup } = await inquirer_1.default.prompt([{
342
- type: 'confirm',
343
- name: 'createSetup',
344
- message: 'No setup scripts selected. Create default Docker setup script (scripts/setup-genbox.sh)?',
345
- default: true
346
- }]);
347
- if (createSetup) {
348
- const scriptsDir = path_1.default.join(process.cwd(), 'scripts');
349
- if (!fs_1.default.existsSync(scriptsDir))
350
- fs_1.default.mkdirSync(scriptsDir);
351
- // Setup directory logic
352
- const repoName = repoUrl ? path_1.default.basename(repoUrl, '.git') : project_name;
353
- const projectDir = repoUrl ? `/home/dev/${repoName}` : '/home/dev'; // If no git, assume files uploaded to root or handled differently
354
- const setupFile = path_1.default.join(scriptsDir, 'setup-genbox.sh');
355
- const setupContent = `#!/bin/bash
356
- # Auto-generated by Genbox
357
- echo "Starting services..."
358
- cd ${projectDir} || echo "Directory ${projectDir} not found, staying in $(pwd)"
359
-
360
- if [ -f "docker-compose.secure.yml" ]; then
361
- docker compose -f docker-compose.secure.yml up -d
362
- elif [ -f "docker-compose.yml" ]; then
363
- docker compose up -d
364
- else
365
- echo "No docker-compose file found in $(pwd)!"
366
- fi
367
- `;
368
- fs_1.default.writeFileSync(setupFile, setupContent, { mode: 0o755 });
369
- console.log(chalk_1.default.green(`Created scripts/setup-genbox.sh`));
370
- extraScripts.push('scripts/setup-genbox.sh');
371
- }
372
- }
373
- // Build repos config
374
- const reposConfig = {};
375
- if (repoUrl) {
376
- const repoName = path_1.default.basename(repoUrl, '.git');
377
- reposConfig[repoName] = {
378
- url: repoUrl,
379
- path: `/home/dev/${repoName}`,
380
- ...(repoAuthMethod && { auth: repoAuthMethod }),
381
- };
382
- }
383
- const config = {
384
- version: '1.0',
385
- project_name: project_name,
386
- system: {
387
- languages: info.languages,
388
- server_size: server_size,
389
- },
390
- scripts: [...selectedScripts, ...extraScripts],
391
- repos: reposConfig,
392
- files: [],
393
- hooks: {
394
- post_checkout: info.languages.node ? ['npm install'] : [],
395
- },
396
- ...(gitAuth && { git_auth: gitAuth }),
397
- };
398
- (0, config_1.saveConfig)(config);
399
- console.log(chalk_1.default.green(`\n✔ Configuration saved to genbox.yaml`));
400
- // 5. Generate .env.genbox file
401
- if (!(0, config_1.hasEnvFile)()) {
402
- // Check for existing .env or .env.local (without reading content yet)
403
- const existingEnvFile = (0, config_1.findExistingEnvFile)();
404
- if (existingEnvFile) {
405
- // Ask consent before reading the file
406
- const { copyEnv } = await inquirer_1.default.prompt([{
407
- type: 'confirm',
408
- name: 'copyEnv',
409
- message: `Found ${existingEnvFile}. May we read and copy it to .env.genbox?`,
410
- default: true
411
- }]);
412
- if (copyEnv) {
413
- // User consented, now read the content
414
- const content = (0, config_1.readEnvFileContent)(existingEnvFile);
415
- (0, config_1.copyExistingEnvToGenbox)(content, existingEnvFile);
416
- console.log(chalk_1.default.green(`✔ Copied ${existingEnvFile} to .env.genbox`));
417
- console.log(chalk_1.default.dim(` Review and update ${(0, config_1.getEnvPath)()} as needed`));
418
297
  }
419
298
  else {
420
- // User declined to copy, offer to create template instead
421
- const { createTemplate } = await inquirer_1.default.prompt([{
422
- type: 'confirm',
423
- name: 'createTemplate',
424
- message: 'Create .env.genbox with template variables instead?',
425
- default: true
426
- }]);
427
- if (createTemplate) {
428
- const envTemplate = (0, config_1.generateEnvTemplate)(project_name);
429
- (0, config_1.saveEnvVars)(envTemplate);
430
- console.log(chalk_1.default.green(`✔ Created .env.genbox with template variables`));
431
- console.log(chalk_1.default.yellow(` Edit ${(0, config_1.getEnvPath)()} with your actual values`));
432
- }
433
- }
434
- }
435
- else {
436
- // No existing .env file, create template
437
- const { createEnv } = await inquirer_1.default.prompt([{
438
- type: 'confirm',
439
- name: 'createEnv',
440
- message: 'Create .env.genbox file for sensitive environment variables?',
441
- default: true
442
- }]);
443
- if (createEnv) {
444
- const envTemplate = (0, config_1.generateEnvTemplate)(project_name);
445
- (0, config_1.saveEnvVars)(envTemplate);
446
- console.log(chalk_1.default.green(`✔ Created .env.genbox with template variables`));
447
- console.log(chalk_1.default.yellow(` Edit ${(0, config_1.getEnvPath)()} with your actual values`));
299
+ console.log(chalk_1.default.dim('No scripts found.'));
448
300
  }
449
301
  }
450
- // Add .env.genbox to .gitignore if it exists
451
- if ((0, config_1.hasEnvFile)()) {
452
- console.log(chalk_1.default.dim(` Remember: Don't commit .env.genbox to version control!`));
453
- const gitignorePath = path_1.default.join(process.cwd(), '.gitignore');
454
- if (fs_1.default.existsSync(gitignorePath)) {
455
- const gitignoreContent = fs_1.default.readFileSync(gitignorePath, 'utf8');
456
- if (!gitignoreContent.includes('.env.genbox')) {
457
- fs_1.default.appendFileSync(gitignorePath, '\n# Genbox environment variables (sensitive)\n.env.genbox\n');
458
- console.log(chalk_1.default.dim(` Added .env.genbox to .gitignore`));
459
- }
460
- }
461
- }
462
- }
463
- else {
464
- console.log(chalk_1.default.dim(` .env.genbox already exists, skipping...`));
465
302
  }
466
- // Add GIT_TOKEN entry if PAT auth was selected
467
- if (repoAuthMethod === 'token' && (0, config_1.hasEnvFile)()) {
468
- (0, config_1.ensureGitTokenInEnv)();
469
- console.log(chalk_1.default.dim(` Added GIT_TOKEN entry to .env.genbox`));
303
+ // Save configuration
304
+ const yamlContent = yaml.dump(v3Config, {
305
+ lineWidth: 120,
306
+ noRefs: true,
307
+ quotingType: '"',
308
+ });
309
+ fs_1.default.writeFileSync(configPath, yamlContent);
310
+ console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
311
+ // Generate .env.genbox
312
+ await setupEnvFile(projectName, v3Config, nonInteractive);
313
+ // Show warnings
314
+ if (generated.warnings.length > 0) {
315
+ console.log('');
316
+ console.log(chalk_1.default.yellow('Warnings:'));
317
+ for (const warning of generated.warnings) {
318
+ console.log(chalk_1.default.dim(` - ${warning}`));
319
+ }
470
320
  }
321
+ // Next steps
471
322
  console.log('');
472
323
  console.log(chalk_1.default.bold('Next steps:'));
473
- console.log(chalk_1.default.dim(` 1. Edit genbox.yaml to configure services, repos, etc.`));
474
- console.log(chalk_1.default.dim(` 2. Edit .env.genbox with your environment variables`));
475
- console.log(chalk_1.default.dim(` 3. Run 'genbox push' to upload configuration to the cloud`));
476
- console.log(chalk_1.default.dim(` 4. Run 'genbox create <name>' to provision a dev environment`));
324
+ console.log(chalk_1.default.dim(` 1. Review and edit ${CONFIG_FILENAME}`));
325
+ console.log(chalk_1.default.dim(` 2. Add secrets to ${ENV_FILENAME}`));
326
+ console.log(chalk_1.default.dim(` 3. Run 'genbox profiles' to see available profiles`));
327
+ console.log(chalk_1.default.dim(` 4. Run 'genbox create <name> --profile <profile>' to create an environment`));
477
328
  }
478
329
  catch (error) {
479
- // Handle user cancellation (Ctrl+C) gracefully
480
330
  if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
481
331
  console.log('');
482
332
  console.log(chalk_1.default.dim('Cancelled.'));
483
333
  process.exit(0);
484
334
  }
485
- // Re-throw other errors
486
335
  throw error;
487
336
  }
488
337
  });
338
+ /**
339
+ * Create default profiles (sync version for non-interactive mode)
340
+ */
341
+ function createDefaultProfilesSync(scan, config) {
342
+ return createProfilesFromScan(scan);
343
+ }
344
+ async function createDefaultProfiles(scan, config) {
345
+ return createProfilesFromScan(scan);
346
+ }
347
+ function createProfilesFromScan(scan) {
348
+ const profiles = {};
349
+ const frontendApps = scan.apps.filter(a => a.type === 'frontend');
350
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
351
+ const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
352
+ // Quick UI profiles for each frontend
353
+ for (const frontend of frontendApps.slice(0, 3)) {
354
+ profiles[`${frontend.name}-quick`] = {
355
+ description: `${frontend.name} only, connected to staging`,
356
+ size: 'small',
357
+ apps: [frontend.name],
358
+ connect_to: 'staging',
359
+ };
360
+ }
361
+ // Full local development
362
+ if (hasApi && frontendApps.length > 0) {
363
+ const primaryFrontend = frontendApps[0];
364
+ profiles[`${primaryFrontend.name}-full`] = {
365
+ description: `${primaryFrontend.name} + local API + DB copy`,
366
+ size: 'large',
367
+ apps: [primaryFrontend.name, 'api'],
368
+ database: {
369
+ mode: 'copy',
370
+ source: 'staging',
371
+ },
372
+ };
373
+ }
374
+ // API development only
375
+ if (hasApi) {
376
+ profiles['api-dev'] = {
377
+ description: 'API with local infrastructure',
378
+ size: 'medium',
379
+ apps: ['api'],
380
+ database: {
381
+ mode: 'local',
382
+ },
383
+ };
384
+ }
385
+ // All frontends + staging
386
+ if (frontendApps.length > 1) {
387
+ profiles['frontends-staging'] = {
388
+ description: 'All frontends with staging backend',
389
+ size: 'medium',
390
+ apps: frontendApps.map(a => a.name),
391
+ connect_to: 'staging',
392
+ };
393
+ }
394
+ // Full stack
395
+ if (scan.apps.length > 1) {
396
+ profiles['full-stack'] = {
397
+ description: 'Everything local with DB copy',
398
+ size: 'xl',
399
+ apps: scan.apps.filter(a => a.type !== 'library').map(a => a.name),
400
+ database: {
401
+ mode: 'copy',
402
+ source: 'staging',
403
+ },
404
+ };
405
+ }
406
+ return profiles;
407
+ }
408
+ /**
409
+ * Setup git authentication
410
+ */
411
+ async function setupGitAuth(gitInfo, projectName) {
412
+ console.log('');
413
+ console.log(chalk_1.default.blue('=== Git Repository Setup ==='));
414
+ console.log(chalk_1.default.dim(`Detected: ${gitInfo.remote}`));
415
+ const authMethod = await prompts.select({
416
+ message: 'How should genbox access this repository?',
417
+ choices: [
418
+ { name: 'Personal Access Token (PAT) - recommended', value: 'token' },
419
+ { name: 'SSH Key', value: 'ssh' },
420
+ { name: 'Public (no auth needed)', value: 'public' },
421
+ ],
422
+ default: 'token',
423
+ });
424
+ let repoUrl = gitInfo.remote;
425
+ let git_auth;
426
+ if (authMethod === 'token') {
427
+ // Convert SSH to HTTPS if needed
428
+ if (gitInfo.type === 'ssh') {
429
+ repoUrl = (0, scan_1.sshToHttps)(gitInfo.remote);
430
+ console.log(chalk_1.default.dim(` Will use HTTPS: ${repoUrl}`));
431
+ }
432
+ git_auth = { method: 'token' };
433
+ // Show token setup instructions
434
+ console.log('');
435
+ console.log(chalk_1.default.yellow(' Add your token to .env.genbox:'));
436
+ console.log(chalk_1.default.white(' GIT_TOKEN=ghp_xxxxxxxxxxxx'));
437
+ }
438
+ else if (authMethod === 'ssh') {
439
+ // Convert HTTPS to SSH if needed
440
+ if (gitInfo.type === 'https') {
441
+ repoUrl = (0, scan_1.httpsToSsh)(gitInfo.remote);
442
+ console.log(chalk_1.default.dim(` Will use SSH: ${repoUrl}`));
443
+ }
444
+ // Detect SSH keys
445
+ const scanKeys = await prompts.confirm({
446
+ message: 'Scan ~/.ssh/ for available keys?',
447
+ default: true,
448
+ });
449
+ let sshKeyPath = path_1.default.join(os.homedir(), '.ssh', 'id_ed25519');
450
+ if (scanKeys) {
451
+ const keys = (0, scan_1.detectSshKeys)();
452
+ if (keys.length > 0) {
453
+ const keyChoices = keys.map((k) => ({
454
+ name: `${k.name} (${k.type})`,
455
+ value: k.path,
456
+ }));
457
+ sshKeyPath = await prompts.select({
458
+ message: 'Select SSH key:',
459
+ choices: keyChoices,
460
+ });
461
+ }
462
+ }
463
+ git_auth = { method: 'ssh', ssh_key_path: sshKeyPath };
464
+ console.log('');
465
+ console.log(chalk_1.default.yellow(' Add your SSH key to .env.genbox:'));
466
+ console.log(chalk_1.default.white(` GIT_SSH_KEY="$(cat ${sshKeyPath})"`));
467
+ }
468
+ const repoName = path_1.default.basename(repoUrl, '.git');
469
+ return {
470
+ repos: {
471
+ [repoName]: {
472
+ url: repoUrl,
473
+ path: `/home/dev/${repoName}`,
474
+ auth: authMethod === 'public' ? undefined : authMethod,
475
+ },
476
+ },
477
+ git_auth,
478
+ };
479
+ }
480
+ /**
481
+ * Setup staging/production environments
482
+ */
483
+ async function setupEnvironments(scan, config) {
484
+ const setupEnvs = await prompts.confirm({
485
+ message: 'Configure staging/production environments?',
486
+ default: true,
487
+ });
488
+ if (!setupEnvs) {
489
+ return undefined;
490
+ }
491
+ console.log('');
492
+ console.log(chalk_1.default.blue('=== Environment Setup ==='));
493
+ console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
494
+ console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
495
+ console.log('');
496
+ const stagingApiUrl = await prompts.input({
497
+ message: 'Staging API URL (leave empty to skip):',
498
+ default: '',
499
+ });
500
+ const environments = {};
501
+ if (stagingApiUrl) {
502
+ environments.staging = {
503
+ description: 'Staging environment',
504
+ api: { gateway: stagingApiUrl },
505
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
506
+ redis: { url: '${STAGING_REDIS_URL}' },
507
+ };
508
+ }
509
+ const setupProd = await prompts.confirm({
510
+ message: 'Also configure production environment?',
511
+ default: false,
512
+ });
513
+ if (setupProd) {
514
+ const prodApiUrl = await prompts.input({
515
+ message: 'Production API URL:',
516
+ default: '',
517
+ });
518
+ if (prodApiUrl) {
519
+ environments.production = {
520
+ description: 'Production (use with caution)',
521
+ api: { gateway: prodApiUrl },
522
+ mongodb: {
523
+ url: '${PROD_MONGODB_URL}',
524
+ read_only: true,
525
+ },
526
+ };
527
+ }
528
+ }
529
+ return Object.keys(environments).length > 0 ? environments : undefined;
530
+ }
531
+ /**
532
+ * Setup .env.genbox file
533
+ */
534
+ async function setupEnvFile(projectName, config, nonInteractive = false) {
535
+ const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
536
+ if (fs_1.default.existsSync(envPath)) {
537
+ console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
538
+ return;
539
+ }
540
+ // Check for existing .env
541
+ const existingEnvFiles = ['.env.local', '.env', '.env.development'];
542
+ let existingEnvPath;
543
+ for (const envFile of existingEnvFiles) {
544
+ const fullPath = path_1.default.join(process.cwd(), envFile);
545
+ if (fs_1.default.existsSync(fullPath)) {
546
+ existingEnvPath = fullPath;
547
+ break;
548
+ }
549
+ }
550
+ if (existingEnvPath) {
551
+ // In non-interactive mode, default to copying existing env
552
+ const copyExisting = nonInteractive ? true : await prompts.confirm({
553
+ message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
554
+ default: true,
555
+ });
556
+ if (copyExisting) {
557
+ const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
558
+ const header = `# Genbox Environment Variables
559
+ # Generated from ${path_1.default.basename(existingEnvPath)}
560
+ # DO NOT COMMIT THIS FILE
561
+ #
562
+ # Add staging/production URLs:
563
+ # STAGING_MONGODB_URL=mongodb+srv://...
564
+ # STAGING_REDIS_URL=redis://...
565
+ # PROD_MONGODB_URL=mongodb+srv://...
566
+ #
567
+ # Git authentication:
568
+ # GIT_TOKEN=ghp_xxxxxxxxxxxx
569
+
570
+ `;
571
+ fs_1.default.writeFileSync(envPath, header + content);
572
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
573
+ }
574
+ }
575
+ else {
576
+ // In non-interactive mode, default to creating template
577
+ const createEnv = nonInteractive ? true : await prompts.confirm({
578
+ message: `Create ${ENV_FILENAME} template?`,
579
+ default: true,
580
+ });
581
+ if (createEnv) {
582
+ const template = generateEnvTemplate(projectName, config);
583
+ fs_1.default.writeFileSync(envPath, template);
584
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
585
+ }
586
+ }
587
+ // Add to .gitignore
588
+ const gitignorePath = path_1.default.join(process.cwd(), '.gitignore');
589
+ if (fs_1.default.existsSync(gitignorePath)) {
590
+ const content = fs_1.default.readFileSync(gitignorePath, 'utf8');
591
+ if (!content.includes(ENV_FILENAME)) {
592
+ fs_1.default.appendFileSync(gitignorePath, `\n# Genbox secrets\n${ENV_FILENAME}\n`);
593
+ console.log(chalk_1.default.dim(` Added ${ENV_FILENAME} to .gitignore`));
594
+ }
595
+ }
596
+ }
597
+ /**
598
+ * Generate .env.genbox template
599
+ */
600
+ function generateEnvTemplate(projectName, config) {
601
+ const lines = [
602
+ '# Genbox Environment Variables',
603
+ `# Project: ${projectName}`,
604
+ '# DO NOT COMMIT THIS FILE',
605
+ '',
606
+ '# ============================================',
607
+ '# STAGING ENVIRONMENT',
608
+ '# ============================================',
609
+ '',
610
+ '# Database',
611
+ 'STAGING_MONGODB_URL=mongodb+srv://user:password@staging.mongodb.net',
612
+ '',
613
+ '# Cache & Queue',
614
+ 'STAGING_REDIS_URL=redis://staging-redis:6379',
615
+ 'STAGING_RABBITMQ_URL=amqp://user:password@staging-rabbitmq:5672',
616
+ '',
617
+ '# ============================================',
618
+ '# PRODUCTION ENVIRONMENT',
619
+ '# ============================================',
620
+ '',
621
+ 'PROD_MONGODB_URL=mongodb+srv://readonly:password@prod.mongodb.net',
622
+ '',
623
+ '# ============================================',
624
+ '# GIT AUTHENTICATION',
625
+ '# ============================================',
626
+ '',
627
+ '# For HTTPS repos (Personal Access Token)',
628
+ 'GIT_TOKEN=ghp_xxxxxxxxxxxx',
629
+ '',
630
+ '# For SSH repos (paste private key content)',
631
+ '# GIT_SSH_KEY="-----BEGIN OPENSSH PRIVATE KEY-----',
632
+ '# ...',
633
+ '# -----END OPENSSH PRIVATE KEY-----"',
634
+ '',
635
+ '# ============================================',
636
+ '# APPLICATION SECRETS',
637
+ '# ============================================',
638
+ '',
639
+ 'JWT_SECRET=your-jwt-secret-here',
640
+ '',
641
+ '# OAuth',
642
+ 'GOOGLE_CLIENT_ID=',
643
+ 'GOOGLE_CLIENT_SECRET=',
644
+ '',
645
+ '# Payments',
646
+ 'STRIPE_SECRET_KEY=sk_test_xxx',
647
+ 'STRIPE_WEBHOOK_SECRET=whsec_xxx',
648
+ '',
649
+ ];
650
+ return lines.join('\n');
651
+ }
652
+ /**
653
+ * Convert GenboxConfigV2 to GenboxConfigV3 format
654
+ */
655
+ function convertV2ToV3(v2Config, scan) {
656
+ // Convert services to apps
657
+ const apps = {};
658
+ for (const [name, service] of Object.entries(v2Config.services || {})) {
659
+ const appConfig = {
660
+ path: service.path || `/home/dev/${v2Config.project.name}/${name}`,
661
+ type: service.type === 'api' ? 'backend' : service.type,
662
+ port: service.port,
663
+ };
664
+ // Only add framework if defined
665
+ if (service.framework) {
666
+ appConfig.framework = service.framework;
667
+ }
668
+ // Only add requires if there are dependencies
669
+ if (service.dependsOn?.length) {
670
+ appConfig.requires = service.dependsOn.reduce((acc, dep) => {
671
+ acc[dep] = 'required';
672
+ return acc;
673
+ }, {});
674
+ }
675
+ // Build commands object without undefined values
676
+ const commands = {};
677
+ if (service.build?.command)
678
+ commands.build = service.build.command;
679
+ if (service.start?.command)
680
+ commands.start = service.start.command;
681
+ if (service.start?.dev)
682
+ commands.dev = service.start.dev;
683
+ if (Object.keys(commands).length > 0) {
684
+ appConfig.commands = commands;
685
+ }
686
+ // Only add env if defined
687
+ if (service.env?.length) {
688
+ appConfig.env = service.env;
689
+ }
690
+ apps[name] = appConfig;
691
+ }
692
+ // Convert infrastructure
693
+ const infrastructure = {};
694
+ if (v2Config.infrastructure?.databases) {
695
+ for (const db of v2Config.infrastructure.databases) {
696
+ infrastructure[db.container || db.type] = {
697
+ type: 'database',
698
+ image: `${db.type}:latest`,
699
+ port: db.port,
700
+ };
701
+ }
702
+ }
703
+ if (v2Config.infrastructure?.caches) {
704
+ for (const cache of v2Config.infrastructure.caches) {
705
+ infrastructure[cache.container || cache.type] = {
706
+ type: 'cache',
707
+ image: `${cache.type}:latest`,
708
+ port: cache.port,
709
+ };
710
+ }
711
+ }
712
+ if (v2Config.infrastructure?.queues) {
713
+ for (const queue of v2Config.infrastructure.queues) {
714
+ infrastructure[queue.container || queue.type] = {
715
+ type: 'queue',
716
+ image: `${queue.type}:latest`,
717
+ port: queue.port,
718
+ management_port: queue.managementPort,
719
+ };
720
+ }
721
+ }
722
+ // Convert repos
723
+ const repos = {};
724
+ for (const [name, repo] of Object.entries(v2Config.repos || {})) {
725
+ repos[name] = {
726
+ url: repo.url,
727
+ path: repo.path,
728
+ branch: repo.branch,
729
+ auth: repo.auth,
730
+ };
731
+ }
732
+ // Build v3 config
733
+ const v3Config = {
734
+ version: '3.0',
735
+ project: {
736
+ name: v2Config.project.name,
737
+ structure: v2Config.project.structure === 'single-app' ? 'single-app' :
738
+ v2Config.project.structure.startsWith('monorepo') ? 'monorepo' :
739
+ v2Config.project.structure,
740
+ description: v2Config.project.description,
741
+ },
742
+ apps,
743
+ infrastructure: Object.keys(infrastructure).length > 0 ? infrastructure : undefined,
744
+ repos: Object.keys(repos).length > 0 ? repos : undefined,
745
+ defaults: {
746
+ size: v2Config.system.size,
747
+ },
748
+ hooks: v2Config.hooks ? {
749
+ post_checkout: v2Config.hooks.postCheckout,
750
+ post_start: v2Config.hooks.postStart,
751
+ pre_start: v2Config.hooks.preStart,
752
+ } : undefined,
753
+ };
754
+ return v3Config;
755
+ }