genbox 1.0.180 → 1.0.182

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.
@@ -508,6 +508,226 @@ function getDtachSocketDir() {
508
508
  function getDtachSocketPath(sessionName) {
509
509
  return path.join(getDtachSocketDir(), `${sessionName}.sock`);
510
510
  }
511
+ /**
512
+ * Special key mappings (inspired by tmux's key-string.c)
513
+ * Maps key names to their terminal byte sequences
514
+ */
515
+ const SPECIAL_KEYS = {
516
+ 'Enter': '\r',
517
+ 'Return': '\r',
518
+ 'Tab': '\t',
519
+ 'Escape': '\x1b',
520
+ 'Space': ' ',
521
+ 'Backspace': '\x7f',
522
+ 'Delete': '\x1b[3~',
523
+ 'Up': '\x1b[A',
524
+ 'Down': '\x1b[B',
525
+ 'Right': '\x1b[C',
526
+ 'Left': '\x1b[D',
527
+ 'Home': '\x1b[H',
528
+ 'End': '\x1b[F',
529
+ 'PageUp': '\x1b[5~',
530
+ 'PageDown': '\x1b[6~',
531
+ 'Insert': '\x1b[2~',
532
+ 'F1': '\x1bOP',
533
+ 'F2': '\x1bOQ',
534
+ 'F3': '\x1bOR',
535
+ 'F4': '\x1bOS',
536
+ 'F5': '\x1b[15~',
537
+ 'F6': '\x1b[17~',
538
+ 'F7': '\x1b[18~',
539
+ 'F8': '\x1b[19~',
540
+ 'F9': '\x1b[20~',
541
+ 'F10': '\x1b[21~',
542
+ 'F11': '\x1b[23~',
543
+ 'F12': '\x1b[24~',
544
+ };
545
+ /**
546
+ * Convert control key notation to byte (e.g., C-c -> \x03)
547
+ * Follows tmux convention: C-a through C-z map to 0x01-0x1a
548
+ */
549
+ function parseControlKey(key) {
550
+ const match = key.match(/^[Cc]-([a-zA-Z@\[\]\\^_])$/);
551
+ if (!match)
552
+ return null;
553
+ const char = match[1].toLowerCase();
554
+ if (char >= 'a' && char <= 'z') {
555
+ return String.fromCharCode(char.charCodeAt(0) - 96); // a=1, z=26
556
+ }
557
+ // Special control chars
558
+ const specials = {
559
+ '@': 0, '[': 27, '\\': 28, ']': 29, '^': 30, '_': 31
560
+ };
561
+ if (specials[char] !== undefined) {
562
+ return String.fromCharCode(specials[char]);
563
+ }
564
+ return null;
565
+ }
566
+ /**
567
+ * Send keys to a dtach session (inspired by tmux's send-keys)
568
+ *
569
+ * This function writes directly to dtach's stdin, bypassing shell interpretation.
570
+ * Like tmux, it supports:
571
+ * - Literal text
572
+ * - Special keys (Enter, Tab, Escape, etc.)
573
+ * - Control sequences (C-c, C-d, etc.)
574
+ *
575
+ * @param socketPath - Path to the dtach socket
576
+ * @param keys - Array of keys/text to send. Each element can be:
577
+ * - A string of literal text
578
+ * - A special key name like "Enter", "Tab", "Escape"
579
+ * - A control sequence like "C-c"
580
+ * @param options - Optional: addNewline (append Enter after all keys)
581
+ */
582
+ function sendKeysToDtach(socketPath, keys, options = {}) {
583
+ // Build the byte sequence to send
584
+ let data = '';
585
+ for (const key of keys) {
586
+ // Check for special key
587
+ if (SPECIAL_KEYS[key]) {
588
+ data += SPECIAL_KEYS[key];
589
+ continue;
590
+ }
591
+ // Check for control key notation
592
+ const ctrlKey = parseControlKey(key);
593
+ if (ctrlKey) {
594
+ data += ctrlKey;
595
+ continue;
596
+ }
597
+ // Literal text
598
+ data += key;
599
+ }
600
+ // Optionally add newline
601
+ if (options.addNewline) {
602
+ data += '\r';
603
+ }
604
+ // Write directly to dtach via spawn (bypasses shell interpretation)
605
+ // This is the key insight from tmux: write bytes directly, don't go through shell
606
+ const proc = (0, child_process_1.spawnSync)('dtach', ['-p', socketPath], {
607
+ input: data,
608
+ timeout: 5000,
609
+ stdio: ['pipe', 'ignore', 'ignore'],
610
+ });
611
+ if (proc.error) {
612
+ throw new Error(`Failed to send keys: ${proc.error.message}`);
613
+ }
614
+ }
615
+ /**
616
+ * Send keys to a remote dtach session via SSH
617
+ * Uses the same direct-write approach as local, but through SSH stdin forwarding
618
+ */
619
+ function sendKeysToRemoteDtach(ipAddress, keyPath, socketPath, keys, options = {}) {
620
+ // Build the byte sequence
621
+ let data = '';
622
+ for (const key of keys) {
623
+ if (SPECIAL_KEYS[key]) {
624
+ data += SPECIAL_KEYS[key];
625
+ continue;
626
+ }
627
+ const ctrlKey = parseControlKey(key);
628
+ if (ctrlKey) {
629
+ data += ctrlKey;
630
+ continue;
631
+ }
632
+ data += key;
633
+ }
634
+ if (options.addNewline) {
635
+ data += '\r';
636
+ }
637
+ // SSH with stdin forwarding to dtach -p
638
+ const proc = (0, child_process_1.spawnSync)('ssh', [
639
+ '-i', keyPath,
640
+ '-o', 'StrictHostKeyChecking=no',
641
+ '-o', 'UserKnownHostsFile=/dev/null',
642
+ '-o', 'ConnectTimeout=5',
643
+ `dev@${ipAddress}`,
644
+ `dtach -p ${socketPath}`
645
+ ], {
646
+ input: data,
647
+ timeout: 30000,
648
+ stdio: ['pipe', 'ignore', 'ignore'],
649
+ });
650
+ if (proc.error) {
651
+ throw new Error(`Failed to send keys to remote: ${proc.error.message}`);
652
+ }
653
+ }
654
+ /**
655
+ * Legacy escape function for cases where shell command is unavoidable
656
+ * Handles special shell characters that could cause issues
657
+ */
658
+ function escapeForShell(str) {
659
+ return str
660
+ .replace(/\\/g, '\\\\') // Backslashes first
661
+ .replace(/'/g, "'\\''") // Single quotes
662
+ .replace(/\$/g, '\\$') // Dollar signs
663
+ .replace(/`/g, '\\`') // Backticks
664
+ .replace(/!/g, '\\!') // History expansion
665
+ .replace(/"/g, '\\"'); // Double quotes
666
+ }
667
+ /**
668
+ * Check if a dtach socket is actually alive (not just file exists)
669
+ * Attempts a brief connection to verify the process is running
670
+ */
671
+ function isDtachSocketAlive(socketPath) {
672
+ if (!fs.existsSync(socketPath)) {
673
+ return false;
674
+ }
675
+ try {
676
+ // Try to send an empty string to test if socket is alive
677
+ // Using timeout to avoid hanging on dead sockets
678
+ (0, child_process_1.execSync)(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
679
+ timeout: 2000,
680
+ stdio: 'ignore'
681
+ });
682
+ return true;
683
+ }
684
+ catch {
685
+ // Socket exists but is stale - clean it up
686
+ try {
687
+ fs.unlinkSync(socketPath);
688
+ console.log(chalk_1.default.dim(`Cleaned up stale socket: ${path.basename(socketPath)}`));
689
+ }
690
+ catch {
691
+ // Ignore cleanup errors
692
+ }
693
+ return false;
694
+ }
695
+ }
696
+ /**
697
+ * Check if a remote dtach socket is alive via SSH
698
+ */
699
+ function isRemoteDtachSocketAlive(ipAddress, keyPath, socketPath) {
700
+ try {
701
+ (0, child_process_1.execSync)(`ssh -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 dev@${ipAddress} "echo -n '' | dtach -p ${socketPath}" 2>/dev/null`, { timeout: 5000, stdio: 'ignore' });
702
+ return true;
703
+ }
704
+ catch {
705
+ return false;
706
+ }
707
+ }
708
+ /**
709
+ * Clean up all stale sockets in the local sockets directory
710
+ * Called on startup to ensure clean state
711
+ */
712
+ function cleanupAllStaleSockets() {
713
+ const socketsDir = getDtachSocketDir();
714
+ let cleaned = 0;
715
+ try {
716
+ const files = fs.readdirSync(socketsDir);
717
+ for (const file of files) {
718
+ if (file.endsWith('.sock')) {
719
+ const socketPath = path.join(socketsDir, file);
720
+ if (!isDtachSocketAlive(socketPath)) {
721
+ cleaned++;
722
+ }
723
+ }
724
+ }
725
+ }
726
+ catch {
727
+ // Directory might not exist yet
728
+ }
729
+ return cleaned;
730
+ }
511
731
  /**
512
732
  * Ensure local dtach is available or get user choice
513
733
  * Returns: 'available' | 'skip' | 'cancel'
@@ -1603,12 +1823,12 @@ async function createNativeSession(options) {
1603
1823
  });
1604
1824
  }
1605
1825
  /**
1606
- * Check if a dtach session is running locally (by checking if socket file exists)
1826
+ * Check if a dtach session is running locally (verifies socket is actually alive)
1607
1827
  */
1608
1828
  function isDtachSessionRunning(sessionName) {
1609
1829
  try {
1610
1830
  const socketPath = getDtachSocketPath(sessionName);
1611
- return fs.existsSync(socketPath);
1831
+ return isDtachSocketAlive(socketPath);
1612
1832
  }
1613
1833
  catch {
1614
1834
  return false;
@@ -1970,6 +2190,11 @@ exports.sessionCommand = new commander_1.Command('session')
1970
2190
  .addCommand(migrate_1.sessionMigrateCommand)
1971
2191
  .action(async (sessionArg, providerArgs, options) => {
1972
2192
  try {
2193
+ // Clean up stale sockets on startup (silently)
2194
+ const cleaned = cleanupAllStaleSockets();
2195
+ if (cleaned > 0) {
2196
+ console.log(chalk_1.default.dim(`Cleaned up ${cleaned} stale socket${cleaned > 1 ? 's' : ''}`));
2197
+ }
1973
2198
  // Multi-session orchestration mode
1974
2199
  if (options.multi) {
1975
2200
  // If sessionArg looks like a provider spec, treat it as part of providers
@@ -233,20 +233,125 @@ class LocalOrchestrator {
233
233
  }
234
234
  }
235
235
  /**
236
- * Send a prompt to a local session
237
- * @deprecated dtach doesn't support programmatic prompt sending like tmux
236
+ * Send keys directly to dtach stdin (inspired by tmux's send-keys)
237
+ * Bypasses shell interpretation by writing directly to the process
238
+ */
239
+ sendKeysDirect(socketPath, data) {
240
+ const { spawnSync } = require('child_process');
241
+ const proc = spawnSync('dtach', ['-p', socketPath], {
242
+ input: data + '\r',
243
+ timeout: 5000,
244
+ stdio: ['pipe', 'ignore', 'ignore'],
245
+ });
246
+ if (proc.error) {
247
+ throw new Error(`Failed to send keys: ${proc.error.message}`);
248
+ }
249
+ }
250
+ /**
251
+ * Send keys to Docker container's dtach session
252
+ */
253
+ sendKeysToDocker(containerId, socketPath, data) {
254
+ const { spawnSync } = require('child_process');
255
+ const proc = spawnSync('docker', [
256
+ 'exec', '-i', containerId,
257
+ 'dtach', '-p', socketPath
258
+ ], {
259
+ input: data + '\r',
260
+ timeout: 10000,
261
+ stdio: ['pipe', 'ignore', 'ignore'],
262
+ });
263
+ if (proc.error) {
264
+ throw new Error(`Failed to send keys to docker: ${proc.error.message}`);
265
+ }
266
+ }
267
+ /**
268
+ * Send keys to Multipass VM's dtach session
269
+ */
270
+ sendKeysToMultipass(vmName, socketPath, data) {
271
+ const { spawnSync } = require('child_process');
272
+ const proc = spawnSync('multipass', [
273
+ 'exec', vmName, '--',
274
+ 'dtach', '-p', socketPath
275
+ ], {
276
+ input: data + '\r',
277
+ timeout: 10000,
278
+ stdio: ['pipe', 'ignore', 'ignore'],
279
+ });
280
+ if (proc.error) {
281
+ throw new Error(`Failed to send keys to multipass: ${proc.error.message}`);
282
+ }
283
+ }
284
+ /**
285
+ * Send keys to remote dtach session via SSH stdin forwarding
286
+ */
287
+ sendKeysToRemote(ipAddress, keyPath, socketPath, data) {
288
+ const { spawnSync } = require('child_process');
289
+ const proc = spawnSync('ssh', [
290
+ '-i', keyPath,
291
+ '-o', 'StrictHostKeyChecking=no',
292
+ '-o', 'UserKnownHostsFile=/dev/null',
293
+ '-o', 'ConnectTimeout=5',
294
+ `dev@${ipAddress}`,
295
+ `dtach -p ${socketPath}`
296
+ ], {
297
+ input: data + '\r',
298
+ timeout: 30000,
299
+ stdio: ['pipe', 'ignore', 'ignore'],
300
+ });
301
+ if (proc.error) {
302
+ throw new Error(`Failed to send keys to remote: ${proc.error.message}`);
303
+ }
304
+ }
305
+ /**
306
+ * Send a prompt to a local session via dtach -p (push mode)
307
+ * Uses direct stdin write (inspired by tmux's send-keys) to bypass shell interpretation
238
308
  */
239
309
  async sendPromptToLocalSession(session, prompt) {
240
- console.log(chalk_1.default.yellow(`Warning: Programmatic prompt sending is not supported with dtach sessions`));
241
- console.log(chalk_1.default.dim(`Please attach to the session manually and enter your prompt.`));
310
+ const os = require('os');
311
+ const path = require('path');
312
+ try {
313
+ switch (session.isolation) {
314
+ case 'docker':
315
+ if (!session.containerId)
316
+ return;
317
+ await new Promise(resolve => setTimeout(resolve, 2000));
318
+ this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
319
+ break;
320
+ case 'multipass':
321
+ if (!session.vmName)
322
+ return;
323
+ await new Promise(resolve => setTimeout(resolve, 2000));
324
+ this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
325
+ break;
326
+ case 'native':
327
+ await new Promise(resolve => setTimeout(resolve, 1000));
328
+ const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
329
+ this.sendKeysDirect(socketPath, prompt);
330
+ break;
331
+ }
332
+ }
333
+ catch (error) {
334
+ console.log(chalk_1.default.yellow(`Warning: Could not send prompt to ${session.name}: ${error.message}`));
335
+ }
242
336
  }
243
337
  /**
244
- * Send a prompt to a cloud session
245
- * @deprecated dtach doesn't support programmatic prompt sending like tmux
338
+ * Send a prompt to a cloud session via SSH + dtach -p
339
+ * Uses direct stdin forwarding to bypass shell interpretation
246
340
  */
247
341
  async sendPromptToCloudSession(session, prompt) {
248
- console.log(chalk_1.default.yellow(`Warning: Programmatic prompt sending is not supported with dtach sessions`));
249
- console.log(chalk_1.default.dim(`Please attach to the session manually and enter your prompt.`));
342
+ if (!session.ipAddress) {
343
+ console.log(chalk_1.default.yellow(`Warning: Cloud session ${session.name} has no IP address`));
344
+ return;
345
+ }
346
+ const socketPath = `/home/dev/.genbox/sockets/${session.name}.sock`;
347
+ try {
348
+ const keyPath = this.getPrivateSshKey();
349
+ await new Promise(resolve => setTimeout(resolve, 2000));
350
+ this.sendKeysToRemote(session.ipAddress, keyPath, socketPath, prompt);
351
+ }
352
+ catch (error) {
353
+ console.log(chalk_1.default.yellow(`Warning: Could not send prompt to cloud session ${session.name}: ${error.message}`));
354
+ }
250
355
  }
251
356
  /**
252
357
  * Get SSH key path for cloud sessions
@@ -293,7 +293,7 @@ class LocalSessionManager {
293
293
  checkNativeStatus(session) {
294
294
  try {
295
295
  const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
296
- if (fs.existsSync(socketPath)) {
296
+ if (this.isDtachSocketAlive(socketPath)) {
297
297
  return 'running';
298
298
  }
299
299
  return 'stopped';
@@ -302,6 +302,80 @@ class LocalSessionManager {
302
302
  return 'stopped';
303
303
  }
304
304
  }
305
+ /**
306
+ * Check if a dtach socket is actually alive (not just file exists)
307
+ */
308
+ isDtachSocketAlive(socketPath) {
309
+ if (!fs.existsSync(socketPath)) {
310
+ return false;
311
+ }
312
+ try {
313
+ // Try to send an empty string to test if socket is alive
314
+ (0, child_process_1.execSync)(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
315
+ timeout: 2000,
316
+ stdio: 'ignore'
317
+ });
318
+ return true;
319
+ }
320
+ catch {
321
+ // Socket exists but is stale - clean it up
322
+ try {
323
+ fs.unlinkSync(socketPath);
324
+ }
325
+ catch {
326
+ // Ignore cleanup errors
327
+ }
328
+ return false;
329
+ }
330
+ }
331
+ /**
332
+ * Send keys directly to dtach stdin (inspired by tmux's send-keys)
333
+ * Bypasses shell interpretation by writing directly to the process
334
+ */
335
+ sendKeysDirect(socketPath, data) {
336
+ const proc = (0, child_process_1.spawnSync)('dtach', ['-p', socketPath], {
337
+ input: data + '\r', // Add Enter key
338
+ timeout: 5000,
339
+ stdio: ['pipe', 'ignore', 'ignore'],
340
+ });
341
+ if (proc.error) {
342
+ throw new Error(`Failed to send keys: ${proc.error.message}`);
343
+ }
344
+ }
345
+ /**
346
+ * Send keys to Docker container's dtach session
347
+ * Uses docker exec with stdin forwarding
348
+ */
349
+ sendKeysToDocker(containerId, socketPath, data) {
350
+ const proc = (0, child_process_1.spawnSync)('docker', [
351
+ 'exec', '-i', containerId,
352
+ 'dtach', '-p', socketPath
353
+ ], {
354
+ input: data + '\r',
355
+ timeout: 10000,
356
+ stdio: ['pipe', 'ignore', 'ignore'],
357
+ });
358
+ if (proc.error) {
359
+ throw new Error(`Failed to send keys to docker: ${proc.error.message}`);
360
+ }
361
+ }
362
+ /**
363
+ * Send keys to Multipass VM's dtach session
364
+ * Uses multipass exec with stdin forwarding
365
+ */
366
+ sendKeysToMultipass(vmName, socketPath, data) {
367
+ const proc = (0, child_process_1.spawnSync)('multipass', [
368
+ 'exec', vmName, '--',
369
+ 'dtach', '-p', socketPath
370
+ ], {
371
+ input: data + '\r',
372
+ timeout: 10000,
373
+ stdio: ['pipe', 'ignore', 'ignore'],
374
+ });
375
+ if (proc.error) {
376
+ throw new Error(`Failed to send keys to multipass: ${proc.error.message}`);
377
+ }
378
+ }
305
379
  /**
306
380
  * List all local sessions
307
381
  */
@@ -866,13 +940,26 @@ final_message: "Cloud-init completed successfully"
866
940
  this.unregisterSession(sessionId);
867
941
  }
868
942
  /**
869
- * Send a prompt to a session
870
- * Note: dtach doesn't support send-keys like tmux, so this functionality is limited.
871
- * This method is kept for API compatibility but may not work as expected.
872
- * @deprecated dtach doesn't support programmatic prompt sending
943
+ * Send a prompt to a session via dtach -p (push mode)
944
+ * Uses direct stdin write (inspired by tmux's send-keys) to bypass shell interpretation
873
945
  */
874
946
  async sendPromptToSession(session, prompt) {
875
- console.warn('Warning: sendPromptToSession is not fully supported with dtach sessions');
947
+ switch (session.isolation) {
948
+ case 'docker':
949
+ if (!session.containerId)
950
+ throw new Error('Session has no container ID');
951
+ this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
952
+ break;
953
+ case 'multipass':
954
+ if (!session.vmName)
955
+ throw new Error('Session has no VM name');
956
+ this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
957
+ break;
958
+ case 'native':
959
+ const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
960
+ this.sendKeysDirect(socketPath, prompt);
961
+ break;
962
+ }
876
963
  // Update last activity
877
964
  session.lastActivityAt = new Date();
878
965
  this.updateSession(session);
@@ -233,17 +233,44 @@ class NativeSessionManager {
233
233
  return this.listSessions().filter(s => this.isSessionRunning(s.dtachSocketName || s.name));
234
234
  }
235
235
  /**
236
- * Check if a dtach session is running (by checking if socket file exists)
236
+ * Check if a dtach session is running (verifies socket is actually alive)
237
237
  */
238
238
  isSessionRunning(sessionName) {
239
239
  try {
240
240
  const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${sessionName}.sock`);
241
- return fs.existsSync(socketPath);
241
+ return this.isDtachSocketAlive(socketPath);
242
242
  }
243
243
  catch {
244
244
  return false;
245
245
  }
246
246
  }
247
+ /**
248
+ * Check if a dtach socket is actually alive (not just file exists)
249
+ */
250
+ isDtachSocketAlive(socketPath) {
251
+ if (!fs.existsSync(socketPath)) {
252
+ return false;
253
+ }
254
+ try {
255
+ // Try to send an empty string to test if socket is alive
256
+ const { execSync } = require('child_process');
257
+ execSync(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
258
+ timeout: 2000,
259
+ stdio: 'ignore'
260
+ });
261
+ return true;
262
+ }
263
+ catch {
264
+ // Socket exists but is stale - clean it up
265
+ try {
266
+ fs.unlinkSync(socketPath);
267
+ }
268
+ catch {
269
+ // Ignore cleanup errors
270
+ }
271
+ return false;
272
+ }
273
+ }
247
274
  /**
248
275
  * Update session status
249
276
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.180",
3
+ "version": "1.0.182",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {