pushary 1.9.136 → 1.9.137

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 (42) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bin/pushary-bell-hook.js +3 -3
  3. package/dist/bin/pushary-claude.js +2 -2
  4. package/dist/bin/pushary-clean.js +9 -9
  5. package/dist/bin/pushary-codex-hook.js +3 -3
  6. package/dist/bin/pushary-codex-session.js +2 -2
  7. package/dist/bin/pushary-daemon.js +3 -3
  8. package/dist/bin/pushary-display-hook.js +36 -24
  9. package/dist/bin/pushary-doctor.js +12 -8
  10. package/dist/bin/pushary-elicitation-hook.js +3 -3
  11. package/dist/bin/pushary-gemini-hook.js +3 -3
  12. package/dist/bin/pushary-hook.js +4 -4
  13. package/dist/bin/pushary-login.js +1 -1
  14. package/dist/bin/pushary-notification-hook.js +3 -3
  15. package/dist/bin/pushary-opencode-hook.js +3 -3
  16. package/dist/bin/pushary-permission-denied-hook.js +4 -4
  17. package/dist/bin/pushary-permission-hook.js +4 -4
  18. package/dist/bin/pushary-post-hook.js +3 -3
  19. package/dist/bin/pushary-prompt-hook.js +3 -3
  20. package/dist/bin/pushary-session-end-hook.js +3 -3
  21. package/dist/bin/pushary-session-start-hook.js +3 -3
  22. package/dist/bin/pushary-setup.js +11 -9
  23. package/dist/bin/pushary-status.js +2 -2
  24. package/dist/bin/pushary-stop-hook.js +3 -3
  25. package/dist/bin/pushary-stopfailure-hook.js +3 -3
  26. package/dist/bin/pushary-transcript-register.js +2 -2
  27. package/dist/bin/pushary-transcripts.js +2 -2
  28. package/dist/bin/pushary-upgrade.js +2 -2
  29. package/dist/{chunk-L7Q57R4J.js → chunk-6WT5OIRM.js} +1 -1
  30. package/dist/{chunk-EQZB5P6I.js → chunk-7NCI2DGB.js} +2 -2
  31. package/dist/{chunk-RPFSVNH6.js → chunk-7VKW7OA5.js} +1 -1
  32. package/dist/{chunk-YURIU333.js → chunk-7XD56GXA.js} +1 -1
  33. package/dist/chunk-B4UIRMOE.js +31 -0
  34. package/dist/{chunk-HKJLPVPZ.js → chunk-CMAB4KQG.js} +1 -1
  35. package/dist/{chunk-FAF5RDWT.js → chunk-MD63J3T7.js} +229 -38
  36. package/dist/{chunk-O5T53LZ5.js → chunk-R6N6FREL.js} +1 -1
  37. package/dist/{chunk-J42EAEX3.js → chunk-XR27HPYC.js} +2 -1
  38. package/dist/{codex-app-cards-switch-Z7TMF2OR.js → codex-app-cards-switch-JU236CG2.js} +3 -3
  39. package/dist/{codex-desktop-launch-HKXGKL3T.js → codex-desktop-launch-LRQ4LU64.js} +1 -1
  40. package/dist/src/index.js +4 -4
  41. package/package.json +1 -1
  42. package/dist/chunk-GACQSKNG.js +0 -75
@@ -34,9 +34,136 @@ import {
34
34
  isRecord
35
35
  } from "./chunk-DL67HX3D.js";
36
36
 
37
+ // src/daemon-entry-guard.ts
38
+ import { mkdirSync, readFileSync } from "fs";
39
+ import { basename, dirname, join } from "path";
40
+ var DAEMON_ENTRY_GUARD_SOURCE = [
41
+ "const { existsSync } = require('node:fs')",
42
+ "if (process.argv[1] && !existsSync(process.argv[1])) process.exit(0)",
43
+ ""
44
+ ].join("\n");
45
+ var DAEMON_ENTRY_GUARD_FILE = "daemon-entry-guard.cjs";
46
+ var NODE_EXECUTABLE = /^node(?:\.exe)?$/i;
47
+ var ENTRY_GUARD_PRELOAD_FLAG = "--require";
48
+ var UNGUARDED = { kind: "unguarded" };
49
+ var daemonEntryGuardPath = (stateDir) => join(stateDir, DAEMON_ENTRY_GUARD_FILE);
50
+ var entryGuardApplies = (platform, execPath) => platform === "darwin" && NODE_EXECUTABLE.test(basename(execPath));
51
+ var holdsDaemonEntryGuard = (path) => {
52
+ try {
53
+ return readFileSync(path, "utf8") === DAEMON_ENTRY_GUARD_SOURCE;
54
+ } catch {
55
+ return false;
56
+ }
57
+ };
58
+ var writeDaemonEntryGuardAt = (path) => {
59
+ if (holdsDaemonEntryGuard(path)) return { kind: "in-place" };
60
+ try {
61
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
62
+ writeFileAtomic(path, DAEMON_ENTRY_GUARD_SOURCE, 384);
63
+ return { kind: "in-place" };
64
+ } catch (error) {
65
+ return { kind: "failed", detail: error instanceof Error ? error.message : String(error) };
66
+ }
67
+ };
68
+ var writeDaemonEntryGuard = (input) => {
69
+ if (!entryGuardApplies(input.platform, input.execPath)) return UNGUARDED;
70
+ const path = daemonEntryGuardPath(input.stateDir);
71
+ const written = writeDaemonEntryGuardAt(path);
72
+ if (written.kind === "in-place") return { kind: "guarded", path };
73
+ input.warn(`[pushary] could not write ${path}, so the background service was installed without its uninstall guard: ${written.detail}
74
+ `);
75
+ return UNGUARDED;
76
+ };
77
+
78
+ // src/diagnostics/service-runtime.ts
79
+ var unescapeXml = (value) => value.replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
80
+ var unquoteSystemdExecutable = (value) => value.replace(/\\(["\\])|%(%)/g, (_match, escaped, percent) => escaped ?? percent ?? "");
81
+ var unquoteSystemdArgument = (value) => value.replace(/\\(["\\])|%(%)|\$(\$)/g, (_match, escaped, percent, dollar) => escaped ?? percent ?? dollar ?? "");
82
+ var readServiceExecPath = (contents, platform) => {
83
+ if (platform === "darwin") {
84
+ const program = contents.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/)?.[1];
85
+ return program ? unescapeXml(program) : null;
86
+ }
87
+ if (platform === "linux") {
88
+ const exec = contents.match(/^ExecStart="((?:[^"\\]|\\.)*)"/m)?.[1];
89
+ return exec ? unquoteSystemdExecutable(exec) : null;
90
+ }
91
+ if (platform === "win32") {
92
+ const command = contents.match(/<Command>([^<]*)<\/Command>/)?.[1];
93
+ return command ? unescapeXml(command) : null;
94
+ }
95
+ return null;
96
+ };
97
+ var readLaunchdProgramArguments = (contents) => {
98
+ const array = contents.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
99
+ return [...array.matchAll(/<string>([^<]*)<\/string>/g)].map((match) => unescapeXml(match[1] ?? ""));
100
+ };
101
+ var systemdExecStartArguments = (contents) => {
102
+ const line = contents.match(/^ExecStart=(.*)$/m)?.[1] ?? "";
103
+ return [...line.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match, index) => (index === 0 ? unquoteSystemdExecutable : unquoteSystemdArgument)(match[1] ?? ""));
104
+ };
105
+ var windowsTaskArguments = (contents) => {
106
+ const args = unescapeXml(contents.match(/<Arguments>([^<]*)<\/Arguments>/)?.[1] ?? "");
107
+ return [...args.matchAll(/"([^"]*)"/g)].map((match) => match[1] ?? "");
108
+ };
109
+ var serviceArguments = (contents, platform) => {
110
+ if (platform === "darwin") return readLaunchdProgramArguments(contents);
111
+ if (platform === "linux") return systemdExecStartArguments(contents);
112
+ if (platform === "win32") return windowsTaskArguments(contents);
113
+ return [];
114
+ };
115
+ var readServiceDaemonEntry = (contents, platform) => serviceArguments(contents, platform).at(-1) ?? null;
116
+ var readServiceEntryGuard = (contents, platform) => {
117
+ if (platform !== "darwin") return null;
118
+ const [, flag, guard] = readLaunchdProgramArguments(contents);
119
+ return flag === ENTRY_GUARD_PRELOAD_FLAG && guard !== void 0 ? guard : null;
120
+ };
121
+ var serviceStartFileChecks = (contents, platform, exists) => {
122
+ const checks = [];
123
+ const entry = readServiceDaemonEntry(contents, platform);
124
+ if (entry !== null) {
125
+ const present = exists(entry);
126
+ checks.push({
127
+ label: "Remote-control service entry",
128
+ passed: present,
129
+ detail: present ? entry : `${entry} is gone, so the background service starts nothing at login; run \`pushary daemon install\` from the Pushary install you use`
130
+ });
131
+ }
132
+ const guard = readServiceEntryGuard(contents, platform);
133
+ if (guard !== null && !exists(guard)) {
134
+ checks.push({
135
+ label: "Remote-control service start guard",
136
+ passed: false,
137
+ detail: `${guard} is gone, so the background service cannot start; run \`pushary daemon install\` to restore it`
138
+ });
139
+ }
140
+ return checks;
141
+ };
142
+ var MANAGER_SEGMENTS = [
143
+ [".nvm", "nvm"],
144
+ ["nvm", "nvm"],
145
+ ["fnm", "fnm"],
146
+ [".fnm", "fnm"],
147
+ [".volta", "volta"],
148
+ ["volta", "volta"],
149
+ [".asdf", "asdf"],
150
+ ["asdf", "asdf"],
151
+ ["Cellar", "homebrew"]
152
+ ];
153
+ var versionManagerOf = (execPath) => {
154
+ const segments = execPath.split(/[/\\]/);
155
+ for (const segment of segments) {
156
+ const hit = MANAGER_SEGMENTS.find(([name]) => name === segment);
157
+ if (hit) return hit[1];
158
+ }
159
+ return null;
160
+ };
161
+ var describeVersionManagerPin = (execPath, manager) => `The background service is pinned to ${execPath}, which ${manager} manages. Switching or upgrading Node strands it; run pushary daemon install afterwards.`;
162
+ var describeServiceNodeRisk = (manager) => manager === "homebrew" ? "a Homebrew version folder; brew upgrade strands the service, and `pushary daemon install` repins it to the stable link" : `managed by ${manager}; a version switch strands the service`;
163
+
37
164
  // src/daemon-lifecycle.ts
38
- import { dirname, join, win32 as win32Path } from "path";
39
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
165
+ import { dirname as dirname2, join as join2, win32 as win32Path } from "path";
166
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
40
167
  import { createHash, randomUUID } from "crypto";
41
168
  import { homedir, uptime, userInfo } from "os";
42
169
  import { execFileSync } from "child_process";
@@ -55,7 +182,7 @@ var remoteProofIsFresh = (timestamp, now) => {
55
182
  };
56
183
  var daemonHasRemoteProof = (status, now = Date.now()) => status.kind === "running" && remoteProofIsFresh(status.lastSuccessfulHeartbeatAt, now) && remoteProofIsFresh(status.lastSuccessfulControlAt, now);
57
184
  var readServiceDefinitionFile = (path) => {
58
- const bytes = readFileSync(path);
185
+ const bytes = readFileSync2(path);
59
186
  const encoding = bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254 ? "utf16le" : "utf-8";
60
187
  return { contents: bytes.toString(encoding), encoding };
61
188
  };
@@ -105,9 +232,9 @@ var daemonServiceManagerAvailable = (options = {}) => {
105
232
  }
106
233
  };
107
234
  var DAEMON_STATE_FILE = "daemon-state.json";
108
- var statePath = (dir) => join(dir, DAEMON_STATE_FILE);
109
- var lockPath = (dir) => join(dir, "daemon.lock");
110
- var targetPath = (dir) => join(dir, "daemon-target.json");
235
+ var statePath = (dir) => join2(dir, DAEMON_STATE_FILE);
236
+ var lockPath = (dir) => join2(dir, "daemon.lock");
237
+ var targetPath = (dir) => join2(dir, "daemon-target.json");
111
238
  var DAEMON_LEASE_RENEW_INTERVAL_MS = 3e4;
112
239
  var LOCAL_LEASE_TIMEOUT_MS = DAEMON_LEASE_RENEW_INTERVAL_MS * 3;
113
240
  var DAEMON_YIELD_CHECK_INTERVAL_MS = 2e3;
@@ -117,28 +244,38 @@ var buildDaemonLaunch = (input) => input.siblingEntry ? { command: input.execPat
117
244
  shell: needsShell(input.daemonBinary, input.platform ?? process.platform)
118
245
  };
119
246
  var xml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
120
- var systemdQuote = (value) => `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
247
+ var systemdQuotedEnvironment = (value) => `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
248
+ var escapeSystemdSpecifiers = (value) => value.replaceAll("%", "%%");
249
+ var systemdQuotedWord = (escaped) => `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
250
+ var systemdExecutableWord = (value) => systemdQuotedWord(escapeSystemdSpecifiers(value));
251
+ var systemdArgumentWord = (value) => systemdQuotedWord(escapeSystemdSpecifiers(value).replaceAll("$", "$$$$"));
252
+ var systemdExecLine = (executable, args) => [systemdExecutableWord(executable), ...args.map(systemdArgumentWord)].join(" ");
253
+ var ENTRY_PRESENT_CHECK = "process.exit(require('node:fs').existsSync(process.argv[1]) ? 0 : 1)";
254
+ var guardedLaunchdProgram = (input) => {
255
+ const guard = input.entryGuard ?? UNGUARDED;
256
+ return guard.kind === "guarded" ? [input.execPath, ENTRY_GUARD_PRELOAD_FLAG, guard.path, input.daemonEntry] : [input.execPath, input.daemonEntry];
257
+ };
121
258
  var buildDaemonService = (input) => {
122
259
  if (input.platform === "darwin") {
123
260
  if (!Number.isInteger(input.uid) || input.uid < 0) return null;
124
261
  const label = "com.pushary.daemon";
125
262
  const domain = `gui/${input.uid}`;
126
263
  const target = `${domain}/${label}`;
127
- const definitionPath = join(input.homeDir, "Library", "LaunchAgents", `${label}.plist`);
264
+ const definitionPath = join2(input.homeDir, "Library", "LaunchAgents", `${label}.plist`);
128
265
  const contents = `<?xml version="1.0" encoding="UTF-8"?>
129
266
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
130
267
  <plist version="1.0">
131
268
  <dict>
132
269
  <key>Label</key><string>${label}</string>
133
270
  <key>ProgramArguments</key>
134
- <array><string>${xml(input.execPath)}</string><string>${xml(input.daemonEntry)}</string></array>
271
+ <array>${guardedLaunchdProgram(input).map((argument) => `<string>${xml(argument)}</string>`).join("")}</array>
135
272
  <key>WorkingDirectory</key><string>${xml(input.homeDir)}</string>
136
273
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(input.pathEnv)}</string></dict>
137
274
  <key>RunAtLoad</key><true/>
138
275
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
139
276
  <key>ThrottleInterval</key><integer>10</integer>
140
- <key>StandardOutPath</key><string>${xml(join(input.stateDir, "daemon.log"))}</string>
141
- <key>StandardErrorPath</key><string>${xml(join(input.stateDir, "daemon-error.log"))}</string>
277
+ <key>StandardOutPath</key><string>${xml(join2(input.stateDir, "daemon.log"))}</string>
278
+ <key>StandardErrorPath</key><string>${xml(join2(input.stateDir, "daemon-error.log"))}</string>
142
279
  </dict>
143
280
  </plist>
144
281
  `;
@@ -157,6 +294,8 @@ var buildDaemonService = (input) => {
157
294
  disable: [{ command: "launchctl", args: ["disable", target] }],
158
295
  canDisableWhenUnregistered: true,
159
296
  uninstall: [{ command: "launchctl", args: ["bootout", domain, definitionPath], ignoreFailure: true }],
297
+ platform: input.platform,
298
+ startGuardPath: daemonEntryGuardPath(input.stateDir),
160
299
  registrationProbe: {
161
300
  command: "launchctl",
162
301
  args: ["print", target],
@@ -177,15 +316,17 @@ var buildDaemonService = (input) => {
177
316
  }
178
317
  if (input.platform === "linux") {
179
318
  const unit = "pushary-daemon.service";
180
- const definitionPath = join(input.homeDir, ".config", "systemd", "user", unit);
319
+ const definitionPath = join2(input.homeDir, ".config", "systemd", "user", unit);
181
320
  const contents = `[Unit]
182
321
  Description=Pushary phone-to-agent daemon
183
322
  After=network-online.target
323
+ ConditionPathExists=${escapeSystemdSpecifiers(input.daemonEntry)}
184
324
 
185
325
  [Service]
186
326
  Type=simple
187
- ExecStart=${systemdQuote(input.execPath)} ${systemdQuote(input.daemonEntry)}
188
- Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
327
+ ExecCondition=${systemdExecLine(input.execPath, ["-e", ENTRY_PRESENT_CHECK, input.daemonEntry])}
328
+ ExecStart=${systemdExecLine(input.execPath, [input.daemonEntry])}
329
+ Environment=${systemdQuotedEnvironment(`PATH=${input.pathEnv}`)}
189
330
  Restart=on-failure
190
331
  RestartSec=10
191
332
  KillMode=process
@@ -216,7 +357,9 @@ WantedBy=default.target
216
357
  args: ["--user", "is-active", unit],
217
358
  negativeOutputPattern: /^(?:inactive|failed|unknown|activating|deactivating)\s*$/m
218
359
  },
219
- reloadAfterUninstall: { command: "systemctl", args: ["--user", "daemon-reload"] }
360
+ reloadAfterUninstall: { command: "systemctl", args: ["--user", "daemon-reload"] },
361
+ platform: input.platform,
362
+ startGuardPath: null
220
363
  };
221
364
  }
222
365
  if (input.platform === "win32") {
@@ -262,6 +405,8 @@ WantedBy=default.target
262
405
  stop: [{ command: "schtasks.exe", args: ["/End", "/TN", taskName], ignoreFailure: true }],
263
406
  disable: [{ command: "schtasks.exe", args: ["/Change", "/TN", taskName, "/Disable"] }],
264
407
  uninstall: [{ command: "schtasks.exe", args: ["/Delete", "/TN", taskName, "/F"], ignoreFailure: true }],
408
+ platform: input.platform,
409
+ startGuardPath: null,
265
410
  registrationProbe: {
266
411
  command: "powershell.exe",
267
412
  args: [
@@ -297,7 +442,7 @@ WantedBy=default.target
297
442
  return null;
298
443
  };
299
444
  var WINDOWS_DAEMON_ENTRY_SEGMENTS = ["dist", "bin", "pushary-daemon.js"];
300
- var readTextFile = (path) => readFileSync(path, "utf8");
445
+ var readTextFile = (path) => readFileSync2(path, "utf8");
301
446
  var windowsPackageDaemonEntries = (globalPrefix) => {
302
447
  const root = globalRootFor(globalPrefix, "win32");
303
448
  return CLI_PACKAGE_NAMES.map((name) => win32Path.join(globalPackageFolder(root, name, "win32"), ...WINDOWS_DAEMON_ENTRY_SEGMENTS));
@@ -316,7 +461,8 @@ var serviceNodeFor = (execPath, exists) => {
316
461
  const linked = homebrewLinkedNodeFor(execPath);
317
462
  return linked !== null && exists(linked) ? linked : execPath;
318
463
  };
319
- var resolveDaemonServiceDefinition = (options = {}) => {
464
+ var serviceExecPathFor = (options) => serviceNodeFor(options.execPath ?? process.execPath, options.entryExists ?? existsSync);
465
+ var resolveDaemonServiceInput = (options) => {
320
466
  const platform = options.platform ?? process.platform;
321
467
  const exists = options.entryExists ?? existsSync;
322
468
  const globalBinary = options.globalBinary ?? resolveGlobalBinary("pushary-daemon");
@@ -333,18 +479,22 @@ var resolveDaemonServiceDefinition = (options = {}) => {
333
479
  }
334
480
  }
335
481
  if (!daemonEntry || !exists(daemonEntry)) return null;
336
- const homeDir = options.homeDir ?? homedir();
337
- return buildDaemonService({
482
+ return {
338
483
  platform,
339
- homeDir,
484
+ homeDir: options.homeDir ?? homedir(),
340
485
  stateDir: options.stateDir ?? pusharyDir(),
341
- execPath: serviceNodeFor(options.execPath ?? process.execPath, exists),
486
+ execPath: serviceExecPathFor(options),
342
487
  daemonEntry,
343
488
  pathEnv: options.pathEnv ?? process.env.PATH ?? "",
344
489
  uid: options.uid ?? process.getuid?.(),
345
490
  entryExists: exists
346
- });
491
+ };
347
492
  };
493
+ var resolveDaemonServiceDefinition = (options = {}) => {
494
+ const input = resolveDaemonServiceInput(options);
495
+ return input ? buildDaemonService(input) : null;
496
+ };
497
+ var daemonEntryGuardPathFor = (options = {}) => entryGuardApplies(options.platform ?? process.platform, serviceExecPathFor(options)) ? daemonEntryGuardPath(options.stateDir ?? pusharyDir()) : null;
348
498
  var runDaemonServiceSteps = (steps, run) => {
349
499
  for (const step of steps) {
350
500
  try {
@@ -409,7 +559,17 @@ var manageDaemonService = (action, options = {}) => {
409
559
  if (action === "uninstall" && !daemonServiceManagerAvailable(options)) {
410
560
  return { kind: "failed", detail: "Native background-service manager is unavailable" };
411
561
  }
412
- return applyDaemonService(action, definition, { run: options.run });
562
+ const applied = applyDaemonService(action, definition, { run: options.run });
563
+ return action === "uninstall" && applied.kind === "ok" ? removeEntryGuardOf(definition) : applied;
564
+ };
565
+ var removeEntryGuardOf = (definition) => {
566
+ if (!definition.startGuardPath) return { kind: "ok" };
567
+ try {
568
+ safeUnlink(definition.startGuardPath);
569
+ return { kind: "ok" };
570
+ } catch (err) {
571
+ return { kind: "failed", detail: `the background service is removed, but its start guard ${definition.startGuardPath} could not be: ${err instanceof Error ? err.message : String(err)}` };
572
+ }
413
573
  };
414
574
  var nominalDaemonService = (options) => buildDaemonService({
415
575
  platform: options.platform ?? process.platform,
@@ -431,7 +591,7 @@ var readInstalledDaemonService = (options = {}) => {
431
591
  try {
432
592
  return {
433
593
  path: definition.definitionPath,
434
- contents: readFileSync(definition.definitionPath, definition.encoding ?? "utf-8")
594
+ contents: readFileSync2(definition.definitionPath, definition.encoding ?? "utf-8")
435
595
  };
436
596
  } catch {
437
597
  return null;
@@ -595,7 +755,7 @@ var acquireDaemonLease = (options = {}) => {
595
755
  const instanceId = randomUUID();
596
756
  const startedAt = new Date(now()).toISOString();
597
757
  const writeLock = () => {
598
- mkdirSync(dir, { recursive: true });
758
+ mkdirSync2(dir, { recursive: true });
599
759
  writeFileSync(lockPath(dir), JSON.stringify({ instanceId, pid }), { flag: "wx", mode: 384 });
600
760
  };
601
761
  try {
@@ -619,7 +779,7 @@ var acquireDaemonLease = (options = {}) => {
619
779
  if (owner && ownerIsFromThisBoot && (options.isProcessAlive ?? processIsAlive)(owner.pid)) {
620
780
  return { kind: "unavailable", detail: "daemon owner is still running or starting" };
621
781
  }
622
- const stale = join(dir, `daemon.lock.${instanceId}.stale`);
782
+ const stale = join2(dir, `daemon.lock.${instanceId}.stale`);
623
783
  let ownsReplacement = false;
624
784
  try {
625
785
  renameSync(lockPath(dir), stale);
@@ -763,8 +923,16 @@ var stopDaemonRunning = async (options = {}) => {
763
923
  safeUnlink(lockPath(dir));
764
924
  return { kind: "stopped" };
765
925
  };
926
+ var writeEntryGuardForService = (input, options) => writeDaemonEntryGuard({
927
+ stateDir: input.stateDir,
928
+ platform: input.platform,
929
+ execPath: input.execPath,
930
+ warn: options.warn ?? ((message) => {
931
+ process.stderr.write(message);
932
+ })
933
+ });
766
934
  var DAEMON_LOGIN_REASONS = ["key_rejected", "plan_inactive", "key_missing", "mac_signed_out"];
767
- var needsLoginPath = (dir) => join(dir, "daemon-needs-login.json");
935
+ var needsLoginPath = (dir) => join2(dir, "daemon-needs-login.json");
768
936
  var isLoginReason = (value) => DAEMON_LOGIN_REASONS.includes(value);
769
937
  var parseDaemonNeedsLogin = (value) => {
770
938
  if (typeof value !== "object" || value === null) return null;
@@ -804,7 +972,7 @@ var DAEMON_LOGIN_BLOCK_DESCRIPTIONS = {
804
972
  var describeDaemonLoginBlock = (record) => DAEMON_LOGIN_BLOCK_DESCRIPTIONS[record.reason](record.at);
805
973
  var recoverDaemonAfterLogin = (options = {}) => {
806
974
  const cleared = clearDaemonNeedsLogin(options);
807
- if (!cleared || readControlMode(join(options.dir ?? pusharyDir(), "config.json")) === "off" || !hasDaemonServiceDefinition(options)) {
975
+ if (!cleared || readControlMode(join2(options.dir ?? pusharyDir(), "config.json")) === "off" || !hasDaemonServiceDefinition(options)) {
808
976
  return { cleared, restarted: false, detail: null };
809
977
  }
810
978
  const started = manageDaemonService("start", options);
@@ -878,12 +1046,26 @@ var probeDaemonServiceState = (definition, run) => {
878
1046
  }
879
1047
  };
880
1048
  var previousEncoding = (snapshot) => snapshot.previousEncoding ?? "utf-8";
1049
+ var launchdEntryGuardArguments = (guard) => `<string>${xml(ENTRY_GUARD_PRELOAD_FLAG)}</string><string>${xml(guard)}</string>`;
1050
+ var restorableContents = (definition, contents) => {
1051
+ const guard = definition.startGuardPath;
1052
+ if (!guard || contents === null || readServiceEntryGuard(contents, definition.platform) !== guard) {
1053
+ return { kind: "ready", contents };
1054
+ }
1055
+ const written = writeDaemonEntryGuardAt(guard);
1056
+ if (written.kind === "in-place") return { kind: "ready", contents };
1057
+ const unguarded = contents.replace(launchdEntryGuardArguments(guard), "");
1058
+ return readServiceEntryGuard(unguarded, definition.platform) === null ? { kind: "ready", contents: unguarded } : { kind: "failed", detail: `could not write the start guard ${guard} the previous service names (${written.detail}), or restore that service without it` };
1059
+ };
881
1060
  var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
1061
+ const restorable = restorableContents(definition, snapshot.previousContents);
1062
+ if (restorable.kind !== "ready") return restorable;
1063
+ const previousContents = restorable.contents;
882
1064
  const removed = applyDaemonService("uninstall", definition, { run });
883
1065
  if (removed.kind !== "ok") return removed;
884
1066
  try {
885
- if (snapshot.previousContents === null) safeUnlink(definition.definitionPath);
886
- else writeFileAtomic(definition.definitionPath, snapshot.previousContents, 384, previousEncoding(snapshot));
1067
+ if (previousContents === null) safeUnlink(definition.definitionPath);
1068
+ else writeFileAtomic(definition.definitionPath, previousContents, 384, previousEncoding(snapshot));
887
1069
  } catch (err) {
888
1070
  return {
889
1071
  kind: "failed",
@@ -892,7 +1074,7 @@ var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
892
1074
  }
893
1075
  if (!snapshot.previousRegistered) {
894
1076
  const reload = definition.reloadAfterUninstall;
895
- if (snapshot.previousContents !== null && reload) {
1077
+ if (previousContents !== null && reload) {
896
1078
  try {
897
1079
  run(reload.command, reload.args);
898
1080
  } catch (err) {
@@ -917,12 +1099,12 @@ var restoreDaemonServiceSnapshot = (definition, snapshot, run) => {
917
1099
  }
918
1100
  return { kind: "ok" };
919
1101
  }
920
- if (snapshot.previousContents === null) {
1102
+ if (previousContents === null) {
921
1103
  return { kind: "failed", detail: "the previous registered service had no recoverable definition" };
922
1104
  }
923
1105
  const restored = applyDaemonService("install", {
924
1106
  ...definition,
925
- contents: snapshot.previousContents,
1107
+ contents: previousContents,
926
1108
  encoding: previousEncoding(snapshot)
927
1109
  }, { run });
928
1110
  if (restored.kind !== "ok") return restored;
@@ -971,19 +1153,22 @@ var recoverDaemonServiceTransaction = (definition, path, run) => {
971
1153
  return { kind: "ok" };
972
1154
  };
973
1155
  var replaceDaemonService = async (options = {}) => {
974
- const definition = resolveDaemonServiceDefinition(options);
975
- if (!definition) {
1156
+ const input = resolveDaemonServiceInput(options);
1157
+ const unguarded = input ? buildDaemonService(input) : null;
1158
+ if (!input || !unguarded) {
976
1159
  return {
977
1160
  kind: "unavailable",
978
1161
  detail: "Could not find the globally installed daemon entry. Run Pushary setup again."
979
1162
  };
980
1163
  }
981
1164
  const run = options.run ?? runDaemonServiceCommand;
982
- const transactionPath = join(options.stateDir ?? options.dir ?? pusharyDir(), "daemon-service-transaction.json");
983
- const recovered = recoverDaemonServiceTransaction(definition, transactionPath, run);
1165
+ const transactionPath = join2(options.stateDir ?? options.dir ?? pusharyDir(), "daemon-service-transaction.json");
1166
+ const recovered = recoverDaemonServiceTransaction(unguarded, transactionPath, run);
984
1167
  if (recovered.kind !== "ok") {
985
1168
  return { kind: "failed", detail: `Could not recover interrupted service replacement: ${recovered.detail}` };
986
1169
  }
1170
+ const definition = buildDaemonService({ ...input, entryGuard: writeEntryGuardForService(input, options) });
1171
+ if (!definition) return { kind: "unavailable", detail: "Could not build the daemon service definition for this platform." };
987
1172
  let previousContents = null;
988
1173
  let previousDefinitionEncoding = "utf-8";
989
1174
  try {
@@ -1088,7 +1273,7 @@ var replaceDaemonService = async (options = {}) => {
1088
1273
  var launchInstalledDaemon = (options) => {
1089
1274
  const entryExists = options.entryExists ?? existsSync;
1090
1275
  const argvEntry = process.argv[1];
1091
- const candidate = options.daemonEntry !== void 0 ? options.daemonEntry : argvEntry ? join(dirname(argvEntry), "pushary-daemon.js") : null;
1276
+ const candidate = options.daemonEntry !== void 0 ? options.daemonEntry : argvEntry ? join2(dirname2(argvEntry), "pushary-daemon.js") : null;
1092
1277
  const siblingEntry = candidate && entryExists(candidate) ? candidate : null;
1093
1278
  const platform = options.platform ?? process.platform;
1094
1279
  const daemonBinary = options.daemonBinary ?? resolveGlobalBinary("pushary-daemon") ?? (platform === "win32" ? "pushary-daemon.cmd" : "pushary-daemon");
@@ -1163,6 +1348,11 @@ var waitForDaemonRemoteProof = async (options = {}) => {
1163
1348
  };
1164
1349
 
1165
1350
  export {
1351
+ readServiceExecPath,
1352
+ serviceStartFileChecks,
1353
+ versionManagerOf,
1354
+ describeVersionManagerPin,
1355
+ describeServiceNodeRisk,
1166
1356
  daemonIsServing,
1167
1357
  daemonProcessIsThere,
1168
1358
  DAEMON_COLD_START_TIMEOUT_MS,
@@ -1172,6 +1362,7 @@ export {
1172
1362
  DAEMON_YIELD_CHECK_INTERVAL_MS,
1173
1363
  xml,
1174
1364
  serviceNodeFor,
1365
+ daemonEntryGuardPathFor,
1175
1366
  parseDaemonAction,
1176
1367
  manageDaemonService,
1177
1368
  hasDaemonServiceDefinition,
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  isAdmitted,
25
25
  readAdmissionRules
26
- } from "./chunk-YURIU333.js";
26
+ } from "./chunk-7XD56GXA.js";
27
27
  import {
28
28
  isDeferAnswer,
29
29
  oncePerSession
@@ -61,7 +61,8 @@ var describeService = (effects) => {
61
61
  if (effects.controlMode === "off") return "skip the phone-start background service (--control off)";
62
62
  const where = effects.serviceDefinitionPath ?? "the phone-start service definition";
63
63
  const condition = effects.controlMode === "on" ? "" : ", when a supported agent and a service manager are present";
64
- return `write ${where} and register it with the OS${condition}`;
64
+ const guard = effects.serviceStartGuardPath ? ` and its start guard ${effects.serviceStartGuardPath}` : "";
65
+ return `write ${where}${guard} and register it with the OS${condition}`;
65
66
  };
66
67
  var describeSetupSideEffects = (effects) => [
67
68
  `run npm install -g ${effects.globalPackage}@${effects.version} (the hooks run from the global copy; up to two minutes)`,
@@ -22,11 +22,11 @@ import {
22
22
  turnCodexAppCardsOff,
23
23
  turnCodexAppCardsOn,
24
24
  whyLaunchctlFailed
25
- } from "./chunk-EQZB5P6I.js";
26
- import "./chunk-RPFSVNH6.js";
25
+ } from "./chunk-7NCI2DGB.js";
26
+ import "./chunk-7VKW7OA5.js";
27
27
  import "./chunk-XCEN33UP.js";
28
28
  import "./chunk-QCGNGEGE.js";
29
- import "./chunk-FAF5RDWT.js";
29
+ import "./chunk-MD63J3T7.js";
30
30
  import "./chunk-H6CL3T2S.js";
31
31
  import "./chunk-WNT4JHK3.js";
32
32
  import "./chunk-PNVTPEO5.js";
@@ -11,7 +11,7 @@ import {
11
11
  installedCodexApp,
12
12
  shellQuoted,
13
13
  writeDesktopProxy
14
- } from "./chunk-RPFSVNH6.js";
14
+ } from "./chunk-7VKW7OA5.js";
15
15
  import "./chunk-XCEN33UP.js";
16
16
  import {
17
17
  CODEX_APP_LAUNCH_VARIABLE
package/dist/src/index.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  handlePreToolUse
3
- } from "../chunk-O5T53LZ5.js";
3
+ } from "../chunk-R6N6FREL.js";
4
4
  import "../chunk-UP5QXEUU.js";
5
5
  import "../chunk-G73ACPR2.js";
6
6
  import "../chunk-2R4OVJHS.js";
7
7
  import "../chunk-HY4ATVCT.js";
8
8
  import "../chunk-YQQGIDBC.js";
9
- import "../chunk-YURIU333.js";
9
+ import "../chunk-7XD56GXA.js";
10
10
  import "../chunk-NN43KGTN.js";
11
11
  import "../chunk-H2RE3IPK.js";
12
- import "../chunk-L7Q57R4J.js";
12
+ import "../chunk-6WT5OIRM.js";
13
13
  import "../chunk-VAE5JBR5.js";
14
14
  import "../chunk-G66WIAST.js";
15
15
  import "../chunk-43DT3QWZ.js";
16
- import "../chunk-FAF5RDWT.js";
16
+ import "../chunk-MD63J3T7.js";
17
17
  import "../chunk-TLK5YLIE.js";
18
18
  import {
19
19
  fetchModeOverride,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushary",
3
- "version": "1.9.136",
3
+ "version": "1.9.137",
4
4
  "description": "Human-in-the-loop (HITL) for AI agents: ask a person for approvals or answers by push notification. One command connects Claude Code, Codex, Cursor, Gemini CLI and more.",
5
5
  "keywords": [
6
6
  "pushary",
@@ -1,75 +0,0 @@
1
- import {
2
- configFilePath
3
- } from "./chunk-IKBTG3OT.js";
4
- import {
5
- readJsonSafe,
6
- writeJsonAtomic
7
- } from "./chunk-TODDEIYG.js";
8
- import {
9
- readSetupOutcomeProperties
10
- } from "./chunk-DL67HX3D.js";
11
-
12
- // src/diagnostics/service-runtime.ts
13
- var unescapeXml = (value) => value.replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
14
- var unquoteSystemd = (value) => value.replaceAll('\\"', '"').replaceAll("\\\\", "\\").replaceAll("%%", "%");
15
- var readServiceExecPath = (contents, platform) => {
16
- if (platform === "darwin") {
17
- const program = contents.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/)?.[1];
18
- return program ? unescapeXml(program) : null;
19
- }
20
- if (platform === "linux") {
21
- const exec = contents.match(/^ExecStart="((?:[^"\\]|\\.)*)"/m)?.[1];
22
- return exec ? unquoteSystemd(exec) : null;
23
- }
24
- if (platform === "win32") {
25
- const command = contents.match(/<Command>([^<]*)<\/Command>/)?.[1];
26
- return command ? unescapeXml(command) : null;
27
- }
28
- return null;
29
- };
30
- var MANAGER_SEGMENTS = [
31
- [".nvm", "nvm"],
32
- ["nvm", "nvm"],
33
- ["fnm", "fnm"],
34
- [".fnm", "fnm"],
35
- [".volta", "volta"],
36
- ["volta", "volta"],
37
- [".asdf", "asdf"],
38
- ["asdf", "asdf"],
39
- ["Cellar", "homebrew"]
40
- ];
41
- var versionManagerOf = (execPath) => {
42
- const segments = execPath.split(/[/\\]/);
43
- for (const segment of segments) {
44
- const hit = MANAGER_SEGMENTS.find(([name]) => name === segment);
45
- if (hit) return hit[1];
46
- }
47
- return null;
48
- };
49
- var describeVersionManagerPin = (execPath, manager) => `The background service is pinned to ${execPath}, which ${manager} manages. Switching or upgrading Node strands it; run pushary daemon install afterwards.`;
50
- var describeServiceNodeRisk = (manager) => manager === "homebrew" ? "a Homebrew version folder; brew upgrade strands the service, and `pushary daemon install` repins it to the stable link" : `managed by ${manager}; a version switch strands the service`;
51
-
52
- // src/setup/failure-record.ts
53
- import { dirname, join } from "path";
54
- var failurePath = () => join(dirname(configFilePath()), "last-setup-failure.json");
55
- var saveSetupFailure = (body, path = failurePath()) => {
56
- const properties = readSetupOutcomeProperties(body);
57
- if (properties.exitCode === null || properties.exitCode === 0) return;
58
- writeJsonAtomic(path, { ...properties, recordedAt: (/* @__PURE__ */ new Date()).toISOString() }, 384);
59
- };
60
- var readSetupFailure = (path = failurePath()) => {
61
- const result = readJsonSafe(path);
62
- if (result.kind !== "ok" || typeof result.value !== "object" || result.value === null || Array.isArray(result.value)) return null;
63
- const body = result.value;
64
- if (!("recordedAt" in body) || typeof body.recordedAt !== "string" || body.recordedAt.length > 30 || !Number.isFinite(Date.parse(body.recordedAt))) return null;
65
- return { ...readSetupOutcomeProperties({ ...body }), recordedAt: body.recordedAt };
66
- };
67
-
68
- export {
69
- saveSetupFailure,
70
- readSetupFailure,
71
- readServiceExecPath,
72
- versionManagerOf,
73
- describeVersionManagerPin,
74
- describeServiceNodeRisk
75
- };