UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

218 lines 9.6 kB
import { execSync } from 'child_process'; import { existsSync } from 'fs'; import * as fs from 'fs/promises'; import * as path from 'path'; import { MAX_CHECKPOINT_FILES } from '../constants.js'; import { loadGitignore } from '../utils/gitignore-loader.js'; import { logWarning } from '../utils/message-queue.js'; /** * Service for capturing and restoring file snapshots for checkpoints */ export class FileSnapshotService { workspaceRoot; constructor(workspaceRoot = process.cwd()) { this.workspaceRoot = workspaceRoot; } /** * Capture the contents of specified files */ async captureFiles(filePaths) { const snapshots = new Map(); for (const filePath of filePaths) { try { const absolutePath = path.resolve(this.workspaceRoot, filePath); // nosemgrep const content = await fs.readFile(absolutePath, 'utf-8'); const relativePath = path.relative(this.workspaceRoot, absolutePath); const normalizedPath = relativePath.split(path.sep).join('/'); snapshots.set(normalizedPath, content); } catch (error) { logWarning('Could not capture file', true, { context: { filePath, error: error instanceof Error ? error.message : 'Unknown error', }, }); } } return snapshots; } /** * Restore files from snapshots */ async restoreFiles(snapshots) { const errors = []; for (const [relativePath, content] of snapshots) { try { const absolutePath = path.resolve(this.workspaceRoot, relativePath); const directory = path.dirname(absolutePath); await fs.mkdir(directory, { recursive: true }); await fs.writeFile(absolutePath, content, 'utf-8'); } catch (error) { errors.push(`Failed to restore ${relativePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } if (errors.length > 0) { throw new Error(`Failed to restore some files:\n${errors.join('\n')}`); } } /** * Get list of modified files in the workspace * Uses git to detect modified files if available, otherwise returns empty array */ getModifiedFiles() { try { const modifiedOutput = execSync('git diff --name-only HEAD', { cwd: this.workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }).trim(); const untrackedOutput = execSync('git ls-files --others --exclude-standard', { cwd: this.workspaceRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }).trim(); const modifiedFiles = modifiedOutput ? modifiedOutput.split('\n').filter(Boolean) : []; const untrackedFiles = untrackedOutput ? untrackedOutput.split('\n').filter(Boolean) : []; const allFiles = [...new Set([...modifiedFiles, ...untrackedFiles])]; const ig = loadGitignore(this.workspaceRoot); const filtered = allFiles.filter(file => !ig.ignores(file)); if (filtered.length > MAX_CHECKPOINT_FILES) { logWarning('Too many modified files detected, limiting to maximum', true, { context: { fileCount: filtered.length, maxFiles: MAX_CHECKPOINT_FILES, }, }); return filtered.slice(0, MAX_CHECKPOINT_FILES); } return filtered; } catch { logWarning('Git not available for file tracking', true, { context: { workspaceRoot: this.workspaceRoot, }, }); return []; } } /** * Get the size of a file snapshot */ getSnapshotSize(snapshots) { let totalSize = 0; for (const content of snapshots.values()) { totalSize += Buffer.byteLength(content, 'utf-8'); } return totalSize; } /** * Validate that all files in the snapshot can be written to their locations */ async validateRestorePath(snapshots) { const errors = []; for (const relativePath of snapshots.keys()) { const absolutePath = path.resolve(this.workspaceRoot, relativePath); // nosemgrep const directory = path.dirname(absolutePath); try { let dirWritable = true; let directoryExists = false; try { const dirStats = await fs.stat(directory); directoryExists = dirStats.isDirectory(); } catch { const parentDir = path.dirname(directory); let parentWritable = true; if (parentDir !== directory) { try { const parentStats = await fs.stat(parentDir); const parentMode = parentStats.mode; // Check if any write permission bit is set - owner: 0o200, group: 0o020, others: 0o002 const parentHasWritePermission = (parentMode & 0o200) !== 0 || (parentMode & 0o020) !== 0 || (parentMode & 0o002) !== 0; if (!parentHasWritePermission) { parentWritable = false; dirWritable = false; errors.push(`Cannot create directory "${directory}": parent directory "${parentDir}" is read-only`); } } catch (_parentStatError) { parentWritable = true; } } if (parentWritable) { try { await fs.mkdir(directory, { recursive: true }); try { const verifyStats = await fs.stat(directory); directoryExists = verifyStats.isDirectory(); } catch { dirWritable = false; directoryExists = false; errors.push(`Cannot create directory "${directory}": directory creation failed`); } } catch (mkdirError) { dirWritable = false; directoryExists = false; errors.push(`Cannot create directory "${directory}": ${mkdirError instanceof Error ? mkdirError.message : 'Unknown error'}`); } } else { directoryExists = false; } } if (dirWritable && directoryExists) { try { const dirStats = await fs.stat(directory); const mode = dirStats.mode; const hasWritePermission = (mode & 0o200) !== 0 || (mode & 0o020) !== 0 || (mode & 0o002) !== 0; if (!hasWritePermission) { dirWritable = false; errors.push(`Directory "${directory}" is not writable: read-only permissions detected`); } } catch (statError) { dirWritable = false; errors.push(`Directory "${directory}" is not writable: ${statError instanceof Error ? statError.message : 'Unknown error'}`); } } // If directory is not writable or was not successfully created, skip further checks for this file if (!dirWritable) { continue; } if (existsSync(absolutePath)) { try { const fileStats = await fs.stat(absolutePath); const mode = fileStats.mode; const hasWritePermission = (mode & 0o200) !== 0 || (mode & 0o020) !== 0 || (mode & 0o002) !== 0; if (!hasWritePermission) { errors.push(`Cannot write to file "${absolutePath}": read-only permissions detected`); } } catch (fileError) { errors.push(`Cannot write to file "${absolutePath}": ${fileError instanceof Error ? fileError.message : 'Unknown error'}`); } } } catch (error) { errors.push(`Cannot validate path for ${relativePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } return { valid: errors.length === 0, errors }; } } //# sourceMappingURL=file-snapshot.js.map