@veewo/gitnexus 1.3.6 → 1.3.7
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 +18 -10
- package/dist/cli/index.js +3 -2
- package/dist/cli/setup.d.ts +4 -3
- package/dist/cli/setup.js +139 -17
- package/dist/cli/setup.test.js +177 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,11 +24,16 @@ npx gitnexus analyze
|
|
|
24
24
|
|
|
25
25
|
That's it. This indexes the codebase, updates `AGENTS.md` / `CLAUDE.md` context files, and (when using project scope) installs repo-local agent skills.
|
|
26
26
|
|
|
27
|
-
To configure MCP + skills, run `npx gitnexus setup
|
|
27
|
+
To configure MCP + skills, run `npx gitnexus setup --agent <claude|opencode|codex>` once (default global mode), or add `--scope project` for project-local mode.
|
|
28
28
|
|
|
29
|
-
`gitnexus setup`
|
|
30
|
-
-
|
|
31
|
-
-
|
|
29
|
+
`gitnexus setup` requires an agent selection:
|
|
30
|
+
- `--agent claude`: configure Claude MCP only
|
|
31
|
+
- `--agent opencode`: configure OpenCode MCP only
|
|
32
|
+
- `--agent codex`: configure Codex MCP only
|
|
33
|
+
|
|
34
|
+
It also supports two scopes:
|
|
35
|
+
- `global` (default): writes MCP to the selected agent's global config + installs global skills
|
|
36
|
+
- `project`: writes MCP to the selected agent's project-local config + installs repo-local skills
|
|
32
37
|
|
|
33
38
|
## Team Deployment and Distribution
|
|
34
39
|
|
|
@@ -38,6 +43,7 @@ For small-team rollout (single stable channel only), follow:
|
|
|
38
43
|
Key links:
|
|
39
44
|
- [npm publish workflow](../.github/workflows/publish.yml)
|
|
40
45
|
- [CLI package config](./package.json)
|
|
46
|
+
- [Agent install + acceptance runbook](../INSTALL-GUIDE.md)
|
|
41
47
|
|
|
42
48
|
### Editor Support
|
|
43
49
|
|
|
@@ -84,14 +90,14 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
|
|
|
84
90
|
|
|
85
91
|
### OpenCode
|
|
86
92
|
|
|
87
|
-
Add to `~/.config/opencode/
|
|
93
|
+
Add to `~/.config/opencode/opencode.json`:
|
|
88
94
|
|
|
89
95
|
```json
|
|
90
96
|
{
|
|
91
97
|
"mcp": {
|
|
92
98
|
"gitnexus": {
|
|
93
|
-
"
|
|
94
|
-
"
|
|
99
|
+
"type": "local",
|
|
100
|
+
"command": ["npx", "-y", "gitnexus@latest", "mcp"]
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
}
|
|
@@ -154,13 +160,14 @@ Your AI agent gets these tools automatically:
|
|
|
154
160
|
## CLI Commands
|
|
155
161
|
|
|
156
162
|
```bash
|
|
157
|
-
gitnexus setup
|
|
158
|
-
gitnexus setup --
|
|
163
|
+
gitnexus setup --agent claude # Global setup for Claude
|
|
164
|
+
gitnexus setup --agent codex # Global setup for Codex
|
|
165
|
+
gitnexus setup --scope project --agent opencode # Project-local setup for OpenCode
|
|
159
166
|
gitnexus analyze [path] # Index a repository (or update stale index)
|
|
160
167
|
gitnexus analyze --force # Force full re-index
|
|
161
168
|
gitnexus analyze --embeddings # Enable semantic embeddings (off by default)
|
|
162
169
|
gitnexus analyze --scope-prefix Assets/NEON/Code --scope-prefix Packages/com.veewo.* # Scoped multi-directory indexing
|
|
163
|
-
gitnexus analyze --scope-manifest
|
|
170
|
+
gitnexus analyze --scope-manifest .gitnexus/sync-manifest.txt --repo-alias neonspark-v1-subset # Scoped indexing + stable repo alias
|
|
164
171
|
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
|
165
172
|
gitnexus serve # Start local HTTP server (multi-repo) for web UI
|
|
166
173
|
gitnexus list # List all indexed repositories
|
|
@@ -216,6 +223,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
|
|
|
216
223
|
Installation rules:
|
|
217
224
|
|
|
218
225
|
- `gitnexus setup` controls skill scope:
|
|
226
|
+
- requires `--agent <claude|opencode|codex>`
|
|
219
227
|
- default `global`: installs to `~/.agents/skills/gitnexus/`
|
|
220
228
|
- `--scope project`: installs to `.agents/skills/gitnexus/` in current repo
|
|
221
229
|
- `gitnexus analyze` always updates `AGENTS.md` / `CLAUDE.md`; skill install follows configured setup scope.
|
package/dist/cli/index.js
CHANGED
|
@@ -59,8 +59,9 @@ program
|
|
|
59
59
|
.version(resolveCliVersion());
|
|
60
60
|
program
|
|
61
61
|
.command('setup')
|
|
62
|
-
.description('One-time setup: configure MCP for
|
|
62
|
+
.description('One-time setup: configure MCP for a selected coding agent (claude/opencode/codex)')
|
|
63
63
|
.option('--scope <scope>', 'Install target: global (default) or project')
|
|
64
|
+
.option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
|
|
64
65
|
.action(setupCommand);
|
|
65
66
|
program
|
|
66
67
|
.command('analyze [path]')
|
|
@@ -69,7 +70,7 @@ program
|
|
|
69
70
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
70
71
|
.option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
|
|
71
72
|
.option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
|
|
72
|
-
.option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard)')
|
|
73
|
+
.option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard; recommended: .gitnexus/sync-manifest.txt)')
|
|
73
74
|
.option('--scope-prefix <pathPrefix>', 'Add a scope path prefix rule (repeatable)', collectValues, [])
|
|
74
75
|
.action(analyzeCommand);
|
|
75
76
|
program
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup Command
|
|
3
3
|
*
|
|
4
|
-
* One-time
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One-time MCP configuration writer with explicit agent targeting.
|
|
5
|
+
* Configures only the selected coding agent's MCP entry
|
|
6
|
+
* in either global or project scope.
|
|
7
7
|
*/
|
|
8
8
|
interface SetupOptions {
|
|
9
9
|
scope?: string;
|
|
10
|
+
agent?: string;
|
|
10
11
|
}
|
|
11
12
|
export declare const setupCommand: (options?: SetupOptions) => Promise<void>;
|
|
12
13
|
export {};
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup Command
|
|
3
3
|
*
|
|
4
|
-
* One-time
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One-time MCP configuration writer with explicit agent targeting.
|
|
5
|
+
* Configures only the selected coding agent's MCP entry
|
|
6
|
+
* in either global or project scope.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
@@ -23,6 +23,15 @@ function resolveSetupScope(rawScope) {
|
|
|
23
23
|
return rawScope;
|
|
24
24
|
throw new Error(`Invalid --scope value "${rawScope}". Use "global" or "project".`);
|
|
25
25
|
}
|
|
26
|
+
function resolveSetupAgent(rawAgent) {
|
|
27
|
+
if (!rawAgent || rawAgent.trim() === '') {
|
|
28
|
+
throw new Error('Missing --agent. Use one of: claude, opencode, codex.');
|
|
29
|
+
}
|
|
30
|
+
if (rawAgent === 'claude' || rawAgent === 'opencode' || rawAgent === 'codex') {
|
|
31
|
+
return rawAgent;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Invalid --agent value "${rawAgent}". Use "claude", "opencode", or "codex".`);
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
36
|
* The MCP server entry for all editors.
|
|
28
37
|
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
|
|
@@ -39,6 +48,13 @@ function getMcpEntry() {
|
|
|
39
48
|
args: ['-y', 'gitnexus@latest', 'mcp'],
|
|
40
49
|
};
|
|
41
50
|
}
|
|
51
|
+
function getOpenCodeMcpEntry() {
|
|
52
|
+
const entry = getMcpEntry();
|
|
53
|
+
return {
|
|
54
|
+
type: 'local',
|
|
55
|
+
command: [entry.command, ...entry.args],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
42
58
|
/**
|
|
43
59
|
* Merge gitnexus entry into an existing MCP config JSON object.
|
|
44
60
|
* Returns the updated config.
|
|
@@ -53,6 +69,20 @@ function mergeMcpConfig(existing) {
|
|
|
53
69
|
existing.mcpServers.gitnexus = getMcpEntry();
|
|
54
70
|
return existing;
|
|
55
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Merge gitnexus entry into an OpenCode config JSON object.
|
|
74
|
+
* Returns the updated config.
|
|
75
|
+
*/
|
|
76
|
+
function mergeOpenCodeConfig(existing) {
|
|
77
|
+
if (!existing || typeof existing !== 'object') {
|
|
78
|
+
existing = {};
|
|
79
|
+
}
|
|
80
|
+
if (!existing.mcp || typeof existing.mcp !== 'object') {
|
|
81
|
+
existing.mcp = {};
|
|
82
|
+
}
|
|
83
|
+
existing.mcp.gitnexus = getOpenCodeMcpEntry();
|
|
84
|
+
return existing;
|
|
85
|
+
}
|
|
56
86
|
/**
|
|
57
87
|
* Try to read a JSON file, returning null if it doesn't exist or is invalid.
|
|
58
88
|
*/
|
|
@@ -84,6 +114,53 @@ async function dirExists(dirPath) {
|
|
|
84
114
|
return false;
|
|
85
115
|
}
|
|
86
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a regular file exists.
|
|
119
|
+
*/
|
|
120
|
+
async function fileExists(filePath) {
|
|
121
|
+
try {
|
|
122
|
+
const stat = await fs.stat(filePath);
|
|
123
|
+
return stat.isFile();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Escape a value for TOML string literals.
|
|
131
|
+
*/
|
|
132
|
+
function toTomlString(value) {
|
|
133
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
134
|
+
}
|
|
135
|
+
function buildCodexMcpTable() {
|
|
136
|
+
const entry = getMcpEntry();
|
|
137
|
+
return [
|
|
138
|
+
'[mcp_servers.gitnexus]',
|
|
139
|
+
`command = ${toTomlString(entry.command)}`,
|
|
140
|
+
`args = [${entry.args.map(toTomlString).join(', ')}]`,
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
function mergeCodexConfig(existingRaw) {
|
|
144
|
+
const table = buildCodexMcpTable();
|
|
145
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
146
|
+
const tablePattern = /\[mcp_servers\.gitnexus\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
|
|
147
|
+
if (tablePattern.test(normalized)) {
|
|
148
|
+
return normalized.replace(tablePattern, table).trimEnd() + '\n';
|
|
149
|
+
}
|
|
150
|
+
const trimmed = normalized.trimEnd();
|
|
151
|
+
if (trimmed.length === 0)
|
|
152
|
+
return `${table}\n`;
|
|
153
|
+
return `${trimmed}\n\n${table}\n`;
|
|
154
|
+
}
|
|
155
|
+
async function resolveOpenCodeConfigPath(opencodeDir) {
|
|
156
|
+
const preferredPath = path.join(opencodeDir, 'opencode.json');
|
|
157
|
+
const legacyPath = path.join(opencodeDir, 'config.json');
|
|
158
|
+
if (await fileExists(preferredPath))
|
|
159
|
+
return preferredPath;
|
|
160
|
+
if (await fileExists(legacyPath))
|
|
161
|
+
return legacyPath;
|
|
162
|
+
return preferredPath;
|
|
163
|
+
}
|
|
87
164
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
88
165
|
async function setupCursor(result) {
|
|
89
166
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
@@ -204,15 +281,12 @@ async function setupOpenCode(result) {
|
|
|
204
281
|
result.skipped.push('OpenCode (not installed)');
|
|
205
282
|
return;
|
|
206
283
|
}
|
|
207
|
-
const configPath =
|
|
284
|
+
const configPath = await resolveOpenCodeConfigPath(opencodeDir);
|
|
208
285
|
try {
|
|
209
286
|
const existing = await readJsonFile(configPath);
|
|
210
|
-
const config = existing
|
|
211
|
-
if (!config.mcp)
|
|
212
|
-
config.mcp = {};
|
|
213
|
-
config.mcp.gitnexus = getMcpEntry();
|
|
287
|
+
const config = mergeOpenCodeConfig(existing);
|
|
214
288
|
await writeJsonFile(configPath, config);
|
|
215
|
-
result.configured.push(
|
|
289
|
+
result.configured.push(`OpenCode (${path.basename(configPath)})`);
|
|
216
290
|
}
|
|
217
291
|
catch (err) {
|
|
218
292
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
@@ -244,6 +318,38 @@ async function setupProjectMcp(repoRoot, result) {
|
|
|
244
318
|
result.errors.push(`Project MCP: ${err.message}`);
|
|
245
319
|
}
|
|
246
320
|
}
|
|
321
|
+
async function setupProjectCodex(repoRoot, result) {
|
|
322
|
+
const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
|
|
323
|
+
try {
|
|
324
|
+
let existingRaw = '';
|
|
325
|
+
try {
|
|
326
|
+
existingRaw = await fs.readFile(codexConfigPath, 'utf-8');
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
if (err?.code !== 'ENOENT')
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
const merged = mergeCodexConfig(existingRaw);
|
|
333
|
+
await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
|
|
334
|
+
await fs.writeFile(codexConfigPath, merged, 'utf-8');
|
|
335
|
+
result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
result.errors.push(`Project Codex MCP: ${err.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function setupProjectOpenCode(repoRoot, result) {
|
|
342
|
+
const opencodePath = path.join(repoRoot, 'opencode.json');
|
|
343
|
+
try {
|
|
344
|
+
const existing = await readJsonFile(opencodePath);
|
|
345
|
+
const merged = mergeOpenCodeConfig(existing);
|
|
346
|
+
await writeJsonFile(opencodePath, merged);
|
|
347
|
+
result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
result.errors.push(`Project OpenCode MCP: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
247
353
|
async function saveSetupScope(scope, result) {
|
|
248
354
|
try {
|
|
249
355
|
const existing = await loadCLIConfig();
|
|
@@ -321,8 +427,10 @@ export const setupCommand = async (options = {}) => {
|
|
|
321
427
|
console.log(' ==============');
|
|
322
428
|
console.log('');
|
|
323
429
|
let scope;
|
|
430
|
+
let agent;
|
|
324
431
|
try {
|
|
325
432
|
scope = resolveSetupScope(options.scope);
|
|
433
|
+
agent = resolveSetupAgent(options.agent);
|
|
326
434
|
}
|
|
327
435
|
catch (err) {
|
|
328
436
|
console.log(` ${err?.message || String(err)}\n`);
|
|
@@ -338,15 +446,20 @@ export const setupCommand = async (options = {}) => {
|
|
|
338
446
|
errors: [],
|
|
339
447
|
};
|
|
340
448
|
if (scope === 'global') {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
449
|
+
// Configure only the selected agent MCP
|
|
450
|
+
if (agent === 'claude') {
|
|
451
|
+
await setupClaudeCode(result);
|
|
452
|
+
// Claude-only hooks should only be installed when Claude is selected.
|
|
453
|
+
await installClaudeCodeHooks(result);
|
|
454
|
+
}
|
|
455
|
+
else if (agent === 'opencode') {
|
|
456
|
+
await setupOpenCode(result);
|
|
457
|
+
}
|
|
458
|
+
else if (agent === 'codex') {
|
|
459
|
+
await setupCodex(result);
|
|
460
|
+
}
|
|
346
461
|
// Install shared global skills once
|
|
347
462
|
await installGlobalAgentSkills(result);
|
|
348
|
-
// Optional Claude-specific hooks
|
|
349
|
-
await installClaudeCodeHooks(result);
|
|
350
463
|
}
|
|
351
464
|
else {
|
|
352
465
|
const repoRoot = getGitRoot(process.cwd());
|
|
@@ -355,7 +468,15 @@ export const setupCommand = async (options = {}) => {
|
|
|
355
468
|
process.exitCode = 1;
|
|
356
469
|
return;
|
|
357
470
|
}
|
|
358
|
-
|
|
471
|
+
if (agent === 'claude') {
|
|
472
|
+
await setupProjectMcp(repoRoot, result);
|
|
473
|
+
}
|
|
474
|
+
else if (agent === 'codex') {
|
|
475
|
+
await setupProjectCodex(repoRoot, result);
|
|
476
|
+
}
|
|
477
|
+
else if (agent === 'opencode') {
|
|
478
|
+
await setupProjectOpenCode(repoRoot, result);
|
|
479
|
+
}
|
|
359
480
|
await installProjectAgentSkills(repoRoot, result);
|
|
360
481
|
}
|
|
361
482
|
await saveSetupScope(scope, result);
|
|
@@ -383,6 +504,7 @@ export const setupCommand = async (options = {}) => {
|
|
|
383
504
|
console.log('');
|
|
384
505
|
console.log(' Summary:');
|
|
385
506
|
console.log(` Scope: ${scope}`);
|
|
507
|
+
console.log(` Agent: ${agent}`);
|
|
386
508
|
console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
|
|
387
509
|
console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
|
|
388
510
|
console.log('');
|
package/dist/cli/setup.test.js
CHANGED
|
@@ -10,16 +10,56 @@ 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
|
-
|
|
13
|
+
async function runSetup(args, env, cwd = packageRoot) {
|
|
14
|
+
return execFileAsync(process.execPath, [cliPath, 'setup', ...args], { cwd, env });
|
|
15
|
+
}
|
|
16
|
+
test('setup requires --agent', async () => {
|
|
14
17
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
15
18
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
env: {
|
|
19
|
+
try {
|
|
20
|
+
await runSetup([], {
|
|
19
21
|
...process.env,
|
|
20
22
|
HOME: fakeHome,
|
|
21
23
|
USERPROFILE: fakeHome,
|
|
22
|
-
}
|
|
24
|
+
});
|
|
25
|
+
assert.fail('expected setup without --agent to fail');
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
assert.equal(typeof err?.stdout, 'string');
|
|
29
|
+
assert.match(err.stdout, /Missing --agent/);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
test('setup rejects invalid --agent', async () => {
|
|
37
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
await runSetup(['--agent', 'cursor'], {
|
|
41
|
+
...process.env,
|
|
42
|
+
HOME: fakeHome,
|
|
43
|
+
USERPROFILE: fakeHome,
|
|
44
|
+
});
|
|
45
|
+
assert.fail('expected setup with invalid --agent to fail');
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
assert.equal(typeof err?.stdout, 'string');
|
|
49
|
+
assert.match(err.stdout, /Invalid --agent value/);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
test('setup installs global skills under ~/.agents/skills/gitnexus', async () => {
|
|
57
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
58
|
+
try {
|
|
59
|
+
await runSetup(['--agent', 'claude'], {
|
|
60
|
+
...process.env,
|
|
61
|
+
HOME: fakeHome,
|
|
62
|
+
USERPROFILE: fakeHome,
|
|
23
63
|
});
|
|
24
64
|
const skillPath = path.join(fakeHome, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
25
65
|
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
@@ -27,7 +67,6 @@ test('setup installs global skills under ~/.agents/skills/gitnexus', async () =>
|
|
|
27
67
|
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
28
68
|
const config = JSON.parse(configRaw);
|
|
29
69
|
assert.equal(config.setupScope, 'global');
|
|
30
|
-
assert.ok(true);
|
|
31
70
|
}
|
|
32
71
|
finally {
|
|
33
72
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
@@ -63,14 +102,11 @@ process.exit(0);
|
|
|
63
102
|
else {
|
|
64
103
|
await fs.writeFile(codexShimPath, `#!/usr/bin/env node\n${shimLogic}`, { mode: 0o755 });
|
|
65
104
|
}
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
USERPROFILE: fakeHome,
|
|
72
|
-
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
73
|
-
},
|
|
105
|
+
await runSetup(['--agent', 'codex'], {
|
|
106
|
+
...process.env,
|
|
107
|
+
HOME: fakeHome,
|
|
108
|
+
USERPROFILE: fakeHome,
|
|
109
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
74
110
|
});
|
|
75
111
|
const outputPath = path.join(fakeHome, '.codex', 'gitnexus-mcp-add.json');
|
|
76
112
|
const raw = await fs.readFile(outputPath, 'utf-8');
|
|
@@ -84,35 +120,143 @@ process.exit(0);
|
|
|
84
120
|
await fs.rm(fakeBin, { recursive: true, force: true });
|
|
85
121
|
}
|
|
86
122
|
});
|
|
87
|
-
test('setup
|
|
123
|
+
test('setup configures OpenCode MCP in ~/.config/opencode/opencode.json', async () => {
|
|
88
124
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
89
|
-
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
90
125
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
126
|
+
const opencodeDir = path.join(fakeHome, '.config', 'opencode');
|
|
127
|
+
await fs.mkdir(opencodeDir, { recursive: true });
|
|
128
|
+
await runSetup(['--agent', 'opencode'], {
|
|
129
|
+
...process.env,
|
|
130
|
+
HOME: fakeHome,
|
|
131
|
+
USERPROFILE: fakeHome,
|
|
98
132
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
const opencodeConfigPath = path.join(opencodeDir, 'opencode.json');
|
|
134
|
+
const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
|
|
135
|
+
const opencodeConfig = JSON.parse(opencodeRaw);
|
|
136
|
+
assert.equal(opencodeConfig.mcp?.gitnexus?.type, 'local');
|
|
137
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['npx', '-y', 'gitnexus@latest', 'mcp']);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
test('setup keeps using legacy ~/.config/opencode/config.json when it already exists', async () => {
|
|
144
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
145
|
+
try {
|
|
146
|
+
const opencodeDir = path.join(fakeHome, '.config', 'opencode');
|
|
147
|
+
const legacyConfigPath = path.join(opencodeDir, 'config.json');
|
|
148
|
+
const preferredConfigPath = path.join(opencodeDir, 'opencode.json');
|
|
149
|
+
await fs.mkdir(opencodeDir, { recursive: true });
|
|
150
|
+
await fs.writeFile(legacyConfigPath, JSON.stringify({ existing: true }, null, 2), 'utf-8');
|
|
151
|
+
await runSetup(['--agent', 'opencode'], {
|
|
152
|
+
...process.env,
|
|
153
|
+
HOME: fakeHome,
|
|
154
|
+
USERPROFILE: fakeHome,
|
|
106
155
|
});
|
|
156
|
+
const legacyRaw = await fs.readFile(legacyConfigPath, 'utf-8');
|
|
157
|
+
const legacyConfig = JSON.parse(legacyRaw);
|
|
158
|
+
assert.equal(legacyConfig.existing, true);
|
|
159
|
+
assert.equal(legacyConfig.mcp?.gitnexus?.type, 'local');
|
|
160
|
+
assert.deepEqual(legacyConfig.mcp?.gitnexus?.command, ['npx', '-y', 'gitnexus@latest', 'mcp']);
|
|
161
|
+
await assert.rejects(fs.access(preferredConfigPath));
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
test('setup --agent opencode does not install Claude hooks', async () => {
|
|
168
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
169
|
+
try {
|
|
170
|
+
const opencodeDir = path.join(fakeHome, '.config', 'opencode');
|
|
171
|
+
const claudeDir = path.join(fakeHome, '.claude');
|
|
172
|
+
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
173
|
+
const claudeHookPath = path.join(claudeDir, 'hooks', 'gitnexus', 'gitnexus-hook.cjs');
|
|
174
|
+
await fs.mkdir(opencodeDir, { recursive: true });
|
|
175
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
176
|
+
await runSetup(['--agent', 'opencode'], {
|
|
177
|
+
...process.env,
|
|
178
|
+
HOME: fakeHome,
|
|
179
|
+
USERPROFILE: fakeHome,
|
|
180
|
+
});
|
|
181
|
+
await assert.rejects(fs.access(claudeSettingsPath));
|
|
182
|
+
await assert.rejects(fs.access(claudeHookPath));
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
test('setup --scope project --agent claude writes only .mcp.json', async () => {
|
|
189
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
190
|
+
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
191
|
+
try {
|
|
192
|
+
await execFileAsync('git', ['init'], { cwd: fakeRepo });
|
|
193
|
+
await runSetup(['--scope', 'project', '--agent', 'claude'], {
|
|
194
|
+
...process.env,
|
|
195
|
+
HOME: fakeHome,
|
|
196
|
+
USERPROFILE: fakeHome,
|
|
197
|
+
}, fakeRepo);
|
|
107
198
|
const projectMcpPath = path.join(fakeRepo, '.mcp.json');
|
|
199
|
+
const codexConfigPath = path.join(fakeRepo, '.codex', 'config.toml');
|
|
200
|
+
const opencodeConfigPath = path.join(fakeRepo, 'opencode.json');
|
|
201
|
+
const projectMcpRaw = await fs.readFile(projectMcpPath, 'utf-8');
|
|
202
|
+
const projectMcp = JSON.parse(projectMcpRaw);
|
|
203
|
+
assert.equal(projectMcp.mcpServers?.gitnexus?.command, 'npx');
|
|
204
|
+
await assert.rejects(fs.access(codexConfigPath));
|
|
205
|
+
await assert.rejects(fs.access(opencodeConfigPath));
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
209
|
+
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
test('setup --scope project --agent codex writes only .codex/config.toml', async () => {
|
|
213
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
214
|
+
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
215
|
+
try {
|
|
216
|
+
await execFileAsync('git', ['init'], { cwd: fakeRepo });
|
|
217
|
+
await runSetup(['--scope', 'project', '--agent', 'codex'], {
|
|
218
|
+
...process.env,
|
|
219
|
+
HOME: fakeHome,
|
|
220
|
+
USERPROFILE: fakeHome,
|
|
221
|
+
}, fakeRepo);
|
|
222
|
+
const projectMcpPath = path.join(fakeRepo, '.mcp.json');
|
|
223
|
+
const codexConfigPath = path.join(fakeRepo, '.codex', 'config.toml');
|
|
224
|
+
const opencodeConfigPath = path.join(fakeRepo, 'opencode.json');
|
|
225
|
+
const codexConfigRaw = await fs.readFile(codexConfigPath, 'utf-8');
|
|
226
|
+
assert.match(codexConfigRaw, /\[mcp_servers\.gitnexus\]/);
|
|
227
|
+
assert.match(codexConfigRaw, /command = "npx"/);
|
|
228
|
+
await assert.rejects(fs.access(projectMcpPath));
|
|
229
|
+
await assert.rejects(fs.access(opencodeConfigPath));
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
233
|
+
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
test('setup --scope project --agent opencode writes only opencode.json', async () => {
|
|
237
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
238
|
+
const fakeRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-repo-'));
|
|
239
|
+
try {
|
|
240
|
+
await execFileAsync('git', ['init'], { cwd: fakeRepo });
|
|
241
|
+
await runSetup(['--scope', 'project', '--agent', 'opencode'], {
|
|
242
|
+
...process.env,
|
|
243
|
+
HOME: fakeHome,
|
|
244
|
+
USERPROFILE: fakeHome,
|
|
245
|
+
}, fakeRepo);
|
|
246
|
+
const projectMcpPath = path.join(fakeRepo, '.mcp.json');
|
|
247
|
+
const codexConfigPath = path.join(fakeRepo, '.codex', 'config.toml');
|
|
248
|
+
const opencodeConfigPath = path.join(fakeRepo, 'opencode.json');
|
|
108
249
|
const localSkillPath = path.join(fakeRepo, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
109
250
|
const globalSkillPath = path.join(fakeHome, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
110
251
|
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
111
|
-
const
|
|
112
|
-
const
|
|
252
|
+
const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
|
|
253
|
+
const opencodeConfig = JSON.parse(opencodeRaw);
|
|
113
254
|
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
114
255
|
const config = JSON.parse(configRaw);
|
|
115
|
-
assert.equal(
|
|
256
|
+
assert.equal(opencodeConfig.mcp?.gitnexus?.type, 'local');
|
|
257
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['npx', '-y', 'gitnexus@latest', 'mcp']);
|
|
258
|
+
await assert.rejects(fs.access(projectMcpPath));
|
|
259
|
+
await assert.rejects(fs.access(codexConfigPath));
|
|
116
260
|
await fs.access(localSkillPath);
|
|
117
261
|
await assert.rejects(fs.access(globalSkillPath));
|
|
118
262
|
assert.equal(config.setupScope, 'project');
|
package/package.json
CHANGED