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
JavaScript
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,
};
}