// npm run doctor: checks everything OScope needs and says how to fix what is missing. import fs from 'node:fs'; import path from 'node:path'; import net from 'node:net'; import { config, ROOT } from '../src/config.mjs'; const ok = s => console.log(` \x1b[32m✓\x1b[0m ${s}`); const bad = (s, fix) => { console.log(` \x1b[31m✗\x1b[0m ${s}${fix ? `\n fix: ${fix}` : ''}`); problems++; }; const note = s => console.log(` \x1b[2m· ${s}\x1b[0m`); let problems = 0; console.log('\nOScope doctor\n'); // 1. Node.js const [maj, min] = process.versions.node.split('.').map(Number); if (maj > 18 || (maj === 18 && min >= 17)) ok(`Node.js ${process.versions.node}`); else bad(`Node.js ${process.versions.node} is too old`, 'install Node.js 20 or newer (LTS) from https://nodejs.org'); // 2. dependencies for (const p of ['ws', 'lightweight-charts']) { if (fs.existsSync(path.join(ROOT, 'node_modules', p, 'package.json'))) ok(`${p} installed`); else bad(`${p} is not installed`, 'run: npm ci'); } // 3. the port: free, or OScope already running there const status = await fetch(`http://127.0.0.1:${config.port}/api/status`, { signal: AbortSignal.timeout(2000) }).then(r => r.json()).catch(() => null); let localFire = false; if (status && status.feed) { ok(`OScope is already running on http://localhost:${config.port} (stream ${status.feed.connected ? 'live' : 'not connected'}, ${status.stats?.trades ?? 0} trades so far)`); localFire = !!status.firehose && status.feed.connected; } else { const free = await new Promise(res => { const s = net.createServer().once('error', () => res(false)).once('listening', () => s.close(() => res(true))).listen(config.port, '127.0.0.1'); }); if (free) ok(`port ${config.port} is free`); else note(`port ${config.port} is used by another program: OScope will take the next free port and print its address`); } // 4. the stream { const { default: WebSocket } = await import('ws'); const url = localFire ? `ws://127.0.0.1:${config.port}/firehose` : config.feedUrl; const r = await new Promise(res => { let n = 0, opened = false; const ws = new WebSocket(url, { handshakeTimeout: 8000 }); const done = why => { try { ws.terminate(); } catch {} res({ n, opened, why }); }; ws.on('open', () => { opened = true; setTimeout(() => done('time'), 5000); }); ws.on('message', () => n++); ws.on('close', code => done('closed ' + code)); ws.on('error', e => done(e.message)); }); if (r.n > 0) ok(`stream works: ${r.n} messages in 5 s from ${url}`); else if (r.opened) bad(`the stream opened but sent nothing (${r.why})`, 'the stream allows only a few connections per internet address: close other programs reading it, or point FEED_URL at a firehose you already run'); else bad(`cannot reach the stream at ${url} (${r.why})`, 'check your internet connection, VPN or firewall (it needs outgoing websockets on port 443)'); } // 5. SOL price const px = await fetch('https://api.coinbase.com/v2/prices/SOL-USD/spot', { signal: AbortSignal.timeout(6000) }).then(r => r.json()).then(j => +j.data.amount).catch(() => 0); if (px > 0) ok(`SOL price: $${px.toFixed(2)}`); else note('SOL price source did not answer: OScope tries two more, dollar values wait until one answers'); // 6. data folder try { fs.mkdirSync(config.dataDir, { recursive: true }); fs.accessSync(config.dataDir, fs.constants.W_OK); ok(`data folder ${path.relative(ROOT, config.dataDir) || config.dataDir} is writable`); } catch { bad(`cannot write ${config.dataDir}`, 'set DATA_DIR in .env to a folder you can write to'); } console.log(problems ? `\n${problems} problem(s). Fix them and run npm run doctor again.\n` : status ? `\nAll good. OScope is running: open http://localhost:${config.port}\n` : `\nAll good. Run: npm start (then open http://localhost:${config.port})\n`); process.exit(problems ? 1 : 0);