UNPKG

devfocus

Version:

CLI tool for developers to block distracting websites

470 lines (419 loc) • 12.4 kB
#!/usr/bin/env node /** * Website Blocker CLI Tool * Cross-platform command-line tool for blocking distracting websites by modifying the system hosts file. */ const fs = require("fs").promises; const { exec } = require("child_process"); const os = require("os"); const { program } = require("commander"); const util = require("util"); const path = require("path"); const execPromise = util.promisify(exec); // Configuration constants const HOSTS_FILE = os.platform() === "win32" ? "C:\\Windows\\System32\\drivers\\etc\\hosts" : "/etc/hosts"; const BLOCK_IP = "127.0.0.1"; const BLOCK_IP6 = "::1"; const MARKER_START = "# WEBSITE_BLOCKER_START"; const MARKER_END = "# WEBSITE_BLOCKER_END"; const BLOCKLIST_FILE = path.join(__dirname, "blocklist.json"); // Error messages const ERRORS = { ADMIN_REQUIRED: "Error: This command requires administrative privileges. Run with sudo on macOS/Linux or as Administrator on Windows.", INVALID_STATE: "Error: Invalid state. Use 'on' or 'off' as the state argument.", INVALID_TIMER: "Error: Invalid timer value. Use a positive integer for the timer.", NO_WEBSITES: 'No websites in block list. Add websites using the "add" command.', HOSTS_READ: "Error reading hosts file", HOSTS_WRITE: "Failed to write to hosts file. Ensure the command is run with sudo on macOS/Linux or as Administrator on Windows.", BLOCKLIST_READ: "Error reading block list", BLOCKLIST_WRITE: "Error writing block list", }; // Success messages const MESSAGES = { ADDED: "Added websites to block list", REMOVED: "Removed websites from block list", FOCUS_ON: "šŸŽÆ Focus mode is ON", FOCUS_OFF: "āœ… Focus mode is OFF āœ… All websites unblocked.", CLEARED: "šŸ—‘ļø Cleared all blocked websites and block list.", RESTART_BROWSER: "šŸ” If the websites still not blocked, it may be necessary to restart the browser", ALL_EXISTS: "All provided websites are already in the block list.", NONE_FOUND: "No matching websites found in the block list.", EMPTY_LIST: "No websites in the block list.", }; /** * Checks if running with administrative privileges */ async function hasAdminPrivileges() { if (os.platform() === "win32") { try { const testFile = "C:\\Windows\\System32\\test.txt"; await fs.writeFile(testFile, "test"); await fs.unlink(testFile); return true; } catch { return false; } } else { // macOS/Linux try { const { stdout } = await execPromise("id -u"); return parseInt(stdout.trim(), 10) === 0; } catch { try { // Check if we can write to the hosts file await fs.access(HOSTS_FILE, fs.constants.W_OK); return true; } catch { return false; } } } } async function readHostsFile() { try { return await fs.readFile(HOSTS_FILE, "utf8"); } catch (error) { throw new Error(`${ERRORS.HOSTS_READ}: ${error.message}`); } } /** * Writes content to the hosts file using platform-appropriate methods */ async function writeHostsFile(content) { try { if (os.platform() === "win32") { // Write to temp file first, this is done to avoid issues with powershell string escaping const tempFile = path.join(os.tmpdir(), `hosts_temp_${Date.now()}.txt`); await fs.writeFile(tempFile, content, "utf8"); // Copy temp file to hosts file with elevated privileges await execPromise( `powershell -Command "Copy-Item '${tempFile}' '${HOSTS_FILE}' -Force"` ); // Clean up temp file await fs.unlink(tempFile); } else { await execPromise( `echo "${content.replace( /"/g, '\\"' )}" | sudo tee "${HOSTS_FILE}" > /dev/null` ); } } catch (error) { throw new Error(`${ERRORS.HOSTS_WRITE}: ${error.message}`); } } /** * Reads the persistent block list from JSON file */ async function readBlockList() { try { const data = await fs.readFile(BLOCKLIST_FILE, "utf8"); return JSON.parse(data).websites || []; } catch (error) { if (error.code === "ENOENT") return []; throw new Error(`${ERRORS.BLOCKLIST_READ}: ${error.message}`); } } /** * Writes the block list to persistent JSON file */ async function writeBlockList(websites) { try { await fs.writeFile(BLOCKLIST_FILE, JSON.stringify({ websites }, null, 2)); } catch (error) { throw new Error(`${ERRORS.BLOCKLIST_WRITE}: ${error.message}`); } } /** * Extracts currently blocked websites from hosts file */ async function getBlockedWebsites() { const hostsContent = await readHostsFile(); const regex = new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}`, "g"); const blockSection = hostsContent.match(regex)?.[0] || ""; const websites = blockSection .split("\n") .filter( (line) => line.includes(BLOCK_IP) && !line.includes(MARKER_START) && !line.includes(MARKER_END) ) .map((line) => line.split(/\s+/)[1]) .filter((site) => site && !site.startsWith("www.")); return [...new Set(websites)]; } /** * Checks if focus mode is currently active */ async function isFocusModeOn() { const blockedWebsites = await getBlockedWebsites(); return blockedWebsites.length > 0; } /** * Adds websites to the block list */ async function addWebsites(websites) { const currentBlockList = await readBlockList(); const newWebsites = [ ...new Set(websites.filter((site) => !currentBlockList.includes(site))), ]; if (newWebsites.length === 0) { console.log(MESSAGES.ALL_EXISTS); return; } const updatedBlockList = [...currentBlockList, ...newWebsites]; await writeBlockList(updatedBlockList); console.log(`${MESSAGES.ADDED}: ${newWebsites.join(", ")} ⚔`); } /** * Removes websites from the block list */ async function removeWebsites(websites) { const currentBlockList = await readBlockList(); const updatedBlockList = currentBlockList.filter( (site) => !websites.includes(site) ); if (currentBlockList.length === updatedBlockList.length) { console.log(MESSAGES.NONE_FOUND); return; } await writeBlockList(updatedBlockList); console.log(`${MESSAGES.REMOVED}: ${websites.join(", ")}`); if (await isFocusModeOn()) { await applyBlockList(); } } /** * Clears all blocked websites */ async function clearWebsites() { await writeBlockList([]); await clearHostsFile(); console.log(`${MESSAGES.CLEARED}\n`); } /** * Displays current block list and focus mode status */ async function listWebsites() { const websites = await readBlockList(); const focusMode = await isFocusModeOn(); if (websites.length === 0) { console.log(MESSAGES.EMPTY_LIST); } else { console.log(`Block list (Focus mode: ${focusMode ? "ON" : "OFF"}):`); websites.forEach((site) => console.log(`- ${site}`)); } } /** * Applies block list to hosts file (enables focus mode) */ async function applyBlockList({ timer = 0 } = {}) { let hostsContent = await readHostsFile(); const websites = await readBlockList(); // Remove existing blocked section const regex = new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}\\n?`, "g"); hostsContent = hostsContent.replace(regex, ""); // Add block entries let newContent = hostsContent.trim(); if (websites.length > 0) { const blockEntries = websites .flatMap((site) => [ `${BLOCK_IP} ${site}`, `${BLOCK_IP} www.${site}`, `${BLOCK_IP6} ${site}`, `${BLOCK_IP6} www.${site}`, ]) .join("\n"); newContent = `${newContent}\n${MARKER_START}\n${blockEntries}\n${MARKER_END}\n`; } await writeHostsFile(newContent); await flushDnsCache(); console.log( `\n${MESSAGES.FOCUS_ON}\n🚫 Blocked websites: ${websites.join(", ")}` ); console.log(`${MESSAGES.RESTART_BROWSER}`); // Handle timer argument if (timer) { console.log( `ā³ Focus mode will automatically turn off in ${timer} hours...` ); setTimeout(async () => { await clearHostsFile(); console.log(`āŒ› Focus mode has been turned off after ${timer} hours.`); }, timer * 1000 * 60 * 60); // Convert hours to milliseconds } } /** * Removes blocked websites from hosts file (disables focus mode) */ async function clearHostsFile() { let hostsContent = await readHostsFile(); const regex = new RegExp(`${MARKER_START}[\\s\\S]*?${MARKER_END}\\n?`, "g"); hostsContent = hostsContent.replace(regex, ""); await writeHostsFile(hostsContent); await flushDnsCache(); console.log(`\n${MESSAGES.FOCUS_OFF}`); } /** * Flushes DNS cache using platform-specific commands */ async function flushDnsCache() { const commands = { darwin: "sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder", linux: "sudo systemd-resolve --flush-caches || sudo resolvectl flush-caches", win32: "ipconfig /flushdns", }; try { const command = commands[os.platform()]; if (command) { await execPromise(command); } } catch (error) { console.warn(`Warning: Could not flush DNS cache: ${error.message}`); } } // CLI setup program .version("1.1.3") .description( "A cross-platform CLI tool to block websites for focused work šŸš€" ); // Focus mode commands program .command("on") .description( "Enable focus mode by blocking websites from the block list. Optionally specify a timer in hours to automatically disable focus mode after the specified time." ) .option( "-h, --hour <hours>", "Timer duration in hours", (value) => { const timer = parseFloat(value, 10); if (isNaN(timer) || timer < 0) { throw new Error(ERRORS.INVALID_TIMER); } return timer; }, 0 // Default to 0 hours ) .action(async (options) => { if (!(await hasAdminPrivileges())) { console.error(ERRORS.ADMIN_REQUIRED); process.exit(1); } try { const websites = await readBlockList(); if (websites.length === 0) { console.log(ERRORS.NO_WEBSITES); return; } await applyBlockList({ timer: options.hour }); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); program .command("off") .description("Disable focus mode (unblock websites)") .action(async () => { if (!(await hasAdminPrivileges())) { console.error(ERRORS.ADMIN_REQUIRED); process.exit(1); } try { await clearHostsFile(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); // Add command program .command("add <websites...>") .description("Add websites to the block list") .action(async (websites) => { try { await addWebsites(websites.map((site) => site.trim())); if (await isFocusModeOn()) { await applyBlockList(); } } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); // Remove command program .command("remove <websites...>") .description("Remove websites from the block list") .action(async (websites) => { try { await removeWebsites(websites.map((site) => site.trim())); if (await isFocusModeOn()) { await applyBlockList(); } } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); // List command program .command("list") .description("List the current block list") .action(async () => { try { await listWebsites(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); // Clear command program .command("clear") .description("Clear all blocked websites and block list") .action(async () => { if (!(await hasAdminPrivileges())) { console.error(ERRORS.ADMIN_REQUIRED); process.exit(1); } try { await clearWebsites(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } }); if (require.main === module) { program.parse(process.argv); } module.exports = { hasAdminPrivileges, readHostsFile, writeHostsFile, readBlockList, writeBlockList, getBlockedWebsites, isFocusModeOn, addWebsites, removeWebsites, clearWebsites, listWebsites, applyBlockList, clearHostsFile, flushDnsCache, };