@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/cpufeatures-7nkbz0at.node +0 -0
- package/dist/index.d.ts +1 -14
- package/dist/index.js +640 -152
- package/dist/lib/activities.d.ts +11 -0
- package/dist/lib/hetzner/client.d.ts +6 -0
- package/dist/lib/hetzner/types.d.ts +13 -0
- package/dist/manager.d.ts +22 -1
- package/dist/mcp/index.js +98 -147
- package/dist/sessions.d.ts +3 -1
- package/dist/sshcrypto-jh8dt6n3.node +0 -0
- package/dist/tmux-manager.d.ts +4 -3
- package/package.json +15 -2
- package/dist/mcp/index.d.ts +0 -8
- package/dist/mcp/stdio.d.ts +0 -8
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
source:
|
|
463
|
-
destination:
|
|
464
|
-
recursive:
|
|
465
|
-
preserve:
|
|
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
|
-
|
|
468
|
-
host:
|
|
469
|
-
user:
|
|
470
|
-
path:
|
|
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
|
-
|
|
473
|
-
host:
|
|
474
|
-
user:
|
|
475
|
-
path:
|
|
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 =
|
|
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 =
|
|
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, {
|
|
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 =
|
|
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 =
|
|
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
|
|
2372
|
-
await
|
|
2373
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|