genbox 1.0.19 → 1.0.20
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 +102 -1
- package/dist/commands/destroy.js +142 -0
- package/dist/commands/init.js +15 -0
- package/dist/commands/rebuild.js +576 -0
- package/dist/commands/scan.js +4 -2
- package/dist/index.js +3 -1
- package/dist/profile-resolver.js +16 -5
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -110,6 +110,8 @@ exports.createCommand = new commander_1.Command('create')
|
|
|
110
110
|
.option('--db-source <source>', 'Database source: staging, production')
|
|
111
111
|
.option('-s, --size <size>', 'Server size: small, medium, large, xl')
|
|
112
112
|
.option('-b, --branch <branch>', 'Git branch to checkout')
|
|
113
|
+
.option('-nb, --new-branch <name>', 'Create a new branch with this name')
|
|
114
|
+
.option('-sb, --source-branch <branch>', 'Source branch to create new branch from (defaults to current/default branch)')
|
|
113
115
|
.option('-y, --yes', 'Skip interactive prompts')
|
|
114
116
|
.option('--dry-run', 'Show what would be created without actually creating')
|
|
115
117
|
.action(async (name, options) => {
|
|
@@ -137,13 +139,19 @@ exports.createCommand = new commander_1.Command('create')
|
|
|
137
139
|
dbSource: options.dbSource,
|
|
138
140
|
size: options.size,
|
|
139
141
|
branch: options.branch,
|
|
142
|
+
newBranch: options.newBranch,
|
|
143
|
+
sourceBranch: options.sourceBranch,
|
|
140
144
|
yes: options.yes,
|
|
141
145
|
dryRun: options.dryRun,
|
|
142
146
|
};
|
|
143
147
|
// Resolve configuration
|
|
144
148
|
console.log(chalk_1.default.blue('Resolving configuration...'));
|
|
145
149
|
console.log('');
|
|
146
|
-
|
|
150
|
+
let resolved = await profileResolver.resolve(config, createOptions);
|
|
151
|
+
// Interactive branch selection if no branch was specified
|
|
152
|
+
if (!options.branch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
|
|
153
|
+
resolved = await promptForBranchOptions(resolved, config);
|
|
154
|
+
}
|
|
147
155
|
// Display resolved configuration
|
|
148
156
|
displayResolvedConfig(resolved);
|
|
149
157
|
// Ask to save as profile (if not using one already)
|
|
@@ -264,6 +272,12 @@ function displayResolvedConfig(resolved) {
|
|
|
264
272
|
for (const repo of resolved.repos) {
|
|
265
273
|
console.log(` • ${repo.name}: ${repo.url}`);
|
|
266
274
|
console.log(chalk_1.default.dim(` → ${repo.path}`));
|
|
275
|
+
if (repo.newBranch) {
|
|
276
|
+
console.log(` Branch: ${chalk_1.default.cyan(repo.newBranch)} ${chalk_1.default.dim(`(new, from ${repo.sourceBranch || 'main'})`)}`);
|
|
277
|
+
}
|
|
278
|
+
else if (repo.branch) {
|
|
279
|
+
console.log(` Branch: ${chalk_1.default.cyan(repo.branch)}`);
|
|
280
|
+
}
|
|
267
281
|
}
|
|
268
282
|
}
|
|
269
283
|
if (resolved.infrastructure.length > 0) {
|
|
@@ -292,6 +306,91 @@ function displayResolvedConfig(resolved) {
|
|
|
292
306
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
293
307
|
console.log('');
|
|
294
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Prompt for branch options interactively
|
|
311
|
+
*/
|
|
312
|
+
async function promptForBranchOptions(resolved, config) {
|
|
313
|
+
// Get the default branch from config or first repo
|
|
314
|
+
const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
|
|
315
|
+
console.log(chalk_1.default.blue('=== Branch Configuration ==='));
|
|
316
|
+
console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
|
|
317
|
+
console.log('');
|
|
318
|
+
const branchChoice = await prompts.select({
|
|
319
|
+
message: 'Branch option:',
|
|
320
|
+
choices: [
|
|
321
|
+
{
|
|
322
|
+
name: `Use default branch (${defaultBranch})`,
|
|
323
|
+
value: 'default',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: 'Use a different existing branch',
|
|
327
|
+
value: 'existing',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'Create a new branch',
|
|
331
|
+
value: 'new',
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
default: 'default',
|
|
335
|
+
});
|
|
336
|
+
if (branchChoice === 'default') {
|
|
337
|
+
// Keep resolved repos as-is
|
|
338
|
+
return resolved;
|
|
339
|
+
}
|
|
340
|
+
if (branchChoice === 'existing') {
|
|
341
|
+
const branchName = await prompts.input({
|
|
342
|
+
message: 'Enter branch name:',
|
|
343
|
+
default: defaultBranch,
|
|
344
|
+
validate: (value) => {
|
|
345
|
+
if (!value.trim())
|
|
346
|
+
return 'Branch name is required';
|
|
347
|
+
return true;
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
// Update all repos with the selected branch
|
|
351
|
+
return {
|
|
352
|
+
...resolved,
|
|
353
|
+
repos: resolved.repos.map(repo => ({
|
|
354
|
+
...repo,
|
|
355
|
+
branch: branchName.trim(),
|
|
356
|
+
newBranch: undefined,
|
|
357
|
+
sourceBranch: undefined,
|
|
358
|
+
})),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (branchChoice === 'new') {
|
|
362
|
+
const newBranchName = await prompts.input({
|
|
363
|
+
message: 'New branch name:',
|
|
364
|
+
validate: (value) => {
|
|
365
|
+
if (!value.trim())
|
|
366
|
+
return 'Branch name is required';
|
|
367
|
+
if (!/^[\w\-./]+$/.test(value))
|
|
368
|
+
return 'Invalid branch name (use letters, numbers, -, _, /, .)';
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
const sourceBranch = await prompts.input({
|
|
373
|
+
message: 'Create from branch:',
|
|
374
|
+
default: defaultBranch,
|
|
375
|
+
validate: (value) => {
|
|
376
|
+
if (!value.trim())
|
|
377
|
+
return 'Source branch is required';
|
|
378
|
+
return true;
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
// Update all repos with new branch info
|
|
382
|
+
return {
|
|
383
|
+
...resolved,
|
|
384
|
+
repos: resolved.repos.map(repo => ({
|
|
385
|
+
...repo,
|
|
386
|
+
branch: newBranchName.trim(),
|
|
387
|
+
newBranch: newBranchName.trim(),
|
|
388
|
+
sourceBranch: sourceBranch.trim(),
|
|
389
|
+
})),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return resolved;
|
|
393
|
+
}
|
|
295
394
|
/**
|
|
296
395
|
* Parse .env.genbox file into segregated sections
|
|
297
396
|
*/
|
|
@@ -505,6 +604,8 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
505
604
|
url: repo.url,
|
|
506
605
|
path: repo.path,
|
|
507
606
|
branch: repo.branch,
|
|
607
|
+
newBranch: repo.newBranch,
|
|
608
|
+
sourceBranch: repo.sourceBranch,
|
|
508
609
|
};
|
|
509
610
|
}
|
|
510
611
|
return {
|
package/dist/commands/destroy.js
CHANGED
|
@@ -170,12 +170,132 @@ async function handleBulkDelete(options) {
|
|
|
170
170
|
console.log(chalk_1.default.yellow(`Completed: ${successCount} destroyed, ${failCount} failed.`));
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Check for uncommitted changes and handle them before destroy
|
|
175
|
+
*/
|
|
176
|
+
async function handleUncommittedChanges(genbox, options) {
|
|
177
|
+
// Skip if --yes flag is used (user explicitly wants to skip this)
|
|
178
|
+
if (options.yes && !options.saveChanges) {
|
|
179
|
+
return { proceed: true };
|
|
180
|
+
}
|
|
181
|
+
// If save-changes is explicitly set, use that value
|
|
182
|
+
if (options.saveChanges === 'trash' || options.saveChanges === 'skip') {
|
|
183
|
+
return { proceed: true, action: options.saveChanges };
|
|
184
|
+
}
|
|
185
|
+
// Check for uncommitted changes by running git status on the genbox
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(chalk_1.default.blue('Checking for uncommitted changes...'));
|
|
188
|
+
try {
|
|
189
|
+
// Try to get git status from the genbox
|
|
190
|
+
const statusResponse = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/exec`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
command: 'cd /home/dev/* 2>/dev/null && git status --porcelain 2>/dev/null || echo ""',
|
|
194
|
+
timeout: 10000,
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
const gitStatus = statusResponse?.stdout?.trim() || '';
|
|
198
|
+
if (!gitStatus) {
|
|
199
|
+
console.log(chalk_1.default.dim(' No uncommitted changes detected.'));
|
|
200
|
+
return { proceed: true, action: 'none' };
|
|
201
|
+
}
|
|
202
|
+
// There are uncommitted changes
|
|
203
|
+
const changeCount = gitStatus.split('\n').filter(line => line.trim()).length;
|
|
204
|
+
console.log(chalk_1.default.yellow(` Found ${changeCount} uncommitted change${changeCount === 1 ? '' : 's'}.`));
|
|
205
|
+
console.log('');
|
|
206
|
+
// If save-changes is set to 'pr', auto-select that option
|
|
207
|
+
if (options.saveChanges === 'pr') {
|
|
208
|
+
return { proceed: true, action: 'pr' };
|
|
209
|
+
}
|
|
210
|
+
// Prompt user for action
|
|
211
|
+
const action = await (0, select_1.default)({
|
|
212
|
+
message: 'What would you like to do with uncommitted changes?',
|
|
213
|
+
choices: [
|
|
214
|
+
{
|
|
215
|
+
name: 'Commit, push, and create PR (recommended)',
|
|
216
|
+
value: 'pr',
|
|
217
|
+
description: 'Creates a PR with your changes before destroying',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'Trash changes',
|
|
221
|
+
value: 'trash',
|
|
222
|
+
description: 'Discard all uncommitted changes',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'Cancel destroy',
|
|
226
|
+
value: 'cancel',
|
|
227
|
+
description: 'Keep the genbox running',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
default: 'pr',
|
|
231
|
+
});
|
|
232
|
+
if (action === 'cancel') {
|
|
233
|
+
return { proceed: false };
|
|
234
|
+
}
|
|
235
|
+
return { proceed: true, action };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// If we can't check (e.g., genbox is not running), just proceed
|
|
239
|
+
console.log(chalk_1.default.dim(' Could not check for changes (genbox may not be accessible).'));
|
|
240
|
+
return { proceed: true, action: 'skip' };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Commit, push and create PR for changes
|
|
245
|
+
*/
|
|
246
|
+
async function createPRForChanges(genbox) {
|
|
247
|
+
const spinner = (0, ora_1.default)('Creating PR for uncommitted changes...').start();
|
|
248
|
+
try {
|
|
249
|
+
// Execute git commands on the genbox to commit, push, and create PR
|
|
250
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
251
|
+
const branchName = `genbox-save/${genbox.name}-${timestamp}`;
|
|
252
|
+
const commands = [
|
|
253
|
+
// Ensure we're in the project directory
|
|
254
|
+
'cd /home/dev/*',
|
|
255
|
+
// Create a new branch for the changes
|
|
256
|
+
`git checkout -b "${branchName}"`,
|
|
257
|
+
// Stage all changes
|
|
258
|
+
'git add -A',
|
|
259
|
+
// Commit with a descriptive message
|
|
260
|
+
`git commit -m "Save changes from genbox '${genbox.name}' before destroy"`,
|
|
261
|
+
// Push to origin
|
|
262
|
+
`git push -u origin "${branchName}"`,
|
|
263
|
+
// Try to create PR using gh CLI if available
|
|
264
|
+
`gh pr create --title "Changes from genbox '${genbox.name}'" --body "Auto-saved changes before genbox destroy" 2>/dev/null || echo "PR_SKIP"`,
|
|
265
|
+
].join(' && ');
|
|
266
|
+
const result = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/exec`, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
command: commands,
|
|
270
|
+
timeout: 60000,
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
if (result?.exitCode !== 0 && !result?.stdout?.includes('PR_SKIP')) {
|
|
274
|
+
spinner.fail(chalk_1.default.red('Failed to save changes'));
|
|
275
|
+
console.log(chalk_1.default.dim(` Error: ${result?.stderr || 'Unknown error'}`));
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (result?.stdout?.includes('PR_SKIP')) {
|
|
279
|
+
spinner.succeed(chalk_1.default.green(`Changes pushed to branch '${branchName}'`));
|
|
280
|
+
console.log(chalk_1.default.dim(' Note: Could not create PR automatically. Create it manually on GitHub.'));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
spinner.succeed(chalk_1.default.green('PR created successfully'));
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
spinner.fail(chalk_1.default.red(`Failed to save changes: ${error.message}`));
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
173
292
|
exports.destroyCommand = new commander_1.Command('destroy')
|
|
174
293
|
.alias('delete')
|
|
175
294
|
.description('Destroy one or more Genboxes')
|
|
176
295
|
.argument('[name]', 'Name of the Genbox to destroy (optional - will prompt if not provided)')
|
|
177
296
|
.option('-y, --yes', 'Skip confirmation')
|
|
178
297
|
.option('-a, --all', 'Bulk delete mode - select multiple genboxes to destroy')
|
|
298
|
+
.option('--save-changes <action>', 'Handle uncommitted changes: pr (commit/push/PR), trash (discard), skip (ignore)')
|
|
179
299
|
.action(async (name, options) => {
|
|
180
300
|
try {
|
|
181
301
|
// Bulk delete mode when --all is used without a specific name
|
|
@@ -195,6 +315,28 @@ exports.destroyCommand = new commander_1.Command('destroy')
|
|
|
195
315
|
if (!target) {
|
|
196
316
|
return;
|
|
197
317
|
}
|
|
318
|
+
// Check for uncommitted changes if genbox is running
|
|
319
|
+
if (target.status === 'running') {
|
|
320
|
+
const { proceed, action } = await handleUncommittedChanges(target, options);
|
|
321
|
+
if (!proceed) {
|
|
322
|
+
console.log(chalk_1.default.dim('Destroy cancelled.'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Handle the action
|
|
326
|
+
if (action === 'pr') {
|
|
327
|
+
const success = await createPRForChanges(target);
|
|
328
|
+
if (!success) {
|
|
329
|
+
const continueAnyway = await (0, confirm_1.default)({
|
|
330
|
+
message: 'Failed to save changes. Continue with destroy anyway?',
|
|
331
|
+
default: false,
|
|
332
|
+
});
|
|
333
|
+
if (!continueAnyway) {
|
|
334
|
+
console.log(chalk_1.default.dim('Destroy cancelled.'));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
198
340
|
// Confirm
|
|
199
341
|
let confirmed = options.yes;
|
|
200
342
|
if (!confirmed) {
|
package/dist/commands/init.js
CHANGED
|
@@ -567,6 +567,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
567
567
|
}
|
|
568
568
|
if (scan.git) {
|
|
569
569
|
console.log(` ${chalk_1.default.dim('Git:')} ${scan.git.remote} (${scan.git.type})`);
|
|
570
|
+
console.log(` ${chalk_1.default.dim('Branch:')} ${chalk_1.default.cyan(scan.git.branch || 'main')}`);
|
|
570
571
|
}
|
|
571
572
|
console.log('');
|
|
572
573
|
// Get project name (use scan value when --from-scan)
|
|
@@ -626,6 +627,20 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
626
627
|
v4Config.defaults = {};
|
|
627
628
|
}
|
|
628
629
|
v4Config.defaults.size = serverSize;
|
|
630
|
+
// Get default branch (use detected branch or allow override)
|
|
631
|
+
const detectedBranch = scan.git?.branch || 'main';
|
|
632
|
+
let defaultBranch = detectedBranch;
|
|
633
|
+
if (!nonInteractive && !options.fromScan) {
|
|
634
|
+
const branchInput = await prompts.input({
|
|
635
|
+
message: 'Default branch for new environments:',
|
|
636
|
+
default: detectedBranch,
|
|
637
|
+
});
|
|
638
|
+
defaultBranch = branchInput || detectedBranch;
|
|
639
|
+
}
|
|
640
|
+
// Store default branch in config defaults
|
|
641
|
+
if (defaultBranch && defaultBranch !== 'main') {
|
|
642
|
+
v4Config.defaults.branch = defaultBranch;
|
|
643
|
+
}
|
|
629
644
|
// Git repository setup - different handling for multi-repo vs single-repo
|
|
630
645
|
// When using --from-scan, skip git selection and use what's in detected.yaml
|
|
631
646
|
const isMultiRepo = isMultiRepoStructure;
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.rebuildCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const prompts = __importStar(require("@inquirer/prompts"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const ora_1 = __importDefault(require("ora"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const config_loader_1 = require("../config-loader");
|
|
48
|
+
const profile_resolver_1 = require("../profile-resolver");
|
|
49
|
+
const api_1 = require("../api");
|
|
50
|
+
const schema_v4_1 = require("../schema-v4");
|
|
51
|
+
function getPublicSshKey() {
|
|
52
|
+
const home = os.homedir();
|
|
53
|
+
const potentialKeys = [
|
|
54
|
+
path.join(home, '.ssh', 'id_ed25519.pub'),
|
|
55
|
+
path.join(home, '.ssh', 'id_rsa.pub'),
|
|
56
|
+
];
|
|
57
|
+
for (const keyPath of potentialKeys) {
|
|
58
|
+
if (fs.existsSync(keyPath)) {
|
|
59
|
+
const content = fs.readFileSync(keyPath, 'utf-8').trim();
|
|
60
|
+
if (content)
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error('No public SSH key found in ~/.ssh/');
|
|
65
|
+
}
|
|
66
|
+
function getPrivateSshKey() {
|
|
67
|
+
const home = os.homedir();
|
|
68
|
+
const potentialKeys = [
|
|
69
|
+
path.join(home, '.ssh', 'id_ed25519'),
|
|
70
|
+
path.join(home, '.ssh', 'id_rsa'),
|
|
71
|
+
];
|
|
72
|
+
for (const keyPath of potentialKeys) {
|
|
73
|
+
if (fs.existsSync(keyPath)) {
|
|
74
|
+
return fs.readFileSync(keyPath, 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
async function findGenboxByName(name) {
|
|
80
|
+
const genboxes = await (0, api_1.fetchApi)('/genboxes');
|
|
81
|
+
return genboxes.find((g) => g.name === name);
|
|
82
|
+
}
|
|
83
|
+
async function rebuildGenbox(id, payload) {
|
|
84
|
+
return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify(payload),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse .env.genbox file into segregated sections
|
|
91
|
+
*/
|
|
92
|
+
function parseEnvGenboxSections(content) {
|
|
93
|
+
const sections = new Map();
|
|
94
|
+
let currentSection = 'GLOBAL';
|
|
95
|
+
let currentContent = [];
|
|
96
|
+
for (const line of content.split('\n')) {
|
|
97
|
+
const sectionMatch = line.match(/^# === ([^=]+) ===$/);
|
|
98
|
+
if (sectionMatch) {
|
|
99
|
+
if (currentContent.length > 0) {
|
|
100
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
101
|
+
}
|
|
102
|
+
currentSection = sectionMatch[1].trim();
|
|
103
|
+
currentContent = [];
|
|
104
|
+
}
|
|
105
|
+
else if (currentSection !== 'END') {
|
|
106
|
+
currentContent.push(line);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (currentContent.length > 0 && currentSection !== 'END') {
|
|
110
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
111
|
+
}
|
|
112
|
+
return sections;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a map of service URL variables based on connection type
|
|
116
|
+
*/
|
|
117
|
+
function buildServiceUrlMap(envVarsFromFile, connectTo) {
|
|
118
|
+
const urlMap = {};
|
|
119
|
+
const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
|
|
120
|
+
const serviceNames = new Set();
|
|
121
|
+
for (const key of Object.keys(envVarsFromFile)) {
|
|
122
|
+
const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
|
|
123
|
+
if (match) {
|
|
124
|
+
serviceNames.add(match[2]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const serviceName of serviceNames) {
|
|
128
|
+
const prefixedKey = `${prefix}${serviceName}`;
|
|
129
|
+
const localKey = `LOCAL_${serviceName}`;
|
|
130
|
+
const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
|
|
131
|
+
if (value) {
|
|
132
|
+
urlMap[serviceName] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!urlMap['API_URL']) {
|
|
136
|
+
const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
|
|
137
|
+
envVarsFromFile['LOCAL_API_URL'] ||
|
|
138
|
+
envVarsFromFile['STAGING_API_URL'];
|
|
139
|
+
if (apiUrl) {
|
|
140
|
+
urlMap['API_URL'] = apiUrl;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return urlMap;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build env content for a specific app
|
|
147
|
+
*/
|
|
148
|
+
function buildAppEnvContent(sections, appName, serviceUrlMap) {
|
|
149
|
+
const parts = [];
|
|
150
|
+
const globalSection = sections.get('GLOBAL');
|
|
151
|
+
if (globalSection) {
|
|
152
|
+
parts.push(globalSection);
|
|
153
|
+
}
|
|
154
|
+
const appSection = sections.get(appName);
|
|
155
|
+
if (appSection) {
|
|
156
|
+
parts.push(appSection);
|
|
157
|
+
}
|
|
158
|
+
let envContent = parts.join('\n\n');
|
|
159
|
+
for (const [varName, value] of Object.entries(serviceUrlMap)) {
|
|
160
|
+
const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
|
|
161
|
+
envContent = envContent.replace(pattern, value);
|
|
162
|
+
}
|
|
163
|
+
envContent = envContent
|
|
164
|
+
.split('\n')
|
|
165
|
+
.filter(line => {
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
|
|
168
|
+
})
|
|
169
|
+
.join('\n')
|
|
170
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
171
|
+
.trim();
|
|
172
|
+
return envContent;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Build rebuild payload from resolved config
|
|
176
|
+
*/
|
|
177
|
+
function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
178
|
+
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
179
|
+
// Build services map
|
|
180
|
+
const services = {};
|
|
181
|
+
for (const app of resolved.apps) {
|
|
182
|
+
if (app.services) {
|
|
183
|
+
for (const [name, svc] of Object.entries(app.services)) {
|
|
184
|
+
services[name] = { port: svc.port, healthcheck: svc.healthcheck };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (app.port) {
|
|
188
|
+
services[app.name] = { port: app.port };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Build files bundle
|
|
192
|
+
const files = [];
|
|
193
|
+
const envFilesToMove = [];
|
|
194
|
+
// Process .env.genbox
|
|
195
|
+
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
196
|
+
if (fs.existsSync(envGenboxPath)) {
|
|
197
|
+
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
198
|
+
const sections = parseEnvGenboxSections(rawEnvContent);
|
|
199
|
+
const globalSection = sections.get('GLOBAL') || '';
|
|
200
|
+
const envVarsFromFile = {};
|
|
201
|
+
for (const line of globalSection.split('\n')) {
|
|
202
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
203
|
+
if (match) {
|
|
204
|
+
let value = match[2].trim();
|
|
205
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
206
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
207
|
+
value = value.slice(1, -1);
|
|
208
|
+
}
|
|
209
|
+
envVarsFromFile[match[1]] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let connectTo;
|
|
213
|
+
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
214
|
+
const profile = config.profiles[resolved.profile];
|
|
215
|
+
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
216
|
+
}
|
|
217
|
+
const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
|
|
218
|
+
if (connectTo && Object.keys(serviceUrlMap).length > 0) {
|
|
219
|
+
console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
|
|
220
|
+
}
|
|
221
|
+
for (const app of resolved.apps) {
|
|
222
|
+
const appPath = config.apps[app.name]?.path || app.name;
|
|
223
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
224
|
+
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
225
|
+
const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
|
|
226
|
+
if (servicesSections.length > 0) {
|
|
227
|
+
for (const serviceSectionName of servicesSections) {
|
|
228
|
+
const serviceName = serviceSectionName.split('/')[1];
|
|
229
|
+
const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
|
|
230
|
+
const stagingName = `${app.name}-${serviceName}.env`;
|
|
231
|
+
const targetPath = `${repoPath}/apps/${serviceName}/.env`;
|
|
232
|
+
files.push({
|
|
233
|
+
path: `/home/dev/.env-staging/${stagingName}`,
|
|
234
|
+
content: serviceEnvContent,
|
|
235
|
+
permissions: '0644',
|
|
236
|
+
});
|
|
237
|
+
envFilesToMove.push({ stagingName, targetPath });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
|
|
242
|
+
files.push({
|
|
243
|
+
path: `/home/dev/.env-staging/${app.name}.env`,
|
|
244
|
+
content: appEnvContent,
|
|
245
|
+
permissions: '0644',
|
|
246
|
+
});
|
|
247
|
+
envFilesToMove.push({
|
|
248
|
+
stagingName: `${app.name}.env`,
|
|
249
|
+
targetPath: `${repoPath}/.env`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Generate setup script
|
|
255
|
+
const setupScript = generateSetupScript(resolved, config, envFilesToMove);
|
|
256
|
+
if (setupScript) {
|
|
257
|
+
files.push({
|
|
258
|
+
path: '/home/dev/setup-genbox.sh',
|
|
259
|
+
content: setupScript,
|
|
260
|
+
permissions: '0755',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const postDetails = [];
|
|
264
|
+
if (setupScript) {
|
|
265
|
+
postDetails.push('su - dev -c "/home/dev/setup-genbox.sh"');
|
|
266
|
+
}
|
|
267
|
+
// Build repos
|
|
268
|
+
const repos = {};
|
|
269
|
+
for (const repo of resolved.repos) {
|
|
270
|
+
repos[repo.name] = {
|
|
271
|
+
url: repo.url,
|
|
272
|
+
path: repo.path,
|
|
273
|
+
branch: repo.branch,
|
|
274
|
+
newBranch: repo.newBranch,
|
|
275
|
+
sourceBranch: repo.sourceBranch,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
publicKey,
|
|
280
|
+
services,
|
|
281
|
+
files,
|
|
282
|
+
postDetails,
|
|
283
|
+
repos,
|
|
284
|
+
privateKey,
|
|
285
|
+
gitToken: envVars.GIT_TOKEN,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Generate setup script
|
|
290
|
+
*/
|
|
291
|
+
function generateSetupScript(resolved, config, envFilesToMove = []) {
|
|
292
|
+
const lines = [
|
|
293
|
+
'#!/bin/bash',
|
|
294
|
+
'# Generated by genbox rebuild',
|
|
295
|
+
'set -e',
|
|
296
|
+
'',
|
|
297
|
+
];
|
|
298
|
+
if (envFilesToMove.length > 0) {
|
|
299
|
+
lines.push('# Move .env files from staging to app directories');
|
|
300
|
+
for (const { stagingName, targetPath } of envFilesToMove) {
|
|
301
|
+
lines.push(`if [ -f "/home/dev/.env-staging/${stagingName}" ]; then`);
|
|
302
|
+
lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
|
|
303
|
+
lines.push(` mv "/home/dev/.env-staging/${stagingName}" "${targetPath}"`);
|
|
304
|
+
lines.push(` echo "Moved .env to ${targetPath}"`);
|
|
305
|
+
lines.push('fi');
|
|
306
|
+
}
|
|
307
|
+
lines.push('rm -rf /home/dev/.env-staging 2>/dev/null || true');
|
|
308
|
+
lines.push('');
|
|
309
|
+
}
|
|
310
|
+
if (resolved.repos.length > 0) {
|
|
311
|
+
lines.push(`cd ${resolved.repos[0].path} || exit 1`);
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
lines.push('# Install dependencies');
|
|
315
|
+
lines.push('if [ -f "pnpm-lock.yaml" ]; then');
|
|
316
|
+
lines.push(' echo "Installing dependencies with pnpm..."');
|
|
317
|
+
lines.push(' pnpm install --frozen-lockfile');
|
|
318
|
+
lines.push('elif [ -f "yarn.lock" ]; then');
|
|
319
|
+
lines.push(' echo "Installing dependencies with yarn..."');
|
|
320
|
+
lines.push(' yarn install --frozen-lockfile');
|
|
321
|
+
lines.push('elif [ -f "bun.lockb" ]; then');
|
|
322
|
+
lines.push(' echo "Installing dependencies with bun..."');
|
|
323
|
+
lines.push(' bun install --frozen-lockfile');
|
|
324
|
+
lines.push('elif [ -f "package-lock.json" ]; then');
|
|
325
|
+
lines.push(' echo "Installing dependencies with npm..."');
|
|
326
|
+
lines.push(' npm ci');
|
|
327
|
+
lines.push('fi');
|
|
328
|
+
const hasLocalApi = resolved.apps.some(a => a.name === 'api' || a.type === 'backend');
|
|
329
|
+
if (hasLocalApi) {
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push('echo "Starting Docker services..."');
|
|
332
|
+
lines.push('if [ -f "docker-compose.yml" ] || [ -f "compose.yml" ]; then');
|
|
333
|
+
lines.push(' docker compose up -d');
|
|
334
|
+
lines.push('fi');
|
|
335
|
+
}
|
|
336
|
+
lines.push('');
|
|
337
|
+
lines.push('echo "Setup complete!"');
|
|
338
|
+
return lines.join('\n');
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Prompt for branch options interactively
|
|
342
|
+
*/
|
|
343
|
+
async function promptForBranchOptions(resolved, config) {
|
|
344
|
+
// Get the default branch from config or first repo
|
|
345
|
+
const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
|
|
346
|
+
console.log(chalk_1.default.blue('=== Branch Configuration ==='));
|
|
347
|
+
console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
|
|
348
|
+
console.log('');
|
|
349
|
+
const branchChoice = await prompts.select({
|
|
350
|
+
message: 'Branch option:',
|
|
351
|
+
choices: [
|
|
352
|
+
{
|
|
353
|
+
name: `Use default branch (${defaultBranch})`,
|
|
354
|
+
value: 'default',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'Use a different existing branch',
|
|
358
|
+
value: 'existing',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'Create a new branch',
|
|
362
|
+
value: 'new',
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
default: 'default',
|
|
366
|
+
});
|
|
367
|
+
if (branchChoice === 'default') {
|
|
368
|
+
return resolved;
|
|
369
|
+
}
|
|
370
|
+
if (branchChoice === 'existing') {
|
|
371
|
+
const branchName = await prompts.input({
|
|
372
|
+
message: 'Enter branch name:',
|
|
373
|
+
default: defaultBranch,
|
|
374
|
+
validate: (value) => {
|
|
375
|
+
if (!value.trim())
|
|
376
|
+
return 'Branch name is required';
|
|
377
|
+
return true;
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
...resolved,
|
|
382
|
+
repos: resolved.repos.map(repo => ({
|
|
383
|
+
...repo,
|
|
384
|
+
branch: branchName.trim(),
|
|
385
|
+
newBranch: undefined,
|
|
386
|
+
sourceBranch: undefined,
|
|
387
|
+
})),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (branchChoice === 'new') {
|
|
391
|
+
const newBranchName = await prompts.input({
|
|
392
|
+
message: 'New branch name:',
|
|
393
|
+
validate: (value) => {
|
|
394
|
+
if (!value.trim())
|
|
395
|
+
return 'Branch name is required';
|
|
396
|
+
if (!/^[\w\-./]+$/.test(value))
|
|
397
|
+
return 'Invalid branch name (use letters, numbers, -, _, /, .)';
|
|
398
|
+
return true;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
const sourceBranch = await prompts.input({
|
|
402
|
+
message: 'Create from branch:',
|
|
403
|
+
default: defaultBranch,
|
|
404
|
+
validate: (value) => {
|
|
405
|
+
if (!value.trim())
|
|
406
|
+
return 'Source branch is required';
|
|
407
|
+
return true;
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
...resolved,
|
|
412
|
+
repos: resolved.repos.map(repo => ({
|
|
413
|
+
...repo,
|
|
414
|
+
branch: newBranchName.trim(),
|
|
415
|
+
newBranch: newBranchName.trim(),
|
|
416
|
+
sourceBranch: sourceBranch.trim(),
|
|
417
|
+
})),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return resolved;
|
|
421
|
+
}
|
|
422
|
+
exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
423
|
+
.description('Rebuild an existing Genbox environment with updated configuration')
|
|
424
|
+
.argument('<name>', 'Name of the Genbox to rebuild')
|
|
425
|
+
.option('-p, --profile <profile>', 'Use a predefined profile')
|
|
426
|
+
.option('-b, --branch <branch>', 'Git branch to checkout')
|
|
427
|
+
.option('-nb, --new-branch <name>', 'Create a new branch with this name')
|
|
428
|
+
.option('-sb, --source-branch <branch>', 'Source branch to create new branch from')
|
|
429
|
+
.option('-y, --yes', 'Skip interactive prompts')
|
|
430
|
+
.action(async (name, options) => {
|
|
431
|
+
try {
|
|
432
|
+
// Find existing genbox
|
|
433
|
+
const spinner = (0, ora_1.default)(`Finding Genbox '${name}'...`).start();
|
|
434
|
+
let genbox;
|
|
435
|
+
try {
|
|
436
|
+
genbox = await findGenboxByName(name);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
spinner.fail(chalk_1.default.red(`Failed to find Genbox: ${error.message}`));
|
|
440
|
+
if (error instanceof api_1.AuthenticationError) {
|
|
441
|
+
console.log('');
|
|
442
|
+
console.log(chalk_1.default.yellow(' Please authenticate first:'));
|
|
443
|
+
console.log(chalk_1.default.cyan(' $ genbox login'));
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!genbox) {
|
|
448
|
+
spinner.fail(chalk_1.default.red(`Genbox '${name}' not found`));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
spinner.succeed(`Found Genbox '${name}'`);
|
|
452
|
+
// Load configuration
|
|
453
|
+
const configLoader = new config_loader_1.ConfigLoader();
|
|
454
|
+
const loadResult = await configLoader.load();
|
|
455
|
+
const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
|
|
456
|
+
if (!loadResult.config || configVersion === 'unknown') {
|
|
457
|
+
console.error(chalk_1.default.red('No valid genbox.yaml configuration found'));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const config = loadResult.config;
|
|
461
|
+
const profileResolver = new profile_resolver_1.ProfileResolver(configLoader);
|
|
462
|
+
// Build options for resolving
|
|
463
|
+
const createOptions = {
|
|
464
|
+
name,
|
|
465
|
+
profile: options.profile,
|
|
466
|
+
branch: options.branch,
|
|
467
|
+
newBranch: options.newBranch,
|
|
468
|
+
sourceBranch: options.sourceBranch,
|
|
469
|
+
yes: options.yes,
|
|
470
|
+
};
|
|
471
|
+
console.log(chalk_1.default.blue('Resolving configuration...'));
|
|
472
|
+
console.log('');
|
|
473
|
+
let resolved = await profileResolver.resolve(config, createOptions);
|
|
474
|
+
// Interactive branch selection if no branch was specified
|
|
475
|
+
if (!options.branch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
|
|
476
|
+
resolved = await promptForBranchOptions(resolved, config);
|
|
477
|
+
}
|
|
478
|
+
// Display what will be rebuilt
|
|
479
|
+
console.log(chalk_1.default.bold('Rebuild Configuration:'));
|
|
480
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
481
|
+
console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
|
|
482
|
+
console.log(` ${chalk_1.default.bold('Project:')} ${resolved.project.name}`);
|
|
483
|
+
if (resolved.profile) {
|
|
484
|
+
console.log(` ${chalk_1.default.bold('Profile:')} ${resolved.profile}`);
|
|
485
|
+
}
|
|
486
|
+
console.log('');
|
|
487
|
+
console.log(` ${chalk_1.default.bold('Apps:')}`);
|
|
488
|
+
for (const app of resolved.apps) {
|
|
489
|
+
console.log(` - ${app.name} (${app.type})`);
|
|
490
|
+
}
|
|
491
|
+
// Display branch info
|
|
492
|
+
if (resolved.repos.length > 0) {
|
|
493
|
+
console.log('');
|
|
494
|
+
console.log(` ${chalk_1.default.bold('Repos:')}`);
|
|
495
|
+
for (const repo of resolved.repos) {
|
|
496
|
+
const branchInfo = repo.newBranch
|
|
497
|
+
? `${chalk_1.default.cyan(repo.newBranch)} ${chalk_1.default.dim(`(new, from ${repo.sourceBranch || 'main'})`)}`
|
|
498
|
+
: repo.branch
|
|
499
|
+
? chalk_1.default.cyan(repo.branch)
|
|
500
|
+
: chalk_1.default.dim('default');
|
|
501
|
+
console.log(` - ${repo.name}: ${branchInfo}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
505
|
+
// Confirm rebuild
|
|
506
|
+
if (!options.yes) {
|
|
507
|
+
console.log('');
|
|
508
|
+
console.log(chalk_1.default.yellow('Warning: Rebuild will reinstall the OS and rerun setup.'));
|
|
509
|
+
console.log(chalk_1.default.yellow('All unsaved work on the server will be lost.'));
|
|
510
|
+
console.log('');
|
|
511
|
+
const confirm = await prompts.confirm({
|
|
512
|
+
message: `Rebuild genbox '${name}'?`,
|
|
513
|
+
default: false,
|
|
514
|
+
});
|
|
515
|
+
if (!confirm) {
|
|
516
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Get SSH keys
|
|
521
|
+
const publicKey = getPublicSshKey();
|
|
522
|
+
// Check if SSH auth is needed for git
|
|
523
|
+
let privateKeyContent;
|
|
524
|
+
const v3Config = config;
|
|
525
|
+
const usesSSH = v3Config.git_auth?.method === 'ssh' ||
|
|
526
|
+
Object.values(config.repos || {}).some(r => r.auth === 'ssh');
|
|
527
|
+
if (usesSSH && !options.yes) {
|
|
528
|
+
const injectKey = await prompts.confirm({
|
|
529
|
+
message: 'Inject SSH private key for git cloning?',
|
|
530
|
+
default: true,
|
|
531
|
+
});
|
|
532
|
+
if (injectKey) {
|
|
533
|
+
privateKeyContent = getPrivateSshKey();
|
|
534
|
+
if (privateKeyContent) {
|
|
535
|
+
console.log(chalk_1.default.dim(' Using local SSH private key'));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Build payload
|
|
540
|
+
const payload = buildRebuildPayload(resolved, config, publicKey, privateKeyContent, configLoader);
|
|
541
|
+
// Execute rebuild
|
|
542
|
+
const rebuildSpinner = (0, ora_1.default)(`Rebuilding Genbox '${name}'...`).start();
|
|
543
|
+
try {
|
|
544
|
+
await rebuildGenbox(genbox._id, payload);
|
|
545
|
+
rebuildSpinner.succeed(chalk_1.default.green(`Genbox '${name}' rebuild initiated!`));
|
|
546
|
+
console.log('');
|
|
547
|
+
console.log(chalk_1.default.dim('Server is rebuilding. This may take a few minutes.'));
|
|
548
|
+
console.log(chalk_1.default.dim('SSH connection will be temporarily unavailable.'));
|
|
549
|
+
console.log('');
|
|
550
|
+
console.log(`Run ${chalk_1.default.cyan(`genbox status ${name}`)} to check progress.`);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
rebuildSpinner.fail(chalk_1.default.red(`Failed to rebuild: ${error.message}`));
|
|
554
|
+
if (error instanceof api_1.AuthenticationError) {
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log(chalk_1.default.yellow(' Please authenticate first:'));
|
|
557
|
+
console.log(chalk_1.default.cyan(' $ genbox login'));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
563
|
+
console.log('');
|
|
564
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (error instanceof api_1.AuthenticationError) {
|
|
568
|
+
console.log(chalk_1.default.red('Not logged in'));
|
|
569
|
+
console.log('');
|
|
570
|
+
console.log(chalk_1.default.yellow(' Please authenticate first:'));
|
|
571
|
+
console.log(chalk_1.default.cyan(' $ genbox login'));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
575
|
+
}
|
|
576
|
+
});
|
package/dist/commands/scan.js
CHANGED
|
@@ -656,10 +656,11 @@ function showSummary(detected) {
|
|
|
656
656
|
console.log(` ${chalk_1.default.cyan(infra.name)}: ${infra.type} (${infra.image})`);
|
|
657
657
|
}
|
|
658
658
|
}
|
|
659
|
-
// Git (root level)
|
|
659
|
+
// Git (root level) - show branch prominently
|
|
660
660
|
if (detected.git?.remote) {
|
|
661
661
|
console.log(`\n Git: ${detected.git.provider || 'git'} (${detected.git.type})`);
|
|
662
|
-
console.log(chalk_1.default.dim(
|
|
662
|
+
console.log(` Remote: ${chalk_1.default.dim(detected.git.remote)}`);
|
|
663
|
+
console.log(` Branch: ${chalk_1.default.cyan(detected.git.branch || 'main')} ${chalk_1.default.dim('← default branch for new environments')}`);
|
|
663
664
|
}
|
|
664
665
|
// Per-app git repos (for multi-repo workspaces)
|
|
665
666
|
const appsWithGit = Object.entries(detected.apps).filter(([, app]) => app.git);
|
|
@@ -669,6 +670,7 @@ function showSummary(detected) {
|
|
|
669
670
|
const git = app.git;
|
|
670
671
|
console.log(` ${chalk_1.default.cyan(name)}: ${git.provider} (${git.type})`);
|
|
671
672
|
console.log(chalk_1.default.dim(` ${git.remote}`));
|
|
673
|
+
console.log(` Branch: ${chalk_1.default.cyan(git.branch || 'main')}`);
|
|
672
674
|
}
|
|
673
675
|
}
|
|
674
676
|
// Service URLs (for staging URL configuration)
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const resolve_1 = require("./commands/resolve");
|
|
|
30
30
|
const validate_1 = require("./commands/validate");
|
|
31
31
|
const migrate_1 = require("./commands/migrate");
|
|
32
32
|
const ssh_setup_1 = require("./commands/ssh-setup");
|
|
33
|
+
const rebuild_1 = require("./commands/rebuild");
|
|
33
34
|
program
|
|
34
35
|
.addCommand(init_1.initCommand)
|
|
35
36
|
.addCommand(create_1.createCommand)
|
|
@@ -52,5 +53,6 @@ program
|
|
|
52
53
|
.addCommand(validate_1.validateCommand)
|
|
53
54
|
.addCommand(migrate_1.migrateCommand)
|
|
54
55
|
.addCommand(migrate_1.deprecationsCommand)
|
|
55
|
-
.addCommand(ssh_setup_1.sshSetupCommand)
|
|
56
|
+
.addCommand(ssh_setup_1.sshSetupCommand)
|
|
57
|
+
.addCommand(rebuild_1.rebuildCommand);
|
|
56
58
|
program.parse(process.argv);
|
package/dist/profile-resolver.js
CHANGED
|
@@ -114,7 +114,7 @@ class ProfileResolver {
|
|
|
114
114
|
apps,
|
|
115
115
|
infrastructure,
|
|
116
116
|
database,
|
|
117
|
-
repos: this.resolveRepos(config, apps),
|
|
117
|
+
repos: this.resolveRepos(config, apps, options),
|
|
118
118
|
env: this.resolveEnvVars(config, apps, infrastructure, database, (0, config_loader_1.getProfileConnection)(profile)),
|
|
119
119
|
hooks: config.hooks || {},
|
|
120
120
|
profile: options.profile,
|
|
@@ -430,9 +430,13 @@ class ProfileResolver {
|
|
|
430
430
|
/**
|
|
431
431
|
* Resolve repositories to clone
|
|
432
432
|
*/
|
|
433
|
-
resolveRepos(config, apps) {
|
|
433
|
+
resolveRepos(config, apps, options) {
|
|
434
434
|
const repos = [];
|
|
435
435
|
const seen = new Set();
|
|
436
|
+
// Determine the effective branch to use
|
|
437
|
+
// Priority: newBranch > branch (CLI) > defaults.branch (config) > repo.branch
|
|
438
|
+
const effectiveBranch = options.newBranch || options.branch;
|
|
439
|
+
const defaultBranch = config.defaults?.branch;
|
|
436
440
|
for (const app of apps) {
|
|
437
441
|
const appConfig = config.apps[app.name];
|
|
438
442
|
// Check if app has specific repo field
|
|
@@ -443,7 +447,10 @@ class ProfileResolver {
|
|
|
443
447
|
name: appConfig.repo,
|
|
444
448
|
url: repoConfig.url,
|
|
445
449
|
path: repoConfig.path,
|
|
446
|
-
branch: repoConfig.branch,
|
|
450
|
+
branch: effectiveBranch || repoConfig.branch || defaultBranch,
|
|
451
|
+
// Track branch creation info for new branches
|
|
452
|
+
newBranch: options.newBranch,
|
|
453
|
+
sourceBranch: options.newBranch ? (options.sourceBranch || repoConfig.branch || defaultBranch || 'main') : undefined,
|
|
447
454
|
});
|
|
448
455
|
seen.add(repoConfig.url);
|
|
449
456
|
}
|
|
@@ -456,7 +463,9 @@ class ProfileResolver {
|
|
|
456
463
|
name: app.name,
|
|
457
464
|
url: repoConfig.url,
|
|
458
465
|
path: repoConfig.path,
|
|
459
|
-
branch: repoConfig.branch,
|
|
466
|
+
branch: effectiveBranch || repoConfig.branch || defaultBranch,
|
|
467
|
+
newBranch: options.newBranch,
|
|
468
|
+
sourceBranch: options.newBranch ? (options.sourceBranch || repoConfig.branch || defaultBranch || 'main') : undefined,
|
|
460
469
|
});
|
|
461
470
|
seen.add(repoConfig.url);
|
|
462
471
|
}
|
|
@@ -470,7 +479,9 @@ class ProfileResolver {
|
|
|
470
479
|
name: mainRepo[0],
|
|
471
480
|
url: mainRepo[1].url,
|
|
472
481
|
path: mainRepo[1].path,
|
|
473
|
-
branch: mainRepo[1].branch,
|
|
482
|
+
branch: effectiveBranch || mainRepo[1].branch || defaultBranch,
|
|
483
|
+
newBranch: options.newBranch,
|
|
484
|
+
sourceBranch: options.newBranch ? (options.sourceBranch || mainRepo[1].branch || defaultBranch || 'main') : undefined,
|
|
474
485
|
});
|
|
475
486
|
}
|
|
476
487
|
}
|