This is the key insight of the whole system. You don't tell launchd what to add or remove. You declare what SHOULD exist, and the manager makes it so.
The Mental Model
Desired state: directories in system-jobs/ and jobs/
Installed state: plists in ~/Library/LaunchAgents/aijs.*.plist
Sync: make installed match desired
New directory? Install it. Directory removed? Uninstall the plist. Nothing changed? Skip it.
See the Plan
bun run src/cli.ts sync --dry-run
This shows what sync WOULD do without doing it:
{
"result": {
"dryRun": true,
"plan": {
"install": ["aijs.system.hello-world", "..."],
"remove": [],
"skip": []
},
"summary": "Would install 4, remove 0, skip 0"
}
}
Try creating a fake stale plist and dry-running again:
touch ~/Library/LaunchAgents/aijs.user.old-job.plist
bun run src/cli.ts sync --dry-run
Now the plan shows "remove": ["aijs.user.old-job"]. The sync detected a plist that doesn't have a corresponding directory — stale, should be cleaned up.
Run the actual sync to clean it up:
bun run src/cli.ts sync
Idempotent
Run sync twice:
bun run src/cli.ts sync
bun run src/cli.ts sync
The second run shows "unchanged": [...] for all jobs. Sync compares the generated plist against the installed one — if they're identical, it skips the job. No unnecessary bootout/bootstrap cycles. Safe to run as often as you want.
This is what "declarative" means in practice. You don't track what you've done. You declare what should be true and let the system converge. Like terraform apply but for launchd jobs on your Mac.
Add a Job, Sync, Remove It, Sync
The full lifecycle:
# Create a job
mkdir -p jobs/experiment
echo '120' > jobs/experiment/schedule
echo '#!/bin/bash
echo "Experiment running"' > jobs/experiment/run
chmod 755 jobs/experiment/run
# Sync — it gets installed
bun run src/cli.ts sync
# Verify
bun run src/cli.ts status | python3 -c "import json,sys; [print(j['name']) for j in json.load(sys.stdin)['result']['jobs']]"
# Remove the job
rm -rf jobs/experiment
# Sync again — it gets removed
bun run src/cli.ts sync
# Verify it's gone
launchctl list | grep experiment
What You Learned
- Declarative sync: declare desired state, system converges
--dry-runshows the plan before executing- Idempotent: safe to repeat, only changes what's different
- Adding a job = mkdir + sync. Removing = rm -rf + sync. That's the entire API.