claude-code-checkpoint
Version:
Automatic project snapshots for Claude Code - never lose your work again
238 lines (197 loc) • 8.09 kB
JavaScript
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);
}
}