diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9260d20 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,87 @@ +# Contributing to the Akua CLI + +This file covers building, testing, and releasing this repository's source. +If you only want to use the `akua` executable, see [README.md](README.md) +instead. + +## Prerequisites + +[mise](https://mise.jdx.dev/) manages the pinned Bun toolchain: + +```sh +mise install +bun install --frozen-lockfile +mise run check +mise run build:binary +./dist/akua --version +./dist/akua --help +./dist/akua commands --limit 1 +``` + +`mise run check` runs the drift check, typecheck/build, and tests — the same +gate CI runs. Run it before opening a PR. + +## Command generation + +The command surface is generated from the public source of truth, +`https://api.akua.dev/v1/openapi.json`: + +```sh +mise run spec:fetch # fetch and stably format openapi/public.json +mise run generate # regenerate the command registry and typed API bindings +mise run generate:check # fail if committed generated output has drifted +``` + +Generation is deterministic and operationId-driven; only operations marked +`x-platform-visibility: PUBLIC` are included, and registry rows are sorted by +operationId. The generated outputs are `src/generated/commands.gen.ts`, +`src/generated/openapi-api.gen.ts`, and +`src/generated/public-operation-executor.gen.ts`. Never hand-edit generated +files; run `mise run generate` and commit the result. + +See [docs/architecture.md](docs/architecture.md) for the full command +derivation rules, the API/auth/config model, output modes, exit codes, and +the rest of the CLI's design contract. + +## Testing + +```sh +bun test +``` + +`mise run check` (drift check, build, tests) is the required gate before +release changes; see [docs/architecture.md](docs/architecture.md#testing-strategy) +for what current test coverage includes. + +## Release process + +`mise run release:package` cross-compiles all five targets, creates archives +and checksums in `dist/release`, and verifies their manifest. +`mise run release:verify` re-verifies an already-packaged release directory. +`mise run release:smoke` extracts and runs the artifact for the current +supported host. CI repeats native smoke tests on every platform in the +release matrix (macOS arm64/x64, glibc Linux arm64/x64, Windows x64). + +Release Please creates the version tag and GitHub Release. Its own workflow +then calls artifact publication directly, so publication does not depend on a +tag event that GitHub may suppress for job-token-created tags. Uploads do not +clobber existing assets. Only after downloading and re-verifying the published +assets does the workflow dispatch the Homebrew manifest URL. The +`HOMEBREW_TAP_TOKEN` secret must be a fine-grained credential scoped only to +the tap repository's dispatch permission; failures remain visible as release +job failures. + +`akua-dev/homebrew-tap` owns the `akua` formula, formula tests, and the +reviewed formula-update PR. This repository requests a formula PR only after +every archive has passed a native install smoke test and all published assets +have passed post-upload verification; it never pushes formula commits itself. + +`scripts/release.ts` is the source of truth for target IDs, Bun targets, +archive names, executable names, SHA-256 files, and release manifests. + +## Repository-specific engineering rules + +See [AGENTS.md](AGENTS.md) for durable, repository-wide engineering rules +(Effect v4 production code conventions, the release contract, ownership +boundaries, and the public API command contract) that apply to any change in +`src/` or `scripts/`. diff --git a/README.md b/README.md index d723cd7..fcc5a41 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,19 @@ # Akua CLI -`akua` is the public Akua Cloud command-line interface. It is a self-contained -Bun/TypeScript executable for humans, automation, and coding agents. The current -MVP implements local token authentication, browser/device login, adaptive -structured output, and executable public operationId-driven commands. Effect -CLI renders the interactive command tree and validates its documented flags; -the generated typed Effect API and its static executor are derived from OpenAPI -for every public operation. +`akua` drives Akua Cloud from a terminal: create Kubernetes clusters, add +machines, package an application, and install it, all from one command. It is +a single self-contained executable, built for three audiences — a person +typing commands interactively, a CI pipeline calling it non-interactively, and +a coding agent driving it programmatically — and every command adapts its +output to whichever one is running it. + +Every command is generated directly from Akua's public API, so the CLI never +drifts out of sync with what the platform can actually do. The canonical executable is `akua`; there is no `cnap` compatibility binary. ## Install -GitHub Releases and Homebrew are the supported install channels. Every GitHub -archive contains the `akua` executable and its adjacent target-native package -runtime, and has an adjacent SHA-256 file. A release also publishes -`checksums.txt`, a complete release manifest, and the exact Homebrew -artifact/checksum mapping. - -### Homebrew - ```sh brew install akua-dev/tap/akua akua --version @@ -35,115 +29,21 @@ brew update brew upgrade akua ``` -The formula is maintained in `akua-dev/homebrew-tap`. A CLI release requests a -reviewed formula PR only after every archive has passed a native install smoke -test and all published assets have passed post-upload verification. +The formula is maintained in `akua-dev/homebrew-tap`. -### GitHub Release: macOS or Linux +No Homebrew? See [manual install](docs/install.md) for checksummed release +archives. -This copy-paste example installs v0.9.0 into `~/.local/bin`. Change `VERSION` -when selecting a newer release. - -```sh -set -eu -VERSION=0.9.0 -case "$(uname -s)-$(uname -m)" in - Darwin-arm64) TARGET=darwin-arm64 ;; - Darwin-x86_64) TARGET=darwin-x64 ;; - Linux-arm64|Linux-aarch64) TARGET=linux-arm64 ;; - Linux-x86_64) TARGET=linux-x64 ;; - *) echo "Unsupported platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;; -esac -ASSET="akua-v${VERSION}-${TARGET}.tar.gz" -BASE="https://github.com/akua-dev/cli/releases/download/v${VERSION}" -curl --fail --location --remote-name "${BASE}/${ASSET}" -curl --fail --location --remote-name "${BASE}/${ASSET}.sha256" -if command -v sha256sum >/dev/null 2>&1; then - sha256sum --check "${ASSET}.sha256" -else - shasum -a 256 --check "${ASSET}.sha256" -fi -INSTALL_ROOT="$HOME/.local/libexec/akua-v${VERSION}" -mkdir -p "$INSTALL_ROOT" "$HOME/.local/bin" -tar -xzf "$ASSET" -C "$INSTALL_ROOT" -ln -sfn "$INSTALL_ROOT/akua" "$HOME/.local/bin/akua" -"$HOME/.local/bin/akua" --version -"$HOME/.local/bin/akua" --help -"$HOME/.local/bin/akua" commands --limit 1 -"$HOME/.local/bin/akua" pkg version -``` - -Ensure `~/.local/bin` is on `PATH`. Manual upgrades repeat these steps with a -newer `VERSION`, replacing `~/.local/bin/akua`. The CLI does not self-update. - -### GitHub Release: Windows x64 - -Run in PowerShell. This installs v0.9.0 into `%USERPROFILE%\bin`; add that -directory to the user `PATH` if it is not already present. - -```powershell -$ErrorActionPreference = "Stop" -$Version = "0.9.0" -$Asset = "akua-v$Version-windows-x64.zip" -$Base = "https://github.com/akua-dev/cli/releases/download/v$Version" -Invoke-WebRequest "$Base/$Asset" -OutFile $Asset -Invoke-WebRequest "$Base/$Asset.sha256" -OutFile "$Asset.sha256" -$Expected = ((Get-Content "$Asset.sha256") -split "\s+")[0].ToLower() -$Actual = (Get-FileHash $Asset -Algorithm SHA256).Hash.ToLower() -if ($Actual -ne $Expected) { throw "SHA-256 mismatch for $Asset" } -Expand-Archive $Asset -DestinationPath .\akua-release -Force -$InstallRoot = "$HOME\libexec\akua-v$Version" -New-Item -ItemType Directory -Force "$InstallRoot" | Out-Null -Copy-Item .\akua-release\* "$InstallRoot" -Recurse -Force -New-Item -ItemType Directory -Force "$HOME\bin" | Out-Null -Copy-Item "$InstallRoot\akua.exe" "$HOME\bin\akua.exe" -Force -Copy-Item "$InstallRoot\node_modules" "$HOME\bin\node_modules" -Recurse -Force -& "$HOME\bin\akua.exe" --version -& "$HOME\bin\akua.exe" --help -& "$HOME\bin\akua.exe" commands --limit 1 -& "$HOME\bin\akua.exe" pkg version -``` - -### Supported release artifacts - -| Platform | Architecture | Asset | Runtime baseline | -| --- | --- | --- | --- | -| macOS | Apple Silicon arm64 | `akua-v0.9.0-darwin-arm64.tar.gz` | Bun darwin arm64 | -| macOS | Intel x64 | `akua-v0.9.0-darwin-x64.tar.gz` | Bun darwin x64 | -| Linux | glibc arm64 | `akua-v0.9.0-linux-arm64.tar.gz` | Bun linux arm64 | -| Linux | glibc x64 | `akua-v0.9.0-linux-x64.tar.gz` | Bun linux x64 baseline | -| Windows | x64 | `akua-v0.9.0-windows-x64.zip` | Bun windows x64 baseline | - -Linux musl, Windows arm64, and other systems are not in the tested release -contract. x64 Linux and Windows use Bun's baseline target for older CPUs. Unix -archives preserve executable mode `0755`; the Windows ZIP contains `akua.exe`. -All archives also contain the target-native package runtime under -`node_modules/@akua-dev`. Keep that directory adjacent to the executable. Bun is -not required on the target machine. - -To audit a whole release, download `checksums.txt` plus the archives and run -`sha256sum --check checksums.txt` on Linux or `shasum -a 256 --check -checksums.txt` on macOS. The adjacent `.sha256` files support single-asset -verification. Release assets are never replaced in place; a changed binary -requires a new version. - -## First use and authentication - -Inspect the installed surface first: +## First commands ```sh akua # complete interactive command tree akua auth --help # authentication subcommands and options akua workspaces --help # generated resource commands -akua commands --limit 5 +akua commands --limit 5 # discover the full command surface ``` -For CI and coding agents, prefer an ephemeral environment credential: - -```sh -export AKUA_API_TOKEN='sk_akua_...' -akua auth status -``` +## Sign in For an interactive browser/device login: @@ -159,6 +59,14 @@ the verification in any browser instead. akua auth login --no-browser ``` +For CI and coding agents, prefer an ephemeral environment credential instead of +an interactive login: + +```sh +export AKUA_API_TOKEN='sk_akua_...' +akua auth status +``` + For a local persisted token without an interactive login: ```sh @@ -173,10 +81,11 @@ akua auth logout removes only the stored `token`, also preserving unknown config keys, and cannot clear `AKUA_API_TOKEN` from the parent process. -## Human and agent output +## Built for humans, CI, and agents -An interactive TTY defaults to human prose. The CLI defaults to compact agent -output when any of these signals are active: +An interactive TTY defaults to human prose. The CLI switches to compact agent +output automatically when any of these signals are active, so an agent gets +usable output without extra flags: - `AGENT=true` or `AGENT=` (for example `AGENT=codex`); - a detected provider environment such as Codex, Claude Code, Cursor, Aider, @@ -200,65 +109,35 @@ The supported modes are `human`, `agent`, `json`, and `quiet`. Success data is written to stdout; progress and warnings belong on stderr. Unknown commands, flags, and output modes fail loudly with stable nonzero exit codes. -## OpenAPI command generation +## Discover and run commands -The public source of truth is -`https://api.akua.dev/v1/openapi.json`. The workflow is deliberately explicit: +Every public Akua operation is available as a generated command, kept current +with the API automatically. Discover the current surface instead of relying on +a fixed list: ```sh -mise run spec:fetch # fetch and stably format openapi/public.json -mise run generate # derive command registry and typed Effect API -mise run generate:check # fail if committed generated output has drifted -mise run check # drift check, typecheck/build, and tests +akua commands --json +akua commands --resource workspaces +akua commands --operation-id workspaces.list ``` -Generation is deterministic and operationId-driven. Only operations marked -`x-platform-visibility: PUBLIC` are included. For example, -`operationId: workspaces.list` becomes `akua workspaces list`; registry rows are -sorted by operationId. The generated outputs are -`src/generated/commands.gen.ts`, `src/generated/openapi-api.gen.ts`, and -`src/generated/public-operation-executor.gen.ts`. - -Generated API commands execute through the typed Effect client and accept one -JSON object from stdin or a named file. Its only keys are `path`, `query`, -`headers`, and `body`: +For example, `operationId: workspaces.list` becomes `akua workspaces list`. +Generated commands accept one JSON object from stdin or a named file. Its only +keys are `path`, `query`, `headers`, and `body`: ```sh printf '{"query":{"limit":5}}' | akua workspaces list --input - akua machines create --input - < ./machine.json ``` -Request input is schema-validated before transport and never included in -diagnostics. The CLI remains provider-neutral: it has no provider-specific commands, +Request input is schema-validated before it is sent and never included in +diagnostics. The CLI stays provider-neutral: it has no provider-specific commands, flags, environment variables, or credential loaders. Provider-specific values -belong only in the generated public API request body. +belong only in the generated request body. -## Development and release validation +## Learn more -Prerequisites are [mise](https://mise.jdx.dev/) and the pinned Bun toolchain: - -```sh -mise install -bun install --frozen-lockfile -mise run check -mise run build:binary -./dist/akua --version -./dist/akua --help -./dist/akua commands --limit 1 -``` +The full command reference and platform guides live at +[docs.akua.dev](https://docs.akua.dev). -`mise run release:package` cross-compiles all five targets, creates archives and -checksums in `dist/release`, and verifies their manifest. `mise run -release:smoke` extracts and runs the artifact for the current supported host. -CI repeats native smoke tests on every platform in the table. - -Release Please creates the version tag and GitHub Release. Its own workflow then -calls artifact publication directly, so publication does not depend on a tag -event that GitHub may suppress for job-token-created tags. Uploads do not -clobber existing assets. Only after downloading and re-verifying the published -assets does the workflow dispatch the Homebrew manifest URL. The -`HOMEBREW_TAP_TOKEN` secret must be a fine-grained credential scoped only to the -tap repository's dispatch permission; failures remain visible as release job -failures. - -See [docs/architecture.md](docs/architecture.md) for the broader CLI contract. +Contributing to this repo? See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..fb7f752 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,98 @@ +# Manual install from GitHub Releases + +[Homebrew](../README.md#install) is the supported install path for almost +everyone. Use this page only when Homebrew isn't available — for example, a +Linux host without Homebrew, or a CI environment that wants a pinned, +checksummed binary instead of going through a package manager. + +Every GitHub Release archive contains the `akua` executable and its +adjacent target-native package runtime, and has an adjacent SHA-256 file. A +release also publishes `checksums.txt` and a complete release manifest. + +## macOS or Linux + +This copy-paste example installs v0.10.1 into `~/.local/bin`. Change `VERSION` +when selecting a newer release. + +```sh +set -eu +VERSION=0.10.1 +case "$(uname -s)-$(uname -m)" in + Darwin-arm64) TARGET=darwin-arm64 ;; + Darwin-x86_64) TARGET=darwin-x64 ;; + Linux-arm64|Linux-aarch64) TARGET=linux-arm64 ;; + Linux-x86_64) TARGET=linux-x64 ;; + *) echo "Unsupported platform: $(uname -s)-$(uname -m)" >&2; exit 1 ;; +esac +ASSET="akua-v${VERSION}-${TARGET}.tar.gz" +BASE="https://github.com/akua-dev/cli/releases/download/v${VERSION}" +curl --fail --location --remote-name "${BASE}/${ASSET}" +curl --fail --location --remote-name "${BASE}/${ASSET}.sha256" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum --check "${ASSET}.sha256" +else + shasum -a 256 --check "${ASSET}.sha256" +fi +INSTALL_ROOT="$HOME/.local/libexec/akua-v${VERSION}" +mkdir -p "$INSTALL_ROOT" "$HOME/.local/bin" +tar -xzf "$ASSET" -C "$INSTALL_ROOT" +ln -sfn "$INSTALL_ROOT/akua" "$HOME/.local/bin/akua" +"$HOME/.local/bin/akua" --version +"$HOME/.local/bin/akua" --help +"$HOME/.local/bin/akua" commands --limit 1 +"$HOME/.local/bin/akua" pkg version +``` + +Ensure `~/.local/bin` is on `PATH`. Manual upgrades repeat these steps with a +newer `VERSION`, replacing `~/.local/bin/akua`. The CLI does not self-update. + +## Windows x64 + +Run in PowerShell. This installs v0.10.1 into `%USERPROFILE%\bin`; add that +directory to the user `PATH` if it is not already present. + +```powershell +$ErrorActionPreference = "Stop" +$Version = "0.10.1" +$Asset = "akua-v$Version-windows-x64.zip" +$Base = "https://github.com/akua-dev/cli/releases/download/v$Version" +Invoke-WebRequest "$Base/$Asset" -OutFile $Asset +Invoke-WebRequest "$Base/$Asset.sha256" -OutFile "$Asset.sha256" +$Expected = ((Get-Content "$Asset.sha256") -split "\s+")[0].ToLower() +$Actual = (Get-FileHash $Asset -Algorithm SHA256).Hash.ToLower() +if ($Actual -ne $Expected) { throw "SHA-256 mismatch for $Asset" } +Expand-Archive $Asset -DestinationPath .\akua-release -Force +$InstallRoot = "$HOME\libexec\akua-v$Version" +New-Item -ItemType Directory -Force "$InstallRoot" | Out-Null +Copy-Item .\akua-release\* "$InstallRoot" -Recurse -Force +New-Item -ItemType Directory -Force "$HOME\bin" | Out-Null +Copy-Item "$InstallRoot\akua.exe" "$HOME\bin\akua.exe" -Force +Copy-Item "$InstallRoot\node_modules" "$HOME\bin\node_modules" -Recurse -Force +& "$HOME\bin\akua.exe" --version +& "$HOME\bin\akua.exe" --help +& "$HOME\bin\akua.exe" commands --limit 1 +& "$HOME\bin\akua.exe" pkg version +``` + +## Supported release artifacts + +| Platform | Architecture | Asset | Runtime baseline | +| --- | --- | --- | --- | +| macOS | Apple Silicon arm64 | `akua-v0.10.1-darwin-arm64.tar.gz` | Bun darwin arm64 | +| macOS | Intel x64 | `akua-v0.10.1-darwin-x64.tar.gz` | Bun darwin x64 | +| Linux | glibc arm64 | `akua-v0.10.1-linux-arm64.tar.gz` | Bun linux arm64 | +| Linux | glibc x64 | `akua-v0.10.1-linux-x64.tar.gz` | Bun linux x64 baseline | +| Windows | x64 | `akua-v0.10.1-windows-x64.zip` | Bun windows x64 baseline | + +Linux musl, Windows arm64, and other systems are not in the tested release +contract. x64 Linux and Windows use Bun's baseline target for older CPUs. Unix +archives preserve executable mode `0755`; the Windows ZIP contains `akua.exe`. +All archives also contain the target-native package runtime under +`node_modules/@akua-dev`. Keep that directory adjacent to the executable. Bun is +not required on the target machine. + +To audit a whole release, download `checksums.txt` plus the archives and run +`sha256sum --check checksums.txt` on Linux or `shasum -a 256 --check +checksums.txt` on macOS. The adjacent `.sha256` files support single-asset +verification. Release assets are never replaced in place; a changed binary +requires a new version. diff --git a/test/docs.test.ts b/test/docs.test.ts index 12d31aa..590c015 100644 --- a/test/docs.test.ts +++ b/test/docs.test.ts @@ -6,19 +6,38 @@ async function text(path: string): Promise { } describe("distribution documentation", () => { - test("README documents only the implemented GitHub Release and Homebrew channels", async () => { + test("README documents Homebrew as the primary install channel", async () => { const readme = await text("README.md"); expect(readme).toContain("brew install akua-dev/tap/akua"); - expect(readme).toContain("https://github.com/akua-dev/cli/releases/download/"); - expect(readme).toContain("akua-v0.9.0-darwin-arm64.tar.gz"); - expect(readme).toContain("akua-v0.9.0-windows-x64.zip"); - expect(readme).toContain("checksums.txt"); - expect(readme).toContain("sha256sum"); - expect(readme).toContain("Get-FileHash"); + expect(readme).toContain("brew upgrade akua"); expect(readme).not.toMatch(/npm (?:install|i)|bun add|npx .*@akua-dev\/cli/); }); + test("README points to docs/install.md instead of inlining GitHub Release steps", async () => { + const readme = await text("README.md"); + + expect(readme).toContain("[manual install](docs/install.md)"); + expect(readme).not.toContain("https://github.com/akua-dev/cli/releases/download/"); + expect(readme).not.toContain("akua-v0.10.1-darwin-arm64.tar.gz"); + expect(readme).not.toContain("akua-v0.10.1-windows-x64.zip"); + expect(readme).not.toContain("checksums.txt"); + expect(readme).not.toContain("sha256sum"); + expect(readme).not.toContain("Get-FileHash"); + }); + + test("docs/install.md documents the checksummed GitHub Release fallback", async () => { + const install = await text("docs/install.md"); + + expect(install).toContain("https://github.com/akua-dev/cli/releases/download/"); + expect(install).toContain("akua-v0.10.1-darwin-arm64.tar.gz"); + expect(install).toContain("akua-v0.10.1-windows-x64.zip"); + expect(install).toContain("checksums.txt"); + expect(install).toContain("sha256sum"); + expect(install).toContain("Get-FileHash"); + expect(install).toContain("Homebrew"); + }); + test("README explains auth, adaptive output, generated API status, and upgrades honestly", async () => { const readme = await text("README.md"); @@ -35,11 +54,8 @@ describe("distribution documentation", () => { "AGENT=true", "AGENT=", "non-TTY", - "mise run spec:fetch", - "mise run generate", - "mise run generate:check", "operationId", - "generated typed Effect API", + "generated directly from Akua's public API", "akua workspaces list --input -", "akua machines create --input -", "provider-specific commands", @@ -49,12 +65,57 @@ describe("distribution documentation", () => { } expect(readme).not.toContain("skills/akua/"); expect(readme).not.toContain("akua-dev/skills"); - expect(readme).toContain("Generated API commands execute"); + expect(readme).toContain("Generated commands accept one JSON object"); expect(readme).not.toContain("generic executor is not yet wired"); expect(readme).not.toContain("agent-skills-standard-following"); expect(readme).not.toContain("skills add akua-dev/skills"); }); + test("README treats the underlying framework as an implementation detail", async () => { + const readme = await text("README.md"); + + expect(readme).not.toMatch(/\bEffect\b/); + expect(readme).toContain("humans, CI, and agents"); + }); + + test("README stays user-facing and defers repo development to CONTRIBUTING.md", async () => { + const readme = await text("README.md"); + + expect(readme).toContain("docs.akua.dev"); + expect(readme).toContain("Contributing to this repo? See [CONTRIBUTING.md](CONTRIBUTING.md)."); + expect(readme).not.toContain("mise run spec:fetch"); + expect(readme).not.toContain("mise run generate"); + expect(readme).not.toContain("mise run generate:check"); + expect(readme).not.toContain("mise run release:package"); + expect(readme).not.toContain("mise run release:smoke"); + expect(readme).not.toContain("mise install"); + expect(readme).not.toContain("Release Please"); + expect(readme).not.toContain("HOMEBREW_TAP_TOKEN"); + }); + + test("CONTRIBUTING documents generation, testing, and release mechanics", async () => { + const contributing = await text("CONTRIBUTING.md"); + + for (const value of [ + "mise install", + "mise run check", + "mise run spec:fetch", + "mise run generate", + "mise run generate:check", + "bun test", + "mise run release:package", + "mise run release:verify", + "mise run release:smoke", + "Release Please", + "HOMEBREW_TAP_TOKEN", + "src/generated/commands.gen.ts", + "docs/architecture.md", + "AGENTS.md", + ]) { + expect(contributing).toContain(value); + } + }); + test("architecture records device authentication and all generated API artifacts", async () => { const architecture = await text("docs/architecture.md");