diff --git a/.gitignore b/.gitignore index e048666..99b1f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -274,3 +274,6 @@ crash.*.log # Sandbox artefact: the harness bind-mounts /dev/null here, it is not a project file .mcp.json + +# Site mockups, the maintainer's own work in progress +maquettes-site/ diff --git a/README.fr.md b/README.fr.md index cf1185a..7522c8a 100644 --- a/README.fr.md +++ b/README.fr.md @@ -53,7 +53,7 @@ **Pointez Terraform et les CLI officielles vers votre propre machine.** -Aucun compte cloud, aucun identifiant, et rien de créé nulle part. +Aucun compte cloud, aucun identifiant de cloud, et rien de créé nulle part. Terraform et OpenTofu pilotent Scaleway et Outscale. Chaque pack a en plus son CLI officiel, et chacun d'eux pilote cet émulateur de bout en bout : @@ -71,6 +71,9 @@ son CLI officiel, et chacun d'eux pilote cet émulateur de bout en bout : **Sur votre machine**, un binaire statique et une stack assez courte pour se lire : +> Il vous faut **Git** et **Terraform >= 1.7.0**. feint lui-même n'a aucune dépendance +> d'exécution : c'est la stack qui réclame le moteur. + ```bash brew install stephrobert/feint/feint git clone https://github.com/stephrobert/feint diff --git a/README.md b/README.md index 4418018..35edaf1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ > [!IMPORTANT] > **What is safe to point at this emulator, and what is not.** > -> **Proven**: 350 of the 373 mounted operations are driven by a real client, on every pull request. `scw`, `octl`, `exo`, Terraform and OpenTofu run against the emulator in CI, and machines really boot: an ssh login on each provider's own default account, isolated subnets, a firewall that filters. The whole chain is described in [docs/conformance.md](docs/conformance.md). +> **Proven**: 350 of the 373 mounted operations are driven by a real client, on every pull request: each pack's own official CLI, and an infrastructure engine wherever a pack admits one, all of them running against the emulator in CI, and machines really boot: an ssh login on each provider's own default account, isolated subnets, a firewall that filters. The whole chain is described in [docs/conformance.md](docs/conformance.md). > > **Not proven**: quotas, prices, real capacity, identifier validation, authentication, eventual consistency. The 50 sections of [docs/limits.md](docs/limits.md) each say what one costs. An emulator with a single implicit account and no price list would have to invent those figures, and somebody would act on them. > @@ -42,7 +42,7 @@ **Point Terraform and the official cloud CLIs at your own machine.** -No cloud account, no credentials, and nothing created anywhere. +No cloud account, no cloud credentials, and nothing created anywhere. Terraform and OpenTofu drive Scaleway and Outscale. Each pack also has its own official CLI, and every one of them drives this emulator end to end: @@ -60,6 +60,9 @@ own official CLI, and every one of them drives this emulator end to end: **On your machine** — one static binary, and a stack short enough to read whole: +> You need **Git** and **Terraform >= 1.7.0**. feint itself has no runtime +> dependency: it is the stack that asks for the engine. + ```bash brew install stephrobert/feint/feint git clone https://github.com/stephrobert/feint diff --git a/docs/install.md b/docs/install.md index 814a77e..a433e45 100644 --- a/docs/install.md +++ b/docs/install.md @@ -101,9 +101,9 @@ nothing is signed until it is released: go install github.com/stephrobert/feint/cmd/feint@latest ``` -That gives you the control plane — every API answers, `scw`, `octl`, `exo` -and Terraform drive it, and nothing runs. It is what CI uses and it needs no -prerequisite at all. +That gives you the control plane — every API answers, `scw`, `octl` and `exo` +drive their own packs, Terraform and OpenTofu drive Scaleway and Outscale, and +nothing runs. It is what CI uses and it needs no prerequisite at all. ## With Homebrew diff --git a/internal/cli/capability_render.go b/internal/cli/capability_render.go index 425b3c0..d038cb3 100644 --- a/internal/cli/capability_render.go +++ b/internal/cli/capability_render.go @@ -147,7 +147,7 @@ func renderPromise(french bool) (string, error) { if french { b.WriteString("**Pointez Terraform et les CLI officielles vers votre propre machine.**\n") - b.WriteString("Aucun compte cloud, aucun identifiant, et rien de créé nulle part.\n\n") + b.WriteString("Aucun compte cloud, aucun identifiant de cloud, et rien de créé nulle part.\n\n") b.WriteString(wrapParagraph(fmt.Sprintf( "%s pilotent %s. Chaque pack a en plus son CLI officiel, et chacun d'eux pilote cet "+ "émulateur de bout en bout :", @@ -155,7 +155,7 @@ func renderPromise(french bool) (string, error) { b.WriteString("\n") } else { b.WriteString("**Point Terraform and the official cloud CLIs at your own machine.**\n") - b.WriteString("No cloud account, no credentials, and nothing created anywhere.\n\n") + b.WriteString("No cloud account, no cloud credentials, and nothing created anywhere.\n\n") b.WriteString(wrapParagraph(fmt.Sprintf( "%s drive %s. Each pack also has its own official CLI, and every one of them drives "+ "this emulator end to end:", diff --git a/internal/cli/docs_banner.go b/internal/cli/docs_banner.go index 72441c4..a40258f 100644 --- a/internal/cli/docs_banner.go +++ b/internal/cli/docs_banner.go @@ -196,7 +196,8 @@ func renderSafety(facts safetyFacts, french bool) string { b.WriteString("> [!IMPORTANT]\n") b.WriteString("> **What is safe to point at this emulator, and what is not.**\n>\n") fmt.Fprintf(&b, "> **Proven**: %d of the %d mounted operations are driven by a real client, on every "+ - "pull request. `scw`, `octl`, `exo`, Terraform and OpenTofu run against the emulator in "+ + "pull request: each pack's own official CLI, and an infrastructure engine "+ + "wherever a pack admits one, all of them running against the emulator in "+ "CI, and machines really boot: an ssh login on each provider's own default account, isolated "+ "subnets, a firewall that filters. The whole chain is described in "+ "[docs/conformance.md](docs/conformance.md).\n>\n", facts.Driven, facts.Mounted) diff --git a/internal/cli/docs_quickstart.go b/internal/cli/docs_quickstart.go index 13db174..3a90833 100644 --- a/internal/cli/docs_quickstart.go +++ b/internal/cli/docs_quickstart.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" ) @@ -91,6 +92,32 @@ func renderQuickstart(goMod, changelog string, french bool) (string, error) { } else { b.WriteString("**On your machine** — one static binary, and a stack short enough to read whole:\n\n") } + + // What the reader needs that these four commands do not install. + // + // They install feint, clone, change directory and run — and the third one + // runs the engine the stack declares, which is not feint. Measured + // 2026-08-28: a reader on a machine carrying neither Git nor Terraform + // cannot reach the `Apply complete!` this block prints underneath, which is + // #593's own complaint one level out: a Quick Start that displays an output + // its own commands cannot produce. + // + // The engine and its floor are read from the stack's own declaration and its + // `required_version`, never typed here: a number written beside a file that + // owns it is a number that goes stale in silence, and this block exists + // because that happened to the resource count. + if engine, floor, ok := quickstartEngineFloor(stack); ok { + if french { + fmt.Fprintf(&b, "> Il vous faut **Git** et **%s %s**. feint lui-même n'a aucune dépendance\n"+ + "> d'exécution : c'est la stack qui réclame le moteur.\n\n", + engineName(engine), floor) + } else { + fmt.Fprintf(&b, "> You need **Git** and **%s %s**. feint itself has no runtime\n"+ + "> dependency: it is the stack that asks for the engine.\n\n", + engineName(engine), floor) + } + } + b.WriteString("```bash\n") fmt.Fprintf(&b, "brew install %s/%s\n", slug, pathBase(slug)) fmt.Fprintf(&b, "git clone https://github.com/%s\n", slug) @@ -364,3 +391,46 @@ func writeSplicedTranslatedBanner(root string, order []string, routes map[string } return os.WriteFile(path, []byte(updated), 0o644) //nolint:gosec // documentation is world-readable by design } + +// quickstartEngineFloor answers the engine a quickstart declares and the lowest +// version its Terraform will accept, both read from the files that own them. +// +// Two files, because the two facts live apart: `feint.yaml` names the engine +// `feint up` will run, and `main.tf`'s `required_version` names the floor that +// engine will refuse to go below. Typing either one beside this block is what +// #593's resource count already proved goes stale. +// +// It answers false rather than guessing when either is missing: a prerequisite +// line that names a version nobody declared is worse than none, because a reader +// installs that version and still fails. +func quickstartEngineFloor(stack string) (engine, floor string, ok bool) { + declaration, err := os.ReadFile(filepath.Join(stack, "feint.yaml")) + if err != nil { + return "", "", false + } + engineLine := regexp.MustCompile(`(?m)^\s*engine:\s*(\S+)\s*$`).FindSubmatch(declaration) + if engineLine == nil { + return "", "", false + } + main, err := os.ReadFile(filepath.Join(stack, "main.tf")) + if err != nil { + return "", "", false + } + version := regexp.MustCompile(`required_version\s*=\s*"([^"]+)"`).FindSubmatch(main) + if version == nil { + return "", "", false + } + return string(engineLine[1]), strings.TrimSpace(string(version[1])), true +} + +// engineName spells an engine the way its own project does. +func engineName(engine string) string { + switch engine { + case "terraform": + return "Terraform" + case "opentofu": + return "OpenTofu" + default: + return engine + } +}