Skip to main content

patchloom/
lib.rs

1#![deny(unsafe_code)]
2//! Patchloom: agent-grade repo operations as a Rust library.
3//!
4//! This crate provides both a CLI binary and a library API for structured
5//! file editing operations. The [`api`] module is the main entry point for
6//! library consumers.
7//!
8//! # Feature flags
9//!
10//! | Feature | Default | Description |
11//! |---------|---------|-------------|
12//! | `core`  | no      | Marker feature for core library usage (no extra deps) |
13//! | `mcp`   | **yes** | MCP server support (adds `tokio`, `rmcp`, `schemars`) |
14//! | `full`  | no      | Everything: currently equivalent to `mcp` |
15//!
16//! ## Embedding as a library
17//!
18//! To use patchloom as a library without the MCP server overhead:
19//!
20//! ```toml
21//! [dependencies]
22//! patchloom = { version = "0.1", default-features = false }
23//! ```
24//!
25//! This gives you the full [`api`] module (doc/replace/md/file/patch operations)
26//! and the [`ops`] module for lower-level access, without pulling in `tokio`
27//! or other async runtime dependencies.
28//!
29//! ## Thread safety
30//!
31//! All public API types ([`api::EditResult`], [`api::ApplyMode`], etc.) are
32//! `Send + Sync`. Library functions are safe to call concurrently from
33//! multiple threads with one constraint:
34//!
35//! - **Different files**: fully safe. Multiple threads can edit different files
36//!   simultaneously with no coordination.
37//! - **Same file**: the caller must serialize access. Concurrent writes to the
38//!   same file are inherently racy (last writer wins). Use a mutex or other
39//!   synchronization if you need to coordinate edits to a single file.
40//!
41//! Backup sessions use unique directory names (nanosecond timestamp +
42//! monotonic counter) so concurrent backup creation never collides.
43//!
44//! Configuration can be loaded once with [`config::CachedConfig`] and reused
45//! across threads, avoiding repeated disk reads.
46
47pub mod api;
48pub mod backup;
49pub mod cli;
50pub mod cmd;
51pub mod config;
52pub mod diff;
53pub mod exit;
54pub mod fallback;
55pub(crate) mod files;
56pub mod ops;
57pub mod plan;
58pub mod schema;
59pub mod selector;
60pub mod write;
61
62pub(crate) use files::*;
63
64use clap::Parser;
65use cli::Cli;
66
67// ---------------------------------------------------------------------------
68// Verbose logging
69// ---------------------------------------------------------------------------
70
71/// Global flag set once at startup; checked by the `verbose!` macro.
72static VERBOSE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
73
74/// Returns `true` if verbose mode is enabled.
75pub fn is_verbose() -> bool {
76    VERBOSE.load(std::sync::atomic::Ordering::Relaxed)
77}
78
79/// Enable verbose mode globally. Called once at startup.
80fn enable_verbose() {
81    VERBOSE.store(true, std::sync::atomic::Ordering::Relaxed);
82}
83
84/// Print a verbose diagnostic message to stderr.
85///
86/// Usage: `verbose!("processing {} files", count);`
87#[macro_export]
88macro_rules! verbose {
89    ($($arg:tt)*) => {
90        if $crate::is_verbose() {
91            eprintln!("[patchloom] {}", format!($($arg)*));
92        }
93    };
94}
95
96/// Run the patchloom CLI. Returns the exit code as a u8.
97pub fn run() -> anyhow::Result<u8> {
98    let cli = Cli::parse();
99
100    // Enable verbose mode from --verbose flag or PATCHLOOM_LOG env var.
101    if cli.global.verbose || std::env::var_os("PATCHLOOM_LOG").is_some() {
102        enable_verbose();
103    }
104
105    let structured = cli.global.json || cli.global.jsonl;
106    let compact = cli.global.jsonl;
107    match cmd::dispatch(cli) {
108        Ok(code) => Ok(code),
109        Err(e) if structured => {
110            let output = serde_json::json!({
111                "ok": false,
112                "error": format!("{e:#}")
113            });
114            let serialized = if compact {
115                serde_json::to_string(&output)
116            } else {
117                serde_json::to_string_pretty(&output)
118            };
119            println!("{}", serialized.unwrap_or_default());
120            Ok(exit::FAILURE)
121        }
122        Err(e) => Err(e),
123    }
124}