Skip to content

fix: report the real --version, and answer "who am I?" per environment (#116, #117) - #146

Merged
2000game merged 4 commits into
mainfrom
fix/version-and-auth-env-116-117
Aug 24, 2026
Merged

2000game merged 4 commits into
mainfrom
fix/version-and-auth-env-116-117

Conversation

@2000game

Copy link
Copy Markdown
Member

Two small CLI-surface bugs that both cost more time than their size suggests.

ct --version printed 0.0.0 (#116)

src/index.ts had .version("0.0.0") as a literal, so a released install and a locally linked dev build reported the same thing — which is worse than reporting nothing, because it reads like evidence. A downstream repo's handoff notes cited 0.0.0 as proof that a global dev build was shadowing the pinned one; it wasn't, and the time went into a PATH problem that did not exist.

The version is now baked in from package.json at build time (both tsup and bun build --compile inline the JSON import), and printed with the resolved entry path, so one command answers both questions:

$ ct --version
1.7.0 (/usr/local/bin/ct)

Verified on all three paths: dist/index.js, npm run dev from source (reports src/index.ts, which makes a dev build obvious), and a real bun build --compile binary. Inside a standalone binary import.meta.url points at /$bunfs/..., which bun's fs shim reports as existing — so that prefix is recognised explicitly and the binary's own path is shown instead.

One thing worth your judgement: baking the version in makes the artifacts version-sensitive at build time, and the release pipeline compiles the binaries in the build job — before semantic-release bumps package.json. Left alone, every released binary would report the 0.1.0 placeholder, i.e. #116 fixed for the npm package but not for the install path the README leads with. So the binaries are now recompiled inside semantic-release's prepare step, exactly where the tarball is already packed for the same reason (#84), with both jobs calling one shared .github/scripts/build-binaries.sh.

The trade-off: the smoke-tested binaries are no longer byte-identical to the attached ones — they differ by the version constant. That is the same trade-off already accepted for the tarball. If you would rather not take it, the last three files of the first commit (.releaserc.json, release.yml, build-binaries.sh) can be dropped on their own and the rest still fixes the npm path.

ct auth status had no --env (#117)

The one command whose entire job is answering "which account is this?" could only answer it for the default host, while every other host-touching command takes -e, --env.

$ ct auth status --env dev          # identity on dev's host
$ ct auth status --all              # preflight: every env in ct.envs.json
dev   https://mychurch-dev.church.tools  ✓ Ada Lovelace (#42) via Keychain
prod  https://mychurch.church.tools      ✗ no token
  • --all exits non-zero when any env has no working token, so CI can gate on it before an apply. A failing env becomes that env's line rather than aborting the run — one unreachable instance cannot hide the others.
  • Token resolution mirrors authedSession exactly (profile tokenEnv → CT_LOGINTOKEN → host-keyed Keychain entry), so a green line means the same command with --env will authenticate the same way. The stored lookup is host-keyed, so one env can never report another's identity — pinned by a test.
  • The token itself is never printed, only where it came from.
  • The host goes to stderr, so ct auth status | jq still sees only the identity JSON.
  • ct auth logout --env <name> clears just that host and leaves other logins in place; the default blob goes too when it holds a copy of the same token, so no secret is orphaned.

Read-only throughout: the only network call is the whoami handshake, and only for an env that has a token to try.

Tab completion needed no changes — it reads options off the program, so ct auth status --env <TAB> completes real env names out of ct.envs.json for free.

Validation

  • npm test — 858 passed, 5 skipped (28 new)
  • npm run typecheck, npx eslint src tests, prettier --check, git diff --check
  • node .github/scripts/docs-staleness.mjs — all 5 pages current
  • Standalone Apple Silicon binary compiled with bun 1.4.0 and run through .github/scripts/smoke-test-binary.sh
  • ct auth status, --env dev, --all, --all --env (rejected) and logout --env <unknown> exercised against a fixture ct.envs.json

Fixes #116
Fixes #117

https://claude.ai/code/session_018JShVZYNLaRb4hF5KbHCXG

`--version` was the literal `.version("0.0.0")`, identical for a released
install and a locally linked dev build. That is worse than a missing version:
it reads like evidence, and in one downstream repo it sent the next person
chasing a PATH problem that did not exist.

The version is now baked in from package.json at build time — `tsup` and
`bun build --compile` both inline the JSON import — and printed alongside the
resolved entry path, so `ct --version` answers "which version" and "which ct"
at once:

    1.7.0 (/usr/local/bin/ct)

Inside a standalone binary `import.meta.url` points into bun's embedded
filesystem, which bun's `fs` shim reports as existing; that prefix is
recognised so the binary's own path on disk is shown instead.

Baking the version in makes the artifacts version-sensitive at BUILD time,
which the release pipeline has to respect: the binaries are now recompiled in
semantic-release's prepare step, after the version bump, for the same reason
the tarball is already packed there (#84). Otherwise every released binary
would report the repo's 0.1.0 placeholder. Both jobs call one shared
build-binaries.sh so the two invocations cannot drift.

Fixes #116

Claude-Session: https://claude.ai/code/session_018JShVZYNLaRb4hF5KbHCXG
`plan`, `apply`, `adopt`, `get`, `coverage`, `state`, `refresh` and
`permissions` all take `-e, --env <name>`; `auth status` did not, so the one
command whose entire job is answering "which account is this?" could only ever
answer it for the default host. Finding out which identity `--env dev` would
use meant running something that touches the host for real, or reading
ct.envs.json and the Keychain by hand.

- `ct auth status --env <name>` resolves that env's host and reports the
  identity there. The host goes to stderr, so `| jq` still sees only the
  identity JSON.
- `ct auth status --all` checks every environment in ct.envs.json — one line
  each, with where the token came from and nothing of the token itself:

      dev   https://mychurch-dev.church.tools  ✓ Ada Lovelace (#42) via Keychain
      prod  https://mychurch.church.tools      ✗ no token

  It exits non-zero if any environment has no working token, so CI can gate on
  it before an apply. A failing env is reported as that env's line rather than
  aborting the run, so one unreachable instance cannot hide the others.
- `ct auth logout --env <name>` clears just that host's credentials and leaves
  other logins in place (the default blob goes too when it holds a copy of the
  same token, so no secret is orphaned).

Token resolution mirrors authedSession exactly — profile `tokenEnv` →
CT_LOGINTOKEN → the host-keyed Keychain entry — so a green line means the same
command with `--env` will authenticate the same way. The stored lookup is
host-keyed, so one env can never report another env's identity. Read-only
throughout: the only network call is the whoami handshake, and only for an env
that has a token to try.

Fixes #117

Claude-Session: https://claude.ai/code/session_018JShVZYNLaRb4hF5KbHCXG
`ct auth status --all` walks every host in ct.envs.json, so the bare
`CT_LOGINTOKEN` fallback fanned one instance's token out to all of them —
as a `login_token=` query parameter, into every instance's access log —
and then reported a green line for envs nothing was configured for. The
ambient token is now offered only to the host it is bound to (`CT_HOST`,
else the stored default login's host); everything else reports no token.

Failures are rendered with `formatError`, so the HTTP status a real
`CtApiError` carries survives — 401, 403 and 500 no longer render alike.
Token resolution moved inside the try, so a credential store that throws
reports that env instead of aborting the sweep, and the preflight now runs
`assertMinVersion` too, so a green line cannot be followed by an apply
that refuses on the instance version.

`ct auth logout --env <name>` still drops the default blob when it holds a
copy of the same token — but it now says so, instead of promising that
other logins are untouched while commands without `--env` lose their host.

Release: the shipped binaries were the only ones never executed — they are
recompiled in semantic-release's prepare step, after the smoke jobs ran, on
an unpinned `bun-version: latest`. Both jobs now pin the same bun, the
recompiled linux binary is smoke-tested again in the release job, and the
smoke script asserts `ct --version`, which nothing in CI had ever run on
the compiled path.

The version constant is injected via `define` (tsup + `bun build`) instead
of a default JSON import esbuild cannot tree-shake, which was inlining the
whole manifest — devDependencies, scripts, dependency list — into the
published bundle; running from source falls back to reading package.json.
Finally, cli-version compared a decoded path against a percent-encoded one,
which fails for any checkout path needing escaping.

Claude-Session: https://claude.ai/code/session_018JShVZYNLaRb4hF5KbHCXG
@2000game
2000game merged commit e1f1798 into main Aug 24, 2026
3 checks passed
@2000game
2000game deleted the fix/version-and-auth-env-116-117 branch August 24, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant