@veewo/gitnexus 1.5.0-rc.4 → 1.5.0
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-runner.d.ts +1 -1
- package/dist/benchmark/analyze-runner.js +4 -3
- package/dist/benchmark/analyze-runner.test.js +7 -0
- package/dist/cli/ai-context.d.ts +0 -1
- package/dist/cli/ai-context.js +15 -6
- package/dist/cli/analyze-options.js +58 -34
- package/dist/cli/analyze-options.test.js +57 -0
- package/dist/cli/analyze-runtime-summary.js +1 -0
- package/dist/cli/analyze-runtime-summary.test.js +10 -0
- package/dist/cli/analyze-summary.d.ts +2 -0
- package/dist/cli/analyze-summary.js +19 -0
- package/dist/cli/analyze.d.ts +11 -0
- package/dist/cli/analyze.js +30 -5
- package/dist/cli/analyze.test.d.ts +1 -0
- package/dist/cli/analyze.test.js +25 -0
- package/dist/cli/benchmark-agent-context.js +1 -1
- package/dist/cli/benchmark-unity.js +1 -1
- package/dist/cli/benchmark-unity.test.js +5 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/scope-manifest-config.d.ts +9 -0
- package/dist/cli/scope-manifest-config.js +37 -0
- package/dist/cli/setup.js +40 -41
- package/dist/cli/setup.test.js +14 -14
- package/dist/cli/sync-manifest.d.ts +27 -0
- package/dist/cli/sync-manifest.js +200 -0
- package/dist/cli/sync-manifest.test.d.ts +1 -0
- package/dist/cli/sync-manifest.test.js +88 -0
- package/dist/core/config/unity-config.d.ts +1 -0
- package/dist/core/config/unity-config.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +2 -1
- package/dist/core/ingestion/call-processor.js +28 -6
- package/dist/core/ingestion/heritage-processor.d.ts +2 -1
- package/dist/core/ingestion/heritage-processor.js +30 -7
- package/dist/core/ingestion/import-processor.d.ts +2 -1
- package/dist/core/ingestion/import-processor.js +28 -6
- package/dist/core/ingestion/parsing-processor.d.ts +5 -3
- package/dist/core/ingestion/parsing-processor.js +46 -13
- package/dist/core/ingestion/pipeline.js +65 -13
- package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +1 -1
- package/dist/core/ingestion/unity-runtime-binding-rules.js +21 -18
- package/dist/core/ingestion/workers/parse-worker.d.ts +2 -0
- package/dist/core/ingestion/workers/parse-worker.js +50 -6
- package/dist/core/tree-sitter/csharp-define-profile.d.ts +6 -0
- package/dist/core/tree-sitter/csharp-define-profile.js +43 -0
- package/dist/core/tree-sitter/csharp-preproc-normalizer.d.ts +14 -0
- package/dist/core/tree-sitter/csharp-preproc-normalizer.js +261 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +10 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/types/pipeline.d.ts +13 -0
- package/package.json +12 -12
- package/scripts/check-sync-manifest-traceability.mjs +203 -0
- package/scripts/tree-sitter-audit-classify.mjs +172 -0
- package/skills/gitnexus-cli.md +36 -4
- package/skills/gitnexus-unity-rule-gen.md +2 -2
package/dist/cli/setup.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'url';
|
|
|
14
14
|
import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
|
|
15
15
|
import { getGitRoot } from '../storage/git.js';
|
|
16
16
|
import { glob } from 'glob';
|
|
17
|
-
import {
|
|
17
|
+
import { resolveCliSpec } from '../config/cli-spec.js';
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = path.dirname(__filename);
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
@@ -47,25 +47,24 @@ async function installLegacyCursorSkills(result) {
|
|
|
47
47
|
result.errors.push(`Cursor skills: ${err.message}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
const DEFAULT_MCP_PACKAGE_SPEC = resolveCliSpec().packageSpec;
|
|
51
50
|
/**
|
|
52
51
|
* The MCP server entry for all editors.
|
|
53
|
-
*
|
|
52
|
+
* Uses the locally installed gitnexus binary.
|
|
54
53
|
*/
|
|
55
|
-
function getMcpEntry(
|
|
54
|
+
function getMcpEntry() {
|
|
56
55
|
if (process.platform === 'win32') {
|
|
57
56
|
return {
|
|
58
57
|
command: 'cmd',
|
|
59
|
-
args: ['/c', '
|
|
58
|
+
args: ['/c', 'gitnexus', 'mcp'],
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
61
|
return {
|
|
63
|
-
command: '
|
|
64
|
-
args: ['
|
|
62
|
+
command: 'gitnexus',
|
|
63
|
+
args: ['mcp'],
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
|
-
function getOpenCodeMcpEntry(
|
|
68
|
-
const entry = getMcpEntry(
|
|
66
|
+
function getOpenCodeMcpEntry() {
|
|
67
|
+
const entry = getMcpEntry();
|
|
69
68
|
return {
|
|
70
69
|
type: 'local',
|
|
71
70
|
command: [entry.command, ...entry.args],
|
|
@@ -75,28 +74,28 @@ function getOpenCodeMcpEntry(mcpPackageSpec) {
|
|
|
75
74
|
* Merge gitnexus entry into an existing MCP config JSON object.
|
|
76
75
|
* Returns the updated config.
|
|
77
76
|
*/
|
|
78
|
-
function mergeMcpConfig(existing
|
|
77
|
+
function mergeMcpConfig(existing) {
|
|
79
78
|
if (!existing || typeof existing !== 'object') {
|
|
80
79
|
existing = {};
|
|
81
80
|
}
|
|
82
81
|
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
83
82
|
existing.mcpServers = {};
|
|
84
83
|
}
|
|
85
|
-
existing.mcpServers.gitnexus = getMcpEntry(
|
|
84
|
+
existing.mcpServers.gitnexus = getMcpEntry();
|
|
86
85
|
return existing;
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
89
88
|
* Merge gitnexus entry into an OpenCode config JSON object.
|
|
90
89
|
* Returns the updated config.
|
|
91
90
|
*/
|
|
92
|
-
function mergeOpenCodeConfig(existing
|
|
91
|
+
function mergeOpenCodeConfig(existing) {
|
|
93
92
|
if (!existing || typeof existing !== 'object') {
|
|
94
93
|
existing = {};
|
|
95
94
|
}
|
|
96
95
|
if (!existing.mcp || typeof existing.mcp !== 'object') {
|
|
97
96
|
existing.mcp = {};
|
|
98
97
|
}
|
|
99
|
-
existing.mcp.gitnexus = getOpenCodeMcpEntry(
|
|
98
|
+
existing.mcp.gitnexus = getOpenCodeMcpEntry();
|
|
100
99
|
return existing;
|
|
101
100
|
}
|
|
102
101
|
/**
|
|
@@ -148,16 +147,16 @@ async function fileExists(filePath) {
|
|
|
148
147
|
function toTomlString(value) {
|
|
149
148
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
150
149
|
}
|
|
151
|
-
function buildCodexMcpTable(
|
|
152
|
-
const entry = getMcpEntry(
|
|
150
|
+
function buildCodexMcpTable() {
|
|
151
|
+
const entry = getMcpEntry();
|
|
153
152
|
return [
|
|
154
153
|
'[mcp_servers.gitnexus]',
|
|
155
154
|
`command = ${toTomlString(entry.command)}`,
|
|
156
155
|
`args = [${entry.args.map(toTomlString).join(', ')}]`,
|
|
157
156
|
].join('\n');
|
|
158
157
|
}
|
|
159
|
-
function mergeCodexConfig(existingRaw
|
|
160
|
-
const table = buildCodexMcpTable(
|
|
158
|
+
function mergeCodexConfig(existingRaw) {
|
|
159
|
+
const table = buildCodexMcpTable();
|
|
161
160
|
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
162
161
|
const tablePattern = /^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m;
|
|
163
162
|
if (tablePattern.test(normalized)) {
|
|
@@ -179,7 +178,7 @@ async function resolveOpenCodeConfigPath(opencodeDir) {
|
|
|
179
178
|
return preferredPath;
|
|
180
179
|
}
|
|
181
180
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
182
|
-
async function setupCursor(result
|
|
181
|
+
async function setupCursor(result) {
|
|
183
182
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
184
183
|
if (!(await dirExists(cursorDir))) {
|
|
185
184
|
result.skipped.push('Cursor (not installed)');
|
|
@@ -188,7 +187,7 @@ async function setupCursor(result, mcpPackageSpec) {
|
|
|
188
187
|
const mcpPath = path.join(cursorDir, 'mcp.json');
|
|
189
188
|
try {
|
|
190
189
|
const existing = await readJsonFile(mcpPath);
|
|
191
|
-
const updated = mergeMcpConfig(existing
|
|
190
|
+
const updated = mergeMcpConfig(existing);
|
|
192
191
|
await writeJsonFile(mcpPath, updated);
|
|
193
192
|
result.configured.push('Cursor');
|
|
194
193
|
}
|
|
@@ -196,7 +195,7 @@ async function setupCursor(result, mcpPackageSpec) {
|
|
|
196
195
|
result.errors.push(`Cursor: ${err.message}`);
|
|
197
196
|
}
|
|
198
197
|
}
|
|
199
|
-
async function setupClaudeCode(result
|
|
198
|
+
async function setupClaudeCode(result) {
|
|
200
199
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
201
200
|
const hasClaude = await dirExists(claudeDir);
|
|
202
201
|
if (!hasClaude) {
|
|
@@ -207,7 +206,7 @@ async function setupClaudeCode(result, mcpPackageSpec) {
|
|
|
207
206
|
console.log('');
|
|
208
207
|
console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
|
|
209
208
|
console.log('');
|
|
210
|
-
console.log(` claude mcp add gitnexus --
|
|
209
|
+
console.log(` claude mcp add gitnexus -- gitnexus mcp`);
|
|
211
210
|
console.log('');
|
|
212
211
|
result.configured.push('Claude Code (MCP manual step printed)');
|
|
213
212
|
}
|
|
@@ -243,7 +242,7 @@ async function installProjectAgentSkills(repoRoot, result) {
|
|
|
243
242
|
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
244
243
|
* Merges hook config without overwriting existing hooks.
|
|
245
244
|
*/
|
|
246
|
-
async function installClaudeCodeHooks(result
|
|
245
|
+
async function installClaudeCodeHooks(result) {
|
|
247
246
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
248
247
|
if (!(await dirExists(claudeDir)))
|
|
249
248
|
return;
|
|
@@ -295,7 +294,7 @@ async function installClaudeCodeHooks(result, mcpPackageSpec) {
|
|
|
295
294
|
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
296
295
|
}
|
|
297
296
|
}
|
|
298
|
-
async function setupOpenCode(result
|
|
297
|
+
async function setupOpenCode(result) {
|
|
299
298
|
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
300
299
|
if (!(await dirExists(opencodeDir))) {
|
|
301
300
|
result.skipped.push('OpenCode (not installed)');
|
|
@@ -304,7 +303,7 @@ async function setupOpenCode(result, mcpPackageSpec) {
|
|
|
304
303
|
const configPath = await resolveOpenCodeConfigPath(opencodeDir);
|
|
305
304
|
try {
|
|
306
305
|
const existing = await readJsonFile(configPath);
|
|
307
|
-
const config = mergeOpenCodeConfig(existing
|
|
306
|
+
const config = mergeOpenCodeConfig(existing);
|
|
308
307
|
await writeJsonFile(configPath, config);
|
|
309
308
|
result.configured.push(`OpenCode (${path.basename(configPath)})`);
|
|
310
309
|
}
|
|
@@ -312,8 +311,8 @@ async function setupOpenCode(result, mcpPackageSpec) {
|
|
|
312
311
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
313
312
|
}
|
|
314
313
|
}
|
|
315
|
-
async function setupCodex(result
|
|
316
|
-
const entry = getMcpEntry(
|
|
314
|
+
async function setupCodex(result) {
|
|
315
|
+
const entry = getMcpEntry();
|
|
317
316
|
try {
|
|
318
317
|
await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { timeout: 15000 });
|
|
319
318
|
result.configured.push('Codex');
|
|
@@ -326,11 +325,11 @@ async function setupCodex(result, mcpPackageSpec) {
|
|
|
326
325
|
result.errors.push(`Codex: ${err.message}`);
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
|
-
async function setupProjectMcp(repoRoot, result
|
|
328
|
+
async function setupProjectMcp(repoRoot, result) {
|
|
330
329
|
const mcpPath = path.join(repoRoot, '.mcp.json');
|
|
331
330
|
try {
|
|
332
331
|
const existing = await readJsonFile(mcpPath);
|
|
333
|
-
const updated = mergeMcpConfig(existing
|
|
332
|
+
const updated = mergeMcpConfig(existing);
|
|
334
333
|
await writeJsonFile(mcpPath, updated);
|
|
335
334
|
result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
|
|
336
335
|
}
|
|
@@ -338,7 +337,7 @@ async function setupProjectMcp(repoRoot, result, mcpPackageSpec) {
|
|
|
338
337
|
result.errors.push(`Project MCP: ${err.message}`);
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
|
-
async function setupProjectCodex(repoRoot, result
|
|
340
|
+
async function setupProjectCodex(repoRoot, result) {
|
|
342
341
|
const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
|
|
343
342
|
try {
|
|
344
343
|
let existingRaw = '';
|
|
@@ -349,7 +348,7 @@ async function setupProjectCodex(repoRoot, result, mcpPackageSpec) {
|
|
|
349
348
|
if (err?.code !== 'ENOENT')
|
|
350
349
|
throw err;
|
|
351
350
|
}
|
|
352
|
-
const merged = mergeCodexConfig(existingRaw
|
|
351
|
+
const merged = mergeCodexConfig(existingRaw);
|
|
353
352
|
await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
|
|
354
353
|
await fs.writeFile(codexConfigPath, merged, 'utf-8');
|
|
355
354
|
result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
|
|
@@ -358,11 +357,11 @@ async function setupProjectCodex(repoRoot, result, mcpPackageSpec) {
|
|
|
358
357
|
result.errors.push(`Project Codex MCP: ${err.message}`);
|
|
359
358
|
}
|
|
360
359
|
}
|
|
361
|
-
async function setupProjectOpenCode(repoRoot, result
|
|
360
|
+
async function setupProjectOpenCode(repoRoot, result) {
|
|
362
361
|
const opencodePath = path.join(repoRoot, 'opencode.json');
|
|
363
362
|
try {
|
|
364
363
|
const existing = await readJsonFile(opencodePath);
|
|
365
|
-
const merged = mergeOpenCodeConfig(existing
|
|
364
|
+
const merged = mergeOpenCodeConfig(existing);
|
|
366
365
|
await writeJsonFile(opencodePath, merged);
|
|
367
366
|
result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
|
|
368
367
|
}
|
|
@@ -510,7 +509,7 @@ export const setupCommand = async (options = {}) => {
|
|
|
510
509
|
explicitVersion: options.cliVersion,
|
|
511
510
|
config: existingConfig,
|
|
512
511
|
});
|
|
513
|
-
const mcpPackageSpec = resolvedCliSpec.packageSpec
|
|
512
|
+
const mcpPackageSpec = resolvedCliSpec.packageSpec;
|
|
514
513
|
const result = {
|
|
515
514
|
configured: [],
|
|
516
515
|
skipped: [],
|
|
@@ -518,7 +517,7 @@ export const setupCommand = async (options = {}) => {
|
|
|
518
517
|
};
|
|
519
518
|
if (scope === 'global') {
|
|
520
519
|
if (legacyCursorMode) {
|
|
521
|
-
await setupCursor(result
|
|
520
|
+
await setupCursor(result);
|
|
522
521
|
await installLegacyCursorSkills(result);
|
|
523
522
|
await saveSetupConfig(scope, mcpPackageSpec, result);
|
|
524
523
|
agent = LEGACY_CURSOR_AGENT;
|
|
@@ -526,15 +525,15 @@ export const setupCommand = async (options = {}) => {
|
|
|
526
525
|
else {
|
|
527
526
|
// Configure only the selected agent MCP
|
|
528
527
|
if (agent === 'claude') {
|
|
529
|
-
await setupClaudeCode(result
|
|
528
|
+
await setupClaudeCode(result);
|
|
530
529
|
// Claude-only hooks should only be installed when Claude is selected.
|
|
531
|
-
await installClaudeCodeHooks(result
|
|
530
|
+
await installClaudeCodeHooks(result);
|
|
532
531
|
}
|
|
533
532
|
else if (agent === 'opencode') {
|
|
534
|
-
await setupOpenCode(result
|
|
533
|
+
await setupOpenCode(result);
|
|
535
534
|
}
|
|
536
535
|
else if (agent === 'codex') {
|
|
537
|
-
await setupCodex(result
|
|
536
|
+
await setupCodex(result);
|
|
538
537
|
}
|
|
539
538
|
// Install shared global skills once
|
|
540
539
|
await installGlobalAgentSkills(result);
|
|
@@ -549,13 +548,13 @@ export const setupCommand = async (options = {}) => {
|
|
|
549
548
|
return;
|
|
550
549
|
}
|
|
551
550
|
if (agent === 'claude') {
|
|
552
|
-
await setupProjectMcp(repoRoot, result
|
|
551
|
+
await setupProjectMcp(repoRoot, result);
|
|
553
552
|
}
|
|
554
553
|
else if (agent === 'codex') {
|
|
555
|
-
await setupProjectCodex(repoRoot, result
|
|
554
|
+
await setupProjectCodex(repoRoot, result);
|
|
556
555
|
}
|
|
557
556
|
else if (agent === 'opencode') {
|
|
558
|
-
await setupProjectOpenCode(repoRoot, result
|
|
557
|
+
await setupProjectOpenCode(repoRoot, result);
|
|
559
558
|
}
|
|
560
559
|
await installProjectAgentSkills(repoRoot, result);
|
|
561
560
|
await saveSetupConfig(scope, mcpPackageSpec, result);
|
package/dist/cli/setup.test.js
CHANGED
|
@@ -10,9 +10,6 @@ const execFileAsync = promisify(execFile);
|
|
|
10
10
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const packageRoot = path.resolve(here, '..', '..');
|
|
12
12
|
const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
|
|
13
|
-
const packageName = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
14
|
-
const expectedMcpPackage = `${packageName.name || 'gitnexus'}@latest`;
|
|
15
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
13
|
async function runSetup(args, env, cwd = packageRoot) {
|
|
17
14
|
return execFileAsync(process.execPath, [cliPath, 'setup', ...args], { cwd, env });
|
|
18
15
|
}
|
|
@@ -30,8 +27,8 @@ test('setup without --agent uses legacy Cursor install path', async () => {
|
|
|
30
27
|
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
31
28
|
const cursorMcpRaw = await fs.readFile(cursorMcpPath, 'utf-8');
|
|
32
29
|
const cursorMcp = JSON.parse(cursorMcpRaw);
|
|
33
|
-
assert.equal(cursorMcp.mcpServers?.gitnexus?.command, '
|
|
34
|
-
assert.deepEqual(cursorMcp.mcpServers?.gitnexus?.args, ['
|
|
30
|
+
assert.equal(cursorMcp.mcpServers?.gitnexus?.command, 'gitnexus');
|
|
31
|
+
assert.deepEqual(cursorMcp.mcpServers?.gitnexus?.args, ['mcp']);
|
|
35
32
|
await fs.access(cursorSkillPath);
|
|
36
33
|
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
37
34
|
const config = JSON.parse(configRaw);
|
|
@@ -142,7 +139,7 @@ process.exit(0);
|
|
|
142
139
|
const raw = await fs.readFile(outputPath, 'utf-8');
|
|
143
140
|
const parsed = JSON.parse(raw);
|
|
144
141
|
assert.deepEqual(parsed.args.slice(0, 4), ['mcp', 'add', 'gitnexus', '--']);
|
|
145
|
-
assert.ok(parsed.args.includes(
|
|
142
|
+
assert.ok(parsed.args.includes('gitnexus'));
|
|
146
143
|
assert.ok(parsed.args.includes('mcp'));
|
|
147
144
|
}
|
|
148
145
|
finally {
|
|
@@ -164,7 +161,7 @@ test('setup configures OpenCode MCP in ~/.config/opencode/opencode.json', async
|
|
|
164
161
|
const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
|
|
165
162
|
const opencodeConfig = JSON.parse(opencodeRaw);
|
|
166
163
|
assert.equal(opencodeConfig.mcp?.gitnexus?.type, 'local');
|
|
167
|
-
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['
|
|
164
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['gitnexus', 'mcp']);
|
|
168
165
|
}
|
|
169
166
|
finally {
|
|
170
167
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
@@ -186,7 +183,8 @@ test('setup --cli-version pins MCP package spec and persists it in config', asyn
|
|
|
186
183
|
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
187
184
|
const savedConfigRaw = await fs.readFile(configPath, 'utf-8');
|
|
188
185
|
const savedConfig = JSON.parse(savedConfigRaw);
|
|
189
|
-
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['
|
|
186
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['gitnexus', 'mcp']);
|
|
187
|
+
// Version is persisted to config, not MCP entry:
|
|
190
188
|
assert.equal(savedConfig.cliPackageSpec, '@veewo/gitnexus@1.4.7-rc');
|
|
191
189
|
assert.equal(savedConfig.cliVersion, '1.4.7-rc');
|
|
192
190
|
}
|
|
@@ -211,7 +209,7 @@ test('setup keeps using legacy ~/.config/opencode/config.json when it already ex
|
|
|
211
209
|
const legacyConfig = JSON.parse(legacyRaw);
|
|
212
210
|
assert.equal(legacyConfig.existing, true);
|
|
213
211
|
assert.equal(legacyConfig.mcp?.gitnexus?.type, 'local');
|
|
214
|
-
assert.deepEqual(legacyConfig.mcp?.gitnexus?.command, ['
|
|
212
|
+
assert.deepEqual(legacyConfig.mcp?.gitnexus?.command, ['gitnexus', 'mcp']);
|
|
215
213
|
await assert.rejects(fs.access(preferredConfigPath));
|
|
216
214
|
}
|
|
217
215
|
finally {
|
|
@@ -254,7 +252,8 @@ test('setup --scope project --agent claude writes only .mcp.json', async () => {
|
|
|
254
252
|
const opencodeConfigPath = path.join(fakeRepo, 'opencode.json');
|
|
255
253
|
const projectMcpRaw = await fs.readFile(projectMcpPath, 'utf-8');
|
|
256
254
|
const projectMcp = JSON.parse(projectMcpRaw);
|
|
257
|
-
assert.equal(projectMcp.mcpServers?.gitnexus?.command, '
|
|
255
|
+
assert.equal(projectMcp.mcpServers?.gitnexus?.command, 'gitnexus');
|
|
256
|
+
assert.deepEqual(projectMcp.mcpServers?.gitnexus?.args, ['mcp']);
|
|
258
257
|
await assert.rejects(fs.access(codexConfigPath));
|
|
259
258
|
await assert.rejects(fs.access(opencodeConfigPath));
|
|
260
259
|
}
|
|
@@ -278,7 +277,8 @@ test('setup --scope project --agent codex writes only .codex/config.toml', async
|
|
|
278
277
|
const opencodeConfigPath = path.join(fakeRepo, 'opencode.json');
|
|
279
278
|
const codexConfigRaw = await fs.readFile(codexConfigPath, 'utf-8');
|
|
280
279
|
assert.match(codexConfigRaw, /\[mcp_servers\.gitnexus\]/);
|
|
281
|
-
assert.match(codexConfigRaw, /command = "
|
|
280
|
+
assert.match(codexConfigRaw, /command = "gitnexus"/);
|
|
281
|
+
assert.match(codexConfigRaw, /args = \["mcp"\]/);
|
|
282
282
|
await assert.rejects(fs.access(projectMcpPath));
|
|
283
283
|
await assert.rejects(fs.access(opencodeConfigPath));
|
|
284
284
|
}
|
|
@@ -316,7 +316,7 @@ test('setup --scope project --agent codex replaces existing gitnexus table witho
|
|
|
316
316
|
const gitnexusTable = gitnexusTableMatch[0];
|
|
317
317
|
assert.equal((gitnexusTable.match(/^command\s*=/gm) || []).length, 1);
|
|
318
318
|
assert.equal((gitnexusTable.match(/^args\s*=/gm) || []).length, 1);
|
|
319
|
-
assert.match(gitnexusTable,
|
|
319
|
+
assert.match(gitnexusTable, /command = "gitnexus"/);
|
|
320
320
|
assert.doesNotMatch(gitnexusTable, /oldpkg@latest/);
|
|
321
321
|
assert.match(codexConfigRaw, /^\[profiles\.default\]$/m);
|
|
322
322
|
}
|
|
@@ -346,7 +346,7 @@ test('setup --scope project --agent codex is idempotent across repeated runs', a
|
|
|
346
346
|
const gitnexusTable = gitnexusTableMatch[0];
|
|
347
347
|
assert.equal((gitnexusTable.match(/^command\s*=/gm) || []).length, 1);
|
|
348
348
|
assert.equal((gitnexusTable.match(/^args\s*=/gm) || []).length, 1);
|
|
349
|
-
assert.match(gitnexusTable,
|
|
349
|
+
assert.match(gitnexusTable, /command = "gitnexus"/);
|
|
350
350
|
}
|
|
351
351
|
finally {
|
|
352
352
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
@@ -374,7 +374,7 @@ test('setup --scope project --agent opencode writes only opencode.json', async (
|
|
|
374
374
|
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
375
375
|
const config = JSON.parse(configRaw);
|
|
376
376
|
assert.equal(opencodeConfig.mcp?.gitnexus?.type, 'local');
|
|
377
|
-
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['
|
|
377
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['gitnexus', 'mcp']);
|
|
378
378
|
await assert.rejects(fs.access(projectMcpPath));
|
|
379
379
|
await assert.rejects(fs.access(codexConfigPath));
|
|
380
380
|
await fs.access(localSkillPath);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SyncManifestScopeOptions {
|
|
2
|
+
scopeManifest?: string;
|
|
3
|
+
scopePrefix?: string[] | string;
|
|
4
|
+
}
|
|
5
|
+
export type SyncManifestPolicy = 'ask' | 'update' | 'keep' | 'error';
|
|
6
|
+
export interface SyncManifestDiffEntry {
|
|
7
|
+
directive: 'extensions' | 'repoAlias' | 'embeddings';
|
|
8
|
+
manifestValue?: string;
|
|
9
|
+
cliValue: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EnforceSyncManifestConsistencyInput {
|
|
12
|
+
manifestPath?: string;
|
|
13
|
+
extensions?: string;
|
|
14
|
+
repoAlias?: string;
|
|
15
|
+
embeddings?: boolean;
|
|
16
|
+
policy?: SyncManifestPolicy;
|
|
17
|
+
stdinIsTTY?: boolean;
|
|
18
|
+
prompt?: (message: string) => Promise<'update' | 'keep'>;
|
|
19
|
+
}
|
|
20
|
+
export interface EnforceSyncManifestConsistencyResult {
|
|
21
|
+
decision: 'none' | 'update' | 'keep';
|
|
22
|
+
diff: SyncManifestDiffEntry[];
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveDefaultSyncManifestPath(repoPath: string): string;
|
|
25
|
+
export declare function shouldAutoUseSyncManifest(options?: SyncManifestScopeOptions): boolean;
|
|
26
|
+
export declare function resolveScopeManifestForAnalyze(repoPath: string, options?: SyncManifestScopeOptions, pathExists?: (candidatePath: string) => Promise<boolean>): Promise<string | undefined>;
|
|
27
|
+
export declare function enforceSyncManifestConsistency(input: EnforceSyncManifestConsistencyInput): Promise<EnforceSyncManifestConsistencyResult>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
import { parseScopeManifestConfig } from './scope-manifest-config.js';
|
|
6
|
+
import { normalizeRepoAlias, parseExtensionList } from './analyze-options.js';
|
|
7
|
+
export function resolveDefaultSyncManifestPath(repoPath) {
|
|
8
|
+
return path.join(repoPath, '.gitnexus', 'sync-manifest.txt');
|
|
9
|
+
}
|
|
10
|
+
export function shouldAutoUseSyncManifest(options) {
|
|
11
|
+
if (options?.scopeManifest)
|
|
12
|
+
return false;
|
|
13
|
+
return parseScopePrefixCount(options?.scopePrefix) === 0;
|
|
14
|
+
}
|
|
15
|
+
export async function resolveScopeManifestForAnalyze(repoPath, options, pathExists = fileExists) {
|
|
16
|
+
if (options?.scopeManifest) {
|
|
17
|
+
return options.scopeManifest;
|
|
18
|
+
}
|
|
19
|
+
if (!shouldAutoUseSyncManifest(options)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const defaultManifestPath = resolveDefaultSyncManifestPath(repoPath);
|
|
23
|
+
if (await pathExists(defaultManifestPath)) {
|
|
24
|
+
return defaultManifestPath;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function enforceSyncManifestConsistency(input) {
|
|
29
|
+
if (!input.manifestPath) {
|
|
30
|
+
return { decision: 'none', diff: [] };
|
|
31
|
+
}
|
|
32
|
+
ensureConcreteManifestPath(input.manifestPath);
|
|
33
|
+
const raw = await fs.readFile(input.manifestPath, 'utf-8');
|
|
34
|
+
const parsed = parseScopeManifestConfig(raw);
|
|
35
|
+
const normalizedDirectives = normalizeManifestDirectives(parsed.directives);
|
|
36
|
+
const diff = computeDiff(normalizedDirectives, input);
|
|
37
|
+
const policy = normalizePolicy(input.policy);
|
|
38
|
+
if (diff.length === 0) {
|
|
39
|
+
if (policy === 'update') {
|
|
40
|
+
throw new Error('Sync manifest rewrite requires non-empty diff entries.');
|
|
41
|
+
}
|
|
42
|
+
return { decision: 'none', diff };
|
|
43
|
+
}
|
|
44
|
+
const decision = await resolveDecision(policy, input.manifestPath, diff, input.stdinIsTTY, input.prompt);
|
|
45
|
+
if (decision === 'update') {
|
|
46
|
+
const nextDirectives = mergeDirectivesForUpdate(normalizedDirectives, input);
|
|
47
|
+
const rewritten = renderSyncManifest(parsed.scopeRules, nextDirectives);
|
|
48
|
+
await fs.writeFile(input.manifestPath, rewritten, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
decision,
|
|
52
|
+
diff,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseScopePrefixCount(scopePrefix) {
|
|
56
|
+
if (Array.isArray(scopePrefix))
|
|
57
|
+
return scopePrefix.length;
|
|
58
|
+
if (typeof scopePrefix === 'string')
|
|
59
|
+
return scopePrefix.trim() ? 1 : 0;
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
async function fileExists(candidatePath) {
|
|
63
|
+
try {
|
|
64
|
+
await fs.stat(candidatePath);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function normalizePolicy(raw) {
|
|
72
|
+
if (!raw)
|
|
73
|
+
return 'ask';
|
|
74
|
+
if (raw === 'ask' || raw === 'update' || raw === 'keep' || raw === 'error')
|
|
75
|
+
return raw;
|
|
76
|
+
throw new Error(`Invalid --sync-manifest-policy value: ${raw}. Use ask|update|keep|error.`);
|
|
77
|
+
}
|
|
78
|
+
function normalizeManifestDirectives(directives) {
|
|
79
|
+
return {
|
|
80
|
+
extensions: normalizeExtensions(directives.extensions),
|
|
81
|
+
repoAlias: normalizeAlias(directives.repoAlias),
|
|
82
|
+
embeddings: normalizeEmbeddings(directives.embeddings),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function computeDiff(manifest, input) {
|
|
86
|
+
const diff = [];
|
|
87
|
+
if (input.extensions !== undefined) {
|
|
88
|
+
const cliValue = normalizeExtensions(input.extensions);
|
|
89
|
+
if (cliValue !== manifest.extensions) {
|
|
90
|
+
diff.push({ directive: 'extensions', manifestValue: manifest.extensions, cliValue: cliValue || '' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (input.repoAlias !== undefined) {
|
|
94
|
+
const cliValue = normalizeAlias(input.repoAlias);
|
|
95
|
+
if (cliValue !== manifest.repoAlias) {
|
|
96
|
+
diff.push({ directive: 'repoAlias', manifestValue: manifest.repoAlias, cliValue: cliValue || '' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (input.embeddings !== undefined) {
|
|
100
|
+
const cliValue = input.embeddings ? 'true' : 'false';
|
|
101
|
+
if (cliValue !== manifest.embeddings) {
|
|
102
|
+
diff.push({ directive: 'embeddings', manifestValue: manifest.embeddings, cliValue });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return diff;
|
|
106
|
+
}
|
|
107
|
+
async function resolveDecision(policy, manifestPath, diff, stdinIsTTY, prompt) {
|
|
108
|
+
if (policy === 'update' || policy === 'keep')
|
|
109
|
+
return policy;
|
|
110
|
+
if (policy === 'error') {
|
|
111
|
+
throw new Error(`${formatMismatchHeader(manifestPath)}\n${formatDiff(diff)}`);
|
|
112
|
+
}
|
|
113
|
+
if (stdinIsTTY === undefined) {
|
|
114
|
+
throw new Error('TTY prompt branch requires concrete stdin.isTTY evidence.');
|
|
115
|
+
}
|
|
116
|
+
const interactive = stdinIsTTY;
|
|
117
|
+
if (!interactive) {
|
|
118
|
+
throw new Error(`${formatMismatchHeader(manifestPath)}\n${formatDiff(diff)}\n` +
|
|
119
|
+
'Non-interactive mode requires --sync-manifest-policy ask|update|keep|error.');
|
|
120
|
+
}
|
|
121
|
+
const promptFn = prompt || defaultPrompt;
|
|
122
|
+
return promptFn([
|
|
123
|
+
formatMismatchHeader(manifestPath),
|
|
124
|
+
formatDiff(diff),
|
|
125
|
+
'Choose: update (rewrite sync-manifest) or keep (continue without rewrite).',
|
|
126
|
+
].join('\n'));
|
|
127
|
+
}
|
|
128
|
+
function mergeDirectivesForUpdate(manifest, input) {
|
|
129
|
+
const merged = { ...manifest };
|
|
130
|
+
if (input.extensions !== undefined) {
|
|
131
|
+
merged.extensions = normalizeExtensions(input.extensions);
|
|
132
|
+
}
|
|
133
|
+
if (input.repoAlias !== undefined) {
|
|
134
|
+
merged.repoAlias = normalizeAlias(input.repoAlias);
|
|
135
|
+
}
|
|
136
|
+
if (input.embeddings !== undefined) {
|
|
137
|
+
merged.embeddings = input.embeddings ? 'true' : 'false';
|
|
138
|
+
}
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
function renderSyncManifest(scopeRules, directives) {
|
|
142
|
+
const lines = [...scopeRules];
|
|
143
|
+
if (directives.extensions)
|
|
144
|
+
lines.push(`@extensions=${directives.extensions}`);
|
|
145
|
+
if (directives.repoAlias)
|
|
146
|
+
lines.push(`@repoAlias=${directives.repoAlias}`);
|
|
147
|
+
if (directives.embeddings)
|
|
148
|
+
lines.push(`@embeddings=${directives.embeddings}`);
|
|
149
|
+
return `${lines.join('\n')}\n`;
|
|
150
|
+
}
|
|
151
|
+
function normalizeExtensions(raw) {
|
|
152
|
+
if (raw === undefined)
|
|
153
|
+
return undefined;
|
|
154
|
+
const parsed = parseExtensionList(raw);
|
|
155
|
+
return parsed.length > 0 ? parsed.join(',') : undefined;
|
|
156
|
+
}
|
|
157
|
+
function normalizeAlias(raw) {
|
|
158
|
+
if (raw === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
return normalizeRepoAlias(raw);
|
|
161
|
+
}
|
|
162
|
+
function normalizeEmbeddings(raw) {
|
|
163
|
+
if (raw === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
const normalized = raw.trim().toLowerCase();
|
|
166
|
+
if (normalized === 'true')
|
|
167
|
+
return 'true';
|
|
168
|
+
if (normalized === 'false')
|
|
169
|
+
return 'false';
|
|
170
|
+
throw new Error(`Invalid @embeddings directive value: ${raw}. Expected true or false.`);
|
|
171
|
+
}
|
|
172
|
+
function formatMismatchHeader(manifestPath) {
|
|
173
|
+
return `Explicit analyze options differ from sync manifest directives: ${manifestPath}`;
|
|
174
|
+
}
|
|
175
|
+
function formatDiff(diff) {
|
|
176
|
+
return diff
|
|
177
|
+
.map((entry) => `- @${entry.directive}: ${entry.manifestValue ?? '<unset>'} -> ${entry.cliValue}`)
|
|
178
|
+
.join('\n');
|
|
179
|
+
}
|
|
180
|
+
async function defaultPrompt(message) {
|
|
181
|
+
const rl = readline.createInterface({ input, output });
|
|
182
|
+
try {
|
|
183
|
+
const answer = await rl.question(`${message}\nUpdate sync-manifest now? [y/N] `);
|
|
184
|
+
return /^y(es)?$/i.test(answer.trim()) ? 'update' : 'keep';
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
rl.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function ensureConcreteManifestPath(manifestPath) {
|
|
191
|
+
const normalized = manifestPath.trim();
|
|
192
|
+
if (!normalized) {
|
|
193
|
+
throw new Error('Invalid placeholder manifest path: empty value.');
|
|
194
|
+
}
|
|
195
|
+
if (/placeholder/i.test(normalized) ||
|
|
196
|
+
/<\s*path\s*>/i.test(normalized) ||
|
|
197
|
+
/todo/i.test(normalized)) {
|
|
198
|
+
throw new Error(`Invalid placeholder manifest path: ${manifestPath}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|