@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
345 lines • 13.7 kB
JavaScript
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