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