{"id":50435011,"url":"https://github.com/hexsprite/condrun","last_synced_at":"2026-05-31T16:32:22.711Z","repository":{"id":361318148,"uuid":"1254017867","full_name":"hexsprite/condrun","owner":"hexsprite","description":"Conditional command runner — gates execution on system-state predicates (wifi SSID, low-data-mode, AC power, interface type) and kills the child if they flip mid-run.","archived":false,"fork":false,"pushed_at":"2026-05-30T04:04:27.000Z","size":154,"stargazers_count":0,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T05:17:28.886Z","etag":null,"topics":["backup","cli","macos","metered-connection","restic","rust","supervisor","wifi"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hexsprite.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-30T03:41:38.000Z","updated_at":"2026-05-30T04:04:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hexsprite/condrun","commit_stats":null,"previous_names":["hexsprite/condrun"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hexsprite/condrun","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fcondrun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fcondrun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fcondrun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fcondrun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hexsprite","download_url":"https://codeload.github.com/hexsprite/condrun/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fcondrun/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33739861,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backup","cli","macos","metered-connection","restic","rust","supervisor","wifi"],"created_at":"2026-05-31T16:32:14.052Z","updated_at":"2026-05-31T16:32:22.703Z","avatar_url":"https://github.com/hexsprite.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# condrun\n\n[![CI](https://github.com/hexsprite/condrun/actions/workflows/ci.yml/badge.svg)](https://github.com/hexsprite/condrun/actions/workflows/ci.yml)\n[![crates.io](https://img.shields.io/crates/v/condrun.svg)](https://crates.io/crates/condrun)\n[![license](https://img.shields.io/crates/l/condrun.svg)](#license)\n\n**Conditional command runner.** Don't burn metered bandwidth on backups.\n\n`condrun` gates command execution on live system-state predicates and kills the child process if those predicates flip mid-run. Wrap a `restic`/`rsync`/`borg`/`duplicacy` cron job with `condrun` and stop worrying about the 2am backup chewing through your phone tether.\n\n## What it does\n\nCron is dumb. It fires at the scheduled time regardless of whether you're on home Wi-Fi, on a Personal Hotspot, in Low Data Mode on the train, or off-network entirely. A bandwidth-sensitive job like `restic backup` doesn't care — it'll happily push 4GB through your cellular connection and surprise you with an overage bill.\n\n`condrun` sits between cron and your job. Before launching the child, it samples the current network state. If the predicates fail (e.g. you asked for \"not metered\" and you're tethered), it exits silently — cron sees a clean run, no log spam. If the predicates pass, it spawns the child and keeps polling. The instant a predicate flips (you tether mid-backup, or toggle Low Data Mode), it sends `SIGTERM`, waits for a configurable grace period, then `SIGKILL`s.\n\nUse cases: `restic`, `rsync`, `borg`, `duplicacy`, `rclone`, or any cron task where bandwidth class matters more than punctuality.\n\n## Install\n\n```bash\ncargo install condrun\n```\n\nOr build from a clone and install the binary directly:\n\n```bash\ngit clone https://github.com/jordanbaker/condrun\ncd condrun\nmake install                       # → ~/.local/bin/condrun (default)\nmake install PREFIX=/usr/local     # → /usr/local/bin/condrun (needs sudo)\nmake uninstall                     # remove\n```\n\nPre-built binaries (when published) will be available from the [GitHub releases page](https://github.com/jordanbaker/condrun/releases). A Homebrew tap (`brew install jordanbaker/tap/condrun`) is planned.\n\n`condrun` currently runs on macOS only — predicate detection uses Apple's `Network.framework` (`NWPathMonitor`), which has no Linux/Windows equivalent. Linux support via NetworkManager / `iwd` is on the roadmap.\n\n## Quick start — restic\n\nThe motivating example. Drop this in your crontab:\n\n```cron\n0 2 * * * condrun run --reject-expensive --reject-low-data -- restic backup -r s3:bucket ~/work\n```\n\nAt 2am every night:\n\n1. `condrun` samples the current network path.\n2. If the connection is **expensive** (cellular, Personal Hotspot) or **constrained** (Low Data Mode on), it exits 0 silently. Cron is happy. No partial upload, no overage.\n3. Otherwise it spawns `restic backup`, polls every 30 seconds, and kills it if you tether mid-job.\n\n## rsync example\n\n```bash\ncondrun run --reject-expensive -- rsync -av ~/photos backup-host:photos/\n```\n\nSingle predicate — only refuses to run on metered links, doesn't care about Low Data Mode.\n\n## Predicate reference (v0.1.0)\n\n| Flag | Behavior | macOS source |\n|---|---|---|\n| `--reject-expensive` | Pass iff current path is **not** marked expensive (cellular, Personal Hotspot) | [`NWPath.isExpensive`](https://developer.apple.com/documentation/network/nwpath/isexpensive) |\n| `--reject-low-data` | Pass iff Low Data Mode is **off** | [`NWPath.isConstrained`](https://developer.apple.com/documentation/network/nwpath/isconstrained) |\n\nSSID matching, AC-power gating, and interface-type predicates ship in v0.1.x. Track [SPEC.md](SPEC.md) §4 for the predicate roadmap.\n\n## Lifecycle flags\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--strict` | `false` | Exit `1` on pre-flight failure (default: silent exit `0`) |\n| `--kill-on-change` | `true` | Kill the child if predicates flip after launch |\n| `--no-kill-on-change` | — | Disable kill-on-change; child runs to completion regardless |\n| `--grace 30s` | `30s` | SIGTERM → SIGKILL grace period |\n| `--poll 30s` | `30s` | Watcher poll interval |\n| `--debounce 0s` | `0s` | Predicate must stay failed this long before triggering kill (debounces flapping) |\n| `--any` | `false` | Compose predicates with OR instead of AND |\n\nSubcommands:\n\n- `condrun run [predicates] -- \u003ccmd\u003e [args...]` — gate, spawn, supervise.\n- `condrun check [predicates]` — evaluate predicates and exit; useful for shell scripts and debugging.\n\n## Exit codes\n\nPer [SPEC.md](SPEC.md) §6:\n\n| Code | Meaning |\n|---|---|\n| `0` | Success, or pre-flight predicates failed in non-strict mode (silent skip) |\n| `1` | Pre-flight predicates failed under `--strict` |\n| `2` | Child exited non-zero (exit code preserved when possible) |\n| `3` | Child killed by `condrun` because predicates flipped mid-run |\n| `4` | CLI parse error, config error, or internal failure |\n\n## How metered detection works\n\nmacOS exposes network-path metadata through [`NWPathMonitor`](https://developer.apple.com/documentation/network/nwpathmonitor) in the Network framework. `condrun` instantiates a monitor, polls the current `NWPath`, and reads two boolean flags:\n\n- [`isExpensive`](https://developer.apple.com/documentation/network/nwpath/isexpensive) — true on cellular interfaces and on Personal Hotspot. This is the OS's own classification of \"billable bandwidth\".\n- [`isConstrained`](https://developer.apple.com/documentation/network/nwpath/isconstrained) — true when the user has enabled Low Data Mode for the active interface (Wi-Fi or cellular).\n\nBecause these flags come from the OS, they correctly track Personal Hotspot even when the laptop sees the tether as plain Wi-Fi. There's no SSID heuristic to maintain, no per-carrier list, no guessing.\n\n## Architecture\n\n`condrun` is built around small trait-based seams so the supervisor logic can be tested without touching the OS:\n\n- **`NetworkState`** — samples the current path. Real impl wraps `NWPathMonitor`; test impl is a static fixture.\n- **`Predicate`** — pure function from `NetworkState` snapshot to pass/fail.\n- **`Spawner` / `ChildHandle`** — abstracts `tokio::process::Command` so the supervisor's race logic can be exercised against a fake child.\n- **`Signals`** — abstracts SIGINT/SIGTERM delivery.\n- **Supervisor** — `tokio::select!` race between poll-tick, child completion, and external signals. Owns the kill-on-change state machine (debounce, SIGTERM, grace, SIGKILL).\n\nv0.1.0 is polling-only (`--poll 30s`). v0.2 will add event-driven `NWPathMonitor` updates so predicate flips are detected without waiting for the next tick.\n\n## Contributing\n\n```bash\n# Unit tests against the production binary (no test scaffolding):\ncargo test\n\n# Unit + integration tests (requires the test-fixture feature for the\n# fake spawner / fake network state used by the integration harness):\ncargo test --features test-fixture\n\n# Integration tests only, run serially (the CLI test harness mutates env):\ncargo test --features test-fixture --test cli -- --test-threads=1\n\n# Opt-in smoke tests against the real macOS Network.framework. Skipped in CI\n# because GitHub Actions runners don't expose realistic network paths;\n# meant to be run on a developer machine before tagging a release:\ncargo test --features platform-tests\n```\n\nLints and formatting:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --all-features --all-targets -- -D warnings\n```\n\n## License\n\nLicensed under either of [Apache License 2.0](LICENSE-APACHE) or\n[MIT license](LICENSE-MIT) at your option.\n\nUnless you explicitly state otherwise, any contribution intentionally\nsubmitted for inclusion in this crate by you, as defined in the Apache-2.0\nlicense, shall be dual licensed as above, without any additional terms or\nconditions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexsprite%2Fcondrun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhexsprite%2Fcondrun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexsprite%2Fcondrun/lists"}