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 +}