@akms/mcp-ssh 0.1.0 → 0.1.1
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/README.md +12 -7
- package/lib/cli.cjs +738 -282
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +736 -280
- package/lib/cli.js.map +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs6 from 'fs';
|
|
4
|
+
import os5 from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { spawnSync, spawn } from 'child_process';
|
|
7
7
|
import { createHash } from 'crypto';
|
|
@@ -26,7 +26,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
26
26
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
27
27
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
28
28
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
29
|
-
var SERVER_VERSION = "0.1.
|
|
29
|
+
var SERVER_VERSION = "0.1.1";
|
|
30
30
|
|
|
31
31
|
// src/_defs/command-rules.ts
|
|
32
32
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -601,7 +601,7 @@ function findExecutable(name) {
|
|
|
601
601
|
for (const directory of directories) {
|
|
602
602
|
for (const suffix of suffixes) {
|
|
603
603
|
const candidate = path.join(directory, `${name}${suffix}`);
|
|
604
|
-
if (
|
|
604
|
+
if (fs6.existsSync(candidate) == true && fs6.statSync(candidate).isFile() == true) {
|
|
605
605
|
return candidate;
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -611,9 +611,9 @@ function findExecutable(name) {
|
|
|
611
611
|
function expandHomePath(inputPath) {
|
|
612
612
|
let expanded = inputPath;
|
|
613
613
|
if (inputPath === "~") {
|
|
614
|
-
expanded =
|
|
614
|
+
expanded = os5.homedir();
|
|
615
615
|
} else if (inputPath.startsWith("~/") == true || inputPath.startsWith("~\\") == true) {
|
|
616
|
-
expanded = path.join(
|
|
616
|
+
expanded = path.join(os5.homedir(), inputPath.slice(2));
|
|
617
617
|
}
|
|
618
618
|
return path.resolve(expanded);
|
|
619
619
|
}
|
|
@@ -742,7 +742,7 @@ function loadHostProfile(env = process.env) {
|
|
|
742
742
|
if (keyPath != null) {
|
|
743
743
|
const expandedKeyPath = expandHomePath(keyPath);
|
|
744
744
|
profile.privateKeyPath = expandedKeyPath;
|
|
745
|
-
if (
|
|
745
|
+
if (fs6.existsSync(expandedKeyPath) == false) {
|
|
746
746
|
_logger.warn(`${SINGLE_HOST_ENV.KEY} does not exist; connections will fail`, {
|
|
747
747
|
private_key_path: expandedKeyPath
|
|
748
748
|
});
|
|
@@ -774,7 +774,7 @@ function loadHostProfile(env = process.env) {
|
|
|
774
774
|
if (jumpKeyPath != null) {
|
|
775
775
|
profile.jump.privateKeyPath = expandHomePath(jumpKeyPath);
|
|
776
776
|
profile.jump.passphrase = readEnv(env, SINGLE_HOST_ENV.JUMP_PASSPHRASE);
|
|
777
|
-
if (
|
|
777
|
+
if (fs6.existsSync(profile.jump.privateKeyPath) == false) {
|
|
778
778
|
_logger.warn(`${SINGLE_HOST_ENV.JUMP_KEY} does not exist; connections will fail`, {
|
|
779
779
|
private_key_path: profile.jump.privateKeyPath
|
|
780
780
|
});
|
|
@@ -1260,14 +1260,14 @@ function inspectRemotePath(remotePath, policy, access) {
|
|
|
1260
1260
|
function toRealPath(inputPath) {
|
|
1261
1261
|
const _logger = $logger.child({ context: "toRealPath" });
|
|
1262
1262
|
try {
|
|
1263
|
-
return
|
|
1263
|
+
return fs6.realpathSync.native(inputPath);
|
|
1264
1264
|
} catch (ex) {
|
|
1265
1265
|
_logger.debug("path does not resolve, falling back to its parent directory", {
|
|
1266
1266
|
reason: ex instanceof Error ? ex.name : "unknown"
|
|
1267
1267
|
});
|
|
1268
1268
|
}
|
|
1269
1269
|
try {
|
|
1270
|
-
const parentRealPath =
|
|
1270
|
+
const parentRealPath = fs6.realpathSync.native(path.dirname(inputPath));
|
|
1271
1271
|
return path.join(parentRealPath, path.basename(inputPath));
|
|
1272
1272
|
} catch (ex) {
|
|
1273
1273
|
_logger.debug("parent directory does not resolve either, comparing lexically", {
|
|
@@ -1359,7 +1359,7 @@ function readPrivateKey(label, keyPath) {
|
|
|
1359
1359
|
return cached;
|
|
1360
1360
|
}
|
|
1361
1361
|
try {
|
|
1362
|
-
const contents =
|
|
1362
|
+
const contents = fs6.readFileSync(keyPath);
|
|
1363
1363
|
PRIVATE_KEY_CACHE.set(keyPath, contents);
|
|
1364
1364
|
return contents;
|
|
1365
1365
|
} catch (ex) {
|
|
@@ -2310,7 +2310,7 @@ async function writeRemoteFile(connection, args) {
|
|
|
2310
2310
|
}
|
|
2311
2311
|
async function uploadFile(connection, localPath, remotePath) {
|
|
2312
2312
|
requirePermittedPath(connection, remotePath, "write");
|
|
2313
|
-
const localStats =
|
|
2313
|
+
const localStats = fs6.statSync(localPath);
|
|
2314
2314
|
if (localStats.isFile() == false) {
|
|
2315
2315
|
throw new Error(`local path is not a file: ${localPath}`);
|
|
2316
2316
|
}
|
|
@@ -2347,7 +2347,7 @@ async function downloadFile(connection, remotePath, localPath) {
|
|
|
2347
2347
|
});
|
|
2348
2348
|
});
|
|
2349
2349
|
});
|
|
2350
|
-
const localStats =
|
|
2350
|
+
const localStats = fs6.statSync(localPath);
|
|
2351
2351
|
const _logger = $logger.child({
|
|
2352
2352
|
context: "downloadFile",
|
|
2353
2353
|
profile: connection.profile.name,
|
|
@@ -2625,36 +2625,152 @@ var PromptClosedError = class extends Error {
|
|
|
2625
2625
|
this.name = "PromptClosedError";
|
|
2626
2626
|
}
|
|
2627
2627
|
};
|
|
2628
|
+
var ESC = "\x1B";
|
|
2629
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`, "k"];
|
|
2630
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`, "j"];
|
|
2631
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`];
|
|
2632
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`];
|
|
2633
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
2634
|
+
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2635
|
+
var KEY_CTRL_C = "";
|
|
2636
|
+
var KEY_CTRL_D = "";
|
|
2637
|
+
function splitKeys(chunk) {
|
|
2638
|
+
const keys = [];
|
|
2639
|
+
let index = 0;
|
|
2640
|
+
while (index < chunk.length) {
|
|
2641
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
2642
|
+
if (isSequence == true) {
|
|
2643
|
+
let end = index + 2;
|
|
2644
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
2645
|
+
end += 1;
|
|
2646
|
+
}
|
|
2647
|
+
keys.push(chunk.slice(index, end + 1));
|
|
2648
|
+
index = end + 1;
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
2652
|
+
const character = String.fromCodePoint(codePoint);
|
|
2653
|
+
keys.push(character);
|
|
2654
|
+
index += character.length;
|
|
2655
|
+
}
|
|
2656
|
+
return keys;
|
|
2657
|
+
}
|
|
2658
|
+
function charWidth(codePoint) {
|
|
2659
|
+
if (codePoint < 4352) {
|
|
2660
|
+
return 1;
|
|
2661
|
+
}
|
|
2662
|
+
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;
|
|
2663
|
+
if (isWide == true) {
|
|
2664
|
+
return 2;
|
|
2665
|
+
}
|
|
2666
|
+
return 1;
|
|
2667
|
+
}
|
|
2668
|
+
function displayWidth(text) {
|
|
2669
|
+
let width = 0;
|
|
2670
|
+
for (const character of text) {
|
|
2671
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
2672
|
+
}
|
|
2673
|
+
return width;
|
|
2674
|
+
}
|
|
2675
|
+
function padToWidth(text, width) {
|
|
2676
|
+
const missing = width - displayWidth(text);
|
|
2677
|
+
if (missing <= 0) {
|
|
2678
|
+
return text;
|
|
2679
|
+
}
|
|
2680
|
+
return `${text}${" ".repeat(missing)}`;
|
|
2681
|
+
}
|
|
2682
|
+
function groupLabelWidths(items) {
|
|
2683
|
+
const widths = items.map(() => 0);
|
|
2684
|
+
let groupStart = 0;
|
|
2685
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
2686
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
2687
|
+
if (atBoundary == false) {
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
let groupWidth = 0;
|
|
2691
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2692
|
+
const item = items[member];
|
|
2693
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
2694
|
+
}
|
|
2695
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2696
|
+
widths[member] = groupWidth;
|
|
2697
|
+
}
|
|
2698
|
+
groupStart = index + 1;
|
|
2699
|
+
}
|
|
2700
|
+
return widths;
|
|
2701
|
+
}
|
|
2702
|
+
function supportsUnicode() {
|
|
2703
|
+
if (process.platform !== "win32") {
|
|
2704
|
+
return true;
|
|
2705
|
+
}
|
|
2706
|
+
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
2707
|
+
}
|
|
2708
|
+
function createStyle(enabled) {
|
|
2709
|
+
const paint = (open, close) => {
|
|
2710
|
+
return (text) => {
|
|
2711
|
+
if (enabled == false || text === "") {
|
|
2712
|
+
return text;
|
|
2713
|
+
}
|
|
2714
|
+
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
2715
|
+
};
|
|
2716
|
+
};
|
|
2717
|
+
return {
|
|
2718
|
+
accent: paint(36, 39),
|
|
2719
|
+
success: paint(32, 39),
|
|
2720
|
+
dim: paint(2, 22),
|
|
2721
|
+
bold: paint(1, 22)
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2628
2724
|
var Prompter = class {
|
|
2629
2725
|
constructor(input, output) {
|
|
2630
2726
|
this.input = input;
|
|
2631
2727
|
this.output = output;
|
|
2632
2728
|
this.lineBuffer = "";
|
|
2633
2729
|
this.pendingLines = [];
|
|
2634
|
-
this.
|
|
2730
|
+
this.lineWaiter = null;
|
|
2635
2731
|
this.inputEnded = false;
|
|
2636
|
-
|
|
2637
|
-
this.
|
|
2732
|
+
/** Set while a key-driven prompt (select, secret) owns the input. */
|
|
2733
|
+
this.keyHandler = null;
|
|
2734
|
+
const ttyInput = input;
|
|
2735
|
+
const ttyOutput = output;
|
|
2736
|
+
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
2737
|
+
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
2738
|
+
if (supportsUnicode() == true) {
|
|
2739
|
+
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
2740
|
+
} else {
|
|
2741
|
+
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
2742
|
+
}
|
|
2638
2743
|
input.setEncoding("utf8");
|
|
2639
2744
|
input.on("data", (chunk) => {
|
|
2640
2745
|
this.onData(chunk);
|
|
2641
2746
|
});
|
|
2642
2747
|
input.on("end", () => {
|
|
2643
2748
|
this.inputEnded = true;
|
|
2644
|
-
if (this.
|
|
2645
|
-
const waiter = this.
|
|
2646
|
-
this.
|
|
2749
|
+
if (this.lineWaiter != null) {
|
|
2750
|
+
const waiter = this.lineWaiter;
|
|
2751
|
+
this.lineWaiter = null;
|
|
2647
2752
|
waiter(null);
|
|
2648
2753
|
}
|
|
2754
|
+
if (this.keyHandler != null) {
|
|
2755
|
+
this.keyHandler(KEY_CTRL_D);
|
|
2756
|
+
}
|
|
2649
2757
|
});
|
|
2650
2758
|
}
|
|
2651
2759
|
/** Stops reading so the process can exit once the command is done. */
|
|
2652
2760
|
close() {
|
|
2761
|
+
if (this.interactive == true) {
|
|
2762
|
+
this.input.setRawMode?.(false);
|
|
2763
|
+
this.output.write(`${ESC}[?25h`);
|
|
2764
|
+
}
|
|
2653
2765
|
this.input.pause();
|
|
2654
2766
|
}
|
|
2655
2767
|
write(text) {
|
|
2656
2768
|
this.output.write(text);
|
|
2657
2769
|
}
|
|
2770
|
+
/** Styling for callers that print around the prompts; plain text off a TTY. */
|
|
2771
|
+
get paint() {
|
|
2772
|
+
return this.style;
|
|
2773
|
+
}
|
|
2658
2774
|
/**
|
|
2659
2775
|
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2660
2776
|
*
|
|
@@ -2663,18 +2779,20 @@ var Prompter = class {
|
|
|
2663
2779
|
async ask(question, defaultValue) {
|
|
2664
2780
|
let suffix = "";
|
|
2665
2781
|
if (defaultValue != null && defaultValue !== "") {
|
|
2666
|
-
suffix = `
|
|
2782
|
+
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2667
2783
|
}
|
|
2668
|
-
this.
|
|
2784
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
2785
|
+
this.output.write(prompt);
|
|
2669
2786
|
const line = await this.readLine();
|
|
2670
2787
|
if (line == null) {
|
|
2671
2788
|
throw new PromptClosedError();
|
|
2672
2789
|
}
|
|
2673
|
-
|
|
2674
|
-
if (
|
|
2675
|
-
|
|
2790
|
+
let answer = line.trim();
|
|
2791
|
+
if (answer === "" && defaultValue != null) {
|
|
2792
|
+
answer = defaultValue;
|
|
2676
2793
|
}
|
|
2677
|
-
|
|
2794
|
+
this.collapseAnswered(prompt, line, question, answer);
|
|
2795
|
+
return answer;
|
|
2678
2796
|
}
|
|
2679
2797
|
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
2680
2798
|
async askRequired(question, defaultValue) {
|
|
@@ -2683,77 +2801,115 @@ var Prompter = class {
|
|
|
2683
2801
|
if (answer !== "") {
|
|
2684
2802
|
return answer;
|
|
2685
2803
|
}
|
|
2686
|
-
this.output.write("
|
|
2804
|
+
this.output.write(` ${this.style.dim("A value is required.")}
|
|
2805
|
+
`);
|
|
2687
2806
|
}
|
|
2688
2807
|
}
|
|
2689
2808
|
async askYesNo(question, defaultValue) {
|
|
2690
|
-
let hint = "
|
|
2809
|
+
let hint = "y/N";
|
|
2691
2810
|
if (defaultValue == true) {
|
|
2692
|
-
hint = "
|
|
2811
|
+
hint = "Y/n";
|
|
2693
2812
|
}
|
|
2694
2813
|
for (; ; ) {
|
|
2695
|
-
this.
|
|
2814
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
2815
|
+
this.output.write(prompt);
|
|
2696
2816
|
const line = await this.readLine();
|
|
2697
2817
|
if (line == null) {
|
|
2698
2818
|
throw new PromptClosedError();
|
|
2699
2819
|
}
|
|
2700
2820
|
const normalized = line.trim().toLowerCase();
|
|
2821
|
+
let answer = null;
|
|
2701
2822
|
if (normalized === "") {
|
|
2702
|
-
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2823
|
+
answer = defaultValue;
|
|
2824
|
+
} else if (normalized === "y" || normalized === "yes") {
|
|
2825
|
+
answer = true;
|
|
2826
|
+
} else if (normalized === "n" || normalized === "no") {
|
|
2827
|
+
answer = false;
|
|
2706
2828
|
}
|
|
2707
|
-
if (
|
|
2708
|
-
|
|
2829
|
+
if (answer != null) {
|
|
2830
|
+
let shown = "no";
|
|
2831
|
+
if (answer == true) {
|
|
2832
|
+
shown = "yes";
|
|
2833
|
+
}
|
|
2834
|
+
this.collapseAnswered(prompt, line, question, shown);
|
|
2835
|
+
return answer;
|
|
2709
2836
|
}
|
|
2710
|
-
this.output.write("
|
|
2837
|
+
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
2838
|
+
`);
|
|
2711
2839
|
}
|
|
2712
2840
|
}
|
|
2713
|
-
/**
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2841
|
+
/**
|
|
2842
|
+
* Picks one of `items`. On a TTY: ↑/↓ (or k/j) move, 1–9 jump, Enter picks, Esc returns
|
|
2843
|
+
* `cancelValue`. Off a TTY: a numbered list, answered by number, key or label.
|
|
2844
|
+
*
|
|
2845
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2846
|
+
*/
|
|
2847
|
+
async select(question, items, options) {
|
|
2848
|
+
const choices = items.filter((item) => "value" in item);
|
|
2849
|
+
if (choices.length === 0) {
|
|
2850
|
+
throw new Error("select needs at least one choice");
|
|
2720
2851
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
`);
|
|
2852
|
+
let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
|
|
2853
|
+
if (startIndex < 0) {
|
|
2854
|
+
startIndex = items.findIndex((item) => "value" in item);
|
|
2855
|
+
}
|
|
2856
|
+
if (this.interactive == true) {
|
|
2857
|
+
const picked2 = await this.selectWithKeys(question, items, startIndex, options?.cancelValue);
|
|
2858
|
+
return picked2;
|
|
2729
2859
|
}
|
|
2860
|
+
const picked = await this.selectWithLines(question, items, startIndex);
|
|
2861
|
+
return picked;
|
|
2730
2862
|
}
|
|
2731
2863
|
/**
|
|
2732
2864
|
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2733
2865
|
* caller says a value already exists.
|
|
2866
|
+
*
|
|
2867
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2734
2868
|
*/
|
|
2735
2869
|
async askSecret(question) {
|
|
2736
|
-
|
|
2737
|
-
if (rawModeStream.isTTY != true || rawModeStream.setRawMode == null) {
|
|
2870
|
+
if (this.interactive == false) {
|
|
2738
2871
|
const answer = await this.ask(question);
|
|
2739
2872
|
return answer;
|
|
2740
2873
|
}
|
|
2741
|
-
this.
|
|
2742
|
-
this.
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
const
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2874
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
2875
|
+
this.output.write(prompt);
|
|
2876
|
+
const ttyInput = this.input;
|
|
2877
|
+
return new Promise((resolve, reject) => {
|
|
2878
|
+
let buffer = "";
|
|
2879
|
+
const finish = () => {
|
|
2880
|
+
this.keyHandler = null;
|
|
2881
|
+
ttyInput.setRawMode?.(false);
|
|
2882
|
+
this.output.write(`\r${ESC}[2K`);
|
|
2883
|
+
};
|
|
2884
|
+
this.keyHandler = (key) => {
|
|
2885
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
2886
|
+
finish();
|
|
2887
|
+
reject(new PromptClosedError());
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
2891
|
+
finish();
|
|
2892
|
+
let shown = this.style.dim("(empty)");
|
|
2893
|
+
if (buffer !== "") {
|
|
2894
|
+
shown = "\u2022".repeat(Math.min(buffer.length, 8));
|
|
2895
|
+
}
|
|
2896
|
+
this.writeAnswered(question, shown);
|
|
2897
|
+
resolve(buffer);
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
if (KEY_BACKSPACE.includes(key) == true) {
|
|
2901
|
+
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
if (key.startsWith(ESC) == true) {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
buffer += key;
|
|
2908
|
+
};
|
|
2909
|
+
ttyInput.setRawMode?.(true);
|
|
2910
|
+
});
|
|
2756
2911
|
}
|
|
2912
|
+
// ── line mode ──────────────────────────────────────────────────────────
|
|
2757
2913
|
readLine() {
|
|
2758
2914
|
const queued = this.pendingLines.shift();
|
|
2759
2915
|
if (queued != null) {
|
|
@@ -2763,36 +2919,16 @@ var Prompter = class {
|
|
|
2763
2919
|
return Promise.resolve(null);
|
|
2764
2920
|
}
|
|
2765
2921
|
return new Promise((resolve) => {
|
|
2766
|
-
this.
|
|
2922
|
+
this.lineWaiter = resolve;
|
|
2767
2923
|
});
|
|
2768
2924
|
}
|
|
2769
|
-
deliver(line) {
|
|
2770
|
-
if (this.waiter != null) {
|
|
2771
|
-
const waiter = this.waiter;
|
|
2772
|
-
this.waiter = null;
|
|
2773
|
-
waiter(line);
|
|
2774
|
-
return;
|
|
2775
|
-
}
|
|
2776
|
-
this.pendingLines.push(line);
|
|
2777
|
-
}
|
|
2778
2925
|
onData(chunk) {
|
|
2779
|
-
if (this.
|
|
2780
|
-
for (const
|
|
2781
|
-
if (
|
|
2782
|
-
|
|
2783
|
-
this.secretBuffer = "";
|
|
2784
|
-
this.deliver(secret);
|
|
2785
|
-
continue;
|
|
2786
|
-
}
|
|
2787
|
-
if (character === "") {
|
|
2788
|
-
this.output.write("\n");
|
|
2789
|
-
process.exit(130);
|
|
2790
|
-
}
|
|
2791
|
-
if (character === "\x7F" || character === "\b") {
|
|
2792
|
-
this.secretBuffer = this.secretBuffer.slice(0, -1);
|
|
2793
|
-
continue;
|
|
2926
|
+
if (this.keyHandler != null) {
|
|
2927
|
+
for (const key of splitKeys(chunk)) {
|
|
2928
|
+
if (this.keyHandler == null) {
|
|
2929
|
+
break;
|
|
2794
2930
|
}
|
|
2795
|
-
this.
|
|
2931
|
+
this.keyHandler(key);
|
|
2796
2932
|
}
|
|
2797
2933
|
return;
|
|
2798
2934
|
}
|
|
@@ -2801,13 +2937,289 @@ var Prompter = class {
|
|
|
2801
2937
|
while (newlineIndex >= 0) {
|
|
2802
2938
|
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
2803
2939
|
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
2804
|
-
this.
|
|
2940
|
+
if (this.lineWaiter != null) {
|
|
2941
|
+
const waiter = this.lineWaiter;
|
|
2942
|
+
this.lineWaiter = null;
|
|
2943
|
+
waiter(line);
|
|
2944
|
+
} else {
|
|
2945
|
+
this.pendingLines.push(line);
|
|
2946
|
+
}
|
|
2805
2947
|
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2806
2948
|
}
|
|
2807
2949
|
}
|
|
2950
|
+
async selectWithLines(question, items, startIndex) {
|
|
2951
|
+
const choices = items.filter((item) => "value" in item);
|
|
2952
|
+
const labelWidths = groupLabelWidths(items);
|
|
2953
|
+
this.output.write(`${this.symbols.question} ${question}
|
|
2954
|
+
`);
|
|
2955
|
+
let number = 0;
|
|
2956
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
2957
|
+
const item = items[index];
|
|
2958
|
+
if ("value" in item == false) {
|
|
2959
|
+
this.output.write("\n");
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
number += 1;
|
|
2963
|
+
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
2964
|
+
if (item.hint != null && item.hint !== "") {
|
|
2965
|
+
line += ` ${item.hint}`;
|
|
2966
|
+
}
|
|
2967
|
+
this.output.write(`${line.trimEnd()}
|
|
2968
|
+
`);
|
|
2969
|
+
}
|
|
2970
|
+
const defaultChoice = items[startIndex];
|
|
2971
|
+
const defaultNumber = choices.indexOf(defaultChoice) + 1;
|
|
2972
|
+
for (; ; ) {
|
|
2973
|
+
this.output.write(`Choice (${defaultNumber}): `);
|
|
2974
|
+
const line = await this.readLine();
|
|
2975
|
+
if (line == null) {
|
|
2976
|
+
throw new PromptClosedError();
|
|
2977
|
+
}
|
|
2978
|
+
const answer = line.trim();
|
|
2979
|
+
if (answer === "") {
|
|
2980
|
+
return defaultChoice.value;
|
|
2981
|
+
}
|
|
2982
|
+
const index = Number(answer);
|
|
2983
|
+
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
2984
|
+
return choices[index - 1].value;
|
|
2985
|
+
}
|
|
2986
|
+
const lowered = answer.toLowerCase();
|
|
2987
|
+
const matched = choices.find((choice) => {
|
|
2988
|
+
const key = choice.key ?? choice.value;
|
|
2989
|
+
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
2990
|
+
});
|
|
2991
|
+
if (matched != null) {
|
|
2992
|
+
return matched.value;
|
|
2993
|
+
}
|
|
2994
|
+
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
2995
|
+
`);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
// ── TTY rendering ──────────────────────────────────────────────────────
|
|
2999
|
+
columns() {
|
|
3000
|
+
const columns = this.output.columns;
|
|
3001
|
+
if (columns == null || columns < 20) {
|
|
3002
|
+
return 80;
|
|
3003
|
+
}
|
|
3004
|
+
return columns;
|
|
3005
|
+
}
|
|
3006
|
+
/** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
|
|
3007
|
+
fitLine(parts, width) {
|
|
3008
|
+
let used = 0;
|
|
3009
|
+
let line = "";
|
|
3010
|
+
for (const part of parts) {
|
|
3011
|
+
let text = "";
|
|
3012
|
+
for (const character of part.text) {
|
|
3013
|
+
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
3014
|
+
if (used + characterWidth > width) {
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
used += characterWidth;
|
|
3018
|
+
text += character;
|
|
3019
|
+
}
|
|
3020
|
+
if (part.paint != null) {
|
|
3021
|
+
line += part.paint(text);
|
|
3022
|
+
} else {
|
|
3023
|
+
line += text;
|
|
3024
|
+
}
|
|
3025
|
+
if (used >= width) {
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return line;
|
|
3030
|
+
}
|
|
3031
|
+
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3032
|
+
writeAnswered(question, answer) {
|
|
3033
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
3034
|
+
`);
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Replaces a cooked-mode prompt and its echoed answer with the one-line record. The
|
|
3038
|
+
* rows are worked out from the widths, since a long answer wraps.
|
|
3039
|
+
*/
|
|
3040
|
+
collapseAnswered(prompt, typed, question, answer) {
|
|
3041
|
+
if (this.interactive == false) {
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3045
|
+
const used = displayWidth(plainPrompt) + displayWidth(typed);
|
|
3046
|
+
const rows = Math.max(1, Math.ceil(used / this.columns()));
|
|
3047
|
+
this.output.write(`${ESC}[${rows}A\r${ESC}[J`);
|
|
3048
|
+
let shown = answer;
|
|
3049
|
+
if (shown === "") {
|
|
3050
|
+
shown = this.style.dim("(empty)");
|
|
3051
|
+
}
|
|
3052
|
+
this.writeAnswered(question, shown);
|
|
3053
|
+
}
|
|
3054
|
+
selectWithKeys(question, items, startIndex, cancelValue) {
|
|
3055
|
+
const ttyInput = this.input;
|
|
3056
|
+
const selectableIndexes = items.flatMap((item, index) => {
|
|
3057
|
+
if ("value" in item) {
|
|
3058
|
+
return [index];
|
|
3059
|
+
}
|
|
3060
|
+
return [];
|
|
3061
|
+
});
|
|
3062
|
+
const labelWidths = groupLabelWidths(items);
|
|
3063
|
+
const ruleWidth = Math.min(Math.max(...labelWidths), 24);
|
|
3064
|
+
let active = startIndex;
|
|
3065
|
+
let renderedLines = 0;
|
|
3066
|
+
const draw = (lines) => {
|
|
3067
|
+
let text = "";
|
|
3068
|
+
if (renderedLines > 1) {
|
|
3069
|
+
text += `${ESC}[${renderedLines - 1}A`;
|
|
3070
|
+
}
|
|
3071
|
+
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
3072
|
+
this.output.write(text);
|
|
3073
|
+
renderedLines = lines.length;
|
|
3074
|
+
};
|
|
3075
|
+
const buildLines = () => {
|
|
3076
|
+
const width = this.columns() - 1;
|
|
3077
|
+
const lines = [
|
|
3078
|
+
this.fitLine([
|
|
3079
|
+
{ text: this.symbols.question, paint: this.style.accent },
|
|
3080
|
+
{ text: " " },
|
|
3081
|
+
{ text: question, paint: this.style.bold }
|
|
3082
|
+
], width)
|
|
3083
|
+
];
|
|
3084
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3085
|
+
const item = items[index];
|
|
3086
|
+
if ("value" in item == false) {
|
|
3087
|
+
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
const isActive = index === active;
|
|
3091
|
+
const parts = [];
|
|
3092
|
+
if (isActive == true) {
|
|
3093
|
+
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
3094
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
3095
|
+
} else {
|
|
3096
|
+
parts.push({ text: " " });
|
|
3097
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
3098
|
+
}
|
|
3099
|
+
if (item.hint != null && item.hint !== "") {
|
|
3100
|
+
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
3101
|
+
}
|
|
3102
|
+
lines.push(this.fitLine(parts, width));
|
|
3103
|
+
}
|
|
3104
|
+
let footer = "\u2191\u2193 move \xB7 enter select";
|
|
3105
|
+
if (cancelValue != null) {
|
|
3106
|
+
footer += " \xB7 esc back";
|
|
3107
|
+
}
|
|
3108
|
+
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3109
|
+
return lines;
|
|
3110
|
+
};
|
|
3111
|
+
const move = (step) => {
|
|
3112
|
+
const position = selectableIndexes.indexOf(active);
|
|
3113
|
+
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
3114
|
+
active = selectableIndexes[next];
|
|
3115
|
+
};
|
|
3116
|
+
return new Promise((resolve, reject) => {
|
|
3117
|
+
const finish = () => {
|
|
3118
|
+
this.keyHandler = null;
|
|
3119
|
+
ttyInput.setRawMode?.(false);
|
|
3120
|
+
draw([]);
|
|
3121
|
+
this.output.write(`${ESC}[?25h`);
|
|
3122
|
+
};
|
|
3123
|
+
this.keyHandler = (key) => {
|
|
3124
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3125
|
+
finish();
|
|
3126
|
+
reject(new PromptClosedError());
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3130
|
+
const picked = items[active];
|
|
3131
|
+
finish();
|
|
3132
|
+
this.writeAnswered(question, picked.label);
|
|
3133
|
+
resolve(picked.value);
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (key === ESC) {
|
|
3137
|
+
if (cancelValue == null) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
finish();
|
|
3141
|
+
resolve(cancelValue);
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
if (KEY_UP.includes(key) == true) {
|
|
3145
|
+
move(-1);
|
|
3146
|
+
} else if (KEY_DOWN.includes(key) == true) {
|
|
3147
|
+
move(1);
|
|
3148
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3149
|
+
active = selectableIndexes[0];
|
|
3150
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3151
|
+
active = selectableIndexes[selectableIndexes.length - 1];
|
|
3152
|
+
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
3153
|
+
active = selectableIndexes[Number(key) - 1];
|
|
3154
|
+
} else {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
draw(buildLines());
|
|
3158
|
+
};
|
|
3159
|
+
ttyInput.setRawMode?.(true);
|
|
3160
|
+
this.output.write(`${ESC}[?25l`);
|
|
3161
|
+
draw(buildLines());
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
2808
3164
|
};
|
|
2809
|
-
var
|
|
3165
|
+
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3166
|
+
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3167
|
+
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
2810
3168
|
var CLEAR_ANSWER = "-";
|
|
3169
|
+
var OTHER_KEY_PATH = "\0other";
|
|
3170
|
+
function findPrivateKeys() {
|
|
3171
|
+
const sshDirectory = path.join(os5.homedir(), ".ssh");
|
|
3172
|
+
let names = [];
|
|
3173
|
+
try {
|
|
3174
|
+
names = fs6.readdirSync(sshDirectory);
|
|
3175
|
+
} catch {
|
|
3176
|
+
return [];
|
|
3177
|
+
}
|
|
3178
|
+
const keys = [];
|
|
3179
|
+
for (const name of names) {
|
|
3180
|
+
if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
const fullPath = path.join(sshDirectory, name);
|
|
3184
|
+
let head = "";
|
|
3185
|
+
try {
|
|
3186
|
+
const stat = fs6.statSync(fullPath);
|
|
3187
|
+
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3188
|
+
continue;
|
|
3189
|
+
}
|
|
3190
|
+
const descriptor = fs6.openSync(fullPath, "r");
|
|
3191
|
+
const buffer = Buffer.alloc(64);
|
|
3192
|
+
const bytesRead = fs6.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3193
|
+
fs6.closeSync(descriptor);
|
|
3194
|
+
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3195
|
+
} catch {
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
|
|
3199
|
+
const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
|
|
3200
|
+
if (isPemKey == false && isPuttyKey == false) {
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
let hint = "";
|
|
3204
|
+
try {
|
|
3205
|
+
const publicKey = fs6.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3206
|
+
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3207
|
+
if (publicKey.length > 2) {
|
|
3208
|
+
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
3209
|
+
}
|
|
3210
|
+
} catch {
|
|
3211
|
+
}
|
|
3212
|
+
keys.push({ path: `~/.ssh/${name}`, hint });
|
|
3213
|
+
}
|
|
3214
|
+
const rank = (key) => {
|
|
3215
|
+
const index = PREFERRED_KEY_NAMES.indexOf(path.basename(key.path));
|
|
3216
|
+
if (index < 0) {
|
|
3217
|
+
return PREFERRED_KEY_NAMES.length;
|
|
3218
|
+
}
|
|
3219
|
+
return index;
|
|
3220
|
+
};
|
|
3221
|
+
return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
|
|
3222
|
+
}
|
|
2811
3223
|
async function askPort(prompter, question, current) {
|
|
2812
3224
|
for (; ; ) {
|
|
2813
3225
|
const answer = await prompter.ask(question, String(current));
|
|
@@ -2826,11 +3238,11 @@ async function askOptional(prompter, question, current) {
|
|
|
2826
3238
|
return answer;
|
|
2827
3239
|
}
|
|
2828
3240
|
async function askOptionalSecret(prompter, question, current) {
|
|
2829
|
-
let suffix = "
|
|
3241
|
+
let suffix = "(empty for none)";
|
|
2830
3242
|
if (current != null) {
|
|
2831
|
-
suffix = `
|
|
3243
|
+
suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
|
|
2832
3244
|
}
|
|
2833
|
-
const answer = await prompter.askSecret(`${question}${suffix}`);
|
|
3245
|
+
const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
|
|
2834
3246
|
if (answer === CLEAR_ANSWER) {
|
|
2835
3247
|
return void 0;
|
|
2836
3248
|
}
|
|
@@ -2839,11 +3251,42 @@ async function askOptionalSecret(prompter, question, current) {
|
|
|
2839
3251
|
}
|
|
2840
3252
|
return answer;
|
|
2841
3253
|
}
|
|
3254
|
+
async function askKeyPath(prompter, current) {
|
|
3255
|
+
const found = findPrivateKeys();
|
|
3256
|
+
let picked = OTHER_KEY_PATH;
|
|
3257
|
+
if (found.length > 0 || current != null) {
|
|
3258
|
+
const items = [];
|
|
3259
|
+
if (current != null && found.some((key) => key.path === current) == false) {
|
|
3260
|
+
items.push({ value: current, label: current, hint: "current" });
|
|
3261
|
+
}
|
|
3262
|
+
for (const key of found) {
|
|
3263
|
+
items.push({ value: key.path, label: key.path, hint: key.hint });
|
|
3264
|
+
}
|
|
3265
|
+
items.push({ separator: true });
|
|
3266
|
+
items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
|
|
3267
|
+
picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
|
|
3268
|
+
}
|
|
3269
|
+
if (picked !== OTHER_KEY_PATH) {
|
|
3270
|
+
return picked;
|
|
3271
|
+
}
|
|
3272
|
+
for (; ; ) {
|
|
3273
|
+
const typed = await prompter.askRequired("Private key path");
|
|
3274
|
+
if (fs6.existsSync(expandHomePath(typed)) == true) {
|
|
3275
|
+
return typed;
|
|
3276
|
+
}
|
|
3277
|
+
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
3278
|
+
`);
|
|
3279
|
+
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
3280
|
+
if (useAnyway == true) {
|
|
3281
|
+
return typed;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
2842
3285
|
async function askHostQuestions(prompter, existing) {
|
|
2843
3286
|
if (existing != null) {
|
|
2844
|
-
prompter.write(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
|
|
3287
|
+
prompter.write(prompter.paint.dim(` Enter keeps the value in brackets; ${CLEAR_ANSWER} clears an optional one.
|
|
2845
3288
|
|
|
2846
|
-
`);
|
|
3289
|
+
`));
|
|
2847
3290
|
}
|
|
2848
3291
|
const host = await prompter.askRequired("Host (hostname or IP)", existing?.host);
|
|
2849
3292
|
const port = await askPort(prompter, "Port", existing?.port ?? DEFAULT_SSH_PORT);
|
|
@@ -2857,60 +3300,60 @@ async function askHostQuestions(prompter, existing) {
|
|
|
2857
3300
|
}
|
|
2858
3301
|
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
2859
3302
|
}
|
|
2860
|
-
const auth = await prompter.
|
|
3303
|
+
const auth = await prompter.select(
|
|
2861
3304
|
"Authentication",
|
|
2862
3305
|
[
|
|
2863
|
-
{
|
|
2864
|
-
{
|
|
2865
|
-
{
|
|
3306
|
+
{ value: "key", label: "Private key file", hint: "only the path is stored \u2014 recommended" },
|
|
3307
|
+
{ value: "password", label: "Password", hint: "stored in the config file as plain text" },
|
|
3308
|
+
{ value: "agent", label: "SSH agent", hint: "keys already loaded in ssh-agent" }
|
|
2866
3309
|
],
|
|
2867
|
-
existing?.auth ?? "key"
|
|
3310
|
+
{ defaultValue: existing?.auth ?? "key" }
|
|
2868
3311
|
);
|
|
2869
3312
|
let keyPath;
|
|
2870
3313
|
let passphrase;
|
|
2871
3314
|
let password;
|
|
2872
3315
|
if (auth === "key") {
|
|
2873
|
-
|
|
2874
|
-
if (defaultKeyPath == null) {
|
|
2875
|
-
defaultKeyPath = DEFAULT_KEY_CANDIDATES.find((candidate) => fs7.existsSync(expandHomePath(candidate)) == true);
|
|
2876
|
-
}
|
|
2877
|
-
for (; ; ) {
|
|
2878
|
-
keyPath = await prompter.askRequired("Private key path", defaultKeyPath);
|
|
2879
|
-
if (fs7.existsSync(expandHomePath(keyPath)) == true) {
|
|
2880
|
-
break;
|
|
2881
|
-
}
|
|
2882
|
-
prompter.write(` ${expandHomePath(keyPath)} does not exist.
|
|
2883
|
-
`);
|
|
2884
|
-
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
2885
|
-
if (useAnyway == true) {
|
|
2886
|
-
break;
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
3316
|
+
keyPath = await askKeyPath(prompter, existing?.keyPath);
|
|
2889
3317
|
passphrase = await askOptionalSecret(prompter, "Key passphrase", existing?.passphrase);
|
|
2890
3318
|
}
|
|
2891
3319
|
if (auth === "password") {
|
|
2892
|
-
prompter.write(" The password is written into the config file as plain text. A key file is safer.\n");
|
|
2893
3320
|
password = await askOptionalSecret(prompter, "Password", existing?.password);
|
|
2894
3321
|
if (password == null) {
|
|
2895
3322
|
prompter.write(" No password given \u2014 falling back to the ssh agent.\n");
|
|
2896
3323
|
}
|
|
2897
3324
|
}
|
|
3325
|
+
let currentRoute = "direct";
|
|
3326
|
+
if (existing?.jumpHost != null && existing.proxyCommand != null) {
|
|
3327
|
+
currentRoute = "jump-proxy";
|
|
3328
|
+
} else if (existing?.jumpHost != null) {
|
|
3329
|
+
currentRoute = "jump";
|
|
3330
|
+
} else if (existing?.proxyCommand != null) {
|
|
3331
|
+
currentRoute = "proxy";
|
|
3332
|
+
}
|
|
3333
|
+
const route = await prompter.select(
|
|
3334
|
+
"How is it reached?",
|
|
3335
|
+
[
|
|
3336
|
+
{ value: "direct", label: "Directly" },
|
|
3337
|
+
{ value: "jump", label: "Through a jump host", hint: "ssh -J" },
|
|
3338
|
+
{ value: "proxy", label: "Through a proxy command", hint: "e.g. cloudflared access ssh" },
|
|
3339
|
+
{ value: "jump-proxy", label: "Proxy command, then a jump host", hint: "cloudflared to the bastion, then on to the host" }
|
|
3340
|
+
],
|
|
3341
|
+
{ defaultValue: currentRoute }
|
|
3342
|
+
);
|
|
2898
3343
|
let jumpHost;
|
|
2899
3344
|
let jumpPort;
|
|
2900
3345
|
let jumpUser;
|
|
2901
|
-
|
|
2902
|
-
if (
|
|
3346
|
+
let proxyCommand;
|
|
3347
|
+
if (route === "jump" || route === "jump-proxy") {
|
|
2903
3348
|
jumpHost = await prompter.askRequired("Jump host", existing?.jumpHost);
|
|
2904
3349
|
jumpPort = await askPort(prompter, "Jump host port", existing?.jumpPort ?? DEFAULT_SSH_PORT);
|
|
2905
3350
|
jumpUser = await prompter.askRequired("Jump host user", existing?.jumpUser ?? username);
|
|
2906
3351
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
if (viaProxy == true) {
|
|
2910
|
-
prompter.write(" %h / %p / %r expand to the first hop's host, port and user.\n");
|
|
3352
|
+
if (route === "proxy" || route === "jump-proxy") {
|
|
3353
|
+
prompter.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
2911
3354
|
proxyCommand = await prompter.askRequired("Proxy command", existing?.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
2912
3355
|
}
|
|
2913
|
-
const description = await askOptional(prompter, "Description shown to the agent
|
|
3356
|
+
const description = await askOptional(prompter, "Description shown to the agent", existing?.description);
|
|
2914
3357
|
const readonly = await prompter.askYesNo("Read-only? (rejects writes, redirections, uploads)", existing?.readonly ?? false);
|
|
2915
3358
|
let currentAllowedPaths;
|
|
2916
3359
|
if (existing != null && existing.allowedPaths.length > 0) {
|
|
@@ -2919,7 +3362,7 @@ async function askHostQuestions(prompter, existing) {
|
|
|
2919
3362
|
let allowedPaths = [];
|
|
2920
3363
|
const allowedPathsAnswer = await askOptional(
|
|
2921
3364
|
prompter,
|
|
2922
|
-
"
|
|
3365
|
+
"Limit file tools to path prefixes (comma-separated, empty = anywhere)",
|
|
2923
3366
|
currentAllowedPaths
|
|
2924
3367
|
);
|
|
2925
3368
|
if (allowedPathsAnswer != null) {
|
|
@@ -2995,10 +3438,10 @@ function readMcpServersJson(document) {
|
|
|
2995
3438
|
return entries;
|
|
2996
3439
|
}
|
|
2997
3440
|
function readDocument(filePath) {
|
|
2998
|
-
if (
|
|
3441
|
+
if (fs6.existsSync(filePath) == false) {
|
|
2999
3442
|
return {};
|
|
3000
3443
|
}
|
|
3001
|
-
const text =
|
|
3444
|
+
const text = fs6.readFileSync(filePath, "utf8");
|
|
3002
3445
|
if (text.trim() === "") {
|
|
3003
3446
|
return {};
|
|
3004
3447
|
}
|
|
@@ -3020,15 +3463,15 @@ function readDocument(filePath) {
|
|
|
3020
3463
|
function createJsonFileTarget(options) {
|
|
3021
3464
|
const { id, label, filePath } = options;
|
|
3022
3465
|
function writeDocument(document) {
|
|
3023
|
-
|
|
3024
|
-
|
|
3466
|
+
fs6.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
3467
|
+
fs6.writeFileSync(filePath, `${JSON.stringify(document, null, 4)}
|
|
3025
3468
|
`, "utf8");
|
|
3026
3469
|
}
|
|
3027
3470
|
return {
|
|
3028
3471
|
id,
|
|
3029
3472
|
label,
|
|
3030
3473
|
location: filePath,
|
|
3031
|
-
detected:
|
|
3474
|
+
detected: fs6.existsSync(filePath),
|
|
3032
3475
|
readEntries() {
|
|
3033
3476
|
const document = readDocument(filePath);
|
|
3034
3477
|
return readMcpServersJson(document);
|
|
@@ -3058,7 +3501,7 @@ function createJsonFileTarget(options) {
|
|
|
3058
3501
|
}
|
|
3059
3502
|
|
|
3060
3503
|
// src/modules/manage/targets/claude-code.ts
|
|
3061
|
-
var CLAUDE_CODE_CONFIG_PATH = path.join(
|
|
3504
|
+
var CLAUDE_CODE_CONFIG_PATH = path.join(os5.homedir(), ".claude.json");
|
|
3062
3505
|
var CMD_META_CHARACTER_PATTERN = /([()\][%!^"`<>&|;, *?])/g;
|
|
3063
3506
|
function toClaudeAddJsonPayload(entry) {
|
|
3064
3507
|
return JSON.stringify({ type: "stdio", command: entry.command, args: entry.args, env: entry.env });
|
|
@@ -3104,10 +3547,10 @@ function runClaude(executable, args) {
|
|
|
3104
3547
|
return { status: "written", detail: CLAUDE_CODE_CONFIG_PATH };
|
|
3105
3548
|
}
|
|
3106
3549
|
function readClaudeEntries() {
|
|
3107
|
-
if (
|
|
3550
|
+
if (fs6.existsSync(CLAUDE_CODE_CONFIG_PATH) == false) {
|
|
3108
3551
|
return {};
|
|
3109
3552
|
}
|
|
3110
|
-
const text =
|
|
3553
|
+
const text = fs6.readFileSync(CLAUDE_CODE_CONFIG_PATH, "utf8");
|
|
3111
3554
|
let document;
|
|
3112
3555
|
try {
|
|
3113
3556
|
document = JSON.parse(text);
|
|
@@ -3126,7 +3569,7 @@ function createClaudeCodeTarget() {
|
|
|
3126
3569
|
id: "claude-code",
|
|
3127
3570
|
label: "Claude Code (user scope)",
|
|
3128
3571
|
location: CLAUDE_CODE_CONFIG_PATH,
|
|
3129
|
-
detected: executable != null ||
|
|
3572
|
+
detected: executable != null || fs6.existsSync(CLAUDE_CODE_CONFIG_PATH) == true,
|
|
3130
3573
|
readEntries: readClaudeEntries,
|
|
3131
3574
|
writeEntry(name, entry) {
|
|
3132
3575
|
if (executable == null) {
|
|
@@ -3578,24 +4021,24 @@ function writeMcpServerToml(text, name, entry) {
|
|
|
3578
4021
|
}
|
|
3579
4022
|
|
|
3580
4023
|
// src/modules/manage/targets/codex.ts
|
|
3581
|
-
var CODEX_CONFIG_PATH = path.join(
|
|
4024
|
+
var CODEX_CONFIG_PATH = path.join(os5.homedir(), ".codex", "config.toml");
|
|
3582
4025
|
function createCodexTarget() {
|
|
3583
4026
|
function readText() {
|
|
3584
|
-
if (
|
|
4027
|
+
if (fs6.existsSync(CODEX_CONFIG_PATH) == false) {
|
|
3585
4028
|
return "";
|
|
3586
4029
|
}
|
|
3587
|
-
return
|
|
4030
|
+
return fs6.readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
3588
4031
|
}
|
|
3589
4032
|
function writeText(text) {
|
|
3590
|
-
|
|
3591
|
-
|
|
4033
|
+
fs6.mkdirSync(path.dirname(CODEX_CONFIG_PATH), { recursive: true });
|
|
4034
|
+
fs6.writeFileSync(CODEX_CONFIG_PATH, text, "utf8");
|
|
3592
4035
|
return { status: "written", detail: CODEX_CONFIG_PATH };
|
|
3593
4036
|
}
|
|
3594
4037
|
return {
|
|
3595
4038
|
id: "codex",
|
|
3596
4039
|
label: "Codex CLI",
|
|
3597
4040
|
location: CODEX_CONFIG_PATH,
|
|
3598
|
-
detected: findExecutable("codex") != null ||
|
|
4041
|
+
detected: findExecutable("codex") != null || fs6.existsSync(CODEX_CONFIG_PATH) == true,
|
|
3599
4042
|
readEntries() {
|
|
3600
4043
|
const text = readText();
|
|
3601
4044
|
return parseMcpServersToml(text);
|
|
@@ -3616,20 +4059,20 @@ function createCodexTarget() {
|
|
|
3616
4059
|
// src/modules/manage/targets/index.ts
|
|
3617
4060
|
function claudeDesktopConfigPath() {
|
|
3618
4061
|
if (process.platform === "win32") {
|
|
3619
|
-
const appData = process.env.APPDATA ?? path.join(
|
|
4062
|
+
const appData = process.env.APPDATA ?? path.join(os5.homedir(), "AppData", "Roaming");
|
|
3620
4063
|
return path.join(appData, "Claude", "claude_desktop_config.json");
|
|
3621
4064
|
}
|
|
3622
4065
|
if (process.platform === "darwin") {
|
|
3623
|
-
return path.join(
|
|
4066
|
+
return path.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
3624
4067
|
}
|
|
3625
|
-
return path.join(
|
|
4068
|
+
return path.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
3626
4069
|
}
|
|
3627
4070
|
function listConfigTargets() {
|
|
3628
4071
|
const targets = [
|
|
3629
4072
|
createClaudeCodeTarget(),
|
|
3630
4073
|
createCodexTarget(),
|
|
3631
4074
|
createJsonFileTarget({ id: "claude-desktop", label: "Claude Desktop", filePath: claudeDesktopConfigPath() }),
|
|
3632
|
-
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path.join(
|
|
4075
|
+
createJsonFileTarget({ id: "cursor", label: "Cursor", filePath: path.join(os5.homedir(), ".cursor", "mcp.json") })
|
|
3633
4076
|
];
|
|
3634
4077
|
const detected = targets.filter((target) => target.detected == true);
|
|
3635
4078
|
const others = targets.filter((target) => target.detected == false);
|
|
@@ -3643,6 +4086,7 @@ var SECRET_ENV_NAMES = [
|
|
|
3643
4086
|
SINGLE_HOST_ENV.JUMP_PASSWORD,
|
|
3644
4087
|
SINGLE_HOST_ENV.JUMP_PASSPHRASE
|
|
3645
4088
|
];
|
|
4089
|
+
var HOST_VALUE_PREFIX = "host:";
|
|
3646
4090
|
function errorMessage(error) {
|
|
3647
4091
|
if (error instanceof Error) {
|
|
3648
4092
|
return error.message;
|
|
@@ -3670,14 +4114,14 @@ function formatForTarget(target, name, entry) {
|
|
|
3670
4114
|
function indent(text) {
|
|
3671
4115
|
return text.replace(/\n$/, "").split("\n").map((line) => ` ${line}`).join("\n");
|
|
3672
4116
|
}
|
|
3673
|
-
function reportWrite(
|
|
4117
|
+
function reportWrite(prompter, result, action) {
|
|
3674
4118
|
if (result.status === "written") {
|
|
3675
|
-
|
|
4119
|
+
prompter.write(` ${prompter.paint.success(action)} ${prompter.paint.dim(result.detail)}
|
|
3676
4120
|
`);
|
|
3677
|
-
|
|
4121
|
+
prompter.write(prompter.paint.dim(" Restart the client (or reconnect it from its MCP menu) to pick up the change.\n\n"));
|
|
3678
4122
|
return;
|
|
3679
4123
|
}
|
|
3680
|
-
|
|
4124
|
+
prompter.write(` Not ${action.toLowerCase()}: ${result.detail}
|
|
3681
4125
|
|
|
3682
4126
|
`);
|
|
3683
4127
|
}
|
|
@@ -3708,13 +4152,13 @@ async function verifyAnswers(prompter, output, initial) {
|
|
|
3708
4152
|
output.write("\n");
|
|
3709
4153
|
if (result.connected == true) {
|
|
3710
4154
|
if (result.hostKeyFingerprint != null && answers.hostKeyFingerprint == null) {
|
|
3711
|
-
const pin = await prompter.askYesNo(
|
|
4155
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
3712
4156
|
if (pin == true) {
|
|
3713
4157
|
answers = { ...answers, hostKeyFingerprint: result.hostKeyFingerprint };
|
|
3714
4158
|
}
|
|
3715
4159
|
}
|
|
3716
4160
|
if (result.jumpHostKeyFingerprint != null && answers.jumpHost != null && answers.jumpHostKeyFingerprint == null) {
|
|
3717
|
-
const pin = await prompter.askYesNo(
|
|
4161
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3718
4162
|
if (pin == true) {
|
|
3719
4163
|
answers = { ...answers, jumpHostKeyFingerprint: result.jumpHostKeyFingerprint };
|
|
3720
4164
|
}
|
|
@@ -3740,23 +4184,23 @@ async function verifyAnswers(prompter, output, initial) {
|
|
|
3740
4184
|
}
|
|
3741
4185
|
}
|
|
3742
4186
|
}
|
|
3743
|
-
const next = await prompter.
|
|
3744
|
-
"What now?",
|
|
4187
|
+
const next = await prompter.select(
|
|
4188
|
+
"The check failed. What now?",
|
|
3745
4189
|
[
|
|
3746
|
-
{ key: "
|
|
3747
|
-
{ key: "
|
|
3748
|
-
{ key: "w", label: "
|
|
3749
|
-
{ key: "c", label: "
|
|
4190
|
+
{ value: "edit", key: "e", label: "Change the answers" },
|
|
4191
|
+
{ value: "retry", key: "r", label: "Try again", hint: "e.g. after starting a tunnel" },
|
|
4192
|
+
{ value: "write", key: "w", label: "Write it anyway", hint: "the host may be unreachable from here" },
|
|
4193
|
+
{ value: "cancel", key: "c", label: "Cancel", hint: "nothing is written" }
|
|
3750
4194
|
],
|
|
3751
|
-
"
|
|
4195
|
+
{ defaultValue: "edit", cancelValue: "cancel" }
|
|
3752
4196
|
);
|
|
3753
|
-
if (next === "
|
|
4197
|
+
if (next === "cancel") {
|
|
3754
4198
|
return null;
|
|
3755
4199
|
}
|
|
3756
|
-
if (next === "
|
|
4200
|
+
if (next === "write") {
|
|
3757
4201
|
return answers;
|
|
3758
4202
|
}
|
|
3759
|
-
if (next === "
|
|
4203
|
+
if (next === "edit") {
|
|
3760
4204
|
output.write("\n");
|
|
3761
4205
|
answers = await askHostQuestions(prompter, answers);
|
|
3762
4206
|
}
|
|
@@ -3767,10 +4211,12 @@ async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
|
3767
4211
|
if (existingName != null) {
|
|
3768
4212
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
3769
4213
|
output.write(`
|
|
3770
|
-
Editing ${existingName}
|
|
4214
|
+
${prompter.paint.bold(`Editing ${existingName}`)}
|
|
3771
4215
|
`);
|
|
3772
4216
|
} else {
|
|
3773
|
-
output.write(
|
|
4217
|
+
output.write(`
|
|
4218
|
+
${prompter.paint.bold("Adding a host")}
|
|
4219
|
+
`);
|
|
3774
4220
|
}
|
|
3775
4221
|
const asked = await askHostQuestions(prompter, existing);
|
|
3776
4222
|
output.write("\n");
|
|
@@ -3788,7 +4234,7 @@ Editing ${existingName}
|
|
|
3788
4234
|
}
|
|
3789
4235
|
const entry = answersToEntry(answers);
|
|
3790
4236
|
output.write(`
|
|
3791
|
-
This goes into ${target.location}
|
|
4237
|
+
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
3792
4238
|
|
|
3793
4239
|
`);
|
|
3794
4240
|
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
@@ -3796,11 +4242,11 @@ Editing ${existingName}
|
|
|
3796
4242
|
`);
|
|
3797
4243
|
const confirmed = await prompter.askYesNo("Write it?", true);
|
|
3798
4244
|
if (confirmed == false) {
|
|
3799
|
-
output.write(" Not written.
|
|
4245
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
3800
4246
|
return;
|
|
3801
4247
|
}
|
|
3802
4248
|
const written = target.writeEntry(answers.name, entry);
|
|
3803
|
-
reportWrite(
|
|
4249
|
+
reportWrite(prompter, written, "Written");
|
|
3804
4250
|
if (written.status === "written" && existingName != null && existingName !== answers.name) {
|
|
3805
4251
|
const removed = target.removeEntry(existingName);
|
|
3806
4252
|
if (removed.status === "unavailable") {
|
|
@@ -3825,14 +4271,14 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3825
4271
|
const env = { ...entry.env };
|
|
3826
4272
|
let changed = false;
|
|
3827
4273
|
if (result.hostKeyFingerprint != null && env[SINGLE_HOST_ENV.HOST_KEY] == null) {
|
|
3828
|
-
const pin = await prompter.askYesNo(
|
|
4274
|
+
const pin = await prompter.askYesNo("Pin this host key?", true);
|
|
3829
4275
|
if (pin == true) {
|
|
3830
4276
|
env[SINGLE_HOST_ENV.HOST_KEY] = result.hostKeyFingerprint;
|
|
3831
4277
|
changed = true;
|
|
3832
4278
|
}
|
|
3833
4279
|
}
|
|
3834
4280
|
if (result.jumpHostKeyFingerprint != null && env[SINGLE_HOST_ENV.JUMP_HOST_KEY] == null) {
|
|
3835
|
-
const pin = await prompter.askYesNo(
|
|
4281
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3836
4282
|
if (pin == true) {
|
|
3837
4283
|
env[SINGLE_HOST_ENV.JUMP_HOST_KEY] = result.jumpHostKeyFingerprint;
|
|
3838
4284
|
changed = true;
|
|
@@ -3840,76 +4286,79 @@ async function checkHost(prompter, output, target, name, entry) {
|
|
|
3840
4286
|
}
|
|
3841
4287
|
if (changed == true) {
|
|
3842
4288
|
const written = target.writeEntry(name, { ...entry, env });
|
|
3843
|
-
reportWrite(
|
|
4289
|
+
reportWrite(prompter, written, "Pinned");
|
|
3844
4290
|
}
|
|
3845
4291
|
}
|
|
3846
|
-
function printHost(
|
|
3847
|
-
|
|
3848
|
-
|
|
4292
|
+
function printHost(prompter, name, entry) {
|
|
4293
|
+
prompter.write(`
|
|
4294
|
+
${prompter.paint.dim('JSON \u2014 Claude Desktop, Cursor, .mcp.json (inside "mcpServers"):')}
|
|
3849
4295
|
|
|
3850
4296
|
`);
|
|
3851
|
-
|
|
3852
|
-
output.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4297
|
+
prompter.write(`${indent(`"${name}": ${JSON.stringify(entry, null, 4)}`)}
|
|
3853
4298
|
|
|
3854
4299
|
`);
|
|
3855
|
-
|
|
3856
|
-
output.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4300
|
+
prompter.write(` ${prompter.paint.dim("TOML \u2014 Codex (~/.codex/config.toml):")}
|
|
3857
4301
|
|
|
3858
4302
|
`);
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
if (names.length === 0) {
|
|
3862
|
-
output.write(" No hosts yet \u2014 add one with a.\n\n");
|
|
3863
|
-
return null;
|
|
3864
|
-
}
|
|
3865
|
-
if (names.length === 1) {
|
|
3866
|
-
return names[0];
|
|
3867
|
-
}
|
|
3868
|
-
for (; ; ) {
|
|
3869
|
-
const answer = await prompter.ask("Which host (number or name, empty to go back)");
|
|
3870
|
-
if (answer === "") {
|
|
3871
|
-
return null;
|
|
3872
|
-
}
|
|
3873
|
-
const index = Number(answer);
|
|
3874
|
-
if (Number.isInteger(index) == true && index >= 1 && index <= names.length) {
|
|
3875
|
-
return names[index - 1];
|
|
3876
|
-
}
|
|
3877
|
-
if (names.includes(answer) == true) {
|
|
3878
|
-
return answer;
|
|
3879
|
-
}
|
|
3880
|
-
output.write(` No host "${answer}".
|
|
4303
|
+
prompter.write(`${indent(renderMcpServerTable(name, entry))}
|
|
4304
|
+
|
|
3881
4305
|
`);
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
function renderHostList(output, target, entries) {
|
|
3885
|
-
const names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3886
|
-
const otherCount = Object.keys(entries).length - names.length;
|
|
3887
|
-
output.write(`
|
|
3888
|
-
${target.label} \u2014 ${target.location}
|
|
4306
|
+
prompter.write(` ${prompter.paint.dim("Claude Code CLI (bash / zsh / Git Bash):")}
|
|
3889
4307
|
|
|
3890
4308
|
`);
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
} else {
|
|
3894
|
-
const rows = names.map((name, index) => {
|
|
3895
|
-
const summary = summarizeEntry(entries[name]);
|
|
3896
|
-
return [String(index + 1), name, summary.endpoint, summary.auth, summary.notes];
|
|
3897
|
-
});
|
|
3898
|
-
const header = ["#", "Server", "Endpoint", "Auth", "Notes"];
|
|
3899
|
-
const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => row[column].length)));
|
|
3900
|
-
for (const row of [header, ...rows]) {
|
|
3901
|
-
const line = row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
3902
|
-
output.write(` ${line}
|
|
4309
|
+
prompter.write(`${indent(formatClaudeAddCommand(name, entry))}
|
|
4310
|
+
|
|
3903
4311
|
`);
|
|
4312
|
+
}
|
|
4313
|
+
function buildHostItems(names, entries) {
|
|
4314
|
+
const nameWidth = Math.max(...names.map((name) => displayWidth(name)));
|
|
4315
|
+
const summaries = names.map((name) => summarizeEntry(entries[name]));
|
|
4316
|
+
const endpointWidth = Math.max(...summaries.map((summary) => displayWidth(summary.endpoint)));
|
|
4317
|
+
return names.map((name, index) => {
|
|
4318
|
+
const summary = summaries[index];
|
|
4319
|
+
const hintParts = [summary.auth];
|
|
4320
|
+
if (summary.notes !== "") {
|
|
4321
|
+
hintParts.push(summary.notes);
|
|
3904
4322
|
}
|
|
4323
|
+
return {
|
|
4324
|
+
value: `${HOST_VALUE_PREFIX}${name}`,
|
|
4325
|
+
key: name,
|
|
4326
|
+
label: `${name.padEnd(nameWidth)} ${summary.endpoint.padEnd(endpointWidth)}`,
|
|
4327
|
+
hint: hintParts.join(" \xB7 ")
|
|
4328
|
+
};
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
async function manageHost(prompter, output, target, entries, name) {
|
|
4332
|
+
const action = await prompter.select(
|
|
4333
|
+
name,
|
|
4334
|
+
[
|
|
4335
|
+
{ value: "edit", key: "e", label: "Edit", hint: "change the answers, re-check, re-write" },
|
|
4336
|
+
{ value: "check", key: "c", label: "Check the connection", hint: "connect now and offer to pin the host key" },
|
|
4337
|
+
{ value: "print", key: "p", label: "Print the entry", hint: "JSON, Codex TOML and a claude command, ready to paste" },
|
|
4338
|
+
{ value: "remove", key: "r", label: "Remove", hint: `from ${target.label}` },
|
|
4339
|
+
{ separator: true },
|
|
4340
|
+
{ value: "back", key: "b", label: "Back" }
|
|
4341
|
+
],
|
|
4342
|
+
{ defaultValue: "edit", cancelValue: "back" }
|
|
4343
|
+
);
|
|
4344
|
+
if (action === "edit") {
|
|
4345
|
+
await addOrEditHost(prompter, output, target, entries, name);
|
|
4346
|
+
return;
|
|
3905
4347
|
}
|
|
3906
|
-
if (
|
|
3907
|
-
output
|
|
3908
|
-
|
|
3909
|
-
|
|
4348
|
+
if (action === "check") {
|
|
4349
|
+
await checkHost(prompter, output, target, name, entries[name]);
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
if (action === "print") {
|
|
4353
|
+
printHost(prompter, name, entries[name]);
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
if (action === "remove") {
|
|
4357
|
+
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
4358
|
+
if (remove == true) {
|
|
4359
|
+
reportWrite(prompter, target.removeEntry(name), "Removed");
|
|
4360
|
+
}
|
|
3910
4361
|
}
|
|
3911
|
-
output.write("\n");
|
|
3912
|
-
return names;
|
|
3913
4362
|
}
|
|
3914
4363
|
async function runConfigCommand(options) {
|
|
3915
4364
|
const { input, output } = options;
|
|
@@ -3925,81 +4374,88 @@ async function runConfigCommand(options) {
|
|
|
3925
4374
|
target = requested;
|
|
3926
4375
|
}
|
|
3927
4376
|
const prompter = new Prompter(input, output);
|
|
4377
|
+
const paint = prompter.paint;
|
|
3928
4378
|
try {
|
|
3929
4379
|
for (; ; ) {
|
|
3930
4380
|
let entries = {};
|
|
3931
4381
|
let names = [];
|
|
3932
|
-
let
|
|
4382
|
+
let readError = null;
|
|
3933
4383
|
try {
|
|
3934
4384
|
entries = target.readEntries();
|
|
3935
|
-
names =
|
|
4385
|
+
names = Object.keys(entries).filter((name) => isOurEntry(entries[name]) == true);
|
|
3936
4386
|
} catch (ex) {
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4387
|
+
readError = errorMessage(ex);
|
|
4388
|
+
}
|
|
4389
|
+
output.write(`
|
|
4390
|
+
${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
4391
|
+
`);
|
|
4392
|
+
if (readError != null) {
|
|
4393
|
+
output.write(` Cannot read it: ${readError}
|
|
3942
4394
|
Fix the file by hand, or switch to another client.
|
|
3943
|
-
|
|
3944
4395
|
`);
|
|
4396
|
+
} else {
|
|
4397
|
+
const otherCount = Object.keys(entries).length - names.length;
|
|
4398
|
+
if (otherCount > 0) {
|
|
4399
|
+
output.write(paint.dim(` ${otherCount} other MCP server(s) in this file are not managed here.
|
|
4400
|
+
`));
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
output.write("\n");
|
|
4404
|
+
const items = [];
|
|
4405
|
+
if (names.length > 0) {
|
|
4406
|
+
items.push(...buildHostItems(names, entries));
|
|
4407
|
+
items.push({ separator: true });
|
|
3945
4408
|
}
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
menu = " a) add e) edit r) remove c) check p) print t) switch client q) quit";
|
|
3949
|
-
} else if (readable == true) {
|
|
3950
|
-
menu = " a) add t) switch client q) quit";
|
|
4409
|
+
if (readError == null) {
|
|
4410
|
+
items.push({ value: "add", key: "a", label: "Add a host" });
|
|
3951
4411
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4412
|
+
const otherTargets = targets.filter((candidate) => candidate.id !== target.id);
|
|
4413
|
+
if (otherTargets.length > 0) {
|
|
4414
|
+
const detectedOthers = otherTargets.filter((candidate) => candidate.detected == true).map((candidate) => candidate.label);
|
|
4415
|
+
let switchHint = "";
|
|
4416
|
+
if (detectedOthers.length > 0) {
|
|
4417
|
+
switchHint = `also found: ${detectedOthers.join(", ")}`;
|
|
4418
|
+
}
|
|
4419
|
+
items.push({ value: "switch", key: "t", label: "Switch client", hint: switchHint });
|
|
4420
|
+
}
|
|
4421
|
+
items.push({ value: "quit", key: "q", label: "Quit" });
|
|
4422
|
+
let question = "No SSH hosts here yet";
|
|
4423
|
+
if (names.length === 1) {
|
|
4424
|
+
question = "1 SSH host \u2014 pick it to manage it";
|
|
4425
|
+
} else if (names.length > 1) {
|
|
4426
|
+
question = `${names.length} SSH hosts \u2014 pick one to manage it`;
|
|
4427
|
+
}
|
|
4428
|
+
let defaultValue = "add";
|
|
4429
|
+
if (names.length > 0) {
|
|
4430
|
+
defaultValue = `${HOST_VALUE_PREFIX}${names[0]}`;
|
|
4431
|
+
}
|
|
4432
|
+
const picked = await prompter.select(question, items, { defaultValue, cancelValue: "quit" });
|
|
4433
|
+
if (picked === "quit") {
|
|
3956
4434
|
return 0;
|
|
3957
4435
|
}
|
|
3958
|
-
if (
|
|
3959
|
-
const
|
|
4436
|
+
if (picked === "switch") {
|
|
4437
|
+
const switched = await prompter.select(
|
|
3960
4438
|
"Which client?",
|
|
3961
4439
|
targets.map((candidate) => {
|
|
3962
|
-
let
|
|
3963
|
-
if (candidate.detected ==
|
|
3964
|
-
|
|
4440
|
+
let hint = candidate.location;
|
|
4441
|
+
if (candidate.detected == false) {
|
|
4442
|
+
hint += " \xB7 not found on this machine";
|
|
3965
4443
|
}
|
|
3966
|
-
return {
|
|
4444
|
+
return { value: candidate.id, label: candidate.label, hint };
|
|
3967
4445
|
}),
|
|
3968
|
-
target.id
|
|
4446
|
+
{ defaultValue: target.id, cancelValue: target.id }
|
|
3969
4447
|
);
|
|
3970
|
-
target = targets.find((candidate) => candidate.id ===
|
|
3971
|
-
continue;
|
|
3972
|
-
}
|
|
3973
|
-
if (readable == false) {
|
|
3974
|
-
output.write(" Not available while the file cannot be read.\n");
|
|
4448
|
+
target = targets.find((candidate) => candidate.id === switched) ?? target;
|
|
3975
4449
|
continue;
|
|
3976
4450
|
}
|
|
3977
4451
|
try {
|
|
3978
|
-
if (
|
|
4452
|
+
if (picked === "add") {
|
|
3979
4453
|
await addOrEditHost(prompter, output, target, entries);
|
|
3980
4454
|
continue;
|
|
3981
4455
|
}
|
|
3982
|
-
if (
|
|
3983
|
-
|
|
3984
|
-
if (name == null) {
|
|
3985
|
-
continue;
|
|
3986
|
-
}
|
|
3987
|
-
if (action === "e") {
|
|
3988
|
-
await addOrEditHost(prompter, output, target, entries, name);
|
|
3989
|
-
} else if (action === "r") {
|
|
3990
|
-
const remove = await prompter.askYesNo(`Remove ${name} from ${target.location}?`, false);
|
|
3991
|
-
if (remove == true) {
|
|
3992
|
-
reportWrite(output, target.removeEntry(name), "Removed");
|
|
3993
|
-
}
|
|
3994
|
-
} else if (action === "c") {
|
|
3995
|
-
await checkHost(prompter, output, target, name, entries[name]);
|
|
3996
|
-
} else {
|
|
3997
|
-
printHost(output, name, entries[name]);
|
|
3998
|
-
}
|
|
3999
|
-
continue;
|
|
4456
|
+
if (picked.startsWith(HOST_VALUE_PREFIX) == true) {
|
|
4457
|
+
await manageHost(prompter, output, target, entries, picked.slice(HOST_VALUE_PREFIX.length));
|
|
4000
4458
|
}
|
|
4001
|
-
output.write(` Unknown choice "${action}".
|
|
4002
|
-
`);
|
|
4003
4459
|
} catch (ex) {
|
|
4004
4460
|
if (ex instanceof PromptClosedError) {
|
|
4005
4461
|
throw ex;
|
|
@@ -4448,7 +4904,7 @@ ${file.content}`);
|
|
|
4448
4904
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4449
4905
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4450
4906
|
requirePermittedLocalPath(resolvedLocalPath, "read");
|
|
4451
|
-
if (
|
|
4907
|
+
if (fs6.existsSync(resolvedLocalPath) == false) {
|
|
4452
4908
|
return textResult(`local file not found: ${resolvedLocalPath}`);
|
|
4453
4909
|
}
|
|
4454
4910
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
@@ -4488,7 +4944,7 @@ ${file.content}`);
|
|
|
4488
4944
|
const { profile, session: openSession } = resolveProfile(context, { session });
|
|
4489
4945
|
const resolvedLocalPath = expandHomePath(localPath);
|
|
4490
4946
|
requirePermittedLocalPath(resolvedLocalPath, "write");
|
|
4491
|
-
|
|
4947
|
+
fs6.mkdirSync(path.dirname(resolvedLocalPath), { recursive: true });
|
|
4492
4948
|
const transferredBytes = await withHostConnection(profile, openSession, async (connection) => {
|
|
4493
4949
|
return await downloadFile(connection, remotePath, resolvedLocalPath);
|
|
4494
4950
|
});
|