atomwrite 0.1.37

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
// SPDX-License-Identifier: MIT OR Apache-2.0
// Included by part1.inc.rs, after EditLoopArgs.

/// How `search` writes its answers on stdout.
///
/// Deliberately separate from [`crate::cli::OutputFormat`], which belongs to
/// `read`: adding a `Text` value there would make `read --format text` fall
/// through to the NDJSON branch, because `read` only tests for `Raw`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum SearchFormat {
    /// One NDJSON record per event (the agent contract, and the default).
    #[default]
    Ndjson,
    /// Plain text, one line per match, shaped like `rg -n`.
    Text,
}

/// Arguments for the search subcommand.
#[derive(Args, Debug)]
#[derive(Clone)]
pub struct SearchArgs {
    /// Search pattern (regex by default).
    ///
    /// A pattern that starts with a hyphen must come after `--`, so that an
    /// unknown flag is reported as an argument error instead of being taken
    /// as the pattern.
    pub pattern: String,

    /// Paths to search within.
    ///
    /// When omitted, `search` scans the workspace root (never the process
    /// current directory) and reports `target_source = "workspace"`. A root
    /// named here is resolved against `--workspace` and reported as `"argv"`.
    #[arg(value_hint = ValueHint::AnyPath)]
    pub paths: Vec<PathBuf>,

    /// Treat pattern as regex.
    #[arg(
        short = 'e',
        long,
        conflicts_with = "fixed",
        help = "Treat pattern as regex (default)",
        action = clap::ArgAction::SetTrue
    )]
    pub regex: bool,

    /// Treat pattern as fixed string.
    #[arg(
        short = 'F',
        long,
        conflicts_with = "regex",
        help = "Treat pattern as fixed string",
        action = clap::ArgAction::SetTrue
    )]
    pub fixed: bool,

    /// Match whole words only.
    #[arg(short = 'w', long, help = "Match whole words only", action = clap::ArgAction::SetTrue)]
    pub word: bool,

    /// Case-insensitive search.
    #[arg(short = 'i', long, help = "Case-insensitive search", action = clap::ArgAction::SetTrue)]
    pub case_insensitive: bool,

    /// Smart case: insensitive when pattern is lowercase.
    #[arg(
        short = 'S',
        long,
        help = "Smart case: insensitive if pattern is lowercase",
        action = clap::ArgAction::SetTrue
    )]
    pub smart_case: bool,

    /// Lines of context around matches.
    #[arg(
        short = 'C',
        long,
        default_value_t = 0,
        help = "Lines of context around matches"
    )]
    pub context: usize,

    /// Glob patterns for file inclusion.
    #[arg(short = 'g', long, action = clap::ArgAction::Append, help = "Include files matching glob")]
    pub include: Vec<String>,

    /// Glob patterns for file exclusion.
    #[arg(long, action = clap::ArgAction::Append, help = "Exclude files matching glob")]
    pub exclude: Vec<String>,

    /// Show only match count per file.
    #[arg(short = 'c', long, help = "Only show match count per file", action = clap::ArgAction::SetTrue)]
    pub count: bool,

    /// Show only filenames with matches.
    #[arg(short = 'l', long, help = "Only show filenames with matches", action = clap::ArgAction::SetTrue)]
    pub files: bool,

    /// Maximum matches per file.
    #[arg(short = 'm', long, help = "Maximum matches per file")]
    pub max_count: Option<u64>,

    /// Enable multi-line matching.
    #[arg(short = 'U', long, help = "Enable multi-line matching", action = clap::ArgAction::SetTrue)]
    pub multiline: bool,

    /// Show non-matching lines.
    #[arg(long, help = "Show lines that do NOT match", action = clap::ArgAction::SetTrue)]
    pub invert: bool,

    /// Sort results by criterion.
    #[arg(long, value_enum, help = "Sort results by criterion")]
    pub sort: Option<SortBy>,

    /// Include FIFO / named pipe files in the search.
    ///
    /// By default `search` skips FIFOs, because opening one blocks until the
    /// other end connects, which can hang automated runs and containers that
    /// have FIFOs in /tmp or /var. Pass `--include-fifo` to open them anyway
    /// (this may hang).
    #[arg(
        long,
        help = "Include FIFO / named pipe files (default: skip to avoid hangs)",
        action = clap::ArgAction::SetTrue
    )]
    pub include_fifo: bool,

    /// Maximum file size in bytes for `search`.
    ///
    /// Larger files are skipped; the run counts them in `files_skipped` on the
    /// `summary` event instead of emitting one event per file. The default is
    /// 10 MiB. Useful for skipping `node_modules`, `target/`, log archives,
    /// and other large generated files.
    #[arg(
        long,
        default_value_t = crate::constants::DEFAULT_SEARCH_MAX_FILESIZE_BYTES,
        help = "Skip files larger than N bytes (10 MiB by default)"
    )]
    pub max_filesize: u64,

    /// Maximum line length in columns for `search` matches.
    ///
    /// Longer lines are truncated with a `...[truncated]` marker. The default
    /// is 500 columns. Useful for minified bundle.js, styles.min.css and other
    /// single-line giant files that explode context windows.
    #[arg(
        long,
        default_value_t = crate::constants::DEFAULT_SEARCH_MAX_COLUMNS,
        help = "Truncate matches longer than N columns (500 by default)"
    )]
    pub max_columns: usize,

    /// Search binary files. Default: skip files that look binary
    /// (`NUL` bytes or binary content detection), matching ripgrep-style safety for agents.
    #[arg(
        long,
        help = "Search binary files (default: skip binary / NUL content)",
        action = clap::ArgAction::SetTrue
    )]
    pub binary: bool,

    /// Suppress per-file `begin` and `end` NDJSON events for files with
    /// zero matches. Default: emit `begin`/`end` for every file visited.
    #[arg(
        long,
        help = "Suppress begin/end events for files with no matches (cleaner output for empty searches)",
        action = clap::ArgAction::SetTrue
    )]
    pub no_begin_end: bool,

    /// Use PCRE2 regex engine for lookahead/lookbehind support.
    /// Requires the `pcre2` feature flag at build time.
    #[arg(
        short = 'P',
        long,
        help = "Use PCRE2 regex engine (requires pcre2 feature)",
        action = clap::ArgAction::SetTrue
    )]
    pub pcre2: bool,

    /// Search target: content (default), files (basename), or both.
    #[arg(
        long,
        value_enum,
        default_value_t = SearchTarget::Content,
        help = "Search target: content, files (basename), or both"
    )]
    pub target: SearchTarget,

    /// Skip first N matches when paginating results.
    #[arg(long, default_value_t = 0, help = "Skip first N matches (pagination offset)")]
    pub offset: u64,

    /// Limit number of match events emitted.
    #[arg(long, help = "Limit number of match events emitted")]
    pub limit: Option<u64>,

    /// Output format: `ndjson` (default) or `text`.
    ///
    /// `text` prints one line per match as `LINE:TEXT`, or `PATH:LINE:TEXT`
    /// when more than one file can match, with `PATH` relative to
    /// `--workspace` and `/` as the separator on every OS. `--context N`
    /// prints context lines with `-` instead of `:` and separates groups with
    /// `--`, the way `rg` does. Nothing else reaches stdout: no `begin`, `end`,
    /// `summary` or colour. `--count` prints `N`, or `PATH:N` for several
    /// files, and `--files` prints `PATH`, matching `rg -c` and `rg -l`.
    /// `--multiline`, `--target files` and `--target both` are rejected,
    /// because a match there is not a single line of a single file.
    /// `--max-columns` does not apply: `rg` returns the whole line, and a
    /// truncated line could not be compared against it byte for byte.
    #[arg(
        long,
        value_enum,
        default_value_t = SearchFormat::Ndjson,
        help = "Output format: ndjson (default) or text"
    )]
    pub format: SearchFormat,
}

/// Arguments for the extract subcommand.
#[derive(Args, Debug)]
pub struct ExtractArgs {
    /// Field names or indices to extract (positional).
    pub fields: Vec<String>,

    /// Local field list: the input columns to extract.
    ///
    /// Use either positional fields or `--field`, not both (exit 65).
    ///
    /// This is not the global `--select` / `--fields`: those project the whole
    /// NDJSON envelope after the extraction, while this flag chooses the input
    /// columns before anything is emitted.
    #[arg(
        long = "field",
        value_name = "FIELD",
        num_args = 1,
        action = clap::ArgAction::Append,
        help = "Local field to extract (repeatable; not global --select; do not combine with positionals)"
    )]
    pub field: Vec<String>,

    /// Delimiter for text mode (default: whitespace).
    #[arg(
        short = 'd',
        long,
        help = "Delimiter for text mode (default: whitespace)"
    )]
    pub delimiter: Option<String>,

    /// Read input from stdin.
    #[arg(long, help = "Read input from stdin", action = clap::ArgAction::SetTrue)]
    pub stdin: bool,
}

/// Arguments for the calc subcommand.
#[derive(Args, Debug)]
pub struct CalcArgs {
    /// Math expression to evaluate.
    ///
    /// The attribute stays: a stolen flag lands here as an expression that
    /// fails to evaluate, so the run exits 65 instead of answering with a
    /// plausible number for something else.
    #[arg(allow_hyphen_values = true)]
    pub expression: Option<String>,

    /// Read expressions from stdin.
    #[arg(long, help = "Read expressions from stdin (one per line)", action = clap::ArgAction::SetTrue)]
    pub stdin: bool,
}

/// Arguments for the transform subcommand.
#[derive(Args, Debug)]
pub struct TransformArgs {
    /// Refuse the cross-device copy-then-rename fallback (exit 91).
    #[command(flatten)]
    pub strict: crate::cli_args::StrictAtomicArgs,

    /// Paths to search for transforms.
    ///
    /// A verb with side effects requires the subject in the invocation line,
    /// so the root list is mandatory: omitting it is a usage error and never
    /// rewrites a tree chosen from ambient state. Name the root explicitly
    /// (e.g. `.` or `src/`). The multi-rule mode (`--rules` /
    /// `--inline-rules`) needs a root for the same reason.
    #[arg(required = true, value_hint = ValueHint::AnyPath)]
    pub paths: Vec<PathBuf>,

    /// AST pattern to match.
    #[arg(
        short = 'p',
        long,
        allow_hyphen_values = true,
        help = "AST pattern to match (single-rule mode)"
    )]
    pub pattern: Option<String>,

    /// Rewrite template for matched patterns.
    #[arg(
        short = 'r',
        long,
        allow_hyphen_values = true,
        help = "Rewrite template (single-rule mode)"
    )]
    pub rewrite: Option<String>,

    /// Source language for AST parsing.
    #[arg(
        short = 'l',
        long = "language",
        help = "Language (rust, js, ts, py, go, etc.) — required for single-rule mode"
    )]
    pub language: Option<String>,

    /// Glob patterns for file inclusion.
    #[arg(short = 'g', long, action = clap::ArgAction::Append, help = "Include files matching glob")]
    pub include: Vec<String>,

    /// Glob patterns for file exclusion.
    #[arg(long, action = clap::ArgAction::Append, help = "Exclude files matching glob")]
    pub exclude: Vec<String>,

    /// Preview without writing.
    #[arg(long, help = "Show diff preview without writing", action = clap::ArgAction::SetTrue)]
    pub dry_run: bool,

    /// Path to a YAML file containing multiple rules.
    #[arg(
        long,
        conflicts_with = "inline_rules",
        help = "Apply multiple rules from a YAML file (conflicts with --inline-rules)",
        value_hint = ValueHint::FilePath
    )]
    pub rules: Option<PathBuf>,

    /// Inline YAML rules (alternative to --rules).
    #[arg(
        long,
        allow_hyphen_values = true,
        conflicts_with = "rules",
        help = "Apply multiple rules from inline YAML string (conflicts with --rules)"
    )]
    pub inline_rules: Option<String>,

    /// Shared backup flags.
    #[command(flatten)]
    pub backup_opts: BackupOpts,


    /// Re-parse output with tree-sitter to detect syntax errors introduced by the rewrite.
    #[arg(
        long,
        help = "Re-parse output with tree-sitter to verify syntax (exit 88 on error)",
        action = clap::ArgAction::SetTrue
    )]
    pub verify_parse: bool,
}

/// Arguments for the `prune-backups` subcommand.
///
/// At least one of `--max-age-secs` / `--max-count` is required (clap group).
#[derive(Args, Debug)]
#[command(group(
    clap::ArgGroup::new("prune_policy")
        .required(true)
        .args(["max_age_secs", "max_count"])
))]
pub struct PruneBackupsArgs {
    /// Target file paths whose `.bak.YYYYMMDD_HHMMSS` siblings will be
    /// considered for pruning.
    #[arg(required = true, value_hint = ValueHint::AnyPath)]
    pub paths: Vec<PathBuf>,

    /// Maximum age (in seconds) of backups that survive. Backups whose
    /// mtime is strictly older than `now - max_age_secs` are pruned.
    /// When both `--max-age-secs` and `--max-count` are passed, age is
    /// applied first and count is applied to the survivors.
    #[arg(
        long,
        value_name = "SECONDS",
        help = "Drop backups older than N seconds"
    )]
    pub max_age_secs: Option<u32>,

    /// Maximum number of backups to keep (most recent by mtime).
    #[arg(long, value_name = "N", help = "Keep at most N most-recent backups")]
    pub max_count: Option<u8>,

    /// Preview without deleting anything.
    #[arg(long, help = "Show what would be pruned without writing", action = clap::ArgAction::SetTrue)]
    pub dry_run: bool,
}

/// Arguments for the apply subcommand.
#[derive(Args, Debug)]
pub struct ApplyArgs {
    /// Refuse the cross-device copy-then-rename fallback (exit 91).
    #[command(flatten)]
    pub strict: crate::cli_args::StrictAtomicArgs,

    #[arg(value_hint = ValueHint::FilePath)]
    /// Target file to apply the patch to.
    pub file: PathBuf,

    /// Patch format (default: auto-detect).
    #[arg(long, value_enum, default_value_t = PatchFormat::Auto, help = "Patch format: auto (detect, refuse when no marker), unified, search-replace, full (whole-file replacement), markdown")]
    pub format: PatchFormat,

    /// Shared backup flags.
    #[command(flatten)]
    pub backup_opts: BackupOpts,

    /// Preview without writing.
    #[arg(long, help = "Show what would be done without writing", action = clap::ArgAction::SetTrue)]
    pub dry_run: bool,

    /// Accept replacing a file above XDG `[write].confirm_large_bytes`.
    #[arg(long, help = "Acknowledge replacing a target above XDG [write].confirm_large_bytes", action = clap::ArgAction::SetTrue)]
    pub ack_overwrite: bool,

    /// Accept a result smaller than XDG `[write].shrink_block_percent`.
    #[arg(long, help = "Allow a result smaller than XDG [write].shrink_block_percent", action = clap::ArgAction::SetTrue)]
    pub allow_shrink: bool,
}

/// Identifier case style for the `case` subcommand.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum IdentifierCase {
    /// `snake_case`
    Snake,
    /// `camelCase`
    Camel,
    /// `PascalCase`
    Pascal,
    /// `kebab-case`
    Kebab,
    /// `SCREAMING_SNAKE_CASE`
    ScreamingSnake,
}

/// Arguments for the `query` subcommand.
///
/// Runs a tree-sitter S-expression pattern against a source file and
/// returns all matching AST nodes as NDJSON. Uses
/// `tree-sitter-language-pack` (downloads parsers on first use; 305
/// languages supported). Without `--query`, prints the parsed tree
/// structure as a compact JSON dump for debugging.
#[derive(Args, Debug)]
pub struct QueryArgs {
    #[arg(value_hint = ValueHint::FilePath)]
    /// Source file to query.
    pub path: PathBuf,
    /// Tree-sitter language override (e.g. "rust", "python"). Auto-detected
    /// from extension if omitted.
    #[arg(
        long,
        value_name = "LANG",
        help = "Language override (auto-detected from extension)"
    )]
    pub language: Option<String>,
    /// Tree-sitter S-expression pattern (e.g. `(function_item name: (identifier) @name)`).
    ///
    /// Use `-Q` or `--query`: the global `-q` is quiet verbosity.
    #[arg(
        short = 'Q',
        long,
        allow_hyphen_values = true,
        value_name = "PATTERN",
        help = "S-expression pattern (-Q/--query; global -q is quiet, not query)"
    )]
    pub query: Option<String>,
    /// Print the full parse tree (no S-expression matching).
    #[arg(long, help = "Print the full tree (no S-expression matching)", action = clap::ArgAction::SetTrue)]
    pub tree: bool,
    /// Print all named node kinds found in the file (no S-expression matching).
    #[arg(long, help = "Print all named node kinds in the file (counts)", action = clap::ArgAction::SetTrue)]
    pub kinds: bool,
    /// Show byte offsets and start positions for every match.
    #[arg(
        long,
        help = "Include byte offsets and start positions for every match",
        action = clap::ArgAction::SetTrue
    )]
    pub positions: bool,
}