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
//! Command-line interface definitions.
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use crate::path_source::Target;
#[derive(Debug, Parser)]
#[command(name = "pathlint", version, about = "Lint PATH against [[expect]] rules", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub global: GlobalOpts,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Lint PATH against expectations (default).
Check(CheckArgs),
/// Write a starter `pathlint.toml` in the current directory.
Init(InitArgs),
/// Inspect the source catalog.
Catalog {
#[command(subcommand)]
action: CatalogCommand,
},
/// Lint the PATH itself (duplicates, missing dirs, env-var
/// shortening candidates, Windows 8.3 short names, malformed
/// entries). Independent of `[[expect]]` rules.
Doctor(DoctorArgs),
/// Show where a command resolves from, which sources it matches,
/// and the most plausible uninstall command. Renamed from `where`
/// in 0.0.14; `pathlint where` keeps working as a visible alias
/// throughout the 0.0.x line.
#[command(visible_alias = "where")]
Trace(TraceArgs),
/// Propose a PATH order that satisfies every applicable
/// `[[expect]]` rule. Read-only by design — pathlint never
/// rewrites PATH, just prints the diff (default) or JSON.
Sort(SortArgs),
}
#[derive(Debug, clap::Args)]
pub struct SortArgs {
/// Print the proposal without touching PATH. This is the only
/// mode `sort` ships today; the flag is opt-in so callers
/// signal awareness that pathlint never mutates PATH and so
/// that adding `--apply` later (post-1.0) is a non-breaking
/// change. As of 0.0.14, `pathlint sort` without `--dry-run`
/// exits 2 with an explanation; a future `--apply` would
/// override this.
#[arg(long, default_value_t = false)]
pub dry_run: bool,
/// Emit the proposal as a JSON object (`SortPlan`) instead of
/// the default before / after diff. Schema is stable through
/// 0.0.x; notes carry a `kind` discriminator so consumers can
/// pattern-match on them.
#[arg(long)]
pub json: bool,
}
#[derive(Debug, clap::Args, Default)]
pub struct CheckArgs {
/// Expand each NG outcome into a multi-line breakdown — resolved
/// path, matched sources, prefer / avoid lists, the underlying
/// diagnosis, and a follow-up hint. Use this when the one-line
/// detail is not enough to figure out why a rule failed.
#[arg(long, conflicts_with = "json")]
pub explain: bool,
/// Emit one JSON array describing every expectation: status,
/// resolved path, matched sources, prefer / avoid, and a
/// `diagnosis` object on failures. Schema is stable through
/// 0.0.x; the diagnosis uses a `kind` discriminator so consumers
/// can match on it. Mutually exclusive with --explain.
#[arg(long)]
pub json: bool,
}
#[derive(Debug, clap::Args)]
pub struct TraceArgs {
/// The command to look up on PATH.
pub command: String,
/// Emit machine-readable JSON instead of the default human
/// output. The schema is described in the README; provenance
/// and uninstall objects use a `kind` discriminator so consumers
/// can match on it.
#[arg(long)]
pub json: bool,
}
#[derive(Debug, clap::Args)]
pub struct DoctorArgs {
/// Only show diagnostics whose kind matches one of the listed
/// values. Mutually exclusive with `--exclude`. Accepts a comma
/// or repeated flag form: `--include duplicate,missing` or
/// `--include duplicate --include missing`.
#[arg(long, value_delimiter = ',', conflicts_with = "exclude")]
pub include: Vec<String>,
/// Suppress diagnostics whose kind matches one of the listed
/// values. Affects exit code too: an excluded `Malformed` no
/// longer escalates to exit 1.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
/// Emit the (already-filtered) diagnostics as a JSON array —
/// machine-readable counterpart of the human view. Each element
/// has `index`, `entry`, `severity`, `kind`, plus any per-kind
/// payload fields (`suggestion`, `canonical`, `first_index`,
/// `reason`, or `diagnostic` + `groups` for the `conflict`
/// kind). Schema is stable through 0.0.x, parallels
/// `check --json`. The include / exclude filters still apply;
/// `--quiet` is ignored in JSON mode (the output is intended
/// to be complete).
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Subcommand)]
pub enum CatalogCommand {
/// List every known source — built-in plus any defined in
/// `pathlint.toml` — with its description and per-OS path.
List(CatalogListArgs),
/// List every declared `[[relation]]` between sources, both
/// built-in (from `plugins/*.toml`) and user-defined (from
/// `pathlint.toml`). Useful for understanding why a doctor
/// diagnostic fires or how `pathlint where` infers provenance.
Relations(CatalogRelationsArgs),
}
#[derive(Debug, clap::Args)]
pub struct CatalogListArgs {
/// Show every per-OS path of each source, not just the one for
/// the running OS.
#[arg(long)]
pub all: bool,
/// Print only source names, one per line.
#[arg(long)]
pub names_only: bool,
}
#[derive(Debug, clap::Args)]
pub struct CatalogRelationsArgs {
/// Emit the relations as a JSON array, with each element
/// carrying its `kind` discriminator. Schema is stable through
/// 0.0.x.
#[arg(long)]
pub json: bool,
}
#[derive(Debug, clap::Args)]
pub struct InitArgs {
/// Also embed the entire built-in source catalog so users can
/// edit per-OS paths field by field. Off by default to keep the
/// starter file short.
#[arg(long)]
pub emit_defaults: bool,
/// Overwrite an existing `pathlint.toml` if one is already present.
#[arg(long)]
pub force: bool,
}
#[derive(Debug, clap::Args)]
pub struct GlobalOpts {
/// PATH source: process (default) / user / machine. user / machine
/// are Windows-only.
#[arg(long, value_enum, default_value_t = TargetArg::Process)]
pub target: TargetArg,
/// Path to pathlint.toml. Default search: ./pathlint.toml then
/// $XDG_CONFIG_HOME/pathlint/pathlint.toml. Renamed from
/// `--rules` in 0.0.14; the old `--rules` spelling stays as an
/// alias throughout the 0.0.x line.
#[arg(long = "config", visible_alias = "rules")]
pub config: Option<PathBuf>,
/// Print every expectation incl. n/a, plus the resolved PATH.
#[arg(short, long)]
pub verbose: bool,
/// Only print failures.
#[arg(short, long)]
pub quiet: bool,
/// Color output.
#[arg(long, value_enum, default_value_t = ColorArg::Auto)]
pub color: ColorArg,
/// ASCII-only output.
#[arg(long)]
pub no_glyphs: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum TargetArg {
Process,
User,
Machine,
}
impl From<TargetArg> for Target {
fn from(t: TargetArg) -> Self {
match t {
TargetArg::Process => Target::Process,
TargetArg::User => Target::User,
TargetArg::Machine => Target::Machine,
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ColorArg {
Auto,
Always,
Never,
}