@pushary/agent-hooks 0.89.6 → 0.89.8

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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/bin/pushary-bell-hook.js +3 -3
  3. package/dist/bin/pushary-bell.js +4 -4
  4. package/dist/bin/pushary-claude.js +6 -6
  5. package/dist/bin/pushary-clean.js +138 -31
  6. package/dist/bin/pushary-codex-bridge.js +4 -4
  7. package/dist/bin/pushary-codex-hook.js +4 -4
  8. package/dist/bin/pushary-codex.js +4 -4
  9. package/dist/bin/pushary-connect.js +2 -2
  10. package/dist/bin/pushary-daemon.js +11 -11
  11. package/dist/bin/pushary-disconnect.js +2 -2
  12. package/dist/bin/pushary-doctor.js +8 -8
  13. package/dist/bin/pushary-elicitation-hook.js +3 -3
  14. package/dist/bin/pushary-gemini-bridge.js +4 -4
  15. package/dist/bin/pushary-gemini-hook.js +4 -4
  16. package/dist/bin/pushary-hook.js +5 -5
  17. package/dist/bin/pushary-login.js +2 -2
  18. package/dist/bin/pushary-logout.js +3 -3
  19. package/dist/bin/pushary-notification-hook.js +4 -4
  20. package/dist/bin/pushary-opencode-hook.js +4 -4
  21. package/dist/bin/pushary-permission-denied-hook.js +5 -5
  22. package/dist/bin/pushary-permission-hook.js +5 -5
  23. package/dist/bin/pushary-post-hook.js +4 -4
  24. package/dist/bin/pushary-prompt-hook.js +4 -4
  25. package/dist/bin/pushary-session-end-hook.js +4 -4
  26. package/dist/bin/pushary-session-start-hook.js +4 -4
  27. package/dist/bin/pushary-setup.js +10 -10
  28. package/dist/bin/pushary-status.js +5 -5
  29. package/dist/bin/pushary-stop-hook.js +4 -4
  30. package/dist/bin/pushary-stopfailure-hook.js +4 -4
  31. package/dist/bin/pushary-upgrade.js +9 -9
  32. package/dist/{chunk-NFI2K2ZQ.js → chunk-4XZCDSNG.js} +1 -1
  33. package/dist/{chunk-QH4AA7QV.js → chunk-A4O5ROZK.js} +1 -1
  34. package/dist/{chunk-OBI4YMCT.js → chunk-BAE45YLP.js} +2 -2
  35. package/dist/{chunk-2HYPZLYY.js → chunk-DKDDLF5P.js} +2 -2
  36. package/dist/{chunk-5TASNKRX.js → chunk-EA3E74P2.js} +5 -5
  37. package/dist/{chunk-MZBT2LXW.js → chunk-EJT2ER6G.js} +2 -2
  38. package/dist/{chunk-WQGMUIDW.js → chunk-FX5QSMPY.js} +1 -1
  39. package/dist/{chunk-V37ZXMH3.js → chunk-GMI5QZLG.js} +15 -9
  40. package/dist/{chunk-475T3XIO.js → chunk-J2JZJZKB.js} +1 -1
  41. package/dist/{chunk-NQNCSPEH.js → chunk-R2AGKCXL.js} +350 -27
  42. package/dist/{chunk-4ZHQNBDP.js → chunk-TF247BET.js} +2 -2
  43. package/dist/{chunk-L3YUHEB6.js → chunk-V6N4Q223.js} +1 -1
  44. package/dist/{chunk-7TS2G4QG.js → chunk-VA7VUNRD.js} +22 -14
  45. package/dist/{chunk-6MTNS63X.js → chunk-XX2V4QS5.js} +12 -1
  46. package/dist/{chunk-VP7SDXYG.js → chunk-ZL7HEF2T.js} +1 -1
  47. package/dist/src/index.js +5 -5
  48. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFileKey
3
- } from "./chunk-L3YUHEB6.js";
3
+ } from "./chunk-V6N4Q223.js";
4
4
 
5
5
  // src/bell/fleet.ts
6
6
  import { closeSync, constants, futimesSync, lstatSync, mkdirSync, openSync, readdirSync, unlinkSync } from "fs";
@@ -8,6 +8,10 @@ import { createHash } from "crypto";
8
8
  import { homedir } from "os";
9
9
  import { join } from "path";
10
10
 
11
+ // src/removal-scope.ts
12
+ var namesAppLocator = (command) => command.includes(HOOK_LOCATOR_BINARY);
13
+ var isProtectedByScope = (command, scope) => scope?.keepAppWiring === true && namesAppLocator(command);
14
+
11
15
  // src/canonical-json.ts
12
16
  var canonicalJson = (value) => {
13
17
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -77,13 +81,14 @@ var readCodexMcpAuth = (config) => {
77
81
  if (typeof pushary.bearer_token === "string") return { method: "legacy-bearer" };
78
82
  return void 0;
79
83
  };
80
- var isPusharyCommand = (hook) => {
84
+ var isPusharyCommand = (hook, scope) => {
81
85
  const command = String(asRecord(hook)?.command ?? "");
86
+ if (isProtectedByScope(command, scope)) return false;
82
87
  return command.includes(CODEX_HOOK_BINARY) || command.includes(HOOK_LOCATOR_BINARY);
83
88
  };
84
- var isPusharyCodexHook = (entry) => {
89
+ var isPusharyCodexHook = (entry, scope) => {
85
90
  const hooks = asRecord(entry)?.hooks;
86
- return Array.isArray(hooks) && hooks.some(isPusharyCommand);
91
+ return Array.isArray(hooks) && hooks.some((hook) => isPusharyCommand(hook, scope));
87
92
  };
88
93
  var addCodexHooks = (config, command) => {
89
94
  const hooks = ensureRecord(config, "hooks");
@@ -102,14 +107,14 @@ var addCodexHooks = (config, command) => {
102
107
  hooks[definition.event] = entries;
103
108
  }
104
109
  };
105
- var removeCodexHooks = (config) => {
110
+ var removeCodexHooks = (config, scope) => {
106
111
  const hooks = asRecord(config.hooks);
107
112
  if (!hooks) return false;
108
113
  let changed = false;
109
114
  for (const definition of CODEX_HOOK_EVENTS) {
110
115
  const entries = hooks[definition.event];
111
116
  if (!Array.isArray(entries)) continue;
112
- const filtered = entries.filter((entry) => !isPusharyCodexHook(entry));
117
+ const filtered = entries.filter((entry) => !isPusharyCodexHook(entry, scope));
113
118
  if (filtered.length !== entries.length) {
114
119
  if (filtered.length === 0) {
115
120
  delete hooks[definition.event];
@@ -127,14 +132,14 @@ var hasCodexHooks = (config) => {
127
132
  if (!hooks) return false;
128
133
  return CODEX_HOOK_EVENTS.some((definition) => {
129
134
  const entries = hooks[definition.event];
130
- return Array.isArray(entries) && entries.some(isPusharyCodexHook);
135
+ return Array.isArray(entries) && entries.some((entry) => isPusharyCodexHook(entry));
131
136
  });
132
137
  };
133
138
  var missingCodexHookEvents = (config) => {
134
139
  const hooks = asRecord(config.hooks);
135
140
  return CODEX_HOOK_EVENTS.filter((definition) => {
136
141
  const entries = hooks?.[definition.event];
137
- return !Array.isArray(entries) || !entries.some(isPusharyCodexHook);
142
+ return !Array.isArray(entries) || !entries.some((entry) => isPusharyCodexHook(entry));
138
143
  }).map((definition) => definition.event);
139
144
  };
140
145
  var codexHookTrustHash = (definition, command) => {
@@ -176,10 +181,10 @@ var codexHookPositions = (hooksConfig) => {
176
181
  for (const definition of CODEX_HOOK_EVENTS) {
177
182
  const entries = hooks?.[definition.event];
178
183
  if (!Array.isArray(entries)) continue;
179
- const entryIndex = entries.findIndex(isPusharyCodexHook);
184
+ const entryIndex = entries.findIndex((entry) => isPusharyCodexHook(entry));
180
185
  if (entryIndex < 0) continue;
181
186
  const inner = asRecord(entries[entryIndex])?.hooks;
182
- const hookIndex = Array.isArray(inner) ? inner.findIndex(isPusharyCommand) : -1;
187
+ const hookIndex = Array.isArray(inner) ? inner.findIndex((hook) => isPusharyCommand(hook)) : -1;
183
188
  positions[definition.event] = [entryIndex, Math.max(hookIndex, 0)];
184
189
  }
185
190
  return positions;
@@ -270,6 +275,7 @@ var geminiMd = () => join2(homedir2(), ".gemini", "GEMINI.md");
270
275
  var pusharyConfigFile = () => join2(homedir2(), ".pushary", "config.json");
271
276
 
272
277
  export {
278
+ isProtectedByScope,
273
279
  canonicalJson,
274
280
  CODEX_HOOK_BINARY,
275
281
  codexHome,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseVersion
3
- } from "./chunk-7TS2G4QG.js";
3
+ } from "./chunk-VA7VUNRD.js";
4
4
 
5
5
  // src/setup/agent-probe.ts
6
6
  import { execSync } from "child_process";
@@ -7,34 +7,49 @@ import {
7
7
  } from "./chunk-PZA7G4CN.js";
8
8
  import {
9
9
  getMachineId
10
- } from "./chunk-2HYPZLYY.js";
10
+ } from "./chunk-DKDDLF5P.js";
11
11
  import {
12
12
  pusharyDir
13
- } from "./chunk-V37ZXMH3.js";
13
+ } from "./chunk-GMI5QZLG.js";
14
14
  import {
15
15
  getPackageVersion
16
16
  } from "./chunk-DINDL2CF.js";
17
17
  import {
18
18
  readJsonSafe,
19
19
  writeFileAtomic,
20
- writeJsonAtomic
21
- } from "./chunk-6MTNS63X.js";
20
+ writeJsonAtomic,
21
+ writeJsonAtomicDurable
22
+ } from "./chunk-XX2V4QS5.js";
22
23
  import {
23
24
  getApiKey
24
25
  } from "./chunk-2UMNXADU.js";
25
26
 
26
27
  // src/daemon-lifecycle.ts
27
28
  import { dirname, join, win32 as win32Path } from "path";
28
- import { existsSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
29
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
29
30
  import { createHash, randomUUID } from "crypto";
30
31
  import { homedir } from "os";
31
32
  import { execFileSync } from "child_process";
32
33
  var daemonNeedsHandoff = (status) => status.kind === "version-mismatch" || status.kind === "configuration-mismatch" || status.kind === "unresponsive" && status.processAlive === true;
34
+ var runDaemonServiceCommand = (command, args) => execFileSync(command, [...args], {
35
+ encoding: "utf-8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ windowsHide: true
38
+ });
39
+ var commandFailureOutput = (error) => {
40
+ const failure = error;
41
+ return [failure.stdout, failure.stderr, failure.message].map((value) => Buffer.isBuffer(value) ? value.toString("utf-8") : typeof value === "string" ? value : "").filter(Boolean).join("\n");
42
+ };
43
+ var isExpectedProbeFailure = (probe, error) => {
44
+ const failure = error;
45
+ const status = typeof failure.status === "number" ? failure.status : typeof failure.code === "number" ? failure.code : null;
46
+ const exitMatches = probe.negativeExitCodes === void 0 || status !== null && probe.negativeExitCodes.includes(status);
47
+ const outputMatches = probe.negativeOutputPattern === void 0 || probe.negativeOutputPattern.test(commandFailureOutput(error));
48
+ return (probe.negativeExitCodes !== void 0 || probe.negativeOutputPattern !== void 0) && exitMatches && outputMatches;
49
+ };
33
50
  var daemonServiceManagerAvailable = (options = {}) => {
34
51
  const platform = options.platform ?? process.platform;
35
- const run = options.run ?? ((command, args) => {
36
- execFileSync(command, [...args], { stdio: "ignore", windowsHide: true });
37
- });
52
+ const run = options.run ?? runDaemonServiceCommand;
38
53
  try {
39
54
  if (platform === "darwin") {
40
55
  const uid = options.uid ?? process.getuid?.();
@@ -48,6 +63,12 @@ var daemonServiceManagerAvailable = (options = {}) => {
48
63
  }
49
64
  if (platform === "win32") {
50
65
  run("sc.exe", ["query", "Schedule"]);
66
+ run("powershell.exe", [
67
+ "-NoProfile",
68
+ "-NonInteractive",
69
+ "-Command",
70
+ "Get-Command Get-ScheduledTask -ErrorAction Stop | Out-Null"
71
+ ]);
51
72
  return true;
52
73
  }
53
74
  return false;
@@ -103,8 +124,25 @@ var buildDaemonService = (input) => {
103
124
  ],
104
125
  start: [{ command: "launchctl", args: ["kickstart", "-k", target] }],
105
126
  stop: [{ command: "launchctl", args: ["kill", "SIGTERM", target], ignoreFailure: true }],
127
+ disable: [{ command: "launchctl", args: ["disable", target] }],
128
+ canDisableWhenUnregistered: true,
106
129
  uninstall: [{ command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true }],
107
- registrationProbe: { command: "launchctl", args: ["print", target] }
130
+ registrationProbe: {
131
+ command: "launchctl",
132
+ args: ["print", target],
133
+ negativeExitCodes: [113]
134
+ },
135
+ enabledProbe: {
136
+ command: "launchctl",
137
+ args: ["print-disabled", domain],
138
+ disabledOutputPattern: new RegExp(`"${label}"\\s*=>\\s*disabled`)
139
+ },
140
+ runningProbe: {
141
+ command: "launchctl",
142
+ args: ["print", target],
143
+ outputPattern: /\bstate = running\b/,
144
+ negativeExitCodes: [113]
145
+ }
108
146
  };
109
147
  }
110
148
  if (input.platform === "linux") {
@@ -134,10 +172,20 @@ WantedBy=default.target
134
172
  ],
135
173
  start: [{ command: "systemctl", args: ["--user", "start", unit] }],
136
174
  stop: [{ command: "systemctl", args: ["--user", "stop", unit], ignoreFailure: true }],
175
+ disable: [{ command: "systemctl", args: ["--user", "disable", unit] }],
137
176
  uninstall: [
138
177
  { command: "systemctl", args: ["--user", "disable", "--now", unit], ignoreFailure: true }
139
178
  ],
140
- registrationProbe: { command: "systemctl", args: ["--user", "is-enabled", unit] },
179
+ registrationProbe: {
180
+ command: "systemctl",
181
+ args: ["--user", "is-enabled", unit],
182
+ negativeOutputPattern: /^(?:disabled|not-found)\s*$/m
183
+ },
184
+ runningProbe: {
185
+ command: "systemctl",
186
+ args: ["--user", "is-active", unit],
187
+ negativeOutputPattern: /^(?:inactive|failed|unknown)\s*$/m
188
+ },
141
189
  reloadAfterUninstall: { command: "systemctl", args: ["--user", "daemon-reload"] }
142
190
  };
143
191
  }
@@ -175,8 +223,38 @@ WantedBy=default.target
175
223
  ],
176
224
  start: [{ command: "schtasks.exe", args: ["/Run", "/TN", taskName] }],
177
225
  stop: [{ command: "schtasks.exe", args: ["/End", "/TN", taskName], ignoreFailure: true }],
226
+ disable: [{ command: "schtasks.exe", args: ["/Change", "/TN", taskName, "/Disable"] }],
178
227
  uninstall: [{ command: "schtasks.exe", args: ["/Delete", "/TN", taskName, "/F"], ignoreFailure: true }],
179
- registrationProbe: { command: "schtasks.exe", args: ["/Query", "/TN", taskName] }
228
+ registrationProbe: {
229
+ command: "powershell.exe",
230
+ args: [
231
+ "-NoProfile",
232
+ "-NonInteractive",
233
+ "-Command",
234
+ "$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }"
235
+ ],
236
+ negativeExitCodes: [3]
237
+ },
238
+ enabledProbe: {
239
+ command: "powershell.exe",
240
+ args: [
241
+ "-NoProfile",
242
+ "-NonInteractive",
243
+ "-Command",
244
+ "$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($task.State -eq 'Disabled') { exit 4 }"
245
+ ],
246
+ negativeExitCodes: [3, 4]
247
+ },
248
+ runningProbe: {
249
+ command: "powershell.exe",
250
+ args: [
251
+ "-NoProfile",
252
+ "-NonInteractive",
253
+ "-Command",
254
+ "$task = @(Get-ScheduledTask -ErrorAction Stop) | Where-Object { $_.TaskName -eq 'Pushary Daemon' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { exit 3 }; if ($task.State -ne 'Running') { exit 4 }"
255
+ ],
256
+ negativeExitCodes: [3, 4]
257
+ }
180
258
  };
181
259
  }
182
260
  return null;
@@ -219,26 +297,30 @@ var resolveDaemonServiceDefinition = (options = {}) => {
219
297
  uid: options.uid ?? process.getuid?.()
220
298
  });
221
299
  };
300
+ var runDaemonServiceSteps = (steps, run) => {
301
+ for (const step of steps) {
302
+ try {
303
+ run(step.command, step.args);
304
+ } catch (err) {
305
+ if (!step.ignoreFailure) throw err;
306
+ }
307
+ }
308
+ };
222
309
  var applyDaemonService = (action, definition, options = {}) => {
223
- const run = options.run ?? ((command, args) => {
224
- execFileSync(command, [...args], { stdio: "ignore", windowsHide: true });
225
- });
310
+ const run = options.run ?? runDaemonServiceCommand;
226
311
  try {
227
312
  if (action === "install") writeFileAtomic(definition.definitionPath, definition.contents, 384);
228
313
  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
- }
314
+ runDaemonServiceSteps(steps, run);
236
315
  if (action === "uninstall") {
237
- let registered = false;
316
+ let registered = true;
238
317
  try {
239
318
  run(definition.registrationProbe.command, definition.registrationProbe.args);
240
- registered = true;
241
- } catch {
319
+ } catch (err) {
320
+ if (!isExpectedProbeFailure(definition.registrationProbe, err)) {
321
+ throw new Error(`Could not verify native background service is absent: ${commandFailureOutput(err) || String(err)}`);
322
+ }
323
+ registered = false;
242
324
  }
243
325
  if (registered) throw new Error("Native background service remains registered");
244
326
  safeUnlink(definition.definitionPath);
@@ -563,8 +645,7 @@ var deactivateDaemonService = async (action, options) => {
563
645
  const platform = options.platform ?? process.platform;
564
646
  const supported = ["darwin", "linux", "win32"].includes(platform);
565
647
  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(action, options) : { kind: "ok" };
648
+ const service = supported && !managerAvailable ? { kind: "failed", detail: "Native background-service manager is unavailable" } : managerAvailable ? manageDaemonService(action, options) : { kind: "ok" };
568
649
  const stopped = await stopDaemonRunning(options);
569
650
  const failures = [
570
651
  service.kind === "ok" ? null : service.detail,
@@ -574,9 +655,251 @@ var deactivateDaemonService = async (action, options) => {
574
655
  };
575
656
  var stopDaemonService = async (options = {}) => deactivateDaemonService("stop", options);
576
657
  var disableDaemonService = async (options = {}) => deactivateDaemonService("uninstall", options);
658
+ var parseDaemonServiceTransaction = (value) => {
659
+ if (typeof value !== "object" || value === null) return null;
660
+ const transaction = value;
661
+ if (transaction.version !== 1 || transaction.phase !== "prepared" && transaction.phase !== "committed" || typeof transaction.definitionPath !== "string" || !(transaction.previousContents === null || typeof transaction.previousContents === "string") || typeof transaction.previousRegistered !== "boolean" || !(transaction.previousEnabled === void 0 || typeof transaction.previousEnabled === "boolean") || typeof transaction.previousRunning !== "boolean") return null;
662
+ return {
663
+ ...transaction,
664
+ previousEnabled: typeof transaction.previousEnabled === "boolean" ? transaction.previousEnabled : transaction.previousRegistered
665
+ };
666
+ };
667
+ var probeDaemonServiceState = (definition, run) => {
668
+ let registered = true;
669
+ try {
670
+ run(definition.registrationProbe.command, definition.registrationProbe.args);
671
+ } catch (err) {
672
+ if (!isExpectedProbeFailure(definition.registrationProbe, err)) {
673
+ return {
674
+ kind: "failed",
675
+ detail: `Could not inspect native service registration: ${commandFailureOutput(err) || String(err)}`
676
+ };
677
+ }
678
+ registered = false;
679
+ }
680
+ let enabled = registered;
681
+ const enabledProbe = definition.enabledProbe;
682
+ if (enabledProbe) {
683
+ try {
684
+ const output = run(enabledProbe.command, enabledProbe.args);
685
+ enabled = !(enabledProbe.disabledOutputPattern && typeof output === "string" && enabledProbe.disabledOutputPattern.test(output));
686
+ } catch (err) {
687
+ if (!isExpectedProbeFailure(enabledProbe, err)) {
688
+ return {
689
+ kind: "failed",
690
+ detail: `Could not inspect whether the native service is enabled: ${commandFailureOutput(err) || String(err)}`
691
+ };
692
+ }
693
+ enabled = false;
694
+ }
695
+ }
696
+ try {
697
+ const output = run(definition.runningProbe.command, definition.runningProbe.args);
698
+ const running = definition.runningProbe.outputPattern ? typeof output === "string" && definition.runningProbe.outputPattern.test(output) : true;
699
+ return { kind: "ok", registered, enabled, running };
700
+ } catch (err) {
701
+ if (!isExpectedProbeFailure(definition.runningProbe, err)) {
702
+ return {
703
+ kind: "failed",
704
+ detail: `Could not inspect whether the native service is running: ${commandFailureOutput(err) || String(err)}`
705
+ };
706
+ }
707
+ return { kind: "ok", registered, enabled, running: false };
708
+ }
709
+ };
710
+ var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
711
+ const removed = applyDaemonService("uninstall", definition, { run });
712
+ if (removed.kind !== "ok") return removed;
713
+ try {
714
+ if (snapshot.previousContents === null) safeUnlink(definition.definitionPath);
715
+ else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384);
716
+ } catch (err) {
717
+ return {
718
+ kind: "failed",
719
+ detail: `could not restore the previous definition: ${err instanceof Error ? err.message : String(err)}`
720
+ };
721
+ }
722
+ if (!snapshot.previousRegistered) {
723
+ const reload = definition.reloadAfterUninstall;
724
+ if (snapshot.previousContents !== null && reload) {
725
+ try {
726
+ run(reload.command, reload.args);
727
+ } catch (err) {
728
+ return { kind: "failed", detail: `could not reload the previous definition: ${commandFailureOutput(err) || String(err)}` };
729
+ }
730
+ }
731
+ if (snapshot.previousRunning) {
732
+ const started = applyDaemonService("start", definition, { run });
733
+ if (started.kind !== "ok") return started;
734
+ }
735
+ if (!snapshot.previousEnabled && definition.canDisableWhenUnregistered) {
736
+ try {
737
+ runDaemonServiceSteps(definition.disable, run);
738
+ } catch (err) {
739
+ return { kind: "failed", detail: `could not restore the previous disabled state: ${commandFailureOutput(err) || String(err)}` };
740
+ }
741
+ }
742
+ const state2 = probeDaemonServiceState(definition, run);
743
+ if (state2.kind !== "ok") return { kind: "failed", detail: state2.detail };
744
+ if (state2.registered || state2.enabled !== snapshot.previousEnabled || state2.running !== snapshot.previousRunning) {
745
+ return { kind: "failed", detail: "could not restore the previous unregistered service state" };
746
+ }
747
+ return { kind: "ok" };
748
+ }
749
+ if (snapshot.previousContents === null) {
750
+ return { kind: "failed", detail: "the previous registered service had no recoverable definition" };
751
+ }
752
+ const restored = applyDaemonService("install", {
753
+ ...definition,
754
+ contents: snapshot.previousContents
755
+ }, { run });
756
+ if (restored.kind !== "ok") return restored;
757
+ if (!snapshot.previousRunning) applyDaemonService("stop", definition, { run });
758
+ if (!snapshot.previousEnabled) {
759
+ try {
760
+ runDaemonServiceSteps(definition.disable, run);
761
+ } catch (err) {
762
+ return { kind: "failed", detail: `could not restore the previous disabled state: ${commandFailureOutput(err) || String(err)}` };
763
+ }
764
+ }
765
+ const state = probeDaemonServiceState(definition, run);
766
+ if (state.kind !== "ok") return { kind: "failed", detail: state.detail };
767
+ if (!state.registered) return { kind: "failed", detail: "the previous service is no longer registered" };
768
+ if (state.enabled !== snapshot.previousEnabled) {
769
+ return { kind: "failed", detail: "could not restore whether the previous service was enabled" };
770
+ }
771
+ if (state.running !== snapshot.previousRunning) {
772
+ return {
773
+ kind: "failed",
774
+ detail: snapshot.previousRunning ? "the previous service did not restart" : "could not restore the previous stopped state"
775
+ };
776
+ }
777
+ return { kind: "ok" };
778
+ };
779
+ var recoverDaemonServiceTransaction = (definition, path, run) => {
780
+ const stored = readJsonSafe(path);
781
+ if (stored.kind === "missing") return { kind: "ok" };
782
+ if (stored.kind !== "ok") {
783
+ return { kind: "failed", detail: `transaction receipt is ${stored.kind}` };
784
+ }
785
+ const transaction = parseDaemonServiceTransaction(stored.value);
786
+ if (!transaction) return { kind: "failed", detail: "transaction receipt is invalid" };
787
+ if (transaction.definitionPath !== definition.definitionPath) {
788
+ return { kind: "failed", detail: "transaction receipt belongs to a different service definition" };
789
+ }
790
+ if (transaction.phase === "prepared") {
791
+ const restored = restoreDaemonServiceSnapshot(definition, transaction, run);
792
+ if (restored.kind !== "ok") return restored;
793
+ }
794
+ try {
795
+ safeUnlink(path);
796
+ } catch (err) {
797
+ return { kind: "failed", detail: `could not clear transaction receipt: ${err instanceof Error ? err.message : String(err)}` };
798
+ }
799
+ return { kind: "ok" };
800
+ };
577
801
  var replaceDaemonService = async (options = {}) => {
802
+ const definition = resolveDaemonServiceDefinition(options);
803
+ if (!definition) {
804
+ return {
805
+ kind: "unavailable",
806
+ detail: "Could not find the globally installed daemon entry. Run Pushary setup again."
807
+ };
808
+ }
809
+ const run = options.run ?? runDaemonServiceCommand;
810
+ const transactionPath = join(options.stateDir ?? options.dir ?? pusharyDir(), "daemon-service-transaction.json");
811
+ const recovered = recoverDaemonServiceTransaction(definition, transactionPath, run);
812
+ if (recovered.kind !== "ok") {
813
+ return { kind: "failed", detail: `Could not recover interrupted service replacement: ${recovered.detail}` };
814
+ }
815
+ let previousContents = null;
816
+ try {
817
+ if (existsSync(definition.definitionPath)) {
818
+ previousContents = readFileSync(definition.definitionPath, "utf-8");
819
+ }
820
+ } catch (err) {
821
+ return { kind: "failed", detail: `Could not snapshot the existing service: ${err instanceof Error ? err.message : String(err)}` };
822
+ }
823
+ const previousState = probeDaemonServiceState(definition, run);
824
+ if (previousState.kind !== "ok") return { kind: "failed", detail: previousState.detail };
825
+ if ((previousState.registered || previousState.running) && previousContents === null) {
826
+ return { kind: "failed", detail: "Could not replace an active native service with no recoverable definition" };
827
+ }
828
+ const transaction = {
829
+ version: 1,
830
+ phase: "prepared",
831
+ definitionPath: definition.definitionPath,
832
+ previousContents,
833
+ previousRegistered: previousState.registered,
834
+ previousEnabled: previousState.enabled,
835
+ previousRunning: previousState.running
836
+ };
837
+ try {
838
+ writeJsonAtomicDurable(transactionPath, transaction, 384);
839
+ } catch (err) {
840
+ return { kind: "failed", detail: `Could not persist the service rollback snapshot: ${err instanceof Error ? err.message : String(err)}` };
841
+ }
578
842
  const stopped = await stopDaemonRunning(options);
579
- return stopped.kind === "unavailable" ? stopped : manageDaemonService("install", options);
843
+ if (stopped.kind === "unavailable") {
844
+ try {
845
+ safeUnlink(transactionPath);
846
+ } catch (err) {
847
+ return {
848
+ kind: "failed",
849
+ detail: `${stopped.detail}; could not clear unused rollback evidence: ${err instanceof Error ? err.message : String(err)}`
850
+ };
851
+ }
852
+ return stopped;
853
+ }
854
+ let installed = applyDaemonService("install", definition, { run });
855
+ if (installed.kind === "ok") {
856
+ const state = probeDaemonServiceState(definition, run);
857
+ if (state.kind !== "ok") {
858
+ installed = { kind: "failed", detail: state.detail };
859
+ } else if (!state.registered || !state.enabled || !state.running) {
860
+ installed = {
861
+ kind: "failed",
862
+ detail: !state.registered ? "Service registration verification failed" : !state.enabled ? "Service enable verification failed" : "Service start verification failed"
863
+ };
864
+ }
865
+ }
866
+ if (installed.kind === "ok") {
867
+ try {
868
+ writeJsonAtomicDurable(transactionPath, { ...transaction, phase: "committed" }, 384);
869
+ } catch (err) {
870
+ const detail = `Could not commit the service replacement: ${err instanceof Error ? err.message : String(err)}`;
871
+ const recovered2 = recoverDaemonServiceTransaction(definition, transactionPath, run);
872
+ return recovered2.kind === "ok" ? { kind: "failed", detail } : {
873
+ kind: "failed",
874
+ detail: `${detail}; recovery failed: ${recovered2.detail}`
875
+ };
876
+ }
877
+ try {
878
+ safeUnlink(transactionPath);
879
+ return installed;
880
+ } catch (err) {
881
+ return {
882
+ kind: "failed",
883
+ detail: `Service replacement committed but its receipt could not be cleared: ${err instanceof Error ? err.message : String(err)}`
884
+ };
885
+ }
886
+ }
887
+ const restored = restoreDaemonServiceSnapshot(definition, transaction, run);
888
+ if (restored.kind !== "ok") {
889
+ return { kind: "failed", detail: `${installed.detail}; rollback failed: ${restored.detail}` };
890
+ }
891
+ try {
892
+ safeUnlink(transactionPath);
893
+ } catch (err) {
894
+ return {
895
+ kind: "failed",
896
+ detail: `${installed.detail}; previous service restored but recovery evidence could not be cleared: ${err instanceof Error ? err.message : String(err)}`
897
+ };
898
+ }
899
+ return {
900
+ kind: "failed",
901
+ detail: `${installed.detail}; ${transaction.previousRegistered ? "previous service restored" : "previous service state restored"}`
902
+ };
580
903
  };
581
904
  var launchInstalledDaemon = (options) => {
582
905
  const entryExists = options.entryExists ?? existsSync;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  pusharyDir
3
- } from "./chunk-V37ZXMH3.js";
3
+ } from "./chunk-GMI5QZLG.js";
4
4
  import {
5
5
  readJsonSafe
6
- } from "./chunk-6MTNS63X.js";
6
+ } from "./chunk-XX2V4QS5.js";
7
7
 
8
8
  // src/process-ownership.ts
9
9
  import { createHash, randomUUID } from "crypto";
@@ -9,7 +9,7 @@ import {
9
9
  readJsonSafe,
10
10
  writeFileAtomic,
11
11
  writeJsonAtomic
12
- } from "./chunk-6MTNS63X.js";
12
+ } from "./chunk-XX2V4QS5.js";
13
13
  import {
14
14
  configFilePath
15
15
  } from "./chunk-2UMNXADU.js";