unsnooze 1.12.1 → 1.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.12.2 — 2026-07-16
6
+
7
+ - **Daemon autostart self-heal**: updating via npm never touches the
8
+ launchd/systemd unit file, so pre-1.12 users would keep the PATH-less unit
9
+ (daemon can't find tmux → every revival dies) until they manually re-ran
10
+ `unsnooze install --daemon`. The daemon now detects a PATH-less unit on
11
+ startup, regenerates it, and reloads itself — every affected user is fixed
12
+ automatically on their first daemon restart after updating (at the latest,
13
+ the next reboot). Manual `unsnooze install --daemon` still works and is no
14
+ longer required.
15
+
5
16
  ## 1.12.1 — 2026-07-16
6
17
 
7
18
  - CI-only: pin the platform in a doctor test whose `launchctl` assertion
package/bin/unsnooze.js CHANGED
@@ -199,6 +199,21 @@ async function main() {
199
199
  const controller = new AbortController();
200
200
  process.on('SIGTERM', () => controller.abort());
201
201
  process.on('SIGINT', () => controller.abort());
202
+ // Self-heal pre-1.12 autostart units: they lack PATH, so this daemon
203
+ // cannot find tmux and every revival dies. Healing rewrites the unit
204
+ // and reloads it — which intentionally kills THIS process; the
205
+ // supervisor restarts us under the fixed unit. One-time: healed units
206
+ // pass the check forever after.
207
+ const installMod = await safeImport('../src/install.js');
208
+ if (installMod?.healDaemonAutostart) {
209
+ try {
210
+ const healed = installMod.healDaemonAutostart();
211
+ if (healed) {
212
+ const lm = await safeImport('../src/logger.js');
213
+ lm?.log('daemon', `autostart unit lacked PATH — regenerated ${healed}, reloading (self-heal)`);
214
+ }
215
+ } catch { /* heal is best-effort — a broken unit must not block the daemon */ }
216
+ }
202
217
  // daemon.log is launchd/systemd-captured stdout+stderr. launchd holds
203
218
  // an open fd on it for our whole lifetime, so rotation must be
204
219
  // copy-truncate: renaming would leave launchd appending to the renamed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unsnooze",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "description": "Auto-resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode & Antigravity when the 5-hour/weekly usage limit resets — tmux, Zellij, VS Code & desktop apps. Wakes every limit-stopped AI coding session automatically.",
5
5
  "keywords": [
6
6
  "claude",
package/src/install.js CHANGED
@@ -244,6 +244,28 @@ export function installDaemonAutostart({ platform = process.platform, dir = null
244
244
  return null; // native Windows: no supported multiplexer to revive into
245
245
  }
246
246
 
247
+ // One-time self-heal for units written before 1.12.0: they carry no PATH, so
248
+ // the daemon cannot find tmux (launchd gives daemons /usr/bin:/bin:… only) and
249
+ // every revival dies with spawn ENOENT. npm updates never touch the unit file,
250
+ // so the daemon repairs it on startup: regenerate + reload. Reloading kills
251
+ // the calling daemon — by design; the supervisor restarts it under the fixed
252
+ // unit. Returns the healed target, or null when nothing needed healing.
253
+ export function healDaemonAutostart({ platform = process.platform, dir = null, activate = defaultActivate } = {}) {
254
+ const marker = platform === 'darwin' ? 'EnvironmentVariables' : 'Environment="PATH=';
255
+ const target = platform === 'darwin'
256
+ ? join(dir || autostartDir(platform), `${DAEMON_LABEL}.plist`)
257
+ : platform === 'linux'
258
+ ? join(dir || autostartDir(platform), 'unsnooze.service')
259
+ : null;
260
+ if (!target || !existsSync(target)) return null; // not a daemon-autostart user
261
+ try {
262
+ if (readFileSync(target, 'utf-8').includes(marker)) return null; // already current
263
+ } catch {
264
+ return null; // unreadable — leave it alone
265
+ }
266
+ return installDaemonAutostart({ platform, dir, activate });
267
+ }
268
+
247
269
  export function uninstallDaemonAutostart({ platform = process.platform, dir = null, activate = defaultActivate } = {}) {
248
270
  if (platform === 'darwin') {
249
271
  const target = join(dir || autostartDir(platform), `${DAEMON_LABEL}.plist`);