Files
mesh-host/internal/bootstrap/choices.go
jschoubben 21474b0144 The installer goes as far as it can, and asks where a human must choose
Twelve steps made a mesh that RUNS and then said "what remains is somebody
else's". The seven things that turn it into a mesh that WORKS — the shared base,
a database provider, the catalogue, the private network, the packet filter — were
typed afterwards, which is how they went missing for weeks without anything
complaining.

Six more steps now: base, store, catalogue, network, filter, extras. Everything
in them is module add, build, assign and push — the same verbs a person types,
through the same commands, so the installer and an operator remain one act.

Where a human must choose, the installer asks. A choice resolves in the order a
person expects: the flag wins; a lone option answers itself ALOUD, because "it
chose for me" and "there was nothing to choose" read identically afterwards
unless one speaks; a terminal is asked; a default fills in; and a required
choice nothing answered refuses naming its flag — a guessed packet filter is a
machine somebody else configured. The filter is required, so the question is
which, not whether. A run without a terminal (the lab, --json) is never left
waiting on a prompt nobody will answer.

Placement is part of the network step, not a separate act — a lesson paid for:
the module installed, the names file was written with no names in it, and
everything reported success because nobody had said where the machine IS. The
hub endpoint derives from the broker address when unsaid: the host other
machines dial is one fact, not two that drift.

Extras fail the run rather than soft-fail: somebody asked for them by name, and
a mesh reporting success minus one thing is reporting the wrong thing.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 21:56:23 +02:00

80 lines
2.6 KiB
Go

package bootstrap
import (
"fmt"
"strings"
)
// What the installer asks a person, and how an unattended run answers.
//
// **The installer goes as far as it can, and where a human must choose, it asks** — a packet
// filter is required, so the question is not whether but which. A run with a terminal is asked;
// a run without one (the lab, an unattended machine) answers with flags, and a required choice
// with no flag, no terminal and no lone option is a refusal rather than a guess.
// Choice is one question the installer needs answered.
type Choice struct {
// Name is the flag that answers it unattended: --packet-filter, --private-network.
Name string
// Question is what a person is asked, ending with what the options are.
Question string
// Options are the acceptable answers. Empty means free-form (an address, a list).
Options []string
// Default is used when nothing and nobody answered. Empty means the choice is required.
Default string
}
// decide resolves one choice, in the order a person would expect:
//
// an explicit answer (the flag) wins; a lone option answers itself, said aloud; a terminal is
// asked; a default fills in; and a required choice nothing answered is refused naming its flag.
func decide(c Choice, answered string, prompt func(Choice) (string, error), say func(string)) (string, error) {
if got := strings.TrimSpace(answered); got != "" {
return validated(c, got)
}
if len(c.Options) == 1 {
// The only answer there is. Said rather than silent, because "it chose for me" and "there
// was nothing to choose" read identically afterwards unless one of them says so.
say(fmt.Sprintf(" %-17s %s — the only option there is", c.Name, c.Options[0]))
return c.Options[0], nil
}
if prompt != nil {
got, err := prompt(c)
if err != nil {
return "", err
}
if strings.TrimSpace(got) == "" && c.Default != "" {
say(fmt.Sprintf(" %-17s %s (default)", c.Name, c.Default))
return c.Default, nil
}
return validated(c, strings.TrimSpace(got))
}
if c.Default != "" {
say(fmt.Sprintf(" %-17s %s (default)", c.Name, c.Default))
return c.Default, nil
}
return "", fmt.Errorf(
"%s must be chosen and nothing chose it: no --%s, no terminal to ask on%s",
c.Name, c.Name, orOptions(c))
}
func validated(c Choice, got string) (string, error) {
if len(c.Options) == 0 {
return got, nil
}
for _, option := range c.Options {
if got == option {
return got, nil
}
}
return "", fmt.Errorf("%q is not a %s this mesh offers. It has %s",
got, c.Name, strings.Join(c.Options, ", "))
}
func orOptions(c Choice) string {
if len(c.Options) == 0 {
return ""
}
return ". It offers " + strings.Join(c.Options, ", ")
}