import fs from 'node:fs'; import path from 'node:path'; import { YonexusError } from '../models/errors'; import type { DocsScope, DocsTopic } from '../models/types'; import { slug } from '../utils/id'; const TOPICS: DocsTopic[] = ['docs', 'notes', 'knowledge', 'rules', 'lessons', 'workflows']; function ensureDirs(base: string, dirs: string[]): void { for (const d of dirs) fs.mkdirSync(path.join(base, d), { recursive: true }); } export class ResourceLayout { constructor(private readonly rootDir: string) {} get organizationsRoot(): string { return path.join(this.rootDir, 'organizations'); } orgPath(orgName: string): string { return path.join(this.organizationsRoot, slug(orgName)); } teamPath(orgName: string, teamName: string): string { return path.join(this.orgPath(orgName), 'teams', slug(teamName)); } agentPath(orgName: string, teamName: string, agentId: string): string { return path.join(this.teamPath(orgName, teamName), 'agents', slug(agentId)); } ensureOrganization(orgName: string): void { const root = this.orgPath(orgName); ensureDirs(root, ['teams', ...TOPICS]); } ensureTeam(orgName: string, teamName: string): void { const root = this.teamPath(orgName, teamName); ensureDirs(root, ['agents', ...TOPICS]); } ensureAgent(orgName: string, teamName: string, agentId: string): void { const root = this.agentPath(orgName, teamName, agentId); ensureDirs(root, TOPICS); } private readTopicFiles(topicRoot: string, keyword: string): string[] { if (!fs.existsSync(topicRoot)) return []; let re: RegExp; try { re = new RegExp(keyword); } catch { throw new YonexusError('VALIDATION_ERROR', 'invalid_regex', { keyword }); } const entries = fs.readdirSync(topicRoot, { withFileTypes: true }); return entries .filter((e) => e.isFile() && re.test(e.name)) .map((e) => path.join(topicRoot, e.name)); } getDocs(scope: DocsScope, topic: DocsTopic, keyword: string): string { const groups: Record<'ORG' | 'DEPT' | 'TEAM' | 'AGENT', string[]> = { ORG: [], DEPT: [], TEAM: [], AGENT: [] }; const orgsRoot = this.organizationsRoot; if (!fs.existsSync(orgsRoot)) { return '----ORG\n----DEPT\n----TEAM\n----AGENT'; } const orgs = fs.readdirSync(orgsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); for (const orgName of orgs) { const orgPath = path.join(orgsRoot, orgName); if (scope === 'organization') { groups.ORG.push(...this.readTopicFiles(path.join(orgPath, topic), keyword)); } const teamsRoot = path.join(orgPath, 'teams'); if (!fs.existsSync(teamsRoot)) continue; const teams = fs.readdirSync(teamsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); for (const teamName of teams) { const teamPath = path.join(teamsRoot, teamName); if (scope === 'team') { groups.TEAM.push(...this.readTopicFiles(path.join(teamPath, topic), keyword)); } const agentsRoot = path.join(teamPath, 'agents'); if (!fs.existsSync(agentsRoot)) continue; const agents = fs.readdirSync(agentsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); for (const agentId of agents) { if (scope === 'agent') { groups.AGENT.push(...this.readTopicFiles(path.join(agentsRoot, agentId, topic), keyword)); } } } } // department folders are not defined in this layout; reserved empty group for compatible output. const printGroup = (k: 'ORG' | 'DEPT' | 'TEAM' | 'AGENT'): string => [`----${k}`, ...groups[k]].join('\n'); return [printGroup('ORG'), printGroup('DEPT'), printGroup('TEAM'), printGroup('AGENT')].join('\n'); } }