UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

345 lines 13.7 kB
import { promises as fs } from 'fs'; import path from 'path'; import { StorageManager } from '../../storage/storage-manager.js'; export class RoadmapStore { storageManager; dataPath = ''; initialized = false; constructor() { this.storageManager = new StorageManager(); } async initialize() { 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) { 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) { 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) => { m.date = new Date(m.date); }); } // Convert release dates if (roadmap.releases) { roadmap.releases.forEach((r) => { r.date = new Date(r.date); }); } return roadmap; } catch (error) { return null; } } async listRoadmaps() { 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) { 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) { 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() { await this.ensureInitialized(); const result = { themes: new Map(), initiatives: new Map(), features: new Map(), milestones: new Map(), releases: new Map(), reviews: new Map() }; // Load themes try { const themesData = await fs.readFile(path.join(this.dataPath, 'themes.json'), 'utf-8'); const themes = 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 = 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 = 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 = 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 = 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 = 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() { await this.ensureInitialized(); const indices = { roadmapThemes: new Map(), themeInitiatives: new Map(), initiativeFeatures: new Map(), releaseFeatures: new Map() }; // 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) { 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: [], initiatives: [], features: [], milestones: [], releases: [] }; // 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) { await this.ensureInitialized(); const importData = JSON.parse(jsonData); const roadmap = importData.roadmap; // Generate new IDs to avoid conflicts const idMap = new Map(); // 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) => 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) => 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) => 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) => bulkData.themes.set(t.id, t)); } if (importData.initiatives) { importData.initiatives.forEach((i) => bulkData.initiatives.set(i.id, i)); } if (importData.features) { importData.features.forEach((f) => bulkData.features.set(f.id, f)); } if (importData.milestones) { importData.milestones.forEach((m) => bulkData.milestones.set(m.id, m)); } if (importData.releases) { importData.releases.forEach((r) => 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; } async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } generateId() { return `${Date.now().toString(16)}-${Math.random().toString(16).substr(2, 8)}`; } } //# sourceMappingURL=store.js.map