Parse a Schedule File
Read a schedule file, handle all three types, write tests.
You've seen schedule files — 60, {"type": "scheduled", "calendar": {...}}, {"type": "on-change", "paths": [...]}. Now you'll build the parser that reads them.
The Function
parseScheduleText takes the raw string contents of a schedule file and returns a structured object the manager can work with.
The inputs are messy — plain integers, JSON objects, empty files, garbage. The output is clean:
type ParsedSchedule =
| { kind: "periodic"; seconds: number; runAtLoad?: boolean }
| { kind: "scheduled"; calendar: Record<string, unknown>; runAtLoad?: boolean }
| { kind: "on-change"; paths: string[]; runAtLoad?: boolean };
interface ParsedScheduleFile {
schedule?: ParsedSchedule;
program?: string;
disabled?: boolean;
}Build It
Open src/manager.ts and find the parseScheduleText function. Here's the logic:
The calendar keys need normalizing — users might write hour but launchd expects Hour. The normalizeCalendarForLaunchd function handles this.
Write Tests
Create src/manager.test.ts:
import { describe, test, expect } from "bun:test";
import { parseScheduleText, normalizeCalendarForLaunchd } from "./manager";
describe("parseScheduleText — periodic", () => {
test("plain integer string", () => {
const result = parseScheduleText("60");
expect(result.schedule).toEqual({ kind: "periodic", seconds: 60 });
});
test("JSON with type: periodic", () => {
const result = parseScheduleText('{"type": "periodic", "seconds": 120}');
expect(result.schedule).toEqual({ kind: "periodic", seconds: 120 });
});
test("infers periodic from seconds field without type", () => {
const result = parseScheduleText('{"seconds": 90}');
expect(result.schedule).toEqual({ kind: "periodic", seconds: 90 });
});
});Run them:
bun testNow write tests for the other two types. For each type, cover:
Verification
bun testAll tests should pass. If you get stuck, look at the three schedule files in system-jobs/ — they're working examples of each type.