ReviewOS

also looking at this

stacks/uplink

fix(app): a fresh install now either works or says why

#8
Merged glennmichael123 wants to merge fix/setup-blockers into main
16 files +512 -75
9 days ago

Closes the three blockers in #5. Each is a way a new install ends up broken while reporting itself healthy.

1. A reply can fail silently

The chain has two halves and setup only proved the inbound one.

Every reply goes through osascript telling Messages to send (app/Uplink/sender.ts#L45), and the first such call raises the macOS Automation prompt, which only somebody sitting at the Mac can answer. The whole premise of the product is that nobody is. reply() then swallowed the refusal: it caught, logged one line and returned, by which point the run was already recorded as done. buddy uplink:doctor was worse than silent, pushing a check hardcoded to ok: true.

So the most likely outcome of a first install was a dashboard showing a successful run and a phone that heard nothing.

  • Sender gains canSend(), which reads from Messages and sends nothing, so setup can raise the prompt while someone is at the Mac.
  • A fourth step in the popover, "Let Uplink send replies", gates readiness.
  • reply() reports whether it delivered, and finish() marks a run failed when the answer never left the Mac.
  • The grant is per app, so uplink:doctor under Terminal cannot answer for Uplink.app. The watcher probes itself and reports through the heartbeat; the doctor says it cannot be checked when the watcher is not running, rather than measuring the wrong subject and reporting green. That is the same class of bug as the existing chat.db fallback.
  • A timeout (-1712) is distinguished from a refusal (-1743), and a not-yet-granted state is re-asked every 15 seconds, so a Messages that was still starting does not leave the app stuck in setup.

2. The downloaded app inherits launchd's bare PATH

The from-source launcher has always built a PATH before spawning the watcher, with the reason in a comment at app/Uplink/launcher.ts#L25. The downloadable app did none of it: app/Desktop/launcher.ts never touched PATH, so the engines, their probes and osascript all inherited launchd's, and a claude installed through bun, npm or nvm was unreachable on a Mac where it works perfectly in a terminal. The person is then told they are not signed in.

Both launchers now build the list in one place so they cannot drift. The user's own locations come first, because Uplink's pinned toolchain must not shadow a CLI they installed themselves, and any existing PATH is kept last so a terminal still wins.

3. A missing CLI is reported as a sign-in failure

findClaude() returns the bare string claude when it finds nothing. The probe spawned that, and Bun throws rather than exiting 127, so the catch produced "Could not run claude. Is Claude Code installed?" rendered inside the step named "Sign in to Claude", whose buttons run claude setup-token and take a pasted token. Both are useless when the binary does not exist, and the command the person is told to run does not exist either.

A probe now answers why: ok, missing, signed-out or failed, decided before spawning anything. Each engine carries how to install it. The doctor names the check "(not installed)" and its fix is the install command plus the docs URL; the popover replaces the sign-in block with an install block and keeps the sign-in instructions hidden until the CLI exists.

Testing

35 tests pass locally, including a new one proving a refused send leaves the run failed rather than done, and five covering the PATH list. The subprocess-spawning suites still cannot run in the authoring environment, so CI is again the first real execution of those.

./pantry/.bin/bun test tests/unit/uplink

Still open on #5

Not in this PR, and each is drafted: the end-to-end self test, a wrong token being stored before it is checked, texts dropped without a reply after the Mac sleeps, the doctor's chat.db check reporting green when Terminal rather than Uplink.app can read it, no error surface or log button in the popover, own handles not seedable from a typed number, no uninstaller, and saving settings cancelling a running task.

🤖 Generated with Claude Code

16 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.