UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

366 lines (311 loc) 10.2 kB
import { promises as fs, accessSync } from 'fs'; import path from 'path'; import os from 'os'; export interface StorageConfig { mode: 'project-local' | 'user-home' | 'custom'; customPath?: string; projectId?: string; autoSync?: boolean; syncInterval?: number; } export interface ProjectMarker { projectId: string; projectName: string; createdAt: string; atlasVersion: string; projectRoot: string; } export interface StorageLocation { base: string; data: string; config: string; exports: string; } export class StorageManager { private config: StorageConfig; private projectRoot: string; private atlasRoot: string; private static readonly MARKER_FILE = '.atlas-mcp'; constructor(config?: Partial<StorageConfig>) { this.projectRoot = this.findProjectRoot(); this.atlasRoot = path.dirname(path.dirname(new URL(import.meta.url).pathname)); this.config = { mode: 'project-local', autoSync: true, syncInterval: 300000, // 5 minutes ...config, }; } /** * Get the base storage path based on current configuration */ async getStorageLocation(): Promise<StorageLocation> { let base: string; switch (this.config.mode) { case 'user-home': const projectId = this.config.projectId || await this.getProjectId(); base = path.join(os.homedir(), '.atlas', 'projects', projectId); break; case 'custom': if (!this.config.customPath) { throw new Error('Custom path not specified for custom storage mode'); } base = this.config.customPath; break; case 'project-local': default: base = path.join(this.projectRoot, '.atlas'); break; } return { base, data: path.join(base, 'data'), config: path.join(base, 'config'), exports: path.join(base, 'exports'), }; } /** * Get the user's project directory (where their code is) */ getProjectRoot(): string { return this.projectRoot; } /** * Get path for storing module data */ async getModuleDataPath(moduleName: string, filename: string): Promise<string> { const location = await this.getStorageLocation(); return path.join(location.data, moduleName, filename); } /** * Get path for user-facing files (docs, configs that user should see) */ getProjectFilePath(...pathSegments: string[]): string { return path.join(this.projectRoot, ...pathSegments); } /** * Ensure storage directories exist */ async ensureStorageDirectories(): Promise<void> { const location = await this.getStorageLocation(); await fs.mkdir(location.base, { recursive: true }); await fs.mkdir(location.data, { recursive: true }); await fs.mkdir(location.config, { recursive: true }); await fs.mkdir(location.exports, { recursive: true }); } /** * Save data to storage */ async saveData(moduleName: string, filename: string, data: any): Promise<void> { const filePath = await this.getModuleDataPath(moduleName, filename); await fs.mkdir(path.dirname(filePath), { recursive: true }); const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); await fs.writeFile(filePath, content, 'utf-8'); } /** * Load data from storage */ async loadData(moduleName: string, filename: string): Promise<any> { const filePath = await this.getModuleDataPath(moduleName, filename); try { const content = await fs.readFile(filePath, 'utf-8'); if (filename.endsWith('.json')) { return JSON.parse(content); } return content; } catch (error: any) { if (error.code === 'ENOENT') { return null; } throw error; } } /** * Export all data to a portable format */ async exportAllData(): Promise<string> { const location = await this.getStorageLocation(); const exportId = `export-${Date.now()}`; const exportPath = path.join(location.exports, exportId); await fs.mkdir(exportPath, { recursive: true }); // Copy all data await this.copyDirectory(location.data, path.join(exportPath, 'data')); await this.copyDirectory(location.config, path.join(exportPath, 'config')); // Create metadata const metadata = { exportedAt: new Date().toISOString(), projectRoot: this.projectRoot, projectId: await this.getProjectId(), version: '1.0.0', }; await fs.writeFile( path.join(exportPath, 'metadata.json'), JSON.stringify(metadata, null, 2) ); return exportPath; } /** * Import data from an export */ async importData(exportPath: string): Promise<void> { const location = await this.getStorageLocation(); // Verify export structure const metadataPath = path.join(exportPath, 'metadata.json'); const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8')); // Backup current data const backupPath = path.join(location.base, 'backup', `backup-${Date.now()}`); await fs.mkdir(path.dirname(backupPath), { recursive: true }); await this.copyDirectory(location.data, backupPath); // Import data await this.copyDirectory(path.join(exportPath, 'data'), location.data); await this.copyDirectory(path.join(exportPath, 'config'), location.config); } /** * Sync specific data to project directory */ async syncToProject(items: Array<{ module: string; file: string; destination: string }>): Promise<void> { for (const item of items) { const sourcePath = await this.getModuleDataPath(item.module, item.file); const destPath = this.getProjectFilePath(item.destination); try { await fs.mkdir(path.dirname(destPath), { recursive: true }); await fs.copyFile(sourcePath, destPath); } catch (error: any) { if (error.code !== 'ENOENT') { throw error; } } } } /** * Initialize a project with a marker file */ async initializeProject(projectName?: string): Promise<ProjectMarker> { const markerPath = path.join(this.projectRoot, StorageManager.MARKER_FILE); // Check if already initialized if (await this.fileExists(markerPath)) { const existingMarker = await this.loadProjectMarker(); if (existingMarker) { throw new Error(`Project already initialized as "${existingMarker.projectName}"`); } } const marker: ProjectMarker = { projectId: this.generateProjectId(), projectName: projectName || path.basename(this.projectRoot), createdAt: new Date().toISOString(), atlasVersion: '1.0.0', projectRoot: this.projectRoot, }; await fs.writeFile(markerPath, JSON.stringify(marker, null, 2), 'utf-8'); return marker; } /** * Load project marker from current or parent directories */ async loadProjectMarker(): Promise<ProjectMarker | null> { const markerPath = path.join(this.projectRoot, StorageManager.MARKER_FILE); try { const content = await fs.readFile(markerPath, 'utf-8'); return JSON.parse(content) as ProjectMarker; } catch (error) { return null; } } /** * Find the project root by looking for marker file */ private findProjectRoot(): string { let currentDir = process.cwd(); const root = path.parse(currentDir).root; // Search up the directory tree for .atlas-mcp marker while (currentDir !== root) { const markerPath = path.join(currentDir, StorageManager.MARKER_FILE); try { // Use sync check during initialization accessSync(markerPath); return currentDir; } catch { // Continue searching up } currentDir = path.dirname(currentDir); } // No marker found, use current directory return process.cwd(); } /** * Get a unique project ID */ private async getProjectId(): Promise<string> { // First try to load from marker file const marker = await this.loadProjectMarker(); if (marker) { return marker.projectId; } // Otherwise generate based on path return this.generateProjectId(); } /** * Generate a new project ID */ private generateProjectId(): string { const projectName = path.basename(this.projectRoot); const pathHash = this.hashString(this.projectRoot); return `${projectName}-${pathHash.substring(0, 8)}`; } /** * Simple string hash function */ private hashString(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } /** * Copy directory recursively */ private async copyDirectory(src: string, dest: string): Promise<void> { await fs.mkdir(dest, { recursive: true }); const entries = await fs.readdir(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { await this.copyDirectory(srcPath, destPath); } else { await fs.copyFile(srcPath, destPath); } } } /** * Check if we're running inside the Atlas MCP repository */ isRunningInAtlasRepo(): boolean { return this.projectRoot === this.atlasRoot; } /** * Ensure we're not saving user data in Atlas repo */ validateNotInAtlasRepo(): void { if (this.isRunningInAtlasRepo() && this.config.mode === 'project-local') { throw new Error( 'Cannot use project-local storage mode when running from Atlas repository. ' + 'Please run from your project directory or use user-home storage mode.' ); } } /** * Check if a file exists */ private async fileExists(filePath: string): Promise<boolean> { try { await fs.access(filePath); return true; } catch { return false; } } }