83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import { readdir, readFile } from 'fs/promises'
|
|
import { existsSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { isValidSchedule, toCronExpr, type CronJob, type InvalidJob, 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<string[]> {
|
|
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 type DiscoveryResult = {
|
|
jobs: CronJob[]
|
|
invalid: InvalidJob[]
|
|
}
|
|
|
|
export async function discoverCronJobs(): Promise<DiscoveryResult> {
|
|
const jobs: CronJob[] = []
|
|
const invalid: InvalidJob[] = []
|
|
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$/, '')
|
|
const id = `${app.name}:${name}`
|
|
|
|
try {
|
|
const source = await readFile(filePath, 'utf-8')
|
|
const match = source.match(SCHEDULE_RE)
|
|
|
|
if (!match) {
|
|
invalid.push({ id, app: app.name, name, file: filePath, error: 'Missing schedule export' })
|
|
continue
|
|
}
|
|
|
|
const schedule = match[1] as Schedule
|
|
|
|
if (!isValidSchedule(schedule)) {
|
|
invalid.push({ id, app: app.name, name, file: filePath, error: `Invalid schedule: "${match[1]}"` })
|
|
continue
|
|
}
|
|
|
|
jobs.push({
|
|
id,
|
|
app: app.name,
|
|
name,
|
|
file: filePath,
|
|
schedule,
|
|
cronExpr: toCronExpr(schedule),
|
|
state: 'idle',
|
|
})
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
invalid.push({ id, app: app.name, name, file: filePath, error: msg })
|
|
}
|
|
}
|
|
}
|
|
|
|
return { jobs, invalid }
|
|
}
|