# The install page, and the scripts behind it

**Status: for discussion, 2026-08-13.** Nothing here is built. It exists so tomorrow's conversation
is about real branches rather than sketched ones — every question below is one the scripts already
ask today.

## The shape agreed

- One **source** — the interactive scripts as they are.
- A **build script** that compiles them into single files, because `curl | bash` cannot fetch libs.
- The build emits **one script per leaf** of the question tree, not one script with pre-seeded
  answers. A person auditing before running reads only their own path.
- Verification is of the **generator**, once: anyone regenerates the leaves from source and diffs
  them against what is published. One thing to trust rather than N.

---

## The questions that actually exist

Forty-seven prompts across the two scripts. Almost none of them should become a branch — the
distinction that matters is:

**A branch** changes which *code* runs. Removing it makes a script genuinely shorter.

**A value** changes a *string*. Removing it makes a script no shorter — it just moves the answer
from a prompt to a constant.

**A consent** is a yes/no about doing a step at all. These are the interesting middle: pre-answering
one lets the build delete the section entirely.

### Branches — these change what code exists

| question | answers | what it eliminates |
| --- | --- | --- |
| operating system | macOS · Debian/Ubuntu · Arch · Fedora | 17 of 26 machine-setup sections on macOS; the whole `case $PM` ladder collapses to one arm |
| machine role | homelab · vps · dev | swap, ballast, earlyoom, sleep/suspend, boot-hang, static addressing — each is role-gated today |
| tailnet | already connected · set one up · none | the entire Tailscale section, its four sub-options and the offscale explanation |
| which half | machine + officer · officer only · machine only | one of the two scripts disappears |

### Consents — pre-answering deletes a section

Docker · fail2ban · unattended-upgrades · Neovim · agent CLIs · shell config · firewall · SSH
hardening · DNS · swap · ballast · earlyoom · inotify · boot-on-start.

Fourteen sections that a leaf script can simply not contain.

### Values — never a branch

Username · install path · git name and email · port · public URL · Postgres connection · timezone ·
locale · LAN CIDR · swap size · swappiness.

These stay as prompts even in a generated script, or arrive as environment variables. Baking them
into a published file would mean publishing somebody's hostname.

---

## Where this collides with `--unattended`

`--unattended` and a generated leaf are the *same mechanism seen twice*: both are "answer these in
advance". The difference is only whether the answer is baked in at build time or supplied at run
time.

Worth deciding tomorrow whether a leaf script is literally `base.sh --unattended` with a header of
constants, or whether the build truly strips the dead branches. The second is what makes it
auditable-by-being-short; the first is what makes it maintainable. **They are not the same artifact,
and the whole plan rests on which one we mean.**

One thing that already exists and should be preserved either way: with no tty, `install_config`
keeps the user's file rather than replacing it. Every unattended answer needs to be conservative in
that same way, and that is a property of each prompt, not of the flag.

---

## The combinatorics

4 OS × 3 roles × 3 tailnet states = **36 leaves** before any consent is considered, and consents
multiply it past anything anyone would publish.

So the tree the install page walks cannot be the full product. Two ways out, to choose between:

1. **Publish a few opinionated leaves** — "Ubuntu VPS, new tailnet", "macOS dev machine", "Ubuntu
   homelab, existing tailnet" — and send everything else to the full interactive script.
2. **Generate on demand** — the page composes the leaf when the questions are answered. Stronger, but
   the artifact is no longer a static file anyone can diff against the repo, which costs the
   verification property the whole design was for.

My inclination is (1), because (2) quietly trades away the thing that made per-leaf scripts worth
building. But it is a real trade and it is yours.

---

## Open, for tomorrow

- Does a leaf strip dead code, or set constants and call the base?
- How many leaves get published, and what happens to the rest?
- Does the install page show the script before running it? It should — that is the moment auditing
  is cheap and nobody will do it afterwards.
- The report from `install-report.md` names a script commit. A generated leaf needs to name the
  source commit it was generated from, or the report cannot be checked against anything.
