UNPKG

verse-of-the-day-cli

Version:

Bible verse of the day on the command line

296 lines (259 loc) 8.13 kB
#!/usr/bin/env node import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import chalk from 'chalk' import clipboardy from 'clipboardy' import readline from 'node:readline/promises' import getVerse from './lib/getVerse.js' const args = process.argv.slice(2); const options = parseArgs(args); if (options.help) { showHelp(); process.exit(0); } if (options.version) { console.log('1.0.0'); process.exit(0); } if (options.command === 'init') { await runInit(); } else { await runVerse(options); } async function runVerse(opts) { const force = opts.force || false; const auto = opts.auto || false; const toClip = opts.copy || false; const cfgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); const verseDir = path.join(cfgHome, 'verse-cli'); const cacheFile = path.join(verseDir, 'cache.json'); let data = {}; try { const cacheContent = fs.readFileSync(cacheFile, 'utf8'); data = JSON.parse(cacheContent); if (!isValidVerseData(data)) { data = {}; } } catch { // silently continue } const today = new Date().toISOString().slice(0, 10); const needFetch = force || data.date !== today; if (needFetch) { console.log(chalk.yellow('Fetching fresh verse...')); try { const fresh = await getVerse(); if (!isValidVerseData(fresh)) { throw new Error('Invalid verse data received from API'); } data = { date: today, ...fresh }; try { fs.mkdirSync(verseDir, { recursive: true }); fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2)); } catch (err) { // silently continue } } catch (err) { if (!data.verse) { console.error(chalk.red('Unable to fetch today\'s verse. Please check your internet connection.')); process.exit(1); } // silently continue and use cached verse } } if (!auto || needFetch) { console.log(chalk.blue('Today\'s verse:')); printVerse(data.reference, data.verse, data.provider); if (toClip) { try { await clipboardy.write(`${data.reference}${data.verse}`); console.log(chalk.gray('✓ Copied to clipboard')); } catch (err) { console.error(chalk.red(`Warning: Could not copy to clipboard: ${err.message}`)); } } } } function isValidVerseData(data) { return data && typeof data.reference === 'string' && typeof data.verse === 'string' && typeof data.provider === 'string' && data.reference.length > 0 && data.verse.length > 0; } function printVerse(reference, verse, provider) { console.log( chalk.green.bold(reference), '\n', chalk.white(verse), '\n', chalk.gray(`Verse courtesy: ${provider}`) ); } async function runInit() { const shell = detectShell(); const { rcPath, snippet } = shellInfo(shell); let rl; try { let rcExists = false; let hasSnippet = false; try { if (fs.existsSync(rcPath)) { rcExists = true; const rcContent = fs.readFileSync(rcPath, 'utf8'); hasSnippet = rcContent.includes(snippet) || rcContent.includes(`# ${snippet}`) || rcContent.includes('verse --auto'); } } catch (err) { console.error(chalk.red(`Warning: Could not read ${rcPath}: ${err.message}`)); } if (hasSnippet) { console.log('⏭ Startup snippet already present – nothing left to do.'); return; } console.log(`\n About to append the following line to ${rcPath}:\n`); console.log(chalk.gray(snippet), '\n'); if (!rcExists) { console.log(chalk.yellow(`Note: ${rcPath} does not exist and will be created.\n`)); } rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = (await rl.question('Proceed? (y/N) ')).trim().toLowerCase(); if (answer !== 'y') { console.log('Aborted.'); return; } try { fs.mkdirSync(path.dirname(rcPath), { recursive: true }); fs.appendFileSync(rcPath, `\n${snippet}\n`); console.log(chalk.green('✓ Added. Open a new terminal to test.')); } catch (err) { console.error(chalk.red(`Error: Could not write to ${rcPath}: ${err.message}`)); console.log(chalk.yellow(`You can manually add this line to your shell profile:\n${snippet}`)); process.exit(1); } } catch (err) { console.error(chalk.red(`Error during initialization: ${err.message}`)); process.exit(1); } finally { if (rl) { rl.close(); } } } function detectShell() { if (process.platform === 'win32') { if (process.env.PSModulePath) { return process.env.PSModulePath.includes('PowerShell\\7') ? 'pwsh' : 'powershell'; } return 'powershell'; } const shellPath = process.env.SHELL || ''; if (shellPath.includes('zsh')) return 'zsh'; if (shellPath.includes('fish')) return 'fish'; if (shellPath.includes('bash')) return 'bash'; return 'bash'; } function shellInfo(shell) { const home = os.homedir(); switch (shell) { case 'bash': return { rcPath: path.join(home, '.bashrc'), snippet: 'if [[ $- == *i* ]]; then verse --auto; fi' }; case 'zsh': return { rcPath: path.join(home, '.zshrc'), snippet: 'if [[ $- == *i* ]]; then verse --auto; fi' }; case 'fish': return { rcPath: path.join(home, '.config', 'fish', 'config.fish'), snippet: 'if status --is-interactive; verse --auto; end' }; case 'powershell': return { rcPath: path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'), snippet: 'if ($Host.UI.RawUI.WindowTitle) { verse --auto }' }; case 'pwsh': return { rcPath: path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'), snippet: 'if ($Host.UI.RawUI.WindowTitle) { verse --auto }' }; default: return { rcPath: path.join(home, '.bashrc'), snippet: 'if [[ $- == *i* ]]; then verse --auto; fi' }; } } function parseArgs(args) { const options = { command: null, force: false, copy: false, auto: false, help: false, version: false }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case 'show': case 's': options.command = 'show'; break; case 'init': options.command = 'init'; break; case '-f': case '--force': options.force = true; break; case '-c': case '--copy': options.copy = true; break; case '--auto': options.auto = true; break; case '-h': case '--help': options.help = true; break; case '-v': case '--version': options.version = true; break; } } return options; } function showHelp() { console.log(`verse - Print the Bible "Verse of the Day" once per day Usage: verse [options] # print today's verse (cached if already fetched) verse show|s [options] # show today's verse (cached, unless --force) verse init # add auto-run snippet to your shell profile Options: -f, --force # fetch a fresh verse even if today's is cached -c, --copy # copy verse to clipboard --auto # internal: run from shell startup (silent if cached) -h, --help # show help -v, --version # show version Examples: $ verse # print today's verse (cached if already fetched) $ verse --force # always pull a fresh verse $ verse show -c # show cached verse and copy to clipboard $ verse show --force # force-refresh via the show command $ verse init # add auto-run to shell startup Home page & docs: https://patriciosebastian.github.io/verse-of-the-day-cli/ `); }