UNPKG

claude-code-checkpoint

Version:

Automatic project snapshots for Claude Code - never lose your work again

238 lines (197 loc) 8.09 kB
import fs from 'fs/promises'; import path from 'path'; import { homedir } from 'os'; import chalk from 'chalk'; import { generateCheckpointHook } from '../templates/checkpoint-hook.js'; import { generateCheckpointCommand } from '../templates/checkpoint-command.js'; import { getShellConfig } from './detector.js'; const CLAUDE_DIR = path.join(homedir(), '.claude'); const CHECKPOINT_DIR = path.join(CLAUDE_DIR, 'checkpoint'); const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json'); export async function installCheckpointSystem(config) { // Ensure directories exist await ensureDirectories(); // Install checkpoint hook await installCheckpointHook(config); // Install checkpoint command await installCheckpointCommand(); // Update Claude settings to include hook await updateClaudeSettings(); // Add checkpoint command to PATH await addToPath(); } async function ensureDirectories() { await fs.mkdir(path.join(CHECKPOINT_DIR, 'hooks'), { recursive: true }); await fs.mkdir(path.join(CHECKPOINT_DIR, 'scripts'), { recursive: true }); await fs.mkdir(path.join(CHECKPOINT_DIR, 'data'), { recursive: true }); } async function installCheckpointHook(config) { const hookContent = generateCheckpointHook(config); const hookPath = path.join(CHECKPOINT_DIR, 'hooks', 'checkpoint-hook.sh'); await fs.writeFile(hookPath, hookContent, { mode: 0o755 }); console.log(chalk.green('✓ Installed checkpoint hook')); } async function installCheckpointCommand() { const commandContent = generateCheckpointCommand(); const commandPath = path.join(CHECKPOINT_DIR, 'scripts', 'checkpoint'); await fs.writeFile(commandPath, commandContent, { mode: 0o755 }); console.log(chalk.green('✓ Installed checkpoint command')); } async function updateClaudeSettings() { let settings = {}; try { const content = await fs.readFile(SETTINGS_FILE, 'utf8'); settings = JSON.parse(content); } catch (error) { // Settings file doesn't exist or is invalid } // Initialize hooks if not present if (!settings.hooks) { settings.hooks = {}; } const checkpointHookPath = path.join(CHECKPOINT_DIR, 'hooks', 'checkpoint-hook.sh'); const checkpointHook = { type: 'command', command: checkpointHookPath }; // Check if Stop hook already exists if (settings.hooks.Stop) { // Stop hook exists, we need to append to it if (!Array.isArray(settings.hooks.Stop)) { // Convert old format to new format settings.hooks.Stop = [{ matcher: '', hooks: [settings.hooks.Stop] }]; } // Check if checkpoint hook already exists const existingHooks = settings.hooks.Stop[0]?.hooks || []; const hasCheckpointHook = existingHooks.some(hook => hook.command && hook.command.includes('checkpoint-hook.sh') ); if (!hasCheckpointHook) { // Add checkpoint hook to existing hooks if (settings.hooks.Stop[0]?.hooks) { settings.hooks.Stop[0].hooks.push(checkpointHook); } else { settings.hooks.Stop[0].hooks = [checkpointHook]; } console.log(chalk.green('✓ Added checkpoint hook to existing Stop hooks')); } else { console.log(chalk.yellow('⚠️ Checkpoint hook already exists in Stop hooks')); } } else { // No Stop hook exists, create new one settings.hooks.Stop = [{ matcher: '', hooks: [checkpointHook] }]; console.log(chalk.green('✓ Created new Stop hook with checkpoint')); } await fs.writeFile(SETTINGS_FILE, JSON.stringify(settings, null, 2)); console.log(chalk.green('✓ Updated Claude settings')); } async function addToPath() { const shellConfig = getShellConfig(); const scriptsPath = path.join(CHECKPOINT_DIR, 'scripts'); try { let content = await fs.readFile(shellConfig, 'utf8'); // Check if already added if (content.includes('claude-code-checkpoint')) { return; } // Add to PATH const pathLine = `\n# Claude Code Checkpoint - Added by claude-code-checkpoint\nexport PATH="${scriptsPath}:$PATH"\n`; content += pathLine; await fs.writeFile(shellConfig, content); console.log(chalk.yellow(`\n📝 Added checkpoint command to ${path.basename(shellConfig)}`)); console.log(chalk.cyan('\n💡 To use immediately, run:')); console.log(chalk.white.bold(` source ${shellConfig}\n`)); } catch (error) { console.error(chalk.red(`\n⚠️ Could not add to PATH: ${error.message}`)); console.log(chalk.gray(' You can manually add this to your shell config:')); console.log(chalk.cyan(` export PATH="${scriptsPath}:$PATH"`)); } } export async function uninstall() { console.log(chalk.yellow('\n🗑️ Uninstalling Claude Code Checkpoint...\n')); try { // Remove Stop hook from settings try { const content = await fs.readFile(SETTINGS_FILE, 'utf8'); const settings = JSON.parse(content); if (settings.hooks && settings.hooks.Stop) { let modified = false; // Handle array format if (Array.isArray(settings.hooks.Stop)) { settings.hooks.Stop.forEach(stopHook => { if (stopHook.hooks && Array.isArray(stopHook.hooks)) { const filteredHooks = stopHook.hooks.filter(hook => !hook.command || !hook.command.includes('checkpoint-hook.sh') ); if (filteredHooks.length !== stopHook.hooks.length) { stopHook.hooks = filteredHooks; modified = true; } } }); // Remove empty Stop hooks settings.hooks.Stop = settings.hooks.Stop.filter(stopHook => stopHook.hooks && stopHook.hooks.length > 0 ); if (settings.hooks.Stop.length === 0) { delete settings.hooks.Stop; } } else if (typeof settings.hooks.Stop === 'string' && settings.hooks.Stop.includes('checkpoint-hook.sh')) { // Handle old string format delete settings.hooks.Stop; modified = true; } if (modified) { await fs.writeFile(SETTINGS_FILE, JSON.stringify(settings, null, 2)); console.log(chalk.green('✓ Removed checkpoint hook from settings')); } } } catch (error) { // Settings file doesn't exist or is invalid } // Remove from PATH const shellConfig = getShellConfig(); try { let content = await fs.readFile(shellConfig, 'utf8'); const lines = content.split('\n'); const filtered = lines.filter(line => !line.includes('claude-code-checkpoint') && !line.includes('checkpoint/scripts') ); if (lines.length !== filtered.length) { await fs.writeFile(shellConfig, filtered.join('\n')); console.log(chalk.green('✓ Removed from PATH')); } } catch (error) { // Shell config doesn't exist } // Ask about checkpoint data console.log(chalk.yellow('\n⚠️ Keep existing checkpoint data?')); console.log(chalk.gray(' Your checkpoints are stored in ~/.claude/checkpoint/data/')); const inquirer = await import('inquirer').then(m => m.default); const { keepData } = await inquirer.prompt([ { type: 'confirm', name: 'keepData', message: 'Keep checkpoint data?', default: true } ]); if (!keepData) { await fs.rm(path.join(CHECKPOINT_DIR, 'data'), { recursive: true, force: true }); console.log(chalk.green('✓ Removed checkpoint data')); } // Remove scripts and hooks await fs.rm(path.join(CHECKPOINT_DIR, 'hooks'), { recursive: true, force: true }); await fs.rm(path.join(CHECKPOINT_DIR, 'scripts'), { recursive: true, force: true }); console.log(chalk.green('\n✅ Claude Code Checkpoint uninstalled successfully!\n')); } catch (error) { console.error(chalk.red('❌ Uninstall failed:'), error.message); } }