Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/docs/content/docs/cli/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ sim configure [options]

</CommandTable>

## Update this global CLI installation to the newest release on its channel

```bash
sim update [options]
```

**Options**

<CommandTable>

| Option | Required | Description |
| --- | --- | --- |
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |

</CommandTable>

## Ask Sim and print the reply

```bash
Expand Down
28 changes: 22 additions & 6 deletions apps/docs/content/docs/cli/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,27 @@ endpoint or stored login.
| `SIM_CREDENTIALS_FILE` | Relocate only the credentials file |
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
| `SIM_DEBUG` | Trace each request's method, URL, status and duration to stderr |
| `SIM_NO_UPDATE_CHECK` | Turn off update checks |
| `SIM_NO_UPDATE_CHECK` | Turn off update checks and notices |

## Update notices
## Updates

The CLI checks for a newer release at most once per day on eligible interactive
invocations. Notices go to stderr and show an upgrade command for the package
manager that installed Sim.
The CLI checks for a newer stable release at most once per day on eligible
interactive invocations. It prints an optional notice to stderr and continues
your command. Installation only happens when you run `sim update`.

Run `sim update` to update immediately, including in CI and when automatic checks
are disabled. It requires a global installation and verifies that the package
manager targets the running copy before installing. Supported managers are npm,
pnpm, Bun, and Yarn Classic. For custom installations, select the manager with
`sim update --package-manager bun`. Project-local and temporary package-runner
copies must be updated through their package manager.

Manual updates preserve the stable, staging, or dev release channel. Installation
failures stop with an error; concurrent update attempts are refused. Installer
output goes to stderr and does not mix with JSON output on stdout.
The updater resolves the channel through the selected package manager before
installing. Older registry or mirror releases are refused; a newer release is
installed by its exact version so a moving tag cannot change the target.

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

Node's `fetch` uses `HTTP(S)_PROXY` when opted in with `NODE_USE_ENV_PROXY=1`
(Node 22.21+ or 24.0+) or `--use-env-proxy` (Node 22.21+ or 24.5+).
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/content/docs/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ or local configuration.

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

The CLI shows optional update notices on eligible interactive invocations. Run
`sim update` when you want to install the update. See
[Updates](/cli/configuration#updates) for installation requirements and how to
disable notices.

To run it without installing, use `npx sim <command>`.

Using Sim as a library instead? See the [TypeScript](/api-reference/typescript)
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ sim configure [options]

</CommandTable>

## sim update

Update this global CLI installation to the newest release on its channel

```bash
sim update [options]
```

**Options**

<CommandTable>

| Option | Required | Description |
| --- | --- | --- |
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |

</CommandTable>

## sim chat

Ask Sim and print the reply
Expand Down
10 changes: 6 additions & 4 deletions apps/docs/content/docs/cli/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ The docs track the current release, so a command that exists here and not in
sim --version
```

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

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

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

## An update notice appears in output I am parsing

Expand Down
34 changes: 30 additions & 4 deletions packages/sim-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ You can also run a command without installing the package globally:
npx sim --help
```

## Updates

The CLI checks for a newer stable release on eligible interactive invocations,
at most once per day. It prints an optional update notice and continues your
command. Updates install only when you run `sim update`.

Update immediately, including in CI or with automatic checks disabled:

```bash
sim update
```

The updater uses the package manager that installed the running copy and verifies
its global installation before making changes. Supported managers are npm, pnpm,
Bun, and Yarn Classic. Use `sim update --package-manager bun` if detection does
not match a custom installation. Manual updates preserve staging and dev channels.
Installation failures exit with an error; concurrent update attempts are refused.
The updater resolves the channel through that package manager, refuses older
releases, and installs the exact version it checked.

Set `SIM_NO_UPDATE_CHECK=1` to disable update notices. Project-local installs and
temporary package-runner copies must be updated through their package manager.

Older releases without `sim update` need one upgrade using the package manager
that installed them before this mechanism becomes available.

## Get started

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

On eligible interactive invocations, `sim` uses a daily cache before asking
`registry.npmjs.org` what is published under the `latest` tag and prints one
line on stderr when a newer version exists. Prerelease installs are skipped
entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
`registry.npmjs.org` what is published under the `latest` tag and prints an
optional notice on stderr when a newer version exists. Prerelease installs are
skipped entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
without a writable cache, each eligible invocation checks again. Concurrent
invocations can also perform duplicate checks. The registry request has a
one-second deadline; the short-lived request process is terminated on expiry.
Expand Down
3 changes: 3 additions & 0 deletions packages/sim-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "2.1.2",
"description": "Sim CLI - talk to the Sim API from your terminal",
"type": "module",
"imports": {
"#sim-cli/*": "./src/*.ts"
},
"bin": {
"sim": "dist/index.js"
},
Expand Down
52 changes: 52 additions & 0 deletions packages/sim-cli/src/commands/update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { installUpdate, announceUpdateIfAvailable } = vi.hoisted(() => ({
installUpdate: vi.fn(),
announceUpdateIfAvailable: vi.fn(),
}))

vi.mock('#sim-cli/update/install', () => ({ installUpdate }))
vi.mock('#sim-cli/update/check', () => ({ announceUpdateIfAvailable }))

import { buildProgram } from '#sim-cli/program'

beforeEach(() => {
vi.clearAllMocks()
installUpdate.mockResolvedValue(undefined)
announceUpdateIfAvailable.mockResolvedValue(undefined)
})

describe('update command wiring', () => {
it('runs a manual update without the daily check or authentication', async () => {
await buildProgram().parseAsync(['node', 'sim', 'update'])
expect(announceUpdateIfAvailable).not.toHaveBeenCalled()
expect(installUpdate).toHaveBeenCalledExactlyOnceWith({ packageManager: undefined })
})

it('passes an explicit package manager to the updater', async () => {
await buildProgram().parseAsync(['node', 'sim', 'update', '--package-manager', 'bun'])
expect(installUpdate).toHaveBeenCalledExactlyOnceWith({ packageManager: 'bun' })
})

it('checks for a notice and continues the requested action without installing', async () => {
const program = buildProgram()
const action = vi.fn()
program.commands.find((command) => command.name() === 'whoami')!.action(action)
await program.parseAsync(['node', 'sim', 'whoami'])
expect(announceUpdateIfAvailable).toHaveBeenCalledOnce()
expect(action).toHaveBeenCalledOnce()
expect(installUpdate).not.toHaveBeenCalled()
})

it('propagates an explicit update failure', async () => {
installUpdate.mockRejectedValueOnce(new Error('installation failed'))
const program = buildProgram()
await expect(program.parseAsync(['node', 'sim', 'update'])).rejects.toThrow(
'installation failed'
)
expect(announceUpdateIfAvailable).not.toHaveBeenCalled()
})
})
15 changes: 15 additions & 0 deletions packages/sim-cli/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command, Option } from 'commander'
import { installUpdate, type PackageManager } from '#sim-cli/update/install'

export function updateCommand(): Command {
return new Command('update')
.description('Update this global CLI installation to the newest release on its channel')
.addOption(
new Option('--package-manager <manager>', 'Package manager that installed this copy').choices(
['npm', 'pnpm', 'bun', 'yarn']
)
)
.action(async (options: { packageManager?: PackageManager }) => {
await installUpdate({ packageManager: options.packageManager })
})
}
3 changes: 2 additions & 1 deletion packages/sim-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import chalk from 'chalk'
import { dump } from 'js-yaml'
import { CliUpdateError } from '#sim-cli/update/install'
import { ProfileConfigError } from './config/index'
import { clientFrom } from './context'
import {
Expand All @@ -23,7 +24,7 @@ async function main() {
try {
await program.parseAsync(process.argv)
} catch (error) {
if (error instanceof ProfileConfigError) {
if (error instanceof ProfileConfigError || error instanceof CliUpdateError) {
console.error(chalk.red(`Error: ${sanitize(error.message)}`))
process.exit(1)
}
Expand Down
8 changes: 7 additions & 1 deletion packages/sim-cli/src/program.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command, Option } from 'commander'
import { updateCommand } from '#sim-cli/commands/update'
import { loginCommand, logoutCommand, profilesCommand, whoamiCommand } from './commands/auth'
import { configureCommand } from './commands/configure'
import { attachCredentialCommands } from './commands/credentials'
Expand Down Expand Up @@ -142,6 +143,8 @@ export function buildProgram(options: { version?: boolean } = {}): Command {
program.addCommand(whoamiCommand())
program.addCommand(profilesCommand())
program.addCommand(configureCommand())
const update = updateCommand()
program.addCommand(update)

for (const command of buildGeneratedCommands()) {
program.addCommand(command)
Expand All @@ -153,7 +156,10 @@ export function buildProgram(options: { version?: boolean } = {}): Command {

program.addHelpText('after', HELP_EPILOGUE)

program.hook('preAction', () => announceUpdateIfAvailable())
program.hook('preAction', async (_program, command) => {
if (command === update) return
await announceUpdateIfAvailable()
})

refuseHelpAfterUnknownCommand(program)
assertNoReservedProgramFlags(program)
Expand Down
4 changes: 1 addition & 3 deletions packages/sim-cli/src/update/check.process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,7 @@ it('preserves a mirror path, query, and reduced request headers', async () => {

expect(result).toMatchObject({ code: 0, signal: null, stderr: '' })
const output = JSON.parse(result.stdout) as CheckOutput
expect(output.notices).toEqual([
'Update available: sim 2.1.2 → 2.1.5. Run: npm install -g sim@latest\n',
])
expect(output.notices).toEqual(['Update available: sim 2.1.2 → 2.1.5. Run: sim update\n'])
expect(requestPath).toBe('/api/npm/repo/-/package/sim/dist-tags?token=abc')
expect(requestHeaders).toMatchObject({
accept: 'application/json',
Expand Down
4 changes: 1 addition & 3 deletions packages/sim-cli/src/update/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ afterEach(() => {
describe('announcing a newer release', () => {
it('names both versions and the command that closes the gap', async () => {
await run()
expect(notices.join('')).toBe(
'Update available: sim 2.1.2 → 2.1.5. Run: npm install -g sim@latest\n'
)
expect(notices.join('')).toBe('Update available: sim 2.1.2 → 2.1.5. Run: sim update\n')
})

it('asks the registry for the dist-tags and nothing else', async () => {
Expand Down
8 changes: 3 additions & 5 deletions packages/sim-cli/src/update/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* in 2.1.5 — sees a help listing without it and concludes the CLI cannot do it.
* The version is the only thing that can tell them otherwise.
*
* Everything here fails silently. A courtesy notice that breaks a command, or
* that writes anything to stdout, is worse than no notice at all.
* Registry and cache failures suppress the courtesy notice. Installation only
* happens when the user explicitly runs `sim update`.
*/

import { spawn } from 'node:child_process'
Expand Down Expand Up @@ -468,8 +468,6 @@ export async function announceUpdateIfAvailable(options: UpdateCheckOptions = {}
if (!isNewerVersion(available, current)) return

const write = options.write ?? ((message: string) => void process.stderr.write(message))
write(
`Update available: sim ${currentVersion} → ${latest}. Run: ${upgradeCommand(modulePath, env)}\n`
)
write(`Update available: sim ${currentVersion} → ${latest}. Run: sim update\n`)
} catch {}
}
Loading
Loading