Add update-available notifications across the CLI, web shell, and desktop app#1182
Conversation
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.
Deploying with
|
| 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 |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | a39e290 | Jun 28 2026, 09:46 PM |
Greptile SummaryThis 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
Confidence Score: 4/5Safe 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
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()
%%{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()
Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
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.
# Conflicts: # apps/local/src/serve.ts
Cloudflare previewTorn down — the PR is closed. |
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):
Cloudflare — links to the upgrade guide (redeploy the Worker):
npm-installed CLI / its local web UI — the copyable upgrade command:
Desktop app — a native action (electron-updater), applied on click:
The CLI prints the notice under its ready banner:
What changed
executor web --foregroundprints an "update available" line under its ready banner when a newer version is published./v1/app/npm/dist-tagsendpoint it fetches (no server handler existed) and the card itself in the multiplayer shell (it only lived in the local shell). OneSidebarUpdateCardin@executor-js/reactnow backs both.npm i -g executor@latestonly upgrades the npm-installed CLI. Each host's vite build injects aVITE_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 withEXECUTOR_DISABLE_UPDATE_CHECK.Reachability
The endpoint had to be reachable on each deployment, not just the dev server:
/v1/*to the backend in their vite dev middleware.serve.ts): already routes through the Effect router; the route wins over the SPA fallback./v1/*torun_worker_firstinwrangler.jsonc(Static Assets was answering with the SPAindex.html).Testing
update-check.test.tspins the resolution order and the semver verdict.local/update-notice.test.ts: the CLI banner notice, the web npm card, and the desktop-native card (injected bridge).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.selfhost|cloudflare/update-endpoint.test.ts: the endpoint answers JSON, not the SPA fallback, on each host.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, andtypecheckall pass.