Skip to main content

coding_tools/cli/
ct_edit.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-edit` command grammar (see [`crate::cli`]); the `ct-edit` bin is a
5//! thin parse-and-dispatch wrapper over this `Cli`.
6
7use std::path::PathBuf;
8
9use clap::Parser;
10
11use crate::explain::Format;
12use crate::pulse::HeartbeatOpts;
13use crate::{blockdoc, pattern};
14
15#[derive(Parser, Debug)]
16#[command(
17    name = "ct-edit",
18    version,
19    about = "Find/replace across selected files, gated by an --expect verdict and previewable with --dry-run.",
20    long_about = "ct-edit applies a find/replace to the files chosen by ct-search-style predicates \
21                  (also reachable as `ct edit`). It computes every replacement first, classifies \
22                  the total against --expect, and writes only when the verdict is SUCCESS and \
23                  --dry-run is not set. --find/--replace accept file:PATH / text:VALUE payloads; \
24                  a multi-line find matches as a literal block. --script runs a .ctb batch \
25                  atomically: everything is verified in memory before anything is written. \
26                  See `ct-edit --explain` for agent-oriented documentation."
27)]
28pub struct Cli {
29    /// Search root (relative or absolute); a file edits just that file, a directory is descended.
30    #[arg(long, default_value = ".")]
31    pub base: PathBuf,
32
33    /// Limit to files whose name matches; '|'-separated alternatives, each substring->glob->regex promoted and anchored.
34    #[arg(long)]
35    pub name: Option<String>,
36
37    /// Include dot-entries (names starting with '.'); default skips them.
38    #[arg(long)]
39    pub hidden: bool,
40
41    /// Follow symlinks while traversing.
42    #[arg(long)]
43    pub follow: bool,
44
45    /// Pattern to find (substring->glob->regex promoted); matched per line. Accepts file:PATH / text:VALUE; a multi-line payload matches as a line-anchored literal block. Required unless --script is given.
46    #[arg(long, conflicts_with = "script")]
47    pub find: Option<String>,
48
49    /// Replacement text. With a regex --find, $1/${name} expand; otherwise literal. Accepts file:PATH / text:VALUE; for a block --find, an empty payload deletes the matched lines. Required unless --script is given.
50    #[arg(long, conflicts_with = "script")]
51    pub replace: Option<String>,
52
53    /// Pin how --find is interpreted (promotion off): literal, glob, or regex.
54    #[arg(long, value_enum, conflicts_with = "script")]
55    pub mode: Option<pattern::Mode>,
56
57    /// Run a .ctb edit script: a batch of find/replace blocks verified in full before any write (see --explain).
58    #[arg(long, value_name = "PATH")]
59    pub script: Option<PathBuf>,
60
61    /// Fence string opening script directive lines (for payloads that contain the default at line start).
62    #[arg(long, default_value = blockdoc::DEFAULT_FENCE, requires = "script")]
63    pub fence: String,
64
65    /// Script edits match pristine content instead of cascading; overlapping edits become a usage error.
66    #[arg(long, requires = "script")]
67    pub no_cascade: bool,
68
69    /// Verdict expectation over the total replacement count: any|none|N|=N|+N|-N (default: any). In scripts, per-edit expect= defaults to =1.
70    #[arg(long, conflicts_with = "script")]
71    pub expect: Option<String>,
72
73    /// Show what would change and the verdict, but write nothing.
74    #[arg(long)]
75    pub dry_run: bool,
76
77    /// Suppress the per-site diff; print only the summary line.
78    #[arg(long)]
79    pub quiet: bool,
80
81    /// Emit a structured JSON result instead of text.
82    #[arg(long)]
83    pub json: bool,
84
85    /// Abort with exit 2 if the scan exceeds SECS seconds (fractional allowed). Never interrupts the write phase: once a SUCCESS verdict starts writing, every write completes.
86    #[arg(long, value_name = "SECS")]
87    pub timeout: Option<f64>,
88
89    #[command(flatten)]
90    pub heartbeat: HeartbeatOpts,
91
92    /// Print agent usage docs (md or json) and exit.
93    #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
94    pub explain: Option<Format>,
95}