diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f66b8b3..1a7f923 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -370,6 +370,38 @@ jobs:
if ($out -match 'listening on') { throw "a sink that failed to bind claimed it was listening: $out" }
if ($err -match '\x1b') { throw "the collision message emitted ANSI escapes; it is read in pipes and pastes: $err" }
+ # packaging/install.ps1 is the `irm | iex` route (design.md §8, added
+ # 2026-08-18). It is the one shipped file no Go test can reach, and it
+ # runs on a stranger's machine before anything else telltale does.
+ #
+ # This step PARSES it and never executes it: running it would download a
+ # release on every push. It runs under Windows PowerShell 5.1 rather than
+ # pwsh, because 5.1 is the shell the trap lives in — it reads a BOM-less
+ # file as ANSI, so a single em dash anywhere in this file breaks the parse
+ # before the script runs. That was measured on 2026-08-18 and it cost four
+ # parser errors under 5.1.26100.9168 while pwsh 7.6.5 read the same file
+ # fine, which is exactly the shape of defect a gate has to hold.
+ - name: Gate (install.ps1 parses under Windows PowerShell 5.1, and is ASCII only)
+ shell: powershell
+ run: |
+ $path = (Resolve-Path packaging/install.ps1).Path
+ $bytes = [IO.File]::ReadAllBytes($path)
+ $high = @($bytes | Where-Object { $_ -ge 0x80 }).Count
+ if ($high -ne 0) {
+ throw "install.ps1 carries $high byte(s) at or above 0x80. Windows PowerShell 5.1 reads this BOM-less file as ANSI and the parse breaks. Keep it ASCII."
+ }
+ $errors = $null
+ $null = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$errors)
+ if ($errors.Count -gt 0) {
+ throw "install.ps1 does not parse: $($errors | ForEach-Object { $_.Message })"
+ }
+ # `exit` inside a piped script ends the user's own shell session, so
+ # the script throws instead. A future edit that reaches for `exit` is
+ # the regression this line catches.
+ if (Select-String -Path $path -Pattern '^\s*exit\b' -Quiet) {
+ throw "install.ps1 calls exit; a script run through irm | iex ends the user's shell that way. Throw instead."
+ }
+
# ADR-002's fast path, on the clock rather than on anecdote (design.md §5,
# amendment 2026-08-16). This runs LAST on purpose: it drives full.json,
# which carries rate_limits and therefore writes the quota relay, and two
diff --git a/README.md b/README.md
index 2e5c208..bd9f1a2 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,18 @@ Every number comes from measured tool output.
> A telltale is the ribbon on a sail. It shows the air. It does not interpret it.
+[](https://github.com/sanlee-ys/telltale/actions/workflows/ci.yml)
+
+
+
@@ -15,6 +27,17 @@ Every number comes from measured tool output.
+
+
**v0.2.0** (2026-08-14). Windows is verified on every commit.
Intel macOS is smoke-checked. `darwin_arm64` and `linux_amd64` are built, not run.
No binary is signed. Check `checksums.txt` on the release.
@@ -31,6 +54,18 @@ go build -o telltale.exe ./cmd/telltale
A source build reports `dev` from `telltale version`. A release binary reports its tag.
+**Windows, one paste** (measured against `v0.2.0`, 2026-08-18)
+
+```powershell
+irm https://raw.githubusercontent.com/sanlee-ys/telltale/main/packaging/install.ps1 | iex
+```
+
+It downloads the latest release, checks the archive against `checksums.txt`,
+refuses on a mismatch, and puts `telltale.exe` on your user `PATH`.
+It needs no administrator rights. The binary it installs is **not signed**,
+and the script says so before it names the next command.
+Source and knobs: [packaging/install.ps1](packaging/install.ps1).
+
**Windows, scoop** (exercised once, 2026-08-14)
```
@@ -57,8 +92,8 @@ passes, run `xattr -d com.apple.quarantine telltale`. Do not add that line
to the `curl` block: `curl` does not set the mark, and the command then
exits 1. The measured walk is in [SECURITY.md](SECURITY.md).
-**Windows, winget.** Not submitted. Use scoop or a source build.
-Draft: [packaging/](packaging/).
+**Windows, winget.** Not submitted. Use the one paste above, scoop, or a
+source build. Draft: [packaging/](packaging/).
Run `telltale` with no arguments for the first frame.
`telltale doctor` is the preflight. `telltale council` opens the room.
@@ -109,6 +144,28 @@ HUD flags: `--vendor all|claude|codex|gemini|agy|cursor|grok`,
`--hide gemini,cursor` (default from `TELLTALE_HUD_HIDE`),
`--ascii` (`TELLTALE_ASCII=1`), `--no-title`. `NO_COLOR` is honoured.
+## First five minutes
+
+Run `telltale doctor` first. It reports what is installed here, it probes no
+login and makes no network call, and it **exits 0 even when every seat is
+missing**. Read its own words before you read this table: each row below is
+keyed to a line `doctor` actually prints.
+
+| What you see | What it means | What to do |
+|---|---|---|
+| `0 checks passed`, and `no seat above passed every check that ran` | telltale is working. `council` drives a vendor CLI, and this machine has none. | Install one vendor CLI and run `doctor` again. `telltale hud` runs either way. |
+| `binary FAILED not found on PATH (looked for codex)` | This shell cannot resolve that vendor. | Open the shell you normally run the vendor in, or put its binary on `PATH`. `doctor` also finds a vendor at a known install location and says `a known install location, not on this shell's PATH`. |
+| `drivable FAILED` under a `binary ok` | The binary is here and council will not seat it. "Is it there" and "can it be driven" have different fixes, so `doctor` refuses to collapse them. | Read the reason on that row. It names the entry point and why: usually a shell shim that takes its prompt as an argument, which council will not put through `cmd.exe`. |
+| `auth not checked` and `network not checked`, on every seat, always | Not a failure and not a soft pass. This report probes neither. | Nothing. A seat that is installed and signed out reports its own auth failure on its column the first time you dispatch to it. |
+| `re-measure §3.x before trusting the fields this adapter sources` | Your vendor runs a version other than the one telltale surveyed. | Nothing on this machine. It is a staleness fact about telltale: no check failed, the tally is unchanged, and the command still exits 0. |
+| `telltale version` says `dev`, or an older tag, after the install | Another `telltale.exe` is earlier on `PATH`. The install script appends its directory rather than jumping the queue. | Run `Get-Command telltale`. It names the one that runs. Remove the other one, or set `TELLTALE_INSTALL_DIR` to the directory it already lives in. |
+| A column in the room stays empty after a dispatch | The seat answered nothing, or the vendor refused the turn. | The column carries the reason. [docs/council.md](docs/council.md) reads the badges and the phase words. |
+| Windows warns before the first run | The binary is unsigned. No telltale release carries an Authenticode signature ([docs/design.md §8](docs/design.md#s8), item 8). | Verify the archive against `checksums.txt`, which is the whole verification this release offers. [SECURITY.md](SECURITY.md) states what that does and does not prove. |
+| The statusline shows nothing, or `bad statusline input: unexpected end of JSON input` | The statusline is wired, not run. The vendor calls it and hands it JSON on stdin, so by hand it gets no payload. | Paste the `statusLine.command` block above, then start a session. |
+
+`telltale doctor` output pastes into an issue as it stands: it is plain text
+with no colour and no alternate screen, for exactly that reason.
+
## What it is
- **`telltale council`:** one brief, five vendor columns. This is the product.
diff --git a/SECURITY.md b/SECURITY.md
index e05a728..44cf059 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -85,11 +85,12 @@ not change any statement in this section.
The consequences, per platform:
-- **Windows.** The binary carries no Authenticode signature. `scoop` and `winget`
- install that same unsigned binary. A direct download through a browser can
- raise a Microsoft Defender SmartScreen prompt, because SmartScreen weighs the
- signature and the download reputation. This project has not measured that
- prompt.
+- **Windows.** The binary carries no Authenticode signature. `scoop`, `winget`
+ and `packaging/install.ps1` install that same unsigned binary, and the script
+ says so in its own output before it names the next command. A direct download
+ through a browser can raise a Microsoft Defender SmartScreen prompt, because
+ SmartScreen weighs the signature and the download reputation. This project has
+ not measured that prompt.
- **macOS.** The `darwin_amd64` and `darwin_arm64` archives are unsigned and not
notarized. macOS applies the `com.apple.quarantine` attribute to a file that a
browser downloads, and Gatekeeper then refuses to run an unsigned, un-notarized
@@ -111,7 +112,8 @@ The consequences, per platform:
**Verify the checksum.** Every release attaches `checksums.txt` with a SHA-256
for each archive. That file tells you the archive is the one the release workflow
produced. It does not tell you who produced it. `scoop` verifies the SHA-256
-itself from the manifest.
+itself from the manifest, and `packaging/install.ps1` verifies it against
+`checksums.txt` before it unpacks anything, deleting the download on a mismatch.
Signing is not planned work with a date. It needs a certificate or an Apple
Developer account that the owner holds, plus release secrets, so it is an owner
diff --git a/STATE.md b/STATE.md
index 4054ab8..bd2a214 100644
--- a/STATE.md
+++ b/STATE.md
@@ -331,6 +331,30 @@ Nothing open. The last one here was the 44 seconds, and it was measured
## Known gaps, not yet owned
+- **The launch playbook's outward half is OWNER WORK and is unowned
+ (2026-08-18).** [design.md §8](docs/design.md#s8)'s 2026-08-18 amendment
+ records the cadence; recording it is all a session may do. Three pieces wait
+ on the owner. **Directory listings** (`awesome-claude-code` and its
+ neighbours) are pull requests to other people's repositories, which takes
+ winget's ruling: a human action, never automated, never opened by a
+ contributor session — and the README badge slot fills only after a listing
+ merges. **The Show HN cadence** is sequenced behind chain link 3 and is
+ pinned to one hypothesis, so a second post tests its own feature's question
+ and says so. **The run-evidence bar's threshold is undecided**: §8 item 2
+ already fixes the KIND of evidence, and the count, the window, and what a
+ miss means are the owner's to name. The sweep's "10 runs in 30 days" is a
+ proposal with no measurement behind it and was deliberately not adopted.
+
+- **`packaging/install.ps1`'s mismatch refusal has no end-to-end live trial
+ (2026-08-18).** The happy path ran on PowerShell 7.6.5 and Windows
+ PowerShell 5.1.26100.9168 against the published `v0.2.0`, and two refusals
+ ran end to end (an arm64 machine, and a tag with no release). Driving the
+ checksum mismatch needs a host that serves a corrupted archive. The
+ comparison itself was measured live over the real `checksums.txt` and a
+ tampered copy of the real archive, and the hashes differed; the three lines
+ that act on that result are unexercised. §8's 2026-08-18 amendment carries
+ the record.
+
- **A live ordinary-turn give-up is owed on the reference box before
2026-09-30.** `x` on an ordinary turn shipped 2026-08-17 with offline tests
only. Whether a real vendor's interrupt lands mid-turn, and whether the
diff --git a/docs/design.md b/docs/design.md
index 9ec178f..71fe5e4 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -6845,7 +6845,7 @@ notarization stay owner decisions, and no contributor builds that pipeline.
| Check | Fires on | Runner | Fails on |
|---|---|---|---|
-| `ci.yml` | push to main, pull request, release | windows-latest, ubuntu-latest | vet, the suite, the build, the binary smokes, the schema gate |
+| `ci.yml` | push to main, pull request, release | windows-latest, ubuntu-latest | vet, the suite, the build, the binary smokes, the schema gate, the install-script gate (added 2026-08-18) |
| `govulncheck.yml` | push to main, pull request, Monday 07:00 UTC | windows-latest | a reachable known vulnerability |
| `codeql.yml` | push to main, pull request, Monday 07:30 UTC | ubuntu-latest | a default-suite alert |
| `dependabot.yml` | weekly | none | nothing. It opens a pull request |
@@ -6967,6 +6967,132 @@ the machine. The tape stays a personal artifact and the repository holds the
script that makes it. What remains is not a tooling item — the owner drives the
eight beats, because a scripted race would be an invented recording.
+#### The one-paste Windows install (added 2026-08-18)
+
+`packaging/install.ps1` is the third Windows route, and it is the only one that
+needs nothing installed first:
+
+```
+irm https://raw.githubusercontent.com/sanlee-ys/telltale/main/packaging/install.ps1 | iex
+```
+
+**It exists because scoop is a prerequisite and winget is not submitted.** Item
+1's "one-command install with scoop/winget first" shipped both of those, and
+both assume the reader already has the package manager. A reader who has
+neither had two choices before this: unpack an archive by hand, or build from
+source. A competitor sweep on 2026-08-17 read the same gap the other way round:
+every lane leader collapses README-read to first-run into one paste, and abtop's
+README carries this PowerShell shape. That is a reading of their documents, not
+a measurement of their installers, and it is cited as such.
+
+**What it verifies, and what it refuses to claim.** The script downloads the
+archive and `checksums.txt`, compares the SHA-256 **before** it unpacks
+anything, and deletes the download on a mismatch. It then prints, in its own
+output rather than only in a document nobody reads at install time, that the
+binary carries no Authenticode signature and that the checksum proves what the
+workflow built and not who built it. That sentence is item 8 restated at the
+one moment the reader can act on it. The script signs nothing and prepares no
+signing pipeline: item 8 stands unchanged.
+
+**Three refusals are in the script rather than in a note.** A machine reporting
+`PROCESSOR_ARCHITECTURE` other than `AMD64` is refused by name, because the
+release builds no `windows/arm64` binary and installing the amd64 one there
+would be the packaging form of a rendered guess. A tag with no published
+release fails with the URL that 404'd, not with a bare status code. A
+`checksums.txt` that names no entry for the archive stops the install rather
+than skipping the check.
+
+**Measured 2026-08-18, against the published `v0.2.0` release.** Windows 11,
+two shells: PowerShell 7.6.5 and Windows PowerShell 5.1.26100.9168. Both
+installed `telltale_0.2.0_windows_amd64.zip`, both computed
+`7a2401aa…33772528`, and that value equals the digest GitHub reports for the
+asset. The installed binary answers `telltale 0.2.0`, so the release ldflags
+survive the route. The `irm | iex` shape was exercised as `Get-Content -Raw |
+Invoke-Expression`, and the calling shell survived it: the script throws and
+never calls `exit`, because `exit` inside a piped script ends the user's
+session. The `PATH` branch was driven once with the real user variable and
+restored byte for byte afterwards: the install directory reached the persisted
+user `PATH` and the running shell's own `$env:Path`. That trial also measured
+the one surprise in this route, and it is recorded rather than smoothed over:
+the directory is APPENDED, so a `telltale.exe` already earlier on `PATH` — a
+`go install` build, in the measured case — goes on winning. `Get-Command
+telltale` names the one that runs. Prepending was the rejected alternative,
+because a script that quietly outranks a binary the operator put there is doing
+something the operator did not ask for. Two refusals ran end to end and
+installed nothing: the arm64 refusal, and a `TELLTALE_VERSION=v0.1.0` run
+against the tag that has no release.
+
+**What has no end-to-end live trial is the mismatch refusal**, because driving
+it needs a host that serves a corrupted archive. Its comparison was measured
+live instead: the real `checksums.txt` was parsed, a byte was appended to the
+real archive, and the two hashes differed. The branch that acts on that
+comparison is three lines below it and is unexercised. STATE.md carries it as a
+known gap.
+
+**One footgun is recorded because it cost a parse, and the gate holds it.** The
+file is ASCII only. Windows PowerShell 5.1 reads a BOM-less file as ANSI, so one
+em dash inside a `throw` produced four parser errors under 5.1 and none under
+PowerShell 7. The `irm | iex` path decodes UTF-8 correctly and would have hidden
+this; the download-then-run path would not. `ci.yml` now parses the file under
+Windows PowerShell 5.1 on every push, rejects any byte at or above 0x80, and
+rejects an `exit` statement. It never executes the script, because executing it
+would download a release on every push. The ASCII arm was measured non-vacuous
+the way the schema gate's mutations are: one em dash appended to a copy, and the
+gate reported three bytes and failed.
+
+**No other channel is reshaped by this.** No Homebrew tap, no npm, no winget
+automation. Items 2 and 7 rule each one, and a one-paste installer is not an
+argument to revisit any of them. macOS and Linux keep the measured `curl` and
+`shasum` walk in the README, which is the same verification without a script.
+
+#### The listing and launch cadence, recorded and not executed (added 2026-08-18)
+
+This subsection records strategy that no contributor may execute. It exists
+because this repository rejects unrecorded strategy, and because the pieces
+below are owner actions on surfaces outside it.
+
+1. **Directory listings.** `awesome-claude-code` and the neighbouring lists are
+ the lane's standing distribution channel, and an inclusion is a pull request
+ to somebody else's repository. That is the same class of act as the winget
+ submission in item 7, and it takes the same ruling: **a human action, never
+ automated, and never opened by a contributor session.** What lands in this
+ repository is the badge slot in `README.md` and this paragraph. The badge
+ goes in only after the listing merges.
+2. **One Show HN per versioned feature, with the maintainer working the
+ thread.** Recorded as the cadence, with one binding limit: item 2 pins the
+ launch to ONE hypothesis, cross-harness visibility of the room, and a serial
+ cadence must not quietly widen that claim. A second post about a second
+ feature tests that feature's own question and is read as such. The first post
+ is chain link 3 in `STATE.md`, and it is sequenced behind links 1 and 2,
+ which are paid.
+3. **Publish the run-evidence bar, the method, and the result.** Item 2 already
+ defines the signal that answers the launch hypothesis: a version-bearing bug
+ report, a real-session screenshot, a pull request grounded in running it,
+ package-manager feedback, or an unsolicited statement of use. Nobody in this
+ lane publishes that bar. Publishing it is the launch story only an
+ honest-gauge product can tell, and it costs nothing to tell, because the bar
+ is written down already.
+
+ **The threshold is an owner decision and is NOT taken here.** The candidate
+ sweep proposed "10 runs in 30 days". That number has no measurement behind
+ it and no ruling, so adopting it would be the invented figure ADR-001
+ refuses. What is settled is the KIND of evidence, quoted above. What the
+ owner names before the post: the count, the window, and what the result
+ means if it is missed. Whatever is published then cites measured evidence,
+ and it never cites a star count or an install count, because telltale
+ measures neither.
+
+**`README.md` carries two slots for this work and no adoption content.** The
+badge slot holds one badge, the CI result, which is GitHub rendering GitHub's
+own run and therefore needs no third-party host and cannot go stale. A
+directory-inclusion badge may join it after that listing merges. A star count,
+a download count, an install count or a "used by" figure never may: telltale
+measures none of them, and a third-party render of an unmeasured number is the
+badge form of a rendered guess. The hero slot is for the animated capture,
+which stays owner-driven under the recording chain above and under the
+2026-08-17 per-frame review ruling. The still SVG hero and the positioning line
+already landed; neither moves here.
+
Neither track discharges what verification already owes: §3.4's remaining passive-tail
items stay open (§3.7's first live Gemini pass ran and passed 2026-08-03), and
adoption work does not buy an exemption from them.
diff --git a/packaging/README.md b/packaging/README.md
index 9c33bb0..dac2d1c 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -70,6 +70,58 @@ user-local with `go install github.com/goreleaser/goreleaser/v2@latest`, which
lands the binary in `$(go env GOPATH)/bin` and needs no admin rights and no
package manager.
+## install.ps1 — the one-paste Windows route
+
+```
+irm https://raw.githubusercontent.com/sanlee-ys/telltale/main/packaging/install.ps1 | iex
+```
+
+The script reads the latest release, downloads
+`telltale__windows_amd64.zip` and `checksums.txt`, compares the
+SHA-256 **before** it unpacks anything, copies `telltale.exe` into
+`%LOCALAPPDATA%\Programs\telltale`, and adds that directory to the user `PATH`.
+No administrator rights, no machine-wide setting. It then prints that the
+binary is unsigned, and names `telltale doctor` as the next command.
+
+The directory is **appended** to `PATH`, never prepended. A `telltale.exe`
+already earlier on `PATH` goes on winning, and `Get-Command telltale` names the
+one that runs. A script that silently outranks a binary the operator put there
+is doing something the operator did not ask for.
+
+Three environment variables, because a piped script takes no parameters:
+
+| Variable | Effect |
+|---|---|
+| `TELLTALE_VERSION` | Install this tag instead of the latest release. |
+| `TELLTALE_INSTALL_DIR` | Put `telltale.exe` here instead of the default. |
+| `TELLTALE_NO_PATH=1` | Skip the user `PATH` edit. |
+
+Exercise it without touching your own machine:
+
+```powershell
+$env:TELLTALE_INSTALL_DIR = "$env:TEMP\telltale-trial"
+$env:TELLTALE_NO_PATH = '1'
+irm https://raw.githubusercontent.com/sanlee-ys/telltale/main/packaging/install.ps1 | iex
+& "$env:TEMP\telltale-trial\telltale.exe" doctor
+```
+
+Two rules for anyone editing this file.
+
+**Keep every character under 0x80.** Windows PowerShell 5.1 reads a BOM-less
+file as ANSI, so one em dash in a string breaks the parse before the script
+runs. Measured 2026-08-18: an em dash in one `throw` produced four parser
+errors under 5.1.26100.9168 and none under PowerShell 7.6.5. The `irm | iex`
+path decodes UTF-8 and hides this; the download-then-run path does not.
+
+**Never call `exit`.** A piped script runs inside the user's shell, so `exit`
+ends their session. The body is a function, every failure is a `throw`, and one
+`try/catch` at the bottom prints the reason.
+
+What the script deliberately does not do: sign anything, prepare a signing
+pipeline, or install on `windows/arm64`. The first two are the owner's decision
+([docs/design.md §8](../docs/design.md), item 8). The third has no binary to
+install, so the script refuses that machine by name.
+
## winget
The three manifests in `winget/` are a **draft**. They are not submitted, and
diff --git a/packaging/install.ps1 b/packaging/install.ps1
new file mode 100644
index 0000000..9b4a155
--- /dev/null
+++ b/packaging/install.ps1
@@ -0,0 +1,175 @@
+<#
+ telltale - the one-paste Windows install.
+
+ irm https://raw.githubusercontent.com/sanlee-ys/telltale/main/packaging/install.ps1 | iex
+
+ What this script does, in order: it reads the release you asked for from
+ the GitHub API, downloads telltale__windows_amd64.zip and
+ checksums.txt, compares the archive's SHA-256 against that file, and only
+ then unpacks the binary. A mismatch deletes the download and stops.
+
+ What this script cannot do: tell you WHO built the archive. No telltale
+ binary carries an Authenticode signature. That is the owner's recorded
+ decision (docs/design.md section 8, item 8), not an oversight, and
+ checksums.txt is the whole of the verification this release can honestly
+ offer. It proves the archive is the one the release workflow produced. It
+ proves nothing about who produced it. Windows SmartScreen can warn on an
+ unsigned binary, and it is correct to.
+
+ This script makes network calls. The gauges do not, and that boundary is
+ unchanged: an installer is not a gauge (CLAUDE.md, the read/write
+ boundary). Nothing here reads a credential store, and nothing here writes
+ outside the install directory and the user PATH entry.
+
+ Environment variables, because a piped script takes no parameters:
+
+ TELLTALE_VERSION a tag, for example v0.2.0. Default: the latest
+ published release.
+ TELLTALE_INSTALL_DIR where telltale.exe lands. Default:
+ $env:LOCALAPPDATA\Programs\telltale
+ TELLTALE_NO_PATH set to 1 to skip the user PATH edit.
+
+ Windows only, and windows_amd64 only. The release builds no windows/arm64
+ binary, so this script refuses that machine by name rather than installing
+ something nobody has run. macOS and Linux use the curl and shasum walk in
+ README.md.
+
+ scoop is the other Windows route and it is the older one:
+ "scoop bucket add telltale https://github.com/sanlee-ys/telltale", then
+ "scoop install telltale". Use scoop if you already have it. This script
+ exists for the machine that does not.
+
+ THIS FILE IS ASCII ONLY, ON PURPOSE. Windows PowerShell 5.1 reads a
+ BOM-less file as ANSI, so a single em dash in a string breaks the parse
+ before any of the above runs. Measured 2026-08-18: an em dash in one
+ throw produced four parser errors under 5.1.26100.9168 and none under
+ PowerShell 7.6.5. Keep every character in this file under 0x80.
+#>
+
+function Install-Telltale {
+ [CmdletBinding()]
+ param()
+
+ $ErrorActionPreference = 'Stop'
+ # Windows PowerShell 5.1 draws a progress bar per response chunk, which
+ # costs more than the download does. It also defaults to whatever the .NET
+ # framework configured, which on an unpatched 5.1 excludes TLS 1.2, and
+ # api.github.com answers nothing else.
+ $ProgressPreference = 'SilentlyContinue'
+ try {
+ [Net.ServicePointManager]::SecurityProtocol = `
+ [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
+ } catch {
+ # PowerShell 7 manages this itself and the type may be absent. A
+ # failure here is not a reason to stop.
+ }
+
+ $repo = 'sanlee-ys/telltale'
+
+ if ($env:OS -ne 'Windows_NT') {
+ throw 'This script installs the Windows build. macOS and Linux use the curl and shasum walk in README.md.'
+ }
+ $arch = $env:PROCESSOR_ARCHITECTURE
+ if ($arch -ne 'AMD64') {
+ throw "This machine reports PROCESSOR_ARCHITECTURE=$arch. The release builds windows_amd64 only, so there is no binary to install here. Build from source: go build -o telltale.exe ./cmd/telltale"
+ }
+
+ # 1. Which release.
+ $tag = $env:TELLTALE_VERSION
+ if (-not $tag) {
+ Write-Host 'telltale: reading the latest release...'
+ $latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" -Headers @{ 'User-Agent' = 'telltale-install' }
+ $tag = $latest.tag_name
+ }
+ if (-not $tag) { throw 'No release tag was found. Set TELLTALE_VERSION to a tag, for example v0.2.0.' }
+ # The archive name carries the version WITHOUT the leading v; the tag keeps
+ # its v only inside the URL. The scoop and winget manifests hard-code the
+ # same shape.
+ $version = $tag -replace '^v', ''
+ $archive = "telltale_${version}_windows_amd64.zip"
+ $base = "https://github.com/$repo/releases/download/$tag"
+
+ # 2. Download the archive and the checksums beside it.
+ $work = Join-Path ([IO.Path]::GetTempPath()) ("telltale-install-" + [Guid]::NewGuid().ToString('N'))
+ New-Item -ItemType Directory -Path $work | Out-Null
+ try {
+ $zip = Join-Path $work $archive
+ $sums = Join-Path $work 'checksums.txt'
+ Write-Host "telltale: downloading $archive ($tag)..."
+ # The URL is named in the failure, because the common failure here is a
+ # tag that has no published release, and a bare "404 (Not Found)" does
+ # not tell the reader which of the two files was missing.
+ foreach ($pair in @(, @("$base/$archive", $zip)) + @(, @("$base/checksums.txt", $sums))) {
+ try {
+ Invoke-WebRequest -Uri $pair[0] -OutFile $pair[1] -UseBasicParsing
+ } catch {
+ throw "$($pair[0]) could not be downloaded. $($_.Exception.Message) Nothing was installed."
+ }
+ }
+
+ # 3. Verify before unpacking. A checksum checked after the binary is
+ # already on PATH is a checksum that verified nothing.
+ $line = Select-String -Path $sums -Pattern $archive -SimpleMatch | Select-Object -First 1
+ if (-not $line) {
+ throw "checksums.txt on $tag names no entry for $archive. Nothing was installed."
+ }
+ $want = ($line.Line -split '\s+')[0].ToLowerInvariant()
+ $got = (Get-FileHash -Path $zip -Algorithm SHA256).Hash.ToLowerInvariant()
+ if ($want -ne $got) {
+ Remove-Item -Path $zip -Force
+ throw "SHA-256 mismatch on $archive. checksums.txt says $want and the download hashes $got. The download was deleted and nothing was installed."
+ }
+ Write-Host "telltale: sha256 ok ($got)"
+
+ # 4. Unpack and place.
+ $dest = $env:TELLTALE_INSTALL_DIR
+ if (-not $dest) { $dest = Join-Path $env:LOCALAPPDATA 'Programs\telltale' }
+ if (-not (Test-Path -LiteralPath $dest)) {
+ New-Item -ItemType Directory -Path $dest -Force | Out-Null
+ }
+ $unpack = Join-Path $work 'unpack'
+ Expand-Archive -LiteralPath $zip -DestinationPath $unpack -Force
+ $exe = Join-Path $unpack 'telltale.exe'
+ if (-not (Test-Path -LiteralPath $exe)) {
+ throw "$archive holds no telltale.exe. Nothing was installed."
+ }
+ Copy-Item -LiteralPath $exe -Destination (Join-Path $dest 'telltale.exe') -Force
+ Write-Host "telltale: installed to $dest"
+
+ # 5. PATH, user scope only. This script asks for no administrator
+ # rights and edits no machine-wide setting.
+ if ($env:TELLTALE_NO_PATH -ne '1') {
+ $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
+ $parts = @()
+ if ($userPath) { $parts = $userPath -split ';' | Where-Object { $_ -ne '' } }
+ if ($parts -notcontains $dest) {
+ [Environment]::SetEnvironmentVariable('Path', (($parts + $dest) -join ';'), 'User')
+ Write-Host "telltale: added $dest to your user PATH. Open a new terminal for it to take effect."
+ }
+ # The running shell gets it too, so the next command below works in
+ # THIS window rather than only in the next one.
+ if (($env:Path -split ';') -notcontains $dest) { $env:Path = "$env:Path;$dest" }
+ }
+
+ Write-Host ''
+ Write-Host 'This binary is NOT signed. No telltale release carries an Authenticode'
+ Write-Host 'signature, by the owner''s decision (docs/design.md section 8, item 8).'
+ Write-Host 'The SHA-256 above is the whole verification: it proves this archive is the'
+ Write-Host 'one the release workflow built, and it proves nothing about who built it.'
+ Write-Host ''
+ Write-Host 'Now run:'
+ Write-Host ' telltale doctor (which vendor CLIs this machine has)'
+ Write-Host ' telltale council (the room)'
+ } finally {
+ Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
+
+# Called rather than left as a definition, because "irm | iex" runs this file
+# for its effect. throw rather than exit throughout: exit inside a piped script
+# ends the user's whole shell session.
+try {
+ Install-Telltale
+} catch {
+ Write-Host "telltale: install failed. $($_.Exception.Message)" -ForegroundColor Red
+}