@pushary/agent-hooks 0.89.3 → 0.89.5

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 (58) hide show
  1. package/README.md +40 -0
  2. package/dist/bin/pushary-bell-hook.js +3 -3
  3. package/dist/bin/pushary-bell.js +9 -10
  4. package/dist/bin/pushary-claude.js +62 -26
  5. package/dist/bin/pushary-clean.js +25 -7
  6. package/dist/bin/pushary-codex-bridge.js +4 -5
  7. package/dist/bin/pushary-codex-hook.js +8 -7
  8. package/dist/bin/pushary-codex.js +6 -4
  9. package/dist/bin/pushary-connect.js +4 -4
  10. package/dist/bin/pushary-daemon.js +155 -40
  11. package/dist/bin/pushary-disconnect.js +3 -4
  12. package/dist/bin/pushary-doctor.js +77 -39
  13. package/dist/bin/pushary-elicitation-hook.js +4 -2
  14. package/dist/bin/pushary-gemini-bridge.js +3 -4
  15. package/dist/bin/pushary-gemini-hook.js +6 -4
  16. package/dist/bin/pushary-hook.js +9 -7
  17. package/dist/bin/pushary-login.js +4 -4
  18. package/dist/bin/pushary-logout.js +5 -6
  19. package/dist/bin/pushary-mode.js +2 -2
  20. package/dist/bin/pushary-notification-hook.js +6 -4
  21. package/dist/bin/pushary-opencode-hook.js +14 -6
  22. package/dist/bin/pushary-permission-denied-hook.js +7 -5
  23. package/dist/bin/pushary-permission-hook.js +7 -5
  24. package/dist/bin/pushary-post-hook.js +6 -4
  25. package/dist/bin/pushary-prompt-hook.js +6 -4
  26. package/dist/bin/pushary-session-end-hook.js +6 -4
  27. package/dist/bin/pushary-session-start-hook.js +6 -4
  28. package/dist/bin/pushary-setup.js +125 -39
  29. package/dist/bin/pushary-stats.js +2 -2
  30. package/dist/bin/pushary-status.js +7 -6
  31. package/dist/bin/pushary-stop-hook.js +6 -4
  32. package/dist/bin/pushary-stopfailure-hook.js +6 -4
  33. package/dist/bin/pushary-suggestions.js +2 -2
  34. package/dist/bin/pushary-upgrade.js +110 -46
  35. package/dist/bin/pushary-wait.js +2 -2
  36. package/dist/bin/pushary.js +1 -1
  37. package/dist/chunk-2HYPZLYY.js +45 -0
  38. package/dist/{chunk-GOEXZ5QJ.js → chunk-3XTFX4CL.js} +10 -2
  39. package/dist/{chunk-ZYPS2XMT.js → chunk-4ZHQNBDP.js} +1 -1
  40. package/dist/{chunk-HUJAAWNL.js → chunk-GKAKAOKC.js} +10 -3
  41. package/dist/{chunk-LGXTBUT4.js → chunk-KOPUTJQC.js} +1 -0
  42. package/dist/{chunk-LALHWDWL.js → chunk-L3YUHEB6.js} +16 -4
  43. package/dist/{chunk-SQOFGPNX.js → chunk-LURY4WNF.js} +1 -1
  44. package/dist/{chunk-JT6TIBYH.js → chunk-MZBT2LXW.js} +4 -4
  45. package/dist/{chunk-3UBYTSCY.js → chunk-NFI2K2ZQ.js} +15 -7
  46. package/dist/chunk-NKHEHEUM.js +56 -0
  47. package/dist/{chunk-3O2XPYLF.js → chunk-OBI4YMCT.js} +1 -1
  48. package/dist/{chunk-LUCFN47K.js → chunk-QH4AA7QV.js} +1 -1
  49. package/dist/chunk-TRQ46MXR.js +648 -0
  50. package/dist/{chunk-VKVHY5HD.js → chunk-V37ZXMH3.js} +11 -3
  51. package/dist/{chunk-MHBC2XUP.js → chunk-VP7SDXYG.js} +1 -1
  52. package/dist/{chunk-B7JE4XHD.js → chunk-WQGMUIDW.js} +1 -1
  53. package/dist/src/index.js +7 -5
  54. package/package.json +1 -1
  55. package/dist/chunk-GIRT7677.js +0 -11
  56. package/dist/chunk-K42SZKYF.js +0 -343
  57. package/dist/chunk-NRGRUOAR.js +0 -25
  58. package/dist/chunk-RN3NOEJF.js +0 -9
@@ -0,0 +1,648 @@
1
+ import {
2
+ needsShell,
3
+ spawnClaude
4
+ } from "./chunk-XHKBHWLX.js";
5
+ import {
6
+ resolveGlobalBinary
7
+ } from "./chunk-PZA7G4CN.js";
8
+ import {
9
+ getMachineId
10
+ } from "./chunk-2HYPZLYY.js";
11
+ import {
12
+ pusharyDir
13
+ } from "./chunk-V37ZXMH3.js";
14
+ import {
15
+ getPackageVersion
16
+ } from "./chunk-DINDL2CF.js";
17
+ import {
18
+ readJsonSafe,
19
+ writeFileAtomic,
20
+ writeJsonAtomic
21
+ } from "./chunk-6MTNS63X.js";
22
+ import {
23
+ getApiKey
24
+ } from "./chunk-2UMNXADU.js";
25
+
26
+ // src/daemon-lifecycle.ts
27
+ import { dirname, join, win32 as win32Path } from "path";
28
+ import { existsSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
29
+ import { createHash, randomUUID } from "crypto";
30
+ import { homedir } from "os";
31
+ import { execFileSync } from "child_process";
32
+ var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
33
+ var daemonServiceManagerAvailable = (options = {}) => {
34
+ const platform = options.platform ?? process.platform;
35
+ const run = options.run ?? ((command, args) => {
36
+ execFileSync(command, [...args], { stdio: "ignore", windowsHide: true });
37
+ });
38
+ try {
39
+ if (platform === "darwin") {
40
+ const uid = options.uid ?? process.getuid?.();
41
+ if (uid === void 0) return false;
42
+ run("launchctl", ["print", `gui/${uid}`]);
43
+ return true;
44
+ }
45
+ if (platform === "linux") {
46
+ run("systemctl", ["--user", "show-environment"]);
47
+ return true;
48
+ }
49
+ if (platform === "win32") {
50
+ run("sc.exe", ["query", "Schedule"]);
51
+ return true;
52
+ }
53
+ return false;
54
+ } catch {
55
+ return false;
56
+ }
57
+ };
58
+ var statePath = (dir) => join(dir, "daemon-state.json");
59
+ var lockPath = (dir) => join(dir, "daemon.lock");
60
+ var targetPath = (dir) => join(dir, "daemon-target.json");
61
+ var DAEMON_LEASE_RENEW_INTERVAL_MS = 3e4;
62
+ var LOCAL_LEASE_TIMEOUT_MS = DAEMON_LEASE_RENEW_INTERVAL_MS * 3;
63
+ var buildDaemonLaunch = (input) => input.siblingEntry ? { command: input.execPath, args: [input.siblingEntry], shell: false } : {
64
+ command: input.daemonBinary,
65
+ args: [],
66
+ shell: needsShell(input.daemonBinary, input.platform ?? process.platform)
67
+ };
68
+ var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
69
+ var systemdQuote = (value) => `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
70
+ var buildDaemonService = (input) => {
71
+ if (input.platform === "darwin") {
72
+ if (!Number.isInteger(input.uid) || input.uid < 0) return null;
73
+ const label = "com.pushary.daemon";
74
+ const domain = `gui/${input.uid}`;
75
+ const target = `${domain}/${label}`;
76
+ const definitionPath = join(input.homeDir, "Library", "LaunchAgents", `${label}.plist`);
77
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
78
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
79
+ <plist version="1.0">
80
+ <dict>
81
+ <key>Label</key><string>${label}</string>
82
+ <key>ProgramArguments</key>
83
+ <array><string>${xml(input.execPath)}</string><string>${xml(input.daemonEntry)}</string></array>
84
+ <key>WorkingDirectory</key><string>${xml(input.homeDir)}</string>
85
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(input.pathEnv)}</string></dict>
86
+ <key>RunAtLoad</key><true/>
87
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
88
+ <key>ThrottleInterval</key><integer>10</integer>
89
+ <key>StandardOutPath</key><string>${xml(join(input.stateDir, "daemon.log"))}</string>
90
+ <key>StandardErrorPath</key><string>${xml(join(input.stateDir, "daemon-error.log"))}</string>
91
+ </dict>
92
+ </plist>
93
+ `;
94
+ const bootstrap = { command: "launchctl", args: ["bootstrap", domain, definitionPath] };
95
+ return {
96
+ definitionPath,
97
+ contents,
98
+ install: [
99
+ { command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true },
100
+ bootstrap,
101
+ { command: "launchctl", args: ["enable", target] },
102
+ { command: "launchctl", args: ["kickstart", "-k", target] }
103
+ ],
104
+ start: [{ command: "launchctl", args: ["kickstart", "-k", target] }],
105
+ stop: [{ command: "launchctl", args: ["kill", "SIGTERM", target], ignoreFailure: true }],
106
+ uninstall: [{ command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true }],
107
+ registrationProbe: { command: "launchctl", args: ["print", target] }
108
+ };
109
+ }
110
+ if (input.platform === "linux") {
111
+ const unit = "pushary-daemon.service";
112
+ const definitionPath = join(input.homeDir, ".config", "systemd", "user", unit);
113
+ const contents = `[Unit]
114
+ Description=Pushary phone-to-agent daemon
115
+ After=network-online.target
116
+
117
+ [Service]
118
+ Type=simple
119
+ ExecStart=${systemdQuote(input.execPath)} ${systemdQuote(input.daemonEntry)}
120
+ WorkingDirectory=${systemdQuote(input.homeDir)}
121
+ Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
122
+ Restart=on-failure
123
+ RestartSec=10
124
+
125
+ [Install]
126
+ WantedBy=default.target
127
+ `;
128
+ return {
129
+ definitionPath,
130
+ contents,
131
+ install: [
132
+ { command: "systemctl", args: ["--user", "daemon-reload"] },
133
+ { command: "systemctl", args: ["--user", "enable", "--now", unit] }
134
+ ],
135
+ start: [{ command: "systemctl", args: ["--user", "start", unit] }],
136
+ stop: [{ command: "systemctl", args: ["--user", "stop", unit], ignoreFailure: true }],
137
+ uninstall: [
138
+ { command: "systemctl", args: ["--user", "disable", "--now", unit], ignoreFailure: true }
139
+ ],
140
+ registrationProbe: { command: "systemctl", args: ["--user", "is-enabled", unit] },
141
+ reloadAfterUninstall: { command: "systemctl", args: ["--user", "daemon-reload"] }
142
+ };
143
+ }
144
+ if (input.platform === "win32") {
145
+ const taskName = "Pushary Daemon";
146
+ const definitionPath = win32Path.join(input.stateDir, "pushary-daemon-task.xml");
147
+ const contents = `<?xml version="1.0" encoding="UTF-8"?>
148
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
149
+ <RegistrationInfo><Description>Pushary phone-to-agent daemon</Description></RegistrationInfo>
150
+ <Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
151
+ <Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
152
+ <Settings>
153
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
154
+ <StartWhenAvailable>true</StartWhenAvailable>
155
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
156
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
157
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
158
+ <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>
159
+ </Settings>
160
+ <Actions Context="Author">
161
+ <Exec>
162
+ <Command>${xml(input.execPath)}</Command>
163
+ <Arguments>&quot;${xml(input.daemonEntry)}&quot;</Arguments>
164
+ <WorkingDirectory>${xml(input.homeDir)}</WorkingDirectory>
165
+ </Exec>
166
+ </Actions>
167
+ </Task>
168
+ `;
169
+ return {
170
+ definitionPath,
171
+ contents,
172
+ install: [
173
+ { command: "schtasks.exe", args: ["/Create", "/TN", taskName, "/XML", definitionPath, "/F"] },
174
+ { command: "schtasks.exe", args: ["/Run", "/TN", taskName] }
175
+ ],
176
+ start: [{ command: "schtasks.exe", args: ["/Run", "/TN", taskName] }],
177
+ stop: [{ command: "schtasks.exe", args: ["/End", "/TN", taskName], ignoreFailure: true }],
178
+ uninstall: [{ command: "schtasks.exe", args: ["/Delete", "/TN", taskName, "/F"], ignoreFailure: true }],
179
+ registrationProbe: { command: "schtasks.exe", args: ["/Query", "/TN", taskName] }
180
+ };
181
+ }
182
+ return null;
183
+ };
184
+ var resolveDaemonServiceDefinition = (options = {}) => {
185
+ const platform = options.platform ?? process.platform;
186
+ const exists = options.entryExists ?? existsSync;
187
+ const globalBinary = options.globalBinary ?? resolveGlobalBinary("pushary-daemon");
188
+ let daemonEntry = options.daemonEntry;
189
+ if (!daemonEntry && globalBinary) {
190
+ if (platform === "win32") {
191
+ const globalRoot = win32Path.dirname(globalBinary);
192
+ const candidate = win32Path.join(
193
+ globalRoot,
194
+ "node_modules",
195
+ "@pushary",
196
+ "agent-hooks",
197
+ "dist",
198
+ "bin",
199
+ "pushary-daemon.js"
200
+ );
201
+ if (exists(candidate)) daemonEntry = candidate;
202
+ } else {
203
+ try {
204
+ const candidate = (options.realpath ?? realpathSync)(globalBinary);
205
+ if (exists(candidate)) daemonEntry = candidate;
206
+ } catch {
207
+ }
208
+ }
209
+ }
210
+ if (!daemonEntry || !exists(daemonEntry)) return null;
211
+ const homeDir = options.homeDir ?? homedir();
212
+ return buildDaemonService({
213
+ platform,
214
+ homeDir,
215
+ stateDir: options.stateDir ?? pusharyDir(),
216
+ execPath: options.execPath ?? process.execPath,
217
+ daemonEntry,
218
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
219
+ uid: options.uid ?? process.getuid?.()
220
+ });
221
+ };
222
+ var applyDaemonService = (action, definition, options = {}) => {
223
+ const run = options.run ?? ((command, args) => {
224
+ execFileSync(command, [...args], { stdio: "ignore", windowsHide: true });
225
+ });
226
+ try {
227
+ if (action === "install") writeFileAtomic(definition.definitionPath, definition.contents, 384);
228
+ const steps = action === "uninstall" ? [...definition.stop, ...definition.uninstall] : definition[action];
229
+ for (const step of steps) {
230
+ try {
231
+ run(step.command, step.args);
232
+ } catch (err) {
233
+ if (!step.ignoreFailure) throw err;
234
+ }
235
+ }
236
+ if (action === "uninstall") {
237
+ let registered = false;
238
+ try {
239
+ run(definition.registrationProbe.command, definition.registrationProbe.args);
240
+ registered = true;
241
+ } catch {
242
+ }
243
+ if (registered) throw new Error("Native background service remains registered");
244
+ safeUnlink(definition.definitionPath);
245
+ const reload = definition.reloadAfterUninstall;
246
+ if (reload) run(reload.command, reload.args);
247
+ }
248
+ return { kind: "ok" };
249
+ } catch (err) {
250
+ return { kind: "failed", detail: err instanceof Error ? err.message : String(err) };
251
+ }
252
+ };
253
+ var DAEMON_ACTIONS = /* @__PURE__ */ new Set([
254
+ "install",
255
+ "start",
256
+ "stop",
257
+ "restart",
258
+ "status",
259
+ "uninstall",
260
+ "foreground"
261
+ ]);
262
+ var parseDaemonAction = (argv) => {
263
+ const args = argv[0] === "daemon" ? argv.slice(1) : argv;
264
+ const action = args[0] ?? "foreground";
265
+ return DAEMON_ACTIONS.has(action) ? action : null;
266
+ };
267
+ var manageDaemonService = (action, options = {}) => {
268
+ const definition = action === "uninstall" ? buildDaemonService({
269
+ platform: options.platform ?? process.platform,
270
+ homeDir: options.homeDir ?? homedir(),
271
+ stateDir: options.stateDir ?? pusharyDir(),
272
+ execPath: options.execPath ?? process.execPath,
273
+ daemonEntry: options.daemonEntry ?? process.execPath,
274
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
275
+ uid: options.uid ?? process.getuid?.()
276
+ }) : resolveDaemonServiceDefinition(options);
277
+ if (!definition) {
278
+ return {
279
+ kind: "unavailable",
280
+ detail: "Could not find the globally installed daemon entry. Run Pushary setup again."
281
+ };
282
+ }
283
+ if (action === "uninstall" && !daemonServiceManagerAvailable(options)) {
284
+ return { kind: "failed", detail: "Native background-service manager is unavailable" };
285
+ }
286
+ return applyDaemonService(action, definition, { run: options.run });
287
+ };
288
+ var hasDaemonServiceDefinition = (options = {}) => {
289
+ const definition = buildDaemonService({
290
+ platform: options.platform ?? process.platform,
291
+ homeDir: options.homeDir ?? homedir(),
292
+ stateDir: options.stateDir ?? pusharyDir(),
293
+ execPath: options.execPath ?? process.execPath,
294
+ daemonEntry: options.daemonEntry ?? process.execPath,
295
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
296
+ uid: options.uid ?? process.getuid?.()
297
+ });
298
+ return definition !== null && existsSync(definition.definitionPath);
299
+ };
300
+ var spawnDetachedDaemon = (plan, options = {}) => {
301
+ const spawnImpl = options.spawnImpl ?? spawnClaude;
302
+ const child = spawnImpl(plan.command, plan.args, {
303
+ detached: true,
304
+ stdio: "ignore",
305
+ windowsHide: true,
306
+ shell: plan.shell
307
+ });
308
+ child.once?.("error", () => {
309
+ });
310
+ child.unref();
311
+ return typeof child.pid === "number" ? child.pid : null;
312
+ };
313
+ var processIsAlive = (pid) => {
314
+ try {
315
+ process.kill(pid, 0);
316
+ return true;
317
+ } catch (err) {
318
+ return err.code === "EPERM";
319
+ }
320
+ };
321
+ var isString = (value) => typeof value === "string" && value.length > 0;
322
+ var isTimestamp = (value) => isString(value) && Number.isFinite(Date.parse(value));
323
+ var currentConfigurationId = () => {
324
+ try {
325
+ return createHash("sha256").update(getApiKey()).digest("hex").slice(0, 16);
326
+ } catch {
327
+ return null;
328
+ }
329
+ };
330
+ var parseDaemonState = (value) => {
331
+ if (typeof value !== "object" || value === null) return null;
332
+ const state = value;
333
+ if (!isString(state.instanceId) || !Number.isInteger(state.pid) || state.pid <= 0 || !isString(state.version) || !(state.configurationId === void 0 || state.configurationId === null || isString(state.configurationId)) || !isString(state.machineId) || !isTimestamp(state.startedAt) || !isTimestamp(state.updatedAt) || !(state.lastSuccessfulHeartbeatAt === null || isTimestamp(state.lastSuccessfulHeartbeatAt))) return null;
334
+ return {
335
+ instanceId: state.instanceId,
336
+ pid: state.pid,
337
+ version: state.version,
338
+ configurationId: state.configurationId ?? null,
339
+ machineId: state.machineId,
340
+ startedAt: state.startedAt,
341
+ updatedAt: state.updatedAt,
342
+ lastSuccessfulHeartbeatAt: state.lastSuccessfulHeartbeatAt
343
+ };
344
+ };
345
+ var parseLockOwner = (value) => {
346
+ if (typeof value !== "object" || value === null) return null;
347
+ const owner = value;
348
+ return isString(owner.instanceId) && Number.isInteger(owner.pid) && owner.pid > 0 ? { instanceId: owner.instanceId, pid: owner.pid } : null;
349
+ };
350
+ var readDaemonStatus = (options = {}) => {
351
+ const dir = options.dir ?? pusharyDir();
352
+ const installedVersion = options.installedVersion ?? getPackageVersion();
353
+ const state = readJsonSafe(statePath(dir));
354
+ if (state.kind === "missing") {
355
+ return {
356
+ kind: "absent",
357
+ installedVersion,
358
+ machineId: null,
359
+ pid: null,
360
+ processAlive: null,
361
+ daemonVersion: null,
362
+ startedAt: null,
363
+ updatedAt: null,
364
+ lastSuccessfulHeartbeatAt: null
365
+ };
366
+ }
367
+ const value = state.kind === "ok" ? parseDaemonState(state.value) : null;
368
+ if (value) {
369
+ const alive = (options.isProcessAlive ?? processIsAlive)(value.pid);
370
+ const fresh = (options.now ?? Date.now)() - Date.parse(value.updatedAt) <= LOCAL_LEASE_TIMEOUT_MS;
371
+ const expectedConfigurationId = options.configurationId === void 0 ? currentConfigurationId() : options.configurationId;
372
+ return {
373
+ kind: !alive || !fresh ? "unresponsive" : value.version !== installedVersion ? "version-mismatch" : expectedConfigurationId !== null && value.configurationId !== null && value.configurationId !== expectedConfigurationId ? "configuration-mismatch" : "running",
374
+ installedVersion,
375
+ machineId: value.machineId,
376
+ pid: value.pid,
377
+ processAlive: alive,
378
+ daemonVersion: value.version,
379
+ startedAt: value.startedAt,
380
+ updatedAt: value.updatedAt,
381
+ lastSuccessfulHeartbeatAt: value.lastSuccessfulHeartbeatAt
382
+ };
383
+ }
384
+ return {
385
+ kind: "invalid",
386
+ installedVersion,
387
+ machineId: null,
388
+ pid: null,
389
+ processAlive: null,
390
+ daemonVersion: null,
391
+ startedAt: null,
392
+ updatedAt: null,
393
+ lastSuccessfulHeartbeatAt: null
394
+ };
395
+ };
396
+ var safeUnlink = (path) => {
397
+ try {
398
+ unlinkSync(path);
399
+ } catch (err) {
400
+ if (err.code !== "ENOENT") throw err;
401
+ }
402
+ };
403
+ var requestDaemonVersion = (version, options = {}) => {
404
+ const dir = options.dir ?? pusharyDir();
405
+ const now = options.now ?? Date.now;
406
+ writeJsonAtomic(targetPath(dir), { version, requestedAt: new Date(now()).toISOString() }, 384);
407
+ };
408
+ var acquireDaemonLease = (options = {}) => {
409
+ const dir = options.dir ?? pusharyDir();
410
+ const installedVersion = options.installedVersion ?? getPackageVersion();
411
+ const machineId = options.machineId ?? getMachineId();
412
+ const pid = options.pid ?? process.pid;
413
+ const now = options.now ?? Date.now;
414
+ const instanceId = randomUUID();
415
+ const startedAt = new Date(now()).toISOString();
416
+ const writeLock = () => {
417
+ mkdirSync(dir, { recursive: true });
418
+ writeFileSync(lockPath(dir), JSON.stringify({ instanceId, pid }), { flag: "wx", mode: 384 });
419
+ };
420
+ try {
421
+ writeLock();
422
+ } catch (err) {
423
+ if (err.code !== "EEXIST") {
424
+ return { kind: "unavailable", detail: err instanceof Error ? err.message : String(err) };
425
+ }
426
+ const status = readDaemonStatus(options);
427
+ if (status.kind === "running" || status.kind === "version-mismatch" || status.kind === "configuration-mismatch") {
428
+ return { kind: "already-running", status };
429
+ }
430
+ const lock = readJsonSafe(lockPath(dir));
431
+ const owner = lock.kind === "ok" ? parseLockOwner(lock.value) : null;
432
+ if (!owner) return { kind: "unavailable", detail: "daemon lock is invalid" };
433
+ if ((options.isProcessAlive ?? processIsAlive)(owner.pid)) {
434
+ return { kind: "unavailable", detail: "daemon owner is still running or starting" };
435
+ }
436
+ const stale = join(dir, `daemon.lock.${instanceId}.stale`);
437
+ let ownsReplacement = false;
438
+ try {
439
+ renameSync(lockPath(dir), stale);
440
+ writeLock();
441
+ ownsReplacement = true;
442
+ safeUnlink(statePath(dir));
443
+ safeUnlink(stale);
444
+ } catch (retryError) {
445
+ if (ownsReplacement) safeUnlink(lockPath(dir));
446
+ try {
447
+ safeUnlink(stale);
448
+ } catch {
449
+ }
450
+ return { kind: "unavailable", detail: retryError instanceof Error ? retryError.message : String(retryError) };
451
+ }
452
+ }
453
+ const state = {
454
+ instanceId,
455
+ pid,
456
+ version: installedVersion,
457
+ configurationId: options.configurationId === void 0 ? currentConfigurationId() : options.configurationId,
458
+ machineId,
459
+ startedAt,
460
+ updatedAt: startedAt,
461
+ lastSuccessfulHeartbeatAt: null
462
+ };
463
+ try {
464
+ writeJsonAtomic(statePath(dir), state, 384);
465
+ } catch (err) {
466
+ safeUnlink(lockPath(dir));
467
+ return { kind: "unavailable", detail: err instanceof Error ? err.message : String(err) };
468
+ }
469
+ let currentState = state;
470
+ const isOwner = () => {
471
+ const owner = readJsonSafe(lockPath(dir));
472
+ return owner.kind === "ok" && owner.value.instanceId === instanceId;
473
+ };
474
+ const updateState = (next) => {
475
+ if (!isOwner()) return;
476
+ try {
477
+ writeJsonAtomic(statePath(dir), next, 384);
478
+ currentState = next;
479
+ } catch {
480
+ }
481
+ };
482
+ return {
483
+ kind: "acquired",
484
+ lease: {
485
+ isOwner,
486
+ renew: (at = now()) => {
487
+ updateState({ ...currentState, updatedAt: new Date(at).toISOString() });
488
+ },
489
+ recordSuccessfulHeartbeat: (at = now()) => {
490
+ const timestamp = new Date(at).toISOString();
491
+ updateState({
492
+ ...currentState,
493
+ updatedAt: timestamp,
494
+ lastSuccessfulHeartbeatAt: timestamp
495
+ });
496
+ },
497
+ shouldYield: () => {
498
+ if (!isOwner()) return true;
499
+ const target = readJsonSafe(targetPath(dir));
500
+ return target.kind === "ok" && (typeof target.value.version === "string" && target.value.version !== installedVersion || isTimestamp(target.value.requestedAt) && Date.parse(target.value.requestedAt) > Date.parse(startedAt));
501
+ },
502
+ release: () => {
503
+ if (!isOwner()) return;
504
+ safeUnlink(statePath(dir));
505
+ safeUnlink(lockPath(dir));
506
+ }
507
+ }
508
+ };
509
+ };
510
+ var hasConfiguration = () => {
511
+ try {
512
+ getApiKey();
513
+ return true;
514
+ } catch {
515
+ return false;
516
+ }
517
+ };
518
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
519
+ var requestDaemonHandoff = async (initial, options = {}) => {
520
+ const now = options.now ?? Date.now;
521
+ const requestedAt = Math.max(
522
+ now(),
523
+ initial.startedAt === null ? 0 : Date.parse(initial.startedAt) + 1
524
+ );
525
+ requestDaemonVersion(options.installedVersion ?? initial.installedVersion, {
526
+ dir: options.dir,
527
+ now: () => requestedAt
528
+ });
529
+ if (initial.pid === null || initial.processAlive !== true) return initial;
530
+ const sleep = options.sleep ?? wait;
531
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
532
+ const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
533
+ let status = initial;
534
+ for (let attempt = 0; attempt < attempts; attempt++) {
535
+ await sleep(pollIntervalMs);
536
+ status = readDaemonStatus(options);
537
+ if (status.pid !== initial.pid || status.processAlive !== true) break;
538
+ }
539
+ return status;
540
+ };
541
+ var stopDaemonRunning = async (options = {}) => {
542
+ const dir = options.dir ?? pusharyDir();
543
+ const initial = readDaemonStatus(options);
544
+ if (initial.pid === null || initial.processAlive !== true) {
545
+ safeUnlink(statePath(dir));
546
+ safeUnlink(lockPath(dir));
547
+ return { kind: "absent" };
548
+ }
549
+ let status;
550
+ try {
551
+ status = await requestDaemonHandoff(initial, options);
552
+ } catch (err) {
553
+ return { kind: "unavailable", detail: err instanceof Error ? err.message : String(err) };
554
+ }
555
+ if (status.pid === initial.pid && status.processAlive === true) {
556
+ return { kind: "unavailable", detail: "daemon did not stop" };
557
+ }
558
+ safeUnlink(statePath(dir));
559
+ safeUnlink(lockPath(dir));
560
+ return { kind: "stopped" };
561
+ };
562
+ var disableDaemonService = async (options = {}) => {
563
+ const platform = options.platform ?? process.platform;
564
+ const supported = ["darwin", "linux", "win32"].includes(platform);
565
+ const managerAvailable = supported && daemonServiceManagerAvailable(options);
566
+ const definitionPresent = supported && hasDaemonServiceDefinition(options);
567
+ const service = definitionPresent && !managerAvailable ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService("uninstall", options) : { kind: "ok" };
568
+ const stopped = await stopDaemonRunning(options);
569
+ const failures = [
570
+ service.kind === "ok" ? null : service.detail,
571
+ stopped.kind === "unavailable" ? stopped.detail : null
572
+ ].filter((detail) => detail !== null);
573
+ return failures.length > 0 ? { kind: "failed", detail: failures.join("; ") } : { kind: "ok" };
574
+ };
575
+ var replaceDaemonService = async (options = {}) => {
576
+ const stopped = await stopDaemonRunning(options);
577
+ return stopped.kind === "unavailable" ? stopped : manageDaemonService("install", options);
578
+ };
579
+ var launchInstalledDaemon = (options) => {
580
+ const entryExists = options.entryExists ?? existsSync;
581
+ const argvEntry = process.argv[1];
582
+ const candidate = options.daemonEntry !== void 0 ? options.daemonEntry : argvEntry ? join(dirname(argvEntry), "pushary-daemon.js") : null;
583
+ const siblingEntry = candidate && entryExists(candidate) ? candidate : null;
584
+ const platform = options.platform ?? process.platform;
585
+ const daemonBinary = options.daemonBinary ?? resolveGlobalBinary("pushary-daemon") ?? (platform === "win32" ? "pushary-daemon.cmd" : "pushary-daemon");
586
+ const plan = buildDaemonLaunch({
587
+ siblingEntry,
588
+ execPath: options.execPath ?? process.execPath,
589
+ daemonBinary,
590
+ platform
591
+ });
592
+ spawnDetachedDaemon(plan, { spawnImpl: options.spawnImpl });
593
+ };
594
+ var ensureDaemonRunning = async (options = {}) => {
595
+ const initial = readDaemonStatus(options);
596
+ if (!(options.configured ?? hasConfiguration)()) {
597
+ return { kind: "not-configured", status: initial, detail: "Pushary is not configured on this machine" };
598
+ }
599
+ if (initial.kind === "running") return { kind: "running", status: initial };
600
+ let handoffStatus;
601
+ try {
602
+ handoffStatus = await requestDaemonHandoff(initial, options);
603
+ } catch (err) {
604
+ return {
605
+ kind: "unavailable",
606
+ status: initial,
607
+ detail: err instanceof Error ? err.message : String(err)
608
+ };
609
+ }
610
+ const sleep = options.sleep ?? wait;
611
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
612
+ const attempts = Math.max(1, Math.ceil((options.waitTimeoutMs ?? 5e3) / pollIntervalMs));
613
+ if (handoffStatus.kind === "running") return { kind: "running", status: handoffStatus };
614
+ if (daemonNeedsHandoff(handoffStatus)) {
615
+ return { kind: "unavailable", status: handoffStatus, detail: "unhealthy daemon did not yield" };
616
+ }
617
+ try {
618
+ (options.spawnDaemon ?? (() => launchInstalledDaemon(options)))();
619
+ } catch (err) {
620
+ return {
621
+ kind: "unavailable",
622
+ status: initial,
623
+ detail: err instanceof Error ? err.message : String(err)
624
+ };
625
+ }
626
+ let status = initial;
627
+ for (let attempt = 0; attempt < attempts; attempt++) {
628
+ status = readDaemonStatus(options);
629
+ if (status.kind === "running") return { kind: "started", status };
630
+ await sleep(pollIntervalMs);
631
+ }
632
+ return { kind: "unavailable", status, detail: `daemon did not become healthy (${status.kind})` };
633
+ };
634
+
635
+ export {
636
+ daemonServiceManagerAvailable,
637
+ DAEMON_LEASE_RENEW_INTERVAL_MS,
638
+ parseDaemonAction,
639
+ manageDaemonService,
640
+ hasDaemonServiceDefinition,
641
+ readDaemonStatus,
642
+ acquireDaemonLease,
643
+ requestDaemonHandoff,
644
+ stopDaemonRunning,
645
+ disableDaemonService,
646
+ replaceDaemonService,
647
+ ensureDaemonRunning
648
+ };
@@ -1,6 +1,3 @@
1
- import {
2
- canonicalJson
3
- } from "./chunk-GIRT7677.js";
4
1
  import {
5
2
  HOOK_BUDGETS,
6
3
  HOOK_LOCATOR_BINARY
@@ -10,6 +7,16 @@ import {
10
7
  import { createHash } from "crypto";
11
8
  import { homedir } from "os";
12
9
  import { join } from "path";
10
+
11
+ // src/canonical-json.ts
12
+ var canonicalJson = (value) => {
13
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
14
+ if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
15
+ const record = value;
16
+ return "{" + Object.keys(record).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(record[key])).join(",") + "}";
17
+ };
18
+
19
+ // src/codex-config.ts
13
20
  var CODEX_HOOK_BINARY = "pushary-codex-hook";
14
21
  var codexHome = () => codexHomeFrom(homedir());
15
22
  var codexHomeFrom = (home) => process.env.CODEX_HOME?.trim() || join(home, ".codex");
@@ -263,6 +270,7 @@ var geminiMd = () => join2(homedir2(), ".gemini", "GEMINI.md");
263
270
  var pusharyConfigFile = () => join2(homedir2(), ".pushary", "config.json");
264
271
 
265
272
  export {
273
+ canonicalJson,
266
274
  CODEX_HOOK_BINARY,
267
275
  codexHome,
268
276
  codexHomeFrom,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMachineId
3
- } from "./chunk-RN3NOEJF.js";
3
+ } from "./chunk-2HYPZLYY.js";
4
4
  import {
5
5
  getPackageVersion
6
6
  } from "./chunk-DINDL2CF.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-LALHWDWL.js";
3
+ } from "./chunk-L3YUHEB6.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";