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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use clap_complete::{generate, Shell};
use std::io;
use std::str::FromStr;
#[derive(Parser)]
#[command(name = "rsconstruct")]
#[command(version = concat!(env!("CARGO_PKG_VERSION")))]
#[command(about = "Rust Build Tool - Incremental build system with templates", long_about = None)]
pub struct Cli {
/// Show skip/restore/cache messages during build
#[arg(short, long, global = true)]
pub verbose: bool,
/// What to show for output files (none, basename, path)
#[arg(short = 'O', long, global = true, value_enum, default_value = "none")]
pub output_display: OutputDisplay,
/// What to show for input files (none, source, all)
#[arg(short = 'I', long, global = true, value_enum, default_value = "source")]
pub input_display: InputDisplay,
/// Path format for displayed files (basename, path)
#[arg(short = 'P', long, global = true, value_enum, default_value = "path")]
pub path_format: PathFormat,
/// Print each child process command before it is executed
#[arg(long, global = true)]
pub show_child_processes: bool,
/// Show tool output even on success (default: only show on failure)
#[arg(long, global = true)]
pub show_output: bool,
/// Output in JSON Lines format (machine-readable)
#[arg(long, global = true)]
pub json: bool,
/// Suppress all output except errors (useful for CI)
#[arg(short, long, global = true)]
pub quiet: bool,
/// Show build phase messages (discover, add_dependencies, etc.)
#[arg(long, global = true)]
pub phases: bool,
#[command(subcommand)]
pub command: Commands,
}
impl Cli {
/// Get the display options from CLI arguments
pub fn display_options(&self) -> DisplayOptions {
DisplayOptions {
output: self.output_display,
input: self.input_display,
path_format: self.path_format,
}
}
}
/// Output format for the dependency graph
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum GraphFormat {
/// DOT format (Graphviz)
Dot,
/// Mermaid diagram format (Markdown-friendly)
Mermaid,
/// JSON format (machine-readable)
Json,
/// Plain text hierarchical view
Text,
/// SVG format (requires Graphviz dot)
#[default]
Svg,
}
/// Viewer for opening the graph
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum GraphViewer {
/// Open as HTML with Mermaid in browser (no dependencies)
Mermaid,
/// Generate and open SVG using Graphviz dot
#[default]
Svg,
}
/// Build phases that can be stopped after
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum BuildPhase {
/// Stop after discovering products (before dependency scanning)
Discover,
/// Stop after adding dependencies (before resolving graph)
AddDependencies,
/// Stop after resolving the dependency graph (before execution)
Resolve,
/// Stop after classifying products (show skip/restore/build counts)
Classify,
/// Run the full build (default)
#[default]
Build,
}
/// What to show for output files in build messages
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum OutputDisplay {
/// Don't show output files
#[default]
None,
/// Show only the filename (e.g., "main.elf")
Basename,
/// Show full relative path (e.g., "out/cc_single_file/main.elf")
Path,
}
/// What to show for input files in build messages
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum InputDisplay {
/// Don't show input files
None,
/// Show only the primary source file (first input)
#[default]
Source,
/// Show all input files including headers/dependencies
All,
}
/// Path format for displayed files
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum PathFormat {
/// Show only the filename (e.g., "main.c")
Basename,
/// Show full relative path (e.g., "src/main.c")
#[default]
Path,
}
/// Display options for product output in build messages
#[derive(Debug, Clone, Copy)]
pub struct DisplayOptions {
pub output: OutputDisplay,
pub input: InputDisplay,
pub path_format: PathFormat,
}
impl Default for DisplayOptions {
fn default() -> Self {
Self {
output: OutputDisplay::None,
input: InputDisplay::Source,
path_format: PathFormat::Path,
}
}
}
impl DisplayOptions {
/// Minimal display: just input source basename
pub fn minimal() -> Self {
Self {
output: OutputDisplay::None,
input: InputDisplay::Source,
path_format: PathFormat::Basename,
}
}
}
#[derive(Subcommand)]
pub enum Commands {
/// Execute an incremental build
Build {
/// Force rebuild even if files haven't changed
#[arg(short, long)]
force: bool,
/// Show what would be built without executing anything
#[arg(short = 'n', long)]
dry_run: bool,
/// Verify tool versions against .tools.versions before building
#[arg(long)]
verify_tool_versions: bool,
/// Stop after a specific build phase
#[arg(long, value_enum, default_value = "build")]
stop_after: BuildPhase,
#[command(flatten)]
shared: SharedBuildArgs,
},
/// Manage the build cache
Cache {
#[command(subcommand)]
action: CacheAction,
},
/// Clean build artifacts
Clean {
#[command(subcommand)]
action: Option<CleanAction>,
},
/// Generate shell completion scripts
Complete {
/// The shells to generate completions for (if none specified, uses config file)
#[arg(value_enum)]
shells: Vec<Shell>,
},
/// Show or inspect configuration
Config {
#[command(subcommand)]
action: ConfigAction,
},
/// Show source file dependencies (e.g., header files for C/C++)
Deps {
#[command(subcommand)]
action: DepsAction,
},
/// Check build environment: tool availability, config validity, common problems
Doctor,
/// Display the build dependency graph
Graph {
#[command(subcommand)]
action: GraphAction,
},
/// Show project information
Info {
#[command(subcommand)]
action: InfoAction,
},
/// Initialize a new rsconstruct project in the current directory
Init,
/// Manage processors
Processors {
#[command(subcommand)]
action: ProcessorAction,
},
/// Count source lines of code (SLOC) by language
Sloc {
/// Show COCOMO effort/cost estimation
#[arg(long)]
cocomo: bool,
/// Annual salary for COCOMO cost estimation (default: 56286)
#[arg(long, default_value = "56286")]
salary: u64,
},
/// Smart config manipulation commands
Smart {
#[command(subcommand)]
action: SmartAction,
},
/// Show the status of each product (up-to-date, stale, or restorable)
Status {
/// Show source file counts by extension per processor
#[arg(long)]
breakdown: bool,
},
/// Manage term checking and fixing in markdown files
Terms {
#[command(subcommand)]
action: TermsAction,
},
/// Search and query frontmatter tags from markdown files
Tags {
#[command(subcommand)]
action: TagsAction,
},
/// Manage external tool dependencies
Tools {
#[command(subcommand)]
action: ToolsAction,
},
/// Print version information
Version,
/// Watch source files and auto-rebuild on changes
Watch {
#[command(flatten)]
shared: SharedBuildArgs,
},
}
#[derive(Subcommand)]
pub enum SmartAction {
/// Disable all processors in rsconstruct.toml (so you can enable them one by one)
DisableAll,
/// Enable all processors in rsconstruct.toml (remove enabled = false from all)
EnableAll,
/// Enable only processors whose files are detected in the project
EnableDetected,
/// Disable a single processor in rsconstruct.toml
Disable {
/// Processor name
name: String,
},
/// Enable a single processor in rsconstruct.toml
Enable {
/// Processor name
name: String,
},
/// Disable all, then enable only detected processors (clean minimal config)
Minimal,
/// Remove all [processor.*] sections, returning to pure defaults
Reset,
/// Auto-detect relevant processors and add them to rsconstruct.toml
Auto,
/// Enable only processors whose files are detected and tools are installed
EnableIfAvailable,
/// Disable all, then enable only the listed processors
Only {
/// Processor names to enable
#[arg(required = true)]
names: Vec<String>,
},
}
#[derive(Subcommand)]
pub enum InfoAction {
/// Show source file counts by extension
Source,
}
#[derive(Subcommand)]
pub enum GraphAction {
/// Print the dependency graph to stdout
Show {
/// Output format
#[arg(short, long, value_enum, default_value = "svg")]
format: GraphFormat,
},
/// Open the dependency graph in a viewer
View {
/// Viewer to use
#[arg(long, value_enum, default_value = "svg")]
viewer: GraphViewer,
},
/// Show graph statistics (products, processors, dependencies)
Stats,
}
#[derive(Subcommand)]
pub enum CleanAction {
/// Remove build output files (preserves cache) [default]
Outputs,
/// Remove all build outputs and cache directories (.rsconstruct/ and out/)
All,
/// Hard clean using git clean (requires git repository)
Git,
/// Remove files not tracked by git and not known as RSConstruct build outputs
Unknown {
/// Show what would be removed without actually deleting
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand)]
pub enum CacheAction {
/// Clear the entire cache
Clear,
/// Show cache size
Size,
/// Remove unreferenced objects from cache
Trim,
/// Remove stale index entries not matching any current product
RemoveStale,
/// List all cache entries and their status
List,
/// Show which cache entries are stale vs current
Stale,
/// Show per-processor cache statistics
Stats,
}
#[derive(Subcommand)]
pub enum ConfigAction {
/// Show the active configuration (defaults merged with rsconstruct.toml overrides)
Show,
/// Show the default configuration (without rsconstruct.toml overrides)
ShowDefault,
/// Validate the configuration for errors and warnings
Validate,
}
#[derive(Subcommand)]
pub enum ProcessorAction {
/// List available processors with status and descriptions
List {
/// Show all built-in processors, not just those enabled in the project
#[arg(short, long)]
all: bool,
},
/// Show source and target files for each processor
Files {
/// Processor name (omit to show all enabled processors)
name: Option<String>,
},
/// Show resolved configuration for a processor
Config {
/// Processor name (omit to show all enabled processors)
name: Option<String>,
/// Show only fields that differ from the default configuration
#[arg(short, long)]
diff: bool,
},
/// Show default configuration for a processor
Defconfig {
/// Processor name
name: String,
},
/// Show the current processor allowlist (for use in rsconstruct.toml [processor] enabled)
Allowlist,
/// Show inter-processor dependencies
Graph {
/// Output format
#[arg(short, long, value_enum, default_value = "text")]
format: GraphFormat,
},
}
#[derive(Subcommand)]
pub enum ToolsAction {
/// List all required external tools
List {
/// Include tools from disabled processors too
#[arg(short, long)]
all: bool,
/// Show all available installation methods for each tool
#[arg(short = 'M', long)]
methods: bool,
},
/// Verify tool versions against .tools.versions lock file
Check,
/// Lock tool versions to .tools.versions (creates or updates the lock file)
Lock,
/// Install missing external tools (all missing, or a specific tool by name)
Install {
/// Tool name to install (omit to install missing tools for detected processors)
name: Option<String>,
/// Skip confirmation prompt
#[arg(short, long)]
yes: bool,
/// Install tools for all enabled processors, not just detected ones
#[arg(short, long)]
all: bool,
},
/// Show tool availability statistics and language runtime breakdown
Stats,
/// Show tool-to-processor dependency graph
Graph {
/// Output format
#[arg(short, long, value_enum, default_value = "dot")]
format: GraphFormat,
/// Open the graph in a browser instead of printing to stdout
#[arg(long)]
view: bool,
},
}
#[derive(Subcommand)]
pub enum DepsAction {
/// List all available dependency analyzers
List,
/// Run dependency analysis without building (scan headers, imports, etc.)
Build,
/// Show analyzer configuration
Config {
/// Analyzer name (e.g., "cpp", "python"); omit to show all
name: Option<String>,
},
/// Show cached dependencies
Show {
#[command(subcommand)]
filter: DepsShowFilter,
},
/// Show statistics about cached dependencies by analyzer
Stats,
/// Clear the dependency cache (all analyzers, or specific one)
Clean {
/// Only clear entries from this analyzer (e.g., "cpp", "python")
#[arg(long)]
analyzer: Option<String>,
},
}
#[derive(Subcommand)]
pub enum DepsShowFilter {
/// Show dependencies for all source files
All,
/// Show dependencies for specific files
Files {
/// Source files to show dependencies for
#[arg(required = true)]
files: Vec<String>,
},
/// Show dependencies for files handled by specific analyzers
Analyzers {
/// Analyzer names (e.g., "cpp", "python")
#[arg(required = true)]
analyzers: Vec<String>,
},
}
#[derive(Subcommand)]
pub enum TermsAction {
/// Auto-fix: add backticks to terms (optionally remove backticks from non-terms)
Fix {
/// Also remove backticks from non-terms
#[arg(long, default_value_t = false)]
remove_non_terms: bool,
},
/// Merge terms from another project's terms directory into the current one
Merge {
/// Path to the other project's terms directory
path: String,
},
/// Show term file and term count statistics
Stats,
}
#[derive(Subcommand)]
pub enum TagsAction {
/// List files matching given tags (AND by default, --or for OR)
Files {
/// Tags: bare values (e.g. "docker") or key:value (e.g. "level:advanced")
#[arg(required = true)]
tags: Vec<String>,
/// Use OR semantics (match files with any of the given tags)
#[arg(long, short)]
or: bool,
},
/// Search for tags containing a substring
Grep {
/// Text to search for in tag names
text: String,
/// Case-insensitive search
#[arg(short, long)]
ignore_case: bool,
},
/// List all unique tags
List,
/// Show each tag with its file count, sorted by frequency
Count,
/// Show tags grouped by prefix/category
Tree,
/// Show statistics about the tags database
Stats,
/// List all tags for a specific file
ForFile {
/// Path to the file
path: String,
},
/// Show the raw frontmatter for a specific file
Frontmatter {
/// Path to the file
path: String,
},
/// List tags in the allowlist (tags_dir) that are not used by any file
Unused {
/// Exit with error if unused tags are found (useful for CI)
#[arg(long)]
strict: bool,
},
/// Validate tags against the allowlist (tags_dir) without building
Validate,
/// Show a coverage matrix of tag categories per file
Matrix,
/// Show percentage of files that have each tag category
Coverage,
/// Find markdown files with no tags at all
Orphans,
/// Run all tag validations without building (lint pass)
Check,
/// Suggest tags for a file based on similarity to other tagged files
Suggest {
/// Path to the file
path: String,
},
/// Merge tags from another project's tags directory into the current one
Merge {
/// Path to the other project's tags directory
path: String,
},
}
/// CLI arguments shared between Build and Watch commands.
#[derive(Args, Clone)]
pub struct SharedBuildArgs {
/// Number of parallel jobs (overrides config file)
#[arg(short, long)]
pub jobs: Option<usize>,
/// Show per-product and total build timing information
#[arg(long)]
pub timings: bool,
/// Continue building after errors, skipping dependents of failed products
#[arg(short = 'k', long)]
pub keep_going: bool,
/// Suppress the build summary
#[arg(long)]
pub no_summary: bool,
/// Batch size for batch-capable processors (0 = no limit, -1 = disable, omit to use config)
#[arg(long, allow_negative_numbers = true)]
pub batch_size: Option<i32>,
/// Only run specific processors (comma-separated list)
#[arg(short, long, value_delimiter = ',')]
pub processors: Option<Vec<String>>,
/// Automatically add misspelled words to words files instead of failing (zspell + aspell)
#[arg(long)]
pub auto_add_words: bool,
/// Show why each product is skipped, restored, or rebuilt
#[arg(long)]
pub explain: bool,
/// Retry failed products up to N times to detect flakiness
#[arg(long, value_name = "N", default_value = "0")]
pub retry: usize,
/// Disable mtime pre-check (always compute full checksums)
#[arg(long)]
pub no_mtime: bool,
/// Only build products matching these file patterns (glob syntax, repeatable)
#[arg(short, long = "target")]
pub targets: Option<Vec<String>>,
/// Only build products whose inputs are under these directories (repeatable)
#[arg(short, long = "dir")]
pub dirs: Option<Vec<String>>,
/// Write a Chrome trace JSON file for build visualization (open in chrome://tracing or Perfetto)
#[arg(long, value_name = "FILE")]
pub trace: Option<String>,
}
impl SharedBuildArgs {
/// Convert to BuildOptions with the given overrides for build-only fields.
pub fn to_build_options(&self, cli: &Cli, force: bool, stop_after: BuildPhase) -> BuildOptions {
// Merge --dir values into targets as glob patterns
let targets = match (&self.targets, &self.dirs) {
(None, None) => None,
(Some(t), None) => Some(t.clone()),
(None, Some(d)) => Some(d.iter().map(|dir| format!("{dir}/**")).collect()),
(Some(t), Some(d)) => {
let mut merged = t.clone();
merged.extend(d.iter().map(|dir| format!("{dir}/**")));
Some(merged)
}
};
BuildOptions {
force,
verbose: cli.verbose,
display_opts: cli.display_options(),
jobs: self.jobs,
timings: self.timings,
keep_going: self.keep_going,
summary: !self.no_summary,
batch_size: self.batch_size.map(|n| if n < 0 { None } else { Some(n as usize) }),
stop_after,
processor_filter: self.processors.clone(),
auto_add_words: self.auto_add_words,
explain: self.explain,
no_mtime: self.no_mtime,
retry: self.retry,
targets,
trace: self.trace.clone(),
}
}
}
/// Options shared by build and watch commands.
#[derive(Clone)]
pub struct BuildOptions {
pub force: bool,
pub verbose: bool,
pub display_opts: DisplayOptions,
pub jobs: Option<usize>,
pub timings: bool,
pub keep_going: bool,
pub summary: bool,
pub batch_size: Option<Option<usize>>,
pub stop_after: BuildPhase,
pub processor_filter: Option<Vec<String>>,
pub auto_add_words: bool,
pub explain: bool,
pub no_mtime: bool,
pub retry: usize,
pub targets: Option<Vec<String>>,
pub trace: Option<String>,
}
/// Parse a shell name string into a Shell enum
pub fn parse_shell(name: &str) -> Option<Shell> {
<Shell as FromStr>::from_str(name).ok()
}
/// Recursively set `hide_short_help = true` on all arguments in a command and its subcommands.
fn hide_all_flags(cmd: clap::Command) -> clap::Command {
let cmd = cmd.mut_args(|arg| {
if arg.get_long().is_some() || arg.get_short().is_some() {
arg.hide_short_help(true)
} else {
arg
}
});
cmd.mut_subcommands(hide_all_flags)
}
/// Parse CLI arguments with all flags hidden from short help (`-h`).
/// Use `--help` to see all flags.
pub fn parse_cli() -> Cli {
let cmd = hide_all_flags(Cli::command());
let matches = cmd.get_matches();
Cli::from_arg_matches(&matches).expect("failed to parse CLI arguments")
}
/// Generate shell completions and print to stdout
pub fn print_completions(shell: Shell) {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "rsconstruct", &mut io::stdout());
}