memtrace 0.3.37 → 0.3.39
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/bin/memtrace.js +131 -64
- package/lib/update-check.js +161 -0
- package/lib/update-prompt.js +102 -0
- package/package.json +4 -4
package/bin/memtrace.js
CHANGED
|
@@ -5,8 +5,11 @@ const os = require("os");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const { spawnSync, spawn } = require("child_process");
|
|
8
|
+
const readline = require("readline");
|
|
8
9
|
const { getBinaryPath } = require("../install.js");
|
|
9
10
|
const { platformBinary, spawnOptionsForPlatform } = require("../lib/spawn-helper");
|
|
11
|
+
const { shouldPromptForUpgrade, isPromptDisabled } = require("../lib/update-prompt");
|
|
12
|
+
const { fetchLatestVersion, readCachedVersion } = require("../lib/update-check");
|
|
10
13
|
|
|
11
14
|
// ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
|
|
12
15
|
// npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
|
|
@@ -127,58 +130,27 @@ try {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// ── Update checker ────────────────────────────────────────────────────────────
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
//
|
|
134
|
+
// Policy (v0.3.39+): always TRY a fresh HTTP fetch first (1.5s timeout —
|
|
135
|
+
// invisible to humans). On any failure (timeout, network, 5xx, parse
|
|
136
|
+
// error), fall back to the on-disk cache. The cache also serves as the
|
|
137
|
+
// Node ↔ Rust IPC handoff for the MCP banner: the Rust binary reads
|
|
138
|
+
// `~/.memtrace/update-check.json` to decide whether to surface an
|
|
139
|
+
// "update available" line in serverInfo.instructions for agents.
|
|
140
|
+
//
|
|
141
|
+
// We dropped the previous 24h TTL because it directly contradicted the
|
|
142
|
+
// visibility goal: a user installing v0.3.36 the hour we shipped v0.3.37
|
|
143
|
+
// would have waited 23 hours before their banner reflected reality.
|
|
144
|
+
//
|
|
145
|
+
// Implementation lives in lib/update-check.js and is unit + property
|
|
146
|
+
// tested with injectable httpGet / cache path.
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
return JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function writeUpdateCache(data) {
|
|
145
|
-
try {
|
|
146
|
-
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
147
|
-
fs.writeFileSync(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
148
|
-
} catch {
|
|
149
|
-
// non-fatal
|
|
150
|
-
}
|
|
151
|
-
}
|
|
148
|
+
const CACHE_FILE = path.join(os.homedir(), ".memtrace", "update-check.json");
|
|
152
149
|
|
|
153
150
|
function checkForUpdate(currentVersion) {
|
|
154
|
-
return
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL) {
|
|
158
|
-
resolve(cache.latestVersion !== currentVersion ? cache.latestVersion : null);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const https = require("https");
|
|
163
|
-
const req = https.get(
|
|
164
|
-
"https://registry.npmjs.org/memtrace/latest",
|
|
165
|
-
{ headers: { Accept: "application/json" }, timeout: 3000 },
|
|
166
|
-
(res) => {
|
|
167
|
-
let body = "";
|
|
168
|
-
res.on("data", (chunk) => (body += chunk));
|
|
169
|
-
res.on("end", () => {
|
|
170
|
-
try {
|
|
171
|
-
const latest = JSON.parse(body).version;
|
|
172
|
-
writeUpdateCache({ checkedAt: Date.now(), latestVersion: latest });
|
|
173
|
-
resolve(latest !== currentVersion ? latest : null);
|
|
174
|
-
} catch {
|
|
175
|
-
resolve(null);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
);
|
|
180
|
-
req.on("error", () => resolve(null));
|
|
181
|
-
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
151
|
+
return fetchLatestVersion({ cachePath: CACHE_FILE }).then((latest) => {
|
|
152
|
+
if (!latest) return null;
|
|
153
|
+
return latest !== currentVersion ? latest : null;
|
|
182
154
|
});
|
|
183
155
|
}
|
|
184
156
|
|
|
@@ -199,6 +171,91 @@ const updateCheckPromise = checkForUpdate(currentVersion).then((latest) => {
|
|
|
199
171
|
}
|
|
200
172
|
});
|
|
201
173
|
|
|
174
|
+
// ── Interactive upgrade prompt for `memtrace start` / `memtrace index` ───
|
|
175
|
+
//
|
|
176
|
+
// `memtrace mcp` is non-interactive (agents would hang) so we never
|
|
177
|
+
// prompt there — that path is handled by the Rust binary's banner in
|
|
178
|
+
// serverInfo.instructions. For interactive long-running commands, if
|
|
179
|
+
// our cached check found a newer version on npm, ask once before
|
|
180
|
+
// delegating to the binary. ENTER defaults to "no" to keep automation
|
|
181
|
+
// scripts unblocked. Set MEMTRACE_NO_UPDATE_PROMPT=1 to opt out.
|
|
182
|
+
async function maybePromptForUpgrade(command) {
|
|
183
|
+
// Wait briefly for the in-flight check to populate the cache, but
|
|
184
|
+
// don't hold the user up if the network is slow. 1.5s budget — if
|
|
185
|
+
// the check hasn't completed, fall back to whatever was cached.
|
|
186
|
+
await Promise.race([
|
|
187
|
+
updateCheckPromise,
|
|
188
|
+
new Promise((r) => setTimeout(r, 1500)),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
// After awaiting the in-flight check, the cache file holds whatever
|
|
192
|
+
// the fresh fetch produced (or last-known-good if it failed). Use
|
|
193
|
+
// readCachedVersion as the single source of truth here.
|
|
194
|
+
const cachedLatest = readCachedVersion(CACHE_FILE);
|
|
195
|
+
|
|
196
|
+
const decision = shouldPromptForUpgrade({
|
|
197
|
+
command,
|
|
198
|
+
isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
199
|
+
cachedLatestVersion: cachedLatest,
|
|
200
|
+
currentVersion,
|
|
201
|
+
suppressEnv: isPromptDisabled(process.env),
|
|
202
|
+
});
|
|
203
|
+
if (!decision) return false;
|
|
204
|
+
|
|
205
|
+
// Ask. Default = no.
|
|
206
|
+
const rl = readline.createInterface({
|
|
207
|
+
input: process.stdin,
|
|
208
|
+
output: process.stdout,
|
|
209
|
+
});
|
|
210
|
+
const answer = await new Promise((resolve) => {
|
|
211
|
+
rl.question(
|
|
212
|
+
`\n[memtrace] Update available: ${currentVersion} → ${cachedLatest}\n` +
|
|
213
|
+
` Upgrade now? [y/N] `,
|
|
214
|
+
resolve,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
rl.close();
|
|
218
|
+
|
|
219
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
220
|
+
process.stdout.write(
|
|
221
|
+
`[memtrace] Continuing with ${currentVersion}. ` +
|
|
222
|
+
`Run \x1b[1mmemtrace install\x1b[0m anytime to upgrade.\n\n`,
|
|
223
|
+
);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Run npm install -g memtrace@latest, then re-exec the user's
|
|
228
|
+
// original command via the new binary on PATH.
|
|
229
|
+
process.stdout.write("[memtrace] Upgrading…\n");
|
|
230
|
+
const npmCmd = platformBinary("npm", process.platform);
|
|
231
|
+
const installResult = spawnSync(
|
|
232
|
+
npmCmd,
|
|
233
|
+
["install", "-g", "memtrace@latest"],
|
|
234
|
+
spawnOptionsForPlatform(process.platform, {
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
env: process.env,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
if (installResult.status !== 0) {
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
"[memtrace] Upgrade failed; continuing with current version.\n\n",
|
|
242
|
+
);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Chain into the new binary.
|
|
247
|
+
const memtraceCmd = platformBinary("memtrace", process.platform);
|
|
248
|
+
const runResult = spawnSync(
|
|
249
|
+
memtraceCmd,
|
|
250
|
+
args,
|
|
251
|
+
spawnOptionsForPlatform(process.platform, {
|
|
252
|
+
stdio: "inherit",
|
|
253
|
+
env: process.env,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
process.exit(runResult.status ?? 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
202
259
|
if (args[0] === "mcp") {
|
|
203
260
|
// MCP mode: async spawn so Node's event loop keeps running long enough
|
|
204
261
|
// for the update check to print its notice to the agent's stderr.
|
|
@@ -216,19 +273,29 @@ if (args[0] === "mcp") {
|
|
|
216
273
|
});
|
|
217
274
|
|
|
218
275
|
} else {
|
|
219
|
-
// All other commands: synchronous pass-through
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
276
|
+
// All other commands: synchronous pass-through, but FIRST give the
|
|
277
|
+
// user a chance to upgrade if cached check found a newer version.
|
|
278
|
+
// The prompt is gated on TTY + start/index command + cache says newer
|
|
279
|
+
// (see lib/update-prompt.js for the gating logic). When the user
|
|
280
|
+
// accepts, maybePromptForUpgrade calls process.exit() and we never
|
|
281
|
+
// reach the spawn below.
|
|
282
|
+
(async () => {
|
|
283
|
+
try {
|
|
284
|
+
await maybePromptForUpgrade(args[0]);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
// Prompting must never block the user's command. Eat any error.
|
|
287
|
+
process.stderr.write(
|
|
288
|
+
`[memtrace] Update prompt failed (continuing): ${e.message}\n`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const result = spawnSync(binaryPath, args, {
|
|
292
|
+
stdio: "inherit",
|
|
293
|
+
env: process.env,
|
|
294
|
+
});
|
|
295
|
+
if (result.error) {
|
|
296
|
+
console.error(`Failed to run memtrace: ${result.error.message}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
process.exit(result.status ?? 0);
|
|
300
|
+
})();
|
|
234
301
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Update-check with cache-as-fallback semantics.
|
|
4
|
+
//
|
|
5
|
+
// Pre-v0.3.39 we cached the npm-registry result for 24h to avoid
|
|
6
|
+
// hammering the registry. The downside: a user installing v0.3.36 the
|
|
7
|
+
// hour we shipped v0.3.37 wouldn't see the upgrade banner for 23 more
|
|
8
|
+
// hours. That defeats the entire visibility goal.
|
|
9
|
+
//
|
|
10
|
+
// New policy:
|
|
11
|
+
// 1. Always TRY a fresh HTTP fetch (1.5s timeout — invisible to humans).
|
|
12
|
+
// 2. On success, overwrite the cache and return fresh value.
|
|
13
|
+
// 3. On any failure (timeout, network error, non-2xx, parse error),
|
|
14
|
+
// fall back to whatever the cache last contained.
|
|
15
|
+
// 4. The cache still serves as the Node ↔ Rust IPC handoff: the Rust
|
|
16
|
+
// MCP banner reads `~/.memtrace/update-check.json` to decide
|
|
17
|
+
// whether to surface "update available" in `serverInfo.instructions`.
|
|
18
|
+
//
|
|
19
|
+
// All IO is dependency-injected so tests can drive arbitrary network
|
|
20
|
+
// failure modes deterministically.
|
|
21
|
+
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const https = require("https");
|
|
25
|
+
|
|
26
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/memtrace/latest";
|
|
27
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the JSON cache file. Returns the parsed object, or null on any
|
|
31
|
+
* IO/parse error. Pure (only file IO at the path you give it).
|
|
32
|
+
*/
|
|
33
|
+
function readUpdateCache(cachePath) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write the cache atomically-ish (mkdir + writeFile). Errors are
|
|
43
|
+
* swallowed — the cache is opportunistic; failing to write must not
|
|
44
|
+
* break the user's command. Returns true if write succeeded, false on
|
|
45
|
+
* any error.
|
|
46
|
+
*/
|
|
47
|
+
function writeUpdateCache(cachePath, data) {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(cachePath, JSON.stringify(data), "utf-8");
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the latest memtrace version on npm with cache fallback.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} opts
|
|
61
|
+
* @param {string} opts.cachePath absolute path to cache file
|
|
62
|
+
* @param {function(string): Promise<{body: string}>} [opts.httpGet] injectable for tests
|
|
63
|
+
* @param {number} [opts.timeoutMs] fetch timeout (default 1500)
|
|
64
|
+
* @param {function(): number} [opts.now] time source (default Date.now)
|
|
65
|
+
* @returns {Promise<string|null>} latest version string, or null when
|
|
66
|
+
* fresh failed AND cache is empty/missing
|
|
67
|
+
*/
|
|
68
|
+
function fetchLatestVersion(opts = {}) {
|
|
69
|
+
if (!opts.cachePath) {
|
|
70
|
+
return Promise.reject(new Error("fetchLatestVersion: opts.cachePath is required"));
|
|
71
|
+
}
|
|
72
|
+
const httpGet = opts.httpGet || defaultHttpGet;
|
|
73
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
74
|
+
const now = opts.now || Date.now;
|
|
75
|
+
const cachePath = opts.cachePath;
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
let settled = false;
|
|
79
|
+
const finish = (value) => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve(value);
|
|
84
|
+
};
|
|
85
|
+
const fallback = () => finish(readCachedVersion(cachePath));
|
|
86
|
+
|
|
87
|
+
const timer = setTimeout(fallback, timeoutMs);
|
|
88
|
+
|
|
89
|
+
Promise.resolve()
|
|
90
|
+
.then(() => httpGet(NPM_LATEST_URL))
|
|
91
|
+
.then((result) => {
|
|
92
|
+
if (!result || typeof result.body !== "string") {
|
|
93
|
+
fallback();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(result.body);
|
|
99
|
+
} catch {
|
|
100
|
+
fallback();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!parsed || typeof parsed.version !== "string" || !parsed.version) {
|
|
104
|
+
fallback();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
writeUpdateCache(cachePath, {
|
|
108
|
+
checkedAt: now(),
|
|
109
|
+
latestVersion: parsed.version,
|
|
110
|
+
});
|
|
111
|
+
finish(parsed.version);
|
|
112
|
+
})
|
|
113
|
+
.catch(fallback);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read the cached version string only (returns null if cache absent
|
|
119
|
+
* or shape is wrong). Useful when callers want to read the IPC handoff
|
|
120
|
+
* without triggering a network fetch.
|
|
121
|
+
*/
|
|
122
|
+
function readCachedVersion(cachePath) {
|
|
123
|
+
const cache = readUpdateCache(cachePath);
|
|
124
|
+
if (!cache || typeof cache.latestVersion !== "string" || !cache.latestVersion) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return cache.latestVersion;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Default HTTP fetcher used in production. Resolves with `{body}` on
|
|
132
|
+
* 2xx, rejects on any other status, network error, or socket close.
|
|
133
|
+
*/
|
|
134
|
+
function defaultHttpGet(url) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const req = https.get(
|
|
137
|
+
url,
|
|
138
|
+
{ headers: { Accept: "application/json" } },
|
|
139
|
+
(res) => {
|
|
140
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
141
|
+
res.resume();
|
|
142
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let body = "";
|
|
146
|
+
res.on("data", (chunk) => (body += chunk));
|
|
147
|
+
res.on("end", () => resolve({ body }));
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
req.on("error", reject);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
NPM_LATEST_URL,
|
|
156
|
+
DEFAULT_TIMEOUT_MS,
|
|
157
|
+
fetchLatestVersion,
|
|
158
|
+
readUpdateCache,
|
|
159
|
+
writeUpdateCache,
|
|
160
|
+
readCachedVersion,
|
|
161
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Interactive upgrade-prompt logic for `memtrace start` / `memtrace
|
|
4
|
+
// index`. Pure helpers here; the side-effecting prompt + npm install
|
|
5
|
+
// path lives in `bin/memtrace.js`.
|
|
6
|
+
//
|
|
7
|
+
// Why:
|
|
8
|
+
// `memtrace start` is the most-typed entrypoint for memtrace users.
|
|
9
|
+
// Pre-v0.3.38, our update detection was a stderr line that users
|
|
10
|
+
// easily missed — and ZERO of the MCP-spawned `memtrace mcp` users
|
|
11
|
+
// ever saw it because their stderr was captured by their agent.
|
|
12
|
+
//
|
|
13
|
+
// So users would sit on stale binaries indefinitely. Even after we
|
|
14
|
+
// shipped v0.3.37 (the portable-MCP-runtime fix), users on v0.3.34
|
|
15
|
+
// would never know to upgrade because the only place they'd see
|
|
16
|
+
// the notice was in the agent's MCP log file.
|
|
17
|
+
//
|
|
18
|
+
// v0.3.38 splits the problem in two:
|
|
19
|
+
// 1. MCP startup banner — the Rust binary now injects an "update
|
|
20
|
+
// available" line into `serverInfo.instructions`, so agents
|
|
21
|
+
// read it as ambient context and surface it to users.
|
|
22
|
+
// 2. Interactive prompt for `memtrace start` (this module) — when
|
|
23
|
+
// a user runs `memtrace start` in a TTY, if the cache shows a
|
|
24
|
+
// newer version on npm we prompt for one-keystroke upgrade.
|
|
25
|
+
//
|
|
26
|
+
// We don't auto-upgrade. We always ask. Surprise package upgrades on
|
|
27
|
+
// "memtrace start" would be hostile to users who pin versions.
|
|
28
|
+
|
|
29
|
+
const COMMANDS_WITH_PROMPT = new Set(["start", "index"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decide whether `memtrace start` (or another long-running command)
|
|
33
|
+
* should pause for an interactive upgrade prompt before delegating
|
|
34
|
+
* to the Rust binary. Pure: no IO, no prompting.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} input
|
|
37
|
+
* @param {string} input.command first positional arg, e.g. "start"
|
|
38
|
+
* @param {boolean} input.isTty true iff stdin AND stdout are TTYs
|
|
39
|
+
* @param {string|null} input.cachedLatestVersion from update-check.json
|
|
40
|
+
* @param {string} input.currentVersion running shim's version
|
|
41
|
+
* @param {boolean} [input.suppressEnv] true to honor MEMTRACE_NO_UPDATE_PROMPT
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function shouldPromptForUpgrade(input) {
|
|
45
|
+
if (!input || typeof input !== "object") return false;
|
|
46
|
+
if (input.suppressEnv) return false;
|
|
47
|
+
if (!input.isTty) return false;
|
|
48
|
+
if (!COMMANDS_WITH_PROMPT.has(input.command)) return false;
|
|
49
|
+
if (typeof input.cachedLatestVersion !== "string") return false;
|
|
50
|
+
if (input.cachedLatestVersion.trim() === "") return false;
|
|
51
|
+
if (input.cachedLatestVersion === input.currentVersion) return false;
|
|
52
|
+
if (!isNewerSemver(input.cachedLatestVersion, input.currentVersion)) return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compare two semver strings. Returns true iff `a > b`. Tolerates
|
|
58
|
+
* pre-release suffixes ("0.3.38-rc1") by treating any pre-release as
|
|
59
|
+
* lower than the same-numbered release. Returns false on parse error.
|
|
60
|
+
*/
|
|
61
|
+
function isNewerSemver(a, b) {
|
|
62
|
+
const pa = parseSemver(a);
|
|
63
|
+
const pb = parseSemver(b);
|
|
64
|
+
if (!pa || !pb) return false;
|
|
65
|
+
if (pa.major !== pb.major) return pa.major > pb.major;
|
|
66
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor;
|
|
67
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch;
|
|
68
|
+
// a.pre absent + b.pre present -> a is newer (release > prerelease)
|
|
69
|
+
if (!pa.pre && pb.pre) return true;
|
|
70
|
+
if (pa.pre && !pb.pre) return false;
|
|
71
|
+
if (pa.pre && pb.pre) return pa.pre > pb.pre;
|
|
72
|
+
return false; // equal
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSemver(s) {
|
|
76
|
+
if (typeof s !== "string") return null;
|
|
77
|
+
const m = s.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
78
|
+
if (!m) return null;
|
|
79
|
+
return {
|
|
80
|
+
major: parseInt(m[1], 10),
|
|
81
|
+
minor: parseInt(m[2], 10),
|
|
82
|
+
patch: parseInt(m[3], 10),
|
|
83
|
+
pre: m[4] || null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check whether MEMTRACE_NO_UPDATE_PROMPT is set to a truthy value.
|
|
89
|
+
* Pure (takes the env object as input for testability).
|
|
90
|
+
*/
|
|
91
|
+
function isPromptDisabled(env) {
|
|
92
|
+
const v = (env && env.MEMTRACE_NO_UPDATE_PROMPT) || "";
|
|
93
|
+
return ["1", "true", "yes", "on"].includes(String(v).toLowerCase());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
COMMANDS_WITH_PROMPT,
|
|
98
|
+
shouldPromptForUpgrade,
|
|
99
|
+
isNewerSemver,
|
|
100
|
+
parseSemver,
|
|
101
|
+
isPromptDisabled,
|
|
102
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
4
4
|
"description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fs-extra": "^11.0.0"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@memtrace/darwin-arm64": "0.3.
|
|
43
|
-
"@memtrace/linux-x64": "0.3.
|
|
44
|
-
"@memtrace/win32-x64": "0.3.
|
|
42
|
+
"@memtrace/darwin-arm64": "0.3.39",
|
|
43
|
+
"@memtrace/linux-x64": "0.3.39",
|
|
44
|
+
"@memtrace/win32-x64": "0.3.39"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=18"
|