UNPKG

ccusage-byobu

Version:

Real-time Claude Code API usage monitoring for byobu/tmux status bars with colored indicators, persistent caching, and automated integration

422 lines (365 loc) 13.1 kB
import { existsSync, mkdirSync, writeFileSync, unlinkSync, chmodSync, statSync, readdirSync, } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; /** * Create the byobu status script content * @param {string} refreshInterval - Refresh interval for byobu (default: '60') * @returns {string} Script content */ function createScriptContent(_refreshInterval = '60') { return `#!/bin/bash # ccusage-byobu status script # Auto-generated by ccusage-byobu --install ccusage-byobu 2>/dev/null || echo "" `; } /** * Get the byobu bin directory path * @returns {string} Path to byobu bin directory */ function getByobuBinDir() { return join(homedir(), '.byobu', 'bin'); } /** * Validate refresh interval value * @param {string} refreshInterval - Refresh interval to validate * @returns {Object} Validation result with isValid boolean and sanitized value */ function validateRefreshInterval(refreshInterval) { // Convert to string if number const intervalStr = String(refreshInterval || '60'); // Check if it's a valid positive integer const intervalNum = parseInt(intervalStr, 10); if (isNaN(intervalNum) || intervalNum <= 0) { return { isValid: false, error: `Invalid refresh interval "${refreshInterval}". Must be a positive integer.`, sanitized: '60', }; } // Reasonable bounds checking if (intervalNum < 5) { return { isValid: false, error: `Refresh interval "${intervalNum}" is too low. Minimum is 5 seconds.`, sanitized: '60', }; } if (intervalNum > 3600) { return { isValid: false, error: `Refresh interval "${intervalNum}" is too high. Maximum is 3600 seconds (1 hour).`, sanitized: '60', }; } return { isValid: true, sanitized: String(intervalNum), }; } /** * Get the script filename based on refresh interval * @param {string} refreshInterval - Refresh interval (default: '60') * @returns {string} Script filename */ function getScriptFilename(refreshInterval = '60') { const validation = validateRefreshInterval(refreshInterval); const validInterval = validation.sanitized; return `${validInterval}_ccusage`; } /** * Get the full script path * @param {string} refreshInterval - Refresh interval (default: '60') * @returns {string} Full path to script file */ function getScriptPath(refreshInterval = '60') { return join(getByobuBinDir(), getScriptFilename(refreshInterval)); } /** * Set file permissions with proper error handling * @param {string} filePath - Path to file * @param {number} mode - Permission mode (e.g., 0o755) * @returns {boolean} True if permissions were set successfully */ function setFilePermissions(filePath, mode) { try { chmodSync(filePath, mode); // Verify permissions were set correctly const fileStats = statSync(filePath); const actualPermissions = fileStats.mode & 0o777; if (actualPermissions !== mode) { console.warn( `Warning: File permissions are ${actualPermissions.toString(8)} instead of ${mode.toString(8)}` ); console.warn(`You may need to manually run: chmod ${mode.toString(8)} "${filePath}"`); return false; } return true; } catch (error) { if (error.code === 'EACCES') { console.error(`Permission denied setting file permissions on: ${filePath}`); console.error('You may need elevated privileges to modify this file'); } else if (error.code === 'ENOENT') { console.error(`File not found when setting permissions: ${filePath}`); } else { console.error(`Failed to set file permissions: ${error.message}`); } console.error(`Manual fix: chmod ${mode.toString(8)} "${filePath}"`); return false; } } /** * Check if byobu bin directory exists and create if needed * @returns {boolean} True if directory exists or was created successfully */ function ensureByobuBinDir() { const binDir = getByobuBinDir(); try { // Check if directory already exists if (existsSync(binDir)) { // Verify it's actually a directory const stats = statSync(binDir); if (!stats.isDirectory()) { console.error(`Path exists but is not a directory: ${binDir}`); return false; } return true; } // Create directory with proper permissions mkdirSync(binDir, { recursive: true, mode: 0o755 }); console.log(`Created byobu bin directory: ${binDir}`); // Verify directory was created successfully if (!existsSync(binDir)) { console.error(`Directory creation appeared to succeed but path does not exist: ${binDir}`); return false; } return true; } catch (error) { // Provide more specific error messages if (error.code === 'EACCES') { console.error(`Permission denied creating byobu bin directory: ${binDir}`); console.error('You may need to run this command with different permissions'); } else if (error.code === 'ENOSPC') { console.error(`No space left on device when creating: ${binDir}`); } else { console.error(`Failed to create byobu bin directory: ${error.message}`); } return false; } } /** * Install the byobu status script * @param {Object} options - Installation options * @param {string} options.refreshInterval - Refresh interval (default: from env or '60') * @returns {boolean} True if installation was successful */ export function install(options = {}) { try { // Get refresh interval from options, environment, or default const rawRefreshInterval = options.refreshInterval || process.env.CCUSAGE_BYOBU_REFRESH || '60'; // Validate refresh interval const validation = validateRefreshInterval(rawRefreshInterval); if (!validation.isValid) { console.error(validation.error); console.error(`Using default refresh interval: ${validation.sanitized} seconds`); } const refreshInterval = validation.sanitized; // Ensure byobu bin directory exists if (!ensureByobuBinDir()) { return false; } const scriptPath = getScriptPath(refreshInterval); // Check if script already exists if (existsSync(scriptPath)) { console.log(`Script already exists at: ${scriptPath}`); console.log('Use --uninstall first to remove existing script, then reinstall.'); return false; } // Create script content const scriptContent = createScriptContent(refreshInterval); // Write script file with initial permissions writeFileSync(scriptPath, scriptContent, { mode: 0o755 }); // Set proper executable permissions using our utility function const permissionsSet = setFilePermissions(scriptPath, 0o755); if (!permissionsSet) { console.warn('Installation completed but file may not be executable'); console.warn('You may need to manually make the script executable'); } console.log(`✓ Successfully installed ccusage-byobu status script`); console.log(` Location: ${scriptPath}`); console.log(` Refresh interval: ${refreshInterval} seconds`); console.log(` Script is now active in byobu status bar`); if (refreshInterval !== '60') { console.log(` Custom refresh interval set via CCUSAGE_BYOBU_REFRESH=${refreshInterval}`); } return true; } catch (error) { console.error(`Installation failed: ${error.message}`); return false; } } /** * Uninstall the byobu status script * @param {Object} options - Uninstallation options * @param {string} options.refreshInterval - Refresh interval to remove (default: from env or '60') * @returns {boolean} True if uninstallation was successful */ export function uninstall(options = {}) { try { // Get refresh interval from options, environment, or default const rawRefreshInterval = options.refreshInterval || process.env.CCUSAGE_BYOBU_REFRESH || '60'; // Validate refresh interval const validation = validateRefreshInterval(rawRefreshInterval); if (!validation.isValid) { console.warn(validation.error); console.warn(`Using default refresh interval: ${validation.sanitized} seconds for uninstall`); } const refreshInterval = validation.sanitized; const scriptPath = getScriptPath(refreshInterval); // Check if script exists if (!existsSync(scriptPath)) { console.log(`No ccusage-byobu script found at: ${scriptPath}`); // Check if other scripts exist const allScripts = listInstalledScripts(); if (allScripts.length > 0) { console.log(`Found ${allScripts.length} other ccusage script(s) with different intervals:`); allScripts.forEach((script) => { console.log(` - ${script.filename} (${script.refreshInterval}s interval)`); }); console.log( 'Use --uninstall-all to remove all scripts, or specify the correct --refresh interval.' ); } else { console.log('No ccusage-byobu scripts are currently installed.'); } return false; } // Remove script file try { unlinkSync(scriptPath); } catch (error) { console.error(`Failed to remove script: ${error.message}`); if (error.code === 'EACCES') { console.error('Permission denied. You may need elevated privileges.'); } return false; } console.log(`✓ Successfully uninstalled ccusage-byobu status script`); console.log(` Removed: ${scriptPath}`); console.log(` byobu status bar will no longer show ccusage metrics`); // Check if other scripts remain const remainingScripts = listInstalledScripts(); if (remainingScripts.length > 0) { console.log(`Note: ${remainingScripts.length} other ccusage script(s) still installed:`); remainingScripts.forEach((script) => { console.log(` - ${script.filename} (${script.refreshInterval}s interval)`); }); } return true; } catch (error) { console.error(`Uninstallation failed: ${error.message}`); return false; } } /** * List all installed ccusage scripts * @returns {Array} Array of installed script information */ export function listInstalledScripts() { const binDir = getByobuBinDir(); if (!existsSync(binDir)) { return []; } try { const files = readdirSync(binDir); return files .filter((file) => file.endsWith('_ccusage')) .map((file) => { const refreshInterval = file.replace('_ccusage', ''); const scriptPath = join(binDir, file); return { refreshInterval, filename: file, scriptPath, isExecutable: (() => { try { const stats = statSync(scriptPath); return (stats.mode & 0o111) !== 0; // Check if any execute bit is set } catch { return false; } })(), }; }) .sort((a, b) => parseInt(a.refreshInterval) - parseInt(b.refreshInterval)); } catch (error) { console.error(`Error listing installed scripts: ${error.message}`); return []; } } /** * Uninstall all ccusage scripts * @returns {Object} Uninstallation results */ export function uninstallAll() { const installedScripts = listInstalledScripts(); if (installedScripts.length === 0) { console.log('No ccusage-byobu scripts found to uninstall.'); return { success: true, removed: [], errors: [] }; } const results = { success: true, removed: [], errors: [], }; console.log(`Found ${installedScripts.length} ccusage script(s) to remove:`); for (const script of installedScripts) { try { unlinkSync(script.scriptPath); console.log(`✓ Removed: ${script.filename} (${script.refreshInterval}s interval)`); results.removed.push(script); } catch (error) { const errorMsg = `Failed to remove ${script.filename}: ${error.message}`; console.error(`✗ ${errorMsg}`); results.errors.push({ script, error: errorMsg }); results.success = false; } } if (results.success) { console.log(`✓ Successfully uninstalled all ccusage-byobu scripts`); } else { console.log(`⚠ Partial uninstall completed with ${results.errors.length} error(s)`); } return results; } /** * Check installation status * @param {Object} options - Check options * @param {string} options.refreshInterval - Refresh interval to check (default: from env or '60') * @returns {Object} Installation status information */ export function checkInstallation(options = {}) { const rawRefreshInterval = options.refreshInterval || process.env.CCUSAGE_BYOBU_REFRESH || '60'; const validation = validateRefreshInterval(rawRefreshInterval); const refreshInterval = validation.sanitized; const scriptPath = getScriptPath(refreshInterval); const binDir = getByobuBinDir(); const allInstalled = listInstalledScripts(); return { isInstalled: existsSync(scriptPath), scriptPath, binDir, binDirExists: existsSync(binDir), refreshInterval, allInstalledScripts: allInstalled, validationWarning: !validation.isValid ? validation.error : null, }; }