setup.mjs
2,458 bytes
| 1 | import { existsSync, writeFileSync } from 'node:fs' |
|---|---|
| 2 | import { createInterface } from 'node:readline' |
| 3 | |
| 4 | const rl = createInterface({ input: process.stdin, output: process.stdout }) |
| 5 | |
| 6 | // Queue lines so answers are not lost between questions when input is piped. |
| 7 | const pending = [] |
| 8 | const waiters = [] |
| 9 | rl.on('line', (line) => { |
| 10 | const waiter = waiters.shift() |
| 11 | if (waiter) waiter(line) |
| 12 | else pending.push(line) |
| 13 | }) |
| 14 | rl.on('close', () => { |
| 15 | for (const waiter of waiters.splice(0)) waiter(null) |
| 16 | }) |
| 17 | |
| 18 | async function ask(question) { |
| 19 | process.stdout.write(question) |
| 20 | const line = |
| 21 | pending.length > 0 ? pending.shift() : await new Promise((resolve) => waiters.push(resolve)) |
| 22 | if (line === null) { |
| 23 | console.error('\nInput ended before setup finished. Nothing changed.') |
| 24 | process.exit(1) |
| 25 | } |
| 26 | return line.trim() |
| 27 | } |
| 28 | |
| 29 | console.log( |
| 30 | 'VoiceTask setup. Writes a local .env file (gitignored, never committed).\n' + |
| 31 | 'You can also skip this and set everything up in the browser after "npm run dev".\n', |
| 32 | ) |
| 33 | |
| 34 | if (existsSync('.env')) { |
| 35 | const answer = await ask('.env already exists. Overwrite it? [y/N] ') |
| 36 | if (answer.toLowerCase() !== 'y') { |
| 37 | console.log('Keeping the existing .env. Nothing changed.') |
| 38 | rl.close() |
| 39 | process.exit(0) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | console.log(`Mode: |
| 44 | 1) Real APIs - needs an Anthropic key (interview) and an OpenAI key (speech-to-text) |
| 45 | 2) Offline demo - no keys, deterministic mock providers |
| 46 | `) |
| 47 | |
| 48 | let mode = '' |
| 49 | while (mode !== '1' && mode !== '2') { |
| 50 | mode = await ask('Choose [1/2]: ') |
| 51 | } |
| 52 | |
| 53 | let anthropicKey = '' |
| 54 | let openaiKey = '' |
| 55 | |
| 56 | if (mode === '1') { |
| 57 | console.log('\nKeys are stored only in your local .env. Input is visible while you paste.\n') |
| 58 | while (!anthropicKey) { |
| 59 | anthropicKey = await ask('Anthropic API key (console.anthropic.com): ') |
| 60 | } |
| 61 | if (!anthropicKey.startsWith('sk-ant-')) { |
| 62 | console.log(' Note: Anthropic keys usually start with "sk-ant-". Saving it anyway.') |
| 63 | } |
| 64 | while (!openaiKey) { |
| 65 | openaiKey = await ask('OpenAI API key (platform.openai.com): ') |
| 66 | } |
| 67 | if (!openaiKey.startsWith('sk-')) { |
| 68 | console.log(' Note: OpenAI keys usually start with "sk-". Saving it anyway.') |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | const env = `# Written by npm run setup. Edit freely; see .env.example for all options. |
| 73 | MOCK_PROVIDERS=${mode === '2' ? '1' : ''} |
| 74 | ANTHROPIC_API_KEY=${anthropicKey} |
| 75 | OPENAI_API_KEY=${openaiKey} |
| 76 | ` |
| 77 | |
| 78 | writeFileSync('.env', env) |
| 79 | console.log('\nWrote .env. Next: npm run dev, then open the printed URL in Chrome or Edge.') |
| 80 | rl.close() |
| 81 | |