memtrace 0.3.37 → 0.3.38

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 CHANGED
@@ -5,8 +5,10 @@ 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");
10
12
 
11
13
  // ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
12
14
  // npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
@@ -199,6 +201,89 @@ const updateCheckPromise = checkForUpdate(currentVersion).then((latest) => {
199
201
  }
200
202
  });
201
203
 
204
+ // ── Interactive upgrade prompt for `memtrace start` / `memtrace index` ───
205
+ //
206
+ // `memtrace mcp` is non-interactive (agents would hang) so we never
207
+ // prompt there — that path is handled by the Rust binary's banner in
208
+ // serverInfo.instructions. For interactive long-running commands, if
209
+ // our cached check found a newer version on npm, ask once before
210
+ // delegating to the binary. ENTER defaults to "no" to keep automation
211
+ // scripts unblocked. Set MEMTRACE_NO_UPDATE_PROMPT=1 to opt out.
212
+ async function maybePromptForUpgrade(command) {
213
+ // Wait briefly for the in-flight check to populate the cache, but
214
+ // don't hold the user up if the network is slow. 1.5s budget — if
215
+ // the check hasn't completed, fall back to whatever was cached.
216
+ await Promise.race([
217
+ updateCheckPromise,
218
+ new Promise((r) => setTimeout(r, 1500)),
219
+ ]);
220
+
221
+ const cache = readUpdateCache();
222
+ const cachedLatest = cache && cache.latestVersion ? cache.latestVersion : null;
223
+
224
+ const decision = shouldPromptForUpgrade({
225
+ command,
226
+ isTty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
227
+ cachedLatestVersion: cachedLatest,
228
+ currentVersion,
229
+ suppressEnv: isPromptDisabled(process.env),
230
+ });
231
+ if (!decision) return false;
232
+
233
+ // Ask. Default = no.
234
+ const rl = readline.createInterface({
235
+ input: process.stdin,
236
+ output: process.stdout,
237
+ });
238
+ const answer = await new Promise((resolve) => {
239
+ rl.question(
240
+ `\n[memtrace] Update available: ${currentVersion} → ${cachedLatest}\n` +
241
+ ` Upgrade now? [y/N] `,
242
+ resolve,
243
+ );
244
+ });
245
+ rl.close();
246
+
247
+ if (!/^y(es)?$/i.test(answer.trim())) {
248
+ process.stdout.write(
249
+ `[memtrace] Continuing with ${currentVersion}. ` +
250
+ `Run \x1b[1mmemtrace install\x1b[0m anytime to upgrade.\n\n`,
251
+ );
252
+ return false;
253
+ }
254
+
255
+ // Run npm install -g memtrace@latest, then re-exec the user's
256
+ // original command via the new binary on PATH.
257
+ process.stdout.write("[memtrace] Upgrading…\n");
258
+ const npmCmd = platformBinary("npm", process.platform);
259
+ const installResult = spawnSync(
260
+ npmCmd,
261
+ ["install", "-g", "memtrace@latest"],
262
+ spawnOptionsForPlatform(process.platform, {
263
+ stdio: "inherit",
264
+ env: process.env,
265
+ }),
266
+ );
267
+ if (installResult.status !== 0) {
268
+ process.stderr.write(
269
+ "[memtrace] Upgrade failed; continuing with current version.\n\n",
270
+ );
271
+ return false;
272
+ }
273
+
274
+ // Chain into the new binary.
275
+ const memtraceCmd = platformBinary("memtrace", process.platform);
276
+ const runResult = spawnSync(
277
+ memtraceCmd,
278
+ args,
279
+ spawnOptionsForPlatform(process.platform, {
280
+ stdio: "inherit",
281
+ env: process.env,
282
+ }),
283
+ );
284
+ process.exit(runResult.status ?? 0);
285
+ }
286
+
202
287
  if (args[0] === "mcp") {
203
288
  // MCP mode: async spawn so Node's event loop keeps running long enough
204
289
  // for the update check to print its notice to the agent's stderr.
@@ -216,19 +301,29 @@ if (args[0] === "mcp") {
216
301
  });
217
302
 
218
303
  } else {
219
- // All other commands: synchronous pass-through. The update check
220
- // already fired above; cache write happens in the background and we
221
- // don't block on it (the http GET timeout is 3 s, well under any
222
- // reasonable user-visible startup budget).
223
- const result = spawnSync(binaryPath, args, {
224
- stdio: "inherit",
225
- env: process.env,
226
- });
227
-
228
- if (result.error) {
229
- console.error(`Failed to run memtrace: ${result.error.message}`);
230
- process.exit(1);
231
- }
232
-
233
- process.exit(result.status ?? 0);
304
+ // All other commands: synchronous pass-through, but FIRST give the
305
+ // user a chance to upgrade if cached check found a newer version.
306
+ // The prompt is gated on TTY + start/index command + cache says newer
307
+ // (see lib/update-prompt.js for the gating logic). When the user
308
+ // accepts, maybePromptForUpgrade calls process.exit() and we never
309
+ // reach the spawn below.
310
+ (async () => {
311
+ try {
312
+ await maybePromptForUpgrade(args[0]);
313
+ } catch (e) {
314
+ // Prompting must never block the user's command. Eat any error.
315
+ process.stderr.write(
316
+ `[memtrace] Update prompt failed (continuing): ${e.message}\n`,
317
+ );
318
+ }
319
+ const result = spawnSync(binaryPath, args, {
320
+ stdio: "inherit",
321
+ env: process.env,
322
+ });
323
+ if (result.error) {
324
+ console.error(`Failed to run memtrace: ${result.error.message}`);
325
+ process.exit(1);
326
+ }
327
+ process.exit(result.status ?? 0);
328
+ })();
234
329
  }
@@ -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.37",
3
+ "version": "0.3.38",
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.37",
43
- "@memtrace/linux-x64": "0.3.37",
44
- "@memtrace/win32-x64": "0.3.37"
42
+ "@memtrace/darwin-arm64": "0.3.38",
43
+ "@memtrace/linux-x64": "0.3.38",
44
+ "@memtrace/win32-x64": "0.3.38"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18"