This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add `pty --version` / `pty version` (#66)

Fresh-clone onboarding papercut: `pty --version` printed 'Unknown command'.
Add a version command (`version`, `--version`, `-v`, `-V`) that prints the
version per house convention '<semver>+<short-sha>' — semver from package.json,
short sha from git when this is a pty checkout, gracefully omitted otherwise
(e.g. an npm install prints bare '<semver>').

- src/version.ts: import-safe module with readPackageVersion / readGitShortSha /
formatVersion / printVersion. The sha lookup is gated on a .git in pty's own
package root, so an npm-installed copy inside an unrelated parent repo never
reports that repo's sha as pty's version.
- cli.ts: version switch cases + a Global help line.
- completions: add `version` (+ --version/-v) to bash & zsh.
- tests/version.test.ts: unit tests for formatVersion (with/without sha) +
integration tests that each form prints the version and exits 0.

Verified: all forms print e.g. 0.11.0+c87a6c6; npm run typecheck clean; tests green.

authored by

Nathan and committed by
GitHub
(Jul 9, 2026, 5:57 PM +0200) bc4d70ce c87a6c64

+108 -2
+2 -2
completions/pty.bash
··· 7 7 COMPREPLY=() 8 8 cur="${COMP_WORDS[COMP_CWORD]}" 9 9 prev="${COMP_WORDS[COMP_CWORD-1]}" 10 - commands="run attach a exec peek send events list ls stats restart kill rm remove gc tag tag-multi emit rename up down test help" 10 + commands="run attach a exec peek send events list ls stats restart kill rm remove gc tag tag-multi emit rename up down test version help" 11 11 12 12 # Complete subcommand (first positional). 13 13 if [[ ${COMP_CWORD} -eq 1 ]]; then 14 14 if [[ "${cur}" == -* ]]; then 15 - COMPREPLY=($(compgen -W "--root --preselect-new --filter-tag --help -h" -- "${cur}")) 15 + COMPREPLY=($(compgen -W "--root --preselect-new --filter-tag --help -h --version -v" -- "${cur}")) 16 16 else 17 17 COMPREPLY=($(compgen -W "${commands}" -- "${cur}")) 18 18 fi
+1
completions/pty.zsh
··· 38 38 'up:Start sessions from pty.toml' 39 39 'down:Stop sessions from pty.toml' 40 40 'test:Run the pty test suite (vitest)' 41 + 'version:Print the version' 41 42 'help:Show usage' 42 43 ) 43 44
+10
src/cli.ts
··· 5 5 import { spawnSync, execFileSync } from "node:child_process"; 6 6 import { randomBytes } from "node:crypto"; 7 7 import { attach, peek, send, queryStats, type StatsResult } from "./client.ts"; 8 + import { printVersion } from "./version.ts"; 8 9 import { parseSeqValue } from "./keys.ts"; 9 10 import { 10 11 listSessions, ··· 162 163 Global: 163 164 pty --root <path> <subcommand> [...] Pin the state registry for this call (== PTY_ROOT env) 164 165 pty help | pty --help | pty -h Show this usage 166 + pty version | pty --version | pty -v Print the version (<semver>+<short-sha>) 165 167 pty test [watch | -t "pattern"] Run the pty test suite (vitest passthrough) 166 168 167 169 Session references (<ref>): the on-disk id (validated: [A-Za-z0-9._-], ≤ 255 chars, ··· 1025 1027 1026 1028 case "test": { 1027 1029 await cmdTest(args.slice(1)); 1030 + break; 1031 + } 1032 + 1033 + case "version": 1034 + case "--version": 1035 + case "-v": 1036 + case "-V": { 1037 + printVersion(); 1028 1038 break; 1029 1039 } 1030 1040
+49
src/version.ts
··· 1 + import * as fs from "node:fs"; 2 + import * as path from "node:path"; 3 + import { execFileSync } from "node:child_process"; 4 + 5 + /** Directory of this module (dist/ when built, src/ when run from source). 6 + * package.json is one level up in both layouts. */ 7 + function selfDir(): string { 8 + return import.meta.dirname ?? path.dirname(new URL(import.meta.url).pathname); 9 + } 10 + 11 + /** Semver from the shipped package.json. Falls back to "0.0.0" if it can't be 12 + * read (should never happen — npm always ships package.json). */ 13 + export function readPackageVersion(): string { 14 + try { 15 + const pkg = JSON.parse(fs.readFileSync(path.join(selfDir(), "..", "package.json"), "utf8")); 16 + return typeof pkg.version === "string" ? pkg.version : "0.0.0"; 17 + } catch { 18 + return "0.0.0"; 19 + } 20 + } 21 + 22 + /** Short git sha of THIS pty checkout, or null when unavailable. Gated on a 23 + * `.git` in pty's own package root so an npm-installed copy sitting inside an 24 + * unrelated parent repo never reports that repo's sha as pty's version. */ 25 + export function readGitShortSha(): string | null { 26 + const root = path.join(selfDir(), ".."); 27 + try { 28 + if (!fs.existsSync(path.join(root, ".git"))) return null; 29 + const sha = execFileSync("git", ["rev-parse", "--short", "HEAD"], { 30 + cwd: root, 31 + encoding: "utf8", 32 + stdio: ["ignore", "pipe", "ignore"], 33 + }).trim(); 34 + return /^[0-9a-f]{4,}$/.test(sha) ? sha : null; 35 + } catch { 36 + return null; 37 + } 38 + } 39 + 40 + /** Format per house convention: `<semver>+<short-sha>`, or bare `<semver>` when 41 + * no git sha is available. Pure — exported for unit testing. */ 42 + export function formatVersion(version: string, sha: string | null): string { 43 + return sha ? `${version}+${sha}` : version; 44 + } 45 + 46 + /** Print the version as `<semver>+<short-sha>` (or bare `<semver>`). */ 47 + export function printVersion(): void { 48 + console.log(formatVersion(readPackageVersion(), readGitShortSha())); 49 + }
+46
tests/version.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import * as fs from "node:fs"; 3 + import * as path from "node:path"; 4 + import { fileURLToPath } from "node:url"; 5 + import { spawnSync } from "node:child_process"; 6 + import { formatVersion } from "../src/version.ts"; 7 + 8 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 + const nodeBin = process.execPath; 10 + const cliPath = path.join(__dirname, "..", "dist", "cli.js"); 11 + const pkgVersion = JSON.parse( 12 + fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"), 13 + ).version as string; 14 + 15 + describe("formatVersion", () => { 16 + it("appends +<sha> when a sha is present", () => { 17 + expect(formatVersion("0.11.0", "abcdef1")).toBe("0.11.0+abcdef1"); 18 + }); 19 + 20 + it("prints bare semver when there is no sha (e.g. npm install)", () => { 21 + expect(formatVersion("0.11.0", null)).toBe("0.11.0"); 22 + }); 23 + }); 24 + 25 + describe("pty --version", () => { 26 + // `<semver>` optionally followed by `+<short-sha>`. Tests run inside the git 27 + // checkout, so the sha part is present here; the no-sha path is covered by 28 + // the formatVersion unit test above. 29 + const VERSION_RE = /^\d+\.\d+\.\d+(\+[0-9a-f]{4,})?$/; 30 + 31 + for (const form of ["--version", "version", "-v", "-V"]) { 32 + it(`\`pty ${form}\` prints the version and exits 0`, () => { 33 + const r = spawnSync(nodeBin, [cliPath, form], { encoding: "utf8", timeout: 15000 }); 34 + expect(r.status).toBe(0); 35 + const out = r.stdout.trim(); 36 + expect(out).toMatch(VERSION_RE); 37 + // The semver part must match package.json. 38 + expect(out.split("+")[0]).toBe(pkgVersion); 39 + }); 40 + } 41 + 42 + it("is not treated as an unknown command", () => { 43 + const r = spawnSync(nodeBin, [cliPath, "--version"], { encoding: "utf8", timeout: 15000 }); 44 + expect(r.stderr).not.toContain("Unknown command"); 45 + }); 46 + });