Skip to main content
Glama
file-plan-repository.ts8.77 kB
/** * File-based Plan Repository Implementation * * Manages plan-level operations: * - Plan directory structure creation/deletion * - Manifest CRUD operations with atomic writes * - Active plans index for workspace tracking * * Uses atomic writes (write-file-atomic) for data integrity on Windows. * * Extends BaseFileRepository to inherit common functionality: * - atomicWriteJSON() for safe file writes * - loadJSON() for file reading */ import * as fs from 'fs/promises'; import * as path from 'path'; import type { PlanRepository } from '../../../domain/repositories/interfaces.js'; import type { PlanManifest, ActivePlansIndex, VersionHistory } from '../../../domain/entities/types.js'; import { BaseFileRepository } from './base-file-repository.js'; /** * File-based implementation of PlanRepository * * Extends BaseFileRepository for common file operations. * * Directory structure: * ``` * baseDir/ * plans/ * {planId}/ * manifest.json * entities/ * requirements.json * solutions.json * decisions.json * phases.json * artifacts.json * links.json * history/ * requirement/ * solution/ * decision/ * phase/ * artifact/ * versions/ * exports/ * active-plans.json * ``` */ export class FilePlanRepository extends BaseFileRepository implements PlanRepository { private readonly plansDir: string; private readonly activePlansPath: string; constructor(baseDir: string) { super(baseDir); this.plansDir = path.join(baseDir, 'plans'); this.activePlansPath = path.join(baseDir, 'active-plans.json'); } /** * Initialize plan storage * * Creates base directory structure for plans and active plans index. * Safe to call multiple times (idempotent). */ public async initialize(): Promise<void> { if (this.isInitializedState()) { return; } await fs.mkdir(this.plansDir, { recursive: true }); await fs.mkdir(path.join(this.baseDir, '.history'), { recursive: true }); this.markInitialized(); } /** * Create plan directory structure * * Creates all necessary subdirectories for a new plan: * - entities/ (for entity JSON files) * - history/ (for version history, with subdirs per entity type) * - versions/ (for plan snapshots/deltas) * - exports/ (for exported files) * * @param planId - Plan ID */ public async createPlan(planId: string): Promise<void> { await this.ensureInitialized(); const planDir = path.join(this.plansDir, planId); // Create main directories await fs.mkdir(planDir, { recursive: true }); await fs.mkdir(path.join(planDir, 'entities'), { recursive: true }); await fs.mkdir(path.join(planDir, 'versions'), { recursive: true }); await fs.mkdir(path.join(planDir, 'exports'), { recursive: true }); // Create history directories for each entity type await fs.mkdir(path.join(planDir, 'history'), { recursive: true }); await fs.mkdir(path.join(planDir, 'history', 'requirement'), { recursive: true }); await fs.mkdir(path.join(planDir, 'history', 'solution'), { recursive: true }); await fs.mkdir(path.join(planDir, 'history', 'decision'), { recursive: true }); await fs.mkdir(path.join(planDir, 'history', 'phase'), { recursive: true }); await fs.mkdir(path.join(planDir, 'history', 'artifact'), { recursive: true }); } /** * Delete plan and all its data * * Recursively removes plan directory and all contents. * Safe to call even if plan doesn't exist. * * @param planId - Plan ID */ public async deletePlan(planId: string): Promise<void> { await this.ensureInitialized(); const planDir = path.join(this.plansDir, planId); await fs.rm(planDir, { recursive: true, force: true }); } /** * List all plan IDs * * Returns array of plan IDs (directory names in plans/). * Returns empty array if plans directory doesn't exist. * * @returns Array of plan IDs */ public async listPlans(): Promise<string[]> { await this.ensureInitialized(); try { const entries = await fs.readdir(this.plansDir, { withFileTypes: true }); return entries .filter((entry) => entry.isDirectory()) .map((entry) => entry.name); } catch { return []; } } /** * Check if plan exists * * @param planId - Plan ID * @returns true if plan directory exists */ public async planExists(planId: string): Promise<boolean> { await this.ensureInitialized(); try { const planDir = path.join(this.plansDir, planId); await fs.access(planDir); return true; } catch { return false; } } /** * Save plan manifest * * Uses atomic write to prevent data corruption (delegates to base class). * * @param planId - Plan ID * @param manifest - Plan manifest object */ public async saveManifest(planId: string, manifest: PlanManifest): Promise<void> { await this.ensureInitialized(); const manifestPath = path.join(this.plansDir, planId, 'manifest.json'); await this.atomicWriteJSON(manifestPath, manifest); } /** * Load plan manifest * * Delegates to base class loadJSON(). * * @param planId - Plan ID * @returns Plan manifest object * @throws If manifest file doesn't exist or is invalid JSON */ public async loadManifest(planId: string): Promise<PlanManifest> { await this.ensureInitialized(); const manifestPath = path.join(this.plansDir, planId, 'manifest.json'); return this.loadJSON<PlanManifest>(manifestPath); } /** * Save active plans index * * Stores workspace -> plan mapping for active plan tracking. * Uses atomic write to prevent data corruption (delegates to base class). * * @param index - Active plans index object */ public async saveActivePlans(index: ActivePlansIndex): Promise<void> { await this.ensureInitialized(); await this.atomicWriteJSON(this.activePlansPath, index); } /** * Load active plans index * * Returns empty object if index file doesn't exist. * * @returns Active plans index object */ public async loadActivePlans(): Promise<ActivePlansIndex> { await this.ensureInitialized(); try { return await this.loadJSON<ActivePlansIndex>(this.activePlansPath); } catch { return {}; } } /** * Save export file * * Saves exported content to the plan's exports directory. * * @param planId - Plan ID * @param filename - Export filename (e.g. 'plan-export.md') * @param content - Export content * @returns Full path to saved file */ public async saveExport(planId: string, filename: string, content: string): Promise<string> { await this.ensureInitialized(); const exportPath = path.join(this.plansDir, planId, 'exports', filename); await fs.writeFile(exportPath, content, 'utf-8'); return exportPath; } /** * Save version history for an entity * * Uses atomic write (delegates to base class). * * @param planId - Plan ID * @param entityType - Entity type (requirement, solution, etc.) * @param entityId - Entity ID * @param history - Version history data */ public async saveVersionHistory(planId: string, entityType: string, entityId: string, history: VersionHistory): Promise<void> { await this.ensureInitialized(); const historyPath = path.join(this.plansDir, planId, 'history', entityType, `${entityId}.json`); await this.atomicWriteJSON(historyPath, history); } /** * Load version history for an entity * * Uses loadJSON from base class. * * @param planId - Plan ID * @param entityType - Entity type * @param entityId - Entity ID * @returns Version history data, or null if not found */ public async loadVersionHistory(planId: string, entityType: string, entityId: string): Promise<VersionHistory | null> { await this.ensureInitialized(); const historyPath = path.join(this.plansDir, planId, 'history', entityType, `${entityId}.json`); try { return await this.loadJSON<VersionHistory>(historyPath); } catch { return null; } } /** * Delete version history for an entity * * @param planId - Plan ID * @param entityType - Entity type * @param entityId - Entity ID */ public async deleteVersionHistory(planId: string, entityType: string, entityId: string): Promise<void> { await this.ensureInitialized(); const historyPath = path.join(this.plansDir, planId, 'history', entityType, `${entityId}.json`); try { await fs.unlink(historyPath); } catch { // File doesn't exist, ignore } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cppmyjob/cpp-mcp-planner'

If you have feedback or need assistance with the MCP directory API, please join our Discord server