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.
- package/dist/commands/create.js +369 -130
- package/dist/commands/db-sync.js +364 -0
- package/dist/commands/init.js +899 -398
- package/dist/commands/profiles.js +333 -0
- package/dist/commands/push.js +140 -47
- package/dist/config-loader.js +529 -0
- package/dist/index.js +5 -1
- package/dist/profile-resolver.js +547 -0
- package/dist/scanner/compose-parser.js +441 -0
- package/dist/scanner/config-generator.js +620 -0
- package/dist/scanner/env-analyzer.js +503 -0
- package/dist/scanner/framework-detector.js +621 -0
- package/dist/scanner/index.js +424 -0
- package/dist/scanner/runtime-detector.js +330 -0
- package/dist/scanner/structure-detector.js +412 -0
- package/dist/scanner/types.js +7 -0
- package/dist/schema-v3.js +12 -0
- package/package.json +2 -1
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
//
|
|
486
|
-
|
|
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
|
+
}
|