ailock
Version:
AI-Proof File Guard - Protect sensitive files from accidental AI modifications
261 lines • 8.48 kB
JavaScript
import { resolve, basename, dirname, relative } from 'path';
import { existsSync, statSync } from 'fs';
import { homedir } from 'os';
import { randomUUID } from 'crypto';
import { getRepoRoot } from './git.js';
/**
* Project detection and management utilities for the new quota system
*/
/**
* Detect if a path belongs to a Git repository and return the repo root
*/
export async function detectGitRepository(filePath) {
try {
const repoRoot = await getRepoRoot(filePath);
return {
isGitRepo: repoRoot !== null,
repoRoot
};
}
catch {
return {
isGitRepo: false,
repoRoot: null
};
}
}
/**
* Generate a display name for a project
*/
export function generateProjectName(rootPath, type) {
const baseName = basename(rootPath);
// Special cases
if (rootPath === '/') {
return 'root';
}
// Home directory detection
const homeDir = homedir();
if (homeDir && rootPath === homeDir) {
return 'home';
}
// Sanitize special characters in project names
const sanitized = baseName ? baseName.replace(/[^a-zA-Z0-9\-_\.]/g, '-').replace(/-+/g, '-') : 'Directory';
// For Git repositories, use the directory name
if (type === 'git') {
return sanitized || 'Repository';
}
// For standalone directories, use a more descriptive name
if (baseName && baseName.startsWith('.')) {
// For hidden directories like ~/.config, show the parent context
const parentName = basename(dirname(rootPath));
return `${parentName}/${sanitized}`.replace(/^\//, '');
}
return sanitized;
}
/**
* Create a new ProjectUnit from a file path
*/
export async function createProjectFromPath(filePath, existingProject) {
const normalizedPath = resolve(filePath);
// Check if this is a file or directory
const isDirectory = existsSync(normalizedPath) &&
statSync(normalizedPath).isDirectory();
// Try to detect if this is part of a Git repository
const gitInfo = await detectGitRepository(normalizedPath);
let rootPath;
let type;
let relativePath;
if (gitInfo.isGitRepo && gitInfo.repoRoot) {
rootPath = gitInfo.repoRoot;
type = 'git';
// Calculate relative path from repo root
relativePath = isDirectory ? '.' :
relative(gitInfo.repoRoot, normalizedPath);
}
else {
// For non-git paths
if (isDirectory) {
rootPath = normalizedPath;
relativePath = '.';
}
else {
rootPath = dirname(normalizedPath);
relativePath = basename(normalizedPath);
}
type = 'directory';
}
const projectName = generateProjectName(rootPath, type);
// If we have an existing project, merge with it
if (existingProject) {
const protectedPaths = [...existingProject.protectedPaths];
if (!protectedPaths.includes(relativePath)) {
protectedPaths.push(relativePath);
}
return {
...existingProject,
protectedPaths,
lastAccessedAt: new Date()
};
}
return {
id: randomUUID(),
rootPath,
type,
name: projectName,
protectedPaths: [relativePath],
createdAt: new Date(),
lastAccessedAt: new Date()
};
}
/**
* Find the project root for a given file path
* Returns the Git repository root if in a repo, otherwise the file's directory
*/
export async function findProjectRoot(filePath) {
const normalizedPath = resolve(filePath);
// Check if this is a directory
const isDirectory = existsSync(normalizedPath) &&
statSync(normalizedPath).isDirectory();
// Try to detect Git repository
const gitInfo = await detectGitRepository(normalizedPath);
if (gitInfo.isGitRepo && gitInfo.repoRoot) {
return gitInfo.repoRoot;
}
// For non-git paths, return the directory itself or parent directory
return isDirectory ? normalizedPath : dirname(normalizedPath);
}
/**
* Check if a file path belongs to an existing project
*/
export async function findMatchingProject(filePath, projects) {
const normalizedPath = resolve(filePath);
// First, find the project root for this path
const projectRoot = await findProjectRoot(filePath);
// Look for a project with matching root
return projects.find(project => project.rootPath === projectRoot) || null;
}
/**
* Check if two paths belong to the same project
*/
export async function belongsToSameProject(path1, path2) {
const [root1, root2] = await Promise.all([
findProjectRoot(path1),
findProjectRoot(path2)
]);
return root1 === root2;
}
/**
* Filter out temp/test directories from a list of paths
* This helps clean up quota counting by ignoring temporary directories
*/
export function filterTempDirectories(paths) {
const tempPatterns = [
/^\/tmp\//,
/^\/var\/tmp\//,
/\/AppData\/Local\/Temp\//,
/\/Temp\//,
/\\Temp\\/,
/node_modules\//,
/\.git\//,
/\.cache\//,
/dist\//,
/build\//,
/coverage\//
];
return paths.filter(path => {
// Skip null/undefined paths
if (!path) {
return false;
}
// Skip obvious system temp directories
if (path === '/tmp' || path === '/var/tmp' || path.startsWith('/tmp/') || path.startsWith('/var/tmp/')) {
return false;
}
// Skip MacOS temp directories but allow our test directories
if (path.includes('/var/folders/') && !path.includes('ailock-test')) {
return false;
}
return !tempPatterns.some(pattern => pattern.test(path));
});
}
/**
* Validate that a project root path is legitimate (not temp/test)
*/
export function isValidProjectRoot(rootPath) {
// Check system directories
const systemPaths = [
'/',
'/etc',
'/usr',
'/bin',
'/sbin',
'/var',
'/tmp',
'/System',
'C:\\',
'C:\\Windows',
'C:\\Program Files',
'C:\\Program Files (x86)'
];
if (systemPaths.includes(rootPath)) {
return false;
}
// Check if it's filtered out by temp directory filter
const filtered = filterTempDirectories([rootPath]);
if (filtered.length === 0) {
return false;
}
// Check if path exists
return existsSync(rootPath);
}
/**
* Clean up and consolidate project paths
* Removes duplicates and merges projects with the same root
*/
export function consolidateProjects(projects) {
const consolidated = new Map();
for (const project of projects) {
const existing = consolidated.get(project.rootPath);
if (existing) {
// Merge protected paths, avoiding duplicates
const allPaths = [...existing.protectedPaths, ...project.protectedPaths];
existing.protectedPaths = [...new Set(allPaths)];
// Keep the earlier creation date
if (project.createdAt < existing.createdAt) {
existing.createdAt = project.createdAt;
}
// Update last accessed time to the latest
if (project.lastAccessedAt && (!existing.lastAccessedAt || project.lastAccessedAt > existing.lastAccessedAt)) {
existing.lastAccessedAt = project.lastAccessedAt;
}
}
else {
consolidated.set(project.rootPath, { ...project });
}
}
return Array.from(consolidated.values())
.filter(project => isValidProjectRoot(project.rootPath))
.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* Generate a user-friendly project display path
*/
export function getProjectDisplayPath(projectPath) {
const homeDir = homedir();
if (homeDir && projectPath.startsWith(homeDir)) {
return projectPath.replace(homeDir, '~');
}
return projectPath;
}
/**
* Get project statistics for display
*/
export function getProjectStats(projects) {
return {
total: projects.length,
gitProjects: projects.filter(p => p.type === 'git').length,
directoryProjects: projects.filter(p => p.type === 'directory').length,
totalProtectedPaths: projects.reduce((sum, p) => sum + p.protectedPaths.length, 0)
};
}
//# sourceMappingURL=project-utils.js.map