sentinel-ir-mcp 0.5.4 → 0.5.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/README.md +8 -1
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/doctor.js +219 -27
package/README.md
CHANGED
|
@@ -28,6 +28,13 @@ of safety. Other languages are not supported yet. What is detected in Python
|
|
|
28
28
|
and Rust (and what is not) is listed in the
|
|
29
29
|
[sentinel-ir README](https://gitlab.com/jakubrojicek11/sentinel-ir#python).
|
|
30
30
|
|
|
31
|
+
**0.5.5** fixes the update check in `doctor`: it goes through the proxy npm
|
|
32
|
+
would use (`npm_config_https_proxy`/`npm_config_proxy`, `HTTPS_PROXY`/`HTTP_PROXY`,
|
|
33
|
+
`NO_PROXY`), follows up to 3 registry redirects (never from https to http), and
|
|
34
|
+
says what it found: "newer than npm's stable-0.5 (0.5.3)" when this copy is
|
|
35
|
+
ahead of npm, and "points to ..., not a release version" for a pre-release
|
|
36
|
+
or broken tag instead of "npm has no tag".
|
|
37
|
+
|
|
31
38
|
**0.5.4** makes `doctor` notice an old copy of itself: it asks npm for the
|
|
32
39
|
newest release on its `stable-X.Y` tag (3 s timeout, skipped offline) and,
|
|
33
40
|
when that is newer, warns and says where the old copy runs from (npx cache,
|
|
@@ -266,7 +273,7 @@ Claude Code, `setup` prints a PowerShell and a Command Prompt version of the
|
|
|
266
273
|
command. What was measured on which system: [COMPATIBILITY.md](COMPATIBILITY.md).
|
|
267
274
|
|
|
268
275
|
`doctor` checks Node.js, the package version (and asks the npm registry, or
|
|
269
|
-
`npm_config_registry`, whether its `stable-X.Y` tag has a newer release), the client config (valid JSON, a
|
|
276
|
+
`npm_config_registry`, through npm's proxy settings, whether its `stable-X.Y` tag has a newer release), the client config (valid JSON, a
|
|
270
277
|
`"sentinel-ir"` entry that starts `sentinel-ir-mcp`, existing folders), then
|
|
271
278
|
starts **its own** server with the same folders, performs a real MCP
|
|
272
279
|
handshake, expects exactly the three tools and calls `analyze_directory` with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinel-ir-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"mcpName": "io.github.jakcmencz/sentinel-ir-mcp",
|
|
5
5
|
"description": "MCP server for sentinel-ir: lets AI agents (Claude Desktop, Cursor, Windsurf, ...) read a compact structural IR of JavaScript, TypeScript, Python and Rust files instead of the raw source. Read-only, sandboxed to allowed folders.",
|
|
6
6
|
"main": "src/server.js",
|
package/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const readline = require("readline");
|
|
4
4
|
const { runSetup } = require("./setup");
|
|
5
|
-
const { runDoctor,
|
|
5
|
+
const { runDoctor, fetchDistTagsIsolated } = require("./doctor");
|
|
6
6
|
const { GUIDE } = require("./guide");
|
|
7
7
|
const { runLog } = require("./logView");
|
|
8
8
|
const { CLIENT_NAMES, PACKAGE_SPEC } = require("./clients");
|
|
@@ -103,7 +103,7 @@ function defaultIo() {
|
|
|
103
103
|
env: process.env,
|
|
104
104
|
platform: process.platform,
|
|
105
105
|
cwd: process.cwd(),
|
|
106
|
-
fetchDistTags,
|
|
106
|
+
fetchDistTags: fetchDistTagsIsolated,
|
|
107
107
|
isTTY,
|
|
108
108
|
prompt: isTTY ? (question) => new Promise((resolve) => {
|
|
109
109
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
package/src/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const http = require("http");
|
|
5
5
|
const https = require("https");
|
|
6
|
+
const net = require("net");
|
|
6
7
|
const path = require("path");
|
|
7
8
|
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
|
|
8
9
|
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
@@ -54,39 +55,204 @@ function npxCopyDir(dir = PACKAGE_DIR) {
|
|
|
54
55
|
return m ? m[1] : null;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// The prefix line of an npmrc file (npm's ${VAR} and ~/ expanded), or null.
|
|
59
|
+
function npmrcPrefix(file, env, home) {
|
|
60
|
+
let text;
|
|
61
|
+
try { text = fs.readFileSync(file, "utf8"); } catch { return null; }
|
|
62
|
+
const m = /^[ \t]*prefix[ \t]*=[ \t]*(.*?)[ \t]*$/m.exec(text);
|
|
63
|
+
if (!m || !m[1]) return null;
|
|
64
|
+
// as npm's ini: a quoted value is literal, an unquoted one ends at an unescaped ; or #
|
|
65
|
+
const q = /^(["'])(.*)\1$/.exec(m[1]);
|
|
66
|
+
let v = (q ? q[2] : m[1].replace(/(^|[^\\])[;#].*$/, "$1").trim().replace(/\\([;#\\])/g, "$1")).replace(/\$\{([^}]+)\}/g, (_, k) => env[k] || "");
|
|
67
|
+
if (/^~[\\/]/.test(v) && home) v = path.join(home, v.slice(2));
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Where npm installs a global sentinel-ir-mcp: the first entry is npm's prefix now; the rest are other
|
|
72
|
+
// places a global install may sit (also %APPDATA%\\npm).
|
|
73
|
+
function globalDirs(env, platform, execPath) {
|
|
74
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
75
|
+
const nodePrefix = platform === "win32" ? p.dirname(execPath) : p.dirname(p.dirname(execPath));
|
|
76
|
+
const home = env.HOME || env.USERPROFILE;
|
|
77
|
+
const cfg = (k) => env[`npm_config_${k}`] || env[`NPM_CONFIG_${k.toUpperCase()}`];
|
|
78
|
+
// npm's prefix: environment, then user, global and builtin npmrc (Homebrew sets it there), then the default
|
|
79
|
+
const builtinPrefix = npmrcPrefix(p.join(nodePrefix, ...(platform === "win32" ? [] : ["lib"]), "node_modules", "npm", "npmrc"), env, home);
|
|
80
|
+
const prefixes = [cfg("prefix"),
|
|
81
|
+
npmrcPrefix(cfg("userconfig") || (home ? p.join(home, ".npmrc") : ""), env, home),
|
|
82
|
+
npmrcPrefix(cfg("globalconfig") || p.join(builtinPrefix || nodePrefix, "etc", "npmrc"), env, home),
|
|
83
|
+
builtinPrefix,
|
|
84
|
+
nodePrefix,
|
|
85
|
+
platform === "win32" && env.APPDATA ? p.join(env.APPDATA, "npm") : null];
|
|
86
|
+
const dirs = prefixes.filter(Boolean).map((x) => p.join(x, ...(platform === "win32" ? [] : ["lib"]), "node_modules", "sentinel-ir-mcp"));
|
|
87
|
+
return [...new Set(dirs)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const realDir = (d) => { try { return fs.realpathSync(d); } catch { return d; } };
|
|
91
|
+
const sameDir = (a, b) => a.replace(/[\\/]+$/, "").toLowerCase() === b.replace(/[\\/]+$/, "").toLowerCase();
|
|
92
|
+
|
|
57
93
|
// Where the package in dir is installed: an npx cache copy, a global install or elsewhere.
|
|
58
|
-
function installKind(dir) {
|
|
94
|
+
function installKind(dir, globals = []) {
|
|
59
95
|
const npx = npxCopyDir(dir);
|
|
60
96
|
if (npx) return { kind: "npx", dir: npx, key: /[^\\/]+$/.exec(npx)[0] };
|
|
61
|
-
if (/[\\/](lib|npm)[\\/]node_modules[\\/]sentinel-ir-mcp[\\/]?$/i.test(dir)) return { kind: "global", dir };
|
|
97
|
+
if (/[\\/](lib|npm)[\\/]node_modules[\\/]sentinel-ir-mcp[\\/]?$/i.test(dir) || globals.some((g) => sameDir(g, dir))) return { kind: "global", dir };
|
|
62
98
|
return { kind: "other", dir };
|
|
63
99
|
}
|
|
64
100
|
|
|
101
|
+
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
102
|
+
|
|
103
|
+
const unescapeLogin = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
104
|
+
|
|
105
|
+
// The proxy npm would use for url (npm's https-proxy and proxy settings, then HTTPS_PROXY/HTTP_PROXY), null for none,
|
|
106
|
+
// false for a proxy setting doctor cannot use (it then skips instead of going direct).
|
|
107
|
+
function proxyFor(url, env) {
|
|
108
|
+
const noProxy = env.npm_config_noproxy || env.NO_PROXY || env.no_proxy || "";
|
|
109
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
110
|
+
for (const rule of noProxy.split(/[\s,]+/).filter(Boolean)) {
|
|
111
|
+
const bare = /^\[(.*)\](:\d+)?$/.exec(rule)?.[1] ?? (rule.split(":").length === 2 ? rule.replace(/:\d+$/, "") : rule);
|
|
112
|
+
const r = bare.replace(/^\*?\.?/, "").replace(/\.$/, "").toLowerCase();
|
|
113
|
+
if (rule === "*" || host === r || host.endsWith("." + r)) return null;
|
|
114
|
+
}
|
|
115
|
+
// npm: its https-proxy, then proxy setting, for http registries too; then the environment.
|
|
116
|
+
const raw = env.npm_config_https_proxy || env.npm_config_proxy || env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy;
|
|
117
|
+
if (!raw) return null;
|
|
118
|
+
try {
|
|
119
|
+
const p = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`);
|
|
120
|
+
return p.protocol === "http:" || p.protocol === "https:" ? p : false;
|
|
121
|
+
} catch { return false; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GET url directly or through proxy; calls back with the response. Adds every request it opens to open.
|
|
125
|
+
function openGet(url, env, tls, open, onResponse, onFail) {
|
|
126
|
+
try { openGetOrThrow(url, env, tls, open, onResponse, onFail); } catch { onFail(); }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function openGetOrThrow(url, env, tls, open, onResponse, onFail) {
|
|
130
|
+
// The registry login goes in a header we build: Node throws on a malformed % in URL credentials.
|
|
131
|
+
const bare = new URL(url.href);
|
|
132
|
+
bare.username = bare.password = "";
|
|
133
|
+
const headers = { accept: "application/json",
|
|
134
|
+
...(url.username || url.password ? { authorization: "Basic " + Buffer.from(`${unescapeLogin(url.username)}:${unescapeLogin(url.password)}`).toString("base64") } : {}) };
|
|
135
|
+
const proxy = proxyFor(url, env);
|
|
136
|
+
if (proxy === false) { onFail(); return; }
|
|
137
|
+
if (!proxy) {
|
|
138
|
+
const req = (url.protocol === "https:" ? https : http).get(bare, { headers, ...tls }, onResponse);
|
|
139
|
+
req.on("error", onFail);
|
|
140
|
+
open.push(req);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const proxyLib = proxy.protocol === "https:" ? https : http;
|
|
144
|
+
const auth = proxy.username
|
|
145
|
+
? { "proxy-authorization": "Basic " + Buffer.from(`${unescapeLogin(proxy.username)}:${unescapeLogin(proxy.password)}`).toString("base64") }
|
|
146
|
+
: {};
|
|
147
|
+
const target = { host: proxy.hostname.replace(/^\[|\]$/g, ""), port: proxy.port || (proxy.protocol === "https:" ? 443 : 80) };
|
|
148
|
+
if (url.protocol === "http:") {
|
|
149
|
+
const req = proxyLib.get({ ...target, path: bare.href, headers: { ...headers, ...auth, host: bare.host } }, onResponse);
|
|
150
|
+
req.on("error", onFail);
|
|
151
|
+
open.push(req);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const connect = proxyLib.request({ ...target, method: "CONNECT", path: `${url.hostname}:${url.port || 443}`, headers: { ...auth, host: `${url.hostname}:${url.port || 443}` } });
|
|
155
|
+
connect.on("connect", (res, socket) => {
|
|
156
|
+
if (res.statusCode !== 200) { socket.destroy(); onFail(); return; }
|
|
157
|
+
const req = https.get(bare, {
|
|
158
|
+
headers, ...tls,
|
|
159
|
+
createConnection: () => require("tls").connect({ socket, host: url.hostname.replace(/^\[|\]$/g, ""), servername: net.isIP(url.hostname.replace(/^\[|\]$/g, "")) ? undefined : url.hostname, ...tls })
|
|
160
|
+
}, onResponse);
|
|
161
|
+
req.on("error", onFail);
|
|
162
|
+
open.push(req);
|
|
163
|
+
});
|
|
164
|
+
connect.on("error", onFail);
|
|
165
|
+
connect.end();
|
|
166
|
+
open.push(connect);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// npm's strict-ssl and cafile settings as TLS options; null when the cafile is unreadable (npm fails then too).
|
|
170
|
+
function npmTls(env) {
|
|
171
|
+
const t = {};
|
|
172
|
+
if (/^false$/i.test(env.npm_config_strict_ssl || "")) t.rejectUnauthorized = false;
|
|
173
|
+
if (env.npm_config_cafile) {
|
|
174
|
+
try { t.ca = [...require("tls").rootCertificates, fs.readFileSync(env.npm_config_cafile, "utf8")]; } catch { return null; }
|
|
175
|
+
}
|
|
176
|
+
return t;
|
|
177
|
+
}
|
|
178
|
+
|
|
65
179
|
// npm dist-tags of sentinel-ir-mcp, or null when the registry does not answer in time.
|
|
66
|
-
|
|
180
|
+
// Uses npm_config_registry and the proxy npm would use; follows up to 3 redirects.
|
|
181
|
+
function fetchDistTags(env, timeoutMs = 3000, tls = npmTls(env)) {
|
|
67
182
|
return new Promise((resolve) => {
|
|
183
|
+
if (tls === null) { resolve(null); return; }
|
|
184
|
+
let open = [];
|
|
185
|
+
let done = false;
|
|
186
|
+
const finish = (v) => {
|
|
187
|
+
if (done) return;
|
|
188
|
+
done = true;
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
for (const r of open) r.destroy();
|
|
191
|
+
resolve(v);
|
|
192
|
+
};
|
|
193
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
194
|
+
const get = (url, hops) => {
|
|
195
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") { finish(null); return; }
|
|
196
|
+
openGet(url, env, tls, open, (res) => {
|
|
197
|
+
if (REDIRECTS.has(res.statusCode) && res.headers.location && hops < 3) {
|
|
198
|
+
res.destroy();
|
|
199
|
+
let next;
|
|
200
|
+
try { next = new URL(res.headers.location, url); } catch { finish(null); return; }
|
|
201
|
+
if (url.protocol === "https:" && next.protocol !== "https:") { finish(null); return; }
|
|
202
|
+
get(next, hops + 1);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (res.statusCode !== 200) { res.resume(); finish(null); return; }
|
|
206
|
+
const chunks = [];
|
|
207
|
+
let size = 0;
|
|
208
|
+
res.on("data", (d) => { chunks.push(d); size += d.length; if (size > 65536) finish(null); });
|
|
209
|
+
res.on("end", () => {
|
|
210
|
+
try {
|
|
211
|
+
const tags = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
212
|
+
finish(tags && typeof tags === "object" && !Array.isArray(tags) ? tags : null);
|
|
213
|
+
} catch { finish(null); }
|
|
214
|
+
});
|
|
215
|
+
res.on("error", () => finish(null));
|
|
216
|
+
res.on("aborted", () => finish(null));
|
|
217
|
+
}, () => finish(null));
|
|
218
|
+
};
|
|
68
219
|
let url;
|
|
69
220
|
try {
|
|
70
221
|
url = new URL("-/package/sentinel-ir-mcp/dist-tags", (env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/?$/, "/"));
|
|
71
|
-
} catch {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
222
|
+
} catch { finish(null); return; }
|
|
223
|
+
get(url, 0);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// fetchDistTags in a child process that is killed on timeout: a DNS lookup that never answers
|
|
228
|
+
// cannot be cancelled and would keep doctor's own process alive for up to 30 s.
|
|
229
|
+
function fetchDistTagsIsolated(env, timeoutMs = 3000) {
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
let out = "";
|
|
232
|
+
let child;
|
|
233
|
+
let done = false;
|
|
234
|
+
const finish = (v) => {
|
|
235
|
+
if (done) return;
|
|
236
|
+
done = true;
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
if (child) child.kill();
|
|
239
|
+
resolve(v);
|
|
240
|
+
};
|
|
241
|
+
const timer = setTimeout(() => finish(null), timeoutMs + 1000);
|
|
242
|
+
try {
|
|
243
|
+
child = require("child_process").spawn(process.execPath,
|
|
244
|
+
["-e", `require(process.argv[1]).fetchDistTags(process.env, ${Number(timeoutMs)}).then((t) => process.stdout.write(JSON.stringify(t) + "\\n"))`, __filename],
|
|
245
|
+
{ env, stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
246
|
+
} catch { finish(null); return; }
|
|
247
|
+
child.on("error", () => finish(null));
|
|
248
|
+
child.on("close", () => finish(null));
|
|
249
|
+
child.stdout.on("data", (d) => {
|
|
250
|
+
out += d;
|
|
251
|
+
if (!out.includes("\n")) return;
|
|
252
|
+
let tags = null;
|
|
253
|
+
try { tags = JSON.parse(out); } catch { /* unusable */ }
|
|
254
|
+
finish(tags && typeof tags === "object" && !Array.isArray(tags) ? tags : null);
|
|
85
255
|
});
|
|
86
|
-
const timer = setTimeout(() => req.destroy(), timeoutMs);
|
|
87
|
-
req.on("timeout", () => req.destroy());
|
|
88
|
-
req.on("error", () => resolve(null));
|
|
89
|
-
req.on("close", () => { clearTimeout(timer); resolve(null); });
|
|
90
256
|
});
|
|
91
257
|
}
|
|
92
258
|
|
|
@@ -97,20 +263,30 @@ async function checkUpdate(io, add) {
|
|
|
97
263
|
const tag = m ? `stable-${m[1]}.${m[2]}` : "latest";
|
|
98
264
|
const tags = await io.fetchDistTags(io.env);
|
|
99
265
|
if (!tags) {
|
|
100
|
-
add("skip", "package.update", "
|
|
266
|
+
add("skip", "package.update", "No usable answer from the npm registry about a newer sentinel-ir-mcp (offline, blocked or an unexpected reply)");
|
|
101
267
|
return;
|
|
102
268
|
}
|
|
103
|
-
const newest = tags[tag];
|
|
104
|
-
if (
|
|
269
|
+
const newest = Object.prototype.hasOwnProperty.call(tags, tag) ? tags[tag] : undefined;
|
|
270
|
+
if (newest === undefined) {
|
|
105
271
|
add("skip", "package.update", `npm has no ${tag} tag for sentinel-ir-mcp`);
|
|
106
272
|
return;
|
|
107
273
|
}
|
|
108
|
-
if (
|
|
274
|
+
if (typeof newest !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(newest)) {
|
|
275
|
+
const shown = JSON.stringify(newest) ?? String(newest);
|
|
276
|
+
add("skip", "package.update", `npm's ${tag} tag points to ${shown.length > 40 ? shown.slice(0, 40) + "..." : shown}, not a release version; no update check`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (newest === version) {
|
|
109
280
|
add("ok", "package.update", `sentinel-ir-mcp ${version} is the newest ${tag} on npm`);
|
|
110
281
|
return;
|
|
111
282
|
}
|
|
283
|
+
if (!olderThan(version, newest)) {
|
|
284
|
+
add("ok", "package.update", `sentinel-ir-mcp ${version} is newer than npm's ${tag} (${newest})`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
112
287
|
const spec = `sentinel-ir-mcp@${tag}`;
|
|
113
|
-
const
|
|
288
|
+
const globals = globalDirs(io.env, io.platform, io.execPath || process.execPath).flatMap((g) => [g, realDir(g)]);
|
|
289
|
+
const from = installKind(io.packageDir || PACKAGE_DIR, globals);
|
|
114
290
|
const where = { npx: `the npx cache (${from.dir})`, global: `a global install (${from.dir})`, other: from.dir }[from.kind];
|
|
115
291
|
const fix = {
|
|
116
292
|
npx: `Close the client, delete that folder (npm 11: npm cache npx rm ${from.key}), then run: npx -y ${spec} setup`,
|
|
@@ -158,6 +334,21 @@ function staleNpxCopies(cacheDir) {
|
|
|
158
334
|
return stale;
|
|
159
335
|
}
|
|
160
336
|
|
|
337
|
+
// A global install other than this copy that is older or runs an old sentinel-ir:
|
|
338
|
+
// bare `npx sentinel-ir-mcp` and client entries without a version start it.
|
|
339
|
+
function checkGlobalInstall(io, add) {
|
|
340
|
+
const running = io.packageDir || PACKAGE_DIR;
|
|
341
|
+
// only npm's current prefix: bare npx starts the global install there, not one left under an old prefix
|
|
342
|
+
for (const dir of globalDirs(io.env, io.platform, io.execPath || process.execPath).slice(0, 1)) {
|
|
343
|
+
if (sameDir(dir, running) || sameDir(realDir(dir), realDir(running)) || !fs.existsSync(path.join(dir, "package.json"))) continue;
|
|
344
|
+
const mcp = readVersion(path.join(dir, "package.json"));
|
|
345
|
+
const ir = readVersion(path.join(dir, "node_modules", "sentinel-ir", "package.json"));
|
|
346
|
+
if (!olderThan(mcp, version) && !olderThan(ir, IR_MIN)) continue;
|
|
347
|
+
add("warn", "package.global", `A global install of sentinel-ir-mcp ${mcp} (sentinel-ir ${ir}) is at ${dir}; bare npx sentinel-ir-mcp and client entries without a version start it`,
|
|
348
|
+
`Run: npm install -g ${PACKAGE_SPEC} (or npm uninstall -g sentinel-ir-mcp)`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
161
352
|
function checkNpxCache(env, platform, add) {
|
|
162
353
|
const cacheDir = npmCacheDir(env, platform);
|
|
163
354
|
const stale = staleNpxCopies(cacheDir);
|
|
@@ -350,6 +541,7 @@ async function runDoctor(opts, io) {
|
|
|
350
541
|
npxCopyDir() ? `Close the client, delete ${npxCopyDir()}, then run: npx -y ${PACKAGE_SPEC} setup` : `Reinstall it so that npm installs sentinel-ir ${dependencies["sentinel-ir"]}.`);
|
|
351
542
|
} else add("ok", "package", `sentinel-ir-mcp ${version}, sentinel-ir ${ir}`);
|
|
352
543
|
await checkUpdate(io, add);
|
|
544
|
+
checkGlobalInstall(io, add);
|
|
353
545
|
checkNpxCache(io.env, io.platform, add);
|
|
354
546
|
|
|
355
547
|
const project = path.resolve(io.cwd, opts.project || ".");
|
|
@@ -401,4 +593,4 @@ async function runDoctor(opts, io) {
|
|
|
401
593
|
return failed ? 1 : 0;
|
|
402
594
|
}
|
|
403
595
|
|
|
404
|
-
module.exports = { runDoctor, entryFolders, olderThan, staleNpxCopies, fetchDistTags, installKind, IR_MIN };
|
|
596
|
+
module.exports = { runDoctor, entryFolders, olderThan, staleNpxCopies, fetchDistTags, fetchDistTagsIsolated, proxyFor, installKind, globalDirs, IR_MIN };
|