Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
223 changes: 80 additions & 143 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.