@ebowwa/terminal 0.3.7 → 0.3.9

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/index.js CHANGED
@@ -15,7 +15,6 @@ var __toESM = (mod, isNodeMode, target) => {
15
15
  });
16
16
  return to;
17
17
  };
18
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
18
  var __export = (target, all) => {
20
19
  for (var name in all)
21
20
  __defProp(target, name, {
@@ -445,34 +444,33 @@ var init_error = __esm(() => {
445
444
  });
446
445
 
447
446
  // node_modules/@ebowwa/codespaces-types/runtime/ssh.js
448
- var require_ssh = __commonJS((exports) => {
449
- Object.defineProperty(exports, "__esModule", { value: true });
450
- exports.FilePreviewOptionsSchema = exports.FilesListOptionsSchema = exports.SCPOptionsSchema = exports.SSHCommandSchema = exports.SSHOptionsSchema = undefined;
451
- var zod_1 = __require("zod");
452
- exports.SSHOptionsSchema = zod_1.z.object({
453
- host: zod_1.z.string().min(1, "Host is required"),
454
- user: zod_1.z.string().default("root"),
455
- timeout: zod_1.z.number().int().positive().default(5),
456
- port: zod_1.z.number().int().positive().max(65535).default(22),
457
- keyPath: zod_1.z.string().optional(),
458
- password: zod_1.z.string().optional()
447
+ import { z } from "zod";
448
+ var SSHOptionsSchema, SSHCommandSchema, SCPOptionsSchema, FilesListOptionsSchema, FilePreviewOptionsSchema;
449
+ var init_ssh = __esm(() => {
450
+ SSHOptionsSchema = z.object({
451
+ host: z.string().min(1, "Host is required"),
452
+ user: z.string().default("root"),
453
+ timeout: z.number().int().positive().default(5),
454
+ port: z.number().int().positive().max(65535).default(22),
455
+ keyPath: z.string().optional(),
456
+ password: z.string().optional()
459
457
  });
460
- exports.SSHCommandSchema = zod_1.z.string().min(1, "Command cannot be empty");
461
- exports.SCPOptionsSchema = exports.SSHOptionsSchema.extend({
462
- source: zod_1.z.string().min(1, "Source is required"),
463
- destination: zod_1.z.string().min(1, "Destination is required"),
464
- recursive: zod_1.z.boolean().default(false),
465
- preserve: zod_1.z.boolean().default(false)
458
+ SSHCommandSchema = z.string().min(1, "Command cannot be empty");
459
+ SCPOptionsSchema = SSHOptionsSchema.extend({
460
+ source: z.string().min(1, "Source is required"),
461
+ destination: z.string().min(1, "Destination is required"),
462
+ recursive: z.boolean().default(false),
463
+ preserve: z.boolean().default(false)
466
464
  });
467
- exports.FilesListOptionsSchema = zod_1.z.object({
468
- host: zod_1.z.string().min(1, "Host is required"),
469
- user: zod_1.z.string().default("root"),
470
- path: zod_1.z.string().default(".")
465
+ FilesListOptionsSchema = z.object({
466
+ host: z.string().min(1, "Host is required"),
467
+ user: z.string().default("root"),
468
+ path: z.string().default(".")
471
469
  });
472
- exports.FilePreviewOptionsSchema = zod_1.z.object({
473
- host: zod_1.z.string().min(1, "Host is required"),
474
- user: zod_1.z.string().default("root"),
475
- path: zod_1.z.string().min(1, "Path is required")
470
+ FilePreviewOptionsSchema = z.object({
471
+ host: z.string().min(1, "Host is required"),
472
+ user: z.string().default("root"),
473
+ path: z.string().min(1, "Path is required")
476
474
  });
477
475
  });
478
476
 
@@ -482,11 +480,11 @@ __export(exports_client, {
482
480
  execSSH: () => execSSH
483
481
  });
484
482
  async function execSSH(command, options) {
485
- const validatedCommand = import_ssh.SSHCommandSchema.safeParse(command);
483
+ const validatedCommand = SSHCommandSchema.safeParse(command);
486
484
  if (!validatedCommand.success) {
487
485
  throw new Error(`Invalid SSH command: ${validatedCommand.error.issues.map((i) => i.message).join(", ")}`);
488
486
  }
489
- const validatedOptions = import_ssh.SSHOptionsSchema.safeParse(options);
487
+ const validatedOptions = SSHOptionsSchema.safeParse(options);
490
488
  if (!validatedOptions.success) {
491
489
  throw new Error(`Invalid SSH options: ${validatedOptions.error.issues.map((i) => i.message).join(", ")}`);
492
490
  }
@@ -506,120 +504,10 @@ async function execSSH(command, options) {
506
504
  throw new SSHError(`SSH command failed: ${validatedCommand.data}`, error);
507
505
  }
508
506
  }
509
- var import_ssh;
510
507
  var init_client = __esm(() => {
511
508
  init_error();
509
+ init_ssh();
512
510
  init_pool();
513
- import_ssh = __toESM(require_ssh(), 1);
514
- });
515
-
516
- // node_modules/@ebowwa/codespaces-types/compile/terminal-websocket.js
517
- var require_terminal_websocket = __commonJS((exports) => {
518
- Object.defineProperty(exports, "__esModule", { value: true });
519
- exports.WebSocketCloseCode = undefined;
520
- exports.WebSocketCloseCode = {
521
- NORMAL_CLOSURE: 1000,
522
- ENDPOINT_GOING_AWAY: 1001,
523
- PROTOCOL_ERROR: 1002,
524
- UNSUPPORTED_DATA: 1003,
525
- NO_STATUS_RECEIVED: 1005,
526
- ABNORMAL_CLOSURE: 1006,
527
- INVALID_FRAME_PAYLOAD_DATA: 1007,
528
- POLICY_VIOLATION: 1008,
529
- MESSAGE_TOO_BIG: 1009,
530
- MISSING_MANDATORY_EXTENSION: 1010,
531
- INTERNAL_ERROR: 1011,
532
- SERVICE_RESTART: 1012,
533
- TRY_AGAIN_LATER: 1013,
534
- SESSION_NOT_FOUND: 4001,
535
- SESSION_ALREADY_CLOSED: 4002,
536
- INVALID_MESSAGE_FORMAT: 4003,
537
- AUTHENTICATION_FAILED: 4004,
538
- CONNECTION_TIMEOUT: 4005,
539
- SESSION_LIMIT_REACHED: 4006,
540
- INVALID_HOST: 4007,
541
- SSH_CONNECTION_FAILED: 4008,
542
- NETWORK_BLOCKED: 4009
543
- };
544
- });
545
-
546
- // node_modules/@ebowwa/codespaces-types/compile/index.js
547
- var require_compile = __commonJS((exports) => {
548
- var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
549
- if (k2 === undefined)
550
- k2 = k;
551
- var desc = Object.getOwnPropertyDescriptor(m, k);
552
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
553
- desc = { enumerable: true, get: function() {
554
- return m[k];
555
- } };
556
- }
557
- Object.defineProperty(o, k2, desc);
558
- } : function(o, m, k, k2) {
559
- if (k2 === undefined)
560
- k2 = k;
561
- o[k2] = m[k];
562
- });
563
- var __exportStar = exports && exports.__exportStar || function(m, exports2) {
564
- for (var p in m)
565
- if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p))
566
- __createBinding(exports2, m, p);
567
- };
568
- Object.defineProperty(exports, "__esModule", { value: true });
569
- exports.LoopStatus = exports.VolumeStatus = exports.ActionStatus = exports.EnvironmentStatus = undefined;
570
- exports.getEnvLocation = getEnvLocation;
571
- exports.getEnvRegionName = getEnvRegionName;
572
- exports.getEnvLocationLabel = getEnvLocationLabel;
573
- __exportStar(require_terminal_websocket(), exports);
574
- var EnvironmentStatus;
575
- (function(EnvironmentStatus2) {
576
- EnvironmentStatus2["Running"] = "running";
577
- EnvironmentStatus2["Stopped"] = "stopped";
578
- EnvironmentStatus2["Creating"] = "creating";
579
- EnvironmentStatus2["Deleting"] = "deleting";
580
- EnvironmentStatus2["Starting"] = "starting";
581
- EnvironmentStatus2["Stopping"] = "stopping";
582
- EnvironmentStatus2["Initializing"] = "initializing";
583
- })(EnvironmentStatus || (exports.EnvironmentStatus = EnvironmentStatus = {}));
584
- var ActionStatus;
585
- (function(ActionStatus2) {
586
- ActionStatus2["Running"] = "running";
587
- ActionStatus2["Success"] = "success";
588
- ActionStatus2["Error"] = "error";
589
- })(ActionStatus || (exports.ActionStatus = ActionStatus = {}));
590
- var VolumeStatus;
591
- (function(VolumeStatus2) {
592
- VolumeStatus2["Creating"] = "creating";
593
- VolumeStatus2["Available"] = "available";
594
- VolumeStatus2["Deleting"] = "deleting";
595
- })(VolumeStatus || (exports.VolumeStatus = VolumeStatus = {}));
596
- var LoopStatus;
597
- (function(LoopStatus2) {
598
- LoopStatus2["Running"] = "running";
599
- LoopStatus2["Stopped"] = "stopped";
600
- LoopStatus2["Error"] = "error";
601
- LoopStatus2["Completed"] = "completed";
602
- })(LoopStatus || (exports.LoopStatus = LoopStatus = {}));
603
- function getEnvLocation(env) {
604
- if (!env)
605
- return null;
606
- return env.location || null;
607
- }
608
- function getEnvRegionName(env) {
609
- var _a;
610
- if (!env)
611
- return "Unknown";
612
- return ((_a = env.location) === null || _a === undefined ? undefined : _a.name) || "Unknown";
613
- }
614
- function getEnvLocationLabel(env) {
615
- var _a, _b, _c;
616
- if (!env)
617
- return "Unknown";
618
- if (((_a = env.location) === null || _a === undefined ? undefined : _a.city) && ((_b = env.location) === null || _b === undefined ? undefined : _b.country)) {
619
- return "".concat(env.location.city, ", ").concat(env.location.country);
620
- }
621
- return ((_c = env.location) === null || _c === undefined ? undefined : _c.name) || "Unknown";
622
- }
623
511
  });
624
512
 
625
513
  // src/tmux.ts
@@ -1264,7 +1152,7 @@ class TmuxSessionManager {
1264
1152
  return { success: false, error: `Node ${nodeId} is not running` };
1265
1153
  }
1266
1154
  try {
1267
- const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, { sessionName });
1155
+ const result = await createOrAttachTmuxSession(node.ip, node.user, node.keyPath, {});
1268
1156
  return {
1269
1157
  success: true,
1270
1158
  sshArgs: result.sshArgs,
@@ -1768,10 +1656,10 @@ async function execViaTmuxParallel(commands, options, timeout = 10) {
1768
1656
  }
1769
1657
  // src/scp.ts
1770
1658
  init_error();
1659
+ init_ssh();
1771
1660
  init_pool();
1772
- var import_ssh2 = __toESM(require_ssh(), 1);
1773
1661
  async function scpUpload(options) {
1774
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1662
+ const validated = SCPOptionsSchema.safeParse(options);
1775
1663
  if (!validated.success) {
1776
1664
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1777
1665
  }
@@ -1798,7 +1686,7 @@ async function scpUpload(options) {
1798
1686
  }
1799
1687
  }
1800
1688
  async function scpDownload(options) {
1801
- const validated = import_ssh2.SCPOptionsSchema.safeParse(options);
1689
+ const validated = SCPOptionsSchema.safeParse(options);
1802
1690
  if (!validated.success) {
1803
1691
  throw new Error(`Invalid SCP options: ${validated.error.issues.map((i) => i.message).join(", ")}`);
1804
1692
  }
@@ -2288,9 +2176,29 @@ async function validateSSHKeyForServer(host, keyPath, hetznerKeyId) {
2288
2176
  }
2289
2177
  // src/pty.ts
2290
2178
  var activeSessions = new Map;
2179
+ var activeReplSessions = new Map;
2291
2180
  function generateSessionId() {
2292
2181
  return `pty-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
2293
2182
  }
2183
+ function stripAnsi(str) {
2184
+ return str.replace(/\x1B\[[0-9;]*m/g, "").replace(/\x1B\][0-9;]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B[\][0-9;]*[A-Za-z]/g, "").replace(/\x1B[()][AB012]/g, "").replace(/\x1B[78]/g, "").replace(/\r\n|\r/g, `
2185
+ `).trim();
2186
+ }
2187
+ function detectPrompt(output) {
2188
+ const promptPatterns = [
2189
+ /[$#]\s*$/,
2190
+ /~\s*$/,
2191
+ /]\$\s*$/,
2192
+ />\s*$/
2193
+ ];
2194
+ const cleanOutput = stripAnsi(output);
2195
+ for (const pattern of promptPatterns) {
2196
+ if (pattern.test(cleanOutput)) {
2197
+ return true;
2198
+ }
2199
+ }
2200
+ return false;
2201
+ }
2294
2202
  async function createPTYSession(host, user = "root", options = {}) {
2295
2203
  const { rows = 24, cols = 80, port = 22, keyPath } = options;
2296
2204
  const sessionId = generateSessionId();
@@ -2315,8 +2223,7 @@ async function createPTYSession(host, user = "root", options = {}) {
2315
2223
  }
2316
2224
  sshArgs.push("-t", "-t");
2317
2225
  sshArgs.push(`${user}@${host}`);
2318
- sshArgs.push("TERM=xterm-256color");
2319
- sshArgs.push("script", "-q", "/dev/null", "/bin/bash");
2226
+ sshArgs.push("TERM=xterm-256color script -q -c 'bash --norc --noprofile' /dev/null");
2320
2227
  const proc = Bun.spawn(sshArgs, {
2321
2228
  stdin: "pipe",
2322
2229
  stdout: "pipe",
@@ -2368,9 +2275,9 @@ async function writeToPTY(sessionId, data) {
2368
2275
  return false;
2369
2276
  }
2370
2277
  try {
2371
- const writer = session.stdin.getWriter();
2372
- await writer.write(new TextEncoder().encode(data));
2373
- writer.releaseLock();
2278
+ const stdin = session.stdin;
2279
+ await stdin.write(new TextEncoder().encode(data));
2280
+ await stdin.flush();
2374
2281
  return true;
2375
2282
  } catch {
2376
2283
  return false;
@@ -2443,13 +2350,204 @@ function getPTYSession(sessionId) {
2443
2350
  function getActivePTYSessions() {
2444
2351
  return Array.from(activeSessions.values());
2445
2352
  }
2353
+ async function createSSHReplSession(options) {
2354
+ const {
2355
+ host,
2356
+ user = "root",
2357
+ shell = "/bin/bash",
2358
+ initCommands = [],
2359
+ timeout = 30000,
2360
+ onConnect
2361
+ } = options;
2362
+ const sessionId = generateSessionId();
2363
+ const startTime = Date.now();
2364
+ const { sessionId: ptyId, initialOutput } = await createPTYSession(host, user, {
2365
+ port: options.port,
2366
+ keyPath: options.keyPath
2367
+ });
2368
+ const session = {
2369
+ id: sessionId,
2370
+ ptyId,
2371
+ host,
2372
+ user,
2373
+ cwd: "~",
2374
+ env: {},
2375
+ lastExitCode: 0,
2376
+ lastCommand: "",
2377
+ createdAt: startTime,
2378
+ connected: true
2379
+ };
2380
+ activeReplSessions.set(sessionId, session);
2381
+ for (const cmd of initCommands) {
2382
+ await execPTY(sessionId, cmd, { timeout });
2383
+ }
2384
+ onConnect?.(session);
2385
+ return session;
2386
+ }
2387
+ async function execPTY(sessionId, command, options = {}) {
2388
+ const replSession = activeReplSessions.get(sessionId);
2389
+ if (!replSession) {
2390
+ throw new Error(`REPL session not found: ${sessionId}`);
2391
+ }
2392
+ const session = activeSessions.get(replSession.ptyId);
2393
+ if (!session) {
2394
+ throw new Error(`PTY session not found: ${replSession.ptyId}`);
2395
+ }
2396
+ const startTime = Date.now();
2397
+ const timeout = options.timeout || 30000;
2398
+ const cmdWithNewline = command.endsWith(`
2399
+ `) ? command : `${command}
2400
+ `;
2401
+ const written = await writeToPTY(replSession.ptyId, cmdWithNewline);
2402
+ if (!written) {
2403
+ throw new Error("Failed to write to PTY");
2404
+ }
2405
+ let output = "";
2406
+ let stderrOutput = "";
2407
+ const deadline = startTime + timeout;
2408
+ while (Date.now() < deadline) {
2409
+ const chunk = await readFromPTY(replSession.ptyId, 200);
2410
+ if (chunk === null) {
2411
+ break;
2412
+ }
2413
+ output += chunk;
2414
+ if (detectPrompt(output)) {
2415
+ break;
2416
+ }
2417
+ }
2418
+ const exitCodeResult = await writeToPTY(replSession.ptyId, `echo "$?"
2419
+ `);
2420
+ await new Promise((resolve) => setTimeout(resolve, 100));
2421
+ const exitChunk = await readFromPTY(replSession.ptyId, 200);
2422
+ const exitCodeMatch = (exitChunk || "").trim().match(/^\d+$/);
2423
+ const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[0], 10) : 0;
2424
+ if (command.startsWith("cd ") || command === "cd") {
2425
+ const cwdMatch = output.match(/^(.+?)\n/);
2426
+ if (cwdMatch) {
2427
+ replSession.cwd = cwdMatch[1].trim();
2428
+ }
2429
+ }
2430
+ const clean = stripAnsi(output).replace(/\r?\n/g, `
2431
+ `).trim();
2432
+ replSession.lastCommand = command;
2433
+ replSession.lastExitCode = exitCode;
2434
+ return {
2435
+ stdout: clean,
2436
+ stderr: stderrOutput,
2437
+ exitCode,
2438
+ cwd: replSession.cwd,
2439
+ duration: Date.now() - startTime,
2440
+ raw: output,
2441
+ clean
2442
+ };
2443
+ }
2444
+ async function execPTYBatch(sessionId, commands, options = {}) {
2445
+ const results = [];
2446
+ const errors = [];
2447
+ const startTime = Date.now();
2448
+ for (const command of commands) {
2449
+ try {
2450
+ const result = await execPTY(sessionId, command, options);
2451
+ results.push(result);
2452
+ if (result.exitCode !== 0 && options.stopOnError) {
2453
+ errors.push({ command, error: new Error(`Command failed with exit code ${result.exitCode}`) });
2454
+ break;
2455
+ }
2456
+ } catch (error) {
2457
+ const err = error instanceof Error ? error : new Error(String(error));
2458
+ errors.push({ command, error: err });
2459
+ if (options.stopOnError) {
2460
+ break;
2461
+ }
2462
+ }
2463
+ }
2464
+ return {
2465
+ results,
2466
+ errors,
2467
+ totalDuration: Date.now() - startTime
2468
+ };
2469
+ }
2470
+ function getSSHReplSession(sessionId) {
2471
+ return activeReplSessions.get(sessionId);
2472
+ }
2473
+ function getActiveSSHReplSessions() {
2474
+ return Array.from(activeReplSessions.values());
2475
+ }
2476
+ async function closeSSHReplSession(sessionId) {
2477
+ const session = activeReplSessions.get(sessionId);
2478
+ if (!session) {
2479
+ return false;
2480
+ }
2481
+ const result = await closePTYSession(session.ptyId);
2482
+ if (result) {
2483
+ session.connected = false;
2484
+ activeReplSessions.delete(sessionId);
2485
+ }
2486
+ return result;
2487
+ }
2446
2488
 
2447
2489
  // src/index.ts
2448
2490
  init_pool();
2449
2491
 
2450
2492
  // src/sessions.ts
2451
2493
  import path2 from "path";
2452
- var import_compile = __toESM(require_compile(), 1);
2494
+
2495
+ // node_modules/@ebowwa/codespaces-types/compile/index.js
2496
+ var WebSocketCloseCode = {
2497
+ NORMAL_CLOSURE: 1000,
2498
+ ENDPOINT_GOING_AWAY: 1001,
2499
+ PROTOCOL_ERROR: 1002,
2500
+ UNSUPPORTED_DATA: 1003,
2501
+ NO_STATUS_RECEIVED: 1005,
2502
+ ABNORMAL_CLOSURE: 1006,
2503
+ INVALID_FRAME_PAYLOAD_DATA: 1007,
2504
+ POLICY_VIOLATION: 1008,
2505
+ MESSAGE_TOO_BIG: 1009,
2506
+ MISSING_MANDATORY_EXTENSION: 1010,
2507
+ INTERNAL_ERROR: 1011,
2508
+ SERVICE_RESTART: 1012,
2509
+ TRY_AGAIN_LATER: 1013,
2510
+ SESSION_NOT_FOUND: 4001,
2511
+ SESSION_ALREADY_CLOSED: 4002,
2512
+ INVALID_MESSAGE_FORMAT: 4003,
2513
+ AUTHENTICATION_FAILED: 4004,
2514
+ CONNECTION_TIMEOUT: 4005,
2515
+ SESSION_LIMIT_REACHED: 4006,
2516
+ INVALID_HOST: 4007,
2517
+ SSH_CONNECTION_FAILED: 4008,
2518
+ NETWORK_BLOCKED: 4009
2519
+ };
2520
+ var EnvironmentStatus;
2521
+ ((EnvironmentStatus2) => {
2522
+ EnvironmentStatus2["Running"] = "running";
2523
+ EnvironmentStatus2["Stopped"] = "stopped";
2524
+ EnvironmentStatus2["Creating"] = "creating";
2525
+ EnvironmentStatus2["Deleting"] = "deleting";
2526
+ EnvironmentStatus2["Starting"] = "starting";
2527
+ EnvironmentStatus2["Stopping"] = "stopping";
2528
+ EnvironmentStatus2["Initializing"] = "initializing";
2529
+ })(EnvironmentStatus ||= {});
2530
+ var ActionStatus;
2531
+ ((ActionStatus2) => {
2532
+ ActionStatus2["Running"] = "running";
2533
+ ActionStatus2["Success"] = "success";
2534
+ ActionStatus2["Error"] = "error";
2535
+ })(ActionStatus ||= {});
2536
+ var VolumeStatus;
2537
+ ((VolumeStatus2) => {
2538
+ VolumeStatus2["Creating"] = "creating";
2539
+ VolumeStatus2["Available"] = "available";
2540
+ VolumeStatus2["Deleting"] = "deleting";
2541
+ })(VolumeStatus ||= {});
2542
+ var LoopStatus;
2543
+ ((LoopStatus2) => {
2544
+ LoopStatus2["Running"] = "running";
2545
+ LoopStatus2["Stopped"] = "stopped";
2546
+ LoopStatus2["Error"] = "error";
2547
+ LoopStatus2["Completed"] = "completed";
2548
+ })(LoopStatus ||= {});
2549
+
2550
+ // src/sessions.ts
2453
2551
  var metadataModule = null;
2454
2552
  function loadMetadata() {
2455
2553
  if (metadataModule === null) {
@@ -2502,7 +2600,7 @@ async function closeSession(sessionId) {
2502
2600
  } catch {}
2503
2601
  }
2504
2602
  try {
2505
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
2603
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, "Session closed");
2506
2604
  } catch {}
2507
2605
  if (session.bootstrapLogStreamer) {
2508
2606
  try {
@@ -2822,14 +2920,14 @@ async function getOrCreateSession(host, user = "root", sessionId = null, keyPath
2822
2920
  console.log(`[Terminal] Process for ${newSessionId} exited with code ${exitCode}`);
2823
2921
  session.closed = true;
2824
2922
  try {
2825
- session.ws?.close(import_compile.WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2923
+ session.ws?.close(WebSocketCloseCode.NORMAL_CLOSURE, `SSH process exited (code: ${exitCode})`);
2826
2924
  } catch {}
2827
2925
  terminalSessions.delete(newSessionId);
2828
2926
  }).catch((err) => {
2829
2927
  console.log(`[Terminal] Process exit error for ${newSessionId}:`, err);
2830
2928
  session.closed = true;
2831
2929
  try {
2832
- session.ws?.close(import_compile.WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2930
+ session.ws?.close(WebSocketCloseCode.INTERNAL_ERROR, "SSH process error");
2833
2931
  } catch {}
2834
2932
  });
2835
2933
  return session;
@@ -3379,14 +3477,392 @@ var RESOURCE_COMMANDS = {
3379
3477
  connections: `cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | wc -l`,
3380
3478
  ports: `cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | grep -v 'local_address' | awk '{print $2}' | cut -d: -f2 | sort -u | tr '\\n' ';' | sed 's/;$//'`
3381
3479
  };
3480
+ // src/network-error-detector.ts
3481
+ function detectNetworkError(error, host, serverStatus) {
3482
+ const errorMessage = typeof error === "string" ? error : error.message;
3483
+ const isConnectionRefused = errorMessage.includes("ECONNREFUSED") || errorMessage.includes("Connection refused") || errorMessage.includes("connect ECONNREFUSED");
3484
+ const isHetznerOrDatacenterIP = host.startsWith("195.") || host.startsWith("148.") || host.startsWith("116.") || host.startsWith("138.") || host.includes("hetzner") || host.includes("fsn1") || host.includes("nbg1") || host.includes("hel1");
3485
+ const serverIsRunning = serverStatus === "running";
3486
+ if (isConnectionRefused && serverIsRunning && isHetznerOrDatacenterIP) {
3487
+ return {
3488
+ type: "NETWORK_BLOCKED",
3489
+ message: "Network appears to be blocking this server",
3490
+ troubleshooting: [
3491
+ "\u2713 Server is running and responding to API",
3492
+ "\u2717 Your network is refusing connections to this IP",
3493
+ "",
3494
+ "\u26A0\uFE0F COMMON CAUSES:",
3495
+ "\u2022 Corporate/public WiFi blocks datacenter IPs",
3496
+ "\u2022 ISP firewall filtering VPS providers",
3497
+ "\u2022 Router content filtering",
3498
+ "",
3499
+ "\uD83D\uDD27 SOLUTIONS:",
3500
+ "1. Try a different network (hotspot, home WiFi, VPN)",
3501
+ "2. Check router firewall settings",
3502
+ "3. Contact ISP about blocking Hetzner IPs",
3503
+ "",
3504
+ "\uD83D\uDCA1 TEST: Works from other networks? = Network block confirmed"
3505
+ ],
3506
+ isLikelyNetworkBlock: true
3507
+ };
3508
+ }
3509
+ if (isConnectionRefused && !serverIsRunning) {
3510
+ return {
3511
+ type: "SERVER_NOT_READY",
3512
+ message: "Server is not ready yet",
3513
+ troubleshooting: [
3514
+ "Server is still starting up",
3515
+ "SSH daemon hasn't started yet",
3516
+ "",
3517
+ "\u23F3 Wait 1-2 minutes and try again"
3518
+ ],
3519
+ isLikelyNetworkBlock: false
3520
+ };
3521
+ }
3522
+ if (errorMessage.includes("All configured authentication") || errorMessage.includes("Authentication failed")) {
3523
+ return {
3524
+ type: "AUTH_FAILED",
3525
+ message: "SSH authentication failed",
3526
+ troubleshooting: [
3527
+ "SSH key issue or wrong credentials",
3528
+ "Check that the SSH key is configured correctly"
3529
+ ],
3530
+ isLikelyNetworkBlock: false
3531
+ };
3532
+ }
3533
+ return {
3534
+ type: "UNKNOWN",
3535
+ message: errorMessage,
3536
+ troubleshooting: [
3537
+ "Check if server is running",
3538
+ "Verify network connectivity",
3539
+ "Check SSH key configuration"
3540
+ ],
3541
+ isLikelyNetworkBlock: false
3542
+ };
3543
+ }
3544
+ function formatNetworkError(diagnosis) {
3545
+ if (diagnosis.isLikelyNetworkBlock) {
3546
+ return `\u26A0\uFE0F ${diagnosis.message}
3547
+
3548
+ ${diagnosis.troubleshooting.join(`
3549
+ `)}`;
3550
+ }
3551
+ return diagnosis.message;
3552
+ }
3553
+ // src/config.ts
3554
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from "fs";
3555
+ import { join, dirname, isAbsolute, resolve } from "path";
3556
+ import { exec as exec2 } from "child_process";
3557
+ import { promisify as promisify3 } from "util";
3558
+ var execAsync3 = promisify3(exec2);
3559
+ var SSH_CONFIG_PATH = join(process.env.HOME || "~", ".ssh", "config");
3560
+ var BLOCK_START = "# >>> hetzner-codespaces managed";
3561
+ var BLOCK_END = "# <<< hetzner-codespaces managed";
3562
+ function resolveKeyPath2(keyPath) {
3563
+ if (isAbsolute(keyPath)) {
3564
+ return keyPath;
3565
+ }
3566
+ const resolved = resolve(process.cwd(), keyPath);
3567
+ if (existsSync(resolved)) {
3568
+ try {
3569
+ return realpathSync(resolved);
3570
+ } catch {
3571
+ return resolved;
3572
+ }
3573
+ }
3574
+ return keyPath;
3575
+ }
3576
+ function readSSHConfig() {
3577
+ if (!existsSync(SSH_CONFIG_PATH)) {
3578
+ return "";
3579
+ }
3580
+ return readFileSync(SSH_CONFIG_PATH, "utf-8");
3581
+ }
3582
+ function writeSSHConfig(content) {
3583
+ const sshDir = dirname(SSH_CONFIG_PATH);
3584
+ if (!existsSync(sshDir)) {
3585
+ mkdirSync(sshDir, { mode: 448, recursive: true });
3586
+ }
3587
+ writeFileSync(SSH_CONFIG_PATH, content, { mode: 384 });
3588
+ }
3589
+ function extractManagedBlock(config) {
3590
+ const startIdx = config.indexOf(BLOCK_START);
3591
+ const endIdx = config.indexOf(BLOCK_END);
3592
+ if (startIdx === -1 || endIdx === -1) {
3593
+ return { before: config, managed: "", after: "" };
3594
+ }
3595
+ return {
3596
+ before: config.substring(0, startIdx),
3597
+ managed: config.substring(startIdx, endIdx + BLOCK_END.length + 1),
3598
+ after: config.substring(endIdx + BLOCK_END.length + 1)
3599
+ };
3600
+ }
3601
+ function parseManagedEntries(managed) {
3602
+ const entries = new Map;
3603
+ const hostRegex = /# node-id: (\S+)\nHost ([^\n]+)\n([\s\S]*?)(?=# node-id:|$)/g;
3604
+ let match;
3605
+ while ((match = hostRegex.exec(managed)) !== null) {
3606
+ const id = match[1];
3607
+ const hosts = match[2].trim();
3608
+ const body = match[3];
3609
+ const hostMatch = body.match(/HostName\s+(\S+)/);
3610
+ const userMatch = body.match(/User\s+(\S+)/);
3611
+ const keyMatch = body.match(/IdentityFile\s+(\S+)/);
3612
+ const portMatch = body.match(/Port\s+(\d+)/);
3613
+ if (hostMatch && keyMatch) {
3614
+ entries.set(id, {
3615
+ id,
3616
+ name: hosts.split(/\s+/)[1] || hosts,
3617
+ host: hostMatch[1],
3618
+ user: userMatch?.[1] || "root",
3619
+ keyPath: keyMatch[1],
3620
+ port: portMatch ? parseInt(portMatch[1]) : 22
3621
+ });
3622
+ }
3623
+ }
3624
+ return entries;
3625
+ }
3626
+ function generateEntryBlock(entry) {
3627
+ const sanitizedName = entry.name.replace(/[^a-zA-Z0-9_-]/g, "-");
3628
+ const aliases = `node-${entry.id} ${sanitizedName}`;
3629
+ const absoluteKeyPath = resolveKeyPath2(entry.keyPath);
3630
+ return `# node-id: ${entry.id}
3631
+ Host ${aliases}
3632
+ HostName ${entry.host}
3633
+ User ${entry.user || "root"}
3634
+ IdentityFile "${absoluteKeyPath}"
3635
+ Port ${entry.port || 22}
3636
+ StrictHostKeyChecking no
3637
+ UserKnownHostsFile /dev/null
3638
+ LogLevel ERROR
3639
+ IdentitiesOnly yes
3640
+
3641
+ `;
3642
+ }
3643
+ function buildManagedBlock(entries) {
3644
+ if (entries.size === 0) {
3645
+ return "";
3646
+ }
3647
+ let block = `${BLOCK_START}
3648
+ `;
3649
+ block += `# Auto-generated SSH aliases for Hetzner nodes
3650
+ `;
3651
+ block += `# Do not edit manually - changes will be overwritten
3652
+
3653
+ `;
3654
+ for (const entry of entries.values()) {
3655
+ block += generateEntryBlock(entry);
3656
+ }
3657
+ block += `${BLOCK_END}
3658
+ `;
3659
+ return block;
3660
+ }
3661
+ function addSSHConfigEntry(entry) {
3662
+ const config = readSSHConfig();
3663
+ const { before, managed, after } = extractManagedBlock(config);
3664
+ const entries = parseManagedEntries(managed);
3665
+ entries.set(entry.id, entry);
3666
+ const newManaged = buildManagedBlock(entries);
3667
+ const newConfig = before.trimEnd() + `
3668
+
3669
+ ` + newManaged + after.trimStart();
3670
+ writeSSHConfig(newConfig);
3671
+ console.log(`[SSH Config] Added alias: ssh node-${entry.id} / ssh ${entry.name.replace(/[^a-zA-Z0-9_-]/g, "-")}`);
3672
+ }
3673
+ function removeSSHConfigEntry(id) {
3674
+ const config = readSSHConfig();
3675
+ const { before, managed, after } = extractManagedBlock(config);
3676
+ const entries = parseManagedEntries(managed);
3677
+ if (!entries.has(id)) {
3678
+ return;
3679
+ }
3680
+ entries.delete(id);
3681
+ const newManaged = buildManagedBlock(entries);
3682
+ const newConfig = before.trimEnd() + (newManaged ? `
3683
+
3684
+ ` + newManaged : "") + after.trimStart();
3685
+ writeSSHConfig(newConfig);
3686
+ console.log(`[SSH Config] Removed alias for node-${id}`);
3687
+ }
3688
+ function updateSSHConfigHost(id, newHost) {
3689
+ const config = readSSHConfig();
3690
+ const { before, managed, after } = extractManagedBlock(config);
3691
+ const entries = parseManagedEntries(managed);
3692
+ const entry = entries.get(id);
3693
+ if (!entry) {
3694
+ console.warn(`[SSH Config] No entry found for node-${id}`);
3695
+ return;
3696
+ }
3697
+ entry.host = newHost;
3698
+ entries.set(id, entry);
3699
+ const newManaged = buildManagedBlock(entries);
3700
+ const newConfig = before.trimEnd() + `
3701
+
3702
+ ` + newManaged + after.trimStart();
3703
+ writeSSHConfig(newConfig);
3704
+ console.log(`[SSH Config] Updated node-${id} host to ${newHost}`);
3705
+ }
3706
+ function listSSHConfigEntries() {
3707
+ const config = readSSHConfig();
3708
+ const { managed } = extractManagedBlock(config);
3709
+ const entries = parseManagedEntries(managed);
3710
+ return Array.from(entries.values());
3711
+ }
3712
+ async function validateSSHConnection(host, keyPath, user = "root", timeoutSeconds = 10) {
3713
+ try {
3714
+ const cmd = [
3715
+ "ssh",
3716
+ "-o",
3717
+ "StrictHostKeyChecking=no",
3718
+ "-o",
3719
+ "UserKnownHostsFile=/dev/null",
3720
+ "-o",
3721
+ `ConnectTimeout=${timeoutSeconds}`,
3722
+ "-o",
3723
+ "IdentitiesOnly=yes",
3724
+ "-o",
3725
+ "BatchMode=yes",
3726
+ "-i",
3727
+ keyPath,
3728
+ `${user}@${host}`,
3729
+ "echo CONNECTION_OK"
3730
+ ].join(" ");
3731
+ const { stdout } = await execAsync3(cmd, { timeout: (timeoutSeconds + 5) * 1000 });
3732
+ if (stdout.includes("CONNECTION_OK")) {
3733
+ return { success: true };
3734
+ }
3735
+ return {
3736
+ success: false,
3737
+ error: "Connection established but test command failed",
3738
+ diagnostics: stdout
3739
+ };
3740
+ } catch (error) {
3741
+ let diagnostics = "";
3742
+ if (!existsSync(keyPath)) {
3743
+ diagnostics += `Key file missing: ${keyPath}
3744
+ `;
3745
+ }
3746
+ try {
3747
+ const { stdout: agentKeys } = await execAsync3("ssh-add -l 2>&1");
3748
+ diagnostics += `ssh-agent keys:
3749
+ ${agentKeys}
3750
+ `;
3751
+ } catch {
3752
+ diagnostics += `ssh-agent: no keys loaded
3753
+ `;
3754
+ }
3755
+ return {
3756
+ success: false,
3757
+ error: error.message || String(error),
3758
+ diagnostics
3759
+ };
3760
+ }
3761
+ }
3762
+ async function ensureCorrectSSHKey(keyPath) {
3763
+ try {
3764
+ const { stdout: ourFingerprint } = await execAsync3(`ssh-keygen -lf "${keyPath}.pub"`);
3765
+ const ourFp = ourFingerprint.split(/\s+/)[1];
3766
+ const { stdout: agentList } = await execAsync3("ssh-add -l 2>&1").catch(() => ({ stdout: "" }));
3767
+ if (!agentList.includes(ourFp)) {
3768
+ await execAsync3("ssh-add -D 2>/dev/null").catch(() => {});
3769
+ await execAsync3(`ssh-add "${keyPath}"`);
3770
+ console.log(`[SSH] Added key to ssh-agent: ${keyPath}`);
3771
+ }
3772
+ } catch (error) {
3773
+ console.warn(`[SSH] Could not configure ssh-agent: ${error}`);
3774
+ }
3775
+ }
3776
+ async function waitForSSHReady(host, keyPath, options = {}) {
3777
+ const {
3778
+ user = "root",
3779
+ maxAttempts = 30,
3780
+ intervalMs = 5000,
3781
+ onAttempt
3782
+ } = options;
3783
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
3784
+ onAttempt?.(attempt, maxAttempts);
3785
+ const result = await validateSSHConnection(host, keyPath, user, 5);
3786
+ if (result.success) {
3787
+ return { success: true, attempts: attempt };
3788
+ }
3789
+ if (result.error?.includes("Permission denied")) {
3790
+ return {
3791
+ success: false,
3792
+ attempts: attempt,
3793
+ error: `SSH key rejected: ${result.error}
3794
+ ${result.diagnostics || ""}`
3795
+ };
3796
+ }
3797
+ if (attempt < maxAttempts) {
3798
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
3799
+ }
3800
+ }
3801
+ return {
3802
+ success: false,
3803
+ attempts: maxAttempts,
3804
+ error: `SSH not ready after ${maxAttempts} attempts (${maxAttempts * intervalMs / 1000}s)`
3805
+ };
3806
+ }
3807
+ async function syncNodesToSSHConfig(nodes, options = {}) {
3808
+ const { validateSSH = false, onProgress } = options;
3809
+ const results = [];
3810
+ const existingEntries = listSSHConfigEntries();
3811
+ const existingIds = new Set(existingEntries.map((e) => e.id));
3812
+ for (const node of nodes) {
3813
+ const result = {
3814
+ id: node.id,
3815
+ name: node.name,
3816
+ ip: node.ip,
3817
+ status: "added"
3818
+ };
3819
+ try {
3820
+ if (existingIds.has(node.id)) {
3821
+ const existing = existingEntries.find((e) => e.id === node.id);
3822
+ if (existing?.host === node.ip) {
3823
+ result.status = "skipped";
3824
+ } else {
3825
+ updateSSHConfigHost(node.id, node.ip);
3826
+ result.status = "updated";
3827
+ }
3828
+ } else {
3829
+ addSSHConfigEntry({
3830
+ id: node.id,
3831
+ name: node.name,
3832
+ host: node.ip,
3833
+ user: "root",
3834
+ keyPath: node.keyPath
3835
+ });
3836
+ result.status = "added";
3837
+ }
3838
+ if (validateSSH && result.status !== "skipped") {
3839
+ const sshResult = await validateSSHConnection(node.ip, node.keyPath);
3840
+ result.sshReady = sshResult.success;
3841
+ if (!sshResult.success) {
3842
+ result.error = sshResult.error;
3843
+ }
3844
+ }
3845
+ } catch (error) {
3846
+ result.status = "error";
3847
+ result.error = String(error);
3848
+ }
3849
+ results.push(result);
3850
+ onProgress?.(result);
3851
+ }
3852
+ return results;
3853
+ }
3382
3854
  export {
3383
3855
  writeToSession,
3384
3856
  writeToPTY,
3385
3857
  waitForTextInPane,
3858
+ waitForSSHReady,
3386
3859
  validateSSHKeyMatch,
3387
3860
  validateSSHKeyForServer,
3861
+ validateSSHConnection,
3862
+ updateSSHConfigHost,
3388
3863
  testSSHKeyConnection,
3389
3864
  testSSHConnection,
3865
+ syncNodesToSSHConfig,
3390
3866
  switchWindow,
3391
3867
  switchPane,
3392
3868
  switchLocalWindow,
@@ -3403,12 +3879,14 @@ export {
3403
3879
  resetTmuxManager,
3404
3880
  renameWindow,
3405
3881
  renameLocalWindow,
3882
+ removeSSHConfigEntry,
3406
3883
  readFromPTY,
3407
3884
  previewFile,
3408
3885
  normalizeFingerprint,
3409
3886
  listWindowPanes,
3410
3887
  listTmuxSessions,
3411
3888
  listSessionWindows,
3889
+ listSSHConfigEntries,
3412
3890
  listLocalWindowPanes,
3413
3891
  listLocalSessions,
3414
3892
  listLocalSessionWindows,
@@ -3430,6 +3908,7 @@ export {
3430
3908
  getSessionCount,
3431
3909
  getSession,
3432
3910
  getSecurityEvents,
3911
+ getSSHReplSession,
3433
3912
  getSSHPool,
3434
3913
  getSSHFingerprint,
3435
3914
  getPaneHistory,
@@ -3443,20 +3922,28 @@ export {
3443
3922
  getDetailedLocalSessionInfo,
3444
3923
  getAllSessions,
3445
3924
  getAllSessionInfo,
3925
+ getActiveSSHReplSessions,
3446
3926
  getActiveSSHConnections,
3447
3927
  getActivePTYSessions,
3448
3928
  generateSessionName,
3449
3929
  generateLocalSessionName,
3930
+ formatNetworkError,
3450
3931
  execViaTmuxParallel,
3451
3932
  execViaTmux,
3452
3933
  execSSHParallel,
3453
3934
  execSSH,
3935
+ execPTYBatch,
3936
+ execPTY,
3454
3937
  ensureTmux,
3938
+ ensureCorrectSSHKey,
3939
+ detectNetworkError,
3455
3940
  detachWebSocket,
3941
+ createSSHReplSession,
3456
3942
  createPTYSession,
3457
3943
  createOrAttachTmuxSession,
3458
3944
  createLocalTmuxSSHSession,
3459
3945
  closeSession,
3946
+ closeSSHReplSession,
3460
3947
  closePTYSession,
3461
3948
  closeGlobalSSHPool,
3462
3949
  clearSecurityEvents,
@@ -3466,6 +3953,7 @@ export {
3466
3953
  capturePane,
3467
3954
  captureLocalPane,
3468
3955
  attachWebSocket,
3956
+ addSSHConfigEntry,
3469
3957
  TmuxSessionManager,
3470
3958
  SSHKeyMismatchError,
3471
3959
  SSHError,