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 /// Walk gitignored / .ignore files too (the .git directory is always skipped); by default the walk skips what git would.
46 #[arg(long)]
47 pub no_ignore: bool,
48
49 /// 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.
50 #[arg(long, conflicts_with = "script")]
51 pub find: Option<String>,
52
53 /// 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.
54 #[arg(long, conflicts_with = "script")]
55 pub replace: Option<String>,
56
57 /// Pin how --find is interpreted (promotion off): literal, glob, or regex.
58 #[arg(long, value_enum, conflicts_with = "script")]
59 pub mode: Option<pattern::Mode>,
60
61 /// Run a .ctb edit script: a batch of find/replace blocks verified in full before any write (see --explain).
62 #[arg(long, value_name = "PATH")]
63 pub script: Option<PathBuf>,
64
65 /// Fence string opening script directive lines (for payloads that contain the default at line start).
66 #[arg(long, default_value = blockdoc::DEFAULT_FENCE, requires = "script")]
67 pub fence: String,
68
69 /// Script edits match pristine content instead of cascading; overlapping edits become a usage error.
70 #[arg(long, requires = "script")]
71 pub no_cascade: bool,
72
73 /// Verdict expectation over the total replacement count: any|none|N|=N|+N|-N (default: any). In scripts, per-edit expect= defaults to =1.
74 #[arg(long, conflicts_with = "script")]
75 pub expect: Option<String>,
76
77 /// Show what would change and the verdict, but write nothing.
78 #[arg(long)]
79 pub dry_run: bool,
80
81 /// Suppress the per-site diff; print only the summary line.
82 #[arg(long)]
83 pub quiet: bool,
84
85 /// Emit a structured JSON result instead of text.
86 #[arg(long)]
87 pub json: bool,
88
89 /// Like `--json`, but pretty-printed (indented).
90 #[arg(long)]
91 pub json_pretty: bool,
92
93 /// 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.
94 #[arg(long, value_name = "SECS")]
95 pub timeout: Option<f64>,
96
97 #[command(flatten)]
98 pub heartbeat: HeartbeatOpts,
99
100 /// Print agent usage docs (md or json) and exit.
101 #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
102 pub explain: Option<Format>,
103}