genbox 1.0.200 → 1.0.202

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.
@@ -1157,7 +1157,7 @@ exports.createCommand = new commander_1.Command('create')
1157
1157
  // Wait for genbox to be ready with progress indicator
1158
1158
  const genboxId = genbox._id || genbox.id;
1159
1159
  const waitResult = await (0, genbox_progress_1.waitForGenboxReady)(genboxId, name, {
1160
- timeout: 180,
1160
+ timeout: 600,
1161
1161
  showProgress: true,
1162
1162
  });
1163
1163
  if (!waitResult.success) {
@@ -334,7 +334,7 @@ exports.newCommand = new commander_1.Command('new')
334
334
  // Wait for genbox to be ready with progress indicator
335
335
  const genboxId = genbox._id || genbox.id;
336
336
  const waitResult = await (0, genbox_progress_1.waitForGenboxReady)(genboxId, projectName, {
337
- timeout: 180,
337
+ timeout: 600,
338
338
  showProgress: true,
339
339
  });
340
340
  if (!waitResult.success) {
@@ -368,7 +368,7 @@ async function startGenbox(genbox) {
368
368
  });
369
369
  // Wait for genbox to be ready with progress indicator
370
370
  const waitResult = await (0, genbox_progress_1.waitForGenboxReady)(genbox.id, genbox.name, {
371
- timeout: 120,
371
+ timeout: 600,
372
372
  showProgress: true,
373
373
  });
374
374
  if (!waitResult.success) {
@@ -495,7 +495,16 @@ async function startSessionOnGenbox(provider, genbox) {
495
495
  const socketDir = getRemoteDtachSocketDir();
496
496
  const socketPath = `${socketDir}/${sessionName}.sock`;
497
497
  // Use projectPath from genbox if available, otherwise detect
498
- const knownProjectPath = targetGenbox.projectPath || '';
498
+ // For local Docker/VM, the workspace is mounted at /home/dev/workspace
499
+ let knownProjectPath = '';
500
+ if (genbox._isLocal) {
501
+ // Local genboxes mount workspace at /home/dev/workspace
502
+ knownProjectPath = '/home/dev/workspace';
503
+ }
504
+ else if (targetGenbox.projectPath) {
505
+ // Cloud genboxes use the path from API
506
+ knownProjectPath = targetGenbox.projectPath;
507
+ }
499
508
  const startScript = `#!/bin/bash
500
509
  # Set terminal environment for color support
501
510
  export TERM=xterm-256color
@@ -51,7 +51,7 @@ function estimateProgress(elapsedSecs, hasIp) {
51
51
  * Wait for a genbox to be ready with real-time progress indicator
52
52
  */
53
53
  async function waitForGenboxReady(genboxId, name, options = {}) {
54
- const { timeout = 180, interval = 2000, showProgress = true, onStatusChange, } = options;
54
+ const { timeout = 600, interval = 2000, showProgress = true, onStatusChange, } = options;
55
55
  const maxAttempts = Math.ceil((timeout * 1000) / interval);
56
56
  const startTime = Date.now();
57
57
  let lastStatus = '';
@@ -57,6 +57,7 @@ const fs = __importStar(require("fs"));
57
57
  const os = __importStar(require("os"));
58
58
  const path = __importStar(require("path"));
59
59
  const child_process_1 = require("child_process");
60
+ const ora_1 = __importDefault(require("ora"));
60
61
  const api_1 = require("../api");
61
62
  const genbox_progress_1 = require("./genbox-progress");
62
63
  const ssh_keys_1 = require("../utils/ssh-keys");
@@ -64,6 +65,10 @@ const config_store_1 = require("../config-store");
64
65
  const unified_session_1 = require("./unified-session");
65
66
  const gemini_auth_1 = require("../utils/gemini-auth");
66
67
  const claude_auth_1 = require("../utils/claude-auth");
68
+ const config_loader_1 = require("../config-loader");
69
+ const profile_resolver_1 = require("../profile-resolver");
70
+ const utils_1 = require("../utils");
71
+ const random_name_1 = require("../random-name");
67
72
  const VM_SIZE_REQUIREMENTS = {
68
73
  small: {
69
74
  cpus: 2,
@@ -585,23 +590,50 @@ async function promptForLocation(includeDirect = true) {
585
590
  }
586
591
  }
587
592
  /**
588
- * Prompt for genbox name
593
+ * Prompt for genbox name with random suggestions
594
+ * Matches the UX from `gb create` command
589
595
  */
590
596
  async function promptForName(defaultName) {
591
597
  try {
592
- const name = await (0, prompts_1.input)({
593
- message: 'Genbox name:',
594
- default: defaultName || `genbox-${Date.now().toString(36)}`,
595
- validate: (v) => {
596
- if (!v.trim())
597
- return 'Name is required';
598
- if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(v.trim())) {
599
- return 'Name must be lowercase, start/end with letter or number';
600
- }
601
- return true;
602
- },
598
+ // Generate 3 random name suggestions
599
+ const suggestions = (0, random_name_1.generateNameSuggestions)(3);
600
+ const nameChoice = await (0, select_1.default)({
601
+ message: 'Environment name:',
602
+ choices: [
603
+ {
604
+ name: `${suggestions[0]} ${chalk_1.default.dim('(random)')}`,
605
+ value: suggestions[0],
606
+ },
607
+ {
608
+ name: `${suggestions[1]} ${chalk_1.default.dim('(random)')}`,
609
+ value: suggestions[1],
610
+ },
611
+ {
612
+ name: `${suggestions[2]} ${chalk_1.default.dim('(random)')}`,
613
+ value: suggestions[2],
614
+ },
615
+ {
616
+ name: 'Enter custom name...',
617
+ value: '__custom__',
618
+ },
619
+ ],
603
620
  });
604
- return name.trim();
621
+ if (nameChoice === '__custom__') {
622
+ const customName = await (0, prompts_1.input)({
623
+ message: 'Enter environment name:',
624
+ default: defaultName,
625
+ validate: (value) => {
626
+ if (!value.trim())
627
+ return 'Name is required';
628
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(value.trim())) {
629
+ return 'Name must be lowercase, start/end with letter or number, and contain only letters, numbers, and hyphens';
630
+ }
631
+ return true;
632
+ },
633
+ });
634
+ return customName.trim();
635
+ }
636
+ return nameChoice;
605
637
  }
606
638
  catch {
607
639
  return null;
@@ -691,7 +723,7 @@ async function createCloudGenbox(name, options = {}) {
691
723
  }
692
724
  // Wait for genbox to be ready with progress indicator
693
725
  const waitResult = await (0, genbox_progress_1.waitForGenboxReady)(genbox.id, name, {
694
- timeout: 180,
726
+ timeout: 600,
695
727
  showProgress: true,
696
728
  });
697
729
  if (!waitResult.success) {
@@ -862,7 +894,8 @@ echo 'dev ALL=(ALL) NOPASSWD:ALL' | tee /etc/sudoers.d/dev
862
894
  chmod 440 /etc/sudoers.d/dev
863
895
  mkdir -p /home/dev/.ssh
864
896
  echo '${publicKey}' > /home/dev/.ssh/authorized_keys
865
- chown -R dev:dev /home/dev/.ssh
897
+ chown -R dev:dev /home/dev/.ssh || true
898
+ chown dev:dev /home/dev || true
866
899
  chmod 700 /home/dev/.ssh
867
900
  chmod 600 /home/dev/.ssh/authorized_keys
868
901
  mkdir -p /run/sshd
@@ -930,6 +963,50 @@ mkdir -p /run/sshd
930
963
  console.log(chalk_1.default.red('\nFailed to start SSH in container.'));
931
964
  return { success: false, error: 'SSH startup timeout' };
932
965
  }
966
+ // Install Node.js and provider CLI via SSH
967
+ showProgress('Installing Node.js', 0, 70);
968
+ const keyPath = (0, ssh_keys_1.getPrivateSshKeyPath)();
969
+ try {
970
+ // Determine CLI package based on provider
971
+ const cliPackage = provider === 'gemini' ? '@google/gemini-cli' : '@anthropic-ai/claude-code';
972
+ const cliCommand = provider === 'gemini' ? 'gemini' : 'claude';
973
+ // Install NVM, Node.js, and CLI
974
+ const installScript = `
975
+ # Install NVM
976
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
977
+ export NVM_DIR="$HOME/.nvm"
978
+ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
979
+
980
+ # Install Node.js 20
981
+ nvm install 20
982
+ nvm use 20
983
+ nvm alias default 20
984
+
985
+ # Add NVM to bashrc for future sessions
986
+ echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.bashrc
987
+ echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> ~/.bashrc
988
+
989
+ # Install CLI
990
+ npm install -g ${cliPackage}
991
+
992
+ echo "Installed: $(node -v), ${cliCommand}"
993
+ `;
994
+ showProgress(`Installing ${cliCommand} CLI`, 0, 85);
995
+ (0, child_process_1.execFileSync)('ssh', [
996
+ '-p', String(sshPort),
997
+ '-i', keyPath,
998
+ '-o', 'StrictHostKeyChecking=no',
999
+ '-o', 'UserKnownHostsFile=/dev/null',
1000
+ '-o', 'LogLevel=ERROR',
1001
+ 'dev@localhost',
1002
+ installScript,
1003
+ ], { timeout: 300000, stdio: 'pipe' }); // 5 minute timeout
1004
+ }
1005
+ catch (err) {
1006
+ // Non-fatal - CLI can be installed manually
1007
+ console.log(chalk_1.default.yellow(`\n Warning: Failed to install CLI automatically.`));
1008
+ console.log(chalk_1.default.dim(` You can install it manually after connecting.`));
1009
+ }
933
1010
  showProgress('Creating session', 0, 95);
934
1011
  // Create session record
935
1012
  const session = await manager.createSession({
@@ -1040,32 +1117,21 @@ async function createLocalVmGenbox(name, provider = 'claude') {
1040
1117
  console.log(chalk_1.default.green(`✓ Gemini credentials detected (${geminiCreds.source})`));
1041
1118
  const isJsonCredentials = geminiCreds.credentials.trim().startsWith('{');
1042
1119
  if (isJsonCredentials) {
1043
- // Escape single quotes in the credentials for use in shell heredoc
1044
- const escapedCreds = geminiCreds.credentials.replace(/'/g, "'\\''");
1120
+ // Base64 encode the credentials to avoid YAML parsing issues
1121
+ const credsBase64 = Buffer.from(geminiCreds.credentials).toString('base64');
1122
+ const settingsJson = JSON.stringify({
1123
+ autoAccept: true,
1124
+ security: { auth: { selectedType: "oauth-personal" }, disableYoloMode: false },
1125
+ tools: { sandbox: false, autoAccept: true }
1126
+ });
1127
+ const settingsBase64 = Buffer.from(settingsJson).toString('base64');
1045
1128
  credentialsScript = `
1046
- # Setup Gemini credentials
1129
+ # Setup Gemini credentials (base64 encoded to avoid YAML issues)
1047
1130
  echo "=== Setting up Gemini CLI Authentication ==="
1048
1131
  mkdir -p /home/dev/.gemini
1049
- cat > /home/dev/.gemini/oauth_creds.json << 'GEMINI_CREDS_EOF'
1050
- ${geminiCreds.credentials}
1051
- GEMINI_CREDS_EOF
1132
+ echo "${credsBase64}" | base64 -d > /home/dev/.gemini/oauth_creds.json
1052
1133
  chmod 600 /home/dev/.gemini/oauth_creds.json
1053
- # Create settings.json for Gemini CLI (YOLO mode)
1054
- cat > /home/dev/.gemini/settings.json << 'GEMINI_SETTINGS_EOF'
1055
- {
1056
- "autoAccept": true,
1057
- "security": {
1058
- "auth": {
1059
- "selectedType": "oauth-personal"
1060
- },
1061
- "disableYoloMode": false
1062
- },
1063
- "tools": {
1064
- "sandbox": false,
1065
- "autoAccept": true
1066
- }
1067
- }
1068
- GEMINI_SETTINGS_EOF
1134
+ echo "${settingsBase64}" | base64 -d > /home/dev/.gemini/settings.json
1069
1135
  chmod 644 /home/dev/.gemini/settings.json
1070
1136
  chown -R dev:dev /home/dev/.gemini
1071
1137
  echo "Gemini CLI authentication configured."
@@ -1096,30 +1162,27 @@ GEMINI_SETTINGS_EOF
1096
1162
  console.log(chalk_1.default.green(`✓ Claude credentials detected (${claudeCreds.source})`));
1097
1163
  const isJsonCredentials = claudeCreds.credentials.trim().startsWith('{');
1098
1164
  if (isJsonCredentials) {
1165
+ // Base64 encode the credentials to avoid YAML parsing issues
1166
+ const credsBase64 = Buffer.from(claudeCreds.credentials).toString('base64');
1167
+ const settingsJson = JSON.stringify({
1168
+ hasCompletedOnboarding: true,
1169
+ theme: "dark",
1170
+ preferredNotifChannel: "terminal"
1171
+ });
1172
+ const settingsBase64 = Buffer.from(settingsJson).toString('base64');
1173
+ const claudeJsonBase64 = Buffer.from(JSON.stringify({
1174
+ hasCompletedOnboarding: true,
1175
+ theme: "dark"
1176
+ })).toString('base64');
1099
1177
  credentialsScript = `
1100
- # Setup Claude credentials
1178
+ # Setup Claude credentials (base64 encoded to avoid YAML issues)
1101
1179
  echo "=== Setting up Claude Code Authentication ==="
1102
1180
  mkdir -p /home/dev/.claude
1103
- cat > /home/dev/.claude/.credentials.json << 'CLAUDE_CREDS_EOF'
1104
- ${claudeCreds.credentials}
1105
- CLAUDE_CREDS_EOF
1181
+ echo "${credsBase64}" | base64 -d > /home/dev/.claude/.credentials.json
1106
1182
  chmod 600 /home/dev/.claude/.credentials.json
1107
- # Create settings to skip onboarding
1108
- cat > /home/dev/.claude/settings.json << 'CLAUDE_SETTINGS_EOF'
1109
- {
1110
- "hasCompletedOnboarding": true,
1111
- "theme": "dark",
1112
- "preferredNotifChannel": "terminal"
1113
- }
1114
- CLAUDE_SETTINGS_EOF
1183
+ echo "${settingsBase64}" | base64 -d > /home/dev/.claude/settings.json
1115
1184
  chmod 644 /home/dev/.claude/settings.json
1116
- # Also create ~/.claude.json for Claude Code v2.x
1117
- cat > /home/dev/.claude.json << 'CLAUDE_JSON_EOF'
1118
- {
1119
- "hasCompletedOnboarding": true,
1120
- "theme": "dark"
1121
- }
1122
- CLAUDE_JSON_EOF
1185
+ echo "${claudeJsonBase64}" | base64 -d > /home/dev/.claude.json
1123
1186
  chmod 644 /home/dev/.claude.json
1124
1187
  chown -R dev:dev /home/dev/.claude /home/dev/.claude.json
1125
1188
  echo "Claude Code authentication configured."
@@ -1234,6 +1297,11 @@ ${credentialsScript}`;
1234
1297
  const stageProgress = (elapsed % 30) / 30 * 10;
1235
1298
  updateProgress(currentStage, Math.floor(stageProgress));
1236
1299
  }, 500);
1300
+ // Capture stderr for error messages
1301
+ let vmStderr = '';
1302
+ vmProcess.stderr.on('data', (data) => {
1303
+ vmStderr += data.toString();
1304
+ });
1237
1305
  // Wait for VM creation
1238
1306
  await new Promise((resolve, reject) => {
1239
1307
  vmProcess.on('close', (code) => {
@@ -1242,7 +1310,8 @@ ${credentialsScript}`;
1242
1310
  resolve();
1243
1311
  }
1244
1312
  else {
1245
- reject(new Error(`VM creation failed with code ${code}`));
1313
+ const errorMsg = vmStderr.trim() || `VM creation failed with code ${code}`;
1314
+ reject(new Error(errorMsg));
1246
1315
  }
1247
1316
  });
1248
1317
  vmProcess.on('error', (err) => {
@@ -1353,12 +1422,303 @@ ${credentialsScript}`;
1353
1422
  return { success: false, error: error.message };
1354
1423
  }
1355
1424
  }
1425
+ /**
1426
+ * Create a local Docker genbox from a project with genbox.yaml
1427
+ */
1428
+ async function createLocalDockerFromProject(config, options) {
1429
+ console.log('');
1430
+ console.log(chalk_1.default.blue('Creating Docker genbox from project...'));
1431
+ console.log(chalk_1.default.dim(`Project: ${config.project?.name || 'unknown'}`));
1432
+ console.log('');
1433
+ // Profile selection if available
1434
+ let selectedProfile;
1435
+ if (config.profiles && Object.keys(config.profiles).length > 0 && !options.skipPrompts) {
1436
+ const profileNames = Object.keys(config.profiles);
1437
+ const choices = profileNames.map(name => {
1438
+ const profile = config.profiles[name];
1439
+ const apps = profile.apps ? profile.apps.join(', ') : 'default apps';
1440
+ const description = profile.description || `Apps: ${apps}`;
1441
+ return {
1442
+ name: `${name} - ${chalk_1.default.dim(description)}`,
1443
+ value: name,
1444
+ };
1445
+ });
1446
+ choices.push({
1447
+ name: `${chalk_1.default.cyan('Custom')} - ${chalk_1.default.dim('Use default configuration')}`,
1448
+ value: '__custom__',
1449
+ });
1450
+ selectedProfile = await (0, select_1.default)({
1451
+ message: 'Select a profile:',
1452
+ choices,
1453
+ });
1454
+ if (selectedProfile === '__custom__') {
1455
+ selectedProfile = undefined;
1456
+ }
1457
+ }
1458
+ // Get name
1459
+ const defaultName = options.provider
1460
+ ? `${options.provider}-${Date.now().toString(36)}`
1461
+ : `genbox-${Date.now().toString(36)}`;
1462
+ const name = options.name || await promptForName(defaultName);
1463
+ if (!name) {
1464
+ console.log(chalk_1.default.dim('\nCancelled.'));
1465
+ return { success: false, error: 'Cancelled' };
1466
+ }
1467
+ console.log('');
1468
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1469
+ console.log(` ${chalk_1.default.bold('Project:')} ${config.project?.name || 'unknown'}`);
1470
+ if (selectedProfile) {
1471
+ console.log(` ${chalk_1.default.bold('Profile:')} ${selectedProfile}`);
1472
+ }
1473
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1474
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1475
+ console.log('');
1476
+ return await createLocalDockerGenbox(name, options.provider || 'claude');
1477
+ }
1478
+ /**
1479
+ * Scaffold a project from a starter template
1480
+ * Returns the path to the scaffolded project
1481
+ */
1482
+ async function scaffoldTemplate(template, projectName) {
1483
+ const spinner = (0, ora_1.default)('Setting up project from template...').start();
1484
+ try {
1485
+ let createCmd;
1486
+ if (template.createCommand) {
1487
+ // Use the template's create command (e.g., npx create-next-app)
1488
+ createCmd = template.createCommand.replace(/\{\{name\}\}/g, projectName);
1489
+ }
1490
+ else if (template.repoUrl) {
1491
+ // Clone from repository
1492
+ createCmd = `git clone https://${template.repoUrl} ${projectName}`;
1493
+ }
1494
+ else {
1495
+ spinner.fail('Template does not support local creation');
1496
+ return { success: false, error: 'Template does not support local creation' };
1497
+ }
1498
+ spinner.text = `Running: ${createCmd.substring(0, 50)}...`;
1499
+ // Run the create command
1500
+ (0, child_process_1.execSync)(createCmd, {
1501
+ stdio: 'pipe',
1502
+ cwd: process.cwd(),
1503
+ timeout: 300000, // 5 minute timeout
1504
+ });
1505
+ const projectPath = path.join(process.cwd(), projectName);
1506
+ // Run setup commands if any
1507
+ if (template.setupCommands && template.setupCommands.length > 0) {
1508
+ for (const cmd of template.setupCommands) {
1509
+ const setupCmd = cmd.replace(/\{\{name\}\}/g, projectName);
1510
+ spinner.text = `Running: ${setupCmd}`;
1511
+ try {
1512
+ (0, child_process_1.execSync)(setupCmd, {
1513
+ stdio: 'pipe',
1514
+ cwd: projectPath,
1515
+ timeout: 300000,
1516
+ });
1517
+ }
1518
+ catch {
1519
+ // Some setup commands may fail, continue
1520
+ }
1521
+ }
1522
+ }
1523
+ spinner.succeed(chalk_1.default.green(`Project scaffolded: ${projectName}`));
1524
+ return { success: true, projectPath };
1525
+ }
1526
+ catch (error) {
1527
+ spinner.fail(chalk_1.default.red(`Failed to scaffold project: ${error.message}`));
1528
+ if (error.message?.includes('command not found')) {
1529
+ console.log(chalk_1.default.yellow('\nMake sure the required tools are installed:'));
1530
+ if (template.createCommand?.includes('npx')) {
1531
+ console.log(chalk_1.default.dim(' - Node.js and npx'));
1532
+ }
1533
+ if (template.createCommand?.includes('bunx')) {
1534
+ console.log(chalk_1.default.dim(' - Bun (https://bun.sh)'));
1535
+ }
1536
+ if (template.repoUrl) {
1537
+ console.log(chalk_1.default.dim(' - Git'));
1538
+ }
1539
+ }
1540
+ return { success: false, error: error.message };
1541
+ }
1542
+ }
1543
+ /**
1544
+ * Create a local Docker genbox from a starter template
1545
+ */
1546
+ async function createLocalDockerFromTemplate(options) {
1547
+ console.log('');
1548
+ console.log(chalk_1.default.blue('No genbox.yaml found - creating Docker genbox from template...'));
1549
+ console.log('');
1550
+ // Fetch available templates
1551
+ let templates = [];
1552
+ try {
1553
+ templates = await (0, api_1.fetchApi)('/genboxes/starter-templates');
1554
+ }
1555
+ catch (error) {
1556
+ console.log(chalk_1.default.yellow('Could not fetch templates, using default setup.'));
1557
+ // Continue without template selection
1558
+ }
1559
+ // Template selection if templates available
1560
+ let template;
1561
+ if (templates.length > 0 && !options.skipPrompts) {
1562
+ const templateChoices = templates.map(t => ({
1563
+ name: `${t.icon} ${t.displayName} - ${chalk_1.default.dim(t.description)}`,
1564
+ value: t.name,
1565
+ }));
1566
+ const selectedTemplate = await (0, select_1.default)({
1567
+ message: 'Select a template:',
1568
+ choices: templateChoices,
1569
+ pageSize: 15,
1570
+ });
1571
+ template = templates.find(t => t.name === selectedTemplate);
1572
+ }
1573
+ // Get name
1574
+ const defaultName = template
1575
+ ? `my-${template.name}-app`
1576
+ : (options.provider ? `${options.provider}-${Date.now().toString(36)}` : `genbox-${Date.now().toString(36)}`);
1577
+ const name = options.name || await promptForName(defaultName);
1578
+ if (!name) {
1579
+ console.log(chalk_1.default.dim('\nCancelled.'));
1580
+ return { success: false, error: 'Cancelled' };
1581
+ }
1582
+ if (template) {
1583
+ console.log('');
1584
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1585
+ console.log(` ${chalk_1.default.bold('Template:')} ${template.icon} ${template.displayName}`);
1586
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1587
+ console.log(` ${chalk_1.default.bold('Features:')} ${template.features.join(', ')}`);
1588
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1589
+ console.log('');
1590
+ // Scaffold the project from template
1591
+ const scaffoldResult = await scaffoldTemplate(template, name);
1592
+ if (!scaffoldResult.success) {
1593
+ return { success: false, error: scaffoldResult.error };
1594
+ }
1595
+ // Change to the scaffolded project directory before creating genbox
1596
+ process.chdir(scaffoldResult.projectPath);
1597
+ console.log(chalk_1.default.dim(`Working directory: ${scaffoldResult.projectPath}`));
1598
+ }
1599
+ return await createLocalDockerGenbox(name, options.provider || 'claude');
1600
+ }
1601
+ /**
1602
+ * Create a local VM genbox from a project with genbox.yaml
1603
+ */
1604
+ async function createLocalVmFromProject(config, options) {
1605
+ console.log('');
1606
+ console.log(chalk_1.default.blue('Creating VM genbox from project...'));
1607
+ console.log(chalk_1.default.dim(`Project: ${config.project?.name || 'unknown'}`));
1608
+ console.log('');
1609
+ // Profile selection if available
1610
+ let selectedProfile;
1611
+ if (config.profiles && Object.keys(config.profiles).length > 0 && !options.skipPrompts) {
1612
+ const profileNames = Object.keys(config.profiles);
1613
+ const choices = profileNames.map(name => {
1614
+ const profile = config.profiles[name];
1615
+ const apps = profile.apps ? profile.apps.join(', ') : 'default apps';
1616
+ const description = profile.description || `Apps: ${apps}`;
1617
+ return {
1618
+ name: `${name} - ${chalk_1.default.dim(description)}`,
1619
+ value: name,
1620
+ };
1621
+ });
1622
+ choices.push({
1623
+ name: `${chalk_1.default.cyan('Custom')} - ${chalk_1.default.dim('Use default configuration')}`,
1624
+ value: '__custom__',
1625
+ });
1626
+ selectedProfile = await (0, select_1.default)({
1627
+ message: 'Select a profile:',
1628
+ choices,
1629
+ });
1630
+ if (selectedProfile === '__custom__') {
1631
+ selectedProfile = undefined;
1632
+ }
1633
+ }
1634
+ // Get name
1635
+ const defaultName = options.provider
1636
+ ? `${options.provider}-${Date.now().toString(36)}`
1637
+ : `genbox-${Date.now().toString(36)}`;
1638
+ const name = options.name || await promptForName(defaultName);
1639
+ if (!name) {
1640
+ console.log(chalk_1.default.dim('\nCancelled.'));
1641
+ return { success: false, error: 'Cancelled' };
1642
+ }
1643
+ console.log('');
1644
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1645
+ console.log(` ${chalk_1.default.bold('Project:')} ${config.project?.name || 'unknown'}`);
1646
+ if (selectedProfile) {
1647
+ console.log(` ${chalk_1.default.bold('Profile:')} ${selectedProfile}`);
1648
+ }
1649
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1650
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1651
+ console.log('');
1652
+ return await createLocalVmGenbox(name, options.provider || 'claude');
1653
+ }
1654
+ /**
1655
+ * Create a local VM genbox from a starter template
1656
+ */
1657
+ async function createLocalVmFromTemplate(options) {
1658
+ console.log('');
1659
+ console.log(chalk_1.default.blue('No genbox.yaml found - creating VM genbox from template...'));
1660
+ console.log('');
1661
+ // Fetch available templates
1662
+ let templates = [];
1663
+ try {
1664
+ templates = await (0, api_1.fetchApi)('/genboxes/starter-templates');
1665
+ }
1666
+ catch (error) {
1667
+ console.log(chalk_1.default.yellow('Could not fetch templates, using default setup.'));
1668
+ // Continue without template selection
1669
+ }
1670
+ // Template selection if templates available
1671
+ let template;
1672
+ if (templates.length > 0 && !options.skipPrompts) {
1673
+ const templateChoices = templates.map(t => ({
1674
+ name: `${t.icon} ${t.displayName} - ${chalk_1.default.dim(t.description)}`,
1675
+ value: t.name,
1676
+ }));
1677
+ const selectedTemplate = await (0, select_1.default)({
1678
+ message: 'Select a template:',
1679
+ choices: templateChoices,
1680
+ pageSize: 15,
1681
+ });
1682
+ template = templates.find(t => t.name === selectedTemplate);
1683
+ }
1684
+ // Get name
1685
+ const defaultName = template
1686
+ ? `my-${template.name}-app`
1687
+ : (options.provider ? `${options.provider}-${Date.now().toString(36)}` : `genbox-${Date.now().toString(36)}`);
1688
+ const name = options.name || await promptForName(defaultName);
1689
+ if (!name) {
1690
+ console.log(chalk_1.default.dim('\nCancelled.'));
1691
+ return { success: false, error: 'Cancelled' };
1692
+ }
1693
+ if (template) {
1694
+ console.log('');
1695
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1696
+ console.log(` ${chalk_1.default.bold('Template:')} ${template.icon} ${template.displayName}`);
1697
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1698
+ console.log(` ${chalk_1.default.bold('Features:')} ${template.features.join(', ')}`);
1699
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1700
+ console.log('');
1701
+ // Scaffold the project from template
1702
+ const scaffoldResult = await scaffoldTemplate(template, name);
1703
+ if (!scaffoldResult.success) {
1704
+ return { success: false, error: scaffoldResult.error };
1705
+ }
1706
+ // Change to the scaffolded project directory before creating genbox
1707
+ process.chdir(scaffoldResult.projectPath);
1708
+ console.log(chalk_1.default.dim(`Working directory: ${scaffoldResult.projectPath}`));
1709
+ }
1710
+ return await createLocalVmGenbox(name, options.provider || 'claude');
1711
+ }
1356
1712
  /**
1357
1713
  * Run the full creation wizard
1358
1714
  *
1359
1715
  * Used by both `gb create` and provider commands for consistent UX.
1360
1716
  */
1361
1717
  async function runCreateWizard(options = {}) {
1718
+ // Step 0: Check if we're in a genbox project (has genbox.yaml)
1719
+ const configLoader = new config_loader_1.ConfigLoader();
1720
+ const loadResult = await configLoader.load();
1721
+ const hasGenboxYaml = !!loadResult.config;
1362
1722
  // Step 1: Determine location (cloud vs local)
1363
1723
  let location = options.location;
1364
1724
  if (!location && !options.skipPrompts) {
@@ -1383,51 +1743,227 @@ async function runCreateWizard(options = {}) {
1383
1743
  }
1384
1744
  // Step 2b: Handle local docker mode
1385
1745
  if (location === 'local-docker') {
1386
- // Get name for local genbox
1387
- const defaultName = options.provider
1388
- ? `${options.provider}-${Date.now().toString(36)}`
1389
- : `genbox-${Date.now().toString(36)}`;
1390
- const name = options.name || await promptForName(defaultName);
1391
- if (!name) {
1392
- console.log(chalk_1.default.dim('\nCancelled.'));
1393
- return { success: false, error: 'Cancelled' };
1746
+ // Check for project or template mode
1747
+ if (hasGenboxYaml) {
1748
+ return await createLocalDockerFromProject(loadResult.config, options);
1749
+ }
1750
+ else {
1751
+ return await createLocalDockerFromTemplate(options);
1394
1752
  }
1395
- return await createLocalDockerGenbox(name, options.provider || 'claude');
1396
1753
  }
1397
1754
  // Step 2c: Handle local VM (multipass) mode
1398
1755
  if (location === 'local-vm') {
1399
- // Get name for local genbox
1400
- const defaultName = options.provider
1401
- ? `${options.provider}-${Date.now().toString(36)}`
1402
- : `genbox-${Date.now().toString(36)}`;
1403
- const name = options.name || await promptForName(defaultName);
1404
- if (!name) {
1405
- console.log(chalk_1.default.dim('\nCancelled.'));
1406
- return { success: false, error: 'Cancelled' };
1756
+ // Check for project or template mode
1757
+ if (hasGenboxYaml) {
1758
+ return await createLocalVmFromProject(loadResult.config, options);
1407
1759
  }
1408
- return await createLocalVmGenbox(name, options.provider || 'claude');
1409
- }
1410
- // Step 3: Cloud mode - get name
1411
- let name = options.name;
1412
- if (!name && !options.skipPrompts) {
1413
- const defaultName = options.provider
1414
- ? `${options.provider}-${Date.now().toString(36)}`
1415
- : `genbox-${Date.now().toString(36)}`;
1416
- const inputName = await promptForName(defaultName);
1417
- if (!inputName) {
1418
- console.log(chalk_1.default.dim('\nCancelled.'));
1419
- return { success: false, error: 'Cancelled' };
1760
+ else {
1761
+ return await createLocalVmFromTemplate(options);
1420
1762
  }
1421
- name = inputName;
1422
1763
  }
1423
- // Generate name if skipping prompts
1424
- name = name || (options.provider
1425
- ? `${options.provider}-${Date.now().toString(36)}`
1426
- : `genbox-${Date.now().toString(36)}`);
1427
- // Step 4: Create cloud genbox
1764
+ // Step 3: Cloud mode - determine project type
1765
+ if (hasGenboxYaml) {
1766
+ // Project mode: use profile selection (like gb create)
1767
+ return await createCloudGenboxFromProject(loadResult.config, configLoader, options);
1768
+ }
1769
+ else {
1770
+ // Template mode: use template selection (like gb new)
1771
+ return await createCloudGenboxFromTemplate(options);
1772
+ }
1773
+ }
1774
+ /**
1775
+ * Create a cloud genbox from a project with genbox.yaml
1776
+ * Uses profile selection and ProfileResolver similar to `gb create`
1777
+ */
1778
+ async function createCloudGenboxFromProject(config, configLoader, options) {
1779
+ console.log('');
1780
+ console.log(chalk_1.default.blue('Creating genbox from project...'));
1781
+ console.log(chalk_1.default.dim(`Project: ${config.project?.name || 'unknown'}`));
1782
+ console.log('');
1783
+ // Profile selection if available
1784
+ let selectedProfile;
1785
+ const profiles = config.profiles || {};
1786
+ if (Object.keys(profiles).length > 0 && !options.skipPrompts) {
1787
+ const profileNames = Object.keys(profiles);
1788
+ const choices = profileNames.map(name => {
1789
+ const profile = profiles[name];
1790
+ const apps = profile.apps ? profile.apps.join(', ') : 'default apps';
1791
+ const description = profile.description || `Apps: ${apps}`;
1792
+ return {
1793
+ name: `${name} - ${chalk_1.default.dim(description)}`,
1794
+ value: name,
1795
+ };
1796
+ });
1797
+ choices.push({
1798
+ name: `${chalk_1.default.cyan('Custom')} - ${chalk_1.default.dim('Use default configuration')}`,
1799
+ value: '__custom__',
1800
+ });
1801
+ selectedProfile = await (0, select_1.default)({
1802
+ message: 'Select a profile:',
1803
+ choices,
1804
+ });
1805
+ if (selectedProfile === '__custom__') {
1806
+ selectedProfile = undefined;
1807
+ }
1808
+ }
1809
+ // Get name
1810
+ const name = options.name || await promptForName();
1811
+ if (!name) {
1812
+ console.log(chalk_1.default.dim('\nCancelled.'));
1813
+ return { success: false, error: 'Cancelled' };
1814
+ }
1815
+ // Use ProfileResolver to resolve full configuration (including repos)
1816
+ const profileResolver = new profile_resolver_1.ProfileResolver(configLoader);
1817
+ const createOptions = {
1818
+ name,
1819
+ profile: selectedProfile,
1820
+ yes: options.skipPrompts,
1821
+ };
1822
+ const resolved = await profileResolver.resolve(config, createOptions);
1823
+ // Display resolved configuration
1824
+ console.log('');
1825
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1826
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1827
+ console.log(` ${chalk_1.default.bold('Project:')} ${resolved.project.name}`);
1828
+ console.log(` ${chalk_1.default.bold('Size:')} ${resolved.size}`);
1829
+ if (selectedProfile) {
1830
+ console.log(` ${chalk_1.default.bold('Profile:')} ${selectedProfile}`);
1831
+ }
1832
+ if (resolved.apps.length > 0) {
1833
+ console.log(` ${chalk_1.default.bold('Apps:')} ${resolved.apps.map(a => a.name).join(', ')}`);
1834
+ }
1835
+ if (resolved.repos.length > 0) {
1836
+ console.log(` ${chalk_1.default.bold('Repos:')} ${resolved.repos.map(r => r.name).join(', ')}`);
1837
+ }
1838
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1839
+ console.log('');
1840
+ // Build repos payload
1841
+ const repos = {};
1842
+ for (const repo of resolved.repos) {
1843
+ repos[repo.name] = {
1844
+ url: repo.url,
1845
+ path: repo.path,
1846
+ branch: repo.branch || config.defaults?.branch || 'main',
1847
+ };
1848
+ }
1849
+ // Get git config for commits
1850
+ const gitConfig = (0, utils_1.getGitConfig)();
1851
+ // Load env vars for GIT_TOKEN
1852
+ const envVars = configLoader.loadEnvVars(process.cwd());
1853
+ // Build full payload
1854
+ const payload = {
1855
+ profile: selectedProfile,
1856
+ workspace: resolved.project.name,
1857
+ size: resolved.size,
1858
+ repos,
1859
+ apps: resolved.apps.map(a => a.name),
1860
+ appConfigs: resolved.apps.map(a => ({
1861
+ name: a.name,
1862
+ path: a.path.startsWith('/') ? a.path : `${resolved.repos[0]?.path || '/home/dev'}/${a.path}`,
1863
+ type: a.type,
1864
+ port: a.port,
1865
+ framework: a.framework,
1866
+ runner: a.runner,
1867
+ docker: a.docker,
1868
+ healthcheck: a.healthcheck,
1869
+ dependsOn: a.dependsOn,
1870
+ commands: a.commands,
1871
+ })),
1872
+ infrastructure: resolved.infrastructure.map(i => ({
1873
+ name: i.name,
1874
+ type: i.type,
1875
+ mode: i.mode,
1876
+ })),
1877
+ database: resolved.database,
1878
+ gitToken: envVars.GIT_TOKEN,
1879
+ gitUserName: gitConfig.userName,
1880
+ gitUserEmail: gitConfig.userEmail,
1881
+ };
1882
+ // Add provider-specific CLI installation
1883
+ if (options.provider === 'claude') {
1884
+ payload.installClaudeCode = true;
1885
+ }
1886
+ else if (options.provider === 'gemini') {
1887
+ payload.installGeminiCli = true;
1888
+ }
1889
+ return await createCloudGenbox(name, {
1890
+ detach: options.detach,
1891
+ payload: { ...options.payload, ...payload },
1892
+ provider: options.provider,
1893
+ });
1894
+ }
1895
+ /**
1896
+ * Create a cloud genbox from a starter template
1897
+ * Uses template selection similar to `gb new`
1898
+ */
1899
+ async function createCloudGenboxFromTemplate(options) {
1900
+ console.log('');
1901
+ console.log(chalk_1.default.blue('No genbox.yaml found - creating from template...'));
1902
+ console.log('');
1903
+ // Fetch available templates
1904
+ let templates = [];
1905
+ try {
1906
+ templates = await (0, api_1.fetchApi)('/genboxes/starter-templates');
1907
+ }
1908
+ catch (error) {
1909
+ console.log(chalk_1.default.red('Failed to fetch templates:', error.message));
1910
+ return { success: false, error: 'Failed to fetch templates' };
1911
+ }
1912
+ if (templates.length === 0) {
1913
+ console.log(chalk_1.default.red('No templates available.'));
1914
+ return { success: false, error: 'No templates available' };
1915
+ }
1916
+ // Template selection
1917
+ let template;
1918
+ if (!options.skipPrompts) {
1919
+ const templateChoices = templates.map(t => ({
1920
+ name: `${t.icon} ${t.displayName} - ${chalk_1.default.dim(t.description)}`,
1921
+ value: t.name,
1922
+ }));
1923
+ const selectedTemplate = await (0, select_1.default)({
1924
+ message: 'Select a template:',
1925
+ choices: templateChoices,
1926
+ pageSize: 15,
1927
+ });
1928
+ template = templates.find(t => t.name === selectedTemplate);
1929
+ }
1930
+ else {
1931
+ // Default to nextjs in non-interactive mode
1932
+ template = templates.find(t => t.name === 'nextjs') || templates[0];
1933
+ console.log(chalk_1.default.dim(`Using template: ${template?.displayName}`));
1934
+ }
1935
+ if (!template) {
1936
+ console.log(chalk_1.default.red('No template selected.'));
1937
+ return { success: false, error: 'No template selected' };
1938
+ }
1939
+ // Get name
1940
+ const defaultName = `my-${template.name}-app`;
1941
+ const name = options.name || await promptForName(defaultName);
1942
+ if (!name) {
1943
+ console.log(chalk_1.default.dim('\nCancelled.'));
1944
+ return { success: false, error: 'Cancelled' };
1945
+ }
1946
+ // Build payload with template
1947
+ const payload = {
1948
+ template: template.name,
1949
+ };
1950
+ // Add provider-specific CLI installation
1951
+ if (options.provider === 'claude') {
1952
+ payload.installClaudeCode = true;
1953
+ }
1954
+ else if (options.provider === 'gemini') {
1955
+ payload.installGeminiCli = true;
1956
+ }
1957
+ console.log('');
1958
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1959
+ console.log(` ${chalk_1.default.bold('Template:')} ${template.icon} ${template.displayName}`);
1960
+ console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
1961
+ console.log(` ${chalk_1.default.bold('Features:')} ${template.features.join(', ')}`);
1962
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
1963
+ console.log('');
1428
1964
  return await createCloudGenbox(name, {
1429
1965
  detach: options.detach,
1430
- payload: options.payload,
1966
+ payload: { ...options.payload, ...payload },
1431
1967
  provider: options.provider,
1432
1968
  });
1433
1969
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.200",
3
+ "version": "1.0.202",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {