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