UNPKG

ssh-kim-cli

Version:

A powerful command-line interface for managing SSH keys with encryption, search, and import/export capabilities

1,106 lines (978 loc) 34.3 kB
import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import clipboardy from 'clipboardy'; import Conf from 'conf'; import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import crypto from 'crypto'; import os from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class SSHKeyManager { constructor() { this.config = new Conf({ projectName: 'ssh-kim-cli', defaults: { keysFilePath: null, defaultSSHDir: this.getDefaultSSHDir(), encryptionPassword: null } }); this.machineKey = this.getMachineKey(); this.passwordKey = this.config.get('encryptionPassword') ? this.deriveKeyFromPassword(this.config.get('encryptionPassword')) : null; this.keysCache = null; } // Get default SSH directory based on platform getDefaultSSHDir() { const homeDir = os.homedir(); return path.join(homeDir, '.ssh'); } // Get machine-specific encryption key getMachineKey() { const machineId = this.getMachineId(); const hash = crypto.createHash('sha256'); hash.update(machineId + 'ssh-kim-machine-key'); return hash.digest(); } // Get machine identifier getMachineId() { // Try hostname first const hostname = os.hostname(); if (hostname && hostname !== 'localhost') { return hostname; } // Fallback to computer name on macOS if (process.platform === 'darwin') { try { const { execSync } = require('child_process'); const computerName = execSync('scutil --get ComputerName', { encoding: 'utf8' }).trim(); if (computerName) return computerName; } catch (error) { // Fallback to hostname } } return 'unknown-machine'; } // Derive encryption key from password deriveKeyFromPassword(password) { const hash = crypto.createHash('sha256'); hash.update(password + 'ssh-kim-password-salt'); return hash.digest(); } // Get encryption key (password-based if set, otherwise machine-specific) getEncryptionKey() { return this.passwordKey || this.machineKey; } // Get the path to the encrypted SSH keys file getKeysFilePath() { const customPath = this.config.get('keysFilePath'); if (customPath) { return customPath; } // Default to app data directory const appDataDir = path.join(process.cwd(), 'data'); if (!fs.existsSync(appDataDir)) { fs.mkdirpSync(appDataDir); } return path.join(appDataDir, 'ssh_keys.enc'); } // Encryption/Decryption methods encryptData(data) { const algorithm = 'aes-256-cbc'; const key = this.getEncryptionKey(); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } decryptData(encryptedData) { const algorithm = 'aes-256-cbc'; const key = this.getEncryptionKey(); const parts = encryptedData.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipheriv(algorithm, key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Load keys from file async loadKeys() { if (this.keysCache) { return this.keysCache; } const filePath = this.getKeysFilePath(); if (!fs.existsSync(filePath)) { this.keysCache = []; return this.keysCache; } try { const encryptedData = await fs.readFile(filePath, 'utf8'); const decryptedData = this.decryptData(encryptedData); this.keysCache = JSON.parse(decryptedData); return this.keysCache; } catch (error) { console.error(chalk.red('Error loading keys:'), error.message); this.keysCache = []; return this.keysCache; } } // Save keys to file async saveKeys(keys) { const filePath = this.getKeysFilePath(); const data = JSON.stringify(keys, null, 2); const encryptedData = this.encryptData(data); await fs.writeFile(filePath, encryptedData, 'utf8'); this.keysCache = keys; } // Generate unique ID generateId() { return crypto.randomUUID(); } // Detect key type from content detectKeyType(keyContent) { if (keyContent.includes('ssh-rsa')) return 'RSA'; if (keyContent.includes('ssh-dss')) return 'DSA'; if (keyContent.includes('ecdsa-sha2')) return 'ECDSA'; if (keyContent.includes('ssh-ed25519')) return 'Ed25519'; return 'Unknown'; } // List all keys async listKeys(options = {}) { const spinner = ora('Loading SSH keys...').start(); try { const keys = await this.loadKeys(); spinner.stop(); if (keys.length === 0) { console.log(chalk.yellow('No SSH keys found.')); console.log(chalk.blue('Use "ssh-kim add" to add your first key.')); return; } // Apply filters let filteredKeys = keys; if (options.search) { const searchTerm = options.search.toLowerCase(); filteredKeys = filteredKeys.filter(key => key.name.toLowerCase().includes(searchTerm) || (key.tag && key.tag.toLowerCase().includes(searchTerm)) || key.key_type.toLowerCase().includes(searchTerm) ); } if (options.tag) { filteredKeys = filteredKeys.filter(key => key.tag && key.tag.toLowerCase() === options.tag.toLowerCase() ); } if (options.type) { filteredKeys = filteredKeys.filter(key => key.key_type.toLowerCase() === options.type.toLowerCase() ); } if (filteredKeys.length === 0) { console.log(chalk.yellow('No keys match the specified criteria.')); return; } // Display keys in table format console.log(chalk.bold.blue(`\nSSH Keys (${filteredKeys.length} found):\n`)); filteredKeys.forEach((key, index) => { const tagDisplay = key.tag ? chalk.cyan(`[${key.tag}]`) : ''; const dateDisplay = new Date(key.last_modified).toLocaleDateString(); console.log(chalk.bold(`${index + 1}. ${key.name}`) + ` ${tagDisplay}`); console.log(` ID: ${chalk.gray(key.id)}`); console.log(` Type: ${chalk.green(key.key_type)}`); console.log(` Modified: ${chalk.gray(dateDisplay)}`); console.log(` Key: ${chalk.gray(key.key.substring(0, 50))}...`); console.log(''); }); } catch (error) { spinner.stop(); console.error(chalk.red('Error listing keys:'), error.message); } } // Add a new key async addKey(options = {}) { try { let name = options.name; let tag = options.tag; let keyContent = options.content; let sourcePath = ''; // Interactive mode if not all options provided if (!name || !keyContent) { const answers = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Enter key name:', default: name, validate: (input) => input.trim() ? true : 'Name is required' }, { type: 'input', name: 'tag', message: 'Enter key tag (optional):', default: tag }, { type: 'list', name: 'source', message: 'How would you like to add the key?', choices: [ { name: 'Enter key content manually', value: 'manual' }, { name: 'Read from file', value: 'file' }, { name: 'Scan common locations', value: 'scan' } ] } ]); name = answers.name; tag = answers.tag || null; if (answers.source === 'manual') { const contentAnswer = await inquirer.prompt([ { type: 'editor', name: 'content', message: 'Enter SSH key content:', validate: (input) => input.trim() ? true : 'Key content is required' } ]); keyContent = contentAnswer.content.trim(); } else if (answers.source === 'file') { const fileAnswer = await inquirer.prompt([ { type: 'input', name: 'filePath', message: 'Enter path to SSH key file:', validate: (input) => { try { return fs.existsSync(input) ? true : 'File does not exist'; } catch { return 'Invalid file path'; } } } ]); sourcePath = fileAnswer.filePath; keyContent = await fs.readFile(fileAnswer.filePath, 'utf8'); } else if (answers.source === 'scan') { const scannedKeys = await this.scanCommonLocations(); if (scannedKeys.length === 0) { console.log(chalk.yellow('No SSH keys found in common locations.')); return; } const keyChoices = scannedKeys.map(key => ({ name: `${key.name} (${key.path})`, value: key })); const selectedKey = await inquirer.prompt([ { type: 'list', name: 'key', message: 'Select a key to add:', choices: keyChoices } ]); keyContent = selectedKey.key.content; sourcePath = selectedKey.key.path; } } // Validate key content if (!keyContent || !keyContent.trim()) { throw new Error('Key content is required'); } const keyType = this.detectKeyType(keyContent); const now = new Date().toISOString(); const newKey = { id: this.generateId(), name: name.trim(), tag: tag ? tag.trim() : null, key: keyContent.trim(), key_type: keyType, created: now, last_modified: now }; const keys = await this.loadKeys(); keys.push(newKey); await this.saveKeys(keys); console.log(chalk.green(`✓ SSH key "${name}" added successfully!`)); console.log(chalk.gray(`ID: ${newKey.id}`)); console.log(chalk.gray(`Type: ${keyType}`)); } catch (error) { console.error(chalk.red('Error adding key:'), error.message); } } // Edit a key async editKey(id, options = {}) { try { const keys = await this.loadKeys(); const keyIndex = keys.findIndex(k => k.id === id); if (keyIndex === -1) { console.error(chalk.red(`Key with ID "${id}" not found.`)); return; } const key = keys[keyIndex]; let updates = {}; // Interactive mode if not all options provided if (!options.name && !options.tag && !options.content) { const answers = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Enter new key name:', default: key.name }, { type: 'input', name: 'tag', message: 'Enter new key tag:', default: key.tag || '' }, { type: 'confirm', name: 'updateContent', message: 'Do you want to update the key content?', default: false } ]); updates.name = answers.name.trim(); updates.tag = answers.tag.trim() || null; if (answers.updateContent) { const contentAnswer = await inquirer.prompt([ { type: 'editor', name: 'content', message: 'Enter new SSH key content:', default: key.key } ]); updates.key = contentAnswer.content.trim(); } } else { if (options.name) updates.name = options.name; if (options.tag !== undefined) updates.tag = options.tag || null; if (options.content) updates.key = options.content; } // Apply updates Object.assign(keys[keyIndex], updates, { last_modified: new Date().toISOString() }); await this.saveKeys(keys); console.log(chalk.green(`✓ SSH key "${keys[keyIndex].name}" updated successfully!`)); } catch (error) { console.error(chalk.red('Error updating key:'), error.message); } } // Delete a key async deleteKey(id, options = {}) { try { const keys = await this.loadKeys(); const keyIndex = keys.findIndex(k => k.id === id); if (keyIndex === -1) { console.error(chalk.red(`Key with ID "${id}" not found.`)); return; } const key = keys[keyIndex]; // Confirmation if (!options.force) { const answer = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to delete "${key.name}"?`, default: false } ]); if (!answer.confirm) { console.log(chalk.yellow('Deletion cancelled.')); return; } } keys.splice(keyIndex, 1); await this.saveKeys(keys); console.log(chalk.green(`✓ SSH key "${key.name}" deleted successfully!`)); } catch (error) { console.error(chalk.red('Error deleting key:'), error.message); } } // Copy key to clipboard async copyKey(id) { try { const keys = await this.loadKeys(); const key = keys.find(k => k.id === id); if (!key) { console.error(chalk.red(`Key with ID "${id}" not found.`)); return; } await clipboardy.write(key.key); console.log(chalk.green(`✓ SSH key "${key.name}" copied to clipboard!`)); } catch (error) { console.error(chalk.red('Error copying key:'), error.message); } } // Scan common SSH locations async scanKeys(options = {}) { const spinner = ora('Scanning for SSH keys...').start(); try { const locations = this.getCommonSSHLocations(); const foundKeys = []; for (const location of locations) { if (fs.existsSync(location)) { const files = await fs.readdir(location); for (const file of files) { if (file.endsWith('.pub')) { const filePath = path.join(location, file); try { const content = await fs.readFile(filePath, 'utf8'); const keyType = this.detectKeyType(content); foundKeys.push({ name: file, path: filePath, content: content.trim(), type: keyType }); } catch (error) { // Skip files that can't be read } } } } } spinner.stop(); if (foundKeys.length === 0) { console.log(chalk.yellow('No SSH keys found in common locations.')); return; } console.log(chalk.bold.blue(`\nFound ${foundKeys.length} SSH keys:\n`)); foundKeys.forEach((key, index) => { console.log(chalk.bold(`${index + 1}. ${key.name}`)); console.log(` Path: ${chalk.gray(key.path)}`); console.log(` Type: ${chalk.green(key.type)}`); console.log(` Key: ${chalk.gray(key.content.substring(0, 50))}...`); console.log(''); }); } catch (error) { spinner.stop(); console.error(chalk.red('Error scanning keys:'), error.message); } } // Get common SSH locations getCommonSSHLocations() { const homeDir = os.homedir(); const locations = [ path.join(homeDir, '.ssh'), path.join(homeDir, 'ssh'), path.join(homeDir, 'Documents', 'ssh') ]; // Add Windows-specific locations if (process.platform === 'win32') { locations.push( path.join(process.env.APPDATA, 'PuTTY'), path.join(process.env.LOCALAPPDATA, 'ssh') ); } return locations; } // Scan common locations and return key objects async scanCommonLocations() { const locations = this.getCommonSSHLocations(); const foundKeys = []; for (const location of locations) { if (fs.existsSync(location)) { const files = await fs.readdir(location); for (const file of files) { if (file.endsWith('.pub')) { const filePath = path.join(location, file); try { const content = await fs.readFile(filePath, 'utf8'); const keyType = this.detectKeyType(content); foundKeys.push({ name: file, path: filePath, content: content.trim(), type: keyType }); } catch (error) { // Skip files that can't be read } } } } } return foundKeys; } // Import keys async importKeys(options = {}) { try { if (options.file) { await this.importFromFile(options.file, options.password); } else if (options.directory) { await this.importFromDirectory(options.directory); } else { const answer = await inquirer.prompt([ { type: 'list', name: 'importType', message: 'How would you like to import keys?', choices: [ { name: 'Import from file', value: 'file' }, { name: 'Import from directory', value: 'directory' }, { name: 'Scan common locations', value: 'scan' } ] } ]); if (answer.importType === 'file') { const fileAnswer = await inquirer.prompt([ { type: 'input', name: 'filePath', message: 'Enter path to SSH key file:', validate: (input) => { try { return fs.existsSync(input) ? true : 'File does not exist'; } catch { return 'Invalid file path'; } } } ]); await this.importFromFile(fileAnswer.filePath); } else if (answer.importType === 'directory') { const dirAnswer = await inquirer.prompt([ { type: 'input', name: 'dirPath', message: 'Enter directory path:', validate: (input) => { try { return fs.existsSync(input) ? true : 'Directory does not exist'; } catch { return 'Invalid directory path'; } } } ]); await this.importFromDirectory(dirAnswer.dirPath); } else { await this.importFromScan(); } } } catch (error) { console.error(chalk.red('Error importing keys:'), error.message); } } // Import from file async importFromFile(filePath, password = null) { try { let content; if (password) { // Handle encrypted import const encryptedData = await fs.readFile(filePath, 'utf8'); const tempKey = this.deriveKeyFromPassword(password); const parts = encryptedData.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipheriv('aes-256-cbc', tempKey, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); content = decrypted; } else { content = await fs.readFile(filePath, 'utf8'); } const data = JSON.parse(content); const keys = data.keys || [data]; // Handle both array and single key const existingKeys = await this.loadKeys(); let importedCount = 0; let duplicateCount = 0; for (const keyData of keys) { // Check for duplicates const isDuplicate = existingKeys.some(existing => existing.key === keyData.key || existing.name === keyData.name ); if (!isDuplicate) { const newKey = { id: this.generateId(), name: keyData.name, tag: keyData.tag || null, key: keyData.key, key_type: keyData.key_type || this.detectKeyType(keyData.key), created: keyData.created || new Date().toISOString(), last_modified: new Date().toISOString() }; existingKeys.push(newKey); importedCount++; } else { duplicateCount++; } } await this.saveKeys(existingKeys); console.log(chalk.green(`✓ Imported ${importedCount} keys successfully!`)); if (duplicateCount > 0) { console.log(chalk.yellow(`Skipped ${duplicateCount} duplicate keys.`)); } } catch (error) { console.error(chalk.red('Error importing from file:'), error.message); } } // Import from directory async importFromDirectory(dirPath) { const files = await fs.readdir(dirPath); const pubFiles = files.filter(file => file.endsWith('.pub')); if (pubFiles.length === 0) { console.log(chalk.yellow('No SSH public key files found in directory.')); return; } const keys = await this.loadKeys(); let importedCount = 0; for (const file of pubFiles) { const filePath = path.join(dirPath, file); try { const content = await fs.readFile(filePath, 'utf8'); const keyType = this.detectKeyType(content); const newKey = { id: this.generateId(), name: file, tag: null, key: content.trim(), key_type: keyType, created: new Date().toISOString(), last_modified: new Date().toISOString() }; keys.push(newKey); importedCount++; } catch (error) { console.log(chalk.yellow(`Skipped ${file}: ${error.message}`)); } } await this.saveKeys(keys); console.log(chalk.green(`✓ Imported ${importedCount} keys successfully!`)); } // Import from scan async importFromScan() { const scannedKeys = await this.scanCommonLocations(); if (scannedKeys.length === 0) { console.log(chalk.yellow('No SSH keys found in common locations.')); return; } const keyChoices = scannedKeys.map(key => ({ name: `${key.name} (${key.path})`, value: key, checked: true })); const selectedKeys = await inquirer.prompt([ { type: 'checkbox', name: 'keys', message: 'Select keys to import:', choices: keyChoices } ]); if (selectedKeys.keys.length === 0) { console.log(chalk.yellow('No keys selected for import.')); return; } const keys = await this.loadKeys(); let importedCount = 0; for (const selectedKey of selectedKeys.keys) { const newKey = { id: this.generateId(), name: selectedKey.name, tag: null, key: selectedKey.content, key_type: selectedKey.type, created: new Date().toISOString(), last_modified: new Date().toISOString() }; keys.push(newKey); importedCount++; } await this.saveKeys(keys); console.log(chalk.green(`✓ Imported ${importedCount} keys successfully!`)); } // Export keys async exportKeys(options = {}) { try { const keys = await this.loadKeys(); if (keys.length === 0) { console.log(chalk.yellow('No keys to export.')); return; } let keysToExport = keys; if (options.id) { const key = keys.find(k => k.id === options.id); if (!key) { console.error(chalk.red(`Key with ID "${options.id}" not found.`)); return; } keysToExport = [key]; } let outputPath = options.file; if (!outputPath) { const answer = await inquirer.prompt([ { type: 'input', name: 'filePath', message: 'Enter output file path:', default: `ssh_keys_export_${new Date().toISOString().split('T')[0]}.json` } ]); outputPath = answer.filePath; } const exportData = { exported_at: new Date().toISOString(), total_keys: keysToExport.length, keys: keysToExport }; if (options.password) { // Encrypted export const data = JSON.stringify(exportData, null, 2); const tempKey = this.deriveKeyFromPassword(options.password); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', tempKey, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const encryptedData = iv.toString('hex') + ':' + encrypted; await fs.writeFile(outputPath, encryptedData); console.log(chalk.green(`✓ Exported ${keysToExport.length} keys to "${outputPath}" (encrypted)`)); } else { // Plain export await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2)); console.log(chalk.green(`✓ Exported ${keysToExport.length} keys to "${outputPath}"`)); } } catch (error) { console.error(chalk.red('Error exporting keys:'), error.message); } } // Show key details async showKey(id) { try { const keys = await this.loadKeys(); const key = keys.find(k => k.id === id); if (!key) { console.error(chalk.red(`Key with ID "${id}" not found.`)); return; } console.log(chalk.bold.blue(`\nSSH Key Details:\n`)); console.log(chalk.bold(`Name: ${key.name}`)); console.log(chalk.gray(`ID: ${key.id}`)); if (key.tag) { console.log(chalk.cyan(`Tag: ${key.tag}`)); } console.log(chalk.green(`Type: ${key.key_type}`)); console.log(chalk.gray(`Created: ${new Date(key.created).toLocaleString()}`)); console.log(chalk.gray(`Modified: ${new Date(key.last_modified).toLocaleString()}`)); console.log(chalk.bold(`\nKey Content:`)); console.log(chalk.gray(key.key)); } catch (error) { console.error(chalk.red('Error showing key:'), error.message); } } // Manage configuration async manageConfig(options = {}) { try { if (options.show) { console.log(chalk.bold.blue(`\nCurrent Configuration:\n`)); console.log(`Keys File Path: ${chalk.gray(this.getKeysFilePath())}`); console.log(`Default SSH Dir: ${chalk.gray(this.config.get('defaultSSHDir'))}`); console.log(`Custom Path Set: ${chalk.gray(this.config.get('keysFilePath') ? 'Yes' : 'No')}`); console.log(`Encryption Mode: ${chalk.gray(this.passwordKey ? 'Password-based' : 'Machine-specific')}`); } else if (options.path) { this.config.set('keysFilePath', options.path); console.log(chalk.green(`✓ Custom keys file path set to: ${options.path}`)); } else if (options.reset) { this.config.delete('keysFilePath'); this.config.delete('encryptionPassword'); this.passwordKey = null; console.log(chalk.green(`✓ Configuration reset to defaults.`)); } else if (options.setPassword) { this.config.set('encryptionPassword', options.setPassword); this.passwordKey = this.deriveKeyFromPassword(options.setPassword); console.log(chalk.green(`✓ Encryption password set successfully.`)); } else if (options.clearPassword) { this.config.delete('encryptionPassword'); this.passwordKey = null; console.log(chalk.green(`✓ Encryption password cleared. Using machine-specific encryption.`)); } else { const answer = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'Show current configuration', value: 'show' }, { name: 'Set custom keys file path', value: 'path' }, { name: 'Set encryption password', value: 'setPassword' }, { name: 'Clear encryption password', value: 'clearPassword' }, { name: 'Reset to defaults', value: 'reset' } ] } ]); if (answer.action === 'show') { await this.manageConfig({ show: true }); } else if (answer.action === 'path') { const pathAnswer = await inquirer.prompt([ { type: 'input', name: 'filePath', message: 'Enter custom keys file path:', default: this.getKeysFilePath() } ]); await this.manageConfig({ path: pathAnswer.filePath }); } else if (answer.action === 'setPassword') { const passwordAnswer = await inquirer.prompt([ { type: 'password', name: 'password', message: 'Enter encryption password:', validate: (input) => input.length >= 6 ? true : 'Password must be at least 6 characters' } ]); await this.manageConfig({ setPassword: passwordAnswer.password }); } else if (answer.action === 'clearPassword') { await this.manageConfig({ clearPassword: true }); } else if (answer.action === 'reset') { await this.manageConfig({ reset: true }); } } } catch (error) { console.error(chalk.red('Error managing configuration:'), error.message); } } // Interactive mode async interactiveMode() { console.log(chalk.bold.blue('SSH Key Manager - Interactive Mode\n')); while (true) { const answer = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'List all keys', value: 'list' }, { name: 'Add new key', value: 'add' }, { name: 'Edit key', value: 'edit' }, { name: 'Delete key', value: 'delete' }, { name: 'Copy key to clipboard', value: 'copy' }, { name: 'Scan for keys', value: 'scan' }, { name: 'Import keys', value: 'import' }, { name: 'Export keys', value: 'export' }, { name: 'Show key details', value: 'show' }, { name: 'Configuration', value: 'config' }, { name: 'Exit', value: 'exit' } ] } ]); try { switch (answer.action) { case 'list': await this.listKeys(); break; case 'add': await this.addKey(); break; case 'edit': const keys = await this.loadKeys(); if (keys.length === 0) { console.log(chalk.yellow('No keys to edit.')); break; } const editChoices = keys.map(key => ({ name: `${key.name} (${key.id})`, value: key.id })); const editAnswer = await inquirer.prompt([ { type: 'list', name: 'keyId', message: 'Select key to edit:', choices: editChoices } ]); await this.editKey(editAnswer.keyId); break; case 'delete': const deleteKeys = await this.loadKeys(); if (deleteKeys.length === 0) { console.log(chalk.yellow('No keys to delete.')); break; } const deleteChoices = deleteKeys.map(key => ({ name: `${key.name} (${key.id})`, value: key.id })); const deleteAnswer = await inquirer.prompt([ { type: 'list', name: 'keyId', message: 'Select key to delete:', choices: deleteChoices } ]); await this.deleteKey(deleteAnswer.keyId); break; case 'copy': const copyKeys = await this.loadKeys(); if (copyKeys.length === 0) { console.log(chalk.yellow('No keys to copy.')); break; } const copyChoices = copyKeys.map(key => ({ name: `${key.name} (${key.id})`, value: key.id })); const copyAnswer = await inquirer.prompt([ { type: 'list', name: 'keyId', message: 'Select key to copy:', choices: copyChoices } ]); await this.copyKey(copyAnswer.keyId); break; case 'scan': await this.scanKeys(); break; case 'import': await this.importKeys(); break; case 'export': await this.exportKeys(); break; case 'show': const showKeys = await this.loadKeys(); if (showKeys.length === 0) { console.log(chalk.yellow('No keys to show.')); break; } const showChoices = showKeys.map(key => ({ name: `${key.name} (${key.id})`, value: key.id })); const showAnswer = await inquirer.prompt([ { type: 'list', name: 'keyId', message: 'Select key to show:', choices: showChoices } ]); await this.showKey(showAnswer.keyId); break; case 'config': await this.manageConfig(); break; case 'exit': console.log(chalk.blue('Goodbye!')); return; } } catch (error) { console.error(chalk.red('Error:'), error.message); } console.log(''); // Add spacing } } }