UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

293 lines 10.1 kB
import { promises as fs, accessSync } from 'fs'; import path from 'path'; import os from 'os'; export class StorageManager { config; projectRoot; atlasRoot; static MARKER_FILE = '.atlas-mcp'; constructor(config) { 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() { let base; 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() { return this.projectRoot; } /** * Get path for storing module data */ async getModuleDataPath(moduleName, filename) { 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) { return path.join(this.projectRoot, ...pathSegments); } /** * Ensure storage directories exist */ async ensureStorageDirectories() { 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, filename, data) { 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, filename) { 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) { if (error.code === 'ENOENT') { return null; } throw error; } } /** * Export all data to a portable format */ async exportAllData() { 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) { 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) { 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) { if (error.code !== 'ENOENT') { throw error; } } } } /** * Initialize a project with a marker file */ async initializeProject(projectName) { 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 = { 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() { const markerPath = path.join(this.projectRoot, StorageManager.MARKER_FILE); try { const content = await fs.readFile(markerPath, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Find the project root by looking for marker file */ findProjectRoot() { 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 */ async getProjectId() { // 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 */ generateProjectId() { const projectName = path.basename(this.projectRoot); const pathHash = this.hashString(this.projectRoot); return `${projectName}-${pathHash.substring(0, 8)}`; } /** * Simple string hash function */ hashString(str) { 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 */ async copyDirectory(src, dest) { 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() { return this.projectRoot === this.atlasRoot; } /** * Ensure we're not saving user data in Atlas repo */ validateNotInAtlasRepo() { 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 */ async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } } //# sourceMappingURL=storage-manager.js.map