fileManager.ts•6.54 kB
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import type { ProjectConfig } from '../types/index.js';
import { CONSTANTS, FileOperationError } from '../constants/index.js';
export class FileManager {
private readonly homeConfigDir: string;
constructor() {
this.homeConfigDir = join(homedir(), '.ai-conversation-logger');
}
private async detectProjectRoot(startPath: string = process.cwd()): Promise<string | null> {
const indicators = ['package.json', '.git', 'Cargo.toml', 'go.mod', 'pom.xml', 'composer.json'];
let currentDir = startPath;
const root = dirname(currentDir);
while (currentDir !== root) {
for (const indicator of indicators) {
const indicatorPath = join(currentDir, indicator);
try {
await fs.access(indicatorPath);
return currentDir;
} catch {
// Continue searching
}
}
currentDir = dirname(currentDir);
}
return null;
}
private getProjectNameFromPath(projectRoot: string): string {
return projectRoot.split('/').pop() || 'unknown-project';
}
async ensureDirectoryExists(dirPath: string): Promise<void> {
try {
await fs.mkdir(dirPath, { recursive: true });
} catch {
throw new FileOperationError(`Failed to create directory`, dirPath, 'mkdir');
}
}
async writeFile(filePath: string, content: string): Promise<void> {
const dir = dirname(filePath);
await this.ensureDirectoryExists(dir);
try {
await fs.writeFile(filePath, content, 'utf8');
} catch (error) {
throw new FileOperationError(`Failed to write file`, filePath, 'write');
}
}
async readFile(filePath: string): Promise<string> {
try {
return await fs.readFile(filePath, 'utf8');
} catch (error) {
if ((error as { code?: string }).code === 'ENOENT') {
return '';
}
throw new FileOperationError(`Failed to read file`, filePath, 'read');
}
}
async fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async getProjectDir(project?: string): Promise<string> {
const projectRoot = await this.detectProjectRoot();
if (projectRoot) {
return join(projectRoot, CONSTANTS.AI_LOGS_DIR);
}
// Fallback to home directory if no project detected
const fallbackProject = project || 'default';
return join(this.homeConfigDir, 'projects', fallbackProject);
}
async getDailyLogPath(project: string, date: string): Promise<string> {
const projectDir = await this.getProjectDir(project);
return join(projectDir, `${date}.md`);
}
async getProjectConfigPath(project?: string): Promise<string> {
const projectDir = await this.getProjectDir(project);
return join(projectDir, 'config.json');
}
getIndexDir(): string {
return join(this.homeConfigDir, 'index');
}
getConfigDir(): string {
return join(this.homeConfigDir, 'config');
}
async getProjectInfo(): Promise<{ name: string; root: string | null; logsDir: string }> {
const projectRoot = await this.detectProjectRoot();
if (projectRoot) {
return {
name: this.getProjectNameFromPath(projectRoot),
root: projectRoot,
logsDir: join(projectRoot, CONSTANTS.AI_LOGS_DIR)
};
}
return {
name: 'default',
root: null,
logsDir: join(this.homeConfigDir, 'projects', 'default')
};
}
async listProjects(): Promise<string[]> {
// For the new design, we primarily work with the current project
// But we can also scan the global registry for known projects
const registryPath = join(this.getIndexDir(), 'projects-registry.json');
try {
const registryContent = await this.readFile(registryPath);
if (registryContent) {
const registry = JSON.parse(registryContent);
return Object.keys(registry);
}
} catch {
// Registry doesn't exist or is corrupted
}
// If no registry, return current project if detected
const projectInfo = await this.getProjectInfo();
if (projectInfo.root) {
return [projectInfo.name];
}
// Fallback: scan home config directory
try {
const projectsDir = join(this.homeConfigDir, 'projects');
const entries = await fs.readdir(projectsDir, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
} catch (error) {
if ((error as { code?: string }).code === 'ENOENT') {
return [];
}
const projectsDir = join(this.homeConfigDir, 'projects');
throw new FileOperationError(`Failed to list projects`, projectsDir, 'readdir');
}
}
async initializeProject(project?: string): Promise<void> {
const projectInfo = await this.getProjectInfo();
const projectDir = await this.getProjectDir(project);
await this.ensureDirectoryExists(projectDir);
const configPath = await this.getProjectConfigPath(project);
const configExists = await this.fileExists(configPath);
if (!configExists) {
const defaultConfig: ProjectConfig = {
name: project || projectInfo.name,
description: `AI conversation logs for project: ${project || projectInfo.name}`,
defaultTags: [],
autoTagging: true,
exportSettings: {
includeCode: true,
includeActions: true
}
};
await this.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
}
// Update global registry
await this.updateProjectRegistry(project || projectInfo.name, projectInfo.root || projectDir);
}
private async updateProjectRegistry(projectName: string, projectPath: string): Promise<void> {
const registryPath = join(this.getIndexDir(), 'projects-registry.json');
await this.ensureDirectoryExists(this.getIndexDir());
let registry: Record<string, { path: string; lastAccessed: string }> = {};
try {
const registryContent = await this.readFile(registryPath);
if (registryContent) {
registry = JSON.parse(registryContent);
}
} catch {
// Registry doesn't exist, start fresh
}
registry[projectName] = {
path: projectPath,
lastAccessed: new Date().toISOString()
};
await this.writeFile(registryPath, JSON.stringify(registry, null, 2));
}
}