/** * Interactive item selection. Uses @clack/prompts when TTY, falls back to numbered list. */ export async function selectItem( items: T[], opts: { message: string; render: (item: T) => string; emptyMessage?: string; }, ): Promise { if (items.length === 0) { console.log(opts.emptyMessage ?? 'No items found.'); return undefined; } const isTTY = process.stdin.isTTY; if (isTTY) { try { const { select } = await import('@clack/prompts'); const result = await select({ message: opts.message, options: items.map((item, i) => ({ value: i, label: opts.render(item), })), }); if (typeof result === 'symbol') { return undefined; } return items[result as number]; } catch { // Fall through to non-interactive } } // Non-interactive: display numbered list and read a number console.log(`\n${opts.message}\n`); for (let i = 0; i < items.length; i++) { console.log(` ${i + 1}. ${opts.render(items[i]!)}`); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => rl.question('\nSelect: ', resolve)); rl.close(); const index = parseInt(answer, 10) - 1; if (isNaN(index) || index < 0 || index >= items.length) { console.error('Invalid selection.'); return undefined; } return items[index]; }