Add update-available notifier for the standalone (curl-installed) Studio CLI#3899
Conversation
fredrikekelund
left a comment
There was a problem hiding this comment.
I was originally envisioning an auto-updater for the standalone CLI. The difference between this packaging approach and npm is that with npm, we have explicitly handed over packaging responsibility to a package manager. In this case, we own packaging and there's no good reason to avoid auto-updating other than the effort it takes to implement.
Still, I think it's an OK tradeoff that we ship this implementation to begin with, but only if we also track bump stats for the number of users that run the standalone CLI. This can help us determine if we should revisit this in the future and implement an auto-updater for the standalone CLI.
| export function isStandaloneInstall(): boolean { | ||
| const home = process.env.STUDIO_CLI_HOME || path.join( os.homedir(), '.studio' ); | ||
| return isPathInside( process.execPath, home ); | ||
| } |
There was a problem hiding this comment.
Did you consider adding a vite.config.standalone.ts file for the CLI that builds the CLI with a global variable, like __IS_PACKAGED_FOR_STANDALONE__?
Plenty of Vite configs, yes, but the explicitness of that approach is quite appealing, IMO.
There was a problem hiding this comment.
I did look at this. The catch is create-standalone-bundle.ts reuses the same apps/cli/dist/cli the desktop app ships, so a __IS_PACKAGED_FOR_STANDALONE__ at build time would also be true for the embedded CLI unless we add a second, standalone-only build (separate dist + divergent bits). Runtime path detection avoids that and keeps app + standalone identical, which is why I went that way.
There was a problem hiding this comment.
The catch is create-standalone-bundle.ts reuses the same apps/cli/dist/cli the desktop app ships
Does it really? I thought the cli:bundle script triggered a second npm run cli:package run (which in turn runs both install:bundle and build:prod for the CLI)..?
There was a problem hiding this comment.
You are right. Added the suggested variable
| try { | ||
| const content = fs.readFileSync( getCliConfigPath(), 'utf8' ); | ||
| const data = JSON.parse( content ); | ||
| return updateCheckSchema.parse( data?.updateCheck ); | ||
| return updateCheckSchema.parse( data?.[ field ] ); | ||
| } catch { | ||
| // File doesn't exist, field missing, or invalid | ||
| } | ||
| return null; |
There was a problem hiding this comment.
Why are we not using readCliConfig() here?
| } | ||
|
|
||
| const prefix = alias ? `STUDIO_CLI_VERSION=${ alias } ` : ''; | ||
| return `${ prefix }curl -fsSL https://wp.build/install.sh | bash`; |
There was a problem hiding this comment.
| return `${ prefix }curl -fsSL https://wp.build/install.sh | bash`; | |
| return `${ prefix }curl -fsSL https://wordpress.studio/install.sh | bash`; |
Just relaying our discussion elsewhere. We're probably looking at this URL for the install command
There was a problem hiding this comment.
For now I am using the public-api endpoint, to confirm the working behavior.
I propose updating the URL in a follow-up, once we have it set up
|
|
||
| if ( platform === 'win32' ) { | ||
| const prefix = alias ? `$env:STUDIO_CLI_VERSION='${ alias }'; ` : ''; | ||
| return `${ prefix }irm https://wp.build/install.ps1 | iex`; |
There was a problem hiding this comment.
Just noting that we need to double-check if irm follows redirects. If not, then I believe curl is available by default on Windows installations these days.
There was a problem hiding this comment.
I think it does, but let's check back after updating the URL
| export function formatUpdateBanner( | ||
| currentVersion: string, | ||
| latestVersion: string, | ||
| updateCommand: string = NPM_UPDATE_COMMAND |
There was a problem hiding this comment.
| updateCommand: string = NPM_UPDATE_COMMAND | |
| updateCommand: string |
Let's not default here. If the function has two modes, then the caller should be explicit about which to use
…mand, readCliConfig
📊 Performance Test ResultsComparing d178196 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
fredrikekelund
left a comment
There was a problem hiding this comment.
LGTM
Looks solid! Left a small comment about the URL we print when there's an update, but we could also handle that in a followup. Up to you.
| // `VAR=x` scopes to curl only, so the piped bash runs install.sh without it and | ||
| // falls back to `latest`. `curl … | VAR=x bash` is what actually pins the channel. | ||
| const prefix = alias ? `STUDIO_CLI_VERSION=${ alias } ` : ''; | ||
| return `curl -fsSL ${ STUDIO_API_BASE }/install.sh | ${ prefix }bash`; |
There was a problem hiding this comment.
Eventually, this should be https://wordpress.studio/install.sh. Would you rather do that in this PR or in a followup, @bcotrim?
|
|
||
| if ( platform === 'win32' ) { | ||
| const prefix = alias ? `$env:STUDIO_CLI_VERSION='${ alias }'; ` : ''; | ||
| return `${ prefix }irm ${ STUDIO_API_BASE }/install.ps1 | iex`; |
There was a problem hiding this comment.
Same thing here. This should be https://wordpress.studio/install.ps1 eventually
…com:Automattic/studio into stu-1772-add-update-workflow-curl-installer
…dio CLI (#3899) ## Related issues - Related to [STU-1772](https://linear.app/a8c/issue/STU-1772) - Requires the wpcom `studio-app/updates` endpoint extension (224187-ghe-Automattic/wpcom) — **merged and live in production**. The curl installers (`install.sh` / `install.ps1`) are now served from public-api too, so the banner points at stable `public-api.wordpress.com` URLs rather than the `wp.build` short link. ## How AI was used in this PR Claude Code researched the Apps CDN / update-endpoint internals, designed the approach (reuse the desktop app's `studio-app/updates` endpoint rather than querying the internal CDN post type), implemented the client + tests, and validated the full request/response contract live (a wpcom sandbox during development, then production). It also caught and fixed a shell bug found during live end-to-end testing: the channel-pinned install command set `STUDIO_CLI_VERSION=` on the `curl` side of the pipe, so the piped `bash` ran the installer without it and 404'd — the env var now sits on the `bash` side. I reviewed all the code myself. ## Proposed Changes - npm-installed CLIs already show an "update available" banner; standalone (curl-installed) CLIs got nothing. This gives curl installs the same nudge: when a newer version exists on the user's channel, the CLI prints a banner pointing them to re-run the installer. - It's channel-aware — production, beta, and nightly users are each checked against their own channel — by reusing the desktop app's existing update endpoint, so the server does the channel logic and version comparison. No coupling to internal CDN data models. The suggested update command is channel-pinned (`STUDIO_CLI_VERSION=nightly|beta`) so users stay on their channel. - It's quiet and non-intrusive: cached 24h, fast 3s non-blocking timeout, fails safe (no banner when offline or the endpoint is unavailable), and suppressed in IPC mode, with `--json`, and for the desktop-embedded CLI. ## Testing Instructions **Automated:** `npm test -- apps/cli/lib/tests/update-notifier.test.ts` (18 tests: banner variants, channel-pinned install commands, install-kind detection, 200/204/error handling, suppression matrix). **End-to-end** — build a hardcoded dev bundle, see the banner, and update from it. Published nightlies don't include this notifier yet, so build a local bundle to exercise it. Run from the repo root: 1. Stamp the CLI to an old dev version so the endpoint reports an update — set `"version": "1.12.0-dev1"` in `apps/cli/package.json`. 2. Build the standalone bundle for your platform/arch: `npm run cli:bundle -- darwin arm64`. (`cli:package` prunes `apps/cli/node_modules`; run `npm install` afterward to restore it.) 3. Install it into `~/.studio` from the local bundle (this repoints `studio` → `~/.studio/bin/studio` and replaces any existing standalone install): ``` curl -fsSL https://public-api.wordpress.com/wpcom/v2/studio-app/install.sh | STUDIO_CLI_URL="$(pwd)/standalone-bundles" bash ``` 4. Run any command — `studio --version`. Expect an **update banner**: `Update available: 1.12.0-dev1 → <latest nightly>`, with `Run curl -fsSL https://public-api.wordpress.com/wpcom/v2/studio-app/install.sh | STUDIO_CLI_VERSION=nightly bash to update`. 5. Run that exact command to update → re-run `studio --version`: it's now the latest nightly and the banner is gone (you're current). 6. Negative checks: with `--json`, or as the desktop-embedded CLI → no banner. Cleanup: revert `apps/cli/package.json`, `npm install`, `rm -rf standalone-bundles`, and (to restore your desktop app's `studio`) `ln -sf "/Applications/Studio.app/Contents/Resources/bin/studio-cli.sh" ~/.local/bin/studio`. ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors? — typecheck clean for changed files, eslint clean, 18/18 tests pass.
Related issues
studio-app/updatesendpoint extension (224187-ghe-Automattic/wpcom) — merged and live in production. The curl installers (install.sh/install.ps1) are now served from public-api too, so the banner points at stablepublic-api.wordpress.comURLs rather than thewp.buildshort link.How AI was used in this PR
Claude Code researched the Apps CDN / update-endpoint internals, designed the approach (reuse the desktop app's
studio-app/updatesendpoint rather than querying the internal CDN post type), implemented the client + tests, and validated the full request/response contract live (a wpcom sandbox during development, then production). It also caught and fixed a shell bug found during live end-to-end testing: the channel-pinned install command setSTUDIO_CLI_VERSION=on thecurlside of the pipe, so the pipedbashran the installer without it and 404'd — the env var now sits on thebashside. I reviewed all the code myself.Proposed Changes
STUDIO_CLI_VERSION=nightly|beta) so users stay on their channel.--json, and for the desktop-embedded CLI.Testing Instructions
Automated:
npm test -- apps/cli/lib/tests/update-notifier.test.ts(18 tests: banner variants, channel-pinned install commands, install-kind detection, 200/204/error handling, suppression matrix).End-to-end — build a hardcoded dev bundle, see the banner, and update from it. Published nightlies don't include this notifier yet, so build a local bundle to exercise it. Run from the repo root:
"version": "1.12.0-dev1"inapps/cli/package.json.npm run cli:bundle -- darwin arm64. (cli:packageprunesapps/cli/node_modules; runnpm installafterward to restore it.)~/.studiofrom the local bundle (this repointsstudio→~/.studio/bin/studioand replaces any existing standalone install):studio --version. Expect an update banner:Update available: 1.12.0-dev1 → <latest nightly>, withRun curl -fsSL https://public-api.wordpress.com/wpcom/v2/studio-app/install.sh | STUDIO_CLI_VERSION=nightly bash to update.studio --version: it's now the latest nightly and the banner is gone (you're current).--json, or as the desktop-embedded CLI → no banner.Cleanup: revert
apps/cli/package.json,npm install,rm -rf standalone-bundles, and (to restore your desktop app'sstudio)ln -sf "/Applications/Studio.app/Contents/Resources/bin/studio-cli.sh" ~/.local/bin/studio.Pre-merge Checklist