ReviewOS

stacks/uplink

feat(app): a downloaded Uplink never learns that a newer version exists

#36
Open glennmichael123 opened this 8 days ago ·
8 days ago

Part of #6.

Every fix that ships after someone downloads the DMG reaches only the people who happen to revisit the site and download again. That includes a fix to app/Uplink/messages-db.ts#L16, the component that opens ~/Library/Messages/chat.db with Full Disk Access and reads the whole history. The data needed to notice a new release already gets written on every release, so what is missing is a comparison, somewhere to fetch the manifest from, and a line in the popover.

What is there today

  • pkg.version is display only. It is passed into the agent at app/Desktop/launcher.ts#L46, returned in the status payload at app/Uplink/desktop-agent.ts#L265, and rendered as the idle subline at app/Uplink/menubar.html#L175. Nothing compares it to anything.
  • The app makes no outbound request of its own. The only fetch in the agent is Bun.serve's inbound handler at app/Uplink/desktop-agent.ts#L280, and the only fetch in the popover is the loopback helper at app/Uplink/menubar.html#L139, called by the status poll at app/Uplink/menubar.html#L265 on the two second interval at app/Uplink/menubar.html#L333.
  • resources/data/release.json is already a machine readable manifest: version at resources/data/release.json#L2, size at #L3, minimum macOS at #L4, and the DMG url at #L5. uplink:release rewrites it on every release at app/Commands/Uplink.ts#L223.
  • It is not reachable over HTTP. The homepage imports it at build time at resources/views/index.stx#L47 and renders it as the download subline at resources/views/index.stx#L50. There is no copy under public/, so a check has to fetch either the GitHub latest release JSON or a published copy of this file.
  • The popover has no links in it. Opening a URL is something the agent does, the way /api/open/full-disk-access shells out to open at app/Uplink/desktop-agent.ts#L300.
  • The agent already owns repeating timers and tears them down: app/Uplink/desktop-agent.ts#L214 and #L219, cleared in stop at app/Uplink/desktop-agent.ts#L353.
  • The homepage footer at resources/views/index.stx#L374 links to Stacks and to GitHub and says nothing about what the app sends anywhere.

Proposal

  1. Add app/Uplink/update.ts with one function that fetches a release manifest and returns { version, url } | null when the published version is newer than the running one. Source is the GitHub latest release JSON for stacksjs/uplink, or a published copy of resources/data/release.json.
  2. Call it from startDesktopAgent in app/Uplink/desktop-agent.ts once at launch and on a 24 hour setInterval alongside the two at #L214 and #L219, cache the result in a local, and clear the timer in stop at #L353.
  3. Carry update: { version, url } | null in the status object next to version at app/Uplink/desktop-agent.ts#L265.
  4. Render it in app/Uplink/menubar.html: when status.update is set, show "0.1.2 available" in the footer next to the existing Pause and Quit buttons, as a button rather than a link.
  5. Add /api/open/download next to the /api/open/* cases at app/Uplink/desktop-agent.ts#L300, which spawns open on the url held by the agent from the last successful fetch.
  6. If the site is to be the manifest source, copy resources/data/release.json into public/ so it is served, since uplink:release already writes it at app/Commands/Uplink.ts#L223.

Requirements

  • This is the app's first outbound request, so the privacy copy has to name it. Until now the only thing that leaves the Mac is what the Claude CLI sends. State the endpoint, that it is a GET carrying no identifier and no version, and that it can be ignored, wherever the "what leaves your Mac" copy lands near resources/views/index.stx#L374.
  • Fail silently when offline. A failed or slow fetch keeps the last known result, writes at most one log line, and never becomes a check or an error in the popover. The four states at app/Uplink/desktop-agent.ts#L262 do not gain a fifth, and update stays null.
  • Compare versions, not strings. 0.1.10 is newer than 0.1.9, and a running version ahead of the manifest is not an update.
  • The url comes from the agent, not from the request body. The origin guard at app/Uplink/desktop-agent.ts#L284 refuses cross site requests, and /api/open/download should still ignore any url a caller sends so the loopback port cannot be turned into an arbitrary open.
  • An in-place updater is out of scope. This issue is a notification and a link to the DMG. An appcast with signatures, or a Homebrew cask as the update channel, is separate work.
  • Say that the Full Disk Access grant survives the update. macOS keys the grant to a bundle id plus its signature, which is why the source build had to take a different bundle id, documented at app/Uplink/service.ts#L14. Replacing the app with another build signed by the same Developer ID keeps the grant, so the copy must not tell people to grant it again.

Acceptance

  • A running app whose version is behind the published manifest shows "update available" within a day without being relaunched, and immediately on the next launch.
  • /api/status carries update: null when the running version matches or exceeds the manifest, and { version, url } when it does not.
  • Clicking the footer button opens the manifest's DMG url in the browser. A POST to /api/open/download carrying a different url in its body opens nothing.
  • With networking off, the popover renders normally, the three setup checks are unchanged, and no error text appears.
  • The homepage copy names the update request among the things the app sends.

Sign in to comment on this issue.