- Authentication: login / logout / whoami with token caching - Accounts: list / create / delete / info - Aliases: list / create / delete - Domains: list - TypeScript + ESM + commander.js - Node fetch (no axios) - Table and --json output modes - Auto token refresh on expiry - MCMAIL_HOST / MCMAIL_USER / MCMAIL_PASSWORD env var support
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
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<string[]> {
|
|
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<string> {
|
|
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' },
|
|
]);
|
|
}
|
|
});
|
|
}
|