gitnexus 1.6.8-rc.2 → 1.6.8-rc.21
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 +20 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.js +1 -0
- package/dist/cli/analyze-config.js +40 -0
- package/dist/cli/analyze.d.ts +22 -0
- package/dist/cli/analyze.js +36 -6
- package/dist/cli/clean.d.ts +1 -0
- package/dist/cli/clean.js +43 -1
- package/dist/cli/eval-server.js +43 -0
- package/dist/cli/help-i18n.js +11 -0
- package/dist/cli/i18n/en.d.ts +13 -0
- package/dist/cli/i18n/en.js +13 -0
- package/dist/cli/i18n/resources.d.ts +26 -0
- package/dist/cli/i18n/zh-CN.d.ts +13 -0
- package/dist/cli/i18n/zh-CN.js +13 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/list.js +12 -0
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/skill-gen.js +1 -0
- package/dist/cli/status.js +26 -5
- package/dist/cli/tool.d.ts +12 -1
- package/dist/cli/tool.js +41 -1
- package/dist/core/embeddings/embedder.js +4 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/include-extractor.js +7 -7
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
- package/dist/core/ingestion/cfg/collect.d.ts +30 -0
- package/dist/core/ingestion/cfg/collect.js +34 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
- package/dist/core/ingestion/cfg/emit.d.ts +144 -0
- package/dist/core/ingestion/cfg/emit.js +315 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +231 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -2
- package/dist/core/ingestion/languages/c-cpp.js +11 -4
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
- package/dist/core/ingestion/languages/cpp/captures.js +24 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +5 -1
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.js +22 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
- package/dist/core/ingestion/pipeline.d.ts +57 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/scope-extractor.js +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/emit.d.ts +124 -0
- package/dist/core/ingestion/taint/emit.js +204 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +664 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
- package/dist/core/ingestion/taint/source-sink-config.js +11 -11
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +70 -53
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +33 -16
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/dist/core/run-analyze.d.ts +87 -0
- package/dist/core/run-analyze.js +280 -25
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/mcp/core/embedder.js +4 -0
- package/dist/mcp/local/local-backend.d.ts +86 -2
- package/dist/mcp/local/local-backend.js +657 -27
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +109 -0
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/analyze-worker.js +7 -1
- package/dist/server/api.js +46 -140
- package/dist/server/git-clone.d.ts +1 -0
- package/dist/server/git-clone.js +1 -1
- package/dist/server/middleware.d.ts +11 -0
- package/dist/server/middleware.js +27 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +31 -0
- package/dist/server/upload-paths.js +51 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/branch-index.d.ts +52 -0
- package/dist/storage/branch-index.js +65 -0
- package/dist/storage/git.d.ts +11 -0
- package/dist/storage/git.js +28 -0
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +32 -10
- package/dist/storage/repo-manager.d.ts +122 -10
- package/dist/storage/repo-manager.js +162 -21
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
- package/hooks/claude/gitnexus-hook.cjs +36 -8
- package/hooks/claude/hook-db-lock-probe.cjs +123 -2
- package/package.json +4 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/skills/gitnexus-guide.md +11 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
- package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
- package/web/assets/index-COMMmbxW.css +2 -0
- package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
- package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
- package/web/index.html +2 -2
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/index-BKWA-m7o.css +0 -2
|
@@ -11,6 +11,10 @@ import path from 'path';
|
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import { getInferredRepoName, resolveRepoIdentityRoot } from './git.js';
|
|
13
13
|
import { logger } from '../core/logger.js';
|
|
14
|
+
import { branchSlug, BRANCHES_DIR, resolveBranchPlacement, } from './branch-index.js';
|
|
15
|
+
// Re-export the #2106 branch primitives (extracted to branch-index.ts, R10) so
|
|
16
|
+
// existing `repo-manager` import sites and tests keep working unchanged.
|
|
17
|
+
export { branchSlug, resolveBranchPlacement };
|
|
14
18
|
/**
|
|
15
19
|
* Normalise a repo path for registry comparison across platforms
|
|
16
20
|
* (#664 review feedback from @evander-wang).
|
|
@@ -64,14 +68,21 @@ export const getStoragePath = (repoPath) => {
|
|
|
64
68
|
return path.join(path.resolve(repoPath), GITNEXUS_DIR);
|
|
65
69
|
};
|
|
66
70
|
/**
|
|
67
|
-
* Get paths to key storage files
|
|
71
|
+
* Get paths to key storage files.
|
|
72
|
+
*
|
|
73
|
+
* `storagePath` is ALWAYS the flat `<repo>/.gitnexus` — content-addressed
|
|
74
|
+
* caches (`parse-cache/`, `parsedfile-store/`) live there and are shared
|
|
75
|
+
* across branches (#2106 KTD7). When `branch` is provided, only `lbugPath` and
|
|
76
|
+
* `metaPath` are scoped under `branches/<slug>/`; the flat call (no `branch`)
|
|
77
|
+
* returns byte-identical paths to the pre-multi-branch behavior.
|
|
68
78
|
*/
|
|
69
|
-
export const getStoragePaths = (repoPath) => {
|
|
79
|
+
export const getStoragePaths = (repoPath, branch) => {
|
|
70
80
|
const storagePath = getStoragePath(repoPath);
|
|
81
|
+
const baseDir = branch ? path.join(storagePath, BRANCHES_DIR, branchSlug(branch)) : storagePath;
|
|
71
82
|
return {
|
|
72
83
|
storagePath,
|
|
73
|
-
lbugPath: path.join(
|
|
74
|
-
metaPath: path.join(
|
|
84
|
+
lbugPath: path.join(baseDir, 'lbug'),
|
|
85
|
+
metaPath: path.join(baseDir, 'meta.json'),
|
|
75
86
|
};
|
|
76
87
|
};
|
|
77
88
|
/**
|
|
@@ -310,7 +321,13 @@ export const readRegistry = async () => {
|
|
|
310
321
|
const writeRegistry = async (entries) => {
|
|
311
322
|
const dir = getGlobalDir();
|
|
312
323
|
await fs.mkdir(dir, { recursive: true });
|
|
313
|
-
|
|
324
|
+
// Atomic tmp+rename (mirrors saveMeta): a crash mid-write can never leave a
|
|
325
|
+
// truncated/half-written registry.json that the next load would treat as
|
|
326
|
+
// empty and silently drop every registered repo (#2106 R9).
|
|
327
|
+
const target = getGlobalRegistryPath();
|
|
328
|
+
const tmp = `${target}.tmp`;
|
|
329
|
+
await fs.writeFile(tmp, JSON.stringify(entries, null, 2), 'utf-8');
|
|
330
|
+
await fs.rename(tmp, target);
|
|
314
331
|
};
|
|
315
332
|
/**
|
|
316
333
|
* Thrown by {@link registerRepo} when a requested name is already in
|
|
@@ -466,22 +483,89 @@ export const registerRepo = async (repoPath, meta, opts) => {
|
|
|
466
483
|
throw new RegistryNameCollisionError(name, collidingEntry.path, resolved);
|
|
467
484
|
}
|
|
468
485
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
486
|
+
// This run's branch summary (non-primary runs only); hoisted so the
|
|
487
|
+
// re-read-before-write merge below can re-apply it against a fresh snapshot.
|
|
488
|
+
const summary = opts?.branch
|
|
489
|
+
? {
|
|
490
|
+
branch: opts.branch,
|
|
491
|
+
indexedAt: meta.indexedAt,
|
|
492
|
+
lastCommit: meta.lastCommit,
|
|
493
|
+
stats: meta.stats,
|
|
494
|
+
}
|
|
495
|
+
: null;
|
|
496
|
+
let entry;
|
|
497
|
+
if (summary) {
|
|
498
|
+
// Non-primary branch run (#2106): keep the primary's top-level fields and
|
|
499
|
+
// upsert this branch into branches[]. One entry per path is preserved.
|
|
500
|
+
// When the registry entry is missing (lost/rebuilt registry.json), rebuild
|
|
501
|
+
// the primary top-level from the FLAT meta.json rather than this branch's
|
|
502
|
+
// meta, so `--branch <primary>` can still resolve (#2106 review).
|
|
503
|
+
const flatMeta = existing ? null : await loadMeta(storagePath);
|
|
504
|
+
const base = existing ?? {
|
|
505
|
+
name,
|
|
506
|
+
path: resolved,
|
|
507
|
+
storagePath,
|
|
508
|
+
indexedAt: flatMeta?.indexedAt ?? meta.indexedAt,
|
|
509
|
+
lastCommit: flatMeta?.lastCommit ?? meta.lastCommit,
|
|
510
|
+
remoteUrl: flatMeta?.remoteUrl ?? meta.remoteUrl,
|
|
511
|
+
stats: flatMeta?.stats ?? meta.stats,
|
|
512
|
+
...(flatMeta?.branch ? { branch: flatMeta.branch } : {}),
|
|
513
|
+
};
|
|
514
|
+
const branches = (base.branches ?? []).filter((b) => b.branch !== summary.branch);
|
|
515
|
+
branches.push(summary);
|
|
516
|
+
entry = { ...base, name, branches };
|
|
480
517
|
}
|
|
481
518
|
else {
|
|
482
|
-
|
|
519
|
+
// Primary/flat run: refresh top-level fields, preserve any branch summaries
|
|
520
|
+
// already recorded for this path so a primary re-analyze does not drop them.
|
|
521
|
+
entry = {
|
|
522
|
+
name,
|
|
523
|
+
path: resolved,
|
|
524
|
+
storagePath,
|
|
525
|
+
indexedAt: meta.indexedAt,
|
|
526
|
+
lastCommit: meta.lastCommit,
|
|
527
|
+
remoteUrl: meta.remoteUrl,
|
|
528
|
+
stats: meta.stats,
|
|
529
|
+
...(meta.branch ? { branch: meta.branch } : {}),
|
|
530
|
+
...(existing?.branches ? { branches: existing.branches } : {}),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// Re-read immediately before writing to narrow the lost-update window (#2106
|
|
534
|
+
// R9): re-derive THIS run's delta against the FRESHEST snapshot so a
|
|
535
|
+
// concurrent change to the OTHER axis (a branch upsert vs a primary refresh)
|
|
536
|
+
// survives instead of being clobbered by a stale entry-time view.
|
|
537
|
+
const fresh = await readRegistry();
|
|
538
|
+
const freshIdx = fresh.findIndex((e) => {
|
|
539
|
+
const a = canonicalizePath(e.path);
|
|
540
|
+
return process.platform === 'win32'
|
|
541
|
+
? a.toLowerCase() === canonicalInput.toLowerCase()
|
|
542
|
+
: a === canonicalInput;
|
|
543
|
+
});
|
|
544
|
+
const freshExisting = freshIdx >= 0 ? fresh[freshIdx] : null;
|
|
545
|
+
let merged;
|
|
546
|
+
if (summary) {
|
|
547
|
+
// Branch run: keep the FRESH top-level + branches, just upsert our summary.
|
|
548
|
+
const base = freshExisting ?? entry;
|
|
549
|
+
const branches = (base.branches ?? []).filter((b) => b.branch !== summary.branch);
|
|
550
|
+
branches.push(summary);
|
|
551
|
+
merged = { ...base, name, branches };
|
|
483
552
|
}
|
|
484
|
-
|
|
553
|
+
else {
|
|
554
|
+
// Primary run: apply our refreshed top-level, but defer to the FRESH
|
|
555
|
+
// branches[] (a concurrent branch upsert or `clean --branch` wins).
|
|
556
|
+
merged = { ...entry };
|
|
557
|
+
if (freshExisting?.branches)
|
|
558
|
+
merged.branches = freshExisting.branches;
|
|
559
|
+
else
|
|
560
|
+
delete merged.branches;
|
|
561
|
+
}
|
|
562
|
+
if (freshIdx >= 0) {
|
|
563
|
+
fresh[freshIdx] = merged;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
fresh.push(merged);
|
|
567
|
+
}
|
|
568
|
+
await writeRegistry(fresh);
|
|
485
569
|
return name;
|
|
486
570
|
};
|
|
487
571
|
/**
|
|
@@ -500,6 +584,36 @@ export const unregisterRepo = async (repoPath) => {
|
|
|
500
584
|
const filtered = entries.filter((e) => !matches(canonicalizePath(e.path), resolved));
|
|
501
585
|
await writeRegistry(filtered);
|
|
502
586
|
};
|
|
587
|
+
/**
|
|
588
|
+
* Remove a single non-primary branch's summary from a repo's registry entry
|
|
589
|
+
* (#2106 R7). Called by `gitnexus clean --branch`. Returns `true` when a
|
|
590
|
+
* matching `branches[]` summary was found and removed; `false` otherwise (so
|
|
591
|
+
* the CLI can report "no such indexed branch" without crashing). The top-level
|
|
592
|
+
* primary entry is left intact; an empty `branches[]` is dropped to keep the
|
|
593
|
+
* registry shape legacy-clean.
|
|
594
|
+
*/
|
|
595
|
+
export const removeBranchIndex = async (repoPath, branch) => {
|
|
596
|
+
const resolved = canonicalizePath(repoPath);
|
|
597
|
+
const matches = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
598
|
+
const entries = await readRegistry();
|
|
599
|
+
const idx = entries.findIndex((e) => matches(canonicalizePath(e.path), resolved));
|
|
600
|
+
if (idx < 0)
|
|
601
|
+
return false;
|
|
602
|
+
const entry = entries[idx];
|
|
603
|
+
const before = entry.branches?.length ?? 0;
|
|
604
|
+
if (!entry.branches || before === 0)
|
|
605
|
+
return false;
|
|
606
|
+
const remaining = entry.branches.filter((b) => b.branch !== branch);
|
|
607
|
+
if (remaining.length === before)
|
|
608
|
+
return false; // branch not recorded
|
|
609
|
+
if (remaining.length > 0)
|
|
610
|
+
entry.branches = remaining;
|
|
611
|
+
else
|
|
612
|
+
delete entry.branches;
|
|
613
|
+
entries[idx] = entry;
|
|
614
|
+
await writeRegistry(entries);
|
|
615
|
+
return true;
|
|
616
|
+
};
|
|
503
617
|
/**
|
|
504
618
|
* Thrown by {@link resolveRegistryEntry} when no registered repo matches
|
|
505
619
|
* the caller's target string (by alias, basename, remote-inferred name,
|
|
@@ -748,7 +862,14 @@ export const resolveRegistryEntry = (entries, target) => {
|
|
|
748
862
|
};
|
|
749
863
|
/**
|
|
750
864
|
* List all registered repos from the global registry.
|
|
751
|
-
*
|
|
865
|
+
*
|
|
866
|
+
* With `validate: true`, prunes only entries whose index is *provably* gone
|
|
867
|
+
* (fs.access on .gitnexus/meta.json fails with ENOENT or ENOTDIR) and persists
|
|
868
|
+
* the result. Entries that are merely "not provably absent" — any other
|
|
869
|
+
* fs.access failure (EIO/EAGAIN/EBUSY/EACCES, etc.) — are KEPT, so a transient
|
|
870
|
+
* I/O storm cannot wipe the registry. A kept entry is therefore "not confirmed
|
|
871
|
+
* present," not "confirmed present"; downstream DB opens are independently and
|
|
872
|
+
* lazily guarded.
|
|
752
873
|
*/
|
|
753
874
|
export const listRegisteredRepos = async (opts) => {
|
|
754
875
|
const entries = await readRegistry();
|
|
@@ -761,8 +882,28 @@ export const listRegisteredRepos = async (opts) => {
|
|
|
761
882
|
await fs.access(path.join(entry.storagePath, 'meta.json'));
|
|
762
883
|
valid.push(entry);
|
|
763
884
|
}
|
|
764
|
-
catch {
|
|
765
|
-
//
|
|
885
|
+
catch (err) {
|
|
886
|
+
// Prune ONLY when the index is provably gone: ENOENT (file absent) or
|
|
887
|
+
// ENOTDIR (a path component is no longer a directory). Every other
|
|
888
|
+
// fs.access failure keeps the entry, because the file may well still
|
|
889
|
+
// exist and we must not wipe the registry on a transient I/O storm
|
|
890
|
+
// (EIO/EAGAIN/EBUSY under swap pressure, NFS hiccups, etc.).
|
|
891
|
+
//
|
|
892
|
+
// Note: some kept codes are NOT necessarily transient — EACCES, for
|
|
893
|
+
// example, can be permanent (a chmod'd directory). Keeping is still the
|
|
894
|
+
// correct conservative choice: a stale-but-kept entry is harmless (DB
|
|
895
|
+
// opens are lazily guarded) and removable via `gitnexus remove`, whereas
|
|
896
|
+
// an over-eager prune destroys data. When in doubt, keep.
|
|
897
|
+
if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
|
|
898
|
+
// Index genuinely removed — safe to prune
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
// Not provably absent — keep entry to prevent mass registry wipe.
|
|
902
|
+
// Warn so an I/O storm becomes observable instead of silently
|
|
903
|
+
// keeping (or, pre-fix, silently wiping) entries.
|
|
904
|
+
logger.warn({ name: entry.name, storagePath: entry.storagePath, code: err?.code }, 'Keeping registry entry despite fs.access failure (not provably absent); not pruning to avoid mass registry wipe.');
|
|
905
|
+
valid.push(entry);
|
|
906
|
+
}
|
|
766
907
|
}
|
|
767
908
|
}
|
|
768
909
|
// If we pruned any entries, save the cleaned registry
|
|
@@ -91,10 +91,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
|
|
|
91
91
|
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Whether opt-in diagnostics should be written to the hook's stderr. Strict
|
|
96
|
+
* hook runners validate hook output, so normal, non-error skip paths must stay
|
|
97
|
+
* silent unless the operator explicitly asks for diagnostics via GITNEXUS_DEBUG.
|
|
98
|
+
* See issue #1913.
|
|
99
|
+
*/
|
|
100
|
+
function isDebugEnabled() {
|
|
101
|
+
return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
function extractAugmentContext(stderr) {
|
|
95
105
|
const output = (stderr || '').trim();
|
|
96
106
|
const marker = output.indexOf('[GitNexus]');
|
|
97
|
-
const debug =
|
|
107
|
+
const debug = isDebugEnabled();
|
|
98
108
|
if (debug && output.length > 0) {
|
|
99
109
|
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
100
110
|
// it when no marker is present) so suppressed diagnostics — LadybugDB lock
|
|
@@ -258,8 +268,14 @@ function buildAfterToolContext(input) {
|
|
|
258
268
|
if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
|
|
259
269
|
const hint = buildStaleIndexHint(gitNexusDir, cwd);
|
|
260
270
|
if (hint) {
|
|
261
|
-
|
|
271
|
+
// The hint always reaches the agent via additionalContext (parts). Mirror
|
|
272
|
+
// it to stderr (for terminal users) only under GITNEXUS_DEBUG, so strict
|
|
273
|
+
// hook runners see no unexpected output on this normal path (#1913). The
|
|
274
|
+
// claude hook never mirrored this to stderr — this aligns the two adapters.
|
|
262
275
|
parts.push(hint);
|
|
276
|
+
if (isDebugEnabled()) {
|
|
277
|
+
process.stderr.write(`${hint}\n`);
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
}
|
|
@@ -268,14 +284,32 @@ function buildAfterToolContext(input) {
|
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
function runAugment(gitNexusDir, cwd, pattern) {
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
// Acquire the per-repo slot BEFORE the DB-owner probe (#2163): the probe
|
|
288
|
+
// itself spawns lsof/ps, so it must be bounded by the same ≤3-per-repo cap
|
|
289
|
+
// as the augment, or concurrent sessions fan out unbounded probe
|
|
290
|
+
// subprocesses. The cheap guards (extractPattern, gitNexusDir lookup) run in
|
|
291
|
+
// buildAfterToolContext before this — moving the acquire any earlier would
|
|
292
|
+
// churn slot files on tool calls that never probe.
|
|
293
|
+
const release = acquireHookSlot(gitNexusDir);
|
|
294
|
+
if (!release) {
|
|
295
|
+
// Normal skip path: all per-repo hook slots are held by concurrent
|
|
296
|
+
// sessions. Stay silent for strict hook runners (issue #1913); surface
|
|
297
|
+
// the reason only under GITNEXUS_DEBUG.
|
|
298
|
+
if (isDebugEnabled()) {
|
|
299
|
+
process.stderr.write('[GitNexus] augment skipped: hook slots saturated\n');
|
|
300
|
+
}
|
|
273
301
|
return '';
|
|
274
302
|
}
|
|
275
|
-
const release = acquireHookSlot(gitNexusDir);
|
|
276
|
-
if (!release) return '';
|
|
277
|
-
const cliPath = resolveCliPath();
|
|
278
303
|
try {
|
|
304
|
+
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
305
|
+
// Normal skip path: the MCP server owns the DB. Stay silent for strict
|
|
306
|
+
// hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
|
|
307
|
+
if (isDebugEnabled()) {
|
|
308
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
309
|
+
}
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
const cliPath = resolveCliPath();
|
|
279
313
|
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
280
314
|
if (!child.error && child.status === 0) {
|
|
281
315
|
return extractAugmentContext(child.stderr || '');
|
|
@@ -338,7 +372,7 @@ function main() {
|
|
|
338
372
|
const handler = handlers[input.hook_event_name || ''];
|
|
339
373
|
if (handler) handler(input);
|
|
340
374
|
} catch (err) {
|
|
341
|
-
if (
|
|
375
|
+
if (isDebugEnabled()) {
|
|
342
376
|
console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
|
|
343
377
|
}
|
|
344
378
|
}
|
|
@@ -110,10 +110,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
|
|
|
110
110
|
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether opt-in diagnostics should be written to the hook's stderr. Strict
|
|
115
|
+
* hook runners (e.g. Codex `PreToolUse`) validate hook output, so normal,
|
|
116
|
+
* non-error skip paths must stay silent unless the operator explicitly asks
|
|
117
|
+
* for diagnostics via GITNEXUS_DEBUG. See issue #1913.
|
|
118
|
+
*/
|
|
119
|
+
function isDebugEnabled() {
|
|
120
|
+
return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
function extractAugmentContext(stderr) {
|
|
114
124
|
const output = (stderr || '').trim();
|
|
115
125
|
const marker = output.indexOf('[GitNexus]');
|
|
116
|
-
const debug =
|
|
126
|
+
const debug = isDebugEnabled();
|
|
117
127
|
if (debug && output.length > 0) {
|
|
118
128
|
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
119
129
|
// it when no marker is present) so suppressed diagnostics — KuzuDB lock
|
|
@@ -249,17 +259,35 @@ function handlePreToolUse(input) {
|
|
|
249
259
|
|
|
250
260
|
const pattern = extractPattern(toolName, toolInput);
|
|
251
261
|
if (!pattern || pattern.length < 3) return;
|
|
252
|
-
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
253
|
-
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
262
|
|
|
263
|
+
// Acquire the per-repo slot BEFORE the DB-owner probe (#2163): the probe
|
|
264
|
+
// itself spawns lsof/ps, so it must be bounded by the same ≤3-per-repo cap
|
|
265
|
+
// as the augment, or concurrent sessions fan out unbounded probe
|
|
266
|
+
// subprocesses. Keep the acquire right after the cheap guards above —
|
|
267
|
+
// moving it earlier would churn slot files on tool calls that never probe.
|
|
257
268
|
const release = acquireHookSlot(gitNexusDir);
|
|
258
|
-
if (!release)
|
|
269
|
+
if (!release) {
|
|
270
|
+
// Normal skip path: all per-repo hook slots are held by concurrent
|
|
271
|
+
// sessions. Stay silent for strict hook runners (issue #1913); surface
|
|
272
|
+
// the reason only when diagnostics are explicitly requested.
|
|
273
|
+
if (isDebugEnabled()) {
|
|
274
|
+
process.stderr.write('[GitNexus] augment skipped: hook slots saturated\n');
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
259
278
|
|
|
260
|
-
const cliPath = resolveCliPath();
|
|
261
279
|
let result = '';
|
|
262
280
|
try {
|
|
281
|
+
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
282
|
+
// Normal skip path: the MCP server owns the DB, so the CLI augment would
|
|
283
|
+
// contend on the lock. Stay silent for strict hook runners (issue #1913);
|
|
284
|
+
// surface the reason only when diagnostics are explicitly requested.
|
|
285
|
+
if (isDebugEnabled()) {
|
|
286
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const cliPath = resolveCliPath();
|
|
263
291
|
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
264
292
|
if (!child.error && child.status === 0) {
|
|
265
293
|
result = extractAugmentContext(child.stderr || '');
|
|
@@ -361,7 +389,7 @@ function main() {
|
|
|
361
389
|
const handler = handlers[input.hook_event_name || ''];
|
|
362
390
|
if (handler) handler(input);
|
|
363
391
|
} catch (err) {
|
|
364
|
-
if (
|
|
392
|
+
if (isDebugEnabled()) {
|
|
365
393
|
console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
|
|
366
394
|
}
|
|
367
395
|
}
|
|
@@ -11,6 +11,22 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Fail-open on most errors; fail-closed only on lsof ETIMEDOUT (Unix) or
|
|
13
13
|
* PowerShell ETIMEDOUT (Windows), matching the hook contract.
|
|
14
|
+
*
|
|
15
|
+
* Unix subprocess containment contract (#2163):
|
|
16
|
+
* - lsof/ps are wrapped in coreutils `timeout`/`gtimeout` when a working
|
|
17
|
+
* wrapper is found (`timeout -k 1 <budget> lsof ...`). If this hook process
|
|
18
|
+
* is itself SIGKILLed (e.g. by the runner's 10s hook timeout) the wrapper
|
|
19
|
+
* survives, SIGTERMs its child at the budget (2s lsof / 1s ps) and SIGKILLs
|
|
20
|
+
* it 1s later — orphan lifetime is bounded at ~3s instead of unbounded.
|
|
21
|
+
* - GITNEXUS_HOOK_TIMEOUT_PATH: the sentinel value `disabled` switches the
|
|
22
|
+
* wrapper off deterministically; any other value is adopted only when it
|
|
23
|
+
* exists AND passes a one-shot `-k` self-test — otherwise resolution FALLS
|
|
24
|
+
* THROUGH to the built-in candidate list (first self-test pass wins), so
|
|
25
|
+
* no malformed value of any shape can silently disable orphan containment.
|
|
26
|
+
* - The gitnexus server is lazy-open + sticky-hold: an idle MCP server holds
|
|
27
|
+
* ZERO lbug fds until the repo's first MCP query, then keeps the fd open.
|
|
28
|
+
* A probe before that first query is therefore always false — a known,
|
|
29
|
+
* pre-existing race, not a bug in this probe.
|
|
14
30
|
*/
|
|
15
31
|
|
|
16
32
|
const fs = require('fs');
|
|
@@ -46,6 +62,80 @@ function resolveHookBinary(tool) {
|
|
|
46
62
|
return tool;
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
// Sentinel:
|
|
66
|
+
// undefined = not resolved yet (resolve lazily, on first lsof/ps fallback)
|
|
67
|
+
// string = self-tested coreutils timeout/gtimeout path (use as wrapper)
|
|
68
|
+
// null = no usable wrapper (disabled, none found, or self-test failed)
|
|
69
|
+
let unixGuardTimeoutCache;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a coreutils `timeout`/`gtimeout` binary to wrap lsof/ps with
|
|
73
|
+
* (#2163). Dead code on Windows (the win32 dispatch returns earlier).
|
|
74
|
+
*
|
|
75
|
+
* GITNEXUS_HOOK_TIMEOUT_PATH semantics: the sentinel `disabled` turns the
|
|
76
|
+
* wrapper off; any other value is only a CANDIDATE — an existing file path
|
|
77
|
+
* is tried first, but it must pass the `-k` self-test to be adopted. On any
|
|
78
|
+
* failure (non-existent path, directory, non-executable file, wrapper
|
|
79
|
+
* without `-k` support, …) resolution falls through to the built-in
|
|
80
|
+
* candidates below, tried in order, first self-test pass wins. This is
|
|
81
|
+
* strictly stronger than the sibling GITNEXUS_HOOK_LSOF_PATH /
|
|
82
|
+
* GITNEXUS_HOOK_PS_PATH overrides (which only check existence): no bad env
|
|
83
|
+
* value of ANY shape can silently disable orphan containment.
|
|
84
|
+
*
|
|
85
|
+
* Lazy self-test: candidates are probed only when the lsof/ps fallback is
|
|
86
|
+
* first reached, and the result is memoized. A candidate is adopted only
|
|
87
|
+
* when `timeout -k 1 1 /bin/sh -c :` exits 0. This rejects wrappers that do
|
|
88
|
+
* not support the coreutils `-k` flag — busybox <1.34, toybox, broken
|
|
89
|
+
* symlinks — which would otherwise exit with a usage error without ever
|
|
90
|
+
* running lsof, silently converting the lsof-ETIMEDOUT fail-closed contract
|
|
91
|
+
* into fail-open (#1492 regression). Only when EVERY candidate fails does
|
|
92
|
+
* the probe fall back to the unwrapped status quo (memoized null).
|
|
93
|
+
* busybox ≥1.34 passes the test and is fully usable (capability, not
|
|
94
|
+
* identity, decides).
|
|
95
|
+
*/
|
|
96
|
+
function passesGuardSelfTest(guard) {
|
|
97
|
+
try {
|
|
98
|
+
const selfTest = spawnSync(guard, ['-k', '1', '1', '/bin/sh', '-c', ':'], {
|
|
99
|
+
encoding: 'utf-8',
|
|
100
|
+
timeout: 3000,
|
|
101
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
});
|
|
104
|
+
return !selfTest.error && selfTest.status === 0;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveUnixGuardTimeout() {
|
|
111
|
+
if (unixGuardTimeoutCache !== undefined) return unixGuardTimeoutCache;
|
|
112
|
+
unixGuardTimeoutCache = null;
|
|
113
|
+
const fromEnv = process.env.GITNEXUS_HOOK_TIMEOUT_PATH;
|
|
114
|
+
const trimmed = fromEnv ? String(fromEnv).trim() : '';
|
|
115
|
+
if (trimmed === 'disabled') return unixGuardTimeoutCache;
|
|
116
|
+
const candidates = [];
|
|
117
|
+
if (trimmed && fs.existsSync(trimmed)) candidates.push(trimmed);
|
|
118
|
+
for (const builtin of [
|
|
119
|
+
'/usr/bin/timeout',
|
|
120
|
+
'/bin/timeout',
|
|
121
|
+
'/opt/homebrew/bin/gtimeout',
|
|
122
|
+
'/usr/local/bin/gtimeout',
|
|
123
|
+
]) {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(builtin)) candidates.push(builtin);
|
|
126
|
+
} catch {
|
|
127
|
+
/* ignore */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
if (passesGuardSelfTest(candidate)) {
|
|
132
|
+
unixGuardTimeoutCache = candidate;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return unixGuardTimeoutCache;
|
|
137
|
+
}
|
|
138
|
+
|
|
49
139
|
function resolveWindowsPowerShellPath() {
|
|
50
140
|
const fromEnv = process.env.GITNEXUS_HOOK_POWERSHELL_PATH;
|
|
51
141
|
if (fromEnv && String(fromEnv).trim() && fs.existsSync(String(fromEnv).trim())) {
|
|
@@ -188,20 +278,46 @@ function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
|
|
|
188
278
|
}
|
|
189
279
|
|
|
190
280
|
function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
|
|
281
|
+
const guard = resolveUnixGuardTimeout();
|
|
191
282
|
const lsofPath = resolveHookBinary('lsof');
|
|
192
|
-
|
|
283
|
+
// The spawnSync timeouts below (lsof 1000ms / ps 500ms) are deliberately
|
|
284
|
+
// SHORTER than the wrapper budgets (2s / 1s): on the supervised path Node's
|
|
285
|
+
// SIGTERM always fires first, so `error.code === 'ETIMEDOUT'` and the
|
|
286
|
+
// fail-closed contract are untouched. The wrapper only matters once this
|
|
287
|
+
// hook process has been SIGKILLed and can no longer deliver that SIGTERM.
|
|
288
|
+
const [lsofCmd, lsofArgs] = guard
|
|
289
|
+
? [guard, ['-k', '1', '2', lsofPath, '-nP', '-t', '--', dbPathAbs]]
|
|
290
|
+
: [lsofPath, ['-nP', '-t', '--', dbPathAbs]];
|
|
291
|
+
const lsof = spawnSync(lsofCmd, lsofArgs, {
|
|
193
292
|
encoding: 'utf-8',
|
|
194
293
|
timeout: 1000,
|
|
195
294
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
196
295
|
windowsHide: true,
|
|
197
296
|
});
|
|
198
297
|
if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
|
|
298
|
+
// Guard-mediated deaths map to "unresponsive holder" (fail-closed). Three
|
|
299
|
+
// result shapes, verified against coreutils 9.1:
|
|
300
|
+
// - signal-death: when `-k` escalates to SIGKILL, coreutils timeout
|
|
301
|
+
// SELF-RAISES the signal, so spawnSync reports {status: null, signal}
|
|
302
|
+
// with no .error (spawnSync's own ETIMEDOUT was handled above). The
|
|
303
|
+
// same shape appears when this hook is frozen >2s (SIGSTOP, laptop
|
|
304
|
+
// suspend) and the guard expires while it sleeps. By construction, a
|
|
305
|
+
// guard-wrapped probe that died by signal without spawnSync ETIMEDOUT
|
|
306
|
+
// is a budget/kill outcome.
|
|
307
|
+
// - 124: budget expired and the child exited after the plain SIGTERM.
|
|
308
|
+
// - 137: NOT the coreutils -k path — only exit-code-propagating wrappers,
|
|
309
|
+
// or a child SIGKILLed externally (e.g. the OOM killer).
|
|
310
|
+
if (guard && lsof.status === null && lsof.signal) return true;
|
|
311
|
+
if (guard && (lsof.status === 124 || lsof.status === 137)) return true;
|
|
199
312
|
|
|
200
313
|
const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
|
|
201
314
|
const psPath = resolveHookBinary('ps');
|
|
202
315
|
for (const pid of pids) {
|
|
203
316
|
if (Number(pid) === myPid) continue;
|
|
204
|
-
const
|
|
317
|
+
const [psCmd, psArgs] = guard
|
|
318
|
+
? [guard, ['-k', '1', '1', psPath, '-p', pid, '-o', 'command=']]
|
|
319
|
+
: [psPath, ['-p', pid, '-o', 'command=']];
|
|
320
|
+
const ps = spawnSync(psCmd, psArgs, {
|
|
205
321
|
encoding: 'utf-8',
|
|
206
322
|
timeout: 500,
|
|
207
323
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -211,6 +327,11 @@ function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
|
|
|
211
327
|
if (ps.error.code === 'ETIMEDOUT') return true;
|
|
212
328
|
continue;
|
|
213
329
|
}
|
|
330
|
+
// Same guard-mediated-death mapping as the lsof call above (signal-death
|
|
331
|
+
// from the -k escalation or a frozen hook; 124 budget expiry; 137 only
|
|
332
|
+
// for exit-code-propagating wrappers / external SIGKILL).
|
|
333
|
+
if (guard && ps.status === null && ps.signal) return true;
|
|
334
|
+
if (guard && (ps.status === 124 || ps.status === 137)) return true;
|
|
214
335
|
if (isGitNexusServerCommand(ps.stdout || '')) return true;
|
|
215
336
|
}
|
|
216
337
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.8-rc.
|
|
3
|
+
"version": "1.6.8-rc.21",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"test:watch": "vitest",
|
|
50
50
|
"test:coverage": "vitest run --coverage",
|
|
51
51
|
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
|
|
52
|
-
"postinstall": "node scripts/
|
|
52
|
+
"postinstall": "node scripts/build-tree-sitter-grammars.cjs",
|
|
53
53
|
"assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs",
|
|
54
54
|
"prepare": "node scripts/build.js",
|
|
55
55
|
"prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@ladybugdb/core": "^0.17.0",
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
61
61
|
"@scarf/scarf": "^1.4.0",
|
|
62
|
+
"busboy": "^1.6.0",
|
|
62
63
|
"cli-progress": "^3.12.0",
|
|
63
64
|
"commander": "^14.0.3",
|
|
64
65
|
"cors": "^2.8.5",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"uuid": "^14.0.0"
|
|
94
95
|
},
|
|
95
96
|
"devDependencies": {
|
|
97
|
+
"@types/busboy": "^1.5.4",
|
|
96
98
|
"@types/cli-progress": "^3.11.6",
|
|
97
99
|
"@types/cors": "^2.8.17",
|
|
98
100
|
"@types/express": "^5.0.6",
|
|
@@ -112,6 +112,24 @@ function findCoverageProblems({ grammars }) {
|
|
|
112
112
|
return problems;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Stray local source-build outputs under `vendor/<name>/build/`. These would
|
|
117
|
+
* ship in the tarball (`files: ["vendor"]` overrides .gitignore/.npmignore) AND
|
|
118
|
+
* shadow the committed prebuilds — `node-gyp-build` resolves `build/Release`
|
|
119
|
+
* BEFORE `prebuilds/`, so a consumer on the publisher's platform would load the
|
|
120
|
+
* stray (possibly stale/wrong) binding instead of the curated prebuild. The
|
|
121
|
+
* build dir is gitignored and only appears if a maintainer source-built locally
|
|
122
|
+
* (e.g. on a no-prebuild platform); refuse to publish it. (#2144 review.)
|
|
123
|
+
*/
|
|
124
|
+
function findStrayBuildArtifacts(vendorDir) {
|
|
125
|
+
if (!fs.existsSync(vendorDir)) return [];
|
|
126
|
+
return fs
|
|
127
|
+
.readdirSync(vendorDir)
|
|
128
|
+
.filter((d) => /^tree-sitter-/.test(d))
|
|
129
|
+
.filter((d) => fs.existsSync(path.join(vendorDir, d, 'build')))
|
|
130
|
+
.map((d) => `vendor/${d}/build`);
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
function collectGrammars(vendorDir, shipsVendorSource) {
|
|
116
134
|
if (!fs.existsSync(vendorDir)) return [];
|
|
117
135
|
return fs
|
|
@@ -141,6 +159,18 @@ function main() {
|
|
|
141
159
|
process.exit(1);
|
|
142
160
|
}
|
|
143
161
|
|
|
162
|
+
const stray = findStrayBuildArtifacts(vendorDir);
|
|
163
|
+
if (stray.length > 0) {
|
|
164
|
+
console.error(
|
|
165
|
+
'[publish-guard] Refusing to publish — stray source-build output under vendor/ would\n' +
|
|
166
|
+
'ship and shadow the committed prebuilds (node-gyp-build loads build/Release before\n' +
|
|
167
|
+
'prebuilds/):',
|
|
168
|
+
);
|
|
169
|
+
for (const s of stray) console.error(` - ${s}`);
|
|
170
|
+
console.error('\nFix: remove it before packing, e.g. `rm -rf gitnexus/vendor/*/build`.');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
const problems = findCoverageProblems({ grammars });
|
|
145
175
|
if (problems.length > 0) {
|
|
146
176
|
console.error('[publish-guard] Refusing to publish — a vendored grammar would ship unusable:');
|
|
@@ -163,6 +193,7 @@ if (require.main === module) main();
|
|
|
163
193
|
|
|
164
194
|
module.exports = {
|
|
165
195
|
findCoverageProblems,
|
|
196
|
+
findStrayBuildArtifacts,
|
|
166
197
|
filesShipsVendorSource,
|
|
167
198
|
isBuildableFromSource,
|
|
168
199
|
sourceBuildSet,
|