Developer control plane CLI — sync and manage developer tooling configurations from any repo. Think of it as "paracetamol for developer setup": distribute configs from central repos and keep local environments in sync.
- Protocol-driven sync — Define what to sync in
protocol.yaml(obligations + recommendations) - Repo-agnostic — Works with any Git repo; no hardcoded URLs
- Multiple domains — ai-kit (Cursor rules, skills), devspace (planned), local (planned)
- Self-updating binary — Single executable with automatic updates from GitHub releases
- Background config sync — Optional hourly pull via launchd (macOS) or cron (Linux), with desktop notifications when updates land
- One-shot install — Optional
install.shenv vars to install devctl, runai-kit setup, and register background sync in one run - Backup before overwrite — Snapshots targets before applying changes
┌─────────────────────────────────────────────────────────────────┐
│ CLI Layer (cli/) │
│ main │ ai-kit │ devspace │ local │ list │ update-cli │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────────┐
│ Core Layer (core/) │
│ protocol_engine │ repo_manager │ versioning │ backup │ updater │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────────┐
│ Utils Layer (utils/) │
│ shell │ yaml_loader │ logging │ notify │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────────┐
│ Local Storage (~/.devctl/) │
│ repos/ │ backups/ │ logs/ │ state.json │ .last_update_check │
└─────────────────────────────────────────────────────────────────┘
| Layer | Module | Responsibility |
|---|---|---|
| CLI | main | Root group, auto-update hook, list, update-cli |
| CLI | ai-kit | setup, sync, update, install/uninstall-background-sync, status, doctor |
| CLI | devspace / local | Domain stubs (planned) |
| Core | repo_manager | Clone/pull Git repos, URL → slug |
| Core | protocol_engine | Parse protocol.yaml, execute file_sync, etc. |
| Core | versioning | Persist repo metadata in state.json |
| Core | backup | Snapshot target before overwrite |
| Core | updater | Self-update binary from GitHub releases |
| Core | config_sync | Check managed repos for new pushes, pull and re-apply, notify |
| Core | background_sync | Install/uninstall launchd (macOS) or cron (Linux) for hourly sync |
| Utils | shell, yaml_loader, logging, notify | Paths, YAML, stderr logging, OS notifications |
User: devctl ai-kit setup --repo https://github.com/org/configs
│
▼
main.cli() ──► _maybe_auto_update() ──► ai_kit.setup()
│ │ │
│ │ ├──► repo_manager.clone_or_pull()
│ │ ├──► protocol_engine.apply_protocols()
│ │ │ ├──► backup.backup_target()
│ │ │ └──► file_sync (merge copy)
│ │ └──► versioning.register_repo()
│ │
│ └── check wi-devctl releases
│
│ (separate: launchd/cron runs devctl ai-kit sync hourly)
│ └── config_sync: fetch → pull if behind → re-apply → notify
│
▼
Repo cloned → ~/.devctl/repos/org-configs/
Configs merged → ~/.cursor (or target from protocol.yaml)
State saved → ~/.devctl/state.json
install.sh downloads a pre-built devctl binary from GitHub Releases. You need at least one release (a v* tag). Supported assets:
devctl-darwin-amd64,devctl-darwin-arm64,devctl-linux-amd64
curl -fsSL https://raw.githubusercontent.com/WorkIndia-Private/wi-devctl/main/install.sh | bashSet these before piping install.sh into bash. After the binary is installed, the script can run ai-kit setup and register hourly sync.
export DEVCTL_AI_KIT_REPO=https://github.com/your-org/your-ai-config-repo
export DEVCTL_AI_KIT_BACKGROUND_SYNC=1
curl -fsSL https://raw.githubusercontent.com/WorkIndia-Private/wi-devctl/main/install.sh | bash| Variable | Effect |
|---|---|
DEVCTL_AI_KIT_REPO |
Run devctl ai-kit setup --repo <url> after install (git required on PATH) |
DEVCTL_AI_KIT_BACKGROUND_SYNC=1 |
Then run devctl ai-kit install-background-sync (macOS / Linux only; skipped elsewhere) |
GITHUB_TOKEN used only for curl when fetching install.sh or release assets does not configure git clone for your config repo. Use SSH or a git credential helper for private repos.
Combine with a private wi-devctl install and one-shot ai-kit:
export GITHUB_TOKEN=ghp_xxx
export DEVCTL_AI_KIT_REPO=git@github.com:your-org/your-ai-config-repo.git
export DEVCTL_AI_KIT_BACKGROUND_SYNC=1
curl -fsSL \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.raw" \
"https://api.github.com/repos/WorkIndia-Private/wi-devctl/contents/install.sh?ref=main" | bashIf you only need the installer from a private repo (no one-shot ai-kit):
export GITHUB_TOKEN=ghp_xxx
curl -fsSL \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.raw" \
"https://api.github.com/repos/WorkIndia-Private/wi-devctl/contents/install.sh?ref=main" | bashdevctl ai-kit setup --repo <url> # Clone / pull config repo, apply protocols
devctl ai-kit install-background-sync # Hourly sync: launchd (macOS) or cron (Linux)
devctl ai-kit uninstall-background-sync # Remove that scheduled job
devctl ai-kit sync # Run sync now; notifies when pulls happen
devctl ai-kit sync -v # Same, with step-by-step messages on stderr
devctl ai-kit update # Pull all managed repos and re-apply (explicit)
devctl ai-kit status # Show drift vs protocol obligations
devctl ai-kit doctor # Validate configs
devctl list # List managed repos
devctl update-cli # Force devctl binary update
devctl --help- Add a
protocol.yamlto your config repo (see examples/protocol.yaml):
version: v1
protocols:
- name: cursor
type: file_sync
source: .cursor
target: ~/.cursor
obligations: [rules/security.json]
recommendations: [skills/debugging.md]- Run setup:
devctl ai-kit setup --repo https://github.com/your-org/ai-configs- (Optional) Install background sync to auto-pull updates hourly and get notified:
devctl ai-kit install-background-sync- Repo is cloned to
~/.devctl/repos/, configs are merged into~/.cursor.
| Field | Description |
|---|---|
source |
Path in repo (relative to root) |
target |
Local path (~ expanded) |
obligations |
Required files under target (reported if missing) |
recommendations |
Optional files (reported if missing) |
protocol.yaml or protocol.yml must live at the root of the repo you sync from.
A domain is a grouped set of CLI commands for a specific use case. Each domain reuses the same core (repo manager, protocol engine, versioning, backup) but adds domain-specific behavior.
| Domain | Purpose | Status |
|---|---|---|
| ai-kit | Sync AI tooling configs (Cursor rules, skills) from repos | ✅ Implemented |
| devspace | Dev environment provisioning (containers, VMs) | 🚧 Planned |
| local | Local dev tooling (env vars, daemons, scripts) | 🚧 Planned |
All domains share the same flow: clone repo → parse protocol.yaml → apply protocols → track state. The protocol engine supports multiple types (currently file_sync; extensible to env_sync, script_run, etc.). Domain-specific logic sits on top of this core.
| Use case | Domain | What it does | Example |
|---|---|---|---|
| AI configs | ai-kit | Sync .cursor rules/skills to ~/.cursor |
Cursor rules, agent skills |
| Dev environments | devspace | Define containers/VMs, start Docker/Podman | Dev containers, Colima setup |
| Local tooling | local | Sync env vars, run setup scripts | .env files, dev daemons |
| Security | (new domain) | Scan for secrets, enforce policies | Pre-commit hooks, policy configs |
- Add
cli/my_domain.pywith a Click group and commands. - Register in
main.py:cli.add_command(my_domain, "my-domain"). - Reuse
repo_manager,protocol_engine,versioningfrom core. - Add new protocol types in
protocol_engine.execute_protocol()if needed (e.g.type: script_run).
Fork the repo and add domains for your org — no hardcoded URLs; each domain works with any repo that includes a protocol.yaml.
| Variable | Description |
|---|---|
DEVCTL_MANIFEST_URL |
Custom manifest URL for updates |
DEVCTL_SKIP_AUTO_UPDATE |
Set to 1 to disable auto-update |
DEVCTL_SKIP_NOTIFY |
Set to 1 to disable OS notifications on sync |
DEVCTL_VERBOSE |
Set to 1 or use -v for verbose output |
DEVCTL_GITHUB_OWNER |
GitHub org (default: WorkIndia-Private) |
DEVCTL_GITHUB_REPO |
Repo name (default: wi-devctl) |
DEVCTL_UPDATE_CHECK_INTERVAL_HOURS |
Hours between auto-update checks (default: 24) |
DEVCTL_CONFIG_SYNC_INTERVAL_MINUTES |
Minutes between ai-kit config sync rate-limit checks; overrides DEVCTL_CONFIG_SYNC_INTERVAL_HOURS when set (fractional ok) |
DEVCTL_CONFIG_SYNC_INTERVAL_HOURS |
Hours between config sync checks when minutes unset (default: 1; fractional ok) |
DEVCTL_AI_KIT_REPO |
(install.sh only) If set, run ai-kit setup after binary install |
DEVCTL_AI_KIT_BACKGROUND_SYNC |
(install.sh only) Set to 1 to run install-background-sync after setup |
After ai-kit setup, install the scheduler so repos stay current without running devctl manually:
devctl ai-kit install-background-sync- macOS:
~/Library/LaunchAgents/com.devctl.config-sync.plist, interval 3600 seconds
Linux: cron line at minute 0 each hour (0 * * * * … ai-kit sync)
The job runs devctl ai-kit sync. When new commits are pulled, devctl shows a desktop notification (macOS: AppleScript; Linux: notify-send). Set DEVCTL_SKIP_NOTIFY=1 to turn notifications off.
Logs: stdout and stderr from the scheduled job are appended to:
~/.devctl/logs/background-sync.log
The file is created at install (empty until the first run). macOS does not run the job until the next interval (3600 s) unless you kickstart (below), so the log can stay empty until then.
tail -f ~/.devctl/logs/background-sync.logIf the log path in your plist/cron is wrong, run devctl ai-kit uninstall-background-sync and install-background-sync again.
Common gotchas
- Do not install with
sudo— the plist and log path are tied to the user that ran install (Path.home()). If you used sudo, logs live under root’s home, not yours. - Buffered output — scheduled runs use a non-TTY stdout; the job sets
PYTHONUNBUFFERED=1so lines appear promptly after each run.
Quick check (macOS) after install — inspect launchd state (e.g. state, last exit status):
launchctl print "gui/$(id -u)/com.devctl.config-sync"Force one run immediately:
launchctl kickstart -k "gui/$(id -u)/com.devctl.config-sync"Verbose manual sync:
devctl ai-kit sync -vInteractive devctl ai-kit update still works when you want explicit pulls and printed output per repo.
wi-devctl/
├── src/devctl/
│ ├── cli/ # main, ai-kit, devspace, local
│ ├── core/ # protocol_engine, repo_manager, versioning, backup, updater, config_sync, background_sync
│ └── utils/ # shell, yaml_loader, logging, notify
├── examples/
│ └── protocol.yaml
├── tests/
├── install.sh
└── .github/workflows/release.yml
git clone https://github.com/WorkIndia-Private/wi-devctl.git
cd wi-devctl
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -e ".[dev]"
# Run tests
pytest
# Build binary locally
pyinstaller --onefile --name devctl --paths src src/devctl/cli/main.py
./dist/devctl --helpRun pytest from the repo root (uses pythonpath = ["src"] in pyproject.toml).
| Area | File | What it covers |
|---|---|---|
| Protocols | tests/test_protocol_engine.py |
Load YAML/YML, validation errors, file_sync, merge behavior, obligations / recommendations present vs missing, unknown type, missing source, apply_protocols across multiple protocols |
| Updater | tests/test_updater.py |
Manifest / version comparison, platform key shape, perform_update rate limit and force paths (no real download) |
| Config sync | tests/test_config_sync.py |
perform_config_sync: no repos, rate limit, pull + notify, skip bad path / no remote updates |
| CLI | tests/test_cli.py |
list, ai-kit sync, update-cli, --version, devspace/local help (DEVCTL_SKIP_AUTO_UPDATE=1, isolated HOME) |
| State | tests/test_versioning.py |
state.json, register_repo, get_repo_version |
| Background install | tests/test_background_sync.py |
launchd plist write (mocked launchctl), missing binary, uninstall when absent |
| Notifications | tests/test_notify.py |
DEVCTL_SKIP_NOTIFY, macOS osascript path |
| Repos | tests/test_repo_manager.py |
URL → slug, fetch_and_has_updates (mocked git) |
End-to-end git clone, auto-update binary replace, and real launchd/cron are not run in CI (use a manual machine or staging for those).
Push a tag to trigger the release workflow:
git tag v1.0.0
git push origin v1.0.0CI builds binaries and creates a GitHub Release. Auto-update uses the GitHub Releases API (or DEVCTL_MANIFEST_URL if set).
MIT