@akms/mcp-ssh 0.1.6 → 0.1.7

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