import { Command } from 'commander'; import { createInterface } from 'readline'; import { authenticate } from '../api.js'; import { saveCredentials, loadCredentials, clearCredentials } from '../credentials.js'; import { printJson, printTable, formatTimestamp } from '../output.js'; /** * Read all stdin lines upfront when not a TTY (piped input). */ async function readStdinLines(): Promise { if (process.stdin.isTTY) return []; return new Promise((resolve) => { const lines: string[] = []; const rl = createInterface({ input: process.stdin, terminal: false }); rl.on('line', (line) => lines.push(line.trim())); rl.on('close', () => resolve(lines)); }); } function promptTTY(question: string, hidden: boolean): Promise { return new Promise((resolve) => { if (hidden) { // Disable echo for password process.stdout.write(question); process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdin.setEncoding('utf8'); let input = ''; const onData = (ch: string) => { if (ch === '\n' || ch === '\r' || ch === '\u0003') { process.stdin.setRawMode?.(false); process.stdin.pause(); process.stdin.removeListener('data', onData); process.stdout.write('\n'); resolve(input); } else if (ch === '\u007F') { input = input.slice(0, -1); } else { input += ch; } }; process.stdin.on('data', onData); } else { const rl = createInterface({ input: process.stdin, output: process.stdout }); rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); } }); } export function registerAuthCommands(program: Command): void { program .command('login') .description('Log in to Mailcheap and cache credentials') .option('--json', 'Output as JSON') .action(async (opts) => { try { let username: string; let password: string; if (process.stdin.isTTY) { username = await promptTTY('Username: ', false); password = await promptTTY('Password: ', true); } else { // Piped/non-interactive: read both lines upfront const lines = await readStdinLines(); username = lines[0] || ''; password = lines[1] || ''; } if (!username || !password) { console.error('Username and password are required.'); process.exit(1); } const creds = await authenticate(username, password); await saveCredentials(creds); if (opts.json) { printJson({ username: creds.username, valid_to: creds.valid_to, valid_to_human: formatTimestamp(creds.valid_to), }); } else { console.log(`✓ Logged in as ${creds.username}`); console.log(` Token valid until: ${formatTimestamp(creds.valid_to)}`); } } catch (err) { console.error(`✗ Login failed: ${(err as Error).message}`); process.exit(1); } }); program .command('logout') .description('Clear cached credentials') .action(async () => { await clearCredentials(); console.log('✓ Logged out.'); }); program .command('whoami') .description('Show current login status') .option('--json', 'Output as JSON') .action(async (opts) => { const creds = await loadCredentials(); if (!creds) { console.log('Not logged in.'); process.exit(1); } const now = Date.now() / 1000; const expired = now > creds.valid_to - 60; if (opts.json) { printJson({ username: creds.username, valid_to: creds.valid_to, valid_to_human: formatTimestamp(creds.valid_to), expired, }); } else { printTable([ { Field: 'Username', Value: creds.username }, { Field: 'Token valid until', Value: formatTimestamp(creds.valid_to) }, { Field: 'Status', Value: expired ? '⚠ Expired' : '✓ Active' }, ]); } }); }