unsnooze 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +42 -3
- package/bin/unsnooze.js +37 -3
- package/package.json +12 -2
- package/src/settings.js +2 -0
- package/src/state.js +7 -1
- package/src/update-check.js +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0 — 2026-07-12
|
|
4
|
+
|
|
5
|
+
- **`unsnooze update`**: one command to update unsnooze itself — runs
|
|
6
|
+
`npm install -g unsnooze@latest` and immediately prints the new version's
|
|
7
|
+
changelog. Update notices and the daemon toast now say `run: unsnooze
|
|
8
|
+
update` instead of the raw npm command.
|
|
9
|
+
|
|
10
|
+
## 1.4.0 — 2026-07-12
|
|
11
|
+
|
|
12
|
+
- **Update notices**: unsnooze now checks the npm registry (at most once a
|
|
13
|
+
day, a plain GET with nothing identifying) and tells you when a new version
|
|
14
|
+
is out — a one-line notice after CLI commands, and a single desktop toast
|
|
15
|
+
per version from the daemon. After you update, the next command shows a
|
|
16
|
+
short "what's new" straight from the bundled changelog. Turn it all off
|
|
17
|
+
with `unsnooze config set updateCheck off`.
|
|
18
|
+
|
|
3
19
|
## 1.3.0 — 2026-07-11
|
|
4
20
|
|
|
5
21
|
- **Per-session wake messages**: `unsnooze message <id|--all> "<text>"` sets a
|
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
[](package.json)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
**Claude Code · Codex CLI · Grok** — when they hit
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
**Claude Code · Codex CLI · Grok** — when they hit the 5-hour or weekly usage limit
|
|
13
|
+
("You've hit your usage limit"), your session just… stops.<br/>
|
|
14
|
+
unsnooze auto-resumes them: it tracks **every** limit-stopped session across all
|
|
15
|
+
your projects and **wakes each one up in tmux the moment the usage limit resets.**
|
|
15
16
|
|
|
16
17
|
```sh
|
|
17
18
|
npm install -g unsnooze && unsnooze setup
|
|
@@ -132,6 +133,7 @@ unsnooze message <id> "text" # per-session wake message (--clear to reset)
|
|
|
132
133
|
unsnooze config list # settings (see below)
|
|
133
134
|
unsnooze config set <k> <v> # e.g. autoResume off
|
|
134
135
|
unsnooze logs [-f] # what unsnooze has been doing
|
|
136
|
+
unsnooze update # update unsnooze itself
|
|
135
137
|
unsnooze daemon # persistent GUI-session watcher (usually run
|
|
136
138
|
# by launchd/systemd via `install --daemon`)
|
|
137
139
|
unsnooze report [agent] # capture a pane to report an undetected banner
|
|
@@ -153,6 +155,7 @@ unsnooze help # full command list (also -h / --help)
|
|
|
153
155
|
| `resumeMessage` | *"Continue where you left off…"* | The message sent to wake a session. Override it for a single session with `unsnooze message <id> "…"` — visible in `unsnooze status`. |
|
|
154
156
|
| `resumeMessages.claude` / `.codex` / `.grok` | `""` | Per-agent override of `resumeMessage`. Empty = use the global message; clear one with `unsnooze config set resumeMessages.claude ""`. |
|
|
155
157
|
| `agents.claude` / `agents.codex` / `agents.grok` | `true` / `true` / `false` | Which CLIs are guarded. |
|
|
158
|
+
| `updateCheck` | `true` | Daily new-version check (a plain GET to the npm registry, nothing identifying is sent). Notices after commands + one desktop toast per version. |
|
|
156
159
|
|
|
157
160
|
Every setting also has a `UNSNOOZE_*` env override (see `src/settings.js`), and
|
|
158
161
|
all timings/paths are tunable via `UNSNOOZE_*` env vars (see `src/config.js`).
|
|
@@ -200,6 +203,42 @@ unsnooze raises **native Windows toasts** through `powershell.exe` (no
|
|
|
200
203
|
not supported — without tmux there is nothing to watch; unsnooze will tell you
|
|
201
204
|
so and run your CLI unwatched instead of breaking it.
|
|
202
205
|
|
|
206
|
+
## FAQ
|
|
207
|
+
|
|
208
|
+
### What does "You've hit your usage limit" mean in Claude Code?
|
|
209
|
+
|
|
210
|
+
Claude subscription plans meter usage in a rolling 5-hour window plus a weekly
|
|
211
|
+
cap. When either runs out, Claude Code stops mid-task and shows the banner with
|
|
212
|
+
a reset time ("resets 3pm"). Nothing is lost — the session can be resumed with
|
|
213
|
+
`claude --resume <id>` once the limit resets. unsnooze does exactly that,
|
|
214
|
+
automatically, for every stopped session.
|
|
215
|
+
|
|
216
|
+
### What about Codex — "You've hit your usage limit. Try again at …"?
|
|
217
|
+
|
|
218
|
+
Same idea: ChatGPT-plan Codex has 5-hour and weekly windows. The Codex TUI
|
|
219
|
+
stays open at the composer after the banner, so unsnooze either types into the
|
|
220
|
+
live pane or reopens the session with `codex resume <id>` at the reset time it
|
|
221
|
+
parsed from the banner.
|
|
222
|
+
|
|
223
|
+
### Does this get around the rate limit?
|
|
224
|
+
|
|
225
|
+
No. unsnooze waits for the reset exactly like you would, resumes once, and
|
|
226
|
+
verifies the limit actually lifted. It replaces the 4am alarm, not the limit.
|
|
227
|
+
|
|
228
|
+
### How do I update, and how do I know when to?
|
|
229
|
+
|
|
230
|
+
`unsnooze update` (or `npm i -g unsnooze`). unsnooze checks the npm registry at most once a day and
|
|
231
|
+
tells you when a newer version exists (a line after CLI commands, plus one
|
|
232
|
+
desktop notification per version); after updating, the next command shows a
|
|
233
|
+
short "what's new" from the changelog. It's a plain registry GET with nothing
|
|
234
|
+
identifying — turn it off with `unsnooze config set updateCheck off`.
|
|
235
|
+
|
|
236
|
+
### Does it work if my laptop was asleep or the terminal was closed?
|
|
237
|
+
|
|
238
|
+
Yes — reset times are stored as absolute timestamps and checked every 30
|
|
239
|
+
seconds, so a laptop that slept through the reset resumes on the next tick,
|
|
240
|
+
and dead panes are reopened by session id in a fresh tmux window.
|
|
241
|
+
|
|
203
242
|
## Development
|
|
204
243
|
|
|
205
244
|
```sh
|
package/bin/unsnooze.js
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
const [, , cmd, ...rest] = process.argv;
|
|
6
6
|
|
|
7
|
+
// Only human-facing commands may print update notices — never the wrapper
|
|
8
|
+
// passthrough, hooks, or daemons (their output lands in agent panes/logs).
|
|
9
|
+
const USER_FACING = new Set(['status', 'resume-now', 'cancel', 'message', 'config', 'logs', 'report', 'help', '-h', '--help', '--help-unsnooze']);
|
|
10
|
+
|
|
11
|
+
async function maybeUpdateNotices() {
|
|
12
|
+
try {
|
|
13
|
+
const { whatsNewNotice, updateNotice, isCacheStale } = await import('../src/update-check.js');
|
|
14
|
+
const whatsNew = whatsNewNotice();
|
|
15
|
+
if (whatsNew) console.error(`\n${whatsNew}`);
|
|
16
|
+
const notice = updateNotice();
|
|
17
|
+
if (notice) console.error(`\n${notice}`);
|
|
18
|
+
if (isCacheStale()) {
|
|
19
|
+
const { spawnDetached } = await import('../src/spawn.js');
|
|
20
|
+
spawnDetached(['_update-check']);
|
|
21
|
+
}
|
|
22
|
+
} catch { /* update UX must never break a command */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
async function main() {
|
|
8
26
|
switch (cmd) {
|
|
9
27
|
case 'status': {
|
|
@@ -64,6 +82,14 @@ async function main() {
|
|
|
64
82
|
const { runResumer } = await import('../src/resumer.js');
|
|
65
83
|
return runResumer();
|
|
66
84
|
}
|
|
85
|
+
case 'update': {
|
|
86
|
+
const { runSelfUpdate } = await import('../src/update-check.js');
|
|
87
|
+
return runSelfUpdate();
|
|
88
|
+
}
|
|
89
|
+
case '_update-check': {
|
|
90
|
+
const { runUpdateCheck } = await import('../src/update-check.js');
|
|
91
|
+
return runUpdateCheck();
|
|
92
|
+
}
|
|
67
93
|
case 'daemon': {
|
|
68
94
|
// Persistent resumer + transcript watcher: detects and revives limit
|
|
69
95
|
// stops from GUI surfaces (VS Code extension, desktop apps) where no
|
|
@@ -73,6 +99,11 @@ async function main() {
|
|
|
73
99
|
const controller = new AbortController();
|
|
74
100
|
process.on('SIGTERM', () => controller.abort());
|
|
75
101
|
process.on('SIGINT', () => controller.abort());
|
|
102
|
+
// Daily update check from the daemon: GUI-only users never run CLI
|
|
103
|
+
// commands, so this is what gets them the "new version" desktop toast.
|
|
104
|
+
const { spawnDetached } = await import('../src/spawn.js');
|
|
105
|
+
spawnDetached(['_update-check']);
|
|
106
|
+
setInterval(() => spawnDetached(['_update-check']), 24 * 3_600_000).unref();
|
|
76
107
|
return runResumer({ persistent: true, watcher: createWatcher(), signal: controller.signal });
|
|
77
108
|
}
|
|
78
109
|
case 'help':
|
|
@@ -89,11 +120,12 @@ Usage:
|
|
|
89
120
|
unsnooze cancel [id|--all] stop tracking session(s)
|
|
90
121
|
unsnooze message <id|--all> <t> set a per-session wake message (--clear to reset)
|
|
91
122
|
unsnooze logs [-f] show (or follow) the unsnooze log
|
|
123
|
+
unsnooze update update unsnooze itself to the latest version
|
|
92
124
|
unsnooze daemon persistent watcher for GUI sessions (VS Code
|
|
93
125
|
extension, desktop apps) — no tmux needed to
|
|
94
126
|
detect; revival still opens in tmux
|
|
95
127
|
unsnooze config [list|get|set] view or change settings (toggles, global +
|
|
96
|
-
per-agent resume messages)
|
|
128
|
+
per-agent resume messages, updateCheck)
|
|
97
129
|
unsnooze setup interactive setup wizard (agents + toggles)
|
|
98
130
|
unsnooze install [--yes] wire up shell wrappers + hooks (non-interactive)
|
|
99
131
|
unsnooze uninstall [--purge] remove wrappers + hooks (and state with --purge)
|
|
@@ -111,5 +143,7 @@ Usage:
|
|
|
111
143
|
}
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
main().then(code => {
|
|
115
|
-
|
|
146
|
+
main().then(async code => {
|
|
147
|
+
if (USER_FACING.has(cmd)) await maybeUpdateNotices();
|
|
148
|
+
process.exitCode = typeof code === 'number' ? code : 0;
|
|
149
|
+
}).catch(err => { console.error(`unsnooze: ${err.stack || err}`); process.exitCode = 1; });
|
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unsnooze",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Auto-resume Claude Code, Codex CLI & Grok sessions in tmux when the usage limit (5-hour or weekly) resets. Wakes every limit-stopped AI coding session automatically.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"claude",
|
|
6
7
|
"claude-code",
|
|
7
8
|
"codex",
|
|
8
9
|
"codex-cli",
|
|
10
|
+
"openai",
|
|
11
|
+
"anthropic",
|
|
9
12
|
"grok",
|
|
10
13
|
"usage-limit",
|
|
14
|
+
"usage limit",
|
|
11
15
|
"rate-limit",
|
|
16
|
+
"rate limit",
|
|
17
|
+
"5-hour limit",
|
|
12
18
|
"auto-resume",
|
|
19
|
+
"auto-retry",
|
|
20
|
+
"resume",
|
|
21
|
+
"retry",
|
|
13
22
|
"tmux",
|
|
14
23
|
"ai-agents",
|
|
24
|
+
"agentic-coding",
|
|
15
25
|
"cli"
|
|
16
26
|
],
|
|
17
27
|
"author": "Saaransh Menon <saaransh.dev2811@gmail.com>",
|
package/src/settings.js
CHANGED
|
@@ -17,6 +17,7 @@ export const DEFAULTS = {
|
|
|
17
17
|
menuAutoAnswer: true, // may unsnooze drive Claude's limit menu (send keys)?
|
|
18
18
|
notifications: true, // desktop notifications on detect/resume
|
|
19
19
|
guiWatch: true, // daemon watches transcripts/rollouts for GUI-session stops
|
|
20
|
+
updateCheck: true, // daily registry version check + update notices/toast
|
|
20
21
|
resumeMessage: 'Continue where you left off. The session was interrupted by a usage limit which has now reset — pick up the task you were working on and finish it.',
|
|
21
22
|
resumeMessages: { claude: '', codex: '', grok: '' }, // per-agent override; '' = use resumeMessage
|
|
22
23
|
agents: { claude: true, codex: true, grok: false }, // grok is experimental
|
|
@@ -28,6 +29,7 @@ const ENV_NAMES = {
|
|
|
28
29
|
menuAutoAnswer: 'UNSNOOZE_MENU_AUTO_ANSWER',
|
|
29
30
|
notifications: 'UNSNOOZE_NOTIFICATIONS',
|
|
30
31
|
guiWatch: 'UNSNOOZE_GUI_WATCH',
|
|
32
|
+
updateCheck: 'UNSNOOZE_UPDATE_CHECK',
|
|
31
33
|
resumeMessage: 'UNSNOOZE_RESUME_MESSAGE',
|
|
32
34
|
'resumeMessages.claude': 'UNSNOOZE_RESUME_MESSAGE_CLAUDE',
|
|
33
35
|
'resumeMessages.codex': 'UNSNOOZE_RESUME_MESSAGE_CODEX',
|
package/src/state.js
CHANGED
|
@@ -96,6 +96,9 @@ export function upsertSession(record) {
|
|
|
96
96
|
...record,
|
|
97
97
|
// Never downgrade a known sessionId to null.
|
|
98
98
|
sessionId: record.sessionId || existing.sessionId,
|
|
99
|
+
// A detection that races an in-flight resume must not flip the record
|
|
100
|
+
// back to 'stopped' — the post-resume verify pass owns that outcome.
|
|
101
|
+
status: existing.status === 'resuming' ? existing.status : record.status,
|
|
99
102
|
key: existingKey,
|
|
100
103
|
};
|
|
101
104
|
state.sessions[existingKey] = merged;
|
|
@@ -114,8 +117,11 @@ function findDuplicate(state, record) {
|
|
|
114
117
|
// Same sessionId living under a pane-based key (a scrape record that later
|
|
115
118
|
// learned its id through a merge).
|
|
116
119
|
if (record.sessionId && s.sessionId === record.sessionId) return key;
|
|
120
|
+
// 'resuming' counts too: while the resumer types into a pane, a scrape can
|
|
121
|
+
// still see the banner for a few hundred ms — that must not fork a second
|
|
122
|
+
// record (it would double-resume the session).
|
|
117
123
|
if (s.pane && s.pane === record.pane
|
|
118
|
-
&& s.status === 'stopped'
|
|
124
|
+
&& (s.status === 'stopped' || s.status === 'resuming')
|
|
119
125
|
&& Math.abs((s.detectedAt || 0) - record.detectedAt) < DEDUPE_WINDOW_MS) {
|
|
120
126
|
return key;
|
|
121
127
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Update notifications. npm can't push updates, so the CLI nudges:
|
|
2
|
+
// - user-facing commands print a one-line notice (from CACHE only — the
|
|
3
|
+
// registry fetch happens in a detached `_update-check`, never inline)
|
|
4
|
+
// - the daemon fires ONE desktop toast per new version
|
|
5
|
+
// - after the user updates, the next command shows "what's new" straight
|
|
6
|
+
// from the CHANGELOG.md bundled in the npm tarball (zero network)
|
|
7
|
+
// All of it is gated on the `updateCheck` setting / UNSNOOZE_UPDATE_CHECK.
|
|
8
|
+
// The check is a plain GET to registry.npmjs.org — nothing identifying.
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { getConfig } from './settings.js';
|
|
15
|
+
import { notify } from './notify.js';
|
|
16
|
+
|
|
17
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
export const PKG_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version;
|
|
19
|
+
|
|
20
|
+
const CACHE_FILE = () => join(process.env.UNSNOOZE_STATE_DIR || join(process.env.HOME || '', '.unsnooze'), 'update-check.json');
|
|
21
|
+
const CHECK_INTERVAL_MS = 24 * 3_600_000;
|
|
22
|
+
const RELEASES_URL = 'https://github.com/saaranshM/unsnooze/releases';
|
|
23
|
+
|
|
24
|
+
export function readCache() {
|
|
25
|
+
try { return JSON.parse(readFileSync(CACHE_FILE(), 'utf-8')); } catch { return {}; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writeCache(patch) {
|
|
29
|
+
const merged = { ...readCache(), ...patch };
|
|
30
|
+
const path = CACHE_FILE();
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
const tmp = join(dirname(path), `.update-check.tmp.${process.pid}`);
|
|
33
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n');
|
|
34
|
+
renameSync(tmp, path);
|
|
35
|
+
return merged;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Plain x.y.z compare. We never publish prereleases; anything that isn't
|
|
39
|
+
// three numbers loses (a garbage registry response must never nag).
|
|
40
|
+
export function isNewer(candidate, current) {
|
|
41
|
+
const parse = v => (typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v)) ? v.split('.').map(Number) : null;
|
|
42
|
+
const a = parse(candidate);
|
|
43
|
+
const b = parse(current);
|
|
44
|
+
if (!a || !b) return false;
|
|
45
|
+
for (let i = 0; i < 3; i++) {
|
|
46
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isCacheStale(now = Date.now()) {
|
|
52
|
+
return now - (readCache().lastCheckedAt || 0) > CHECK_INTERVAL_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function fetchLatest({ fetcher = globalThis.fetch, timeoutMs = 3000 } = {}) {
|
|
56
|
+
try {
|
|
57
|
+
const ctrl = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetcher('https://registry.npmjs.org/unsnooze/latest', {
|
|
61
|
+
signal: ctrl.signal, headers: { accept: 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) return null;
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
return typeof body?.version === 'string' ? body.version : null;
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
return null; // offline, timeout, bad JSON — never the CLI's problem
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Cache-only, instant. Printed to stderr after user-facing commands.
|
|
75
|
+
export function updateNotice(pkgVersion = PKG_VERSION) {
|
|
76
|
+
if (!getConfig('updateCheck')) return null;
|
|
77
|
+
const { latest } = readCache();
|
|
78
|
+
if (!latest || !isNewer(latest, pkgVersion)) return null;
|
|
79
|
+
return `unsnooze ${latest} is available (you have ${pkgVersion}) — run: unsnooze update · ${RELEASES_URL}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The "## <version>" block from the CHANGELOG.md that ships in the tarball.
|
|
83
|
+
export function changelogSection(version, maxLines = 6) {
|
|
84
|
+
try {
|
|
85
|
+
const lines = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf-8').split('\n');
|
|
86
|
+
const start = lines.findIndex(l => l.startsWith(`## ${version}`));
|
|
87
|
+
if (start === -1) return null;
|
|
88
|
+
const rest = lines.slice(start + 1);
|
|
89
|
+
const end = rest.findIndex(l => l.startsWith('## '));
|
|
90
|
+
const section = rest.slice(0, end === -1 ? undefined : end)
|
|
91
|
+
.filter(l => l.trim() !== '')
|
|
92
|
+
.slice(0, maxLines);
|
|
93
|
+
return section.length ? section.join('\n') : null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Once after an update: "you're now on X, here's what changed."
|
|
100
|
+
export function whatsNewNotice(pkgVersion = PKG_VERSION) {
|
|
101
|
+
if (!getConfig('updateCheck')) return null;
|
|
102
|
+
const { lastRunVersion } = readCache();
|
|
103
|
+
if (lastRunVersion === pkgVersion) return null;
|
|
104
|
+
writeCache({ lastRunVersion: pkgVersion });
|
|
105
|
+
if (!lastRunVersion) return null; // first ever run — nothing to announce
|
|
106
|
+
const section = changelogSection(pkgVersion);
|
|
107
|
+
return `Updated to ${pkgVersion}.` + (section ? ` What's new:\n${section}` : ` ${RELEASES_URL}/tag/v${pkgVersion}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The detached `_update-check`: fetch, cache, toast once per new version.
|
|
111
|
+
export async function runUpdateCheck({ fetcher, notifier = notify, now = Date.now() } = {}) {
|
|
112
|
+
if (!getConfig('updateCheck')) return 0;
|
|
113
|
+
const latest = await fetchLatest({ fetcher });
|
|
114
|
+
if (!latest) return 0;
|
|
115
|
+
const cache = writeCache({ lastCheckedAt: now, latest });
|
|
116
|
+
if (isNewer(latest, PKG_VERSION) && cache.notifiedVersion !== latest) {
|
|
117
|
+
notifier('unsnooze update available', `${latest} is out (you have ${PKG_VERSION}) — run: unsnooze update`);
|
|
118
|
+
writeCache({ notifiedVersion: latest });
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// `unsnooze update` — self-update via npm, then show what changed. npm -g
|
|
124
|
+
// overwrites this install in place, so re-reading package.json/CHANGELOG.md
|
|
125
|
+
// after a successful install yields the NEW version's info.
|
|
126
|
+
export function runSelfUpdate({ runner = spawnSync, print = console.log } = {}) {
|
|
127
|
+
print(`unsnooze ${PKG_VERSION} — updating via npm install -g unsnooze@latest …`);
|
|
128
|
+
const r = runner('npm', ['install', '-g', 'unsnooze@latest'], { stdio: 'inherit' });
|
|
129
|
+
if (r.error || r.status !== 0) {
|
|
130
|
+
print(`unsnooze: update failed (${r.error ? r.error.message : `npm exited ${r.status}`}).`);
|
|
131
|
+
print('unsnooze: try it manually: npm install -g unsnooze@latest');
|
|
132
|
+
print('unsnooze: (permission errors usually mean your npm prefix needs sudo or a user-writable prefix)');
|
|
133
|
+
return r.status || 1;
|
|
134
|
+
}
|
|
135
|
+
let newVersion = PKG_VERSION;
|
|
136
|
+
try {
|
|
137
|
+
newVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version;
|
|
138
|
+
} catch { /* moved install dir — the generic message below still holds */ }
|
|
139
|
+
if (newVersion === PKG_VERSION) {
|
|
140
|
+
print(`unsnooze: already up to date (${PKG_VERSION}).`);
|
|
141
|
+
} else {
|
|
142
|
+
const section = changelogSection(newVersion);
|
|
143
|
+
print(`unsnooze: updated to ${newVersion}.${section ? ` What's new:\n${section}` : ''}`);
|
|
144
|
+
}
|
|
145
|
+
// Don't repeat "what's new" on the next command.
|
|
146
|
+
writeCache({ lastRunVersion: newVersion });
|
|
147
|
+
return 0;
|
|
148
|
+
}
|