import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { ensureRunsDir } from './runs-dir.js'; import type { GuidelineId } from './guideline-store.js'; export type JourneyId = string; export type JourneyNodeId = string; export type JourneyEdgeId = string; export interface JourneyNode { id: JourneyNodeId; creationUtc: string; action: string | null; tools: string[]; metadata: Record; description: string | null; labels: string[]; } export interface JourneyEdge { id: JourneyEdgeId; creationUtc: string; source: JourneyNodeId; target: JourneyNodeId; condition: string | null; metadata: Record; } export interface Journey { id: JourneyId; creationUtc: string; description: string; triggers: GuidelineId[]; title: string; rootId: JourneyNodeId; tags: string[]; labels: string[]; priority: number; } interface JourneyDocument { id: string; version: string; creation_utc: string; title: string; description: string; root_id: JourneyNodeId; labels: string[]; priority: number; } interface NodeDocument { id: string; node_id: JourneyNodeId; journey_id: JourneyId; creation_utc: string; action: string | null; tools: string[]; metadata: Record; description: string | null; labels: string[]; } interface EdgeDocument { id: string; journey_id: JourneyId; creation_utc: string; source: JourneyNodeId; target: JourneyNodeId; condition: string | null; metadata: Record; } interface TriggerDocument { id: string; journey_id: JourneyId; trigger: GuidelineId; creation_utc: string; } function generateId(): string { const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; let result = ''; for (let i = 0; i < 10; i++) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; } function dbPath(name: string, projectRoot?: string): string { const dir = join(ensureRunsDir(projectRoot), '..', 'journeys'); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); return join(dir, `${name}.json`); } function readCollection(name: string, projectRoot?: string): T[] { try { return JSON.parse(readFileSync(dbPath(name, projectRoot), 'utf-8')) as T[]; } catch { return []; } } function writeCollection(name: string, data: T[], projectRoot?: string): void { writeFileSync(dbPath(name, projectRoot), JSON.stringify(data, null, 2), 'utf-8'); } export class JourneyStore { createJourney(params: { title: string; description: string; triggers?: GuidelineId[]; labels?: string[]; priority?: number; }, projectRoot?: string): Journey { const id = `jny_${generateId()}`; const rootId = `node_${generateId()}`; const creationUtc = new Date().toISOString(); const journey: Journey = { id, creationUtc, description: params.description, triggers: params.triggers || [], title: params.title, rootId, tags: [], labels: params.labels || [], priority: params.priority || 0, }; const journeys = readCollection('journeys', projectRoot); journeys.push({ id, version: '0.7.0', creation_utc: creationUtc, title: params.title, description: params.description, root_id: rootId, labels: params.labels || [], priority: params.priority || 0, }); writeCollection('journeys', journeys, projectRoot); const root: JourneyNode = { id: rootId, creationUtc, action: null, tools: [], metadata: {}, description: null, labels: [], }; const nodes = readCollection('nodes', projectRoot); nodes.push({ id: `nd_${generateId()}`, node_id: rootId, journey_id: id, creation_utc: creationUtc, action: null, tools: [], metadata: {}, description: null, labels: [], }); writeCollection('nodes', nodes, projectRoot); return journey; } readJourney(id: JourneyId, projectRoot?: string): Journey { const journeys = readCollection('journeys', projectRoot); const doc = journeys.find(j => j.id === id); if (!doc) throw new Error(`Journey '${id}' not found`); const triggers = readCollection('triggers', projectRoot) .filter(t => t.journey_id === id) .map(t => t.trigger); return { id: doc.id, creationUtc: doc.creation_utc, description: doc.description, triggers, title: doc.title, rootId: doc.root_id, tags: [], labels: doc.labels || [], priority: doc.priority || 0, }; } deleteJourney(id: JourneyId, projectRoot?: string): void { let journeys = readCollection('journeys', projectRoot); const idx = journeys.findIndex(j => j.id === id); if (idx === -1) throw new Error(`Journey '${id}' not found`); journeys.splice(idx, 1); writeCollection('journeys', journeys, projectRoot); let nodes = readCollection('nodes', projectRoot); nodes = nodes.filter(n => n.journey_id !== id); writeCollection('nodes', nodes, projectRoot); let edges = readCollection('edges', projectRoot); edges = edges.filter(e => e.journey_id !== id); writeCollection('edges', edges, projectRoot); let triggers = readCollection('triggers', projectRoot); triggers = triggers.filter(t => t.journey_id !== id); writeCollection('triggers', triggers, projectRoot); } listJourneys(projectRoot?: string): Journey[] { const journeys = readCollection('journeys', projectRoot); return journeys.map(j => this.readJourney(j.id, projectRoot)); } createNode(journeyId: JourneyId, params: { action?: string | null; tools?: string[]; description?: string | null; labels?: string[]; id?: JourneyNodeId; }, projectRoot?: string): JourneyNode { const nodeId = params.id || `node_${generateId()}`; const creationUtc = new Date().toISOString(); const node: JourneyNode = { id: nodeId, creationUtc, action: params.action ?? null, tools: params.tools || [], metadata: {}, description: params.description ?? null, labels: params.labels || [], }; const nodes = readCollection('nodes', projectRoot); nodes.push({ id: `nd_${generateId()}`, node_id: nodeId, journey_id: journeyId, creation_utc: creationUtc, action: node.action, tools: node.tools, metadata: node.metadata, description: node.description, labels: node.labels, }); writeCollection('nodes', nodes, projectRoot); return node; } listNodes(journeyId: JourneyId, projectRoot?: string): JourneyNode[] { const docs = readCollection('nodes', projectRoot) .filter(n => n.journey_id === journeyId); const nodes = docs.map(d => ({ id: d.node_id, creationUtc: d.creation_utc, action: d.action, tools: d.tools, metadata: d.metadata, description: d.description, labels: d.labels || [], })); nodes.push({ id: 'end' as JourneyNodeId, creationUtc: new Date().toISOString(), action: null, tools: [], metadata: {}, description: null, labels: [], }); return nodes; } createEdge(journeyId: JourneyId, params: { source: JourneyNodeId; target: JourneyNodeId; condition?: string | null; }, projectRoot?: string): JourneyEdge { const creationUtc = new Date().toISOString(); const edge: JourneyEdge = { id: `edge_${generateId()}`, creationUtc, source: params.source, target: params.target, condition: params.condition ?? null, metadata: {}, }; const edges = readCollection('edges', projectRoot); edges.push({ id: edge.id, journey_id: journeyId, creation_utc: creationUtc, source: params.source, target: params.target, condition: params.condition ?? null, metadata: {}, }); writeCollection('edges', edges, projectRoot); return edge; } listEdges(journeyId: JourneyId, nodeId?: JourneyNodeId, projectRoot?: string): JourneyEdge[] { let docs = readCollection('edges', projectRoot) .filter(e => e.journey_id === journeyId); if (nodeId) { docs = docs.filter(e => e.source === nodeId || e.target === nodeId); } return docs.map(d => ({ id: d.id, creationUtc: d.creation_utc, source: d.source, target: d.target, condition: d.condition, metadata: d.metadata, })); } deleteEdge(edgeId: JourneyEdgeId, projectRoot?: string): void { let edges = readCollection('edges', projectRoot); const idx = edges.findIndex(e => e.id === edgeId); if (idx === -1) throw new Error(`Edge '${edgeId}' not found`); edges.splice(idx, 1); writeCollection('edges', edges, projectRoot); } addTrigger(journeyId: JourneyId, trigger: GuidelineId, projectRoot?: string): boolean { const triggers = readCollection('triggers', projectRoot); if (triggers.find(t => t.journey_id === journeyId && t.trigger === trigger)) { return false; } triggers.push({ id: `trg_${generateId()}`, journey_id: journeyId, trigger, creation_utc: new Date().toISOString(), }); writeCollection('triggers', triggers, projectRoot); return true; } removeTrigger(journeyId: JourneyId, trigger: GuidelineId, projectRoot?: string): boolean { let triggers = readCollection('triggers', projectRoot); const len = triggers.length; triggers = triggers.filter(t => !(t.journey_id === journeyId && t.trigger === trigger)); writeCollection('triggers', triggers, projectRoot); return triggers.length < len; } }