@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
366 lines (311 loc) • 10.2 kB
text/typescript
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;
}
}
}