feat(circle): require Circle CLI >= 1.1.1; init upgrades, doctor reports - #196
Merged
Merged
Conversation
ensureCircle() was a no-op whenever `circle` was on PATH, so an existing
1.0.0 install (which only knows ARC-TESTNET) would never move and would
trip the skill's Arc deposit gate forever. It now decides install /
upgrade / ok from MIN_CIRCLE_CLI_VERSION ("1.1.1") and runs
`npm install -g @circle-fin/cli@latest` when below it. `selat init`
checks the version even when the binary is present; `selat doctor` prints
the installed version and fails below the floor with the upgrade command.
circleVersion() / versionAtLeast() / circleInstallPlan() are exported and
the decision logic is unit-tested. The init fixture's fake `circle` now
answers --version (a shim that can't is treated as needing reinstall).
README: floor stated; arc listed among fundable chains (direct only).
Tests: 579/579.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first run of this branch's suite ran a real `npm install -g @circle-fin/cli@latest` on the host: the init fixture's fake `circle` didn't answer --version, the new floor logic classified it as "upgrade", and ensureCircle did what it says. A test must not be able to mutate the developer's global npm tree. ensureCircle now refuses installs when NODE_TEST_CONTEXT (set by node --test, inherited by spawned children) or SELAT_NO_INSTALL=1 is in the environment, and the init fixture additionally shadows `npm` with a script that exits 97. Verified by breaking the fixture's --version answer: the test fails on the refusal and no npm log is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every test that spawns bin/selat.mjs inherited the host PATH, so any subprocess a fixture didn't cover fell through to the real binary. New test/helpers/closed-env.mjs builds PATH from the test's fixture dirs plus one private tools dir holding symlinks to exactly node, sh, bash and env — nothing else resolves, not npm, not the real circle. (Including node's own bin dir would not have closed anything: on nvm installs it holds the whole global tree.) closedEnv() also sets SELAT_NO_INSTALL=1. All eight spawning test files switched. That exposed a dependency in lib/sh.mjs: hasBin() shelled out to `command -v` / `which`, which are themselves PATH lookups — under a restricted PATH they vanish and every binary reads as missing. hasBin now walks PATH directly (statSync + X_OK), no subprocess, which is also cheaper on the `selat run` hot path. Windows keeps `where`. Tests: 579/579. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A global npm install is a host change. `selat init` used to do it unprompted whenever `circle` was missing (and, since this branch, when it was below the floor). Now: in a terminal it asks (default yes); with no TTY it refuses and prints the command, unless --install-circle-cli is passed as explicit consent. An up-to-date CLI is never touched. test/init-circle-consent.test.mjs covers all four branches under the closed PATH: refuse-missing, refuse-old, consent reaches ensureCircle (where the test-context guard stops it — layers 1 and 2 both visible), and up-to-date untouched. Tests: 583/583. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… flag)
Reverts the consent gate: `selat init` installs @circle-fin/cli@latest
when the Circle CLI is missing and upgrades it when below
MIN_CIRCLE_CLI_VERSION, automatically, as it always installed when
missing. --install-circle-cli is gone. init now states which case it hit
("not found — installing" / "1.0.0 is below the 1.1.1 floor — upgrading").
The protections stay: ensureCircle refuses under NODE_TEST_CONTEXT /
SELAT_NO_INSTALL, and spawning tests run under the closed PATH.
test/init-circle-autoinstall.test.mjs pins missing → install attempted,
old → upgrade attempted, current → untouched, with the guard (not npm)
stopping the attempt every time.
Tests: 582/582.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
SELAT-DEV
added a commit
that referenced
this pull request
Sep 18, 2026
@selat-ai/selat-discovery floor ^0.25.0 -> ^0.26.1 (Arc deposits via the Circle CLI, raw-key path removed, Circle CLI floor 1.1.1). Carries #195 (fund: Arc through the agent wallet) and #196 (Circle CLI >= 1.1.1; init auto-upgrades, doctor reports). Co-authored-by: Karen Sheng <ksherlocked@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
selatnow requires Circle CLI ≥ 1.1.1 and enforces it — with consent, and without any way for tests to touch the host.Floor (commit 1)
lib/circle.mjs:MIN_CIRCLE_CLI_VERSION = "1.1.1",circleVersion(),versionAtLeast(),circleInstallPlan()(pure: install / upgrade / ok).ensureCircle()was a no-op whenevercircleexisted — it now upgrades an older install vianpm install -g @circle-fin/cli@latest.selat doctor:✓ circle 1.1.2 on PATH, or✗ circle 1.0.0 is below the 1.1.1 floor …with the upgrade command.arclisted among fundable chains (direct only).Guard (commit 2) — the first suite run on this branch ran a real
npm install -g @circle-fin/cli@lateston my machine: the init fixture's fakecircledidn't answer--version, the new logic said "upgrade", andensureCircledid it.ensureCirclenow refuses installs underNODE_TEST_CONTEXT(set bynode --test, inherited by children) orSELAT_NO_INSTALL=1.Closed PATH for spawning tests (commit 3) — the class, not the instance. Every test that spawns
bin/selat.mjsinherited the host PATH, so any uncovered subprocess fell through to the real binary.test/helpers/closed-env.mjsbuilds PATH from the fixture dirs plus one private tools dir with symlinks to exactlynode/sh/bash/env— nothing else resolves (node's own bin dir is deliberately not included: on nvm it carries the whole global tree, including the realcircleandnpm). All eight spawning test files switched. This exposed thathasBin()shelled out tocommand -v/which— themselves PATH lookups that vanish under a restricted PATH — so it now walks PATH directly (no subprocess; cheaper on theselat runhot path).Auto-install stays (commit 4) —
selat initinstalls@circle-fin/cli@latestwhen the Circle CLI is missing and upgrades it when below the floor, automatically (no prompt, no flag), as it always installed when missing. It now states which case it hit.test/init-circle-autoinstall.test.mjspins missing → install, old → upgrade, current → untouched — each under the closed PATH, where the test-context guard (not npm) stops the attempt.Why
1.1.1 is the first Circle CLI that lists Arc mainnet; selat-discovery 0.26.0's Arc deposit path assumes it. Without this, every existing install stays on 1.0.0 forever. Companion: SELAT-AI/selat-discovery#174.
Verified
initsaysCircle CLI 1.1.2 on PATH, no prompt.npm,circle,whichall unresolvable; the consent tests show the gate → ensureCircle → guard chain end to end.npm test: 583/583; the only npm log written during the run isnpm testitself.🤖 Generated with Claude Code