# Install-on-demand bootstrap

How an adapter ships spt-core *with itself*. The contract: **the canonical
install one-liner is also every adapter's pack-in installer** — there is no
second mechanism, no vendored binary, no bespoke fetch logic to maintain.
Your adapter checks for `spt`, and runs the official script when it's
missing.

The scripts are non-interactive **by construction** (they run unattended),
idempotent (safe to re-run), sha256-verify what they download, and register
the user PATH. Served from the permanent canonical URL:

- `https://sabermage.github.io/spt-releases/install.sh`
- `https://sabermage.github.io/spt-releases/install.ps1`

## The generic contract

```text
if `spt` is on PATH        -> done (optionally check `spt --version` ≥ your floor)
else                       -> run the official one-liner for the OS
then                       -> first invocation may need the absolute path (Windows)
then                       -> register your manifest: spt adapter add --github <org>/<repo>
```

After first install, spt-core keeps itself current (signed self-update), so the
bootstrap can leave upgrades to spt-core. The remaining bootstrap step is to
register your adapter — see
[Activate the adapter](#activate-the-adapter--register-your-manifest) below.

## Check-and-install: POSIX sh

Drop this into your adapter's bootstrap (plugin install step, postinstall
script, first-run guard):

```sh
if ! command -v spt >/dev/null 2>&1; then
  echo "spt-core not found - installing..."
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
  # current shell may not see the PATH update yet:
  SPT="$HOME/.local/bin/spt"
else
  SPT="spt"
fi
"$SPT" --version
```

## Check-and-install: PowerShell

```powershell
if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {
    Write-Output "spt-core not found - installing..."
    irm https://sabermage.github.io/spt-releases/install.ps1 | iex
    # The user-PATH registration only reaches NEW terminals -- use the
    # absolute install path for everything in THIS process:
    $spt = Join-Path $env:LOCALAPPDATA 'spt-core\bin\spt.exe'
} else {
    $spt = 'spt'
}
& $spt --version
```

## Activate the adapter — register your manifest

Installing the binary is the first half of a pack-in; registering your manifest
is the second. Installing the binary makes `spt` available; **`spt adapter add`
activates your adapter** — registration is what lights up its profiles,
`[strings]` bodies, `[digest]` extractor, and hooks and makes it show in
`spt adapter list`. So the step right after the binary check is registering the
manifest:

```sh
# after `spt` is confirmed present (above):
# from a GitHub release — ships built binaries, source-free, versioned:
"$SPT" adapter add --release <your-org>/<your-adapter-repo>               # latest
"$SPT" adapter add --release <your-org>/<your-adapter-repo> --tag v1.0.0  # pinned
# ...or clone a repo whose ROOT holds manifest.toml:
"$SPT" adapter add --github <your-org>/<your-adapter-repo>
# ...or a local directory your harness ships:
"$SPT" adapter add ./adapter
```

`adapter add` is **manifest-first** — a clean add proves the cross-field manifest
shape — and it conducts your
[`[update]`](manifest.md#update--adapter-self-update) avenue once (install is the
first update). Confirm with `spt adapter list`: your adapter and its version
appear. Keep this idempotent in your bootstrap the same way the binary check is —
register when `adapter list` shows your adapter missing or below the expected
version.

**`--release` is the recommended distribution.** It fetches a `.spt` archive
asset — a tar whose root holds `manifest.toml` + `strings/` + the binaries the
manifest points at — from the named GitHub release, extracts it to the durable
registry home, and registers the root. That ships your **built binaries**,
source-free and **versioned by tag** (`--tag`, default the latest release), and
first-acquisition trusts HTTPS + GitHub exactly like the install one-liner's
first binary fetch. A development **monorepo stays a monorepo**: your release CI
packs the archive (`tar -czf adapter.spt manifest.toml strings/ bin/…`) and
uploads it as a release asset, so the adapter ships straight from your existing
repo. Override the asset name with `--asset` (default `adapter.spt`).

**Cover several platforms in one `.spt` (since v0.13.2).** To ship binaries for
more than one OS/arch in a single asset, add a **target-triple subdirectory** at
the archive root per platform and put that platform's binaries inside it, leaving
the shared `manifest.toml` + `strings/` at the root:

```text
adapter.spt
├── manifest.toml                # shared — at the root
├── strings/                     # shared — at the root
├── x86_64-pc-windows-msvc/      # one platform's binaries…
│   └── bin/…                    #   …in the same relative layout a flat .spt uses
└── x86_64-unknown-linux-gnu/
    └── bin/…
```

On install, spt-core extracts the shared root plus **only the current node's
triple**, flattened into the install dir — so the bare-name
`<install_dir>/<program>` resolution above is unchanged; mirror, under each
triple, exactly the per-platform tree a flat `.spt` would place at the root. The
recognized triples are `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu`; a
root subdirectory whose name is **not** a recognized triple is treated as a shared
root entry (so binaries for other platforms still ship as **separate
single-platform assets**, one selected per node with `--asset`). A multi-platform
archive that lacks the recipient's triple is refused with a clear
`NoArtifactForPlatform` error — never a silent partial install — and requires
`min_spt_core_version >= 0.13.2`. A flat archive (no triple subdirectories)
installs exactly as before.

`--github` is the alternative for an adapter whose **repo root already holds
`manifest.toml`**: it clones the repo and registers the clone root (`adapter add`
resolves a directory source to `<dir>/manifest.toml` at the root). Local
development uses the directory form, which takes any path or filename:
`spt adapter add ./adapters/my-adapter.toml`.

What registration holds under `adapters/<name>/` follows your
[`[update]`](manifest.md#update--adapter-self-update) avenue: a `delegated` or
`gh_release` adapter is **pointer-mode** (the manifest and `strings/` are read
live from the durable home), and a `file_pull` (or avenue-less) adapter is
**copy-mode** (the `manifest.toml` and `strings/` are copied in). Publish the
binaries your manifest references in the `.spt` (or repo) too, and reference them
**by bare name**: since v0.8.0, a command template's program token resolves
against the adapter's install dir before `PATH`, so a `.spt` that ships its
binaries is **self-contained** — the shipped binary is found without any PATH
placement. (Absolute paths still work; an unshipped tool still falls back to
`PATH`.) This applies to the `[session.psyche_init]` runner, the `[digest]`
extractor, and `spt adapter digest-proof`.

> **"Install the plugin, get the adapter for free" — include the activation
> step.** The [`[update]`](manifest.md#update--adapter-self-update) avenues
> keep a *registered* adapter current. The straightforward path for a
> `--release`-distributed adapter is **`gh_release`** (since v0.8.0): declare
> `avenue = "gh_release", repo = "your-org/your-adapter"` and
> `spt adapter update` ships the latest release `.spt` to the node — fetched,
> optionally verified against your `signing_key`, re-extracted, and
> re-registered. The other avenues: `delegated` (your harness's own updater
> installs the content — set `self_verifies = true` to attest it verifies what
> it installs), and `file_pull` (its automatic network-pull transport is **on
> the roadmap**). Deliver the manifest with `adapter add --release` (or
> `--github`, or a packed local dir) and let `gh_release` carry updates.

## The Windows PATH-refresh gotcha

The installer registers the binary directory on the **user** PATH via the
registry. Registry PATH changes reach **new** processes; an already-running
process — including the terminal (and *your bootstrap*) that just ran the
installer — keeps the PATH it started with.

So: **the first invocation after an install must use the absolute path**
(`%LOCALAPPDATA%\spt-core\bin\spt.exe`; the installer prints it). Every new
terminal after that finds `spt` normally. The snippets above bake this in.
On Linux the equivalent (a `~/.profile` entry the current shell hasn't
sourced) is handled the same way: `$HOME/.local/bin/spt` absolutely, once.

## Pinning and air-gapped installs

The scripts are configured by environment knobs, so the pipe-to-shell form
stays canonical:

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Install a specific release tag instead of latest |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_ASSET_BASE` | A URL or local directory holding the release assets + `SHA256SUMS` directly (CI, air-gap, mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

Example — pin a version inside a CI job:

```sh
SPT_INSTALL_VERSION=v0.1.0 \
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

## Trust model

First fetch trusts HTTPS + GitHub and verifies the binary's sha256 against
the release's `SHA256SUMS`. From then on, `spt update` performs full Ed25519
signature verification against the two-key trust anchor embedded in every
binary — so the installer is the strong link only once.
