---
name: macos-launchd-service
description: Set up macOS launchd service for auto-starting Python applications
allowed-tools: Read, Write, Bash, Glob
---
# macOS launchd Service Setup
Generate complete launchd service infrastructure for Python applications on macOS.
## What This Skill Creates
```
launchd/
├── install.sh # Automated service installer
├── uninstall.sh # Service uninstaller
├── {project}.plist.template # Service configuration
dev.sh # Development mode script
view-logs.sh # Log viewing helper
```
## When to Use This Skill
- **Web services**: FastAPI, Flask apps that should auto-start
- **Background services**: Daemons, periodic tasks
- **Development servers**: Local services you want always running
## Before Running
**Requirements**:
- macOS 10.10+ with launchd
- Python project with uv and .venv
- `pyproject.toml` with `[project.scripts]` defining a CLI command
**Check pyproject.toml**:
```toml
[project.scripts]
yourapp = "yourapp.cli:main"
```
**IMPORTANT - Replacing Existing Setup**:
If the project already has launchd/ directory or existing scripts:
- **ALWAYS use the parameters provided by the user**
- **DO NOT read values from existing files**
- The user wants to replace with NEW values, not keep old ones
- Overwrite existing files with the new parameter values
## Step-by-Step Process
### 1. Gather Information from User
**Ask the user for these parameters** (use AskUserQuestion if needed):
- **Domain** (e.g., "dev.pborenstein", "com.pborenstein") - Reverse domain notation for service label
- Default suggestion: `dev.{username}` (e.g., "dev.philip")
- Best practice: Use owned domain (e.g., "dev.pborenstein" if you own pborenstein.dev)
- **Project name** (e.g., "temoa", "apantli") - lowercase, no spaces
- **Module name** (e.g., "temoa", "apantli") - Python import name
- **Port number** (e.g., 4001, 4000) - unique port for this service
- **CLI command** (e.g., "temoa server", "python3 -m apantli.server")
- **Dev command** (e.g., "temoa server --reload", "python3 -m apantli.server --reload")
- **Process name** (e.g., "temoa server", "apantli.server") - for detecting running processes
**Use these exact values provided by the user** - do not infer from existing files.
### 2. Suggest Defaults from pyproject.toml (Optional)
Only if the user hasn't provided values, you may suggest defaults from `pyproject.toml`:
```bash
# Suggest project name
grep "^name" pyproject.toml
# Suggest CLI command from [project.scripts]
grep -A 5 "\[project.scripts\]" pyproject.toml
```
**But always use what the user explicitly provides.**
### 3. Generate launchd Directory
Create `launchd/` directory:
```bash
mkdir -p launchd
```
### 4. Generate Files from Templates
For each template in `skills/macos-launchd-service/templates/`, perform substitutions:
**Substitution variables**:
User-provided parameters (use values from step 1):
- `{{DOMAIN}}` - Reverse domain notation (e.g., "dev.pborenstein", "com.pborenstein")
- `{{PROJECT_NAME}}` - Project name (e.g., "temoa")
- `{{MODULE_NAME}}` - Python module name for import check
- `{{PORT}}` - Port number
- `{{CLI_COMMAND}}` - Full CLI command as plist array elements
- `{{DEV_COMMAND}}` - Development mode command
- `{{PROCESS_NAME}}` - Process name for pkill/pgrep
Auto-detected variables (these are filled in by install.sh at runtime):
- `{{HOME}}` - User's home directory
- `{{PROJECT_DIR}}` - Absolute path to project directory
- `{{VENV_PYTHON}}` - Path to venv Python interpreter
- `{{VENV_BIN}}` - Path to venv bin directory
**CLI_COMMAND special handling**:
Must be converted to plist array format:
```
Input: "temoa server --host 0.0.0.0 --port 4001"
Output:
{{VENV_BIN}}/temoa
server
--host
0.0.0.0
--port
4001
```
### 5. Generated Files
**install.sh** (from `install.sh.template`):
- Auto-detects environment (username, paths, venv)
- Validates venv and module are installed
- Generates service plist from template
- Installs and loads service
- Shows access information and management commands
**uninstall.sh** (from `uninstall.sh.template`):
- Checks if service exists
- Shows what will be removed
- Asks for confirmation
- Stops running service
- Removes plist file
- Confirms uninstall complete
**{project}.plist.template** (from `service.plist.template`):
- Service configuration with substitution placeholders
- Used by install.sh to generate final plist
- Configured with RunAtLoad, KeepAlive for production
**dev.sh** (from `dev.sh.template`):
- Stops launchd service if running
- Runs app with auto-reload for development
- Uses caffeinate to prevent sleep
- Offers to restore service on exit
**view-logs.sh** (from `view-logs.sh.template`):
- Modes: app logs, errors, or all
- Uses tail -f for live viewing
- Logs location: `~/Library/Logs/{project}.log`
### 6. Make Scripts Executable
```bash
chmod +x launchd/install.sh
chmod +x launchd/uninstall.sh
chmod +x dev.sh
chmod +x view-logs.sh
```
### 7. Provide Next Steps
After generation, tell the user:
```
✓ Generated launchd service structure
Next steps:
1. Review generated files in launchd/
2. Run: ./launchd/install.sh
3. Access your service at: http://localhost:{PORT}
4. View logs: ./view-logs.sh
5. Development mode: ./dev.sh
Service will auto-start on login and auto-restart on crash.
Manage service:
Stop: launchctl unload ~/Library/LaunchAgents/{DOMAIN}.{project}.plist
Start: launchctl load ~/Library/LaunchAgents/{DOMAIN}.{project}.plist
Status: launchctl list | grep {project}
```
## Example: temoa
**Input parameters**:
- Domain: dev.pborenstein
- Project name: temoa
- Module name: temoa
- Port: 4001
- CLI command: temoa server --host 0.0.0.0 --port 4001 --log-level info
- Dev command: temoa server --reload
- Process name: temoa server
**Generated CLI_COMMAND for plist**:
```xml
{{VENV_BIN}}/temoa
server
--host
0.0.0.0
--port
4001
--log-level
info
```
## Implementation Notes
**Reading templates**:
Templates are in `skills/macos-launchd-service/templates/`:
- `install.sh.template`
- `uninstall.sh.template`
- `service.plist.template`
- `dev.sh.template`
- `view-logs.sh.template`
**Writing generated files**:
- `launchd/install.sh`
- `launchd/uninstall.sh`
- `launchd/{PROJECT_NAME}.plist.template`
- `dev.sh`
- `view-logs.sh`
**String substitution**:
Simple replace all instances of each `{{VARIABLE}}` with its value.
**CLI_COMMAND conversion**:
Split on spaces, wrap each token in ` TOKEN` with proper indentation.
## Validation
After generation, verify:
- All 5 files created (install.sh, uninstall.sh, plist.template, dev.sh, view-logs.sh)
- Scripts are executable
- No leftover `{{VARIABLES}}` in files
- CLI_COMMAND properly formatted as plist array
## Common Issues
**Port conflicts**: Use `lsof -i :{PORT}` to check if port is available
**Module not found**: User needs to run `uv sync` first
**Permission errors**: Ensure `~/Library/LaunchAgents` exists
## See Also
- [README.md](README.md) - Detailed usage guide
- Example implementations: temoa, apantli in nahuatl-projects