genbox 1.0.3 → 1.0.5

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,952 @@ 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';
54
+ /**
55
+ * Detect git repositories in app directories (for multi-repo workspaces)
56
+ */
57
+ function detectAppGitRepos(apps, rootDir) {
58
+ const { execSync } = require('child_process');
59
+ const repos = [];
60
+ for (const app of apps) {
61
+ const appDir = path_1.default.join(rootDir, app.path);
62
+ const gitDir = path_1.default.join(appDir, '.git');
63
+ if (!fs_1.default.existsSync(gitDir))
64
+ continue;
65
+ try {
66
+ const remote = execSync('git remote get-url origin', {
67
+ cwd: appDir,
68
+ stdio: 'pipe',
69
+ encoding: 'utf8',
70
+ }).trim();
71
+ if (!remote)
72
+ continue;
73
+ const isSSH = remote.startsWith('git@') || remote.startsWith('ssh://');
74
+ let provider = 'other';
75
+ if (remote.includes('github.com'))
76
+ provider = 'github';
77
+ else if (remote.includes('gitlab.com'))
78
+ provider = 'gitlab';
79
+ else if (remote.includes('bitbucket.org'))
80
+ provider = 'bitbucket';
81
+ let branch = 'main';
82
+ try {
83
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
84
+ cwd: appDir,
85
+ stdio: 'pipe',
86
+ encoding: 'utf8',
87
+ }).trim();
88
+ }
89
+ catch { }
90
+ repos.push({
91
+ appName: app.name,
92
+ appPath: app.path,
93
+ remote,
94
+ type: isSSH ? 'ssh' : 'https',
95
+ provider,
96
+ branch,
97
+ });
98
+ }
99
+ catch {
100
+ // No git remote in this directory
101
+ }
102
+ }
103
+ return repos;
104
+ }
105
+ /**
106
+ * Find .env files in app directories
107
+ */
108
+ function findAppEnvFiles(apps, rootDir) {
109
+ const envFiles = [];
110
+ const envPatterns = ['.env', '.env.local', '.env.development'];
111
+ for (const app of apps) {
112
+ const appDir = path_1.default.join(rootDir, app.path);
113
+ for (const pattern of envPatterns) {
114
+ const envPath = path_1.default.join(appDir, pattern);
115
+ if (fs_1.default.existsSync(envPath)) {
116
+ envFiles.push({
117
+ appName: app.name,
118
+ envFile: pattern,
119
+ fullPath: envPath,
120
+ });
121
+ break; // Only take the first match per app
122
+ }
123
+ }
124
+ }
125
+ return envFiles;
126
+ }
49
127
  exports.initCommand = new commander_1.Command('init')
50
128
  .description('Initialize a new Genbox configuration')
51
- .action(async () => {
129
+ .option('--v2', 'Use legacy v2 format (single-app only)')
130
+ .option('--workspace', 'Initialize as workspace config (for multi-repo projects)')
131
+ .option('--force', 'Overwrite existing configuration')
132
+ .option('-y, --yes', 'Use defaults without prompting')
133
+ .option('--exclude <dirs>', 'Comma-separated directories to exclude')
134
+ .option('--name <name>', 'Project name (for non-interactive mode)')
135
+ .action(async (options) => {
52
136
  try {
53
- if ((0, config_1.hasConfig)()) {
137
+ const configPath = path_1.default.join(process.cwd(), CONFIG_FILENAME);
138
+ const nonInteractive = options.yes || !process.stdin.isTTY;
139
+ // Check for existing config
140
+ if (fs_1.default.existsSync(configPath) && !options.force) {
141
+ if (nonInteractive) {
142
+ console.log(chalk_1.default.yellow('genbox.yaml already exists. Use --force to overwrite.'));
143
+ return;
144
+ }
54
145
  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
- ]);
146
+ const overwrite = await prompts.confirm({
147
+ message: 'Do you want to overwrite it?',
148
+ default: false,
149
+ });
63
150
  if (!overwrite) {
64
151
  return;
65
152
  }
66
153
  }
67
154
  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,
155
+ console.log('');
156
+ // Get directory exclusions
157
+ let exclude = [];
158
+ if (options.exclude) {
159
+ exclude = options.exclude.split(',').map((d) => d.trim()).filter(Boolean);
160
+ }
161
+ else if (!nonInteractive) {
162
+ const excludeDirs = await prompts.input({
163
+ message: 'Directories to exclude (comma-separated, or empty to skip):',
164
+ default: '',
165
+ });
166
+ exclude = excludeDirs
167
+ ? excludeDirs.split(',').map((d) => d.trim()).filter(Boolean)
168
+ : [];
169
+ }
170
+ // Scan project (skip scripts initially - we'll ask about them later)
171
+ const spinner = (0, ora_1.default)('Scanning project...').start();
172
+ const scanner = new scanner_1.ProjectScanner();
173
+ const scan = await scanner.scan(process.cwd(), { exclude, skipScripts: true });
174
+ spinner.succeed('Project scanned');
175
+ // Display scan results
176
+ console.log('');
177
+ console.log(chalk_1.default.bold('Detected:'));
178
+ console.log(` ${chalk_1.default.dim('Project:')} ${scan.projectName}`);
179
+ console.log(` ${chalk_1.default.dim('Structure:')} ${scan.structure.type} (${scan.structure.confidence} confidence)`);
180
+ if (scan.runtimes.length > 0) {
181
+ const runtimeStr = scan.runtimes
182
+ .map(r => `${r.language}${r.version ? ` ${r.version}` : ''}`)
183
+ .join(', ');
184
+ console.log(` ${chalk_1.default.dim('Runtimes:')} ${runtimeStr}`);
185
+ }
186
+ if (scan.frameworks.length > 0) {
187
+ const frameworkStr = scan.frameworks.map(f => f.name).join(', ');
188
+ console.log(` ${chalk_1.default.dim('Frameworks:')} ${frameworkStr}`);
189
+ }
190
+ if (scan.apps.length > 0) {
191
+ console.log(` ${chalk_1.default.dim('Apps:')} ${scan.apps.length} discovered`);
192
+ for (const app of scan.apps.slice(0, 5)) {
193
+ console.log(` - ${app.name} (${app.type}${app.framework ? `, ${app.framework}` : ''})`);
80
194
  }
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',
195
+ if (scan.apps.length > 5) {
196
+ console.log(` ... and ${scan.apps.length - 5} more`);
104
197
  }
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:'));
198
+ }
199
+ if (scan.compose) {
200
+ console.log(` ${chalk_1.default.dim('Docker:')} ${scan.compose.applications.length} services`);
201
+ }
202
+ if (scan.git) {
203
+ console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
204
+ }
205
+ console.log('');
206
+ // Get project name
207
+ const projectName = nonInteractive
208
+ ? (options.name || scan.projectName)
209
+ : await prompts.input({
210
+ message: 'Project name:',
211
+ default: scan.projectName,
212
+ });
213
+ // Determine if workspace or single project
214
+ let isWorkspace = options.workspace;
215
+ if (!isWorkspace && (scan.structure.type.startsWith('monorepo') || scan.structure.type === 'hybrid')) {
216
+ if (nonInteractive) {
217
+ isWorkspace = true; // Default to workspace for monorepos
218
+ }
219
+ else {
220
+ isWorkspace = await prompts.confirm({
221
+ message: 'Detected monorepo/workspace structure. Configure as workspace?',
222
+ default: true,
223
+ });
224
+ }
225
+ }
226
+ // Generate initial config (v2 format)
227
+ const generator = new config_generator_1.ConfigGenerator();
228
+ const generated = generator.generate(scan);
229
+ // Convert to v3 format
230
+ const v3Config = convertV2ToV3(generated.config, scan);
231
+ // Update project name
232
+ v3Config.project.name = projectName;
233
+ // Ask about profiles
234
+ let createProfiles = true;
235
+ if (!nonInteractive) {
236
+ createProfiles = await prompts.confirm({
237
+ message: 'Create predefined profiles for common scenarios?',
238
+ default: true,
239
+ });
240
+ }
241
+ if (createProfiles) {
242
+ v3Config.profiles = nonInteractive
243
+ ? createDefaultProfilesSync(scan, v3Config)
244
+ : await createDefaultProfiles(scan, v3Config);
245
+ }
246
+ // Get server size
247
+ const serverSize = nonInteractive
248
+ ? generated.config.system.size
249
+ : await prompts.select({
250
+ message: 'Default server size:',
251
+ choices: [
252
+ { name: 'Small - 2 CPU, 4GB RAM', value: 'small' },
253
+ { name: 'Medium - 4 CPU, 8GB RAM', value: 'medium' },
254
+ { name: 'Large - 8 CPU, 16GB RAM', value: 'large' },
255
+ { name: 'XL - 16 CPU, 32GB RAM', value: 'xl' },
256
+ ],
257
+ default: generated.config.system.size,
258
+ });
259
+ if (!v3Config.defaults) {
260
+ v3Config.defaults = {};
261
+ }
262
+ v3Config.defaults.size = serverSize;
263
+ // Git repository setup - different handling for multi-repo vs single-repo
264
+ const isMultiRepo = scan.structure.type === 'hybrid';
265
+ if (isMultiRepo) {
266
+ // Multi-repo workspace: detect git repos in app directories
267
+ const appGitRepos = detectAppGitRepos(scan.apps, process.cwd());
268
+ if (appGitRepos.length > 0 && !nonInteractive) {
143
269
  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'));
270
+ console.log(chalk_1.default.blue('=== Git Repositories ==='));
271
+ console.log(chalk_1.default.dim(`Found ${appGitRepos.length} git repositories in app directories`));
272
+ const repoChoices = appGitRepos.map(repo => ({
273
+ name: `${repo.appName} - ${repo.remote}`,
274
+ value: repo.appName,
275
+ checked: true, // Default to include all
276
+ }));
277
+ const selectedRepos = await prompts.checkbox({
278
+ message: 'Select repositories to include:',
279
+ choices: repoChoices,
280
+ });
281
+ if (selectedRepos.length > 0) {
282
+ v3Config.repos = {};
283
+ for (const repoName of selectedRepos) {
284
+ const repo = appGitRepos.find(r => r.appName === repoName);
285
+ v3Config.repos[repo.appName] = {
286
+ url: repo.remote,
287
+ path: `/home/dev/${projectName}/${repo.appPath}`,
288
+ branch: repo.branch !== 'main' && repo.branch !== 'master' ? repo.branch : undefined,
289
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
290
+ };
291
+ }
173
292
  }
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('');
178
293
  }
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;
294
+ else if (appGitRepos.length > 0) {
295
+ // Non-interactive: include all repos
296
+ v3Config.repos = {};
297
+ for (const repo of appGitRepos) {
298
+ v3Config.repos[repo.appName] = {
299
+ url: repo.remote,
300
+ path: `/home/dev/${projectName}/${repo.appPath}`,
301
+ auth: repo.type === 'ssh' ? 'ssh' : 'token',
302
+ };
186
303
  }
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
- }
304
+ }
305
+ }
306
+ else if (scan.git) {
307
+ // Single repo or monorepo with root git
308
+ if (nonInteractive) {
309
+ const repoName = path_1.default.basename(scan.git.remote, '.git').replace(/.*[:/]/, '');
310
+ v3Config.repos = {
311
+ [repoName]: {
312
+ url: scan.git.remote,
313
+ path: `/home/dev/${repoName}`,
314
+ auth: scan.git.type === 'ssh' ? 'ssh' : 'token',
315
+ },
316
+ };
317
+ }
318
+ else {
319
+ const gitConfig = await setupGitAuth(scan.git, projectName);
320
+ if (gitConfig.repos) {
321
+ v3Config.repos = gitConfig.repos;
240
322
  }
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 };
323
+ if (gitConfig.git_auth) {
324
+ v3Config.git_auth = gitConfig.git_auth;
252
325
  }
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
326
  }
259
- // If 'public', no auth needed
260
327
  }
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
- ]);
328
+ else if (!nonInteractive && !isMultiRepo) {
329
+ // Only ask to add repo for non-multi-repo projects
330
+ const addRepo = await prompts.confirm({
331
+ message: 'No git remote detected. Add a repository?',
332
+ default: false,
333
+ });
273
334
  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;
335
+ const repoUrl = await prompts.input({
336
+ message: 'Repository URL (HTTPS recommended):',
337
+ validate: (value) => {
338
+ if (!value)
339
+ return 'Repository URL is required';
340
+ if (!value.startsWith('git@') && !value.startsWith('https://')) {
341
+ return 'Enter a valid git URL';
286
342
  }
287
- }
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'));
343
+ return true;
344
+ },
345
+ });
346
+ const repoName = path_1.default.basename(repoUrl, '.git');
347
+ v3Config.repos = {
348
+ [repoName]: {
349
+ url: repoUrl,
350
+ path: `/home/dev/${repoName}`,
351
+ auth: repoUrl.startsWith('git@') ? 'ssh' : 'token',
352
+ },
353
+ };
354
+ }
355
+ }
356
+ // Environment configuration (skip in non-interactive mode)
357
+ if (!nonInteractive) {
358
+ const envConfig = await setupEnvironments(scan, v3Config, isMultiRepo);
359
+ if (envConfig) {
360
+ v3Config.environments = envConfig;
361
+ }
362
+ }
363
+ // Script selection - always show multi-select UI (skip in non-interactive mode)
364
+ if (!nonInteractive) {
365
+ // Scan for scripts
366
+ const scriptsSpinner = (0, ora_1.default)('Scanning for scripts...').start();
367
+ const fullScan = await scanner.scan(process.cwd(), { exclude, skipScripts: false });
368
+ scriptsSpinner.stop();
369
+ if (fullScan.scripts.length > 0) {
370
+ console.log('');
371
+ console.log(chalk_1.default.blue('=== Setup Scripts ==='));
372
+ // Group scripts by directory for display
373
+ const scriptsByDir = new Map();
374
+ for (const script of fullScan.scripts) {
375
+ const dir = script.path.includes('/') ? script.path.split('/')[0] : '(root)';
376
+ const existing = scriptsByDir.get(dir) || [];
377
+ existing.push(script);
378
+ scriptsByDir.set(dir, existing);
295
379
  }
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,
304
- }
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') };
321
- }
322
- }
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 };
333
- }
334
- repoAuthMethod = 'ssh';
380
+ // Show grouped scripts
381
+ for (const [dir, scripts] of scriptsByDir) {
382
+ console.log(chalk_1.default.dim(` ${dir}/ (${scripts.length} scripts)`));
383
+ }
384
+ // Let user select scripts with multi-select
385
+ const scriptChoices = fullScan.scripts.map(s => ({
386
+ name: `${s.path} (${s.stage})`,
387
+ value: s.path,
388
+ checked: s.path.startsWith('scripts/'), // Default select scripts/ directory
389
+ }));
390
+ const selectedScripts = await prompts.checkbox({
391
+ message: 'Select scripts to include (space to toggle, enter to confirm):',
392
+ choices: scriptChoices,
393
+ });
394
+ if (selectedScripts.length > 0) {
395
+ v3Config.scripts = fullScan.scripts
396
+ .filter(s => selectedScripts.includes(s.path))
397
+ .map(s => ({
398
+ name: s.name,
399
+ path: s.path,
400
+ stage: s.stage,
401
+ }));
335
402
  }
336
403
  }
404
+ else {
405
+ console.log(chalk_1.default.dim('No scripts found.'));
406
+ }
337
407
  }
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');
408
+ // Save configuration
409
+ const yamlContent = yaml.dump(v3Config, {
410
+ lineWidth: 120,
411
+ noRefs: true,
412
+ quotingType: '"',
413
+ });
414
+ fs_1.default.writeFileSync(configPath, yamlContent);
415
+ console.log(chalk_1.default.green(`\n✔ Configuration saved to ${CONFIG_FILENAME}`));
416
+ // Generate .env.genbox
417
+ await setupEnvFile(projectName, v3Config, nonInteractive, scan, isMultiRepo);
418
+ // Show warnings
419
+ if (generated.warnings.length > 0) {
420
+ console.log('');
421
+ console.log(chalk_1.default.yellow('Warnings:'));
422
+ for (const warning of generated.warnings) {
423
+ console.log(chalk_1.default.dim(` - ${warning}`));
371
424
  }
372
425
  }
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
- };
426
+ // Next steps
427
+ console.log('');
428
+ console.log(chalk_1.default.bold('Next steps:'));
429
+ console.log(chalk_1.default.dim(` 1. Review and edit ${CONFIG_FILENAME}`));
430
+ console.log(chalk_1.default.dim(` 2. Add secrets to ${ENV_FILENAME}`));
431
+ console.log(chalk_1.default.dim(` 3. Run 'genbox profiles' to see available profiles`));
432
+ console.log(chalk_1.default.dim(` 4. Run 'genbox create <name> --profile <profile>' to create an environment`));
433
+ }
434
+ catch (error) {
435
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
436
+ console.log('');
437
+ console.log(chalk_1.default.dim('Cancelled.'));
438
+ process.exit(0);
382
439
  }
383
- const config = {
384
- version: '1.0',
385
- project_name: project_name,
386
- system: {
387
- languages: info.languages,
388
- server_size: server_size,
440
+ throw error;
441
+ }
442
+ });
443
+ /**
444
+ * Create default profiles (sync version for non-interactive mode)
445
+ */
446
+ function createDefaultProfilesSync(scan, config) {
447
+ return createProfilesFromScan(scan);
448
+ }
449
+ async function createDefaultProfiles(scan, config) {
450
+ return createProfilesFromScan(scan);
451
+ }
452
+ function createProfilesFromScan(scan) {
453
+ const profiles = {};
454
+ const frontendApps = scan.apps.filter(a => a.type === 'frontend');
455
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
456
+ const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
457
+ // Quick UI profiles for each frontend
458
+ for (const frontend of frontendApps.slice(0, 3)) {
459
+ profiles[`${frontend.name}-quick`] = {
460
+ description: `${frontend.name} only, connected to staging`,
461
+ size: 'small',
462
+ apps: [frontend.name],
463
+ connect_to: 'staging',
464
+ };
465
+ }
466
+ // Full local development
467
+ if (hasApi && frontendApps.length > 0) {
468
+ const primaryFrontend = frontendApps[0];
469
+ profiles[`${primaryFrontend.name}-full`] = {
470
+ description: `${primaryFrontend.name} + local API + DB copy`,
471
+ size: 'large',
472
+ apps: [primaryFrontend.name, 'api'],
473
+ database: {
474
+ mode: 'copy',
475
+ source: 'staging',
389
476
  },
390
- scripts: [...selectedScripts, ...extraScripts],
391
- repos: reposConfig,
392
- files: [],
393
- hooks: {
394
- post_checkout: info.languages.node ? ['npm install'] : [],
477
+ };
478
+ }
479
+ // API development only
480
+ if (hasApi) {
481
+ profiles['api-dev'] = {
482
+ description: 'API with local infrastructure',
483
+ size: 'medium',
484
+ apps: ['api'],
485
+ database: {
486
+ mode: 'local',
395
487
  },
396
- ...(gitAuth && { git_auth: gitAuth }),
397
488
  };
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`));
489
+ }
490
+ // All frontends + staging
491
+ if (frontendApps.length > 1) {
492
+ profiles['frontends-staging'] = {
493
+ description: 'All frontends with staging backend',
494
+ size: 'medium',
495
+ apps: frontendApps.map(a => a.name),
496
+ connect_to: 'staging',
497
+ };
498
+ }
499
+ // Full stack
500
+ if (scan.apps.length > 1) {
501
+ profiles['full-stack'] = {
502
+ description: 'Everything local with DB copy',
503
+ size: 'xl',
504
+ apps: scan.apps.filter(a => a.type !== 'library').map(a => a.name),
505
+ database: {
506
+ mode: 'copy',
507
+ source: 'staging',
508
+ },
509
+ };
510
+ }
511
+ return profiles;
512
+ }
513
+ /**
514
+ * Setup git authentication
515
+ */
516
+ async function setupGitAuth(gitInfo, projectName) {
517
+ console.log('');
518
+ console.log(chalk_1.default.blue('=== Git Repository Setup ==='));
519
+ console.log(chalk_1.default.dim(`Detected: ${gitInfo.remote}`));
520
+ const authMethod = await prompts.select({
521
+ message: 'How should genbox access this repository?',
522
+ choices: [
523
+ { name: 'Personal Access Token (PAT) - recommended', value: 'token' },
524
+ { name: 'SSH Key', value: 'ssh' },
525
+ { name: 'Public (no auth needed)', value: 'public' },
526
+ ],
527
+ default: 'token',
528
+ });
529
+ let repoUrl = gitInfo.remote;
530
+ let git_auth;
531
+ if (authMethod === 'token') {
532
+ // Convert SSH to HTTPS if needed
533
+ if (gitInfo.type === 'ssh') {
534
+ repoUrl = (0, scan_1.sshToHttps)(gitInfo.remote);
535
+ console.log(chalk_1.default.dim(` Will use HTTPS: ${repoUrl}`));
536
+ }
537
+ git_auth = { method: 'token' };
538
+ // Show token setup instructions
539
+ console.log('');
540
+ console.log(chalk_1.default.yellow(' Add your token to .env.genbox:'));
541
+ console.log(chalk_1.default.white(' GIT_TOKEN=ghp_xxxxxxxxxxxx'));
542
+ }
543
+ else if (authMethod === 'ssh') {
544
+ // Convert HTTPS to SSH if needed
545
+ if (gitInfo.type === 'https') {
546
+ repoUrl = (0, scan_1.httpsToSsh)(gitInfo.remote);
547
+ console.log(chalk_1.default.dim(` Will use SSH: ${repoUrl}`));
548
+ }
549
+ // Detect SSH keys
550
+ const scanKeys = await prompts.confirm({
551
+ message: 'Scan ~/.ssh/ for available keys?',
552
+ default: true,
553
+ });
554
+ let sshKeyPath = path_1.default.join(os.homedir(), '.ssh', 'id_ed25519');
555
+ if (scanKeys) {
556
+ const keys = (0, scan_1.detectSshKeys)();
557
+ if (keys.length > 0) {
558
+ const keyChoices = keys.map((k) => ({
559
+ name: `${k.name} (${k.type})`,
560
+ value: k.path,
561
+ }));
562
+ sshKeyPath = await prompts.select({
563
+ message: 'Select SSH key:',
564
+ choices: keyChoices,
565
+ });
566
+ }
567
+ }
568
+ git_auth = { method: 'ssh', ssh_key_path: sshKeyPath };
569
+ console.log('');
570
+ console.log(chalk_1.default.yellow(' Add your SSH key to .env.genbox:'));
571
+ console.log(chalk_1.default.white(` GIT_SSH_KEY="$(cat ${sshKeyPath})"`));
572
+ }
573
+ const repoName = path_1.default.basename(repoUrl, '.git');
574
+ return {
575
+ repos: {
576
+ [repoName]: {
577
+ url: repoUrl,
578
+ path: `/home/dev/${repoName}`,
579
+ auth: authMethod === 'public' ? undefined : authMethod,
580
+ },
581
+ },
582
+ git_auth,
583
+ };
584
+ }
585
+ /**
586
+ * Setup staging/production environments
587
+ */
588
+ async function setupEnvironments(scan, config, isMultiRepo = false) {
589
+ const setupEnvs = await prompts.confirm({
590
+ message: 'Configure staging/production environments?',
591
+ default: true,
592
+ });
593
+ if (!setupEnvs) {
594
+ return undefined;
595
+ }
596
+ console.log('');
597
+ console.log(chalk_1.default.blue('=== Environment Setup ==='));
598
+ console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
599
+ console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
600
+ console.log('');
601
+ const environments = {};
602
+ if (isMultiRepo) {
603
+ // For multi-repo: configure API URLs per backend app
604
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
605
+ if (backendApps.length > 0) {
606
+ console.log(chalk_1.default.dim('Configure staging API URLs for each backend service:'));
607
+ const stagingApi = {};
608
+ for (const app of backendApps) {
609
+ const url = await prompts.input({
610
+ message: ` ${app.name} staging URL (leave empty to skip):`,
611
+ default: '',
612
+ });
613
+ if (url) {
614
+ stagingApi[app.name] = url;
418
615
  }
419
- 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`));
616
+ }
617
+ if (Object.keys(stagingApi).length > 0) {
618
+ environments.staging = {
619
+ description: 'Staging environment',
620
+ api: stagingApi,
621
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
622
+ redis: { url: '${STAGING_REDIS_URL}' },
623
+ };
624
+ }
625
+ }
626
+ else {
627
+ // No backend apps, just ask for a single URL
628
+ const stagingApiUrl = await prompts.input({
629
+ message: 'Staging API URL (leave empty to skip):',
630
+ default: '',
631
+ });
632
+ if (stagingApiUrl) {
633
+ environments.staging = {
634
+ description: 'Staging environment',
635
+ api: { gateway: stagingApiUrl },
636
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
637
+ redis: { url: '${STAGING_REDIS_URL}' },
638
+ };
639
+ }
640
+ }
641
+ }
642
+ else {
643
+ // Single repo: simple single URL
644
+ const stagingApiUrl = await prompts.input({
645
+ message: 'Staging API URL (leave empty to skip):',
646
+ default: '',
647
+ });
648
+ if (stagingApiUrl) {
649
+ environments.staging = {
650
+ description: 'Staging environment',
651
+ api: { gateway: stagingApiUrl },
652
+ mongodb: { url: '${STAGING_MONGODB_URL}' },
653
+ redis: { url: '${STAGING_REDIS_URL}' },
654
+ };
655
+ }
656
+ }
657
+ const setupProd = await prompts.confirm({
658
+ message: 'Also configure production environment?',
659
+ default: false,
660
+ });
661
+ if (setupProd) {
662
+ if (isMultiRepo) {
663
+ const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
664
+ if (backendApps.length > 0) {
665
+ console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
666
+ const prodApi = {};
667
+ for (const app of backendApps) {
668
+ const url = await prompts.input({
669
+ message: ` ${app.name} production URL:`,
670
+ default: '',
671
+ });
672
+ if (url) {
673
+ prodApi[app.name] = url;
432
674
  }
433
675
  }
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`));
676
+ if (Object.keys(prodApi).length > 0) {
677
+ environments.production = {
678
+ description: 'Production (use with caution)',
679
+ api: prodApi,
680
+ mongodb: {
681
+ url: '${PROD_MONGODB_URL}',
682
+ read_only: true,
683
+ },
684
+ };
448
685
  }
449
686
  }
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
- }
687
+ }
688
+ else {
689
+ const prodApiUrl = await prompts.input({
690
+ message: 'Production API URL:',
691
+ default: '',
692
+ });
693
+ if (prodApiUrl) {
694
+ environments.production = {
695
+ description: 'Production (use with caution)',
696
+ api: { gateway: prodApiUrl },
697
+ mongodb: {
698
+ url: '${PROD_MONGODB_URL}',
699
+ read_only: true,
700
+ },
701
+ };
702
+ }
703
+ }
704
+ }
705
+ return Object.keys(environments).length > 0 ? environments : undefined;
706
+ }
707
+ /**
708
+ * Setup .env.genbox file
709
+ */
710
+ async function setupEnvFile(projectName, config, nonInteractive = false, scan, isMultiRepo = false) {
711
+ const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
712
+ if (fs_1.default.existsSync(envPath)) {
713
+ console.log(chalk_1.default.dim(` ${ENV_FILENAME} already exists, skipping...`));
714
+ return;
715
+ }
716
+ // For multi-repo: find env files in app directories
717
+ if (isMultiRepo && scan) {
718
+ const appEnvFiles = findAppEnvFiles(scan.apps, process.cwd());
719
+ if (appEnvFiles.length > 0 && !nonInteractive) {
720
+ console.log('');
721
+ console.log(chalk_1.default.blue('=== Environment Files ==='));
722
+ console.log(chalk_1.default.dim(`Found .env files in ${appEnvFiles.length} app directories`));
723
+ const envChoices = appEnvFiles.map(env => ({
724
+ name: `${env.appName}/${env.envFile}`,
725
+ value: env.fullPath,
726
+ checked: true,
727
+ }));
728
+ const selectedEnvFiles = await prompts.checkbox({
729
+ message: 'Select .env files to merge into .env.genbox:',
730
+ choices: envChoices,
731
+ });
732
+ if (selectedEnvFiles.length > 0) {
733
+ let mergedContent = `# Genbox Environment Variables
734
+ # Merged from: ${selectedEnvFiles.map(f => path_1.default.relative(process.cwd(), f)).join(', ')}
735
+ # DO NOT COMMIT THIS FILE
736
+ #
737
+ # Add staging/production URLs:
738
+ # STAGING_MONGODB_URL=mongodb+srv://...
739
+ # STAGING_REDIS_URL=redis://...
740
+ # PROD_MONGODB_URL=mongodb+srv://...
741
+ #
742
+ # Git authentication:
743
+ # GIT_TOKEN=ghp_xxxxxxxxxxxx
744
+
745
+ `;
746
+ for (const envFilePath of selectedEnvFiles) {
747
+ const appInfo = appEnvFiles.find(e => e.fullPath === envFilePath);
748
+ const content = fs_1.default.readFileSync(envFilePath, 'utf8');
749
+ mergedContent += `\n# === ${appInfo?.appName || path_1.default.dirname(envFilePath)} ===\n`;
750
+ mergedContent += content;
751
+ mergedContent += '\n';
460
752
  }
753
+ fs_1.default.writeFileSync(envPath, mergedContent);
754
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${selectedEnvFiles.length} app env files`));
755
+ }
756
+ }
757
+ else if (appEnvFiles.length > 0 && nonInteractive) {
758
+ // Non-interactive: merge all env files
759
+ let mergedContent = `# Genbox Environment Variables
760
+ # Merged from app directories
761
+ # DO NOT COMMIT THIS FILE
762
+
763
+ `;
764
+ for (const envFile of appEnvFiles) {
765
+ const content = fs_1.default.readFileSync(envFile.fullPath, 'utf8');
766
+ mergedContent += `\n# === ${envFile.appName} ===\n`;
767
+ mergedContent += content;
768
+ mergedContent += '\n';
769
+ }
770
+ fs_1.default.writeFileSync(envPath, mergedContent);
771
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${appEnvFiles.length} app env files`));
772
+ }
773
+ }
774
+ // If no env file created yet, check for root .env
775
+ if (!fs_1.default.existsSync(envPath)) {
776
+ const existingEnvFiles = ['.env.local', '.env', '.env.development'];
777
+ let existingEnvPath;
778
+ for (const envFile of existingEnvFiles) {
779
+ const fullPath = path_1.default.join(process.cwd(), envFile);
780
+ if (fs_1.default.existsSync(fullPath)) {
781
+ existingEnvPath = fullPath;
782
+ break;
783
+ }
784
+ }
785
+ if (existingEnvPath) {
786
+ const copyExisting = nonInteractive ? true : await prompts.confirm({
787
+ message: `Found ${path_1.default.basename(existingEnvPath)}. Copy to ${ENV_FILENAME}?`,
788
+ default: true,
789
+ });
790
+ if (copyExisting) {
791
+ const content = fs_1.default.readFileSync(existingEnvPath, 'utf8');
792
+ const header = `# Genbox Environment Variables
793
+ # Generated from ${path_1.default.basename(existingEnvPath)}
794
+ # DO NOT COMMIT THIS FILE
795
+ #
796
+ # Add staging/production URLs:
797
+ # STAGING_MONGODB_URL=mongodb+srv://...
798
+ # STAGING_REDIS_URL=redis://...
799
+ # PROD_MONGODB_URL=mongodb+srv://...
800
+ #
801
+ # Git authentication:
802
+ # GIT_TOKEN=ghp_xxxxxxxxxxxx
803
+
804
+ `;
805
+ fs_1.default.writeFileSync(envPath, header + content);
806
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} from ${path_1.default.basename(existingEnvPath)}`));
461
807
  }
462
808
  }
463
809
  else {
464
- console.log(chalk_1.default.dim(` .env.genbox already exists, skipping...`));
810
+ const createEnv = nonInteractive ? true : await prompts.confirm({
811
+ message: `Create ${ENV_FILENAME} template?`,
812
+ default: true,
813
+ });
814
+ if (createEnv) {
815
+ const template = generateEnvTemplate(projectName, config);
816
+ fs_1.default.writeFileSync(envPath, template);
817
+ console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME} template`));
818
+ }
465
819
  }
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`));
820
+ }
821
+ // Add to .gitignore
822
+ const gitignorePath = path_1.default.join(process.cwd(), '.gitignore');
823
+ if (fs_1.default.existsSync(gitignorePath)) {
824
+ const content = fs_1.default.readFileSync(gitignorePath, 'utf8');
825
+ if (!content.includes(ENV_FILENAME)) {
826
+ fs_1.default.appendFileSync(gitignorePath, `\n# Genbox secrets\n${ENV_FILENAME}\n`);
827
+ console.log(chalk_1.default.dim(` Added ${ENV_FILENAME} to .gitignore`));
470
828
  }
471
- console.log('');
472
- 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`));
477
829
  }
478
- catch (error) {
479
- // Handle user cancellation (Ctrl+C) gracefully
480
- if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
481
- console.log('');
482
- console.log(chalk_1.default.dim('Cancelled.'));
483
- process.exit(0);
830
+ }
831
+ /**
832
+ * Generate .env.genbox template
833
+ */
834
+ function generateEnvTemplate(projectName, config) {
835
+ const lines = [
836
+ '# Genbox Environment Variables',
837
+ `# Project: ${projectName}`,
838
+ '# DO NOT COMMIT THIS FILE',
839
+ '',
840
+ '# ============================================',
841
+ '# STAGING ENVIRONMENT',
842
+ '# ============================================',
843
+ '',
844
+ '# Database',
845
+ 'STAGING_MONGODB_URL=mongodb+srv://user:password@staging.mongodb.net',
846
+ '',
847
+ '# Cache & Queue',
848
+ 'STAGING_REDIS_URL=redis://staging-redis:6379',
849
+ 'STAGING_RABBITMQ_URL=amqp://user:password@staging-rabbitmq:5672',
850
+ '',
851
+ '# ============================================',
852
+ '# PRODUCTION ENVIRONMENT',
853
+ '# ============================================',
854
+ '',
855
+ 'PROD_MONGODB_URL=mongodb+srv://readonly:password@prod.mongodb.net',
856
+ '',
857
+ '# ============================================',
858
+ '# GIT AUTHENTICATION',
859
+ '# ============================================',
860
+ '',
861
+ '# For HTTPS repos (Personal Access Token)',
862
+ 'GIT_TOKEN=ghp_xxxxxxxxxxxx',
863
+ '',
864
+ '# For SSH repos (paste private key content)',
865
+ '# GIT_SSH_KEY="-----BEGIN OPENSSH PRIVATE KEY-----',
866
+ '# ...',
867
+ '# -----END OPENSSH PRIVATE KEY-----"',
868
+ '',
869
+ '# ============================================',
870
+ '# APPLICATION SECRETS',
871
+ '# ============================================',
872
+ '',
873
+ 'JWT_SECRET=your-jwt-secret-here',
874
+ '',
875
+ '# OAuth',
876
+ 'GOOGLE_CLIENT_ID=',
877
+ 'GOOGLE_CLIENT_SECRET=',
878
+ '',
879
+ '# Payments',
880
+ 'STRIPE_SECRET_KEY=sk_test_xxx',
881
+ 'STRIPE_WEBHOOK_SECRET=whsec_xxx',
882
+ '',
883
+ ];
884
+ return lines.join('\n');
885
+ }
886
+ /**
887
+ * Convert GenboxConfigV2 to GenboxConfigV3 format
888
+ */
889
+ function convertV2ToV3(v2Config, scan) {
890
+ // Convert services to apps
891
+ const apps = {};
892
+ for (const [name, service] of Object.entries(v2Config.services || {})) {
893
+ const appConfig = {
894
+ path: service.path || `/home/dev/${v2Config.project.name}/${name}`,
895
+ type: service.type === 'api' ? 'backend' : service.type,
896
+ port: service.port,
897
+ };
898
+ // Only add framework if defined
899
+ if (service.framework) {
900
+ appConfig.framework = service.framework;
484
901
  }
485
- // Re-throw other errors
486
- throw error;
902
+ // Only add requires if there are dependencies
903
+ if (service.dependsOn?.length) {
904
+ appConfig.requires = service.dependsOn.reduce((acc, dep) => {
905
+ acc[dep] = 'required';
906
+ return acc;
907
+ }, {});
908
+ }
909
+ // Build commands object without undefined values
910
+ const commands = {};
911
+ if (service.build?.command)
912
+ commands.build = service.build.command;
913
+ if (service.start?.command)
914
+ commands.start = service.start.command;
915
+ if (service.start?.dev)
916
+ commands.dev = service.start.dev;
917
+ if (Object.keys(commands).length > 0) {
918
+ appConfig.commands = commands;
919
+ }
920
+ // Only add env if defined
921
+ if (service.env?.length) {
922
+ appConfig.env = service.env;
923
+ }
924
+ apps[name] = appConfig;
487
925
  }
488
- });
926
+ // Convert infrastructure
927
+ const infrastructure = {};
928
+ if (v2Config.infrastructure?.databases) {
929
+ for (const db of v2Config.infrastructure.databases) {
930
+ infrastructure[db.container || db.type] = {
931
+ type: 'database',
932
+ image: `${db.type}:latest`,
933
+ port: db.port,
934
+ };
935
+ }
936
+ }
937
+ if (v2Config.infrastructure?.caches) {
938
+ for (const cache of v2Config.infrastructure.caches) {
939
+ infrastructure[cache.container || cache.type] = {
940
+ type: 'cache',
941
+ image: `${cache.type}:latest`,
942
+ port: cache.port,
943
+ };
944
+ }
945
+ }
946
+ if (v2Config.infrastructure?.queues) {
947
+ for (const queue of v2Config.infrastructure.queues) {
948
+ infrastructure[queue.container || queue.type] = {
949
+ type: 'queue',
950
+ image: `${queue.type}:latest`,
951
+ port: queue.port,
952
+ management_port: queue.managementPort,
953
+ };
954
+ }
955
+ }
956
+ // Convert repos
957
+ const repos = {};
958
+ for (const [name, repo] of Object.entries(v2Config.repos || {})) {
959
+ repos[name] = {
960
+ url: repo.url,
961
+ path: repo.path,
962
+ branch: repo.branch,
963
+ auth: repo.auth,
964
+ };
965
+ }
966
+ // Build v3 config
967
+ const v3Config = {
968
+ version: '3.0',
969
+ project: {
970
+ name: v2Config.project.name,
971
+ structure: v2Config.project.structure === 'single-app' ? 'single-app' :
972
+ v2Config.project.structure.startsWith('monorepo') ? 'monorepo' :
973
+ v2Config.project.structure,
974
+ description: v2Config.project.description,
975
+ },
976
+ apps,
977
+ infrastructure: Object.keys(infrastructure).length > 0 ? infrastructure : undefined,
978
+ repos: Object.keys(repos).length > 0 ? repos : undefined,
979
+ defaults: {
980
+ size: v2Config.system.size,
981
+ },
982
+ hooks: v2Config.hooks ? {
983
+ post_checkout: v2Config.hooks.postCheckout,
984
+ post_start: v2Config.hooks.postStart,
985
+ pre_start: v2Config.hooks.preStart,
986
+ } : undefined,
987
+ };
988
+ return v3Config;
989
+ }