recast
CLI for safe, atomic, transparent multi-file text rewrites. Pure Rust. Tuned for LLM coding agents driving mechanical edits; equally usable by humans for mechanical refactors.
recast differs from sed / sd / a Python heredoc in five places:
- Match-required guard. Default
--at-least 1exits non-zero when nothing matches. Silent no-match is impossible by default. - Idempotency check. Refuses non-convergent rewrites; reports "already applied" on the second run.
- Atomic apply. Two-phase commit (sibling temp + fsync + rename)
with rollback if any per-file step fails. Crash-recovery sweep
reconciles leftover
.recast.bak/.recast.tmpsiblings. - Agent-friendly JSON.
--jsonemits a single-line, schema-locked report for plan / apply / check / error. - Three pattern modes. Regex (default), Rhai script callback
(
--script), or tree-sitter structural (--lang+--query/--ast).
Status: alpha (v0.1.6). All phases of PLAN.md landed.
- ๐ฆ Install:
cargo install recast-cli(crates.io/recast-cli) - ๐ Library: crates.io/recast-core ยท docs.rs/recast-core
- ๐ฅ Pre-built binaries: GitHub Releases
- ๐ Hosted docs: https://stoica-mihai.github.io/recast/
- ๐ Operating manual:
AGENTS.md
Install
Pre-built binary
Download the matching artifact for your platform from Releases:
| OS | Targets |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl |
| Linux aarch64 | aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl |
| macOS x86_64 | x86_64-apple-darwin |
| macOS Apple Silicon | aarch64-apple-darwin |
The musl builds are statically linked โ drop into an Alpine container
or distroless image without a glibc dependency.
Cargo install (from crates.io)
The crate on crates.io is named recast-cli (the bare recast name was
already taken by an unrelated serialization-format library). The
installed binary is still called recast โ that's the command name
everything in this README uses.
Cargo install (from source)
Stock install ships every grammar, the Rhai script engine, and JSON
output. Opt out via --no-default-features and pick only the features
you want (see ยง Cargo features).
Usage
Diff preview (default)
Prints a unified diff per file plus a summary line. No writes.
Atomic apply
Two-phase commit: each file is written to a sibling temp + fsync, then renamed into place. A failure mid-rename triggers reverse-rename of every already-renamed file from its backup, leaving the tree bit-identical to the pre-image.
CI gate
# exit 0: nothing would change
# exit 1: at least one file would change
Capture groups
$1, ${name} interpolated; use --literal (-L) to disable
interpolation.
Filters
Stdin mode
|
# fn new_name() {}
Reads one buffer, rewrites once, writes to stdout. Skips the walker and commit phases. The match-count guard still applies.
Scripted replacement (Rhai callback)
--script takes a path to a Rhai script that runs per match. The
script's return value (coerced to string) becomes the replacement. The
positional REPLACEMENT is still required but ignored โ pass "".
|
# version 4
The script sees captures (array; index 0 is the full match) and
whole (full-match alias โ match is a Rhai reserved keyword).
Structural rewrite (tree-sitter)
Two modes. Both require --lang <LANG>.
Friendly --ast โ write the pattern in the target language with
$NAME (single-node) and $$$NAME (variable-shape subtree) placeholders:
Matches any function regardless of param count or body shape; rewrites the name and keeps the original args/body verbatim.
Raw --query โ pass a tree-sitter S-expression query directly:
Capture named @root (or, absent that, the outermost capture in each
match) defines the byte range to replace. The template uses
$capture_name / ${capture_name} for substitution.
Supported languages
| Language | CLI name | Feature |
|---|---|---|
| Rust | rust, rs |
lang-rust |
| TypeScript | typescript, ts |
lang-ts |
| TSX | tsx |
lang-ts |
| JavaScript | javascript, js, jsx |
lang-js |
| Python | python, py |
lang-python |
| Bash | bash, sh, shell |
lang-bash |
| Go | go, golang |
lang-go |
| JSON | json |
lang-json |
| Markdown | markdown, md |
lang-md |
Crash recovery
If a --apply crashes mid-commit (panic, signal, power loss), the tree
may have leftover .foo.recast.bak.N / .foo.recast.tmp.N siblings.
Reconcile with:
Restores from backup when the target is missing; deletes stale temps and backups when the target is present.
Shell completions
Also supported: elvish, powershell.
Agent-friendly JSON
# {"kind":"apply","outcome":"changes","files_scanned":12,"files_written":3,"total_matches":7}
Schema documented in PLAN.md ยง7.1.
Snapshot-locked in crates/recast-core/src/snapshots/ โ every
field-name / shape change is a visible PR diff.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, or "no changes needed" |
1 |
--check set and at least one file would change |
2 |
Match-count guard violated (--at-least / --at-most) |
3 |
Internal error (regex parse, I/O, non-convergent pattern, script error, โฆ) |
Cargo features
| Feature | Default | What it enables |
|---|---|---|
script |
โ | Rhai script callback (--script) |
lang-rust |
โ | Rust grammar for structural mode |
lang-ts |
โ | TypeScript + TSX grammars |
lang-js |
โ | JavaScript + JSX grammar |
lang-python |
โ | Python grammar |
lang-bash |
โ | Bash grammar |
lang-go |
โ | Go grammar |
lang-json |
โ | JSON grammar |
lang-md |
โ | Markdown grammar |
lang-all |
โ | Meta โ enables every lang-* above |
Structural mode requires at least one lang-* feature. Drop the ones
you don't need to keep the binary lean:
Build from source
127 tests on Linux + macOS. Proptest harness covers every public entry point with randomized input.
License
MIT. See LICENSE.