@akms/mcp-ssh 0.1.6 → 0.1.7
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/CHANGELOG.md +205 -0
- package/README.md +5 -0
- package/lib/cli.cjs +477 -364
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +480 -367
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +6 -2
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +6 -2
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
package/lib/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createHash } from 'crypto';
|
|
|
8
8
|
import { Duplex } from 'stream';
|
|
9
9
|
import { StringDecoder } from 'string_decoder';
|
|
10
10
|
import ssh2 from 'ssh2';
|
|
11
|
-
import { setInterval, clearInterval } from 'timers';
|
|
11
|
+
import { setInterval as setInterval$1, clearInterval as clearInterval$1 } from 'timers';
|
|
12
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
|
|
@@ -27,7 +27,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
27
27
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
28
28
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
29
29
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
30
|
-
var SERVER_VERSION = "0.1.
|
|
30
|
+
var SERVER_VERSION = "0.1.7";
|
|
31
31
|
|
|
32
32
|
// src/_defs/command-rules.ts
|
|
33
33
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -1640,7 +1640,7 @@ var SshConnection = class {
|
|
|
1640
1640
|
* @throws {Error} If the connection was closed, authentication fails, the host is
|
|
1641
1641
|
* unreachable, the handshake times out, or the carrier cannot be opened.
|
|
1642
1642
|
*/
|
|
1643
|
-
async connect() {
|
|
1643
|
+
async connect(onProgress) {
|
|
1644
1644
|
if (this.disposed == true) {
|
|
1645
1645
|
throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
|
|
1646
1646
|
}
|
|
@@ -1662,6 +1662,9 @@ var SshConnection = class {
|
|
|
1662
1662
|
this.observedFingerprint = fingerprint;
|
|
1663
1663
|
});
|
|
1664
1664
|
this.connecting = (async () => {
|
|
1665
|
+
if (this.profile.jump != null || this.profile.proxyCommand != null) {
|
|
1666
|
+
onProgress?.("transport");
|
|
1667
|
+
}
|
|
1665
1668
|
const transport = await openTransport(this.profile, (fingerprint) => {
|
|
1666
1669
|
this.observedJumpFingerprint = fingerprint;
|
|
1667
1670
|
});
|
|
@@ -1672,6 +1675,7 @@ var SshConnection = class {
|
|
|
1672
1675
|
if (transport != null) {
|
|
1673
1676
|
connectConfig.sock = transport.sock;
|
|
1674
1677
|
}
|
|
1678
|
+
onProgress?.("host");
|
|
1675
1679
|
await new Promise((resolve, reject) => {
|
|
1676
1680
|
const client = new Client();
|
|
1677
1681
|
this.pendingClient = client;
|
|
@@ -2127,7 +2131,7 @@ var SshSessionManager = class {
|
|
|
2127
2131
|
if (this.sweepTimer != null) {
|
|
2128
2132
|
return;
|
|
2129
2133
|
}
|
|
2130
|
-
this.sweepTimer = setInterval(() => {
|
|
2134
|
+
this.sweepTimer = setInterval$1(() => {
|
|
2131
2135
|
const expiredAt = Date.now() - SESSION_IDLE_TIMEOUT_MS;
|
|
2132
2136
|
for (const session of Array.from(this.sessions.values())) {
|
|
2133
2137
|
if (session.isBusy == true) {
|
|
@@ -2151,7 +2155,7 @@ var SshSessionManager = class {
|
|
|
2151
2155
|
if (this.sweepTimer == null) {
|
|
2152
2156
|
return;
|
|
2153
2157
|
}
|
|
2154
|
-
clearInterval(this.sweepTimer);
|
|
2158
|
+
clearInterval$1(this.sweepTimer);
|
|
2155
2159
|
this.sweepTimer = null;
|
|
2156
2160
|
}
|
|
2157
2161
|
};
|
|
@@ -2370,384 +2374,107 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2370
2374
|
return localStats.size;
|
|
2371
2375
|
}
|
|
2372
2376
|
|
|
2373
|
-
// src/modules/manage/
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
output.write(" The entry has no host.\n");
|
|
2379
|
-
}
|
|
2380
|
-
return profile;
|
|
2381
|
-
} catch (ex) {
|
|
2382
|
-
let detail = String(ex);
|
|
2383
|
-
if (ex instanceof Error) {
|
|
2384
|
-
detail = ex.message;
|
|
2385
|
-
}
|
|
2386
|
-
output.write(` The server would refuse to start with this entry: ${detail}
|
|
2387
|
-
`);
|
|
2388
|
-
return null;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
function describeAuth(profile) {
|
|
2392
|
-
if (profile.privateKeyPath != null) {
|
|
2393
|
-
return `key ${profile.privateKeyPath}`;
|
|
2394
|
-
}
|
|
2395
|
-
if (profile.password != null) {
|
|
2396
|
-
return "password";
|
|
2397
|
-
}
|
|
2398
|
-
return "ssh agent";
|
|
2399
|
-
}
|
|
2400
|
-
async function runConnectionCheck(profile, output) {
|
|
2401
|
-
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
2402
|
-
const auth = describeAuth(profile);
|
|
2403
|
-
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
2404
|
-
`);
|
|
2405
|
-
if (profile.proxyCommand != null) {
|
|
2406
|
-
output.write(` via proxy command: ${profile.proxyCommand}
|
|
2407
|
-
`);
|
|
2377
|
+
// src/modules/manage/prompter.ts
|
|
2378
|
+
var PromptClosedError = class extends Error {
|
|
2379
|
+
constructor() {
|
|
2380
|
+
super("input closed before the prompt was answered");
|
|
2381
|
+
this.name = "PromptClosedError";
|
|
2408
2382
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2383
|
+
};
|
|
2384
|
+
var PromptBackError = class extends Error {
|
|
2385
|
+
constructor() {
|
|
2386
|
+
super("prompt left with esc");
|
|
2387
|
+
this.name = "PromptBackError";
|
|
2412
2388
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2389
|
+
};
|
|
2390
|
+
var ESC = "\x1B";
|
|
2391
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
2392
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
2393
|
+
var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
|
|
2394
|
+
var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
|
|
2395
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
2396
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
2397
|
+
var KEY_DELETE = [`${ESC}[3~`];
|
|
2398
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
2399
|
+
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2400
|
+
var KEY_CTRL_C = "";
|
|
2401
|
+
var KEY_CTRL_D = "";
|
|
2402
|
+
var KEY_CLEAR_LINE = "";
|
|
2403
|
+
function splitKeys(chunk) {
|
|
2404
|
+
const keys = [];
|
|
2405
|
+
let index = 0;
|
|
2406
|
+
while (index < chunk.length) {
|
|
2407
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
2408
|
+
if (isSequence == true) {
|
|
2409
|
+
let end = index + 2;
|
|
2410
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
2411
|
+
end += 1;
|
|
2412
|
+
}
|
|
2413
|
+
keys.push(chunk.slice(index, end + 1));
|
|
2414
|
+
index = end + 1;
|
|
2415
|
+
continue;
|
|
2425
2416
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2417
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
2418
|
+
const character = String.fromCodePoint(codePoint);
|
|
2419
|
+
keys.push(character);
|
|
2420
|
+
index += character.length;
|
|
2428
2421
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
output.write(` FAILED ${endpoint} (${auth})
|
|
2435
|
-
`);
|
|
2436
|
-
output.write(` ${detail}
|
|
2437
|
-
`);
|
|
2422
|
+
return keys;
|
|
2423
|
+
}
|
|
2424
|
+
function isPrintableKey(key) {
|
|
2425
|
+
if (key.startsWith(ESC) == true) {
|
|
2426
|
+
return false;
|
|
2438
2427
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
`);
|
|
2428
|
+
const codePoint = key.codePointAt(0) ?? 0;
|
|
2429
|
+
return codePoint >= 32 && codePoint !== 127;
|
|
2430
|
+
}
|
|
2431
|
+
function charWidth(codePoint) {
|
|
2432
|
+
if (codePoint < 4352) {
|
|
2433
|
+
return 1;
|
|
2446
2434
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
pinNote = "pinned";
|
|
2451
|
-
}
|
|
2452
|
-
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
2453
|
-
`);
|
|
2435
|
+
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
2436
|
+
if (isWide == true) {
|
|
2437
|
+
return 2;
|
|
2454
2438
|
}
|
|
2455
|
-
return
|
|
2456
|
-
connected,
|
|
2457
|
-
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
2458
|
-
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
2459
|
-
};
|
|
2439
|
+
return 1;
|
|
2460
2440
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
SINGLE_HOST_ENV.PORT,
|
|
2466
|
-
SINGLE_HOST_ENV.USER,
|
|
2467
|
-
SINGLE_HOST_ENV.KEY,
|
|
2468
|
-
SINGLE_HOST_ENV.PASSPHRASE,
|
|
2469
|
-
SINGLE_HOST_ENV.PASSWORD,
|
|
2470
|
-
SINGLE_HOST_ENV.DESCRIPTION,
|
|
2471
|
-
SINGLE_HOST_ENV.JUMP_HOST,
|
|
2472
|
-
SINGLE_HOST_ENV.JUMP_PORT,
|
|
2473
|
-
SINGLE_HOST_ENV.JUMP_USER,
|
|
2474
|
-
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
2475
|
-
SINGLE_HOST_ENV.READONLY,
|
|
2476
|
-
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
2477
|
-
SINGLE_HOST_ENV.HOST_KEY,
|
|
2478
|
-
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
2479
|
-
];
|
|
2480
|
-
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
2481
|
-
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
2482
|
-
function isOurEntry(entry) {
|
|
2483
|
-
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
2484
|
-
return true;
|
|
2441
|
+
function displayWidth(text) {
|
|
2442
|
+
let width = 0;
|
|
2443
|
+
for (const character of text) {
|
|
2444
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
2485
2445
|
}
|
|
2486
|
-
|
|
2487
|
-
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
2446
|
+
return width;
|
|
2488
2447
|
}
|
|
2489
|
-
function
|
|
2490
|
-
|
|
2491
|
-
|
|
2448
|
+
function padToWidth(text, width) {
|
|
2449
|
+
const missing = width - displayWidth(text);
|
|
2450
|
+
if (missing <= 0) {
|
|
2451
|
+
return text;
|
|
2492
2452
|
}
|
|
2493
|
-
return {
|
|
2453
|
+
return `${text}${" ".repeat(missing)}`;
|
|
2494
2454
|
}
|
|
2495
|
-
function
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
2502
|
-
if (answers.auth === "key" && answers.keyPath != null) {
|
|
2503
|
-
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
2504
|
-
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
2505
|
-
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
if (answers.auth === "password" && answers.password != null) {
|
|
2509
|
-
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
2510
|
-
}
|
|
2511
|
-
if (answers.hostKeyFingerprint != null) {
|
|
2512
|
-
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
2513
|
-
}
|
|
2514
|
-
if (answers.description != null && answers.description !== "") {
|
|
2515
|
-
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
2516
|
-
}
|
|
2517
|
-
if (answers.jumpHost != null) {
|
|
2518
|
-
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
2519
|
-
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
2520
|
-
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
2521
|
-
}
|
|
2522
|
-
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
2523
|
-
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
2524
|
-
}
|
|
2525
|
-
if (answers.jumpHostKeyFingerprint != null) {
|
|
2526
|
-
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
2530
|
-
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
2531
|
-
}
|
|
2532
|
-
if (answers.readonly == true) {
|
|
2533
|
-
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
2534
|
-
}
|
|
2535
|
-
if (answers.allowedPaths.length > 0) {
|
|
2536
|
-
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
2537
|
-
}
|
|
2538
|
-
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
2539
|
-
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
2455
|
+
function groupLabelWidths(items) {
|
|
2456
|
+
const widths = items.map(() => 0);
|
|
2457
|
+
let groupStart = 0;
|
|
2458
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
2459
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
2460
|
+
if (atBoundary == false) {
|
|
2540
2461
|
continue;
|
|
2541
2462
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2463
|
+
let groupWidth = 0;
|
|
2464
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2465
|
+
const item = items[member];
|
|
2466
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
2544
2467
|
}
|
|
2545
|
-
|
|
2468
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2469
|
+
widths[member] = groupWidth;
|
|
2470
|
+
}
|
|
2471
|
+
groupStart = index + 1;
|
|
2546
2472
|
}
|
|
2547
|
-
|
|
2548
|
-
return {
|
|
2549
|
-
command: launch.command,
|
|
2550
|
-
args: launch.args,
|
|
2551
|
-
env
|
|
2552
|
-
};
|
|
2473
|
+
return widths;
|
|
2553
2474
|
}
|
|
2554
|
-
function
|
|
2555
|
-
if (
|
|
2556
|
-
return
|
|
2557
|
-
}
|
|
2558
|
-
const parsed = Number(value);
|
|
2559
|
-
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
2560
|
-
return DEFAULT_SSH_PORT;
|
|
2561
|
-
}
|
|
2562
|
-
return parsed;
|
|
2563
|
-
}
|
|
2564
|
-
function isTruthy(value) {
|
|
2565
|
-
if (value == null) {
|
|
2566
|
-
return false;
|
|
2567
|
-
}
|
|
2568
|
-
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
2569
|
-
}
|
|
2570
|
-
function entryToAnswers(name, entry) {
|
|
2571
|
-
const env = entry.env;
|
|
2572
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
2573
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
2574
|
-
let auth = "agent";
|
|
2575
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2576
|
-
auth = "key";
|
|
2577
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2578
|
-
auth = "password";
|
|
2579
|
-
}
|
|
2580
|
-
let allowedPaths = [];
|
|
2581
|
-
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
2582
|
-
if (allowedPathsValue != null) {
|
|
2583
|
-
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
2584
|
-
}
|
|
2585
|
-
const extraEnv = {};
|
|
2586
|
-
for (const [variable, value] of Object.entries(env)) {
|
|
2587
|
-
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
2588
|
-
extraEnv[variable] = value;
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
const answers = {
|
|
2592
|
-
name,
|
|
2593
|
-
host,
|
|
2594
|
-
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
2595
|
-
username,
|
|
2596
|
-
auth,
|
|
2597
|
-
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
2598
|
-
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
2599
|
-
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
2600
|
-
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
2601
|
-
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
2602
|
-
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
2603
|
-
allowedPaths,
|
|
2604
|
-
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
2605
|
-
extraEnv
|
|
2606
|
-
};
|
|
2607
|
-
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
2608
|
-
if (jumpHost != null) {
|
|
2609
|
-
answers.jumpHost = jumpHost;
|
|
2610
|
-
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
2611
|
-
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
2612
|
-
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
2613
|
-
}
|
|
2614
|
-
return answers;
|
|
2615
|
-
}
|
|
2616
|
-
function summarizeEntry(entry) {
|
|
2617
|
-
const env = entry.env;
|
|
2618
|
-
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
2619
|
-
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
2620
|
-
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
2621
|
-
let auth = "agent";
|
|
2622
|
-
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
2623
|
-
auth = "key";
|
|
2624
|
-
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
2625
|
-
auth = "password";
|
|
2626
|
-
}
|
|
2627
|
-
const notes = [];
|
|
2628
|
-
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
2629
|
-
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
2630
|
-
}
|
|
2631
|
-
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
2632
|
-
notes.push("proxy");
|
|
2633
|
-
}
|
|
2634
|
-
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
2635
|
-
notes.push("readonly");
|
|
2636
|
-
}
|
|
2637
|
-
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
2638
|
-
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
2639
|
-
}
|
|
2640
|
-
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
2641
|
-
notes.push("pinned");
|
|
2642
|
-
}
|
|
2643
|
-
return {
|
|
2644
|
-
endpoint: `${username}@${host}:${port}`,
|
|
2645
|
-
auth,
|
|
2646
|
-
notes: notes.join(", ")
|
|
2647
|
-
};
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
// src/modules/manage/prompter.ts
|
|
2651
|
-
var PromptClosedError = class extends Error {
|
|
2652
|
-
constructor() {
|
|
2653
|
-
super("input closed before the prompt was answered");
|
|
2654
|
-
this.name = "PromptClosedError";
|
|
2655
|
-
}
|
|
2656
|
-
};
|
|
2657
|
-
var PromptBackError = class extends Error {
|
|
2658
|
-
constructor() {
|
|
2659
|
-
super("prompt left with esc");
|
|
2660
|
-
this.name = "PromptBackError";
|
|
2661
|
-
}
|
|
2662
|
-
};
|
|
2663
|
-
var ESC = "\x1B";
|
|
2664
|
-
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
2665
|
-
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
2666
|
-
var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
|
|
2667
|
-
var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
|
|
2668
|
-
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
2669
|
-
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
2670
|
-
var KEY_DELETE = [`${ESC}[3~`];
|
|
2671
|
-
var KEY_ENTER = ["\r", "\n"];
|
|
2672
|
-
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2673
|
-
var KEY_CTRL_C = "";
|
|
2674
|
-
var KEY_CTRL_D = "";
|
|
2675
|
-
var KEY_CLEAR_LINE = "";
|
|
2676
|
-
function splitKeys(chunk) {
|
|
2677
|
-
const keys = [];
|
|
2678
|
-
let index = 0;
|
|
2679
|
-
while (index < chunk.length) {
|
|
2680
|
-
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
2681
|
-
if (isSequence == true) {
|
|
2682
|
-
let end = index + 2;
|
|
2683
|
-
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
2684
|
-
end += 1;
|
|
2685
|
-
}
|
|
2686
|
-
keys.push(chunk.slice(index, end + 1));
|
|
2687
|
-
index = end + 1;
|
|
2688
|
-
continue;
|
|
2689
|
-
}
|
|
2690
|
-
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
2691
|
-
const character = String.fromCodePoint(codePoint);
|
|
2692
|
-
keys.push(character);
|
|
2693
|
-
index += character.length;
|
|
2694
|
-
}
|
|
2695
|
-
return keys;
|
|
2696
|
-
}
|
|
2697
|
-
function isPrintableKey(key) {
|
|
2698
|
-
if (key.startsWith(ESC) == true) {
|
|
2699
|
-
return false;
|
|
2700
|
-
}
|
|
2701
|
-
const codePoint = key.codePointAt(0) ?? 0;
|
|
2702
|
-
return codePoint >= 32 && codePoint !== 127;
|
|
2703
|
-
}
|
|
2704
|
-
function charWidth(codePoint) {
|
|
2705
|
-
if (codePoint < 4352) {
|
|
2706
|
-
return 1;
|
|
2707
|
-
}
|
|
2708
|
-
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
2709
|
-
if (isWide == true) {
|
|
2710
|
-
return 2;
|
|
2711
|
-
}
|
|
2712
|
-
return 1;
|
|
2713
|
-
}
|
|
2714
|
-
function displayWidth(text) {
|
|
2715
|
-
let width = 0;
|
|
2716
|
-
for (const character of text) {
|
|
2717
|
-
width += charWidth(character.codePointAt(0) ?? 0);
|
|
2718
|
-
}
|
|
2719
|
-
return width;
|
|
2720
|
-
}
|
|
2721
|
-
function padToWidth(text, width) {
|
|
2722
|
-
const missing = width - displayWidth(text);
|
|
2723
|
-
if (missing <= 0) {
|
|
2724
|
-
return text;
|
|
2725
|
-
}
|
|
2726
|
-
return `${text}${" ".repeat(missing)}`;
|
|
2727
|
-
}
|
|
2728
|
-
function groupLabelWidths(items) {
|
|
2729
|
-
const widths = items.map(() => 0);
|
|
2730
|
-
let groupStart = 0;
|
|
2731
|
-
for (let index = 0; index <= items.length; index += 1) {
|
|
2732
|
-
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
2733
|
-
if (atBoundary == false) {
|
|
2734
|
-
continue;
|
|
2735
|
-
}
|
|
2736
|
-
let groupWidth = 0;
|
|
2737
|
-
for (let member = groupStart; member < index; member += 1) {
|
|
2738
|
-
const item = items[member];
|
|
2739
|
-
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
2740
|
-
}
|
|
2741
|
-
for (let member = groupStart; member < index; member += 1) {
|
|
2742
|
-
widths[member] = groupWidth;
|
|
2743
|
-
}
|
|
2744
|
-
groupStart = index + 1;
|
|
2745
|
-
}
|
|
2746
|
-
return widths;
|
|
2747
|
-
}
|
|
2748
|
-
function supportsUnicode() {
|
|
2749
|
-
if (process.platform !== "win32") {
|
|
2750
|
-
return true;
|
|
2475
|
+
function supportsUnicode() {
|
|
2476
|
+
if (process.platform !== "win32") {
|
|
2477
|
+
return true;
|
|
2751
2478
|
}
|
|
2752
2479
|
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
2753
2480
|
}
|
|
@@ -2823,6 +2550,17 @@ var Prompter = class {
|
|
|
2823
2550
|
get paint() {
|
|
2824
2551
|
return this.style;
|
|
2825
2552
|
}
|
|
2553
|
+
/** Keeps a report visible until Enter on a TTY; Esc goes back. Pipes need no extra answer. */
|
|
2554
|
+
async pause() {
|
|
2555
|
+
if (this.interactive == false) {
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
if (this.inputEnded == true) {
|
|
2559
|
+
throw new PromptClosedError();
|
|
2560
|
+
}
|
|
2561
|
+
const prompt = this.fitLine([{ text: "Press Enter to continue", paint: this.style.dim }], this.columns() - 1);
|
|
2562
|
+
await this.readText(prompt, false);
|
|
2563
|
+
}
|
|
2826
2564
|
/**
|
|
2827
2565
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2828
2566
|
*
|
|
@@ -3270,7 +3008,378 @@ var Prompter = class {
|
|
|
3270
3008
|
}
|
|
3271
3009
|
};
|
|
3272
3010
|
|
|
3273
|
-
// src/modules/manage/
|
|
3011
|
+
// src/modules/manage/check-progress.ts
|
|
3012
|
+
var ESC2 = "\x1B";
|
|
3013
|
+
var CheckProgress = class {
|
|
3014
|
+
constructor(output) {
|
|
3015
|
+
this.output = output;
|
|
3016
|
+
this.unicode = supportsUnicode();
|
|
3017
|
+
this.frames = this.unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
|
|
3018
|
+
this.timer = null;
|
|
3019
|
+
this.label = null;
|
|
3020
|
+
this.startedAt = 0;
|
|
3021
|
+
this.frame = 0;
|
|
3022
|
+
this.enabled = output.isTTY === true && process.env.TERM !== "dumb";
|
|
3023
|
+
this.color = process.env.NO_COLOR == null;
|
|
3024
|
+
}
|
|
3025
|
+
/** Advancing means the preceding phase actually completed successfully. */
|
|
3026
|
+
start(label) {
|
|
3027
|
+
if (this.enabled == false) {
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
this.finish(true);
|
|
3031
|
+
this.label = label;
|
|
3032
|
+
this.startedAt = Date.now();
|
|
3033
|
+
this.frame = 0;
|
|
3034
|
+
this.draw(this.frames[0], 36);
|
|
3035
|
+
this.timer = setInterval(() => {
|
|
3036
|
+
this.frame = (this.frame + 1) % this.frames.length;
|
|
3037
|
+
this.draw(this.frames[this.frame], 36);
|
|
3038
|
+
}, 80);
|
|
3039
|
+
this.timer.unref();
|
|
3040
|
+
}
|
|
3041
|
+
/** Leaves the completed phase and its duration visible. */
|
|
3042
|
+
finish(ok) {
|
|
3043
|
+
if (this.label == null) {
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
this.clearTimer();
|
|
3047
|
+
const symbol = ok ? this.unicode ? "\u2714" : "OK" : this.unicode ? "\u2716" : "X";
|
|
3048
|
+
this.draw(symbol, ok ? 32 : 31);
|
|
3049
|
+
this.output.write("\n");
|
|
3050
|
+
this.label = null;
|
|
3051
|
+
}
|
|
3052
|
+
/** Clears an unfinished phase without recording a result. */
|
|
3053
|
+
stop() {
|
|
3054
|
+
this.clearTimer();
|
|
3055
|
+
if (this.label != null) {
|
|
3056
|
+
this.output.write(`\r${ESC2}[2K`);
|
|
3057
|
+
this.label = null;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
clearTimer() {
|
|
3061
|
+
if (this.timer != null) {
|
|
3062
|
+
clearInterval(this.timer);
|
|
3063
|
+
this.timer = null;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
draw(symbol, color) {
|
|
3067
|
+
const columns = this.output.columns;
|
|
3068
|
+
const width = Math.max(0, (columns != null && columns > 0 ? columns : 80) - 1);
|
|
3069
|
+
const elapsed = ` ${((Date.now() - this.startedAt) / 1e3).toFixed(1)}s`;
|
|
3070
|
+
const prefix = ` ${symbol} `;
|
|
3071
|
+
const labelWidth = Math.min(48, Math.max(0, width - displayWidth(prefix) - displayWidth(elapsed)));
|
|
3072
|
+
const label = this.fit(this.label ?? "", labelWidth);
|
|
3073
|
+
const line = this.fit(`${prefix}${label}${" ".repeat(labelWidth - displayWidth(label))}${elapsed}`, width);
|
|
3074
|
+
const painted = this.color ? `${ESC2}[${color}m${line}${ESC2}[39m` : line;
|
|
3075
|
+
this.output.write(`\r${ESC2}[2K${painted}`);
|
|
3076
|
+
}
|
|
3077
|
+
fit(text, width) {
|
|
3078
|
+
if (displayWidth(text) <= width) {
|
|
3079
|
+
return text;
|
|
3080
|
+
}
|
|
3081
|
+
if (width < 1) {
|
|
3082
|
+
return "";
|
|
3083
|
+
}
|
|
3084
|
+
let result = "";
|
|
3085
|
+
let used = 0;
|
|
3086
|
+
for (const character of text) {
|
|
3087
|
+
const size = displayWidth(character);
|
|
3088
|
+
if (used + size > width - 1) {
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3091
|
+
result += character;
|
|
3092
|
+
used += size;
|
|
3093
|
+
}
|
|
3094
|
+
return `${result}${this.unicode ? "\u2026" : "~"}`;
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
// src/modules/manage/connection-check.ts
|
|
3099
|
+
function profileFromEnv(output, env) {
|
|
3100
|
+
try {
|
|
3101
|
+
const profile = loadHostProfile(env);
|
|
3102
|
+
if (profile == null) {
|
|
3103
|
+
output.write(" The entry has no host.\n");
|
|
3104
|
+
}
|
|
3105
|
+
return profile;
|
|
3106
|
+
} catch (ex) {
|
|
3107
|
+
let detail = String(ex);
|
|
3108
|
+
if (ex instanceof Error) {
|
|
3109
|
+
detail = ex.message;
|
|
3110
|
+
}
|
|
3111
|
+
output.write(` The server would refuse to start with this entry: ${detail}
|
|
3112
|
+
`);
|
|
3113
|
+
return null;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
function describeAuth(profile) {
|
|
3117
|
+
if (profile.privateKeyPath != null) {
|
|
3118
|
+
return `key ${profile.privateKeyPath}`;
|
|
3119
|
+
}
|
|
3120
|
+
if (profile.password != null) {
|
|
3121
|
+
return "password";
|
|
3122
|
+
}
|
|
3123
|
+
return "ssh agent";
|
|
3124
|
+
}
|
|
3125
|
+
async function runConnectionCheck(profile, output) {
|
|
3126
|
+
const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
|
|
3127
|
+
const auth = describeAuth(profile);
|
|
3128
|
+
output.write(`Checking ${profile.name} \u2192 ${endpoint}
|
|
3129
|
+
`);
|
|
3130
|
+
if (profile.proxyCommand != null) {
|
|
3131
|
+
output.write(` via proxy command: ${profile.proxyCommand}
|
|
3132
|
+
`);
|
|
3133
|
+
}
|
|
3134
|
+
if (profile.jump != null) {
|
|
3135
|
+
output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
|
|
3136
|
+
`);
|
|
3137
|
+
}
|
|
3138
|
+
output.write("\n");
|
|
3139
|
+
const connection = new SshConnection(profile);
|
|
3140
|
+
const progress = new CheckProgress(output);
|
|
3141
|
+
const startedAt = Date.now();
|
|
3142
|
+
let connected = false;
|
|
3143
|
+
let detail = "";
|
|
3144
|
+
try {
|
|
3145
|
+
progress.start("Preparing SSH connection");
|
|
3146
|
+
await connection.connect((stage) => {
|
|
3147
|
+
if (stage === "transport") {
|
|
3148
|
+
progress.start(profile.jump != null ? "Jump host connection and forwarding" : "Starting proxy connection");
|
|
3149
|
+
} else {
|
|
3150
|
+
progress.start("SSH handshake and authentication");
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
connected = true;
|
|
3154
|
+
} catch (ex) {
|
|
3155
|
+
detail = String(ex);
|
|
3156
|
+
if (ex instanceof Error) {
|
|
3157
|
+
detail = ex.message;
|
|
3158
|
+
}
|
|
3159
|
+
} finally {
|
|
3160
|
+
progress.finish(connected);
|
|
3161
|
+
connection.disconnect();
|
|
3162
|
+
}
|
|
3163
|
+
const durationMs = Date.now() - startedAt;
|
|
3164
|
+
if (connected == true) {
|
|
3165
|
+
output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
|
|
3166
|
+
`);
|
|
3167
|
+
} else {
|
|
3168
|
+
output.write(` FAILED ${endpoint} (${auth})
|
|
3169
|
+
`);
|
|
3170
|
+
output.write(` ${detail}
|
|
3171
|
+
`);
|
|
3172
|
+
}
|
|
3173
|
+
if (connection.hostKeyFingerprint != null) {
|
|
3174
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
|
|
3175
|
+
if (profile.hostKeyFingerprint != null) {
|
|
3176
|
+
pinNote = "pinned";
|
|
3177
|
+
}
|
|
3178
|
+
output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
|
|
3179
|
+
`);
|
|
3180
|
+
}
|
|
3181
|
+
if (connection.jumpHostKeyFingerprint != null) {
|
|
3182
|
+
let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
|
|
3183
|
+
if (profile.jump?.hostKeyFingerprint != null) {
|
|
3184
|
+
pinNote = "pinned";
|
|
3185
|
+
}
|
|
3186
|
+
output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
|
|
3187
|
+
`);
|
|
3188
|
+
}
|
|
3189
|
+
return {
|
|
3190
|
+
connected,
|
|
3191
|
+
hostKeyFingerprint: connection.hostKeyFingerprint,
|
|
3192
|
+
jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/modules/manage/entry.ts
|
|
3197
|
+
var MANAGED_ENV_NAMES = [
|
|
3198
|
+
SINGLE_HOST_ENV.HOST,
|
|
3199
|
+
SINGLE_HOST_ENV.PORT,
|
|
3200
|
+
SINGLE_HOST_ENV.USER,
|
|
3201
|
+
SINGLE_HOST_ENV.KEY,
|
|
3202
|
+
SINGLE_HOST_ENV.PASSPHRASE,
|
|
3203
|
+
SINGLE_HOST_ENV.PASSWORD,
|
|
3204
|
+
SINGLE_HOST_ENV.DESCRIPTION,
|
|
3205
|
+
SINGLE_HOST_ENV.JUMP_HOST,
|
|
3206
|
+
SINGLE_HOST_ENV.JUMP_PORT,
|
|
3207
|
+
SINGLE_HOST_ENV.JUMP_USER,
|
|
3208
|
+
SINGLE_HOST_ENV.PROXY_COMMAND,
|
|
3209
|
+
SINGLE_HOST_ENV.READONLY,
|
|
3210
|
+
SINGLE_HOST_ENV.ALLOWED_PATHS,
|
|
3211
|
+
SINGLE_HOST_ENV.HOST_KEY,
|
|
3212
|
+
SINGLE_HOST_ENV.JUMP_HOST_KEY
|
|
3213
|
+
];
|
|
3214
|
+
var JUMP_ENV_PREFIX = "SSH_JUMP_";
|
|
3215
|
+
var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
|
|
3216
|
+
function isOurEntry(entry) {
|
|
3217
|
+
if (entry.args.includes(PACKAGE_NAME) == true) {
|
|
3218
|
+
return true;
|
|
3219
|
+
}
|
|
3220
|
+
const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
3221
|
+
return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
|
|
3222
|
+
}
|
|
3223
|
+
function launchCommand() {
|
|
3224
|
+
if (process.platform === "win32") {
|
|
3225
|
+
return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
|
|
3226
|
+
}
|
|
3227
|
+
return { command: "npx", args: ["-y", PACKAGE_NAME] };
|
|
3228
|
+
}
|
|
3229
|
+
function answersToEntry(answers) {
|
|
3230
|
+
const env = {};
|
|
3231
|
+
env[SINGLE_HOST_ENV.HOST] = answers.host;
|
|
3232
|
+
if (answers.port !== DEFAULT_SSH_PORT) {
|
|
3233
|
+
env[SINGLE_HOST_ENV.PORT] = String(answers.port);
|
|
3234
|
+
}
|
|
3235
|
+
env[SINGLE_HOST_ENV.USER] = answers.username;
|
|
3236
|
+
if (answers.auth === "key" && answers.keyPath != null) {
|
|
3237
|
+
env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
|
|
3238
|
+
if (answers.passphrase != null && answers.passphrase !== "") {
|
|
3239
|
+
env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
if (answers.auth === "password" && answers.password != null) {
|
|
3243
|
+
env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
|
|
3244
|
+
}
|
|
3245
|
+
if (answers.hostKeyFingerprint != null) {
|
|
3246
|
+
env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
|
|
3247
|
+
}
|
|
3248
|
+
if (answers.description != null && answers.description !== "") {
|
|
3249
|
+
env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
|
|
3250
|
+
}
|
|
3251
|
+
if (answers.jumpHost != null) {
|
|
3252
|
+
env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
|
|
3253
|
+
if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
|
|
3254
|
+
env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
|
|
3255
|
+
}
|
|
3256
|
+
if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
|
|
3257
|
+
env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
|
|
3258
|
+
}
|
|
3259
|
+
if (answers.jumpHostKeyFingerprint != null) {
|
|
3260
|
+
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
if (answers.proxyCommand != null && answers.proxyCommand !== "") {
|
|
3264
|
+
env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
|
|
3265
|
+
}
|
|
3266
|
+
if (answers.readonly == true) {
|
|
3267
|
+
env[SINGLE_HOST_ENV.READONLY] = "true";
|
|
3268
|
+
}
|
|
3269
|
+
if (answers.allowedPaths.length > 0) {
|
|
3270
|
+
env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
|
|
3271
|
+
}
|
|
3272
|
+
for (const [name, value] of Object.entries(answers.extraEnv)) {
|
|
3273
|
+
if (MANAGED_ENV_NAMES.includes(name) == true) {
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
env[name] = value;
|
|
3280
|
+
}
|
|
3281
|
+
const launch = launchCommand();
|
|
3282
|
+
return {
|
|
3283
|
+
command: launch.command,
|
|
3284
|
+
args: launch.args,
|
|
3285
|
+
env
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
function readPort(value) {
|
|
3289
|
+
if (value == null) {
|
|
3290
|
+
return DEFAULT_SSH_PORT;
|
|
3291
|
+
}
|
|
3292
|
+
const parsed = Number(value);
|
|
3293
|
+
if (Number.isInteger(parsed) == false || parsed <= 0) {
|
|
3294
|
+
return DEFAULT_SSH_PORT;
|
|
3295
|
+
}
|
|
3296
|
+
return parsed;
|
|
3297
|
+
}
|
|
3298
|
+
function isTruthy(value) {
|
|
3299
|
+
if (value == null) {
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
|
|
3303
|
+
}
|
|
3304
|
+
function entryToAnswers(name, entry) {
|
|
3305
|
+
const env = entry.env;
|
|
3306
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "";
|
|
3307
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "";
|
|
3308
|
+
let auth = "agent";
|
|
3309
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3310
|
+
auth = "key";
|
|
3311
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3312
|
+
auth = "password";
|
|
3313
|
+
}
|
|
3314
|
+
let allowedPaths = [];
|
|
3315
|
+
const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
|
|
3316
|
+
if (allowedPathsValue != null) {
|
|
3317
|
+
allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3318
|
+
}
|
|
3319
|
+
const extraEnv = {};
|
|
3320
|
+
for (const [variable, value] of Object.entries(env)) {
|
|
3321
|
+
if (MANAGED_ENV_NAMES.includes(variable) == false) {
|
|
3322
|
+
extraEnv[variable] = value;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
const answers = {
|
|
3326
|
+
name,
|
|
3327
|
+
host,
|
|
3328
|
+
port: readPort(env[SINGLE_HOST_ENV.PORT]),
|
|
3329
|
+
username,
|
|
3330
|
+
auth,
|
|
3331
|
+
keyPath: env[SINGLE_HOST_ENV.KEY],
|
|
3332
|
+
passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
|
|
3333
|
+
password: env[SINGLE_HOST_ENV.PASSWORD],
|
|
3334
|
+
description: env[SINGLE_HOST_ENV.DESCRIPTION],
|
|
3335
|
+
proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
|
|
3336
|
+
readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
|
|
3337
|
+
allowedPaths,
|
|
3338
|
+
hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
|
|
3339
|
+
extraEnv
|
|
3340
|
+
};
|
|
3341
|
+
const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
|
|
3342
|
+
if (jumpHost != null) {
|
|
3343
|
+
answers.jumpHost = jumpHost;
|
|
3344
|
+
answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
|
|
3345
|
+
answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
|
|
3346
|
+
answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
|
|
3347
|
+
}
|
|
3348
|
+
return answers;
|
|
3349
|
+
}
|
|
3350
|
+
function summarizeEntry(entry) {
|
|
3351
|
+
const env = entry.env;
|
|
3352
|
+
const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
|
|
3353
|
+
const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
|
|
3354
|
+
const username = env[SINGLE_HOST_ENV.USER] ?? "?";
|
|
3355
|
+
let auth = "agent";
|
|
3356
|
+
if (env[SINGLE_HOST_ENV.KEY] != null) {
|
|
3357
|
+
auth = "key";
|
|
3358
|
+
} else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
|
|
3359
|
+
auth = "password";
|
|
3360
|
+
}
|
|
3361
|
+
const notes = [];
|
|
3362
|
+
if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
|
|
3363
|
+
notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
|
|
3364
|
+
}
|
|
3365
|
+
if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
|
|
3366
|
+
notes.push("proxy");
|
|
3367
|
+
}
|
|
3368
|
+
if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
|
|
3369
|
+
notes.push("readonly");
|
|
3370
|
+
}
|
|
3371
|
+
if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
|
|
3372
|
+
notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
|
|
3373
|
+
}
|
|
3374
|
+
if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
|
|
3375
|
+
notes.push("pinned");
|
|
3376
|
+
}
|
|
3377
|
+
return {
|
|
3378
|
+
endpoint: `${username}@${host}:${port}`,
|
|
3379
|
+
auth,
|
|
3380
|
+
notes: notes.join(", ")
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3274
3383
|
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3275
3384
|
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3276
3385
|
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
@@ -3626,10 +3735,12 @@ async function testConnection(prompter, output, state) {
|
|
|
3626
3735
|
if (profile == null) {
|
|
3627
3736
|
output.write("\n");
|
|
3628
3737
|
state.lastTest = "failed";
|
|
3738
|
+
await prompter.pause();
|
|
3629
3739
|
return false;
|
|
3630
3740
|
}
|
|
3631
3741
|
const result = await runConnectionCheck(profile, output);
|
|
3632
3742
|
output.write("\n");
|
|
3743
|
+
await prompter.pause();
|
|
3633
3744
|
if (result.connected == true) {
|
|
3634
3745
|
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3635
3746
|
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
@@ -4738,10 +4849,12 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
4738
4849
|
const profile = profileFromEnv(output, entry.env);
|
|
4739
4850
|
if (profile == null) {
|
|
4740
4851
|
output.write("\n");
|
|
4852
|
+
await prompter.pause();
|
|
4741
4853
|
return;
|
|
4742
4854
|
}
|
|
4743
4855
|
const result = await runConnectionCheck(profile, output);
|
|
4744
4856
|
output.write("\n");
|
|
4857
|
+
await prompter.pause();
|
|
4745
4858
|
if (result.connected == false) {
|
|
4746
4859
|
return;
|
|
4747
4860
|
}
|