@veewo/gitnexus 1.3.9 → 1.3.11-rc.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/dist/benchmark/analyze-memory-sampler.d.ts +10 -0
- package/dist/benchmark/analyze-memory-sampler.js +12 -0
- package/dist/benchmark/analyze-memory-sampler.test.js +12 -0
- package/dist/benchmark/io.test.js +48 -5
- package/dist/benchmark/u2-e2e/config.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +25 -3
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +44 -1
- package/dist/benchmark/unity-lazy-context-sampler.d.ts +58 -0
- package/dist/benchmark/unity-lazy-context-sampler.js +217 -0
- package/dist/benchmark/unity-lazy-context-sampler.test.js +32 -0
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze-close-policy.d.ts +5 -0
- package/dist/cli/analyze-close-policy.js +9 -0
- package/dist/cli/analyze-close-policy.test.js +12 -0
- package/dist/cli/analyze-multi-scope-regression.test.js +1 -1
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- package/dist/cli/analyze-runtime-summary.d.ts +2 -0
- package/dist/cli/analyze-runtime-summary.js +9 -0
- package/dist/cli/analyze-runtime-summary.test.js +14 -0
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +95 -41
- package/dist/cli/eval-server.js +3 -0
- package/dist/cli/exit-code.d.ts +13 -0
- package/dist/cli/exit-code.js +25 -0
- package/dist/cli/exit-code.test.js +28 -0
- package/dist/cli/index.js +9 -2
- package/dist/cli/mcp.js +3 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- package/dist/cli/setup.js +3 -2
- package/dist/cli/setup.test.js +67 -0
- package/dist/cli/tool.d.ts +3 -1
- package/dist/cli/tool.js +2 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/filesystem-walker.d.ts +6 -0
- package/dist/core/ingestion/filesystem-walker.js +17 -0
- package/dist/core/ingestion/filesystem-walker.test.js +51 -0
- package/dist/core/ingestion/pipeline.js +4 -3
- package/dist/core/ingestion/unity-parity-seed.d.ts +9 -0
- package/dist/core/ingestion/unity-parity-seed.js +69 -0
- package/dist/core/ingestion/unity-parity-seed.test.d.ts +1 -0
- package/dist/core/ingestion/unity-parity-seed.test.js +35 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +2 -0
- package/dist/core/ingestion/unity-resource-processor.js +87 -53
- package/dist/core/ingestion/unity-resource-processor.test.js +37 -39
- package/dist/core/kuzu/csv-generator.d.ts +20 -1
- package/dist/core/kuzu/csv-generator.js +92 -25
- package/dist/core/kuzu/csv-generator.test.d.ts +1 -0
- package/dist/core/kuzu/csv-generator.test.js +28 -0
- package/dist/core/kuzu/kuzu-adapter.js +35 -54
- package/dist/core/kuzu/relationship-pair-buckets.d.ts +17 -0
- package/dist/core/kuzu/relationship-pair-buckets.js +79 -0
- package/dist/core/kuzu/relationship-pair-buckets.test.d.ts +1 -0
- package/dist/core/kuzu/relationship-pair-buckets.test.js +10 -0
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +1 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.js +8 -1
- package/dist/core/unity/resolver.d.ts +3 -0
- package/dist/core/unity/resolver.js +56 -2
- package/dist/core/unity/resolver.test.js +46 -0
- package/dist/core/unity/scan-context.d.ts +5 -0
- package/dist/core/unity/scan-context.js +133 -44
- package/dist/core/unity/scan-context.test.js +41 -2
- package/dist/core/unity/serialized-type-index.d.ts +5 -0
- package/dist/core/unity/serialized-type-index.js +44 -13
- package/dist/core/unity/serialized-type-index.test.js +9 -1
- package/dist/mcp/local/local-backend.d.ts +16 -0
- package/dist/mcp/local/local-backend.js +320 -4
- package/dist/mcp/local/local-backend.unity-merge.test.d.ts +1 -0
- package/dist/mcp/local/local-backend.unity-merge.test.js +261 -0
- package/dist/mcp/local/unity-enrichment.d.ts +15 -0
- package/dist/mcp/local/unity-enrichment.js +69 -5
- package/dist/mcp/local/unity-enrichment.test.js +69 -1
- package/dist/mcp/local/unity-lazy-config.d.ts +6 -0
- package/dist/mcp/local/unity-lazy-config.js +7 -0
- package/dist/mcp/local/unity-lazy-config.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-config.test.js +9 -0
- package/dist/mcp/local/unity-lazy-hydrator.d.ts +15 -0
- package/dist/mcp/local/unity-lazy-hydrator.js +43 -0
- package/dist/mcp/local/unity-lazy-hydrator.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-hydrator.test.js +66 -0
- package/dist/mcp/local/unity-lazy-overlay.d.ts +3 -0
- package/dist/mcp/local/unity-lazy-overlay.js +89 -0
- package/dist/mcp/local/unity-lazy-overlay.test.d.ts +1 -0
- package/dist/mcp/local/unity-lazy-overlay.test.js +83 -0
- package/dist/mcp/local/unity-parity-cache.d.ts +7 -0
- package/dist/mcp/local/unity-parity-cache.js +88 -0
- package/dist/mcp/local/unity-parity-cache.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-cache.test.js +143 -0
- package/dist/mcp/local/unity-parity-seed-loader.d.ts +2 -0
- package/dist/mcp/local/unity-parity-seed-loader.js +30 -0
- package/dist/mcp/local/unity-parity-seed-loader.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-seed-loader.test.js +25 -0
- package/dist/mcp/local/unity-parity-warmup-queue.d.ts +6 -0
- package/dist/mcp/local/unity-parity-warmup-queue.js +28 -0
- package/dist/mcp/local/unity-parity-warmup-queue.test.d.ts +1 -0
- package/dist/mcp/local/unity-parity-warmup-queue.test.js +15 -0
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/mcp/tools.js +24 -2
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/package.json +6 -3
- package/skills/gitnexus-cli.md +18 -0
- package/skills/gitnexus-debugging.md +16 -2
- package/skills/gitnexus-exploring.md +15 -1
- package/skills/gitnexus-guide.md +15 -0
- package/skills/gitnexus-impact-analysis.md +2 -0
- package/skills/gitnexus-refactoring.md +5 -1
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
- /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/analyze-memory-sampler.test.d.ts} +0 -0
- /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/unity-lazy-context-sampler.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → cli/analyze-close-policy.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/config-loader.test.d.ts → cli/analyze-runtime-summary.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → cli/exit-code.test.d.ts} +0 -0
- /package/dist/{mcp/local/cluster-aggregation.test.d.ts → core/ingestion/filesystem-walker.test.d.ts} +0 -0
|
@@ -54,3 +54,38 @@ export async function resolveAnalyzeScopeRules(options) {
|
|
|
54
54
|
}
|
|
55
55
|
return normalizedRules;
|
|
56
56
|
}
|
|
57
|
+
function parseScopePrefixCount(scopePrefix) {
|
|
58
|
+
if (Array.isArray(scopePrefix))
|
|
59
|
+
return scopePrefix.length;
|
|
60
|
+
if (typeof scopePrefix === 'string')
|
|
61
|
+
return scopePrefix.trim() ? 1 : 0;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
export async function resolveEffectiveAnalyzeOptions(options, stored) {
|
|
65
|
+
const includeExtensionsFromCli = parseExtensionList(options?.extensions);
|
|
66
|
+
const scopeRulesFromCli = await resolveAnalyzeScopeRules({
|
|
67
|
+
scopeManifest: options?.scopeManifest,
|
|
68
|
+
scopePrefix: options?.scopePrefix,
|
|
69
|
+
});
|
|
70
|
+
const repoAliasFromCli = normalizeRepoAlias(options?.repoAlias);
|
|
71
|
+
const hasCliExtensions = options?.extensions !== undefined;
|
|
72
|
+
const hasCliScope = Boolean(options?.scopeManifest) || parseScopePrefixCount(options?.scopePrefix) > 0;
|
|
73
|
+
const hasCliRepoAlias = options?.repoAlias !== undefined;
|
|
74
|
+
const canReuse = options?.reuseOptions !== false;
|
|
75
|
+
const includeExtensions = hasCliExtensions
|
|
76
|
+
? includeExtensionsFromCli
|
|
77
|
+
: (canReuse ? (stored?.includeExtensions || []) : []);
|
|
78
|
+
const scopeRules = hasCliScope
|
|
79
|
+
? scopeRulesFromCli
|
|
80
|
+
: (canReuse ? (stored?.scopeRules || []) : []);
|
|
81
|
+
const repoAlias = hasCliRepoAlias
|
|
82
|
+
? repoAliasFromCli
|
|
83
|
+
: (canReuse ? normalizeRepoAlias(stored?.repoAlias) : undefined);
|
|
84
|
+
const embeddings = options?.embeddings ?? (canReuse ? Boolean(stored?.embeddings) : false);
|
|
85
|
+
return {
|
|
86
|
+
includeExtensions: [...includeExtensions],
|
|
87
|
+
scopeRules: [...scopeRules],
|
|
88
|
+
repoAlias,
|
|
89
|
+
embeddings,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules } from './analyze-options.js';
|
|
6
|
+
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules, resolveEffectiveAnalyzeOptions, } from './analyze-options.js';
|
|
7
7
|
test('parseExtensionList normalizes dot prefixes', () => {
|
|
8
8
|
const exts = parseExtensionList('cs,.ts, go ');
|
|
9
9
|
assert.deepEqual(exts, ['.cs', '.ts', '.go']);
|
|
@@ -34,3 +34,44 @@ test('resolveAnalyzeScopeRules fails when manifest has no usable rule', async ()
|
|
|
34
34
|
await fs.writeFile(manifestPath, '# only comments\n\n', 'utf-8');
|
|
35
35
|
await assert.rejects(resolveAnalyzeScopeRules({ scopeManifest: manifestPath }), /no valid scope rules/i);
|
|
36
36
|
});
|
|
37
|
+
test('resolveEffectiveAnalyzeOptions reuses stored settings when CLI omits them', async () => {
|
|
38
|
+
const resolved = await resolveEffectiveAnalyzeOptions({}, {
|
|
39
|
+
includeExtensions: ['.cs'],
|
|
40
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
41
|
+
repoAlias: 'neonspark-v1-subset',
|
|
42
|
+
embeddings: true,
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(resolved.includeExtensions, ['.cs']);
|
|
45
|
+
assert.deepEqual(resolved.scopeRules, ['Assets/NEON/Code']);
|
|
46
|
+
assert.equal(resolved.repoAlias, 'neonspark-v1-subset');
|
|
47
|
+
assert.equal(resolved.embeddings, true);
|
|
48
|
+
});
|
|
49
|
+
test('resolveEffectiveAnalyzeOptions disables reuse via reuseOptions=false', async () => {
|
|
50
|
+
const resolved = await resolveEffectiveAnalyzeOptions({ reuseOptions: false }, {
|
|
51
|
+
includeExtensions: ['.cs'],
|
|
52
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
53
|
+
repoAlias: 'neonspark-v1-subset',
|
|
54
|
+
embeddings: true,
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(resolved.includeExtensions, []);
|
|
57
|
+
assert.deepEqual(resolved.scopeRules, []);
|
|
58
|
+
assert.equal(resolved.repoAlias, undefined);
|
|
59
|
+
assert.equal(resolved.embeddings, false);
|
|
60
|
+
});
|
|
61
|
+
test('resolveEffectiveAnalyzeOptions prefers explicit CLI values over stored settings', async () => {
|
|
62
|
+
const resolved = await resolveEffectiveAnalyzeOptions({
|
|
63
|
+
extensions: '.ts',
|
|
64
|
+
scopePrefix: ['src'],
|
|
65
|
+
repoAlias: 'new-alias',
|
|
66
|
+
embeddings: false,
|
|
67
|
+
}, {
|
|
68
|
+
includeExtensions: ['.cs'],
|
|
69
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
70
|
+
repoAlias: 'old-alias',
|
|
71
|
+
embeddings: true,
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(resolved.includeExtensions, ['.ts']);
|
|
74
|
+
assert.deepEqual(resolved.scopeRules, ['src']);
|
|
75
|
+
assert.equal(resolved.repoAlias, 'new-alias');
|
|
76
|
+
assert.equal(resolved.embeddings, false);
|
|
77
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function toPipelineRuntimeSummary(input) {
|
|
2
|
+
return {
|
|
3
|
+
totalFileCount: input.totalFileCount,
|
|
4
|
+
communityResult: input.communityResult,
|
|
5
|
+
processResult: input.processResult,
|
|
6
|
+
unityResult: input.unityResult,
|
|
7
|
+
scopeDiagnostics: input.scopeDiagnostics,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
|
|
4
|
+
test('toPipelineRuntimeSummary drops graph reference and preserves reporting fields', () => {
|
|
5
|
+
const out = toPipelineRuntimeSummary({
|
|
6
|
+
totalFileCount: 12,
|
|
7
|
+
communityResult: { stats: { totalCommunities: 3 } },
|
|
8
|
+
processResult: { stats: { totalProcesses: 2 } },
|
|
9
|
+
unityResult: { diagnostics: ['scanContext: scripts=1'] },
|
|
10
|
+
});
|
|
11
|
+
assert.equal('graph' in out, false);
|
|
12
|
+
assert.equal(out.totalFileCount, 12);
|
|
13
|
+
assert.equal(out.communityResult?.stats.totalCommunities, 3);
|
|
14
|
+
});
|
package/dist/cli/analyze.d.ts
CHANGED
package/dist/cli/analyze.js
CHANGED
|
@@ -18,8 +18,11 @@ import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
|
18
18
|
import { generateAIContextFiles } from './ai-context.js';
|
|
19
19
|
import fs from 'fs/promises';
|
|
20
20
|
import { registerClaudeHook } from './claude-hooks.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveEffectiveAnalyzeOptions } from './analyze-options.js';
|
|
22
22
|
import { formatFallbackSummary, formatUnityDiagnosticsSummary } from './analyze-summary.js';
|
|
23
|
+
import { resolveChildProcessExit } from './exit-code.js';
|
|
24
|
+
import { shouldCloseKuzuOnAnalyzeExit } from './analyze-close-policy.js';
|
|
25
|
+
import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
|
|
23
26
|
const HEAP_MB = 8192;
|
|
24
27
|
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
25
28
|
/** Re-exec the process with an 8GB heap if we're currently below that. */
|
|
@@ -37,7 +40,11 @@ function ensureHeap() {
|
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
catch (e) {
|
|
40
|
-
|
|
43
|
+
const resolved = resolveChildProcessExit(e, 1);
|
|
44
|
+
if (resolved.bySignal && resolved.signal) {
|
|
45
|
+
console.error(` analyze subprocess terminated by signal ${resolved.signal}`);
|
|
46
|
+
}
|
|
47
|
+
process.exitCode = resolved.code;
|
|
41
48
|
}
|
|
42
49
|
return true;
|
|
43
50
|
}
|
|
@@ -62,22 +69,6 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
62
69
|
if (ensureHeap())
|
|
63
70
|
return;
|
|
64
71
|
console.log('\n GitNexus Analyzer\n');
|
|
65
|
-
let includeExtensions = [];
|
|
66
|
-
let scopeRules = [];
|
|
67
|
-
let repoAlias;
|
|
68
|
-
try {
|
|
69
|
-
includeExtensions = parseExtensionList(options?.extensions);
|
|
70
|
-
scopeRules = await resolveAnalyzeScopeRules({
|
|
71
|
-
scopeManifest: options?.scopeManifest,
|
|
72
|
-
scopePrefix: options?.scopePrefix,
|
|
73
|
-
});
|
|
74
|
-
repoAlias = normalizeRepoAlias(options?.repoAlias);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
console.log(` ${error?.message || String(error)}\n`);
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
72
|
let repoPath;
|
|
82
73
|
if (inputPath) {
|
|
83
74
|
repoPath = path.resolve(inputPath);
|
|
@@ -99,15 +90,52 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
99
90
|
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
|
100
91
|
const currentCommit = getCurrentCommit(repoPath);
|
|
101
92
|
const existingMeta = await loadMeta(storagePath);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
93
|
+
let includeExtensions = [];
|
|
94
|
+
let scopeRules = [];
|
|
95
|
+
let repoAlias;
|
|
96
|
+
let embeddingsEnabled = false;
|
|
97
|
+
try {
|
|
98
|
+
const effectiveOptions = await resolveEffectiveAnalyzeOptions({
|
|
99
|
+
extensions: options?.extensions,
|
|
100
|
+
scopeManifest: options?.scopeManifest,
|
|
101
|
+
scopePrefix: options?.scopePrefix,
|
|
102
|
+
repoAlias: options?.repoAlias,
|
|
103
|
+
embeddings: options?.embeddings,
|
|
104
|
+
reuseOptions: options?.reuseOptions,
|
|
105
|
+
}, existingMeta?.analyzeOptions);
|
|
106
|
+
includeExtensions = effectiveOptions.includeExtensions;
|
|
107
|
+
scopeRules = effectiveOptions.scopeRules;
|
|
108
|
+
repoAlias = effectiveOptions.repoAlias;
|
|
109
|
+
embeddingsEnabled = effectiveOptions.embeddings;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.log(` ${error?.message || String(error)}\n`);
|
|
113
|
+
process.exitCode = 1;
|
|
109
114
|
return;
|
|
110
115
|
}
|
|
116
|
+
if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
|
|
117
|
+
const hasScopePrefixInput = Array.isArray(options?.scopePrefix)
|
|
118
|
+
? options.scopePrefix.length > 0
|
|
119
|
+
: Boolean(options?.scopePrefix);
|
|
120
|
+
const hasCliOverrides = options?.extensions !== undefined ||
|
|
121
|
+
Boolean(options?.scopeManifest) ||
|
|
122
|
+
hasScopePrefixInput ||
|
|
123
|
+
options?.repoAlias !== undefined ||
|
|
124
|
+
options?.embeddings !== undefined ||
|
|
125
|
+
options?.reuseOptions === false;
|
|
126
|
+
if (!hasCliOverrides) {
|
|
127
|
+
console.log(' Already up to date\n');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (options?.reuseOptions !== false &&
|
|
131
|
+
includeExtensions.length === 0 &&
|
|
132
|
+
scopeRules.length === 0 &&
|
|
133
|
+
!repoAlias &&
|
|
134
|
+
!embeddingsEnabled) {
|
|
135
|
+
console.log(' Already up to date\n');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
111
139
|
// Single progress bar for entire pipeline
|
|
112
140
|
const bar = new cliProgress.SingleBar({
|
|
113
141
|
format: ' {bar} {percentage}% | {phase}',
|
|
@@ -128,6 +156,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
128
156
|
aborted = true;
|
|
129
157
|
bar.stop();
|
|
130
158
|
console.log('\n Interrupted — cleaning up...');
|
|
159
|
+
if (!shouldCloseKuzuOnAnalyzeExit()) {
|
|
160
|
+
process.exit(130);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
131
163
|
closeKuzu().catch(() => { }).finally(() => process.exit(130));
|
|
132
164
|
};
|
|
133
165
|
process.on('SIGINT', sigintHandler);
|
|
@@ -171,7 +203,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
171
203
|
// ── Cache embeddings from existing index before rebuild ────────────
|
|
172
204
|
let cachedEmbeddingNodeIds = new Set();
|
|
173
205
|
let cachedEmbeddings = [];
|
|
174
|
-
if (
|
|
206
|
+
if (embeddingsEnabled && existingMeta && !options?.force) {
|
|
175
207
|
try {
|
|
176
208
|
updateBar(0, 'Caching embeddings...');
|
|
177
209
|
await initKuzu(kuzuPath);
|
|
@@ -225,6 +257,8 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
225
257
|
const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
|
|
226
258
|
updateBar(progress, msg);
|
|
227
259
|
});
|
|
260
|
+
const pipelineRuntime = toPipelineRuntimeSummary(pipelineResult);
|
|
261
|
+
pipelineResult = undefined;
|
|
228
262
|
const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
|
|
229
263
|
const kuzuWarnings = kuzuResult.warnings;
|
|
230
264
|
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
@@ -259,7 +293,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
259
293
|
let embeddingTime = '0.0';
|
|
260
294
|
let embeddingSkipped = true;
|
|
261
295
|
let embeddingSkipReason = 'off (use --embeddings to enable)';
|
|
262
|
-
if (
|
|
296
|
+
if (embeddingsEnabled) {
|
|
263
297
|
if (stats.nodes > EMBEDDING_NODE_LIMIT) {
|
|
264
298
|
embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
|
|
265
299
|
}
|
|
@@ -284,39 +318,48 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
284
318
|
repoPath,
|
|
285
319
|
lastCommit: currentCommit,
|
|
286
320
|
indexedAt: new Date().toISOString(),
|
|
321
|
+
analyzeOptions: {
|
|
322
|
+
includeExtensions,
|
|
323
|
+
scopeRules,
|
|
324
|
+
repoAlias,
|
|
325
|
+
embeddings: embeddingsEnabled,
|
|
326
|
+
},
|
|
287
327
|
stats: {
|
|
288
|
-
files:
|
|
328
|
+
files: pipelineRuntime.totalFileCount,
|
|
289
329
|
nodes: stats.nodes,
|
|
290
330
|
edges: stats.edges,
|
|
291
|
-
communities:
|
|
292
|
-
processes:
|
|
331
|
+
communities: pipelineRuntime.communityResult?.stats.totalCommunities,
|
|
332
|
+
processes: pipelineRuntime.processResult?.stats.totalProcesses,
|
|
293
333
|
},
|
|
294
334
|
};
|
|
295
335
|
await saveMeta(storagePath, meta);
|
|
336
|
+
await persistUnityParitySeed(storagePath, pipelineRuntime.unityResult?.paritySeed);
|
|
296
337
|
const registeredRepo = await registerRepo(repoPath, meta, { repoAlias });
|
|
297
338
|
await addToGitignore(repoPath);
|
|
298
339
|
const hookResult = await registerClaudeHook();
|
|
299
340
|
const projectName = path.basename(repoPath);
|
|
300
341
|
let aggregatedClusterCount = 0;
|
|
301
|
-
if (
|
|
342
|
+
if (pipelineRuntime.communityResult?.communities) {
|
|
302
343
|
const groups = new Map();
|
|
303
|
-
for (const c of
|
|
344
|
+
for (const c of pipelineRuntime.communityResult.communities) {
|
|
304
345
|
const label = c.heuristicLabel || c.label || 'Unknown';
|
|
305
346
|
groups.set(label, (groups.get(label) || 0) + c.symbolCount);
|
|
306
347
|
}
|
|
307
348
|
aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
|
|
308
349
|
}
|
|
309
350
|
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
310
|
-
files:
|
|
351
|
+
files: pipelineRuntime.totalFileCount,
|
|
311
352
|
nodes: stats.nodes,
|
|
312
353
|
edges: stats.edges,
|
|
313
|
-
communities:
|
|
354
|
+
communities: pipelineRuntime.communityResult?.stats.totalCommunities,
|
|
314
355
|
clusters: aggregatedClusterCount,
|
|
315
|
-
processes:
|
|
356
|
+
processes: pipelineRuntime.processResult?.stats.totalProcesses,
|
|
316
357
|
}, {
|
|
317
358
|
skillScope: ((await loadCLIConfig()).setupScope === 'global') ? 'global' : 'project',
|
|
318
359
|
});
|
|
319
|
-
|
|
360
|
+
if (shouldCloseKuzuOnAnalyzeExit()) {
|
|
361
|
+
await closeKuzu();
|
|
362
|
+
}
|
|
320
363
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
321
364
|
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
|
|
322
365
|
// Since the process exits immediately after, Node.js reclaims everything.
|
|
@@ -334,9 +377,9 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
334
377
|
console.log(` Repo Name: ${registeredRepo.name}`);
|
|
335
378
|
console.log(` Repo Alias: ${registeredRepo.alias || 'none'}`);
|
|
336
379
|
console.log(` Scope Rules: ${scopeRules.length}`);
|
|
337
|
-
console.log(` Scoped Files: ${
|
|
338
|
-
if (scopeRules.length > 0 &&
|
|
339
|
-
const diagnostics =
|
|
380
|
+
console.log(` Scoped Files: ${pipelineRuntime.totalFileCount}`);
|
|
381
|
+
if (scopeRules.length > 0 && pipelineRuntime.scopeDiagnostics) {
|
|
382
|
+
const diagnostics = pipelineRuntime.scopeDiagnostics;
|
|
340
383
|
console.log(` Scope Overlap Files: ${diagnostics.overlapFiles} (${diagnostics.dedupedMatchCount} duplicate matches removed)`);
|
|
341
384
|
if (diagnostics.normalizedCollisions.length === 0) {
|
|
342
385
|
console.log(' Scope Collisions: none');
|
|
@@ -351,11 +394,11 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
351
394
|
}
|
|
352
395
|
}
|
|
353
396
|
}
|
|
354
|
-
const unitySummaryLines = formatUnityDiagnosticsSummary(
|
|
397
|
+
const unitySummaryLines = formatUnityDiagnosticsSummary(pipelineRuntime.unityResult?.diagnostics);
|
|
355
398
|
for (const line of unitySummaryLines) {
|
|
356
399
|
console.log(` ${line}`);
|
|
357
400
|
}
|
|
358
|
-
console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${
|
|
401
|
+
console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineRuntime.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineRuntime.processResult?.stats.totalProcesses || 0} flows`);
|
|
359
402
|
console.log(` KuzuDB ${kuzuTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
|
|
360
403
|
if (includeExtensions.length > 0) {
|
|
361
404
|
console.log(` File filter: ${includeExtensions.join(', ')}`);
|
|
@@ -389,3 +432,14 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
389
432
|
process.exit(0);
|
|
390
433
|
}
|
|
391
434
|
};
|
|
435
|
+
async function persistUnityParitySeed(storagePath, seed) {
|
|
436
|
+
const seedPath = path.join(storagePath, 'unity-parity-seed.json');
|
|
437
|
+
if (!seed) {
|
|
438
|
+
try {
|
|
439
|
+
await fs.rm(seedPath, { force: true });
|
|
440
|
+
}
|
|
441
|
+
catch { }
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
await fs.writeFile(seedPath, JSON.stringify(seed), 'utf-8');
|
|
445
|
+
}
|
package/dist/cli/eval-server.js
CHANGED
|
@@ -253,6 +253,9 @@ function getNextStepHint(toolName) {
|
|
|
253
253
|
}
|
|
254
254
|
// ─── Server ───────────────────────────────────────────────────────────
|
|
255
255
|
export async function evalServerCommand(options) {
|
|
256
|
+
if (!process.env.GITNEXUS_UNITY_PARITY_WARMUP) {
|
|
257
|
+
process.env.GITNEXUS_UNITY_PARITY_WARMUP = '1';
|
|
258
|
+
}
|
|
256
259
|
const port = parseInt(options?.port || '4848');
|
|
257
260
|
const idleTimeoutSec = parseInt(options?.idleTimeout || '0');
|
|
258
261
|
const backend = new LocalBackend();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ChildExitResolution {
|
|
2
|
+
code: number;
|
|
3
|
+
signal?: string;
|
|
4
|
+
bySignal: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Convert a signal name to a conventional shell exit code (128 + signal number).
|
|
8
|
+
*/
|
|
9
|
+
export declare function getSignalExitCode(signal?: string | null): number | null;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize child-process termination details into a stable numeric exit code.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveChildProcessExit(error: any, fallbackCode?: number): ChildExitResolution;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { constants as osConstants } from 'node:os';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a signal name to a conventional shell exit code (128 + signal number).
|
|
4
|
+
*/
|
|
5
|
+
export function getSignalExitCode(signal) {
|
|
6
|
+
if (!signal)
|
|
7
|
+
return null;
|
|
8
|
+
const signalNumber = osConstants.signals[signal];
|
|
9
|
+
if (typeof signalNumber !== 'number')
|
|
10
|
+
return null;
|
|
11
|
+
return 128 + signalNumber;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalize child-process termination details into a stable numeric exit code.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveChildProcessExit(error, fallbackCode = 1) {
|
|
17
|
+
if (typeof error?.status === 'number') {
|
|
18
|
+
return { code: error.status, bySignal: false };
|
|
19
|
+
}
|
|
20
|
+
if (typeof error?.signal === 'string') {
|
|
21
|
+
const code = getSignalExitCode(error.signal) ?? fallbackCode;
|
|
22
|
+
return { code, signal: error.signal, bySignal: true };
|
|
23
|
+
}
|
|
24
|
+
return { code: fallbackCode, bySignal: false };
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { constants as osConstants } from 'node:os';
|
|
4
|
+
import { getSignalExitCode, resolveChildProcessExit } from './exit-code.js';
|
|
5
|
+
test('getSignalExitCode maps signal number using 128+N convention', () => {
|
|
6
|
+
const signalNumber = osConstants.signals.SIGSEGV;
|
|
7
|
+
const expected = typeof signalNumber === 'number' ? 128 + signalNumber : null;
|
|
8
|
+
assert.equal(getSignalExitCode('SIGSEGV'), expected);
|
|
9
|
+
});
|
|
10
|
+
test('resolveChildProcessExit prefers explicit status when present', () => {
|
|
11
|
+
const resolved = resolveChildProcessExit({ status: 42, signal: 'SIGSEGV' }, 1);
|
|
12
|
+
assert.equal(resolved.code, 42);
|
|
13
|
+
assert.equal(resolved.bySignal, false);
|
|
14
|
+
assert.equal(resolved.signal, undefined);
|
|
15
|
+
});
|
|
16
|
+
test('resolveChildProcessExit maps signal-based termination', () => {
|
|
17
|
+
const signalNumber = osConstants.signals.SIGSEGV;
|
|
18
|
+
const expected = typeof signalNumber === 'number' ? 128 + signalNumber : 1;
|
|
19
|
+
const resolved = resolveChildProcessExit({ signal: 'SIGSEGV' }, 1);
|
|
20
|
+
assert.equal(resolved.code, expected);
|
|
21
|
+
assert.equal(resolved.bySignal, true);
|
|
22
|
+
assert.equal(resolved.signal, 'SIGSEGV');
|
|
23
|
+
});
|
|
24
|
+
test('resolveChildProcessExit falls back to default code for unknown errors', () => {
|
|
25
|
+
const resolved = resolveChildProcessExit({ message: 'boom' }, 7);
|
|
26
|
+
assert.equal(resolved.code, 7);
|
|
27
|
+
assert.equal(resolved.bySignal, false);
|
|
28
|
+
});
|
package/dist/cli/index.js
CHANGED
|
@@ -14,8 +14,11 @@ if (!process.env.NODE_OPTIONS?.includes('--max-old-space-size')) {
|
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
catch (e) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const resolved = resolveChildProcessExit(e, 1);
|
|
18
|
+
if (resolved.bySignal && resolved.signal) {
|
|
19
|
+
process.stderr.write(`gitnexus: child process terminated by signal ${resolved.signal}\n`);
|
|
20
|
+
}
|
|
21
|
+
process.exit(resolved.code);
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
}
|
|
@@ -38,6 +41,7 @@ import { benchmarkUnityCommand } from './benchmark-unity.js';
|
|
|
38
41
|
import { benchmarkAgentContextCommand } from './benchmark-agent-context.js';
|
|
39
42
|
import { unityBindingsCommand } from './unity-bindings.js';
|
|
40
43
|
import { benchmarkU2E2ECommand } from './benchmark-u2-e2e.js';
|
|
44
|
+
import { resolveChildProcessExit } from './exit-code.js';
|
|
41
45
|
function resolveCliVersion() {
|
|
42
46
|
try {
|
|
43
47
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -69,6 +73,7 @@ program
|
|
|
69
73
|
.command('analyze [path]')
|
|
70
74
|
.description('Index a repository (full analysis)')
|
|
71
75
|
.option('-f, --force', 'Force full re-index even if up to date')
|
|
76
|
+
.option('--no-reuse-options', 'Do not reuse stored analyze options from previous index')
|
|
72
77
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
73
78
|
.option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
|
|
74
79
|
.option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
|
|
@@ -124,6 +129,7 @@ program
|
|
|
124
129
|
.option('-l, --limit <n>', 'Max processes to return (default: 5)')
|
|
125
130
|
.option('--content', 'Include full symbol source code')
|
|
126
131
|
.option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
|
|
132
|
+
.option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
|
|
127
133
|
.action(queryCommand);
|
|
128
134
|
program
|
|
129
135
|
.command('context [name]')
|
|
@@ -133,6 +139,7 @@ program
|
|
|
133
139
|
.option('-f, --file <path>', 'File path to disambiguate common names')
|
|
134
140
|
.option('--content', 'Include full symbol source code')
|
|
135
141
|
.option('--unity-resources <mode>', 'Unity resource retrieval mode: off|on|auto', 'off')
|
|
142
|
+
.option('--unity-hydration <mode>', 'Unity hydration mode when resources are enabled: parity|compact', 'compact')
|
|
136
143
|
.action(contextCommand);
|
|
137
144
|
program
|
|
138
145
|
.command('unity-bindings <symbol>')
|
package/dist/cli/mcp.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import { startMCPServer } from '../mcp/server.js';
|
|
9
9
|
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
10
10
|
export const mcpCommand = async () => {
|
|
11
|
+
if (!process.env.GITNEXUS_UNITY_PARITY_WARMUP) {
|
|
12
|
+
process.env.GITNEXUS_UNITY_PARITY_WARMUP = '1';
|
|
13
|
+
}
|
|
11
14
|
// Prevent unhandled errors from crashing the MCP server process.
|
|
12
15
|
// KuzuDB lock conflicts and transient errors should degrade gracefully.
|
|
13
16
|
process.on('uncaughtException', (err) => {
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { readRegistry, registerRepo } from '../storage/repo-manager.js';
|
|
6
|
+
import { loadMeta, readRegistry, registerRepo, saveMeta } from '../storage/repo-manager.js';
|
|
7
7
|
function makeMeta(repoPath, lastCommit) {
|
|
8
8
|
return {
|
|
9
9
|
repoPath,
|
|
@@ -38,3 +38,26 @@ test('registerRepo stores alias and rejects collisions on different paths', asyn
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
+
test('saveMeta/loadMeta persists analyzeOptions for future re-index reuse', async () => {
|
|
42
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-meta-'));
|
|
43
|
+
const storagePath = path.join(tmpDir, '.gitnexus');
|
|
44
|
+
await saveMeta(storagePath, {
|
|
45
|
+
repoPath: tmpDir,
|
|
46
|
+
lastCommit: 'abc1234',
|
|
47
|
+
indexedAt: '2026-03-12T00:00:00.000Z',
|
|
48
|
+
analyzeOptions: {
|
|
49
|
+
includeExtensions: ['.cs'],
|
|
50
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
51
|
+
repoAlias: 'neonspark-v1-subset',
|
|
52
|
+
embeddings: true,
|
|
53
|
+
},
|
|
54
|
+
stats: { files: 1, nodes: 2, edges: 3 },
|
|
55
|
+
});
|
|
56
|
+
const meta = await loadMeta(storagePath);
|
|
57
|
+
assert.deepEqual(meta?.analyzeOptions, {
|
|
58
|
+
includeExtensions: ['.cs'],
|
|
59
|
+
scopeRules: ['Assets/NEON/Code'],
|
|
60
|
+
repoAlias: 'neonspark-v1-subset',
|
|
61
|
+
embeddings: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
package/dist/cli/setup.js
CHANGED
|
@@ -164,9 +164,10 @@ function buildCodexMcpTable() {
|
|
|
164
164
|
function mergeCodexConfig(existingRaw) {
|
|
165
165
|
const table = buildCodexMcpTable();
|
|
166
166
|
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
167
|
-
const tablePattern =
|
|
167
|
+
const tablePattern = /^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m;
|
|
168
168
|
if (tablePattern.test(normalized)) {
|
|
169
|
-
|
|
169
|
+
// Keep exactly one table by replacing the whole previous section block.
|
|
170
|
+
return normalized.replace(tablePattern, `${table}\n\n`).trimEnd() + '\n';
|
|
170
171
|
}
|
|
171
172
|
const trimmed = normalized.trimEnd();
|
|
172
173
|
if (trimmed.length === 0)
|
package/dist/cli/setup.test.js
CHANGED
|
@@ -12,6 +12,7 @@ const packageRoot = path.resolve(here, '..', '..');
|
|
|
12
12
|
const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
|
|
13
13
|
const packageName = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
14
14
|
const expectedMcpPackage = `${packageName.name || 'gitnexus'}@latest`;
|
|
15
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
16
|
async function runSetup(args, env, cwd = packageRoot) {
|
|
16
17
|
return execFileAsync(process.execPath, [cliPath, 'setup', ...args], { cwd, env });
|
|
17
18
|
}
|
|
@@ -235,6 +236,72 @@ test('setup --scope project --agent codex writes only .codex/config.toml', async
|
|
|
235
236
|
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
236
237
|
}
|
|
237
238
|
});
|
|
239
|
+
test('setup --scope project --agent codex replaces existing gitnexus table without duplicate keys', async () => {
|
|
240
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
241
|
+
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
242
|
+
try {
|
|
243
|
+
await execFileAsync('git', ['init'], { cwd: fakeRepo });
|
|
244
|
+
const codexConfigPath = path.join(fakeRepo, '.codex', 'config.toml');
|
|
245
|
+
await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
|
|
246
|
+
await fs.writeFile(codexConfigPath, [
|
|
247
|
+
'[mcp_servers.gitnexus]',
|
|
248
|
+
'command = "npx"',
|
|
249
|
+
'args = ["-y", "oldpkg@latest", "mcp"]',
|
|
250
|
+
'',
|
|
251
|
+
'[profiles.default]',
|
|
252
|
+
'model = "gpt-5"',
|
|
253
|
+
'',
|
|
254
|
+
].join('\n'), 'utf-8');
|
|
255
|
+
await runSetup(['--scope', 'project', '--agent', 'codex'], {
|
|
256
|
+
...process.env,
|
|
257
|
+
HOME: fakeHome,
|
|
258
|
+
USERPROFILE: fakeHome,
|
|
259
|
+
}, fakeRepo);
|
|
260
|
+
const codexConfigRaw = await fs.readFile(codexConfigPath, 'utf-8');
|
|
261
|
+
const tableMatches = codexConfigRaw.match(/^\[mcp_servers\.gitnexus\]$/gm) || [];
|
|
262
|
+
assert.equal(tableMatches.length, 1);
|
|
263
|
+
const gitnexusTableMatch = codexConfigRaw.match(/^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m);
|
|
264
|
+
assert.ok(gitnexusTableMatch, 'expected [mcp_servers.gitnexus] table');
|
|
265
|
+
const gitnexusTable = gitnexusTableMatch[0];
|
|
266
|
+
assert.equal((gitnexusTable.match(/^command\s*=/gm) || []).length, 1);
|
|
267
|
+
assert.equal((gitnexusTable.match(/^args\s*=/gm) || []).length, 1);
|
|
268
|
+
assert.match(gitnexusTable, new RegExp(escapeRegExp(expectedMcpPackage)));
|
|
269
|
+
assert.doesNotMatch(gitnexusTable, /oldpkg@latest/);
|
|
270
|
+
assert.match(codexConfigRaw, /^\[profiles\.default\]$/m);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
274
|
+
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
test('setup --scope project --agent codex is idempotent across repeated runs', async () => {
|
|
278
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
279
|
+
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
280
|
+
try {
|
|
281
|
+
await execFileAsync('git', ['init'], { cwd: fakeRepo });
|
|
282
|
+
const env = {
|
|
283
|
+
...process.env,
|
|
284
|
+
HOME: fakeHome,
|
|
285
|
+
USERPROFILE: fakeHome,
|
|
286
|
+
};
|
|
287
|
+
await runSetup(['--scope', 'project', '--agent', 'codex'], env, fakeRepo);
|
|
288
|
+
await runSetup(['--scope', 'project', '--agent', 'codex'], env, fakeRepo);
|
|
289
|
+
const codexConfigPath = path.join(fakeRepo, '.codex', 'config.toml');
|
|
290
|
+
const codexConfigRaw = await fs.readFile(codexConfigPath, 'utf-8');
|
|
291
|
+
const tableMatches = codexConfigRaw.match(/^\[mcp_servers\.gitnexus\]$/gm) || [];
|
|
292
|
+
assert.equal(tableMatches.length, 1);
|
|
293
|
+
const gitnexusTableMatch = codexConfigRaw.match(/^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m);
|
|
294
|
+
assert.ok(gitnexusTableMatch, 'expected [mcp_servers.gitnexus] table');
|
|
295
|
+
const gitnexusTable = gitnexusTableMatch[0];
|
|
296
|
+
assert.equal((gitnexusTable.match(/^command\s*=/gm) || []).length, 1);
|
|
297
|
+
assert.equal((gitnexusTable.match(/^args\s*=/gm) || []).length, 1);
|
|
298
|
+
assert.match(gitnexusTable, new RegExp(escapeRegExp(expectedMcpPackage)));
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
302
|
+
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
238
305
|
test('setup --scope project --agent opencode writes only opencode.json', async () => {
|
|
239
306
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
240
307
|
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|