unsnooze 1.19.4 → 1.19.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/package.json +1 -1
- package/src/agents/agy.js +4 -23
- package/src/hook.js +2 -1
- package/src/monitor.js +1 -4
- package/src/multiplexers/tmux.js +65 -27
- package/src/resumer.js +0 -6
- package/src/update-check.js +47 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.19.5 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
Sessions resume again under psmux, Antigravity no longer wakes early, and
|
|
6
|
+
`unsnooze update` updates the copy you are running.
|
|
7
|
+
|
|
8
|
+
### psmux (tmux on native Windows)
|
|
9
|
+
|
|
10
|
+
- Every Claude revival in psmux failed five times with `ready timeout`. psmux
|
|
11
|
+
ran the launch command through a shell, so the space in
|
|
12
|
+
`C:\Program Files\nodejs\node.exe` closed the window before Claude started.
|
|
13
|
+
Each revival now opens its own psmux session with its arguments passed
|
|
14
|
+
through as-is, which also keeps a Codex resume message in one piece (#43).
|
|
15
|
+
- A resume message can no longer be typed into an unrelated window. psmux
|
|
16
|
+
sends keys for a pane that has closed to some other pane, and every psmux
|
|
17
|
+
session numbers its first pane `%1`. unsnooze now addresses panes as
|
|
18
|
+
`session:%N` and checks the pane still exists before reading or typing (#43).
|
|
19
|
+
|
|
20
|
+
### Antigravity (`agy`)
|
|
21
|
+
|
|
22
|
+
- A session could be woken hours before its quota came back. The `Resets in
|
|
23
|
+
3h` countdown was dated from the last prompt sent in that folder, which can
|
|
24
|
+
be much older than the banner. It is now dated from when unsnooze reads the
|
|
25
|
+
banner, so a wake can be slightly late but not early (#40).
|
|
26
|
+
|
|
27
|
+
### `unsnooze update`
|
|
28
|
+
|
|
29
|
+
- With two npm installs on one machine (for example unsnooze under `~/.local`
|
|
30
|
+
and nvm's npm first on PATH), the update went into the other npm's folder
|
|
31
|
+
and the running copy never changed. The update now targets the folder the
|
|
32
|
+
running copy was installed into (#41).
|
|
33
|
+
- It no longer prints "already up to date" when a newer version is known and
|
|
34
|
+
this copy did not change. It says what happened and exits with an error (#41).
|
|
35
|
+
|
|
3
36
|
## 1.19.4 — 2026-09-30
|
|
4
37
|
|
|
5
38
|
Antigravity's new quota banner, and a shorter README.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unsnooze",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.5",
|
|
4
4
|
"description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, Antigravity, and Cursor CLI sessions when 5-hour or weekly usage limits reset—on macOS, Linux and Windows, with or without a terminal multiplexer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/agents/agy.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// Deferred by design: the hooks.json channel (schema drifted between builds)
|
|
13
13
|
// and the loopback RetrieveUserQuotaSummary endpoint (authoritative per-meter
|
|
14
14
|
// resetTime) — the latter is the natural future resetProbe seam.
|
|
15
|
+
//
|
|
16
|
+
// No latestBannerAt: history.jsonl only records prompts, never banners, so its
|
|
17
|
+
// newest timestamp can predate the banner by hours. Anchoring a relative
|
|
18
|
+
// "Resets in 3h" to it fires the wake early. Countdowns anchor to scrape time.
|
|
15
19
|
|
|
16
20
|
import { openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
17
21
|
import { homedir } from 'node:os';
|
|
@@ -106,28 +110,6 @@ export function latestSessionId(cwd, aroundTs = null, agyDir = AGY_DIR()) {
|
|
|
106
110
|
return recentIds.size === 1 ? recentIds.values().next().value : null;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
export function latestBannerAt(cwd, aroundTs = null, agyDir = AGY_DIR()) {
|
|
110
|
-
const text = tailBytes(join(agyDir, 'history.jsonl'));
|
|
111
|
-
if (!text) return null;
|
|
112
|
-
const lines = text.split('\n').filter(l => l.trim());
|
|
113
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
114
|
-
let entry;
|
|
115
|
-
try { entry = JSON.parse(lines[i]); } catch { continue; }
|
|
116
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
117
|
-
if (!Object.values(entry).some(v => v === cwd)) continue;
|
|
118
|
-
const timestamp = typeof entry.timestamp === 'number'
|
|
119
|
-
? entry.timestamp
|
|
120
|
-
: typeof entry.timestamp === 'string' ? Date.parse(entry.timestamp) : NaN;
|
|
121
|
-
if (Number.isFinite(timestamp)) {
|
|
122
|
-
if (!Number.isFinite(aroundTs)) return timestamp;
|
|
123
|
-
if (timestamp <= (aroundTs + 5000) && (aroundTs - timestamp) <= SESSION_RECENCY_MS) {
|
|
124
|
-
return timestamp;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
113
|
export default {
|
|
132
114
|
id: 'agy',
|
|
133
115
|
name: 'Antigravity CLI (Google)',
|
|
@@ -143,7 +125,6 @@ export default {
|
|
|
143
125
|
// v1: every agent launches the bare TUI and gets the prompt typed once idle.
|
|
144
126
|
launchArgs(message) { return { args: [], messageViaPane: true }; },
|
|
145
127
|
latestSessionId,
|
|
146
|
-
latestBannerAt,
|
|
147
128
|
isForegroundCommand(cmd) {
|
|
148
129
|
return cmd === 'agy' || cmd === 'node' || cmd === 'unsnooze';
|
|
149
130
|
},
|
package/src/hook.js
CHANGED
|
@@ -16,6 +16,7 @@ import { parseResetTime, resetAtMs } from './time-parser.js';
|
|
|
16
16
|
import { upsertSession } from './state.js';
|
|
17
17
|
import { latestRateLimitFromTranscript } from './watchers/claude.js';
|
|
18
18
|
import { getMultiplexer } from './multiplexer.js';
|
|
19
|
+
import { tmuxPaneFromEnv } from './multiplexers/tmux.js';
|
|
19
20
|
import { spawnResumerIfNeeded, ensureDaemon } from './spawn.js';
|
|
20
21
|
import { makeLogger } from './logger.js';
|
|
21
22
|
import { addressHash } from './lease.js';
|
|
@@ -47,7 +48,7 @@ export function hookContext(env = process.env, payload = {}) {
|
|
|
47
48
|
const pane = managedMux
|
|
48
49
|
? (env.UNSNOOZE_PANE || null)
|
|
49
50
|
: (muxName === 'herdr' ? env.HERDR_PANE_ID
|
|
50
|
-
: muxName === 'zellij' ? env.ZELLIJ_PANE_ID : env
|
|
51
|
+
: muxName === 'zellij' ? env.ZELLIJ_PANE_ID : tmuxPaneFromEnv(env) || payload.tmux_pane) || null;
|
|
51
52
|
const paneOwner = muxName === 'herdr'
|
|
52
53
|
? (managedMux ? env.UNSNOOZE_PANE_OWNER : (env.HERDR_SESSION || 'default'))
|
|
53
54
|
: muxName === 'zellij'
|
package/src/monitor.js
CHANGED
|
@@ -113,13 +113,10 @@ export function createMonitor({
|
|
|
113
113
|
detectedAt,
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
const bannerAt = typeof agent.latestBannerAt === 'function'
|
|
117
|
-
? agent.latestBannerAt(cwd, detectedAt)
|
|
118
|
-
: null;
|
|
119
116
|
return {
|
|
120
117
|
resetLine: paneResetLine,
|
|
121
118
|
limitType: paneLimitType,
|
|
122
|
-
bannerAt,
|
|
119
|
+
bannerAt: null,
|
|
123
120
|
sessionId,
|
|
124
121
|
via: null, // filled by caller (scrape/hook)
|
|
125
122
|
detectedAt,
|
package/src/multiplexers/tmux.js
CHANGED
|
@@ -6,9 +6,11 @@ import { resolveSessionName, SessionCreateError } from './session-name.js';
|
|
|
6
6
|
|
|
7
7
|
const execFileAsync = promisify(execFileCb);
|
|
8
8
|
|
|
9
|
+
// windowsHide: the console-less monitor polls through the async path every few
|
|
10
|
+
// seconds (see workspace.js). The sync new-session needs the real console.
|
|
9
11
|
function defaultSpawner(file, args, { sync = false, ...options } = {}) {
|
|
10
12
|
if (sync) return spawnSync(file, args, options);
|
|
11
|
-
return execFileAsync(file, args, options).then(({ stdout }) => stdout);
|
|
13
|
+
return execFileAsync(file, args, { windowsHide: true, ...options }).then(({ stdout }) => stdout);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function envArgs(env = {}) {
|
|
@@ -51,8 +53,43 @@ const SESSION_START_ERROR_RE = new RegExp([
|
|
|
51
53
|
|
|
52
54
|
export { SessionCreateError };
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
// tmux on native Windows is psmux, and three of its differences break revival
|
|
57
|
+
// (verified against psmux 3.3.6, which reports itself as "tmux 3.3.6"):
|
|
58
|
+
// - Command words are joined with spaces and run through pwsh/cmd unless `--`
|
|
59
|
+
// precedes them, and only new-session honours `--` — new-window always goes
|
|
60
|
+
// through the shell. `C:\Program Files\nodejs\node.exe` then dies at
|
|
61
|
+
// "C:\Program" and the window closes before the agent ever starts.
|
|
62
|
+
// - Pane ids are per session: every session's first pane is %1, and a bare
|
|
63
|
+
// `-t %1` means whichever session is newest. `session:%1` is exact.
|
|
64
|
+
// - A target pane that is gone resolves to some other pane: send-keys exits 0
|
|
65
|
+
// and types into it, capture-pane reads it.
|
|
66
|
+
// Real tmux accepts `--` and `session:%N` too, so an MSYS2 tmux is unharmed.
|
|
67
|
+
|
|
68
|
+
// This pane's id as unsnooze addresses it: `session:%N` inside psmux, which
|
|
69
|
+
// exports PSMUX_SESSION; the bare server-global %N under real tmux.
|
|
70
|
+
export function tmuxPaneFromEnv(env = process.env) {
|
|
71
|
+
if (!env.TMUX_PANE) return null;
|
|
72
|
+
return env.PSMUX_SESSION ? `${env.PSMUX_SESSION}:${env.TMUX_PANE}` : env.TMUX_PANE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createTmux({ spawner = defaultSpawner, env = process.env, platform = process.platform } = {}) {
|
|
55
76
|
const run = (...args) => spawner('tmux', args);
|
|
77
|
+
const psmux = platform === 'win32';
|
|
78
|
+
const PANE_FMT = psmux ? '#{session_name}:#{pane_id}' : '#{pane_id}';
|
|
79
|
+
const RAW = psmux ? ['--'] : [];
|
|
80
|
+
// A pane-targeted command, refused for a pane that is gone — real tmux does
|
|
81
|
+
// that itself; psmux would run it against another pane.
|
|
82
|
+
const onPane = async (pane, cmd, ...args) => {
|
|
83
|
+
if (psmux && !(await backend.paneAlive(pane))) throw new Error(`can't find pane: ${pane}`);
|
|
84
|
+
return run(cmd, '-t', pane, ...args);
|
|
85
|
+
};
|
|
86
|
+
const sessionTaken = name => {
|
|
87
|
+
try {
|
|
88
|
+
return spawner('tmux', ['has-session', '-t', name], { sync: true, stdio: 'ignore', env }).status === 0;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
56
93
|
|
|
57
94
|
const backend = {
|
|
58
95
|
name: 'tmux',
|
|
@@ -69,33 +106,33 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
69
106
|
inside() { return !!env.TMUX; },
|
|
70
107
|
currentPaneId() {
|
|
71
108
|
if (env.UNSNOOZE_MUX === 'tmux' && env.UNSNOOZE_PANE) return env.UNSNOOZE_PANE;
|
|
72
|
-
return env
|
|
109
|
+
return tmuxPaneFromEnv(env);
|
|
73
110
|
},
|
|
74
111
|
|
|
75
112
|
async capturePane(pane, lines = 200) {
|
|
76
|
-
return
|
|
113
|
+
return onPane(pane, 'capture-pane', '-p', '-S', `-${lines}`);
|
|
77
114
|
},
|
|
78
115
|
|
|
79
116
|
async capturePaneVisible(pane) {
|
|
80
|
-
return
|
|
117
|
+
return onPane(pane, 'capture-pane', '-p');
|
|
81
118
|
},
|
|
82
119
|
|
|
83
120
|
async sendText(pane, text) {
|
|
84
|
-
await
|
|
121
|
+
await onPane(pane, 'send-keys', '-l', text);
|
|
85
122
|
await new Promise(resolve => setTimeout(resolve, SUBMIT_DELAY_MS));
|
|
86
|
-
await
|
|
123
|
+
await onPane(pane, 'send-keys', 'Enter');
|
|
87
124
|
},
|
|
88
125
|
|
|
89
126
|
async sendKey(pane, key) {
|
|
90
|
-
await
|
|
127
|
+
await onPane(pane, 'send-keys', key);
|
|
91
128
|
},
|
|
92
129
|
|
|
93
130
|
async paneAlive(pane) {
|
|
94
131
|
try {
|
|
95
132
|
// tmux 3.7b prints a blank line and exits 0 for a nonexistent target,
|
|
96
|
-
// so the exit code alone is not
|
|
97
|
-
// pane id back.
|
|
98
|
-
const out = await run('display-message', '-t', pane, '-p',
|
|
133
|
+
// and psmux names a different pane, so the exit code alone is not
|
|
134
|
+
// evidence — the output must echo the pane id back.
|
|
135
|
+
const out = await run('display-message', '-t', pane, '-p', PANE_FMT);
|
|
99
136
|
return out.trim() === pane;
|
|
100
137
|
} catch {
|
|
101
138
|
return false;
|
|
@@ -106,7 +143,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
106
143
|
// code (same tmux 3.7b blank-success pitfall as paneAlive).
|
|
107
144
|
async sessionForPane(pane) {
|
|
108
145
|
try {
|
|
109
|
-
const out = (await
|
|
146
|
+
const out = (await onPane(pane, 'display-message', '-p', '#{session_name}')).trim();
|
|
110
147
|
return out || null;
|
|
111
148
|
} catch {
|
|
112
149
|
return null;
|
|
@@ -133,7 +170,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
133
170
|
|
|
134
171
|
async paneTty(pane) {
|
|
135
172
|
try {
|
|
136
|
-
const out = (await
|
|
173
|
+
const out = (await onPane(pane, 'display-message', '-p', '#{pane_tty}')).trim();
|
|
137
174
|
return out || null;
|
|
138
175
|
} catch {
|
|
139
176
|
return null;
|
|
@@ -166,7 +203,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
166
203
|
|
|
167
204
|
async paneCurrentCommand(pane) {
|
|
168
205
|
try {
|
|
169
|
-
return (await
|
|
206
|
+
return (await onPane(pane, 'display-message', '-p', '#{pane_current_command}')).trim();
|
|
170
207
|
} catch {
|
|
171
208
|
return null;
|
|
172
209
|
}
|
|
@@ -178,7 +215,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
178
215
|
// older tmux fails quietly and callers fall back to lease checks.
|
|
179
216
|
async stampPaneOwner(pane, leaseId) {
|
|
180
217
|
try {
|
|
181
|
-
await
|
|
218
|
+
await onPane(pane, 'set-option', '-p', '@unsnooze_owner', String(leaseId));
|
|
182
219
|
return true;
|
|
183
220
|
} catch {
|
|
184
221
|
return false;
|
|
@@ -189,7 +226,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
189
226
|
// both null — trust stdout, not the exit code (tmux 3.7b blank-success).
|
|
190
227
|
async paneOwnerStamp(pane) {
|
|
191
228
|
try {
|
|
192
|
-
const out = (await
|
|
229
|
+
const out = (await onPane(pane, 'display-message', '-p', '#{@unsnooze_owner}')).trim();
|
|
193
230
|
return out || null;
|
|
194
231
|
} catch {
|
|
195
232
|
return null;
|
|
@@ -217,7 +254,7 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
217
254
|
|
|
218
255
|
async listSessionPanes(sessionName) {
|
|
219
256
|
try {
|
|
220
|
-
const out = await run('list-panes', '-t', sessionName, '-F',
|
|
257
|
+
const out = await run('list-panes', '-t', sessionName, '-F', PANE_FMT);
|
|
221
258
|
return out.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
222
259
|
} catch {
|
|
223
260
|
return [];
|
|
@@ -235,7 +272,15 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
235
272
|
async newWindow(sessionName, cwd, launchSpec) {
|
|
236
273
|
// Environment flags require tmux >= 3.0 for new-window and >= 3.2 for
|
|
237
274
|
// new-session. Older tmux fails revival with an "unknown flag -e" error.
|
|
238
|
-
const launch = [...envArgs(launchSpec.env), launchSpec.file, ...(launchSpec.args || [])];
|
|
275
|
+
const launch = [...envArgs(launchSpec.env), ...RAW, launchSpec.file, ...(launchSpec.args || [])];
|
|
276
|
+
// psmux: new-window would run the command through a shell, so each
|
|
277
|
+
// revival opens its own session — resumer and prompt-queue persist it.
|
|
278
|
+
if (psmux) {
|
|
279
|
+
const session = resolveSessionName(sessionName, sessionTaken);
|
|
280
|
+
const pane = await run('new-session', '-d', '-s', session, '-c', cwd,
|
|
281
|
+
'-P', '-F', PANE_FMT, ...launch);
|
|
282
|
+
return { pane: pane.trim(), paneOwner: null, session };
|
|
283
|
+
}
|
|
239
284
|
let pane;
|
|
240
285
|
if (!(await backend.sessionExists(sessionName))) {
|
|
241
286
|
pane = await run('new-session', '-d', '-s', sessionName, '-c', cwd,
|
|
@@ -254,20 +299,13 @@ export function createTmux({ spawner = defaultSpawner, env = process.env } = {})
|
|
|
254
299
|
// tmux in the foreground also gives Ctrl-C to the active pane directly.
|
|
255
300
|
// Its -e environment flags require tmux >= 3.2; older versions fail
|
|
256
301
|
// revival with an "unknown flag -e" error.
|
|
257
|
-
const name = resolveSessionName(wrappedSessionName(env),
|
|
258
|
-
try {
|
|
259
|
-
return spawner('tmux', ['has-session', '-t', candidate],
|
|
260
|
-
{ sync: true, stdio: 'ignore', env }).status === 0;
|
|
261
|
-
} catch {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
});
|
|
302
|
+
const name = resolveSessionName(wrappedSessionName(env), sessionTaken);
|
|
265
303
|
launchSpec.onSessionCreated?.(name);
|
|
266
304
|
// Session name is discovered live via sessionForPane at record-write
|
|
267
305
|
// time — do NOT inject UNSNOOZE_SESSION_NAME (would leak into daemons
|
|
268
306
|
// spawned from the agent via {...process.env}).
|
|
269
307
|
const args = ['new-session', '-s', name,
|
|
270
|
-
...envArgs(launchSpec.env), launchSpec.file, ...(launchSpec.args || [])];
|
|
308
|
+
...envArgs(launchSpec.env), ...RAW, launchSpec.file, ...(launchSpec.args || [])];
|
|
271
309
|
// stderr is piped, not inherited: the tmux client draws its UI on the
|
|
272
310
|
// stdin/stdout tty; stderr carries only tmux's own error messages, and
|
|
273
311
|
// capturing them is how a session-start failure is told apart from the
|
package/src/resumer.js
CHANGED
|
@@ -342,10 +342,6 @@ export async function probeFallback(rec, {
|
|
|
342
342
|
return null;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
if (!bannerAt && typeof agent.latestBannerAt === 'function') {
|
|
346
|
-
bannerAt = agent.latestBannerAt(rec.cwd, now);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
345
|
// Banner still present. If it now parses to a real time, upgrade off fallback.
|
|
350
346
|
if (resetLine) {
|
|
351
347
|
const { at, source } = resetAtMs(parseResetTime(resetLine), {
|
|
@@ -959,8 +955,6 @@ export async function verifyOne(key, { resolveMux = resolveRecordMux } = {}) {
|
|
|
959
955
|
if (fromTx) {
|
|
960
956
|
resetLine = fromTx.resetLine ?? resetLine;
|
|
961
957
|
bannerAt = fromTx.timestampMs;
|
|
962
|
-
} else if (typeof agent.latestBannerAt === 'function') {
|
|
963
|
-
bannerAt = agent.latestBannerAt(rec.cwd, Date.now());
|
|
964
958
|
}
|
|
965
959
|
const { at, source } = resetAtMs(parseResetTime(resetLine), {
|
|
966
960
|
marginMs: RESET_MARGIN_MS,
|
package/src/update-check.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFileSync, existsSync } from 'node:fs';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import { join, dirname } from 'node:path';
|
|
12
|
+
import { join, dirname, posix, win32 } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { getConfig } from './settings.js';
|
|
15
15
|
import { notify } from './notify.js';
|
|
@@ -225,24 +225,59 @@ export async function runUpdateCheck({ fetcher, notifier = notify, now = Date.no
|
|
|
225
225
|
return 0;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
// The npm prefix this copy was installed into, so `unsnooze update` replaces
|
|
229
|
+
// the copy that is running. With two npm prefixes on one machine (nvm's npm
|
|
230
|
+
// first on PATH while unsnooze lives under ~/.local, or Homebrew's npm next
|
|
231
|
+
// to nvm), a bare `npm install -g` writes into npm's own prefix and leaves
|
|
232
|
+
// the copy the wrappers, the StopFailure hook and the daemon unit exec as it
|
|
233
|
+
// was. npm's global layout is <prefix>/lib/node_modules/unsnooze plus a
|
|
234
|
+
// <prefix>/bin/unsnooze link on POSIX, <prefix>\node_modules\unsnooze plus
|
|
235
|
+
// <prefix>\unsnooze.cmd on Windows. Anything else (a checkout or `npm link`,
|
|
236
|
+
// npx, a pnpm/yarn/bun global, a project dependency) is null: npm's default
|
|
237
|
+
// prefix, as before.
|
|
238
|
+
export function installPrefix({ root = ROOT, platform = process.platform, exists = existsSync } = {}) {
|
|
239
|
+
const win = platform === 'win32';
|
|
240
|
+
const path = win ? win32 : posix;
|
|
241
|
+
const modules = path.dirname(root);
|
|
242
|
+
if (path.basename(root) !== 'unsnooze' || path.basename(modules) !== 'node_modules') return null;
|
|
243
|
+
const parent = path.dirname(modules);
|
|
244
|
+
if (win) return exists(path.join(parent, 'unsnooze.cmd')) ? parent : null;
|
|
245
|
+
if (path.basename(parent) !== 'lib') return null;
|
|
246
|
+
const prefix = path.dirname(parent);
|
|
247
|
+
return exists(path.join(prefix, 'bin', 'unsnooze')) ? prefix : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// `unsnooze update` — self-update via npm, then show what changed. With
|
|
251
|
+
// --prefix pointed at this copy's own prefix, npm -g overwrites this install
|
|
252
|
+
// in place, so re-reading package.json/CHANGELOG.md after a successful
|
|
253
|
+
// install yields the NEW version's info.
|
|
254
|
+
export function runSelfUpdate({ runner = spawnSync, print = console.log, root = ROOT, current = PKG_VERSION } = {}) {
|
|
255
|
+
const prefix = installPrefix({ root });
|
|
256
|
+
const args = ['install', '-g', ...(prefix ? ['--prefix', prefix] : []), 'unsnooze@latest'];
|
|
257
|
+
const command = ['npm', ...args].map(a => (/\s/.test(a) ? `"${a}"` : a)).join(' ');
|
|
258
|
+
print(`unsnooze ${current} — updating via ${command} …`);
|
|
259
|
+
const r = runner('npm', args, { stdio: 'inherit' });
|
|
234
260
|
if (r.error || r.status !== 0) {
|
|
235
261
|
print(`unsnooze: update failed (${r.error ? r.error.message : `npm exited ${r.status}`}).`);
|
|
236
|
-
print(
|
|
262
|
+
print(`unsnooze: try it manually: ${command}`);
|
|
237
263
|
print('unsnooze: (permission errors usually mean your npm prefix needs sudo or a user-writable prefix)');
|
|
238
264
|
return r.status || 1;
|
|
239
265
|
}
|
|
240
|
-
let newVersion
|
|
266
|
+
let newVersion;
|
|
241
267
|
try {
|
|
242
|
-
newVersion = JSON.parse(readFileSync(join(
|
|
268
|
+
newVersion = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version;
|
|
243
269
|
} catch { /* moved install dir — the generic message below still holds */ }
|
|
244
|
-
|
|
245
|
-
|
|
270
|
+
// npm exited 0 but this copy is unchanged, while the registry check already
|
|
271
|
+
// knows a newer release: most likely npm installed it into another prefix.
|
|
272
|
+
const { latest } = readCache();
|
|
273
|
+
if (newVersion === current && isNewer(latest, current)) {
|
|
274
|
+
print(`unsnooze: npm finished, but this copy is still ${current} and ${latest} is out.`);
|
|
275
|
+
print(`unsnooze: npm may have installed it into its own prefix (\`npm prefix -g\`) instead of ${root}.`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
newVersion ??= current;
|
|
279
|
+
if (newVersion === current) {
|
|
280
|
+
print(`unsnooze: already up to date (${current}).`);
|
|
246
281
|
} else {
|
|
247
282
|
const section = changelogSection(newVersion);
|
|
248
283
|
print(`unsnooze: updated to ${newVersion}.${section ? ` What's new:\n${section}` : ''}`);
|