From 06e44af2cd1929fb9c0d4cb7ba89efbde78ae96e Mon Sep 17 00:00:00 2001 From: Ilyes512 Date: Sun, 13 Sep 2026 21:17:58 +0200 Subject: [PATCH] docs(readme): cut the README back to an overview and a pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Scripting section documented the stdout/stderr contract, NDJSON semantics, table widths and the Repository column label — a reference section the README had no business carrying. The parts the docs site was missing move to global-flags and template; the rest was already there. --- README.md | 224 ++++++--------------- docs/content/docs/commands/global-flags.md | 23 +++ docs/content/docs/commands/template.md | 17 ++ 3 files changed, 99 insertions(+), 165 deletions(-) diff --git a/README.md b/README.md index 521f58d..a47dd8a 100644 --- a/README.md +++ b/README.md @@ -6,210 +6,104 @@

-A general-purpose developer CLI for scaffolding projects from templates. Define variables, write template files, run hooks — `specs` handles the rest. +Scaffold a project from a template — a git repository or a directory on disk. -![specs use — answering the prompts and running the template's hooks](docs/static/demo/use.gif) +A template is an ordinary tree of files plus a `project.yml` that declares the variables it needs. +`specs` asks for them, renders the tree, and runs the template's hooks. Templates you reuse can be +registered under a name, and `specs` tracks whether each one has moved ahead of what you have. ---- +![specs use — answering the prompts and running the template's hooks](docs/static/demo/use.gif) -## Installation +```yaml +# project.yml — the value is the default, the type decides the prompt +projectName: "my-app" +useDocker: false +license: + - MIT + - Apache-2.0 -**Homebrew (macOS):** +computed: + packagePath: "github.com/{{ username }}/{{ .projectName }}" -```sh -brew install specsnl/tap/specs +hooks: + post-use: + - git init ``` -**Release candidates** — the `@rc` cask tracks every tag, prereleases included: - ```sh -brew install specsnl/tap/specs@rc +specs use specsnl/my-template ./my-project # one-off: fetch, render, discard +specs template download specsnl/my-template mine # or register it once… +specs template use mine ./my-project # …and reuse it by name +specs template list # what's registered, and what has updates ``` -Both casks install a binary called `specs`, so pick one: `brew uninstall specs` before installing -`specs@rc`, and the other way round. - -**From source:** +What it does, in one list: -```sh -go install github.com/specsnl/specs-cli@latest -``` - -**Download a binary** from the [releases page](https://github.com/specsnl/specs-cli/releases). +- **Renders a whole tree**, file names included, with 200+ + [Sprout](https://github.com/go-sprout/sprout) functions available. Files can be conditional, + copied verbatim, or given explicit permissions. +- **Prompts only where something can answer.** With stdin not a terminal, a missing value fails + immediately and names itself instead of hanging a CI job. `--arg`, `--values` and `--use-defaults` + make a run unattended. +- **Keeps templates current.** `template list` reports each one's status — remote templates against + their git remote, saved ones against their source directory — and `template upgrade` applies it. +- **Scripts cleanly.** stdout carries the answer, stderr the narration, so `2>/dev/null` leaves + exactly the data. `--output json` makes it NDJSON. --- -## Quick start - -Use a template directly without registering it first: - -```sh -specs use specsnl/my-template ./my-project -``` - -Or register a template and reuse it later: - -```sh -specs template download specsnl/my-template my-template -specs template use my-template ./my-project -``` - -You can also register a local directory as a template: - -```sh -specs template save ./my-template my-template -``` - -### Keeping templates up to date - -`specs template list` shows an update `Status` for each registered template: - -- **Remote templates** (from `download`) are checked against their git remote. -- **Local templates** (from `save`) are checked against their **source directory on disk** — - `update available` means the source path has moved ahead of what was saved (`source missing` - if that path is gone). Uncommitted changes on the saved commit (a "dirty" working tree) are not - treated as an update, so a dirty source is not reported as perpetually out of date. - -`specs template upgrade [name]` applies available updates: remote templates are re-cloned, local -templates are re-copied from their source path. Cached statuses refresh automatically once older -than 24 hours or when written by a different `specs` version. - -### Scripting - -stdout carries the answer, stderr the narration — so discarding stderr leaves exactly the data, -in either format (`--output` / `-o` selects `pretty` or `json`): +## Install ```sh -specs template list -o json 2>/dev/null | jq -r .name -specs version -o json 2>/dev/null # {"version":"v0.0.13"} -specs template validate ./my-template -o json 2>/dev/null # {"valid":true} -``` - -`pretty` and `json` are the only accepted values; anything else exits non-zero naming the flag, -rather than being silently treated as `pretty`. - -`json` is NDJSON throughout: **one object per line**, a table row included, so a killed or failed -run still leaves every completed row readable. The keys are snake_case and independent of the -column headings the pretty table prints, and each value keeps its own type — a count is a number, -a timestamp is a timestamp, and a field with no value is absent rather than the `-` the table -shows. - -Prompts only happen where something can answer them. With stdin not a terminal — a CI job, or -`< /dev/null` — a template still missing a value fails immediately and names it, instead of -blocking until the runner times out: - -```console -$ specs use specsnl/go-service ./out < /dev/null -error cannot prompt for values: stdin is not a terminal -missing values for: project_name -provide them with --arg Key=Value, with --values, or take the schema defaults with --use-defaults +brew install specsnl/tap/specs ``` -Supply every variable and the command runs unattended. `--non-interactive` forces the same -refusal at a terminal, so you can check a command before CI does. A remote template's hook -confirmation is taken as "no" rather than an error: the template applies, the hooks are skipped, -and `--yes` opts in. +Or `go install github.com/specsnl/specs-cli@latest`, or download a binary for your platform from the +[releases page](https://github.com/specsnl/specs-cli/releases). -Commands that only change the filesystem (`use`, `template save`, `template download`, …) narrate -what they did on stderr and write nothing to stdout. - -Pretty tables are capped to the width of your terminal: when a table does not fit, its widest -columns shrink and their cells wrap onto extra lines rather than the table breaking apart. Redirect -stdout to a file or a pipe and the full natural width is written instead; set `COLUMNS` to pin a -width there (`COLUMNS=100 specs template list | less -R`). - -The `Repository` column shows a **label**, not the raw value: a GitHub URL reads as -`specsnl/specs-cli` since GitHub is the default host, another host keeps its name -(`gitlab.com/acme/tpl`), and a saved path collapses `$HOME` to `~`. The label is clickable in -terminals that support hyperlinks (iTerm2, WezTerm, kitty, Ghostty, GNOME Terminal, Windows -Terminal, …) and opens the full URL, staying one link even when the column wraps it over several -lines. Terminals without support simply show the label, and a redirect to a file or a pipe writes -plain text. - -`--output json` always carries the value as stored, never the label — so scripts read the full URL: +Release candidates are a separate, opt-in cask that tracks every tag, prereleases included: ```sh -specs template list -o json 2>/dev/null | jq -r .repository +brew install specsnl/tap/specs@rc ``` -For a template registered with `template save`, that value is the source path, with your home -directory written as `~` (e.g. `~/code/my-template`). Templates saved by versions before this -change carry a `local:` prefix instead; that form is still read, and migrates on the next -`template upgrade`. +Both casks provide a `specs` command and cannot be installed side by side — `brew uninstall specs` +before installing `specs@rc`, and the other way round. --- -## The project file - -A template's `project.yml` declares its variables, defaults, computed values, and hooks. A few `__`-prefixed keys are reserved by `specs` and never exposed as template variables: - -| Key | Purpose | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `__delimiters` | Override the default `{{ }}` template delimiters with a custom pair (e.g. `[[ ]]`). | -| `__specs__version` | Declare a [semver](https://github.com/Masterminds/semver) constraint on the `specs` CLI version required to use the template, e.g. `__specs__version: ^0.1.0`. `specs use` and `specs template use` refuse to run the template unless the running binary satisfies the constraint (development builds are exempt; `specs template save` skips the check). | - -See the [documentation](https://cli.specs.dev) for the full project-file reference. - ---- - -## Development environment - -### Overview - -The repo ships a `Dockerfile` and a `compose.yml` that together define a self-contained build and test environment. Contributors don't need a local Go installation — all builds and tests run inside a Docker container that pins the exact Go version and tooling. +## Getting started -| File | Role | -|---------------------|------------------------------------------------------------------------------------------| -| `Dockerfile` | Defines the build image — Go 1.26 + tooling, used by `task build` and `task test` | -| `compose.yml` | Wires the Dockerfile stages into named services consumed by the Taskfile | -| `Taskfile.dist.yml` | Orchestrates all developer workflows; wraps Docker Compose so you never call it directly | - -**Requirements:** [Task](https://taskfile.dev) and Docker. - -### Getting started - -Build the images once before running any task: - -```sh -task dc:build -``` - -Then use the standard tasks: - -```sh -task build # Build the binary for the current platform -task test # Run unit tests -task test:update # Rewrite the output golden files, then review the diff -``` - -List all available tasks: +Point `specs use` at any template and it will ask you the rest: ```sh -task --list +specs use specsnl/my-template ./my-project ``` -### How it works +Writing your own starts with a `project.yml`. That file, every command and flag, the template +functions, scripting and CI, and how it is built are all on the same site: +**[cli.specs.dev](https://cli.specs.dev)**. -`task dc:build` builds all Docker Compose services in the `build` profile. The key service is `go-builder`, built from the `builder-download` stage of the `Dockerfile`. It mounts the repository root and two Docker volumes — one for the Go module cache and one for the build cache — so subsequent runs are fast. - -`task test` and `task build` spin up a one-off `go-builder` container (`docker compose run --rm`), run the Go command inside it, then discard the container. The service doesn't need to be started in advance — it is ephemeral by design. - -`task build` also invokes `docker buildx bake` using the `go-binary` service to produce a statically linked binary and copy it out of the image into the project root. +--- -### Without Docker (escape hatch) +## Contributing -If you already have Go 1.26+ installed locally, you can bypass the container entirely: +Every command runs through [Task](https://taskfile.dev), which wraps the Docker Compose services +that pin the Go and tooling versions — so a check runs the same way locally as it does in CI. No +local Go installation needed. Run `task --list` for the full set. ```sh -go build ./... -go test ./... +task dc:build # build the images once +task build # build the binary for the current platform +task test # run the unit tests ``` -CI always runs through Docker and the Taskfile. The container is the source of truth for reproducible builds. - -### CI and agent execution +With Go 1.26+ installed you can bypass the container entirely — `go build ./...`, `go test ./...` — +but CI always runs through Docker and the Taskfile, so that is the source of truth. -All CI and agent workflows follow the same rule: use `task` commands, never call `docker compose` directly. See [`.github/instructions/executing-commands.md`](.github/instructions/executing-commands.md) for the authoritative execution rules. +Conventions, workflow, and the house rules that reviews are held to: [AGENTS.md](./AGENTS.md), and +the execution rules in +[`.github/instructions/executing-commands.md`](.github/instructions/executing-commands.md). --- diff --git a/docs/content/docs/commands/global-flags.md b/docs/content/docs/commands/global-flags.md index 3eae137..ad78f19 100644 --- a/docs/content/docs/commands/global-flags.md +++ b/docs/content/docs/commands/global-flags.md @@ -41,3 +41,26 @@ $ specs version --output json 2>/dev/null Commands that only act on the filesystem (`template save`, `template download`, `use`, …) narrate what they did and write nothing to stdout. `--debug` logging is separate again and always goes to stderr. + +`json` is NDJSON throughout — **one object per line**, a table row included, so a killed or failed +run still leaves every completed row readable. The keys are snake_case and independent of the +column headings the pretty table prints, and each value keeps its own type: a count is a number, a +timestamp is a timestamp, and a field with no value is absent rather than the `-` the table shows. + +## Pretty tables + +A pretty table is capped to the width of your terminal: when it does not fit, its widest columns +shrink and their cells wrap onto extra lines rather than the table breaking apart. Redirect stdout +to a file or a pipe and the full natural width is written instead — set `COLUMNS` to pin a width +there: + +```sh +COLUMNS=100 specs template list | less -R +``` + +Cells that stand for a URL are printed as a **label** and hyperlinked to the full value, using +[OSC 8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). Terminals that support +hyperlinks (iTerm2, WezTerm, kitty, Ghostty, GNOME Terminal, Windows Terminal, …) make the label +clickable, and it stays one link even when the column wraps it over several lines. Terminals +without support simply show the label, and a redirect to a file or a pipe writes plain text. +`--output json` always carries the value as stored, never the label. diff --git a/docs/content/docs/commands/template.md b/docs/content/docs/commands/template.md index 62872d7..d14dec3 100644 --- a/docs/content/docs/commands/template.md +++ b/docs/content/docs/commands/template.md @@ -30,6 +30,23 @@ For machine-readable `list` and `update` output, use the global `--output json` still yields `[]`, with the explanation narrated on stderr — while `validate` answers `{"valid": true|false}` and `version` answers `{"version": "…"}`. +## The Repository column + +`template list` prints a **label** in its `Repository` column, not the raw stored value. A GitHub +URL reads as `specsnl/specs-cli`, since GitHub is the default host; any other host keeps its name +(`gitlab.com/acme/tpl`); and a saved path collapses `$HOME` to `~`. The label is clickable in +terminals that support hyperlinks — see [Pretty tables](global-flags#pretty-tables). + +`--output json` carries the value as stored, so scripts read the full URL: + +```sh +specs template list -o json 2>/dev/null | jq -r .repository +``` + +For a template registered with `template save`, that value is the source path with your home +directory written as `~` (e.g. `~/code/my-template`). Templates saved by older versions carry a +`local:` prefix instead; that form is still read, and migrates on the next `template upgrade`. + ## Update status The `Status` column reflects where each template's "source of truth" lives: