diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390e576..b3510ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -291,15 +291,30 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} PRERELEASE: ${{ needs.release.outputs.prerelease }} + VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail tag=$([ "$PRERELEASE" = true ] && echo next || echo latest) + # Already-published versions are skipped rather than retried. npm + # refuses to publish over one, and under `set -e` that aborts the whole + # job — so a run that failed part-way through, as v0.1.0's did on a + # token that could not bypass 2FA, could not simply be re-run. It died + # on the first package that had landed, leaving the rest unpublished + # and the install page naming a channel that 404s. + publish() { + name=$(node -p "require('./$1/package.json').name") + if npm view "$name@$VERSION" version >/dev/null 2>&1; then + echo "::notice::$name@$VERSION is already published; skipping" + return 0 + fi + npm publish "$1" --access public --provenance --tag "$2" + } # Platform packages first: the wrapper's optionalDependencies name # them, and a wrapper published ahead of them installs a placeholder. for pkg in dist/npm/tula-*; do - npm publish "$pkg" --access public --provenance --tag "$tag" + publish "$pkg" "$tag" done - npm publish dist/npm/tula --access public --provenance --tag "$tag" + publish dist/npm/tula "$tag" homebrew: needs: release diff --git a/AGENTS.md b/AGENTS.md index 0fd90b3..a77ded9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -806,6 +806,7 @@ release is already public, while the site is telling people to use them. | The Vercel root directory is `site` | `vercel.json`'s headers are read from there and nowhere else | `curl -sI https://usetu.la/` | | Vercel includes files outside that root | the build script copies `install.sh` in from there; without it the build fails | `curl -sI https://usetu.la/install.sh` | | `APPLE_*` secrets | optional; without them macOS ships unsigned | `gh secret list` | +| A `release` environment with a required reviewer | `release.yml` names it on all three jobs, and naming it does nothing until it exists — GitHub silently creates an unprotected one on first use, and the run publishes unreviewed | `curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/hsnice16/tula/environments/release` | Set each variable last, after its token exists: `true` without the token turns a skipped job into a failed one, and it fails after the GitHub release is public. @@ -833,10 +834,17 @@ and the second resolves the name as an account, so it succeeds for anybody's. The scope list on npm's token page is the answer — it offers only what you can publish to. -npm publishes from a token, so account 2FA never gates CI. `auth-only` is -therefore free — `npm profile enable-2fa auth-only` — and worth having: without -it a password is enough to publish `@hsnice16/tula`, which is the one entrance this -project's supply-chain argument would not cover. +The token has to be one that bypasses 2FA, and not every kind does. A classic +**Publish** token does not: with 2FA on writes it demands an OTP, and the job +fails `EOTP` after the GitHub release is already public — which is how v0.1.0 +went out with npm empty for half an hour. Use a **granular access token** scoped +to the `@hsnice16` *scope* with read and write, or a classic **Automation** +token. Scope, not package: the per-package selector cannot name packages that do +not exist yet, which is every one of them on a first release. + +Do not reach for `npm profile enable-2fa auth-only` instead. It makes a password +enough to publish `@hsnice16/tula` — the one entrance this project's supply-chain +argument would not cover — and buys nothing the token type does not already give. `gh` is also what verifies an attestation, so a maintainer who cannot run `gh attestation verify` cannot check the first release the way the install page diff --git a/CHANGELOG.md b/CHANGELOG.md index d45272f..332030f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ CI and build plumbing, refactors, and doc-only edits — stays in commit message ## [Unreleased] +## [0.1.1] - 2026-09-06 + +### Changed + +- **A version you already have is not downloaded again.** The installer resolved the version and then downloaded and re-verified the whole 21 MB archive regardless of whether that exact build was already on disk — so the install line pasted a second time, which is how most people fix a PATH problem, paid the full download to arrive at the file it already had. It now recognises the version under `~/.tula/versions` and goes straight to relinking and the PATH line — 2.7s to 0.3s on a warm link, and from minutes to under a second on a cold one. `TULA_FORCE=1` fetches and checks again for anyone who wants that. The short cut needs the launcher symlink to be there already, not just a directory: anyone able to write under the install tree could otherwise plant a version directory before tula was ever installed and have the first install adopt it unread, where that install used to download over it. +- **The download says how far along it is.** `-s` silenced curl's meter for every fetch, including the one that takes minutes, so a single `downloading` line sat unchanged for the whole transfer and read as a hang — 152 seconds of silence, in one capture, between the first line and the next. The download is GitHub's and is not faster; it is now visibly moving. The archive keeps the meter now; the 386-byte checksum file does not. A transfer under 1 KB/s for 30 seconds is ended rather than waited on, which is the difference between a slow link and a dead one. +- **Paths in the installer's report are written from home.** `~/.tula/bin/tula` rather than the absolute form, which is mostly the reader's own username and the part they would redact before pasting a screenshot into an issue. The `export PATH=` line stays absolute, because a `~` inside those quotes is not a home directory to any shell. +- **The banner carries a `v` before the version, and the directory you are in.** The `v` is how the tag, the release and every `git` and `npm` line about the build already spell it; the bare number was the odd one out. The directory is written from home too, which keeps the reader's own username off a screenshot. tula reads nothing from it — the book is the same wherever the shell was opened — but it is what tells two terminals apart at a glance. It is stripped of control characters and capped like any other text tula did not write: every byte but `/` and NUL is legal in a directory name, so a cloned repository can carry one named to repaint whatever draws it. + ## [0.1.0] - 2026-09-06 The first release: real venues on a published install path, the query shell, and @@ -161,5 +170,6 @@ what breaks first. - `KeyScope` is tri-state. Kraken exposes no endpoint reporting a key's permissions, and every endpoint gated on trade permission mutates an order, so `canTrade` is `unknown` rather than guessed at. Withdraw scope is provable, and is proven. - Kraken margin and open orders are not read yet, so on a margin account this is not a complete Kraken picture. -[Unreleased]: https://github.com/hsnice16/tula/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/hsnice16/tula/compare/v0.1.1...HEAD +[0.1.1]: https://github.com/hsnice16/tula/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/hsnice16/tula/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 723846a..3080d28 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ the sigstore-backed attestation proving this repository's release workflow built it wherever the GitHub CLI can — saying so either way. Check one by hand: ```bash -gh attestation verify tula-v0.1.0-darwin-arm64.tar.gz --repo hsnice16/tula \ +gh attestation verify tula-v0.1.1-darwin-arm64.tar.gz --repo hsnice16/tula \ --signer-workflow hsnice16/tula/.github/workflows/release.yml ``` diff --git a/SECURITY.md b/SECURITY.md index 8e1f032..d5e09e1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -133,7 +133,7 @@ keyless, so there is no signing key for this project to generate, publish, rotat or lose. ```bash -gh attestation verify tula-v0.1.0-darwin-arm64.tar.gz --repo hsnice16/tula \ +gh attestation verify tula-v0.1.1-darwin-arm64.tar.gz --repo hsnice16/tula \ --signer-workflow hsnice16/tula/.github/workflows/release.yml ``` diff --git a/install.sh b/install.sh index 9031071..66ac300 100755 --- a/install.sh +++ b/install.sh @@ -14,6 +14,8 @@ # TULA_INSTALL_DIR root of the install tree (default ~/.tula) # TULA_REQUIRE_ATTESTATION refuse to install at all unless provenance is proven # TULA_NO_MODIFY_PATH do not touch any shell profile +# TULA_FORCE download and check again even if this version is +# already installed set -eu REPO="hsnice16/tula" @@ -25,6 +27,15 @@ BROWSE="https://github.com/$REPO/releases" say() { printf '%s\n' "$*"; } note() { printf ' %s\n' "$*"; } +# The absolute form of a path under $HOME is mostly the reader's own username, +# and it wraps the line on a narrow window. +tilde() { + case "$1" in + "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;; + *) printf '%s' "$1" ;; + esac +} + # Every failure names the next step. Someone stuck here has money at risk and no # way to tell a broken download from a hostile one. die() { @@ -45,6 +56,17 @@ fetch() { curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --retry-connrefused "$1" -o "$2" } +# The archive is tens of megabytes and the only step here that takes minutes, so +# it is the one fetch that keeps curl's meter: `-s` left a single "downloading" +# line standing unchanged for the whole transfer, which reads as a hang rather +# than as work. --speed-limit ends a stalled one instead of waiting on it for as +# long as the OS will — under 1 KB/s for 30s is not a slow link, it is a dead one, +# and --retry then gets another go at it. +fetch_archive() { + curl --proto '=https' --tlsv1.2 -fSL --retry 3 --retry-connrefused \ + --speed-limit 1024 --speed-time 30 -# "$1" -o "$2" +} + need curl need tar need uname @@ -215,36 +237,62 @@ esac TARGET=$(detect_target) ARCHIVE="tula-v$VERSION-$TARGET.tar.gz" BASE="https://github.com/$REPO/releases/download/v$VERSION" +VERSION_DIR="$INSTALL_DIR/versions/$VERSION" UNVERIFIED= UNVERIFIED_WHY= say "" say "tula $VERSION — $TARGET" -TMP=$(mktemp -d 2>/dev/null || mktemp -d -t tula) -trap 'rm -rf "$TMP"' EXIT INT TERM +# Every version stays on disk under its own number, so a run asking for one that +# is already there has nothing to fetch. It downloaded and re-verified the whole +# archive regardless — minutes of it, on a link where that is minutes — to arrive +# at the file it already had. The launcher and the PATH line are still put right +# below, because repairing those is the other reason to run this twice. +# +# The launcher has to be a symlink already for this to count as "installed", and +# that is the security of it, not a tidiness check. A directory alone is +# something anyone who can write under $INSTALL_DIR can put there before tula is +# ever installed — and a first install used to overwrite whatever it found, +# where this would adopt it and link it unread. Requiring the link narrows the +# fast path to a tree this script has already built and verified once; replacing +# the binary under that link is an attack the download never prevented anyway, +# since the launcher points at it either way. +# +# TULA_FORCE=1 fetches and checks again regardless. +ALREADY= +if [ -z "${TULA_FORCE:-}" ] && [ -x "$VERSION_DIR/tula" ] && [ -L "$BIN_DIR/tula" ]; then + ALREADY=1 +fi -note "downloading" -fetch "$BASE/$ARCHIVE" "$TMP/$ARCHIVE" || - die "No build of $VERSION for $TARGET." \ - "Releases: https://github.com/$REPO/releases" -fetch "$BASE/checksums.txt" "$TMP/checksums.txt" || - die "Could not download checksums.txt for $VERSION." \ - "tula will not install a binary it cannot check." +if [ -z "$ALREADY" ]; then + TMP=$(mktemp -d 2>/dev/null || mktemp -d -t tula) + trap 'rm -rf "$TMP"' EXIT INT TERM -note "checking the download matches its published checksum" -verify_checksum "$TMP/$ARCHIVE" "$TMP/checksums.txt" + note "downloading" + fetch_archive "$BASE/$ARCHIVE" "$TMP/$ARCHIVE" || + die "No build of $VERSION for $TARGET." \ + "Releases: https://github.com/$REPO/releases" + fetch "$BASE/checksums.txt" "$TMP/checksums.txt" || + die "Could not download checksums.txt for $VERSION." \ + "tula will not install a binary it cannot check." -note "checking it was built by $REPO" -verify_attestation "$TMP/$ARCHIVE" + note "checking the download matches its published checksum" + verify_checksum "$TMP/$ARCHIVE" "$TMP/checksums.txt" -VERSION_DIR="$INSTALL_DIR/versions/$VERSION" -mkdir -p "$VERSION_DIR" "$BIN_DIR" -tar -xzf "$TMP/$ARCHIVE" -C "$VERSION_DIR" || - die "Could not unpack $ARCHIVE." "The download may be truncated; try again." -[ -f "$VERSION_DIR/tula" ] || die "$ARCHIVE did not contain a tula binary." \ - "Report it: https://github.com/$REPO/issues" -chmod 755 "$VERSION_DIR/tula" + note "checking it was built by $REPO" + verify_attestation "$TMP/$ARCHIVE" + + mkdir -p "$VERSION_DIR" "$BIN_DIR" + tar -xzf "$TMP/$ARCHIVE" -C "$VERSION_DIR" || + die "Could not unpack $ARCHIVE." "The download may be truncated; try again." + [ -f "$VERSION_DIR/tula" ] || die "$ARCHIVE did not contain a tula binary." \ + "Report it: https://github.com/$REPO/issues" + chmod 755 "$VERSION_DIR/tula" +else + note "already installed — nothing to download" + mkdir -p "$BIN_DIR" +fi # Every version stays on disk under its own number and the launcher is a symlink, # so moving between them is a link flip rather than a re-download — including @@ -300,11 +348,14 @@ fi say "" if [ -n "$REPLACED" ]; then say "Installed tula $VERSION, and left your launcher alone." - note "yours: $LAUNCHER" - note "this build: $VERSION_DIR/tula" + note "yours: $(tilde "$LAUNCHER")" + note "this build: $(tilde "$VERSION_DIR/tula")" note "to switch: ln -sf \"$VERSION_DIR/tula\" \"$LAUNCHER\"" +elif [ -n "$ALREADY" ]; then + say "tula $VERSION is already installed at $(tilde "$LAUNCHER")" + note "nothing was downloaded — TULA_FORCE=1 fetches and checks it again" else - say "Installed tula $VERSION to $LAUNCHER" + say "Installed tula $VERSION to $(tilde "$LAUNCHER")" fi if [ -n "$UNVERIFIED" ]; then diff --git a/package.json b/package.json index e0e8201..8f6d360 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hsnice16/tula", - "version": "0.1.0", + "version": "0.1.1", "description": "Your true exposure, what breaks first, and more, across every venue at once.", "license": "MIT", "type": "module", diff --git a/scripts/install-test.sh b/scripts/install-test.sh index f2a9552..f99f3a8 100755 --- a/scripts/install-test.sh +++ b/scripts/install-test.sh @@ -23,6 +23,13 @@ bad() { fail=$((fail + 1)) } +# Whether this machine already had tula before the suite started. The leak check +# at the bottom used to ask only whether ~/.tula existed, which is true on every +# machine that dogfoods the tool — so the suite failed for anyone who had run the +# published install line, and passed on CI only because CI never had. +TREE_BEFORE=0 +[ -e "$HOME/.tula" ] && TREE_BEFORE=1 + # A release exactly as the workflow lays one out: two archives and a checksums # file listing both. RELEASE="$WORK/release" @@ -311,6 +318,53 @@ else bad "installing twice leaves one PATH line, wherever the install directory is" "wrote $blocks blocks; $out" fi +# A version already on disk is the commonest second run there is — the install +# line pasted again to fix PATH — and it used to fetch and re-verify the whole +# archive to arrive at the file it already had. Proving it downloads nothing +# means taking the archives away: a run that still succeeds cannot have fetched +# one. +H="$WORK/h11" +mkdir -p "$H" +run "$H" >/dev/null 2>&1 +HOLD="$WORK/hold" +mkdir -p "$HOLD" +mv "$RELEASE"/*.tar.gz "$HOLD/" +out=$(run "$H") +if [ -x "$H/.tula/versions/9.9.9/tula" ] && + case "$out" in *"already installed"*) true ;; *) false ;; esac; then + ok "a version already on disk is not downloaded again" +else + bad "a version already on disk is not downloaded again" "$out" +fi + +# And the escape hatch has to really reach the network, or it is a flag that +# says it re-checked and did not. With the archives still gone it has nothing to +# fetch, so it must fail where the run above passed. +out=$(run "$H" TULA_FORCE=1) +case "$out" in + *"No build of 9.9.9"*) ok "TULA_FORCE=1 fetches again rather than trusting the disk" ;; + *) bad "TULA_FORCE=1 fetches again rather than trusting the disk" "$out" ;; +esac +mv "$HOLD"/*.tar.gz "$RELEASE/" + +# A version directory is not proof of an install. Anyone able to write under the +# install tree can put one there before tula has ever been installed, and the +# fast path would otherwise adopt whatever binary it found and link it unread — +# where a first install used to overwrite it. The launcher being a symlink +# already is what says this script built the tree. +H="$WORK/h12" +mkdir -p "$H/.tula/versions/9.9.9" +printf '#!/bin/sh\necho planted\n' >"$H/.tula/versions/9.9.9/tula" +chmod 755 "$H/.tula/versions/9.9.9/tula" +out=$(run "$H") +if case "$out" in *"already installed"*) false ;; *) true ;; esac && + ! grep -q planted "$H/.tula/versions/9.9.9/tula"; then + ok "a planted version directory is downloaded over, not adopted" +else + bad "a planted version directory is downloaded over, not adopted" "$out" +fi + + # Nothing above may have touched a profile outside the sandbox. This test edits # shell config, so a leak is silent, permanent and in someone's real home. leaked=0 @@ -321,8 +375,14 @@ done [ "$leaked" -eq 0 ] && ok "writes no shell profile outside the sandbox" || bad "writes no shell profile outside the sandbox" -[ -e "$HOME/.tula" ] && bad "creates no install tree outside the sandbox" || - ok "creates no install tree outside the sandbox" +# Two ways to tell a leak from the developer's own install: the fixture's version +# is one no real release carries, and a tree that was not there before the suite +# ran did not get there by itself. +leaked_tree=0 +[ -e "$HOME/.tula/versions/9.9.9" ] && leaked_tree=1 +[ "$TREE_BEFORE" = 0 ] && [ -e "$HOME/.tula" ] && leaked_tree=1 +[ "$leaked_tree" -eq 0 ] && ok "creates no install tree outside the sandbox" || + bad "creates no install tree outside the sandbox" echo echo " $pass passed, $fail failed" diff --git a/site/app/install/page.tsx b/site/app/install/page.tsx index fa19a9a..64c2b37 100644 --- a/site/app/install/page.tsx +++ b/site/app/install/page.tsx @@ -159,6 +159,11 @@ const CHANNELS: Channel[] = [ command again does the same thing, and is the one that still works when tula will not start.
+
+ It downloads nothing when the newest release is the one you already have — it says so
+ and relinks, which takes about a second. TULA_FORCE=1 fetches and checks
+ that same version again anyway.
+
diff --git a/site/components/Session.tsx b/site/components/Session.tsx index cb14bba..bb8a24d 100644 --- a/site/components/Session.tsx +++ b/site/components/Session.tsx @@ -179,10 +179,14 @@ export const Banner = () => ( preserveAspectRatio="xMidYMin meet" /> - {`tula ${VERSION}`} + {`tula v${VERSION}`} Your true exposure, what breaks first, and more, across every venue at once. + {/* The binary prints the directory the shell was opened in. A frame + offered as its output has to carry the line, and the path is the one + part of it this picture has to invent. */} + ~/trading Connected: wallet, hyperliquid, aave, kraken diff --git a/site/lib/site.ts b/site/lib/site.ts index bee95b5..b0a82bc 100644 --- a/site/lib/site.ts +++ b/site/lib/site.ts @@ -10,7 +10,7 @@ export const NAME = 'tula' * the two disagree and `release-cut.sh` bumps this with them: a frame offered * as the tool's own output cannot print a release that was never cut. */ -export const VERSION = '0.1.0' +export const VERSION = '0.1.1' /** * GA4, for the site alone. Held here rather than read from `process.env`: an diff --git a/src/core/paths.test.ts b/src/core/paths.test.ts new file mode 100644 index 0000000..0082596 --- /dev/null +++ b/src/core/paths.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from 'bun:test' +import { homedir } from 'node:os' +import { homeRelative } from './paths.js' + +/** + * The banner draws the working directory, and Ink writes text verbatim — so a + * directory name is a string somebody else may have chosen. Every byte but `/` + * and NUL is legal in one, which a cloned repository or an unpacked archive can + * carry onto a machine without its owner ever typing it. + */ +describe('homeRelative', () => { + const ESC = String.fromCharCode(27) + + test('writes a path under home from the tilde', () => { + expect(homeRelative(`${homedir()}/Documents/tula`)).toBe('~/Documents/tula') + expect(homeRelative(homedir())).toBe('~') + }) + + test('leaves a path outside home alone', () => { + expect(homeRelative('/usr/local/bin')).toBe('/usr/local/bin') + }) + + // The name is not a prefix of home, and matching on the bare string would + // rewrite a sibling directory as though it sat inside it. + test('a sibling of home is not inside it', () => { + expect(homeRelative(`${homedir()}-backup/x`)).toBe(`${homedir()}-backup/x`) + }) + + test('an escape sequence in a directory name never reaches the screen', () => { + const evil = `/tmp/${ESC}[2K\rtula v9.9.9 CONNECTED: kraken` + const out = homeRelative(evil) + expect(out).not.toContain(ESC) + expect(out).not.toContain('\r') + }) + + test('a path too long to sit beside the mark is elided from the head', () => { + const out = homeRelative(`/x/${'a'.repeat(4096)}/tail`) + expect(out.length).toBeLessThanOrEqual(64) + expect(out.startsWith('…')).toBe(true) + // The tail is what says which directory this is. + expect(out.endsWith('/tail')).toBe(true) + }) +}) diff --git a/src/core/paths.ts b/src/core/paths.ts index 6c053c6..1b25cc7 100644 --- a/src/core/paths.ts +++ b/src/core/paths.ts @@ -15,3 +15,25 @@ export const configDir = (): string => /** The tree `install.sh` writes. `TULA_INSTALL_DIR` is the same name it reads. */ export const installDir = (): string => process.env['TULA_INSTALL_DIR'] ?? join(homedir(), '.tula') + +/** A banner line sits beside a 7-column mark; past this a path is wrapping. */ +const MAX_PATH = 64 + +/** + * A path as somebody reads it rather than as the filesystem spells it. The + * absolute form of anything under $HOME is mostly the reader's own username, + * which is noise on screen and the part they would have to redact before + * pasting a screenshot into an issue. + * + * Stripped and capped because a directory name is somebody else's text as much + * as a venue's error is: every byte but `/` and NUL is legal in one, ESC + * included, and a repository or an archive can carry a directory named to + * repaint whatever draws it. The head is what gets elided — a path identifies + * itself by its tail. + */ +export const homeRelative = (path: string): string => { + const home = homedir() + const clean = path.replace(/[\p{Cc}\p{Cf}]+/gu, '') + const short = clean === home ? '~' : clean.startsWith(`${home}/`) ? `~${clean.slice(home.length)}` : clean + return short.length > MAX_PATH ? `…${short.slice(short.length - MAX_PATH + 1)}` : short +} diff --git a/src/ui/app.tsx b/src/ui/app.tsx index e083d7d..0043c55 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -27,6 +27,7 @@ import { pendingUpdate } from '../update/check.js' import type { Connector } from '../connectors/types.js' import { TulaError } from '../core/errors.js' import * as secrets from '../secrets/store.js' +import { homeRelative } from '../core/paths.js' import { APP_DESCRIPTION, APP_VERSION, REPO_URL } from '../version.js' import { ConnectFlow } from './ConnectFlow.js' import { connectable, type Connectable, type ConnectorCredentials } from '../connectors/types.js' @@ -391,8 +392,14 @@ export function App({ session, connectors, initialApiKey, initialVenues, agent: id: -1, kind: 'banner', text: [ - `tula ${APP_VERSION}`, + // `v` because that is how the tag, the release and every `git` and `npm` + // line about this build spell it; the bare number was the odd one out. + `tula v${APP_VERSION}`, APP_DESCRIPTION, + // Where the shell was opened. tula reads nothing from it — the book is + // the same in any directory — but it is what tells two terminals apart + // at a glance, and every other tool in one prints it. + homeRelative(process.cwd()), ...(initialVenues.length > 0 ? [`Connected: ${initialVenues.join(', ')}`] : []), ].join('\n'), }), diff --git a/src/ui/screen.test.ts b/src/ui/screen.test.ts index 20a006c..0548088 100644 --- a/src/ui/screen.test.ts +++ b/src/ui/screen.test.ts @@ -1027,7 +1027,7 @@ test('an answer that stops for a tool still says it is working', async () => { } }) -const BANNER = `tula ${APP_VERSION}` +const BANNER = `tula v${APP_VERSION}` /** The mark sits in its own column beside the banner, so the name is not alone on its row. */ const bannerRow = (row: string) => row.trimEnd().endsWith(BANNER) @@ -1042,8 +1042,14 @@ test('the session opens with a banner, written once', async () => { const at = rows.findIndex(bannerRow) expect(rows[at]).toMatch(/▄▄▄ +tula/) expect(rows[at + 1]).toMatch(/^ +▄▟ {3}▙▄ +Your true exposure/) - expect(rows[at + 2]?.trim()).toBe('▄▄▄▄▄▄▄') + // The mark's last row now shares its line with the working directory, which + // is the third banner line and the one that made this block as tall as the + // mark rather than one row shorter than it. + expect(rows[at + 2]).toMatch(/^ *▄▄▄▄▄▄▄ +[~/]/) expect(rows[at]?.indexOf('tula')).toBe(rows[at + 1]?.indexOf('Your') ?? -1) + expect(rows[at + 1]?.indexOf('Your')).toBe( + rows[at + 2]?.search(/[~/]\S*$/) ?? -1, + ) // It is a transcript entry, so it scrolls away with the rest rather than // being redrawn — and a redraw that reissued it would stack a second copy. await screen.press('/help\r') diff --git a/src/version.ts b/src/version.ts index 5006f30..7660a51 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,5 +1,5 @@ export const APP_NAME = 'tula' -export const APP_VERSION = '0.1.0' +export const APP_VERSION = '0.1.1' /** * SemVer says a hyphen means pre-release, and `release.yml` already reads it