Skip to main content

nd_300/
cli.rs

1use clap::{Args, Parser, Subcommand, ValueEnum};
2
3use crate::speedtest::TestDuration;
4
5/// ND-300: Cross-platform network diagnostic tool
6#[derive(Parser)]
7#[command(
8    name = "nd300",
9    author,
10    version,
11    disable_version_flag = true,
12    about = "ND-300 Network Diagnostic - QubeTX Developer Tools",
13    long_about = "ND-300 Network Diagnostic - QubeTX Developer Tools\n\n\
14        Cross-platform network diagnostics with 25+ concurrent checks,\n\
15        multi-stage network repair, and DNS configuration management.",
16    after_long_help = "EXAMPLES:\n\
17        \x20 nd300              Run standard diagnostics\n\
18        \x20 nd300 -t           Technician mode (deep diagnostics)\n\
19        \x20 nd300 dns          Change DNS servers and verify connectivity\n\
20        \x20 nd300 -d           Same as 'nd300 dns' (legacy flag form)\n\
21        \x20 nd300 fix          Diagnostic-driven triage and recovery loop\n\
22        \x20 nd300 -f           Same as 'nd300 fix' (legacy flag form)\n\
23        \x20 nd300 update       Check for updates and install\n\
24        \x20 nd300 clear-dns    Reset DNS cache\n\
25        \x20 nd300 uninstall    Remove nd300 from this system\n\
26        \x20 nd300 --fast       Skip speed test for faster execution\n\
27        \x20 nd300 --json       Output results as JSON\n\n\
28        Run 'nd300 --help' for full details, or 'nd300 -h' for a summary."
29)]
30pub struct Nd300Cli {
31    /// Technician mode - full technical report with deep diagnostics
32    #[arg(
33        short = 't',
34        long = "tech",
35        alias = "technician",
36        help_heading = "Modes",
37        global = true
38    )]
39    pub tech: bool,
40
41    /// Custom title for the report header
42    #[arg(short = 'T', long, help_heading = "Modes", global = true)]
43    pub title: Option<String>,
44
45    /// Output results as JSON
46    #[arg(long, help_heading = "Output", global = true)]
47    pub json: bool,
48
49    /// Use ASCII characters instead of Unicode box-drawing
50    #[arg(long, help_heading = "Output", global = true)]
51    pub ascii: bool,
52
53    /// Disable colored output
54    #[arg(long, help_heading = "Output", global = true)]
55    pub no_color: bool,
56
57    /// Show additional debug/trace information
58    #[arg(long, help_heading = "Output", global = true)]
59    pub verbose: bool,
60
61    /// Skip the speed test (faster execution)
62    #[arg(long, help_heading = "Speed Test", global = true)]
63    pub fast: bool,
64
65    /// Speed test duration in seconds (per direction, per provider)
66    #[arg(long, default_value = "15", help_heading = "Speed Test", global = true)]
67    pub speed_duration: u64,
68
69    /// Change DNS servers and verify connectivity
70    #[arg(short = 'd', long = "dns", help_heading = "Actions")]
71    pub dns: bool,
72
73    /// Diagnostic-driven triage and recovery loop (legacy flag form of `nd300 fix`)
74    #[arg(short = 'f', long = "fix", help_heading = "Actions")]
75    pub fix: bool,
76
77    /// Clear DNS cache and exit (legacy flag form of `nd300 clear-dns`)
78    #[arg(short = 'c', long = "clear-dns", help_heading = "Actions")]
79    pub clear_dns: bool,
80
81    /// Uninstall nd300 from this system (legacy flag form of `nd300 uninstall`)
82    #[arg(long = "uninstall", help_heading = "Actions")]
83    pub uninstall: bool,
84
85    /// Check for updates and install the latest version (legacy flag form of `nd300 update`)
86    #[arg(long = "update", help_heading = "Actions")]
87    pub update: bool,
88
89    /// Auto-confirm Medium-cost prompts when running the fix flow. Does NOT
90    /// bypass High-risk action prompts (Y/N is always required for those).
91    #[arg(
92        short = 'y',
93        long = "yes",
94        alias = "non-interactive",
95        help_heading = "Actions",
96        global = true
97    )]
98    pub yes: bool,
99
100    /// Print version
101    #[arg(short = 'v', long = "version", action = clap::ArgAction::Version)]
102    pub version: (),
103
104    /// Action subcommand. If present, takes precedence over the legacy action flags.
105    #[command(subcommand)]
106    pub command: Option<Nd300Command>,
107}
108
109/// Action subcommands. These are equivalent to the long-running action flags
110/// (`-d`, `-f`, `--update`, `--clear-dns`, `--uninstall`) — both forms are
111/// supported and mean the same thing. The subcommand form is the preferred way
112/// going forward; the flag form is kept so existing scripts continue to work.
113#[derive(Subcommand, Debug, Clone)]
114pub enum Nd300Command {
115    /// Diagnostic-driven triage loop: tests → identifies failures → applies
116    /// targeted fixes → re-tests → repeats. Bounded by iteration count, wall
117    /// clock, and per-action attempt caps. Works for technical and non-technical
118    /// users; high-risk actions always require Y/N confirmation.
119    Fix(FixArgs),
120
121    /// Change DNS servers and verify connectivity. On success, falls through to
122    /// running standard diagnostics (identical to the legacy `-d`/`--dns` flag).
123    Dns,
124
125    /// Check for updates and install the latest release.
126    Update,
127
128    /// Clear the DNS cache and exit.
129    #[command(name = "clear-dns")]
130    ClearDns,
131
132    /// Uninstall nd300 from this system.
133    Uninstall,
134
135    /// Cross-method install cleanup (HIDDEN). Invoked by the Windows installers
136    /// (and the silent self-update path) to consolidate to a single install:
137    /// remove a shadowing older `cargo install` copy and/or the other Windows
138    /// edition. Safe to run anywhere — it never deletes the running install,
139    /// cargo/rustup, the `.cargo\bin` PATH entry, or anything outside the
140    /// nd300/speedqx allowlist, and it always exits 0 (cleanup is advisory).
141    #[command(name = "migrate-cleanup", hide = true)]
142    MigrateCleanup(MigrateArgs),
143
144    /// Remove registered Windows installer channels that conflict with a
145    /// deliberate fresh install. Hidden installer-only contract.
146    #[command(name = "install-takeover", hide = true)]
147    InstallTakeover(InstallTakeoverArgs),
148
149    /// Transaction-bound installer bookkeeping. Hidden installer-only contract.
150    #[command(name = "install-maintenance", hide = true)]
151    InstallMaintenance(InstallMaintenanceArgs),
152}
153
154/// Per-subcommand arguments for `fix`. Currently a placeholder — the `--yes`
155/// flag lives at the top level (global), so `nd300 fix --yes`,
156/// `nd300 --yes fix`, `nd300 -f -y`, and `nd300 -y -f` all work and share the
157/// same field on `Nd300Cli`. New fix-specific options can be added here later
158/// without affecting the legacy `-f` flag form.
159#[derive(Args, Debug, Clone, Default)]
160pub struct FixArgs {}
161
162/// Arguments for the hidden `migrate-cleanup` subcommand. With NO target flag,
163/// the command defaults to `--cargo-copy` only (the safest, never-needs-admin
164/// consolidation). All flags are tool-agnostic so this contract can be mirrored
165/// to TR-300 unchanged.
166#[derive(Args, Debug, Clone, Default)]
167pub struct MigrateArgs {
168    /// Remove a shadowing older `cargo install` / cargo-dist copy in `.cargo\bin`.
169    #[arg(long = "cargo-copy")]
170    pub cargo_copy: bool,
171
172    /// Remove the OTHER Windows edition (Global perMachine <-> Corporate perUser).
173    #[arg(long = "other-edition")]
174    pub other_edition: bool,
175
176    /// Suppress human output (installer invokes this so the wizard stays clean).
177    /// Ignored when `--json` is set.
178    #[arg(long = "quiet")]
179    pub quiet: bool,
180
181    /// Detect and report what WOULD be removed without deleting anything.
182    #[arg(long = "dry-run")]
183    pub dry_run: bool,
184
185    /// Emit a machine-readable JSON report instead of human text.
186    #[arg(long = "json")]
187    pub json: bool,
188
189    /// The invoking user's profile dir (e.g. `C:\Users\alice`). Used to resolve
190    /// that user's `.cargo\bin` and `%LocalAppData%` when this process runs as a
191    /// different user (a perMachine installer launched elevated / as SYSTEM).
192    #[arg(long = "user-profile", value_name = "PATH")]
193    pub user_profile: Option<String>,
194
195    /// The invoking user's CARGO_HOME (e.g. `C:\Users\alice\.cargo`). Takes
196    /// precedence over `--user-profile` for locating the cargo-bin directory.
197    #[arg(long = "cargo-home", value_name = "PATH")]
198    pub cargo_home: Option<String>,
199
200    /// Installer-declared origin for classifying custom install locations.
201    /// This is an internal installer contract, not a user-facing option.
202    #[arg(long = "install-origin", value_enum, hide = true)]
203    pub install_origin: Option<MigrateInstallOrigin>,
204
205    /// Remove versioned images retired while a running Windows binary was
206    /// upgraded in place. This is an internal installer contract.
207    #[arg(long = "retired-update", hide = true)]
208    pub retired_update: bool,
209}
210
211/// Valid installer origins accepted by the hidden migration helper.
212///
213/// Keep the Windows values in lockstep with the four Windows installers and
214/// `actions::update::InstallOrigin::json_id`; `macos-pkg` is reserved for the
215/// signed Apple Installer postinstall migration hook.
216#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
217pub enum MigrateInstallOrigin {
218    MsiGlobal,
219    MsiCorporate,
220    ExeGlobal,
221    ExeCorporate,
222    MacosPkg,
223}
224
225/// Arguments for the hidden pre-install channel-takeover helper.
226#[derive(Args, Debug, Clone)]
227pub struct InstallTakeoverArgs {
228    /// Channel that the fresh installer intends to make authoritative.
229    #[arg(long, value_enum, hide = true)]
230    pub target: InstallTakeoverTarget,
231
232    /// Registry scope visible to this installer phase.
233    #[arg(long, value_enum, default_value = "all", hide = true)]
234    pub scope: InstallTakeoverScope,
235}
236
237/// Fresh-install channels understood by the Windows takeover helper.
238#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
239pub enum InstallTakeoverTarget {
240    MsiGlobal,
241    MsiCorporate,
242    ExeGlobal,
243    ExeCorporate,
244    Standalone,
245}
246
247/// Registry scope processed by one takeover-helper invocation.
248#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
249pub enum InstallTakeoverScope {
250    All,
251    User,
252    Machine,
253}
254
255/// Arguments for hidden post-install/uninstall bookkeeping.
256#[derive(Args, Debug, Clone)]
257pub struct InstallMaintenanceArgs {
258    #[arg(long, value_enum, hide = true)]
259    pub action: InstallMaintenanceAction,
260
261    #[arg(long, value_enum, hide = true)]
262    pub origin: MigrateInstallOrigin,
263
264    #[arg(long, value_name = "PATH", hide = true)]
265    pub current_path: Option<String>,
266
267    #[arg(long, value_name = "PATH", hide = true)]
268    pub legacy_path: Option<String>,
269}
270
271#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
272pub enum InstallMaintenanceAction {
273    SetMarker,
274    ClearMarker,
275    RepairGlobalPath,
276}
277
278/// SpeedQX Internet Speed Test - QubeTX Developer Tools
279///
280/// Eight-provider speed test (SpeedQX Methodology v4) using Cloudflare, M-Lab
281/// NDT7, M-Lab MSAK, LibreSpeed, fast.com, CacheFly, Vultr, and Apple
282/// networkQuality.
283#[derive(Parser)]
284#[command(
285    name = "speedqx",
286    author,
287    version,
288    disable_version_flag = true,
289    about = "SpeedQX Internet Speed Test - QubeTX Developer Tools",
290    long_about = "SpeedQX Internet Speed Test - QubeTX Developer Tools\n\n\
291        Eight-provider speed test (SpeedQX Methodology v4) using Cloudflare, M-Lab NDT7,\n\
292        M-Lab MSAK (multi-stream), LibreSpeed, fast.com (Netflix), CacheFly, Vultr, and\n\
293        Apple networkQuality. Providers run sequentially and are merged (capacity +\n\
294        consensus with confidence intervals) for maximum accuracy. --fast runs a quick\n\
295        three-provider subset with early stopping.",
296    after_long_help = "EXAMPLES:\n\
297        \x20 speedqx                     Run the full 8-provider test\n\
298        \x20 speedqx --fast              Quick run (Cloudflare + NDT7 + MSAK, early-stop)\n\
299        \x20 speedqx --duration 60       60s per direction for the fixed-duration providers\n\
300        \x20 speedqx --fastcom-duration 30  Override fast.com to 30s/dir\n\
301        \x20 speedqx --skip-msak --skip-apple  Drop MSAK + Apple from the full run\n\
302        \x20 speedqx update              Check for updates and install\n\
303        \x20 speedqx --update            Same as 'speedqx update' (legacy flag form)\n\
304        \x20 speedqx --json              Output results as JSON\n\n\
305        Run 'speedqx --help' for full details, or 'speedqx -h' for a summary."
306)]
307pub struct SpeedQXCli {
308    /// Output results as JSON
309    #[arg(long, help_heading = "Output", global = true)]
310    pub json: bool,
311
312    /// Use ASCII characters instead of Unicode box-drawing
313    #[arg(long, help_heading = "Output", global = true)]
314    pub ascii: bool,
315
316    /// Disable colored output
317    #[arg(long, help_heading = "Output", global = true)]
318    pub no_color: bool,
319
320    /// Test duration per direction for CF/NDT7/LibreSpeed: seconds or "auto"
321    #[arg(
322        long,
323        default_value = "30",
324        value_parser = parse_duration,
325        help_heading = "Speed Test"
326    )]
327    pub duration: TestDuration,
328
329    /// Test duration per direction for fast.com: seconds or "auto" (default: auto)
330    #[arg(
331        long,
332        default_value = "auto",
333        value_parser = parse_duration,
334        help_heading = "Speed Test"
335    )]
336    pub fastcom_duration: TestDuration,
337
338    /// Number of latency probes (advisory; the dense engine is duration-scaled)
339    #[arg(long, default_value = "20", help_heading = "Speed Test")]
340    pub latency_probes: u32,
341
342    /// FAST mode: Cloudflare + NDT7 + MSAK with early-stop (default is the full
343    /// 8-provider run)
344    #[arg(long = "fast", help_heading = "Speed Test")]
345    pub fast: bool,
346
347    /// Skip the M-Lab MSAK multi-stream provider
348    #[arg(long = "skip-msak", help_heading = "Speed Test")]
349    pub skip_msak: bool,
350
351    /// Skip the Apple networkQuality provider
352    #[arg(long = "skip-apple", help_heading = "Speed Test")]
353    pub skip_apple: bool,
354
355    /// Check for updates and install the latest version (legacy flag form of `speedqx update`)
356    #[arg(long = "update", help_heading = "Actions")]
357    pub update: bool,
358
359    /// Print version
360    #[arg(short = 'v', long = "version", action = clap::ArgAction::Version)]
361    pub version: (),
362
363    /// Action subcommand. If present, takes precedence over the legacy `--update` flag.
364    #[command(subcommand)]
365    pub command: Option<SpeedQXCommand>,
366}
367
368/// Action subcommands for `speedqx`. Mirrors the `nd300` pattern: subcommand
369/// form is preferred, legacy `--update` flag is kept so existing scripts work.
370#[derive(Subcommand, Debug, Clone)]
371pub enum SpeedQXCommand {
372    /// Check for updates and install the latest release.
373    Update,
374}
375
376fn parse_duration(s: &str) -> Result<TestDuration, String> {
377    if s.eq_ignore_ascii_case("auto") {
378        Ok(TestDuration::Auto)
379    } else {
380        s.parse::<u64>()
381            .map(TestDuration::Seconds)
382            .map_err(|_| format!("invalid duration '{}': expected a number or \"auto\"", s))
383    }
384}