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
80 lines
2.6 KiB
Go
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, ", ")
|
|
}
|