From 716cf68c1e269d3009d8bf31336e3f00bc90c7e0 Mon Sep 17 00:00:00 2001 From: San Lee <295248956+sanlee-ys@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:10:06 -0400 Subject: [PATCH] packaging: one paste installs telltale on Windows, and says the binary is unsigned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `irm .../packaging/install.ps1 | iex` is the third Windows route and the only one that needs nothing installed first. scoop and winget both assume the reader already has the package manager; a reader with neither had to unpack an archive by hand or build from source. The script downloads the release, compares the SHA-256 against checksums.txt BEFORE it unpacks anything, and deletes the download on a mismatch. It then prints, at the one moment the reader can act on it, that the binary carries no Authenticode signature and that the checksum proves what the workflow built and not who built it. It signs nothing and prepares no signing pipeline; design.md §8 item 8 stands unchanged. No brew tap, no npm, no winget automation. Measured 2026-08-18 against the published v0.2.0, on PowerShell 7.6.5 and Windows PowerShell 5.1.26100.9168: both installed the archive, both computed the digest GitHub reports for the asset, and the installed binary answered `telltale 0.2.0`. Two refusals ran end to end and installed nothing. The mismatch refusal has no end-to-end trial and STATE.md records that gap. README gains a first-five-minutes table, every row keyed to a line `telltale doctor` actually prints, so a stranger can tell this preflight working from telltale failing. CI now parses install.ps1 under Windows PowerShell 5.1 and rejects any byte at or above 0x80: 5.1 reads a BOM-less file as ANSI, and one em dash cost four parser errors while pwsh read the same file fine. README also gains a badge slot and a hero slot, and design.md records the listing and Show HN cadence as strategy no contributor may execute. The run-evidence bar's threshold stays the owner's to name; the sweep's proposed number has no measurement behind it and was not adopted. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 32 +++++++ README.md | 61 +++++++++++++- SECURITY.md | 14 ++-- STATE.md | 24 ++++++ docs/design.md | 128 +++++++++++++++++++++++++++- packaging/README.md | 52 ++++++++++++ packaging/install.ps1 | 175 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 477 insertions(+), 9 deletions(-) create mode 100644 packaging/install.ps1 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. +[![CI](https://github.com/sanlee-ys/telltale/actions/workflows/ci.yml/badge.svg)](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 +}