|
1 | 1 | import { createInterface, type Interface } from "node:readline/promises"; |
| 2 | +import { emitKeypressEvents } from "node:readline"; |
2 | 3 | import process from "node:process"; |
3 | 4 | import { |
4 | 5 | getWebHost, |
@@ -55,32 +56,94 @@ async function askRequired(rl: Interface, prompt: string): Promise<string> { |
55 | 56 | } |
56 | 57 | } |
57 | 58 |
|
58 | | -function parseSelection(input: string, max: number): number[] | null { |
59 | | - const trimmed = input.trim(); |
60 | | - if (!trimmed) return null; |
61 | | - |
62 | | - const values = trimmed.split(",").map((part) => part.trim()).filter(Boolean); |
63 | | - if (values.length === 0) return null; |
64 | | - |
65 | | - const numbers = new Set<number>(); |
66 | | - for (const value of values) { |
67 | | - const parsed = Number.parseInt(value, 10); |
68 | | - if (!Number.isFinite(parsed) || parsed < 1 || parsed > max) { |
69 | | - return []; |
70 | | - } |
71 | | - numbers.add(parsed); |
72 | | - } |
73 | | - |
74 | | - return Array.from(numbers).sort((a, b) => a - b); |
75 | | -} |
76 | | - |
77 | 59 | function detectAgents(): AgentOption[] { |
78 | 60 | return agentOptions.map((agent) => ({ |
79 | 61 | ...agent, |
80 | 62 | installed: Boolean(Bun.which(agent.command)), |
81 | 63 | })); |
82 | 64 | } |
83 | 65 |
|
| 66 | +async function selectAgentsWithKeyboard(agents: AgentOption[], defaultSelected: number[]): Promise<number[]> { |
| 67 | + if (!process.stdin.isTTY || !process.stdout.isTTY) { |
| 68 | + return defaultSelected; |
| 69 | + } |
| 70 | + |
| 71 | + return new Promise((resolve) => { |
| 72 | + const selected = new Set(defaultSelected.map((index) => index - 1)); |
| 73 | + let cursor = 0; |
| 74 | + const lineCount = agents.length + 2; |
| 75 | + |
| 76 | + const render = (initial = false): void => { |
| 77 | + if (!initial) { |
| 78 | + process.stdout.write(`\x1b[${lineCount}F`); |
| 79 | + } |
| 80 | + process.stdout.write("\x1b[J"); |
| 81 | + console.log("Step 2/2: Select coding agents to enable."); |
| 82 | + console.log("Use Up/Down to move, Space to toggle, Enter to confirm."); |
| 83 | + for (const [index, agent] of agents.entries()) { |
| 84 | + const pointer = index === cursor ? ">" : " "; |
| 85 | + const checked = selected.has(index) ? "x" : " "; |
| 86 | + const status = agent.installed ? "installed" : "not found"; |
| 87 | + console.log(` ${pointer} [${checked}] ${agent.label} (${agent.command}) - ${status}`); |
| 88 | + } |
| 89 | + }; |
| 90 | + |
| 91 | + const cleanup = (): void => { |
| 92 | + process.stdin.off("keypress", onKeypress); |
| 93 | + if (process.stdin.isTTY) { |
| 94 | + process.stdin.setRawMode(false); |
| 95 | + } |
| 96 | + process.stdin.pause(); |
| 97 | + }; |
| 98 | + |
| 99 | + const finalize = (): void => { |
| 100 | + cleanup(); |
| 101 | + process.stdout.write("\n"); |
| 102 | + resolve(Array.from(selected).sort((a, b) => a - b).map((index) => index + 1)); |
| 103 | + }; |
| 104 | + |
| 105 | + const onKeypress = (_input: string, key: { name?: string; ctrl?: boolean }): void => { |
| 106 | + if (key.ctrl && key.name === "c") { |
| 107 | + cleanup(); |
| 108 | + process.kill(process.pid, "SIGINT"); |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + if (key.name === "up") { |
| 113 | + cursor = (cursor - 1 + agents.length) % agents.length; |
| 114 | + render(); |
| 115 | + return; |
| 116 | + } |
| 117 | + |
| 118 | + if (key.name === "down") { |
| 119 | + cursor = (cursor + 1) % agents.length; |
| 120 | + render(); |
| 121 | + return; |
| 122 | + } |
| 123 | + |
| 124 | + if (key.name === "space") { |
| 125 | + if (selected.has(cursor)) { |
| 126 | + selected.delete(cursor); |
| 127 | + } else { |
| 128 | + selected.add(cursor); |
| 129 | + } |
| 130 | + render(); |
| 131 | + return; |
| 132 | + } |
| 133 | + |
| 134 | + if (key.name === "return" || key.name === "enter") { |
| 135 | + finalize(); |
| 136 | + } |
| 137 | + }; |
| 138 | + |
| 139 | + emitKeypressEvents(process.stdin); |
| 140 | + process.stdin.setRawMode(true); |
| 141 | + process.stdin.resume(); |
| 142 | + process.stdin.on("keypress", onKeypress); |
| 143 | + render(true); |
| 144 | + }); |
| 145 | +} |
| 146 | + |
84 | 147 | async function setupSlackWorkspaces(rl: Interface, config: OdeConfig): Promise<OdeConfig> { |
85 | 148 | const wantsSetup = await askYesNo( |
86 | 149 | rl, |
@@ -141,28 +204,13 @@ async function setupCodingAgents(rl: Interface, config: OdeConfig): Promise<OdeC |
141 | 204 | .filter((entry) => entry.installed) |
142 | 205 | .map((entry) => entry.index + 1); |
143 | 206 |
|
144 | | - console.log("Step 2/2: Select coding agents to enable."); |
145 | | - for (const [index, agent] of agents.entries()) { |
146 | | - const selected = defaultSelected.includes(index + 1) ? "x" : " "; |
147 | | - const status = agent.installed ? "installed" : "not found"; |
148 | | - console.log(` ${index + 1}. [${selected}] ${agent.label} (${agent.command}) - ${status}`); |
149 | | - } |
150 | | - |
151 | | - let selectedIndices: number[] | null = null; |
152 | | - while (selectedIndices === null) { |
153 | | - const input = await ask( |
154 | | - rl, |
155 | | - "Choose agents by number (comma-separated). Press Enter to keep detected defaults: " |
156 | | - ); |
157 | | - const parsed = parseSelection(input, agents.length); |
158 | | - if (parsed !== null && parsed.length === 0) { |
159 | | - console.log("Please enter valid numbers from the list, like 1,3."); |
160 | | - continue; |
161 | | - } |
162 | | - selectedIndices = parsed; |
| 207 | + rl.pause(); |
| 208 | + let finalIndices: number[]; |
| 209 | + try { |
| 210 | + finalIndices = await selectAgentsWithKeyboard(agents, defaultSelected); |
| 211 | + } finally { |
| 212 | + rl.resume(); |
163 | 213 | } |
164 | | - |
165 | | - const finalIndices = selectedIndices ?? defaultSelected; |
166 | 214 | const selectedIds = new Set<AgentId>( |
167 | 215 | finalIndices.map((index) => agents[index - 1]!.id) |
168 | 216 | ); |
|
0 commit comments