@akms/mcp-ssh 0.1.5 → 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/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
 
@@ -22,11 +22,12 @@ var DEFAULT_MAX_READ_FILE_BYTES = 2e5;
22
22
  var KEEPALIVE_INTERVAL_MS = 15e3;
23
23
  var SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
24
24
  var SESSION_SWEEP_INTERVAL_MS = 6e4;
25
+ var SHUTDOWN_DEADLINE_MS = 5e3;
25
26
  var CWD_MARKER = "__AKMS_SSH_CWD__";
26
27
  var MAX_DIRECTORY_ENTRIES = 1e3;
27
28
  var SERVER_NAME = "akms-mcp-ssh";
28
29
  var PACKAGE_NAME = "@akms/mcp-ssh";
29
- var SERVER_VERSION = "0.1.5";
30
+ var SERVER_VERSION = "0.1.7";
30
31
 
31
32
  // src/_defs/command-rules.ts
32
33
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -489,6 +490,10 @@ var LEVEL_WEIGHTS = {
489
490
  error: 4
490
491
  };
491
492
  var SECRET_META_KEYS = ["password", "passphrase", "privatekey", "privatekeypath", "secret", "token"];
493
+ var stderrBroken = false;
494
+ process.stderr.on("error", () => {
495
+ stderrBroken = true;
496
+ });
492
497
  function resolveThreshold() {
493
498
  const configured = process.env[LOG_LEVEL_ENV];
494
499
  if (configured == null) {
@@ -512,6 +517,9 @@ function maskMeta(meta) {
512
517
  return masked;
513
518
  }
514
519
  function write(level, message, meta) {
520
+ if (stderrBroken == true) {
521
+ return;
522
+ }
515
523
  const threshold = resolveThreshold();
516
524
  if (threshold === "silent") {
517
525
  return;
@@ -525,8 +533,12 @@ function write(level, message, meta) {
525
533
  if (Object.keys(masked).length > 0) {
526
534
  line += ` ${JSON.stringify(masked)}`;
527
535
  }
528
- process.stderr.write(`${line}
536
+ try {
537
+ process.stderr.write(`${line}
529
538
  `);
539
+ } catch {
540
+ stderrBroken = true;
541
+ }
530
542
  }
531
543
  function createLogger(boundMeta) {
532
544
  return {
@@ -1628,7 +1640,7 @@ var SshConnection = class {
1628
1640
  * @throws {Error} If the connection was closed, authentication fails, the host is
1629
1641
  * unreachable, the handshake times out, or the carrier cannot be opened.
1630
1642
  */
1631
- async connect() {
1643
+ async connect(onProgress) {
1632
1644
  if (this.disposed == true) {
1633
1645
  throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1634
1646
  }
@@ -1650,6 +1662,9 @@ var SshConnection = class {
1650
1662
  this.observedFingerprint = fingerprint;
1651
1663
  });
1652
1664
  this.connecting = (async () => {
1665
+ if (this.profile.jump != null || this.profile.proxyCommand != null) {
1666
+ onProgress?.("transport");
1667
+ }
1653
1668
  const transport = await openTransport(this.profile, (fingerprint) => {
1654
1669
  this.observedJumpFingerprint = fingerprint;
1655
1670
  });
@@ -1660,6 +1675,7 @@ var SshConnection = class {
1660
1675
  if (transport != null) {
1661
1676
  connectConfig.sock = transport.sock;
1662
1677
  }
1678
+ onProgress?.("host");
1663
1679
  await new Promise((resolve, reject) => {
1664
1680
  const client = new Client();
1665
1681
  this.pendingClient = client;
@@ -2115,7 +2131,7 @@ var SshSessionManager = class {
2115
2131
  if (this.sweepTimer != null) {
2116
2132
  return;
2117
2133
  }
2118
- this.sweepTimer = setInterval(() => {
2134
+ this.sweepTimer = setInterval$1(() => {
2119
2135
  const expiredAt = Date.now() - SESSION_IDLE_TIMEOUT_MS;
2120
2136
  for (const session of Array.from(this.sessions.values())) {
2121
2137
  if (session.isBusy == true) {
@@ -2139,7 +2155,7 @@ var SshSessionManager = class {
2139
2155
  if (this.sweepTimer == null) {
2140
2156
  return;
2141
2157
  }
2142
- clearInterval(this.sweepTimer);
2158
+ clearInterval$1(this.sweepTimer);
2143
2159
  this.sweepTimer = null;
2144
2160
  }
2145
2161
  };
@@ -2358,386 +2374,109 @@ async function downloadFile(connection, remotePath, localPath) {
2358
2374
  return localStats.size;
2359
2375
  }
2360
2376
 
2361
- // src/modules/manage/connection-check.ts
2362
- function profileFromEnv(output, env) {
2363
- try {
2364
- const profile = loadHostProfile(env);
2365
- if (profile == null) {
2366
- output.write(" The entry has no host.\n");
2367
- }
2368
- return profile;
2369
- } catch (ex) {
2370
- let detail = String(ex);
2371
- if (ex instanceof Error) {
2372
- detail = ex.message;
2373
- }
2374
- output.write(` The server would refuse to start with this entry: ${detail}
2375
- `);
2376
- return null;
2377
- }
2378
- }
2379
- function describeAuth(profile) {
2380
- if (profile.privateKeyPath != null) {
2381
- return `key ${profile.privateKeyPath}`;
2382
- }
2383
- if (profile.password != null) {
2384
- return "password";
2385
- }
2386
- return "ssh agent";
2387
- }
2388
- async function runConnectionCheck(profile, output) {
2389
- const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
2390
- const auth = describeAuth(profile);
2391
- output.write(`Checking ${profile.name} \u2192 ${endpoint}
2392
- `);
2393
- if (profile.proxyCommand != null) {
2394
- output.write(` via proxy command: ${profile.proxyCommand}
2395
- `);
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";
2396
2382
  }
2397
- if (profile.jump != null) {
2398
- output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
2399
- `);
2383
+ };
2384
+ var PromptBackError = class extends Error {
2385
+ constructor() {
2386
+ super("prompt left with esc");
2387
+ this.name = "PromptBackError";
2400
2388
  }
2401
- output.write("\n");
2402
- const connection = new SshConnection(profile);
2403
- const startedAt = Date.now();
2404
- let connected = false;
2405
- let detail = "";
2406
- try {
2407
- await connection.connect();
2408
- connected = true;
2409
- } catch (ex) {
2410
- detail = String(ex);
2411
- if (ex instanceof Error) {
2412
- detail = ex.message;
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;
2413
2416
  }
2414
- } finally {
2415
- connection.disconnect();
2417
+ const codePoint = chunk.codePointAt(index) ?? 0;
2418
+ const character = String.fromCodePoint(codePoint);
2419
+ keys.push(character);
2420
+ index += character.length;
2416
2421
  }
2417
- const durationMs = Date.now() - startedAt;
2418
- if (connected == true) {
2419
- output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
2420
- `);
2421
- } else {
2422
- output.write(` FAILED ${endpoint} (${auth})
2423
- `);
2424
- output.write(` ${detail}
2425
- `);
2422
+ return keys;
2423
+ }
2424
+ function isPrintableKey(key) {
2425
+ if (key.startsWith(ESC) == true) {
2426
+ return false;
2426
2427
  }
2427
- if (connection.hostKeyFingerprint != null) {
2428
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
2429
- if (profile.hostKeyFingerprint != null) {
2430
- pinNote = "pinned";
2431
- }
2432
- output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
2433
- `);
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;
2434
2434
  }
2435
- if (connection.jumpHostKeyFingerprint != null) {
2436
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
2437
- if (profile.jump?.hostKeyFingerprint != null) {
2438
- pinNote = "pinned";
2439
- }
2440
- output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
2441
- `);
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;
2442
2438
  }
2443
- return {
2444
- connected,
2445
- hostKeyFingerprint: connection.hostKeyFingerprint,
2446
- jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
2447
- };
2439
+ return 1;
2448
2440
  }
2449
-
2450
- // src/modules/manage/entry.ts
2451
- var MANAGED_ENV_NAMES = [
2452
- SINGLE_HOST_ENV.HOST,
2453
- SINGLE_HOST_ENV.PORT,
2454
- SINGLE_HOST_ENV.USER,
2455
- SINGLE_HOST_ENV.KEY,
2456
- SINGLE_HOST_ENV.PASSPHRASE,
2457
- SINGLE_HOST_ENV.PASSWORD,
2458
- SINGLE_HOST_ENV.DESCRIPTION,
2459
- SINGLE_HOST_ENV.JUMP_HOST,
2460
- SINGLE_HOST_ENV.JUMP_PORT,
2461
- SINGLE_HOST_ENV.JUMP_USER,
2462
- SINGLE_HOST_ENV.PROXY_COMMAND,
2463
- SINGLE_HOST_ENV.READONLY,
2464
- SINGLE_HOST_ENV.ALLOWED_PATHS,
2465
- SINGLE_HOST_ENV.HOST_KEY,
2466
- SINGLE_HOST_ENV.JUMP_HOST_KEY
2467
- ];
2468
- var JUMP_ENV_PREFIX = "SSH_JUMP_";
2469
- var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
2470
- function isOurEntry(entry) {
2471
- if (entry.args.includes(PACKAGE_NAME) == true) {
2472
- return true;
2441
+ function displayWidth(text) {
2442
+ let width = 0;
2443
+ for (const character of text) {
2444
+ width += charWidth(character.codePointAt(0) ?? 0);
2473
2445
  }
2474
- const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
2475
- return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
2446
+ return width;
2476
2447
  }
2477
- function launchCommand() {
2478
- if (process.platform === "win32") {
2479
- return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
2448
+ function padToWidth(text, width) {
2449
+ const missing = width - displayWidth(text);
2450
+ if (missing <= 0) {
2451
+ return text;
2480
2452
  }
2481
- return { command: "npx", args: ["-y", PACKAGE_NAME] };
2453
+ return `${text}${" ".repeat(missing)}`;
2482
2454
  }
2483
- function answersToEntry(answers) {
2484
- const env = {};
2485
- env[SINGLE_HOST_ENV.HOST] = answers.host;
2486
- if (answers.port !== DEFAULT_SSH_PORT) {
2487
- env[SINGLE_HOST_ENV.PORT] = String(answers.port);
2488
- }
2489
- env[SINGLE_HOST_ENV.USER] = answers.username;
2490
- if (answers.auth === "key" && answers.keyPath != null) {
2491
- env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
2492
- if (answers.passphrase != null && answers.passphrase !== "") {
2493
- env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
2494
- }
2495
- }
2496
- if (answers.auth === "password" && answers.password != null) {
2497
- env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
2498
- }
2499
- if (answers.hostKeyFingerprint != null) {
2500
- env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
2501
- }
2502
- if (answers.description != null && answers.description !== "") {
2503
- env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
2504
- }
2505
- if (answers.jumpHost != null) {
2506
- env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
2507
- if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
2508
- env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
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) {
2461
+ continue;
2509
2462
  }
2510
- if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
2511
- env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
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));
2512
2467
  }
2513
- if (answers.jumpHostKeyFingerprint != null) {
2514
- env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
2468
+ for (let member = groupStart; member < index; member += 1) {
2469
+ widths[member] = groupWidth;
2515
2470
  }
2471
+ groupStart = index + 1;
2516
2472
  }
2517
- if (answers.proxyCommand != null && answers.proxyCommand !== "") {
2518
- env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
2519
- }
2520
- if (answers.readonly == true) {
2521
- env[SINGLE_HOST_ENV.READONLY] = "true";
2522
- }
2523
- if (answers.allowedPaths.length > 0) {
2524
- env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
2525
- }
2526
- for (const [name, value] of Object.entries(answers.extraEnv)) {
2527
- if (MANAGED_ENV_NAMES.includes(name) == true) {
2528
- continue;
2529
- }
2530
- if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
2531
- continue;
2532
- }
2533
- env[name] = value;
2473
+ return widths;
2474
+ }
2475
+ function supportsUnicode() {
2476
+ if (process.platform !== "win32") {
2477
+ return true;
2534
2478
  }
2535
- const launch = launchCommand();
2536
- return {
2537
- command: launch.command,
2538
- args: launch.args,
2539
- env
2540
- };
2541
- }
2542
- function readPort(value) {
2543
- if (value == null) {
2544
- return DEFAULT_SSH_PORT;
2545
- }
2546
- const parsed = Number(value);
2547
- if (Number.isInteger(parsed) == false || parsed <= 0) {
2548
- return DEFAULT_SSH_PORT;
2549
- }
2550
- return parsed;
2551
- }
2552
- function isTruthy(value) {
2553
- if (value == null) {
2554
- return false;
2555
- }
2556
- return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
2557
- }
2558
- function entryToAnswers(name, entry) {
2559
- const env = entry.env;
2560
- const host = env[SINGLE_HOST_ENV.HOST] ?? "";
2561
- const username = env[SINGLE_HOST_ENV.USER] ?? "";
2562
- let auth = "agent";
2563
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2564
- auth = "key";
2565
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2566
- auth = "password";
2567
- }
2568
- let allowedPaths = [];
2569
- const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
2570
- if (allowedPathsValue != null) {
2571
- allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
2572
- }
2573
- const extraEnv = {};
2574
- for (const [variable, value] of Object.entries(env)) {
2575
- if (MANAGED_ENV_NAMES.includes(variable) == false) {
2576
- extraEnv[variable] = value;
2577
- }
2578
- }
2579
- const answers = {
2580
- name,
2581
- host,
2582
- port: readPort(env[SINGLE_HOST_ENV.PORT]),
2583
- username,
2584
- auth,
2585
- keyPath: env[SINGLE_HOST_ENV.KEY],
2586
- passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
2587
- password: env[SINGLE_HOST_ENV.PASSWORD],
2588
- description: env[SINGLE_HOST_ENV.DESCRIPTION],
2589
- proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
2590
- readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
2591
- allowedPaths,
2592
- hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
2593
- extraEnv
2594
- };
2595
- const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
2596
- if (jumpHost != null) {
2597
- answers.jumpHost = jumpHost;
2598
- answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
2599
- answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
2600
- answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
2601
- }
2602
- return answers;
2603
- }
2604
- function summarizeEntry(entry) {
2605
- const env = entry.env;
2606
- const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
2607
- const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
2608
- const username = env[SINGLE_HOST_ENV.USER] ?? "?";
2609
- let auth = "agent";
2610
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2611
- auth = "key";
2612
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2613
- auth = "password";
2614
- }
2615
- const notes = [];
2616
- if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
2617
- notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
2618
- }
2619
- if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
2620
- notes.push("proxy");
2621
- }
2622
- if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
2623
- notes.push("readonly");
2624
- }
2625
- if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
2626
- notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
2627
- }
2628
- if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
2629
- notes.push("pinned");
2630
- }
2631
- return {
2632
- endpoint: `${username}@${host}:${port}`,
2633
- auth,
2634
- notes: notes.join(", ")
2635
- };
2636
- }
2637
-
2638
- // src/modules/manage/prompter.ts
2639
- var PromptClosedError = class extends Error {
2640
- constructor() {
2641
- super("input closed before the prompt was answered");
2642
- this.name = "PromptClosedError";
2643
- }
2644
- };
2645
- var PromptBackError = class extends Error {
2646
- constructor() {
2647
- super("prompt left with esc");
2648
- this.name = "PromptBackError";
2649
- }
2650
- };
2651
- var ESC = "\x1B";
2652
- var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
2653
- var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
2654
- var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
2655
- var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
2656
- var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
2657
- var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
2658
- var KEY_DELETE = [`${ESC}[3~`];
2659
- var KEY_ENTER = ["\r", "\n"];
2660
- var KEY_BACKSPACE = ["\x7F", "\b"];
2661
- var KEY_CTRL_C = "";
2662
- var KEY_CTRL_D = "";
2663
- var KEY_CLEAR_LINE = "";
2664
- function splitKeys(chunk) {
2665
- const keys = [];
2666
- let index = 0;
2667
- while (index < chunk.length) {
2668
- const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
2669
- if (isSequence == true) {
2670
- let end = index + 2;
2671
- while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
2672
- end += 1;
2673
- }
2674
- keys.push(chunk.slice(index, end + 1));
2675
- index = end + 1;
2676
- continue;
2677
- }
2678
- const codePoint = chunk.codePointAt(index) ?? 0;
2679
- const character = String.fromCodePoint(codePoint);
2680
- keys.push(character);
2681
- index += character.length;
2682
- }
2683
- return keys;
2684
- }
2685
- function isPrintableKey(key) {
2686
- if (key.startsWith(ESC) == true) {
2687
- return false;
2688
- }
2689
- const codePoint = key.codePointAt(0) ?? 0;
2690
- return codePoint >= 32 && codePoint !== 127;
2691
- }
2692
- function charWidth(codePoint) {
2693
- if (codePoint < 4352) {
2694
- return 1;
2695
- }
2696
- 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;
2697
- if (isWide == true) {
2698
- return 2;
2699
- }
2700
- return 1;
2701
- }
2702
- function displayWidth(text) {
2703
- let width = 0;
2704
- for (const character of text) {
2705
- width += charWidth(character.codePointAt(0) ?? 0);
2706
- }
2707
- return width;
2708
- }
2709
- function padToWidth(text, width) {
2710
- const missing = width - displayWidth(text);
2711
- if (missing <= 0) {
2712
- return text;
2713
- }
2714
- return `${text}${" ".repeat(missing)}`;
2715
- }
2716
- function groupLabelWidths(items) {
2717
- const widths = items.map(() => 0);
2718
- let groupStart = 0;
2719
- for (let index = 0; index <= items.length; index += 1) {
2720
- const atBoundary = index === items.length || "value" in items[index] == false;
2721
- if (atBoundary == false) {
2722
- continue;
2723
- }
2724
- let groupWidth = 0;
2725
- for (let member = groupStart; member < index; member += 1) {
2726
- const item = items[member];
2727
- groupWidth = Math.max(groupWidth, displayWidth(item.label));
2728
- }
2729
- for (let member = groupStart; member < index; member += 1) {
2730
- widths[member] = groupWidth;
2731
- }
2732
- groupStart = index + 1;
2733
- }
2734
- return widths;
2735
- }
2736
- function supportsUnicode() {
2737
- if (process.platform !== "win32") {
2738
- return true;
2739
- }
2740
- return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2479
+ return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2741
2480
  }
2742
2481
  function createStyle(enabled) {
2743
2482
  const paint = (open, close) => {
@@ -2811,6 +2550,17 @@ var Prompter = class {
2811
2550
  get paint() {
2812
2551
  return this.style;
2813
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
+ }
2814
2564
  /**
2815
2565
  * Free-text prompt. Enter accepts `defaultValue` when one is shown.
2816
2566
  *
@@ -3117,148 +2867,519 @@ var Prompter = class {
3117
2867
  if (columns == null || columns < 20) {
3118
2868
  return 80;
3119
2869
  }
3120
- return columns;
2870
+ return columns;
2871
+ }
2872
+ /** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
2873
+ fitLine(parts, width) {
2874
+ let used = 0;
2875
+ let line = "";
2876
+ for (const part of parts) {
2877
+ let text = "";
2878
+ for (const character of part.text) {
2879
+ const characterWidth = charWidth(character.codePointAt(0) ?? 0);
2880
+ if (used + characterWidth > width) {
2881
+ break;
2882
+ }
2883
+ used += characterWidth;
2884
+ text += character;
2885
+ }
2886
+ if (part.paint != null) {
2887
+ line += part.paint(text);
2888
+ } else {
2889
+ line += text;
2890
+ }
2891
+ if (used >= width) {
2892
+ break;
2893
+ }
2894
+ }
2895
+ return line;
2896
+ }
2897
+ /** The one-line record an answered prompt leaves behind on a TTY. */
2898
+ writeAnswered(question, answer) {
2899
+ if (this.interactive == false || this.transient == true) {
2900
+ return;
2901
+ }
2902
+ this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
2903
+ `);
2904
+ }
2905
+ /** @returns The picked value, or null on Esc. */
2906
+ selectWithKeys(question, items, startIndex) {
2907
+ const ttyInput = this.input;
2908
+ const selectableIndexes = items.flatMap((item, index) => {
2909
+ if ("value" in item) {
2910
+ return [index];
2911
+ }
2912
+ return [];
2913
+ });
2914
+ const labelWidths = groupLabelWidths(items);
2915
+ const ruleWidth = Math.min(Math.max(...labelWidths), 24);
2916
+ let active = startIndex;
2917
+ let renderedLines = 0;
2918
+ const draw = (lines) => {
2919
+ let text = "";
2920
+ if (renderedLines > 1) {
2921
+ text += `${ESC}[${renderedLines - 1}A`;
2922
+ }
2923
+ text += `\r${ESC}[J${lines.join("\n")}`;
2924
+ this.output.write(text);
2925
+ renderedLines = lines.length;
2926
+ };
2927
+ const buildLines = () => {
2928
+ const width = this.columns() - 1;
2929
+ const lines = [
2930
+ this.fitLine([
2931
+ { text: this.symbols.question, paint: this.style.accent },
2932
+ { text: " " },
2933
+ { text: question, paint: this.style.bold }
2934
+ ], width)
2935
+ ];
2936
+ for (let index = 0; index < items.length; index += 1) {
2937
+ const item = items[index];
2938
+ if ("value" in item == false) {
2939
+ lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
2940
+ continue;
2941
+ }
2942
+ const isActive = index === active;
2943
+ const parts = [];
2944
+ if (isActive == true) {
2945
+ parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
2946
+ parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
2947
+ } else {
2948
+ parts.push({ text: " " });
2949
+ parts.push({ text: padToWidth(item.label, labelWidths[index]) });
2950
+ }
2951
+ if (item.hint != null && item.hint !== "") {
2952
+ parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
2953
+ }
2954
+ lines.push(this.fitLine(parts, width));
2955
+ }
2956
+ lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
2957
+ return lines;
2958
+ };
2959
+ const move = (step) => {
2960
+ const position = selectableIndexes.indexOf(active);
2961
+ const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
2962
+ active = selectableIndexes[next];
2963
+ };
2964
+ return new Promise((resolve, reject) => {
2965
+ const finish = () => {
2966
+ this.keyHandler = null;
2967
+ ttyInput.setRawMode?.(false);
2968
+ draw([]);
2969
+ this.output.write(`${ESC}[?25h`);
2970
+ };
2971
+ this.keyHandler = (key) => {
2972
+ if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
2973
+ finish();
2974
+ reject(new PromptClosedError());
2975
+ return;
2976
+ }
2977
+ if (KEY_ENTER.includes(key) == true) {
2978
+ const picked = items[active];
2979
+ finish();
2980
+ this.writeAnswered(question, picked.label);
2981
+ resolve(picked.value);
2982
+ return;
2983
+ }
2984
+ if (key === ESC) {
2985
+ finish();
2986
+ resolve(null);
2987
+ return;
2988
+ }
2989
+ if (KEY_UP.includes(key) == true) {
2990
+ move(-1);
2991
+ } else if (KEY_DOWN.includes(key) == true) {
2992
+ move(1);
2993
+ } else if (KEY_HOME.includes(key) == true) {
2994
+ active = selectableIndexes[0];
2995
+ } else if (KEY_END.includes(key) == true) {
2996
+ active = selectableIndexes[selectableIndexes.length - 1];
2997
+ } else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
2998
+ active = selectableIndexes[Number(key) - 1];
2999
+ } else {
3000
+ return;
3001
+ }
3002
+ draw(buildLines());
3003
+ };
3004
+ ttyInput.setRawMode?.(true);
3005
+ this.output.write(`${ESC}[?25l`);
3006
+ draw(buildLines());
3007
+ });
3008
+ }
3009
+ };
3010
+
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
+ }
3121
3262
  }
3122
- /** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
3123
- fitLine(parts, width) {
3124
- let used = 0;
3125
- let line = "";
3126
- for (const part of parts) {
3127
- let text = "";
3128
- for (const character of part.text) {
3129
- const characterWidth = charWidth(character.codePointAt(0) ?? 0);
3130
- if (used + characterWidth > width) {
3131
- break;
3132
- }
3133
- used += characterWidth;
3134
- text += character;
3135
- }
3136
- if (part.paint != null) {
3137
- line += part.paint(text);
3138
- } else {
3139
- line += text;
3140
- }
3141
- if (used >= width) {
3142
- break;
3143
- }
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;
3144
3275
  }
3145
- return line;
3276
+ if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
3277
+ continue;
3278
+ }
3279
+ env[name] = value;
3146
3280
  }
3147
- /** The one-line record an answered prompt leaves behind on a TTY. */
3148
- writeAnswered(question, answer) {
3149
- if (this.interactive == false || this.transient == true) {
3150
- return;
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;
3151
3323
  }
3152
- this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
3153
- `);
3154
3324
  }
3155
- /** @returns The picked value, or null on Esc. */
3156
- selectWithKeys(question, items, startIndex) {
3157
- const ttyInput = this.input;
3158
- const selectableIndexes = items.flatMap((item, index) => {
3159
- if ("value" in item) {
3160
- return [index];
3161
- }
3162
- return [];
3163
- });
3164
- const labelWidths = groupLabelWidths(items);
3165
- const ruleWidth = Math.min(Math.max(...labelWidths), 24);
3166
- let active = startIndex;
3167
- let renderedLines = 0;
3168
- const draw = (lines) => {
3169
- let text = "";
3170
- if (renderedLines > 1) {
3171
- text += `${ESC}[${renderedLines - 1}A`;
3172
- }
3173
- text += `\r${ESC}[J${lines.join("\n")}`;
3174
- this.output.write(text);
3175
- renderedLines = lines.length;
3176
- };
3177
- const buildLines = () => {
3178
- const width = this.columns() - 1;
3179
- const lines = [
3180
- this.fitLine([
3181
- { text: this.symbols.question, paint: this.style.accent },
3182
- { text: " " },
3183
- { text: question, paint: this.style.bold }
3184
- ], width)
3185
- ];
3186
- for (let index = 0; index < items.length; index += 1) {
3187
- const item = items[index];
3188
- if ("value" in item == false) {
3189
- lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
3190
- continue;
3191
- }
3192
- const isActive = index === active;
3193
- const parts = [];
3194
- if (isActive == true) {
3195
- parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
3196
- parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
3197
- } else {
3198
- parts.push({ text: " " });
3199
- parts.push({ text: padToWidth(item.label, labelWidths[index]) });
3200
- }
3201
- if (item.hint != null && item.hint !== "") {
3202
- parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
3203
- }
3204
- lines.push(this.fitLine(parts, width));
3205
- }
3206
- lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
3207
- return lines;
3208
- };
3209
- const move = (step) => {
3210
- const position = selectableIndexes.indexOf(active);
3211
- const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
3212
- active = selectableIndexes[next];
3213
- };
3214
- return new Promise((resolve, reject) => {
3215
- const finish = () => {
3216
- this.keyHandler = null;
3217
- ttyInput.setRawMode?.(false);
3218
- draw([]);
3219
- this.output.write(`${ESC}[?25h`);
3220
- };
3221
- this.keyHandler = (key) => {
3222
- if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
3223
- finish();
3224
- reject(new PromptClosedError());
3225
- return;
3226
- }
3227
- if (KEY_ENTER.includes(key) == true) {
3228
- const picked = items[active];
3229
- finish();
3230
- this.writeAnswered(question, picked.label);
3231
- resolve(picked.value);
3232
- return;
3233
- }
3234
- if (key === ESC) {
3235
- finish();
3236
- resolve(null);
3237
- return;
3238
- }
3239
- if (KEY_UP.includes(key) == true) {
3240
- move(-1);
3241
- } else if (KEY_DOWN.includes(key) == true) {
3242
- move(1);
3243
- } else if (KEY_HOME.includes(key) == true) {
3244
- active = selectableIndexes[0];
3245
- } else if (KEY_END.includes(key) == true) {
3246
- active = selectableIndexes[selectableIndexes.length - 1];
3247
- } else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
3248
- active = selectableIndexes[Number(key) - 1];
3249
- } else {
3250
- return;
3251
- }
3252
- draw(buildLines());
3253
- };
3254
- ttyInput.setRawMode?.(true);
3255
- this.output.write(`${ESC}[?25l`);
3256
- draw(buildLines());
3257
- });
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];
3258
3347
  }
3259
- };
3260
-
3261
- // src/modules/manage/host-form.ts
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
+ }
3262
3383
  var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
3263
3384
  var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
3264
3385
  var MAX_KEY_FILE_BYTES = 64 * 1024;
@@ -3614,10 +3735,12 @@ async function testConnection(prompter, output, state) {
3614
3735
  if (profile == null) {
3615
3736
  output.write("\n");
3616
3737
  state.lastTest = "failed";
3738
+ await prompter.pause();
3617
3739
  return false;
3618
3740
  }
3619
3741
  const result = await runConnectionCheck(profile, output);
3620
3742
  output.write("\n");
3743
+ await prompter.pause();
3621
3744
  if (result.connected == true) {
3622
3745
  if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
3623
3746
  const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
@@ -4726,10 +4849,12 @@ async function checkHost(prompter, output, target, name, entry) {
4726
4849
  const profile = profileFromEnv(output, entry.env);
4727
4850
  if (profile == null) {
4728
4851
  output.write("\n");
4852
+ await prompter.pause();
4729
4853
  return;
4730
4854
  }
4731
4855
  const result = await runConnectionCheck(profile, output);
4732
4856
  output.write("\n");
4857
+ await prompter.pause();
4733
4858
  if (result.connected == false) {
4734
4859
  return;
4735
4860
  }
@@ -5765,6 +5890,12 @@ async function main() {
5765
5890
  instance.shutdown();
5766
5891
  process.exit(exitCode);
5767
5892
  }
5893
+ process.on("uncaughtException", (error) => {
5894
+ _logger.error(error, "uncaught exception; the server keeps running");
5895
+ });
5896
+ process.on("unhandledRejection", (reason) => {
5897
+ _logger.error(reason, "unhandled rejection; the server keeps running");
5898
+ });
5768
5899
  let shuttingDown = false;
5769
5900
  const shutdown = (signal) => {
5770
5901
  if (shuttingDown == true) {
@@ -5772,6 +5903,10 @@ async function main() {
5772
5903
  }
5773
5904
  shuttingDown = true;
5774
5905
  _logger.info("shutting down", { signal });
5906
+ setTimeout(() => {
5907
+ _logger.warn("shutdown did not finish in time; exiting anyway", { deadline_ms: SHUTDOWN_DEADLINE_MS });
5908
+ process.exit(0);
5909
+ }, SHUTDOWN_DEADLINE_MS).unref();
5775
5910
  instance.shutdown();
5776
5911
  void instance.server.close().finally(() => {
5777
5912
  process.exit(0);
@@ -5791,7 +5926,8 @@ async function main() {
5791
5926
  _logger.info("mcp server ready on stdio", {
5792
5927
  server: SERVER_NAME,
5793
5928
  version: SERVER_VERSION,
5794
- profile: instance.profile?.name ?? null
5929
+ profile: instance.profile?.name ?? null,
5930
+ pid: process.pid
5795
5931
  });
5796
5932
  }
5797
5933
  main().catch((error) => {