gitnexus 1.6.3-rc.9 → 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.
- package/README.md +21 -5
- package/dist/_shared/graph/types.d.ts +16 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +4 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/def-index.js +2 -2
- package/dist/_shared/scope-resolution/def-index.js.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +54 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/position-index.js +2 -2
- package/dist/_shared/scope-resolution/position-index.js.map +1 -1
- package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +5 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.js +21 -5
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.js +3 -2
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +11 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.js +35 -4
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +31 -1
- package/dist/cli/clean.js +19 -1
- package/dist/cli/group.js +73 -0
- package/dist/cli/index-repo.js +8 -1
- package/dist/cli/index.js +26 -1
- package/dist/cli/list.js +11 -1
- package/dist/cli/remove.d.ts +30 -0
- package/dist/cli/remove.js +99 -0
- package/dist/cli/setup.js +185 -57
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +42 -0
- package/dist/config/ignore-service.d.ts +9 -0
- package/dist/config/ignore-service.js +80 -13
- package/dist/core/embedding-mode.d.ts +30 -0
- package/dist/core/embedding-mode.js +30 -0
- package/dist/core/embeddings/ast-utils.js +22 -22
- package/dist/core/embeddings/chunker.js +30 -25
- package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
- package/dist/core/embeddings/embedding-pipeline.js +15 -6
- package/dist/core/embeddings/text-generator.d.ts +1 -1
- package/dist/core/embeddings/text-generator.js +33 -24
- package/dist/core/embeddings/types.d.ts +43 -1
- package/dist/core/embeddings/types.js +101 -29
- package/dist/core/git-staleness.d.ts +18 -0
- package/dist/core/git-staleness.js +108 -0
- package/dist/core/graph/graph.js +115 -20
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/group/config-parser.d.ts +4 -0
- package/dist/core/group/config-parser.js +18 -1
- package/dist/core/group/cross-impact.d.ts +41 -0
- package/dist/core/group/cross-impact.js +441 -0
- package/dist/core/group/extractors/http-patterns/php.js +126 -18
- package/dist/core/group/group-path-utils.d.ts +17 -0
- package/dist/core/group/group-path-utils.js +40 -0
- package/dist/core/group/resolve-at-member.d.ts +10 -0
- package/dist/core/group/resolve-at-member.js +31 -0
- package/dist/core/group/service.d.ts +9 -0
- package/dist/core/group/service.js +259 -25
- package/dist/core/group/types.d.ts +30 -0
- package/dist/core/ingestion/ast-cache.d.ts +16 -1
- package/dist/core/ingestion/ast-cache.js +14 -2
- package/dist/core/ingestion/call-processor.js +9 -0
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/filesystem-walker.js +6 -4
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +139 -0
- package/dist/core/ingestion/framework-detection.js +6 -2
- package/dist/core/ingestion/import-processor.js +4 -0
- package/dist/core/ingestion/import-resolvers/python.js +9 -6
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -33
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
- package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
- package/dist/core/ingestion/languages/csharp/arity.js +37 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/captures.js +249 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
- package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
- package/dist/core/ingestion/languages/csharp/index.js +82 -0
- package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
- package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
- package/dist/core/ingestion/languages/csharp/query.js +515 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
- package/dist/core/ingestion/languages/csharp.js +14 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +106 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/python/import-target.js +99 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +9 -0
- package/dist/core/ingestion/languages/python/query.js +267 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
- package/dist/core/ingestion/languages/python.js +14 -0
- package/dist/core/ingestion/model/method-registry.d.ts +9 -0
- package/dist/core/ingestion/model/method-registry.js +4 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
- package/dist/core/ingestion/model/semantic-model.js +55 -0
- package/dist/core/ingestion/mro-processor.js +38 -22
- package/dist/core/ingestion/parsing-processor.d.ts +18 -1
- package/dist/core/ingestion/parsing-processor.js +45 -11
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
- package/dist/core/ingestion/registry-primary-flag.js +111 -0
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
- package/dist/core/ingestion/scope-extractor.d.ts +86 -0
- package/dist/core/ingestion/scope-extractor.js +758 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
- package/dist/core/ingestion/utils/ast-helpers.js +70 -0
- package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
- package/dist/core/ingestion/utils/max-file-size.js +52 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
- package/dist/core/ingestion/workers/parse-worker.js +57 -21
- package/dist/core/lbug/lbug-adapter.d.ts +22 -2
- package/dist/core/lbug/lbug-adapter.js +58 -14
- package/dist/core/lbug/pool-adapter.d.ts +17 -0
- package/dist/core/lbug/pool-adapter.js +24 -14
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +74 -19
- package/dist/core/search/bm25-index.d.ts +18 -0
- package/dist/core/search/bm25-index.js +125 -12
- package/dist/core/tree-sitter/parser-loader.js +6 -1
- package/dist/mcp/local/local-backend.d.ts +67 -3
- package/dist/mcp/local/local-backend.js +296 -34
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +100 -17
- package/dist/mcp/tools.d.ts +4 -1
- package/dist/mcp/tools.js +75 -54
- package/dist/server/api.js +6 -2
- package/dist/storage/git.d.ts +49 -0
- package/dist/storage/git.js +111 -0
- package/dist/storage/repo-manager.d.ts +246 -1
- package/dist/storage/repo-manager.js +391 -9
- package/package.json +7 -6
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
- package/skills/gitnexus-cli.md +1 -0
package/dist/cli/group.js
CHANGED
|
@@ -149,6 +149,79 @@ export function registerGroupCommands(program) {
|
|
|
149
149
|
console.log(`\nWrote contracts.json (${result.contracts.length} contracts, ${result.crossLinks.length} cross-links)`);
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
|
+
group
|
|
153
|
+
.command('impact <name>')
|
|
154
|
+
.description('Cross-repo impact for a symbol in one member repo of a group')
|
|
155
|
+
.requiredOption('--target <symbol>', 'Symbol or file name to analyze')
|
|
156
|
+
.requiredOption('--repo <groupPath>', 'Member path from group.yaml (e.g. app/backend), not the indexed repo name')
|
|
157
|
+
.option('--direction <dir>', 'upstream or downstream', 'upstream')
|
|
158
|
+
.option('--service <path>', 'Optional monorepo service directory prefix (path filter)')
|
|
159
|
+
.option('--subgroup <path>', 'Optional prefix limiting which group repos participate in cross fan-out')
|
|
160
|
+
.option('--max-depth <n>', 'Max graph traversal depth')
|
|
161
|
+
.option('--cross-depth <n>', 'Cross-repository hop depth')
|
|
162
|
+
.option('--min-confidence <n>', 'Minimum relation confidence (0–1)')
|
|
163
|
+
.option('--include-tests', 'Include test files in traversal', false)
|
|
164
|
+
.option('--timeout-ms <n>', 'Phase-1 local impact wall time in milliseconds')
|
|
165
|
+
.option('--json', 'JSON output')
|
|
166
|
+
.action(async (name, opts) => {
|
|
167
|
+
const { LocalBackend } = await import('../mcp/local/local-backend.js');
|
|
168
|
+
const backend = new LocalBackend();
|
|
169
|
+
try {
|
|
170
|
+
await backend.init();
|
|
171
|
+
const payload = {
|
|
172
|
+
name,
|
|
173
|
+
repo: opts.repo,
|
|
174
|
+
target: opts.target,
|
|
175
|
+
direction: opts.direction || 'upstream',
|
|
176
|
+
};
|
|
177
|
+
if (opts.service)
|
|
178
|
+
payload.service = opts.service;
|
|
179
|
+
if (opts.subgroup)
|
|
180
|
+
payload.subgroup = opts.subgroup;
|
|
181
|
+
if (opts.maxDepth !== undefined && opts.maxDepth !== '') {
|
|
182
|
+
const n = parseInt(String(opts.maxDepth), 10);
|
|
183
|
+
if (!Number.isNaN(n))
|
|
184
|
+
payload.maxDepth = n;
|
|
185
|
+
}
|
|
186
|
+
if (opts.crossDepth !== undefined && opts.crossDepth !== '') {
|
|
187
|
+
const n = parseInt(String(opts.crossDepth), 10);
|
|
188
|
+
if (!Number.isNaN(n))
|
|
189
|
+
payload.crossDepth = n;
|
|
190
|
+
}
|
|
191
|
+
if (opts.minConfidence !== undefined && opts.minConfidence !== '') {
|
|
192
|
+
const n = parseFloat(String(opts.minConfidence));
|
|
193
|
+
if (!Number.isNaN(n))
|
|
194
|
+
payload.minConfidence = n;
|
|
195
|
+
}
|
|
196
|
+
if (opts.timeoutMs !== undefined && opts.timeoutMs !== '') {
|
|
197
|
+
const n = parseInt(String(opts.timeoutMs), 10);
|
|
198
|
+
if (!Number.isNaN(n))
|
|
199
|
+
payload.timeoutMs = n;
|
|
200
|
+
}
|
|
201
|
+
if (opts.includeTests)
|
|
202
|
+
payload.includeTests = true;
|
|
203
|
+
const raw = await backend.getGroupService().groupImpact(payload);
|
|
204
|
+
if (raw && typeof raw === 'object' && 'error' in raw) {
|
|
205
|
+
console.error(String(raw.error));
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (opts.json) {
|
|
210
|
+
console.log(JSON.stringify(raw, null, 2));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const summary = raw?.summary;
|
|
214
|
+
const risk = raw?.risk;
|
|
215
|
+
console.log(`Group impact for "${name}" (${String(opts.repo)}): risk=${risk ?? '?'}`);
|
|
216
|
+
if (summary) {
|
|
217
|
+
console.log(` direct=${summary.direct ?? 0} processes=${summary.processes_affected ?? 0} cross=${summary.cross_repo_hits ?? 0}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await backend.dispose().catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
152
225
|
group
|
|
153
226
|
.command('query <name> <query>')
|
|
154
227
|
.description('Search execution flows across all repos in a group')
|
package/dist/cli/index-repo.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
13
|
import { getStoragePaths, loadMeta, addToGitignore, registerRepo, } from '../storage/repo-manager.js';
|
|
14
|
-
import { getGitRoot, isGitRepo } from '../storage/git.js';
|
|
14
|
+
import { getGitRoot, getRemoteUrl, isGitRepo } from '../storage/git.js';
|
|
15
15
|
export const indexCommand = async (inputPathParts, options) => {
|
|
16
16
|
console.log('\n GitNexus Index\n');
|
|
17
17
|
const inputPath = inputPathParts?.length ? inputPathParts.join(' ') : undefined;
|
|
@@ -88,6 +88,13 @@ export const indexCommand = async (inputPathParts, options) => {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
// ── Register in global registry ───────────────────────────────────
|
|
91
|
+
// Refresh the on-disk meta with a freshly captured `remoteUrl` if
|
|
92
|
+
// it's missing, so an `index` of an older `.gitnexus/` still gets
|
|
93
|
+
// sibling-clone fingerprinting on subsequent use without forcing a
|
|
94
|
+
// full re-analyze.
|
|
95
|
+
if (!meta.remoteUrl && isGitRepo(repoPath)) {
|
|
96
|
+
meta.remoteUrl = getRemoteUrl(repoPath);
|
|
97
|
+
}
|
|
91
98
|
await registerRepo(repoPath, meta);
|
|
92
99
|
await addToGitignore(repoPath);
|
|
93
100
|
const projectName = path.basename(repoPath);
|
package/dist/cli/index.js
CHANGED
|
@@ -18,12 +18,23 @@ program
|
|
|
18
18
|
.description('Index a repository (full analysis)')
|
|
19
19
|
.option('-f, --force', 'Force full re-index even if up to date')
|
|
20
20
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
21
|
+
.option('--drop-embeddings', 'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' +
|
|
22
|
+
'preserves any embeddings already present in the index.')
|
|
21
23
|
.option('--skills', 'Generate repo-specific skill files from detected communities')
|
|
22
24
|
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
|
|
23
25
|
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
|
|
24
26
|
.option('--skip-git', 'Index a folder without requiring a .git directory')
|
|
27
|
+
.option('--name <alias>', 'Register this repo under a custom name in ~/.gitnexus/registry.json ' +
|
|
28
|
+
'(disambiguates repos whose paths share a basename, e.g. two different .../app folders)')
|
|
29
|
+
.option('--allow-duplicate-name', 'Register this repo even if another path already uses the same --name alias. ' +
|
|
30
|
+
'Leaves `-r <name>` ambiguous for the two paths; use -r <path> to disambiguate.')
|
|
25
31
|
.option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
|
|
26
|
-
.
|
|
32
|
+
.option('--max-file-size <kb>', 'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).')
|
|
33
|
+
.addHelpText('after', '\nEnvironment variables:\n' +
|
|
34
|
+
' GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n' +
|
|
35
|
+
' GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n' +
|
|
36
|
+
'\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n' +
|
|
37
|
+
' `!__tests__/` to index a directory that is auto-filtered by default (#771).')
|
|
27
38
|
.action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
|
|
28
39
|
program
|
|
29
40
|
.command('index [path...]')
|
|
@@ -55,6 +66,12 @@ program
|
|
|
55
66
|
.option('-f, --force', 'Skip confirmation prompt')
|
|
56
67
|
.option('--all', 'Clean all indexed repos')
|
|
57
68
|
.action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
|
|
69
|
+
program
|
|
70
|
+
.command('remove <target>')
|
|
71
|
+
.description('Delete the GitNexus index for a registered repo (by alias, name, or absolute path). ' +
|
|
72
|
+
'Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.')
|
|
73
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
74
|
+
.action(createLazyAction(() => import('./remove.js'), 'removeCommand'));
|
|
58
75
|
program
|
|
59
76
|
.command('wiki [path]')
|
|
60
77
|
.description('Generate repository wiki from knowledge graph')
|
|
@@ -107,6 +124,14 @@ program
|
|
|
107
124
|
.description('Execute raw Cypher query against the knowledge graph')
|
|
108
125
|
.option('-r, --repo <name>', 'Target repository')
|
|
109
126
|
.action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
|
|
127
|
+
program
|
|
128
|
+
.command('detect-changes')
|
|
129
|
+
.alias('detect_changes')
|
|
130
|
+
.description('Map git diff hunks to indexed symbols and affected execution flows')
|
|
131
|
+
.option('-s, --scope <scope>', 'What to analyze: unstaged, staged, all, or compare', 'unstaged')
|
|
132
|
+
.option('-b, --base-ref <ref>', 'Branch/commit for compare scope (e.g. main)')
|
|
133
|
+
.option('-r, --repo <name>', 'Target repository')
|
|
134
|
+
.action(createLazyAction(() => import('./tool.js'), 'detectChangesCommand'));
|
|
110
135
|
// ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
|
|
111
136
|
program
|
|
112
137
|
.command('eval-server')
|
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
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
68
|
+
* OpenCode uses a different MCP format: { type: "local", command: [...] }
|
|
69
|
+
* where command is a flat array (command + args combined).
|
|
69
70
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
function getOpenCodeMcpEntry() {
|
|
72
|
+
const bin = resolveGitnexusBin();
|
|
73
|
+
if (bin) {
|
|
74
|
+
return { type: 'local', command: [bin, 'mcp'] };
|
|
73
75
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
+
if (process.platform === 'win32') {
|
|
77
|
+
return { type: 'local', command: ['cmd', '/c', 'npx', '-y', 'gitnexus@latest', 'mcp'] };
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
return existing;
|
|
79
|
+
return { type: 'local', command: ['npx', '-y', 'gitnexus@latest', 'mcp'] };
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
|
-
*
|
|
82
|
+
* Detect indentation style from file content.
|
|
83
|
+
* Returns formatting options matching the file's existing style.
|
|
82
84
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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}`);
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -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>;
|