UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities

218 lines (203 loc) • 7.82 kB
const fs = require('fs'); const path = require('path'); const os = require('os'); const net = require('net'); const chalk = require('chalk'); const { getServerStatus } = require('./ai-helper'); const { DEFAULT_PORT } = require('./config'); function checkNodeVersion() { const version = process.versions.node || '0.0.0'; const major = parseInt(version.split('.')[0], 10) || 0; const ok = major >= 16; return { check: 'node_version', success: ok, details: { version, requirement: '>=16' }, fix: ok ? null : 'Upgrade Node.js to v18+ (recommended)' }; } async function checkPort(port) { // If server is running, we consider port check OK const status = await getServerStatus(port); if (status.running) { return { check: 'port', success: true, details: { port, running: true, url: status.url }, fix: null }; } // Otherwise ensure port is free to bind const free = await new Promise((resolve) => { const tester = net.createServer() .once('error', () => resolve(false)) .once('listening', () => tester.once('close', () => resolve(true)).close()) .listen(port, '127.0.0.1'); }); return { check: 'port', success: free, details: { port, running: false, free }, fix: free ? null : `Use a different port: npx spaps local --port ${port + 1}` }; } function checkEnvFile() { const envPath = path.resolve(process.cwd(), '.env.local'); const exists = fs.existsSync(envPath); let hasApiUrl = false; if (exists) { try { const content = fs.readFileSync(envPath, 'utf8'); hasApiUrl = /SPAPS_API_URL\s*=/.test(content); } catch {} } return { check: 'env_file', success: exists && hasApiUrl, details: { path: envPath, exists, hasApiUrl }, fix: exists ? (hasApiUrl ? null : 'Add SPAPS_API_URL to .env.local (http://localhost:3300)') : 'Run: npx spaps init' }; } function checkWritePermissions() { const dir = path.resolve(process.cwd(), '.spaps'); try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const tmp = path.join(dir, '_doctor.tmp'); fs.writeFileSync(tmp, 'ok'); fs.unlinkSync(tmp); return { check: 'write_permissions', success: true, details: { dir }, fix: null }; } catch (e) { return { check: 'write_permissions', success: false, details: { dir, error: e.message }, fix: `Make directory writable: chmod -R u+rw ${dir}` }; } } function checkSDKInstalled() { try { require.resolve('spaps-sdk', { paths: [process.cwd()] }); return { check: 'sdk_installed', success: true, details: { package: 'spaps-sdk' }, fix: null }; } catch { return { check: 'sdk_installed', success: false, details: { package: 'spaps-sdk' }, fix: 'npm install spaps-sdk' }; } } function checkStripeMode(stripeModeOpt) { const mode = (stripeModeOpt || (process.env.USE_REAL_STRIPE === 'false' ? 'mock' : 'real')).toLowerCase(); const needsKey = mode === 'real'; const hasKey = Boolean(process.env.STRIPE_SECRET_KEY); const ok = mode === 'mock' || (mode === 'real' && hasKey); return { check: 'stripe_mode', success: ok, details: { mode, needsKey, hasKey }, fix: ok ? null : (mode === 'real' ? 'Set STRIPE_SECRET_KEY or run with --stripe mock' : null) }; } function checkEnvTest() { const envPath = path.resolve(process.cwd(), '.env.test'); if (!fs.existsSync(envPath)) { return { check: 'env_test', success: false, details: { path: envPath, exists: false }, fix: 'Create .env.test with SPAPS_API_URL=http://localhost:3300 (no real network keys)' }; } try { const content = fs.readFileSync(envPath, 'utf8'); const hasLocalUrl = /SPAPS_API_URL\s*=\s*http:\/\/localhost:\d+/.test(content); const hasApiKey = /SPAPS_API_KEY\s*=\s*\S+/.test(content); const warns = []; if (!hasLocalUrl) warns.push('SPAPS_API_URL should point to localhost'); if (hasApiKey) warns.push('SPAPS_API_KEY should not be set in tests'); return { check: 'env_test', success: hasLocalUrl && !hasApiKey, details: { path: envPath, hasLocalUrl, hasApiKey }, fix: warns.length ? warns.join(' | ') : null }; } catch (e) { return { check: 'env_test', success: false, details: { error: e.message }, fix: 'Ensure .env.test is readable' }; } } async function checkNextJsPort() { const defaultNextPort = 3000; const inUse = await new Promise((resolve) => { const tester = net.createServer() .once('error', () => resolve(true)) .once('listening', () => tester.once('close', () => resolve(false)).close()) .listen(defaultNextPort, '127.0.0.1'); }); return { check: 'next_port', success: true, details: { port: defaultNextPort, inUse, note: inUse ? 'Next.js likely running (good)' : 'Port free' }, fix: null }; } async function checkWebhook(port) { const status = await getServerStatus(port); if (!status.running) { return { check: 'webhook', success: false, details: { running: false }, fix: `Start server: npx spaps local --port ${port} --stripe mock` }; } try { const http = require('http'); const payload = JSON.stringify({ id: 'evt_doctor_' + Date.now(), type: 'checkout.session.completed', data: { object: { id: 'cs_doctor_' + Date.now() } } }); const ok = await new Promise((resolve) => { const req = http.request({ hostname: 'localhost', port, path: '/api/stripe/webhooks', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }, (res) => { resolve(res.statusCode >= 200 && res.statusCode < 300); }); req.on('error', () => resolve(false)); req.write(payload); req.end(); }); return { check: 'webhook', success: ok, details: { path: '/api/stripe/webhooks' }, fix: ok ? null : 'Use --stripe mock or ensure webhook handler is reachable' }; } catch (e) { return { check: 'webhook', success: false, details: { error: e.message }, fix: 'Use --stripe mock or ensure server is running' }; } } function formatHuman(results) { const ok = results.every(r => r.success); console.log(chalk.yellow('\nšŸ  SPAPS Doctor\n')); results.forEach(r => { const icon = r.success ? chalk.green('āœ”') : chalk.red('āœ–'); console.log(`${icon} ${r.check} ${chalk.gray(JSON.stringify(r.details))}`); if (!r.success && r.fix) console.log(chalk.cyan(` fix: ${r.fix}`)); }); console.log(); console.log(ok ? chalk.green('All checks passed!') : chalk.red('Some checks failed. See fixes above.')); } async function runDoctor({ port = DEFAULT_PORT, stripe = null, json = false } = {}) { const results = []; results.push(checkNodeVersion()); results.push(await checkPort(port)); // Warn if using 3000 which often collides with Next.js if (port === 3000) { results.push({ check: 'spaps_port_vs_next', success: false, details: { spaps_port: port, suggestion: 'Use 3300 for SPAPS to avoid Next.js conflicts' }, fix: 'Run: npx spaps local --port 3300' }); } else { results.push({ check: 'spaps_port_vs_next', success: true, details: { spaps_port: port }, fix: null }); } results.push(checkEnvFile()); results.push(checkWritePermissions()); results.push(checkSDKInstalled()); results.push(checkStripeMode(stripe)); results.push(checkEnvTest()); results.push(await checkNextJsPort()); results.push(await checkWebhook(port)); const ok = results.every(r => r.success); const payload = { success: ok, results, next_steps: ok ? [] : ['Apply suggested fixes and re-run: npx spaps doctor --json'] }; if (json) { console.log(JSON.stringify(payload, null, 2)); } else { formatHuman(results); } return payload; } module.exports = { runDoctor };