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:
- Trim whitespace. Empty string →
{ disabled: true }. - Plain integer (regex:
/^\d+$/) → periodic schedule with that many seconds. - JSON parse. If it fails →
{ disabled: true }. - Check for
disabled: truein the JSON → disabled. - Check the
typefield → route to periodic, scheduled, or on-change. - No type field? Infer from shape —
secondsmeans periodic,calendarmeans scheduled,pathsmeans on-change.
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 test
Now write tests for the other two types. For each type, cover:
- The explicit
typefield format - The inferred format (no type, just shape)
- Edge cases (empty string, invalid JSON, disabled flag)
Verification
bun test
All tests should pass. If you get stuck, look at the three schedule files in system-jobs/ — they're working examples of each type.
What You Learned
- Schedule files are intentionally loose — plain integers for simple cases, JSON for everything else
- The parser is defensive — bad input → disabled, never a crash
- Calendar key normalization (
hour→Hour) is a small thing that prevents a confusing launchd error - Tests are the fastest way to understand a parser — write the test, see what the function actually does