import { readdir, readFile } from 'fs/promises' import { existsSync } from 'fs' import { join } from 'path' import { isValidSchedule, toCronExpr, type CronJob, type Schedule } from './schedules' const APPS_DIR = process.env.APPS_DIR! const SCHEDULE_RE = /export\s+const\s+schedule\s*=\s*['"]([^'"]+)['"]/ export async function getApps(): Promise { const entries = await readdir(APPS_DIR, { withFileTypes: true }) const apps: string[] = [] for (const entry of entries) { if (!entry.isDirectory()) continue // Check if it has a current symlink (valid app) if (existsSync(join(APPS_DIR, entry.name, 'current'))) { apps.push(entry.name) } } return apps.sort() } export async function discoverCronJobs(): Promise { const jobs: CronJob[] = [] const apps = await readdir(APPS_DIR, { withFileTypes: true }) for (const app of apps) { if (!app.isDirectory()) continue const cronDir = join(APPS_DIR, app.name, 'current', 'cron') if (!existsSync(cronDir)) continue const files = await readdir(cronDir) for (const file of files) { if (!file.endsWith('.ts')) continue const filePath = join(cronDir, file) const name = file.replace(/\.ts$/, '') try { const source = await readFile(filePath, 'utf-8') const match = source.match(SCHEDULE_RE) if (!match) { console.error(`No schedule export found in ${filePath}`) continue } const schedule = match[1] as Schedule if (!isValidSchedule(schedule)) { console.error(`Invalid schedule in ${filePath}: ${schedule}`) continue } jobs.push({ id: `${app.name}:${name}`, app: app.name, name, file: filePath, schedule, cronExpr: toCronExpr(schedule), state: 'idle', }) } catch (e) { console.error(`Failed to load cron file ${filePath}:`, e) } } } return jobs }