pushary 1.9.133 → 1.9.134

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.134
4
+
5
+ - Linux: on a machine you reach over SSH, phone-start keeps running after you log out, and starts again after a reboot. Setup, `pushary upgrade` and `pushary daemon install` now turn on systemd lingering for your user unless you are signed in to a desktop session there, and say so. Where the system refuses, they print the `sudo loginctl enable-linger` command instead. Before, systemd stopped the daemon when your last SSH session closed, while setup still reported phone-start as working. `pushary doctor` has a new row, "Remote-control service survives logout".
6
+
3
7
  ## 1.9.133
4
8
 
5
9
  - The background daemon starts again by itself after its lock file was left empty or cut off, for example when the disk filled up or the computer lost power while it was starting, and, on macOS and Linux, after a power cut or forced restart left a lock that names a process number another program now uses. Before, it exited at once and was restarted again and again, writing an error each time, and even `pushary daemon restart` could not clear the second case.
@@ -8,6 +8,10 @@ import {
8
8
  } from "../chunk-6JY44IZI.js";
9
9
  import "../chunk-FT5MTCKU.js";
10
10
  import "../chunk-PG23ENSN.js";
11
+ import {
12
+ keepLinuxDaemonAfterLogout,
13
+ lingerReportOf
14
+ } from "../chunk-AIZYJJGD.js";
11
15
  import {
12
16
  HEARTBEAT_INTERVAL_MS,
13
17
  sendHeartbeat
@@ -2305,6 +2309,9 @@ var main = async () => {
2305
2309
  return EXIT.UNREACHABLE;
2306
2310
  }
2307
2311
  process.stdout.write("[pushary] daemon service installed and online\n");
2312
+ const linger = lingerReportOf(keepLinuxDaemonAfterLogout());
2313
+ if (linger) (linger.tone === "warning" ? process.stderr : process.stdout).write(`[pushary] ${linger.text}
2314
+ `);
2308
2315
  return EXIT.OK;
2309
2316
  }
2310
2317
  if (action === "start") {
@@ -32,6 +32,11 @@ import {
32
32
  roundTripEvidence,
33
33
  verifyRoundTrip
34
34
  } from "../chunk-L5RSLIBQ.js";
35
+ import {
36
+ daemonStopsAtLogout,
37
+ lingerDoctorDetail,
38
+ readLinuxLinger
39
+ } from "../chunk-AIZYJJGD.js";
35
40
  import {
36
41
  readMachineReadiness,
37
42
  readManagedControlEvidence
@@ -1198,6 +1203,8 @@ var main = async () => {
1198
1203
  } else if (serviceFile) {
1199
1204
  console.log(` ${warn} Remote-control service runtime ${dim(`(could not read the node path out of ${serviceFile.path})`)}`);
1200
1205
  }
1206
+ const linger = serviceFile && controlPolicy === "enabled" ? readLinuxLinger() : null;
1207
+ if (linger) check(!daemonStopsAtLogout(linger), "Remote-control service survives logout", lingerDoctorDetail(linger));
1201
1208
  let serverProof = null;
1202
1209
  if (controlPolicy === "enabled" && apiKey && daemon.machineId) {
1203
1210
  serverProof = await readMachineReadiness(apiKey, expectedLaunchCapabilities(configuredManagedProviders), {
@@ -91,6 +91,10 @@ import {
91
91
  roundTripWasAnswered,
92
92
  verifyRoundTrip
93
93
  } from "../chunk-L5RSLIBQ.js";
94
+ import {
95
+ keepLinuxDaemonAfterLogout,
96
+ lingerReportOf
97
+ } from "../chunk-AIZYJJGD.js";
94
98
  import {
95
99
  readManagedControlEvidence,
96
100
  waitForMachineReadiness
@@ -894,6 +898,15 @@ var warnVersionManagerPin = (execPath) => {
894
898
  const manager = versionManagerOf(execPath);
895
899
  if (manager) console.log(` ${yellow("!")} ${describeVersionManagerPin(execPath, manager)}`);
896
900
  };
901
+ var reportLinger = (report) => {
902
+ if (!report) return;
903
+ if (report.tone === "note") {
904
+ console.log(` ${dim(report.text)}`);
905
+ return;
906
+ }
907
+ console.log(` ${yellow("!")} ${report.text}`);
908
+ noteManual(report.text);
909
+ };
897
910
  var installGlobally2 = async () => {
898
911
  if (globalInstallReady) return;
899
912
  const version = getPackageVersion();
@@ -2810,6 +2823,7 @@ var main = async () => {
2810
2823
  const service = await replaceDaemonService();
2811
2824
  if (service.kind === "ok") {
2812
2825
  warnVersionManagerPin(serviceNodeFor(process.execPath, existsSync));
2826
+ reportLinger(lingerReportOf(keepLinuxDaemonAfterLogout()));
2813
2827
  controlFailureStage = "daemon-start";
2814
2828
  daemon = await ensureDaemonRunning({ spawnDaemon: () => {
2815
2829
  }, waitTimeoutMs: DAEMON_COLD_START_TIMEOUT_MS });
@@ -34,6 +34,10 @@ import {
34
34
  describeCodexTrustConfirmation
35
35
  } from "../chunk-RVGYXUOC.js";
36
36
  import "../chunk-L5RSLIBQ.js";
37
+ import {
38
+ keepLinuxDaemonAfterLogout,
39
+ lingerReportOf
40
+ } from "../chunk-AIZYJJGD.js";
37
41
  import {
38
42
  waitForMachineReadiness
39
43
  } from "../chunk-FJST6ZXC.js";
@@ -347,6 +351,8 @@ var finishUpgrade = async () => {
347
351
  var startBackgroundService = async (apiKey, providers) => {
348
352
  const service = await replaceDaemonService();
349
353
  if (service.kind !== "ok") return "did_not_start";
354
+ const linger = lingerReportOf(keepLinuxDaemonAfterLogout());
355
+ if (linger) console.log(linger.tone === "warning" ? `! ${linger.text}` : linger.text);
350
356
  const daemon = await ensureDaemonRunning({ spawnDaemon: () => {
351
357
  }, waitTimeoutMs: DAEMON_COLD_START_TIMEOUT_MS });
352
358
  if (daemon.kind !== "running" && daemon.kind !== "started") return "did_not_start";
@@ -0,0 +1,95 @@
1
+ // src/linux-linger.ts
2
+ import { execFileSync } from "child_process";
3
+ import { userInfo } from "os";
4
+ var LOGINCTL_TIMEOUT_MS = 5e3;
5
+ var DESKTOP_SESSION_TYPES = /* @__PURE__ */ new Set(["x11", "wayland", "mir"]);
6
+ var STOPS_AT_LOGOUT = "stops when your last SSH session closes, and does not start after a reboot until you log in";
7
+ var NOT_NEEDED = { kind: "not-needed" };
8
+ var runLoginctl = (args) => execFileSync("loginctl", [...args], {
9
+ encoding: "utf8",
10
+ timeout: LOGINCTL_TIMEOUT_MS,
11
+ stdio: ["ignore", "pipe", "ignore"]
12
+ });
13
+ var propertyOf = (line) => {
14
+ const separator = line.indexOf("=");
15
+ return separator < 0 ? [line.trim(), ""] : [line.slice(0, separator).trim(), line.slice(separator + 1).trim()];
16
+ };
17
+ var propertiesOf = (output) => new Map(output.split("\n").map(propertyOf));
18
+ var lingerMayMatter = (platform, env) => platform === "linux" && !env.WSL_DISTRO_NAME;
19
+ var CLOSING_SESSION_STATE = "closing";
20
+ var sessionIsDesktop = (session, run) => {
21
+ if (session === "") return false;
22
+ try {
23
+ const properties = propertiesOf(run(["show-session", session, "--property=Type", "--property=State"]));
24
+ return DESKTOP_SESSION_TYPES.has(properties.get("Type") ?? "") && properties.get("State") !== CLOSING_SESSION_STATE;
25
+ } catch {
26
+ return false;
27
+ }
28
+ };
29
+ var daemonStopsAtLogout = (facts) => !facts.linger && !facts.desktopSession;
30
+ var readLingerFacts = (user, run) => {
31
+ try {
32
+ const properties = propertiesOf(run(["show-user", user, "--property=Linger", "--property=Display"]));
33
+ const linger = properties.get("Linger");
34
+ if (linger !== "yes" && linger !== "no") return null;
35
+ return { linger: linger === "yes", desktopSession: sessionIsDesktop(properties.get("Display") ?? "", run) };
36
+ } catch {
37
+ return null;
38
+ }
39
+ };
40
+ var userOf = (options) => {
41
+ try {
42
+ return options.user ?? userInfo().username;
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+ var readLinuxLinger = (options = {}) => {
48
+ if (!lingerMayMatter(options.platform ?? process.platform, options.env ?? process.env)) return null;
49
+ const user = userOf(options);
50
+ return user === null ? null : readLingerFacts(user, options.run ?? runLoginctl);
51
+ };
52
+ var keepLinuxDaemonAfterLogout = (options = {}) => {
53
+ if (!lingerMayMatter(options.platform ?? process.platform, options.env ?? process.env)) return NOT_NEEDED;
54
+ const user = userOf(options);
55
+ if (user === null) return NOT_NEEDED;
56
+ const run = options.run ?? runLoginctl;
57
+ const facts = readLingerFacts(user, run);
58
+ if (!facts || !daemonStopsAtLogout(facts)) return NOT_NEEDED;
59
+ try {
60
+ run(["--no-ask-password", "enable-linger", user]);
61
+ } catch {
62
+ return { kind: "needs-admin", user };
63
+ }
64
+ return readLingerFacts(user, run)?.linger === true ? { kind: "enabled", user } : { kind: "needs-admin", user };
65
+ };
66
+ var lingerReportOf = (outcome) => {
67
+ if (outcome.kind === "enabled") {
68
+ return {
69
+ tone: "note",
70
+ text: `Phone-start keeps running after you log out of this machine: turned on systemd lingering for ${outcome.user}. Undo with \`loginctl disable-linger\`.`
71
+ };
72
+ }
73
+ if (outcome.kind === "needs-admin") {
74
+ return {
75
+ tone: "warning",
76
+ text: `Phone-start ${STOPS_AT_LOGOUT}. To keep it running: \`sudo loginctl enable-linger ${outcome.user}\``
77
+ };
78
+ }
79
+ return null;
80
+ };
81
+ var lingerDoctorDetail = (facts) => {
82
+ if (daemonStopsAtLogout(facts)) {
83
+ return `no: it ${STOPS_AT_LOGOUT}; run \`pushary daemon install\`, or \`sudo loginctl enable-linger $USER\``;
84
+ }
85
+ if (facts.linger) return "systemd lingering is on";
86
+ return "while you are signed in to the desktop";
87
+ };
88
+
89
+ export {
90
+ daemonStopsAtLogout,
91
+ readLinuxLinger,
92
+ keepLinuxDaemonAfterLogout,
93
+ lingerReportOf,
94
+ lingerDoctorDetail
95
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushary",
3
- "version": "1.9.133",
3
+ "version": "1.9.134",
4
4
  "description": "Human-in-the-loop (HITL) for AI agents: ask a person for approvals or answers by push notification. One command connects Claude Code, Codex, Cursor, Gemini CLI and more.",
5
5
  "keywords": [
6
6
  "pushary",