atomwrite 0.1.36

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
Documentation
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
// SPDX-License-Identifier: MIT OR Apache-2.0
// Included by part1.inc.rs (A-MONO-001 — after EditLoopArgs).

/// Arguments for the search subcommand.
#[derive(Args, Debug)]
#[derive(Clone)]
pub struct SearchArgs {
    /// Search pattern (regex by default).
    #[arg(allow_hyphen_values = true)]
    pub pattern: String,

    /// Paths to search within.
    #[arg(default_value = ".", 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 (G56).
    ///
    /// By default, atomwrite skips FIFOs because `open()` on a FIFO blocks
    /// indefinitely until the other end connects — this can cause atomwrite
    /// to hang in automated local runs / Docker environments that have FIFOs in /tmp or /var.
    /// Pass `--include-fifo` to opt back into the legacy behavior of
    /// opening FIFOs (which 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` (G68).
    ///
    /// Files larger than this are skipped silently (with a `skipped` event
    /// when `--count` or `--files` is active). Default: 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 (default: constants::DEFAULT_SEARCH_MAX_FILESIZE_BYTES)"
    )]
    pub max_filesize: u64,

    /// Maximum line length in columns for `search` matches (G68).
    ///
    /// Lines longer than this are truncated with a `...[truncated]` marker.
    /// Default: 500. Useful for skipping 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 (default: constants::DEFAULT_SEARCH_MAX_COLUMNS)"
    )]
    pub max_columns: usize,

    /// Search binary files (A-008). Default: skip files that look binary
    /// (`NUL` / `content_inspector`), 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 (GAP-2026-010). Default: emit `begin`/`end` for every
    /// file visited (back-compat).
    #[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 (v0.1.30).
    #[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 (v0.1.30).
    #[arg(long, default_value_t = 0, help = "Skip first N matches (pagination offset)")]
    pub offset: u64,

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

/// 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 (agent-native; v0.1.36 residual).
    ///
    /// Prefer one of: positional fields **or** `--field`, not both (exit 65).
    ///
    /// **Not** global `--select` / `--fields`: those project the whole NDJSON
    /// envelope after extract. This flag chooses input columns before emit.
    /// (`--select` cannot be local — it collides with the global shape flag.)
    #[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.
    #[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 {
    /// Paths to search for transforms.
    #[arg(default_value = ".", 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 (G44).
    #[arg(long, help = "Apply multiple rules from a YAML file", value_hint = ValueHint::FilePath)]
    pub rules: Option<PathBuf>,

    /// Inline YAML rules (alternative to --rules).
    #[arg(
        long,
        allow_hyphen_values = true,
        help = "Apply multiple rules from inline YAML string"
    )]
    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 (ADR-0040).
///
/// G-036: 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 {
    #[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, unified, search-replace, full, 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,
}

/// Identifier case style (v14 Tier 3 `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 (v14 Tier 3, v0.1.12).
///
/// 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)`).
    ///
    /// **Must use `-Q` / `--query`** — global `-q` is quiet verbosity (A-005).
    #[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,
}