@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.cjs CHANGED
@@ -36,7 +36,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
36
36
  var MAX_DIRECTORY_ENTRIES = 1e3;
37
37
  var SERVER_NAME = "akms-mcp-ssh";
38
38
  var PACKAGE_NAME = "@akms/mcp-ssh";
39
- var SERVER_VERSION = "0.1.6";
39
+ var SERVER_VERSION = "0.1.7";
40
40
 
41
41
  // src/_defs/command-rules.ts
42
42
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -1649,7 +1649,7 @@ var SshConnection = class {
1649
1649
  * @throws {Error} If the connection was closed, authentication fails, the host is
1650
1650
  * unreachable, the handshake times out, or the carrier cannot be opened.
1651
1651
  */
1652
- async connect() {
1652
+ async connect(onProgress) {
1653
1653
  if (this.disposed == true) {
1654
1654
  throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1655
1655
  }
@@ -1671,6 +1671,9 @@ var SshConnection = class {
1671
1671
  this.observedFingerprint = fingerprint;
1672
1672
  });
1673
1673
  this.connecting = (async () => {
1674
+ if (this.profile.jump != null || this.profile.proxyCommand != null) {
1675
+ onProgress?.("transport");
1676
+ }
1674
1677
  const transport = await openTransport(this.profile, (fingerprint) => {
1675
1678
  this.observedJumpFingerprint = fingerprint;
1676
1679
  });
@@ -1681,6 +1684,7 @@ var SshConnection = class {
1681
1684
  if (transport != null) {
1682
1685
  connectConfig.sock = transport.sock;
1683
1686
  }
1687
+ onProgress?.("host");
1684
1688
  await new Promise((resolve, reject) => {
1685
1689
  const client = new Client();
1686
1690
  this.pendingClient = client;
@@ -2379,384 +2383,107 @@ async function downloadFile(connection, remotePath, localPath) {
2379
2383
  return localStats.size;
2380
2384
  }
2381
2385
 
2382
- // src/modules/manage/connection-check.ts
2383
- function profileFromEnv(output, env) {
2384
- try {
2385
- const profile = loadHostProfile(env);
2386
- if (profile == null) {
2387
- output.write(" The entry has no host.\n");
2388
- }
2389
- return profile;
2390
- } catch (ex) {
2391
- let detail = String(ex);
2392
- if (ex instanceof Error) {
2393
- detail = ex.message;
2394
- }
2395
- output.write(` The server would refuse to start with this entry: ${detail}
2396
- `);
2397
- return null;
2398
- }
2399
- }
2400
- function describeAuth(profile) {
2401
- if (profile.privateKeyPath != null) {
2402
- return `key ${profile.privateKeyPath}`;
2403
- }
2404
- if (profile.password != null) {
2405
- return "password";
2406
- }
2407
- return "ssh agent";
2408
- }
2409
- async function runConnectionCheck(profile, output) {
2410
- const endpoint = `${profile.username}@${profile.host}:${profile.port}`;
2411
- const auth = describeAuth(profile);
2412
- output.write(`Checking ${profile.name} \u2192 ${endpoint}
2413
- `);
2414
- if (profile.proxyCommand != null) {
2415
- output.write(` via proxy command: ${profile.proxyCommand}
2416
- `);
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";
2417
2391
  }
2418
- if (profile.jump != null) {
2419
- output.write(` via jump host: ${profile.jump.username}@${profile.jump.host}:${profile.jump.port}
2420
- `);
2392
+ };
2393
+ var PromptBackError = class extends Error {
2394
+ constructor() {
2395
+ super("prompt left with esc");
2396
+ this.name = "PromptBackError";
2421
2397
  }
2422
- output.write("\n");
2423
- const connection = new SshConnection(profile);
2424
- const startedAt = Date.now();
2425
- let connected = false;
2426
- let detail = "";
2427
- try {
2428
- await connection.connect();
2429
- connected = true;
2430
- } catch (ex) {
2431
- detail = String(ex);
2432
- if (ex instanceof Error) {
2433
- 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;
2434
2425
  }
2435
- } finally {
2436
- connection.disconnect();
2426
+ const codePoint = chunk.codePointAt(index) ?? 0;
2427
+ const character = String.fromCodePoint(codePoint);
2428
+ keys.push(character);
2429
+ index += character.length;
2437
2430
  }
2438
- const durationMs = Date.now() - startedAt;
2439
- if (connected == true) {
2440
- output.write(` OK ${endpoint} (${auth}, ${durationMs}ms)
2441
- `);
2442
- } else {
2443
- output.write(` FAILED ${endpoint} (${auth})
2444
- `);
2445
- output.write(` ${detail}
2446
- `);
2431
+ return keys;
2432
+ }
2433
+ function isPrintableKey(key) {
2434
+ if (key.startsWith(ESC) == true) {
2435
+ return false;
2447
2436
  }
2448
- if (connection.hostKeyFingerprint != null) {
2449
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.HOST_KEY} to it to detect a changed key`;
2450
- if (profile.hostKeyFingerprint != null) {
2451
- pinNote = "pinned";
2452
- }
2453
- output.write(` host key ${connection.hostKeyFingerprint} (${pinNote})
2454
- `);
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;
2455
2443
  }
2456
- if (connection.jumpHostKeyFingerprint != null) {
2457
- let pinNote = `not pinned \u2014 set ${SINGLE_HOST_ENV.JUMP_HOST_KEY} to it to detect a changed key`;
2458
- if (profile.jump?.hostKeyFingerprint != null) {
2459
- pinNote = "pinned";
2460
- }
2461
- output.write(` jump host key ${connection.jumpHostKeyFingerprint} (${pinNote})
2462
- `);
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;
2463
2447
  }
2464
- return {
2465
- connected,
2466
- hostKeyFingerprint: connection.hostKeyFingerprint,
2467
- jumpHostKeyFingerprint: connection.jumpHostKeyFingerprint
2468
- };
2448
+ return 1;
2469
2449
  }
2470
-
2471
- // src/modules/manage/entry.ts
2472
- var MANAGED_ENV_NAMES = [
2473
- SINGLE_HOST_ENV.HOST,
2474
- SINGLE_HOST_ENV.PORT,
2475
- SINGLE_HOST_ENV.USER,
2476
- SINGLE_HOST_ENV.KEY,
2477
- SINGLE_HOST_ENV.PASSPHRASE,
2478
- SINGLE_HOST_ENV.PASSWORD,
2479
- SINGLE_HOST_ENV.DESCRIPTION,
2480
- SINGLE_HOST_ENV.JUMP_HOST,
2481
- SINGLE_HOST_ENV.JUMP_PORT,
2482
- SINGLE_HOST_ENV.JUMP_USER,
2483
- SINGLE_HOST_ENV.PROXY_COMMAND,
2484
- SINGLE_HOST_ENV.READONLY,
2485
- SINGLE_HOST_ENV.ALLOWED_PATHS,
2486
- SINGLE_HOST_ENV.HOST_KEY,
2487
- SINGLE_HOST_ENV.JUMP_HOST_KEY
2488
- ];
2489
- var JUMP_ENV_PREFIX = "SSH_JUMP_";
2490
- var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,48}$/;
2491
- function isOurEntry(entry) {
2492
- if (entry.args.includes(PACKAGE_NAME) == true) {
2493
- return true;
2450
+ function displayWidth(text) {
2451
+ let width = 0;
2452
+ for (const character of text) {
2453
+ width += charWidth(character.codePointAt(0) ?? 0);
2494
2454
  }
2495
- const commandBase = entry.command.replace(/\\/g, "/").split("/").pop() ?? "";
2496
- return commandBase === SERVER_NAME || commandBase.startsWith(`${SERVER_NAME}.`) == true;
2455
+ return width;
2497
2456
  }
2498
- function launchCommand() {
2499
- if (process.platform === "win32") {
2500
- 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;
2501
2461
  }
2502
- return { command: "npx", args: ["-y", PACKAGE_NAME] };
2462
+ return `${text}${" ".repeat(missing)}`;
2503
2463
  }
2504
- function answersToEntry(answers) {
2505
- const env = {};
2506
- env[SINGLE_HOST_ENV.HOST] = answers.host;
2507
- if (answers.port !== DEFAULT_SSH_PORT) {
2508
- env[SINGLE_HOST_ENV.PORT] = String(answers.port);
2509
- }
2510
- env[SINGLE_HOST_ENV.USER] = answers.username;
2511
- if (answers.auth === "key" && answers.keyPath != null) {
2512
- env[SINGLE_HOST_ENV.KEY] = answers.keyPath;
2513
- if (answers.passphrase != null && answers.passphrase !== "") {
2514
- env[SINGLE_HOST_ENV.PASSPHRASE] = answers.passphrase;
2515
- }
2516
- }
2517
- if (answers.auth === "password" && answers.password != null) {
2518
- env[SINGLE_HOST_ENV.PASSWORD] = answers.password;
2519
- }
2520
- if (answers.hostKeyFingerprint != null) {
2521
- env[SINGLE_HOST_ENV.HOST_KEY] = answers.hostKeyFingerprint;
2522
- }
2523
- if (answers.description != null && answers.description !== "") {
2524
- env[SINGLE_HOST_ENV.DESCRIPTION] = answers.description;
2525
- }
2526
- if (answers.jumpHost != null) {
2527
- env[SINGLE_HOST_ENV.JUMP_HOST] = answers.jumpHost;
2528
- if (answers.jumpPort != null && answers.jumpPort !== DEFAULT_SSH_PORT) {
2529
- env[SINGLE_HOST_ENV.JUMP_PORT] = String(answers.jumpPort);
2530
- }
2531
- if (answers.jumpUser != null && answers.jumpUser !== answers.username) {
2532
- env[SINGLE_HOST_ENV.JUMP_USER] = answers.jumpUser;
2533
- }
2534
- if (answers.jumpHostKeyFingerprint != null) {
2535
- env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = answers.jumpHostKeyFingerprint;
2536
- }
2537
- }
2538
- if (answers.proxyCommand != null && answers.proxyCommand !== "") {
2539
- env[SINGLE_HOST_ENV.PROXY_COMMAND] = answers.proxyCommand;
2540
- }
2541
- if (answers.readonly == true) {
2542
- env[SINGLE_HOST_ENV.READONLY] = "true";
2543
- }
2544
- if (answers.allowedPaths.length > 0) {
2545
- env[SINGLE_HOST_ENV.ALLOWED_PATHS] = answers.allowedPaths.join(",");
2546
- }
2547
- for (const [name, value] of Object.entries(answers.extraEnv)) {
2548
- if (MANAGED_ENV_NAMES.includes(name) == true) {
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) {
2549
2470
  continue;
2550
2471
  }
2551
- if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
2552
- continue;
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));
2553
2476
  }
2554
- env[name] = value;
2477
+ for (let member = groupStart; member < index; member += 1) {
2478
+ widths[member] = groupWidth;
2479
+ }
2480
+ groupStart = index + 1;
2555
2481
  }
2556
- const launch = launchCommand();
2557
- return {
2558
- command: launch.command,
2559
- args: launch.args,
2560
- env
2561
- };
2482
+ return widths;
2562
2483
  }
2563
- function readPort(value) {
2564
- if (value == null) {
2565
- return DEFAULT_SSH_PORT;
2566
- }
2567
- const parsed = Number(value);
2568
- if (Number.isInteger(parsed) == false || parsed <= 0) {
2569
- return DEFAULT_SSH_PORT;
2570
- }
2571
- return parsed;
2572
- }
2573
- function isTruthy(value) {
2574
- if (value == null) {
2575
- return false;
2576
- }
2577
- return TRUTHY_ENV_VALUES.includes(value.trim().toLowerCase());
2578
- }
2579
- function entryToAnswers(name, entry) {
2580
- const env = entry.env;
2581
- const host = env[SINGLE_HOST_ENV.HOST] ?? "";
2582
- const username = env[SINGLE_HOST_ENV.USER] ?? "";
2583
- let auth = "agent";
2584
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2585
- auth = "key";
2586
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2587
- auth = "password";
2588
- }
2589
- let allowedPaths = [];
2590
- const allowedPathsValue = env[SINGLE_HOST_ENV.ALLOWED_PATHS];
2591
- if (allowedPathsValue != null) {
2592
- allowedPaths = allowedPathsValue.split(",").map((item) => item.trim()).filter((item) => item !== "");
2593
- }
2594
- const extraEnv = {};
2595
- for (const [variable, value] of Object.entries(env)) {
2596
- if (MANAGED_ENV_NAMES.includes(variable) == false) {
2597
- extraEnv[variable] = value;
2598
- }
2599
- }
2600
- const answers = {
2601
- name,
2602
- host,
2603
- port: readPort(env[SINGLE_HOST_ENV.PORT]),
2604
- username,
2605
- auth,
2606
- keyPath: env[SINGLE_HOST_ENV.KEY],
2607
- passphrase: env[SINGLE_HOST_ENV.PASSPHRASE],
2608
- password: env[SINGLE_HOST_ENV.PASSWORD],
2609
- description: env[SINGLE_HOST_ENV.DESCRIPTION],
2610
- proxyCommand: env[SINGLE_HOST_ENV.PROXY_COMMAND],
2611
- readonly: isTruthy(env[SINGLE_HOST_ENV.READONLY]),
2612
- allowedPaths,
2613
- hostKeyFingerprint: env[SINGLE_HOST_ENV.HOST_KEY],
2614
- extraEnv
2615
- };
2616
- const jumpHost = env[SINGLE_HOST_ENV.JUMP_HOST];
2617
- if (jumpHost != null) {
2618
- answers.jumpHost = jumpHost;
2619
- answers.jumpPort = readPort(env[SINGLE_HOST_ENV.JUMP_PORT]);
2620
- answers.jumpUser = env[SINGLE_HOST_ENV.JUMP_USER] ?? username;
2621
- answers.jumpHostKeyFingerprint = env[SINGLE_HOST_ENV.JUMP_HOST_KEY];
2622
- }
2623
- return answers;
2624
- }
2625
- function summarizeEntry(entry) {
2626
- const env = entry.env;
2627
- const host = env[SINGLE_HOST_ENV.HOST] ?? "?";
2628
- const port = env[SINGLE_HOST_ENV.PORT] ?? String(DEFAULT_SSH_PORT);
2629
- const username = env[SINGLE_HOST_ENV.USER] ?? "?";
2630
- let auth = "agent";
2631
- if (env[SINGLE_HOST_ENV.KEY] != null) {
2632
- auth = "key";
2633
- } else if (env[SINGLE_HOST_ENV.PASSWORD] != null) {
2634
- auth = "password";
2635
- }
2636
- const notes = [];
2637
- if (env[SINGLE_HOST_ENV.JUMP_HOST] != null) {
2638
- notes.push(`jump ${env[SINGLE_HOST_ENV.JUMP_HOST]}`);
2639
- }
2640
- if (env[SINGLE_HOST_ENV.PROXY_COMMAND] != null) {
2641
- notes.push("proxy");
2642
- }
2643
- if (isTruthy(env[SINGLE_HOST_ENV.READONLY]) == true) {
2644
- notes.push("readonly");
2645
- }
2646
- if (env[SINGLE_HOST_ENV.ALLOWED_PATHS] != null) {
2647
- notes.push(`paths ${env[SINGLE_HOST_ENV.ALLOWED_PATHS]}`);
2648
- }
2649
- if (env[SINGLE_HOST_ENV.HOST_KEY] != null) {
2650
- notes.push("pinned");
2651
- }
2652
- return {
2653
- endpoint: `${username}@${host}:${port}`,
2654
- auth,
2655
- notes: notes.join(", ")
2656
- };
2657
- }
2658
-
2659
- // src/modules/manage/prompter.ts
2660
- var PromptClosedError = class extends Error {
2661
- constructor() {
2662
- super("input closed before the prompt was answered");
2663
- this.name = "PromptClosedError";
2664
- }
2665
- };
2666
- var PromptBackError = class extends Error {
2667
- constructor() {
2668
- super("prompt left with esc");
2669
- this.name = "PromptBackError";
2670
- }
2671
- };
2672
- var ESC = "\x1B";
2673
- var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
2674
- var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
2675
- var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
2676
- var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
2677
- var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
2678
- var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
2679
- var KEY_DELETE = [`${ESC}[3~`];
2680
- var KEY_ENTER = ["\r", "\n"];
2681
- var KEY_BACKSPACE = ["\x7F", "\b"];
2682
- var KEY_CTRL_C = "";
2683
- var KEY_CTRL_D = "";
2684
- var KEY_CLEAR_LINE = "";
2685
- function splitKeys(chunk) {
2686
- const keys = [];
2687
- let index = 0;
2688
- while (index < chunk.length) {
2689
- const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
2690
- if (isSequence == true) {
2691
- let end = index + 2;
2692
- while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
2693
- end += 1;
2694
- }
2695
- keys.push(chunk.slice(index, end + 1));
2696
- index = end + 1;
2697
- continue;
2698
- }
2699
- const codePoint = chunk.codePointAt(index) ?? 0;
2700
- const character = String.fromCodePoint(codePoint);
2701
- keys.push(character);
2702
- index += character.length;
2703
- }
2704
- return keys;
2705
- }
2706
- function isPrintableKey(key) {
2707
- if (key.startsWith(ESC) == true) {
2708
- return false;
2709
- }
2710
- const codePoint = key.codePointAt(0) ?? 0;
2711
- return codePoint >= 32 && codePoint !== 127;
2712
- }
2713
- function charWidth(codePoint) {
2714
- if (codePoint < 4352) {
2715
- return 1;
2716
- }
2717
- 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;
2718
- if (isWide == true) {
2719
- return 2;
2720
- }
2721
- return 1;
2722
- }
2723
- function displayWidth(text) {
2724
- let width = 0;
2725
- for (const character of text) {
2726
- width += charWidth(character.codePointAt(0) ?? 0);
2727
- }
2728
- return width;
2729
- }
2730
- function padToWidth(text, width) {
2731
- const missing = width - displayWidth(text);
2732
- if (missing <= 0) {
2733
- return text;
2734
- }
2735
- return `${text}${" ".repeat(missing)}`;
2736
- }
2737
- function groupLabelWidths(items) {
2738
- const widths = items.map(() => 0);
2739
- let groupStart = 0;
2740
- for (let index = 0; index <= items.length; index += 1) {
2741
- const atBoundary = index === items.length || "value" in items[index] == false;
2742
- if (atBoundary == false) {
2743
- continue;
2744
- }
2745
- let groupWidth = 0;
2746
- for (let member = groupStart; member < index; member += 1) {
2747
- const item = items[member];
2748
- groupWidth = Math.max(groupWidth, displayWidth(item.label));
2749
- }
2750
- for (let member = groupStart; member < index; member += 1) {
2751
- widths[member] = groupWidth;
2752
- }
2753
- groupStart = index + 1;
2754
- }
2755
- return widths;
2756
- }
2757
- function supportsUnicode() {
2758
- if (process.platform !== "win32") {
2759
- return true;
2484
+ function supportsUnicode() {
2485
+ if (process.platform !== "win32") {
2486
+ return true;
2760
2487
  }
2761
2488
  return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
2762
2489
  }
@@ -2832,6 +2559,17 @@ var Prompter = class {
2832
2559
  get paint() {
2833
2560
  return this.style;
2834
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
+ }
2835
2573
  /**
2836
2574
  * Free-text prompt. Enter accepts `defaultValue` when one is shown.
2837
2575
  *
@@ -3279,7 +3017,378 @@ var Prompter = class {
3279
3017
  }
3280
3018
  };
3281
3019
 
3282
- // src/modules/manage/host-form.ts
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
+ }
3271
+ }
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;
3284
+ }
3285
+ if (answers.jumpHost == null && name.startsWith(JUMP_ENV_PREFIX) == true) {
3286
+ continue;
3287
+ }
3288
+ env[name] = value;
3289
+ }
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;
3332
+ }
3333
+ }
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];
3356
+ }
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
+ }
3283
3392
  var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
3284
3393
  var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
3285
3394
  var MAX_KEY_FILE_BYTES = 64 * 1024;
@@ -3635,10 +3744,12 @@ async function testConnection(prompter, output, state) {
3635
3744
  if (profile == null) {
3636
3745
  output.write("\n");
3637
3746
  state.lastTest = "failed";
3747
+ await prompter.pause();
3638
3748
  return false;
3639
3749
  }
3640
3750
  const result = await runConnectionCheck(profile, output);
3641
3751
  output.write("\n");
3752
+ await prompter.pause();
3642
3753
  if (result.connected == true) {
3643
3754
  if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
3644
3755
  const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
@@ -4747,10 +4858,12 @@ async function checkHost(prompter, output, target, name, entry) {
4747
4858
  const profile = profileFromEnv(output, entry.env);
4748
4859
  if (profile == null) {
4749
4860
  output.write("\n");
4861
+ await prompter.pause();
4750
4862
  return;
4751
4863
  }
4752
4864
  const result = await runConnectionCheck(profile, output);
4753
4865
  output.write("\n");
4866
+ await prompter.pause();
4754
4867
  if (result.connected == false) {
4755
4868
  return;
4756
4869
  }