import fs from 'node:fs'; import path from 'node:path'; import inquirer from 'inquirer'; import { loadConfig, resolveConfigPaths } from './keyman.config.js'; import { copyKey } from './keyman.copy.js'; import { decryptKeys } from './keyman.decrypt.js'; import { encryptKeys } from './keyman.encrypt.js'; import { generateKey } from './keyman.generate.js'; import { listKeys } from './keyman.list.js'; import { extractAgePublicKey } from './keyman.utils.js'; // šŸ”¹ Main function to resolve paths and manage flow export async function keyman() { // Load configuration from .keymanrc.json or use defaults const config = loadConfig(); const paths = resolveConfigPaths(config); console.log(`\nšŸ“ Vault Root: ${paths.vaultRoot}`); console.log(`šŸ”‘ Keys Directory: ${paths.keysDir}`); console.log(`šŸ“‚ Temp Directory: ${paths.tmpDir}`); console.log(`šŸ” Age Key: ${paths.keyPath}\n`); // Get USER input const { user } = await inquirer.prompt<{ user: string }>([ { type: 'input', name: 'user', message: 'Specify USER (default: @current):', default: '@current', }, ]); const homeDir = user === '@current' ? process.env.HOME || '' : `/home/${user}`; if (!homeDir) { console.error('Error: Unable to determine HOME directory.'); process.exit(1); } const sshDir = path.join(homeDir, '.ssh'); // 0700 because the vault holds the age identity and, in tmp, plaintext private // keys. keysDir is created here too: decrypt used to read it before anything // created it. for (const dir of [paths.vaultRoot, paths.keysDir, paths.tmpDir]) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } // Resolved on demand, because only generate and encrypt need a recipient, and // remembered once it succeeds. Retried while it has not: creating the identity // mid-session should not mean restarting. let recipient: string | null = null; const ageRecipient = async () => { recipient ??= await extractAgePublicKey(paths.keyPath); if (!recipient) { console.error(` Create one with: age-keygen -o ${paths.keyPath}`); } return recipient; }; // Main loop - keep showing menu until user quits let running = true; while (running) { console.log(`\n${'='.repeat(50)}`); // šŸ”¹ Show category selection const { category } = await inquirer.prompt<{ category: string }>([ { type: 'list', name: 'category', message: 'Select operation:', choices: [ { name: 'šŸ“‹ List keys', value: 'list' }, { name: 'šŸ“ Copy public key', value: 'copy' }, { name: 'šŸ†• Generate key', value: 'generate' }, { name: 'šŸ”’ Encrypt keys', value: 'encrypt' }, { name: 'šŸ”“ Decrypt keys', value: 'decrypt' }, { name: 'āŒ Quit', value: 'quit' }, ], }, ]); switch (category) { case 'list': await listKeys(sshDir, paths.keysDir, paths.tmpDir); break; case 'copy': await copyKey(sshDir, paths.tmpDir); break; case 'generate': { const pubkey = await ageRecipient(); if (pubkey) { await generateKey(paths.tmpDir, paths.keysDir, pubkey); } break; } case 'encrypt': { const pubkey = await ageRecipient(); if (pubkey) { await encryptKeys(sshDir, paths.keysDir, paths.tmpDir, pubkey); } break; } case 'decrypt': await decryptKeys(sshDir, paths.keysDir, paths.tmpDir, paths.keyPath); break; case 'quit': console.log('\nšŸ‘‹ Goodbye!\n'); running = false; break; } } }