@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
JavaScript
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