@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.cjs CHANGED
@@ -31,11 +31,12 @@ var DEFAULT_MAX_READ_FILE_BYTES = 2e5;
31
31
  var KEEPALIVE_INTERVAL_MS = 15e3;
32
32
  var SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
33
33
  var SESSION_SWEEP_INTERVAL_MS = 6e4;
34
+ var SHUTDOWN_DEADLINE_MS = 5e3;
34
35
  var CWD_MARKER = "__AKMS_SSH_CWD__";
35
36
  var MAX_DIRECTORY_ENTRIES = 1e3;
36
37
  var SERVER_NAME = "akms-mcp-ssh";
37
38
  var PACKAGE_NAME = "@akms/mcp-ssh";
38
- var SERVER_VERSION = "0.1.5";
39
+ var SERVER_VERSION = "0.1.7";
39
40
 
40
41
  // src/_defs/command-rules.ts
41
42
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -498,6 +499,10 @@ var LEVEL_WEIGHTS = {
498
499
  error: 4
499
500
  };
500
501
  var SECRET_META_KEYS = ["password", "passphrase", "privatekey", "privatekeypath", "secret", "token"];
502
+ var stderrBroken = false;
503
+ process.stderr.on("error", () => {
504
+ stderrBroken = true;
505
+ });
501
506
  function resolveThreshold() {
502
507
  const configured = process.env[LOG_LEVEL_ENV];
503
508
  if (configured == null) {
@@ -521,6 +526,9 @@ function maskMeta(meta) {
521
526
  return masked;
522
527
  }
523
528
  function write(level, message, meta) {
529
+ if (stderrBroken == true) {
530
+ return;
531
+ }
524
532
  const threshold = resolveThreshold();
525
533
  if (threshold === "silent") {
526
534
  return;
@@ -534,8 +542,12 @@ function write(level, message, meta) {
534
542
  if (Object.keys(masked).length > 0) {
535
543
  line += ` ${JSON.stringify(masked)}`;
536
544
  }
537
- process.stderr.write(`${line}
545
+ try {
546
+ process.stderr.write(`${line}
538
547
  `);
548
+ } catch {
549
+ stderrBroken = true;
550
+ }
539
551
  }
540
552
  function createLogger(boundMeta) {
541
553
  return {
@@ -1637,7 +1649,7 @@ var SshConnection = class {
1637
1649
  * @throws {Error} If the connection was closed, authentication fails, the host is
1638
1650
  * unreachable, the handshake times out, or the carrier cannot be opened.
1639
1651
  */
1640
- async connect() {
1652
+ async connect(onProgress) {
1641
1653
  if (this.disposed == true) {
1642
1654
  throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1643
1655
  }
@@ -1659,6 +1671,9 @@ var SshConnection = class {
1659
1671
  this.observedFingerprint = fingerprint;
1660
1672
  });
1661
1673
  this.connecting = (async () => {
1674
+ if (this.profile.jump != null || this.profile.proxyCommand != null) {
1675
+ onProgress?.("transport");
1676
+ }
1662
1677
  const transport = await openTransport(this.profile, (fingerprint) => {
1663
1678
  this.observedJumpFingerprint = fingerprint;
1664
1679
  });
@@ -1669,6 +1684,7 @@ var SshConnection = class {
1669
1684
  if (transport != null) {
1670
1685
  connectConfig.sock = transport.sock;
1671
1686
  }
1687
+ onProgress?.("host");
1672
1688
  await new Promise((resolve, reject) => {
1673
1689
  const client = new Client();
1674
1690
  this.pendingClient = client;
@@ -2367,386 +2383,109 @@ async function downloadFile(connection, remotePath, localPath) {
2367
2383
  return localStats.size;
2368
2384
  }
2369
2385
 
2370
- // src/modules/manage/connection-check.ts
2371
- function profileFromEnv(output, env) {
2372
- try {
2373
- const profile = loadHostProfile(env);
2374
- if (profile == null) {
2375
- output.write(" The entry has no host.\n");
2376
- }
2377
- return profile;
2378
- } catch (ex) {
2379
- let detail = String(ex);
2380
- if (ex instanceof Error) {
2381
- detail = ex.message;
2382
- }
2383
- output.write(` The server would refuse to start with this entry: ${detail}
2384
- `);
2385
- return null;
2386
- }
2387
- }
2388
- function describeAuth(profile) {
2389
- if (profile.privateKeyPath != null) {
2390
- return `key ${profile.privateKeyPath}`;
2391
- }
2392
- if (profile.password != null) {
2393
- return "password";
2394
- }
2395
- return "ssh agent";
2396
- }
2397
- async function runConnectionCheck(profile, output) {
2398
- const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
2399
- const auth = describeAuth(profile);
2400
- output.write(`Checking ${profile.name} \u2192 ${endpoint}
2401
- `);
2402
- if (profile.proxyCommand != null) {
2403
- output.write(` via proxy command: ${profile.proxyCommand}
2404
- `);
2386
+ // src/modules/manage/prompter.ts
2387
+ var PromptClosedError = class extends Error {
2388
+ constructor() {
2389
+ super("input closed before the prompt was answered");
2390
+ this.name = "PromptClosedError";
2405
2391
  }
2406
- if (profile.jump != null) {
2407
- output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
2408
- `);
2392
+ };
2393
+ var PromptBackError = class extends Error {
2394
+ constructor() {
2395
+ super("prompt left with esc");
2396
+ this.name = "PromptBackError";
2409
2397
  }
2410
- output.write("\n");
2411
- const connection = new SshConnection(profile);
2412
- const startedAt = Date.now();
2413
- let connected = false;
2414
- let detail = "";
2415
- try {
2416
- await connection.connect();
2417
- connected = true;
2418
- } catch (ex) {
2419
- detail = String(ex);
2420
- if (ex instanceof Error) {
2421
- detail = ex.message;
2398
+ };
2399
+ var ESC = "\x1B";
2400
+ var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
2401
+ var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
2402
+ var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
2403
+ var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
2404
+ var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
2405
+ var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
2406
+ var KEY_DELETE = [`${ESC}[3~`];
2407
+ var KEY_ENTER = ["\r", "\n"];
2408
+ var KEY_BACKSPACE = ["\x7F", "\b"];
2409
+ var KEY_CTRL_C = "";
2410
+ var KEY_CTRL_D = "";
2411
+ var KEY_CLEAR_LINE = "";
2412
+ function splitKeys(chunk) {
2413
+ const keys = [];
2414
+ let index = 0;
2415
+ while (index < chunk.length) {
2416
+ const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
2417
+ if (isSequence == true) {
2418
+ let end = index + 2;
2419
+ while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
2420
+ end += 1;
2421
+ }
2422
+ keys.push(chunk.slice(index, end + 1));
2423
+ index = end + 1;
2424
+ continue;
2422
2425
  }
2423
- } finally {
2424
- connection.disconnect();
2426
+ const codePoint = chunk.codePointAt(index) ?? 0;
2427
+ const character = String.fromCodePoint(codePoint);
2428
+ keys.push(character);
2429
+ index += character.length;
2425
2430
  }
2426
- const durationMs = Date.now() - startedAt;
2427
- if (connected == true) {
2428
- output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
2429
- `);
2430
- } else {
2431
- output.write(` FAILED ${endpoint} (${auth})
2432
- `);
2433
- output.write(` ${detail}
2434
- `);
2431
+ return keys;
2432
+ }
2433
+ function isPrintableKey(key) {
2434
+ if (key.startsWith(ESC) == true) {
2435
+ return false;
2435
2436
  }
2436
- if (connection.hostKeyFingerprint != null) {
2437
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
2438
- if (profile.hostKeyFingerprint != null) {
2439
- pinNote = "pinned";
2440
- }
2441
- output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
2442
- `);
2437
+ const codePoint = key.codePointAt(0) ?? 0;
2438
+ return codePoint >= 32 && codePoint !== 127;
2439
+ }
2440
+ function charWidth(codePoint) {
2441
+ if (codePoint < 4352) {
2442
+ return 1;
2443
2443
  }
2444
- if (connection.jumpHostKeyFingerprint != null) {
2445
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
2446
- if (profile.jump?.hostKeyFingerprint != null) {
2447
- pinNote = "pinned";
2448
- }
2449
- output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
2450
- `);
2444
+ 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;
2445
+ if (isWide == true) {
2446
+ return 2;
2451
2447
  }
2452
- return {
2453
- connected,
2454
- hostKeyFingerprint: connection.hostKeyFingerprint,
2455
- jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
2456
- };
2448
+ return 1;
2457
2449
  }
2458
-
2459
- // src/modules/manage/entry.ts
2460
- var MANAGED_ENV_NAMES = [
2461
- SINGLE_HOST_ENV.HOST,
2462
- SINGLE_HOST_ENV.PORT,
2463
- SINGLE_HOST_ENV.USER,
2464
- SINGLE_HOST_ENV.KEY,
2465
- SINGLE_HOST_ENV.PASSPHRASE,
2466
- SINGLE_HOST_ENV.PASSWORD,
2467
- SINGLE_HOST_ENV.DESCRIPTION,
2468
- SINGLE_HOST_ENV.JUMP_HOST,
2469
- SINGLE_HOST_ENV.JUMP_PORT,
2470
- SINGLE_HOST_ENV.JUMP_USER,
2471
- SINGLE_HOST_ENV.PROXY_COMMAND,
2472
- SINGLE_HOST_ENV.READONLY,
2473
- SINGLE_HOST_ENV.ALLOWED_PATHS,
2474
- SINGLE_HOST_ENV.HOST_KEY,
2475
- SINGLE_HOST_ENV.JUMP_HOST_KEY
2476
- ];
2477
- var JUMP_ENV_PREFIX = "SSH_JUMP_";
2478
- var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
2479
- function isOurEntry(entry) {
2480
- if (entry.args.includes(PACKAGE_NAME) == true) {
2481
- return true;
2450
+ function displayWidth(text) {
2451
+ let width = 0;
2452
+ for (const character of text) {
2453
+ width += charWidth(character.codePointAt(0) ?? 0);
2482
2454
  }
2483
- const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
2484
- return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
2455
+ return width;
2485
2456
  }
2486
- function launchCommand() {
2487
- if (process.platform === "win32") {
2488
- return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
2457
+ function padToWidth(text, width) {
2458
+ const missing = width - displayWidth(text);
2459
+ if (missing <= 0) {
2460
+ return text;
2489
2461
  }
2490
- return { command: "npx", args: ["-y", PACKAGE_NAME] };
2462
+ return `${text}${" ".repeat(missing)}`;
2491
2463
  }
2492
- function answersToEntry(answers) {
2493
- const env = {};
2494
- env[SINGLE_HOST_ENV.HOST] = answers.host;
2495
- if (answers.port !== DEFAULT_SSH_PORT) {
2496
- env[SINGLE_HOST_ENV.PORT] = String(answers.port);
2497
- }
2498
- env[SINGLE_HOST_ENV.USER] = answers.username;
2499
- if (answers.auth === "key" && answers.keyPath != null) {
2500
- env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
2501
- if (answers.passphrase != null && answers.passphrase !== "") {
2502
- env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
2503
- }
2504
- }
2505
- if (answers.auth === "password" && answers.password != null) {
2506
- env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
2507
- }
2508
- if (answers.hostKeyFingerprint != null) {
2509
- env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
2510
- }
2511
- if (answers.description != null && answers.description !== "") {
2512
- env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
2513
- }
2514
- if (answers.jumpHost != null) {
2515
- env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
2516
- if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
2517
- env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
2464
+ function groupLabelWidths(items) {
2465
+ const widths = items.map(() => 0);
2466
+ let groupStart = 0;
2467
+ for (let index = 0; index <= items.length; index += 1) {
2468
+ const atBoundary = index === items.length || "value" in items[index] == false;
2469
+ if (atBoundary == false) {
2470
+ continue;
2518
2471
  }
2519
- if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
2520
- env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
2472
+ let groupWidth = 0;
2473
+ for (let member = groupStart; member < index; member += 1) {
2474
+ const item = items[member];
2475
+ groupWidth = Math.max(groupWidth, displayWidth(item.label));
2521
2476
  }
2522
- if (answers.jumpHostKeyFingerprint != null) {
2523
- env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
2477
+ for (let member = groupStart; member < index; member += 1) {
2478
+ widths[member] = groupWidth;
2524
2479
  }
2480
+ groupStart = index + 1;
2525
2481
  }
2526
- if (answers.proxyCommand != null && answers.proxyCommand !== "") {
2527
- env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
2528
- }
2529
- if (answers.readonly == true) {
2530
- env[SINGLE_HOST_ENV.READONLY] = "true";
2531
- }
2532
- if (answers.allowedPaths.length > 0) {
2533
- env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
2534
- }
2535
- for (const [name, value] of Object.entries(answers.extraEnv)) {
2536
- if (MANAGED_ENV_NAMES.includes(name) == true) {
2537
- continue;
2538
- }
2539
- if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
2540
- continue;
2541
- }
2542
- env[name] = value;
2482
+ return widths;
2483
+ }
2484
+ function supportsUnicode() {
2485
+ if (process.platform !== "win32") {
2486
+ return true;
2543
2487
  }
2544
- const launch = launchCommand();
2545
- return {
2546
- command: launch.command,
2547
- args: launch.args,
2548
- env
2549
- };
2550
- }
2551
- function readPort(value) {
2552
- if (value == null) {
2553
- return DEFAULT_SSH_PORT;
2554
- }
2555
- const parsed = Number(value);
2556
- if (Number.isInteger(parsed) == false || parsed <= 0) {
2557
- return DEFAULT_SSH_PORT;
2558
- }
2559
- return parsed;
2560
- }
2561
- function isTruthy(value) {
2562
- if (value == null) {
2563
- return false;
2564
- }
2565
- return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
2566
- }
2567
- function entryToAnswers(name, entry) {
2568
- const env = entry.env;
2569
- const host = env[SINGLE_HOST_ENV.HOST] ?? "";
2570
- const username = env[SINGLE_HOST_ENV.USER] ?? "";
2571
- let auth = "agent";
2572
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2573
- auth = "key";
2574
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2575
- auth = "password";
2576
- }
2577
- let allowedPaths = [];
2578
- const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
2579
- if (allowedPathsValue != null) {
2580
- allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
2581
- }
2582
- const extraEnv = {};
2583
- for (const [variable, value] of Object.entries(env)) {
2584
- if (MANAGED_ENV_NAMES.includes(variable) == false) {
2585
- extraEnv[variable] = value;
2586
- }
2587
- }
2588
- const answers = {
2589
- name,
2590
- host,
2591
- port: readPort(env[SINGLE_HOST_ENV.PORT]),
2592
- username,
2593
- auth,
2594
- keyPath: env[SINGLE_HOST_ENV.KEY],
2595
- passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
2596
- password: env[SINGLE_HOST_ENV.PASSWORD],
2597
- description: env[SINGLE_HOST_ENV.DESCRIPTION],
2598
- proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
2599
- readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
2600
- allowedPaths,
2601
- hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
2602
- extraEnv
2603
- };
2604
- const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
2605
- if (jumpHost != null) {
2606
- answers.jumpHost = jumpHost;
2607
- answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
2608
- answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
2609
- answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
2610
- }
2611
- return answers;
2612
- }
2613
- function summarizeEntry(entry) {
2614
- const env = entry.env;
2615
- const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
2616
- const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
2617
- const username = env[SINGLE_HOST_ENV.USER] ?? "?";
2618
- let auth = "agent";
2619
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2620
- auth = "key";
2621
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2622
- auth = "password";
2623
- }
2624
- const notes = [];
2625
- if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
2626
- notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
2627
- }
2628
- if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
2629
- notes.push("proxy");
2630
- }
2631
- if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
2632
- notes.push("readonly");
2633
- }
2634
- if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
2635
- notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
2636
- }
2637
- if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
2638
- notes.push("pinned");
2639
- }
2640
- return {
2641
- endpoint: `${username}@${host}:${port}`,
2642
- auth,
2643
- notes: notes.join(", ")
2644
- };
2645
- }
2646
-
2647
- // src/modules/manage/prompter.ts
2648
- var PromptClosedError = class extends Error {
2649
- constructor() {
2650
- super("input closed before the prompt was answered");
2651
- this.name = "PromptClosedError";
2652
- }
2653
- };
2654
- var PromptBackError = class extends Error {
2655
- constructor() {
2656
- super("prompt left with esc");
2657
- this.name = "PromptBackError";
2658
- }
2659
- };
2660
- var ESC = "\x1B";
2661
- var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
2662
- var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
2663
- var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
2664
- var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
2665
- var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
2666
- var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
2667
- var KEY_DELETE = [`${ESC}[3~`];
2668
- var KEY_ENTER = ["\r", "\n"];
2669
- var KEY_BACKSPACE = ["\x7F", "\b"];
2670
- var KEY_CTRL_C = "";
2671
- var KEY_CTRL_D = "";
2672
- var KEY_CLEAR_LINE = "";
2673
- function splitKeys(chunk) {
2674
- const keys = [];
2675
- let index = 0;
2676
- while (index < chunk.length) {
2677
- const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
2678
- if (isSequence == true) {
2679
- let end = index + 2;
2680
- while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
2681
- end += 1;
2682
- }
2683
- keys.push(chunk.slice(index, end + 1));
2684
- index = end + 1;
2685
- continue;
2686
- }
2687
- const codePoint = chunk.codePointAt(index) ?? 0;
2688
- const character = String.fromCodePoint(codePoint);
2689
- keys.push(character);
2690
- index += character.length;
2691
- }
2692
- return keys;
2693
- }
2694
- function isPrintableKey(key) {
2695
- if (key.startsWith(ESC) == true) {
2696
- return false;
2697
- }
2698
- const codePoint = key.codePointAt(0) ?? 0;
2699
- return codePoint >= 32 && codePoint !== 127;
2700
- }
2701
- function charWidth(codePoint) {
2702
- if (codePoint < 4352) {
2703
- return 1;
2704
- }
2705
- 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;
2706
- if (isWide == true) {
2707
- return 2;
2708
- }
2709
- return 1;
2710
- }
2711
- function displayWidth(text) {
2712
- let width = 0;
2713
- for (const character of text) {
2714
- width += charWidth(character.codePointAt(0) ?? 0);
2715
- }
2716
- return width;
2717
- }
2718
- function padToWidth(text, width) {
2719
- const missing = width - displayWidth(text);
2720
- if (missing <= 0) {
2721
- return text;
2722
- }
2723
- return `${text}${" ".repeat(missing)}`;
2724
- }
2725
- function groupLabelWidths(items) {
2726
- const widths = items.map(() => 0);
2727
- let groupStart = 0;
2728
- for (let index = 0; index <= items.length; index += 1) {
2729
- const atBoundary = index === items.length || "value" in items[index] == false;
2730
- if (atBoundary == false) {
2731
- continue;
2732
- }
2733
- let groupWidth = 0;
2734
- for (let member = groupStart; member < index; member += 1) {
2735
- const item = items[member];
2736
- groupWidth = Math.max(groupWidth, displayWidth(item.label));
2737
- }
2738
- for (let member = groupStart; member < index; member += 1) {
2739
- widths[member] = groupWidth;
2740
- }
2741
- groupStart = index + 1;
2742
- }
2743
- return widths;
2744
- }
2745
- function supportsUnicode() {
2746
- if (process.platform !== "win32") {
2747
- return true;
2748
- }
2749
- return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2488
+ return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2750
2489
  }
2751
2490
  function createStyle(enabled) {
2752
2491
  const paint = (open, close) => {
@@ -2820,6 +2559,17 @@ var Prompter = class {
2820
2559
  get paint() {
2821
2560
  return this.style;
2822
2561
  }
2562
+ /** Keeps a report visible until Enter on a TTY; Esc goes back. Pipes need no extra answer. */
2563
+ async pause() {
2564
+ if (this.interactive == false) {
2565
+ return;
2566
+ }
2567
+ if (this.inputEnded == true) {
2568
+ throw new PromptClosedError();
2569
+ }
2570
+ const prompt = this.fitLine([{ text: "Press Enter to continue", paint: this.style.dim }], this.columns() - 1);
2571
+ await this.readText(prompt, false);
2572
+ }
2823
2573
  /**
2824
2574
  * Free-text prompt. Enter accepts `defaultValue` when one is shown.
2825
2575
  *
@@ -3126,148 +2876,519 @@ var Prompter = class {
3126
2876
  if (columns == null || columns < 20) {
3127
2877
  return 80;
3128
2878
  }
3129
- return columns;
2879
+ return columns;
2880
+ }
2881
+ /** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
2882
+ fitLine(parts, width) {
2883
+ let used = 0;
2884
+ let line = "";
2885
+ for (const part of parts) {
2886
+ let text = "";
2887
+ for (const character of part.text) {
2888
+ const characterWidth = charWidth(character.codePointAt(0) ?? 0);
2889
+ if (used + characterWidth > width) {
2890
+ break;
2891
+ }
2892
+ used += characterWidth;
2893
+ text += character;
2894
+ }
2895
+ if (part.paint != null) {
2896
+ line += part.paint(text);
2897
+ } else {
2898
+ line += text;
2899
+ }
2900
+ if (used >= width) {
2901
+ break;
2902
+ }
2903
+ }
2904
+ return line;
2905
+ }
2906
+ /** The one-line record an answered prompt leaves behind on a TTY. */
2907
+ writeAnswered(question, answer) {
2908
+ if (this.interactive == false || this.transient == true) {
2909
+ return;
2910
+ }
2911
+ this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
2912
+ `);
2913
+ }
2914
+ /** @returns The picked value, or null on Esc. */
2915
+ selectWithKeys(question, items, startIndex) {
2916
+ const ttyInput = this.input;
2917
+ const selectableIndexes = items.flatMap((item, index) => {
2918
+ if ("value" in item) {
2919
+ return [index];
2920
+ }
2921
+ return [];
2922
+ });
2923
+ const labelWidths = groupLabelWidths(items);
2924
+ const ruleWidth = Math.min(Math.max(...labelWidths), 24);
2925
+ let active = startIndex;
2926
+ let renderedLines = 0;
2927
+ const draw = (lines) => {
2928
+ let text = "";
2929
+ if (renderedLines > 1) {
2930
+ text += `${ESC}[${renderedLines - 1}A`;
2931
+ }
2932
+ text += `\r${ESC}[J${lines.join("\n")}`;
2933
+ this.output.write(text);
2934
+ renderedLines = lines.length;
2935
+ };
2936
+ const buildLines = () => {
2937
+ const width = this.columns() - 1;
2938
+ const lines = [
2939
+ this.fitLine([
2940
+ { text: this.symbols.question, paint: this.style.accent },
2941
+ { text: " " },
2942
+ { text: question, paint: this.style.bold }
2943
+ ], width)
2944
+ ];
2945
+ for (let index = 0; index < items.length; index += 1) {
2946
+ const item = items[index];
2947
+ if ("value" in item == false) {
2948
+ lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
2949
+ continue;
2950
+ }
2951
+ const isActive = index === active;
2952
+ const parts = [];
2953
+ if (isActive == true) {
2954
+ parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
2955
+ parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
2956
+ } else {
2957
+ parts.push({ text: " " });
2958
+ parts.push({ text: padToWidth(item.label, labelWidths[index]) });
2959
+ }
2960
+ if (item.hint != null && item.hint !== "") {
2961
+ parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
2962
+ }
2963
+ lines.push(this.fitLine(parts, width));
2964
+ }
2965
+ lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
2966
+ return lines;
2967
+ };
2968
+ const move = (step) => {
2969
+ const position = selectableIndexes.indexOf(active);
2970
+ const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
2971
+ active = selectableIndexes[next];
2972
+ };
2973
+ return new Promise((resolve, reject) => {
2974
+ const finish = () => {
2975
+ this.keyHandler = null;
2976
+ ttyInput.setRawMode?.(false);
2977
+ draw([]);
2978
+ this.output.write(`${ESC}[?25h`);
2979
+ };
2980
+ this.keyHandler = (key) => {
2981
+ if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
2982
+ finish();
2983
+ reject(new PromptClosedError());
2984
+ return;
2985
+ }
2986
+ if (KEY_ENTER.includes(key) == true) {
2987
+ const picked = items[active];
2988
+ finish();
2989
+ this.writeAnswered(question, picked.label);
2990
+ resolve(picked.value);
2991
+ return;
2992
+ }
2993
+ if (key === ESC) {
2994
+ finish();
2995
+ resolve(null);
2996
+ return;
2997
+ }
2998
+ if (KEY_UP.includes(key) == true) {
2999
+ move(-1);
3000
+ } else if (KEY_DOWN.includes(key) == true) {
3001
+ move(1);
3002
+ } else if (KEY_HOME.includes(key) == true) {
3003
+ active = selectableIndexes[0];
3004
+ } else if (KEY_END.includes(key) == true) {
3005
+ active = selectableIndexes[selectableIndexes.length - 1];
3006
+ } else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
3007
+ active = selectableIndexes[Number(key) - 1];
3008
+ } else {
3009
+ return;
3010
+ }
3011
+ draw(buildLines());
3012
+ };
3013
+ ttyInput.setRawMode?.(true);
3014
+ this.output.write(`${ESC}[?25l`);
3015
+ draw(buildLines());
3016
+ });
3017
+ }
3018
+ };
3019
+
3020
+ // src/modules/manage/check-progress.ts
3021
+ var ESC2 = "\x1B";
3022
+ var CheckProgress = class {
3023
+ constructor(output) {
3024
+ this.output = output;
3025
+ this.unicode = supportsUnicode();
3026
+ this.frames = this.unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
3027
+ this.timer = null;
3028
+ this.label = null;
3029
+ this.startedAt = 0;
3030
+ this.frame = 0;
3031
+ this.enabled = output.isTTY === true && process.env.TERM !== "dumb";
3032
+ this.color = process.env.NO_COLOR == null;
3033
+ }
3034
+ /** Advancing means the preceding phase actually completed successfully. */
3035
+ start(label) {
3036
+ if (this.enabled == false) {
3037
+ return;
3038
+ }
3039
+ this.finish(true);
3040
+ this.label = label;
3041
+ this.startedAt = Date.now();
3042
+ this.frame = 0;
3043
+ this.draw(this.frames[0], 36);
3044
+ this.timer = setInterval(() => {
3045
+ this.frame = (this.frame + 1) % this.frames.length;
3046
+ this.draw(this.frames[this.frame], 36);
3047
+ }, 80);
3048
+ this.timer.unref();
3049
+ }
3050
+ /** Leaves the completed phase and its duration visible. */
3051
+ finish(ok) {
3052
+ if (this.label == null) {
3053
+ return;
3054
+ }
3055
+ this.clearTimer();
3056
+ const symbol = ok ? this.unicode ? "\u2714" : "OK" : this.unicode ? "\u2716" : "X";
3057
+ this.draw(symbol, ok ? 32 : 31);
3058
+ this.output.write("\n");
3059
+ this.label = null;
3060
+ }
3061
+ /** Clears an unfinished phase without recording a result. */
3062
+ stop() {
3063
+ this.clearTimer();
3064
+ if (this.label != null) {
3065
+ this.output.write(`\r${ESC2}[2K`);
3066
+ this.label = null;
3067
+ }
3068
+ }
3069
+ clearTimer() {
3070
+ if (this.timer != null) {
3071
+ clearInterval(this.timer);
3072
+ this.timer = null;
3073
+ }
3074
+ }
3075
+ draw(symbol, color) {
3076
+ const columns = this.output.columns;
3077
+ const width = Math.max(0, (columns != null && columns > 0 ? columns : 80) - 1);
3078
+ const elapsed = ` ${((Date.now() - this.startedAt) / 1e3).toFixed(1)}s`;
3079
+ const prefix = ` ${symbol} `;
3080
+ const labelWidth = Math.min(48, Math.max(0, width - displayWidth(prefix) - displayWidth(elapsed)));
3081
+ const label = this.fit(this.label ?? "", labelWidth);
3082
+ const line = this.fit(`${prefix}${label}${" ".repeat(labelWidth - displayWidth(label))}${elapsed}`, width);
3083
+ const painted = this.color ? `${ESC2}[${color}m${line}${ESC2}[39m` : line;
3084
+ this.output.write(`\r${ESC2}[2K${painted}`);
3085
+ }
3086
+ fit(text, width) {
3087
+ if (displayWidth(text) <= width) {
3088
+ return text;
3089
+ }
3090
+ if (width < 1) {
3091
+ return "";
3092
+ }
3093
+ let result = "";
3094
+ let used = 0;
3095
+ for (const character of text) {
3096
+ const size = displayWidth(character);
3097
+ if (used + size > width - 1) {
3098
+ break;
3099
+ }
3100
+ result += character;
3101
+ used += size;
3102
+ }
3103
+ return `${result}${this.unicode ? "\u2026" : "~"}`;
3104
+ }
3105
+ };
3106
+
3107
+ // src/modules/manage/connection-check.ts
3108
+ function profileFromEnv(output, env) {
3109
+ try {
3110
+ const profile = loadHostProfile(env);
3111
+ if (profile == null) {
3112
+ output.write(" The entry has no host.\n");
3113
+ }
3114
+ return profile;
3115
+ } catch (ex) {
3116
+ let detail = String(ex);
3117
+ if (ex instanceof Error) {
3118
+ detail = ex.message;
3119
+ }
3120
+ output.write(` The server would refuse to start with this entry: ${detail}
3121
+ `);
3122
+ return null;
3123
+ }
3124
+ }
3125
+ function describeAuth(profile) {
3126
+ if (profile.privateKeyPath != null) {
3127
+ return `key ${profile.privateKeyPath}`;
3128
+ }
3129
+ if (profile.password != null) {
3130
+ return "password";
3131
+ }
3132
+ return "ssh agent";
3133
+ }
3134
+ async function runConnectionCheck(profile, output) {
3135
+ const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
3136
+ const auth = describeAuth(profile);
3137
+ output.write(`Checking ${profile.name} \u2192 ${endpoint}
3138
+ `);
3139
+ if (profile.proxyCommand != null) {
3140
+ output.write(` via proxy command: ${profile.proxyCommand}
3141
+ `);
3142
+ }
3143
+ if (profile.jump != null) {
3144
+ output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
3145
+ `);
3146
+ }
3147
+ output.write("\n");
3148
+ const connection = new SshConnection(profile);
3149
+ const progress = new CheckProgress(output);
3150
+ const startedAt = Date.now();
3151
+ let connected = false;
3152
+ let detail = "";
3153
+ try {
3154
+ progress.start("Preparing SSH connection");
3155
+ await connection.connect((stage) => {
3156
+ if (stage === "transport") {
3157
+ progress.start(profile.jump != null ? "Jump host connection and forwarding" : "Starting proxy connection");
3158
+ } else {
3159
+ progress.start("SSH handshake and authentication");
3160
+ }
3161
+ });
3162
+ connected = true;
3163
+ } catch (ex) {
3164
+ detail = String(ex);
3165
+ if (ex instanceof Error) {
3166
+ detail = ex.message;
3167
+ }
3168
+ } finally {
3169
+ progress.finish(connected);
3170
+ connection.disconnect();
3171
+ }
3172
+ const durationMs = Date.now() - startedAt;
3173
+ if (connected == true) {
3174
+ output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
3175
+ `);
3176
+ } else {
3177
+ output.write(` FAILED ${endpoint} (${auth})
3178
+ `);
3179
+ output.write(` ${detail}
3180
+ `);
3181
+ }
3182
+ if (connection.hostKeyFingerprint != null) {
3183
+ let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
3184
+ if (profile.hostKeyFingerprint != null) {
3185
+ pinNote = "pinned";
3186
+ }
3187
+ output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
3188
+ `);
3189
+ }
3190
+ if (connection.jumpHostKeyFingerprint != null) {
3191
+ let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
3192
+ if (profile.jump?.hostKeyFingerprint != null) {
3193
+ pinNote = "pinned";
3194
+ }
3195
+ output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
3196
+ `);
3197
+ }
3198
+ return {
3199
+ connected,
3200
+ hostKeyFingerprint: connection.hostKeyFingerprint,
3201
+ jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
3202
+ };
3203
+ }
3204
+
3205
+ // src/modules/manage/entry.ts
3206
+ var MANAGED_ENV_NAMES = [
3207
+ SINGLE_HOST_ENV.HOST,
3208
+ SINGLE_HOST_ENV.PORT,
3209
+ SINGLE_HOST_ENV.USER,
3210
+ SINGLE_HOST_ENV.KEY,
3211
+ SINGLE_HOST_ENV.PASSPHRASE,
3212
+ SINGLE_HOST_ENV.PASSWORD,
3213
+ SINGLE_HOST_ENV.DESCRIPTION,
3214
+ SINGLE_HOST_ENV.JUMP_HOST,
3215
+ SINGLE_HOST_ENV.JUMP_PORT,
3216
+ SINGLE_HOST_ENV.JUMP_USER,
3217
+ SINGLE_HOST_ENV.PROXY_COMMAND,
3218
+ SINGLE_HOST_ENV.READONLY,
3219
+ SINGLE_HOST_ENV.ALLOWED_PATHS,
3220
+ SINGLE_HOST_ENV.HOST_KEY,
3221
+ SINGLE_HOST_ENV.JUMP_HOST_KEY
3222
+ ];
3223
+ var JUMP_ENV_PREFIX = "SSH_JUMP_";
3224
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
3225
+ function isOurEntry(entry) {
3226
+ if (entry.args.includes(PACKAGE_NAME) == true) {
3227
+ return true;
3228
+ }
3229
+ const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
3230
+ return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
3231
+ }
3232
+ function launchCommand() {
3233
+ if (process.platform === "win32") {
3234
+ return { command: "cmd", args: ["/c", "npx", "-y", PACKAGE_NAME] };
3235
+ }
3236
+ return { command: "npx", args: ["-y", PACKAGE_NAME] };
3237
+ }
3238
+ function answersToEntry(answers) {
3239
+ const env = {};
3240
+ env[SINGLE_HOST_ENV.HOST] = answers.host;
3241
+ if (answers.port !== DEFAULT_SSH_PORT) {
3242
+ env[SINGLE_HOST_ENV.PORT] = String(answers.port);
3243
+ }
3244
+ env[SINGLE_HOST_ENV.USER] = answers.username;
3245
+ if (answers.auth === "key" && answers.keyPath != null) {
3246
+ env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
3247
+ if (answers.passphrase != null && answers.passphrase !== "") {
3248
+ env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
3249
+ }
3250
+ }
3251
+ if (answers.auth === "password" && answers.password != null) {
3252
+ env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
3253
+ }
3254
+ if (answers.hostKeyFingerprint != null) {
3255
+ env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
3256
+ }
3257
+ if (answers.description != null && answers.description !== "") {
3258
+ env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
3259
+ }
3260
+ if (answers.jumpHost != null) {
3261
+ env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
3262
+ if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
3263
+ env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
3264
+ }
3265
+ if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
3266
+ env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
3267
+ }
3268
+ if (answers.jumpHostKeyFingerprint != null) {
3269
+ env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
3270
+ }
3130
3271
  }
3131
- /** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
3132
- fitLine(parts, width) {
3133
- let used = 0;
3134
- let line = "";
3135
- for (const part of parts) {
3136
- let text = "";
3137
- for (const character of part.text) {
3138
- const characterWidth = charWidth(character.codePointAt(0) ?? 0);
3139
- if (used + characterWidth > width) {
3140
- break;
3141
- }
3142
- used += characterWidth;
3143
- text += character;
3144
- }
3145
- if (part.paint != null) {
3146
- line += part.paint(text);
3147
- } else {
3148
- line += text;
3149
- }
3150
- if (used >= width) {
3151
- break;
3152
- }
3272
+ if (answers.proxyCommand != null && answers.proxyCommand !== "") {
3273
+ env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
3274
+ }
3275
+ if (answers.readonly == true) {
3276
+ env[SINGLE_HOST_ENV.READONLY] = "true";
3277
+ }
3278
+ if (answers.allowedPaths.length > 0) {
3279
+ env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
3280
+ }
3281
+ for (const [name, value] of Object.entries(answers.extraEnv)) {
3282
+ if (MANAGED_ENV_NAMES.includes(name) == true) {
3283
+ continue;
3153
3284
  }
3154
- return line;
3285
+ if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
3286
+ continue;
3287
+ }
3288
+ env[name] = value;
3155
3289
  }
3156
- /** The one-line record an answered prompt leaves behind on a TTY. */
3157
- writeAnswered(question, answer) {
3158
- if (this.interactive == false || this.transient == true) {
3159
- return;
3290
+ const launch = launchCommand();
3291
+ return {
3292
+ command: launch.command,
3293
+ args: launch.args,
3294
+ env
3295
+ };
3296
+ }
3297
+ function readPort(value) {
3298
+ if (value == null) {
3299
+ return DEFAULT_SSH_PORT;
3300
+ }
3301
+ const parsed = Number(value);
3302
+ if (Number.isInteger(parsed) == false || parsed <= 0) {
3303
+ return DEFAULT_SSH_PORT;
3304
+ }
3305
+ return parsed;
3306
+ }
3307
+ function isTruthy(value) {
3308
+ if (value == null) {
3309
+ return false;
3310
+ }
3311
+ return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
3312
+ }
3313
+ function entryToAnswers(name, entry) {
3314
+ const env = entry.env;
3315
+ const host = env[SINGLE_HOST_ENV.HOST] ?? "";
3316
+ const username = env[SINGLE_HOST_ENV.USER] ?? "";
3317
+ let auth = "agent";
3318
+ if (env[SINGLE_HOST_ENV.KEY] != null) {
3319
+ auth = "key";
3320
+ } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
3321
+ auth = "password";
3322
+ }
3323
+ let allowedPaths = [];
3324
+ const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
3325
+ if (allowedPathsValue != null) {
3326
+ allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
3327
+ }
3328
+ const extraEnv = {};
3329
+ for (const [variable, value] of Object.entries(env)) {
3330
+ if (MANAGED_ENV_NAMES.includes(variable) == false) {
3331
+ extraEnv[variable] = value;
3160
3332
  }
3161
- this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
3162
- `);
3163
3333
  }
3164
- /** @returns The picked value, or null on Esc. */
3165
- selectWithKeys(question, items, startIndex) {
3166
- const ttyInput = this.input;
3167
- const selectableIndexes = items.flatMap((item, index) => {
3168
- if ("value" in item) {
3169
- return [index];
3170
- }
3171
- return [];
3172
- });
3173
- const labelWidths = groupLabelWidths(items);
3174
- const ruleWidth = Math.min(Math.max(...labelWidths), 24);
3175
- let active = startIndex;
3176
- let renderedLines = 0;
3177
- const draw = (lines) => {
3178
- let text = "";
3179
- if (renderedLines > 1) {
3180
- text += `${ESC}[${renderedLines - 1}A`;
3181
- }
3182
- text += `\r${ESC}[J${lines.join("\n")}`;
3183
- this.output.write(text);
3184
- renderedLines = lines.length;
3185
- };
3186
- const buildLines = () => {
3187
- const width = this.columns() - 1;
3188
- const lines = [
3189
- this.fitLine([
3190
- { text: this.symbols.question, paint: this.style.accent },
3191
- { text: " " },
3192
- { text: question, paint: this.style.bold }
3193
- ], width)
3194
- ];
3195
- for (let index = 0; index < items.length; index += 1) {
3196
- const item = items[index];
3197
- if ("value" in item == false) {
3198
- lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
3199
- continue;
3200
- }
3201
- const isActive = index === active;
3202
- const parts = [];
3203
- if (isActive == true) {
3204
- parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
3205
- parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
3206
- } else {
3207
- parts.push({ text: " " });
3208
- parts.push({ text: padToWidth(item.label, labelWidths[index]) });
3209
- }
3210
- if (item.hint != null && item.hint !== "") {
3211
- parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
3212
- }
3213
- lines.push(this.fitLine(parts, width));
3214
- }
3215
- lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
3216
- return lines;
3217
- };
3218
- const move = (step) => {
3219
- const position = selectableIndexes.indexOf(active);
3220
- const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
3221
- active = selectableIndexes[next];
3222
- };
3223
- return new Promise((resolve, reject) => {
3224
- const finish = () => {
3225
- this.keyHandler = null;
3226
- ttyInput.setRawMode?.(false);
3227
- draw([]);
3228
- this.output.write(`${ESC}[?25h`);
3229
- };
3230
- this.keyHandler = (key) => {
3231
- if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
3232
- finish();
3233
- reject(new PromptClosedError());
3234
- return;
3235
- }
3236
- if (KEY_ENTER.includes(key) == true) {
3237
- const picked = items[active];
3238
- finish();
3239
- this.writeAnswered(question, picked.label);
3240
- resolve(picked.value);
3241
- return;
3242
- }
3243
- if (key === ESC) {
3244
- finish();
3245
- resolve(null);
3246
- return;
3247
- }
3248
- if (KEY_UP.includes(key) == true) {
3249
- move(-1);
3250
- } else if (KEY_DOWN.includes(key) == true) {
3251
- move(1);
3252
- } else if (KEY_HOME.includes(key) == true) {
3253
- active = selectableIndexes[0];
3254
- } else if (KEY_END.includes(key) == true) {
3255
- active = selectableIndexes[selectableIndexes.length - 1];
3256
- } else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
3257
- active = selectableIndexes[Number(key) - 1];
3258
- } else {
3259
- return;
3260
- }
3261
- draw(buildLines());
3262
- };
3263
- ttyInput.setRawMode?.(true);
3264
- this.output.write(`${ESC}[?25l`);
3265
- draw(buildLines());
3266
- });
3334
+ const answers = {
3335
+ name,
3336
+ host,
3337
+ port: readPort(env[SINGLE_HOST_ENV.PORT]),
3338
+ username,
3339
+ auth,
3340
+ keyPath: env[SINGLE_HOST_ENV.KEY],
3341
+ passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
3342
+ password: env[SINGLE_HOST_ENV.PASSWORD],
3343
+ description: env[SINGLE_HOST_ENV.DESCRIPTION],
3344
+ proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
3345
+ readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
3346
+ allowedPaths,
3347
+ hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
3348
+ extraEnv
3349
+ };
3350
+ const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
3351
+ if (jumpHost != null) {
3352
+ answers.jumpHost = jumpHost;
3353
+ answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
3354
+ answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
3355
+ answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
3267
3356
  }
3268
- };
3269
-
3270
- // src/modules/manage/host-form.ts
3357
+ return answers;
3358
+ }
3359
+ function summarizeEntry(entry) {
3360
+ const env = entry.env;
3361
+ const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
3362
+ const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
3363
+ const username = env[SINGLE_HOST_ENV.USER] ?? "?";
3364
+ let auth = "agent";
3365
+ if (env[SINGLE_HOST_ENV.KEY] != null) {
3366
+ auth = "key";
3367
+ } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
3368
+ auth = "password";
3369
+ }
3370
+ const notes = [];
3371
+ if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
3372
+ notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
3373
+ }
3374
+ if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
3375
+ notes.push("proxy");
3376
+ }
3377
+ if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
3378
+ notes.push("readonly");
3379
+ }
3380
+ if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
3381
+ notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
3382
+ }
3383
+ if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
3384
+ notes.push("pinned");
3385
+ }
3386
+ return {
3387
+ endpoint: `${username}@${host}:${port}`,
3388
+ auth,
3389
+ notes: notes.join(", ")
3390
+ };
3391
+ }
3271
3392
  var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
3272
3393
  var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
3273
3394
  var MAX_KEY_FILE_BYTES = 64 * 1024;
@@ -3623,10 +3744,12 @@ async function testConnection(prompter, output, state) {
3623
3744
  if (profile == null) {
3624
3745
  output.write("\n");
3625
3746
  state.lastTest = "failed";
3747
+ await prompter.pause();
3626
3748
  return false;
3627
3749
  }
3628
3750
  const result = await runConnectionCheck(profile, output);
3629
3751
  output.write("\n");
3752
+ await prompter.pause();
3630
3753
  if (result.connected == true) {
3631
3754
  if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
3632
3755
  const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
@@ -4735,10 +4858,12 @@ async function checkHost(prompter, output, target, name, entry) {
4735
4858
  const profile = profileFromEnv(output, entry.env);
4736
4859
  if (profile == null) {
4737
4860
  output.write("\n");
4861
+ await prompter.pause();
4738
4862
  return;
4739
4863
  }
4740
4864
  const result = await runConnectionCheck(profile, output);
4741
4865
  output.write("\n");
4866
+ await prompter.pause();
4742
4867
  if (result.connected == false) {
4743
4868
  return;
4744
4869
  }
@@ -5774,6 +5899,12 @@ async function main() {
5774
5899
  instance.shutdown();
5775
5900
  process.exit(exitCode);
5776
5901
  }
5902
+ process.on("uncaughtException", (error) => {
5903
+ _logger.error(error, "uncaught exception; the server keeps running");
5904
+ });
5905
+ process.on("unhandledRejection", (reason) => {
5906
+ _logger.error(reason, "unhandled rejection; the server keeps running");
5907
+ });
5777
5908
  let shuttingDown = false;
5778
5909
  const shutdown = (signal) => {
5779
5910
  if (shuttingDown == true) {
@@ -5781,6 +5912,10 @@ async function main() {
5781
5912
  }
5782
5913
  shuttingDown = true;
5783
5914
  _logger.info("shutting down", { signal });
5915
+ setTimeout(() => {
5916
+ _logger.warn("shutdown did not finish in time; exiting anyway", { deadline_ms: SHUTDOWN_DEADLINE_MS });
5917
+ process.exit(0);
5918
+ }, SHUTDOWN_DEADLINE_MS).unref();
5784
5919
  instance.shutdown();
5785
5920
  void instance.server.close().finally(() => {
5786
5921
  process.exit(0);
@@ -5800,7 +5935,8 @@ async function main() {
5800
5935
  _logger.info("mcp server ready on stdio", {
5801
5936
  server: SERVER_NAME,
5802
5937
  version: SERVER_VERSION,
5803
- profile: instance.profile?.name ?? null
5938
+ profile: instance.profile?.name ?? null,
5939
+ pid: process.pid
5804
5940
  });
5805
5941
  }
5806
5942
  main().catch((error) => {