Skip to content

2. UI + dry run

Omisen edited this page Aug 14, 2026 · 3 revisions

The guided interface that dresses the engine, and the functional --dry-run. It lives in src/prompt.rs (input with inquire), src/progress.rs (progress with indicatif) and in src/main.rs's flow.

This is a dressing layer, not logic: it does not touch the rollback and does not change the steps. The value is entirely in the decoupling — if the UI is a shell around an engine that does not know it, you can change it (or add another) without touching the installation and the rollback.


The principle: a decoupled UI

From CLAUDE.md: the same installer runs interactively or non-interactively through one flow.

  • The steps know nothing about the UI: they do not import inquire/indicatif and print no prompts. They read from the Context and log through tracing.
  • Input collection happens before the engine, in prompt.rs, and populates the Context. The engine receives an already-resolved Context and does not know whether the values came from inquire, from --config or from the defaults.
  • Progress is an observer of the engine (ProgressReporter), not part of the steps.

The proof it is done right: every earlier test stayed green without modification. execute's signature did not change (it delegates to execute_with_reporter with a NoopReporter), and the Step trait was not touched. A suit of clothes, not a rewritten body.

A structural test verifies the decoupling: no step imports inquire/indicatif, and the engine does not use indicatif:: — it depends only on the abstraction.


Part 1 — Input with inquire

prompt.rs implements input collection behind the same boundary as the config layer: the cascade logic stays in config; only the how of asking changes.

Field Widget
Version Select (16.0/17.0/18.0/19.0)
OS user, DB name, port, install subdir Text with inline validation (reusing config's validators)
Admin password Password, masked → goes into the Secret, never logged
Nginx, final confirmation Confirm

Only fields not passed on the CLI are prompted for: the priority stays CLI > env > interactive > default. Without a TTY, main bypasses the prompts and uses CLI → env → default: inquire never blocks when there is no terminal.


Part 2 — Progress with indicatif

The engine notifies an abstraction, not indicatif:

trait ProgressReporter {
    fn step_start(&self, name, index, total);
    fn step_done(&self, name);
    fn step_failed(&self, name);
    fn rollback_start(&self, total);   // the rollback is visible: the user sees it is undoing
    fn undo_start(&self, name);
    fn undo_done(&self, name);
}
Impl Use
IndicatifReporter interactive TTY: spinner + [pos/len] bar with the current step
LogReporter no TTY / output to a file: tracing only
NoopReporter silent (the default for execute)

The reporter is passed to execute_with_reporter, not to the Step trait: the steps stay unaware.


Part 3 — A functional --dry-run (G2)

dry_run_plan shows the complete plan without mutating:

for each step:  snapshot (read-only)  →  run in dry-run (LOGS the intent, mutates nothing)
                no persistence · no rollback
  • In dry-run, each step distinguishes “would act” from “no-op (pre-existing)” based on its own snapshot (“I would create the directory” vs “already there, skip”).
  • An unavailable snapshot does not interrupt the plan: it is reported and the plan carries on.
  • In dry-run main skips the preflight checks that require root: it is a preview without sudo, useful for understanding what will happen and for validating a .env before running it for real.

Honesty about “without sudo”. The plan interrogates the system: every step takes its own snapshot, and some of them ask PostgreSQL through sudo. Without privileges those questions get no answer, the steps involved show up as “snapshot unavailable”, and the plan — while true — is incomplete. The installer says so before printing it, rather than leaving it to be discovered in a warning line somewhere in the output.

Every sudo invocation also passes -n (non-interactive): a missing permission becomes an immediate error instead of a silent wait on a password prompt. In production we are already root, so nothing changes there.

Tests: in dry-run the engine calls no mutating operation of SystemOps and does not write the state.


Design notes

  • execute unchanged → full compatibility with the earlier tests. execute_with_reporter (and rollback_with_reporter) were added; the Step trait did not change.
  • The reporter is selected automatically in main: TTY + a real installation → indicatif; otherwise logs.
  • The password is never logged: masked Password on input, Secret for storage.

Clone this wiki locally