gitnexus 1.6.3-rc.8 → 1.6.3

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.
Files changed (285) hide show
  1. package/README.md +21 -5
  2. package/dist/_shared/graph/types.d.ts +16 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +20 -2
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/index.js +11 -0
  7. package/dist/_shared/index.js.map +1 -1
  8. package/dist/_shared/scope-resolution/def-index.js +2 -2
  9. package/dist/_shared/scope-resolution/def-index.js.map +1 -1
  10. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
  11. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
  12. package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
  13. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
  14. package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
  15. package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
  17. package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
  18. package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
  19. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
  20. package/dist/_shared/scope-resolution/parsed-file.js +54 -0
  21. package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
  22. package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
  23. package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/position-index.js +2 -2
  25. package/dist/_shared/scope-resolution/position-index.js.map +1 -1
  26. package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
  27. package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
  28. package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
  29. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
  30. package/dist/_shared/scope-resolution/reference-site.js +24 -0
  31. package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
  32. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
  33. package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
  34. package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
  35. package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
  36. package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
  37. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
  38. package/dist/_shared/scope-resolution/registries/context.js +44 -0
  39. package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
  40. package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
  41. package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
  42. package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
  43. package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
  44. package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
  45. package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
  46. package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
  47. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
  48. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
  49. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
  50. package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
  51. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
  52. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
  53. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
  54. package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
  55. package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
  56. package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
  57. package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
  58. package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
  59. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
  60. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
  61. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
  62. package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
  63. package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
  64. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
  65. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  66. package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
  67. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  68. package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
  69. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  70. package/dist/_shared/scope-resolution/scope-tree.js +3 -2
  71. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  72. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
  73. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
  74. package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
  75. package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
  76. package/dist/_shared/scope-resolution/types.d.ts +11 -0
  77. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  78. package/dist/cli/ai-context.js +35 -4
  79. package/dist/cli/analyze.d.ts +27 -0
  80. package/dist/cli/analyze.js +31 -1
  81. package/dist/cli/clean.js +19 -1
  82. package/dist/cli/group.js +73 -0
  83. package/dist/cli/index-repo.js +8 -1
  84. package/dist/cli/index.js +26 -1
  85. package/dist/cli/list.js +11 -1
  86. package/dist/cli/remove.d.ts +30 -0
  87. package/dist/cli/remove.js +99 -0
  88. package/dist/cli/setup.js +185 -57
  89. package/dist/cli/tool.d.ts +5 -0
  90. package/dist/cli/tool.js +42 -0
  91. package/dist/config/ignore-service.d.ts +9 -0
  92. package/dist/config/ignore-service.js +80 -13
  93. package/dist/core/embedding-mode.d.ts +30 -0
  94. package/dist/core/embedding-mode.js +30 -0
  95. package/dist/core/embeddings/ast-utils.js +22 -22
  96. package/dist/core/embeddings/chunker.js +30 -25
  97. package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
  98. package/dist/core/embeddings/embedding-pipeline.js +15 -6
  99. package/dist/core/embeddings/text-generator.d.ts +1 -1
  100. package/dist/core/embeddings/text-generator.js +33 -24
  101. package/dist/core/embeddings/types.d.ts +43 -1
  102. package/dist/core/embeddings/types.js +101 -29
  103. package/dist/core/git-staleness.d.ts +18 -0
  104. package/dist/core/git-staleness.js +108 -0
  105. package/dist/core/graph/graph.js +115 -20
  106. package/dist/core/graph/types.d.ts +12 -1
  107. package/dist/core/group/config-parser.d.ts +4 -0
  108. package/dist/core/group/config-parser.js +18 -1
  109. package/dist/core/group/cross-impact.d.ts +41 -0
  110. package/dist/core/group/cross-impact.js +441 -0
  111. package/dist/core/group/extractors/http-patterns/php.js +126 -18
  112. package/dist/core/group/group-path-utils.d.ts +17 -0
  113. package/dist/core/group/group-path-utils.js +40 -0
  114. package/dist/core/group/resolve-at-member.d.ts +10 -0
  115. package/dist/core/group/resolve-at-member.js +31 -0
  116. package/dist/core/group/service.d.ts +9 -0
  117. package/dist/core/group/service.js +259 -25
  118. package/dist/core/group/types.d.ts +30 -0
  119. package/dist/core/ingestion/ast-cache.d.ts +16 -1
  120. package/dist/core/ingestion/ast-cache.js +14 -2
  121. package/dist/core/ingestion/call-processor.js +9 -0
  122. package/dist/core/ingestion/emit-references.d.ts +88 -0
  123. package/dist/core/ingestion/emit-references.js +229 -0
  124. package/dist/core/ingestion/filesystem-walker.js +6 -4
  125. package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
  126. package/dist/core/ingestion/finalize-orchestrator.js +139 -0
  127. package/dist/core/ingestion/framework-detection.js +6 -2
  128. package/dist/core/ingestion/import-processor.js +4 -0
  129. package/dist/core/ingestion/import-resolvers/python.js +9 -6
  130. package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
  131. package/dist/core/ingestion/import-target-adapter.js +95 -0
  132. package/dist/core/ingestion/language-provider.d.ts +36 -33
  133. package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
  134. package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
  135. package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
  136. package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
  137. package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
  138. package/dist/core/ingestion/languages/csharp/arity.js +37 -0
  139. package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
  140. package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
  141. package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
  142. package/dist/core/ingestion/languages/csharp/captures.js +249 -0
  143. package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
  144. package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
  145. package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
  146. package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
  147. package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
  148. package/dist/core/ingestion/languages/csharp/index.js +82 -0
  149. package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
  150. package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
  151. package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
  152. package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
  153. package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
  154. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
  155. package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
  156. package/dist/core/ingestion/languages/csharp/query.js +515 -0
  157. package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
  158. package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
  159. package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
  160. package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
  161. package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
  162. package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
  163. package/dist/core/ingestion/languages/csharp.js +14 -0
  164. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
  165. package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
  166. package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
  167. package/dist/core/ingestion/languages/python/arity.js +38 -0
  168. package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
  169. package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
  170. package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
  171. package/dist/core/ingestion/languages/python/captures.js +106 -0
  172. package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
  173. package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
  174. package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
  175. package/dist/core/ingestion/languages/python/import-target.js +99 -0
  176. package/dist/core/ingestion/languages/python/index.d.ts +80 -0
  177. package/dist/core/ingestion/languages/python/index.js +80 -0
  178. package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
  179. package/dist/core/ingestion/languages/python/interpret.js +191 -0
  180. package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
  181. package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
  182. package/dist/core/ingestion/languages/python/query.d.ts +9 -0
  183. package/dist/core/ingestion/languages/python/query.js +267 -0
  184. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
  185. package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
  186. package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
  187. package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
  188. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
  189. package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
  190. package/dist/core/ingestion/languages/python.js +14 -0
  191. package/dist/core/ingestion/model/method-registry.d.ts +9 -0
  192. package/dist/core/ingestion/model/method-registry.js +4 -0
  193. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
  194. package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
  195. package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
  196. package/dist/core/ingestion/model/semantic-model.js +55 -0
  197. package/dist/core/ingestion/mro-processor.js +38 -22
  198. package/dist/core/ingestion/parsing-processor.d.ts +18 -1
  199. package/dist/core/ingestion/parsing-processor.js +45 -11
  200. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  201. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  202. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
  203. package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
  204. package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
  205. package/dist/core/ingestion/pipeline.js +2 -1
  206. package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
  207. package/dist/core/ingestion/registry-primary-flag.js +111 -0
  208. package/dist/core/ingestion/resolve-references.d.ts +63 -0
  209. package/dist/core/ingestion/resolve-references.js +175 -0
  210. package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
  211. package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
  212. package/dist/core/ingestion/scope-extractor.d.ts +86 -0
  213. package/dist/core/ingestion/scope-extractor.js +758 -0
  214. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
  215. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
  216. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
  217. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
  218. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
  219. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
  220. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
  221. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
  222. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
  223. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
  224. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
  225. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
  226. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
  227. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
  228. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
  229. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
  230. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
  231. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
  232. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
  233. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
  234. package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
  235. package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
  236. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
  237. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
  238. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
  239. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
  240. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
  241. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
  242. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
  243. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
  244. package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
  245. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
  246. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
  247. package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
  248. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
  249. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
  250. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
  251. package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
  252. package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
  253. package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
  254. package/dist/core/ingestion/shadow-harness.d.ts +113 -0
  255. package/dist/core/ingestion/shadow-harness.js +148 -0
  256. package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
  257. package/dist/core/ingestion/utils/ast-helpers.js +70 -0
  258. package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
  259. package/dist/core/ingestion/utils/max-file-size.js +52 -0
  260. package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
  261. package/dist/core/ingestion/workers/parse-worker.js +57 -21
  262. package/dist/core/lbug/lbug-adapter.d.ts +22 -2
  263. package/dist/core/lbug/lbug-adapter.js +58 -14
  264. package/dist/core/lbug/pool-adapter.d.ts +17 -0
  265. package/dist/core/lbug/pool-adapter.js +24 -14
  266. package/dist/core/run-analyze.d.ts +32 -0
  267. package/dist/core/run-analyze.js +74 -19
  268. package/dist/core/search/bm25-index.d.ts +18 -0
  269. package/dist/core/search/bm25-index.js +125 -12
  270. package/dist/core/tree-sitter/parser-loader.js +6 -1
  271. package/dist/mcp/local/local-backend.d.ts +67 -3
  272. package/dist/mcp/local/local-backend.js +296 -34
  273. package/dist/mcp/resources.d.ts +31 -0
  274. package/dist/mcp/resources.js +100 -17
  275. package/dist/mcp/tools.d.ts +4 -1
  276. package/dist/mcp/tools.js +75 -54
  277. package/dist/server/api.js +6 -2
  278. package/dist/storage/git.d.ts +49 -0
  279. package/dist/storage/git.js +111 -0
  280. package/dist/storage/repo-manager.d.ts +246 -1
  281. package/dist/storage/repo-manager.js +391 -9
  282. package/package.json +7 -6
  283. package/scripts/bench-scope-resolution.ts +134 -0
  284. package/scripts/ci-list-migrated-languages.ts +24 -0
  285. package/skills/gitnexus-cli.md +1 -0
package/dist/cli/list.js CHANGED
@@ -12,11 +12,21 @@ export const listCommand = async () => {
12
12
  return;
13
13
  }
14
14
  console.log(`\n Indexed Repositories (${entries.length})\n`);
15
+ // Count occurrences of each name so colliding entries can be
16
+ // disambiguated in the header (#829). Unique-name entries render
17
+ // identically to pre-#829 output; only collisions gain a suffix.
18
+ const nameCounts = new Map();
19
+ for (const e of entries) {
20
+ const key = e.name.toLowerCase();
21
+ nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
22
+ }
15
23
  for (const entry of entries) {
16
24
  const indexedDate = new Date(entry.indexedAt).toLocaleString();
17
25
  const stats = entry.stats || {};
18
26
  const commitShort = entry.lastCommit?.slice(0, 7) || 'unknown';
19
- console.log(` ${entry.name}`);
27
+ const hasCollision = (nameCounts.get(entry.name.toLowerCase()) ?? 0) > 1;
28
+ const header = hasCollision ? `${entry.name} (${entry.path})` : entry.name;
29
+ console.log(` ${header}`);
20
30
  console.log(` Path: ${entry.path}`);
21
31
  console.log(` Indexed: ${indexedDate}`);
22
32
  console.log(` Commit: ${commitShort}`);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Remove Command (#664)
3
+ *
4
+ * Delete the `.gitnexus/` index for a registered repo and unregister it
5
+ * from the global registry (~/.gitnexus/registry.json). The target is
6
+ * identified by alias / basename-derived name / remote-inferred name /
7
+ * absolute path — no `--repo` flag, just a positional argument so the
8
+ * destructive-command ergonomics match `clean` (which is also
9
+ * destructive but scoped to `process.cwd()`).
10
+ *
11
+ * Compared to `clean`:
12
+ * - `clean` acts on the repo discovered by walking up from cwd.
13
+ * - `remove` acts on any registered repo identified by name or path.
14
+ *
15
+ * Behaviour notes:
16
+ * - Idempotent on unknown targets: exits 0 with a warning so that
17
+ * `remove X && analyze Y` keeps working in scripts. Per #664:
18
+ * "behave atomically and idempotently so retries are safe".
19
+ * - Atomic order mirrors `clean`: fs.rm FIRST, then unregister. A
20
+ * partial failure leaves the registry pointing at a missing dir
21
+ * (recoverable by `listRegisteredRepos({ validate: true })` on
22
+ * next read) rather than the opposite, which would orphan
23
+ * .gitnexus/ directories on disk.
24
+ * - `-f` / `--force` matches the confirmation-skip semantics of
25
+ * `clean -f`. (Distinct from `analyze --force`, which re-indexes;
26
+ * here there is no pipeline, so no conflation.)
27
+ */
28
+ export declare const removeCommand: (target: string, options?: {
29
+ force?: boolean;
30
+ }) => Promise<void>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Remove Command (#664)
3
+ *
4
+ * Delete the `.gitnexus/` index for a registered repo and unregister it
5
+ * from the global registry (~/.gitnexus/registry.json). The target is
6
+ * identified by alias / basename-derived name / remote-inferred name /
7
+ * absolute path — no `--repo` flag, just a positional argument so the
8
+ * destructive-command ergonomics match `clean` (which is also
9
+ * destructive but scoped to `process.cwd()`).
10
+ *
11
+ * Compared to `clean`:
12
+ * - `clean` acts on the repo discovered by walking up from cwd.
13
+ * - `remove` acts on any registered repo identified by name or path.
14
+ *
15
+ * Behaviour notes:
16
+ * - Idempotent on unknown targets: exits 0 with a warning so that
17
+ * `remove X && analyze Y` keeps working in scripts. Per #664:
18
+ * "behave atomically and idempotently so retries are safe".
19
+ * - Atomic order mirrors `clean`: fs.rm FIRST, then unregister. A
20
+ * partial failure leaves the registry pointing at a missing dir
21
+ * (recoverable by `listRegisteredRepos({ validate: true })` on
22
+ * next read) rather than the opposite, which would orphan
23
+ * .gitnexus/ directories on disk.
24
+ * - `-f` / `--force` matches the confirmation-skip semantics of
25
+ * `clean -f`. (Distinct from `analyze --force`, which re-indexes;
26
+ * here there is no pipeline, so no conflation.)
27
+ */
28
+ import fs from 'fs/promises';
29
+ import { readRegistry, resolveRegistryEntry, assertSafeStoragePath, unregisterRepo, RegistryNotFoundError, RegistryAmbiguousTargetError, UnsafeStoragePathError, } from '../storage/repo-manager.js';
30
+ export const removeCommand = async (target, options) => {
31
+ // Read the registry snapshot once and pass it to the resolver — this
32
+ // lets us render the "before" state in the dry-run path without a
33
+ // second disk read.
34
+ const entries = await readRegistry();
35
+ let entry;
36
+ try {
37
+ entry = resolveRegistryEntry(entries, target);
38
+ }
39
+ catch (err) {
40
+ if (err instanceof RegistryNotFoundError) {
41
+ // Idempotent: missing target is a no-op warning, not an error.
42
+ // The `availableNames` hint comes from the error itself so users
43
+ // can see what they might have meant.
44
+ console.warn(`Nothing to remove: ${err.message}`);
45
+ return;
46
+ }
47
+ if (err instanceof RegistryAmbiguousTargetError) {
48
+ // Duplicate aliases are allowed via --allow-duplicate-name (#829);
49
+ // refuse to guess which one the user meant — surface the full list
50
+ // and exit non-zero so scripts don't silently pick the wrong repo.
51
+ console.error(`Error: ${err.message}`);
52
+ process.exit(1);
53
+ }
54
+ throw err;
55
+ }
56
+ // Confirmation gate — same shape as `clean`. Default is a dry-run
57
+ // that describes what would be deleted; `--force` actually deletes.
58
+ if (!options?.force) {
59
+ console.log(`This will delete the GitNexus index for: ${entry.name}`);
60
+ console.log(` Path: ${entry.path}`);
61
+ console.log(` Storage: ${entry.storagePath}`);
62
+ console.log('\nRun with --force to confirm deletion.');
63
+ return;
64
+ }
65
+ // Safety guard (#1003 review — @magyargergo): refuse to proceed if
66
+ // the registry entry's `storagePath` isn't the canonical
67
+ // `<entry.path>/.gitnexus` subfolder. `~/.gitnexus/registry.json` is
68
+ // user-writable, so a corrupted or hand-edited entry could point
69
+ // storagePath at the repo root, an empty string (→ cwd), a parent
70
+ // dir, or anywhere else; `fs.rm(recursive: true, force: true)` on
71
+ // any of those would be a runtime disaster. Bail before touching
72
+ // disk, with an actionable hint for recovering a broken registry.
73
+ try {
74
+ assertSafeStoragePath(entry);
75
+ }
76
+ catch (err) {
77
+ if (err instanceof UnsafeStoragePathError) {
78
+ console.error(`Error: ${err.message}`);
79
+ process.exit(1);
80
+ }
81
+ throw err;
82
+ }
83
+ // Deletion order: fs.rm first, then unregister. If fs.rm fails mid-way,
84
+ // the registry entry stays so the user can retry. If fs.rm succeeds but
85
+ // unregister throws (e.g. ENOSPC on registry write), the entry becomes
86
+ // orphaned — `listRegisteredRepos({ validate: true })` prunes those on
87
+ // next read, so the failure is self-healing.
88
+ try {
89
+ await fs.rm(entry.storagePath, { recursive: true, force: true });
90
+ await unregisterRepo(entry.path);
91
+ console.log(`Removed: ${entry.name}`);
92
+ console.log(` Path: ${entry.path}`);
93
+ console.log(` Storage: ${entry.storagePath}`);
94
+ }
95
+ catch (err) {
96
+ console.error(`Failed to remove ${entry.name}:`, err);
97
+ process.exit(1);
98
+ }
99
+ };
package/dist/cli/setup.js CHANGED
@@ -12,6 +12,7 @@ import { execFile, execFileSync } from 'child_process';
12
12
  import { promisify } from 'util';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { glob } from 'glob';
15
+ import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
15
16
  import { getGlobalDir } from '../storage/repo-manager.js';
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
@@ -64,37 +65,61 @@ function getMcpEntry() {
64
65
  };
65
66
  }
66
67
  /**
67
- * Merge gitnexus entry into an existing MCP config JSON object.
68
- * Returns the updated config.
68
+ * OpenCode uses a different MCP format: { type: "local", command: [...] }
69
+ * where command is a flat array (command + args combined).
69
70
  */
70
- function mergeMcpConfig(existing) {
71
- if (!existing || typeof existing !== 'object') {
72
- existing = {};
71
+ function getOpenCodeMcpEntry() {
72
+ const bin = resolveGitnexusBin();
73
+ if (bin) {
74
+ return { type: 'local', command: [bin, 'mcp'] };
73
75
  }
74
- if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
75
- existing.mcpServers = {};
76
+ if (process.platform === 'win32') {
77
+ return { type: 'local', command: ['cmd', '/c', 'npx', '-y', 'gitnexus@latest', 'mcp'] };
76
78
  }
77
- existing.mcpServers.gitnexus = getMcpEntry();
78
- return existing;
79
+ return { type: 'local', command: ['npx', '-y', 'gitnexus@latest', 'mcp'] };
79
80
  }
80
81
  /**
81
- * Try to read a JSON file, returning null if it doesn't exist or is invalid.
82
+ * Detect indentation style from file content.
83
+ * Returns formatting options matching the file's existing style.
82
84
  */
83
- async function readJsonFile(filePath) {
84
- try {
85
- const raw = await fs.readFile(filePath, 'utf-8');
86
- return JSON.parse(raw);
87
- }
88
- catch {
89
- return null;
90
- }
85
+ function detectIndentation(raw) {
86
+ const firstIndented = raw.match(/^( +|\t)/m);
87
+ if (!firstIndented)
88
+ return { tabSize: 2, insertSpaces: true };
89
+ if (firstIndented[1] === '\t')
90
+ return { tabSize: 1, insertSpaces: false };
91
+ return { tabSize: firstIndented[1].length, insertSpaces: true };
91
92
  }
92
93
  /**
93
- * Write JSON to a file, creating parent directories if needed.
94
+ * Merge a key/value pair into a JSONC config file, preserving comments and formatting.
95
+ * If the file is genuinely corrupt (not valid JSONC), leaves it untouched.
94
96
  */
95
- async function writeJsonFile(filePath, data) {
96
- await fs.mkdir(path.dirname(filePath), { recursive: true });
97
- await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
97
+ async function mergeJsoncFile(filePath, keyPath, value) {
98
+ let raw;
99
+ try {
100
+ raw = await fs.readFile(filePath, 'utf-8');
101
+ }
102
+ catch {
103
+ raw = '';
104
+ }
105
+ if (raw.trim().length === 0) {
106
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
107
+ const formattingOptions = { tabSize: 2, insertSpaces: true };
108
+ const edits = modify('{}', keyPath, value, { formattingOptions });
109
+ const result = applyEdits('{}', edits);
110
+ await fs.writeFile(filePath, result, 'utf-8');
111
+ return true;
112
+ }
113
+ const parseErrors = [];
114
+ const tree = parseTree(raw, parseErrors);
115
+ if (tree && tree.type === 'object' && parseErrors.length === 0) {
116
+ const formattingOptions = detectIndentation(raw);
117
+ const edits = modify(raw, keyPath, value, { formattingOptions });
118
+ const result = applyEdits(raw, edits);
119
+ await fs.writeFile(filePath, result, 'utf-8');
120
+ return true;
121
+ }
122
+ return false;
98
123
  }
99
124
  /**
100
125
  * Check if a directory exists
@@ -117,10 +142,13 @@ async function setupCursor(result) {
117
142
  }
118
143
  const mcpPath = path.join(cursorDir, 'mcp.json');
119
144
  try {
120
- const existing = await readJsonFile(mcpPath);
121
- const updated = mergeMcpConfig(existing);
122
- await writeJsonFile(mcpPath, updated);
123
- result.configured.push('Cursor');
145
+ const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
146
+ if (ok) {
147
+ result.configured.push('Cursor');
148
+ }
149
+ else {
150
+ result.errors.push('Cursor: mcp.json is corrupt — skipping to preserve existing content');
151
+ }
124
152
  }
125
153
  catch (err) {
126
154
  result.errors.push(`Cursor: ${err.message}`);
@@ -135,10 +163,13 @@ async function setupClaudeCode(result) {
135
163
  // Claude Code stores MCP config in ~/.claude.json
136
164
  const mcpPath = path.join(os.homedir(), '.claude.json');
137
165
  try {
138
- const existing = await readJsonFile(mcpPath);
139
- const updated = mergeMcpConfig(existing);
140
- await writeJsonFile(mcpPath, updated);
141
- result.configured.push('Claude Code');
166
+ const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
167
+ if (ok) {
168
+ result.configured.push('Claude Code');
169
+ }
170
+ else {
171
+ result.errors.push('Claude Code: .claude.json is corrupt — skipping to preserve existing content');
172
+ }
142
173
  }
143
174
  catch (err) {
144
175
  result.errors.push(`Claude Code: ${err.message}`);
@@ -162,9 +193,71 @@ async function installClaudeCodeSkills(result) {
162
193
  result.errors.push(`Claude Code skills: ${err.message}`);
163
194
  }
164
195
  }
196
+ /**
197
+ * Check whether an event array already contains a gitnexus-hook entry.
198
+ */
199
+ function hasGitnexusHook(hooksObj, eventName) {
200
+ const entries = hooksObj?.[eventName];
201
+ if (!Array.isArray(entries))
202
+ return false;
203
+ return entries.some((h) => Array.isArray(h.hooks) &&
204
+ h.hooks.some((hh) => typeof hh.command === 'string' && hh.command.includes('gitnexus-hook')));
205
+ }
206
+ /**
207
+ * Merge hook entries into a JSONC settings file, preserving comments and formatting.
208
+ * Uses chained modify()+applyEdits() calls to append to arrays without a full
209
+ * JSON.stringify roundtrip that would strip comments.
210
+ */
211
+ async function mergeHooksJsonc(filePath, entries) {
212
+ let raw;
213
+ try {
214
+ raw = await fs.readFile(filePath, 'utf-8');
215
+ }
216
+ catch {
217
+ raw = '';
218
+ }
219
+ if (raw.trim().length === 0) {
220
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
221
+ const hooks = {};
222
+ for (const { eventName, value } of entries) {
223
+ hooks[eventName] = [value];
224
+ }
225
+ const formattingOptions = { tabSize: 2, insertSpaces: true };
226
+ const edits = modify('{}', ['hooks'], hooks, { formattingOptions });
227
+ await fs.writeFile(filePath, applyEdits('{}', edits), 'utf-8');
228
+ return true;
229
+ }
230
+ const parseErrors = [];
231
+ const tree = parseTree(raw, parseErrors);
232
+ if (!tree || tree.type !== 'object' || parseErrors.length > 0) {
233
+ return false;
234
+ }
235
+ const formattingOptions = detectIndentation(raw);
236
+ let current = raw;
237
+ for (const { eventName, value } of entries) {
238
+ // Re-parse after each edit to get a fresh insertion index.
239
+ const currentTree = parseTree(current, []);
240
+ const hooksNode = currentTree?.children?.find((c) => c.type === 'property' && c.children?.[0]?.value === 'hooks');
241
+ const eventNode = hooksNode?.children?.[1]?.children?.find((c) => c.type === 'property' && c.children?.[0]?.value === eventName);
242
+ let insertIndex;
243
+ if (eventNode?.children?.[1] && Array.isArray(eventNode.children[1].children)) {
244
+ insertIndex = eventNode.children[1].children.length;
245
+ }
246
+ else {
247
+ insertIndex = 0;
248
+ }
249
+ const edits = modify(current, ['hooks', eventName, insertIndex], value, {
250
+ formattingOptions,
251
+ });
252
+ current = applyEdits(current, edits);
253
+ }
254
+ await fs.writeFile(filePath, current, 'utf-8');
255
+ return true;
256
+ }
165
257
  /**
166
258
  * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
167
- * Merges hook config without overwriting existing hooks.
259
+ * Merges hook config without overwriting existing hooks, preserving
260
+ * comments and formatting in the JSONC file.
168
261
  */
169
262
  async function installClaudeCodeHooks(result) {
170
263
  const claudeDir = path.join(os.homedir(), '.claude');
@@ -181,8 +274,6 @@ async function installClaudeCodeHooks(result) {
181
274
  const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
182
275
  try {
183
276
  let content = await fs.readFile(src, 'utf-8');
184
- // Inject resolved CLI path so the copied hook can find the CLI
185
- // even when it's no longer inside the npm package tree
186
277
  const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
187
278
  const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
188
279
  const jsonCli = JSON.stringify(normalizedCli);
@@ -194,25 +285,62 @@ async function installClaudeCodeHooks(result) {
194
285
  }
195
286
  const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
196
287
  const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
197
- // Merge hook config into ~/.claude/settings.json
198
- const existing = (await readJsonFile(settingsPath)) || {};
199
- if (!existing.hooks)
200
- existing.hooks = {};
201
- function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
202
- if (!existing.hooks[eventName])
203
- existing.hooks[eventName] = [];
204
- const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus-hook')));
205
- if (!hasHook) {
206
- existing.hooks[eventName].push({
207
- matcher,
208
- hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
209
- });
288
+ // Check which hook events need entries (idempotent: skip if already registered)
289
+ const parsed = await (async () => {
290
+ try {
291
+ const r = await fs.readFile(settingsPath, 'utf-8');
292
+ return parseJsonc(r);
293
+ }
294
+ catch {
295
+ return null;
210
296
  }
297
+ })();
298
+ const hookEntries = [];
299
+ // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
300
+ // Session context is delivered via CLAUDE.md / skills instead.
301
+ if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse')) {
302
+ hookEntries.push({
303
+ eventName: 'PreToolUse',
304
+ value: {
305
+ matcher: 'Grep|Glob|Bash',
306
+ hooks: [
307
+ {
308
+ type: 'command',
309
+ command: hookCmd,
310
+ timeout: 10,
311
+ statusMessage: 'Enriching with GitNexus graph context...',
312
+ },
313
+ ],
314
+ },
315
+ });
316
+ }
317
+ if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse')) {
318
+ hookEntries.push({
319
+ eventName: 'PostToolUse',
320
+ value: {
321
+ matcher: 'Bash',
322
+ hooks: [
323
+ {
324
+ type: 'command',
325
+ command: hookCmd,
326
+ timeout: 10,
327
+ statusMessage: 'Checking GitNexus index freshness...',
328
+ },
329
+ ],
330
+ },
331
+ });
332
+ }
333
+ if (hookEntries.length === 0) {
334
+ result.configured.push('Claude Code hooks (already configured)');
335
+ return;
336
+ }
337
+ const ok = await mergeHooksJsonc(settingsPath, hookEntries);
338
+ if (ok) {
339
+ result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
340
+ }
341
+ else {
342
+ result.errors.push('Claude Code hooks: settings.json is corrupt — skipping to preserve existing content');
211
343
  }
212
- ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
213
- ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
214
- await writeJsonFile(settingsPath, existing);
215
- result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
216
344
  }
217
345
  catch (err) {
218
346
  result.errors.push(`Claude Code hooks: ${err.message}`);
@@ -226,13 +354,13 @@ async function setupOpenCode(result) {
226
354
  }
227
355
  const configPath = path.join(opencodeDir, 'opencode.json');
228
356
  try {
229
- const existing = await readJsonFile(configPath);
230
- const config = existing || {};
231
- if (!config.mcp)
232
- config.mcp = {};
233
- config.mcp.gitnexus = getMcpEntry();
234
- await writeJsonFile(configPath, config);
235
- result.configured.push('OpenCode');
357
+ const ok = await mergeJsoncFile(configPath, ['mcp', 'gitnexus'], getOpenCodeMcpEntry());
358
+ if (ok) {
359
+ result.configured.push('OpenCode');
360
+ }
361
+ else {
362
+ result.errors.push('OpenCode: opencode.json is corrupt — skipping to preserve existing content');
363
+ }
236
364
  }
237
365
  catch (err) {
238
366
  result.errors.push(`OpenCode: ${err.message}`);
@@ -36,3 +36,8 @@ export declare function impactCommand(target: string, options?: {
36
36
  export declare function cypherCommand(query: string, options?: {
37
37
  repo?: string;
38
38
  }): Promise<void>;
39
+ export declare function detectChangesCommand(options?: {
40
+ scope?: string;
41
+ baseRef?: string;
42
+ repo?: string;
43
+ }): Promise<void>;
package/dist/cli/tool.js CHANGED
@@ -124,3 +124,45 @@ export async function cypherCommand(query, options) {
124
124
  });
125
125
  output(result);
126
126
  }
127
+ function formatDetectChangesResult(result) {
128
+ if (result?.error)
129
+ return `Error: ${result.error}`;
130
+ const summary = result?.summary || {};
131
+ if ((summary.changed_count || 0) === 0) {
132
+ return 'No changes detected.';
133
+ }
134
+ const lines = [];
135
+ lines.push(`Changes: ${summary.changed_files || 0} files, ${summary.changed_count || 0} symbols`);
136
+ lines.push(`Affected processes: ${summary.affected_count || 0}`);
137
+ lines.push(`Risk level: ${summary.risk_level || 'unknown'}`);
138
+ lines.push('');
139
+ const changed = result?.changed_symbols || [];
140
+ if (changed.length > 0) {
141
+ lines.push('Changed symbols:');
142
+ for (const symbol of changed.slice(0, 15)) {
143
+ lines.push(` ${symbol.type} ${symbol.name} → ${symbol.filePath}`);
144
+ }
145
+ if (changed.length > 15) {
146
+ lines.push(` ... and ${changed.length - 15} more`);
147
+ }
148
+ lines.push('');
149
+ }
150
+ const affected = result?.affected_processes || [];
151
+ if (affected.length > 0) {
152
+ lines.push('Affected execution flows:');
153
+ for (const processInfo of affected.slice(0, 10)) {
154
+ const steps = (processInfo.changed_steps || []).map((s) => s.symbol).join(', ');
155
+ lines.push(` • ${processInfo.name} (${processInfo.step_count} steps) — changed: ${steps}`);
156
+ }
157
+ }
158
+ return lines.join('\n').trim();
159
+ }
160
+ export async function detectChangesCommand(options) {
161
+ const backend = await getBackend();
162
+ const result = await backend.callTool('detect_changes', {
163
+ scope: options?.scope || 'unstaged',
164
+ base_ref: options?.baseRef,
165
+ repo: options?.repo,
166
+ });
167
+ output(formatDetectChangesResult(result));
168
+ }
@@ -19,6 +19,15 @@ export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions
19
19
  *
20
20
  * Returns an IgnoreLike object for glob's `ignore` option,
21
21
  * enabling directory-level pruning during traversal.
22
+ *
23
+ * Precedence (#771): user's `.gitnexusignore` negation patterns take
24
+ * priority over the hardcoded list, matching `.gitignore` semantics.
25
+ * An explicit `!pattern` rule unignores descendants even when they
26
+ * would otherwise be blocked by DEFAULT_IGNORE_LIST — UNLESS a more
27
+ * specific rule in the same file re-ignores a subset (e.g.
28
+ * `!__tests__/` paired with `__tests__/generated/` blocks the child
29
+ * while leaving the parent negated). Last-match-wins is enforced by
30
+ * consulting `ig.ignores(rel)` after `hasExplicitUnignore`.
22
31
  */
23
32
  export declare const createIgnoreFilter: (repoPath: string, options?: IgnoreOptions) => Promise<{
24
33
  ignored(p: Path): boolean;