genbox 1.0.207 → 1.0.209

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.
@@ -42,6 +42,7 @@ const chalk_1 = __importDefault(require("chalk"));
42
42
  const api_1 = require("../api");
43
43
  const genbox_selector_1 = require("../genbox-selector");
44
44
  const ssh_config_1 = require("../ssh-config");
45
+ const ssh_proxy_1 = require("../utils/ssh-proxy");
45
46
  const os = __importStar(require("os"));
46
47
  const path = __importStar(require("path"));
47
48
  const fs = __importStar(require("fs"));
@@ -85,14 +86,14 @@ exports.connectCommand = new commander_1.Command('connect')
85
86
  (0, ssh_config_1.addSshConfigEntry)({ name: localSession.name, ipAddress: localSession.ipAddress });
86
87
  }
87
88
  console.log(chalk_1.default.dim(`Connecting to local VM ${chalk_1.default.bold(localSession.name)} (${localSession.ipAddress})...`));
89
+ console.log(chalk_1.default.dim(` Image auto-upload: ${chalk_1.default.green('enabled')} - Paste local image paths, they'll be uploaded automatically`));
88
90
  console.log(chalk_1.default.dim(` You can also use: ssh genbox-${localSession.name}`));
89
- const sshArgs = [
90
- '-i', keyPath,
91
- '-o', 'StrictHostKeyChecking=no',
92
- '-o', 'UserKnownHostsFile=/dev/null',
93
- `dev@${localSession.ipAddress}`
94
- ];
95
- const ssh = (0, child_process_1.spawn)('ssh', sshArgs, { stdio: 'inherit' });
91
+ // Use SSH proxy with input interception for automatic image uploads
92
+ const ssh = (0, ssh_proxy_1.createSshProxy)({
93
+ ipAddress: localSession.ipAddress,
94
+ keyPath,
95
+ genboxName: localSession.name,
96
+ });
96
97
  ssh.on('close', (code) => {
97
98
  if (code !== 0) {
98
99
  console.log(chalk_1.default.dim(`Connection closed with code ${code}`));
@@ -129,15 +130,14 @@ exports.connectCommand = new commander_1.Command('connect')
129
130
  }
130
131
  // 3. Get Key
131
132
  const keyPath = getPrivateSshKey();
132
- // 4. Connect
133
+ // 4. Connect via SSH proxy with input interception for automatic image uploads
133
134
  console.log(chalk_1.default.dim(`Connecting to ${chalk_1.default.bold(target.name)} (${target.ipAddress})...`));
134
- const sshArgs = [
135
- '-i', keyPath,
136
- '-o', 'StrictHostKeyChecking=no',
137
- '-o', 'UserKnownHostsFile=/dev/null',
138
- `dev@${target.ipAddress}`
139
- ];
140
- const ssh = (0, child_process_1.spawn)('ssh', sshArgs, { stdio: 'inherit' });
135
+ console.log(chalk_1.default.dim(` Image auto-upload: ${chalk_1.default.green('enabled')} - Paste local image paths, they'll be uploaded automatically`));
136
+ const ssh = (0, ssh_proxy_1.createSshProxy)({
137
+ ipAddress: target.ipAddress,
138
+ keyPath,
139
+ genboxName: target.name,
140
+ });
141
141
  ssh.on('close', (code) => {
142
142
  if (code !== 0) {
143
143
  console.log(chalk_1.default.dim(`Connection closed with code ${code}`));
@@ -58,9 +58,11 @@ const select_1 = __importDefault(require("@inquirer/select"));
58
58
  const prompts_1 = require("@inquirer/prompts");
59
59
  const api_1 = require("../api");
60
60
  const ssh_keys_1 = require("../utils/ssh-keys");
61
+ const ssh_proxy_1 = require("../utils/ssh-proxy");
61
62
  const unified_session_1 = require("../lib/unified-session");
62
63
  const genbox_progress_1 = require("../lib/genbox-progress");
63
64
  const genbox_wizard_1 = require("../lib/genbox-wizard");
65
+ const interactive_prompt_1 = require("../utils/interactive-prompt");
64
66
  const child_process_1 = require("child_process");
65
67
  const os = __importStar(require("os"));
66
68
  const path = __importStar(require("path"));
@@ -441,7 +443,8 @@ async function startLocalMultipass(genbox) {
441
443
  /**
442
444
  * Create and attach to a session on a genbox
443
445
  */
444
- async function startSessionOnGenbox(provider, genbox) {
446
+ async function startSessionOnGenbox(provider, genbox, options = {}) {
447
+ const { initialPrompt } = options;
445
448
  let targetGenbox = genbox;
446
449
  let sshTarget;
447
450
  let sshPortArgs = [];
@@ -481,6 +484,8 @@ async function startSessionOnGenbox(provider, genbox) {
481
484
  }
482
485
  const keyPath = requireSshKey();
483
486
  const sessionName = `${provider}-${Date.now().toString(36)}`;
487
+ // Always start CLI without prompt - we'll inject it after attaching
488
+ // This keeps the session interactive instead of one-shot mode
484
489
  const cliCommand = provider === 'claude'
485
490
  ? 'claude --dangerously-skip-permissions'
486
491
  : provider === 'gemini'
@@ -587,19 +592,23 @@ exec ${cliCommand}
587
592
  // Wait for session to start
588
593
  await new Promise(resolve => setTimeout(resolve, 1000));
589
594
  console.log(chalk_1.default.green(`Session created: ${sessionName}`));
595
+ console.log(chalk_1.default.dim(`Image auto-upload: ${chalk_1.default.green('enabled')} - Paste local image paths, they'll be uploaded automatically`));
590
596
  console.log(chalk_1.default.dim('Tip: Detach with Ctrl+\\\n'));
591
- // Attach to session
592
- const sshArgs = [
593
- '-t',
594
- ...sshPortArgs,
595
- '-i', keyPath,
596
- '-o', 'StrictHostKeyChecking=no',
597
- '-o', 'UserKnownHostsFile=/dev/null',
598
- '-o', 'LogLevel=ERROR',
599
- sshTarget,
600
- `dtach -a ${socketPath}`,
601
- ];
602
- const proc = (0, child_process_1.spawn)('ssh', sshArgs, { stdio: 'inherit' });
597
+ // Attach to session using SSH proxy with image path interception
598
+ // Extract IP address from sshTarget (format: dev@IP or dev@localhost)
599
+ const attachIpAddress = sshTarget.split('@')[1];
600
+ // Get port from sshPortArgs if it's a Docker container
601
+ const sshPort = sshPortArgs.length >= 2 ? parseInt(sshPortArgs[1], 10) : undefined;
602
+ const proc = (0, ssh_proxy_1.attachWithProxy)({
603
+ ipAddress: attachIpAddress === 'localhost' ? '127.0.0.1' : attachIpAddress,
604
+ keyPath,
605
+ genboxName: targetGenbox.name,
606
+ socketPath,
607
+ user: 'dev',
608
+ port: sshPort,
609
+ verbose: true,
610
+ initialPrompt, // Inject prompt after connection
611
+ });
603
612
  await new Promise(resolve => proc.on('close', () => resolve()));
604
613
  console.log(chalk_1.default.dim('\nDetached from session.'));
605
614
  console.log(chalk_1.default.dim(`Reattach with: ${chalk_1.default.cyan(`gb ${provider} --attach ${sessionName}`)}`));
@@ -618,22 +627,20 @@ async function attachToSession(session) {
618
627
  console.log(chalk_1.default.dim('\nDetached from session.'));
619
628
  }
620
629
  else {
621
- // Cloud genbox session
630
+ // Cloud genbox session - use SSH proxy with image path interception
622
631
  const keyPath = requireSshKey();
623
632
  const remoteSocketDir = getRemoteDtachSocketDir();
624
633
  const socketPath = `${remoteSocketDir}/${session.name}.sock`;
625
634
  console.log(chalk_1.default.dim(`\nAttaching to ${session.name} on ${session.genboxName}...`));
635
+ console.log(chalk_1.default.dim(`Image auto-upload: ${chalk_1.default.green('enabled')} - Paste local image paths, they'll be uploaded automatically`));
626
636
  console.log(chalk_1.default.dim('Tip: Detach with Ctrl+\\\n'));
627
- const sshArgs = [
628
- '-t',
629
- '-i', keyPath,
630
- '-o', 'StrictHostKeyChecking=no',
631
- '-o', 'UserKnownHostsFile=/dev/null',
632
- '-o', 'LogLevel=ERROR',
633
- `dev@${session.ipAddress}`,
634
- `dtach -a ${socketPath}`,
635
- ];
636
- const proc = (0, child_process_1.spawn)('ssh', sshArgs, { stdio: 'inherit' });
637
+ const proc = (0, ssh_proxy_1.attachWithProxy)({
638
+ ipAddress: session.ipAddress,
639
+ keyPath,
640
+ genboxName: session.genboxName,
641
+ socketPath,
642
+ user: 'dev',
643
+ });
637
644
  await new Promise(resolve => proc.on('close', () => resolve()));
638
645
  console.log(chalk_1.default.dim('\nDetached from session.'));
639
646
  }
@@ -675,7 +682,8 @@ async function killSession(session) {
675
682
  /**
676
683
  * Start a direct (no isolation) session
677
684
  */
678
- async function startDirectSession(provider) {
685
+ async function startDirectSession(provider, options = {}) {
686
+ const { initialPrompt } = options;
679
687
  // Check for dtach
680
688
  const dtachStatus = await ensureLocalDtach();
681
689
  if (dtachStatus === 'cancel') {
@@ -693,6 +701,7 @@ async function startDirectSession(provider) {
693
701
  syncEnabled: false, // Local-first: no API sync by default
694
702
  });
695
703
  const sessionName = session.name;
704
+ // Always start CLI without prompt - inject after connection
696
705
  const cliCommand = provider === 'claude'
697
706
  ? 'claude --dangerously-skip-permissions'
698
707
  : provider === 'gemini'
@@ -722,7 +731,39 @@ async function startDirectSession(provider) {
722
731
  }
723
732
  // Mark session as running
724
733
  await sessionManager.markRunning(session.id);
725
- const proc = (0, child_process_1.spawn)('bash', ['-c', cmd], { stdio: 'inherit' });
734
+ // For direct mode, we need to pipe stdin to inject the prompt
735
+ const proc = (0, child_process_1.spawn)('bash', ['-c', cmd], {
736
+ stdio: initialPrompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
737
+ });
738
+ // Inject initial prompt after CLI is ready
739
+ if (initialPrompt && proc.stdin) {
740
+ setTimeout(() => {
741
+ if (proc.stdin && !proc.killed) {
742
+ // Send prompt character by character with slower typing
743
+ const chars = initialPrompt.split('');
744
+ let i = 0;
745
+ const typeChar = () => {
746
+ if (i < chars.length && proc.stdin && !proc.killed) {
747
+ proc.stdin.write(chars[i]);
748
+ i++;
749
+ setTimeout(typeChar, 15); // 15ms between chars (slower, more reliable)
750
+ }
751
+ else if (proc.stdin && !proc.killed) {
752
+ // All chars sent, press Enter, then pipe user input
753
+ setTimeout(() => {
754
+ proc.stdin?.write('\r');
755
+ // Continue piping user input after prompt is sent
756
+ setTimeout(() => {
757
+ process.stdin.pipe(proc.stdin);
758
+ process.stdin.resume();
759
+ }, 200);
760
+ }, 100);
761
+ }
762
+ };
763
+ typeChar();
764
+ }
765
+ }, 3000); // 3 second delay for local sessions
766
+ }
726
767
  await new Promise((resolve) => {
727
768
  proc.on('close', () => {
728
769
  if (useDtach && fs.existsSync(socketPath)) {
@@ -892,6 +933,179 @@ async function runInteractiveFlow(provider) {
892
933
  break;
893
934
  }
894
935
  }
936
+ /**
937
+ * Interactive prompt mode flow (-p flag)
938
+ *
939
+ * This flow:
940
+ * 1. Collects multiline prompt (inline or via $EDITOR with -e)
941
+ * 2. Collects image paths (optional)
942
+ * 3. Shows target selection (genbox vs direct)
943
+ * 4. Uploads images to genbox if needed (waits for genbox to be ready)
944
+ * 5. Starts session with the composed prompt
945
+ */
946
+ async function runPromptModeFlow(provider, options = {}) {
947
+ const { useEditor = false } = options;
948
+ console.log(chalk_1.default.bold(`\n${provider.charAt(0).toUpperCase() + provider.slice(1)} - Interactive Prompt Mode`));
949
+ if (useEditor) {
950
+ console.log(chalk_1.default.dim('Compose your prompt in $EDITOR with optional images\n'));
951
+ }
952
+ else {
953
+ console.log(chalk_1.default.dim('Compose your prompt with optional images\n'));
954
+ }
955
+ // Step 1: Collect prompt and images
956
+ const collected = await (0, interactive_prompt_1.collectPromptWithImages)({ useEditor });
957
+ if (!collected) {
958
+ console.log(chalk_1.default.dim('\nCancelled.'));
959
+ return;
960
+ }
961
+ const { text, images } = collected;
962
+ // Show what was collected
963
+ console.log(chalk_1.default.dim(`\nPrompt: ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`));
964
+ if (images.length > 0) {
965
+ console.log(chalk_1.default.dim(`Images: ${images.length} file(s)`));
966
+ }
967
+ // Step 2: Select target (simplified menu since we have a specific prompt)
968
+ console.log(chalk_1.default.dim('\nFetching genboxes...'));
969
+ const genboxes = await getGenboxes(true);
970
+ const targetChoices = [];
971
+ // Running genboxes first (can upload immediately)
972
+ const runningGenboxes = genboxes.filter(g => g.status === 'running' && g.ipAddress);
973
+ const stoppedGenboxes = genboxes.filter(g => g.status === 'stopped');
974
+ if (runningGenboxes.length > 0) {
975
+ for (const g of runningGenboxes) {
976
+ const localLabel = g._isLocal ? chalk_1.default.yellow(' (local)') : '';
977
+ targetChoices.push({
978
+ name: `${chalk_1.default.green('●')} ${g.name}${localLabel} ${chalk_1.default.dim('- ready, can upload immediately')}`,
979
+ value: { type: 'genbox', genbox: g },
980
+ });
981
+ }
982
+ }
983
+ if (stoppedGenboxes.length > 0) {
984
+ for (const g of stoppedGenboxes) {
985
+ const localLabel = g._isLocal ? chalk_1.default.yellow(' (local)') : '';
986
+ targetChoices.push({
987
+ name: `${chalk_1.default.yellow('○')} ${g.name}${localLabel} ${chalk_1.default.dim('- will start, then upload')}`,
988
+ value: { type: 'genbox', genbox: g },
989
+ });
990
+ }
991
+ }
992
+ // Create new option
993
+ targetChoices.push({
994
+ name: chalk_1.default.dim('─────────────────────────────────────'),
995
+ value: { type: 'cancel' },
996
+ disabled: true,
997
+ });
998
+ targetChoices.push({
999
+ name: chalk_1.default.green('+') + ' Create new Genbox ' + chalk_1.default.dim('(will upload after creation)'),
1000
+ value: { type: 'create-genbox' },
1001
+ });
1002
+ // Direct option (no upload needed)
1003
+ if (images.length > 0) {
1004
+ targetChoices.push({
1005
+ name: chalk_1.default.dim(' Run directly (images stay local)'),
1006
+ value: { type: 'direct' },
1007
+ });
1008
+ }
1009
+ else {
1010
+ targetChoices.push({
1011
+ name: chalk_1.default.dim(' Run directly on this machine'),
1012
+ value: { type: 'direct' },
1013
+ });
1014
+ }
1015
+ targetChoices.push({
1016
+ name: chalk_1.default.dim(' Cancel'),
1017
+ value: { type: 'cancel' },
1018
+ });
1019
+ let targetAction;
1020
+ try {
1021
+ targetAction = await (0, select_1.default)({
1022
+ message: 'Where should this run?',
1023
+ choices: targetChoices.filter(c => !c.disabled),
1024
+ });
1025
+ }
1026
+ catch {
1027
+ console.log(chalk_1.default.dim('\nCancelled.'));
1028
+ return;
1029
+ }
1030
+ // Step 3: Handle based on target
1031
+ switch (targetAction.type) {
1032
+ case 'genbox': {
1033
+ const genbox = targetAction.genbox;
1034
+ let finalPrompt;
1035
+ if (images.length > 0) {
1036
+ // Upload images to genbox (waits for ready if needed)
1037
+ const uploadResult = await (0, interactive_prompt_1.uploadImagesToGenbox)(images, genbox, {
1038
+ showProgress: true,
1039
+ waitForReady: true,
1040
+ });
1041
+ if (uploadResult.errors.length > 0 && uploadResult.remotePaths.length === 0) {
1042
+ console.log(chalk_1.default.yellow('\nWarning: Could not upload images. Proceeding with prompt only.'));
1043
+ finalPrompt = text;
1044
+ }
1045
+ else {
1046
+ if (uploadResult.errors.length > 0) {
1047
+ console.log(chalk_1.default.yellow(`\nWarning: ${uploadResult.errors.length} image(s) failed to upload.`));
1048
+ }
1049
+ finalPrompt = (0, interactive_prompt_1.buildPromptWithImages)(text, uploadResult.remotePaths, { isRemote: true });
1050
+ }
1051
+ }
1052
+ else {
1053
+ finalPrompt = text;
1054
+ }
1055
+ // Start session with prompt
1056
+ await startSessionOnGenbox(provider, genbox, { initialPrompt: finalPrompt });
1057
+ break;
1058
+ }
1059
+ case 'create-genbox': {
1060
+ // Use wizard to create genbox
1061
+ const result = await (0, genbox_wizard_1.runCreateWizard)({
1062
+ provider,
1063
+ includeDirect: false,
1064
+ });
1065
+ if (!result.success || !result.genbox) {
1066
+ console.log(chalk_1.default.dim('\nGenbox creation cancelled or failed.'));
1067
+ return;
1068
+ }
1069
+ const genbox = result.genbox;
1070
+ let finalPrompt;
1071
+ if (images.length > 0) {
1072
+ // Upload images to newly created genbox
1073
+ const uploadResult = await (0, interactive_prompt_1.uploadImagesToGenbox)(images, genbox, {
1074
+ showProgress: true,
1075
+ waitForReady: true, // Wizard already waits, but just in case
1076
+ });
1077
+ if (uploadResult.remotePaths.length > 0) {
1078
+ finalPrompt = (0, interactive_prompt_1.buildPromptWithImages)(text, uploadResult.remotePaths, { isRemote: true });
1079
+ }
1080
+ else {
1081
+ console.log(chalk_1.default.yellow('\nWarning: Could not upload images. Proceeding with prompt only.'));
1082
+ finalPrompt = text;
1083
+ }
1084
+ }
1085
+ else {
1086
+ finalPrompt = text;
1087
+ }
1088
+ await startSessionOnGenbox(provider, genbox, { initialPrompt: finalPrompt });
1089
+ break;
1090
+ }
1091
+ case 'direct': {
1092
+ const proceed = await showDirectModeWarning();
1093
+ if (!proceed) {
1094
+ console.log(chalk_1.default.dim('\nCancelled.'));
1095
+ return;
1096
+ }
1097
+ // Direct mode: use local paths
1098
+ const finalPrompt = images.length > 0
1099
+ ? (0, interactive_prompt_1.buildPromptWithImages)(text, images, { isRemote: false })
1100
+ : text;
1101
+ await startDirectSession(provider, { initialPrompt: finalPrompt });
1102
+ break;
1103
+ }
1104
+ case 'cancel':
1105
+ console.log(chalk_1.default.dim('\nCancelled.'));
1106
+ break;
1107
+ }
1108
+ }
895
1109
  /**
896
1110
  * Create list subcommand for a provider
897
1111
  */
@@ -1194,9 +1408,13 @@ function createProviderCommand(provider) {
1194
1408
  .option('--on <genbox>', 'Use specific genbox')
1195
1409
  .option('--direct', 'Run directly on this machine (no isolation)')
1196
1410
  .option('-y, --yes', 'Skip confirmations')
1411
+ .option('-p, --prompt-mode', 'Interactive prompt mode: compose prompt with optional images')
1412
+ .option('-e, --editor', 'Use $EDITOR for prompt input (with -p)')
1197
1413
  .addHelpText('after', `
1198
1414
  Examples:
1199
1415
  gb ${provider} Interactive mode
1416
+ gb ${provider} -p Prompt mode: inline multiline input
1417
+ gb ${provider} -p -e Prompt mode: open $EDITOR for input
1200
1418
  gb ${provider} list List all ${provider} sessions
1201
1419
  gb ${provider} attach [session] Attach to a session
1202
1420
  gb ${provider} stop [session] Stop a session
@@ -1207,6 +1425,11 @@ Examples:
1207
1425
  `)
1208
1426
  .action(async (options) => {
1209
1427
  try {
1428
+ // -p / --prompt-mode: Interactive prompt with optional images
1429
+ if (options.promptMode) {
1430
+ await runPromptModeFlow(provider, { useEditor: options.editor });
1431
+ return;
1432
+ }
1210
1433
  // --on: Use specific genbox
1211
1434
  if (options.on) {
1212
1435
  const genboxes = await getGenboxes();