@sdsrs/code-graph 0.120.0 → 0.121.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.120.0",
7
+ "version": "0.121.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -808,7 +808,17 @@ function unadopt({ cwd, home } = {}) {
808
808
  // Also sweep any legacy memory-dir remnants (uninstall before auto-migration ran).
809
809
  const migrated = migrateLegacyMemoryDir({ cwd, home });
810
810
 
811
- const registryUpdated = removeAdopted(effectiveCwd, home);
811
+ // Deregister ONLY what we actually cleaned. The registry is the sole record
812
+ // of which repos carry a managed block — `uninstall --unadopt-all` and the
813
+ // uninstall-time sweep are both driven by it — so dropping the entry for a
814
+ // project whose CLAUDE.md we could not rewrite (root-owned file, read-only
815
+ // mount, EPERM dir) strands that block with nothing left pointing at it.
816
+ // Harmless while the caller only ever passed the current cwd; load-bearing
817
+ // since the uninstall sweep walks the whole list, because an unconditional
818
+ // deregister empties the file and removeCacheResidue() only preserves a
819
+ // NON-EMPTY registry — so the failed project's record died with the cache.
820
+ const cleanupFailed = claudeMdUnwritable || claudeMdUnreadable;
821
+ const registryUpdated = cleanupFailed ? false : removeAdopted(effectiveCwd, home);
812
822
  return {
813
823
  ok: true, fileRemoved, blockPruned, claudeMdRemoved,
814
824
  claudeMdUnreadable, claudeMdUnwritable, registryUpdated,
@@ -889,7 +899,7 @@ if (require.main === module) {
889
899
 
890
900
  module.exports = {
891
901
  adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
892
- readAdoptedProjects, recordAdopted, removeAdopted, adoptedRegistryFile,
902
+ readAdoptedProjects, readAdoptedResult, recordAdopted, removeAdopted, adoptedRegistryFile,
893
903
  isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
894
904
  detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
895
905
  claudeMdPath, detailDir, detailPath,
@@ -601,12 +601,13 @@ function cleanupDisabledStatusline() {
601
601
  // our composite from settings.json, so Claude Code stops invoking us.
602
602
  let cacheRemoved = false;
603
603
  let unadopted = [];
604
+ let registryUnusable = false;
604
605
  if (uninstalled) {
605
- unadopted = unadoptRegisteredProjects();
606
+ ({ unadopted, registryUnusable } = unadoptRegisteredProjects());
606
607
  cacheRemoved = removeCacheResidue();
607
608
  }
608
609
 
609
- return { cleaned: true, settingsChanged, cacheRemoved, unadopted };
610
+ return { cleaned: true, settingsChanged, cacheRemoved, unadopted, registryUnusable };
610
611
  }
611
612
 
612
613
  /**
@@ -622,12 +623,20 @@ function cleanupDisabledStatusline() {
622
623
  */
623
624
  function unadoptRegisteredProjects() {
624
625
  const out = [];
625
- let readAdoptedProjects, unadopt;
626
- try { ({ readAdoptedProjects, unadopt } = require('./adopt')); }
627
- catch { return out; } // POSIX-only helper unavailable — teardown continues
628
- let projects = [];
629
- try { projects = readAdoptedProjects() || []; } catch { return out; }
630
- for (const project of projects) {
626
+ let readAdoptedResult, unadopt;
627
+ try { ({ readAdoptedResult, unadopt } = require('./adopt')); }
628
+ catch { return { unadopted: out, registryUnusable: false }; } // POSIX-only helper unavailable — teardown continues
629
+ // readAdoptedResult, NOT the lenient readAdoptedProjects(): that wrapper
630
+ // collapses "unreadable / truncated / wrong shape" into `[]`, which here is
631
+ // indistinguishable from "no projects to clean" — so a corrupt registry would
632
+ // silently sweep nothing AND still be deleted by removeCacheResidue() below,
633
+ // taking the only record of which repos carry a managed block with it. adopt.js
634
+ // documents the same bit for the same reason: only a genuinely ABSENT file may
635
+ // be read as empty.
636
+ let res;
637
+ try { res = readAdoptedResult(); } catch { return { unadopted: out, registryUnusable: true }; }
638
+ if (res && res.unusable) return { unadopted: out, registryUnusable: true };
639
+ for (const project of (res && res.list) || []) {
631
640
  try {
632
641
  const r = unadopt({ cwd: project });
633
642
  out.push({ project, cleaned: !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved)) });
@@ -635,7 +644,36 @@ function unadoptRegisteredProjects() {
635
644
  out.push({ project, cleaned: false, error: (e && e.message) || String(e) });
636
645
  }
637
646
  }
638
- return out;
647
+ reportUnadoptSweep(out);
648
+ return { unadopted: out, registryUnusable: false };
649
+ }
650
+
651
+ /**
652
+ * Tell the user their repos were just edited. Everything else about this
653
+ * teardown is invisible by construction: it runs inside a statusline render,
654
+ * both callers `process.exit(0)` on `cleaned` and discard the return value, and
655
+ * Claude Code fires no uninstall hook — so without this line the first signal is
656
+ * unexplained `CLAUDE.md` diffs in `git status` across several repositories.
657
+ * stderr, because a statusline's stdout IS the status line.
658
+ */
659
+ function reportUnadoptSweep(entries) {
660
+ try {
661
+ const cleaned = entries.filter((e) => e && e.cleaned).map((e) => e.project);
662
+ const failed = entries.filter((e) => e && !e.cleaned).map((e) => e.project);
663
+ if (!cleaned.length && !failed.length) return;
664
+ const lines = [];
665
+ if (cleaned.length) {
666
+ lines.push(`[code-graph] Plugin uninstalled — removed the managed CLAUDE.md block from ${cleaned.length} project(s):`);
667
+ for (const p of cleaned.slice(0, 10)) lines.push(` ${p}`);
668
+ if (cleaned.length > 10) lines.push(` …and ${cleaned.length - 10} more`);
669
+ lines.push(' Your own text outside the block was kept; .code-graph/ index dirs are untouched.');
670
+ }
671
+ if (failed.length) {
672
+ lines.push(`[code-graph] Could NOT clean ${failed.length} project(s) — remove the block by hand or run \`code-graph-mcp unadopt\` there:`);
673
+ for (const p of failed.slice(0, 10)) lines.push(` ${p}`);
674
+ }
675
+ process.stderr.write(lines.join('\n') + '\n');
676
+ } catch { /* a notice must never be able to fail a teardown */ }
639
677
  }
640
678
 
641
679
  // --- Scope Conflict Detection ---
@@ -1716,9 +1754,20 @@ function removeCacheResidue() {
1716
1754
  try {
1717
1755
  registryPath = require('./adopt').adoptedRegistryFile();
1718
1756
  const raw = fs.existsSync(registryPath) ? fs.readFileSync(registryPath) : null;
1719
- const parsed = raw ? JSON.parse(raw.toString('utf8')) : null;
1720
- if (Array.isArray(parsed) && parsed.length) registry = raw;
1721
- } catch { /* POSIX-only helper, unreadable, or corrupt — nothing to preserve */ }
1757
+ if (raw) {
1758
+ let parsed = null;
1759
+ let usable = true;
1760
+ try { parsed = JSON.parse(raw.toString('utf8')); } catch { usable = false; }
1761
+ // Preserve a NON-EMPTY list (projects still carry a block) and anything we
1762
+ // could not READ as a list. The unusable case used to fall into the same
1763
+ // "nothing to preserve" catch as a missing file — the strictly worse
1764
+ // outcome, since a registry we cannot parse is the one whose contents we
1765
+ // are least able to reconstruct, and the sweep above deliberately skips it
1766
+ // rather than guessing. Only a genuinely EMPTY array strands nothing and
1767
+ // is allowed to go with the cache dir.
1768
+ if (!usable || !Array.isArray(parsed) || parsed.length) registry = raw;
1769
+ }
1770
+ } catch { /* POSIX-only helper or an unreadable path — nothing to preserve */ }
1722
1771
  try {
1723
1772
  fs.rmSync(CACHE_DIR, { recursive: true, force: true });
1724
1773
  } catch { return false; }
@@ -35,7 +35,7 @@ jobs:
35
35
  node-version: '20'
36
36
  - name: Build snapshot
37
37
  run: |
38
- npx -y -p @sdsrs/code-graph@0.120.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.121.0 code-graph-mcp snapshot create --out snapshot.db
39
39
  zstd -9 snapshot.db -o snapshot.db.zst
40
40
  mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
41
41
  - name: Upload to release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.120.0",
3
+ "version": "0.121.0",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.120.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.120.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.120.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.120.0",
42
- "@sdsrs/code-graph-win32-x64": "0.120.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.121.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.121.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.121.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.121.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.121.0"
43
43
  }
44
44
  }