diff --git a/README.md b/README.md index 0db621e..f2798c9 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,12 @@ To run the checkout as `git fi` in other repositories for a while: ```bash npm run trial:on # build, npm link, load this copy's completion -npm run trial:off # unlink, restore the published version +npm run trial:off # unlink, restore the version it replaced ``` -`trial:on` symlinks the checkout onto your `PATH`, so later `npm run build`s take effect without reinstalling. It also runs `install-completions --write .trial/completions` and appends a marked block to `~/.zshrc` that puts that directory on your `fpath` — the same command, writing the same two files, that a user installs with (`COMPLETE-06`), so a trial exercises the shipped install path rather than a shortcut around it. Only `~/.zshrc` is touched; under bash you get the linked binary and wire the completion yourself. `trial:off` deletes the block and `.trial/`, then reinstalls `@gettyimages/git-fi` from npm. Open a new terminal after either one. +`trial:on` symlinks the checkout onto your `PATH`, so later `npm run build`s take effect without reinstalling. It also runs `install-completions --write .trial/completions` and appends a marked block to `~/.zshrc` that puts that directory on your `fpath` — the same command, writing the same two files, that a user installs with (`COMPLETE-06`), so a trial exercises the shipped install path rather than a shortcut around it. Only `~/.zshrc` is touched; under bash you get the linked binary and wire the completion yourself. `trial:off` deletes the block and `.trial/`, then reinstalls the exact version the trial displaced, which `trial:on` recorded before linking over it (`BUILD-03`). Where that reference has been copied outside the marked block, `trial:off` names the lines rather than editing them, since the directory they point at is the one it just removed (`BUILD-04`). Open a new terminal after either one. + +`git fi --version` is what says which build is in force: a trial answers `-dev.g`, an installed copy answers the bare version (`BUILD-02`). If `git fi` runs something other than what you expect, `which -a git-fi` lists every `git-fi` on your `PATH` in resolution order. diff --git a/SPEC.md b/SPEC.md index e748a4b..d456405 100644 --- a/SPEC.md +++ b/SPEC.md @@ -754,10 +754,12 @@ Platform Compatibility Build Provenance -A developer can put an unpublished checkout on PATH under the same `git fi` name the published install uses. These requirements keep the two tellable apart, so a bug report names the build it came from. +A developer can put an unpublished checkout on PATH under the same `git fi` name the published install uses. These requirements keep the two tellable apart, so a bug report names the build it came from, and govern what ending that trial hands back. - `BUILD-01` git-fi shall treat itself as a dev build when a `.git` entry exists at its own package root. A published tarball carries none — npm's `files` list ships `dist`, `man`, `completions`, and the postinstall script — so the marker separates a linked checkout from an installed copy. It shall be read at the package root rather than the working directory, which is a git repository on every ordinary run. - `BUILD-02` On a dev build, `--version` (`OPTION-05`) shall report the released version followed by `-dev.g`, naming the commit the build came from, and shall append `.dirty` when the checkout's tree differs from that commit. The `g` prefix is what keeps the identifier valid semver: a sha of all digits would otherwise read as a numeric identifier, which may not carry leading zeros. When no commit can be read, git-fi shall report `-dev` alone rather than dropping the marker. +- `BUILD-03` Ending a trial shall restore the exact published version the trial displaced. `npm link` overwrites the global install, so the version is read before the link and kept under `.trial/`; `trial:off` then installs that version rather than whatever is newest, because a trial borrows an install and should hand back the one it borrowed. Where no published version was recorded — the package was absent, or already linked to a checkout — the trial shall say which of the two it found and install the latest, rather than leave the developer with no `git fi` at all. Where a version was recorded, the trial shall install that version or fail saying so; a recorded version that no longer resolves is not grounds for substituting another. A recorded value that is not a bare version shall be refused rather than passed to the installer, which reads the wider `name@spec` grammar. +- `BUILD-04` Ending a trial shall remove only the rc-file block it wrote, delimited by its own markers, and shall report any remaining line naming the trial's completion directory by its literal path rather than editing it away. That directory is deleted as the trial ends, so a hand-copied `fpath` entry naming it would otherwise be left pointing at nothing, and a line the developer wrote is not the script's to remove. A reference spelled through `~` or a variable is not detected. ## `INSTALL` diff --git a/STATUS.md b/STATUS.md index 52d749a..946a5c9 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,7 +2,7 @@ Tracks implementation status of each requirement in [SPEC.md](/SPEC.md). -**Last updated:** 2026-09-15 +**Last updated:** 2026-09-17 Locations name the file and the enclosing symbol rather than a line range, so an edit elsewhere in the same file leaves the row correct. `git grep` the symbol to @@ -12,8 +12,8 @@ land on it. | Status | Count | |---------|-------| -| Covered | 134 | -| Total | 134 | +| Covered | 136 | +| Total | 136 | ## `PRE` @@ -277,6 +277,8 @@ Build Provenance |--------|--------------------------|---------|--------------------------------| | BUILD-01 | Dev build detection | Covered | `src/build-info.ts` (`isDevBuild`) | | BUILD-02 | `--version` names the commit | Covered | `src/build-info.ts` (`describeVersion`), `src/index.ts` (`parseArgs`) | +| BUILD-03 | Trial restores the version it displaced | Covered | `scripts/trial.sh` (`installed_state`) | +| BUILD-04 | Trial removes only its own rc block | Covered | `scripts/trial.sh` (`off`) | ## `INSTALL` diff --git a/justfile b/justfile index 5295200..1fe67cc 100644 --- a/justfile +++ b/justfile @@ -37,7 +37,7 @@ verify: trial-on: npm run trial:on -# Unlink the checkout and reinstall the published @gettyimages/git-fi +# Unlink the checkout and restore the @gettyimages/git-fi version it replaced trial-off: npm run trial:off diff --git a/scripts/trial.sh b/scripts/trial.sh index 9fac5ae..b3814a6 100644 --- a/scripts/trial.sh +++ b/scripts/trial.sh @@ -14,13 +14,57 @@ set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd)" compdir="$root/.trial/completions" +restore="$root/.trial/replaced-version" zshrc="${HOME}/.zshrc" marker="git-fi-trial" +pkg="@gettyimages/git-fi" + +# What the global install is right now: a version for a registry install, +# `linked` where it already points at a checkout, or empty where the package is +# absent. `npm link` overwrites that install, so the answer has to be taken +# before it runs and kept for `off` to restore. `npm ls` exits non-zero on some +# tree problems while still printing usable JSON, so its status is discarded +# rather than aborting the run under `pipefail`. +installed_state() { + { npm ls -g --depth=0 --json "$pkg" 2>/dev/null || true; } | node -e ' + const raw = require("fs").readFileSync(0, "utf8"); + try { + const dep = (JSON.parse(raw).dependencies ?? {})[process.argv[1]]; + if (!dep) process.exit(0); + // A linked checkout reports `resolved` as a file: URL; only a + // registry install names a version to come back to. + if (String(dep.resolved ?? "").startsWith("file:")) process.stdout.write("linked"); + else if (dep.version) process.stdout.write(dep.version); + } catch (e) { + process.stderr.write(`could not read the global ${process.argv[1]}: ${e.message}\n`); + } + ' "$pkg" +} case "${1:-}" in on) cd "$root" npm run build + + mkdir -p "$root/.trial" + if [ ! -f "$restore" ]; then + state="$(installed_state)" + case "$state" in + linked) + : >"$restore" + echo "$pkg already points at a checkout, so the version it displaced is gone; trial:off will install the latest. Pin by hand if you need a particular one." + ;; + "") + : >"$restore" + echo "No published $pkg is installed; trial:off will install the latest." + ;; + *) + printf '%s\n' "$state" >"$restore" + echo "Trial will restore $pkg@$state" + ;; + esac + fi + npm link node "$root/dist/index.js" install-completions --write "$compdir" @@ -47,15 +91,57 @@ on) echo "Open a new terminal, then try: git fi git fi -a git-fi " ;; off) + version="" + [ -f "$restore" ] && version="$(cat "$restore")" + + # `npm i -g @` takes far more than a version — a URL, a git ref, + # a `file:` path, an `npm:` alias — and this value comes off disk. Anything + # that isn't a bare version is refused rather than fetched. + case "$version" in + "") ;; + -* | *[!0-9A-Za-z.+-]*) + echo "! $restore does not hold a version: $version" >&2 + echo " Refusing to install it. Delete the file and re-run to get the latest." >&2 + exit 1 + ;; + esac + if [ -f "$zshrc" ]; then tmp="$(mktemp)" sed "/# BEGIN ${marker}/,/# END ${marker}/d" "$zshrc" >"$tmp" && mv "$tmp" "$zshrc" + + # Only the marked block is ours to remove, so a hand-copied reference to + # the same directory survives it — and `.trial/` goes on the next line, + # leaving that fpath entry pointing at nothing. Name it rather than + # delete a line the user wrote. + if grep -qF "$compdir" "$zshrc"; then + echo "! $zshrc still names $compdir outside the ${marker} block:" >&2 + grep -nF "$compdir" "$zshrc" >&2 + echo " That directory is about to go; remove those lines by hand." >&2 + fi fi + rm -rf "$root/.trial" - npm rm -g @gettyimages/git-fi || true - npm i -g @gettyimages/git-fi + state="$(installed_state)" + + if [ -n "$version" ]; then + # The exact version the trial displaced, not whatever is newest: a trial + # should hand back the install it borrowed. + npm rm -g "$pkg" || true + npm i -g "$pkg@$version" + elif [ -n "$state" ] && [ "$state" != "linked" ]; then + # Nothing is linked, so no trial is running and this install is someone's + # own choice. Reinstalling here would replace it with whatever is newest, + # which is the outcome the recorded version exists to prevent. + echo "No trial to revert; $pkg@$state is installed from the registry. Leaving it alone." + else + npm rm -g "$pkg" || true + echo "No recorded version to come back to; installing the latest $pkg." + npm i -g "$pkg" + fi + echo - echo "Reverted. Open a new terminal." + echo "Reverted. Open a new terminal, then \`git fi --version\` to confirm." ;; *) echo "usage: npm run trial:on | npm run trial:off" >&2