feat: migrate ssh menu to Bun/TypeScript
Replace the Bash implementation and jq/yq dependency chain with a Bun-based TypeScript script, add Bun/TypeScript project files, and update docs and sample config notes to match the new CLI and behavior.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
node_modules/
|
||||
config/
|
||||
|
||||
+7
-4
@@ -1,13 +1,16 @@
|
||||
# Project Memory
|
||||
|
||||
- Project: mtm-ssh-menu
|
||||
- Type: Bash SSH target picker using `fzf` and YAML config.
|
||||
- Type: TypeScript/Bun SSH target picker with YAML config.
|
||||
- Main entry point: `mtm-ssh-menu`
|
||||
- Configuration: `~/.config/mtm-ssh-menu/global.yaml` and `hosts/*.yaml` by default.
|
||||
- Dependencies: `bash`, `ssh`, `fzf`, `jq`, `yq`.
|
||||
- Dependencies: `bun`, `ssh`, `fzf`.
|
||||
- Sample config is stored in `sample-config/` and mirrors current script behavior.
|
||||
- Sample `global.yaml` includes `office`, `dev`, and `backup` jump hosts.
|
||||
- Documentation language: English.
|
||||
- Current task focus: keep README and memory aligned with the working script and sample configuration.
|
||||
- Sample host files currently use numbered names: `01_prod.yaml`, `02_staging.yaml`, `03_dev.yaml`.
|
||||
- Known follow-up improvements:
|
||||
- Each group should have default values.
|
||||
- `sample-config/hosts/01_prod.yaml` contains `web-1` and `db-1`; `02_staging.yaml` contains `app-1` and `worker-1`; `03_dev.yaml` contains `api-1` and `web-1`.
|
||||
- CLI usage currently exposes `-c/--config-dir` and `-k/--known-hosts` (optional user) as documented in the script.
|
||||
- `-k` without a user defaults known-host entries to `root`.
|
||||
- Host-specific `options` override global `default_options`; `ssh_options` in sample config is currently unused.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mtm-ssh-menu
|
||||
|
||||
A Bash SSH target picker built around `fzf` and YAML configuration.
|
||||
A Bun/TypeScript SSH target picker built around `fzf` and YAML configuration.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -14,10 +14,9 @@ A Bash SSH target picker built around `fzf` and YAML configuration.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `bun`
|
||||
- `ssh`
|
||||
- `fzf`
|
||||
- `jq`
|
||||
- `yq`
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -25,11 +24,14 @@ A Bash SSH target picker built around `fzf` and YAML configuration.
|
||||
./mtm-ssh-menu [--config-dir DIR] [-k [user]]
|
||||
```
|
||||
|
||||
The usage shown by the script is `sshm [--config-dir DIR] [-k [user]]`.
|
||||
|
||||
Options:
|
||||
|
||||
- `-h`, `--help`: show help
|
||||
- `--config-dir DIR`: use a custom configuration directory
|
||||
- `-k`, `--known-hosts [user]`: include hosts from `~/.ssh/known_hosts`
|
||||
- `-c`, `--config-dir DIR`: use a custom configuration directory
|
||||
- `-k`, `--known-hosts [user]`: include hosts from `~/.ssh/known_hosts` and optionally set the user
|
||||
- When `-k` is used without a user, the selected entry uses `root`
|
||||
|
||||
By default, the script reads configuration from:
|
||||
|
||||
@@ -47,14 +49,56 @@ ssh:
|
||||
default_options: ""
|
||||
jump_hosts:
|
||||
office: "user@192.168.10.11"
|
||||
office-alt: "user@192.168.10.11:2222"
|
||||
|
||||
dev: "user@192.168.10.11:2222"
|
||||
backup: "admin@10.0.0.5:2200"
|
||||
```
|
||||
|
||||
### `hosts/01_prod.yaml`
|
||||
|
||||
```yaml
|
||||
group: prod
|
||||
servers:
|
||||
- name: web-1
|
||||
aliases:
|
||||
- frontend
|
||||
- web
|
||||
host: web-1.prod.internal
|
||||
jump_host: office
|
||||
|
||||
- name: db-1
|
||||
aliases:
|
||||
- database
|
||||
- sql
|
||||
host: 10.0.0.20
|
||||
jump_host: office
|
||||
```
|
||||
|
||||
### `hosts/02_staging.yaml`
|
||||
|
||||
```yaml
|
||||
group: staging
|
||||
servers:
|
||||
- name: app-1
|
||||
aliases:
|
||||
- app
|
||||
- application
|
||||
host: staging-app.example.com
|
||||
user: ubuntu
|
||||
port: 22
|
||||
ssh_options: ""
|
||||
|
||||
- name: worker-1
|
||||
aliases:
|
||||
- worker
|
||||
host: 10.20.0.15
|
||||
user: ubuntu
|
||||
port: 22
|
||||
```
|
||||
|
||||
### `hosts/03_dev.yaml`
|
||||
|
||||
```yaml
|
||||
group: dev
|
||||
servers:
|
||||
- name: api-1
|
||||
aliases:
|
||||
@@ -73,22 +117,9 @@ servers:
|
||||
port: 22
|
||||
```
|
||||
|
||||
### `hosts/03_dev.yaml`
|
||||
|
||||
```yaml
|
||||
group: dev
|
||||
servers:
|
||||
- name: web-1
|
||||
aliases:
|
||||
- frontend
|
||||
- web
|
||||
host: web-1.prod.internal
|
||||
jump_host: office
|
||||
```
|
||||
|
||||
The file name does not need to match the `group` value. The script reads the `group` from the YAML content and uses the file path internally to load the selected host.
|
||||
|
||||
The sample configuration now uses numbered file names such as `01_prod.yaml`, `02_staging.yaml`, and `03_dev.yaml`.
|
||||
The sample configuration uses numbered file names: `01_prod.yaml`, `02_staging.yaml`, and `03_dev.yaml`.
|
||||
|
||||
Each host entry can define:
|
||||
|
||||
@@ -108,11 +139,13 @@ A complete sample configuration is available in `sample-config/`.
|
||||
- `jump_host` values are resolved by name from `global.yaml`.
|
||||
- Jump hosts support `user@host` and `user@host:port`; if the port is omitted, `22` is used.
|
||||
- If a host does not define a user or port, global defaults are used.
|
||||
- `default_options` is present in the config but not consumed by the current script.
|
||||
- The sample config includes `ssh_options` in one file, but the current script does not consume it yet.
|
||||
- `default_options` is consumed for hosts that do not define their own `options`, and is appended as raw SSH options.
|
||||
- `ssh_options` appears in the sample config but is not used by the current script; only `options` is read.
|
||||
- Sample host examples are stored as `sample-config/hosts/01_prod.yaml`, `02_staging.yaml`, and `03_dev.yaml`.
|
||||
- The `group` key is required for each host config file; the file name is only an internal storage detail.
|
||||
- When `-k` is used, entries from `known_hosts` are shown with the selected user or `root` by default.
|
||||
- Known-host entries always use port `22`.
|
||||
- `known_hosts` entries use port `22`.
|
||||
- The script uses strict Bash mode (`set -euo pipefail`).
|
||||
- The script is implemented in Bun and launches `fzf` plus `ssh` from a parsed YAML configuration.
|
||||
- The current implementation builds the final SSH command as a string and executes it through Bun.
|
||||
- The script name shown in usage is `sshm`, while the repository file is `mtm-ssh-menu`.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mtm-ssh-menu",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
}
|
||||
}
|
||||
+181
-175
@@ -1,183 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#!/usr/bin/env bun
|
||||
|
||||
CONFIG_DIR="$HOME/.config/mtm-ssh-menu"
|
||||
USE_KNOWN_HOSTS=0
|
||||
USE_KNOWN_HOSTS_USER=$(whoami)
|
||||
import { YAML, $ } from "bun";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: sshm [--config-dir DIR] [-k [user]]
|
||||
|
||||
Options:
|
||||
-h | --help Show this help message
|
||||
--config-dir DIR Use a custom config directory
|
||||
-k | --known-hosts [user] Include known_hosts
|
||||
|
||||
The config directory must contain:
|
||||
- global.yaml
|
||||
- hosts/*.yaml
|
||||
EOF
|
||||
type Args = {
|
||||
help: boolean
|
||||
configDir: string
|
||||
knownHosts?: string | true
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--config-dir)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
printf 'Error: --config-dir requires a value.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
CONFIG_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--known-hosts|-k)
|
||||
USE_KNOWN_HOSTS=1
|
||||
if [ $# -eq 1 ]; then
|
||||
shift
|
||||
else
|
||||
USE_KNOWN_HOSTS_USER="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Error: unknown argument: %s\n' "$1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
parse_args "$@"
|
||||
|
||||
dependency_check() {
|
||||
if ! command -v yq >/dev/null 2>&1; then
|
||||
printf 'Error: yq is required but not installed.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
printf 'Error: jq is required but not installed.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v ssh >/dev/null 2>&1; then
|
||||
printf 'Error: ssh is required but not installed.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v fzf >/dev/null 2>&1; then
|
||||
printf 'Error: fzf is required but not installed.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
dependency_check
|
||||
|
||||
GLOBAL_CONFIG="$CONFIG_DIR/global.yaml"
|
||||
HOSTS_DIR="$CONFIG_DIR/hosts"
|
||||
DEFAULT_SSH_USER="root"
|
||||
DEFAULT_SSH_PORT="22"
|
||||
DEFAULT_SSH_OPTIONS=""
|
||||
SSH_JUMP_HOSTS={}
|
||||
SERVERS=""
|
||||
|
||||
load_config() {
|
||||
local GLOBAL_CONFIG_CONTENT
|
||||
GLOBAL_CONFIG_CONTENT="$(<"$GLOBAL_CONFIG")"
|
||||
DEFAULT_SSH_USER=$(yq -r '.ssh.default_user // "'$DEFAULT_SSH_USER'"' <<<"$GLOBAL_CONFIG_CONTENT")
|
||||
DEFAULT_SSH_PORT=$(yq -r '.ssh.default_port // "'$DEFAULT_SSH_PORT'"' <<<"$GLOBAL_CONFIG_CONTENT")
|
||||
DEFAULT_SSH_OPTIONS=$(yq -r '.ssh.default_options // "'"$DEFAULT_SSH_OPTIONS"'"' <<<"$GLOBAL_CONFIG_CONTENT")
|
||||
SSH_JUMP_HOSTS=$(yq -r '.ssh.jump_hosts // {}' <<<"$GLOBAL_CONFIG_CONTENT")
|
||||
|
||||
shopt -s nullglob
|
||||
for file in "$HOSTS_DIR"/*.yaml; do
|
||||
local FILE_CONTENT GROUP_NAME GROUP_SERVERS INDEX
|
||||
FILE_CONTENT="$(<"$file")"
|
||||
GROUP_NAME=$(yq -r '.group // empty' <<<"$FILE_CONTENT")
|
||||
if [ "$GROUP_NAME" = "" ]; then
|
||||
printf 'Error: missing group name in config file: %s\n' "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
GROUP_SERVERS=$(yq -r '.servers // []' <<<"$FILE_CONTENT")
|
||||
INDEX=0
|
||||
for ((i=0; i<$(jq -r '. | length' <<<"$GROUP_SERVERS"); i++)); do
|
||||
local SSH_SERVER_NAME SSH_SERVER_ALIASES ALIASES SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_USER
|
||||
SSH_SERVER_NAME=$(jq -r '.'"[$i]"'.name // ""' <<<"$GROUP_SERVERS")
|
||||
SSH_SERVER_ALIASES=$(jq -r '.'"[$i]"'.aliases // ""' <<<"$GROUP_SERVERS")
|
||||
SSH_SERVER_HOST=$(jq -r '.'"[$i]"'.host // ""' <<<"$GROUP_SERVERS")
|
||||
SSH_SERVER_PORT=$(jq -r '.'"[$i]"'.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$GROUP_SERVERS")
|
||||
SSH_SERVER_USER=$(jq -r '.'"[$i]"'.user // "'"$DEFAULT_SSH_USER"'"' <<<"$GROUP_SERVERS")
|
||||
ALIASES=""
|
||||
if [ "$SSH_SERVER_ALIASES" != "" ]; then
|
||||
# shellcheck disable=SC2027
|
||||
ALIASES="("$(echo "$SSH_SERVER_ALIASES" | jq -r 'join(", ")')")"
|
||||
fi
|
||||
if [ "$SERVERS" != "" ]; then
|
||||
SERVERS+="\n"
|
||||
fi
|
||||
SERVERS+="${GROUP_NAME} | ${file} | ${INDEX} | ${SSH_SERVER_HOST} | ${SSH_SERVER_NAME} ${ALIASES} | ${SSH_SERVER_USER} | ${SSH_SERVER_PORT}"
|
||||
INDEX=$((INDEX+1))
|
||||
done
|
||||
done
|
||||
shopt -u nullglob
|
||||
}
|
||||
load_known_hosts() {
|
||||
local FILE_CONTENT GROUP_NAME
|
||||
FILE_CONTENT=$(cat "$HOME"/.ssh/known_hosts | awk -F ' ' '{print $1}' | sort | awk '!seen[$1]++')
|
||||
GROUP_NAME="Local"
|
||||
for host in $FILE_CONTENT; do
|
||||
if [ "$SERVERS" != "" ]; then
|
||||
SERVERS+="\n"
|
||||
fi
|
||||
SERVERS+="${GROUP_NAME} | known_hosts | ${host} | ${host} | ${USE_KNOWN_HOSTS_USER} | 22"
|
||||
done
|
||||
}
|
||||
popup_menu() {
|
||||
local SERVER
|
||||
SERVER=$(echo -e "${SERVERS}" | column -t -s "|" -o "|" | fzf -e --layout=reverse --with-nth=1,4,5,6,7 --delimiter="|")
|
||||
local GROUP_NAME GROUP_SOURCE HOST_INDEX
|
||||
GROUP_NAME=$(echo "$SERVER" | awk -F '|' '{print $1}' | xargs)
|
||||
GROUP_SOURCE=$(echo "$SERVER" | awk -F '|' '{print $2}' | xargs)
|
||||
HOST_INDEX=$(echo "$SERVER" | awk -F '|' '{print $3}' | xargs)
|
||||
ssh_connect "$GROUP_SOURCE" "$HOST_INDEX"
|
||||
}
|
||||
ssh_connect() {
|
||||
if [[ "$HOST_INDEX" != *"."* ]]; then
|
||||
local SERVER SSH_SERVER_USER SSH_SERVER_HOST SSH_SERVER_PORT SSH_SERVER_OPTIONS SSH_JUMP_HOST
|
||||
SERVER=$(yq -r '.servers['"$2"']' "$1")
|
||||
SSH_SERVER_USER=$(jq -r '.user // "'"$DEFAULT_SSH_USER"'"' <<<"$SERVER")
|
||||
SSH_SERVER_HOST=$(jq -r '.host // ""' <<<"$SERVER")
|
||||
SSH_SERVER_PORT=$(jq -r '.port // "'"$DEFAULT_SSH_PORT"'"' <<<"$SERVER")
|
||||
|
||||
SSH_SERVER_OPTIONS="-p $SSH_SERVER_PORT"
|
||||
SSH_JUMP_HOST=$(jq -r '.jump_host // ""' <<<"$SERVER")
|
||||
if [ "$SSH_JUMP_HOST" != "" ]; then
|
||||
SSH_JUMP_HOST=$(jq -r '.'"$SSH_JUMP_HOST"' // ""' <<<"${SSH_JUMP_HOSTS}")
|
||||
SSH_JUMP_HOST_USER=$(cut -d'@' -f1 <<<"$SSH_JUMP_HOST")
|
||||
SSH_JUMP_HOST_TARGET=$(cut -d'@' -f2- <<<"$SSH_JUMP_HOST")
|
||||
SSH_JUMP_HOST_TARGET_HOST=$(cut -d':' -f1 <<<"$SSH_JUMP_HOST_TARGET")
|
||||
SSH_JUMP_HOST_TARGET_PORT=$(cut -s -d':' -f2 <<<"$SSH_JUMP_HOST_TARGET")
|
||||
if [ "$SSH_JUMP_HOST_TARGET_PORT" = "" ]; then
|
||||
SSH_JUMP_HOST_TARGET_PORT="22"
|
||||
fi
|
||||
SSH_JUMP_HOST="-t ${SSH_JUMP_HOST_USER}@${SSH_JUMP_HOST_TARGET_HOST} ssh -p ${SSH_JUMP_HOST_TARGET_PORT}"
|
||||
SSH_SERVER_OPTIONS=""
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_SERVER_OPTIONS} ${SSH_JUMP_HOST} "${SSH_SERVER_USER}"@"${SSH_SERVER_HOST}"
|
||||
else
|
||||
ssh "${USE_KNOWN_HOSTS_USER}"@"${2}"
|
||||
fi
|
||||
const usage = () => {
|
||||
console.log("Usage: sshm [--config-dir DIR] [-k [user]]");
|
||||
console.log("");
|
||||
console.log("Options:");
|
||||
console.log(" -h | --help Show this help message");
|
||||
console.log(" -c | --config-dir DIR Use a custom config directory");
|
||||
console.log(" -k | --known-hosts [user] Include known_hosts (root if no user provided)");
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "$USE_KNOWN_HOSTS" == 1 ]; then
|
||||
load_known_hosts
|
||||
fi
|
||||
load_config
|
||||
popup_menu
|
||||
const parseArgs = (argv: string[]): Args => {
|
||||
const args: Args = {
|
||||
help: false,
|
||||
configDir: `${Bun.env.HOME ?? ""}/.config/mtm-ssh-menu`,
|
||||
}
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
|
||||
switch (arg) {
|
||||
case "-h":
|
||||
case "--help":
|
||||
args.help = true;
|
||||
break;
|
||||
|
||||
case "-c":
|
||||
case "--config-dir": {
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith("-")) {
|
||||
throw new Error(`${arg} requires a directory path`);
|
||||
}
|
||||
args.configDir = value;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
|
||||
case "-k":
|
||||
case "--known-hosts": {
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith("-")) {
|
||||
args.knownHosts = next;
|
||||
i++;
|
||||
} else {
|
||||
args.knownHosts = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
main
|
||||
let args: Args;
|
||||
|
||||
try {
|
||||
args = parseArgs(Bun.argv.slice(2));
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.help) {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
const config: any = {
|
||||
global: YAML.parse(await Bun.file(args.configDir + '/global.yaml').text()),
|
||||
hosts: [],
|
||||
}
|
||||
|
||||
const loadKnownHosts = async () => {
|
||||
const rawFile = await Bun.file(`${Bun.env.HOME ?? ""}/.ssh/known_hosts`).text();
|
||||
rawFile.split('\n').forEach(line => {
|
||||
const parts = line.split(' ');
|
||||
if (parts[0]) {
|
||||
const target = {
|
||||
user: 'root',
|
||||
host: '',
|
||||
port: 22,
|
||||
name: '',
|
||||
}
|
||||
if (args.knownHosts != true) target.user = args.knownHosts as string;
|
||||
if (parts[0].startsWith("[")) {
|
||||
const hostparts: any = parts[0].split(":");
|
||||
target.host = hostparts[0].substring(1, hostparts[0].length - 1);
|
||||
target.port = hostparts[1];
|
||||
target.name = parts[1] as string;
|
||||
} else {
|
||||
target.host = parts[0];
|
||||
target.port = 22;
|
||||
target.name = parts[1] as string;
|
||||
}
|
||||
const record = {
|
||||
index: config.hosts.length,
|
||||
group: ".ssh",
|
||||
host: target.host,
|
||||
user: target.user,
|
||||
port: target.port,
|
||||
name: target.name,
|
||||
aliases: [],
|
||||
};
|
||||
config.hosts.push(record);
|
||||
}
|
||||
});
|
||||
}
|
||||
const loadHosts = async () => {
|
||||
const configHostsDir = args.configDir + '/hosts/';
|
||||
const configHostsFiles = readdirSync(configHostsDir).sort();
|
||||
for (let i = 0; i < configHostsFiles.length; i++) {
|
||||
if (extname(configHostsFiles[i] as string) === '.yaml') {
|
||||
const file: any = YAML.parse(await Bun.file(configHostsDir + configHostsFiles[i]).text());
|
||||
const groupName = file.group;
|
||||
file.servers.forEach((host: any) => {
|
||||
const record = {
|
||||
index: config.hosts.length,
|
||||
group: groupName,
|
||||
host: host.host,
|
||||
user: host?.user || config.global.ssh.default_user,
|
||||
port: host?.port || config.global.ssh.default_port,
|
||||
jump_host: host?.jump_host,
|
||||
options: host?.options || config.global.ssh.default_options,
|
||||
name: host?.name || host.host,
|
||||
aliases: host?.aliases || [],
|
||||
}
|
||||
config.hosts.push(record);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.knownHosts) await loadKnownHosts();
|
||||
await loadHosts();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
const config = await loadConfig();
|
||||
|
||||
const parseJumpHost = (name: string) => {
|
||||
return config.global.ssh.jump_hosts[name];
|
||||
}
|
||||
const tuiMenu = async () => {
|
||||
let records: string = "";
|
||||
config.hosts.forEach((host: any) => {
|
||||
if (records.length > 0) records += "\n";
|
||||
records += `${host.index} | ${host.group} | ${host.host} | ${host.name}`;
|
||||
if (host.aliases.length > 0) records += ` (${host.aliases.join(", ")})`;
|
||||
records += ` | ${host.user} | ${host.port}`;
|
||||
});
|
||||
try {
|
||||
return await $`echo "${records}" | fzf -e --layout=reverse --with-nth=2,3,4,5,6 --accept-nth=1 --delimiter="|"`.text();
|
||||
} catch (error) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const buildSSHCommnand = (host: any) => {
|
||||
const args = ["ssh"];
|
||||
if (host?.port !== 22) args.push("-p", String(host.port));
|
||||
if (host?.options?.length > 0) args.push(...String(host.options).split(/\s+/).filter(Boolean));
|
||||
const jump_host = parseJumpHost(host.jump_host);
|
||||
if (jump_host) args.push("-t", jump_host, "ssh");
|
||||
args.push(`${host.user}@${host.host}`);
|
||||
return args;
|
||||
}
|
||||
|
||||
const hostIndex = Number(await tuiMenu());
|
||||
|
||||
const cmd = buildSSHCommnand(config.hosts[hostIndex]);
|
||||
console.log(cmd.join(" "));
|
||||
|
||||
await $`${cmd}`;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mtm-ssh-menu",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"run": "mtm-ssh-menu"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user