@veewo/gitnexus 1.3.5 → 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 +23 -11
- package/dist/cli/ai-context.d.ts +4 -1
- package/dist/cli/ai-context.js +19 -13
- package/dist/cli/ai-context.test.js +23 -1
- package/dist/cli/analyze.js +3 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/setup.d.ts +9 -4
- package/dist/cli/setup.js +207 -20
- package/dist/cli/setup.test.js +200 -14
- package/dist/storage/repo-manager.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,11 +22,18 @@ AI coding tools don't understand your codebase structure. They edit a function w
|
|
|
22
22
|
npx gitnexus analyze
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
That's it. This indexes the codebase,
|
|
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
|
|
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`
|
|
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
|
|
30
37
|
|
|
31
38
|
## Team Deployment and Distribution
|
|
32
39
|
|
|
@@ -36,6 +43,7 @@ For small-team rollout (single stable channel only), follow:
|
|
|
36
43
|
Key links:
|
|
37
44
|
- [npm publish workflow](../.github/workflows/publish.yml)
|
|
38
45
|
- [CLI package config](./package.json)
|
|
46
|
+
- [Agent install + acceptance runbook](../INSTALL-GUIDE.md)
|
|
39
47
|
|
|
40
48
|
### Editor Support
|
|
41
49
|
|
|
@@ -82,14 +90,14 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
|
|
|
82
90
|
|
|
83
91
|
### OpenCode
|
|
84
92
|
|
|
85
|
-
Add to `~/.config/opencode/
|
|
93
|
+
Add to `~/.config/opencode/opencode.json`:
|
|
86
94
|
|
|
87
95
|
```json
|
|
88
96
|
{
|
|
89
97
|
"mcp": {
|
|
90
98
|
"gitnexus": {
|
|
91
|
-
"
|
|
92
|
-
"
|
|
99
|
+
"type": "local",
|
|
100
|
+
"command": ["npx", "-y", "gitnexus@latest", "mcp"]
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
}
|
|
@@ -152,12 +160,14 @@ Your AI agent gets these tools automatically:
|
|
|
152
160
|
## CLI Commands
|
|
153
161
|
|
|
154
162
|
```bash
|
|
155
|
-
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
|
|
156
166
|
gitnexus analyze [path] # Index a repository (or update stale index)
|
|
157
167
|
gitnexus analyze --force # Force full re-index
|
|
158
168
|
gitnexus analyze --embeddings # Enable semantic embeddings (off by default)
|
|
159
169
|
gitnexus analyze --scope-prefix Assets/NEON/Code --scope-prefix Packages/com.veewo.* # Scoped multi-directory indexing
|
|
160
|
-
gitnexus analyze --scope-manifest
|
|
170
|
+
gitnexus analyze --scope-manifest .gitnexus/sync-manifest.txt --repo-alias neonspark-v1-subset # Scoped indexing + stable repo alias
|
|
161
171
|
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
|
162
172
|
gitnexus serve # Start local HTTP server (multi-repo) for web UI
|
|
163
173
|
gitnexus list # List all indexed repositories
|
|
@@ -212,9 +222,11 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
|
|
|
212
222
|
|
|
213
223
|
Installation rules:
|
|
214
224
|
|
|
215
|
-
- `gitnexus
|
|
216
|
-
-
|
|
217
|
-
-
|
|
225
|
+
- `gitnexus setup` controls skill scope:
|
|
226
|
+
- requires `--agent <claude|opencode|codex>`
|
|
227
|
+
- default `global`: installs to `~/.agents/skills/gitnexus/`
|
|
228
|
+
- `--scope project`: installs to `.agents/skills/gitnexus/` in current repo
|
|
229
|
+
- `gitnexus analyze` always updates `AGENTS.md` / `CLAUDE.md`; skill install follows configured setup scope.
|
|
218
230
|
|
|
219
231
|
## Requirements
|
|
220
232
|
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
|
|
6
6
|
* CLAUDE.md is for Claude Code which only reads that file.
|
|
7
7
|
*/
|
|
8
|
+
type SkillScope = 'project' | 'global';
|
|
8
9
|
interface RepoStats {
|
|
9
10
|
files?: number;
|
|
10
11
|
nodes?: number;
|
|
@@ -16,7 +17,9 @@ interface RepoStats {
|
|
|
16
17
|
/**
|
|
17
18
|
* Generate AI context files after indexing
|
|
18
19
|
*/
|
|
19
|
-
export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats
|
|
20
|
+
export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, options?: {
|
|
21
|
+
skillScope?: SkillScope;
|
|
22
|
+
}): Promise<{
|
|
20
23
|
files: string[];
|
|
21
24
|
}>;
|
|
22
25
|
export {};
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -23,7 +23,10 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
|
23
23
|
* - One-line quick start (read context resource) gives agents an entry point
|
|
24
24
|
* - Tools/Resources sections are labeled "Reference" — agents treat them as lookup, not workflow
|
|
25
25
|
*/
|
|
26
|
-
function generateGitNexusContent(projectName, stats) {
|
|
26
|
+
function generateGitNexusContent(projectName, stats, skillScope) {
|
|
27
|
+
const skillRoot = skillScope === 'global'
|
|
28
|
+
? '~/.agents/skills/gitnexus'
|
|
29
|
+
: '.agents/skills/gitnexus';
|
|
27
30
|
return `${GITNEXUS_START_MARKER}
|
|
28
31
|
# GitNexus MCP
|
|
29
32
|
|
|
@@ -41,12 +44,12 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
|
|
|
41
44
|
|
|
42
45
|
| Task | Read this skill file |
|
|
43
46
|
|------|---------------------|
|
|
44
|
-
| Understand architecture / "How does X work?" |
|
|
45
|
-
| Blast radius / "What breaks if I change X?" |
|
|
46
|
-
| Trace bugs / "Why is X failing?" |
|
|
47
|
-
| Rename / extract / split / refactor |
|
|
48
|
-
| Tools, resources, schema reference |
|
|
49
|
-
| Index, status, clean, wiki CLI commands |
|
|
47
|
+
| Understand architecture / "How does X work?" | \`${skillRoot}/gitnexus-exploring/SKILL.md\` |
|
|
48
|
+
| Blast radius / "What breaks if I change X?" | \`${skillRoot}/gitnexus-impact-analysis/SKILL.md\` |
|
|
49
|
+
| Trace bugs / "Why is X failing?" | \`${skillRoot}/gitnexus-debugging/SKILL.md\` |
|
|
50
|
+
| Rename / extract / split / refactor | \`${skillRoot}/gitnexus-refactoring/SKILL.md\` |
|
|
51
|
+
| Tools, resources, schema reference | \`${skillRoot}/gitnexus-guide/SKILL.md\` |
|
|
52
|
+
| Index, status, clean, wiki CLI commands | \`${skillRoot}/gitnexus-cli/SKILL.md\` |
|
|
50
53
|
|
|
51
54
|
${GITNEXUS_END_MARKER}`;
|
|
52
55
|
}
|
|
@@ -164,8 +167,9 @@ Use GitNexus tools to accomplish this task.
|
|
|
164
167
|
/**
|
|
165
168
|
* Generate AI context files after indexing
|
|
166
169
|
*/
|
|
167
|
-
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats) {
|
|
168
|
-
const
|
|
170
|
+
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, options) {
|
|
171
|
+
const skillScope = options?.skillScope === 'global' ? 'global' : 'project';
|
|
172
|
+
const content = generateGitNexusContent(projectName, stats, skillScope);
|
|
169
173
|
const createdFiles = [];
|
|
170
174
|
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Codex, Cline, etc.)
|
|
171
175
|
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
|
@@ -175,10 +179,12 @@ export async function generateAIContextFiles(repoPath, _storagePath, projectName
|
|
|
175
179
|
const claudePath = path.join(repoPath, 'CLAUDE.md');
|
|
176
180
|
const claudeResult = await upsertGitNexusSection(claudePath, content);
|
|
177
181
|
createdFiles.push(`CLAUDE.md (${claudeResult})`);
|
|
178
|
-
// Install skills
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
// Install repo-local skills only when project scope is selected.
|
|
183
|
+
if (skillScope === 'project') {
|
|
184
|
+
const installedSkills = await installSkills(repoPath);
|
|
185
|
+
if (installedSkills.length > 0) {
|
|
186
|
+
createdFiles.push(`.agents/skills/gitnexus/ (${installedSkills.length} skills)`);
|
|
187
|
+
}
|
|
182
188
|
}
|
|
183
189
|
return { files: createdFiles };
|
|
184
190
|
}
|
|
@@ -11,7 +11,7 @@ test('generateAIContextFiles installs repo skills under .agents/skills/gitnexus'
|
|
|
11
11
|
nodes: 1,
|
|
12
12
|
edges: 2,
|
|
13
13
|
processes: 3,
|
|
14
|
-
});
|
|
14
|
+
}, { skillScope: 'project' });
|
|
15
15
|
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
|
16
16
|
const claudePath = path.join(repoPath, 'CLAUDE.md');
|
|
17
17
|
const skillPath = path.join(repoPath, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
@@ -28,3 +28,25 @@ test('generateAIContextFiles installs repo skills under .agents/skills/gitnexus'
|
|
|
28
28
|
await fs.rm(repoPath, { recursive: true, force: true });
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
|
+
test('generateAIContextFiles with global scope skips repo skill install', async () => {
|
|
32
|
+
const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ai-context-global-'));
|
|
33
|
+
try {
|
|
34
|
+
const result = await generateAIContextFiles(repoPath, '', 'demo-repo', {
|
|
35
|
+
nodes: 1,
|
|
36
|
+
edges: 2,
|
|
37
|
+
processes: 3,
|
|
38
|
+
}, { skillScope: 'global' });
|
|
39
|
+
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
|
40
|
+
const claudePath = path.join(repoPath, 'CLAUDE.md');
|
|
41
|
+
const localSkillsDir = path.join(repoPath, '.agents', 'skills', 'gitnexus');
|
|
42
|
+
const agentsContent = await fs.readFile(agentsPath, 'utf-8');
|
|
43
|
+
const claudeContent = await fs.readFile(claudePath, 'utf-8');
|
|
44
|
+
assert.match(agentsContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
|
|
45
|
+
assert.match(claudeContent, /~\/\.agents\/skills\/gitnexus\/gitnexus-exploring\/SKILL\.md/);
|
|
46
|
+
assert.ok(!result.files.some((entry) => entry.includes('.agents/skills/gitnexus/')), 'did not expect repo-local skills in generated file summary');
|
|
47
|
+
await assert.rejects(fs.access(localSkillsDir));
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await fs.rm(repoPath, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
package/dist/cli/analyze.js
CHANGED
|
@@ -13,7 +13,7 @@ import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReuse
|
|
|
13
13
|
// loaded when embeddings are not requested. This avoids crashes on Node
|
|
14
14
|
// versions whose ABI is not yet supported by the native binary (#89).
|
|
15
15
|
// disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
|
|
16
|
-
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
|
|
16
|
+
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, loadCLIConfig } from '../storage/repo-manager.js';
|
|
17
17
|
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
18
18
|
import { generateAIContextFiles } from './ai-context.js';
|
|
19
19
|
import fs from 'fs/promises';
|
|
@@ -312,6 +312,8 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
312
312
|
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
313
313
|
clusters: aggregatedClusterCount,
|
|
314
314
|
processes: pipelineResult.processResult?.stats.totalProcesses,
|
|
315
|
+
}, {
|
|
316
|
+
skillScope: ((await loadCLIConfig()).setupScope === 'global') ? 'global' : 'project',
|
|
315
317
|
});
|
|
316
318
|
await closeKuzu();
|
|
317
319
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
package/dist/cli/index.js
CHANGED
|
@@ -59,7 +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
|
+
.option('--scope <scope>', 'Install target: global (default) or project')
|
|
64
|
+
.option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
|
|
63
65
|
.action(setupCommand);
|
|
64
66
|
program
|
|
65
67
|
.command('analyze [path]')
|
|
@@ -68,7 +70,7 @@ program
|
|
|
68
70
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
69
71
|
.option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
|
|
70
72
|
.option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
|
|
71
|
-
.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)')
|
|
72
74
|
.option('--scope-prefix <pathPrefix>', 'Add a scope path prefix rule (repeatable)', collectValues, [])
|
|
73
75
|
.action(analyzeCommand);
|
|
74
76
|
program
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -1,8 +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
|
+
scope?: string;
|
|
10
|
+
agent?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const setupCommand: (options?: SetupOptions) => Promise<void>;
|
|
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';
|
|
@@ -11,10 +11,27 @@ import os from 'os';
|
|
|
11
11
|
import { execFile } from 'node:child_process';
|
|
12
12
|
import { promisify } from 'node:util';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import { getGlobalDir } from '../storage/repo-manager.js';
|
|
14
|
+
import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
|
|
15
|
+
import { getGitRoot } from '../storage/git.js';
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
function resolveSetupScope(rawScope) {
|
|
20
|
+
if (!rawScope || rawScope.trim() === '')
|
|
21
|
+
return 'global';
|
|
22
|
+
if (rawScope === 'global' || rawScope === 'project')
|
|
23
|
+
return rawScope;
|
|
24
|
+
throw new Error(`Invalid --scope value "${rawScope}". Use "global" or "project".`);
|
|
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
|
+
}
|
|
18
35
|
/**
|
|
19
36
|
* The MCP server entry for all editors.
|
|
20
37
|
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
|
|
@@ -31,6 +48,13 @@ function getMcpEntry() {
|
|
|
31
48
|
args: ['-y', 'gitnexus@latest', 'mcp'],
|
|
32
49
|
};
|
|
33
50
|
}
|
|
51
|
+
function getOpenCodeMcpEntry() {
|
|
52
|
+
const entry = getMcpEntry();
|
|
53
|
+
return {
|
|
54
|
+
type: 'local',
|
|
55
|
+
command: [entry.command, ...entry.args],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* Merge gitnexus entry into an existing MCP config JSON object.
|
|
36
60
|
* Returns the updated config.
|
|
@@ -45,6 +69,20 @@ function mergeMcpConfig(existing) {
|
|
|
45
69
|
existing.mcpServers.gitnexus = getMcpEntry();
|
|
46
70
|
return existing;
|
|
47
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
|
+
}
|
|
48
86
|
/**
|
|
49
87
|
* Try to read a JSON file, returning null if it doesn't exist or is invalid.
|
|
50
88
|
*/
|
|
@@ -76,6 +114,53 @@ async function dirExists(dirPath) {
|
|
|
76
114
|
return false;
|
|
77
115
|
}
|
|
78
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
|
+
}
|
|
79
164
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
80
165
|
async function setupCursor(result) {
|
|
81
166
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
@@ -125,6 +210,18 @@ async function installGlobalAgentSkills(result) {
|
|
|
125
210
|
result.errors.push(`Global agent skills: ${err.message}`);
|
|
126
211
|
}
|
|
127
212
|
}
|
|
213
|
+
async function installProjectAgentSkills(repoRoot, result) {
|
|
214
|
+
const skillsDir = path.join(repoRoot, '.agents', 'skills', 'gitnexus');
|
|
215
|
+
try {
|
|
216
|
+
const installed = await installSkillsTo(skillsDir);
|
|
217
|
+
if (installed.length > 0) {
|
|
218
|
+
result.configured.push(`Project agent skills (${installed.length} skills → ${path.relative(repoRoot, skillsDir)}/)`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
result.errors.push(`Project agent skills: ${err.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
128
225
|
/**
|
|
129
226
|
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
130
227
|
* Merges hook config without overwriting existing hooks.
|
|
@@ -184,15 +281,12 @@ async function setupOpenCode(result) {
|
|
|
184
281
|
result.skipped.push('OpenCode (not installed)');
|
|
185
282
|
return;
|
|
186
283
|
}
|
|
187
|
-
const configPath =
|
|
284
|
+
const configPath = await resolveOpenCodeConfigPath(opencodeDir);
|
|
188
285
|
try {
|
|
189
286
|
const existing = await readJsonFile(configPath);
|
|
190
|
-
const config = existing
|
|
191
|
-
if (!config.mcp)
|
|
192
|
-
config.mcp = {};
|
|
193
|
-
config.mcp.gitnexus = getMcpEntry();
|
|
287
|
+
const config = mergeOpenCodeConfig(existing);
|
|
194
288
|
await writeJsonFile(configPath, config);
|
|
195
|
-
result.configured.push(
|
|
289
|
+
result.configured.push(`OpenCode (${path.basename(configPath)})`);
|
|
196
290
|
}
|
|
197
291
|
catch (err) {
|
|
198
292
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
@@ -212,6 +306,60 @@ async function setupCodex(result) {
|
|
|
212
306
|
result.errors.push(`Codex: ${err.message}`);
|
|
213
307
|
}
|
|
214
308
|
}
|
|
309
|
+
async function setupProjectMcp(repoRoot, result) {
|
|
310
|
+
const mcpPath = path.join(repoRoot, '.mcp.json');
|
|
311
|
+
try {
|
|
312
|
+
const existing = await readJsonFile(mcpPath);
|
|
313
|
+
const updated = mergeMcpConfig(existing);
|
|
314
|
+
await writeJsonFile(mcpPath, updated);
|
|
315
|
+
result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
result.errors.push(`Project MCP: ${err.message}`);
|
|
319
|
+
}
|
|
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
|
+
}
|
|
353
|
+
async function saveSetupScope(scope, result) {
|
|
354
|
+
try {
|
|
355
|
+
const existing = await loadCLIConfig();
|
|
356
|
+
await saveCLIConfig({ ...existing, setupScope: scope });
|
|
357
|
+
result.configured.push(`Default setup scope (${scope})`);
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
result.errors.push(`Persist setup scope: ${err.message}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
215
363
|
// ─── Skill Installation ───────────────────────────────────────────
|
|
216
364
|
const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impact-analysis', 'gitnexus-refactoring', 'gitnexus-guide', 'gitnexus-cli'];
|
|
217
365
|
/**
|
|
@@ -273,11 +421,22 @@ async function copyDirRecursive(src, dest) {
|
|
|
273
421
|
}
|
|
274
422
|
}
|
|
275
423
|
// ─── Main command ──────────────────────────────────────────────────
|
|
276
|
-
export const setupCommand = async () => {
|
|
424
|
+
export const setupCommand = async (options = {}) => {
|
|
277
425
|
console.log('');
|
|
278
426
|
console.log(' GitNexus Setup');
|
|
279
427
|
console.log(' ==============');
|
|
280
428
|
console.log('');
|
|
429
|
+
let scope;
|
|
430
|
+
let agent;
|
|
431
|
+
try {
|
|
432
|
+
scope = resolveSetupScope(options.scope);
|
|
433
|
+
agent = resolveSetupAgent(options.agent);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
console.log(` ${err?.message || String(err)}\n`);
|
|
437
|
+
process.exitCode = 1;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
281
440
|
// Ensure global directory exists
|
|
282
441
|
const globalDir = getGlobalDir();
|
|
283
442
|
await fs.mkdir(globalDir, { recursive: true });
|
|
@@ -286,15 +445,41 @@ export const setupCommand = async () => {
|
|
|
286
445
|
skipped: [],
|
|
287
446
|
errors: [],
|
|
288
447
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
448
|
+
if (scope === 'global') {
|
|
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
|
+
}
|
|
461
|
+
// Install shared global skills once
|
|
462
|
+
await installGlobalAgentSkills(result);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const repoRoot = getGitRoot(process.cwd());
|
|
466
|
+
if (!repoRoot) {
|
|
467
|
+
console.log(' --scope project requires running inside a git repository\n');
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
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
|
+
}
|
|
480
|
+
await installProjectAgentSkills(repoRoot, result);
|
|
481
|
+
}
|
|
482
|
+
await saveSetupScope(scope, result);
|
|
298
483
|
// Print results
|
|
299
484
|
if (result.configured.length > 0) {
|
|
300
485
|
console.log(' Configured:');
|
|
@@ -318,6 +503,8 @@ export const setupCommand = async () => {
|
|
|
318
503
|
}
|
|
319
504
|
console.log('');
|
|
320
505
|
console.log(' Summary:');
|
|
506
|
+
console.log(` Scope: ${scope}`);
|
|
507
|
+
console.log(` Agent: ${agent}`);
|
|
321
508
|
console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
|
|
322
509
|
console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
|
|
323
510
|
console.log('');
|
package/dist/cli/setup.test.js
CHANGED
|
@@ -10,20 +10,63 @@ 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 () => {
|
|
17
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
18
|
+
try {
|
|
19
|
+
try {
|
|
20
|
+
await runSetup([], {
|
|
21
|
+
...process.env,
|
|
22
|
+
HOME: fakeHome,
|
|
23
|
+
USERPROFILE: fakeHome,
|
|
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 () => {
|
|
14
37
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
15
38
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
env: {
|
|
39
|
+
try {
|
|
40
|
+
await runSetup(['--agent', 'cursor'], {
|
|
19
41
|
...process.env,
|
|
20
42
|
HOME: fakeHome,
|
|
21
43
|
USERPROFILE: fakeHome,
|
|
22
|
-
}
|
|
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');
|
|
65
|
+
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
25
66
|
await fs.access(skillPath);
|
|
26
|
-
|
|
67
|
+
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
68
|
+
const config = JSON.parse(configRaw);
|
|
69
|
+
assert.equal(config.setupScope, 'global');
|
|
27
70
|
}
|
|
28
71
|
finally {
|
|
29
72
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
@@ -59,14 +102,11 @@ process.exit(0);
|
|
|
59
102
|
else {
|
|
60
103
|
await fs.writeFile(codexShimPath, `#!/usr/bin/env node\n${shimLogic}`, { mode: 0o755 });
|
|
61
104
|
}
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
USERPROFILE: fakeHome,
|
|
68
|
-
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
69
|
-
},
|
|
105
|
+
await runSetup(['--agent', 'codex'], {
|
|
106
|
+
...process.env,
|
|
107
|
+
HOME: fakeHome,
|
|
108
|
+
USERPROFILE: fakeHome,
|
|
109
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
70
110
|
});
|
|
71
111
|
const outputPath = path.join(fakeHome, '.codex', 'gitnexus-mcp-add.json');
|
|
72
112
|
const raw = await fs.readFile(outputPath, 'utf-8');
|
|
@@ -80,3 +120,149 @@ process.exit(0);
|
|
|
80
120
|
await fs.rm(fakeBin, { recursive: true, force: true });
|
|
81
121
|
}
|
|
82
122
|
});
|
|
123
|
+
test('setup configures OpenCode MCP in ~/.config/opencode/opencode.json', async () => {
|
|
124
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
125
|
+
try {
|
|
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,
|
|
132
|
+
});
|
|
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,
|
|
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);
|
|
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');
|
|
249
|
+
const localSkillPath = path.join(fakeRepo, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
250
|
+
const globalSkillPath = path.join(fakeHome, '.agents', 'skills', 'gitnexus', 'gitnexus-exploring', 'SKILL.md');
|
|
251
|
+
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
252
|
+
const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
|
|
253
|
+
const opencodeConfig = JSON.parse(opencodeRaw);
|
|
254
|
+
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
255
|
+
const config = JSON.parse(configRaw);
|
|
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));
|
|
260
|
+
await fs.access(localSkillPath);
|
|
261
|
+
await assert.rejects(fs.access(globalSkillPath));
|
|
262
|
+
assert.equal(config.setupScope, 'project');
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
266
|
+
await fs.rm(fakeRepo, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
});
|
package/package.json
CHANGED