ssh-manager-pro
Version:
🔑 Professional SSH key manager with automatic clipboard integration, cross-platform support, and zero-configuration setup
345 lines (301 loc) • 10.2 kB
JavaScript
const { Command } = require("commander");
const chalk = require("chalk");
const ora = require("ora");
const inquirer = require("inquirer");
const SSHManager = require("./utils/ssh");
const ClipboardManager = require("./utils/clipboard-simple");
const StatusCommand = require("./commands/status");
const config = require("../config/default.json");
const program = new Command();
const sshManager = new SSHManager(config.ssh);
const clipboardManager = new ClipboardManager(config.clipboard);
// Global error handler
process.on("uncaughtException", (error) => {
console.error(chalk.red("✗ Unexpected error:"), error.message);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error(chalk.red("✗ Unhandled promise rejection:"), reason);
process.exit(1);
});
// Utility functions
const log = {
success: (msg) => console.log(chalk.green("✓"), msg),
error: (msg) => console.log(chalk.red("✗"), msg),
warning: (msg) => console.log(chalk.yellow("âš "), msg),
info: (msg) => console.log(chalk.blue("ℹ"), msg),
dim: (msg) => console.log(chalk.dim(msg)),
};
// Program configuration
program
.name("ssh-manager")
.description("A robust SSH key manager with clipboard integration")
.version("1.0.0")
.option("-v, --verbose", "enable verbose output")
.option("--no-color", "disable colored output");
// Generate command
program
.command("generate")
.alias("gen")
.description("Generate a new SSH key pair and copy public key to clipboard")
.option("-t, --type <type>", "key type (rsa, ed25519, ecdsa)", "rsa")
.option("-b, --bits <bits>", "key size in bits", "4096")
.option("-n, --name <name>", "key name (without extension)")
.option("-c, --comment <comment>", "key comment")
.option("-p, --passphrase <passphrase>", "key passphrase")
.option("-f, --force", "overwrite existing key")
.option("-i, --interactive", "interactive mode")
.action(async (options) => {
try {
let keyOptions = { ...options };
// Interactive mode
if (options.interactive) {
keyOptions = await promptForKeyOptions(keyOptions);
}
// Set defaults
keyOptions.keyType = keyOptions.type || "rsa";
keyOptions.keySize = parseInt(keyOptions.bits) || 4096;
keyOptions.keyName = keyOptions.name || `id_${keyOptions.keyType}`;
keyOptions.overwrite = options.force || false;
const spinner = ora("Generating SSH key pair...").start();
try {
const result = await sshManager.generateKeyPair(keyOptions);
spinner.succeed("SSH key pair generated successfully");
// Display key information
log.info(`Key type: ${result.keyType.toUpperCase()}`);
log.info(`Key size: ${result.keySize} bits`);
log.info(`Private key: ${result.privateKeyPath}`);
log.info(`Public key: ${result.publicKeyPath}`);
log.info(`Fingerprint: ${result.fingerprint}`);
// Copy public key to clipboard
const publicKey = sshManager.getPublicKey(result.publicKeyPath);
const clipResult = await clipboardManager.copyWithNotification(
publicKey,
"SSH public key"
);
log.success(clipResult.message);
log.dim(`Key length: ${clipResult.length} characters`);
} catch (error) {
spinner.fail("SSH key generation failed");
throw error;
}
} catch (error) {
log.error(error.message);
process.exit(1);
}
});
// Copy command
program
.command("copy")
.alias("cp")
.description("Copy existing SSH public key to clipboard")
.option("-n, --name <name>", "key name to copy")
.option("-l, --list", "list available keys first")
.action(async (options) => {
try {
const keys = sshManager.listKeys();
if (keys.length === 0) {
log.warning(
"No SSH keys found. Generate one first with: ssh-manager generate"
);
return;
}
let selectedKey;
if (options.list || !options.name) {
// Show available keys and let user select
selectedKey = await selectKey(keys, "Select key to copy:");
} else {
// Find key by name
selectedKey = keys.find((key) => key.name === options.name);
if (!selectedKey) {
log.error(`Key not found: ${options.name}`);
log.info("Available keys:");
keys.forEach((key) => log.dim(` - ${key.name} (${key.type})`));
return;
}
}
const spinner = ora("Copying public key to clipboard...").start();
try {
const publicKey = sshManager.getPublicKey(selectedKey.publicKeyPath);
const clipResult = await clipboardManager.copyWithNotification(
publicKey,
"SSH public key"
);
spinner.succeed("Public key copied to clipboard");
log.info(
`Key: ${selectedKey.name} (${selectedKey.type.toUpperCase()})`
);
log.dim(`Length: ${clipResult.length} characters`);
} catch (error) {
spinner.fail("Failed to copy key");
throw error;
}
} catch (error) {
log.error(error.message);
process.exit(1);
}
});
// List command
program
.command("list")
.alias("ls")
.description("List all SSH keys")
.option("-d, --detailed", "show detailed information")
.action(async (options) => {
try {
const keys = sshManager.listKeys();
if (keys.length === 0) {
log.warning("No SSH keys found");
log.info("Generate your first key with: ssh-manager generate");
return;
}
console.log(chalk.bold("\nSSH Keys:"));
console.log(chalk.dim("─".repeat(60)));
for (const key of keys) {
const status = key.exists ? chalk.green("✓") : chalk.red("✗");
const keyType = chalk.cyan(key.type.toUpperCase().padEnd(8));
const keyName = chalk.white(key.name);
console.log(`${status} ${keyType} ${keyName}`);
if (options.detailed) {
log.dim(` Public: ${key.publicKeyPath}`);
log.dim(
` Private: ${key.privateKeyPath} ${
key.exists ? "" : "(missing)"
}`
);
log.dim(` Created: ${key.created.toLocaleDateString()}`);
log.dim(` Size: ${key.size} bytes`);
console.log();
}
}
if (!options.detailed) {
log.dim("\nUse --detailed for more information");
}
} catch (error) {
log.error(error.message);
process.exit(1);
}
});
// Delete command
program
.command("delete")
.alias("del")
.description("Delete SSH key pair")
.option("-n, --name <name>", "key name to delete")
.option("-f, --force", "skip confirmation")
.action(async (options) => {
try {
const keys = sshManager.listKeys();
if (keys.length === 0) {
log.warning("No SSH keys found");
return;
}
let selectedKey;
if (!options.name) {
selectedKey = await selectKey(keys, "Select key to delete:");
} else {
selectedKey = keys.find((key) => key.name === options.name);
if (!selectedKey) {
log.error(`Key not found: ${options.name}`);
return;
}
}
// Confirmation
if (!options.force) {
const { confirmed } = await inquirer.prompt([
{
type: "confirm",
name: "confirmed",
message: `Are you sure you want to delete key "${selectedKey.name}"?`,
default: false,
},
]);
if (!confirmed) {
log.info("Deletion cancelled");
return;
}
}
const result = sshManager.deleteKey(selectedKey.name);
log.success(
`Deleted ${result.deleted.join(" and ")} key(s): ${result.keyName}`
);
} catch (error) {
log.error(error.message);
process.exit(1);
}
});
// Helper functions
async function promptForKeyOptions(existing = {}) {
const questions = [
{
type: "list",
name: "type",
message: "Select key type:",
choices: ["rsa", "ed25519", "ecdsa"],
default: existing.type || "rsa",
},
{
type: "list",
name: "bits",
message: "Select key size:",
choices: (answers) => {
if (answers.type === "rsa") return ["2048", "3072", "4096"];
if (answers.type === "ecdsa") return ["256", "384", "521"];
return ["256"]; // ed25519 has fixed size
},
default: existing.bits || "4096",
when: (answers) => answers.type !== "ed25519",
},
{
type: "input",
name: "name",
message: "Key name (without extension):",
default: (answers) => existing.name || `id_${answers.type}`,
},
{
type: "input",
name: "comment",
message: "Comment (optional):",
default: existing.comment || "",
},
];
return await inquirer.prompt(questions);
}
async function selectKey(keys, message) {
const choices = keys.map((key) => ({
name: `${key.name} (${key.type.toUpperCase()}) ${
key.exists ? "" : "- missing private key"
}`,
value: key,
}));
const { selectedKey } = await inquirer.prompt([
{
type: "list",
name: "selectedKey",
message,
choices,
},
]);
return selectedKey;
}
// Status command
program
.command("status")
.alias("st")
.description("Show SSH manager and system status")
.action(async () => {
try {
const statusCommand = new StatusCommand(config);
await statusCommand.execute();
} catch (error) {
log.error(error.message);
process.exit(1);
}
});
// Parse command line arguments
program.parse();
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
}