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