@akms/mcp-ssh 0.1.2 → 0.1.3
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 +262 -262
- package/lib/cli.cjs +1209 -1085
- package/lib/cli.cjs.map +1 -1
- package/lib/cli.js +1209 -1085
- 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 +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/cli.cjs
CHANGED
|
@@ -35,7 +35,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
|
|
|
35
35
|
var MAX_DIRECTORY_ENTRIES = 1e3;
|
|
36
36
|
var SERVER_NAME = "akms-mcp-ssh";
|
|
37
37
|
var PACKAGE_NAME = "@akms/mcp-ssh";
|
|
38
|
-
var SERVER_VERSION = "0.1.
|
|
38
|
+
var SERVER_VERSION = "0.1.3";
|
|
39
39
|
|
|
40
40
|
// src/_defs/command-rules.ts
|
|
41
41
|
var CRITICAL_PATH_PATTERN = new RegExp(
|
|
@@ -2643,1208 +2643,1306 @@ function summarizeEntry(entry) {
|
|
|
2643
2643
|
notes: notes.join(", ")
|
|
2644
2644
|
};
|
|
2645
2645
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
var
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
var FIELD_NAMES = {
|
|
2654
|
-
"host": "Host",
|
|
2655
|
-
"port": "Port",
|
|
2656
|
-
"user": "User",
|
|
2657
|
-
"name": "Server name",
|
|
2658
|
-
"auth": "Authentication",
|
|
2659
|
-
"key": "Private key",
|
|
2660
|
-
"passphrase": "Passphrase",
|
|
2661
|
-
"password": "Password",
|
|
2662
|
-
"route": "Route",
|
|
2663
|
-
"jump-host": "Jump host",
|
|
2664
|
-
"jump-port": "Jump port",
|
|
2665
|
-
"jump-user": "Jump user",
|
|
2666
|
-
"proxy": "Proxy command",
|
|
2667
|
-
"description": "Description",
|
|
2668
|
-
"readonly": "Read-only",
|
|
2669
|
-
"paths": "File paths",
|
|
2670
|
-
"host-key": "Host key",
|
|
2671
|
-
"jump-host-key": "Jump host key"
|
|
2672
|
-
};
|
|
2673
|
-
var AUTH_LABELS = {
|
|
2674
|
-
key: "Private key file",
|
|
2675
|
-
password: "Password",
|
|
2676
|
-
agent: "SSH agent"
|
|
2677
|
-
};
|
|
2678
|
-
var ROUTE_LABELS = {
|
|
2679
|
-
"direct": "Directly",
|
|
2680
|
-
"jump": "Through a jump host",
|
|
2681
|
-
"proxy": "Through a proxy command",
|
|
2682
|
-
"jump-proxy": "Proxy command, then a jump host"
|
|
2646
|
+
|
|
2647
|
+
// src/modules/manage/prompter.ts
|
|
2648
|
+
var PromptClosedError = class extends Error {
|
|
2649
|
+
constructor() {
|
|
2650
|
+
super("input closed before the prompt was answered");
|
|
2651
|
+
this.name = "PromptClosedError";
|
|
2652
|
+
}
|
|
2683
2653
|
};
|
|
2684
|
-
var
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
try {
|
|
2689
|
-
names = fs6__default.default.readdirSync(sshDirectory);
|
|
2690
|
-
} catch {
|
|
2691
|
-
return [];
|
|
2654
|
+
var PromptBackError = class extends Error {
|
|
2655
|
+
constructor() {
|
|
2656
|
+
super("prompt left with esc");
|
|
2657
|
+
this.name = "PromptBackError";
|
|
2692
2658
|
}
|
|
2659
|
+
};
|
|
2660
|
+
var ESC = "\x1B";
|
|
2661
|
+
var KEY_UP = [`${ESC}[A`, `${ESC}OA`];
|
|
2662
|
+
var KEY_DOWN = [`${ESC}[B`, `${ESC}OB`];
|
|
2663
|
+
var KEY_RIGHT = [`${ESC}[C`, `${ESC}OC`];
|
|
2664
|
+
var KEY_LEFT = [`${ESC}[D`, `${ESC}OD`];
|
|
2665
|
+
var KEY_HOME = [`${ESC}[H`, `${ESC}OH`, `${ESC}[1~`, ""];
|
|
2666
|
+
var KEY_END = [`${ESC}[F`, `${ESC}OF`, `${ESC}[4~`, ""];
|
|
2667
|
+
var KEY_DELETE = [`${ESC}[3~`];
|
|
2668
|
+
var KEY_ENTER = ["\r", "\n"];
|
|
2669
|
+
var KEY_BACKSPACE = ["\x7F", "\b"];
|
|
2670
|
+
var KEY_CTRL_C = "";
|
|
2671
|
+
var KEY_CTRL_D = "";
|
|
2672
|
+
var KEY_CLEAR_LINE = "";
|
|
2673
|
+
function splitKeys(chunk) {
|
|
2693
2674
|
const keys = [];
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
const stat = fs6__default.default.statSync(fullPath);
|
|
2702
|
-
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
2703
|
-
continue;
|
|
2675
|
+
let index = 0;
|
|
2676
|
+
while (index < chunk.length) {
|
|
2677
|
+
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
2678
|
+
if (isSequence == true) {
|
|
2679
|
+
let end = index + 2;
|
|
2680
|
+
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
2681
|
+
end += 1;
|
|
2704
2682
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
2708
|
-
fs6__default.default.closeSync(descriptor);
|
|
2709
|
-
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
2710
|
-
} catch {
|
|
2711
|
-
continue;
|
|
2712
|
-
}
|
|
2713
|
-
const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
|
|
2714
|
-
const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
|
|
2715
|
-
if (isPemKey == false && isPuttyKey == false) {
|
|
2683
|
+
keys.push(chunk.slice(index, end + 1));
|
|
2684
|
+
index = end + 1;
|
|
2716
2685
|
continue;
|
|
2717
2686
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
if (publicKey.length > 2) {
|
|
2723
|
-
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
2724
|
-
}
|
|
2725
|
-
} catch {
|
|
2726
|
-
}
|
|
2727
|
-
keys.push({ path: `~/.ssh/${name}`, hint });
|
|
2687
|
+
const codePoint = chunk.codePointAt(index) ?? 0;
|
|
2688
|
+
const character = String.fromCodePoint(codePoint);
|
|
2689
|
+
keys.push(character);
|
|
2690
|
+
index += character.length;
|
|
2728
2691
|
}
|
|
2729
|
-
|
|
2730
|
-
const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
|
|
2731
|
-
if (index < 0) {
|
|
2732
|
-
return PREFERRED_KEY_NAMES.length;
|
|
2733
|
-
}
|
|
2734
|
-
return index;
|
|
2735
|
-
};
|
|
2736
|
-
return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
|
|
2692
|
+
return keys;
|
|
2737
2693
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
const parsed = Number(answer);
|
|
2742
|
-
if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
|
|
2743
|
-
return parsed;
|
|
2744
|
-
}
|
|
2745
|
-
prompter.write(" Enter a port number between 1 and 65535.\n");
|
|
2694
|
+
function isPrintableKey(key) {
|
|
2695
|
+
if (key.startsWith(ESC) == true) {
|
|
2696
|
+
return false;
|
|
2746
2697
|
}
|
|
2698
|
+
const codePoint = key.codePointAt(0) ?? 0;
|
|
2699
|
+
return codePoint >= 32 && codePoint !== 127;
|
|
2747
2700
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
return void 0;
|
|
2701
|
+
function charWidth(codePoint) {
|
|
2702
|
+
if (codePoint < 4352) {
|
|
2703
|
+
return 1;
|
|
2752
2704
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
let suffix = "(empty for none)";
|
|
2757
|
-
if (current != null) {
|
|
2758
|
-
suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
|
|
2705
|
+
const isWide = codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65072 && codePoint <= 65103 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141;
|
|
2706
|
+
if (isWide == true) {
|
|
2707
|
+
return 2;
|
|
2759
2708
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2709
|
+
return 1;
|
|
2710
|
+
}
|
|
2711
|
+
function displayWidth(text) {
|
|
2712
|
+
let width = 0;
|
|
2713
|
+
for (const character of text) {
|
|
2714
|
+
width += charWidth(character.codePointAt(0) ?? 0);
|
|
2763
2715
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2716
|
+
return width;
|
|
2717
|
+
}
|
|
2718
|
+
function padToWidth(text, width) {
|
|
2719
|
+
const missing = width - displayWidth(text);
|
|
2720
|
+
if (missing <= 0) {
|
|
2721
|
+
return text;
|
|
2766
2722
|
}
|
|
2767
|
-
return
|
|
2723
|
+
return `${text}${" ".repeat(missing)}`;
|
|
2768
2724
|
}
|
|
2769
|
-
|
|
2770
|
-
const
|
|
2771
|
-
let
|
|
2772
|
-
|
|
2773
|
-
const
|
|
2774
|
-
if (
|
|
2775
|
-
|
|
2776
|
-
}
|
|
2777
|
-
for (const key of found) {
|
|
2778
|
-
items.push({ value: key.path, label: key.path, hint: key.hint });
|
|
2725
|
+
function groupLabelWidths(items) {
|
|
2726
|
+
const widths = items.map(() => 0);
|
|
2727
|
+
let groupStart = 0;
|
|
2728
|
+
for (let index = 0; index <= items.length; index += 1) {
|
|
2729
|
+
const atBoundary = index === items.length || "value" in items[index] == false;
|
|
2730
|
+
if (atBoundary == false) {
|
|
2731
|
+
continue;
|
|
2779
2732
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
if (picked !== OTHER_KEY_PATH) {
|
|
2785
|
-
return picked;
|
|
2786
|
-
}
|
|
2787
|
-
for (; ; ) {
|
|
2788
|
-
const typed = await prompter.askRequired("Private key path");
|
|
2789
|
-
if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
|
|
2790
|
-
return typed;
|
|
2733
|
+
let groupWidth = 0;
|
|
2734
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2735
|
+
const item = items[member];
|
|
2736
|
+
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
2791
2737
|
}
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
2795
|
-
if (useAnyway == true) {
|
|
2796
|
-
return typed;
|
|
2738
|
+
for (let member = groupStart; member < index; member += 1) {
|
|
2739
|
+
widths[member] = groupWidth;
|
|
2797
2740
|
}
|
|
2741
|
+
groupStart = index + 1;
|
|
2798
2742
|
}
|
|
2743
|
+
return widths;
|
|
2799
2744
|
}
|
|
2800
|
-
function
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
function routeUsesProxy(route) {
|
|
2804
|
-
return route === "proxy" || route === "jump-proxy";
|
|
2805
|
-
}
|
|
2806
|
-
function deriveServerName(host) {
|
|
2807
|
-
return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
2808
|
-
}
|
|
2809
|
-
function finalizeAnswers(state) {
|
|
2810
|
-
const answers = { ...state.answers };
|
|
2811
|
-
if (routeUsesJump(state.route) == false) {
|
|
2812
|
-
answers.jumpHost = void 0;
|
|
2813
|
-
answers.jumpPort = void 0;
|
|
2814
|
-
answers.jumpUser = void 0;
|
|
2815
|
-
answers.jumpHostKeyFingerprint = void 0;
|
|
2816
|
-
}
|
|
2817
|
-
if (routeUsesProxy(state.route) == false) {
|
|
2818
|
-
answers.proxyCommand = void 0;
|
|
2819
|
-
}
|
|
2820
|
-
if (answers.auth !== "key") {
|
|
2821
|
-
answers.keyPath = void 0;
|
|
2822
|
-
answers.passphrase = void 0;
|
|
2823
|
-
}
|
|
2824
|
-
if (answers.auth !== "password") {
|
|
2825
|
-
answers.password = void 0;
|
|
2745
|
+
function supportsUnicode() {
|
|
2746
|
+
if (process.platform !== "win32") {
|
|
2747
|
+
return true;
|
|
2826
2748
|
}
|
|
2827
|
-
return
|
|
2749
|
+
return process.env.WT_SESSION != null || process.env.TERM_PROGRAM === "vscode" || process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm" || process.env.ConEmuTask != null;
|
|
2828
2750
|
}
|
|
2829
|
-
function
|
|
2830
|
-
|
|
2751
|
+
function createStyle(enabled) {
|
|
2752
|
+
const paint = (open, close) => {
|
|
2753
|
+
return (text) => {
|
|
2754
|
+
if (enabled == false || text === "") {
|
|
2755
|
+
return text;
|
|
2756
|
+
}
|
|
2757
|
+
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
2758
|
+
};
|
|
2759
|
+
};
|
|
2760
|
+
return {
|
|
2761
|
+
accent: paint(36, 39),
|
|
2762
|
+
success: paint(32, 39),
|
|
2763
|
+
dim: paint(2, 22),
|
|
2764
|
+
bold: paint(1, 22)
|
|
2765
|
+
};
|
|
2831
2766
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2767
|
+
var Prompter = class {
|
|
2768
|
+
constructor(input, output) {
|
|
2769
|
+
this.input = input;
|
|
2770
|
+
this.output = output;
|
|
2771
|
+
this.lineBuffer = "";
|
|
2772
|
+
this.pendingLines = [];
|
|
2773
|
+
this.lineWaiter = null;
|
|
2774
|
+
this.inputEnded = false;
|
|
2775
|
+
/** Set while a key-driven prompt owns the input. */
|
|
2776
|
+
this.keyHandler = null;
|
|
2777
|
+
/**
|
|
2778
|
+
* When true, an answered prompt leaves nothing behind on a TTY instead of its
|
|
2779
|
+
* `✔ question · answer` line. The settings form sets it: the form itself shows every
|
|
2780
|
+
* value, so it can redraw in place rather than scroll a trail of edits.
|
|
2781
|
+
*/
|
|
2782
|
+
this.transient = false;
|
|
2783
|
+
const ttyInput = input;
|
|
2784
|
+
const ttyOutput = output;
|
|
2785
|
+
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
2786
|
+
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
2787
|
+
if (supportsUnicode() == true) {
|
|
2788
|
+
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
2789
|
+
} else {
|
|
2790
|
+
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
2791
|
+
}
|
|
2792
|
+
input.setEncoding("utf8");
|
|
2793
|
+
input.on("data", (chunk) => {
|
|
2794
|
+
this.onData(chunk);
|
|
2795
|
+
});
|
|
2796
|
+
input.on("end", () => {
|
|
2797
|
+
this.inputEnded = true;
|
|
2798
|
+
if (this.lineWaiter != null) {
|
|
2799
|
+
const waiter = this.lineWaiter;
|
|
2800
|
+
this.lineWaiter = null;
|
|
2801
|
+
waiter(null);
|
|
2802
|
+
}
|
|
2803
|
+
if (this.keyHandler != null) {
|
|
2804
|
+
this.keyHandler(KEY_CTRL_D);
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2843
2807
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2808
|
+
/** Stops reading so the process can exit once the command is done. */
|
|
2809
|
+
close() {
|
|
2810
|
+
if (this.interactive == true) {
|
|
2811
|
+
this.input.setRawMode?.(false);
|
|
2812
|
+
this.output.write(`${ESC}[?25h`);
|
|
2813
|
+
}
|
|
2814
|
+
this.input.pause();
|
|
2846
2815
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2816
|
+
write(text) {
|
|
2817
|
+
this.output.write(text);
|
|
2849
2818
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2819
|
+
/** Styling for callers that print around the prompts; plain text off a TTY. */
|
|
2820
|
+
get paint() {
|
|
2821
|
+
return this.style;
|
|
2852
2822
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2823
|
+
/**
|
|
2824
|
+
* Free-text prompt. Enter accepts `defaultValue` when one is shown.
|
|
2825
|
+
*
|
|
2826
|
+
* @throws {PromptBackError} On Esc.
|
|
2827
|
+
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2828
|
+
*/
|
|
2829
|
+
async ask(question, defaultValue) {
|
|
2830
|
+
let suffix = "";
|
|
2831
|
+
if (defaultValue != null && defaultValue !== "") {
|
|
2832
|
+
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
2833
|
+
}
|
|
2834
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)}${suffix} `;
|
|
2835
|
+
const typed = await this.readText(prompt, false);
|
|
2836
|
+
let answer = typed.trim();
|
|
2837
|
+
if (answer === "" && defaultValue != null) {
|
|
2838
|
+
answer = defaultValue;
|
|
2839
|
+
}
|
|
2840
|
+
let shown = answer;
|
|
2841
|
+
if (shown === "") {
|
|
2842
|
+
shown = this.style.dim("(empty)");
|
|
2843
|
+
}
|
|
2844
|
+
this.writeAnswered(question, shown);
|
|
2845
|
+
return answer;
|
|
2858
2846
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2847
|
+
/** Like `ask`, re-asking until something non-empty is entered. */
|
|
2848
|
+
async askRequired(question, defaultValue) {
|
|
2849
|
+
for (; ; ) {
|
|
2850
|
+
const answer = await this.ask(question, defaultValue);
|
|
2851
|
+
if (answer !== "") {
|
|
2852
|
+
return answer;
|
|
2853
|
+
}
|
|
2854
|
+
this.output.write(` ${this.style.dim("A value is required.")}
|
|
2855
|
+
`);
|
|
2856
|
+
}
|
|
2864
2857
|
}
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2858
|
+
/**
|
|
2859
|
+
* @throws {PromptBackError} On Esc — the caller decides what "neither" means.
|
|
2860
|
+
* @throws {PromptClosedError} If the input closes before a line arrives.
|
|
2861
|
+
*/
|
|
2862
|
+
async askYesNo(question, defaultValue) {
|
|
2863
|
+
let hint = "y/N";
|
|
2864
|
+
if (defaultValue == true) {
|
|
2865
|
+
hint = "Y/n";
|
|
2866
|
+
}
|
|
2867
|
+
for (; ; ) {
|
|
2868
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} ${this.style.dim(`(${hint})`)} `;
|
|
2869
|
+
const typed = await this.readText(prompt, false);
|
|
2870
|
+
const normalized = typed.trim().toLowerCase();
|
|
2871
|
+
let answer = null;
|
|
2872
|
+
if (normalized === "") {
|
|
2873
|
+
answer = defaultValue;
|
|
2874
|
+
} else if (normalized === "y" || normalized === "yes") {
|
|
2875
|
+
answer = true;
|
|
2876
|
+
} else if (normalized === "n" || normalized === "no") {
|
|
2877
|
+
answer = false;
|
|
2878
|
+
}
|
|
2879
|
+
if (answer != null) {
|
|
2880
|
+
let shown = "no";
|
|
2881
|
+
if (answer == true) {
|
|
2882
|
+
shown = "yes";
|
|
2883
|
+
}
|
|
2884
|
+
this.writeAnswered(question, shown);
|
|
2885
|
+
return answer;
|
|
2886
|
+
}
|
|
2887
|
+
this.output.write(` ${this.style.dim("Answer y or n.")}
|
|
2888
|
+
`);
|
|
2889
|
+
}
|
|
2871
2890
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2891
|
+
/**
|
|
2892
|
+
* Picks one of `items`. On a TTY: ↑/↓ move, 1–9 jump, Enter picks, Esc goes back. Off a
|
|
2893
|
+
* TTY: a numbered list, answered by number, key or label.
|
|
2894
|
+
*
|
|
2895
|
+
* @throws {PromptBackError} On Esc without a `cancelValue`.
|
|
2896
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2897
|
+
*/
|
|
2898
|
+
async select(question, items, options) {
|
|
2899
|
+
const choices = items.filter((item) => "value" in item);
|
|
2900
|
+
if (choices.length === 0) {
|
|
2901
|
+
throw new Error("select needs at least one choice");
|
|
2902
|
+
}
|
|
2903
|
+
let startIndex = items.findIndex((item) => "value" in item && item.value === options?.defaultValue);
|
|
2904
|
+
if (startIndex < 0) {
|
|
2905
|
+
startIndex = items.findIndex((item) => "value" in item);
|
|
2906
|
+
}
|
|
2907
|
+
let picked;
|
|
2908
|
+
if (this.interactive == true) {
|
|
2909
|
+
picked = await this.selectWithKeys(question, items, startIndex);
|
|
2910
|
+
} else {
|
|
2911
|
+
picked = await this.selectWithLines(question, items, startIndex);
|
|
2912
|
+
}
|
|
2913
|
+
if (picked != null) {
|
|
2914
|
+
return picked;
|
|
2915
|
+
}
|
|
2916
|
+
if (options?.cancelValue != null) {
|
|
2917
|
+
return options.cancelValue;
|
|
2918
|
+
}
|
|
2919
|
+
throw new PromptBackError();
|
|
2889
2920
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2921
|
+
/**
|
|
2922
|
+
* Reads a value without echoing it on a TTY. Empty means "none", or "keep" when the
|
|
2923
|
+
* caller says a value already exists.
|
|
2924
|
+
*
|
|
2925
|
+
* @throws {PromptBackError} On Esc.
|
|
2926
|
+
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
2927
|
+
*/
|
|
2928
|
+
async askSecret(question) {
|
|
2929
|
+
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
2930
|
+
const secret = await this.readText(prompt, true);
|
|
2931
|
+
let shown = this.style.dim("(empty)");
|
|
2932
|
+
if (secret !== "") {
|
|
2933
|
+
shown = "\u2022".repeat(Math.min(secret.length, 8));
|
|
2934
|
+
}
|
|
2935
|
+
this.writeAnswered(question, shown);
|
|
2936
|
+
return secret;
|
|
2892
2937
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2938
|
+
// ── input ──────────────────────────────────────────────────────────────
|
|
2939
|
+
/**
|
|
2940
|
+
* One text answer, through the line editor on a TTY and a line read off one. The prompt
|
|
2941
|
+
* is erased once answered; the caller writes the summary line.
|
|
2942
|
+
*
|
|
2943
|
+
* @throws {PromptBackError} On Esc, or a line of just `ESC` off a TTY.
|
|
2944
|
+
*/
|
|
2945
|
+
async readText(prompt, secret) {
|
|
2946
|
+
if (this.interactive == false) {
|
|
2947
|
+
this.output.write(prompt);
|
|
2948
|
+
const line = await this.readLine();
|
|
2949
|
+
if (line == null) {
|
|
2950
|
+
throw new PromptClosedError();
|
|
2951
|
+
}
|
|
2952
|
+
if (line === ESC) {
|
|
2953
|
+
throw new PromptBackError();
|
|
2954
|
+
}
|
|
2955
|
+
return line;
|
|
2900
2956
|
}
|
|
2901
|
-
|
|
2957
|
+
const result = await this.editLine(prompt, secret);
|
|
2958
|
+
if (result.kind === "back") {
|
|
2959
|
+
throw new PromptBackError();
|
|
2960
|
+
}
|
|
2961
|
+
return result.text;
|
|
2902
2962
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
if (
|
|
2906
|
-
|
|
2963
|
+
readLine() {
|
|
2964
|
+
const queued = this.pendingLines.shift();
|
|
2965
|
+
if (queued != null) {
|
|
2966
|
+
return Promise.resolve(queued);
|
|
2907
2967
|
}
|
|
2908
|
-
|
|
2968
|
+
if (this.inputEnded == true) {
|
|
2969
|
+
return Promise.resolve(null);
|
|
2970
|
+
}
|
|
2971
|
+
return new Promise((resolve) => {
|
|
2972
|
+
this.lineWaiter = resolve;
|
|
2973
|
+
});
|
|
2909
2974
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
if (loginUser === "") {
|
|
2918
|
-
loginUser = "\u2026";
|
|
2975
|
+
onData(chunk) {
|
|
2976
|
+
if (this.keyHandler != null) {
|
|
2977
|
+
for (const key of splitKeys(chunk)) {
|
|
2978
|
+
if (this.keyHandler == null) {
|
|
2979
|
+
break;
|
|
2980
|
+
}
|
|
2981
|
+
this.keyHandler(key);
|
|
2919
2982
|
}
|
|
2920
|
-
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
this.lineBuffer += chunk;
|
|
2986
|
+
let newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2987
|
+
while (newlineIndex >= 0) {
|
|
2988
|
+
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
2989
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
2990
|
+
if (this.lineWaiter != null) {
|
|
2991
|
+
const waiter = this.lineWaiter;
|
|
2992
|
+
this.lineWaiter = null;
|
|
2993
|
+
waiter(line);
|
|
2994
|
+
} else {
|
|
2995
|
+
this.pendingLines.push(line);
|
|
2996
|
+
}
|
|
2997
|
+
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
2921
2998
|
}
|
|
2922
|
-
rows.push(fieldRow("jump-user", jumpUser));
|
|
2923
2999
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
3000
|
+
/**
|
|
3001
|
+
* A one-line editor in raw mode: insert at the cursor, Backspace / Delete, ←/→, Home /
|
|
3002
|
+
* End, Ctrl+U clears, Enter answers, Esc goes back. Secrets draw nothing after the
|
|
3003
|
+
* prompt. Cooked mode is not used for text on a TTY because it never reports Esc: the
|
|
3004
|
+
* terminal swallows it or stuffs the `ESC` byte into the value.
|
|
3005
|
+
*/
|
|
3006
|
+
editLine(prompt, secret) {
|
|
3007
|
+
const ttyInput = this.input;
|
|
3008
|
+
const plainPrompt = prompt.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3009
|
+
let characters = [];
|
|
3010
|
+
let cursor = 0;
|
|
3011
|
+
const draw = () => {
|
|
3012
|
+
let text = `\r${ESC}[2K${prompt}`;
|
|
3013
|
+
if (secret == false) {
|
|
3014
|
+
text += characters.join("");
|
|
3015
|
+
const column = displayWidth(plainPrompt) + displayWidth(characters.slice(0, cursor).join(""));
|
|
3016
|
+
text += `\r${ESC}[${column}C`;
|
|
3017
|
+
}
|
|
3018
|
+
this.output.write(text);
|
|
3019
|
+
};
|
|
3020
|
+
return new Promise((resolve, reject) => {
|
|
3021
|
+
const finish = () => {
|
|
3022
|
+
this.keyHandler = null;
|
|
3023
|
+
ttyInput.setRawMode?.(false);
|
|
3024
|
+
this.output.write(`\r${ESC}[2K`);
|
|
3025
|
+
};
|
|
3026
|
+
this.keyHandler = (key) => {
|
|
3027
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3028
|
+
finish();
|
|
3029
|
+
reject(new PromptClosedError());
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3033
|
+
finish();
|
|
3034
|
+
resolve({ kind: "entered", text: characters.join("") });
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
if (key === ESC) {
|
|
3038
|
+
finish();
|
|
3039
|
+
resolve({ kind: "back" });
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (KEY_BACKSPACE.includes(key) == true) {
|
|
3043
|
+
if (cursor > 0) {
|
|
3044
|
+
characters.splice(cursor - 1, 1);
|
|
3045
|
+
cursor -= 1;
|
|
3046
|
+
}
|
|
3047
|
+
} else if (KEY_DELETE.includes(key) == true) {
|
|
3048
|
+
characters.splice(cursor, 1);
|
|
3049
|
+
} else if (KEY_LEFT.includes(key) == true) {
|
|
3050
|
+
cursor = Math.max(0, cursor - 1);
|
|
3051
|
+
} else if (KEY_RIGHT.includes(key) == true) {
|
|
3052
|
+
cursor = Math.min(characters.length, cursor + 1);
|
|
3053
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3054
|
+
cursor = 0;
|
|
3055
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3056
|
+
cursor = characters.length;
|
|
3057
|
+
} else if (key === KEY_CLEAR_LINE) {
|
|
3058
|
+
characters = [];
|
|
3059
|
+
cursor = 0;
|
|
3060
|
+
} else if (isPrintableKey(key) == true) {
|
|
3061
|
+
characters.splice(cursor, 0, key);
|
|
3062
|
+
cursor += 1;
|
|
3063
|
+
} else {
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
draw();
|
|
3067
|
+
};
|
|
3068
|
+
ttyInput.setRawMode?.(true);
|
|
3069
|
+
draw();
|
|
3070
|
+
});
|
|
2926
3071
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3072
|
+
async selectWithLines(question, items, startIndex) {
|
|
3073
|
+
const choices = items.filter((item) => "value" in item);
|
|
3074
|
+
const labelWidths = groupLabelWidths(items);
|
|
3075
|
+
this.output.write(`${this.symbols.question} ${question}
|
|
3076
|
+
`);
|
|
3077
|
+
let number = 0;
|
|
3078
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3079
|
+
const item = items[index];
|
|
3080
|
+
if ("value" in item == false) {
|
|
3081
|
+
this.output.write("\n");
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
number += 1;
|
|
3085
|
+
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
3086
|
+
if (item.hint != null && item.hint !== "") {
|
|
3087
|
+
line += ` ${item.hint}`;
|
|
3088
|
+
}
|
|
3089
|
+
this.output.write(`${line.trimEnd()}
|
|
3090
|
+
`);
|
|
3091
|
+
}
|
|
3092
|
+
const defaultChoice = items[startIndex];
|
|
3093
|
+
const defaultNumber = choices.indexOf(defaultChoice) + 1;
|
|
3094
|
+
for (; ; ) {
|
|
3095
|
+
this.output.write(`Choice (${defaultNumber}): `);
|
|
3096
|
+
const line = await this.readLine();
|
|
3097
|
+
if (line == null) {
|
|
3098
|
+
throw new PromptClosedError();
|
|
3099
|
+
}
|
|
3100
|
+
if (line === ESC) {
|
|
3101
|
+
return null;
|
|
3102
|
+
}
|
|
3103
|
+
const answer = line.trim();
|
|
3104
|
+
if (answer === "") {
|
|
3105
|
+
return defaultChoice.value;
|
|
3106
|
+
}
|
|
3107
|
+
const index = Number(answer);
|
|
3108
|
+
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
3109
|
+
return choices[index - 1].value;
|
|
3110
|
+
}
|
|
3111
|
+
const lowered = answer.toLowerCase();
|
|
3112
|
+
const matched = choices.find((choice) => {
|
|
3113
|
+
const key = choice.key ?? choice.value;
|
|
3114
|
+
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
3115
|
+
});
|
|
3116
|
+
if (matched != null) {
|
|
3117
|
+
return matched.value;
|
|
3118
|
+
}
|
|
3119
|
+
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
3120
|
+
`);
|
|
3121
|
+
}
|
|
2931
3122
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3123
|
+
// ── TTY rendering ──────────────────────────────────────────────────────
|
|
3124
|
+
columns() {
|
|
3125
|
+
const columns = this.output.columns;
|
|
3126
|
+
if (columns == null || columns < 20) {
|
|
3127
|
+
return 80;
|
|
3128
|
+
}
|
|
3129
|
+
return columns;
|
|
2936
3130
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3131
|
+
/** Joins styled parts, cutting the text (not the styles) once `width` columns are used. */
|
|
3132
|
+
fitLine(parts, width) {
|
|
3133
|
+
let used = 0;
|
|
3134
|
+
let line = "";
|
|
3135
|
+
for (const part of parts) {
|
|
3136
|
+
let text = "";
|
|
3137
|
+
for (const character of part.text) {
|
|
3138
|
+
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
3139
|
+
if (used + characterWidth > width) {
|
|
3140
|
+
break;
|
|
3141
|
+
}
|
|
3142
|
+
used += characterWidth;
|
|
3143
|
+
text += character;
|
|
3144
|
+
}
|
|
3145
|
+
if (part.paint != null) {
|
|
3146
|
+
line += part.paint(text);
|
|
3147
|
+
} else {
|
|
3148
|
+
line += text;
|
|
3149
|
+
}
|
|
3150
|
+
if (used >= width) {
|
|
3151
|
+
break;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return line;
|
|
2941
3155
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3156
|
+
/** The one-line record an answered prompt leaves behind on a TTY. */
|
|
3157
|
+
writeAnswered(question, answer) {
|
|
3158
|
+
if (this.interactive == false || this.transient == true) {
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
this.output.write(`${this.style.success(this.symbols.tick)} ${question} ${this.style.dim("\xB7")} ${this.style.accent(answer)}
|
|
3162
|
+
`);
|
|
2948
3163
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3164
|
+
/** @returns The picked value, or null on Esc. */
|
|
3165
|
+
selectWithKeys(question, items, startIndex) {
|
|
3166
|
+
const ttyInput = this.input;
|
|
3167
|
+
const selectableIndexes = items.flatMap((item, index) => {
|
|
3168
|
+
if ("value" in item) {
|
|
3169
|
+
return [index];
|
|
3170
|
+
}
|
|
3171
|
+
return [];
|
|
3172
|
+
});
|
|
3173
|
+
const labelWidths = groupLabelWidths(items);
|
|
3174
|
+
const ruleWidth = Math.min(Math.max(...labelWidths), 24);
|
|
3175
|
+
let active = startIndex;
|
|
3176
|
+
let renderedLines = 0;
|
|
3177
|
+
const draw = (lines) => {
|
|
3178
|
+
let text = "";
|
|
3179
|
+
if (renderedLines > 1) {
|
|
3180
|
+
text += `${ESC}[${renderedLines - 1}A`;
|
|
3181
|
+
}
|
|
3182
|
+
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
3183
|
+
this.output.write(text);
|
|
3184
|
+
renderedLines = lines.length;
|
|
3185
|
+
};
|
|
3186
|
+
const buildLines = () => {
|
|
3187
|
+
const width = this.columns() - 1;
|
|
3188
|
+
const lines = [
|
|
3189
|
+
this.fitLine([
|
|
3190
|
+
{ text: this.symbols.question, paint: this.style.accent },
|
|
3191
|
+
{ text: " " },
|
|
3192
|
+
{ text: question, paint: this.style.bold }
|
|
3193
|
+
], width)
|
|
3194
|
+
];
|
|
3195
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
3196
|
+
const item = items[index];
|
|
3197
|
+
if ("value" in item == false) {
|
|
3198
|
+
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
const isActive = index === active;
|
|
3202
|
+
const parts = [];
|
|
3203
|
+
if (isActive == true) {
|
|
3204
|
+
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
3205
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
3206
|
+
} else {
|
|
3207
|
+
parts.push({ text: " " });
|
|
3208
|
+
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
3209
|
+
}
|
|
3210
|
+
if (item.hint != null && item.hint !== "") {
|
|
3211
|
+
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
3212
|
+
}
|
|
3213
|
+
lines.push(this.fitLine(parts, width));
|
|
3214
|
+
}
|
|
3215
|
+
lines.push(this.fitLine([{ text: "\u2191\u2193 move \xB7 enter select \xB7 esc back", paint: this.style.dim }], width));
|
|
3216
|
+
return lines;
|
|
3217
|
+
};
|
|
3218
|
+
const move = (step) => {
|
|
3219
|
+
const position = selectableIndexes.indexOf(active);
|
|
3220
|
+
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
3221
|
+
active = selectableIndexes[next];
|
|
3222
|
+
};
|
|
3223
|
+
return new Promise((resolve, reject) => {
|
|
3224
|
+
const finish = () => {
|
|
3225
|
+
this.keyHandler = null;
|
|
3226
|
+
ttyInput.setRawMode?.(false);
|
|
3227
|
+
draw([]);
|
|
3228
|
+
this.output.write(`${ESC}[?25h`);
|
|
3229
|
+
};
|
|
3230
|
+
this.keyHandler = (key) => {
|
|
3231
|
+
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3232
|
+
finish();
|
|
3233
|
+
reject(new PromptClosedError());
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
if (KEY_ENTER.includes(key) == true) {
|
|
3237
|
+
const picked = items[active];
|
|
3238
|
+
finish();
|
|
3239
|
+
this.writeAnswered(question, picked.label);
|
|
3240
|
+
resolve(picked.value);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (key === ESC) {
|
|
3244
|
+
finish();
|
|
3245
|
+
resolve(null);
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
if (KEY_UP.includes(key) == true) {
|
|
3249
|
+
move(-1);
|
|
3250
|
+
} else if (KEY_DOWN.includes(key) == true) {
|
|
3251
|
+
move(1);
|
|
3252
|
+
} else if (KEY_HOME.includes(key) == true) {
|
|
3253
|
+
active = selectableIndexes[0];
|
|
3254
|
+
} else if (KEY_END.includes(key) == true) {
|
|
3255
|
+
active = selectableIndexes[selectableIndexes.length - 1];
|
|
3256
|
+
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
3257
|
+
active = selectableIndexes[Number(key) - 1];
|
|
3258
|
+
} else {
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
draw(buildLines());
|
|
3262
|
+
};
|
|
3263
|
+
ttyInput.setRawMode?.(true);
|
|
3264
|
+
this.output.write(`${ESC}[?25l`);
|
|
3265
|
+
draw(buildLines());
|
|
3266
|
+
});
|
|
2957
3267
|
}
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
// src/modules/manage/host-form.ts
|
|
3271
|
+
var PREFERRED_KEY_NAMES = ["id_ed25519", "id_ecdsa", "id_rsa"];
|
|
3272
|
+
var NON_KEY_FILE_NAMES = ["config", "known_hosts", "known_hosts.old", "authorized_keys", "environment"];
|
|
3273
|
+
var MAX_KEY_FILE_BYTES = 64 * 1024;
|
|
3274
|
+
var CLEAR_ANSWER = "-";
|
|
3275
|
+
var OTHER_KEY_PATH = "\0other";
|
|
3276
|
+
var FIELD_NAME_WIDTH = 17;
|
|
3277
|
+
var SUB_FIELDS = ["key", "passphrase", "password", "jump-host", "jump-port", "jump-user", "proxy"];
|
|
3278
|
+
var FIELD_NAMES = {
|
|
3279
|
+
"host": "Host",
|
|
3280
|
+
"port": "Port",
|
|
3281
|
+
"user": "User",
|
|
3282
|
+
"name": "Server name",
|
|
3283
|
+
"auth": "Authentication",
|
|
3284
|
+
"key": "Private key",
|
|
3285
|
+
"passphrase": "Passphrase",
|
|
3286
|
+
"password": "Password",
|
|
3287
|
+
"route": "Route",
|
|
3288
|
+
"jump-host": "Jump host",
|
|
3289
|
+
"jump-port": "Jump port",
|
|
3290
|
+
"jump-user": "Jump user",
|
|
3291
|
+
"proxy": "Proxy command",
|
|
3292
|
+
"description": "Description",
|
|
3293
|
+
"readonly": "Read-only",
|
|
3294
|
+
"paths": "File paths",
|
|
3295
|
+
"host-key": "Host key",
|
|
3296
|
+
"jump-host-key": "Jump host key"
|
|
3297
|
+
};
|
|
3298
|
+
var AUTH_LABELS = {
|
|
3299
|
+
key: "Private key file",
|
|
3300
|
+
password: "Password",
|
|
3301
|
+
agent: "SSH agent"
|
|
3302
|
+
};
|
|
3303
|
+
var ROUTE_LABELS = {
|
|
3304
|
+
"direct": "Directly",
|
|
3305
|
+
"jump": "Through a jump host",
|
|
3306
|
+
"proxy": "Through a proxy command",
|
|
3307
|
+
"jump-proxy": "Proxy command, then a jump host"
|
|
3308
|
+
};
|
|
3309
|
+
var REQUIRED_MARK = "(required)";
|
|
3310
|
+
function findPrivateKeys() {
|
|
3311
|
+
const sshDirectory = path__default.default.join(os5__default.default.homedir(), ".ssh");
|
|
3312
|
+
let names = [];
|
|
3313
|
+
try {
|
|
3314
|
+
names = fs6__default.default.readdirSync(sshDirectory);
|
|
3315
|
+
} catch {
|
|
2962
3316
|
return [];
|
|
2963
|
-
});
|
|
2964
|
-
let index = values.indexOf(edited) + 1;
|
|
2965
|
-
if (SUB_FIELDS.includes(edited) == false) {
|
|
2966
|
-
while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
|
|
2967
|
-
index += 1;
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
const next = values[index];
|
|
2971
|
-
if (next == null || next === "test") {
|
|
2972
|
-
return "save";
|
|
2973
3317
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
const result = await runConnectionCheck(profile, output);
|
|
2986
|
-
output.write("\n");
|
|
2987
|
-
if (result.connected == true) {
|
|
2988
|
-
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
2989
|
-
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
2990
|
-
if (pin == true) {
|
|
2991
|
-
state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
|
|
3318
|
+
const keys = [];
|
|
3319
|
+
for (const name of names) {
|
|
3320
|
+
if (name.endsWith(".pub") == true || NON_KEY_FILE_NAMES.includes(name) == true) {
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
const fullPath = path__default.default.join(sshDirectory, name);
|
|
3324
|
+
let head = "";
|
|
3325
|
+
try {
|
|
3326
|
+
const stat = fs6__default.default.statSync(fullPath);
|
|
3327
|
+
if (stat.isFile() == false || stat.size > MAX_KEY_FILE_BYTES) {
|
|
3328
|
+
continue;
|
|
2992
3329
|
}
|
|
3330
|
+
const descriptor = fs6__default.default.openSync(fullPath, "r");
|
|
3331
|
+
const buffer = Buffer.alloc(64);
|
|
3332
|
+
const bytesRead = fs6__default.default.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
3333
|
+
fs6__default.default.closeSync(descriptor);
|
|
3334
|
+
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
3335
|
+
} catch {
|
|
3336
|
+
continue;
|
|
2993
3337
|
}
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3338
|
+
const isPemKey = head.startsWith("-----BEGIN") == true && head.includes("PRIVATE KEY") == true;
|
|
3339
|
+
const isPuttyKey = head.startsWith("PuTTY-User-Key-File") == true;
|
|
3340
|
+
if (isPemKey == false && isPuttyKey == false) {
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
let hint = "";
|
|
3344
|
+
try {
|
|
3345
|
+
const publicKey = fs6__default.default.readFileSync(`${fullPath}.pub`, "utf8").trim().split(/\s+/);
|
|
3346
|
+
hint = publicKey[0].replace(/^ssh-/, "");
|
|
3347
|
+
if (publicKey.length > 2) {
|
|
3348
|
+
hint += ` \xB7 ${publicKey.slice(2).join(" ")}`;
|
|
2998
3349
|
}
|
|
3350
|
+
} catch {
|
|
2999
3351
|
}
|
|
3000
|
-
|
|
3001
|
-
state.testedSnapshot = snapshotOf(state);
|
|
3002
|
-
return true;
|
|
3352
|
+
keys.push({ path: `~/.ssh/${name}`, hint });
|
|
3003
3353
|
}
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
if (repin == true) {
|
|
3009
|
-
state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
|
|
3010
|
-
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3354
|
+
const rank = (key) => {
|
|
3355
|
+
const index = PREFERRED_KEY_NAMES.indexOf(path__default.default.basename(key.path));
|
|
3356
|
+
if (index < 0) {
|
|
3357
|
+
return PREFERRED_KEY_NAMES.length;
|
|
3011
3358
|
}
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3359
|
+
return index;
|
|
3360
|
+
};
|
|
3361
|
+
return keys.sort((left, right) => rank(left) - rank(right) || left.path.localeCompare(right.path));
|
|
3362
|
+
}
|
|
3363
|
+
async function askPort(prompter, question, current) {
|
|
3364
|
+
for (; ; ) {
|
|
3365
|
+
const answer = await prompter.ask(question, String(current));
|
|
3366
|
+
const parsed = Number(answer);
|
|
3367
|
+
if (Number.isInteger(parsed) == true && parsed > 0 && parsed <= 65535) {
|
|
3368
|
+
return parsed;
|
|
3020
3369
|
}
|
|
3370
|
+
prompter.write(" Enter a port number between 1 and 65535.\n");
|
|
3021
3371
|
}
|
|
3022
|
-
state.lastTest = "failed";
|
|
3023
|
-
return false;
|
|
3024
3372
|
}
|
|
3025
|
-
async function
|
|
3026
|
-
const
|
|
3027
|
-
if (
|
|
3028
|
-
|
|
3029
|
-
if (answers.host !== "") {
|
|
3030
|
-
current = answers.host;
|
|
3031
|
-
}
|
|
3032
|
-
answers.host = await prompter.askRequired("Host (hostname or IP)", current);
|
|
3033
|
-
if (state.nameTouched == false) {
|
|
3034
|
-
answers.name = deriveServerName(answers.host);
|
|
3035
|
-
}
|
|
3036
|
-
return;
|
|
3373
|
+
async function askOptional(prompter, question, current) {
|
|
3374
|
+
const answer = await prompter.ask(question, current);
|
|
3375
|
+
if (answer === "" || answer === CLEAR_ANSWER) {
|
|
3376
|
+
return void 0;
|
|
3037
3377
|
}
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3378
|
+
return answer;
|
|
3379
|
+
}
|
|
3380
|
+
async function askOptionalSecret(prompter, question, current) {
|
|
3381
|
+
let suffix = "(empty for none)";
|
|
3382
|
+
if (current != null) {
|
|
3383
|
+
suffix = `(enter keeps current, ${CLEAR_ANSWER} clears)`;
|
|
3041
3384
|
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
current = answers.username;
|
|
3046
|
-
}
|
|
3047
|
-
answers.username = await prompter.askRequired("Login user", current);
|
|
3048
|
-
return;
|
|
3385
|
+
const answer = await prompter.askSecret(`${question} ${prompter.paint.dim(suffix)}`);
|
|
3386
|
+
if (answer === CLEAR_ANSWER) {
|
|
3387
|
+
return void 0;
|
|
3049
3388
|
}
|
|
3050
|
-
if (
|
|
3051
|
-
|
|
3052
|
-
if (answers.name !== "") {
|
|
3053
|
-
current = answers.name;
|
|
3054
|
-
}
|
|
3055
|
-
for (; ; ) {
|
|
3056
|
-
const name = await prompter.askRequired("Server name the agent sees", current);
|
|
3057
|
-
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
3058
|
-
answers.name = name;
|
|
3059
|
-
state.nameTouched = true;
|
|
3060
|
-
return;
|
|
3061
|
-
}
|
|
3062
|
-
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
3063
|
-
}
|
|
3389
|
+
if (answer === "") {
|
|
3390
|
+
return current;
|
|
3064
3391
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
{
|
|
3074
|
-
);
|
|
3075
|
-
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3076
|
-
answers.keyPath = await askKeyPath(prompter, void 0);
|
|
3392
|
+
return answer;
|
|
3393
|
+
}
|
|
3394
|
+
async function askKeyPath(prompter, current) {
|
|
3395
|
+
const found = findPrivateKeys();
|
|
3396
|
+
let picked = OTHER_KEY_PATH;
|
|
3397
|
+
if (found.length > 0 || current != null) {
|
|
3398
|
+
const items = [];
|
|
3399
|
+
if (current != null && found.some((key) => key.path === current) == false) {
|
|
3400
|
+
items.push({ value: current, label: current, hint: "current" });
|
|
3077
3401
|
}
|
|
3078
|
-
|
|
3079
|
-
|
|
3402
|
+
for (const key of found) {
|
|
3403
|
+
items.push({ value: key.path, label: key.path, hint: key.hint });
|
|
3080
3404
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
answers.keyPath = await askKeyPath(prompter, answers.keyPath);
|
|
3085
|
-
return;
|
|
3086
|
-
}
|
|
3087
|
-
if (id === "passphrase") {
|
|
3088
|
-
answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
|
|
3089
|
-
return;
|
|
3405
|
+
items.push({ separator: true });
|
|
3406
|
+
items.push({ value: OTHER_KEY_PATH, key: "other", label: "Another path\u2026" });
|
|
3407
|
+
picked = await prompter.select("Private key", items, { defaultValue: current ?? found[0]?.path });
|
|
3090
3408
|
}
|
|
3091
|
-
if (
|
|
3092
|
-
|
|
3093
|
-
return;
|
|
3409
|
+
if (picked !== OTHER_KEY_PATH) {
|
|
3410
|
+
return picked;
|
|
3094
3411
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
{ value: "direct", label: ROUTE_LABELS.direct },
|
|
3100
|
-
{ value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
|
|
3101
|
-
{ value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
|
|
3102
|
-
{ value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
|
|
3103
|
-
],
|
|
3104
|
-
{ defaultValue: state.route }
|
|
3105
|
-
);
|
|
3106
|
-
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3107
|
-
answers.jumpHost = await prompter.askRequired("Jump host");
|
|
3412
|
+
for (; ; ) {
|
|
3413
|
+
const typed = await prompter.askRequired("Private key path");
|
|
3414
|
+
if (fs6__default.default.existsSync(expandHomePath(typed)) == true) {
|
|
3415
|
+
return typed;
|
|
3108
3416
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3417
|
+
prompter.write(` ${expandHomePath(typed)} does not exist.
|
|
3418
|
+
`);
|
|
3419
|
+
const useAnyway = await prompter.askYesNo("Use this path anyway?", false);
|
|
3420
|
+
if (useAnyway == true) {
|
|
3421
|
+
return typed;
|
|
3111
3422
|
}
|
|
3112
|
-
return;
|
|
3113
3423
|
}
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3424
|
+
}
|
|
3425
|
+
function routeUsesJump(route) {
|
|
3426
|
+
return route === "jump" || route === "jump-proxy";
|
|
3427
|
+
}
|
|
3428
|
+
function routeUsesProxy(route) {
|
|
3429
|
+
return route === "proxy" || route === "jump-proxy";
|
|
3430
|
+
}
|
|
3431
|
+
function deriveServerName(host) {
|
|
3432
|
+
return `ssh-${host}`.replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 48);
|
|
3433
|
+
}
|
|
3434
|
+
function finalizeAnswers(state) {
|
|
3435
|
+
const answers = { ...state.answers };
|
|
3436
|
+
if (routeUsesJump(state.route) == false) {
|
|
3437
|
+
answers.jumpHost = void 0;
|
|
3438
|
+
answers.jumpPort = void 0;
|
|
3439
|
+
answers.jumpUser = void 0;
|
|
3440
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3117
3441
|
}
|
|
3118
|
-
if (
|
|
3119
|
-
answers.
|
|
3120
|
-
return;
|
|
3442
|
+
if (routeUsesProxy(state.route) == false) {
|
|
3443
|
+
answers.proxyCommand = void 0;
|
|
3121
3444
|
}
|
|
3122
|
-
if (
|
|
3123
|
-
answers.
|
|
3124
|
-
|
|
3445
|
+
if (answers.auth !== "key") {
|
|
3446
|
+
answers.keyPath = void 0;
|
|
3447
|
+
answers.passphrase = void 0;
|
|
3125
3448
|
}
|
|
3126
|
-
if (
|
|
3127
|
-
|
|
3128
|
-
answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3129
|
-
return;
|
|
3449
|
+
if (answers.auth !== "password") {
|
|
3450
|
+
answers.password = void 0;
|
|
3130
3451
|
}
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3452
|
+
return answers;
|
|
3453
|
+
}
|
|
3454
|
+
function snapshotOf(state) {
|
|
3455
|
+
return JSON.stringify(finalizeAnswers(state));
|
|
3456
|
+
}
|
|
3457
|
+
function cloneState(state) {
|
|
3458
|
+
return {
|
|
3459
|
+
...state,
|
|
3460
|
+
answers: {
|
|
3461
|
+
...state.answers,
|
|
3462
|
+
allowedPaths: [...state.answers.allowedPaths],
|
|
3463
|
+
extraEnv: { ...state.answers.extraEnv }
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
function restoreState(state, snapshot) {
|
|
3468
|
+
const restored = cloneState(snapshot);
|
|
3469
|
+
state.answers = restored.answers;
|
|
3470
|
+
state.route = restored.route;
|
|
3471
|
+
state.nameTouched = restored.nameTouched;
|
|
3472
|
+
state.lastTest = restored.lastTest;
|
|
3473
|
+
state.testedSnapshot = restored.testedSnapshot;
|
|
3474
|
+
}
|
|
3475
|
+
function missingFields(state) {
|
|
3476
|
+
const { answers } = state;
|
|
3477
|
+
const missing = [];
|
|
3478
|
+
if (answers.host === "") {
|
|
3479
|
+
missing.push("host");
|
|
3134
3480
|
}
|
|
3135
|
-
if (
|
|
3136
|
-
|
|
3137
|
-
return;
|
|
3481
|
+
if (answers.username === "") {
|
|
3482
|
+
missing.push("user");
|
|
3138
3483
|
}
|
|
3139
|
-
if (
|
|
3140
|
-
|
|
3141
|
-
if (answers.allowedPaths.length > 0) {
|
|
3142
|
-
current = answers.allowedPaths.join(",");
|
|
3143
|
-
}
|
|
3144
|
-
const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
|
|
3145
|
-
answers.allowedPaths = [];
|
|
3146
|
-
if (typed != null) {
|
|
3147
|
-
answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3148
|
-
}
|
|
3149
|
-
return;
|
|
3484
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3485
|
+
missing.push("key");
|
|
3150
3486
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
if (isJump == true) {
|
|
3154
|
-
pinned = answers.jumpHostKeyFingerprint;
|
|
3487
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3488
|
+
missing.push("password");
|
|
3155
3489
|
}
|
|
3156
|
-
if (
|
|
3157
|
-
|
|
3158
|
-
if (unpin == true && isJump == true) {
|
|
3159
|
-
answers.jumpHostKeyFingerprint = void 0;
|
|
3160
|
-
} else if (unpin == true) {
|
|
3161
|
-
answers.hostKeyFingerprint = void 0;
|
|
3162
|
-
}
|
|
3163
|
-
return;
|
|
3490
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3491
|
+
missing.push("jump-host");
|
|
3164
3492
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
|
|
3168
|
-
`);
|
|
3169
|
-
return;
|
|
3493
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3494
|
+
missing.push("proxy");
|
|
3170
3495
|
}
|
|
3171
|
-
|
|
3496
|
+
return missing;
|
|
3172
3497
|
}
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
if (existing != null) {
|
|
3177
|
-
let route = "direct";
|
|
3178
|
-
if (existing.jumpHost != null && existing.proxyCommand != null) {
|
|
3179
|
-
route = "jump-proxy";
|
|
3180
|
-
} else if (existing.jumpHost != null) {
|
|
3181
|
-
route = "jump";
|
|
3182
|
-
} else if (existing.proxyCommand != null) {
|
|
3183
|
-
route = "proxy";
|
|
3184
|
-
}
|
|
3185
|
-
state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
|
|
3186
|
-
} else {
|
|
3187
|
-
const foundKeys = findPrivateKeys();
|
|
3188
|
-
state = {
|
|
3189
|
-
answers: {
|
|
3190
|
-
name: "",
|
|
3191
|
-
host: "",
|
|
3192
|
-
port: DEFAULT_SSH_PORT,
|
|
3193
|
-
username: "",
|
|
3194
|
-
auth: "key",
|
|
3195
|
-
keyPath: foundKeys[0]?.path,
|
|
3196
|
-
readonly: false,
|
|
3197
|
-
allowedPaths: [],
|
|
3198
|
-
extraEnv: {}
|
|
3199
|
-
},
|
|
3200
|
-
route: "direct",
|
|
3201
|
-
nameTouched: false,
|
|
3202
|
-
lastTest: "never",
|
|
3203
|
-
testedSnapshot: null
|
|
3204
|
-
};
|
|
3498
|
+
function describePin(fingerprint) {
|
|
3499
|
+
if (fingerprint == null) {
|
|
3500
|
+
return "not pinned";
|
|
3205
3501
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
if (
|
|
3210
|
-
|
|
3502
|
+
return `pinned \xB7 ${fingerprint.slice(0, 19)}\u2026`;
|
|
3503
|
+
}
|
|
3504
|
+
function valueOrRequired(value) {
|
|
3505
|
+
if (value == null || value === "") {
|
|
3506
|
+
return REQUIRED_MARK;
|
|
3211
3507
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
if (missing.length > 0) {
|
|
3248
|
-
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3249
|
-
`);
|
|
3250
|
-
cursor = missing[0];
|
|
3251
|
-
continue;
|
|
3252
|
-
}
|
|
3253
|
-
const name = state.answers.name;
|
|
3254
|
-
if (name !== originalName && takenNames.includes(name) == true) {
|
|
3255
|
-
const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
|
|
3256
|
-
if (replace == false) {
|
|
3257
|
-
cursor = "name";
|
|
3258
|
-
continue;
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
if (snapshotOf(state) !== state.testedSnapshot) {
|
|
3262
|
-
const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
|
|
3263
|
-
if (testFirst == true) {
|
|
3264
|
-
const connected = await testConnection(prompter, output, state);
|
|
3265
|
-
if (connected == false) {
|
|
3266
|
-
const next = await prompter.select(
|
|
3267
|
-
"The check failed. What now?",
|
|
3268
|
-
[
|
|
3269
|
-
{ value: "back", key: "back", label: "Back to the settings" },
|
|
3270
|
-
{ value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
|
|
3271
|
-
{ value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
|
|
3272
|
-
],
|
|
3273
|
-
{ defaultValue: "back", cancelValue: "back" }
|
|
3274
|
-
);
|
|
3275
|
-
if (next === "discard") {
|
|
3276
|
-
return null;
|
|
3277
|
-
}
|
|
3278
|
-
if (next === "back") {
|
|
3279
|
-
cursor = "test";
|
|
3280
|
-
continue;
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
return finalizeAnswers(state);
|
|
3286
|
-
}
|
|
3287
|
-
await editField(prompter, output, state, picked);
|
|
3288
|
-
cursor = nextCursor(state, destination, picked);
|
|
3508
|
+
return value;
|
|
3509
|
+
}
|
|
3510
|
+
function fieldRow(id, value, hint) {
|
|
3511
|
+
let name = FIELD_NAMES[id];
|
|
3512
|
+
if (SUB_FIELDS.includes(id) == true) {
|
|
3513
|
+
name = ` ${name}`;
|
|
3514
|
+
}
|
|
3515
|
+
return {
|
|
3516
|
+
value: id,
|
|
3517
|
+
key: id,
|
|
3518
|
+
label: `${name.padEnd(FIELD_NAME_WIDTH)}${value}`,
|
|
3519
|
+
hint
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
function buildRows(state, destination) {
|
|
3523
|
+
const { answers } = state;
|
|
3524
|
+
const rows = [];
|
|
3525
|
+
rows.push(fieldRow("host", valueOrRequired(answers.host)));
|
|
3526
|
+
rows.push(fieldRow("port", String(answers.port)));
|
|
3527
|
+
rows.push(fieldRow("user", valueOrRequired(answers.username)));
|
|
3528
|
+
let nameValue = answers.name;
|
|
3529
|
+
let nameHint = "what the agent sees";
|
|
3530
|
+
if (state.nameTouched == false) {
|
|
3531
|
+
nameHint = "follows the host until you change it";
|
|
3532
|
+
}
|
|
3533
|
+
if (nameValue === "") {
|
|
3534
|
+
nameValue = "ssh-<host>";
|
|
3535
|
+
}
|
|
3536
|
+
rows.push(fieldRow("name", nameValue, nameHint));
|
|
3537
|
+
rows.push(fieldRow("auth", AUTH_LABELS[answers.auth]));
|
|
3538
|
+
if (answers.auth === "key") {
|
|
3539
|
+
rows.push(fieldRow("key", valueOrRequired(answers.keyPath)));
|
|
3540
|
+
let passphrase = "none";
|
|
3541
|
+
if (answers.passphrase != null) {
|
|
3542
|
+
passphrase = "set";
|
|
3289
3543
|
}
|
|
3290
|
-
|
|
3291
|
-
prompter.transient = previousTransient;
|
|
3544
|
+
rows.push(fieldRow("passphrase", passphrase));
|
|
3292
3545
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
this.name = "PromptClosedError";
|
|
3546
|
+
if (answers.auth === "password") {
|
|
3547
|
+
let password = REQUIRED_MARK;
|
|
3548
|
+
if (answers.password != null) {
|
|
3549
|
+
password = "set";
|
|
3550
|
+
}
|
|
3551
|
+
rows.push(fieldRow("password", password, "stored in the config file as plain text"));
|
|
3300
3552
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
var KEY_CTRL_D = "";
|
|
3311
|
-
function splitKeys(chunk) {
|
|
3312
|
-
const keys = [];
|
|
3313
|
-
let index = 0;
|
|
3314
|
-
while (index < chunk.length) {
|
|
3315
|
-
const isSequence = chunk[index] === ESC && (chunk[index + 1] === "[" || chunk[index + 1] === "O");
|
|
3316
|
-
if (isSequence == true) {
|
|
3317
|
-
let end = index + 2;
|
|
3318
|
-
while (end < chunk.length && /[0-9;]/.test(chunk[end]) == true) {
|
|
3319
|
-
end += 1;
|
|
3553
|
+
rows.push(fieldRow("route", ROUTE_LABELS[state.route]));
|
|
3554
|
+
if (routeUsesJump(state.route) == true) {
|
|
3555
|
+
rows.push(fieldRow("jump-host", valueOrRequired(answers.jumpHost)));
|
|
3556
|
+
rows.push(fieldRow("jump-port", String(answers.jumpPort ?? DEFAULT_SSH_PORT)));
|
|
3557
|
+
let jumpUser = answers.jumpUser;
|
|
3558
|
+
if (jumpUser == null || jumpUser === answers.username) {
|
|
3559
|
+
let loginUser = answers.username;
|
|
3560
|
+
if (loginUser === "") {
|
|
3561
|
+
loginUser = "\u2026";
|
|
3320
3562
|
}
|
|
3321
|
-
|
|
3322
|
-
index = end + 1;
|
|
3323
|
-
continue;
|
|
3563
|
+
jumpUser = `same as login (${loginUser})`;
|
|
3324
3564
|
}
|
|
3325
|
-
|
|
3326
|
-
const character = String.fromCodePoint(codePoint);
|
|
3327
|
-
keys.push(character);
|
|
3328
|
-
index += character.length;
|
|
3565
|
+
rows.push(fieldRow("jump-user", jumpUser));
|
|
3329
3566
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
function charWidth(codePoint) {
|
|
3333
|
-
if (codePoint < 4352) {
|
|
3334
|
-
return 1;
|
|
3567
|
+
if (routeUsesProxy(state.route) == true) {
|
|
3568
|
+
rows.push(fieldRow("proxy", valueOrRequired(answers.proxyCommand), "%h %p %r \u2192 first hop"));
|
|
3335
3569
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3570
|
+
rows.push(fieldRow("description", answers.description ?? "none", "shown to the agent"));
|
|
3571
|
+
let readonly = "no";
|
|
3572
|
+
if (answers.readonly == true) {
|
|
3573
|
+
readonly = "yes";
|
|
3339
3574
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
for (const character of text) {
|
|
3345
|
-
width += charWidth(character.codePointAt(0) ?? 0);
|
|
3575
|
+
rows.push(fieldRow("readonly", readonly, "enter toggles"));
|
|
3576
|
+
let paths = "anywhere";
|
|
3577
|
+
if (answers.allowedPaths.length > 0) {
|
|
3578
|
+
paths = answers.allowedPaths.join(", ");
|
|
3346
3579
|
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
if (missing <= 0) {
|
|
3352
|
-
return text;
|
|
3580
|
+
rows.push(fieldRow("paths", paths, "where the file tools may read and write"));
|
|
3581
|
+
rows.push(fieldRow("host-key", describePin(answers.hostKeyFingerprint)));
|
|
3582
|
+
if (routeUsesJump(state.route) == true) {
|
|
3583
|
+
rows.push(fieldRow("jump-host-key", describePin(answers.jumpHostKeyFingerprint)));
|
|
3353
3584
|
}
|
|
3354
|
-
|
|
3585
|
+
rows.push({ separator: true });
|
|
3586
|
+
let testHint = "not run yet";
|
|
3587
|
+
if (state.lastTest === "ok") {
|
|
3588
|
+
testHint = "last run: OK";
|
|
3589
|
+
} else if (state.lastTest === "failed") {
|
|
3590
|
+
testHint = "last run: failed";
|
|
3591
|
+
}
|
|
3592
|
+
rows.push({ value: "test", key: "test", label: "Test the connection", hint: testHint });
|
|
3593
|
+
rows.push({ value: "save", key: "save", label: "Save", hint: `to ${destination}` });
|
|
3594
|
+
rows.push({ value: "cancel", key: "cancel", label: "Cancel" });
|
|
3595
|
+
return rows;
|
|
3355
3596
|
}
|
|
3356
|
-
function
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
if (
|
|
3362
|
-
|
|
3363
|
-
}
|
|
3364
|
-
let groupWidth = 0;
|
|
3365
|
-
for (let member = groupStart; member < index; member += 1) {
|
|
3366
|
-
const item = items[member];
|
|
3367
|
-
groupWidth = Math.max(groupWidth, displayWidth(item.label));
|
|
3597
|
+
function nextCursor(state, destination, edited) {
|
|
3598
|
+
if (edited === "readonly") {
|
|
3599
|
+
return edited;
|
|
3600
|
+
}
|
|
3601
|
+
const values = buildRows(state, destination).flatMap((row) => {
|
|
3602
|
+
if ("value" in row) {
|
|
3603
|
+
return [row.value];
|
|
3368
3604
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3605
|
+
return [];
|
|
3606
|
+
});
|
|
3607
|
+
let index = values.indexOf(edited) + 1;
|
|
3608
|
+
if (SUB_FIELDS.includes(edited) == false) {
|
|
3609
|
+
while (index < values.length && SUB_FIELDS.includes(values[index]) == true) {
|
|
3610
|
+
index += 1;
|
|
3371
3611
|
}
|
|
3372
|
-
groupStart = index + 1;
|
|
3373
3612
|
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
if (process.platform !== "win32") {
|
|
3378
|
-
return true;
|
|
3613
|
+
const next = values[index];
|
|
3614
|
+
if (next == null || next === "test") {
|
|
3615
|
+
return "save";
|
|
3379
3616
|
}
|
|
3380
|
-
return
|
|
3617
|
+
return next;
|
|
3381
3618
|
}
|
|
3382
|
-
function
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3619
|
+
async function testConnection(prompter, output, state) {
|
|
3620
|
+
const answers = finalizeAnswers(state);
|
|
3621
|
+
output.write("\n");
|
|
3622
|
+
const profile = profileFromEnv(output, answersToEntry(answers).env);
|
|
3623
|
+
if (profile == null) {
|
|
3624
|
+
output.write("\n");
|
|
3625
|
+
state.lastTest = "failed";
|
|
3626
|
+
return false;
|
|
3627
|
+
}
|
|
3628
|
+
const result = await runConnectionCheck(profile, output);
|
|
3629
|
+
output.write("\n");
|
|
3630
|
+
if (result.connected == true) {
|
|
3631
|
+
if (result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint == null) {
|
|
3632
|
+
const pin = await prompter.askYesNo("Pin this host key? (a changed key then aborts the connection)", true);
|
|
3633
|
+
if (pin == true) {
|
|
3634
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint;
|
|
3387
3635
|
}
|
|
3388
|
-
return `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
3389
|
-
};
|
|
3390
|
-
};
|
|
3391
|
-
return {
|
|
3392
|
-
accent: paint(36, 39),
|
|
3393
|
-
success: paint(32, 39),
|
|
3394
|
-
dim: paint(2, 22),
|
|
3395
|
-
bold: paint(1, 22)
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
var Prompter = class {
|
|
3399
|
-
constructor(input, output) {
|
|
3400
|
-
this.input = input;
|
|
3401
|
-
this.output = output;
|
|
3402
|
-
this.lineBuffer = "";
|
|
3403
|
-
this.pendingLines = [];
|
|
3404
|
-
this.lineWaiter = null;
|
|
3405
|
-
this.inputEnded = false;
|
|
3406
|
-
/** Set while a key-driven prompt (select, secret) owns the input. */
|
|
3407
|
-
this.keyHandler = null;
|
|
3408
|
-
/**
|
|
3409
|
-
* When true, an answered prompt leaves nothing behind on a TTY instead of its
|
|
3410
|
-
* `✔ question · answer` line. The settings form sets it: the form itself shows every
|
|
3411
|
-
* value, so it can redraw in place rather than scroll a trail of edits.
|
|
3412
|
-
*/
|
|
3413
|
-
this.transient = false;
|
|
3414
|
-
const ttyInput = input;
|
|
3415
|
-
const ttyOutput = output;
|
|
3416
|
-
this.interactive = ttyInput.isTTY == true && ttyInput.setRawMode != null && ttyOutput.isTTY == true;
|
|
3417
|
-
this.style = createStyle(this.interactive == true && process.env.NO_COLOR == null);
|
|
3418
|
-
if (supportsUnicode() == true) {
|
|
3419
|
-
this.symbols = { question: "?", pointer: "\u276F", tick: "\u2714", rule: "\u2500" };
|
|
3420
|
-
} else {
|
|
3421
|
-
this.symbols = { question: "?", pointer: ">", tick: "\u221A", rule: "-" };
|
|
3422
3636
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
input.on("end", () => {
|
|
3428
|
-
this.inputEnded = true;
|
|
3429
|
-
if (this.lineWaiter != null) {
|
|
3430
|
-
const waiter = this.lineWaiter;
|
|
3431
|
-
this.lineWaiter = null;
|
|
3432
|
-
waiter(null);
|
|
3433
|
-
}
|
|
3434
|
-
if (this.keyHandler != null) {
|
|
3435
|
-
this.keyHandler(KEY_CTRL_D);
|
|
3637
|
+
if (result.jumpHostKeyFingerprint != null && routeUsesJump(state.route) == true && state.answers.jumpHostKeyFingerprint == null) {
|
|
3638
|
+
const pin = await prompter.askYesNo("Pin the jump host key too?", true);
|
|
3639
|
+
if (pin == true) {
|
|
3640
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint;
|
|
3436
3641
|
}
|
|
3437
|
-
});
|
|
3438
|
-
}
|
|
3439
|
-
/** Stops reading so the process can exit once the command is done. */
|
|
3440
|
-
close() {
|
|
3441
|
-
if (this.interactive == true) {
|
|
3442
|
-
this.input.setRawMode?.(false);
|
|
3443
|
-
this.output.write(`${ESC}[?25h`);
|
|
3444
3642
|
}
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
this.output.write(text);
|
|
3643
|
+
state.lastTest = "ok";
|
|
3644
|
+
state.testedSnapshot = snapshotOf(state);
|
|
3645
|
+
return true;
|
|
3449
3646
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3647
|
+
const hostKeyChanged = result.hostKeyFingerprint != null && state.answers.hostKeyFingerprint != null && result.hostKeyFingerprint !== state.answers.hostKeyFingerprint;
|
|
3648
|
+
if (hostKeyChanged == true) {
|
|
3649
|
+
output.write(" The host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3650
|
+
const repin = await prompter.askYesNo(`Pin ${result.hostKeyFingerprint} instead?`, false);
|
|
3651
|
+
if (repin == true) {
|
|
3652
|
+
state.answers.hostKeyFingerprint = result.hostKeyFingerprint ?? void 0;
|
|
3653
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3654
|
+
}
|
|
3453
3655
|
}
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
if (defaultValue != null && defaultValue !== "") {
|
|
3462
|
-
suffix = ` ${this.style.dim(`(${defaultValue})`)}`;
|
|
3656
|
+
const jumpKeyChanged = result.jumpHostKeyFingerprint != null && state.answers.jumpHostKeyFingerprint != null && result.jumpHostKeyFingerprint !== state.answers.jumpHostKeyFingerprint;
|
|
3657
|
+
if (jumpKeyChanged == true) {
|
|
3658
|
+
output.write(" The jump host presented a different key than the pinned one. Re-pin only if you know it was rebuilt.\n");
|
|
3659
|
+
const repin = await prompter.askYesNo(`Pin ${result.jumpHostKeyFingerprint} instead?`, false);
|
|
3660
|
+
if (repin == true) {
|
|
3661
|
+
state.answers.jumpHostKeyFingerprint = result.jumpHostKeyFingerprint ?? void 0;
|
|
3662
|
+
output.write(" Pinned the new key \u2014 test again to confirm.\n");
|
|
3463
3663
|
}
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3664
|
+
}
|
|
3665
|
+
state.lastTest = "failed";
|
|
3666
|
+
return false;
|
|
3667
|
+
}
|
|
3668
|
+
async function editField(prompter, output, state, id) {
|
|
3669
|
+
const { answers } = state;
|
|
3670
|
+
if (id === "host") {
|
|
3671
|
+
let current;
|
|
3672
|
+
if (answers.host !== "") {
|
|
3673
|
+
current = answers.host;
|
|
3469
3674
|
}
|
|
3470
|
-
|
|
3471
|
-
if (
|
|
3472
|
-
|
|
3675
|
+
answers.host = await prompter.askRequired("Host (hostname or IP)", current);
|
|
3676
|
+
if (state.nameTouched == false) {
|
|
3677
|
+
answers.name = deriveServerName(answers.host);
|
|
3473
3678
|
}
|
|
3474
|
-
|
|
3475
|
-
return answer;
|
|
3679
|
+
return;
|
|
3476
3680
|
}
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
`);
|
|
3681
|
+
if (id === "port") {
|
|
3682
|
+
answers.port = await askPort(prompter, "Port", answers.port);
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
if (id === "user") {
|
|
3686
|
+
let current;
|
|
3687
|
+
if (answers.username !== "") {
|
|
3688
|
+
current = answers.username;
|
|
3486
3689
|
}
|
|
3690
|
+
answers.username = await prompter.askRequired("Login user", current);
|
|
3691
|
+
return;
|
|
3487
3692
|
}
|
|
3488
|
-
|
|
3489
|
-
let
|
|
3490
|
-
if (
|
|
3491
|
-
|
|
3693
|
+
if (id === "name") {
|
|
3694
|
+
let current;
|
|
3695
|
+
if (answers.name !== "") {
|
|
3696
|
+
current = answers.name;
|
|
3492
3697
|
}
|
|
3493
3698
|
for (; ; ) {
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
}
|
|
3500
|
-
const normalized = line.trim().toLowerCase();
|
|
3501
|
-
let answer = null;
|
|
3502
|
-
if (normalized === "") {
|
|
3503
|
-
answer = defaultValue;
|
|
3504
|
-
} else if (normalized === "y" || normalized === "yes") {
|
|
3505
|
-
answer = true;
|
|
3506
|
-
} else if (normalized === "n" || normalized === "no") {
|
|
3507
|
-
answer = false;
|
|
3508
|
-
}
|
|
3509
|
-
if (answer != null) {
|
|
3510
|
-
let shown = "no";
|
|
3511
|
-
if (answer == true) {
|
|
3512
|
-
shown = "yes";
|
|
3513
|
-
}
|
|
3514
|
-
this.collapseAnswered(prompt, line, question, shown);
|
|
3515
|
-
return answer;
|
|
3699
|
+
const name = await prompter.askRequired("Server name the agent sees", current);
|
|
3700
|
+
if (SERVER_NAME_PATTERN.test(name) == true) {
|
|
3701
|
+
answers.name = name;
|
|
3702
|
+
state.nameTouched = true;
|
|
3703
|
+
return;
|
|
3516
3704
|
}
|
|
3517
|
-
|
|
3518
|
-
`);
|
|
3705
|
+
prompter.write(" Use letters, digits, '-' and '_' only (max 48) \u2014 the name becomes part of every tool name.\n");
|
|
3519
3706
|
}
|
|
3520
3707
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
if (startIndex < 0) {
|
|
3534
|
-
startIndex = items.findIndex((item) => "value" in item);
|
|
3708
|
+
if (id === "auth") {
|
|
3709
|
+
answers.auth = await prompter.select(
|
|
3710
|
+
"Authentication",
|
|
3711
|
+
[
|
|
3712
|
+
{ value: "key", label: AUTH_LABELS.key, hint: "only the path is stored \u2014 recommended" },
|
|
3713
|
+
{ value: "password", label: AUTH_LABELS.password, hint: "stored in the config file as plain text" },
|
|
3714
|
+
{ value: "agent", label: AUTH_LABELS.agent, hint: "keys already loaded in ssh-agent" }
|
|
3715
|
+
],
|
|
3716
|
+
{ defaultValue: answers.auth }
|
|
3717
|
+
);
|
|
3718
|
+
if (answers.auth === "key" && answers.keyPath == null) {
|
|
3719
|
+
answers.keyPath = await askKeyPath(prompter, void 0);
|
|
3535
3720
|
}
|
|
3536
|
-
if (
|
|
3537
|
-
|
|
3538
|
-
return picked2;
|
|
3721
|
+
if (answers.auth === "password" && answers.password == null) {
|
|
3722
|
+
answers.password = await askOptionalSecret(prompter, "Password", void 0);
|
|
3539
3723
|
}
|
|
3540
|
-
|
|
3541
|
-
return picked;
|
|
3724
|
+
return;
|
|
3542
3725
|
}
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
*
|
|
3547
|
-
* @throws {PromptClosedError} On Ctrl+C or when the input closes.
|
|
3548
|
-
*/
|
|
3549
|
-
async askSecret(question) {
|
|
3550
|
-
if (this.interactive == false) {
|
|
3551
|
-
const answer = await this.ask(question);
|
|
3552
|
-
return answer;
|
|
3553
|
-
}
|
|
3554
|
-
const prompt = `${this.style.accent(this.symbols.question)} ${this.style.bold(question)} `;
|
|
3555
|
-
this.output.write(prompt);
|
|
3556
|
-
const ttyInput = this.input;
|
|
3557
|
-
return new Promise((resolve, reject) => {
|
|
3558
|
-
let buffer = "";
|
|
3559
|
-
const finish = () => {
|
|
3560
|
-
this.keyHandler = null;
|
|
3561
|
-
ttyInput.setRawMode?.(false);
|
|
3562
|
-
this.output.write(`\r${ESC}[2K`);
|
|
3563
|
-
};
|
|
3564
|
-
this.keyHandler = (key) => {
|
|
3565
|
-
if (key === KEY_CTRL_C || key === KEY_CTRL_D) {
|
|
3566
|
-
finish();
|
|
3567
|
-
reject(new PromptClosedError());
|
|
3568
|
-
return;
|
|
3569
|
-
}
|
|
3570
|
-
if (KEY_ENTER.includes(key) == true) {
|
|
3571
|
-
finish();
|
|
3572
|
-
let shown = this.style.dim("(empty)");
|
|
3573
|
-
if (buffer !== "") {
|
|
3574
|
-
shown = "\u2022".repeat(Math.min(buffer.length, 8));
|
|
3575
|
-
}
|
|
3576
|
-
this.writeAnswered(question, shown);
|
|
3577
|
-
resolve(buffer);
|
|
3578
|
-
return;
|
|
3579
|
-
}
|
|
3580
|
-
if (KEY_BACKSPACE.includes(key) == true) {
|
|
3581
|
-
buffer = Array.from(buffer).slice(0, -1).join("");
|
|
3582
|
-
return;
|
|
3583
|
-
}
|
|
3584
|
-
if (key.startsWith(ESC) == true) {
|
|
3585
|
-
return;
|
|
3586
|
-
}
|
|
3587
|
-
buffer += key;
|
|
3588
|
-
};
|
|
3589
|
-
ttyInput.setRawMode?.(true);
|
|
3590
|
-
});
|
|
3726
|
+
if (id === "key") {
|
|
3727
|
+
answers.keyPath = await askKeyPath(prompter, answers.keyPath);
|
|
3728
|
+
return;
|
|
3591
3729
|
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
if (queued != null) {
|
|
3596
|
-
return Promise.resolve(queued);
|
|
3597
|
-
}
|
|
3598
|
-
if (this.inputEnded == true) {
|
|
3599
|
-
return Promise.resolve(null);
|
|
3600
|
-
}
|
|
3601
|
-
return new Promise((resolve) => {
|
|
3602
|
-
this.lineWaiter = resolve;
|
|
3603
|
-
});
|
|
3730
|
+
if (id === "passphrase") {
|
|
3731
|
+
answers.passphrase = await askOptionalSecret(prompter, "Key passphrase", answers.passphrase);
|
|
3732
|
+
return;
|
|
3604
3733
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
if (this.keyHandler == null) {
|
|
3609
|
-
break;
|
|
3610
|
-
}
|
|
3611
|
-
this.keyHandler(key);
|
|
3612
|
-
}
|
|
3613
|
-
return;
|
|
3614
|
-
}
|
|
3615
|
-
this.lineBuffer += chunk;
|
|
3616
|
-
let newlineIndex = this.lineBuffer.indexOf("\n");
|
|
3617
|
-
while (newlineIndex >= 0) {
|
|
3618
|
-
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
3619
|
-
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
3620
|
-
if (this.lineWaiter != null) {
|
|
3621
|
-
const waiter = this.lineWaiter;
|
|
3622
|
-
this.lineWaiter = null;
|
|
3623
|
-
waiter(line);
|
|
3624
|
-
} else {
|
|
3625
|
-
this.pendingLines.push(line);
|
|
3626
|
-
}
|
|
3627
|
-
newlineIndex = this.lineBuffer.indexOf("\n");
|
|
3628
|
-
}
|
|
3734
|
+
if (id === "password") {
|
|
3735
|
+
answers.password = await askOptionalSecret(prompter, "Password", answers.password);
|
|
3736
|
+
return;
|
|
3629
3737
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
let line = ` ${String(number).padStart(2)}) ${padToWidth(item.label, labelWidths[index])}`;
|
|
3644
|
-
if (item.hint != null && item.hint !== "") {
|
|
3645
|
-
line += ` ${item.hint}`;
|
|
3646
|
-
}
|
|
3647
|
-
this.output.write(`${line.trimEnd()}
|
|
3648
|
-
`);
|
|
3738
|
+
if (id === "route") {
|
|
3739
|
+
state.route = await prompter.select(
|
|
3740
|
+
"Route",
|
|
3741
|
+
[
|
|
3742
|
+
{ value: "direct", label: ROUTE_LABELS.direct },
|
|
3743
|
+
{ value: "jump", label: ROUTE_LABELS.jump, hint: "ssh -J" },
|
|
3744
|
+
{ value: "proxy", label: ROUTE_LABELS.proxy, hint: "e.g. cloudflared access ssh" },
|
|
3745
|
+
{ value: "jump-proxy", label: ROUTE_LABELS["jump-proxy"], hint: "cloudflared to the bastion, then on to the host" }
|
|
3746
|
+
],
|
|
3747
|
+
{ defaultValue: state.route }
|
|
3748
|
+
);
|
|
3749
|
+
if (routeUsesJump(state.route) == true && answers.jumpHost == null) {
|
|
3750
|
+
answers.jumpHost = await prompter.askRequired("Jump host");
|
|
3649
3751
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
for (; ; ) {
|
|
3653
|
-
this.output.write(`Choice (${defaultNumber}): `);
|
|
3654
|
-
const line = await this.readLine();
|
|
3655
|
-
if (line == null) {
|
|
3656
|
-
throw new PromptClosedError();
|
|
3657
|
-
}
|
|
3658
|
-
const answer = line.trim();
|
|
3659
|
-
if (answer === "") {
|
|
3660
|
-
return defaultChoice.value;
|
|
3661
|
-
}
|
|
3662
|
-
const index = Number(answer);
|
|
3663
|
-
if (Number.isInteger(index) == true && index >= 1 && index <= choices.length) {
|
|
3664
|
-
return choices[index - 1].value;
|
|
3665
|
-
}
|
|
3666
|
-
const lowered = answer.toLowerCase();
|
|
3667
|
-
const matched = choices.find((choice) => {
|
|
3668
|
-
const key = choice.key ?? choice.value;
|
|
3669
|
-
return key.toLowerCase() === lowered || choice.label.toLowerCase() === lowered;
|
|
3670
|
-
});
|
|
3671
|
-
if (matched != null) {
|
|
3672
|
-
return matched.value;
|
|
3673
|
-
}
|
|
3674
|
-
this.output.write(` Pick a number from 1 to ${choices.length}.
|
|
3675
|
-
`);
|
|
3752
|
+
if (routeUsesProxy(state.route) == true && answers.proxyCommand == null) {
|
|
3753
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", "cloudflared access ssh --hostname %h");
|
|
3676
3754
|
}
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
if (id === "jump-host") {
|
|
3758
|
+
answers.jumpHost = await prompter.askRequired("Jump host", answers.jumpHost);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
if (id === "jump-port") {
|
|
3762
|
+
answers.jumpPort = await askPort(prompter, "Jump host port", answers.jumpPort ?? DEFAULT_SSH_PORT);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (id === "jump-user") {
|
|
3766
|
+
answers.jumpUser = await askOptional(prompter, "Jump host user (empty = same as the login user)", answers.jumpUser);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (id === "proxy") {
|
|
3770
|
+
output.write(prompter.paint.dim(" %h / %p / %r expand to the first hop's host, port and user.\n"));
|
|
3771
|
+
answers.proxyCommand = await prompter.askRequired("Proxy command", answers.proxyCommand ?? "cloudflared access ssh --hostname %h");
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
if (id === "description") {
|
|
3775
|
+
answers.description = await askOptional(prompter, "Description shown to the agent", answers.description);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
if (id === "readonly") {
|
|
3779
|
+
answers.readonly = answers.readonly == false;
|
|
3780
|
+
return;
|
|
3677
3781
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
return 80;
|
|
3782
|
+
if (id === "paths") {
|
|
3783
|
+
let current;
|
|
3784
|
+
if (answers.allowedPaths.length > 0) {
|
|
3785
|
+
current = answers.allowedPaths.join(",");
|
|
3683
3786
|
}
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
let used = 0;
|
|
3689
|
-
let line = "";
|
|
3690
|
-
for (const part of parts) {
|
|
3691
|
-
let text = "";
|
|
3692
|
-
for (const character of part.text) {
|
|
3693
|
-
const characterWidth = charWidth(character.codePointAt(0) ?? 0);
|
|
3694
|
-
if (used + characterWidth > width) {
|
|
3695
|
-
break;
|
|
3696
|
-
}
|
|
3697
|
-
used += characterWidth;
|
|
3698
|
-
text += character;
|
|
3699
|
-
}
|
|
3700
|
-
if (part.paint != null) {
|
|
3701
|
-
line += part.paint(text);
|
|
3702
|
-
} else {
|
|
3703
|
-
line += text;
|
|
3704
|
-
}
|
|
3705
|
-
if (used >= width) {
|
|
3706
|
-
break;
|
|
3707
|
-
}
|
|
3787
|
+
const typed = await askOptional(prompter, "Limit file tools to path prefixes (comma-separated, empty = anywhere)", current);
|
|
3788
|
+
answers.allowedPaths = [];
|
|
3789
|
+
if (typed != null) {
|
|
3790
|
+
answers.allowedPaths = typed.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
3708
3791
|
}
|
|
3709
|
-
return
|
|
3792
|
+
return;
|
|
3710
3793
|
}
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3794
|
+
const isJump = id === "jump-host-key";
|
|
3795
|
+
let pinned = answers.hostKeyFingerprint;
|
|
3796
|
+
if (isJump == true) {
|
|
3797
|
+
pinned = answers.jumpHostKeyFingerprint;
|
|
3798
|
+
}
|
|
3799
|
+
if (pinned != null) {
|
|
3800
|
+
const unpin = await prompter.askYesNo(`Unpin ${pinned}?`, false);
|
|
3801
|
+
if (unpin == true && isJump == true) {
|
|
3802
|
+
answers.jumpHostKeyFingerprint = void 0;
|
|
3803
|
+
} else if (unpin == true) {
|
|
3804
|
+
answers.hostKeyFingerprint = void 0;
|
|
3715
3805
|
}
|
|
3716
|
-
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
const missing = missingFields(state);
|
|
3809
|
+
if (missing.length > 0) {
|
|
3810
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first \u2014 the key is pinned by testing the connection.
|
|
3717
3811
|
`);
|
|
3812
|
+
return;
|
|
3718
3813
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
if (shown === "") {
|
|
3733
|
-
shown = this.style.dim("(empty)");
|
|
3814
|
+
await testConnection(prompter, output, state);
|
|
3815
|
+
}
|
|
3816
|
+
async function runHostForm(prompter, output, options) {
|
|
3817
|
+
const { existing, takenNames, destination } = options;
|
|
3818
|
+
let state;
|
|
3819
|
+
if (existing != null) {
|
|
3820
|
+
let route = "direct";
|
|
3821
|
+
if (existing.jumpHost != null && existing.proxyCommand != null) {
|
|
3822
|
+
route = "jump-proxy";
|
|
3823
|
+
} else if (existing.jumpHost != null) {
|
|
3824
|
+
route = "jump";
|
|
3825
|
+
} else if (existing.proxyCommand != null) {
|
|
3826
|
+
route = "proxy";
|
|
3734
3827
|
}
|
|
3735
|
-
|
|
3736
|
-
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
text += `\r${ESC}[J${lines.join("\n")}`;
|
|
3755
|
-
this.output.write(text);
|
|
3756
|
-
renderedLines = lines.length;
|
|
3757
|
-
};
|
|
3758
|
-
const buildLines = () => {
|
|
3759
|
-
const width = this.columns() - 1;
|
|
3760
|
-
const lines = [
|
|
3761
|
-
this.fitLine([
|
|
3762
|
-
{ text: this.symbols.question, paint: this.style.accent },
|
|
3763
|
-
{ text: " " },
|
|
3764
|
-
{ text: question, paint: this.style.bold }
|
|
3765
|
-
], width)
|
|
3766
|
-
];
|
|
3767
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
3768
|
-
const item = items[index];
|
|
3769
|
-
if ("value" in item == false) {
|
|
3770
|
-
lines.push(this.fitLine([{ text: ` ${this.symbols.rule.repeat(ruleWidth)}`, paint: this.style.dim }], width));
|
|
3771
|
-
continue;
|
|
3772
|
-
}
|
|
3773
|
-
const isActive = index === active;
|
|
3774
|
-
const parts = [];
|
|
3775
|
-
if (isActive == true) {
|
|
3776
|
-
parts.push({ text: `${this.symbols.pointer} `, paint: this.style.accent });
|
|
3777
|
-
parts.push({ text: padToWidth(item.label, labelWidths[index]), paint: this.style.accent });
|
|
3778
|
-
} else {
|
|
3779
|
-
parts.push({ text: " " });
|
|
3780
|
-
parts.push({ text: padToWidth(item.label, labelWidths[index]) });
|
|
3781
|
-
}
|
|
3782
|
-
if (item.hint != null && item.hint !== "") {
|
|
3783
|
-
parts.push({ text: ` ${item.hint}`, paint: this.style.dim });
|
|
3784
|
-
}
|
|
3785
|
-
lines.push(this.fitLine(parts, width));
|
|
3786
|
-
}
|
|
3787
|
-
let footer = "\u2191\u2193 move \xB7 enter select";
|
|
3788
|
-
if (cancelValue != null) {
|
|
3789
|
-
footer += " \xB7 esc back";
|
|
3790
|
-
}
|
|
3791
|
-
lines.push(this.fitLine([{ text: footer, paint: this.style.dim }], width));
|
|
3792
|
-
return lines;
|
|
3793
|
-
};
|
|
3794
|
-
const move = (step) => {
|
|
3795
|
-
const position = selectableIndexes.indexOf(active);
|
|
3796
|
-
const next = (position + step + selectableIndexes.length) % selectableIndexes.length;
|
|
3797
|
-
active = selectableIndexes[next];
|
|
3828
|
+
state = { answers: { ...existing }, route, nameTouched: true, lastTest: "never", testedSnapshot: null };
|
|
3829
|
+
} else {
|
|
3830
|
+
const foundKeys = findPrivateKeys();
|
|
3831
|
+
state = {
|
|
3832
|
+
answers: {
|
|
3833
|
+
name: "",
|
|
3834
|
+
host: "",
|
|
3835
|
+
port: DEFAULT_SSH_PORT,
|
|
3836
|
+
username: "",
|
|
3837
|
+
auth: "key",
|
|
3838
|
+
keyPath: foundKeys[0]?.path,
|
|
3839
|
+
readonly: false,
|
|
3840
|
+
allowedPaths: [],
|
|
3841
|
+
extraEnv: {}
|
|
3842
|
+
},
|
|
3843
|
+
route: "direct",
|
|
3844
|
+
nameTouched: false,
|
|
3845
|
+
lastTest: "never",
|
|
3846
|
+
testedSnapshot: null
|
|
3798
3847
|
};
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3848
|
+
}
|
|
3849
|
+
const initialSnapshot = snapshotOf(state);
|
|
3850
|
+
const originalName = existing?.name;
|
|
3851
|
+
let title = "New host";
|
|
3852
|
+
if (existing != null) {
|
|
3853
|
+
title = `Editing ${existing.name}`;
|
|
3854
|
+
}
|
|
3855
|
+
output.write(prompter.paint.dim(`
|
|
3856
|
+
Pick a setting to change it. In a text field, Enter keeps the value shown and ${CLEAR_ANSWER} clears it.
|
|
3857
|
+
|
|
3858
|
+
`));
|
|
3859
|
+
const previousTransient = prompter.transient;
|
|
3860
|
+
prompter.transient = true;
|
|
3861
|
+
try {
|
|
3862
|
+
let cursor = "host";
|
|
3863
|
+
for (; ; ) {
|
|
3864
|
+
const picked = await prompter.select(title, buildRows(state, destination), { defaultValue: cursor, cancelValue: "cancel" });
|
|
3865
|
+
const before = cloneState(state);
|
|
3866
|
+
try {
|
|
3867
|
+
if (picked === "cancel") {
|
|
3868
|
+
if (snapshotOf(state) !== initialSnapshot) {
|
|
3869
|
+
const discard = await prompter.askYesNo("Discard the changes?", false);
|
|
3870
|
+
if (discard == false) {
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return null;
|
|
3811
3875
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3876
|
+
const missing = missingFields(state);
|
|
3877
|
+
if (picked === "test") {
|
|
3878
|
+
if (missing.length > 0) {
|
|
3879
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3880
|
+
`);
|
|
3881
|
+
cursor = missing[0];
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
const connected = await testConnection(prompter, output, state);
|
|
3885
|
+
cursor = "test";
|
|
3886
|
+
if (connected == true) {
|
|
3887
|
+
cursor = "save";
|
|
3888
|
+
}
|
|
3889
|
+
continue;
|
|
3818
3890
|
}
|
|
3819
|
-
if (
|
|
3820
|
-
if (
|
|
3821
|
-
|
|
3891
|
+
if (picked === "save") {
|
|
3892
|
+
if (missing.length > 0) {
|
|
3893
|
+
output.write(` Fill in ${missing.map((field) => FIELD_NAMES[field]).join(", ")} first.
|
|
3894
|
+
`);
|
|
3895
|
+
cursor = missing[0];
|
|
3896
|
+
continue;
|
|
3822
3897
|
}
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3898
|
+
const name = state.answers.name;
|
|
3899
|
+
if (name !== originalName && takenNames.includes(name) == true) {
|
|
3900
|
+
const replace = await prompter.askYesNo(`"${name}" already exists in this file. Replace it?`, false);
|
|
3901
|
+
if (replace == false) {
|
|
3902
|
+
cursor = "name";
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
if (snapshotOf(state) !== state.testedSnapshot) {
|
|
3907
|
+
const testFirst = await prompter.askYesNo("Test the connection before saving?", true);
|
|
3908
|
+
if (testFirst == true) {
|
|
3909
|
+
const connected = await testConnection(prompter, output, state);
|
|
3910
|
+
if (connected == false) {
|
|
3911
|
+
const next = await prompter.select(
|
|
3912
|
+
"The check failed. What now?",
|
|
3913
|
+
[
|
|
3914
|
+
{ value: "back", key: "back", label: "Back to the settings" },
|
|
3915
|
+
{ value: "save", key: "save", label: "Save anyway", hint: "the host may be unreachable from here" },
|
|
3916
|
+
{ value: "discard", key: "discard", label: "Discard", hint: "nothing is written" }
|
|
3917
|
+
],
|
|
3918
|
+
{ defaultValue: "back", cancelValue: "back" }
|
|
3919
|
+
);
|
|
3920
|
+
if (next === "discard") {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
if (next === "back") {
|
|
3924
|
+
cursor = "test";
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
return finalizeAnswers(state);
|
|
3826
3931
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
active = selectableIndexes[0];
|
|
3833
|
-
} else if (KEY_END.includes(key) == true) {
|
|
3834
|
-
active = selectableIndexes[selectableIndexes.length - 1];
|
|
3835
|
-
} else if (/^[1-9]$/.test(key) == true && Number(key) <= selectableIndexes.length) {
|
|
3836
|
-
active = selectableIndexes[Number(key) - 1];
|
|
3837
|
-
} else {
|
|
3838
|
-
return;
|
|
3932
|
+
await editField(prompter, output, state, picked);
|
|
3933
|
+
cursor = nextCursor(state, destination, picked);
|
|
3934
|
+
} catch (ex) {
|
|
3935
|
+
if (ex instanceof PromptBackError == false) {
|
|
3936
|
+
throw ex;
|
|
3839
3937
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3938
|
+
restoreState(state, before);
|
|
3939
|
+
cursor = picked;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
} finally {
|
|
3943
|
+
prompter.transient = previousTransient;
|
|
3846
3944
|
}
|
|
3847
|
-
}
|
|
3945
|
+
}
|
|
3848
3946
|
function toMcpServerEntry(value) {
|
|
3849
3947
|
if (typeof value !== "object" || value == null) {
|
|
3850
3948
|
return null;
|
|
@@ -4577,7 +4675,7 @@ async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
|
4577
4675
|
if (existingName != null) {
|
|
4578
4676
|
existing = entryToAnswers(existingName, entries[existingName]);
|
|
4579
4677
|
}
|
|
4580
|
-
|
|
4678
|
+
let answers = await runHostForm(prompter, output, {
|
|
4581
4679
|
existing,
|
|
4582
4680
|
takenNames: Object.keys(entries),
|
|
4583
4681
|
destination: target.label
|
|
@@ -4586,18 +4684,40 @@ async function addOrEditHost(prompter, output, target, entries, existingName) {
|
|
|
4586
4684
|
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4587
4685
|
return;
|
|
4588
4686
|
}
|
|
4589
|
-
|
|
4590
|
-
|
|
4687
|
+
let entry = answersToEntry(answers);
|
|
4688
|
+
for (; ; ) {
|
|
4689
|
+
output.write(`
|
|
4591
4690
|
${prompter.paint.dim(`This goes into ${target.location}:`)}
|
|
4592
4691
|
|
|
4593
4692
|
`);
|
|
4594
|
-
|
|
4693
|
+
output.write(`${indent(formatForTarget(target, answers.name, maskSecrets(entry)))}
|
|
4595
4694
|
|
|
4596
4695
|
`);
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4696
|
+
let confirmed;
|
|
4697
|
+
try {
|
|
4698
|
+
confirmed = await prompter.askYesNo("Write it?", true);
|
|
4699
|
+
} catch (ex) {
|
|
4700
|
+
if (ex instanceof PromptBackError == false) {
|
|
4701
|
+
throw ex;
|
|
4702
|
+
}
|
|
4703
|
+
const revised = await runHostForm(prompter, output, {
|
|
4704
|
+
existing: answers,
|
|
4705
|
+
takenNames: Object.keys(entries),
|
|
4706
|
+
destination: target.label
|
|
4707
|
+
});
|
|
4708
|
+
if (revised == null) {
|
|
4709
|
+
output.write(" Cancelled \u2014 nothing was written.\n\n");
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
answers = revised;
|
|
4713
|
+
entry = answersToEntry(answers);
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
if (confirmed == false) {
|
|
4717
|
+
output.write(" Not written. Pick the host and choose Print to get the entry ready to paste.\n\n");
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
break;
|
|
4601
4721
|
}
|
|
4602
4722
|
const written = target.writeEntry(answers.name, entry);
|
|
4603
4723
|
reportWrite(prompter, written, "Written");
|
|
@@ -4714,7 +4834,7 @@ async function manageHost(prompter, output, target, entries, name) {
|
|
|
4714
4834
|
}
|
|
4715
4835
|
}
|
|
4716
4836
|
}
|
|
4717
|
-
async function
|
|
4837
|
+
async function runSetupCommand(options) {
|
|
4718
4838
|
const { input, output } = options;
|
|
4719
4839
|
const targets = options.targets ?? listConfigTargets();
|
|
4720
4840
|
let target = targets[0];
|
|
@@ -4814,6 +4934,9 @@ ${paint.bold(target.label)} ${paint.dim(target.location)}
|
|
|
4814
4934
|
if (ex instanceof PromptClosedError) {
|
|
4815
4935
|
throw ex;
|
|
4816
4936
|
}
|
|
4937
|
+
if (ex instanceof PromptBackError) {
|
|
4938
|
+
continue;
|
|
4939
|
+
}
|
|
4817
4940
|
output.write(` Failed: ${errorMessage(ex)}
|
|
4818
4941
|
|
|
4819
4942
|
`);
|
|
@@ -5521,11 +5644,12 @@ function createSshMcpServer() {
|
|
|
5521
5644
|
}
|
|
5522
5645
|
|
|
5523
5646
|
// src/cli.ts
|
|
5647
|
+
var SETUP_COMMAND_NAMES = ["setup", "config"];
|
|
5524
5648
|
function parseArguments(argv) {
|
|
5525
|
-
const parsed = { showHelp: false, showVersion: false, checkOnly: false,
|
|
5649
|
+
const parsed = { showHelp: false, showVersion: false, checkOnly: false, setupMode: false };
|
|
5526
5650
|
let rest = argv;
|
|
5527
|
-
if (argv[0]
|
|
5528
|
-
parsed.
|
|
5651
|
+
if (argv[0] != null && SETUP_COMMAND_NAMES.includes(argv[0]) == true) {
|
|
5652
|
+
parsed.setupMode = true;
|
|
5529
5653
|
rest = argv.slice(1);
|
|
5530
5654
|
}
|
|
5531
5655
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -5559,17 +5683,17 @@ var HELP_TEXT = [
|
|
|
5559
5683
|
"",
|
|
5560
5684
|
"Usage:",
|
|
5561
5685
|
" akms-mcp-ssh [--check]",
|
|
5562
|
-
" akms-mcp-ssh
|
|
5686
|
+
" akms-mcp-ssh setup [--target <client>]",
|
|
5563
5687
|
"",
|
|
5564
5688
|
"Commands:",
|
|
5565
|
-
"
|
|
5689
|
+
" setup Manage the SSH hosts registered in an MCP client's config \u2014",
|
|
5566
5690
|
" list, add, edit, remove, check, print. Asks plain questions,",
|
|
5567
5691
|
" so the SSH_* names below never have to be typed.",
|
|
5568
5692
|
"",
|
|
5569
5693
|
"Options:",
|
|
5570
5694
|
" --check Connect to the configured host, report the result, and exit.",
|
|
5571
5695
|
" Prints the host key fingerprint so it can be pinned.",
|
|
5572
|
-
" --target <id>
|
|
5696
|
+
" --target <id> setup: claude-code | codex | claude-desktop | cursor",
|
|
5573
5697
|
" (default: the first one installed)",
|
|
5574
5698
|
" -v, --version Print the version and exit",
|
|
5575
5699
|
" -h, --help Print this help and exit",
|
|
@@ -5603,7 +5727,7 @@ var HELP_TEXT = [
|
|
|
5603
5727
|
].join("\n");
|
|
5604
5728
|
async function runCheckCommand(profile) {
|
|
5605
5729
|
if (profile == null) {
|
|
5606
|
-
process.stdout.write(`No SSH host is registered \u2014 set ${SINGLE_HOST_ENV.HOST} and ${SINGLE_HOST_ENV.USER}, or run: akms-mcp-ssh
|
|
5730
|
+
process.stdout.write(`No SSH host is registered \u2014 set ${SINGLE_HOST_ENV.HOST} and ${SINGLE_HOST_ENV.USER}, or run: akms-mcp-ssh setup
|
|
5607
5731
|
`);
|
|
5608
5732
|
return 1;
|
|
5609
5733
|
}
|
|
@@ -5626,11 +5750,11 @@ async function main() {
|
|
|
5626
5750
|
`);
|
|
5627
5751
|
return;
|
|
5628
5752
|
}
|
|
5629
|
-
if (cliArguments.
|
|
5753
|
+
if (cliArguments.setupMode == true) {
|
|
5630
5754
|
if (process.env[LOG_LEVEL_ENV] == null) {
|
|
5631
5755
|
process.env[LOG_LEVEL_ENV] = "warn";
|
|
5632
5756
|
}
|
|
5633
|
-
const exitCode = await
|
|
5757
|
+
const exitCode = await runSetupCommand({
|
|
5634
5758
|
input: process.stdin,
|
|
5635
5759
|
output: process.stdout,
|
|
5636
5760
|
targetId: cliArguments.targetId
|