Repository navigation
feat!: Watcher builder API, monorepo-aware init, and CI that actually runs the tests - #59
Conversation
The library surface was a set of free functions taking six positional
`Option` arguments, mixed error types (`Box<dyn Error>` in one place,
`notify::Error` in another), and a `process::exit(1)` inside `run::config`
that killed the host process when any workspace failed.
Add `Watcher`/`WatcherBuilder` plus chainable `Workspace` setters as the
supported entry point, and curate the crate-root re-exports:
Watcher::builder()
.workspace(Workspace::new("./api").cmd("cargo build")
.bin_path("target/debug/api"))
.build()?
.run()
.await?;
Every fallible entry point now returns `rustywatch::Result<T>` over a
`#[non_exhaustive] Error` enum, so callers can match on the failure instead
of string-matching a boxed trait object.
Behaviour changes beyond the signatures:
- `Watcher::run` aborts sibling workspaces and returns the first failure
rather than calling `process::exit`; only `main` maps `Err` to an exit
code, so the engine is safe to embed.
- Validation is eager: empty workspace lists, blank `dir`, missing `cmd`
and ignore patterns that are not valid globs all fail at `build()`.
- `CompiledFilter` is wired into the watch path, replacing the legacy
suffix/substring `is_ignored`, which never matched files inside an
ignored directory unless the pattern ended in `/`.
- Debounce timings are configurable instead of hardcoded.
- `recommended_watcher(..).unwrap()` and the panicking send inside the
notify callback are gone.
BREAKING CHANGE: `run::run`, `watch::notify::watcher`,
`config::helper::read` and `watch::filter::is_ignored` are removed;
`Config::validate` returns `rustywatch::Result<()>`; `reload` and
`ReloadController::new` take a `&Workspace` instead of loose arguments.
`rustywatch init` only looked at the current directory, so a monorepo had
to be configured workspace by workspace. It now scans subdirectories
(`--depth`, default 2) and turns every project it finds into a workspace,
skipping hidden directories and build/dependency output so a vendored copy
inside `node_modules/` is not mistaken for a real project.
Fix the Rust template while here: it emitted `dir: src` together with
`bin_path: ./target/debug/{name}`. Since a relative `bin_path` resolves
against the watch directory, that pointed at `src/target/debug/{name}`,
which never exists — the binary silently never started, with no error
logged. The template now watches the crate root.
Generated defaults are also read from the manifests rather than assumed:
the JavaScript command is whichever of `dev`/`start`/`serve`/`watch` the
`package.json` actually declares, the Python command points at an entry
file that exists, and a workspace with a `.env` gets `env_file` set.
Correctness fixes in the non-interactive path:
- `--yes` with an existing config used to open an interactive `Confirm`,
blocking a mode that is supposed to never prompt. It now errors and
asks for `--force`.
- Running without `--yes` outside a terminal errors early instead of
failing inside the prompt library.
- `Config::validate` runs before anything is written, so `init` cannot
produce a file that `rustywatch` will reject.
Add `--dir`, `--depth` and `--dry-run`, validate interactive input (the
watch directory must exist, the command is required, ignore patterns must
compile as globs), and split the pure helpers out of the prompt I/O so
they can be unit-tested.
BREAKING CHANGE: `ProjectTemplate::to_workspace` takes the config-relative
directory and the on-disk directory; `ProjectTemplate::default_watch_dir`
is removed.
`draw_memory_gauge` computed `used / total` without guarding a zero total. On any platform where sysinfo cannot read total memory that is NaN, and `LineGauge::ratio` panics outside 0.0..=1.0 — taking down the dashboard while the terminal is still in raw mode, so the shell is left broken. Clamp both gauges through a shared `gauge_ratio` helper and guard the division in `App::update_history`, which was pushing the same NaN into the sparkline history. Also make the monitor testable: extract `sort_processes` and `matches_search` as pure functions, and add a test-only `App::with_processes` that injects a fixed process list and an empty `System`. Assertions are deterministic and `kill_process` can never reach a live PID. `ui.rs` now renders into `ratatui::backend::TestBackend` across all three responsive layouts, the help overlay and the confirm dialogs. Coverage: theme.rs 0% -> 100%, ui.rs 0% -> 98%, app.rs 54% -> 89%.
CI never ran `cargo test`. The only test step was `cargo llvm-cov --lib`, so every integration test and every doc test was compiled by nobody and could rot unnoticed. Add a `test` job on ubuntu-latest and macos-14 running `cargo test --locked --all-targets` plus a separate `cargo test --locked --doc`, since `--all-targets` deliberately skips doc tests. Add two more gates: - `docs`: `cargo doc --no-deps` with `RUSTDOCFLAGS=-D warnings`, which catches broken intra-doc links in the public API. - `msrv`: `cargo check` pinned to the declared minimum toolchain. The MSRV job immediately showed that `rust-version = "1.81.0"` was false — the pinned `globset` requires edition 2024, so 1.81 cannot even parse its manifest. Verified 1.81 fails and 1.85 passes, and corrected the declaration to 1.85.0. Clippy now runs with `--all-targets` so test and bench code is linted too, and the existing coverage job is renamed to `coverage` to free up the `test` name. Bump the version to 0.4.0 for the breaking library API and init changes.
|
Warning Review limit reached
Next review available in: 7 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (31)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying rustywatch with
|
| Latest commit: |
fb219e2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5aac505c.rustywatch.pages.dev |
| Branch Preview URL: | https://feat-library-api-init-and-ci.rustywatch.pages.dev |
`typos` insists `ratatui` should be `ratatouille`, so the job has been failing since CLAUDE.md was added in f0f0c28 — that file names the crate. Add a `_typos.toml` allowlist rather than avoiding the crate's own name.
Three related changes plus the CI to keep them honest. Every commit builds on its own (verified with
cargo check --all-targetsat each SHA).1. Library API —
feat(api)!The public surface was free functions with six positional
Optionarguments, two different error types, and aprocess::exit(1)insiderun::configthat killed the host process when any workspace failed.rustywatch::Result<T>over a#[non_exhaustive] Errorenum — no boxed trait objects, no barenotify::Error.Watcher::runaborts sibling workspaces and returns the first failure. Nothing in the library callsprocess::exit— onlymaindoes, on the returnedErr.dir, missingcmd, invalid ignore globs all fail atbuild().CompiledFilteris wired into the watch path, replacing the legacy suffix-matchingis_ignored(which never matched files inside an ignored directory unless the pattern ended in/).unwrap()s removed from the notify setup.2. Init —
feat(init)!Bug fix: the Rust template emitted
dir: srcwithbin_path: ./target/debug/{name}. A relativebin_pathresolves against the watch directory, so it pointed atsrc/target/debug/{name}— the binary silently never started, with nothing logged. Verified both directions on a real crate before and after.initnow scans subdirectories, so a monorepo is configured in one run:Hidden dirs and build/dependency output are skipped, so a decoy project inside
node_modules/is not picked up. Defaults are read from manifests rather than assumed — the JS command is whichever ofdev/start/serve/watchpackage.jsondeclares, the Python command points at an entry file that exists, and a.envgets wired toenv_file.Non-interactive correctness:
--yeswith an existing config used to open an interactive prompt (blocking a mode that should never prompt) — it now errors and asks for--force. No TTY without--yeserrors early instead of failing inside the prompt library.Config::validateruns before anything is written.New flags:
--dir,--depth,--dry-run.3. Monitor crash —
fix(monitor)draw_memory_gaugecomputedused / totalunguarded. Where sysinfo cannot read total memory that isNaN, andLineGauge::ratiopanics outside0.0..=1.0— taking down the dashboard while the terminal is in raw mode, leaving the user's shell broken. Found by the new render tests. Both gauges now clamp through a shared helper, andApp::update_historyno longer pushes NaN into the sparkline.Tests
134 → 243. Library coverage 67.8% → 89.4%.
monitor/theme.rsmonitor/ui.rsmonitor/app.rswatch/reload_controller.rstests/library_api.rs— the public API as a downstream crate sees it, using only root re-exports.tests/init.rs— theinitCLI end-to-end (monorepo scan,--dry-run,--forceguard, no-TTY guard, generated config round-trips throughConfig::from_file).monitor/ui.rsrenders intoratatui::backend::TestBackendacross all three responsive layouts.App::with_processesthat injects a fixed list and an emptySystem, so they're deterministic andkill_processcan never reach a live PID.New integration tests use
env!("CARGO_BIN_EXE_rustywatch")instead of a nestedcargo run— faster, no build-lock contention.CI
CI never ran
cargo test. The only test step wascargo llvm-cov --lib, so every integration test and doc test was compiled by nobody.testjob onubuntu-latest+macos-14:cargo test --locked --all-targets, plus a separatecargo test --locked --doc(--all-targetsskips doc tests).docsjob:cargo doc --no-depswithRUSTDOCFLAGS=-D warnings, catching broken intra-doc links.msrvjob pinned to the declared minimum.--all-targets, so tests and benches are linted.testjob renamed tocoverage(unchanged otherwise, so the Codecov trend stays comparable).The MSRV job immediately caught that
rust-version = "1.81.0"was false — the pinnedglobsetneeds edition 2024. Verified 1.81 fails and 1.85 passes; corrected to1.85.0.Version
0.2.11→0.4.0. Breaking, and existing tags already reachv0.3.12, so0.3.0would collide.Docs
New
reference/library-api.md(API tables, error variants, migration table),usage/init.mdrewritten, README library + init sections, and CLAUDE.md architecture/testing notes. Also correctedreference.md, which documented--bin_path/--bin_arg— clap emits--bin-path/--bin-arg.Review notes
run::run,watch::notify::watcher,config::helper::read,watch::filter::is_ignored,ProjectTemplate::default_watch_dir.rustywatchwith no--cmdand no config now errors instead of watching an empty command;initoutput no longer emitsnullfields.cfg!(test)is false in integration tests, soWatcher::runwatches forever fromtests/— those tests only construct and validate, and that constraint is written into CLAUDE.md.🤖 Generated with Claude Code