gitnexus 1.5.3 → 1.6.1
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 +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +30 -4
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/fs-utils.d.ts +10 -0
- package/dist/core/group/extractors/fs-utils.js +24 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +25 -0
- package/dist/core/group/extractors/grpc-extractor.js +386 -0
- package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
- package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
- package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
- package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
- package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
- package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
- package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
- package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
- package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/go.js +215 -0
- package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
- package/dist/core/group/extractors/http-patterns/index.js +44 -0
- package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/java.js +253 -0
- package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/http-patterns/node.js +354 -0
- package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/php.js +70 -0
- package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +133 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
- package/dist/core/group/extractors/http-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +21 -0
- package/dist/core/group/extractors/http-route-extractor.js +391 -0
- package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
- package/dist/core/group/extractors/manifest-extractor.js +235 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +8 -0
- package/dist/core/group/extractors/topic-extractor.js +97 -0
- package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/go.js +120 -0
- package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
- package/dist/core/group/extractors/topic-patterns/index.js +38 -0
- package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/java.js +80 -0
- package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/topic-patterns/node.js +155 -0
- package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/python.js +116 -0
- package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
- package/dist/core/group/extractors/topic-patterns/types.js +10 -0
- package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
- package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +212 -0
- package/dist/core/ingestion/binding-accumulator.js +336 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/cobol-processor.d.ts +1 -1
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +1 -13
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-config.js +1 -1
- package/dist/core/ingestion/language-provider.d.ts +14 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +43 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +52 -3
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/markdown-processor.d.ts +1 -1
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +14 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +286 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
- package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
- package/dist/core/ingestion/method-extractors/generic.js +84 -17
- package/dist/core/ingestion/method-types.d.ts +29 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +297 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +5 -4
- package/dist/core/ingestion/mro-processor.js +311 -107
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +224 -87
- package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
- package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/index.js +22 -0
- package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
- package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
- package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
- package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
- package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
- package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
- package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
- package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
- package/dist/core/ingestion/pipeline-phases/types.js +37 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
- package/dist/core/ingestion/pipeline.d.ts +18 -10
- package/dist/core/ingestion/pipeline.js +66 -1410
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
- package/dist/core/ingestion/tree-sitter-queries.js +90 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +32 -44
- package/dist/core/ingestion/utils/ast-helpers.js +157 -573
- package/dist/core/ingestion/utils/env.d.ts +10 -0
- package/dist/core/ingestion/utils/env.js +10 -0
- package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
- package/dist/core/ingestion/utils/graph-sort.js +100 -0
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +469 -200
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +134 -27
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +9 -3
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/vendor/tree-sitter-proto/binding.gyp +30 -0
- package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-proto/package.json +18 -0
- package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
- package/vendor/tree-sitter-proto/src/parser.c +10149 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
|
@@ -55,6 +55,10 @@ export class JobManager {
|
|
|
55
55
|
getJob(id) {
|
|
56
56
|
return this.jobs.get(id);
|
|
57
57
|
}
|
|
58
|
+
/** Return a snapshot of all tracked jobs for inspection. */
|
|
59
|
+
listJobs() {
|
|
60
|
+
return Array.from(this.jobs.values());
|
|
61
|
+
}
|
|
58
62
|
updateJob(id, update) {
|
|
59
63
|
const job = this.jobs.get(id);
|
|
60
64
|
if (!job)
|
package/dist/server/api.d.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* REST API for browser-based clients to query the local .gitnexus/ index.
|
|
5
5
|
* Also hosts the MCP server over StreamableHTTP for remote AI tool access.
|
|
6
6
|
*
|
|
7
|
-
* Security: binds to
|
|
7
|
+
* Security: binds to localhost by default (use --host to override).
|
|
8
8
|
* CORS is restricted to localhost, private/LAN networks, and the deployed site.
|
|
9
9
|
*/
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
|
|
10
12
|
/**
|
|
11
13
|
* Determine whether an HTTP Origin header value is allowed by CORS policy.
|
|
12
14
|
*
|
|
@@ -25,4 +27,21 @@
|
|
|
25
27
|
* @returns `true` if the origin is allowed, `false` otherwise.
|
|
26
28
|
*/
|
|
27
29
|
export declare const isAllowedOrigin: (origin: string | undefined) => boolean;
|
|
30
|
+
type GraphStreamRecord = {
|
|
31
|
+
type: 'node';
|
|
32
|
+
data: GraphNode;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'relationship';
|
|
35
|
+
data: GraphRelationship;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'error';
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
export declare class ClientDisconnectedError extends Error {
|
|
41
|
+
constructor();
|
|
42
|
+
}
|
|
43
|
+
export declare const isIgnorableGraphQueryError: (err: unknown) => boolean;
|
|
44
|
+
export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
|
|
45
|
+
export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
|
|
28
46
|
export declare const createServer: (port: number, host?: string) => Promise<void>;
|
|
47
|
+
export {};
|
package/dist/server/api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* REST API for browser-based clients to query the local .gitnexus/ index.
|
|
5
5
|
* Also hosts the MCP server over StreamableHTTP for remote AI tool access.
|
|
6
6
|
*
|
|
7
|
-
* Security: binds to
|
|
7
|
+
* Security: binds to localhost by default (use --host to override).
|
|
8
8
|
* CORS is restricted to localhost, private/LAN networks, and the deployed site.
|
|
9
9
|
*/
|
|
10
10
|
import express from 'express';
|
|
@@ -13,8 +13,8 @@ import path from 'path';
|
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { loadMeta, listRegisteredRepos, getStoragePath } from '../storage/repo-manager.js';
|
|
16
|
-
import { executeQuery, executePrepared, executeWithReusedStatement, closeLbug, withLbugDb, } from '../core/lbug/lbug-adapter.js';
|
|
17
|
-
import { isWriteQuery } from '../
|
|
16
|
+
import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, closeLbug, withLbugDb, } from '../core/lbug/lbug-adapter.js';
|
|
17
|
+
import { isWriteQuery } from '../core/lbug/pool-adapter.js';
|
|
18
18
|
import { NODE_TABLES } from '../_shared/index.js';
|
|
19
19
|
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
20
20
|
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
@@ -91,72 +91,181 @@ export const isAllowedOrigin = (origin) => {
|
|
|
91
91
|
return true;
|
|
92
92
|
return false;
|
|
93
93
|
};
|
|
94
|
+
export class ClientDisconnectedError extends Error {
|
|
95
|
+
constructor() {
|
|
96
|
+
super('Client disconnected during graph stream');
|
|
97
|
+
this.name = 'ClientDisconnectedError';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export const isIgnorableGraphQueryError = (err) => {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
return (message.includes('does not exist') ||
|
|
103
|
+
message.includes('not found') ||
|
|
104
|
+
message.includes('No table named'));
|
|
105
|
+
};
|
|
106
|
+
const ensureStreamIsWritable = (res, signal) => {
|
|
107
|
+
if (signal?.aborted || res.destroyed || res.writableEnded) {
|
|
108
|
+
throw new ClientDisconnectedError();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const waitForDrain = async (res, signal) => {
|
|
112
|
+
ensureStreamIsWritable(res, signal);
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
res.off('drain', onDrain);
|
|
116
|
+
res.off('close', onClose);
|
|
117
|
+
signal?.removeEventListener('abort', onAbort);
|
|
118
|
+
};
|
|
119
|
+
const onDrain = () => {
|
|
120
|
+
cleanup();
|
|
121
|
+
resolve();
|
|
122
|
+
};
|
|
123
|
+
const onClose = () => {
|
|
124
|
+
cleanup();
|
|
125
|
+
reject(new ClientDisconnectedError());
|
|
126
|
+
};
|
|
127
|
+
const onAbort = () => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(new ClientDisconnectedError());
|
|
130
|
+
};
|
|
131
|
+
res.once('drain', onDrain);
|
|
132
|
+
res.once('close', onClose);
|
|
133
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
134
|
+
if (signal?.aborted || res.destroyed || res.writableEnded) {
|
|
135
|
+
onAbort();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
ensureStreamIsWritable(res, signal);
|
|
139
|
+
};
|
|
140
|
+
const isClientDisconnectWriteError = (err) => {
|
|
141
|
+
if (!(err instanceof Error))
|
|
142
|
+
return false;
|
|
143
|
+
return (err.code === 'ERR_STREAM_DESTROYED' ||
|
|
144
|
+
err.code === 'EPIPE' ||
|
|
145
|
+
err.code === 'ECONNRESET' ||
|
|
146
|
+
err.message.includes('write after end'));
|
|
147
|
+
};
|
|
148
|
+
export const writeNdjsonRecord = async (res, record, signal) => {
|
|
149
|
+
ensureStreamIsWritable(res, signal);
|
|
150
|
+
try {
|
|
151
|
+
const canContinue = res.write(JSON.stringify(record) + '\n');
|
|
152
|
+
if (!canContinue) {
|
|
153
|
+
await waitForDrain(res, signal);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (isClientDisconnectWriteError(err)) {
|
|
158
|
+
throw new ClientDisconnectedError();
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
94
163
|
const buildGraph = async (includeContent = false) => {
|
|
95
164
|
const nodes = [];
|
|
96
165
|
for (const table of NODE_TABLES) {
|
|
97
166
|
try {
|
|
98
|
-
|
|
99
|
-
if (table === 'File') {
|
|
100
|
-
query = includeContent
|
|
101
|
-
? `MATCH (n:File) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content`
|
|
102
|
-
: `MATCH (n:File) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
103
|
-
}
|
|
104
|
-
else if (table === 'Folder') {
|
|
105
|
-
query = `MATCH (n:Folder) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
106
|
-
}
|
|
107
|
-
else if (table === 'Community') {
|
|
108
|
-
query = `MATCH (n:Community) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.cohesion AS cohesion, n.symbolCount AS symbolCount`;
|
|
109
|
-
}
|
|
110
|
-
else if (table === 'Process') {
|
|
111
|
-
query = `MATCH (n:Process) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
query = includeContent
|
|
115
|
-
? `MATCH (n:${table}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.content AS content`
|
|
116
|
-
: `MATCH (n:${table}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
|
|
117
|
-
}
|
|
118
|
-
const rows = await executeQuery(query);
|
|
167
|
+
const rows = await executeQuery(getNodeQuery(table, includeContent));
|
|
119
168
|
for (const row of rows) {
|
|
120
|
-
nodes.push(
|
|
121
|
-
id: row.id ?? row[0],
|
|
122
|
-
label: table,
|
|
123
|
-
properties: {
|
|
124
|
-
name: row.name ?? row.label ?? row[1],
|
|
125
|
-
filePath: row.filePath ?? row[2],
|
|
126
|
-
startLine: row.startLine,
|
|
127
|
-
endLine: row.endLine,
|
|
128
|
-
content: includeContent ? row.content : undefined,
|
|
129
|
-
heuristicLabel: row.heuristicLabel,
|
|
130
|
-
cohesion: row.cohesion,
|
|
131
|
-
symbolCount: row.symbolCount,
|
|
132
|
-
processType: row.processType,
|
|
133
|
-
stepCount: row.stepCount,
|
|
134
|
-
communities: row.communities,
|
|
135
|
-
entryPointId: row.entryPointId,
|
|
136
|
-
terminalId: row.terminalId,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
169
|
+
nodes.push(mapGraphNodeRow(table, row, includeContent));
|
|
139
170
|
}
|
|
140
171
|
}
|
|
141
|
-
catch {
|
|
142
|
-
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (!isIgnorableGraphQueryError(err)) {
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
143
176
|
}
|
|
144
177
|
}
|
|
145
178
|
const relationships = [];
|
|
146
|
-
const relRows = await executeQuery(
|
|
179
|
+
const relRows = await executeQuery(GRAPH_RELATIONSHIP_QUERY);
|
|
147
180
|
for (const row of relRows) {
|
|
148
|
-
relationships.push(
|
|
149
|
-
id: `${row.sourceId}_${row.type}_${row.targetId}`,
|
|
150
|
-
type: row.type,
|
|
151
|
-
sourceId: row.sourceId,
|
|
152
|
-
targetId: row.targetId,
|
|
153
|
-
confidence: row.confidence,
|
|
154
|
-
reason: row.reason,
|
|
155
|
-
step: row.step,
|
|
156
|
-
});
|
|
181
|
+
relationships.push(mapGraphRelationshipRow(row));
|
|
157
182
|
}
|
|
158
183
|
return { nodes, relationships };
|
|
159
184
|
};
|
|
185
|
+
const GRAPH_RELATIONSHIP_QUERY = `MATCH (a)-[r:CodeRelation]->(b) RETURN a.id AS sourceId, b.id AS targetId, ` +
|
|
186
|
+
`r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step`;
|
|
187
|
+
const quoteNodeTable = (table) => `\`${table.replace(/`/g, '``')}\``;
|
|
188
|
+
const getNodeQuery = (table, includeContent) => {
|
|
189
|
+
const tableLabel = quoteNodeTable(table);
|
|
190
|
+
if (table === 'File') {
|
|
191
|
+
return includeContent
|
|
192
|
+
? `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content`
|
|
193
|
+
: `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
194
|
+
}
|
|
195
|
+
if (table === 'Folder') {
|
|
196
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
197
|
+
}
|
|
198
|
+
if (table === 'Community') {
|
|
199
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.cohesion AS cohesion, n.symbolCount AS symbolCount`;
|
|
200
|
+
}
|
|
201
|
+
if (table === 'Process') {
|
|
202
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
|
|
203
|
+
}
|
|
204
|
+
if (table === 'Route') {
|
|
205
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware`;
|
|
206
|
+
}
|
|
207
|
+
if (table === 'Tool') {
|
|
208
|
+
return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.description AS description`;
|
|
209
|
+
}
|
|
210
|
+
return includeContent
|
|
211
|
+
? `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.content AS content`
|
|
212
|
+
: `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine`;
|
|
213
|
+
};
|
|
214
|
+
const mapGraphNodeRow = (table, row, includeContent) => ({
|
|
215
|
+
id: row.id ?? row[0],
|
|
216
|
+
label: table,
|
|
217
|
+
properties: {
|
|
218
|
+
name: row.name ?? row.label ?? row[1],
|
|
219
|
+
filePath: row.filePath ?? row[2],
|
|
220
|
+
startLine: row.startLine,
|
|
221
|
+
endLine: row.endLine,
|
|
222
|
+
content: includeContent ? row.content : undefined,
|
|
223
|
+
responseKeys: row.responseKeys,
|
|
224
|
+
errorKeys: row.errorKeys,
|
|
225
|
+
middleware: row.middleware,
|
|
226
|
+
heuristicLabel: row.heuristicLabel,
|
|
227
|
+
cohesion: row.cohesion,
|
|
228
|
+
symbolCount: row.symbolCount,
|
|
229
|
+
description: row.description,
|
|
230
|
+
processType: row.processType,
|
|
231
|
+
stepCount: row.stepCount,
|
|
232
|
+
communities: row.communities,
|
|
233
|
+
entryPointId: row.entryPointId,
|
|
234
|
+
terminalId: row.terminalId,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const mapGraphRelationshipRow = (row) => ({
|
|
238
|
+
id: `${row.sourceId}_${row.type}_${row.targetId}`,
|
|
239
|
+
type: row.type,
|
|
240
|
+
sourceId: row.sourceId,
|
|
241
|
+
targetId: row.targetId,
|
|
242
|
+
confidence: row.confidence,
|
|
243
|
+
reason: row.reason,
|
|
244
|
+
step: row.step,
|
|
245
|
+
});
|
|
246
|
+
export const streamGraphNdjson = async (res, includeContent = false, signal) => {
|
|
247
|
+
for (const table of NODE_TABLES) {
|
|
248
|
+
try {
|
|
249
|
+
await streamQuery(getNodeQuery(table, includeContent), async (row) => {
|
|
250
|
+
await writeNdjsonRecord(res, {
|
|
251
|
+
type: 'node',
|
|
252
|
+
data: mapGraphNodeRow(table, row, includeContent),
|
|
253
|
+
}, signal);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
if (!isIgnorableGraphQueryError(err)) {
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
await streamQuery(GRAPH_RELATIONSHIP_QUERY, async (row) => {
|
|
263
|
+
await writeNdjsonRecord(res, {
|
|
264
|
+
type: 'relationship',
|
|
265
|
+
data: mapGraphRelationshipRow(row),
|
|
266
|
+
}, signal);
|
|
267
|
+
});
|
|
268
|
+
};
|
|
160
269
|
/**
|
|
161
270
|
* Mount an SSE progress endpoint for a JobManager.
|
|
162
271
|
* Handles: initial state, terminal events, heartbeat, event IDs, client disconnect.
|
|
@@ -249,17 +358,29 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
249
358
|
app.disable('x-powered-by');
|
|
250
359
|
// CORS: allow localhost, private/LAN networks, and the deployed site.
|
|
251
360
|
// Non-browser requests (curl, server-to-server) have no origin and are allowed.
|
|
361
|
+
// Disallowed origins get the response without Access-Control-Allow-Origin,
|
|
362
|
+
// so the browser blocks it. We pass `false` instead of throwing an Error to
|
|
363
|
+
// avoid crashing into Express's default error handler (which returned 500).
|
|
252
364
|
app.use(cors({
|
|
253
365
|
origin: (origin, callback) => {
|
|
254
|
-
|
|
255
|
-
callback(null, true);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
callback(new Error('Not allowed by CORS'));
|
|
259
|
-
}
|
|
366
|
+
callback(null, isAllowedOrigin(origin));
|
|
260
367
|
},
|
|
261
368
|
}));
|
|
262
369
|
app.use(express.json({ limit: '10mb' }));
|
|
370
|
+
// Support Chromium Private Network Access (required since Chrome 130+).
|
|
371
|
+
// Without this header, Chrome/Edge/Brave/Arc block public->loopback requests
|
|
372
|
+
// which breaks bridge mode entirely.
|
|
373
|
+
app.use((_req, res, next) => {
|
|
374
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
375
|
+
next();
|
|
376
|
+
});
|
|
377
|
+
// Handle PNA preflight: Chromium sends Access-Control-Request-Private-Network
|
|
378
|
+
// on OPTIONS requests and expects the allow header in the response.
|
|
379
|
+
// Note: the actual Allow-Private-Network header is already set by the global
|
|
380
|
+
// middleware above, so we just need to call next() here.
|
|
381
|
+
app.options('*', (_req, res, next) => {
|
|
382
|
+
next();
|
|
383
|
+
});
|
|
263
384
|
// Initialize MCP backend (multi-repo, shared across all MCP sessions)
|
|
264
385
|
const backend = new LocalBackend();
|
|
265
386
|
await backend.init();
|
|
@@ -278,14 +399,75 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
278
399
|
const releaseRepoLock = (repoPath) => {
|
|
279
400
|
activeRepoPaths.delete(repoPath);
|
|
280
401
|
};
|
|
281
|
-
|
|
282
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Maximum time the hold-queue will wait for an active analysis job to complete.
|
|
404
|
+
* Must stay in sync with the frontend's `fetchRepoInfo({ awaitAnalysis: true })` timeout.
|
|
405
|
+
*/
|
|
406
|
+
const HOLD_QUEUE_TIMEOUT_SECS = 300; // 5 minutes
|
|
407
|
+
// Helper: resolve a repo by name from the global registry, or default to first.
|
|
408
|
+
// Pass `req` to enable early exit if the client disconnects during the hold-queue wait.
|
|
409
|
+
const resolveRepo = async (repoName, isRetry = false, req) => {
|
|
283
410
|
const repos = await listRegisteredRepos();
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
411
|
+
let found = null;
|
|
412
|
+
// Normalize: if a full path is passed, extract just the basename.
|
|
413
|
+
// e.g. "C:\Users\LENOVO\.gitnexus\repos\todo.txt-cli" -> "todo.txt-cli"
|
|
414
|
+
const normalizedName = repoName ? path.basename(repoName) : undefined;
|
|
415
|
+
if (normalizedName) {
|
|
416
|
+
found =
|
|
417
|
+
repos.find((r) => r.name === normalizedName) ||
|
|
418
|
+
repos.find((r) => r.name.toLowerCase() === normalizedName.toLowerCase()) ||
|
|
419
|
+
null;
|
|
420
|
+
}
|
|
421
|
+
else if (repos.length > 0) {
|
|
422
|
+
found = repos[0]; // default to first repo
|
|
423
|
+
}
|
|
424
|
+
// If not yet in the registry, check whether a background job is actively cloning or
|
|
425
|
+
// analyzing this repo. Hold the connection open (up to 5 minutes) until it completes.
|
|
426
|
+
// We only wait for in-progress jobs ('queued'|'cloning'|'analyzing') — a 'complete' job
|
|
427
|
+
// whose repo is still missing means the registry sync failed; the fallback below handles it.
|
|
428
|
+
if (!found && normalizedName) {
|
|
429
|
+
const lower = normalizedName.toLowerCase();
|
|
430
|
+
// Track client disconnect to cancel the wait early
|
|
431
|
+
let clientGone = false;
|
|
432
|
+
req?.on('close', () => {
|
|
433
|
+
clientGone = true;
|
|
434
|
+
});
|
|
435
|
+
for (const job of jobManager.listJobs()) {
|
|
436
|
+
const isMatch = job.repoName?.toLowerCase() === lower ||
|
|
437
|
+
(job.repoUrl && path.basename(job.repoUrl).replace('.git', '').toLowerCase() === lower) ||
|
|
438
|
+
(job.repoPath && path.basename(job.repoPath).toLowerCase() === lower);
|
|
439
|
+
if (isMatch && ['queued', 'cloning', 'analyzing'].includes(job.status)) {
|
|
440
|
+
if (process.env.DEBUG) {
|
|
441
|
+
console.log(`[debug] resolveRepo waiting for active job ${job.id} (${normalizedName})...`);
|
|
442
|
+
}
|
|
443
|
+
for (let wait = 0; wait < HOLD_QUEUE_TIMEOUT_SECS; wait++) {
|
|
444
|
+
if (clientGone)
|
|
445
|
+
return null; // client disconnected — stop polling
|
|
446
|
+
const currentJob = jobManager.getJob(job.id);
|
|
447
|
+
if (!currentJob || currentJob.status === 'failed')
|
|
448
|
+
break;
|
|
449
|
+
if (currentJob.status === 'complete') {
|
|
450
|
+
await backend.init();
|
|
451
|
+
const freshRepos = await listRegisteredRepos();
|
|
452
|
+
return freshRepos.find((r) => r.name === normalizedName) || null;
|
|
453
|
+
}
|
|
454
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
455
|
+
}
|
|
456
|
+
// Timed out — signal to the caller with a specific message
|
|
457
|
+
return { __timedOut: true, repoName: normalizedName };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Emergency fallback: re-sync the registry to handle Windows file-system race conditions
|
|
462
|
+
// (e.g. registry file not yet flushed after clone completes).
|
|
463
|
+
if (!found && normalizedName && !isRetry) {
|
|
464
|
+
if (process.env.DEBUG) {
|
|
465
|
+
console.log(`[debug] resolveRepo 404 for "${normalizedName}". Triggering deep init...`);
|
|
466
|
+
}
|
|
467
|
+
await backend.init();
|
|
468
|
+
return await resolveRepo(normalizedName, true, req);
|
|
469
|
+
}
|
|
470
|
+
return found;
|
|
289
471
|
};
|
|
290
472
|
// SSE heartbeat — clients connect to detect server liveness instantly.
|
|
291
473
|
// When the server shuts down, the TCP connection drops and the client's
|
|
@@ -341,11 +523,18 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
341
523
|
// Get repo info
|
|
342
524
|
app.get('/api/repo', async (req, res) => {
|
|
343
525
|
try {
|
|
344
|
-
const entry = await resolveRepo(requestedRepo(req));
|
|
526
|
+
const entry = await resolveRepo(requestedRepo(req), false, req);
|
|
345
527
|
if (!entry) {
|
|
346
528
|
res.status(404).json({ error: 'Repository not found. Run: gitnexus analyze' });
|
|
347
529
|
return;
|
|
348
530
|
}
|
|
531
|
+
// Timed out waiting for an active analysis job
|
|
532
|
+
if (entry.__timedOut) {
|
|
533
|
+
res.status(503).json({
|
|
534
|
+
error: `Repository analysis for "${entry.repoName}" is taking longer than expected. Please try again in a moment.`,
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
349
538
|
const meta = await loadMeta(entry.storagePath);
|
|
350
539
|
res.json({
|
|
351
540
|
name: entry.name,
|
|
@@ -423,11 +612,56 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
423
612
|
}
|
|
424
613
|
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
425
614
|
const includeContent = req.query.includeContent === 'true';
|
|
615
|
+
const stream = req.query.stream === 'true';
|
|
616
|
+
if (stream) {
|
|
617
|
+
const abortController = new AbortController();
|
|
618
|
+
let responseFinished = false;
|
|
619
|
+
const markFinished = () => {
|
|
620
|
+
responseFinished = true;
|
|
621
|
+
};
|
|
622
|
+
const abortStreaming = () => {
|
|
623
|
+
if (!responseFinished) {
|
|
624
|
+
abortController.abort();
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
|
628
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
629
|
+
res.flushHeaders();
|
|
630
|
+
req.once('aborted', abortStreaming);
|
|
631
|
+
res.once('finish', markFinished);
|
|
632
|
+
res.once('close', abortStreaming);
|
|
633
|
+
try {
|
|
634
|
+
await withLbugDb(lbugPath, async () => streamGraphNdjson(res, includeContent, abortController.signal));
|
|
635
|
+
if (!abortController.signal.aborted && !res.writableEnded) {
|
|
636
|
+
res.end();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
req.off('aborted', abortStreaming);
|
|
641
|
+
res.off('finish', markFinished);
|
|
642
|
+
res.off('close', abortStreaming);
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
426
646
|
const graph = await withLbugDb(lbugPath, async () => buildGraph(includeContent));
|
|
427
647
|
res.json(graph);
|
|
428
648
|
}
|
|
429
649
|
catch (err) {
|
|
430
|
-
|
|
650
|
+
if (err instanceof ClientDisconnectedError) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const message = err.message || 'Failed to build graph';
|
|
654
|
+
if (res.headersSent) {
|
|
655
|
+
try {
|
|
656
|
+
res.write(JSON.stringify({ type: 'error', error: message }) + '\n');
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// Best-effort only after streaming has started.
|
|
660
|
+
}
|
|
661
|
+
res.end();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
res.status(500).json({ error: message });
|
|
431
665
|
}
|
|
432
666
|
});
|
|
433
667
|
// Execute Cypher query
|
|
@@ -1134,7 +1368,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1134
1368
|
// to the caller instead of crashing with an unhandled 'error' event.
|
|
1135
1369
|
await new Promise((resolve, reject) => {
|
|
1136
1370
|
const server = app.listen(port, host, () => {
|
|
1137
|
-
|
|
1371
|
+
const displayHost = host === '::' || host === '0.0.0.0' ? 'localhost' : host;
|
|
1372
|
+
console.log(`GitNexus server running on http://${displayHost}:${port}`);
|
|
1138
1373
|
resolve();
|
|
1139
1374
|
});
|
|
1140
1375
|
server.on('error', (err) => reject(err));
|
|
@@ -10,7 +10,8 @@ export declare function extractRepoName(url: string): string;
|
|
|
10
10
|
export declare function getCloneDir(repoName: string): string;
|
|
11
11
|
/**
|
|
12
12
|
* Validate a git URL to prevent SSRF attacks.
|
|
13
|
-
* Only allows https:// and http:// schemes. Blocks private/internal addresses
|
|
13
|
+
* Only allows https:// and http:// schemes. Blocks private/internal addresses,
|
|
14
|
+
* IPv6 private ranges, cloud metadata hostnames, and numeric IP encodings.
|
|
14
15
|
*/
|
|
15
16
|
export declare function validateGitUrl(url: string): void;
|
|
16
17
|
export interface CloneProgress {
|
package/dist/server/git-clone.js
CHANGED
|
@@ -8,6 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
|
+
import { isIP } from 'net';
|
|
11
12
|
/** Extract the repository name from a git URL (HTTPS or SSH). */
|
|
12
13
|
export function extractRepoName(url) {
|
|
13
14
|
const cleaned = url.replace(/\/+$/, '');
|
|
@@ -18,9 +19,17 @@ export function extractRepoName(url) {
|
|
|
18
19
|
export function getCloneDir(repoName) {
|
|
19
20
|
return path.join(os.homedir(), '.gitnexus', 'repos', repoName);
|
|
20
21
|
}
|
|
22
|
+
// Cloud metadata hostnames that must never be reachable via user-supplied URLs
|
|
23
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
24
|
+
'localhost',
|
|
25
|
+
'metadata.google.internal',
|
|
26
|
+
'metadata.azure.com',
|
|
27
|
+
'metadata.internal',
|
|
28
|
+
]);
|
|
21
29
|
/**
|
|
22
30
|
* Validate a git URL to prevent SSRF attacks.
|
|
23
|
-
* Only allows https:// and http:// schemes. Blocks private/internal addresses
|
|
31
|
+
* Only allows https:// and http:// schemes. Blocks private/internal addresses,
|
|
32
|
+
* IPv6 private ranges, cloud metadata hostnames, and numeric IP encodings.
|
|
24
33
|
*/
|
|
25
34
|
export function validateGitUrl(url) {
|
|
26
35
|
let parsed;
|
|
@@ -34,14 +43,91 @@ export function validateGitUrl(url) {
|
|
|
34
43
|
throw new Error('Only https:// and http:// git URLs are allowed');
|
|
35
44
|
}
|
|
36
45
|
const host = parsed.hostname.toLowerCase();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
// Block known dangerous hostnames (cloud metadata services)
|
|
47
|
+
if (BLOCKED_HOSTNAMES.has(host)) {
|
|
48
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
49
|
+
}
|
|
50
|
+
// Strip IPv6 brackets if present (URL parser behavior varies across Node versions)
|
|
51
|
+
let normalizedHost = host;
|
|
52
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
53
|
+
normalizedHost = host.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
// Check if this is an IPv6 address
|
|
56
|
+
// Use manual colon detection as fallback since isIP may return 0 for some
|
|
57
|
+
// normalized IPv6 forms (e.g. ::ffff:7f00:1)
|
|
58
|
+
const isIPv6 = isIP(normalizedHost) === 6 || normalizedHost.includes(':');
|
|
59
|
+
if (isIPv6) {
|
|
60
|
+
assertNotPrivateIPv6(normalizedHost);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Check if this is an IPv4 address (including numeric encodings)
|
|
64
|
+
if (isIP(normalizedHost) === 4) {
|
|
65
|
+
assertNotPrivateIPv4(normalizedHost);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// For non-IP hostnames, check for numeric IP tricks
|
|
69
|
+
// Decimal encoding: 2130706433 = 127.0.0.1
|
|
70
|
+
// Hex encoding: 0x7f000001 = 127.0.0.1
|
|
71
|
+
if (/^\d+$/.test(host) || /^0x[0-9a-f]+$/i.test(host)) {
|
|
72
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
73
|
+
}
|
|
74
|
+
// Standard IPv4 regex checks for dotted notation
|
|
75
|
+
if (/^127\./.test(host) ||
|
|
40
76
|
/^10\./.test(host) ||
|
|
41
77
|
/^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
|
|
42
78
|
/^192\.168\./.test(host) ||
|
|
43
79
|
/^169\.254\./.test(host) ||
|
|
44
|
-
/^0\./.test(host)
|
|
80
|
+
/^0\./.test(host) ||
|
|
81
|
+
host === '0.0.0.0' ||
|
|
82
|
+
/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(host) ||
|
|
83
|
+
/^198\.1[89]\./.test(host)) {
|
|
84
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function assertNotPrivateIPv6(ip) {
|
|
88
|
+
// Expand common compressed forms for comparison
|
|
89
|
+
const lower = ip.toLowerCase();
|
|
90
|
+
// IPv6 loopback
|
|
91
|
+
if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') {
|
|
92
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
93
|
+
}
|
|
94
|
+
// Unspecified address
|
|
95
|
+
if (lower === '::' || lower === '0:0:0:0:0:0:0:0') {
|
|
96
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
97
|
+
}
|
|
98
|
+
// IPv6 Unique Local Address (fc00::/7 = fc and fd prefixes)
|
|
99
|
+
if (lower.startsWith('fc') || lower.startsWith('fd')) {
|
|
100
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
101
|
+
}
|
|
102
|
+
// IPv6 link-local (fe80::/10)
|
|
103
|
+
if (lower.startsWith('fe80') ||
|
|
104
|
+
lower.startsWith('fe8') ||
|
|
105
|
+
lower.startsWith('fe9') ||
|
|
106
|
+
lower.startsWith('fea') ||
|
|
107
|
+
lower.startsWith('feb')) {
|
|
108
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
109
|
+
}
|
|
110
|
+
// IPv4-mapped IPv6 (::ffff:x.x.x.x or ::ffff:hex:hex)
|
|
111
|
+
// Node may normalize ::ffff:127.0.0.1 to ::ffff:7f00:1
|
|
112
|
+
if (lower.startsWith('::ffff:')) {
|
|
113
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
114
|
+
}
|
|
115
|
+
// Also catch the expanded form: 0:0:0:0:0:ffff:
|
|
116
|
+
if (lower.includes(':ffff:')) {
|
|
117
|
+
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function assertNotPrivateIPv4(ip) {
|
|
121
|
+
const parts = ip.split('.').map(Number);
|
|
122
|
+
const [a, b] = parts;
|
|
123
|
+
if (a === 127 ||
|
|
124
|
+
a === 10 ||
|
|
125
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
126
|
+
(a === 192 && b === 168) ||
|
|
127
|
+
(a === 169 && b === 254) ||
|
|
128
|
+
a === 0 ||
|
|
129
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
130
|
+
(a === 198 && (b === 18 || b === 19))) {
|
|
45
131
|
throw new Error('Cloning from private/internal addresses is not allowed');
|
|
46
132
|
}
|
|
47
133
|
}
|
|
@@ -69,6 +155,13 @@ function runGit(args, cwd) {
|
|
|
69
155
|
const proc = spawn('git', args, {
|
|
70
156
|
cwd,
|
|
71
157
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
158
|
+
env: {
|
|
159
|
+
...process.env,
|
|
160
|
+
// Prevent git from prompting for credentials (hangs the process)
|
|
161
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
162
|
+
// Ensure no credential helper tries to open a GUI prompt
|
|
163
|
+
GIT_ASKPASS: process.platform === 'win32' ? 'echo' : '/bin/true',
|
|
164
|
+
},
|
|
72
165
|
});
|
|
73
166
|
let stderr = '';
|
|
74
167
|
proc.stderr.on('data', (chunk) => {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -16,3 +16,16 @@ export declare const getGitRoot: (fromPath: string) => string | null;
|
|
|
16
16
|
* @returns `true` when `.git` is present, `false` otherwise.
|
|
17
17
|
*/
|
|
18
18
|
export declare const hasGitDir: (dirPath: string) => boolean;
|
|
19
|
+
export interface DiffHunk {
|
|
20
|
+
startLine: number;
|
|
21
|
+
endLine: number;
|
|
22
|
+
}
|
|
23
|
+
export interface FileDiff {
|
|
24
|
+
filePath: string;
|
|
25
|
+
hunks: DiffHunk[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse unified diff output (with -U0) into per-file hunk ranges.
|
|
29
|
+
* Extracts the new-file line ranges from @@ hunk headers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseDiffHunks(diffOutput: string): FileDiff[];
|