verse-of-the-day-cli
Version:
Bible verse of the day on the command line
296 lines (259 loc) • 8.13 kB
JavaScript
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/
`);
}