Skip to content

Commit f45cab0

Browse files
feat(cli): add explicit self-update command and update notices (#7753)
* feat(cli): add explicit self-update command and update notices * fix(cli): reject update downgrades and explain installation errors * fix(cli): preserve update failures during lock cleanup
1 parent 0bae22b commit f45cab0

17 files changed

Lines changed: 1023 additions & 27 deletions

apps/docs/content/docs/cli/commands.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ sim configure [options]
124124

125125
</CommandTable>
126126

127+
## Update this global CLI installation to the newest release on its channel
128+
129+
```bash
130+
sim update [options]
131+
```
132+
133+
**Options**
134+
135+
<CommandTable>
136+
137+
| Option | Required | Description |
138+
| --- | --- | --- |
139+
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |
140+
141+
</CommandTable>
142+
127143
## Ask Sim and print the reply
128144

129145
```bash

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,27 @@ endpoint or stored login.
120120
| `SIM_CREDENTIALS_FILE` | Relocate only the credentials file |
121121
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
122122
| `SIM_DEBUG` | Trace each request's method, URL, status and duration to stderr |
123-
| `SIM_NO_UPDATE_CHECK` | Turn off update checks |
123+
| `SIM_NO_UPDATE_CHECK` | Turn off update checks and notices |
124124

125-
## Update notices
125+
## Updates
126126

127-
The CLI checks for a newer release at most once per day on eligible interactive
128-
invocations. Notices go to stderr and show an upgrade command for the package
129-
manager that installed Sim.
127+
The CLI checks for a newer stable release at most once per day on eligible
128+
interactive invocations. It prints an optional notice to stderr and continues
129+
your command. Installation only happens when you run `sim update`.
130+
131+
Run `sim update` to update immediately, including in CI and when automatic checks
132+
are disabled. It requires a global installation and verifies that the package
133+
manager targets the running copy before installing. Supported managers are npm,
134+
pnpm, Bun, and Yarn Classic. For custom installations, select the manager with
135+
`sim update --package-manager bun`. Project-local and temporary package-runner
136+
copies must be updated through their package manager.
137+
138+
Manual updates preserve the stable, staging, or dev release channel. Installation
139+
failures stop with an error; concurrent update attempts are refused. Installer
140+
output goes to stderr and does not mix with JSON output on stdout.
141+
The updater resolves the channel through the selected package manager before
142+
installing. Older registry or mirror releases are refused; a newer release is
143+
installed by its exact version so a moving tag cannot change the target.
130144

131145
Checks are skipped in CI, when stderr is redirected, under `npm exec` or `npx`,
132146
from a repository checkout, and for prerelease versions. Set
@@ -141,7 +155,9 @@ malformed non-empty values disable the check. Redirects are not followed.
141155
The daily cache is `~/.sim/update-check.json`, or under `SIM_CONFIG_DIR`.
142156
`SIM_CONFIG_FILE` and `SIM_CREDENTIALS_FILE` do not relocate it. If the cache
143157
cannot be written, eligible invocations may check again. Concurrent commands
144-
can also each check. Requests have a one-second deadline.
158+
can also each check. Registry checks have a one-second deadline; package-manager
159+
installation has a five-minute deadline. Registry-check failures suppress the
160+
check, while installer failures are reported.
145161

146162
Node's `fetch` uses `HTTP(S)_PROXY` when opted in with `NODE_USE_ENV_PROXY=1`
147163
(Node 22.21+ or 24.0+) or `--use-env-proxy` (Node 22.21+ or 24.5+).

apps/docs/content/docs/cli/index.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ or local configuration.
3434

3535
Requires Node.js 20 or newer. Verify with `sim --version`.
3636

37+
The CLI shows optional update notices on eligible interactive invocations. Run
38+
`sim update` when you want to install the update. See
39+
[Updates](/cli/configuration#updates) for installation requirements and how to
40+
disable notices.
41+
3742
To run it without installing, use `npx sim <command>`.
3843

3944
Using Sim as a library instead? See the [TypeScript](/api-reference/typescript)

apps/docs/content/docs/cli/reference.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ sim configure [options]
103103

104104
</CommandTable>
105105

106+
## sim update
107+
108+
Update this global CLI installation to the newest release on its channel
109+
110+
```bash
111+
sim update [options]
112+
```
113+
114+
**Options**
115+
116+
<CommandTable>
117+
118+
| Option | Required | Description |
119+
| --- | --- | --- |
120+
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |
121+
122+
</CommandTable>
123+
106124
## sim chat
107125

108126
Ask Sim and print the reply

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ The docs track the current release, so a command that exists here and not in
9999
sim --version
100100
```
101101

102-
Then upgrade with the package manager you installed it with — using a different
102+
Run `sim update` to update the active global installation. If this older release
103+
does not yet have the `update` command, upgrade with the package manager you installed it with — using a different
103104
one installs a second copy instead of replacing the executable on your `PATH`:
104105

105106
<Tabs items={['npm', 'pnpm', 'bun', 'Yarn Classic']}>
@@ -125,9 +126,10 @@ one installs a second copy instead of replacing the executable on your `PATH`:
125126
</Tab>
126127
</Tabs>
127128

128-
The CLI can also tell you this through a cached daily check on eligible
129-
invocations, and the command it prints already matches your installation. It
130-
stays quiet when stderr is redirected, in CI, and under `npm exec` or `npx`.
129+
The CLI also shows an optional update notice through a cached daily check on
130+
eligible interactive invocations. Your command continues, and you choose when to
131+
run `sim update`. Checks stay quiet when stderr is redirected, in CI, and under
132+
`npm exec` or `npx`.
131133

132134
## An update notice appears in output I am parsing
133135

packages/sim-cli/README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,32 @@ You can also run a command without installing the package globally:
2525
npx sim --help
2626
```
2727

28+
## Updates
29+
30+
The CLI checks for a newer stable release on eligible interactive invocations,
31+
at most once per day. It prints an optional update notice and continues your
32+
command. Updates install only when you run `sim update`.
33+
34+
Update immediately, including in CI or with automatic checks disabled:
35+
36+
```bash
37+
sim update
38+
```
39+
40+
The updater uses the package manager that installed the running copy and verifies
41+
its global installation before making changes. Supported managers are npm, pnpm,
42+
Bun, and Yarn Classic. Use `sim update --package-manager bun` if detection does
43+
not match a custom installation. Manual updates preserve staging and dev channels.
44+
Installation failures exit with an error; concurrent update attempts are refused.
45+
The updater resolves the channel through that package manager, refuses older
46+
releases, and installs the exact version it checked.
47+
48+
Set `SIM_NO_UPDATE_CHECK=1` to disable update notices. Project-local installs and
49+
temporary package-runner copies must be updated through their package manager.
50+
51+
Older releases without `sim update` need one upgrade using the package manager
52+
that installed them before this mechanism becomes available.
53+
2854
## Get started
2955

3056
Sign in to the default profile:
@@ -290,12 +316,12 @@ The main environment variables are:
290316
| `SIM_CONFIG_DIR` | Base directory for CLI config, credentials, and the update cache |
291317
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely |
292318
| `SIM_DEBUG` | Print request diagnostics to stderr |
293-
| `SIM_NO_UPDATE_CHECK` | Turn off the update notice |
319+
| `SIM_NO_UPDATE_CHECK` | Turn off update notices |
294320

295321
On eligible interactive invocations, `sim` uses a daily cache before asking
296-
`registry.npmjs.org` what is published under the `latest` tag and prints one
297-
line on stderr when a newer version exists. Prerelease installs are skipped
298-
entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
322+
`registry.npmjs.org` what is published under the `latest` tag and prints an
323+
optional notice on stderr when a newer version exists. Prerelease installs are
324+
skipped entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
299325
without a writable cache, each eligible invocation checks again. Concurrent
300326
invocations can also perform duplicate checks. The registry request has a
301327
one-second deadline; the short-lived request process is terminated on expiry.

packages/sim-cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "2.1.2",
44
"description": "Sim CLI - talk to the Sim API from your terminal",
55
"type": "module",
6+
"imports": {
7+
"#sim-cli/*": "./src/*.ts"
8+
},
69
"bin": {
710
"sim": "dist/index.js"
811
},
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const { installUpdate, announceUpdateIfAvailable } = vi.hoisted(() => ({
7+
installUpdate: vi.fn(),
8+
announceUpdateIfAvailable: vi.fn(),
9+
}))
10+
11+
vi.mock('#sim-cli/update/install', () => ({ installUpdate }))
12+
vi.mock('#sim-cli/update/check', () => ({ announceUpdateIfAvailable }))
13+
14+
import { buildProgram } from '#sim-cli/program'
15+
16+
beforeEach(() => {
17+
vi.clearAllMocks()
18+
installUpdate.mockResolvedValue(undefined)
19+
announceUpdateIfAvailable.mockResolvedValue(undefined)
20+
})
21+
22+
describe('update command wiring', () => {
23+
it('runs a manual update without the daily check or authentication', async () => {
24+
await buildProgram().parseAsync(['node', 'sim', 'update'])
25+
expect(announceUpdateIfAvailable).not.toHaveBeenCalled()
26+
expect(installUpdate).toHaveBeenCalledExactlyOnceWith({ packageManager: undefined })
27+
})
28+
29+
it('passes an explicit package manager to the updater', async () => {
30+
await buildProgram().parseAsync(['node', 'sim', 'update', '--package-manager', 'bun'])
31+
expect(installUpdate).toHaveBeenCalledExactlyOnceWith({ packageManager: 'bun' })
32+
})
33+
34+
it('checks for a notice and continues the requested action without installing', async () => {
35+
const program = buildProgram()
36+
const action = vi.fn()
37+
program.commands.find((command) => command.name() === 'whoami')!.action(action)
38+
await program.parseAsync(['node', 'sim', 'whoami'])
39+
expect(announceUpdateIfAvailable).toHaveBeenCalledOnce()
40+
expect(action).toHaveBeenCalledOnce()
41+
expect(installUpdate).not.toHaveBeenCalled()
42+
})
43+
44+
it('propagates an explicit update failure', async () => {
45+
installUpdate.mockRejectedValueOnce(new Error('installation failed'))
46+
const program = buildProgram()
47+
await expect(program.parseAsync(['node', 'sim', 'update'])).rejects.toThrow(
48+
'installation failed'
49+
)
50+
expect(announceUpdateIfAvailable).not.toHaveBeenCalled()
51+
})
52+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Command, Option } from 'commander'
2+
import { installUpdate, type PackageManager } from '#sim-cli/update/install'
3+
4+
export function updateCommand(): Command {
5+
return new Command('update')
6+
.description('Update this global CLI installation to the newest release on its channel')
7+
.addOption(
8+
new Option('--package-manager <manager>', 'Package manager that installed this copy').choices(
9+
['npm', 'pnpm', 'bun', 'yarn']
10+
)
11+
)
12+
.action(async (options: { packageManager?: PackageManager }) => {
13+
await installUpdate({ packageManager: options.packageManager })
14+
})
15+
}

packages/sim-cli/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import chalk from 'chalk'
44
import { dump } from 'js-yaml'
5+
import { CliUpdateError } from '#sim-cli/update/install'
56
import { ProfileConfigError } from './config/index'
67
import { clientFrom } from './context'
78
import {
@@ -23,7 +24,7 @@ async function main() {
2324
try {
2425
await program.parseAsync(process.argv)
2526
} catch (error) {
26-
if (error instanceof ProfileConfigError) {
27+
if (error instanceof ProfileConfigError || error instanceof CliUpdateError) {
2728
console.error(chalk.red(`Error: ${sanitize(error.message)}`))
2829
process.exit(1)
2930
}

0 commit comments

Comments
 (0)