genbox 1.0.58 → 1.0.60

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.
@@ -244,19 +244,12 @@ function buildRebuildPayload(resolved, config, publicKey, privateKey, configLoad
244
244
  }
245
245
  }
246
246
  }
247
- // Generate setup script
248
- const setupScript = generateSetupScript(resolved, config, envFilesToMove);
249
- if (setupScript) {
250
- files.push({
251
- path: '/home/dev/setup-genbox.sh',
252
- content: setupScript,
253
- permissions: '0755',
254
- });
255
- }
247
+ // NOTE: Setup script generation removed from CLI.
248
+ // The API generates a comprehensive setup script with clone commands at /root/setup-genbox.sh
249
+ // and copies it to /home/dev/setup-genbox.sh via runcmd. This avoids having two conflicting
250
+ // scripts in write_files where the CLI version lacked clone commands.
251
+ // The envFilesToMove variable is kept as API needs this info for .env file movement.
256
252
  const postDetails = [];
257
- if (setupScript) {
258
- postDetails.push('su - dev -c "/home/dev/setup-genbox.sh"');
259
- }
260
253
  // Build repos
261
254
  const repos = {};
262
255
  for (const repo of resolved.repos) {
@@ -298,58 +291,6 @@ function validateGitCredentials(repos, gitToken, privateKey) {
298
291
  }
299
292
  return { warnings, errors };
300
293
  }
301
- /**
302
- * Generate setup script
303
- */
304
- function generateSetupScript(resolved, config, envFilesToMove = []) {
305
- const lines = [
306
- '#!/bin/bash',
307
- '# Generated by genbox rebuild',
308
- 'set -e',
309
- '',
310
- ];
311
- if (envFilesToMove.length > 0) {
312
- lines.push('# Move .env files from staging to app directories');
313
- for (const { stagingName, targetPath } of envFilesToMove) {
314
- lines.push(`if [ -f "/home/dev/.env-staging/${stagingName}" ]; then`);
315
- lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
316
- lines.push(` mv "/home/dev/.env-staging/${stagingName}" "${targetPath}"`);
317
- lines.push(` echo "Moved .env to ${targetPath}"`);
318
- lines.push('fi');
319
- }
320
- lines.push('rm -rf /home/dev/.env-staging 2>/dev/null || true');
321
- lines.push('');
322
- }
323
- if (resolved.repos.length > 0) {
324
- lines.push(`cd ${resolved.repos[0].path} || exit 1`);
325
- lines.push('');
326
- }
327
- lines.push('# Install dependencies');
328
- lines.push('if [ -f "pnpm-lock.yaml" ]; then');
329
- lines.push(' echo "Installing dependencies with pnpm..."');
330
- lines.push(' pnpm install --frozen-lockfile');
331
- lines.push('elif [ -f "yarn.lock" ]; then');
332
- lines.push(' echo "Installing dependencies with yarn..."');
333
- lines.push(' yarn install --frozen-lockfile');
334
- lines.push('elif [ -f "bun.lockb" ]; then');
335
- lines.push(' echo "Installing dependencies with bun..."');
336
- lines.push(' bun install --frozen-lockfile');
337
- lines.push('elif [ -f "package-lock.json" ]; then');
338
- lines.push(' echo "Installing dependencies with npm..."');
339
- lines.push(' npm ci');
340
- lines.push('fi');
341
- const hasLocalApi = resolved.apps.some(a => a.name === 'api' || a.type === 'backend');
342
- if (hasLocalApi) {
343
- lines.push('');
344
- lines.push('echo "Starting Docker services..."');
345
- lines.push('if [ -f "docker-compose.yml" ] || [ -f "compose.yml" ]; then');
346
- lines.push(' docker compose up -d');
347
- lines.push('fi');
348
- }
349
- lines.push('');
350
- lines.push('echo "Setup complete!"');
351
- return lines.join('\n');
352
- }
353
294
  /**
354
295
  * Prompt for branch options interactively
355
296
  */
@@ -145,10 +145,55 @@ function displayTimingBreakdown(timing) {
145
145
  console.log('');
146
146
  }
147
147
  }
148
+ // Watch mode: tail cloud-init logs in realtime
149
+ function tailCloudInitLogs(ip, keyPath) {
150
+ const sshOpts = [
151
+ '-i', keyPath,
152
+ '-o', 'IdentitiesOnly=yes',
153
+ '-o', 'StrictHostKeyChecking=no',
154
+ '-o', 'UserKnownHostsFile=/dev/null',
155
+ '-o', 'LogLevel=ERROR',
156
+ '-o', 'ConnectTimeout=10',
157
+ `dev@${ip}`,
158
+ 'sudo tail -f /var/log/cloud-init-output.log 2>/dev/null'
159
+ ];
160
+ const child = (0, child_process_1.spawn)('ssh', sshOpts, {
161
+ stdio: ['pipe', 'pipe', 'pipe'],
162
+ });
163
+ child.stdout?.on('data', (data) => {
164
+ const lines = data.toString().split('\n');
165
+ for (const line of lines) {
166
+ if (line.trim()) {
167
+ // Color-code certain log lines
168
+ if (line.startsWith('===') || line.includes('===')) {
169
+ console.log(chalk_1.default.cyan(line));
170
+ }
171
+ else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
172
+ console.log(chalk_1.default.red(line));
173
+ }
174
+ else if (line.toLowerCase().includes('done') || line.toLowerCase().includes('complete')) {
175
+ console.log(chalk_1.default.green(line));
176
+ }
177
+ else {
178
+ console.log(line);
179
+ }
180
+ }
181
+ }
182
+ });
183
+ child.stderr?.on('data', (data) => {
184
+ // Ignore SSH stderr (connection warnings, etc.)
185
+ });
186
+ return child;
187
+ }
188
+ // Sleep helper for async code
189
+ function sleep(ms) {
190
+ return new Promise(resolve => setTimeout(resolve, ms));
191
+ }
148
192
  exports.statusCommand = new commander_1.Command('status')
149
193
  .description('Check cloud-init progress and service status of a Genbox')
150
194
  .argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
151
195
  .option('-a, --all', 'Select from all genboxes (not just current project)')
196
+ .option('-w, --watch', 'Watch mode: tail logs in realtime during setup, refresh status periodically when running')
152
197
  .action(async (name, options) => {
153
198
  try {
154
199
  // 1. Select Genbox (interactive if no name provided)
@@ -256,7 +301,118 @@ exports.statusCommand = new commander_1.Command('status')
256
301
  }
257
302
  // If DB says PROVISIONING, check if we can SSH to get progress
258
303
  if (dbStatus === 'provisioning') {
259
- // Try to SSH and check cloud-init progress
304
+ // In watch mode, handle SSH connection retries and log tailing
305
+ if (options.watch) {
306
+ // Set up Ctrl+C handler
307
+ let stopWatching = false;
308
+ const cleanup = () => {
309
+ stopWatching = true;
310
+ console.log('\n' + chalk_1.default.dim('Watch mode stopped.'));
311
+ process.exit(0);
312
+ };
313
+ process.on('SIGINT', cleanup);
314
+ process.on('SIGTERM', cleanup);
315
+ // Wait for SSH to become available
316
+ let sshAvailable = false;
317
+ while (!sshAvailable && !stopWatching) {
318
+ const status = sshExec(target.ipAddress, keyPath, 'cloud-init status 2>&1');
319
+ if (!status) {
320
+ console.log(chalk_1.default.yellow('[INFO] Waiting for server to boot... (Ctrl+C to exit)'));
321
+ await sleep(5000);
322
+ continue;
323
+ }
324
+ const sshErrors = [
325
+ 'Operation timed out',
326
+ 'Connection refused',
327
+ 'Connection timed out',
328
+ 'No route to host',
329
+ 'Network is unreachable',
330
+ 'ssh: connect to host',
331
+ ];
332
+ if (sshErrors.some(err => status.includes(err))) {
333
+ console.log(chalk_1.default.yellow('[INFO] Server is still initializing - SSH is not yet available... (Ctrl+C to exit)'));
334
+ await sleep(5000);
335
+ continue;
336
+ }
337
+ sshAvailable = true;
338
+ // Check if cloud-init is still running
339
+ if (status.includes('running')) {
340
+ // Get timing breakdown
341
+ const timing = getTimingBreakdown(target.ipAddress, keyPath);
342
+ if (timing.total !== null) {
343
+ console.log(chalk_1.default.yellow(`[INFO] Setup in progress... (elapsed: ${formatDuration(timing.total)})`));
344
+ }
345
+ else {
346
+ console.log(chalk_1.default.yellow('[INFO] Setup in progress...'));
347
+ }
348
+ console.log('');
349
+ // Show timing breakdown so far
350
+ if (timing.sshReady !== null) {
351
+ console.log(chalk_1.default.blue('[INFO] === Timing So Far ==='));
352
+ console.log(` SSH Ready: ${formatDuration(timing.sshReady)}`);
353
+ if (timing.total !== null) {
354
+ console.log(` Elapsed: ${formatDuration(timing.total)}`);
355
+ }
356
+ console.log('');
357
+ }
358
+ console.log(chalk_1.default.blue('[INFO] Tailing setup logs in realtime (Ctrl+C to exit):'));
359
+ console.log(chalk_1.default.dim('─'.repeat(60)));
360
+ const tailProcess = tailCloudInitLogs(target.ipAddress, keyPath);
361
+ // Update cleanup to kill tail process
362
+ process.removeListener('SIGINT', cleanup);
363
+ process.removeListener('SIGTERM', cleanup);
364
+ const cleanupWithTail = () => {
365
+ stopWatching = true;
366
+ tailProcess.kill();
367
+ console.log('\n' + chalk_1.default.dim('Watch mode stopped.'));
368
+ process.exit(0);
369
+ };
370
+ process.on('SIGINT', cleanupWithTail);
371
+ process.on('SIGTERM', cleanupWithTail);
372
+ // Wait for tail process to end
373
+ await new Promise((resolve) => {
374
+ tailProcess.on('close', () => resolve());
375
+ tailProcess.on('error', () => resolve());
376
+ });
377
+ // Clean up handlers
378
+ process.removeListener('SIGINT', cleanupWithTail);
379
+ process.removeListener('SIGTERM', cleanupWithTail);
380
+ if (!stopWatching) {
381
+ console.log('');
382
+ console.log(chalk_1.default.blue('[INFO] Log stream ended. Checking final status...'));
383
+ console.log('');
384
+ await sleep(2000);
385
+ // Check final status
386
+ const finalStatus = sshExec(target.ipAddress, keyPath, 'cloud-init status 2>&1');
387
+ if (finalStatus.includes('done')) {
388
+ console.log(chalk_1.default.green('[SUCCESS] Setup completed!'));
389
+ const finalTiming = getTimingBreakdown(target.ipAddress, keyPath);
390
+ displayTimingBreakdown(finalTiming);
391
+ }
392
+ else if (finalStatus.includes('error')) {
393
+ console.log(chalk_1.default.red('[ERROR] Setup encountered an error!'));
394
+ console.log(chalk_1.default.dim(' Run `gb connect` to investigate.'));
395
+ }
396
+ else {
397
+ console.log(chalk_1.default.yellow(`[INFO] Final status: ${finalStatus}`));
398
+ }
399
+ }
400
+ }
401
+ else if (status.includes('done')) {
402
+ console.log(chalk_1.default.green('[SUCCESS] Setup completed!'));
403
+ console.log(chalk_1.default.dim(' (Waiting for status update...)'));
404
+ }
405
+ else if (status.includes('error')) {
406
+ console.log(chalk_1.default.red('[ERROR] Setup encountered an error!'));
407
+ console.log(chalk_1.default.dim(' Run `gb connect` to investigate.'));
408
+ }
409
+ else {
410
+ console.log(chalk_1.default.yellow(`[INFO] Status: ${status}`));
411
+ }
412
+ }
413
+ return;
414
+ }
415
+ // Non-watch mode: single status check
260
416
  let status = sshExec(target.ipAddress, keyPath, 'cloud-init status 2>&1');
261
417
  if (!status) {
262
418
  console.log(chalk_1.default.yellow('[WARN] Unable to connect to server. It may still be booting.'));
@@ -275,6 +431,8 @@ exports.statusCommand = new commander_1.Command('status')
275
431
  console.log(chalk_1.default.yellow('[INFO] Server is still initializing - SSH is not yet available.'));
276
432
  console.log(chalk_1.default.dim(' This is normal for newly created servers.'));
277
433
  console.log(chalk_1.default.dim(' Please wait 1-2 minutes and try again.'));
434
+ console.log('');
435
+ console.log(chalk_1.default.dim('Tip: Use `gb status -w` to watch progress in realtime'));
278
436
  return;
279
437
  }
280
438
  // Cloud-init still running
@@ -303,6 +461,8 @@ exports.statusCommand = new commander_1.Command('status')
303
461
  if (logOutput) {
304
462
  console.log(logOutput);
305
463
  }
464
+ console.log('');
465
+ console.log(chalk_1.default.dim('Tip: Use `gb status -w` to watch progress in realtime'));
306
466
  return;
307
467
  }
308
468
  // Cloud-init done but callback might not have been received yet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.58",
3
+ "version": "1.0.60",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {