import { constants } from 'node:fs'; import { access, mkdir, writeFile, unlink } from 'node:fs/promises'; import path from 'node:path'; import { nanoid } from 'nanoid'; import { COURSE_FRAMEWORK_DIR } from '@/lib/course-framework/store'; import { COURSE_MANIFEST_DIR } from '@/lib/course-manifest-repo/store'; import { COURSEWARES_DIR } from '@/lib/courseware-repo/store'; import { getServerProviders, getServerTTSProviders } from '@/lib/server/provider-config'; import { CLASSROOM_JOBS_DIR, CLASSROOMS_DIR } from '@/lib/server/classroom-storage'; import { configuredLearningRuntimeToken } from './auth'; import { configuredLearningEngineRuntimeBaseUrl } from './ops-engine-control'; import { resolveDeploymentRole } from '@/lib/config/deployment-role'; export const LEARNING_ENGINE_SERVICE = 'openmaic-learning-engine'; export const LEARNING_ENGINE_API_VERSION = 'v1'; export type LearningReadinessCheck = { ok: boolean; detail?: string; }; function dataRoot(): string { return path.resolve(process.env.LEARNING_DATA_DIR?.trim() || path.join(process.cwd(), 'data')); } export function learningPersistenceDirectories(): Record { const root = dataRoot(); return { classrooms: process.env.CLASSROOM_DATA_DIR || CLASSROOMS_DIR, classroomJobs: process.env.CLASSROOM_JOBS_DIR || CLASSROOM_JOBS_DIR, courseFrameworks: process.env.COURSE_FRAMEWORK_DIR || COURSE_FRAMEWORK_DIR, courseManifests: process.env.COURSE_MANIFEST_DIR || COURSE_MANIFEST_DIR, coursewares: process.env.COURSEWARE_DATA_DIR || COURSEWARES_DIR, coursewareBundles: process.env.COURSEWARE_BUNDLE_DIR || path.join(root, 'courseware-bundles'), learningPackages: process.env.LEARNING_COURSE_PACKAGE_DIR || process.env.MAKELORE_COURSE_PACKAGE_DIR || path.join(root, 'makelore-packages'), learningCourses: process.env.LEARNING_COURSE_STORE_DIR || process.env.MAKELORE_COURSE_STORE_DIR || path.join(root, 'makelore-courses'), }; } async function verifyWritableDirectory(directory: string): Promise { await mkdir(directory, { recursive: true }); await access(directory, constants.R_OK | constants.W_OK); const probe = path.join(directory, `.learning-ready-${process.pid}-${nanoid(6)}.tmp`); await writeFile(probe, 'ready', { flag: 'wx' }); await unlink(probe); } function configuredOrigin(name: string): boolean { const raw = process.env[name]?.trim(); if (!raw) return false; try { const url = new URL(raw); return ( (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password && !url.search && !url.hash && (url.pathname === '' || url.pathname === '/') ); } catch { return false; } } export async function inspectLearningReadiness(): Promise<{ ready: boolean; checks: Record; }> { const role = resolveDeploymentRole( process.env.OPENMAIC_DEPLOYMENT_ROLE ?? process.env.NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE, process.env.NODE_ENV, ); const checks: Record = role === 'ops' ? { worksIdentityOrigin: configuredOrigin('WORKS_SQUARE_API_BASE_URL') ? { ok: true } : { ok: false, detail: 'WORKS_SQUARE_API_BASE_URL is not configured as an origin' }, opsPublicOrigin: configuredOrigin('OPS_PUBLIC_ORIGIN') ? { ok: true } : { ok: false, detail: 'OPS_PUBLIC_ORIGIN is not configured as an origin' }, engineControlEndpoint: configuredLearningEngineRuntimeBaseUrl() ? { ok: true } : { ok: false, detail: 'LEARNING_ENGINE_BASE_URL or COURSE_PUBLISH_SERVER_BASE_URL is not configured', }, runtimeToken: configuredLearningRuntimeToken() ? { ok: true } : { ok: false, detail: 'LEARNING_ENGINE_TOKEN is not configured' }, } : { runtimeToken: configuredLearningRuntimeToken() ? { ok: true } : { ok: false, detail: 'LEARNING_ENGINE_TOKEN is not configured' }, llmProvider: Object.keys(getServerProviders()).length > 0 ? { ok: true } : { ok: false, detail: 'No server-managed LLM provider is configured' }, ttsProvider: Object.values(getServerTTSProviders()).some((provider) => !provider.disabled) ? { ok: true } : { ok: false, detail: 'No enabled server-managed TTS provider is configured' }, }; for (const [name, directory] of Object.entries(learningPersistenceDirectories())) { try { await verifyWritableDirectory(directory); checks[`storage.${name}`] = { ok: true }; } catch (error) { checks[`storage.${name}`] = { ok: false, detail: error instanceof Error ? error.message : 'Storage is not writable', }; } } return { ready: Object.values(checks).every((check) => check.ok), checks }; }