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.
- 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 +20 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +11 -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/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- 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
|
@@ -6,8 +6,49 @@
|
|
|
6
6
|
* so the MCP server can discover indexed repos from any cwd.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
|
+
import { realpathSync } from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import os from 'os';
|
|
12
|
+
import { getInferredRepoName } from './git.js';
|
|
13
|
+
/**
|
|
14
|
+
* Normalise a repo path for registry comparison across platforms
|
|
15
|
+
* (#664 review feedback from @evander-wang).
|
|
16
|
+
*
|
|
17
|
+
* Why this exists: `path.resolve` alone is NOT enough for
|
|
18
|
+
* cross-platform registry stability.
|
|
19
|
+
* - **macOS**: tmpdirs and `/var` are symlinks to `/private/var`.
|
|
20
|
+
* A child process that stored `/private/var/folders/.../repo` in
|
|
21
|
+
* the registry cannot later be matched by an outer caller that
|
|
22
|
+
* supplies the symlink form `/var/folders/.../repo`. `path.resolve`
|
|
23
|
+
* does not follow symlinks; `realpathSync.native` does.
|
|
24
|
+
* - **Windows**: GitHub runners surface tmpdirs in 8.3 short-name
|
|
25
|
+
* form (`RUNNERA~1\...`), but `process.cwd()` often returns the
|
|
26
|
+
* long form (`runneradmin\...`). `realpathSync.native` normalises
|
|
27
|
+
* both sides to the long-name canonical path.
|
|
28
|
+
*
|
|
29
|
+
* Fallback behaviour: if the path does not exist on disk (e.g. a user
|
|
30
|
+
* passed `gitnexus remove some-alias` and the alias misses every
|
|
31
|
+
* registry entry, or the caller is resolving a path that was deleted
|
|
32
|
+
* after registration), we return `path.resolve(p)` rather than
|
|
33
|
+
* throwing. This preserves the idempotent-on-missing semantics of
|
|
34
|
+
* `resolveRegistryEntry` / `remove`.
|
|
35
|
+
*
|
|
36
|
+
* Backwards compatibility: this function is applied to BOTH the
|
|
37
|
+
* caller-supplied input AND each stored `entry.path` at compare time
|
|
38
|
+
* inside `resolveRegistryEntry`, so registries written by older
|
|
39
|
+
* versions (where `registerRepo` only ran `path.resolve`) still match
|
|
40
|
+
* correctly. Newly-written entries are canonicalised at write time too
|
|
41
|
+
* so the registry stabilises over analyze/re-analyze cycles.
|
|
42
|
+
*/
|
|
43
|
+
export const canonicalizePath = (p) => {
|
|
44
|
+
const resolved = path.resolve(p);
|
|
45
|
+
try {
|
|
46
|
+
return realpathSync.native(resolved);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
11
52
|
const GITNEXUS_DIR = '.gitnexus';
|
|
12
53
|
// ─── Local Storage Helpers ─────────────────────────────────────────────
|
|
13
54
|
/**
|
|
@@ -196,46 +237,352 @@ const writeRegistry = async (entries) => {
|
|
|
196
237
|
await fs.mkdir(dir, { recursive: true });
|
|
197
238
|
await fs.writeFile(getGlobalRegistryPath(), JSON.stringify(entries, null, 2), 'utf-8');
|
|
198
239
|
};
|
|
240
|
+
/**
|
|
241
|
+
* Thrown by {@link registerRepo} when a requested name is already in
|
|
242
|
+
* use by a DIFFERENT path. The CLI layer surfaces this as an actionable
|
|
243
|
+
* error instead of relying on `.message` string-matching.
|
|
244
|
+
*
|
|
245
|
+
* The colliding alias is exposed as `err.registryName` (not `err.name`).
|
|
246
|
+
* `err.name` keeps its inherited `Error.prototype.name` semantics (the
|
|
247
|
+
* class name) so downstream code can do the usual `err.name ===
|
|
248
|
+
* 'RegistryNameCollisionError'` checks; use the `kind` discriminant or
|
|
249
|
+
* `instanceof RegistryNameCollisionError` for type-safe narrowing.
|
|
250
|
+
*/
|
|
251
|
+
export class RegistryNameCollisionError extends Error {
|
|
252
|
+
registryName;
|
|
253
|
+
existingPath;
|
|
254
|
+
requestedPath;
|
|
255
|
+
kind = 'RegistryNameCollisionError';
|
|
256
|
+
constructor(registryName, existingPath, requestedPath) {
|
|
257
|
+
super(`Registry name "${registryName}" is already used by "${existingPath}".\n` +
|
|
258
|
+
`Pass --name <alias> to register "${requestedPath}" under a different name, ` +
|
|
259
|
+
`or --allow-duplicate-name to allow both paths under the same name (leaves -r <name> ambiguous for these two).`);
|
|
260
|
+
this.registryName = registryName;
|
|
261
|
+
this.existingPath = existingPath;
|
|
262
|
+
this.requestedPath = requestedPath;
|
|
263
|
+
this.name = 'RegistryNameCollisionError';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Returns true when a previously-registered entry's `name` differs from
|
|
267
|
+
* both `path.basename(entry.path)` and the git-remote-derived name —
|
|
268
|
+
* i.e. a user explicitly aliased it via `analyze --name <alias>` on a
|
|
269
|
+
* prior run. Used to preserve the alias across re-analyses that omit
|
|
270
|
+
* `--name`. The remote-derived name is treated as an inference, not a
|
|
271
|
+
* custom alias, so re-analyses keep tracking remote renames.
|
|
272
|
+
*
|
|
273
|
+
* `inferredName` is passed in (rather than re-derived) so callers can
|
|
274
|
+
* avoid a second `git config` subprocess invocation. */
|
|
275
|
+
const hasCustomAlias = (entry, inferredName) => {
|
|
276
|
+
const resolved = path.resolve(entry.path);
|
|
277
|
+
if (entry.name === path.basename(resolved))
|
|
278
|
+
return false;
|
|
279
|
+
if (inferredName && entry.name === inferredName)
|
|
280
|
+
return false;
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
199
283
|
/**
|
|
200
284
|
* Register (add or update) a repo in the global registry.
|
|
201
285
|
* Called after `gitnexus analyze` completes.
|
|
286
|
+
*
|
|
287
|
+
* Name resolution precedence (#829, #979):
|
|
288
|
+
* 1. explicit `opts.name` (from `analyze --name <alias>`)
|
|
289
|
+
* 2. preserved alias on an existing entry for this path
|
|
290
|
+
* 3. `git config --get remote.origin.url` repo name (#979 — recovers
|
|
291
|
+
* a meaningful name for monorepo subprojects, git worktrees, and
|
|
292
|
+
* Gas-Town-style `<rig>/refinery/rig/` layouts where the basename
|
|
293
|
+
* is generic)
|
|
294
|
+
* 4. `path.basename(repoPath)` (the original default)
|
|
295
|
+
*
|
|
296
|
+
* Duplicate-name guard: if another path already uses the resolved
|
|
297
|
+
* `name`, throw {@link RegistryNameCollisionError} unless
|
|
298
|
+
* `opts.allowDuplicateName` is set. The guard ONLY fires when the user explicitly passed a
|
|
299
|
+
* `name`; un-aliased basename collisions continue to register silently
|
|
300
|
+
* so existing users who don't know about `--name` see no behaviour
|
|
301
|
+
* change.
|
|
302
|
+
*
|
|
303
|
+
* Returns the `name` that was actually written to the registry — the
|
|
304
|
+
* caller can re-use it to keep AGENTS.md / skill files aligned with the
|
|
305
|
+
* MCP-visible repo name (#979).
|
|
202
306
|
*/
|
|
203
|
-
export const registerRepo = async (repoPath, meta) => {
|
|
307
|
+
export const registerRepo = async (repoPath, meta, opts) => {
|
|
308
|
+
// Preserve the caller's chosen path form in the registry — don't
|
|
309
|
+
// canonicalise at write time. This matters for two reasons:
|
|
310
|
+
// 1. `list` and error messages show the path the user actually
|
|
311
|
+
// knows (e.g. the 8.3 short form they typed), not a runtime-
|
|
312
|
+
// resolved long form they've never seen.
|
|
313
|
+
// 2. Keeps pre-existing #829 test assertions that compare
|
|
314
|
+
// `err.existingPath` against `path.resolve(tmpPath)` stable.
|
|
315
|
+
// Canonicalisation is applied at COMPARE points only (see below),
|
|
316
|
+
// which is where the cross-platform divergence actually matters.
|
|
204
317
|
const resolved = path.resolve(repoPath);
|
|
205
|
-
const name = path.basename(resolved);
|
|
206
318
|
const { storagePath } = getStoragePaths(resolved);
|
|
319
|
+
// Canonical form used strictly for comparison — `realpathSync.native`
|
|
320
|
+
// expands macOS /var → /private/var and Windows 8.3 → long-name,
|
|
321
|
+
// falling back to `path.resolve` when the path doesn't exist.
|
|
322
|
+
const canonicalInput = canonicalizePath(repoPath);
|
|
207
323
|
const entries = await readRegistry();
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
324
|
+
const existingIdx = entries.findIndex((e) => {
|
|
325
|
+
// Canonicalise the STORED entry too so pre-canonicalisation
|
|
326
|
+
// registries (written by older versions, or paths passed in a
|
|
327
|
+
// different form) still match correctly. `canonicalizePath` falls
|
|
328
|
+
// back to `path.resolve` when the path no longer exists on disk,
|
|
329
|
+
// so stale entries that have been rm'd externally still resolve
|
|
330
|
+
// to a stable key instead of throwing.
|
|
331
|
+
const a = canonicalizePath(e.path);
|
|
332
|
+
const b = canonicalInput;
|
|
211
333
|
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
212
334
|
});
|
|
335
|
+
const existing = existingIdx >= 0 ? entries[existingIdx] : null;
|
|
336
|
+
// Precedence: explicit --name > preserved alias > remote-inferred > basename.
|
|
337
|
+
// Skip the `git config` subprocess entirely when --name was passed —
|
|
338
|
+
// the remote isn't consulted in that case.
|
|
339
|
+
let name;
|
|
340
|
+
let isPreservedAlias = false;
|
|
341
|
+
if (opts?.name !== undefined) {
|
|
342
|
+
name = opts.name;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
// Compute the remote-derived name at most once. It feeds both the
|
|
346
|
+
// alias-preservation check (`hasCustomAlias` needs it to distinguish
|
|
347
|
+
// a sticky user alias from a previously-stored remote inference) and
|
|
348
|
+
// the fallback name when neither --name nor a preserved alias apply.
|
|
349
|
+
const inferred = getInferredRepoName(resolved);
|
|
350
|
+
if (existing && hasCustomAlias(existing, inferred)) {
|
|
351
|
+
name = existing.name;
|
|
352
|
+
isPreservedAlias = true;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
name = inferred ?? path.basename(resolved);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Duplicate-name guard: only fire when the user EXPLICITLY asked for
|
|
359
|
+
// this name (via opts.name or a preserved alias). Unqualified basename
|
|
360
|
+
// and remote-inferred collisions are preserved for backward-compat —
|
|
361
|
+
// they still register, and the user sees the ambiguity at `-r` / `list`
|
|
362
|
+
// resolution time (which is already improved by the disambiguated error
|
|
363
|
+
// messages and list output #829 ships).
|
|
364
|
+
const explicitName = opts?.name !== undefined || isPreservedAlias;
|
|
365
|
+
if (explicitName && !opts?.allowDuplicateName) {
|
|
366
|
+
// Compare canonical-vs-canonical here too so `/var/foo` and
|
|
367
|
+
// `/private/var/foo` (same repo, different form) aren't treated as
|
|
368
|
+
// two colliding paths.
|
|
369
|
+
const collidingEntry = entries.find((e, i) => i !== existingIdx &&
|
|
370
|
+
e.name.toLowerCase() === name.toLowerCase() &&
|
|
371
|
+
canonicalizePath(e.path) !== canonicalInput);
|
|
372
|
+
if (collidingEntry) {
|
|
373
|
+
throw new RegistryNameCollisionError(name, collidingEntry.path, resolved);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
213
376
|
const entry = {
|
|
214
377
|
name,
|
|
215
378
|
path: resolved,
|
|
216
379
|
storagePath,
|
|
217
380
|
indexedAt: meta.indexedAt,
|
|
218
381
|
lastCommit: meta.lastCommit,
|
|
382
|
+
remoteUrl: meta.remoteUrl,
|
|
219
383
|
stats: meta.stats,
|
|
220
384
|
};
|
|
221
|
-
if (
|
|
222
|
-
entries[
|
|
385
|
+
if (existingIdx >= 0) {
|
|
386
|
+
entries[existingIdx] = entry;
|
|
223
387
|
}
|
|
224
388
|
else {
|
|
225
389
|
entries.push(entry);
|
|
226
390
|
}
|
|
227
391
|
await writeRegistry(entries);
|
|
392
|
+
return name;
|
|
228
393
|
};
|
|
229
394
|
/**
|
|
230
395
|
* Remove a repo from the global registry.
|
|
231
396
|
* Called after `gitnexus clean`.
|
|
232
397
|
*/
|
|
233
398
|
export const unregisterRepo = async (repoPath) => {
|
|
234
|
-
|
|
399
|
+
// Canonicalise BOTH sides so an unregister call issued with the
|
|
400
|
+
// symlink form (`/var/folders/.../repo`) still matches an entry
|
|
401
|
+
// written with the realpath form (`/private/var/folders/.../repo`),
|
|
402
|
+
// and vice versa. Matches the semantics of `registerRepo` and
|
|
403
|
+
// `resolveRegistryEntry` post-#1003 review.
|
|
404
|
+
const resolved = canonicalizePath(repoPath);
|
|
235
405
|
const entries = await readRegistry();
|
|
236
|
-
const
|
|
406
|
+
const matches = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
407
|
+
const filtered = entries.filter((e) => !matches(canonicalizePath(e.path), resolved));
|
|
237
408
|
await writeRegistry(filtered);
|
|
238
409
|
};
|
|
410
|
+
/**
|
|
411
|
+
* Thrown by {@link resolveRegistryEntry} when no registered repo matches
|
|
412
|
+
* the caller's target string (by alias, basename, remote-inferred name,
|
|
413
|
+
* or resolved path). CLI callers that want idempotent "remove" semantics
|
|
414
|
+
* should catch this and exit 0 with a warning; non-idempotent callers
|
|
415
|
+
* (e.g. MCP tools) can surface the error directly.
|
|
416
|
+
*/
|
|
417
|
+
export class RegistryNotFoundError extends Error {
|
|
418
|
+
target;
|
|
419
|
+
availableNames;
|
|
420
|
+
kind = 'RegistryNotFoundError';
|
|
421
|
+
constructor(target, availableNames) {
|
|
422
|
+
const hint = availableNames.length > 0
|
|
423
|
+
? ` Available: ${availableNames.join(', ')}.`
|
|
424
|
+
: ' No repositories are currently registered.';
|
|
425
|
+
super(`No registered repo matches "${target}".${hint}`);
|
|
426
|
+
this.target = target;
|
|
427
|
+
this.availableNames = availableNames;
|
|
428
|
+
this.name = 'RegistryNotFoundError';
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Thrown by {@link resolveRegistryEntry} when the target string matches
|
|
433
|
+
* the `name` of two or more entries — only possible when the user
|
|
434
|
+
* previously registered duplicates via `analyze --name X
|
|
435
|
+
* --allow-duplicate-name` (#829). The error carries enough information
|
|
436
|
+
* for the caller to render an actionable disambiguation hint without
|
|
437
|
+
* string-matching on `.message`.
|
|
438
|
+
*
|
|
439
|
+
* `kind` is a string literal discriminant (same pattern as
|
|
440
|
+
* {@link RegistryNameCollisionError}) so callers can narrow via
|
|
441
|
+
* `err.kind === 'RegistryAmbiguousTargetError'` without importing the
|
|
442
|
+
* class.
|
|
443
|
+
*/
|
|
444
|
+
export class RegistryAmbiguousTargetError extends Error {
|
|
445
|
+
target;
|
|
446
|
+
matches;
|
|
447
|
+
kind = 'RegistryAmbiguousTargetError';
|
|
448
|
+
constructor(target, matches) {
|
|
449
|
+
const listing = matches.map((m) => ` - ${m.name} (${m.path})`).join('\n');
|
|
450
|
+
super(`Multiple registered repos match "${target}":\n${listing}\n` +
|
|
451
|
+
`Pass the absolute path instead to disambiguate.`);
|
|
452
|
+
this.target = target;
|
|
453
|
+
this.matches = matches;
|
|
454
|
+
this.name = 'RegistryAmbiguousTargetError';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Thrown by {@link assertSafeStoragePath} when a registry entry's
|
|
459
|
+
* `storagePath` does NOT point at the expected `<entry.path>/.gitnexus`
|
|
460
|
+
* subfolder. CLI destructive commands (`remove`, `clean --all`) should
|
|
461
|
+
* catch this and exit non-zero without deleting anything — the usual
|
|
462
|
+
* cause is a corrupted or hand-edited `~/.gitnexus/registry.json`, and
|
|
463
|
+
* proceeding would mean `fs.rm(recursive: true)` on whatever odd path
|
|
464
|
+
* the entry is pointing at.
|
|
465
|
+
*/
|
|
466
|
+
export class UnsafeStoragePathError extends Error {
|
|
467
|
+
entry;
|
|
468
|
+
expectedStoragePath;
|
|
469
|
+
actualStoragePath;
|
|
470
|
+
kind = 'UnsafeStoragePathError';
|
|
471
|
+
constructor(entry, expectedStoragePath, actualStoragePath) {
|
|
472
|
+
super(`Refusing to remove storage path for safety: expected ` +
|
|
473
|
+
`"${expectedStoragePath}" under the repo's .gitnexus subfolder, ` +
|
|
474
|
+
`but the registry entry has "${actualStoragePath}". ` +
|
|
475
|
+
`This usually means the registry entry is corrupted or was ` +
|
|
476
|
+
`hand-edited. Delete the entry manually from ~/.gitnexus/registry.json ` +
|
|
477
|
+
`and re-run analyze.`);
|
|
478
|
+
this.entry = entry;
|
|
479
|
+
this.expectedStoragePath = expectedStoragePath;
|
|
480
|
+
this.actualStoragePath = actualStoragePath;
|
|
481
|
+
this.name = 'UnsafeStoragePathError';
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Guard rail for destructive CLI paths (`remove` #664,
|
|
486
|
+
* `clean --all` #258, future MCP `remove` tool): verify that a
|
|
487
|
+
* registry entry's `storagePath` is the canonical `<repo>/.gitnexus`
|
|
488
|
+
* subfolder of its `path`. If not, throw {@link UnsafeStoragePathError}
|
|
489
|
+
* so the caller exits without touching disk.
|
|
490
|
+
*
|
|
491
|
+
* Why this exists (#1003 review — @magyargergo):
|
|
492
|
+
* - `~/.gitnexus/registry.json` is a plain-text user-writable file.
|
|
493
|
+
* A corrupted, hand-edited, or downgrade/upgrade-racing entry
|
|
494
|
+
* could plausibly end up with `storagePath === ""` (resolves to
|
|
495
|
+
* cwd), `storagePath === path` (the repo root!), `storagePath`
|
|
496
|
+
* equal to a parent/sibling of the repo, or simply any arbitrary
|
|
497
|
+
* filesystem path.
|
|
498
|
+
* - `fs.rm(recursive: true, force: true)` on ANY of those would be
|
|
499
|
+
* a runtime disaster — at best delete the user's working tree, at
|
|
500
|
+
* worst nuke an unrelated directory tree they happen to own.
|
|
501
|
+
* - `clean` (default, cwd-scoped) is safe by construction — it
|
|
502
|
+
* re-derives storagePath from `findRepo(cwd)` and never trusts
|
|
503
|
+
* the registry field. But `clean --all` DOES iterate the registry
|
|
504
|
+
* and trust each entry's stored storagePath (same shape as
|
|
505
|
+
* `remove`), so this helper must be wired into that loop too.
|
|
506
|
+
* - `server/api.ts` recomputes storagePath from `getStoragePath(entry.path)`
|
|
507
|
+
* and so is likewise safe-by-construction.
|
|
508
|
+
*
|
|
509
|
+
* Pure string check — does NOT require the paths to exist on disk.
|
|
510
|
+
* Windows: case-insensitive; POSIX: case-sensitive. Matches the
|
|
511
|
+
* comparison shape used elsewhere in this module.
|
|
512
|
+
*/
|
|
513
|
+
export const assertSafeStoragePath = (entry) => {
|
|
514
|
+
const expected = path.join(path.resolve(entry.path), '.gitnexus');
|
|
515
|
+
const actual = path.resolve(entry.storagePath);
|
|
516
|
+
const matches = process.platform === 'win32'
|
|
517
|
+
? expected.toLowerCase() === actual.toLowerCase()
|
|
518
|
+
: expected === actual;
|
|
519
|
+
if (!matches) {
|
|
520
|
+
throw new UnsafeStoragePathError(entry, expected, actual);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* Resolve a user-supplied target string (from `gitnexus remove <target>`
|
|
525
|
+
* or equivalent MCP tool argument) to a single registry entry.
|
|
526
|
+
*
|
|
527
|
+
* Match precedence (first hit wins, subsequent tiers are only tried if
|
|
528
|
+
* the prior tier produces zero matches):
|
|
529
|
+
* 1. Exact resolved-path match (Windows: case-insensitive).
|
|
530
|
+
* Paths are unique by registry construction, so a path match can
|
|
531
|
+
* never be ambiguous.
|
|
532
|
+
* 2. Exact `name` match (case-insensitive). If ≥ 2 entries share the
|
|
533
|
+
* name — only possible via `--allow-duplicate-name` (#829) —
|
|
534
|
+
* throws {@link RegistryAmbiguousTargetError}.
|
|
535
|
+
*
|
|
536
|
+
* No fuzzy / partial matching — unambiguous, scriptable behaviour is
|
|
537
|
+
* more important than convenience for destructive commands.
|
|
538
|
+
*
|
|
539
|
+
* Throws {@link RegistryNotFoundError} if no entry matches.
|
|
540
|
+
*
|
|
541
|
+
* `entries` is passed in (rather than re-read) so callers that already
|
|
542
|
+
* hold the registry snapshot (e.g. to print a "before" state) can avoid
|
|
543
|
+
* a second disk read, and so tests can inject fixtures without touching
|
|
544
|
+
* `GITNEXUS_HOME`.
|
|
545
|
+
*/
|
|
546
|
+
export const resolveRegistryEntry = (entries, target) => {
|
|
547
|
+
// Tier 1: path match. Canonicalise BOTH sides so symlink and
|
|
548
|
+
// Windows-8.3 quirks don't cause a false miss — e.g. the caller
|
|
549
|
+
// passes `/var/folders/.../repo` while the registry has
|
|
550
|
+
// `/private/var/folders/.../repo` (both resolve to the same
|
|
551
|
+
// `realpath.native`). See `canonicalizePath` for the rationale.
|
|
552
|
+
//
|
|
553
|
+
// Canonicalising the STORED entry (not just the input) is what gives
|
|
554
|
+
// us backward-compat for registries written by versions that only
|
|
555
|
+
// ran `path.resolve` — both get canonicalised here at compare time.
|
|
556
|
+
const canonicalTarget = canonicalizePath(target);
|
|
557
|
+
const pathMatch = entries.find((e) => {
|
|
558
|
+
const a = canonicalizePath(e.path);
|
|
559
|
+
const b = canonicalTarget;
|
|
560
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
561
|
+
});
|
|
562
|
+
if (pathMatch)
|
|
563
|
+
return pathMatch;
|
|
564
|
+
// Tier 2: name match. Case-insensitive on all platforms — registry
|
|
565
|
+
// name collisions are already filtered case-insensitively in
|
|
566
|
+
// `registerRepo`, so "APP" vs "app" are considered the same key.
|
|
567
|
+
const targetLower = target.toLowerCase();
|
|
568
|
+
const nameMatches = entries.filter((e) => e.name.toLowerCase() === targetLower);
|
|
569
|
+
if (nameMatches.length === 1)
|
|
570
|
+
return nameMatches[0];
|
|
571
|
+
if (nameMatches.length > 1) {
|
|
572
|
+
throw new RegistryAmbiguousTargetError(target, nameMatches);
|
|
573
|
+
}
|
|
574
|
+
// Tier 3: miss. Build the available-names hint ONCE; resolveRepo-style
|
|
575
|
+
// disambiguated labels (`app (/path)`) are applied when the same name
|
|
576
|
+
// appears in multiple entries so the user sees the same hint shape as
|
|
577
|
+
// `-r <name>` errors.
|
|
578
|
+
const nameCounts = new Map();
|
|
579
|
+
for (const e of entries) {
|
|
580
|
+
const key = e.name.toLowerCase();
|
|
581
|
+
nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
|
|
582
|
+
}
|
|
583
|
+
const availableNames = entries.map((e) => (nameCounts.get(e.name.toLowerCase()) ?? 0) > 1 ? `${e.name} (${e.path})` : e.name);
|
|
584
|
+
throw new RegistryNotFoundError(target, availableNames);
|
|
585
|
+
};
|
|
239
586
|
/**
|
|
240
587
|
* List all registered repos from the global registry.
|
|
241
588
|
* Optionally validates that each entry's .gitnexus/ still exists.
|
|
@@ -297,3 +644,38 @@ export const saveCLIConfig = async (config) => {
|
|
|
297
644
|
}
|
|
298
645
|
}
|
|
299
646
|
};
|
|
647
|
+
// ─── Sibling-clone detection ─────────────────────────────────────────────
|
|
648
|
+
//
|
|
649
|
+
// A "sibling clone" is a different on-disk path that points at the same
|
|
650
|
+
// logical repository (same `origin` remote URL) as a registered index.
|
|
651
|
+
// This shows up in three operationally important shapes (see issue):
|
|
652
|
+
//
|
|
653
|
+
// 1. The same repo is checked out under multiple paths (worktrees,
|
|
654
|
+
// multi-agent workspaces). Only one is indexed; the others silently
|
|
655
|
+
// diverge from the graph.
|
|
656
|
+
// 2. The indexed clone is itself behind its own HEAD (the existing
|
|
657
|
+
// `checkStaleness` already handles this case).
|
|
658
|
+
// 3. A query is issued from a `cwd` that lives inside a sibling clone
|
|
659
|
+
// whose HEAD has drifted from the indexed `lastCommit`.
|
|
660
|
+
//
|
|
661
|
+
// Detection is intentionally remote-URL-based and does NOT walk the
|
|
662
|
+
// filesystem hunting for unregistered clones — only registered entries
|
|
663
|
+
// are considered. The `cwd`-driven branch ({@link checkSiblingDrift})
|
|
664
|
+
// also accepts an unregistered cwd, because the live caller's working
|
|
665
|
+
// directory is the one place we can cheaply learn about an
|
|
666
|
+
// unregistered clone.
|
|
667
|
+
/**
|
|
668
|
+
* Find other registered entries whose `remoteUrl` matches the given
|
|
669
|
+
* one, excluding `selfPath` (case-insensitive on Windows). Entries
|
|
670
|
+
* without a `remoteUrl` are ignored — we cannot prove sibling-ness
|
|
671
|
+
* without a fingerprint.
|
|
672
|
+
*/
|
|
673
|
+
export const findSiblingClones = async (remoteUrl, selfPath) => {
|
|
674
|
+
if (!remoteUrl)
|
|
675
|
+
return [];
|
|
676
|
+
const entries = await readRegistry();
|
|
677
|
+
const isWin = process.platform === 'win32';
|
|
678
|
+
const norm = (p) => (isWin ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
679
|
+
const self = norm(selfPath);
|
|
680
|
+
return entries.filter((e) => e.remoteUrl === remoteUrl && norm(e.path) !== self);
|
|
681
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.3
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -60,11 +60,12 @@
|
|
|
60
60
|
"cors": "^2.8.5",
|
|
61
61
|
"express": "^4.19.2",
|
|
62
62
|
"glob": "^13.0.6",
|
|
63
|
-
"graphology": "^0.
|
|
63
|
+
"graphology": "^0.26.0",
|
|
64
64
|
"graphology-indices": "^0.17.0",
|
|
65
65
|
"graphology-utils": "^2.3.0",
|
|
66
66
|
"ignore": "^7.0.5",
|
|
67
67
|
"js-yaml": "^4.1.1",
|
|
68
|
+
"jsonc-parser": "^3.3.1",
|
|
68
69
|
"lru-cache": "^11.0.0",
|
|
69
70
|
"mnemonist": "^0.40.3",
|
|
70
71
|
"onnxruntime-node": "^1.24.0",
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"tree-sitter-ruby": "^0.23.1",
|
|
82
83
|
"tree-sitter-rust": "0.23.1",
|
|
83
84
|
"tree-sitter-typescript": "^0.23.2",
|
|
84
|
-
"uuid": "^
|
|
85
|
+
"uuid": "^14.0.0"
|
|
85
86
|
},
|
|
86
87
|
"optionalDependencies": {
|
|
87
88
|
"node-addon-api": "^8.0.0",
|
|
@@ -92,14 +93,14 @@
|
|
|
92
93
|
"tree-sitter-swift": "^0.6.0"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"gitnexus-shared": "file:../gitnexus-shared",
|
|
96
96
|
"@types/cli-progress": "^3.11.6",
|
|
97
97
|
"@types/cors": "^2.8.17",
|
|
98
98
|
"@types/express": "^4.17.21",
|
|
99
99
|
"@types/js-yaml": "^4.0.9",
|
|
100
|
-
"@types/node": "^
|
|
101
|
-
"@types/uuid": "^
|
|
100
|
+
"@types/node": "^25.6.0",
|
|
101
|
+
"@types/uuid": "^11.0.0",
|
|
102
102
|
"@vitest/coverage-v8": "^4.0.18",
|
|
103
|
+
"gitnexus-shared": "file:../gitnexus-shared",
|
|
103
104
|
"tsx": "^4.0.0",
|
|
104
105
|
"typescript": "^5.4.5",
|
|
105
106
|
"vitest": "^4.0.18"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic benchmark for scope-resolution. Builds a large in-memory
|
|
3
|
+
* Python workspace and times runScopeResolution against it directly,
|
|
4
|
+
* isolating the resolution cost from parse / heritage / pipeline
|
|
5
|
+
* overhead.
|
|
6
|
+
*
|
|
7
|
+
* Usage: REGISTRY_PRIMARY_PYTHON=1 npx tsx scripts/bench-scope-resolution.ts
|
|
8
|
+
*/
|
|
9
|
+
process.env.REGISTRY_PRIMARY_PYTHON = '1';
|
|
10
|
+
|
|
11
|
+
import { generateId } from '../src/lib/utils.js';
|
|
12
|
+
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
|
|
13
|
+
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
|
|
14
|
+
import { pythonScopeResolver } from '../src/core/ingestion/languages/python/scope-resolver.js';
|
|
15
|
+
|
|
16
|
+
const N_CLASSES = Number(process.env.BENCH_CLASSES ?? '60');
|
|
17
|
+
const N_USERS = Number(process.env.BENCH_USERS ?? '40');
|
|
18
|
+
const ITERS = Number(process.env.BENCH_ITERS ?? '5');
|
|
19
|
+
|
|
20
|
+
function buildWorkspace(): { path: string; content: string }[] {
|
|
21
|
+
const files: { path: string; content: string }[] = [];
|
|
22
|
+
|
|
23
|
+
// Build N_CLASSES "model" files, each defining a class with a few methods.
|
|
24
|
+
for (let i = 0; i < N_CLASSES; i++) {
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
for (let j = 0; j < 5; j++) {
|
|
27
|
+
lines.push(`class Model${i}_${j}:`);
|
|
28
|
+
lines.push(` name: str`);
|
|
29
|
+
lines.push(` def save(self) -> bool:`);
|
|
30
|
+
lines.push(` return True`);
|
|
31
|
+
lines.push(` def update(self, name: str) -> "Model${i}_${j}":`);
|
|
32
|
+
lines.push(` self.name = name`);
|
|
33
|
+
lines.push(` return self`);
|
|
34
|
+
lines.push(` def get_other(self) -> "Model${i}_${(j + 1) % 5}":`);
|
|
35
|
+
lines.push(` return Model${i}_${(j + 1) % 5}()`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
}
|
|
38
|
+
files.push({ path: `models/m${i}.py`, content: lines.join('\n') });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build N_USERS "user" files that import from a few model files
|
|
42
|
+
// and exercise the receiver-bound dispatcher heavily.
|
|
43
|
+
for (let u = 0; u < N_USERS; u++) {
|
|
44
|
+
const targets = [u % N_CLASSES, (u + 1) % N_CLASSES, (u + 2) % N_CLASSES];
|
|
45
|
+
const imports = targets
|
|
46
|
+
.map((t) => `from models.m${t} import Model${t}_0, Model${t}_1, Model${t}_2`)
|
|
47
|
+
.join('\n');
|
|
48
|
+
const calls: string[] = [];
|
|
49
|
+
for (let k = 0; k < 30; k++) {
|
|
50
|
+
const t = targets[k % 3]!;
|
|
51
|
+
const j = k % 3;
|
|
52
|
+
calls.push(` m${k} = Model${t}_${j}()`);
|
|
53
|
+
calls.push(` m${k}.save()`);
|
|
54
|
+
calls.push(` m${k}.update("x").save()`);
|
|
55
|
+
calls.push(` m${k}.get_other().save()`);
|
|
56
|
+
}
|
|
57
|
+
const content = `${imports}\n\ndef use_${u}() -> None:\n${calls.join('\n')}\n`;
|
|
58
|
+
files.push({ path: `app/u${u}.py`, content });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildGraph(files: { path: string; content: string }[]) {
|
|
65
|
+
const graph = createKnowledgeGraph();
|
|
66
|
+
// Pre-populate File / Class / Function nodes the resolver expects.
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
const fileId = generateId('File', f.path);
|
|
69
|
+
graph.addNode({
|
|
70
|
+
id: fileId,
|
|
71
|
+
label: 'File',
|
|
72
|
+
properties: { name: f.path, filePath: f.path },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Lightweight regex-extract class & def names so the lookup index
|
|
76
|
+
// has something to find. Real pipeline builds these via parse phase;
|
|
77
|
+
// for the bench this stand-in is enough to exercise the resolver.
|
|
78
|
+
const classRe = /^class (\w+)/gm;
|
|
79
|
+
const defRe = /^\s*def (\w+)/gm;
|
|
80
|
+
let m: RegExpExecArray | null;
|
|
81
|
+
while ((m = classRe.exec(f.content)) !== null) {
|
|
82
|
+
const name = m[1]!;
|
|
83
|
+
const id = generateId('Class', `${f.path}:${name}`);
|
|
84
|
+
graph.addNode({
|
|
85
|
+
id,
|
|
86
|
+
label: 'Class',
|
|
87
|
+
properties: { name, filePath: f.path, qualifiedName: name },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
while ((m = defRe.exec(f.content)) !== null) {
|
|
91
|
+
const name = m[1]!;
|
|
92
|
+
const id = generateId('Function', `${f.path}:${name}`);
|
|
93
|
+
graph.addNode({
|
|
94
|
+
id,
|
|
95
|
+
label: 'Function',
|
|
96
|
+
properties: { name, filePath: f.path, qualifiedName: name },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return graph;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const files = buildWorkspace();
|
|
105
|
+
console.log(`bench: ${files.length} files (${N_CLASSES} models × 5 classes + ${N_USERS} users)`);
|
|
106
|
+
console.log(` × ${ITERS} iterations\n`);
|
|
107
|
+
|
|
108
|
+
// Warmup
|
|
109
|
+
for (let i = 0; i < 2; i++) {
|
|
110
|
+
const graph = buildGraph(files);
|
|
111
|
+
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const samples: number[] = [];
|
|
115
|
+
for (let i = 0; i < ITERS; i++) {
|
|
116
|
+
const graph = buildGraph(files);
|
|
117
|
+
const start = process.hrtime.bigint();
|
|
118
|
+
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
|
|
119
|
+
const end = process.hrtime.bigint();
|
|
120
|
+
const ms = Number(end - start) / 1_000_000;
|
|
121
|
+
samples.push(ms);
|
|
122
|
+
console.log(` iter ${i + 1}: ${ms.toFixed(0)} ms`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
samples.sort((a, b) => a - b);
|
|
126
|
+
const median = samples[Math.floor(samples.length / 2)]!;
|
|
127
|
+
const min = samples[0]!;
|
|
128
|
+
console.log(`\nmin: ${min.toFixed(0)} ms · median: ${median.toFixed(0)} ms`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
console.error(err);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI helper — emits the `MIGRATED_LANGUAGES` set as a JSON matrix array for
|
|
3
|
+
* GitHub Actions (`.github/workflows/ci-scope-parity.yml`).
|
|
4
|
+
*
|
|
5
|
+
* Consumed by the `discover` job in that workflow. Each entry has:
|
|
6
|
+
* - `slug`: lowercase language id, matching `test/integration/resolvers/<slug>.test.ts`.
|
|
7
|
+
* - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_<envvar>` toggle.
|
|
8
|
+
*
|
|
9
|
+
* Run with `npx tsx scripts/ci-list-migrated-languages.ts`. The script
|
|
10
|
+
* writes a single JSON array to stdout (no wrapper object) so the
|
|
11
|
+
* workflow can pipe it straight into `$GITHUB_OUTPUT`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
|
15
|
+
|
|
16
|
+
const entries = [...MIGRATED_LANGUAGES].map((slug) => {
|
|
17
|
+
const s = String(slug);
|
|
18
|
+
return {
|
|
19
|
+
slug: s,
|
|
20
|
+
envvar: s.toUpperCase().replace(/-/g, '_'),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.stdout.write(JSON.stringify(entries));
|
package/skills/gitnexus-cli.md
CHANGED
|
@@ -21,6 +21,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
|
|
|
21
21
|
| -------------- | ---------------------------------------------------------------- |
|
|
22
22
|
| `--force` | Force full re-index even if up to date |
|
|
23
23
|
| `--embeddings` | Enable embedding generation for semantic search (off by default) |
|
|
24
|
+
| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
|
|
24
25
|
|
|
25
26
|
**When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook runs `analyze` automatically after `git commit` and `git merge`, preserving embeddings if previously generated.
|
|
26
27
|
|