Lesson

Register with launchd

installJob: write the plist, bootout the old, bootstrap the new, enable it.

You can generate a plist. Now you need to get launchd to load it. This is the installJob function — the bridge between your job definition and the OS.

The Lifecycle

Every install follows four steps:

  1. Write the plist to ~/Library/LaunchAgents/{label}.plist
  2. Bootout the old registration (if any) — launchctl bootout gui/{uid} {plist}
  3. Bootstrap the new registration — launchctl bootstrap gui/{uid} {plist}
  4. Enable it — launchctl enable gui/{uid}/{label}

This sequence is important. If you bootstrap without booting out first, you get "service already loaded." If you skip enable, the job might not start on schedule.

Try It Manually

Uninstall a job, then reinstall it step by step:

# Uninstall
bun run src/cli.ts uninstall hello-world

# Verify it's gone
launchctl list | grep aijs.system.hello-world
# (no output)

# Reinstall
bun run src/cli.ts install hello-world

# Verify it's back
launchctl list | grep aijs.system.hello-world
# -  0  aijs.system.hello-world

The install command shows the before/after state:

{
  "result": {
    "name": "hello-world",
    "wasLoaded": false,
    "nowLoaded": true
  }
}

The gui/ Domain

launchd organizes jobs into domains. Your user-level jobs live in gui/{uid}:

echo "Your domain: gui/$(id -u)"
# gui/501

This means your jobs run as YOUR user, no root required. They have access to your files, your environment, your network. They start when you log in and stop when you log out.

What Can Go Wrong

bun run src/cli.ts doctor hello-world

The doctor checks for the common failures:

  • Plist not installed — run sync or install
  • Plist fails validation — bad XML, check the generator
  • Loaded but non-zero exit — the job ran and errored, check logs
  • Run script not executable — needs chmod 755
  • No shebang — launchd doesn't know how to run it

File Permissions Matter

  • Run script: must be 755 (executable). The manager does chmod 755 automatically.
  • Plist file: must be 644. Launchd ignores plists with group/other write permissions. The manager writes with { mode: 0o644 }.

Verification

# Full round trip: uninstall → verify gone → install → verify running → kick → check logs
bun run src/cli.ts uninstall hello-world
launchctl list | grep hello-world  # should be empty
bun run src/cli.ts install hello-world
launchctl list | grep hello-world  # should show the job
bun run src/cli.ts kick hello-world
bun run src/cli.ts logs hello-world  # should show new output

What You Learned

  • The install sequence: write → bootout → bootstrap → enable
  • Jobs live in gui/{uid} — user-level, no root
  • File permissions matter: 755 for scripts, 644 for plists
  • jobs doctor catches the common failures before you debug manually