relay-companion 0.1.577 → 0.1.578

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,10 @@ async function pauseRecoveryChecks({ homeDir, timeoutMs = 3 * 60_000, pollMs = 2
33
33
  sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
34
34
  const intent = require("./recovery-intent.cjs");
35
35
  const wasStopped = intent.stopped(homeDir);
36
- intent.setStopped(true, homeDir);
36
+ // Recorded as held by this setup, so Relay's own setup and pill launch during
37
+ // activation leave it paused; a dead holder no longer counts.
38
+ const hold = () => intent.setStopped(true, homeDir, { heldBy: "setup", pid: process.pid });
39
+ hold();
37
40
  const runLock = path.join(homeDir, ".relay", "recovery", "run.lock");
38
41
  for (const deadline = Date.now() + timeoutMs; ;) {
39
42
  try { bootstrap.acquireCanonicalLock(runLock).release(); break; }
@@ -44,7 +47,7 @@ async function pauseRecoveryChecks({ homeDir, timeoutMs = 3 * 60_000, pollMs = 2
44
47
  }
45
48
  }
46
49
  return {
47
- repause: () => intent.setStopped(true, homeDir),
50
+ repause: hold,
48
51
  resume: succeeded => intent.setStopped(succeeded ? false : wasStopped, homeDir),
49
52
  };
50
53
  }
@@ -107,6 +110,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
107
110
  homeDir = os.homedir(), verify = verifyBundle, extract = extractBundle, activate = bootstrap.activateRuntime,
108
111
  acquireLock = bootstrap.acquireCanonicalLock, health = require("./runtime-health.cjs").exactRuntimeHealth,
109
112
  recover = recovery.recoverWithCandidate, pauseRecovery = pauseRecoveryChecks,
113
+ hidePill = require("./previous-pill.cjs").hidePreviousPill,
110
114
  drain = require("./update-activity.cjs").drainCalls, download = bootstrap.downloadVerifiedArtifact,
111
115
  onProgress = () => {}, signal } = {}) {
112
116
  onProgress({ phase: "verifying", canCancel: false });
@@ -128,7 +132,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
128
132
  const journalPath = path.join(homeDir, ".relay", "application-migration.json");
129
133
  let releaseDrain;
130
134
  let downloadDirectory;
131
- let pausedRecovery, succeeded = false;
135
+ let pausedRecovery, previousPill, succeeded = false;
132
136
  try {
133
137
  const recordedCurrent = recovery.marker(path.join(runtimeRoot, "current.json"));
134
138
  const removed = recovery.marker(path.join(homeDir, ".relay", "application-uninstall.json"));
@@ -199,6 +203,8 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
199
203
  return { ok: true, applicationOnly: true, owner, runtime: current, updateOwner: "canonical-runtime" };
200
204
  }
201
205
  pausedRecovery = await pauseRecovery({ homeDir });
206
+ // Only the old pill goes now; its background service runs until the switch.
207
+ if (current?.active === true) previousPill = hidePill({ current });
202
208
  if (bundle.receipt.runtimeDelivery === "download") {
203
209
  signal?.throwIfAborted();
204
210
  // Stage beside releases: tar requires archive and destination on one volume.
@@ -275,6 +281,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
275
281
  try { releaseDrain?.(); }
276
282
  finally {
277
283
  try { pausedRecovery?.resume(succeeded); } catch { /* recovery reads a missing intent as running */ }
284
+ if (!succeeded) try { previousPill?.restore(); } catch { /* recovery restores a missing pill */ }
278
285
  lock.release();
279
286
  if (downloadDirectory) fs.rmSync(downloadDirectory, { recursive: true, force: true });
280
287
  }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ // The installer's setup leaves the existing Relay running while it downloads,
3
+ // so a failed download still leaves a working Relay. Its pill, though, stayed
4
+ // on screen beside the setup window looking active (Shane, 2026-10-02). Hide
5
+ // only the old pill when setup starts; the background service keeps running
6
+ // until activation switches over and starts the new pill. If setup fails, the
7
+ // previous Relay opens its pill again.
8
+ const { spawnSync, spawn } = require("node:child_process");
9
+
10
+ const MAC_PILL_LABEL = "work.relay.companion.pill";
11
+ // Every stock pill runs relay-companion's overlay/main.cjs under Electron.
12
+ const WINDOWS_STOP_PILL_PS = [
13
+ "$p=Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {",
14
+ " $_.CommandLine -and ($_.CommandLine -match '[\\\\/]relay-companion[\\\\/]') -and ($_.CommandLine -match '[\\\\/]overlay[\\\\/]main')",
15
+ "}; foreach($x in $p){ try { Invoke-CimMethod -InputObject $x -MethodName Terminate -ErrorAction Stop | Out-Null } catch {} }",
16
+ ].join(" ");
17
+
18
+ function hidePreviousPill({ current, platform = process.platform, uid = process.getuid?.(), run = spawnSync, launch = spawn } = {}) {
19
+ let hidden = false;
20
+ try {
21
+ if (platform === "win32") {
22
+ const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_STOP_PILL_PS], { windowsHide: true, timeout: 60_000 });
23
+ hidden = !result?.error && result?.status === 0;
24
+ } else if (platform === "darwin") {
25
+ // launchd keeps the pill alive, so stop its job rather than its process.
26
+ const result = run("/bin/launchctl", ["bootout", `gui/${uid}/${MAC_PILL_LABEL}`], { timeout: 30_000 });
27
+ hidden = !result?.error && result?.status === 0;
28
+ } else {
29
+ const result = run("pkill", ["-u", String(uid), "-f", "relay-companion/overlay/main"], { timeout: 30_000 });
30
+ hidden = !result?.error && result?.status === 0;
31
+ }
32
+ } catch { hidden = false; }
33
+ return {
34
+ hidden,
35
+ // Only an active previous runtime can reopen its own pill.
36
+ restore() {
37
+ if (!hidden || current?.active !== true || typeof current.node !== "string" || typeof current.bin !== "string") return false;
38
+ try {
39
+ const child = launch(current.node, [current.bin, "pill"], { detached: true, stdio: "ignore", windowsHide: true });
40
+ child.unref?.();
41
+ return true;
42
+ } catch { return false; }
43
+ },
44
+ };
45
+ }
46
+
47
+ module.exports = { hidePreviousPill, WINDOWS_STOP_PILL_PS, MAC_PILL_LABEL };
@@ -1,11 +1,33 @@
1
1
  "use strict";
2
2
  const fs = require("node:fs"), os = require("node:os"), path = require("node:path");
3
3
  const file = homeDir => path.join(homeDir, ".relay", "recovery", "intent.json");
4
+ function read(homeDir) {
5
+ try { return { value: JSON.parse(fs.readFileSync(file(homeDir), "utf8")) }; }
6
+ catch (error) { return { missing: error.code === "ENOENT" }; }
7
+ }
8
+ function alive(pid) {
9
+ try { process.kill(pid, 0); return true; } catch (error) { return error.code === "EPERM"; }
10
+ }
11
+ // The installer's setup pauses recovery while it replaces Relay and records
12
+ // itself as the holder. A hold whose setup process is gone no longer counts,
13
+ // so a killed setup can never leave recovery paused for good.
14
+ function heldBySetup(value) {
15
+ return value?.stopped === true && value.heldBy === "setup" && Number.isInteger(value.pid) && alive(value.pid);
16
+ }
4
17
  function stopped(homeDir = os.homedir()) {
5
- try { return JSON.parse(fs.readFileSync(file(homeDir), "utf8")).stopped === true; }
6
- catch (error) { return error.code !== "ENOENT"; }
18
+ const { value, missing } = read(homeDir);
19
+ if (!value) return !missing;
20
+ if (value.stopped === true && value.heldBy === "setup") return heldBySetup(value);
21
+ return value.stopped === true;
22
+ }
23
+ function setStopped(value, homeDir = os.homedir(), { heldBy, pid } = {}) {
24
+ require("./recovery-launcher.cjs").write(file(homeDir), { schema: 1, stopped: value, at: Date.now(), ...(value && heldBy ? { heldBy, pid } : {}) });
7
25
  }
8
- function setStopped(value, homeDir = os.homedir()) {
9
- require("./recovery-launcher.cjs").write(file(homeDir), { schema: 1, stopped: value, at: Date.now() });
26
+ // Starting Relay (setup, the pill) resumes recovery, except while an
27
+ // installer's setup holds the pause; that setup lifts it when it ends.
28
+ function resumeUnlessHeld(homeDir = os.homedir()) {
29
+ if (heldBySetup(read(homeDir).value)) return false;
30
+ setStopped(false, homeDir);
31
+ return true;
10
32
  }
11
- module.exports = { stopped, setStopped };
33
+ module.exports = { stopped, setStopped, resumeUnlessHeld };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.577",
3
+ "version": "0.1.578",
4
4
  "description": "Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.",
5
5
  "homepage": "https://sendrelays.com/get-started",
6
6
  "repository": {