diff --git a/CLAUDE.md b/CLAUDE.md index 5801ec9..8f70ce1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,11 +3,18 @@ Guidance for working in this repository. This file holds **only** high-level coding and operational principles plus the architectural overview as it takes shape. It must never describe product functionality or per-feature behaviour. -The README.md holds the project-structure overview, the build instructions, -and the release process. +The README.md holds the project-structure overview and the setup instructions +for local development and releasing. -**This repository will be public.** Never commit secrets, tokens, or customer -data: the entire git history ships when the repository opens. +**This repository is public.** Never commit secrets, tokens, or customer data: +the entire git history ships. + +**Built in tandem with the server.** The server lives at +`~/projects/superstack-next` (github.com/siliconwitchery/superstack-next) and +owns the JSON API this binary speaks. A change on either side of the wire +usually implies one on the other, so read its CLAUDE.md before changing +anything that crosses it. The two files share the coding principles below +verbatim; a change to one belongs in both. ## Coding principles @@ -16,7 +23,9 @@ General and meant to be reused verbatim across projects. - **Complete names.** Use descriptive, whole-word names for non-trivial variables (`tuiWidth`, not `boxW`). Short names are acceptable only for receivers, loop indices, `err`, `ok`, and a framework's own idiomatic short - names, which stay reserved for that exact framework type. + names: `w` for an `http.ResponseWriter`, `r` for an `*http.Request`. Keep + those reserved for that exact type: a reader that is not an `*http.Request` + is `reader`, not `r`. - **Breathing room.** Separate a statement that produces a value from the statement that consumes it with a blank line. For example: an assignment, a blank line, then the `if err != nil` check. Group code into readable @@ -49,6 +58,14 @@ General and meant to be reused verbatim across projects. - **Table-driven tests.** Express tests as a table of input → expected cases iterated in a loop, not as repeated near-identical assertions. +## Operational principles + +- Build and run with cgo disabled for a static, dependency-free binary: + `CGO_ENABLED=0 go build`. Run `go vet` and `go test` with `CGO_ENABLED=0` + too, so neither reaches for a C toolchain that need not exist. +- Keep the dependency set small: nothing a distribution's packager would balk + at. + ## Architecture `main.go` is the composition root. It holds one table of sections and commands, @@ -65,14 +82,6 @@ Targets are flags rather than positions. Every command that touches devices takes `--fleet` or `--device`, so an operation that can apply to one device or to many stays one verb instead of splitting into a noun-verb pair per level. -## Operational principles - -- Build and run with cgo disabled for a static, dependency-free binary: - `CGO_ENABLED=0 go build`. Run `go vet` and `go test` with `CGO_ENABLED=0` - too, so neither reaches for a C toolchain that need not exist. -- Keep the dependency set small: nothing a distribution's packager would balk - at. - ## Releases The version lives in one place, `version` in `main.go`. Bump it, then tag the @@ -108,6 +117,20 @@ running `goreleaser release --snapshot` rather than by rebuilding the same targets by hand, so the release workflow is never the first thing to exercise that config. +Every publisher carries `skip_upload: auto`, so a tag with a prerelease suffix +publishes a GitHub release and touches no package manager. That is how the +pipeline gets exercised without shipping. It leaves the four publisher pushes +themselves untested, which only a real tag proves. + +Three credentials sit behind the release, and the workflow's guard checks only +that they are present, never that they work. `TAP_GITHUB_TOKEN` is +fine-grained and scoped to the tap and the bucket. `WINGET_GITHUB_TOKEN` has to +be a classic token, because a fine-grained one cannot reach a repository +outside its resource owner and so cannot open the pull request against +Microsoft. `AUR_KEY` is a passphraseless SSH key registered with an AUR +account. GoReleaser reports a failed winget pull request without failing the +run, so that step needs checking by hand after a release. + The generated changelog is deliberately disabled, so a fresh release starts with an empty body. Write the notes into it afterwards; GoReleaser keeps an existing body and will not overwrite them on a re-run. @@ -139,3 +162,10 @@ security bullet when nothing was fixed. After adding or changing a major feature, re-read this file and update it so the principles and the architectural overview stay accurate. + +Keep the README minimal as the project grows: the structure overview and the +setup steps, nothing else. Numbered imperative steps, self-contained command +blocks, constraints stated bare. No rationale, no explanation of how something +works, no troubleshooting. Anything that explains rather than instructs belongs +in this file, and a step that needs a paragraph to justify it is a sign the +step itself is wrong. diff --git a/README.md b/README.md index 49968ff..4b43c6e 100644 --- a/README.md +++ b/README.md @@ -2,209 +2,146 @@ `superstack` is the command line interface to Superstack: sign in, claim devices, push Lua code, and stream events and logs from your fleet. It is a -single static binary talking to the Superstack server's JSON API. - -## Install - -Every option below needs a published release to exist first. - -### Linux and macOS +single static binary talking to the Superstack server's JSON API. The server is +a separate project; this repo is the CLI only. It is laid out as follows: ```sh -curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | sh -``` - -To install a specific version instead: - -```sh -curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | VERSION=1.2.3 sh +├── .github/dependabot.yml # Weekly action and module update PRs +├── .github/workflows # CI on pull requests, release on v* tags +├── .gitignore +├── .goreleaser.yaml # Build matrix and every publishing target +├── CLAUDE.md # Coding principles and architectural overview +├── flake.lock # Pins nixpkgs +├── flake.nix # The superstack package, and the dev shell +├── go.mod +├── install.sh # curl-to-shell installer for Linux and macOS +├── LICENSE +├── main.go # Entry point, version constant, and the command table +├── main_test.go +└── README.md ``` -### macOS, with Homebrew +## Install -```sh -brew install --cask siliconwitchery/tap/superstack -``` +Each option needs a published release. -### Windows, with winget +- **Linux and macOS.** Pin a version by putting `VERSION=1.2.3` on the `sh` + side of the pipe. -```sh -winget install SiliconWitchery.Superstack -``` + ```sh + curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | sh + ``` -### Windows, with Scoop +- **Homebrew:** -```sh -scoop bucket add siliconwitchery https://github.com/siliconwitchery/scoop-bucket -scoop install superstack -``` + ```sh + brew install --cask siliconwitchery/tap/superstack + ``` -### Arch Linux +- **winget:** -```sh -yay -S superstack-bin -``` + ```sh + winget install SiliconWitchery.Superstack + ``` -### Nix +- **Scoop:** -Superstack is not in nixpkgs yet. Until it is, install it from this flake: + ```sh + scoop bucket add siliconwitchery https://github.com/siliconwitchery/scoop-bucket + scoop install superstack + ``` -```sh -nix profile install github:siliconwitchery/superstack-cli -``` +- **Arch Linux:** -Or run it without installing anything: + ```sh + yay -S superstack-bin + ``` -```sh -nix run github:siliconwitchery/superstack-cli -``` +- **Nix.** Not in nixpkgs yet. -### Any platform, by hand + ```sh + nix profile install github:siliconwitchery/superstack-cli + ``` -Download an archive for your platform from the -[releases page](https://github.com/siliconwitchery/superstack-cli/releases), -unpack it, and move `superstack` somewhere on your `PATH`. +- **Any platform.** Download an archive from the + [releases page](https://github.com/siliconwitchery/superstack-cli/releases), + unpack it, and move `superstack` onto your `PATH`. -## Build +## Local development 1. Install [Nix](https://nixos.org) with flakes enabled. -2. Clone the repository: - - ```sh - git clone git@github.com:siliconwitchery/superstack-cli.git - cd superstack-cli - ``` - -3. Enter the dev shell: +1. Clone the repository and enter the dev shell: ```sh + git clone git@github.com:siliconwitchery/superstack-cli.git ~/projects/superstack-cli + cd ~/projects/superstack-cli nix develop ``` -4. Build and run: +1. Build and run: ```sh go build -o superstack . ./superstack ``` -## Release - -### One-time setup +## Release setup -Every step here is required before the first release. A release fails if any -of the three secrets is missing. +Do everything below once. -#### Homebrew and Scoop +1. Create public repositories `siliconwitchery/homebrew-tap` and + `siliconwitchery/scoop-bucket`, each with a README. -1. Create a public repository `siliconwitchery/homebrew-tap`, ticking "Add a - README file" so it has a default branch. - -2. Create a public repository `siliconwitchery/scoop-bucket`, ticking "Add a - README file" so it has a default branch. +1. Fork `microsoft/winget-pkgs` into `siliconwitchery`. -3. Go to Settings > Developer settings > Personal access tokens > Fine-grained - tokens, and generate a token with: +1. Add a fine-grained token (Settings > Developer settings > Personal access + tokens) as the Actions secret `TAP_GITHUB_TOKEN`: - Resource owner: `siliconwitchery` - - Repository access: only `homebrew-tap` and `scoop-bucket` + - Repository access: `homebrew-tap` and `scoop-bucket` - Permissions: Contents, read and write -4. In this repository, go to Settings > Secrets and variables > Actions > New - repository secret. Name it `TAP_GITHUB_TOKEN` and paste the token in. - -#### winget - -1. Fork `microsoft/winget-pkgs` into `siliconwitchery`. +1. Add a classic token with the `public_repo` scope as the Actions secret + `WINGET_GITHUB_TOKEN`. A fine-grained token cannot open the pull request. -2. Go to Settings > Developer settings > Personal access tokens > Tokens - (classic), and generate a token with the `public_repo` scope. It has to be - a classic token, because a fine-grained token cannot open a pull request - against a repository outside its resource owner. - -3. In this repository, add it as an Actions secret named - `WINGET_GITHUB_TOKEN`. - -#### AUR - -1. Register an account at - [aur.archlinux.org/register](https://aur.archlinux.org/register). - -2. Generate an SSH key with no passphrase: +1. Register at [aur.archlinux.org](https://aur.archlinux.org/register), then: ```sh ssh-keygen -t ed25519 -N "" -f aur_key - ``` - -3. Print the public key and paste it into "SSH Public Key" in your AUR account - settings: - - ```sh - cat aur_key.pub - ``` - -4. Print the private key and add it to this repository as an Actions secret - named `AUR_KEY`: - - ```sh - cat aur_key - ``` - -5. Delete both key files: - - ```sh + cat aur_key.pub # paste into SSH Public Key in your AUR account settings + cat aur_key # add as the Actions secret AUR_KEY rm aur_key aur_key.pub ``` -### Cutting a release +1. Add one ruleset (Settings > Rules > Rulesets) targeting the default branch: + require a pull request with 0 approvals, require the `build` status check, + block force pushes, restrict deletions. Add a second targeting `v*` tags: + block force pushes, restrict deletions. -1. Bump `version` in `main.go`. The tag you push next must match it exactly, or - the release workflow refuses to build. +## Releasing -2. Commit and push: +1. Bump `version` in `main.go`, then open and merge a pull request: ```sh + git checkout -b version-0.1.0 git commit -am "Version 0.1.0" - git push + git push -u origin version-0.1.0 ``` -3. Tag that commit and push the tag: +1. Merging creates a new commit, and the tag has to point at that one: ```sh + git checkout main && git pull + grep '^const version' main.go git tag v0.1.0 git push origin v0.1.0 ``` -4. Wait for the release workflow to finish. It builds static binaries for - Linux, macOS, and Windows on amd64 and arm64, publishes the GitHub release - with checksums, updates the Homebrew tap and the Scoop bucket, pushes to - the AUR, and opens a winget pull request. +1. Write the release notes into the empty release body on GitHub, following the + shape in CLAUDE.md. -5. Write the release notes into the release body on GitHub. It starts empty by - design. Follow the shape in CLAUDE.md. - -6. Check that the winget pull request opened against `microsoft/winget-pkgs`. - It is the one step GoReleaser reports without failing the run, so a failure - there is easy to miss. - -## Repository layout - -```sh -├── .github/dependabot.yml # Weekly action and module update PRs -├── .github/workflows # CI on PRs and pushes to main, release on v* tags -├── .gitignore -├── .goreleaser.yaml # Build matrix and every publishing target -├── CLAUDE.md # Coding principles and release conventions -├── flake.lock # Pinned nixpkgs for the package and the dev shell -├── flake.nix # The superstack package, and the dev shell -├── go.mod -├── install.sh # curl-to-shell installer for Linux and macOS -├── LICENSE -├── main.go # Entry point, version constant, and the command table -├── main_test.go -└── README.md -``` +1. Check that the winget pull request opened against `microsoft/winget-pkgs`. -CI runs `goreleaser release --snapshot` on every pull request, so a mistake in -`.goreleaser.yaml` fails there rather than halfway through a tagged release. +A tag carrying a prerelease suffix, `v0.0.2-rc1`, publishes a GitHub prerelease +and skips every package manager. Tags cannot be moved or deleted.