Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions crates/uffs-cli/src/commands/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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\
Expand Down
11 changes: 10 additions & 1 deletion docs/architecture/cli-grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ different (each internally-consistent) conventions.
| `uffs aggregate\|agg <preset>` | `uffs --agg <preset>` | — | `--format` |
| `uffs daemon <a>` | `uffs --daemon <a>` | `start` `status` `stats` `stop` `kill` `restart` `load` `hibernate` `preload` `forget` `status_drives` | `--data-dir` `--mft-file` `--elevate` |
| `uffs mcp <a>` | `uffs --mcp <a>` | `run` `start` `status` `stop` `kill` `restart` `reload` | `--bind` `--port` `--data-dir` |
| `uffs update [--acquire\|--apply\|--snapshot]` + `uffs update doctor` | `uffs --update [<a>]` | ***(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 [<a>]` | ***(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)* | — | — |

Expand All @@ -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 |
Expand Down
Loading