Skip to content

feat(circle): require Circle CLI >= 1.1.1; init upgrades, doctor reports - #196

Merged
SELAT-DEV merged 5 commits into
mainfrom
fix/circle-cli-floor-1.1.1
Sep 18, 2026
Merged

SELAT-DEV merged 5 commits into
mainfrom
fix/circle-cli-floor-1.1.1

Conversation

@SELAT-DEV

@SELAT-DEV SELAT-DEV commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What

selat now 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 whenever circle existed — it now upgrades an older install via npm 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.
  • README: floor stated; arc listed among fundable chains (direct only).

Guard (commit 2) — the first suite run on this branch ran a real npm install -g @circle-fin/cli@latest on my machine: the init fixture's fake circle didn't answer --version, the new logic said "upgrade", and ensureCircle did it. ensureCircle now refuses installs under NODE_TEST_CONTEXT (set by node --test, inherited by children) or SELAT_NO_INSTALL=1.

Closed PATH for spawning tests (commit 3) — the class, not the instance. Every test that spawns bin/selat.mjs inherited the host PATH, so any uncovered subprocess fell through to the real binary. test/helpers/closed-env.mjs builds PATH from the fixture dirs plus one private tools dir with symlinks to exactly node/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 real circle and npm). All eight spawning test files switched. This exposed that hasBin() shelled out to command -v/which — themselves PATH lookups that vanish under a restricted PATH — so it now walks PATH directly (no subprocess; cheaper on the selat run hot path).

Auto-install stays (commit 4)selat init installs @circle-fin/cli@latest when 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.mjs pins 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

  • Real CLI (1.1.2): doctor passes; init says Circle CLI 1.1.2 on PATH, no prompt.
  • Under the closed PATH: npm, circle, which all 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 is npm test itself.

🤖 Generated with Claude Code

karensheng and others added 5 commits September 17, 2026 21:56
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>
@SELAT-DEV
SELAT-DEV merged commit 4d20858 into main Sep 18, 2026
1 check passed
@SELAT-DEV
SELAT-DEV deleted the fix/circle-cli-floor-1.1.1 branch September 18, 2026 02:28
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants