Skip to content

Add update-available notifier for the standalone (curl-installed) Studio CLI#3899

Merged
bcotrim merged 12 commits into
trunkfrom
stu-1772-add-update-workflow-curl-installer
Jun 26, 2026
Merged

Add update-available notifier for the standalone (curl-installed) Studio CLI#3899
bcotrim merged 12 commits into
trunkfrom
stu-1772-add-update-workflow-curl-installer

Conversation

@bcotrim

@bcotrim bcotrim commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Related issues

  • Related to 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

  • Have you checked for TypeScript, React or other console errors? — typecheck clean for changed files, eslint clean, 18/18 tests pass.

@fredrikekelund fredrikekelund left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/cli/lib/update-notifier.ts Outdated
Comment on lines +57 to +60
export function isStandaloneInstall(): boolean {
const home = process.env.STUDIO_CLI_HOME || path.join( os.homedir(), '.studio' );
return isPathInside( process.execPath, home );
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bcotrim bcotrim Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)..?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Added the suggested variable

Comment thread apps/cli/lib/update-notifier.ts Outdated
Comment on lines 38 to 45
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not using readCliConfig() here?

Comment thread apps/cli/lib/update-notifier.ts Outdated
}

const prefix = alias ? `STUDIO_CLI_VERSION=${ alias } ` : '';
return `${ prefix }curl -fsSL https://wp.build/install.sh | bash`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/cli/lib/update-notifier.ts Outdated

if ( platform === 'win32' ) {
const prefix = alias ? `$env:STUDIO_CLI_VERSION='${ alias }'; ` : '';
return `${ prefix }irm https://wp.build/install.ps1 | iex`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it does, but let's check back after updating the URL

Comment thread apps/cli/lib/update-notifier.ts Outdated
export function formatUpdateBanner(
currentVersion: string,
latestVersion: string,
updateCommand: string = NPM_UPDATE_COMMAND

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

@bcotrim bcotrim marked this pull request as ready for review June 24, 2026 10:34
@bcotrim bcotrim requested a review from fredrikekelund June 24, 2026 10:34
@wpmobilebot

wpmobilebot commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing d178196 vs trunk

app-size

Metric trunk d178196 Diff Change
App Size (Mac) 1311.91 MB 1311.91 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk d178196 Diff Change
load 1040 ms 1052 ms +12 ms ⚪ 0.0%

site-startup

Metric trunk d178196 Diff Change
siteCreation 6511 ms 6477 ms 34 ms ⚪ 0.0%
siteStartup 6505 ms 6998 ms +493 ms 🔴 7.6%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@fredrikekelund fredrikekelund left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :shipit: 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.

Comment thread apps/cli/lib/update-notifier.ts Outdated
// `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`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, this should be https://wordpress.studio/install.sh. Would you rather do that in this PR or in a followup, @bcotrim?

Comment thread apps/cli/lib/update-notifier.ts Outdated

if ( platform === 'win32' ) {
const prefix = alias ? `$env:STUDIO_CLI_VERSION='${ alias }'; ` : '';
return `${ prefix }irm ${ STUDIO_API_BASE }/install.ps1 | iex`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here. This should be https://wordpress.studio/install.ps1 eventually

@bcotrim bcotrim merged commit 17451f7 into trunk Jun 26, 2026
11 checks passed
@bcotrim bcotrim deleted the stu-1772-add-update-workflow-curl-installer branch June 26, 2026 08:50
gcsecsey pushed a commit that referenced this pull request Jun 26, 2026
…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.
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.

3 participants