import * as fs from "fs"; import * as path from "path"; import * as os from "os"; export const AGE_GROUPS = [ "middle_school", "high_school", "undergraduate", "graduate", "post_graduate", "professional", "adult_learner", ] as const; export type AgeGroup = typeof AGE_GROUPS[number]; export interface WorkspaceResult { isActive: boolean; root: string | null; ageGroup: AgeGroup | null; } const HOME = os.homedir(); const MAX_LEVELS = 15; function readAgeGroupFromIdentity(workspaceRoot: string): AgeGroup | null { try { const identityPath = path.join(workspaceRoot, ".pisces", "identity.json"); if (!fs.existsSync(identityPath)) return null; const parsed = JSON.parse(fs.readFileSync(identityPath, "utf-8")) as Record; const raw = parsed["ageGroup"]; return (AGE_GROUPS as readonly string[]).includes(raw as string) ? (raw as AgeGroup) : null; } catch { return null; } } /** * Walks up from startCwd looking for a .pisces marker file or directory. * Bounded at 15 levels and stops at the user's home directory. */ export function findWorkspace(startCwd: string): WorkspaceResult { let dir = startCwd; let levels = 0; while (levels < MAX_LEVELS) { if (fs.existsSync(path.join(dir, ".pisces"))) { return { isActive: true, root: dir, ageGroup: readAgeGroupFromIdentity(dir) }; } const parent = path.dirname(dir); if (parent === dir || dir === HOME) break; dir = parent; levels++; } return { isActive: false, root: null, ageGroup: null }; } // ─── Singleton cache ──────────────────────────────────────────────────────── // Lazily initialised on first read so tests can mock findWorkspace() before // the module evaluates any filesystem calls. let _cached: WorkspaceResult | null = null; /** * Returns the last resolved workspace state, initialising from process.cwd() * on first call. Use syncWorkspaceState() to re-evaluate after a cwd change. */ export function getWorkspaceState(): WorkspaceResult { if (_cached === null) { _cached = findWorkspace(process.cwd()); } return _cached; } /** * Re-evaluates workspace state for the given cwd, updates the cache, * and returns the result. Called by the gatekeeper on resources_discover. */ export function syncWorkspaceState(cwd: string): WorkspaceResult { _cached = findWorkspace(cwd); return _cached; }