Skip to content

feat!: Watcher builder API, monorepo-aware init, and CI that actually runs the tests - #59

Merged
ak9024 merged 5 commits into
mainfrom
feat/library-api-init-and-ci
Aug 1, 2026
Merged

ak9024 merged 5 commits into
mainfrom
feat/library-api-init-and-ci

Conversation

@ak9024

@ak9024 ak9024 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Three related changes plus the CI to keep them honest. Every commit builds on its own (verified with cargo check --all-targets at each SHA).

1. Library API — feat(api)!

The public surface was free functions with six positional Option arguments, two different error types, and a process::exit(1) inside run::config that killed the host process when any workspace failed.

// before
run::run(dir, cmd, ignore, bin_path, bin_arg, env_vars).await?;

// now
Watcher::builder()
    .workspace(Workspace::new("./api").cmd("cargo build").bin_path("target/debug/api"))
    .build()?      // validates here
    .run()         // -> Result<(), rustywatch::Error>
    .await?;
  • Everything fallible returns rustywatch::Result<T> over a #[non_exhaustive] Error enum — no boxed trait objects, no bare notify::Error.
  • Watcher::run aborts sibling workspaces and returns the first failure. Nothing in the library calls process::exit — only main does, on the returned Err.
  • Eager validation: blank dir, missing cmd, invalid ignore globs all fail at build().
  • CompiledFilter is wired into the watch path, replacing the legacy suffix-matching is_ignored (which never matched files inside an ignored directory unless the pattern ended in /).
  • Debounce timings configurable; two unwrap()s removed from the notify setup.

2. Init — feat(init)!

Bug fix: the Rust template emitted dir: src with bin_path: ./target/debug/{name}. A relative bin_path resolves against the watch directory, so it pointed at src/target/debug/{name} — the binary silently never started, with nothing logged. Verified both directions on a real crate before and after.

init now scans subdirectories, so a monorepo is configured in one run:

$ rustywatch init --yes
Detected 3 project(s):
  - .              (Rust — shop)
  - services/api   (Go — api)
  - web            (Node.js — storefront)
Created rustywatch.yaml with 3 workspace(s).

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 of dev/start/serve/watch package.json declares, the Python command points at an entry file that exists, and a .env gets wired to env_file.

Non-interactive correctness: --yes with 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 --yes errors early instead of failing inside the prompt library. Config::validate runs before anything is written.

New flags: --dir, --depth, --dry-run.

3. Monitor crash — fix(monitor)

draw_memory_gauge computed used / total unguarded. 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 in raw mode, leaving the user's shell broken. Found by the new render tests. Both gauges now clamp through a shared helper, and App::update_history no longer pushes NaN into the sparkline.

Tests

134 → 243. Library coverage 67.8% → 89.4%.

Area Before After
monitor/theme.rs 0% 100%
monitor/ui.rs 0% 98%
monitor/app.rs 54% 89%
watch/reload_controller.rs 82% 96%
  • tests/library_api.rs — the public API as a downstream crate sees it, using only root re-exports.
  • tests/init.rs — the init CLI end-to-end (monorepo scan, --dry-run, --force guard, no-TTY guard, generated config round-trips through Config::from_file).
  • monitor/ui.rs renders into ratatui::backend::TestBackend across all three responsive layouts.
  • Monitor tests use a test-only App::with_processes that injects a fixed list and an empty System, so they're deterministic and kill_process can never reach a live PID.

New integration tests use env!("CARGO_BIN_EXE_rustywatch") instead of a nested cargo run — faster, no build-lock contention.

CI

CI never ran cargo test. The only test step was cargo llvm-cov --lib, so every integration test and doc test was compiled by nobody.

  • New test job on ubuntu-latest + macos-14: cargo test --locked --all-targets, plus a separate cargo test --locked --doc (--all-targets skips doc tests).
  • New docs job: cargo doc --no-deps with RUSTDOCFLAGS=-D warnings, catching broken intra-doc links.
  • New msrv job pinned to the declared minimum.
  • Clippy runs with --all-targets, so tests and benches are linted.
  • Old test job renamed to coverage (unchanged otherwise, so the Codecov trend stays comparable).

The MSRV job immediately caught that rust-version = "1.81.0" was false — the pinned globset needs edition 2024. Verified 1.81 fails and 1.85 passes; corrected to 1.85.0.

Version

0.2.11 → 0.4.0. Breaking, and existing tags already reach v0.3.12, so 0.3.0 would collide.

Docs

New reference/library-api.md (API tables, error variants, migration table), usage/init.md rewritten, README library + init sections, and CLAUDE.md architecture/testing notes. Also corrected reference.md, which documented --bin_path/--bin_arg — clap emits --bin-path/--bin-arg.

Review notes

  • Breaking removals: run::run, watch::notify::watcher, config::helper::read, watch::filter::is_ignored, ProjectTemplate::default_watch_dir.
  • CLI-visible: rustywatch with no --cmd and no config now errors instead of watching an empty command; init output no longer emits null fields.
  • cfg!(test) is false in integration tests, so Watcher::run watches forever from tests/ — those tests only construct and validate, and that constraint is written into CLAUDE.md.

🤖 Generated with Claude Code

ak9024 added 4 commits August 1, 2026 12:50
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.
@ak9024 ak9024 self-assigned this Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@ak9024, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be0521bb-7e61-449c-8d0c-203cbd04bc76

📥 Commits

Reviewing files that changed from the base of the PR and between f0f0c28 and fb219e2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (31)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • Cargo.toml
  • README.md
  • _typos.toml
  • src/args.rs
  • src/config/helper.rs
  • src/config/mod.rs
  • src/config/schema.rs
  • src/error.rs
  • src/init/detect.rs
  • src/init/generator.rs
  • src/init/prompts.rs
  • src/init/templates.rs
  • src/lib.rs
  • src/main.rs
  • src/monitor/app.rs
  • src/monitor/service.rs
  • src/monitor/theme.rs
  • src/monitor/ui.rs
  • src/run.rs
  • src/watch/filter.rs
  • src/watch/notify.rs
  • src/watch/reload.rs
  • src/watch/reload_controller.rs
  • src/watcher.rs
  • tests/init.rs
  • tests/library_api.rs
  • web/src/content/docs/reference/library-api.md
  • web/src/content/docs/reference/reference.md
  • web/src/content/docs/usage/init.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Deploying rustywatch with  Cloudflare Pages  Cloudflare Pages

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

View logs

`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.
@ak9024
ak9024 merged commit 72adc87 into main Aug 1, 2026
26 checks passed
@ak9024
ak9024 deleted the feat/library-api-init-and-ci branch August 1, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant