| |
| 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'); |
| |
| |
| 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'); |
| |
| |
| 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'); |
| } |
| |
| |
| 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`); |
| } |
| |
| |
| { |
| 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)'); |
| } |
| |
| |
| 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'); |
| |
| |
| 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); |