UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

477 lines (415 loc) 14.1 kB
import { promises as fs } from 'fs'; import path from 'path'; import { StorageManager } from '../../storage/storage-manager.js'; import { ProductRoadmap, RoadmapTheme, Initiative, Feature, Milestone, Release, RoadmapReview } from './types.js'; export class RoadmapStore { private storageManager: StorageManager; private dataPath: string = ''; private initialized: boolean = false; constructor() { this.storageManager = new StorageManager(); } async initialize(): Promise<void> { if (this.initialized) return; const location = await this.storageManager.getStorageLocation(); this.dataPath = path.join(location.data, 'roadmaps'); // Ensure directory exists await fs.mkdir(this.dataPath, { recursive: true }); this.initialized = true; } // Roadmap operations async saveRoadmap(roadmap: ProductRoadmap): Promise<void> { await this.ensureInitialized(); const filePath = path.join(this.dataPath, `roadmap-${roadmap.id}.json`); await fs.writeFile(filePath, JSON.stringify(roadmap, null, 2)); } async loadRoadmap(roadmapId: string): Promise<ProductRoadmap | null> { await this.ensureInitialized(); const filePath = path.join(this.dataPath, `roadmap-${roadmapId}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const roadmap = JSON.parse(data); // Convert dates roadmap.createdAt = new Date(roadmap.createdAt); roadmap.updatedAt = new Date(roadmap.updatedAt); // Convert milestone dates if (roadmap.milestones) { roadmap.milestones.forEach((m: Milestone) => { m.date = new Date(m.date); }); } // Convert release dates if (roadmap.releases) { roadmap.releases.forEach((r: Release) => { r.date = new Date(r.date); }); } return roadmap; } catch (error) { return null; } } async listRoadmaps(): Promise<string[]> { await this.ensureInitialized(); try { const files = await fs.readdir(this.dataPath); return files .filter(f => f.startsWith('roadmap-') && f.endsWith('.json')) .map(f => f.replace('roadmap-', '').replace('.json', '')); } catch (error) { return []; } } async deleteRoadmap(roadmapId: string): Promise<void> { await this.ensureInitialized(); const filePath = path.join(this.dataPath, `roadmap-${roadmapId}.json`); try { await fs.unlink(filePath); } catch (error) { // Ignore if file doesn't exist } } // Bulk data operations async saveBulkData( data: { themes: RoadmapTheme[]; initiatives: Initiative[]; features: Feature[]; milestones: Milestone[]; releases: Release[]; reviews: RoadmapReview[]; } ): Promise<void> { await this.ensureInitialized(); // Save each data type await Promise.all([ fs.writeFile( path.join(this.dataPath, 'themes.json'), JSON.stringify(data.themes, null, 2) ), fs.writeFile( path.join(this.dataPath, 'initiatives.json'), JSON.stringify(data.initiatives, null, 2) ), fs.writeFile( path.join(this.dataPath, 'features.json'), JSON.stringify(data.features, null, 2) ), fs.writeFile( path.join(this.dataPath, 'milestones.json'), JSON.stringify(data.milestones, null, 2) ), fs.writeFile( path.join(this.dataPath, 'releases.json'), JSON.stringify(data.releases, null, 2) ), fs.writeFile( path.join(this.dataPath, 'reviews.json'), JSON.stringify(data.reviews, null, 2) ) ]); } async loadBulkData(): Promise<{ themes: Map<string, RoadmapTheme>; initiatives: Map<string, Initiative>; features: Map<string, Feature>; milestones: Map<string, Milestone>; releases: Map<string, Release>; reviews: Map<string, RoadmapReview>; }> { await this.ensureInitialized(); const result = { themes: new Map<string, RoadmapTheme>(), initiatives: new Map<string, Initiative>(), features: new Map<string, Feature>(), milestones: new Map<string, Milestone>(), releases: new Map<string, Release>(), reviews: new Map<string, RoadmapReview>() }; // Load themes try { const themesData = await fs.readFile( path.join(this.dataPath, 'themes.json'), 'utf-8' ); const themes: RoadmapTheme[] = JSON.parse(themesData); themes.forEach(theme => result.themes.set(theme.id, theme)); } catch (error) { // File doesn't exist yet } // Load initiatives try { const initiativesData = await fs.readFile( path.join(this.dataPath, 'initiatives.json'), 'utf-8' ); const initiatives: Initiative[] = JSON.parse(initiativesData); initiatives.forEach(init => result.initiatives.set(init.id, init)); } catch (error) { // File doesn't exist yet } // Load features try { const featuresData = await fs.readFile( path.join(this.dataPath, 'features.json'), 'utf-8' ); const features: Feature[] = JSON.parse(featuresData); features.forEach(feat => result.features.set(feat.id, feat)); } catch (error) { // File doesn't exist yet } // Load milestones try { const milestonesData = await fs.readFile( path.join(this.dataPath, 'milestones.json'), 'utf-8' ); const milestones: Milestone[] = JSON.parse(milestonesData); milestones.forEach(m => { m.date = new Date(m.date); result.milestones.set(m.id, m); }); } catch (error) { // File doesn't exist yet } // Load releases try { const releasesData = await fs.readFile( path.join(this.dataPath, 'releases.json'), 'utf-8' ); const releases: Release[] = JSON.parse(releasesData); releases.forEach(r => { r.date = new Date(r.date); result.releases.set(r.id, r); }); } catch (error) { // File doesn't exist yet } // Load reviews try { const reviewsData = await fs.readFile( path.join(this.dataPath, 'reviews.json'), 'utf-8' ); const reviews: RoadmapReview[] = JSON.parse(reviewsData); reviews.forEach(review => { review.reviewDate = new Date(review.reviewDate); if (review.nextReviewDate) { review.nextReviewDate = new Date(review.nextReviewDate); } result.reviews.set(review.id, review); }); } catch (error) { // File doesn't exist yet } return result; } // Index operations for efficient queries async buildIndices(): Promise<{ roadmapThemes: Map<string, string[]>; // roadmapId -> themeIds themeInitiatives: Map<string, string[]>; // themeId -> initiativeIds initiativeFeatures: Map<string, string[]>; // initiativeId -> featureIds releaseFeatures: Map<string, string[]>; // releaseId -> featureIds }> { await this.ensureInitialized(); const indices = { roadmapThemes: new Map<string, string[]>(), themeInitiatives: new Map<string, string[]>(), initiativeFeatures: new Map<string, string[]>(), releaseFeatures: new Map<string, string[]>() }; // Build indices from roadmaps const roadmapIds = await this.listRoadmaps(); for (const roadmapId of roadmapIds) { const roadmap = await this.loadRoadmap(roadmapId); if (roadmap) { indices.roadmapThemes.set( roadmapId, roadmap.themes.map(t => typeof t === 'string' ? t : t.id) ); } } // Build other indices from bulk data const bulkData = await this.loadBulkData(); // Theme -> Initiatives for (const [themeId, theme] of bulkData.themes) { indices.themeInitiatives.set( themeId, theme.initiatives.map(i => typeof i === 'string' ? i : i.id) ); } // Initiative -> Features for (const [initId, initiative] of bulkData.initiatives) { indices.initiativeFeatures.set( initId, initiative.features.map(f => typeof f === 'string' ? f : f.id) ); } // Release -> Features for (const [releaseId, release] of bulkData.releases) { indices.releaseFeatures.set(releaseId, release.features); } return indices; } // Export/Import operations async exportRoadmap(roadmapId: string): Promise<string> { await this.ensureInitialized(); const roadmap = await this.loadRoadmap(roadmapId); if (!roadmap) { throw new Error(`Roadmap ${roadmapId} not found`); } const bulkData = await this.loadBulkData(); const exportData = { roadmap, themes: [] as RoadmapTheme[], initiatives: [] as Initiative[], features: [] as Feature[], milestones: [] as Milestone[], releases: [] as Release[] }; // Collect related data for (const theme of roadmap.themes) { const themeId = typeof theme === 'string' ? theme : theme.id; const themeData = bulkData.themes.get(themeId); if (themeData) { exportData.themes.push(themeData); // Collect initiatives for (const init of themeData.initiatives) { const initId = typeof init === 'string' ? init : init.id; const initData = bulkData.initiatives.get(initId); if (initData) { exportData.initiatives.push(initData); // Collect features for (const feat of initData.features) { const featId = typeof feat === 'string' ? feat : feat.id; const featData = bulkData.features.get(featId); if (featData) { exportData.features.push(featData); } } } } } } // Collect milestones for (const milestone of roadmap.milestones) { const milestoneId = typeof milestone === 'string' ? milestone : milestone.id; const milestoneData = bulkData.milestones.get(milestoneId); if (milestoneData) { exportData.milestones.push(milestoneData); } } // Collect releases for (const release of roadmap.releases) { const releaseId = typeof release === 'string' ? release : release.id; const releaseData = bulkData.releases.get(releaseId); if (releaseData) { exportData.releases.push(releaseData); } } return JSON.stringify(exportData, null, 2); } async importRoadmap(jsonData: string): Promise<string> { await this.ensureInitialized(); const importData = JSON.parse(jsonData); const roadmap = importData.roadmap; // Generate new IDs to avoid conflicts const idMap = new Map<string, string>(); // Generate new roadmap ID const newRoadmapId = `roadmap-${this.generateId()}`; idMap.set(roadmap.id, newRoadmapId); roadmap.id = newRoadmapId; // Update theme IDs if (importData.themes) { for (const theme of importData.themes) { const newId = `theme-${this.generateId()}`; idMap.set(theme.id, newId); theme.id = newId; } } // Update initiative IDs if (importData.initiatives) { for (const init of importData.initiatives) { const newId = `initiative-${this.generateId()}`; idMap.set(init.id, newId); init.id = newId; init.themeId = idMap.get(init.themeId) || init.themeId; } } // Update feature IDs if (importData.features) { for (const feat of importData.features) { const newId = `feature-${this.generateId()}`; idMap.set(feat.id, newId); feat.id = newId; feat.initiativeId = idMap.get(feat.initiativeId) || feat.initiativeId; } } // Update references in roadmap roadmap.themes = roadmap.themes.map((t: any) => idMap.get(typeof t === 'string' ? t : t.id) || t ); // Update references in themes if (importData.themes) { for (const theme of importData.themes) { theme.initiatives = theme.initiatives.map((i: any) => idMap.get(typeof i === 'string' ? i : i.id) || i ); } } // Update references in initiatives if (importData.initiatives) { for (const init of importData.initiatives) { init.features = init.features.map((f: any) => idMap.get(typeof f === 'string' ? f : f.id) || f ); } } // Save imported data await this.saveRoadmap(roadmap); const bulkData = await this.loadBulkData(); // Merge imported data if (importData.themes) { importData.themes.forEach((t: RoadmapTheme) => bulkData.themes.set(t.id, t)); } if (importData.initiatives) { importData.initiatives.forEach((i: Initiative) => bulkData.initiatives.set(i.id, i)); } if (importData.features) { importData.features.forEach((f: Feature) => bulkData.features.set(f.id, f)); } if (importData.milestones) { importData.milestones.forEach((m: Milestone) => bulkData.milestones.set(m.id, m)); } if (importData.releases) { importData.releases.forEach((r: Release) => bulkData.releases.set(r.id, r)); } await this.saveBulkData({ themes: Array.from(bulkData.themes.values()), initiatives: Array.from(bulkData.initiatives.values()), features: Array.from(bulkData.features.values()), milestones: Array.from(bulkData.milestones.values()), releases: Array.from(bulkData.releases.values()), reviews: Array.from(bulkData.reviews.values()) }); return newRoadmapId; } private async ensureInitialized(): Promise<void> { if (!this.initialized) { await this.initialize(); } } private generateId(): string { return `${Date.now().toString(16)}-${Math.random().toString(16).substr(2, 8)}`; } }