Skip to content

Add update-available notifications across the CLI, web shell, and desktop app#1182

Merged
RhysSullivan merged 6 commits into
mainfrom
feat/cli-update-notice
Jun 28, 2026
Merged

Add update-available notifications across the CLI, web shell, and desktop app#1182
RhysSullivan merged 6 commits into
mainfrom
feat/cli-update-notice

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Tells users when a newer Executor is published, with the right upgrade action for how they deployed.

Screenshots

The card's action depends on the deployment, because the way you upgrade does:

Self-host (Docker / bare) — links to the upgrade guide (image pull / rebuild, not npm):

docker update card

Cloudflare — links to the upgrade guide (redeploy the Worker):

cloudflare update card

npm-installed CLI / its local web UI — the copyable upgrade command:

web update card

Desktop app — a native action (electron-updater), applied on click:

desktop app update card
desktop app restarting

The CLI prints the notice under its ready banner:

Executor is ready.
Open:    http://127.0.0.1:4790/?_token=...

Update available: 1.5.22 -> 99.0.0
Run npm i -g executor@latest to update.

Press Ctrl+C to stop.

What changed

  • CLI: executor web --foreground prints an "update available" line under its ready banner when a newer version is published.
  • Web shell: the sidebar update card now works and is shared by both shells (the local/desktop single-user shell and the multiplayer shell used by self-host, Cloudflare, and cloud). It needed the /v1/app/npm/dist-tags endpoint it fetches (no server handler existed) and the card itself in the multiplayer shell (it only lived in the local shell). One SidebarUpdateCard in @executor-js/react now backs both.
  • Upgrade action is per-deployment: npm i -g executor@latest only upgrades the npm-installed CLI. Each host's vite build injects a VITE_UPGRADE_HINT, and the card picks the right action: the copyable npm command for the CLI, a link to the host's upgrade guide for self-host/Cloudflare (the steps vary too much for one correct command), the native restart for desktop, and nothing for managed cloud.

The "is a newer version published?" verdict comes from one resolver in @executor-js/api (update-check.ts), shared with the CLI notice. The check is best-effort and offline-safe, and can be turned off with EXECUTOR_DISABLE_UPDATE_CHECK.

Reachability

The endpoint had to be reachable on each deployment, not just the dev server:

  • local and self-host dev servers: forwarded /v1/* to the backend in their vite dev middleware.
  • self-host Docker (production serve.ts): already routes through the Effect router; the route wins over the SPA fallback.
  • Cloudflare: added /v1/* to run_worker_first in wrangler.jsonc (Static Assets was answering with the SPA index.html).

Testing

  • Unit: update-check.test.ts pins the resolution order and the semver verdict.
  • e2e local/update-notice.test.ts: the CLI banner notice, the web npm card, and the desktop-native card (injected bridge).
  • e2e desktop/update-card.test.ts: the real Electron app shows the native "Restart to update" card and no npm command; clicking it drives the install action.
  • e2e selfhost|cloudflare/update-endpoint.test.ts: the endpoint answers JSON, not the SPA fallback, on each host.
  • e2e selfhost|cloudflare/update-card-render.test.ts: the card paints on the self-host / Docker UI and the real Cloudflare worker, linking to the upgrade guide with no npm command.
  • format, lint, and typecheck all pass.

The CLI prints an "update available" line under its ready banner, and the
web shell's sidebar update card now works via a new /v1/app/npm/dist-tags
endpoint that serves the published npm dist-tags. The desktop app shows a
native "Restart to update" action wired to electron-updater instead of the
npm command. One shared, offline-safe resolver in @executor-js/api backs all
surfaces; disable with EXECUTOR_DISABLE_UPDATE_CHECK.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing a39e290 Commit Preview URL

Branch Preview URL
Jun 28 2026, 09:45 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud a39e290 Jun 28 2026, 09:46 PM

@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires up update-available notifications end-to-end: the CLI prints a nudge under its ready banner, and the web sidebar gains a shared SidebarUpdateCard that picks the right upgrade action per deployment (native restart for desktop, copyable npm command for local, upgrade-guide link for selfhost/Cloudflare, nothing for managed cloud). A new /v1/app/npm/dist-tags endpoint backed by a shared resolveDistTags resolver with in-process caching and a 1.5 s timeout feeds both surfaces, with routing changes for every host to ensure the endpoint is reachable in production.

  • Shared resolver (packages/core/api/src/update-check.ts): semver comparison, 10-min success TTL, 60-s negative TTL for offline/error cases, env-override seams for tests — used by both the CLI banner and the web card so the two surfaces can never disagree.
  • Desktop IPC wiring: autoUpdater events are mapped to a DesktopUpdateStatus union pushed to the renderer via a broadcast channel; the preload bridge exposes getUpdateStatus, onUpdateStatus, and installUpdate; a receivedPush flag prevents the async initial snapshot from overwriting a push that already arrived.
  • Per-host routing: /v1/* added to run_worker_first in wrangler.jsonc, the dev-middleware proxy in local/selfhost vite configs, and the production Bun server — all ensuring the endpoint wins over the SPA fallback on every deployment.

Confidence Score: 4/5

Safe to merge for desktop and local/npm users; the selfhost and Cloudflare update card needs attention before those deployments go out.

The hardcoded VITE_APP_VERSION placeholders for selfhost and Cloudflare cause the update card to fire unconditionally — any real npm version compares as newer than 0.0.0-selfhost or 0.0.0-cloudflare, so updateAvailable is always true. Everything else — the shared resolver, desktop IPC, routing changes, and tests — is well-structured and correct.

apps/host-selfhost/vite.config.ts and apps/host-cloudflare/vite.config.ts — both need a real build-time version rather than the placeholder strings.

Important Files Changed

Filename Overview
packages/core/api/src/update-check.ts New shared resolver: semver comparison, dist-tags fetch with 1.5 s timeout, 10 min success TTL, 60 s negative TTL, env-override test seam. Logic and negative-caching are correct.
packages/core/api/src/server/npm-dist-tags.ts New GET /v1/app/npm/dist-tags route; proxies resolveDistTags with a 5-minute public cache header. Clean and intentionally auth-free.
packages/react/src/components/update-card.tsx Shared SidebarUpdateCard for all deployments. Correctly branches on desktop bridge vs VITE_UPGRADE_HINT. Minor: useLatestVersion fetch fires unnecessarily in the desktop path.
packages/react/src/hooks/desktop-update.ts IPC bridge hook with receivedPush guard to prevent initial-snapshot regression. install() silently discards Promise errors from the IPC call.
apps/host-selfhost/vite.config.ts VITE_APP_VERSION hardcoded to 0.0.0-selfhost causes the update card to permanently show for all selfhost deployments because it always compares as older than any real npm version.
apps/host-cloudflare/vite.config.ts VITE_APP_VERSION hardcoded to 0.0.0-cloudflare; same permanent-card issue as the selfhost config. Also adds /v1/* to run_worker_first correctly.
apps/desktop/src/main/index.ts Adds IPC handlers for update status get/push/install; applyFakeUpdateFromEnv guarded by !app.isPackaged; autoUpdater events feed setUpdateStatus correctly.
apps/cli/src/main.ts Best-effort upgrade nudge printed under the ready banner; Effect.promise wraps the async checkForUpdate; correct null-guards before printing.
packages/core/api/src/update-check.test.ts Unit tests covering disable, JSON override, single-value override, registry fallback, failure swallow, negative-cache behaviour, and checkForUpdate verdicts. Good coverage.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as CLI (main.ts)
    participant Web as Web Shell (SidebarUpdateCard)
    participant Server as Effect Router
    participant NPM as npm Registry

    CLI->>Server: startup: checkForUpdate(CLI_VERSION)
    Server->>NPM: GET dist-tags (1.5 s timeout)
    NPM-->>Server: "{ latest: 1.6.0 }"
    Server-->>CLI: "{ updateAvailable: true, command: npm i -g executor@latest }"
    CLI->>CLI: console.log update notice

    Web->>Server: fetch /v1/app/npm/dist-tags
    Server->>NPM: GET dist-tags (cached or fresh)
    NPM-->>Server: "{ latest: 1.6.0 }"
    Server-->>Web: "{ latest: 1.6.0 } Cache-Control max-age=300"
    Web->>Web: compareVersions(APP_VERSION, 1.6.0)
    alt Desktop bridge present
        Web->>Web: show DesktopUpdateCard from IPC
    else VITE_UPGRADE_HINT is npm
        Web->>Web: show NpmUpdateCard copyable command
    else selfhost or cloudflare
        Web->>Web: show LinkUpdateCard upgrade guide
    end

    participant Main as Electron Main
    participant Renderer as Renderer useDesktopUpdate
    Main->>Renderer: push UPDATE_STATUS_CHANNEL
    Renderer->>Main: invoke UPDATE_STATUS_GET_CHANNEL
    Renderer->>Main: invoke UPDATE_INSTALL_CHANNEL
    Main->>Main: autoUpdater.quitAndInstall()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as CLI (main.ts)
    participant Web as Web Shell (SidebarUpdateCard)
    participant Server as Effect Router
    participant NPM as npm Registry

    CLI->>Server: startup: checkForUpdate(CLI_VERSION)
    Server->>NPM: GET dist-tags (1.5 s timeout)
    NPM-->>Server: "{ latest: 1.6.0 }"
    Server-->>CLI: "{ updateAvailable: true, command: npm i -g executor@latest }"
    CLI->>CLI: console.log update notice

    Web->>Server: fetch /v1/app/npm/dist-tags
    Server->>NPM: GET dist-tags (cached or fresh)
    NPM-->>Server: "{ latest: 1.6.0 }"
    Server-->>Web: "{ latest: 1.6.0 } Cache-Control max-age=300"
    Web->>Web: compareVersions(APP_VERSION, 1.6.0)
    alt Desktop bridge present
        Web->>Web: show DesktopUpdateCard from IPC
    else VITE_UPGRADE_HINT is npm
        Web->>Web: show NpmUpdateCard copyable command
    else selfhost or cloudflare
        Web->>Web: show LinkUpdateCard upgrade guide
    end

    participant Main as Electron Main
    participant Renderer as Renderer useDesktopUpdate
    Main->>Renderer: push UPDATE_STATUS_CHANNEL
    Renderer->>Main: invoke UPDATE_STATUS_GET_CHANNEL
    Renderer->>Main: invoke UPDATE_INSTALL_CHANNEL
    Main->>Main: autoUpdater.quitAndInstall()
Loading

Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread packages/react/src/hooks/desktop-update.ts
Comment thread apps/desktop/src/main/index.ts
Comment thread packages/core/api/src/update-check.ts Outdated
Cloudflare's Workers Static Assets answered /v1/app/npm/dist-tags with the SPA
index.html (it was not in run_worker_first), so the update card stayed dark
there. Forward /v1/* to the Worker. The production Docker image (serve.ts)
already routes it through the Effect router. Add reachability scenarios for both
hosts asserting the path answers JSON, not the SPA fallback.
… cloud)

The card lived only in the local/desktop shell, so self-host and Cloudflare
served the dist-tags endpoint but never rendered a card. Extract the card (npm
and desktop-native variants) into one shared SidebarUpdateCard in
@executor-js/react and drop it into both shells, so every web deployment shows
it. Add per-host render scenarios proving the card paints on the self-host /
Docker UI and on the real Cloudflare worker.
`npm i -g executor@latest` only upgrades the npm-installed CLI. Self-host and
Cloudflare deploy by pulling/rebuilding the image or redeploying the Worker, so
the npm command was misleading there (the same trap the desktop app avoids).
Each host's vite build now injects a VITE_UPGRADE_HINT, and the sidebar card
picks the right affordance: the copyable npm command for the CLI, a link to the
host's upgrade guide for self-host/Cloudflare, the native restart for desktop,
and nothing for managed cloud.
…he failures

- desktop-update hook: an initial getUpdateStatus() snapshot could overwrite a
  newer push that arrived first; ignore the snapshot once a push has landed.
- applyFakeUpdateFromEnv: bail when app.isPackaged so a stray env var can never
  seed a phantom update in a real build.
- resolveDistTags: negative-cache empty/failed registry lookups for 60s so an
  offline server pays the fetch timeout once per window, not per request.
@RhysSullivan RhysSullivan changed the title Add update-available notifications to the CLI and desktop app Add update-available notifications across the CLI, web shell, and desktop app Jun 28, 2026
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@RhysSullivan RhysSullivan merged commit b6c8968 into main Jun 28, 2026
15 checks passed
@RhysSullivan RhysSullivan deleted the feat/cli-update-notice branch June 28, 2026 21:48
@RhysSullivan RhysSullivan mentioned this pull request Jun 29, 2026
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.

1 participant