pushary 1.8.0

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.
Files changed (186) hide show
  1. package/CHANGELOG.md +1943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +330 -0
  4. package/data/SKILL.md +591 -0
  5. package/data/cowork/SKILL.md +77 -0
  6. package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
  7. package/data/cursor-plugin/CHANGELOG.md +70 -0
  8. package/data/cursor-plugin/CONTRIBUTING.md +40 -0
  9. package/data/cursor-plugin/LICENSE +21 -0
  10. package/data/cursor-plugin/README.md +113 -0
  11. package/data/cursor-plugin/SECURITY.md +33 -0
  12. package/data/cursor-plugin/assets/logo.png +0 -0
  13. package/data/cursor-plugin/commands/notify-when-done.md +14 -0
  14. package/data/cursor-plugin/commands/pushary-test.md +13 -0
  15. package/data/cursor-plugin/hooks/hooks.json +75 -0
  16. package/data/cursor-plugin/mcp.json +11 -0
  17. package/data/cursor-plugin/rules/pushary.mdc +40 -0
  18. package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
  19. package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
  20. package/data/cursor-plugin/scripts/redaction.mjs +51 -0
  21. package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
  22. package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
  23. package/data/vscode-plugin/.mcp.json +11 -0
  24. package/data/vscode-plugin/CHANGELOG.md +49 -0
  25. package/data/vscode-plugin/CONTRIBUTING.md +40 -0
  26. package/data/vscode-plugin/LICENSE +21 -0
  27. package/data/vscode-plugin/README.md +128 -0
  28. package/data/vscode-plugin/SECURITY.md +56 -0
  29. package/data/vscode-plugin/assets/logo.png +0 -0
  30. package/data/vscode-plugin/commands/notify-when-done.md +14 -0
  31. package/data/vscode-plugin/commands/pushary-test.md +13 -0
  32. package/data/vscode-plugin/hooks/hooks.json +60 -0
  33. package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
  34. package/data/vscode-plugin/scripts/redaction.mjs +51 -0
  35. package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
  36. package/dist/bin/pushary-bell-hook.d.ts +1 -0
  37. package/dist/bin/pushary-bell-hook.js +46 -0
  38. package/dist/bin/pushary-bell.d.ts +1 -0
  39. package/dist/bin/pushary-bell.js +146 -0
  40. package/dist/bin/pushary-claude.d.ts +1 -0
  41. package/dist/bin/pushary-claude.js +1916 -0
  42. package/dist/bin/pushary-clean.d.ts +1 -0
  43. package/dist/bin/pushary-clean.js +1300 -0
  44. package/dist/bin/pushary-codex-bridge.d.ts +1 -0
  45. package/dist/bin/pushary-codex-bridge.js +599 -0
  46. package/dist/bin/pushary-codex-hook.d.ts +1 -0
  47. package/dist/bin/pushary-codex-hook.js +886 -0
  48. package/dist/bin/pushary-codex.d.ts +1 -0
  49. package/dist/bin/pushary-codex.js +125 -0
  50. package/dist/bin/pushary-connect.d.ts +1 -0
  51. package/dist/bin/pushary-connect.js +100 -0
  52. package/dist/bin/pushary-cowork.d.ts +1 -0
  53. package/dist/bin/pushary-cowork.js +36 -0
  54. package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
  55. package/dist/bin/pushary-daemon-supervisor.js +27 -0
  56. package/dist/bin/pushary-daemon.d.ts +1 -0
  57. package/dist/bin/pushary-daemon.js +878 -0
  58. package/dist/bin/pushary-disconnect.d.ts +1 -0
  59. package/dist/bin/pushary-disconnect.js +264 -0
  60. package/dist/bin/pushary-doctor.d.ts +1 -0
  61. package/dist/bin/pushary-doctor.js +1433 -0
  62. package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
  63. package/dist/bin/pushary-elicitation-hook.js +149 -0
  64. package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
  65. package/dist/bin/pushary-gemini-bridge.js +339 -0
  66. package/dist/bin/pushary-gemini-hook.d.ts +1 -0
  67. package/dist/bin/pushary-gemini-hook.js +381 -0
  68. package/dist/bin/pushary-hook.d.ts +1 -0
  69. package/dist/bin/pushary-hook.js +95 -0
  70. package/dist/bin/pushary-login.d.ts +1 -0
  71. package/dist/bin/pushary-login.js +198 -0
  72. package/dist/bin/pushary-logout.d.ts +1 -0
  73. package/dist/bin/pushary-logout.js +147 -0
  74. package/dist/bin/pushary-mcp.d.ts +1 -0
  75. package/dist/bin/pushary-mcp.js +114 -0
  76. package/dist/bin/pushary-mode.d.ts +1 -0
  77. package/dist/bin/pushary-mode.js +130 -0
  78. package/dist/bin/pushary-notification-hook.d.ts +1 -0
  79. package/dist/bin/pushary-notification-hook.js +56 -0
  80. package/dist/bin/pushary-opencode-hook.d.ts +1 -0
  81. package/dist/bin/pushary-opencode-hook.js +338 -0
  82. package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
  83. package/dist/bin/pushary-permission-denied-hook.js +60 -0
  84. package/dist/bin/pushary-permission-hook.d.ts +1 -0
  85. package/dist/bin/pushary-permission-hook.js +65 -0
  86. package/dist/bin/pushary-post-hook.d.ts +1 -0
  87. package/dist/bin/pushary-post-hook.js +70 -0
  88. package/dist/bin/pushary-prompt-hook.d.ts +1 -0
  89. package/dist/bin/pushary-prompt-hook.js +61 -0
  90. package/dist/bin/pushary-session-end-hook.d.ts +1 -0
  91. package/dist/bin/pushary-session-end-hook.js +59 -0
  92. package/dist/bin/pushary-session-start-hook.d.ts +1 -0
  93. package/dist/bin/pushary-session-start-hook.js +72 -0
  94. package/dist/bin/pushary-setup.d.ts +1 -0
  95. package/dist/bin/pushary-setup.js +2660 -0
  96. package/dist/bin/pushary-stats.d.ts +1 -0
  97. package/dist/bin/pushary-stats.js +44 -0
  98. package/dist/bin/pushary-status.d.ts +1 -0
  99. package/dist/bin/pushary-status.js +262 -0
  100. package/dist/bin/pushary-stop-hook.d.ts +1 -0
  101. package/dist/bin/pushary-stop-hook.js +63 -0
  102. package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
  103. package/dist/bin/pushary-stopfailure-hook.js +56 -0
  104. package/dist/bin/pushary-suggestions.d.ts +1 -0
  105. package/dist/bin/pushary-suggestions.js +100 -0
  106. package/dist/bin/pushary-transcript-register.d.ts +1 -0
  107. package/dist/bin/pushary-transcript-register.js +40 -0
  108. package/dist/bin/pushary-transcripts.d.ts +1 -0
  109. package/dist/bin/pushary-transcripts.js +77 -0
  110. package/dist/bin/pushary-upgrade.d.ts +1 -0
  111. package/dist/bin/pushary-upgrade.js +599 -0
  112. package/dist/bin/pushary-wait.d.ts +1 -0
  113. package/dist/bin/pushary-wait.js +122 -0
  114. package/dist/bin/pushary.d.ts +1 -0
  115. package/dist/bin/pushary.js +74 -0
  116. package/dist/chunk-2ABTSGFT.js +173 -0
  117. package/dist/chunk-2OY3ZZ5N.js +124 -0
  118. package/dist/chunk-3EGEA4KH.js +44 -0
  119. package/dist/chunk-3EVPNIBE.js +185 -0
  120. package/dist/chunk-5OP5MQI7.js +45 -0
  121. package/dist/chunk-6J2JC2RT.js +149 -0
  122. package/dist/chunk-7QLSKOSU.js +19 -0
  123. package/dist/chunk-7Z7Q4GGS.js +88 -0
  124. package/dist/chunk-A2A2YTMG.js +835 -0
  125. package/dist/chunk-AHV4LNB5.js +377 -0
  126. package/dist/chunk-AXPCESYO.js +197 -0
  127. package/dist/chunk-C2WCPBF7.js +86 -0
  128. package/dist/chunk-CJVVKOSR.js +73 -0
  129. package/dist/chunk-CULUZTWQ.js +2117 -0
  130. package/dist/chunk-CVQYGMSR.js +142 -0
  131. package/dist/chunk-CVWG2N3Y.js +16 -0
  132. package/dist/chunk-D2FX3EPW.js +15 -0
  133. package/dist/chunk-DB3ONZB4.js +44 -0
  134. package/dist/chunk-DE4Y6TCC.js +160 -0
  135. package/dist/chunk-E4FJTUH4.js +249 -0
  136. package/dist/chunk-EPCXUP3P.js +41 -0
  137. package/dist/chunk-EX3GAPA4.js +44 -0
  138. package/dist/chunk-EYK3GSRH.js +143 -0
  139. package/dist/chunk-GMXKITVA.js +70 -0
  140. package/dist/chunk-GPEGOGRJ.js +49 -0
  141. package/dist/chunk-GU3EJCVV.js +43 -0
  142. package/dist/chunk-HQ6G3B5R.js +59 -0
  143. package/dist/chunk-HZVVNGEW.js +22 -0
  144. package/dist/chunk-IPVKH2ST.js +231 -0
  145. package/dist/chunk-J5TIV3J6.js +238 -0
  146. package/dist/chunk-JU6W3XAU.js +64 -0
  147. package/dist/chunk-JYG4B33G.js +227 -0
  148. package/dist/chunk-JZTHGAPA.js +503 -0
  149. package/dist/chunk-KGDWDSNL.js +102 -0
  150. package/dist/chunk-KVQBWI5T.js +99 -0
  151. package/dist/chunk-KZERVKTD.js +16 -0
  152. package/dist/chunk-LXQ5FBBD.js +47 -0
  153. package/dist/chunk-M5ICYGHT.js +138 -0
  154. package/dist/chunk-M5JDZ45W.js +943 -0
  155. package/dist/chunk-MDEPU45E.js +1226 -0
  156. package/dist/chunk-MPWXGONV.js +157 -0
  157. package/dist/chunk-N6IOSZOE.js +185 -0
  158. package/dist/chunk-NGDD6TXY.js +192 -0
  159. package/dist/chunk-NQTHFF4W.js +2830 -0
  160. package/dist/chunk-OKXS7WDZ.js +68 -0
  161. package/dist/chunk-OLQRRFBX.js +21 -0
  162. package/dist/chunk-P7WQEVWA.js +307 -0
  163. package/dist/chunk-POFBE6ZY.js +12 -0
  164. package/dist/chunk-PQ4K74WL.js +30 -0
  165. package/dist/chunk-PUZZELE6.js +37 -0
  166. package/dist/chunk-Q7E4I6UA.js +767 -0
  167. package/dist/chunk-QCGNGEGE.js +15 -0
  168. package/dist/chunk-R7437YI3.js +151 -0
  169. package/dist/chunk-RHR6NARM.js +305 -0
  170. package/dist/chunk-SAXBC4AZ.js +60 -0
  171. package/dist/chunk-SGZJ5LYH.js +76 -0
  172. package/dist/chunk-SIFVFP6T.js +70 -0
  173. package/dist/chunk-SLFABHW3.js +150 -0
  174. package/dist/chunk-T7E75EKA.js +67 -0
  175. package/dist/chunk-TY4JT7W3.js +38 -0
  176. package/dist/chunk-U4TMAANR.js +111 -0
  177. package/dist/chunk-UI36QSBI.js +177 -0
  178. package/dist/chunk-VQKRG3AC.js +339 -0
  179. package/dist/chunk-WE2J62NC.js +1131 -0
  180. package/dist/chunk-XHKBHWLX.js +14 -0
  181. package/dist/chunk-ZSZSSJCV.js +275 -0
  182. package/dist/chunk-ZVSKMPBZ.js +135 -0
  183. package/dist/reapply-CW3MA66V.js +34 -0
  184. package/dist/src/index.d.ts +204 -0
  185. package/dist/src/index.js +64 -0
  186. package/package.json +108 -0
@@ -0,0 +1,878 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ inspectConfiguredManagedProviders
4
+ } from "../chunk-VQKRG3AC.js";
5
+ import "../chunk-SIFVFP6T.js";
6
+ import "../chunk-RHR6NARM.js";
7
+ import {
8
+ HEARTBEAT_INTERVAL_MS,
9
+ sendHeartbeat
10
+ } from "../chunk-DE4Y6TCC.js";
11
+ import "../chunk-AXPCESYO.js";
12
+ import {
13
+ detectProviderLiveCapabilities,
14
+ hasSpawnProvider,
15
+ resolveControlPolicy
16
+ } from "../chunk-HQ6G3B5R.js";
17
+ import {
18
+ SESSION_REAP_SILENCE_MS,
19
+ describeIdleWindow,
20
+ findLiveProcessBySpawnId,
21
+ listProcessProofs,
22
+ ownsLiveProcess,
23
+ silentSpawnedProofs
24
+ } from "../chunk-ZSZSSJCV.js";
25
+ import {
26
+ startRegisteredTranscripts
27
+ } from "../chunk-JZTHGAPA.js";
28
+ import "../chunk-SGZJ5LYH.js";
29
+ import "../chunk-EYK3GSRH.js";
30
+ import "../chunk-ZVSKMPBZ.js";
31
+ import "../chunk-C2WCPBF7.js";
32
+ import "../chunk-6J2JC2RT.js";
33
+ import {
34
+ DAEMON_LEASE_RENEW_INTERVAL_MS,
35
+ acquireDaemonLease,
36
+ clearDaemonNeedsLogin,
37
+ daemonServiceManagerAvailable,
38
+ describeDaemonLoginBlock,
39
+ ensureDaemonRunning,
40
+ hasDaemonServiceDefinition,
41
+ manageDaemonService,
42
+ parseDaemonAction,
43
+ readDaemonNeedsLogin,
44
+ readDaemonStatus,
45
+ replaceDaemonService,
46
+ requestDaemonHandoff,
47
+ stopDaemonRunning,
48
+ writeDaemonNeedsLogin
49
+ } from "../chunk-WE2J62NC.js";
50
+ import "../chunk-3EVPNIBE.js";
51
+ import "../chunk-OKXS7WDZ.js";
52
+ import "../chunk-MPWXGONV.js";
53
+ import "../chunk-SAXBC4AZ.js";
54
+ import "../chunk-OLQRRFBX.js";
55
+ import {
56
+ spawnClaude
57
+ } from "../chunk-XHKBHWLX.js";
58
+ import "../chunk-EPCXUP3P.js";
59
+ import {
60
+ writeHuman
61
+ } from "../chunk-QCGNGEGE.js";
62
+ import {
63
+ exitOnHelpFlag
64
+ } from "../chunk-HZVVNGEW.js";
65
+ import "../chunk-GMXKITVA.js";
66
+ import {
67
+ readControlMode
68
+ } from "../chunk-P7WQEVWA.js";
69
+ import {
70
+ getMachineId
71
+ } from "../chunk-LXQ5FBBD.js";
72
+ import "../chunk-7QLSKOSU.js";
73
+ import {
74
+ EXIT
75
+ } from "../chunk-KZERVKTD.js";
76
+ import "../chunk-DB3ONZB4.js";
77
+ import {
78
+ resolveBinary
79
+ } from "../chunk-A2A2YTMG.js";
80
+ import "../chunk-T7E75EKA.js";
81
+ import {
82
+ getApiKey,
83
+ getBaseUrl
84
+ } from "../chunk-GPEGOGRJ.js";
85
+ import {
86
+ NPX_LATEST_COMMAND
87
+ } from "../chunk-E4FJTUH4.js";
88
+ import "../chunk-NQTHFF4W.js";
89
+
90
+ // src/spawn-daemon.ts
91
+ import { execFileSync, spawn } from "child_process";
92
+ import { existsSync, statSync } from "fs";
93
+ import { homedir } from "os";
94
+ import { basename, dirname, join } from "path";
95
+
96
+ // src/stop-requests.ts
97
+ var STOP_CAPABILITY = "stop";
98
+ var STOP_ACK_CAPABILITY = "stop-ack";
99
+ var windowsTaskkillArgs = (pid) => ["/PID", String(pid), "/T", "/F"];
100
+ var isStopRequest = (value) => {
101
+ if (!value || typeof value !== "object") return false;
102
+ const candidate = value;
103
+ return (candidate.id === void 0 || typeof candidate.id === "string" && candidate.id.length > 0 && candidate.id.length <= 64) && (candidate.spawnId === void 0 || typeof candidate.spawnId === "string" && candidate.spawnId.length > 0 && candidate.spawnId.length <= 64) && typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && typeof candidate.pid === "number" && Number.isInteger(candidate.pid) && candidate.pid > 1;
104
+ };
105
+ var parseStopRequests = (value) => Array.isArray(value) ? value.filter(isStopRequest) : [];
106
+ var applyStop = (request, deps) => {
107
+ if (request.pid === deps.selfPid) return "skipped_self";
108
+ if (!deps.ownsProcess(request)) return "refused";
109
+ try {
110
+ deps.kill(request.pid, "SIGTERM");
111
+ return "stopped";
112
+ } catch (err) {
113
+ const code = err?.code;
114
+ return code === "ESRCH" ? "already_gone" : "refused";
115
+ }
116
+ };
117
+ var applyStopAndVerify = async (request, deps, attempts = 100, intervalMs = 100) => {
118
+ if (!deps.isAlive(request.pid)) return "already_gone";
119
+ if (!await deps.ownsProcess(request)) return "refused";
120
+ const outcome = applyStop(request, { ...deps, ownsProcess: () => true });
121
+ if (outcome !== "stopped") return outcome;
122
+ for (let attempt = 0; attempt < attempts; attempt++) {
123
+ await deps.sleep(intervalMs);
124
+ if (!deps.isAlive(request.pid)) return "stopped";
125
+ }
126
+ return "signal_sent";
127
+ };
128
+
129
+ // src/spawn-daemon.ts
130
+ var DRAIN_PATH = "/api/agent/spawn/drain";
131
+ var ACK_PATH = "/api/agent/spawn/ack";
132
+ var SPAWN_PROTOCOL_VERSION = 2;
133
+ var FAST_POLL_MS = 3e3;
134
+ var SLOW_POLL_MS = 3e4;
135
+ var FAST_DECAY_TICKS = 3;
136
+ var MAX_ERROR_BACKOFF_MS = 12e4;
137
+ var MAX_ERROR_STREAK = 6;
138
+ var REQUEST_TIMEOUT_MS = 1e4;
139
+ var STOP_PROTOCOL_VERSION = 2;
140
+ var MAX_SPAWNS_PER_WINDOW = 5;
141
+ var SPAWN_WINDOW_MS = 6e4;
142
+ var VERSION_CHECK_INTERVAL_MS = 2e3;
143
+ var CONTROL_PROBE_INTERVAL_MS = 6e4;
144
+ var MAX_TRACKED_PROCESSES = 100;
145
+ var MAX_COMPLETED_STOPS = 20;
146
+ var SESSION_REAP_INTERVAL_MS = 6e4;
147
+ var controlProbeDue = (lastProbeAt, at = Date.now()) => lastProbeAt === 0 || at - lastProbeAt >= CONTROL_PROBE_INTERVAL_MS;
148
+ var parseControlProbe = (value) => {
149
+ if (typeof value !== "object" || value === null) return null;
150
+ const id = value.id;
151
+ return typeof id === "string" && id.length > 0 && id.length <= 128 ? id : null;
152
+ };
153
+ var daemonSignals = (platform = process.platform) => platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
154
+ var parseSpawnClaim = (value) => {
155
+ if (typeof value !== "object" || value === null) return null;
156
+ const request = value;
157
+ if (typeof request.id !== "string" || typeof request.prompt !== "string") return null;
158
+ return {
159
+ id: request.id,
160
+ prompt: request.prompt,
161
+ ...typeof request.claimId === "string" ? { claimId: request.claimId } : {},
162
+ ...Number.isInteger(request.attemptCount) ? { attemptCount: request.attemptCount } : {},
163
+ ...typeof request.cwd === "string" ? { cwd: request.cwd } : {},
164
+ ...typeof request.agent === "string" ? { agent: request.agent } : {},
165
+ ...typeof request.model === "string" ? { model: request.model } : {},
166
+ ...typeof request.resumeSessionId === "string" ? { resumeSessionId: request.resumeSessionId } : {}
167
+ };
168
+ };
169
+ var stopAcknowledgement = (request, machineId, state) => request.id ? { stopId: request.id, machineId, state } : null;
170
+ var spawnAcknowledgement = (request, machineId, outcome) => {
171
+ if (!request.claimId) return null;
172
+ return outcome.state === "started" ? {
173
+ spawnId: request.id,
174
+ claimId: request.claimId,
175
+ machineId,
176
+ state: "started",
177
+ pid: outcome.pid ?? null
178
+ } : {
179
+ spawnId: request.id,
180
+ claimId: request.claimId,
181
+ machineId,
182
+ state: "failed",
183
+ failure: {
184
+ code: "launch_failed",
185
+ message: outcome.message.slice(0, 240),
186
+ retryable: false
187
+ }
188
+ };
189
+ };
190
+ var launchSpawnClaim = async (request, runtime) => {
191
+ const existingPid = await runtime.findExistingPid(request.id);
192
+ if (existingPid !== null) {
193
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "started", pid: existingPid }));
194
+ return;
195
+ }
196
+ let pid;
197
+ try {
198
+ pid = await runtime.launch(request);
199
+ } catch (err) {
200
+ const message = err instanceof Error ? err.message : String(err);
201
+ runtime.reportLaunchError(message);
202
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "failed", message }));
203
+ return;
204
+ }
205
+ await runtime.acknowledge(spawnAcknowledgement(request, runtime.machineId, { state: "started", pid }));
206
+ };
207
+ var DrainError = class extends Error {
208
+ constructor(status) {
209
+ super(`drain ${status}`);
210
+ this.status = status;
211
+ this.name = "DrainError";
212
+ }
213
+ };
214
+ var isAuthFailure = (err) => {
215
+ if (typeof err !== "object" || err === null) return false;
216
+ const candidate = err;
217
+ if (candidate.name !== "DrainError") return false;
218
+ return candidate.status === 401 || candidate.status === 403;
219
+ };
220
+ var normalizeLaunchAgent = (requestedAgent) => {
221
+ if (requestedAgent === void 0 || requestedAgent === "claude") return "claude";
222
+ if (requestedAgent === "codex" || requestedAgent === "gemini") return requestedAgent;
223
+ return null;
224
+ };
225
+ var providerLaunchError = (requestedAgent, binaries, configuredProviders) => {
226
+ const agent = normalizeLaunchAgent(requestedAgent);
227
+ if (!agent) return `Unsupported agent provider: ${requestedAgent}`;
228
+ if (configuredProviders && !configuredProviders.includes(agent)) {
229
+ return `${agent === "claude" ? "Claude Code" : agent === "codex" ? "Codex" : "Gemini"} is not configured for Pushary on this machine`;
230
+ }
231
+ if (binaries[agent]) return null;
232
+ return agent === "claude" ? "Claude Code is not installed on this machine" : `${agent === "codex" ? "Codex" : "Gemini"} CLI is not installed on this machine`;
233
+ };
234
+ var resolveSpawnDirectory = (requested, options = {}) => {
235
+ const fallbackDir = options.fallbackDir ?? process.cwd();
236
+ const value = requested?.trim();
237
+ if (!value) return { ok: true, cwd: fallbackDir };
238
+ const homeDir = options.homeDir ?? homedir();
239
+ const cwd = value === "~" ? homeDir : value.startsWith("~/") || value.startsWith("~\\") ? join(homeDir, value.slice(2)) : value;
240
+ if (!(options.exists ?? existsSync)(cwd)) {
241
+ return { ok: false, message: `Directory does not exist: ${value}` };
242
+ }
243
+ try {
244
+ if (!(options.isDirectory ?? ((path) => statSync(path).isDirectory()))(cwd)) {
245
+ return { ok: false, message: `Not a directory: ${value}` };
246
+ }
247
+ } catch {
248
+ return { ok: false, message: `Not a directory: ${value}` };
249
+ }
250
+ return { ok: true, cwd };
251
+ };
252
+ var isFlagShapedPrompt = (prompt) => prompt.trimStart().startsWith("-");
253
+ var buildSpawnLaunch = (req, opts) => {
254
+ if (isFlagShapedPrompt(req.prompt)) return null;
255
+ const agent = normalizeLaunchAgent(req.agent);
256
+ if (!agent) return null;
257
+ const model = typeof req.model === "string" && req.model.length > 0 ? ["--model", req.model] : [];
258
+ if (agent === "claude") {
259
+ const claudeArgs = ["claude", "--remote", ...model, "-p", req.prompt];
260
+ return opts.spawnDirect ? { direct: true, command: opts.execPath, args: [opts.siblingEntry, ...claudeArgs], agent } : { direct: false, command: opts.pusharyBin, args: claudeArgs, agent };
261
+ }
262
+ if (agent === "codex") {
263
+ return {
264
+ direct: true,
265
+ command: opts.execPath,
266
+ args: [opts.codexBridgeEntry],
267
+ agent
268
+ };
269
+ }
270
+ return {
271
+ direct: true,
272
+ command: opts.execPath,
273
+ args: [opts.geminiBridgeEntry],
274
+ agent
275
+ };
276
+ };
277
+ var spawnRateAllows = (timestamps, now, maxPerWindow = MAX_SPAWNS_PER_WINDOW, windowMs = SPAWN_WINDOW_MS) => {
278
+ while (timestamps.length > 0 && now - timestamps[0] > windowMs) timestamps.shift();
279
+ return timestamps.length < maxPerWindow;
280
+ };
281
+ var stderr = (msg) => {
282
+ process.stderr.write(msg);
283
+ };
284
+ var log = (message) => {
285
+ writeHuman(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}
286
+ `);
287
+ };
288
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
289
+ var trackFailureStreak = (name, sinks) => {
290
+ let failing = false;
291
+ let lastDetail = "";
292
+ return {
293
+ fail(detail) {
294
+ sinks.recordError(`${name}: ${detail}`);
295
+ if (!failing || detail !== lastDetail) sinks.log(`${name} failing: ${detail}`);
296
+ failing = true;
297
+ lastDetail = detail;
298
+ },
299
+ recover() {
300
+ if (failing) sinks.log(`${name} recovered`);
301
+ failing = false;
302
+ lastDetail = "";
303
+ }
304
+ };
305
+ };
306
+ var runSpawnDaemon = async () => {
307
+ let apiKey;
308
+ try {
309
+ apiKey = getApiKey();
310
+ } catch {
311
+ stderr(`[pushary] daemon needs an API key. Run \`${NPX_LATEST_COMMAND} setup\`.
312
+ `);
313
+ return 1;
314
+ }
315
+ const loginBlock = readDaemonNeedsLogin();
316
+ if (loginBlock.kind === "blocked") {
317
+ stderr(`[pushary] daemon not starting: ${describeDaemonLoginBlock(loginBlock.record)}
318
+ `);
319
+ return 0;
320
+ }
321
+ if (loginBlock.kind === "stale") clearDaemonNeedsLogin();
322
+ const machineId = getMachineId();
323
+ const baseUrl = getBaseUrl();
324
+ const pusharyBin = resolveBinary("pushary") ?? "pushary";
325
+ const configuredProviders = inspectConfiguredManagedProviders();
326
+ const providerCapabilities = detectProviderLiveCapabilities(configuredProviders);
327
+ const siblingEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary.js") : "";
328
+ const codexBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-codex-bridge.js") : "pushary-codex-bridge";
329
+ const geminiBridgeEntry = process.argv[1] ? join(dirname(process.argv[1]), "pushary-gemini-bridge.js") : "pushary-gemini-bridge";
330
+ const spawnDirect = siblingEntry.length > 0 && existsSync(siblingEntry);
331
+ const existing = readDaemonStatus();
332
+ let handoff;
333
+ try {
334
+ handoff = await requestDaemonHandoff(existing);
335
+ } catch (err) {
336
+ stderr(`[pushary] daemon could not request a handoff: ${err instanceof Error ? err.message : String(err)}
337
+ `);
338
+ return 1;
339
+ }
340
+ if (existing.pid !== null && handoff.pid === existing.pid && handoff.processAlive === true) {
341
+ stderr("[pushary] daemon could not replace the existing process; run `pushary doctor` for details\n");
342
+ return 1;
343
+ }
344
+ const lifecycle = acquireDaemonLease({ machineId });
345
+ if (lifecycle.kind === "already-running") {
346
+ writeHuman(`[pushary] daemon already online as pid ${lifecycle.status.pid ?? "unknown"}
347
+ `);
348
+ return 0;
349
+ }
350
+ if (lifecycle.kind === "unavailable") {
351
+ stderr(`[pushary] daemon could not acquire its local lease: ${lifecycle.detail}
352
+ `);
353
+ return 1;
354
+ }
355
+ const daemonLease = lifecycle.lease;
356
+ const sinks = { log, recordError: (message) => daemonLease.recordError(message) };
357
+ const drainHealth = trackFailureStreak("drain", sinks);
358
+ const heartbeatHealth = trackFailureStreak("heartbeat", sinks);
359
+ let stopped = false;
360
+ let idleTicks = 0;
361
+ let errorStreak = 0;
362
+ let lastControlProbeAt = 0;
363
+ const spawnTimestamps = [];
364
+ const launchedProcesses = /* @__PURE__ */ new Map();
365
+ const completedStops = /* @__PURE__ */ new Map();
366
+ let timer;
367
+ let inflight;
368
+ let finish = () => {
369
+ };
370
+ let heartbeat;
371
+ let versionWatch;
372
+ let leaseRenewal;
373
+ let sessionReaper;
374
+ let reaping = false;
375
+ let rateCapped = false;
376
+ const launch = (req) => {
377
+ if (launchedProcesses.size >= MAX_TRACKED_PROCESSES) {
378
+ return Promise.reject(new Error("This daemon is already tracking 100 active sessions"));
379
+ }
380
+ const directory = resolveSpawnDirectory(req.cwd);
381
+ if (!directory.ok) return Promise.reject(new Error(directory.message));
382
+ const agent = normalizeLaunchAgent(req.agent);
383
+ if (!agent) return Promise.reject(new Error(`Unsupported agent provider: ${req.agent}`));
384
+ const providerBin = resolveBinary(agent);
385
+ const unavailable = providerLaunchError(req.agent, {
386
+ claude: agent === "claude" ? providerBin ?? null : null,
387
+ codex: agent === "codex" ? providerBin ?? null : null,
388
+ gemini: agent === "gemini" ? providerBin ?? null : null
389
+ }, configuredProviders);
390
+ if (unavailable) return Promise.reject(new Error(unavailable));
391
+ spawnTimestamps.push(Date.now());
392
+ const cwd = directory.cwd;
393
+ const env = {
394
+ ...process.env,
395
+ PUSHARY_SPAWN_ID: req.id,
396
+ PUSHARY_SPAWN_CLAIM_ID: req.claimId,
397
+ ...req.agent === "codex" || req.agent === "gemini" ? {
398
+ PUSHARY_BRIDGE_CWD: cwd,
399
+ PUSHARY_BRIDGE_PROMPT: req.prompt || void 0,
400
+ PUSHARY_BRIDGE_MODEL: req.model || void 0,
401
+ PUSHARY_BRIDGE_RESUME_SESSION_ID: req.resumeSessionId || void 0
402
+ } : {},
403
+ ...req.agent === "codex" ? {
404
+ PUSHARY_CODEX_COMMAND: providerBin ?? "codex"
405
+ } : {},
406
+ ...req.agent === "gemini" ? {
407
+ PUSHARY_GEMINI_COMMAND: providerBin ?? "gemini"
408
+ } : {}
409
+ };
410
+ const plan = buildSpawnLaunch(req, {
411
+ spawnDirect,
412
+ execPath: process.execPath,
413
+ siblingEntry,
414
+ pusharyBin,
415
+ codexBridgeEntry,
416
+ geminiBridgeEntry
417
+ });
418
+ if (!plan) {
419
+ return Promise.reject(new Error("The prompt looks like a command-line flag"));
420
+ }
421
+ return new Promise((resolve, reject) => {
422
+ try {
423
+ const child = plan.direct ? spawn(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env }) : spawnClaude(plan.command, plan.args, { cwd, detached: true, stdio: "ignore", env });
424
+ let started = false;
425
+ child.once("error", (err) => {
426
+ if (!started) reject(err);
427
+ else {
428
+ log(`spawn ${req.id}: launched session reported an error: ${err.message}`);
429
+ daemonLease.recordError(`spawn ${req.id}: ${err.message}`);
430
+ }
431
+ });
432
+ child.once("spawn", () => {
433
+ started = true;
434
+ if (typeof child.pid === "number") {
435
+ launchedProcesses.set(child.pid, req.id);
436
+ child.once("close", () => launchedProcesses.delete(child.pid));
437
+ }
438
+ child.unref();
439
+ log(`spawn ${req.id}: launched a phone-requested ${plan.agent} session in ${basename(cwd)} (pid ${child.pid ?? "unknown"})`);
440
+ resolve(typeof child.pid === "number" ? child.pid : null);
441
+ });
442
+ } catch (err) {
443
+ reject(err);
444
+ }
445
+ });
446
+ };
447
+ const kill = process.platform === "win32" ? (pid) => {
448
+ execFileSync("taskkill", windowsTaskkillArgs(pid), { stdio: "ignore" });
449
+ } : process.kill.bind(process);
450
+ const stopDeps = {
451
+ kill,
452
+ ownsProcess: (request) => {
453
+ const launched = request.spawnId ? launchedProcesses.get(request.pid) === request.spawnId : !request.id && launchedProcesses.has(request.pid);
454
+ return launched || ownsLiveProcess(request);
455
+ },
456
+ selfPid: process.pid,
457
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
458
+ isAlive: (pid) => {
459
+ try {
460
+ process.kill(pid, 0);
461
+ return true;
462
+ } catch (err) {
463
+ return err.code === "EPERM";
464
+ }
465
+ }
466
+ };
467
+ const reapSilentSessions = async () => {
468
+ if (stopped || reaping) return;
469
+ reaping = true;
470
+ try {
471
+ const now = Date.now();
472
+ const silent = silentSpawnedProofs(listProcessProofs({ reapDead: true }), now, SESSION_REAP_SILENCE_MS);
473
+ for (const proof of silent) {
474
+ if (stopped) return;
475
+ const outcome = await applyStopAndVerify(
476
+ { sessionId: proof.sessionId, pid: proof.pid, spawnId: proof.spawnId },
477
+ stopDeps
478
+ );
479
+ log(`session ${proof.sessionId} (pid ${proof.pid}) was silent for ${describeIdleWindow(now - proof.lastSeenAt)}: ${outcome}`);
480
+ }
481
+ } finally {
482
+ reaping = false;
483
+ }
484
+ };
485
+ const runStop = async (stop) => {
486
+ const outcome = stop.id ? completedStops.get(stop.id) ?? await apply() : await apply();
487
+ if (stop.id && !completedStops.has(stop.id)) {
488
+ if (completedStops.size === MAX_COMPLETED_STOPS) {
489
+ const oldest = completedStops.keys().next().value;
490
+ if (oldest) completedStops.delete(oldest);
491
+ }
492
+ completedStops.set(stop.id, outcome);
493
+ }
494
+ const said = {
495
+ stopped: `stopped session ${stop.sessionId}`,
496
+ already_gone: `session ${stop.sessionId} had already exited`,
497
+ signal_sent: `sent the stop signal to session ${stop.sessionId}; shutdown is still finishing`,
498
+ refused: `could not stop session ${stop.sessionId}, the system refused the signal`,
499
+ skipped_self: `refused to stop session ${stop.sessionId}: that pid is this daemon`
500
+ };
501
+ log(said[outcome]);
502
+ const acknowledgement = stopAcknowledgement(stop, machineId, outcome);
503
+ if (acknowledgement) {
504
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
505
+ const response = await fetch(`${baseUrl}/api/agent/stop/ack`, {
506
+ method: "POST",
507
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
508
+ body: JSON.stringify(acknowledgement),
509
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
510
+ }).catch(() => null);
511
+ if (response?.ok) {
512
+ completedStops.delete(acknowledgement.stopId);
513
+ break;
514
+ }
515
+ if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 200 * (attempt + 1)));
516
+ }
517
+ }
518
+ async function apply() {
519
+ return applyStopAndVerify(stop, stopDeps);
520
+ }
521
+ };
522
+ const acknowledgeControlProbe = async (probeId) => {
523
+ const response = await fetch(`${baseUrl}${DRAIN_PATH}`, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
526
+ body: JSON.stringify({ machineId, controlProbeAck: probeId }),
527
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
528
+ }).catch(() => null);
529
+ return response?.ok === true;
530
+ };
531
+ const drain = async (signal) => {
532
+ const controlProbe = controlProbeDue(lastControlProbeAt);
533
+ if (controlProbe) lastControlProbeAt = Date.now();
534
+ const res = await fetch(`${baseUrl}${DRAIN_PATH}`, {
535
+ method: "POST",
536
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
537
+ body: JSON.stringify({
538
+ machineId,
539
+ spawnProtocol: SPAWN_PROTOCOL_VERSION,
540
+ stopProtocol: STOP_PROTOCOL_VERSION,
541
+ controlProbe
542
+ }),
543
+ signal
544
+ });
545
+ if (!res.ok) throw new DrainError(res.status);
546
+ const data = await res.json();
547
+ const response = typeof data === "object" && data !== null ? data : {};
548
+ await Promise.all(parseStopRequests(response.stops).map(runStop));
549
+ const controlProbeId = parseControlProbe(response.controlProbe);
550
+ return {
551
+ request: parseSpawnClaim(response.spawn),
552
+ controlProbeId,
553
+ // Released servers ignore the request field and omit the response field.
554
+ // Their successful authenticated drain remains the strongest proof they expose.
555
+ legacyControlProof: controlProbe && !Object.hasOwn(response, "controlProbe")
556
+ };
557
+ };
558
+ const acknowledge = async (body) => {
559
+ if (!body) return;
560
+ let lastError;
561
+ for (let attempt = 0; attempt < 3 && !stopped; attempt++) {
562
+ const controller = new AbortController();
563
+ inflight = controller;
564
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
565
+ try {
566
+ const response = await fetch(`${baseUrl}${ACK_PATH}`, {
567
+ method: "POST",
568
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
569
+ body: JSON.stringify(body),
570
+ signal: controller.signal
571
+ });
572
+ if (response.ok) return;
573
+ const error = new Error(`spawn ${body.spawnId} acknowledgement failed (${response.status})`);
574
+ if (response.status < 500) {
575
+ lastError = error;
576
+ break;
577
+ }
578
+ lastError = error;
579
+ } catch (err) {
580
+ lastError = new Error(`spawn ${body.spawnId} acknowledgement failed: ${errMsg(err)}`);
581
+ } finally {
582
+ clearTimeout(timeout);
583
+ if (inflight === controller) inflight = void 0;
584
+ }
585
+ }
586
+ throw lastError ?? new Error(`spawn ${body.spawnId} acknowledgement stopped`);
587
+ };
588
+ const launchClaim = (request) => launchSpawnClaim(request, {
589
+ machineId,
590
+ findExistingPid: async (spawnId) => {
591
+ for (const [pid, launchedSpawnId] of launchedProcesses) {
592
+ if (launchedSpawnId === spawnId) return pid;
593
+ }
594
+ return findLiveProcessBySpawnId(spawnId);
595
+ },
596
+ launch,
597
+ acknowledge,
598
+ reportLaunchError: (message) => {
599
+ log(`spawn ${request.id}: could not launch session: ${message}`);
600
+ daemonLease.recordError(`spawn ${request.id}: ${message}`);
601
+ }
602
+ });
603
+ const nextDelay = () => {
604
+ if (errorStreak > 0) return Math.min(SLOW_POLL_MS * 2 ** (errorStreak - 1), MAX_ERROR_BACKOFF_MS);
605
+ return idleTicks >= FAST_DECAY_TICKS ? SLOW_POLL_MS : FAST_POLL_MS;
606
+ };
607
+ const jitter = (ms) => Math.max(1, Math.round(ms * (0.85 + Math.random() * 0.3)));
608
+ const tick = async () => {
609
+ if (stopped) return;
610
+ if (!daemonLease.isOwner()) {
611
+ finish();
612
+ return;
613
+ }
614
+ if (!spawnRateAllows(spawnTimestamps, Date.now())) {
615
+ if (!rateCapped) log(`spawn rate cap reached (${MAX_SPAWNS_PER_WINDOW} in ${SPAWN_WINDOW_MS / 1e3}s); leaving the queue until the window frees`);
616
+ rateCapped = true;
617
+ timer = setTimeout(tick, jitter(nextDelay()));
618
+ return;
619
+ }
620
+ rateCapped = false;
621
+ const controller = new AbortController();
622
+ inflight = controller;
623
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
624
+ try {
625
+ const drained = await drain(controller.signal);
626
+ if (!daemonLease.isOwner()) {
627
+ finish();
628
+ return;
629
+ }
630
+ errorStreak = 0;
631
+ drainHealth.recover();
632
+ if (drained.request) {
633
+ idleTicks = 0;
634
+ await launchClaim(drained.request);
635
+ } else {
636
+ idleTicks++;
637
+ }
638
+ const controlProved = drained.legacyControlProof || drained.controlProbeId !== null && await acknowledgeControlProbe(drained.controlProbeId);
639
+ if (controlProved) {
640
+ daemonLease.recordSuccessfulControl();
641
+ }
642
+ } catch (err) {
643
+ if (isAuthFailure(err)) {
644
+ const status = err.status;
645
+ writeDaemonNeedsLogin(status === 401 ? "key_rejected" : "plan_inactive");
646
+ log(
647
+ status === 401 ? "daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again." : "daemon stopping: this workspace has no active plan, so the queue is closed. Pick one at pushary.com/dashboard/settings?tab=billing"
648
+ );
649
+ finish();
650
+ return;
651
+ }
652
+ errorStreak = Math.min(errorStreak + 1, MAX_ERROR_STREAK);
653
+ drainHealth.fail(errMsg(err));
654
+ } finally {
655
+ clearTimeout(timeout);
656
+ inflight = void 0;
657
+ }
658
+ if (!stopped) timer = setTimeout(tick, jitter(nextDelay()));
659
+ };
660
+ const stopTranscripts = startRegisteredTranscripts(apiKey, machineId);
661
+ const teardown = () => {
662
+ stopTranscripts();
663
+ if (stopped) return;
664
+ stopped = true;
665
+ if (timer) clearTimeout(timer);
666
+ if (heartbeat) clearInterval(heartbeat);
667
+ if (versionWatch) clearInterval(versionWatch);
668
+ if (leaseRenewal) clearInterval(leaseRenewal);
669
+ if (sessionReaper) clearInterval(sessionReaper);
670
+ inflight?.abort();
671
+ try {
672
+ daemonLease.release();
673
+ } catch (err) {
674
+ log(`daemon could not release its local lease: ${errMsg(err)}`);
675
+ }
676
+ };
677
+ const beat = async () => {
678
+ if (stopped) return;
679
+ if (!daemonLease.isOwner()) {
680
+ finish();
681
+ return;
682
+ }
683
+ const result = await sendHeartbeat(apiKey, {
684
+ repoKey: basename(process.cwd()),
685
+ capabilities: [
686
+ ...hasSpawnProvider(providerCapabilities) ? ["spawn"] : [],
687
+ STOP_CAPABILITY,
688
+ STOP_ACK_CAPABILITY,
689
+ ...providerCapabilities
690
+ ]
691
+ });
692
+ if (result.kind === "ok") {
693
+ daemonLease.recordSuccessfulHeartbeat();
694
+ heartbeatHealth.recover();
695
+ }
696
+ if (result.kind === "failed") heartbeatHealth.fail(result.detail);
697
+ if (result.kind === "unauthorized" && !stopped) {
698
+ writeDaemonNeedsLogin("key_rejected");
699
+ log("daemon stopping: this machine's key was rejected. Run `pushary login` to sign in again.");
700
+ finish();
701
+ }
702
+ };
703
+ void beat();
704
+ heartbeat = setInterval(() => void beat(), HEARTBEAT_INTERVAL_MS);
705
+ leaseRenewal = setInterval(() => daemonLease.renew(), DAEMON_LEASE_RENEW_INTERVAL_MS);
706
+ versionWatch = setInterval(() => {
707
+ if (daemonLease.shouldYield()) finish();
708
+ }, VERSION_CHECK_INTERVAL_MS);
709
+ sessionReaper = setInterval(() => void reapSilentSessions(), SESSION_REAP_INTERVAL_MS);
710
+ log(hasSpawnProvider(providerCapabilities) ? `daemon online (pid ${process.pid}): this machine (${machineId}) can now be sent a new session from your phone. Ctrl-C to stop.` : `daemon online (pid ${process.pid}): no supported Claude, Codex, or Gemini CLI was found, so phone-start is unavailable. Ctrl-C to stop.`);
711
+ timer = setTimeout(tick, 0);
712
+ const signals = daemonSignals();
713
+ await new Promise((resolve) => {
714
+ const onSignal = () => finish();
715
+ for (const signal of signals) process.on(signal, onSignal);
716
+ finish = () => {
717
+ teardown();
718
+ for (const signal of signals) process.off(signal, onSignal);
719
+ resolve();
720
+ };
721
+ });
722
+ return 0;
723
+ };
724
+
725
+ // src/daemon-sessions.ts
726
+ import { execFileSync as execFileSync2 } from "child_process";
727
+ var readResidentSetKb = (pid) => {
728
+ if (process.platform === "win32") return null;
729
+ try {
730
+ const output = execFileSync2("ps", ["-o", "rss=", "-p", String(pid)], {
731
+ encoding: "utf8",
732
+ stdio: ["ignore", "pipe", "ignore"]
733
+ });
734
+ const kb = Number.parseInt(output.trim(), 10);
735
+ return Number.isFinite(kb) ? kb : null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ };
740
+ var formatAge = (ms) => {
741
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
742
+ const hours = Math.floor(totalSeconds / 3600);
743
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
744
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
745
+ if (minutes > 0) return `${minutes}m`;
746
+ return `${totalSeconds}s`;
747
+ };
748
+ var formatMemory = (kb) => kb === null ? "n/a" : kb >= 1024 ? `${Math.round(kb / 1024)} MB` : `${kb} KB`;
749
+ var describeSessionProof = (entry, now, rssKb) => [
750
+ entry.sessionId,
751
+ `pid ${entry.pid}`,
752
+ entry.alive ? "alive" : "dead, reaped",
753
+ `age ${formatAge(now - entry.startedAt)}`,
754
+ `seen ${formatAge(now - entry.lastSeenAt)} ago`,
755
+ `rss ${formatMemory(rssKb)}`,
756
+ ...entry.spawnId ? [`spawn ${entry.spawnId}`] : []
757
+ ].join(" ");
758
+ var describeSessionProofs = (entries, now, residentSet) => {
759
+ const live = entries.filter((entry) => entry.alive).length;
760
+ const reaped = entries.length - live;
761
+ if (entries.length === 0) return ["Sessions: none"];
762
+ const header = `Sessions: ${live} live${reaped > 0 ? `, ${reaped} dead proof${reaped === 1 ? "" : "s"} reaped` : ""}`;
763
+ return [
764
+ header,
765
+ ...entries.map((entry) => ` ${describeSessionProof(entry, now, entry.alive ? residentSet(entry.pid) : null)}`)
766
+ ];
767
+ };
768
+
769
+ // bin/pushary-daemon.ts
770
+ exitOnHelpFlag("daemon");
771
+ var serviceFailure = (result) => result.kind === "ok" ? null : result.detail;
772
+ var applyService = (action) => serviceFailure(manageDaemonService(action));
773
+ var waitForManagedDaemon = async () => {
774
+ const result = await ensureDaemonRunning({ spawnDaemon: () => {
775
+ } });
776
+ return "detail" in result ? result.detail : null;
777
+ };
778
+ var main = async () => {
779
+ const action = parseDaemonAction(process.argv.slice(2));
780
+ if (!action) {
781
+ process.stderr.write("Unknown daemon action. Use install, start, stop, restart, status, uninstall, or foreground.\n");
782
+ return EXIT.USAGE;
783
+ }
784
+ if (action === "foreground") return await runSpawnDaemon();
785
+ if (action === "status") {
786
+ const status = readDaemonStatus();
787
+ process.stdout.write(`Pushary daemon: ${status.kind}${status.pid ? ` (pid ${status.pid})` : ""}
788
+ `);
789
+ if (status.lastError) process.stdout.write(`Last error: ${status.lastError.message} at ${status.lastError.at}
790
+ `);
791
+ const sessions = describeSessionProofs(listProcessProofs({ reapDead: true }), Date.now(), readResidentSetKb);
792
+ process.stdout.write(`${sessions.join("\n")}
793
+ `);
794
+ return status.kind === "running" ? EXIT.OK : EXIT.UNREACHABLE;
795
+ }
796
+ if (action === "install" || action === "start" || action === "restart") {
797
+ if (!getApiKey()) {
798
+ process.stderr.write("[pushary] daemon service has no readable API key; re-run setup.\n");
799
+ return EXIT.FAILED;
800
+ }
801
+ const status = readDaemonStatus();
802
+ const controlPolicy = resolveControlPolicy({
803
+ mode: readControlMode() ?? (action === "install" ? "on" : null),
804
+ providerAvailable: hasSpawnProvider(detectProviderLiveCapabilities(inspectConfiguredManagedProviders())),
805
+ managerAvailable: daemonServiceManagerAvailable(),
806
+ servicePresent: status.kind !== "absent" || hasDaemonServiceDefinition()
807
+ });
808
+ if (controlPolicy !== "enabled") {
809
+ process.stderr.write(`[pushary] daemon service is ${controlPolicy}; run setup with --control on to configure phone-start safely.
810
+ `);
811
+ return EXIT.FAILED;
812
+ }
813
+ }
814
+ if (action === "install") {
815
+ const installed = await replaceDaemonService();
816
+ if (installed.kind !== "ok") {
817
+ process.stderr.write(`[pushary] daemon service install failed: ${installed.detail}
818
+ `);
819
+ return EXIT.FAILED;
820
+ }
821
+ const unhealthy = await waitForManagedDaemon();
822
+ if (unhealthy) {
823
+ process.stderr.write(`[pushary] daemon service installed but unavailable: ${unhealthy}
824
+ `);
825
+ return EXIT.UNREACHABLE;
826
+ }
827
+ process.stdout.write("[pushary] daemon service installed and online\n");
828
+ return EXIT.OK;
829
+ }
830
+ if (action === "start") {
831
+ const failed2 = applyService("start");
832
+ if (failed2) {
833
+ process.stderr.write(`[pushary] daemon service could not start: ${failed2}
834
+ `);
835
+ return EXIT.FAILED;
836
+ }
837
+ const unhealthy = await waitForManagedDaemon();
838
+ if (unhealthy) {
839
+ process.stderr.write(`[pushary] daemon service did not become healthy: ${unhealthy}
840
+ `);
841
+ return EXIT.UNREACHABLE;
842
+ }
843
+ process.stdout.write("[pushary] daemon service online\n");
844
+ return EXIT.OK;
845
+ }
846
+ const serviceAction = action === "uninstall" ? "uninstall" : "stop";
847
+ const failed = applyService(serviceAction);
848
+ const stopped = await stopDaemonRunning();
849
+ if (failed || stopped.kind === "unavailable") {
850
+ const detail = [failed, stopped.kind === "unavailable" ? stopped.detail : null].filter(Boolean).join("; ");
851
+ process.stderr.write(`[pushary] daemon could not stop: ${detail}
852
+ `);
853
+ return EXIT.FAILED;
854
+ }
855
+ if (action === "restart") {
856
+ const startFailed = applyService("start");
857
+ if (startFailed) {
858
+ process.stderr.write(`[pushary] daemon service could not restart: ${startFailed}
859
+ `);
860
+ return EXIT.FAILED;
861
+ }
862
+ const unhealthy = await waitForManagedDaemon();
863
+ if (unhealthy) {
864
+ process.stderr.write(`[pushary] daemon service did not become healthy: ${unhealthy}
865
+ `);
866
+ return EXIT.UNREACHABLE;
867
+ }
868
+ process.stdout.write("[pushary] daemon service restarted\n");
869
+ return EXIT.OK;
870
+ }
871
+ process.stdout.write(action === "uninstall" ? "[pushary] daemon service removed\n" : "[pushary] daemon stopped\n");
872
+ return EXIT.OK;
873
+ };
874
+ main().then((code) => process.exit(code)).catch((err) => {
875
+ process.stderr.write(`[pushary] daemon error: ${err instanceof Error ? err.message : String(err)}
876
+ `);
877
+ process.exit(EXIT.FAILED);
878
+ });