@sdsrs/code-graph 0.101.0 → 0.102.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/LICENSE +21 -0
- package/bin/cli.js +18 -5
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/adopt.js +46 -3
- package/claude-plugin/scripts/auto-update.js +139 -8
- package/claude-plugin/scripts/doctor.js +24 -0
- package/claude-plugin/scripts/find-binary.js +101 -35
- package/claude-plugin/scripts/install-lock.js +48 -0
- package/claude-plugin/scripts/launcher-install.js +146 -0
- package/claude-plugin/scripts/lifecycle.js +80 -6
- package/claude-plugin/scripts/mcp-launcher.js +71 -62
- package/claude-plugin/scripts/npm-exec.js +15 -0
- package/claude-plugin/scripts/session-init.js +7 -4
- package/claude-plugin/scripts/statusline.js +29 -5
- package/claude-plugin/scripts/version-utils.js +10 -2
- package/package.json +6 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sdsrss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/cli.js
CHANGED
|
@@ -54,15 +54,28 @@ if (sub === "uninstall") {
|
|
|
54
54
|
}
|
|
55
55
|
const lifecycle = require("../claude-plugin/scripts/lifecycle");
|
|
56
56
|
const { unadopt } = require("../claude-plugin/scripts/adopt");
|
|
57
|
-
const r = lifecycle.uninstall();
|
|
57
|
+
const r = lifecycle.uninstall({ purgeGlobal: process.argv.slice(3).includes("--purge-global") });
|
|
58
58
|
let ua = { ok: false };
|
|
59
59
|
try { ua = unadopt(); } catch { /* best-effort — settings/cache already cleaned */ }
|
|
60
60
|
const projectUnadopted = !!(ua && (ua.blockPruned || ua.fileRemoved || ua.claudeMdRemoved));
|
|
61
|
-
|
|
61
|
+
let out =
|
|
62
62
|
`Uninstalled code-graph-mcp | settings cleaned=${r.settingsChanged}` +
|
|
63
|
-
` | this project unadopted=${projectUnadopted}\n
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
` | this project unadopted=${projectUnadopted}\n`;
|
|
64
|
+
if (r.globalPkgsRemoved.length) {
|
|
65
|
+
out += ` Removed global npm package(s): ${r.globalPkgsRemoved.join(", ")}\n`;
|
|
66
|
+
}
|
|
67
|
+
if (r.globalPkgsRemaining.length) {
|
|
68
|
+
out += ` Global npm package(s) still installed: ${r.globalPkgsRemaining.join(", ")}\n` +
|
|
69
|
+
` Remove with: npm uninstall -g ${r.globalPkgsRemaining.join(" ")}` +
|
|
70
|
+
(r.pluginInstalledGlobals ? "\n" : " (or re-run with --purge-global)\n");
|
|
71
|
+
}
|
|
72
|
+
const otherAdopted = r.adoptedProjects.filter((p) => p !== process.cwd());
|
|
73
|
+
if (otherAdopted.length) {
|
|
74
|
+
out += " Other adopted project(s) — run `code-graph-mcp unadopt` + `rm -rf .code-graph` in each:\n" +
|
|
75
|
+
otherAdopted.map((p) => ` ${p}\n`).join("");
|
|
76
|
+
}
|
|
77
|
+
out += " Also run `/plugin uninstall code-graph-mcp` in Claude Code to sync its UI state.\n";
|
|
78
|
+
process.stdout.write(out);
|
|
66
79
|
process.exit(0);
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -339,7 +339,46 @@ function platformGuard() {
|
|
|
339
339
|
// now lives in project-detect.js — the single activation gate shared with
|
|
340
340
|
// mcp-launcher.js and session-init.js. Imported above and re-exported below.
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
// ── Adopted-projects registry ───────────────────────────────
|
|
343
|
+
// ~/.cache/code-graph/adopted-projects.json — every project adopt() has touched.
|
|
344
|
+
// Sole consumer is lifecycle.js uninstall(): without this list it cannot tell
|
|
345
|
+
// the user WHICH projects still carry a managed CLAUDE.md block + .code-graph/
|
|
346
|
+
// index dir (adoption state is otherwise only discoverable per-project).
|
|
347
|
+
// Best-effort: registry loss only degrades uninstall guidance, never adoption.
|
|
348
|
+
|
|
349
|
+
function adoptedRegistryFile(home) {
|
|
350
|
+
return path.join(home || os.homedir(), '.cache', 'code-graph', 'adopted-projects.json');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function readAdoptedProjects(home) {
|
|
354
|
+
try {
|
|
355
|
+
const list = JSON.parse(fs.readFileSync(adoptedRegistryFile(home), 'utf8'));
|
|
356
|
+
return Array.isArray(list) ? list.filter((p) => typeof p === 'string') : [];
|
|
357
|
+
} catch { return []; }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function recordAdopted(projectDir, home) {
|
|
361
|
+
try {
|
|
362
|
+
const file = adoptedRegistryFile(home);
|
|
363
|
+
const list = readAdoptedProjects(home);
|
|
364
|
+
const abs = path.resolve(projectDir);
|
|
365
|
+
if (list.includes(abs)) return;
|
|
366
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
367
|
+
writeFileAtomic(file, JSON.stringify([...list, abs], null, 2) + '\n');
|
|
368
|
+
} catch { /* best-effort */ }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function removeAdopted(projectDir, home) {
|
|
372
|
+
try {
|
|
373
|
+
const list = readAdoptedProjects(home);
|
|
374
|
+
const abs = path.resolve(projectDir);
|
|
375
|
+
const next = list.filter((p) => p !== abs);
|
|
376
|
+
if (next.length === list.length) return;
|
|
377
|
+
writeFileAtomic(adoptedRegistryFile(home), JSON.stringify(next, null, 2) + '\n');
|
|
378
|
+
} catch { /* best-effort */ }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function adopt({ cwd, templatePath, home } = {}) {
|
|
343
382
|
const blocked = platformGuard();
|
|
344
383
|
if (blocked) return blocked;
|
|
345
384
|
|
|
@@ -376,6 +415,7 @@ function adopt({ cwd, templatePath } = {}) {
|
|
|
376
415
|
const exists = fs.existsSync(cPath);
|
|
377
416
|
const current = exists ? fs.readFileSync(cPath, 'utf8') : '';
|
|
378
417
|
if (current.includes(block)) {
|
|
418
|
+
recordAdopted(effectiveCwd, home);
|
|
379
419
|
return { ok: true, detailPath: dPath, claudeMdPath: cPath, detailWritten, claudeMdWritten: false, created: false, healed: false };
|
|
380
420
|
}
|
|
381
421
|
const cleaned = exists ? stripSentinelBlock(current) : '';
|
|
@@ -383,6 +423,7 @@ function adopt({ cwd, templatePath } = {}) {
|
|
|
383
423
|
const base = cleaned.replace(/\n+$/, '');
|
|
384
424
|
const prefix = base ? base + '\n\n' : '';
|
|
385
425
|
writeFileAtomic(cPath, prefix + block + '\n');
|
|
426
|
+
recordAdopted(effectiveCwd, home);
|
|
386
427
|
return { ok: true, detailPath: dPath, claudeMdPath: cPath, detailWritten, claudeMdWritten: true, created: !exists, healed };
|
|
387
428
|
}
|
|
388
429
|
|
|
@@ -495,12 +536,12 @@ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
|
|
|
495
536
|
// shipped template / 管理块 漂移时重跑 adopt 对齐。
|
|
496
537
|
// opt-out: CODE_GRAPH_NO_TEMPLATE_REFRESH=1(锁定手动编辑)。
|
|
497
538
|
if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd })) {
|
|
498
|
-
const result = adopt({ cwd });
|
|
539
|
+
const result = adopt({ cwd, home });
|
|
499
540
|
return { attempted: true, reason: 'refreshed', result, migrated };
|
|
500
541
|
}
|
|
501
542
|
return { attempted: false, reason: 'already-adopted', migrated };
|
|
502
543
|
}
|
|
503
|
-
const result = adopt({ cwd });
|
|
544
|
+
const result = adopt({ cwd, home });
|
|
504
545
|
return { attempted: true, reason: 'adopted', result, migrated };
|
|
505
546
|
}
|
|
506
547
|
|
|
@@ -544,6 +585,7 @@ function unadopt({ cwd, home } = {}) {
|
|
|
544
585
|
// Also sweep any legacy memory-dir remnants (uninstall before auto-migration ran).
|
|
545
586
|
const migrated = migrateLegacyMemoryDir({ cwd, home });
|
|
546
587
|
|
|
588
|
+
removeAdopted(effectiveCwd, home);
|
|
547
589
|
return { ok: true, fileRemoved, blockPruned, claudeMdRemoved, target: dPath, claudeMdPath: cPath, migrated };
|
|
548
590
|
}
|
|
549
591
|
|
|
@@ -603,6 +645,7 @@ if (require.main === module) {
|
|
|
603
645
|
|
|
604
646
|
module.exports = {
|
|
605
647
|
adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
|
|
648
|
+
readAdoptedProjects, recordAdopted, removeAdopted, adoptedRegistryFile,
|
|
606
649
|
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
|
|
607
650
|
detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
|
|
608
651
|
claudeMdPath, detailDir, detailPath,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
const { execFileSync } = require('child_process');
|
|
3
|
+
const { execFileSync, spawn } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const http = require('http');
|
|
@@ -9,9 +9,11 @@ const path = require('path');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
11
11
|
const { claudeHome } = require('./claude-config');
|
|
12
|
-
const { clearCache: clearBinaryCache } = require('./find-binary');
|
|
12
|
+
const { clearCache: clearBinaryCache, globalNodeModulesCandidates, PLATFORM_PKG, detectLibc } = require('./find-binary');
|
|
13
13
|
const { readBinaryVersion, isDevMode } = require('./version-utils');
|
|
14
14
|
const { cgTmpDir } = require('./tmp-dir');
|
|
15
|
+
const { npmSpawnOpts } = require('./npm-exec');
|
|
16
|
+
const { acquireLock } = require('./install-lock');
|
|
15
17
|
|
|
16
18
|
// ── Environment Checks ────────────────────────────────────
|
|
17
19
|
|
|
@@ -56,9 +58,13 @@ function isForceMode(argv = process.argv.slice(2)) {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// ── Platform → GitHub release asset name mapping ──────────
|
|
59
|
-
function getPlatformAssetName() {
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
function getPlatformAssetName({ platform = os.platform(), arch = os.arch(), libc = null } = {}) {
|
|
62
|
+
// No musl asset is published: the glibc linux build downloads fine but cannot
|
|
63
|
+
// exec on Alpine, so promoteVerifiedBinary always rejected it and — with the
|
|
64
|
+
// binary still missing — every SessionStart bypassed the throttle and pulled
|
|
65
|
+
// the same futile ~40MB again. Null stops the download path entirely; the
|
|
66
|
+
// launcher surfaces unsupportedPlatformHint (cargo install / glibc image).
|
|
67
|
+
if (platform === 'linux' && (libc || detectLibc()) === 'musl') return null;
|
|
62
68
|
const key = `${platform}-${arch}`;
|
|
63
69
|
const map = {
|
|
64
70
|
'linux-x64': 'code-graph-mcp-linux-x64',
|
|
@@ -284,7 +290,11 @@ function cachedBinaryPath() {
|
|
|
284
290
|
function cachedBinaryNeedsUpdate(latest, { binaryPath = cachedBinaryPath(), readVersion = readBinaryVersion } = {}) {
|
|
285
291
|
if (!latest || !latest.binaryUrl) return false;
|
|
286
292
|
if (!fs.existsSync(binaryPath)) return true;
|
|
287
|
-
|
|
293
|
+
const current = readVersion(binaryPath);
|
|
294
|
+
if (!current) return true; // unreadable/broken binary — let the heal replace it
|
|
295
|
+
// Ordered compare, not string inequality: a binary NEWER than releases/latest
|
|
296
|
+
// (dev build, or the API momentarily lagging a publish) must not be downgraded.
|
|
297
|
+
return compareVersions(current, latest.version) < 0;
|
|
288
298
|
}
|
|
289
299
|
|
|
290
300
|
/**
|
|
@@ -298,7 +308,10 @@ function cachedBinaryNeedsUpdate(latest, { binaryPath = cachedBinaryPath(), read
|
|
|
298
308
|
function cachedBinaryStaleVsState(state, { binaryPath = cachedBinaryPath(), readVersion = readBinaryVersion } = {}) {
|
|
299
309
|
if (!state || !state.latestVersion) return false;
|
|
300
310
|
if (!fs.existsSync(binaryPath)) return false;
|
|
301
|
-
|
|
311
|
+
const current = readVersion(binaryPath);
|
|
312
|
+
if (!current) return true; // unreadable/broken — bypass throttle so the heal runs
|
|
313
|
+
// Ordered compare (see cachedBinaryNeedsUpdate): newer-than-state is not stale.
|
|
314
|
+
return compareVersions(current, state.latestVersion) < 0;
|
|
302
315
|
}
|
|
303
316
|
|
|
304
317
|
/**
|
|
@@ -556,7 +569,98 @@ async function selfHealStaleBinary(latest, { needsUpdate = cachedBinaryNeedsUpda
|
|
|
556
569
|
return await download(latest);
|
|
557
570
|
}
|
|
558
571
|
|
|
572
|
+
// ── Global npm package self-heal ───────────────────────────
|
|
573
|
+
// The `code-graph-mcp` CLI on the user's PATH is the GLOBAL npm shell package
|
|
574
|
+
// (@sdsrs/code-graph) — a delivery surface entirely outside the marketplace
|
|
575
|
+
// plugin, so /plugin update and the binary self-heal above never touch it. In
|
|
576
|
+
// the field it drifts for months (a 0.46.0 wrapper delegating to a 0.101.0
|
|
577
|
+
// binary) and users were expected to run `npm update -g` by hand — which also
|
|
578
|
+
// breaks on unrelated npm-config quirks (EALLOWGIT). Same story for a platform
|
|
579
|
+
// package installed EXPLICITLY at the global top level (the old launcher's
|
|
580
|
+
// manual-install hint suggested exactly that): that relic was the 0.16.6
|
|
581
|
+
// landmine behind the MCP connect-timeout incident.
|
|
582
|
+
//
|
|
583
|
+
// Heal contract: refresh ONLY what the user already installed globally (never
|
|
584
|
+
// introduce a global install they didn't ask for), one bounded npm run per
|
|
585
|
+
// release target, silent failure (an unhealable npm env must not block or spam).
|
|
586
|
+
|
|
587
|
+
const SHELL_PKG = '@sdsrs/code-graph';
|
|
588
|
+
const GLOBAL_PKG_HEAL_MAX_ATTEMPTS = 3;
|
|
589
|
+
const GLOBAL_PKG_HEAL_TIMEOUT_MS = 180000; // npm resolves + downloads the platform optionalDependency (~40MB)
|
|
590
|
+
|
|
591
|
+
/** Installed version of a top-level GLOBAL npm package, or null when absent. */
|
|
592
|
+
function globalPkgVersion(name, roots = null) {
|
|
593
|
+
for (const root of (roots || globalNodeModulesCandidates())) {
|
|
594
|
+
try {
|
|
595
|
+
const pkg = readJson(path.join(root, name, 'package.json'));
|
|
596
|
+
if (pkg && pkg.version) return pkg.version;
|
|
597
|
+
} catch { /* not installed under this root */ }
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Globally-installed packages of ours whose version lags `latestVersion`. */
|
|
603
|
+
function staleGlobalPkgs(latestVersion, roots = null) {
|
|
604
|
+
const out = [];
|
|
605
|
+
for (const name of [SHELL_PKG, PLATFORM_PKG]) {
|
|
606
|
+
const ver = globalPkgVersion(name, roots);
|
|
607
|
+
if (ver && compareVersions(ver, latestVersion) < 0) out.push({ name, version: ver });
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** One targeted `npm install -g` for the given specs. Resolves true on exit 0. */
|
|
613
|
+
function npmInstallGlobal(specs) {
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
if (!commandExists('npm')) { resolve(false); return; }
|
|
616
|
+
const child = spawn('npm', ['install', '-g', ...specs], npmSpawnOpts({
|
|
617
|
+
timeout: GLOBAL_PKG_HEAL_TIMEOUT_MS,
|
|
618
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
619
|
+
}));
|
|
620
|
+
let stderr = '';
|
|
621
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
622
|
+
child.on('error', () => resolve(false));
|
|
623
|
+
child.on('exit', (code) => {
|
|
624
|
+
if (code === 0) {
|
|
625
|
+
console.error(`[code-graph] global npm package(s) refreshed: ${specs.join(' ')}`);
|
|
626
|
+
resolve(true);
|
|
627
|
+
} else {
|
|
628
|
+
const tail = stderr.trim().split('\n').slice(-2).join(' | ');
|
|
629
|
+
console.error(`[code-graph] global npm refresh failed (exit ${code}): ${tail}`);
|
|
630
|
+
resolve(false);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Self-heal globally-installed shell/platform packages to `latest.version`.
|
|
638
|
+
* Returns a state patch (spread into the update-state save): attempts are
|
|
639
|
+
* counted PER target version so a persistently-failing npm env stops being
|
|
640
|
+
* retried after GLOBAL_PKG_HEAL_MAX_ATTEMPTS, and the counter re-arms when the
|
|
641
|
+
* next release moves the target.
|
|
642
|
+
*/
|
|
643
|
+
async function selfHealGlobalPkgs(latest, state, {
|
|
644
|
+
readStale = staleGlobalPkgs,
|
|
645
|
+
install = npmInstallGlobal,
|
|
646
|
+
} = {}) {
|
|
647
|
+
if (!latest || !latest.version) return {};
|
|
648
|
+
const stale = readStale(latest.version);
|
|
649
|
+
if (stale.length === 0) {
|
|
650
|
+
// Healthy (or nothing installed globally) — clear any leftover counter.
|
|
651
|
+
return state.globalPkgHealAttempts ? { globalPkgHealAttempts: 0, globalPkgHealVersion: null } : {};
|
|
652
|
+
}
|
|
653
|
+
const attempts = state.globalPkgHealVersion === latest.version ? (state.globalPkgHealAttempts || 0) : 0;
|
|
654
|
+
if (attempts >= GLOBAL_PKG_HEAL_MAX_ATTEMPTS) return {};
|
|
655
|
+
const ok = await install(stale.map((s) => `${s.name}@${latest.version}`));
|
|
656
|
+
return {
|
|
657
|
+
globalPkgHealVersion: latest.version,
|
|
658
|
+
globalPkgHealAttempts: ok ? 0 : attempts + 1,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
559
662
|
async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
663
|
+
let installLock = null;
|
|
560
664
|
try {
|
|
561
665
|
// Skip in dev mode — unless the launcher explicitly requested a missing-
|
|
562
666
|
// binary install, in which case we MUST proceed regardless of mode (the
|
|
@@ -596,6 +700,19 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
596
700
|
// Compare versions
|
|
597
701
|
const hasUpdate = compareVersions(latest.version, installedVersion) > 0;
|
|
598
702
|
|
|
703
|
+
// Inter-process gate for every mutating path below (plugin-cache copy,
|
|
704
|
+
// binary download, global npm heals): concurrent sessions racing here ran
|
|
705
|
+
// parallel `npm install -g` against one global prefix and clobbered each
|
|
706
|
+
// other's state-file counters (rateLimited, heal attempts). Skip-if-held:
|
|
707
|
+
// the holder does the work and its state outcome wins. The launcher's
|
|
708
|
+
// install chain already holds this lock across its spawn of this script —
|
|
709
|
+
// it marks that with CODE_GRAPH_INSTALL_LOCK_HELD so we don't deadlock
|
|
710
|
+
// against our own parent.
|
|
711
|
+
if (process.env.CODE_GRAPH_INSTALL_LOCK_HELD !== '1') {
|
|
712
|
+
installLock = acquireLock(path.join(CACHE_DIR, 'install.lock'));
|
|
713
|
+
if (!installLock) return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
599
716
|
if (hasUpdate) {
|
|
600
717
|
const result = await downloadAndInstall(latest);
|
|
601
718
|
const success = result.pluginUpdated;
|
|
@@ -615,7 +732,10 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
615
732
|
binaryUpdated: result.binaryUpdated,
|
|
616
733
|
marketplaceRefreshed: result.marketplaceRefreshed,
|
|
617
734
|
};
|
|
618
|
-
|
|
735
|
+
// Keep any globally-installed shell/platform npm packages in step with
|
|
736
|
+
// the release the plugin just moved to (see selfHealGlobalPkgs).
|
|
737
|
+
const globalHeal = await selfHealGlobalPkgs(latest, state);
|
|
738
|
+
saveState({ ...newState, ...globalHeal });
|
|
619
739
|
|
|
620
740
|
return {
|
|
621
741
|
updateAvailable: !success,
|
|
@@ -632,6 +752,12 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
632
752
|
// failure observed in the field (shell at v0.45, binary pinned at v0.16.6).
|
|
633
753
|
const selfHealedBinary = await selfHealStaleBinary(latest);
|
|
634
754
|
|
|
755
|
+
// Same for the GLOBAL npm delivery surface (the `code-graph-mcp` CLI on
|
|
756
|
+
// PATH + any explicitly-installed platform package): nothing else ever
|
|
757
|
+
// updates it, and stale copies drift for months (0.46.0 wrapper) or years
|
|
758
|
+
// (the 0.16.6 platform relic).
|
|
759
|
+
const globalHeal = await selfHealGlobalPkgs(latest, state);
|
|
760
|
+
|
|
635
761
|
saveState({
|
|
636
762
|
...state,
|
|
637
763
|
installedVersion,
|
|
@@ -640,6 +766,7 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
640
766
|
updateAvailable: false,
|
|
641
767
|
rateLimited: false,
|
|
642
768
|
binaryUpdated: selfHealedBinary || state.binaryUpdated,
|
|
769
|
+
...globalHeal,
|
|
643
770
|
});
|
|
644
771
|
return selfHealedBinary
|
|
645
772
|
? { updated: false, binaryUpdated: true, from: installedVersion, to: installedVersion }
|
|
@@ -647,6 +774,8 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
647
774
|
} catch {
|
|
648
775
|
// Silent failure — never block session
|
|
649
776
|
return null;
|
|
777
|
+
} finally {
|
|
778
|
+
if (installLock) installLock.release();
|
|
650
779
|
}
|
|
651
780
|
}
|
|
652
781
|
|
|
@@ -656,7 +785,9 @@ module.exports = {
|
|
|
656
785
|
isSilentMode, isInstallMissingMode, isForceMode,
|
|
657
786
|
requestJson, resolveProxy, parseLatestRelease, fetchLatestRelease,
|
|
658
787
|
downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate, cachedBinaryStaleVsState,
|
|
788
|
+
getPlatformAssetName,
|
|
659
789
|
selfHealStaleBinary,
|
|
790
|
+
selfHealGlobalPkgs, staleGlobalPkgs, globalPkgVersion, npmInstallGlobal,
|
|
660
791
|
downloadAndInstall, refreshMarketplaceClone, marketplaceCloneDir,
|
|
661
792
|
};
|
|
662
793
|
|
|
@@ -8,6 +8,7 @@ const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-util
|
|
|
8
8
|
const {
|
|
9
9
|
getPluginVersion, readJson, healthCheck, CACHE_DIR,
|
|
10
10
|
settingsPath, surveyHookCoverage,
|
|
11
|
+
installedGlobalPkgs, GLOBAL_INSTALL_MARKER, SHELL_PKG,
|
|
11
12
|
} = require('./lifecycle');
|
|
12
13
|
const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
|
|
13
14
|
|
|
@@ -278,6 +279,29 @@ function runDiagnostics() {
|
|
|
278
279
|
}
|
|
279
280
|
} catch { /* probe failed — skip */ }
|
|
280
281
|
|
|
282
|
+
// 9. Global npm residue — the launcher's background install (or the user)
|
|
283
|
+
// may have `npm install -g`'d the shell + platform packages. Surface what
|
|
284
|
+
// exists and who owns cleanup: with the plugin-install marker,
|
|
285
|
+
// `lifecycle.js uninstall` removes them; without it they are treated as
|
|
286
|
+
// user-installed and a plugin uninstall leaves them on PATH.
|
|
287
|
+
try {
|
|
288
|
+
const { globalPkgVersion } = require('./auto-update');
|
|
289
|
+
const { PLATFORM_PKG } = require('./find-binary');
|
|
290
|
+
const found = [SHELL_PKG, PLATFORM_PKG]
|
|
291
|
+
.map((name) => ({ name, version: globalPkgVersion(name) }))
|
|
292
|
+
.filter((p) => p.version);
|
|
293
|
+
if (found.length) {
|
|
294
|
+
const marker = !!readJson(GLOBAL_INSTALL_MARKER);
|
|
295
|
+
results.push({
|
|
296
|
+
name: 'Global npm packages',
|
|
297
|
+
status: 'ok',
|
|
298
|
+
detail: found.map((p) => `${p.name}@${p.version}`).join(', ') + (marker
|
|
299
|
+
? ' — plugin-installed; `node lifecycle.js uninstall` removes them'
|
|
300
|
+
: ` — no plugin-install marker; uninstall leaves them (remove: npm uninstall -g ${found.map((p) => p.name).join(' ')})`),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
} catch { /* probe failed — skip */ }
|
|
304
|
+
|
|
281
305
|
return results;
|
|
282
306
|
}
|
|
283
307
|
|