Skip to content

Commit 1573b40

Browse files
committed
fix(cli): harden and simplify update checks
1 parent e0e0f01 commit 1573b40

10 files changed

Lines changed: 841 additions & 542 deletions

File tree

apps/docs/content/docs/cli/configuration.mdx

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -114,35 +114,41 @@ shared profile cannot also set its own endpoint or API key.
114114
| `SIM_API_KEY` | API key — skips `sim login` entirely |
115115
| `SIM_WORKSPACE` | Workspace to target |
116116
| `SIM_OUTPUT` | Output format |
117-
| `SIM_CONFIG_DIR` | Relocate both files away from `~/.sim` |
117+
| `SIM_CONFIG_DIR` | Relocate the config directory and update cache; file-specific overrides below still win |
118118
| `SIM_CONFIG_FILE` | Relocate only the config file |
119119
| `SIM_CREDENTIALS_FILE` | Relocate only the credentials file |
120120
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
121121
| `SIM_DEBUG` | Trace each request's method, URL, status and duration to stderr |
122-
| `SIM_NO_UPDATE_CHECK` | Turn off the once-a-day update notice |
122+
| `SIM_NO_UPDATE_CHECK` | Turn off update checks |
123123

124124
## Update notices
125125

126-
At most once a day, and only when stderr is a terminal, the CLI asks
126+
On eligible invocations, the CLI uses a daily cache before asking
127127
`registry.npmjs.org` what is published under the `latest` tag. Prerelease
128-
installs are skipped entirely rather than compared against their own channel,
129-
so a `-preview` or `-dev` build is never told to upgrade. When a newer one exists it prints a single line on stderr naming
130-
both versions and the command that upgrades:
128+
installs are skipped entirely, so a `-preview` or `-dev` build is never told to
129+
upgrade. When a newer one exists, it prints a single line on stderr naming both
130+
versions and the command that upgrades:
131131

132132
```
133133
Update available: sim 2.1.2 → 2.1.5. Run: npm install -g sim@latest
134134
```
135135

136-
The request carries the CLI version and nothing else — no Sim API key, no
137-
workspace, no command — and it never follows a redirect away from the registry
138-
it asked.
136+
Apart from the configured registry URL, the request identifies only the CLI
137+
version — no Sim API key, workspace, or command — and it never follows a
138+
redirect away from the registry it asked.
139139

140140
One caveat worth stating plainly: if you point `npm_config_registry` at a
141-
private mirror, the check goes to that mirror instead of npm, and any
142-
credentials embedded in that URL (an Artifactory or Nexus `?token=…`) are sent
143-
with it — they have to be, or the mirror would reject the request. Those are
144-
your registry's credentials, not Sim's, and they go only to the host you
145-
configured.
141+
private mirror, the check goes to that mirror instead of npm. Query-string
142+
credentials (an Artifactory or Nexus `?token=…`, for example) are preserved and
143+
sent as part of the configured registry request — they have to be, or the
144+
mirror would reject it. As with other registry traffic, configured proxies or
145+
TLS inspection can observe what that network setup permits. A registry URL
146+
containing username/password userinfo, such as
147+
`https://user:password@registry.example`, is rejected and no update check is
148+
made.
149+
150+
Malformed and non-HTTP(S) configured registry values also disable the update
151+
check rather than making an unexpected request to the public registry.
146152

147153
The notice is skipped entirely when:
148154

@@ -153,27 +159,33 @@ The notice is skipped entirely when:
153159
- the CLI is running under `npx`, which resolves the newest version every time
154160
- the CLI is running from a checkout of the sim repository, whose version
155161
deliberately trails the published one
156-
- the installed version is a prerelease from the `staging` or `dev` channel
157-
158-
The once-a-day pace comes from a timestamp in `update-check.json`, kept beside
159-
the config and credentials files: `~/.sim/update-check.json` by default, and
160-
under `SIM_CONFIG_DIR` when that is set. If it cannot be written — a read-only
161-
home in a container, or a `~/.sim` left root-owned by an earlier `sudo` install
162-
— the pace cannot be remembered, so the check runs once per command instead of
163-
once per day. It stays bounded by the same one-second timeout, and
164-
`SIM_NO_UPDATE_CHECK=1` still turns it off.
165-
166-
Set `npm_config_registry` to ask a mirror instead; its path and query are
167-
preserved, so a token-authenticated Artifactory or Nexus base works.
162+
- the installed version is a prerelease
163+
164+
The daily pace comes from a timestamp in the config directory's
165+
`update-check.json`: `~/.sim/update-check.json` by default, or under
166+
`SIM_CONFIG_DIR` when that is set. `SIM_CONFIG_FILE` and
167+
`SIM_CREDENTIALS_FILE` do not move the cache, so it may not sit beside a file
168+
relocated with either of those variables.
169+
170+
This throttle is best-effort across processes. Two commands that start together
171+
can both see a stale cache and check. Cache replacement is atomic, so either
172+
complete write can win without leaving a partially interleaved file. If the
173+
cache cannot be written — for example, because the config directory is
174+
read-only — every eligible invocation attempts a check because there is no
175+
timestamp to reuse.
176+
177+
The registry check has a one-second deadline. On expiry, the CLI terminates its
178+
short-lived request process so stalled DNS, connection, or response work cannot
179+
remain active and delay the command. `SIM_NO_UPDATE_CHECK=1` still turns the
180+
check off.
168181

169182
The command the notice prints matches how Sim was installed — `npm install -g`,
170183
`pnpm add -g`, `bun add -g`, or `yarn global add` — so running it updates the
171184
executable already on your `PATH` rather than installing a second copy under a
172185
different package manager.
173186

174-
Node ignores `HTTPS_PROXY` unless you also set `NODE_USE_ENV_PROXY=1`, and only
175-
from Node 22.21 and 24.5. The CLI warns when a proxy is configured but will not
176-
be used.
187+
Node's `fetch` uses `HTTP(S)_PROXY` when opted in with `NODE_USE_ENV_PROXY=1`
188+
(Node 22.21+ or 24.0+) or `--use-env-proxy` (Node 22.21+ or 24.5+).
177189

178190
For CI, set `SIM_API_KEY` and `SIM_WORKSPACE` and nothing needs to touch the
179191
filesystem at all.

apps/docs/content/docs/cli/troubleshooting.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ one installs a second copy instead of replacing the executable on your `PATH`:
118118
</Tab>
119119
</Tabs>
120120

121-
The CLI normally tells you this itself, once a day, on stderr, and the command
122-
it prints already matches your installation. It stays quiet when stderr is
123-
redirected, in CI, and under `npx`.
121+
The CLI can also tell you this through a cached daily check on eligible
122+
invocations, and the command it prints already matches your installation. It
123+
stays quiet when stderr is redirected, in CI, and under `npx`.
124124

125125
## An update notice appears in output I am parsing
126126

packages/sim-cli/README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,21 +256,25 @@ The main environment variables are:
256256
| `SIM_API_KEY` | API key, usually for CI |
257257
| `SIM_WORKSPACE` | Workspace to target |
258258
| `SIM_OUTPUT` | `table`, `json`, `yaml`, or `text` |
259-
| `SIM_CONFIG_DIR` | Directory containing CLI config and credentials |
259+
| `SIM_CONFIG_DIR` | Base directory for CLI config, credentials, and the update cache |
260260
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely |
261261
| `SIM_DEBUG` | Print request diagnostics to stderr |
262262
| `SIM_NO_UPDATE_CHECK` | Turn off the update notice |
263263

264-
Once a day, at an interactive terminal, `sim` asks `registry.npmjs.org` what is
265-
published under the `latest` tag and prints one line on stderr when a newer
266-
version exists. Prerelease installs are skipped entirely. The once-a-day pace
267-
depends on a writable `~/.sim`; without one the check runs per command, still
268-
bounded by a one-second timeout. It sends nothing but its own version and never
269-
your Sim API key. If `npm_config_registry` points at a private mirror, the check
270-
goes there instead and carries whatever credentials that URL embeds, since the
271-
mirror would otherwise refuse it. Set `SIM_NO_UPDATE_CHECK=1` to turn it off;
272-
the full list of cases where it stays quiet is in the
273-
[configuration guide](https://docs.sim.ai/cli/configuration).
264+
On eligible interactive invocations, `sim` uses a daily cache before asking
265+
`registry.npmjs.org` what is published under the `latest` tag and prints one
266+
line on stderr when a newer version exists. Prerelease installs are skipped
267+
entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
268+
without a writable cache, each eligible invocation checks again. Concurrent
269+
invocations can also perform duplicate checks. The registry request has a
270+
one-second deadline; the short-lived request process is terminated on expiry.
271+
Apart from the configured registry URL, it sends only its own version and never
272+
your Sim API key. If `npm_config_registry` points at a private mirror, its query
273+
string is preserved, including any query-string credentials. Registry URLs
274+
containing username/password userinfo are rejected. Set
275+
`SIM_NO_UPDATE_CHECK=1` to turn it off; malformed or non-HTTP(S) configured
276+
registry values also fail closed. The full list of cases where it stays quiet
277+
is in the [configuration guide](https://docs.sim.ai/cli/configuration).
274278

275279
## Documentation
276280

packages/sim-cli/src/program.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ describe('the update check', () => {
198198
await parse(['--help'], program)
199199
expect(fired).toBe(0)
200200

201-
// And the sentinel is not inert: the same hook does fire for a real action,
202-
// which is what makes the assertion above mean something.
203201
const dir = mkdtempSync(join(tmpdir(), 'sim-cli-program-'))
202+
const previousConfigDir = process.env.SIM_CONFIG_DIR
204203
process.env.SIM_CONFIG_DIR = dir
205204
try {
206205
await parse(['configure', '--set-output', 'json'], program)
207206
expect(fired).toBe(1)
208207
} finally {
209-
process.env.SIM_CONFIG_DIR = undefined
208+
if (previousConfigDir === undefined) Reflect.deleteProperty(process.env, 'SIM_CONFIG_DIR')
209+
else process.env.SIM_CONFIG_DIR = previousConfigDir
210210
rmSync(dir, { recursive: true, force: true })
211211
}
212212
})
@@ -227,8 +227,6 @@ describe('the update check', () => {
227227
const preAction = preActionHooks(program)
228228

229229
expect(preAction).toHaveLength(1)
230-
// Invoking it must resolve, never throw: it runs in front of the user's
231-
// command, and a rejection here would fail the command itself.
232230
await expect(preAction[0](program, program)).resolves.toBeUndefined()
233231
})
234232
})

packages/sim-cli/src/program.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,6 @@ export function buildProgram(options: { version?: boolean } = {}): Command {
152152

153153
program.addHelpText('after', HELP_EPILOGUE)
154154

155-
// Root hooks are inherited by the whole tree, and commander answers `--help`
156-
// and `--version` during parsing without ever reaching an action — so the two
157-
// invocations that must stay instant are excluded by construction.
158155
program.hook('preAction', () => announceUpdateIfAvailable())
159156

160157
refuseHelpAfterUnknownCommand(program)

0 commit comments

Comments
 (0)