/** * Entity file management functions for the knowledge base. * * Maps entity types to directory paths, reads/writes/lists/deletes entity YAML files. * Uses fs/promises for all file operations. */ import { readFile, writeFile, readdir, unlink, access, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import type { Entity, EntityType } from '../schemas/types'; import { serializeEntity, deserializeEntity } from './yaml-utils'; import { validateEntity } from '../schemas/validate'; /** * Maps entity types to their kb/ subdirectory names. */ const ENTITY_TYPE_DIRS: Record = { person: 'profiles', experience: 'experiences', skill: 'skills', organization: 'organizations', project: 'projects', certification: 'certifications', tandem: 'tandems', }; /** * Returns the directory path for a given entity type. */ export function getEntityDir(type: EntityType, basePath: string = process.cwd()): string { const dir = ENTITY_TYPE_DIRS[type]; return join(basePath, 'kb', dir); } /** * Constructs the full file path for an entity given its type and ID. */ export function getEntityPath(type: EntityType, id: string, basePath: string = process.cwd()): string { return join(getEntityDir(type, basePath), `${id}.yaml`); } /** * Reads an entity from its YAML file. * Throws a descriptive error if the file is not found. */ export async function readEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise { const filePath = getEntityPath(type, id, basePath); let content: string; try { content = await readFile(filePath, 'utf-8'); } catch (err: unknown) { if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`); } throw err; } return deserializeEntity(content); } /** * Writes an entity to its YAML file. * Validates the entity before writing; rejects invalid entities with validation errors. */ export async function writeEntity(entity: Entity, basePath: string = process.cwd()): Promise { const result = validateEntity(entity); if (!result.valid) { throw new Error(`Validation failed for entity "${entity.id}": ${result.errors.join('; ')}`); } const filePath = getEntityPath(entity.type, entity.id, basePath); const dir = getEntityDir(entity.type, basePath); await mkdir(dir, { recursive: true }); const yaml = serializeEntity(entity); await writeFile(filePath, yaml, 'utf-8'); } /** * Lists all entity IDs in a given type directory. * Returns an array of entity IDs (file names without .yaml extension). */ export async function listEntities(type: EntityType, basePath: string = process.cwd()): Promise { const dir = getEntityDir(type, basePath); let files: string[]; try { files = await readdir(dir); } catch (err: unknown) { if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { return []; } throw err; } return files .filter((f) => f.endsWith('.yaml')) .map((f) => f.replace(/\.yaml$/, '')); } /** * Deletes an entity file. * Throws a descriptive error if the file is not found. */ export async function deleteEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise { const filePath = getEntityPath(type, id, basePath); try { await unlink(filePath); } catch (err: unknown) { if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`); } throw err; } } /** * Checks if an entity file exists. */ export async function entityExists(type: EntityType, id: string, basePath: string = process.cwd()): Promise { const filePath = getEntityPath(type, id, basePath); try { await access(filePath); return true; } catch { return false; } }