genbox 1.0.63 → 1.0.65
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 +14 -294
- package/dist/commands/rebuild.js +501 -237
- package/dist/utils/branch-prompt.js +231 -0
- package/dist/utils/env-parser.js +127 -0
- package/dist/utils/git-utils.js +49 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/ssh-keys.js +98 -0
- package/package.json +1 -1
package/dist/commands/rebuild.js
CHANGED
|
@@ -44,130 +44,344 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
44
44
|
const fs = __importStar(require("fs"));
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
46
|
const os = __importStar(require("os"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
47
48
|
const config_loader_1 = require("../config-loader");
|
|
48
49
|
const profile_resolver_1 = require("../profile-resolver");
|
|
49
50
|
const api_1 = require("../api");
|
|
50
51
|
const genbox_selector_1 = require("../genbox-selector");
|
|
51
52
|
const schema_v4_1 = require("../schema-v4");
|
|
52
53
|
const db_utils_1 = require("../db-utils");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
54
|
+
const utils_1 = require("../utils");
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// SSH Utilities for Soft Rebuild
|
|
57
|
+
// ============================================================================
|
|
58
|
+
function sshExec(ip, keyPath, command, timeoutSecs = 30) {
|
|
59
|
+
const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=${timeoutSecs}`;
|
|
60
|
+
try {
|
|
61
|
+
const result = (0, child_process_1.execSync)(`ssh ${sshOpts} dev@${ip} "${command.replace(/"/g, '\\"')}"`, {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
timeout: (timeoutSecs + 10) * 1000,
|
|
64
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
+
});
|
|
66
|
+
return { success: true, output: result.trim() };
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const potentialKeys = [
|
|
71
|
-
path.join(home, '.ssh', 'id_ed25519'),
|
|
72
|
-
path.join(home, '.ssh', 'id_rsa'),
|
|
73
|
-
];
|
|
74
|
-
for (const keyPath of potentialKeys) {
|
|
75
|
-
if (fs.existsSync(keyPath)) {
|
|
76
|
-
return fs.readFileSync(keyPath, 'utf-8');
|
|
77
|
-
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const stderr = error.stderr?.toString().trim() || '';
|
|
70
|
+
const stdout = error.stdout?.toString().trim() || '';
|
|
71
|
+
return { success: false, output: stdout, error: stderr || error.message };
|
|
78
72
|
}
|
|
79
|
-
return undefined;
|
|
80
73
|
}
|
|
81
|
-
async function
|
|
82
|
-
return (
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
async function sshExecStream(ip, keyPath, command, options = {}) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const sshArgs = [
|
|
77
|
+
'-i', keyPath,
|
|
78
|
+
'-o', 'IdentitiesOnly=yes',
|
|
79
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
80
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
81
|
+
'-o', 'LogLevel=ERROR',
|
|
82
|
+
'-o', `ConnectTimeout=${options.timeoutSecs || 30}`,
|
|
83
|
+
`dev@${ip}`,
|
|
84
|
+
command,
|
|
85
|
+
];
|
|
86
|
+
const child = (0, child_process_1.spawn)('ssh', sshArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
87
|
+
child.stdout?.on('data', (data) => {
|
|
88
|
+
const lines = data.toString().split('\n');
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (line.trim() && options.onStdout) {
|
|
91
|
+
options.onStdout(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
child.stderr?.on('data', (data) => {
|
|
96
|
+
const lines = data.toString().split('\n');
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (line.trim() && options.onStderr) {
|
|
99
|
+
options.onStderr(line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
child.on('close', (code) => {
|
|
104
|
+
resolve({ success: code === 0, code: code || 0 });
|
|
105
|
+
});
|
|
106
|
+
child.on('error', () => {
|
|
107
|
+
resolve({ success: false, code: -1 });
|
|
108
|
+
});
|
|
85
109
|
});
|
|
86
110
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
async function scpUpload(localPath, ip, remotePath, keyPath) {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const scpArgs = [
|
|
114
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
115
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
116
|
+
'-o', 'ConnectTimeout=30',
|
|
117
|
+
'-i', keyPath,
|
|
118
|
+
localPath,
|
|
119
|
+
`dev@${ip}:${remotePath}`,
|
|
120
|
+
];
|
|
121
|
+
const child = (0, child_process_1.spawn)('scp', scpArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
122
|
+
let stderr = '';
|
|
123
|
+
child.stderr?.on('data', (data) => {
|
|
124
|
+
stderr += data.toString();
|
|
125
|
+
});
|
|
126
|
+
child.on('close', (code) => {
|
|
127
|
+
if (code === 0) {
|
|
128
|
+
resolve({ success: true });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
resolve({ success: false, error: stderr.trim() || 'SCP failed' });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
child.on('error', (err) => {
|
|
135
|
+
resolve({ success: false, error: err.message });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
111
138
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const serviceNames = new Set();
|
|
119
|
-
for (const key of Object.keys(envVarsFromFile)) {
|
|
120
|
-
const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
|
|
121
|
-
if (match) {
|
|
122
|
-
serviceNames.add(match[2]);
|
|
139
|
+
async function runSoftRebuild(options) {
|
|
140
|
+
const { genbox, resolved, config, keyPath, envFiles, snapshotId, snapshotS3Key, gitToken, onStep, onLog } = options;
|
|
141
|
+
const ip = genbox.ipAddress;
|
|
142
|
+
const log = (line, type) => {
|
|
143
|
+
if (onLog) {
|
|
144
|
+
onLog(line, type);
|
|
123
145
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
// Step 1: Stop all services
|
|
149
|
+
onStep?.('Stopping services...');
|
|
150
|
+
log('Stopping PM2 processes...', 'dim');
|
|
151
|
+
await sshExec(ip, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 kill 2>/dev/null || true', 30);
|
|
152
|
+
// Find and stop docker compose in repo directories
|
|
153
|
+
for (const repo of resolved.repos) {
|
|
154
|
+
log(`Stopping Docker Compose in ${repo.path}...`, 'dim');
|
|
155
|
+
await sshExec(ip, keyPath, `cd ${repo.path} 2>/dev/null && docker compose down 2>/dev/null || true`, 60);
|
|
156
|
+
}
|
|
157
|
+
// Step 2: Clean up repo directories
|
|
158
|
+
onStep?.('Cleaning up repositories...');
|
|
159
|
+
for (const repo of resolved.repos) {
|
|
160
|
+
log(`Removing ${repo.path}...`, 'dim');
|
|
161
|
+
const cleanResult = await sshExec(ip, keyPath, `rm -rf ${repo.path}`, 30);
|
|
162
|
+
if (!cleanResult.success) {
|
|
163
|
+
log(`Warning: Failed to clean ${repo.path}: ${cleanResult.error}`, 'error');
|
|
164
|
+
}
|
|
131
165
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
166
|
+
// Step 3: Clone repositories with correct branches
|
|
167
|
+
onStep?.('Cloning repositories...');
|
|
168
|
+
for (const repo of resolved.repos) {
|
|
169
|
+
const repoUrl = gitToken && repo.url.startsWith('https://')
|
|
170
|
+
? repo.url.replace('https://', `https://${gitToken}@`)
|
|
171
|
+
: repo.url;
|
|
172
|
+
// Determine branch to checkout
|
|
173
|
+
const sourceBranch = repo.sourceBranch || repo.branch || 'main';
|
|
174
|
+
const targetBranch = repo.newBranch || repo.branch || 'main';
|
|
175
|
+
log(`Cloning ${repo.name} (branch: ${sourceBranch})...`, 'info');
|
|
176
|
+
// Create parent directory if needed
|
|
177
|
+
const parentDir = path.dirname(repo.path);
|
|
178
|
+
await sshExec(ip, keyPath, `mkdir -p ${parentDir}`, 10);
|
|
179
|
+
// Clone the repo
|
|
180
|
+
const cloneCmd = `git clone --depth=1 --single-branch --branch ${sourceBranch} '${repoUrl}' ${repo.path}`;
|
|
181
|
+
const cloneResult = await sshExecStream(ip, keyPath, cloneCmd, {
|
|
182
|
+
onStdout: (line) => log(line, 'dim'),
|
|
183
|
+
onStderr: (line) => {
|
|
184
|
+
// Git outputs progress to stderr, filter out non-errors
|
|
185
|
+
if (!line.includes('Cloning into') && !line.includes('Receiving objects') && !line.includes('Resolving deltas')) {
|
|
186
|
+
log(line, 'dim');
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
timeoutSecs: 120,
|
|
190
|
+
});
|
|
191
|
+
if (!cloneResult.success) {
|
|
192
|
+
return { success: false, error: `Failed to clone ${repo.name}` };
|
|
193
|
+
}
|
|
194
|
+
// Create new branch if needed
|
|
195
|
+
if (repo.newBranch && repo.newBranch !== sourceBranch) {
|
|
196
|
+
log(`Creating new branch: ${repo.newBranch}`, 'info');
|
|
197
|
+
const branchResult = await sshExec(ip, keyPath, `cd ${repo.path} && git checkout -b ${repo.newBranch}`, 10);
|
|
198
|
+
if (!branchResult.success) {
|
|
199
|
+
log(`Warning: Failed to create branch ${repo.newBranch}: ${branchResult.error}`, 'error');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
log(`✓ Cloned ${repo.name}`, 'success');
|
|
203
|
+
}
|
|
204
|
+
// Step 4: Upload and place .env files
|
|
205
|
+
if (envFiles.length > 0) {
|
|
206
|
+
onStep?.('Setting up environment files...');
|
|
207
|
+
// Create staging directory
|
|
208
|
+
await sshExec(ip, keyPath, 'mkdir -p ~/.env-staging', 10);
|
|
209
|
+
for (const envFile of envFiles) {
|
|
210
|
+
log(`Writing ${envFile.stagingName}...`, 'dim');
|
|
211
|
+
// Write env file content to a temp file locally, then SCP it
|
|
212
|
+
const tempLocalPath = path.join(os.tmpdir(), `genbox-env-${Date.now()}-${envFile.stagingName}`);
|
|
213
|
+
fs.writeFileSync(tempLocalPath, envFile.content);
|
|
214
|
+
const scpResult = await scpUpload(tempLocalPath, ip, `~/.env-staging/${envFile.stagingName}`, keyPath);
|
|
215
|
+
fs.unlinkSync(tempLocalPath);
|
|
216
|
+
if (!scpResult.success) {
|
|
217
|
+
log(`Warning: Failed to upload ${envFile.stagingName}: ${scpResult.error}`, 'error');
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Move to final location
|
|
221
|
+
const parentDir = path.dirname(envFile.remotePath);
|
|
222
|
+
await sshExec(ip, keyPath, `mkdir -p ${parentDir}`, 10);
|
|
223
|
+
const moveResult = await sshExec(ip, keyPath, `mv ~/.env-staging/${envFile.stagingName} ${envFile.remotePath}`, 10);
|
|
224
|
+
if (!moveResult.success) {
|
|
225
|
+
log(`Warning: Failed to move ${envFile.stagingName} to ${envFile.remotePath}`, 'error');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
139
228
|
}
|
|
229
|
+
// Step 5: Install dependencies
|
|
230
|
+
onStep?.('Installing dependencies...');
|
|
231
|
+
for (const repo of resolved.repos) {
|
|
232
|
+
// Check for package.json
|
|
233
|
+
const hasPackageJson = await sshExec(ip, keyPath, `test -f ${repo.path}/package.json && echo yes || echo no`, 10);
|
|
234
|
+
if (hasPackageJson.output === 'yes') {
|
|
235
|
+
log(`Installing npm dependencies in ${repo.name}...`, 'info');
|
|
236
|
+
// Determine package manager (prefer pnpm)
|
|
237
|
+
const hasPnpmLock = await sshExec(ip, keyPath, `test -f ${repo.path}/pnpm-lock.yaml && echo yes || echo no`, 10);
|
|
238
|
+
const installCmd = hasPnpmLock.output === 'yes'
|
|
239
|
+
? `cd ${repo.path} && source ~/.nvm/nvm.sh && pnpm install --frozen-lockfile`
|
|
240
|
+
: `cd ${repo.path} && source ~/.nvm/nvm.sh && npm install`;
|
|
241
|
+
const installResult = await sshExecStream(ip, keyPath, installCmd, {
|
|
242
|
+
onStdout: (line) => {
|
|
243
|
+
if (line.includes('Packages:') || line.includes('added') || line.includes('Done')) {
|
|
244
|
+
log(line, 'dim');
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
timeoutSecs: 300,
|
|
248
|
+
});
|
|
249
|
+
if (!installResult.success) {
|
|
250
|
+
log(`Warning: npm install failed in ${repo.name}`, 'error');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
log(`✓ Dependencies installed in ${repo.name}`, 'success');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Step 6: Start Docker Compose services
|
|
258
|
+
onStep?.('Starting Docker services...');
|
|
259
|
+
for (const repo of resolved.repos) {
|
|
260
|
+
const hasDockerCompose = await sshExec(ip, keyPath, `test -f ${repo.path}/docker-compose.yml -o -f ${repo.path}/docker-compose.yaml -o -f ${repo.path}/compose.yaml && echo yes || echo no`, 10);
|
|
261
|
+
if (hasDockerCompose.output === 'yes') {
|
|
262
|
+
log(`Starting Docker Compose in ${repo.name}...`, 'info');
|
|
263
|
+
const composeResult = await sshExecStream(ip, keyPath, `cd ${repo.path} && docker compose up -d`, {
|
|
264
|
+
onStdout: (line) => log(line, 'dim'),
|
|
265
|
+
timeoutSecs: 180,
|
|
266
|
+
});
|
|
267
|
+
if (!composeResult.success) {
|
|
268
|
+
log(`Warning: Docker Compose failed in ${repo.name}`, 'error');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
log(`✓ Docker services started in ${repo.name}`, 'success');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Step 7: Restore database if snapshot provided
|
|
276
|
+
if (snapshotId && snapshotS3Key) {
|
|
277
|
+
onStep?.('Restoring database...');
|
|
278
|
+
log('Fetching database snapshot...', 'info');
|
|
279
|
+
try {
|
|
280
|
+
// Get download URL from API
|
|
281
|
+
const downloadInfo = await (0, api_1.getSnapshotDownloadUrl)(snapshotId);
|
|
282
|
+
// Download snapshot on the server
|
|
283
|
+
log('Downloading snapshot to server...', 'dim');
|
|
284
|
+
const downloadCmd = `curl -sL -o /tmp/db-snapshot.gz '${downloadInfo.downloadUrl}'`;
|
|
285
|
+
const downloadResult = await sshExecStream(ip, keyPath, downloadCmd, {
|
|
286
|
+
timeoutSecs: 300,
|
|
287
|
+
});
|
|
288
|
+
if (!downloadResult.success) {
|
|
289
|
+
log('Warning: Failed to download database snapshot', 'error');
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// Wait for MongoDB to be ready
|
|
293
|
+
log('Waiting for MongoDB...', 'dim');
|
|
294
|
+
await sshExec(ip, keyPath, `
|
|
295
|
+
for i in {1..30}; do
|
|
296
|
+
if docker ps --format '{{.Names}}' | grep -q mongodb; then
|
|
297
|
+
container=$(docker ps --format '{{.Names}}' | grep mongodb | head -1)
|
|
298
|
+
if docker exec $container mongosh --quiet --eval "db.runCommand({ping:1})" 2>/dev/null; then
|
|
299
|
+
break
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
sleep 2
|
|
303
|
+
done
|
|
304
|
+
`, 120);
|
|
305
|
+
// Get MongoDB container name and port
|
|
306
|
+
const containerResult = await sshExec(ip, keyPath, "docker ps --format '{{.Names}}' | grep -i mongo | head -1", 10);
|
|
307
|
+
const mongoContainer = containerResult.output || 'mongodb';
|
|
308
|
+
// Get the MongoDB port
|
|
309
|
+
const portResult = await sshExec(ip, keyPath, `docker port ${mongoContainer} 27017 2>/dev/null | cut -d: -f2 || echo 27017`, 10);
|
|
310
|
+
const mongoPort = portResult.output.trim() || '27017';
|
|
311
|
+
// Restore the database
|
|
312
|
+
log('Restoring database from snapshot...', 'info');
|
|
313
|
+
const restoreCmd = `mongorestore --host localhost --port ${mongoPort} --archive=/tmp/db-snapshot.gz --gzip --drop`;
|
|
314
|
+
const restoreResult = await sshExecStream(ip, keyPath, restoreCmd, {
|
|
315
|
+
onStdout: (line) => log(line, 'dim'),
|
|
316
|
+
timeoutSecs: 300,
|
|
317
|
+
});
|
|
318
|
+
if (!restoreResult.success) {
|
|
319
|
+
log('Warning: Database restore may have failed (non-zero exit)', 'error');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
log('✓ Database restored', 'success');
|
|
323
|
+
}
|
|
324
|
+
// Cleanup
|
|
325
|
+
await sshExec(ip, keyPath, 'rm -f /tmp/db-snapshot.gz', 10);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
log(`Warning: Database restore failed: ${error.message}`, 'error');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Step 8: Start PM2 services
|
|
333
|
+
onStep?.('Starting application services...');
|
|
334
|
+
for (const app of resolved.apps) {
|
|
335
|
+
const appConfig = config.apps[app.name];
|
|
336
|
+
if (!appConfig)
|
|
337
|
+
continue;
|
|
338
|
+
// Find the repo path for this app
|
|
339
|
+
const appPath = appConfig.path || app.name;
|
|
340
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
341
|
+
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : null);
|
|
342
|
+
if (!repoPath)
|
|
343
|
+
continue;
|
|
344
|
+
// Check for PM2 ecosystem file or package.json scripts
|
|
345
|
+
const hasPm2Config = await sshExec(ip, keyPath, `test -f ${repoPath}/ecosystem.config.js -o -f ${repoPath}/ecosystem.config.cjs && echo yes || echo no`, 10);
|
|
346
|
+
if (hasPm2Config.output === 'yes') {
|
|
347
|
+
log(`Starting ${app.name} with PM2...`, 'info');
|
|
348
|
+
const pm2Result = await sshExecStream(ip, keyPath, `cd ${repoPath} && source ~/.nvm/nvm.sh && pm2 start ecosystem.config.js 2>/dev/null || pm2 start ecosystem.config.cjs`, {
|
|
349
|
+
onStdout: (line) => log(line, 'dim'),
|
|
350
|
+
timeoutSecs: 60,
|
|
351
|
+
});
|
|
352
|
+
if (pm2Result.success) {
|
|
353
|
+
log(`✓ Started ${app.name}`, 'success');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Check for start script in package.json
|
|
358
|
+
const hasStartScript = await sshExec(ip, keyPath, `grep -q '"start"' ${repoPath}/package.json 2>/dev/null && echo yes || echo no`, 10);
|
|
359
|
+
if (hasStartScript.output === 'yes') {
|
|
360
|
+
log(`Starting ${app.name} with PM2 (npm start)...`, 'info');
|
|
361
|
+
const pm2Result = await sshExecStream(ip, keyPath, `cd ${repoPath} && source ~/.nvm/nvm.sh && pm2 start npm --name ${app.name} -- start`, {
|
|
362
|
+
onStdout: (line) => log(line, 'dim'),
|
|
363
|
+
timeoutSecs: 60,
|
|
364
|
+
});
|
|
365
|
+
if (pm2Result.success) {
|
|
366
|
+
log(`✓ Started ${app.name}`, 'success');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Step 9: Save PM2 process list
|
|
372
|
+
log('Saving PM2 process list...', 'dim');
|
|
373
|
+
await sshExec(ip, keyPath, 'source ~/.nvm/nvm.sh && pm2 save 2>/dev/null || true', 10);
|
|
374
|
+
return { success: true };
|
|
140
375
|
}
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Build env content for a specific app
|
|
145
|
-
*/
|
|
146
|
-
function buildAppEnvContent(sections, appName, serviceUrlMap) {
|
|
147
|
-
const parts = [];
|
|
148
|
-
const globalSection = sections.get('GLOBAL');
|
|
149
|
-
if (globalSection) {
|
|
150
|
-
parts.push(globalSection);
|
|
151
|
-
}
|
|
152
|
-
const appSection = sections.get(appName);
|
|
153
|
-
if (appSection) {
|
|
154
|
-
parts.push(appSection);
|
|
155
|
-
}
|
|
156
|
-
let envContent = parts.join('\n\n');
|
|
157
|
-
for (const [varName, value] of Object.entries(serviceUrlMap)) {
|
|
158
|
-
const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
|
|
159
|
-
envContent = envContent.replace(pattern, value);
|
|
376
|
+
catch (error) {
|
|
377
|
+
return { success: false, error: error.message };
|
|
160
378
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
.join('\n')
|
|
168
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
169
|
-
.trim();
|
|
170
|
-
return envContent;
|
|
379
|
+
}
|
|
380
|
+
async function rebuildGenbox(id, payload) {
|
|
381
|
+
return (0, api_1.fetchApi)(`/genboxes/${id}/rebuild`, {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
body: JSON.stringify(payload),
|
|
384
|
+
});
|
|
171
385
|
}
|
|
172
386
|
/**
|
|
173
387
|
* Build rebuild payload from resolved config
|
|
@@ -188,26 +402,15 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
188
402
|
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
189
403
|
if (fs.existsSync(envGenboxPath)) {
|
|
190
404
|
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
191
|
-
const sections = parseEnvGenboxSections(rawEnvContent);
|
|
405
|
+
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
192
406
|
const globalSection = sections.get('GLOBAL') || '';
|
|
193
|
-
const envVarsFromFile =
|
|
194
|
-
for (const line of globalSection.split('\n')) {
|
|
195
|
-
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
196
|
-
if (match) {
|
|
197
|
-
let value = match[2].trim();
|
|
198
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
199
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
200
|
-
value = value.slice(1, -1);
|
|
201
|
-
}
|
|
202
|
-
envVarsFromFile[match[1]] = value;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
407
|
+
const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
|
|
205
408
|
let connectTo;
|
|
206
409
|
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
207
410
|
const profile = config.profiles[resolved.profile];
|
|
208
411
|
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
209
412
|
}
|
|
210
|
-
const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
|
|
413
|
+
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
211
414
|
if (connectTo && Object.keys(serviceUrlMap).length > 0) {
|
|
212
415
|
console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
|
|
213
416
|
}
|
|
@@ -219,7 +422,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
219
422
|
if (servicesSections.length > 0) {
|
|
220
423
|
for (const serviceSectionName of servicesSections) {
|
|
221
424
|
const serviceName = serviceSectionName.split('/')[1];
|
|
222
|
-
const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
|
|
425
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
223
426
|
const stagingName = `${app.name}-${serviceName}.env`;
|
|
224
427
|
const targetPath = `${repoPath}/apps/${serviceName}/.env`;
|
|
225
428
|
files.push({
|
|
@@ -231,7 +434,7 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
231
434
|
}
|
|
232
435
|
}
|
|
233
436
|
else {
|
|
234
|
-
const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
|
|
437
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
235
438
|
files.push({
|
|
236
439
|
path: `/home/dev/.env-staging/${app.name}.env`,
|
|
237
440
|
content: appEnvContent,
|
|
@@ -253,8 +456,6 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
253
456
|
// Build repos
|
|
254
457
|
const repos = {};
|
|
255
458
|
for (const repo of resolved.repos) {
|
|
256
|
-
// DEBUG: Log repo values
|
|
257
|
-
console.log(chalk_1.default.dim(` [DEBUG] Repo ${repo.name}: branch=${repo.branch}, newBranch=${repo.newBranch}, sourceBranch=${repo.sourceBranch}`));
|
|
258
459
|
repos[repo.name] = {
|
|
259
460
|
url: repo.url,
|
|
260
461
|
path: repo.path,
|
|
@@ -263,6 +464,8 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
263
464
|
sourceBranch: repo.sourceBranch,
|
|
264
465
|
};
|
|
265
466
|
}
|
|
467
|
+
// Get local git config for commits
|
|
468
|
+
const gitConfig = (0, utils_1.getGitConfig)();
|
|
266
469
|
return {
|
|
267
470
|
publicKey,
|
|
268
471
|
services,
|
|
@@ -271,110 +474,10 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
|
|
|
271
474
|
repos,
|
|
272
475
|
privateKey,
|
|
273
476
|
gitToken: envVars.GIT_TOKEN,
|
|
477
|
+
gitUserName: gitConfig.userName,
|
|
478
|
+
gitUserEmail: gitConfig.userEmail,
|
|
274
479
|
};
|
|
275
480
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Validate git configuration and warn about missing credentials
|
|
278
|
-
*/
|
|
279
|
-
function validateGitCredentials(repos, gitToken, privateKey) {
|
|
280
|
-
const warnings = [];
|
|
281
|
-
const errors = [];
|
|
282
|
-
for (const repo of repos) {
|
|
283
|
-
const isHttps = repo.url.startsWith('https://');
|
|
284
|
-
const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
|
|
285
|
-
const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
|
|
286
|
-
if (isHttps && !gitToken && isPrivateRepo) {
|
|
287
|
-
warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
|
|
288
|
-
warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
|
|
289
|
-
}
|
|
290
|
-
if (isSsh && !privateKey) {
|
|
291
|
-
warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return { warnings, errors };
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Prompt for branch options interactively
|
|
298
|
-
*/
|
|
299
|
-
async function promptForBranchOptions(resolved, config) {
|
|
300
|
-
// Get the default branch from config or first repo
|
|
301
|
-
const defaultBranch = config.defaults?.branch || resolved.repos[0]?.branch || 'main';
|
|
302
|
-
console.log(chalk_1.default.blue('=== Branch Configuration ==='));
|
|
303
|
-
console.log(chalk_1.default.dim(`Default branch: ${defaultBranch}`));
|
|
304
|
-
console.log('');
|
|
305
|
-
const branchChoice = await prompts.select({
|
|
306
|
-
message: 'Branch option:',
|
|
307
|
-
choices: [
|
|
308
|
-
{
|
|
309
|
-
name: `Use default branch (${defaultBranch})`,
|
|
310
|
-
value: 'default',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
name: 'Use a different existing branch',
|
|
314
|
-
value: 'existing',
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
name: 'Create a new branch',
|
|
318
|
-
value: 'new',
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
default: 'default',
|
|
322
|
-
});
|
|
323
|
-
if (branchChoice === 'default') {
|
|
324
|
-
return resolved;
|
|
325
|
-
}
|
|
326
|
-
if (branchChoice === 'existing') {
|
|
327
|
-
const branchName = await prompts.input({
|
|
328
|
-
message: 'Enter branch name:',
|
|
329
|
-
default: defaultBranch,
|
|
330
|
-
validate: (value) => {
|
|
331
|
-
if (!value.trim())
|
|
332
|
-
return 'Branch name is required';
|
|
333
|
-
return true;
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
return {
|
|
337
|
-
...resolved,
|
|
338
|
-
repos: resolved.repos.map(repo => ({
|
|
339
|
-
...repo,
|
|
340
|
-
branch: branchName.trim(),
|
|
341
|
-
newBranch: undefined,
|
|
342
|
-
sourceBranch: undefined,
|
|
343
|
-
})),
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
if (branchChoice === 'new') {
|
|
347
|
-
const newBranchName = await prompts.input({
|
|
348
|
-
message: 'New branch name:',
|
|
349
|
-
validate: (value) => {
|
|
350
|
-
if (!value.trim())
|
|
351
|
-
return 'Branch name is required';
|
|
352
|
-
if (!/^[\w\-./]+$/.test(value))
|
|
353
|
-
return 'Invalid branch name (use letters, numbers, -, _, /, .)';
|
|
354
|
-
return true;
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
const sourceBranch = await prompts.input({
|
|
358
|
-
message: 'Create from branch:',
|
|
359
|
-
default: defaultBranch,
|
|
360
|
-
validate: (value) => {
|
|
361
|
-
if (!value.trim())
|
|
362
|
-
return 'Source branch is required';
|
|
363
|
-
return true;
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
return {
|
|
367
|
-
...resolved,
|
|
368
|
-
repos: resolved.repos.map(repo => ({
|
|
369
|
-
...repo,
|
|
370
|
-
branch: newBranchName.trim(),
|
|
371
|
-
newBranch: newBranchName.trim(),
|
|
372
|
-
sourceBranch: sourceBranch.trim(),
|
|
373
|
-
})),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
return resolved;
|
|
377
|
-
}
|
|
378
481
|
exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
379
482
|
.description('Rebuild an existing Genbox environment with updated configuration')
|
|
380
483
|
.argument('[name]', 'Name of the Genbox to rebuild (optional - will prompt if not provided)')
|
|
@@ -387,6 +490,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
387
490
|
.option('--db-source <source>', 'Database source for copy mode: staging, production')
|
|
388
491
|
.option('--db-dump <path>', 'Path to existing database dump file')
|
|
389
492
|
.option('-y, --yes', 'Skip interactive prompts')
|
|
493
|
+
.option('--hard', 'Full rebuild (reinstall OS) instead of soft rebuild')
|
|
390
494
|
.action(async (name, options) => {
|
|
391
495
|
try {
|
|
392
496
|
// Select genbox (interactive if no name provided)
|
|
@@ -484,7 +588,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
484
588
|
// Interactive branch selection only if no branch options specified and no stored branch
|
|
485
589
|
// Skip if: -b, -f, -n, stored branch, or -y
|
|
486
590
|
if (!options.branch && !options.fromBranch && !storedBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
|
|
487
|
-
resolved = await
|
|
591
|
+
resolved = await (0, utils_1.promptForBranchOptionsRebuild)(resolved, config);
|
|
488
592
|
}
|
|
489
593
|
// Display what will be rebuilt
|
|
490
594
|
console.log(chalk_1.default.bold('Rebuild Configuration:'));
|
|
@@ -522,11 +626,20 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
522
626
|
// Confirm rebuild
|
|
523
627
|
if (!options.yes) {
|
|
524
628
|
console.log('');
|
|
525
|
-
|
|
526
|
-
|
|
629
|
+
if (options.hard) {
|
|
630
|
+
console.log(chalk_1.default.yellow('Warning: Hard rebuild will reinstall the OS and rerun setup.'));
|
|
631
|
+
console.log(chalk_1.default.yellow('All unsaved work on the server will be lost.'));
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
console.log(chalk_1.default.blue('Soft rebuild will:'));
|
|
635
|
+
console.log(chalk_1.default.dim(' • Stop services (PM2, Docker Compose)'));
|
|
636
|
+
console.log(chalk_1.default.dim(' • Delete and re-clone repositories'));
|
|
637
|
+
console.log(chalk_1.default.dim(' • Reinstall dependencies and restart services'));
|
|
638
|
+
console.log(chalk_1.default.yellow('Note: Uncommitted changes in repos will be lost.'));
|
|
639
|
+
}
|
|
527
640
|
console.log('');
|
|
528
641
|
const confirm = await prompts.confirm({
|
|
529
|
-
message:
|
|
642
|
+
message: `${options.hard ? 'Hard rebuild' : 'Rebuild'} genbox '${selectedName}'?`,
|
|
530
643
|
default: false,
|
|
531
644
|
});
|
|
532
645
|
if (!confirm) {
|
|
@@ -535,7 +648,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
535
648
|
}
|
|
536
649
|
}
|
|
537
650
|
// Get SSH keys
|
|
538
|
-
const publicKey = getPublicSshKey();
|
|
651
|
+
const publicKey = (0, utils_1.getPublicSshKey)();
|
|
539
652
|
// Check if SSH auth is needed for git
|
|
540
653
|
let privateKeyContent;
|
|
541
654
|
const v3Config = config;
|
|
@@ -547,7 +660,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
547
660
|
default: true,
|
|
548
661
|
});
|
|
549
662
|
if (injectKey) {
|
|
550
|
-
privateKeyContent = getPrivateSshKey();
|
|
663
|
+
privateKeyContent = (0, utils_1.getPrivateSshKey)();
|
|
551
664
|
if (privateKeyContent) {
|
|
552
665
|
console.log(chalk_1.default.dim(' Using local SSH private key'));
|
|
553
666
|
}
|
|
@@ -674,7 +787,7 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
674
787
|
}
|
|
675
788
|
// Validate git credentials before rebuilding
|
|
676
789
|
const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
|
|
677
|
-
const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
|
|
790
|
+
const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
|
|
678
791
|
if (gitValidation.warnings.length > 0) {
|
|
679
792
|
console.log('');
|
|
680
793
|
console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
|
|
@@ -698,6 +811,157 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
698
811
|
return;
|
|
699
812
|
}
|
|
700
813
|
}
|
|
814
|
+
// ================================================================
|
|
815
|
+
// SOFT REBUILD (default)
|
|
816
|
+
// ================================================================
|
|
817
|
+
if (!options.hard) {
|
|
818
|
+
// Verify server is accessible
|
|
819
|
+
if (!genbox.ipAddress) {
|
|
820
|
+
console.log(chalk_1.default.red('Error: Genbox has no IP address. It may still be provisioning.'));
|
|
821
|
+
console.log(chalk_1.default.dim(' Use --hard for a full rebuild, or wait for provisioning to complete.'));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
// Get SSH key path
|
|
825
|
+
let sshKeyPath;
|
|
826
|
+
try {
|
|
827
|
+
sshKeyPath = (0, utils_1.getPrivateSshKeyPath)();
|
|
828
|
+
}
|
|
829
|
+
catch (error) {
|
|
830
|
+
console.log(chalk_1.default.red(error.message));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// Test SSH connection
|
|
834
|
+
console.log('');
|
|
835
|
+
console.log(chalk_1.default.dim('Testing SSH connection...'));
|
|
836
|
+
const testResult = sshExec(genbox.ipAddress, sshKeyPath, 'echo ok', 10);
|
|
837
|
+
if (!testResult.success || testResult.output !== 'ok') {
|
|
838
|
+
console.log(chalk_1.default.red('Error: Cannot connect to genbox via SSH.'));
|
|
839
|
+
console.log(chalk_1.default.dim(` ${testResult.error || 'Connection failed'}`));
|
|
840
|
+
console.log(chalk_1.default.dim(' Use --hard for a full rebuild if the server is unresponsive.'));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
console.log(chalk_1.default.green('✓ SSH connection verified'));
|
|
844
|
+
// Build env files for soft rebuild
|
|
845
|
+
const envFilesForSoftRebuild = [];
|
|
846
|
+
const envGenboxPath = path.join(process.cwd(), '.env.genbox');
|
|
847
|
+
if (fs.existsSync(envGenboxPath)) {
|
|
848
|
+
const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
|
|
849
|
+
const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
|
|
850
|
+
const globalSection = sections.get('GLOBAL') || '';
|
|
851
|
+
const envVarsFromFile = {};
|
|
852
|
+
for (const line of globalSection.split('\n')) {
|
|
853
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
854
|
+
if (match) {
|
|
855
|
+
let value = match[2].trim();
|
|
856
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
857
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
858
|
+
value = value.slice(1, -1);
|
|
859
|
+
}
|
|
860
|
+
envVarsFromFile[match[1]] = value;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
let connectTo;
|
|
864
|
+
if (resolved.profile && config.profiles?.[resolved.profile]) {
|
|
865
|
+
const profile = config.profiles[resolved.profile];
|
|
866
|
+
connectTo = (0, config_loader_1.getProfileConnection)(profile);
|
|
867
|
+
}
|
|
868
|
+
const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
|
|
869
|
+
for (const app of resolved.apps) {
|
|
870
|
+
const appConfig = config.apps[app.name];
|
|
871
|
+
const appPath = appConfig?.path || app.name;
|
|
872
|
+
const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
|
|
873
|
+
(resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
|
|
874
|
+
const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
|
|
875
|
+
if (servicesSections.length > 0) {
|
|
876
|
+
for (const serviceSectionName of servicesSections) {
|
|
877
|
+
const serviceName = serviceSectionName.split('/')[1];
|
|
878
|
+
const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
|
|
879
|
+
envFilesForSoftRebuild.push({
|
|
880
|
+
stagingName: `${app.name}-${serviceName}.env`,
|
|
881
|
+
remotePath: `${repoPath}/apps/${serviceName}/.env`,
|
|
882
|
+
content: serviceEnvContent,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
|
|
888
|
+
envFilesForSoftRebuild.push({
|
|
889
|
+
stagingName: `${app.name}.env`,
|
|
890
|
+
remotePath: `${repoPath}/.env`,
|
|
891
|
+
content: appEnvContent,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// Get git token from env
|
|
897
|
+
const envVars = configLoader.loadEnvVars(process.cwd());
|
|
898
|
+
// Run soft rebuild
|
|
899
|
+
console.log('');
|
|
900
|
+
console.log(chalk_1.default.blue('=== Starting Soft Rebuild ==='));
|
|
901
|
+
console.log('');
|
|
902
|
+
let currentStep = '';
|
|
903
|
+
const rebuildResult = await runSoftRebuild({
|
|
904
|
+
genbox,
|
|
905
|
+
resolved,
|
|
906
|
+
config,
|
|
907
|
+
keyPath: sshKeyPath,
|
|
908
|
+
envFiles: envFilesForSoftRebuild,
|
|
909
|
+
snapshotId,
|
|
910
|
+
snapshotS3Key,
|
|
911
|
+
gitToken: envVars.GIT_TOKEN,
|
|
912
|
+
onStep: (step) => {
|
|
913
|
+
currentStep = step;
|
|
914
|
+
console.log(chalk_1.default.blue(`\n=== ${step} ===`));
|
|
915
|
+
},
|
|
916
|
+
onLog: (line, type) => {
|
|
917
|
+
switch (type) {
|
|
918
|
+
case 'error':
|
|
919
|
+
console.log(chalk_1.default.red(` ${line}`));
|
|
920
|
+
break;
|
|
921
|
+
case 'success':
|
|
922
|
+
console.log(chalk_1.default.green(` ${line}`));
|
|
923
|
+
break;
|
|
924
|
+
case 'info':
|
|
925
|
+
console.log(chalk_1.default.cyan(` ${line}`));
|
|
926
|
+
break;
|
|
927
|
+
case 'dim':
|
|
928
|
+
default:
|
|
929
|
+
console.log(chalk_1.default.dim(` ${line}`));
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
console.log('');
|
|
935
|
+
if (rebuildResult.success) {
|
|
936
|
+
console.log(chalk_1.default.green(`✓ Soft rebuild completed successfully!`));
|
|
937
|
+
console.log('');
|
|
938
|
+
console.log(`Run ${chalk_1.default.cyan(`genbox status ${selectedName}`)} to check service status.`);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
console.log(chalk_1.default.red(`✗ Soft rebuild failed: ${rebuildResult.error}`));
|
|
942
|
+
console.log(chalk_1.default.dim(` Failed during: ${currentStep}`));
|
|
943
|
+
console.log(chalk_1.default.dim(` Use --hard for a full OS reinstall if needed.`));
|
|
944
|
+
}
|
|
945
|
+
// Notify API about the rebuild (for tracking)
|
|
946
|
+
try {
|
|
947
|
+
await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/soft-rebuild-completed`, {
|
|
948
|
+
method: 'POST',
|
|
949
|
+
body: JSON.stringify({
|
|
950
|
+
success: rebuildResult.success,
|
|
951
|
+
branch: resolved.repos[0]?.branch,
|
|
952
|
+
newBranch: resolved.repos[0]?.newBranch,
|
|
953
|
+
sourceBranch: resolved.repos[0]?.sourceBranch,
|
|
954
|
+
}),
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
// Silently ignore API notification failures
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
// ================================================================
|
|
963
|
+
// HARD REBUILD (--hard flag)
|
|
964
|
+
// ================================================================
|
|
701
965
|
// Build payload
|
|
702
966
|
const payload = buildRebuildPayload(resolved, config, publicKey, privateKeyContent, configLoader);
|
|
703
967
|
// Add database info to payload if we have a snapshot
|
|
@@ -712,11 +976,11 @@ exports.rebuildCommand = new commander_1.Command('rebuild')
|
|
|
712
976
|
else if (dbMode === 'local' || dbMode === 'fresh') {
|
|
713
977
|
payload.database = { mode: 'local' };
|
|
714
978
|
}
|
|
715
|
-
// Execute rebuild
|
|
716
|
-
const rebuildSpinner = (0, ora_1.default)(`
|
|
979
|
+
// Execute hard rebuild via API
|
|
980
|
+
const rebuildSpinner = (0, ora_1.default)(`Hard rebuilding Genbox '${selectedName}'...`).start();
|
|
717
981
|
try {
|
|
718
982
|
await rebuildGenbox(genbox._id, payload);
|
|
719
|
-
rebuildSpinner.succeed(chalk_1.default.green(`Genbox '${selectedName}' rebuild initiated!`));
|
|
983
|
+
rebuildSpinner.succeed(chalk_1.default.green(`Genbox '${selectedName}' hard rebuild initiated!`));
|
|
720
984
|
console.log('');
|
|
721
985
|
console.log(chalk_1.default.dim('Server is rebuilding. This may take a few minutes.'));
|
|
722
986
|
console.log(chalk_1.default.dim('SSH connection will be temporarily unavailable.'));
|