# Panama: one command to update a machine ## Why now Updating a machine currently takes three commands, and one of them asks the full first-install questionnaire every time: ```sh panama update # actually: commit & push my edits, or pull if clean panama migrate # the repairs panama upgrade # ./install, interview and all ``` Nobody will do that routinely, which means machines drift. The interview in particular is pure ceremony on an already-configured machine: it asks about NVIDIA, Secure Boot, git identity and extras categories, none of which have changed since the machine was built. ## What is broken **1. The interview runs on every upgrade, and nothing needs it.** Five of the seven stages read no interview answer at all: | Stage | Reads from the interview | |---|---| | `install-packages` | `PANAMA_EXTRAS` only, for the *optional* categories. Empty means base lists still install. | | `link-dotfiles` | nothing | | `link-user` | `PANAMA_USER_CONTENT`, and falls back to the decision recorded at `$XDG_STATE_HOME/panama/user-content` | | `change-settings` | nothing | | `link-vicinae-scripts` | nothing | | `setup-identity` | git name/email/editor, gh login, ssh key — genuinely first-run | | `install-hardware` | NVIDIA, MOK hash, debloat, firmware — genuinely first-run | `link-user` already anticipates this in its own comment: *"Running this stage by hand outside an install honours the recorded answer instead, so `panama upgrade` on an already-configured machine does not need re-asking."* The capability is there; `install:91` just calls the interview unconditionally with no way past it. **2. `panama update` is named for the wrong job.** It means "review, commit & sync local changes" — a git workflow command. README and `config/dot/quickshell/manual/05-making-it-yours.md` both document it that way. The command a person reaches for to update their machine is therefore the one command that does not. **3. Most updates need no stages at all, and there is no way to tell.** Dotfiles are symlinks into the checkout, so an edit to any existing `config/dot/**` file is live the instant `git pull` returns. A pull only needs stages when it brings something structurally new: a new `config/dot//` directory, anything under `config/copy/`, a new package, or a gsettings change. Today the only way to be sure is to run everything. ## Decisions 1. **`panama update` becomes the machine-update command.** The git workflow moves to `panama sync`, unchanged in intent. One verb per job; nothing branches on state the user cannot see. 2. **`install` gains `--upgrade`**, which skips the interview and trims `STAGES` to the five that need no answers. One stage list in the repository, so the two paths cannot drift. 3. **`install-packages` runs when a content hash of `setup/packages/**` differs from the recorded one.** Not a git range: Panama is developed in place, and a hand-edited uncommitted package list must still trigger an install. 4. **A dirty tree is stashed, pulled over, and popped.** The machine update is never blocked by uncommitted work. 5. **A conflicted pop resets the tree, keeps the stash, warns, and continues.** Conflict markers must never reach a live config; the update proceeds against the clean upstream checkout. 6. **The migrate notification is untouched.** It says "repairs are waiting" and it will keep doing exactly that. 7. **`panama sync` commits before pulling**, so it needs no stash at all. ## Design ### Command surface | Command | Job | |---|---| | `panama update` | Bring **this machine** up to date. The routine command. | | `panama sync` | Commit & push **my edits**. Today's `cmd_update`, renamed and simplified. | | `panama upgrade` | Full `./install`, interview included. New machine, or to change an answer. | | `panama migrate` | Repairs only. Unchanged; still what the login notification runs. | ### `panama update` ``` 1. Record pre-pull HEAD. 2. Dirty tree? git stash push --include-untracked -m "panama-update-" 3. git pull --ff-only (failure is reported, never fatal) 4. Stashed? git stash pop on conflict: git reset --hard HEAD leave the stash in place record a warning for the summary 5. exec install --upgrade ``` Steps 1–4 are the only new logic in `panama update`; the packages gate below is the only new logic in `install`. Everything after step 5 is `install`'s existing tail. The pull is `--ff-only`. A diverged branch is a thing to resolve deliberately, not something an update command should paper over; it reports and continues to the stages, because the machine should still be brought in line with what is checked out. ### `install --upgrade` `install` has no argument parsing today, so this adds the first. `cmd_upgrade` already forwards `"$@"`, so `panama upgrade --upgrade` would work but is pointless and undocumented. `--upgrade` changes exactly four things: - The interview block (`install:91-98`) is skipped. No `PANAMA_ANSWERS` file is created, so every `PANAMA_*` answer is unset and each stage takes its documented empty-answer path. - `hostnamectl` is skipped — there is no answer to apply. - `STAGES` drops `setup-identity` and `install-hardware`. - The closing message says the machine was updated rather than *"Panama installed. Log out and choose the Hyprland session to start it."* Everything else is inherited unchanged and deliberately so: the `sudo -v` keepalive (`change-settings` and `install-packages` both need root), the per-stage failure collection, the migrations block, `panama-doctor --summary`, the `post-upgrade` hook, and the exit code. One correction to the migrations block is required. It currently baselines when the marker directory is absent, on the reasoning that a machine with no markers was just built from this checkout. That inference is only sound during a real install. Under `--upgrade` it must always take `run`; the migrations are all self-guarding and a no-op on a machine that does not need them. ### The packages hash ```sh STATE="${XDG_STATE_HOME:-$HOME/.local/state}/panama" hash_packages() { find "$PANAMA_PATH/setup/packages" -maxdepth 1 -type f -exec sha256sum {} + \ | sort | sha256sum | cut -d' ' -f1 } ``` `install-packages` is skipped when the hash matches `$STATE/packages-hash`. `-maxdepth 1` is deliberate and excludes `setup/packages/extras/`. An answer-free run has an empty `PANAMA_EXTRAS` and installs no optional category, so hashing those files would flip the hash, run the stage, install nothing extra, and record the new hash as though it had. **Optional categories cannot be re-applied by `panama update` at all**, because which ones this machine chose is never recorded — the interview's answers are transient by design. Adding to a category you already have is `panama apps`, which is what it is for. This is a known limitation, not an oversight. The hash is written **only after the stage succeeds**, mirroring the rule `panama-migrate` already documents for its markers: a step that did not complete has not happened, and recording it as done hides it forever. Two consequences worth stating: a full `./install` always runs the stage and writes the hash regardless of any recorded value, and the *first* `panama update` after this ships will run `install-packages` once, because no hash exists yet. Both are correct. `panama update --packages` forces the stage regardless of the hash, forwarding `--packages` to `install --upgrade`. This is the one flag worth adding, because the hash cannot see a package that dnf removed behind Panama's back. ### `panama sync` Today's `cmd_update` with the stash dance removed. Because the changes are committed before the pull, there is nothing to stash: ``` 1. Clean tree? Say so and stop. Pulling is `panama update`'s job now. 2. Show status + diff, including untracked files as new-file diffs. 3. Confirm, read a commit message, git add -A && git commit. 4. Behind upstream? git pull --rebase 5. Offer to push. ``` A rebase conflict lands on committed work, which is an ordinary and recoverable place to be, rather than on a stash. ### Conflict handling Verified against a throwaway repository rather than assumed: - A conflicted `git stash pop` **keeps** the stash entry. - Conflict markers are written into the working file. On this repository that means directly into a live `~/.config` path via the symlink — a broken `.qml` can cost the running shell. - `git reset --hard HEAD` clears the markers, and `stash@{0}` survives it. So on conflict: reset, keep the stash, and report it in the **final summary** rather than only in scrollback, where twenty minutes of dnf output will bury it. ``` ! Your local changes conflicted with what was pulled. They are safe at stash@{0}: panama-update-1787680000 Restore with: git stash pop ``` ## What does not change - `bin/panama-migrate` — no change of any kind. - `bin/panama-migrate-notify` — still runs `panama-migrate run` in a terminal. - The seven stage scripts — none of them are edited. This works precisely because they already handle absent answers. - `panama upgrade` — same behaviour, same interview. ## Files touched | File | Change | |---|---| | `install` | Argument parsing; `--upgrade` skips interview + hostname, trims `STAGES`, forces migrate `run`, alters the closing message. Packages-hash gate around `install-packages`. | | `bin/panama` | `cmd_update` rewritten; `cmd_sync` added (the old body, minus the stash); dispatcher, usage block and header comment updated. | | `README.md` | The upgrade-path paragraph and the command table. | | `config/dot/quickshell/manual/05-making-it-yours.md` | The line telling the reader `panama update` will offer to commit. | | `tests/setup/` | New contracts, below. | ## Tests Four contracts, each pinning a decision that would otherwise rot silently: 1. **`install --upgrade` never invokes the interview.** The regression that started this. Assert `setup/scripts/interview` is not executed. 2. **`--upgrade` runs exactly the five answer-free stages**, and never `setup-identity` or `install-hardware`. 3. **The packages hash gates the stage**, and is not written when the stage exits non-zero. Note this is a narrow guarantee: `install-packages` logs and steps over individual package failures internally and still exits 0, so a single missing package does not hold the hash back. Only a stage-level failure does. 4. **A conflicted pop leaves no conflict markers in the tree and keeps the stash.** Runs against a temporary repository, as the verification above did. Contract 2 is the one that matters most over time: it fails the moment a stage is added to `STAGES` without a decision about which path owns it. ## Verification On this machine, after implementation: ```sh panama test update # the new contracts panama update # expect: no questions, one sudo prompt, packages skipped panama update # expect: idempotent, still no questions ``` The second run is the real check. Everything must be a no-op, and `install-packages` must be skipped on both. ## Out of scope - Any change to how migrations work. They are correct. - A GUI entry point for updates. The notification covers repairs; a full update is a terminal action because it needs a password and prints a summary worth reading. - Rewriting the seven stage scripts. Their empty-answer behaviour is what makes this design a flag rather than a refactor.