From 9c8c0a9fc6bc00e5e9d09745f07dd370a3c4c98b Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:05:44 -0700 Subject: [PATCH 1/2] feat(update): doctor redirects to the update flow to fix update-class issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uffs --update doctor` already diagnoses; now, when it finds an issue the update flow fixes — out of date, version-skewed, or **missing a core binary** — it redirects there instead of teaching the health-check helper the core set: - piped / non-TTY: prints a `→ run \`uffs --update\`` hint; - interactive TTY: asks "Run `uffs --update` now to fix this? [y/N]"; - `--repair` / `repair`: runs the update flow automatically (after the helper's local self-heal — resume/rollback, sweep backups, restart services). Keeps the architecture clean: the helper stays a dumb snapshot processor; the CLI (which owns the core set) decides what "incomplete" means and how to fix it. `--offline` skips the redirect (assess needs the release feed). The prompt never blocks a non-TTY caller — it returns "no" and falls back to the hint. Host clippy (strict) clean; uffs-cli tests pass. Co-Authored-By: Claude Opus 4.8 --- crates/uffs-cli/src/commands/update/mod.rs | 61 ++++++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/crates/uffs-cli/src/commands/update/mod.rs b/crates/uffs-cli/src/commands/update/mod.rs index 9d22dcca6..95ad4789a 100644 --- a/crates/uffs-cli/src/commands/update/mod.rs +++ b/crates/uffs-cli/src/commands/update/mod.rs @@ -110,7 +110,26 @@ pub(crate) fn run_update(args: &[String]) -> Result<()> { if repair && !forwarded.iter().any(|arg| arg == "--repair") { forwarded.push("--repair".to_owned()); } - return doctor::spawn(&snapshot_path, &forwarded); + // Helper health check (+ local self-heal when --repair): journal, + // backups, services, broker, release reach. Captured (not `?`-propagated) + // so a reported failure doesn't pre-empt the update-flow fix below. + let health = doctor::spawn(&snapshot_path, &forwarded); + + // Update-class issues — out-of-date, version-skewed, or **missing a core + // binary** — are fixed by the update flow itself, which already owns the + // core set. So doctor *redirects* there rather than teaching the + // health-check helper that set. `--offline` skips this (assess needs the + // release feed). With `--repair` we run it; interactively we ask; piped + // we just point. + if !args.iter().any(|arg| arg == "--offline") + && matches!(assess(&report), UpdatePlan::Available { .. }) + { + if repair || prompt_yes_no("Run `uffs --update` now to fix this?") { + return run_automatic_update(&report, verbose); + } + print_update_redirect_hint(); + } + return health; } let report = detect(); @@ -208,6 +227,35 @@ fn has_missing_core(report: &DetectionReport) -> bool { }) } +/// Point the user at the update flow — the fix for an out-of-date, skewed, or +/// incomplete install (doctor detects; `uffs --update` repairs). +#[expect(clippy::print_stdout, reason = "CLI user-facing output")] +fn print_update_redirect_hint() { + println!( + "\n\u{2192} Run `uffs --update` to bring the install up to date and complete the core set." + ); +} + +/// Ask a yes/no question on an interactive terminal. Returns `false` **without +/// prompting** when stdin is not a TTY (scripts / pipes / CI), so callers fall +/// back to a printed hint instead of blocking on a read that can't be answered. +#[expect(clippy::print_stdout, reason = "interactive CLI prompt")] +fn prompt_yes_no(question: &str) -> bool { + use std::io::{IsTerminal as _, Write as _}; + if !std::io::stdin().is_terminal() { + return false; + } + print!("\n{question} [y/N] "); + if std::io::stdout().flush().is_err() { + return false; + } + let mut line = String::new(); + if std::io::stdin().read_line(&mut line).is_err() { + return false; + } + matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes") +} + /// Run the full end-to-end update when one is needed; otherwise report the /// install is current. Journaled + auto-rollback (delegated to `apply`). fn run_automatic_update(report: &DetectionReport, verbose: bool) -> Result<()> { @@ -462,10 +510,13 @@ fn print_help() { \x20 atomically swap + smoke-test, commit, restart.\n\ \x20 Journaled + auto-rollback on failure.\n\ \x20 doctor End-to-end health check (versions, dirs, journal,\n\ - \x20 backups, services, broker pipe, release reach).\n\ - \x20 repair Diagnose + self-heal (= doctor --repair):\n\ - \x20 resume/roll back an interrupted update, sweep\n\ - \x20 stale backups, restart stopped services.\n\ + \x20 backups, services, broker pipe, release reach). If\n\ + \x20 out of date / skewed / missing a core binary, it\n\ + \x20 points to `uffs --update` (asks first on a TTY).\n\ + \x20 repair Diagnose + self-heal (= doctor --repair): resume/\n\ + \x20 roll back an interrupted update, sweep stale\n\ + \x20 backups, restart stopped services — and run the\n\ + \x20 update flow if the install is out of date.\n\ \x20 recover Finish or roll back an interrupted update now\n\ \x20 (foreground; the on-demand self-heal).\n\ \x20 bins Print the core binary stems (one per line) —\n\ From a78b12f5b265ad54673d04139504c95ec27652f2 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:15:59 -0700 Subject: [PATCH 2/2] docs: note repair/bins actions + completeness in cli-grammar & design doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev-facing docs to match the user manual: - cli-grammar.md: add `repair` + `bins` to the `--update` action surface; note repair = doctor --repair alias, the doctor→update redirect, and that bare update reconciles the whole core set (adds a missing core binary). - UFFS-Self-Update-Feasibility-and-Design.md: add a §4.1 as-built addendum (action-based grammar) covering completeness (#447/#448), the repair verb + doctor redirect (#446/#449), and the journal to_version fix (#446). Co-Authored-By: Claude Opus 4.8 --- docs/architecture/cli-grammar.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/architecture/cli-grammar.md b/docs/architecture/cli-grammar.md index a9351dc76..daf7e1565 100644 --- a/docs/architecture/cli-grammar.md +++ b/docs/architecture/cli-grammar.md @@ -167,7 +167,7 @@ different (each internally-consistent) conventions. | `uffs aggregate\|agg ` | `uffs --agg ` | — | `--format` | | `uffs daemon ` | `uffs --daemon ` | `start` `status` `stats` `stop` `kill` `restart` `load` `hibernate` `preload` `forget` `status_drives` | `--data-dir` `--mft-file` `--elevate` | | `uffs mcp ` | `uffs --mcp ` | `run` `start` `status` `stop` `kill` `restart` `reload` | `--bind` `--port` `--data-dir` | -| `uffs update [--acquire\|--apply\|--snapshot]` + `uffs update doctor` | `uffs --update []` | ***(none = update end-to-end if needed)*** `check` `snapshot` `acquire` `apply` `doctor` `recover` | `--version` `--repair` `--offline` `--repo` `-v` | +| `uffs update [--acquire\|--apply\|--snapshot]` + `uffs update doctor` | `uffs --update []` | ***(none = update end-to-end if needed, incl. completing a missing core binary)*** `check` `snapshot` `acquire` `apply` `doctor` `repair` `recover` `bins` | `--version` `--repair` `--offline` `--repo` `-v` | | `uffs status` | `uffs --status` | — | — | | `uffs --help / --version` | `uffs --help / --version` *(unchanged; global)* | — | — | @@ -179,6 +179,15 @@ different (each internally-consistent) conventions. the update subsystem, so this keeps the surface uniform. (A top-level `--doctor` convenience alias is an open question, §12.) +`repair` is a first-class **alias for `doctor --repair`** (a bare `--repair` +flag routes there too) — verbs stay bare, options stay dashed, and the user +never has to remember which `repair` is. `doctor`/`repair` **redirect to the +update flow** when they find an update-class issue (out of date, version-skewed, +or a missing core binary) rather than duplicating the updater's logic. `bins` +prints the canonical core binary set (the single source of truth all flows +share); bare `uffs --update` reconciles that whole set, *adding* a missing core +binary, not just version-matching the ones present. + ## 6. Edge cases & escapes | Input | Result |