🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

eazyssh

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eazyssh - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+20
dist/config.d.ts
export interface SshHost {
name: string;
hostname?: string;
user?: string;
port?: string;
identityFile?: string;
extras: Record<string, string>;
}
export declare function readSshConfig(): Promise<SshHost[]>;
export declare function parseSshConfig(content: string): SshHost[];
/**
* Atomically append a host to the SSH config file using a temp file.
*/
export declare function appendHostToConfig(host: SshHost): Promise<void>;
/**
* Remove a host from the SSH config file atomically.
* Handles multiple hosts in a single Host directive and wildcard patterns.
*/
export declare function removeHostFromConfig(name: string): Promise<boolean>;
export declare function formatHostLabel(host: SshHost): string;
import { readFile, writeFile, rename } from "fs/promises";
import { homedir } from "os";
import path from "path";
import { randomBytes } from "crypto";
const SSH_CONFIG_PATH = process.env.SSH_CONFIG || path.join(homedir(), ".ssh", "config");
export async function readSshConfig() {
try {
const content = await readFile(SSH_CONFIG_PATH, "utf-8");
return parseSshConfig(content);
}
catch {
return [];
}
}
export function parseSshConfig(content) {
const hosts = [];
const lines = content.split("\n");
let currentHost = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
continue;
const hostMatch = trimmed.match(/^Host\s+(.+)$/i);
if (hostMatch) {
// Support multiple hosts in single Host directive (space-separated)
const hostNames = hostMatch[1].trim().split(/\s+/);
for (const name of hostNames) {
// Skip wildcard hosts
if (name === "*" || name.includes("*") || name.includes("?"))
continue;
if (currentHost)
hosts.push(currentHost);
currentHost = { name, extras: {} };
}
continue;
}
if (currentHost) {
const keyMatch = trimmed.match(/^(\w+)\s+(.+)$/);
if (keyMatch) {
const [, key, value] = keyMatch;
const lowerKey = key.toLowerCase();
switch (lowerKey) {
case "hostname":
currentHost.hostname = value.trim();
break;
case "user":
currentHost.user = value.trim();
break;
case "port":
currentHost.port = value.trim();
break;
case "identityfile":
currentHost.identityFile = value.trim();
break;
default:
currentHost.extras[lowerKey] = value.trim();
}
}
}
}
if (currentHost)
hosts.push(currentHost);
return hosts;
}
/**
* Atomically append a host to the SSH config file using a temp file.
*/
export async function appendHostToConfig(host) {
const lines = [
"",
`Host ${host.name}`,
];
if (host.hostname)
lines.push(` HostName ${host.hostname}`);
if (host.user)
lines.push(` User ${host.user}`);
if (host.port)
lines.push(` Port ${host.port}`);
if (host.identityFile)
lines.push(` IdentityFile ${host.identityFile}`);
for (const [key, value] of Object.entries(host.extras)) {
lines.push(` ${key} ${value}`);
}
// Read existing content first, then write atomically
try {
const existingContent = await readFile(SSH_CONFIG_PATH, "utf-8");
const newContent = existingContent + lines.join("\n") + "\n";
await writeAtomically(SSH_CONFIG_PATH, newContent);
}
catch {
// File doesn't exist, create new
await writeAtomically(SSH_CONFIG_PATH, lines.join("\n") + "\n");
}
}
/**
* Write content to a file atomically using a temp file and rename.
*/
async function writeAtomically(filePath, content) {
const dir = path.dirname(filePath);
const tempFileName = `.ssh_config_tmp_${randomBytes(8).toString("hex")}`;
const tempPath = path.join(dir, tempFileName);
try {
await writeFile(tempPath, content, "utf-8");
await rename(tempPath, filePath);
}
catch (error) {
// Clean up temp file on error
try {
await writeFile(tempPath, "", "utf-8");
}
catch {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Remove a host from the SSH config file atomically.
* Handles multiple hosts in a single Host directive and wildcard patterns.
*/
export async function removeHostFromConfig(name) {
const content = await readFile(SSH_CONFIG_PATH, "utf-8");
const lines = content.split("\n");
const result = [];
let insideTarget = false;
let found = false;
for (const line of lines) {
const trimmed = line.trim();
const hostMatch = trimmed.match(/^Host\s+(.+)$/i);
if (hostMatch) {
// Handle multiple hosts in Host directive
const hostNames = hostMatch[1].trim().split(/\s+/);
const matchingHosts = hostNames.filter(h => h === name);
if (matchingHosts.length > 0) {
found = true;
// If only one host matches, skip this Host line entirely
if (matchingHosts.length === hostNames.length) {
// All hosts in this directive match, skip the entire line
insideTarget = true;
continue;
}
else {
// Partial match - rewrite Host line with remaining hosts
const remainingHosts = hostNames.filter(h => h !== name).join(" ");
result.push(`Host ${remainingHosts}`);
insideTarget = true;
continue;
}
}
else {
insideTarget = false;
}
}
else if (insideTarget && trimmed === "") {
insideTarget = false;
continue;
}
if (!insideTarget) {
result.push(line);
}
}
await writeAtomically(SSH_CONFIG_PATH, result.join("\n") + "\n");
return found;
}
export function formatHostLabel(host) {
const name = host.name.padEnd(20);
const userHost = host.user
? `${host.user}@${host.hostname || host.name}`
: (host.hostname || host.name);
const port = host.port ? `:${host.port}` : "";
return `${name} ${userHost}${port}`;
}
import type { SshHost } from "./config.js";
export declare function hasFzf(): boolean;
export declare function selectHostWithFzf(hosts: SshHost[]): Promise<SshHost | null>;
export declare function connectSsh(host: SshHost): Promise<void>;
import { execSync } from "child_process";
import { platform } from "os";
import { formatHostLabel } from "./config.js";
export function hasFzf() {
try {
const isWindows = platform() === "win32";
const command = isWindows ? "where.exe fzf" : "which fzf";
execSync(command, { stdio: "ignore" });
return true;
}
catch {
return false;
}
}
export async function selectHostWithFzf(hosts) {
const { spawn } = await import("child_process");
return new Promise((resolve) => {
const options = [
"--ansi",
"--prompt=SSH> ",
"--height=40%",
"--reverse",
];
const proc = spawn("fzf", options, {
stdio: ["pipe", "pipe", "inherit"],
});
const input = hosts.map((h) => formatHostLabel(h)).join("\n");
proc.stdin.write(input);
proc.stdin.end();
let output = "";
proc.stdout.on("data", (data) => {
output += data.toString();
});
proc.on("close", (code) => {
if (code !== 0 || !output.trim()) {
resolve(null);
return;
}
const selected = output.trim();
const host = hosts.find((h) => formatHostLabel(h) === selected);
resolve(host || null);
});
});
}
/**
* Validate and sanitize a string for use as SSH hostname/user.
* Only allows alphanumeric characters, dots, hyphens, and underscores.
*/
function validateSshParam(value, paramName) {
// SSH hostnames and usernames should only contain these characters
const validPattern = /^[a-zA-Z0-9.\-_@]+$/;
if (!validPattern.test(value)) {
throw new Error(`Invalid ${paramName}: contains disallowed characters`);
}
return value;
}
export async function connectSsh(host) {
const { spawn } = await import("child_process");
const args = [];
// Build the target safely
let target;
try {
const hostname = validateSshParam(host.hostname || host.name, "hostname");
if (host.user) {
const user = validateSshParam(host.user, "user");
target = `${user}@${hostname}`;
}
else {
target = hostname;
}
}
catch (error) {
throw error;
}
args.push(target);
if (host.port) {
// Validate port number
const port = parseInt(host.port, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port number: ${host.port}`);
}
args.push("-p", host.port);
}
return new Promise((resolve, reject) => {
const proc = spawn("ssh", args, {
stdio: "inherit",
});
proc.on("close", (code) => {
if (code === 0)
resolve();
else if (code === null)
reject(new Error("ssh process was killed by signal"));
else
reject(new Error(`ssh exited with code ${code}`));
});
proc.on("error", reject);
});
}
#!/usr/bin/env node
export {};
#!/usr/bin/env node
import { Command } from "commander";
import { readSshConfig, appendHostToConfig, removeHostFromConfig, formatHostLabel, } from "./config.js";
import { hasFzf, selectHostWithFzf, connectSsh } from "./fzf.js";
const program = new Command();
program
.name("eazyssh")
.description("A simple SSH host manager CLI")
.version("0.1.0");
program
.command("connect")
.description("Select a host with fzf and SSH connect")
.action(async () => {
const hosts = await readSshConfig();
if (hosts.length === 0) {
console.error("No hosts found in ~/.ssh/config");
process.exit(1);
}
if (!hasFzf()) {
console.error("fzf not found. Install it: https://github.com/junegunn/fzf");
process.exit(1);
}
const selected = await selectHostWithFzf(hosts);
if (!selected) {
process.exit(0);
}
await connectSsh(selected);
});
program
.command("list")
.alias("ls")
.description("List all SSH hosts")
.action(async () => {
const hosts = await readSshConfig();
for (const host of hosts) {
console.log(formatHostLabel(host));
}
});
/**
* Validate port number string.
* Returns true if valid port number (1-65535).
*/
function isValidPort(port) {
const num = parseInt(port, 10);
return !isNaN(num) && num >= 1 && num <= 65535 && String(num) === port;
}
program
.command("add <name>")
.description("Add a new SSH host")
.option("-H, --hostname <hostname>", "Hostname or IP address")
.option("-u, --user <user>", "SSH username")
.option("-p, --port <port>", "SSH port (1-65535)")
.option("-i, --identity-file <path>", "Path to identity file")
.action(async (name, options) => {
if (!options.hostname) {
console.error("Error: --hostname is required");
process.exit(1);
}
// Validate port if provided
if (options.port && !isValidPort(options.port)) {
console.error("Error: --port must be a number between 1 and 65535");
process.exit(1);
}
const host = {
name,
hostname: options.hostname,
user: options.user,
port: options.port,
identityFile: options.identityFile,
extras: {},
};
await appendHostToConfig(host);
console.log(`Host '${name}' added successfully.`);
});
program
.command("remove <name>")
.alias("rm")
.description("Remove an SSH host")
.action(async (name) => {
const removed = await removeHostFromConfig(name);
if (removed) {
console.log(`Host '${name}' removed successfully.`);
}
else {
console.error(`Host '${name}' not found.`);
process.exit(1);
}
});
program
.command("show <name>")
.description("Show details of an SSH host")
.action(async (name) => {
const hosts = await readSshConfig();
const host = hosts.find((h) => h.name === name);
if (!host) {
console.error(`Host '${name}' not found.`);
process.exit(1);
}
console.log(`Host: ${host.name}`);
if (host.hostname)
console.log(` HostName: ${host.hostname}`);
if (host.user)
console.log(` User: ${host.user}`);
if (host.port)
console.log(` Port: ${host.port}`);
if (host.identityFile)
console.log(` IdentityFile: ${host.identityFile}`);
for (const [key, value] of Object.entries(host.extras)) {
console.log(` ${key}: ${value}`);
}
});
program.parse();
+16
-7
{
"name": "eazyssh",
"version": "1.0.0",
"description": "",
"main": "index.js",
"version": "1.0.1",
"description": "A simple SSH host manager CLI",
"type": "module",
"bin": {
"eazyssh": "./dist/index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},

@@ -14,5 +19,9 @@ "keywords": [],

"dependencies": {
"commander": "^14.0.3",
"consola": "^3.4.2"
"commander": "^14.0.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
}
}
version: 2
updates:
- package-ecosystem: "npm"
directory: "/eazyssh"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(ci)"
CLAUDE.md
{
"name": "eazyssh",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "eazyssh",
"version": "0.1.0",
"dependencies": {
"commander": "^14.0.3"
},
"bin": {
"eazyssh": "dist/index.js"
},
"devDependencies": {
"@types/node": "^25.5.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.4",
"@esbuild/android-arm": "0.27.4",
"@esbuild/android-arm64": "0.27.4",
"@esbuild/android-x64": "0.27.4",
"@esbuild/darwin-arm64": "0.27.4",
"@esbuild/darwin-x64": "0.27.4",
"@esbuild/freebsd-arm64": "0.27.4",
"@esbuild/freebsd-x64": "0.27.4",
"@esbuild/linux-arm": "0.27.4",
"@esbuild/linux-arm64": "0.27.4",
"@esbuild/linux-ia32": "0.27.4",
"@esbuild/linux-loong64": "0.27.4",
"@esbuild/linux-mips64el": "0.27.4",
"@esbuild/linux-ppc64": "0.27.4",
"@esbuild/linux-riscv64": "0.27.4",
"@esbuild/linux-s390x": "0.27.4",
"@esbuild/linux-x64": "0.27.4",
"@esbuild/netbsd-arm64": "0.27.4",
"@esbuild/netbsd-x64": "0.27.4",
"@esbuild/openbsd-arm64": "0.27.4",
"@esbuild/openbsd-x64": "0.27.4",
"@esbuild/openharmony-arm64": "0.27.4",
"@esbuild/sunos-x64": "0.27.4",
"@esbuild/win32-arm64": "0.27.4",
"@esbuild/win32-ia32": "0.27.4",
"@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.6",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
}
}
}
{
"name": "eazyssh",
"version": "0.1.0",
"description": "A simple SSH host manager CLI",
"type": "module",
"bin": {
"eazyssh": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},
"dependencies": {
"commander": "^14.0.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
}
}
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
commander:
specifier: ^14.0.3
version: 14.0.3
devDependencies:
'@types/node':
specifier: ^25.5.0
version: 25.5.0
tsx:
specifier: ^4.19.0
version: 4.21.0
typescript:
specifier: ^5.6.0
version: 5.9.3
packages:
'@esbuild/aix-ppc64@0.27.4':
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.4':
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.4':
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.4':
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.4':
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.4':
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.4':
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.4':
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.4':
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.4':
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.4':
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.4':
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.4':
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.4':
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.4':
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.4':
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.4':
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.4':
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.4':
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.4':
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.4':
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.4':
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.4':
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.4':
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.4':
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.4':
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
esbuild@0.27.4:
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-tsconfig@4.13.7:
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
hasBin: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
snapshots:
'@esbuild/aix-ppc64@0.27.4':
optional: true
'@esbuild/android-arm64@0.27.4':
optional: true
'@esbuild/android-arm@0.27.4':
optional: true
'@esbuild/android-x64@0.27.4':
optional: true
'@esbuild/darwin-arm64@0.27.4':
optional: true
'@esbuild/darwin-x64@0.27.4':
optional: true
'@esbuild/freebsd-arm64@0.27.4':
optional: true
'@esbuild/freebsd-x64@0.27.4':
optional: true
'@esbuild/linux-arm64@0.27.4':
optional: true
'@esbuild/linux-arm@0.27.4':
optional: true
'@esbuild/linux-ia32@0.27.4':
optional: true
'@esbuild/linux-loong64@0.27.4':
optional: true
'@esbuild/linux-mips64el@0.27.4':
optional: true
'@esbuild/linux-ppc64@0.27.4':
optional: true
'@esbuild/linux-riscv64@0.27.4':
optional: true
'@esbuild/linux-s390x@0.27.4':
optional: true
'@esbuild/linux-x64@0.27.4':
optional: true
'@esbuild/netbsd-arm64@0.27.4':
optional: true
'@esbuild/netbsd-x64@0.27.4':
optional: true
'@esbuild/openbsd-arm64@0.27.4':
optional: true
'@esbuild/openbsd-x64@0.27.4':
optional: true
'@esbuild/openharmony-arm64@0.27.4':
optional: true
'@esbuild/sunos-x64@0.27.4':
optional: true
'@esbuild/win32-arm64@0.27.4':
optional: true
'@esbuild/win32-ia32@0.27.4':
optional: true
'@esbuild/win32-x64@0.27.4':
optional: true
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
commander@14.0.3: {}
esbuild@0.27.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.4
'@esbuild/android-arm': 0.27.4
'@esbuild/android-arm64': 0.27.4
'@esbuild/android-x64': 0.27.4
'@esbuild/darwin-arm64': 0.27.4
'@esbuild/darwin-x64': 0.27.4
'@esbuild/freebsd-arm64': 0.27.4
'@esbuild/freebsd-x64': 0.27.4
'@esbuild/linux-arm': 0.27.4
'@esbuild/linux-arm64': 0.27.4
'@esbuild/linux-ia32': 0.27.4
'@esbuild/linux-loong64': 0.27.4
'@esbuild/linux-mips64el': 0.27.4
'@esbuild/linux-ppc64': 0.27.4
'@esbuild/linux-riscv64': 0.27.4
'@esbuild/linux-s390x': 0.27.4
'@esbuild/linux-x64': 0.27.4
'@esbuild/netbsd-arm64': 0.27.4
'@esbuild/netbsd-x64': 0.27.4
'@esbuild/openbsd-arm64': 0.27.4
'@esbuild/openbsd-x64': 0.27.4
'@esbuild/openharmony-arm64': 0.27.4
'@esbuild/sunos-x64': 0.27.4
'@esbuild/win32-arm64': 0.27.4
'@esbuild/win32-ia32': 0.27.4
'@esbuild/win32-x64': 0.27.4
fsevents@2.3.3:
optional: true
get-tsconfig@4.13.7:
dependencies:
resolve-pkg-maps: 1.0.0
resolve-pkg-maps@1.0.0: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.4
get-tsconfig: 4.13.7
optionalDependencies:
fsevents: 2.3.3
typescript@5.9.3: {}
undici-types@7.18.2: {}
import { readFile, writeFile, rename } from "fs/promises";
import { homedir } from "os";
import path from "path";
import { randomBytes } from "crypto";
export interface SshHost {
name: string;
hostname?: string;
user?: string;
port?: string;
identityFile?: string;
extras: Record<string, string>;
}
const SSH_CONFIG_PATH = process.env.SSH_CONFIG || path.join(homedir(), ".ssh", "config");
export async function readSshConfig(): Promise<SshHost[]> {
try {
const content = await readFile(SSH_CONFIG_PATH, "utf-8");
return parseSshConfig(content);
} catch {
return [];
}
}
export function parseSshConfig(content: string): SshHost[] {
const hosts: SshHost[] = [];
const lines = content.split("\n");
let currentHost: SshHost | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const hostMatch = trimmed.match(/^Host\s+(.+)$/i);
if (hostMatch) {
// Support multiple hosts in single Host directive (space-separated)
const hostNames = hostMatch[1].trim().split(/\s+/);
for (const name of hostNames) {
// Skip wildcard hosts
if (name === "*" || name.includes("*") || name.includes("?")) continue;
if (currentHost) hosts.push(currentHost);
currentHost = { name, extras: {} };
}
continue;
}
if (currentHost) {
const keyMatch = trimmed.match(/^(\w+)\s+(.+)$/);
if (keyMatch) {
const [, key, value] = keyMatch;
const lowerKey = key.toLowerCase();
switch (lowerKey) {
case "hostname":
currentHost.hostname = value.trim();
break;
case "user":
currentHost.user = value.trim();
break;
case "port":
currentHost.port = value.trim();
break;
case "identityfile":
currentHost.identityFile = value.trim();
break;
default:
currentHost.extras[lowerKey] = value.trim();
}
}
}
}
if (currentHost) hosts.push(currentHost);
return hosts;
}
/**
* Atomically append a host to the SSH config file using a temp file.
*/
export async function appendHostToConfig(host: SshHost): Promise<void> {
const lines = [
"",
`Host ${host.name}`,
];
if (host.hostname) lines.push(` HostName ${host.hostname}`);
if (host.user) lines.push(` User ${host.user}`);
if (host.port) lines.push(` Port ${host.port}`);
if (host.identityFile) lines.push(` IdentityFile ${host.identityFile}`);
for (const [key, value] of Object.entries(host.extras)) {
lines.push(` ${key} ${value}`);
}
// Read existing content first, then write atomically
try {
const existingContent = await readFile(SSH_CONFIG_PATH, "utf-8");
const newContent = existingContent + lines.join("\n") + "\n";
await writeAtomically(SSH_CONFIG_PATH, newContent);
} catch {
// File doesn't exist, create new
await writeAtomically(SSH_CONFIG_PATH, lines.join("\n") + "\n");
}
}
/**
* Write content to a file atomically using a temp file and rename.
*/
async function writeAtomically(filePath: string, content: string): Promise<void> {
const dir = path.dirname(filePath);
const tempFileName = `.ssh_config_tmp_${randomBytes(8).toString("hex")}`;
const tempPath = path.join(dir, tempFileName);
try {
await writeFile(tempPath, content, "utf-8");
await rename(tempPath, filePath);
} catch (error) {
// Clean up temp file on error
try {
await writeFile(tempPath, "", "utf-8");
} catch {
// Ignore cleanup errors
}
throw error;
}
}
/**
* Remove a host from the SSH config file atomically.
* Handles multiple hosts in a single Host directive and wildcard patterns.
*/
export async function removeHostFromConfig(name: string): Promise<boolean> {
const content = await readFile(SSH_CONFIG_PATH, "utf-8");
const lines = content.split("\n");
const result: string[] = [];
let insideTarget = false;
let found = false;
for (const line of lines) {
const trimmed = line.trim();
const hostMatch = trimmed.match(/^Host\s+(.+)$/i);
if (hostMatch) {
// Handle multiple hosts in Host directive
const hostNames = hostMatch[1].trim().split(/\s+/);
const matchingHosts = hostNames.filter(h => h === name);
if (matchingHosts.length > 0) {
found = true;
// If only one host matches, skip this Host line entirely
if (matchingHosts.length === hostNames.length) {
// All hosts in this directive match, skip the entire line
insideTarget = true;
continue;
} else {
// Partial match - rewrite Host line with remaining hosts
const remainingHosts = hostNames.filter(h => h !== name).join(" ");
result.push(`Host ${remainingHosts}`);
insideTarget = true;
continue;
}
} else {
insideTarget = false;
}
} else if (insideTarget && trimmed === "") {
// Empty line ends the current host block
insideTarget = false;
}
if (!insideTarget) {
result.push(line);
}
}
await writeAtomically(SSH_CONFIG_PATH, result.join("\n") + "\n");
return found;
}
export function formatHostLabel(host: SshHost): string {
const name = host.name.padEnd(20);
const userHost = host.user
? `${host.user}@${host.hostname || host.name}`
: (host.hostname || host.name);
const port = host.port ? `:${host.port}` : "";
return `${name} ${userHost}${port}`;
}
import { execSync, spawn } from "child_process";
import { platform } from "os";
import type { SshHost } from "./config.js";
import { formatHostLabel } from "./config.js";
export function hasFzf(): boolean {
try {
const isWindows = platform() === "win32";
const command = isWindows ? "where.exe fzf" : "which fzf";
execSync(command, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
export async function selectHostWithFzf(
hosts: SshHost[]
): Promise<SshHost | null> {
const { spawn } = await import("child_process");
return new Promise((resolve) => {
const options = [
"--ansi",
"--prompt=SSH> ",
"--height=40%",
"--reverse",
];
const proc = spawn("fzf", options, {
stdio: ["pipe", "pipe", "inherit"],
});
const input = hosts.map((h) => formatHostLabel(h)).join("\n");
proc.stdin.write(input);
proc.stdin.end();
let output = "";
proc.stdout.on("data", (data: Buffer) => {
output += data.toString();
});
proc.on("close", (code: number) => {
if (code !== 0 || !output.trim()) {
resolve(null);
return;
}
const selected = output.trim();
const host = hosts.find((h) => formatHostLabel(h) === selected);
resolve(host || null);
});
});
}
/**
* Validate and sanitize a string for use as SSH hostname/user.
* Only allows alphanumeric characters, dots, hyphens, and underscores.
*/
function validateSshParam(value: string, paramName: string): string {
// SSH hostnames and usernames should only contain these characters
const validPattern = /^[a-zA-Z0-9.\-_@]+$/;
if (!validPattern.test(value)) {
throw new Error(`Invalid ${paramName}: contains disallowed characters`);
}
return value;
}
export async function connectSsh(host: SshHost): Promise<void> {
const { spawn } = await import("child_process");
const args: string[] = [];
// Build the target safely
let target: string;
try {
const hostname = validateSshParam(host.hostname || host.name, "hostname");
if (host.user) {
const user = validateSshParam(host.user, "user");
target = `${user}@${hostname}`;
} else {
target = hostname;
}
} catch (error) {
throw error;
}
args.push(target);
if (host.port) {
// Validate port number
const port = parseInt(host.port, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`Invalid port number: ${host.port}`);
}
args.push("-p", host.port);
}
return new Promise((resolve, reject) => {
const proc = spawn("ssh", args, {
stdio: "inherit",
});
proc.on("close", (code) => {
if (code === 0) resolve();
else reject(new Error(`ssh exited with code ${code}`));
});
proc.on("error", reject);
});
}
#!/usr/bin/env node
import { Command } from "commander";
import {
readSshConfig,
appendHostToConfig,
removeHostFromConfig,
formatHostLabel,
type SshHost,
} from "./config.js";
import { hasFzf, selectHostWithFzf, connectSsh } from "./fzf.js";
const program = new Command();
program
.name("eazyssh")
.description("A simple SSH host manager CLI")
.version("0.1.0");
program
.command("connect")
.description("Select a host with fzf and SSH connect")
.action(async () => {
const hosts = await readSshConfig();
if (hosts.length === 0) {
console.error("No hosts found in ~/.ssh/config");
process.exit(1);
}
if (!hasFzf()) {
console.error("fzf not found. Install it: https://github.com/junegunn/fzf");
process.exit(1);
}
const selected = await selectHostWithFzf(hosts);
if (!selected) {
process.exit(0);
}
await connectSsh(selected);
});
program
.command("list")
.alias("ls")
.description("List all SSH hosts")
.action(async () => {
const hosts = await readSshConfig();
for (const host of hosts) {
console.log(formatHostLabel(host));
}
});
/**
* Validate port number string.
* Returns true if valid port number (1-65535).
*/
function isValidPort(port: string): boolean {
const num = parseInt(port, 10);
return !isNaN(num) && num >= 1 && num <= 65535 && String(num) === port;
}
program
.command("add <name>")
.description("Add a new SSH host")
.option("-H, --hostname <hostname>", "Hostname or IP address")
.option("-u, --user <user>", "SSH username")
.option("-p, --port <port>", "SSH port (1-65535)")
.option("-i, --identity-file <path>", "Path to identity file")
.action(async (name, options) => {
if (!options.hostname) {
console.error("Error: --hostname is required");
process.exit(1);
}
// Validate port if provided
if (options.port && !isValidPort(options.port)) {
console.error("Error: --port must be a number between 1 and 65535");
process.exit(1);
}
const host: SshHost = {
name,
hostname: options.hostname,
user: options.user,
port: options.port,
identityFile: options.identityFile,
extras: {},
};
await appendHostToConfig(host);
console.log(`Host '${name}' added successfully.`);
});
program
.command("remove <name>")
.alias("rm")
.description("Remove an SSH host")
.action(async (name) => {
const removed = await removeHostFromConfig(name);
if (removed) {
console.log(`Host '${name}' removed successfully.`);
} else {
console.error(`Host '${name}' not found.`);
process.exit(1);
}
});
program
.command("show <name>")
.description("Show details of an SSH host")
.action(async (name) => {
const hosts = await readSshConfig();
const host = hosts.find((h) => h.name === name);
if (!host) {
console.error(`Host '${name}' not found.`);
process.exit(1);
}
console.log(`Host: ${host.name}`);
if (host.hostname) console.log(` HostName: ${host.hostname}`);
if (host.user) console.log(` User: ${host.user}`);
if (host.port) console.log(` Port: ${host.port}`);
if (host.identityFile) console.log(` IdentityFile: ${host.identityFile}`);
for (const [key, value] of Object.entries(host.extras)) {
console.log(` ${key}: ${value}`);
}
});
program.parse();
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1773821835,
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

Sorry, the diff of this file is not supported yet

# eazyssh
`~/.ssh/config` を読み書きして、fzf でホストを選んで SSH 接続する CLI ツール。
## インストール
### npm
```bash
git clone https://github.com/nazozokc/simple-ssh
cd simple-ssh
npm install && npm run build
npm link
```
### Nix
```bash
nix run github:nazozokc/simple-ssh
```
## 使い方
```bash
eazyssh # fzf でホストを選んで接続(デフォルト)
eazyssh ls # ホスト一覧
eazyssh add myserver -H 192.168.1.10 -u nazozo -i ~/.ssh/id_ed25519
eazyssh show myserver
eazyssh rm myserver
```
### `add` オプション
| オプション | 説明 |
|---|---|
| `-H, --hostname` | 接続先ホスト名 or IP |
| `-u, --user` | ログインユーザー |
| `-p, --port` | ポート番号 |
| `-i, --identity` | 秘密鍵のパス |
## 依存
- [fzf](https://github.com/junegunn/cli) — ホスト選択UI
- Node.js 20+
## ライセンス
MIT