@veewo/gitnexus 1.4.6-rc → 1.4.8-rc
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 +13 -9
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +6 -3
- package/dist/cli/analyze.js +16 -2
- package/dist/cli/index.js +2 -0
- package/dist/cli/setup.d.ts +2 -0
- package/dist/cli/setup.js +74 -62
- package/dist/cli/setup.test.js +44 -0
- package/dist/cli/status.js +8 -1
- package/dist/config/cli-spec.d.ts +29 -0
- package/dist/config/cli-spec.js +87 -0
- package/dist/mcp/resources.js +11 -2
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/storage/repo-manager.js +13 -0
- package/hooks/claude/gitnexus-hook.cjs +44 -6
- package/hooks/claude/pre-tool-use.sh +5 -1
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +19 -8
- package/skills/gitnexus-debugging.md +1 -1
- package/skills/gitnexus-exploring.md +1 -1
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-impact-analysis.md +1 -1
- package/skills/gitnexus-pr-review.md +1 -1
- package/skills/gitnexus-refactoring.md +1 -1
package/README.md
CHANGED
|
@@ -18,13 +18,17 @@ AI coding tools don't understand your codebase structure. They edit a function w
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
+
# Optional: pin one version for the whole session (example RC)
|
|
22
|
+
# export GITNEXUS_CLI_SPEC="@veewo/gitnexus@1.4.7-rc"
|
|
23
|
+
GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
|
|
24
|
+
|
|
21
25
|
# Index your repo (run from repo root)
|
|
22
|
-
npx -y
|
|
26
|
+
npx -y "${GITNEXUS_CLI_SPEC}" analyze
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command.
|
|
26
30
|
|
|
27
|
-
To configure MCP for your editor, run `npx -y
|
|
31
|
+
To configure MCP for your editor, run `npx -y "${GITNEXUS_CLI_SPEC}" setup --cli-spec "$GITNEXUS_CLI_SPEC"` once — or set it up manually below.
|
|
28
32
|
|
|
29
33
|
`gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
|
|
30
34
|
|
|
@@ -52,7 +56,7 @@ If you prefer to configure manually instead of using `gitnexus setup`:
|
|
|
52
56
|
### Claude Code (full support — MCP + skills + hooks)
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
|
-
claude mcp add gitnexus -- npx -y
|
|
59
|
+
claude mcp add gitnexus -- npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" mcp
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
### Cursor / Windsurf
|
|
@@ -137,10 +141,10 @@ Your AI agent gets these tools automatically:
|
|
|
137
141
|
|
|
138
142
|
```bash
|
|
139
143
|
gitnexus setup # Configure MCP for your editors (one-time)
|
|
140
|
-
npx -y
|
|
141
|
-
npx -y
|
|
142
|
-
npx -y
|
|
143
|
-
npx -y
|
|
144
|
+
npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze [path] # Index a repository (or update stale index)
|
|
145
|
+
npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --force # Force full re-index
|
|
146
|
+
npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --embeddings # Enable embedding generation (slower, better search)
|
|
147
|
+
npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze --verbose # Log skipped files when parsers are unavailable
|
|
144
148
|
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
|
145
149
|
gitnexus serve # Start local HTTP server (multi-repo) for web UI
|
|
146
150
|
gitnexus list # List all indexed repositories
|
|
@@ -153,7 +157,7 @@ gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-m
|
|
|
153
157
|
|
|
154
158
|
## Multi-Repo Support
|
|
155
159
|
|
|
156
|
-
GitNexus supports indexing multiple repositories. Each `npx -y
|
|
160
|
+
GitNexus supports indexing multiple repositories. Each `npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically.
|
|
157
161
|
|
|
158
162
|
## Supported Languages
|
|
159
163
|
|
|
@@ -188,7 +192,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
|
|
|
188
192
|
- **Impact Analysis** — Analyze blast radius before changes
|
|
189
193
|
- **Refactoring** — Plan safe refactors using dependency mapping
|
|
190
194
|
|
|
191
|
-
Installed automatically by both `npx -y
|
|
195
|
+
Installed automatically by both `npx -y "${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}" analyze` (per-repo) and `gitnexus setup` (global).
|
|
192
196
|
|
|
193
197
|
## Requirements
|
|
194
198
|
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ interface RepoStats {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, options?: {
|
|
22
22
|
skillScope?: SkillScope;
|
|
23
|
+
cliPackageSpec?: string;
|
|
23
24
|
}, generatedSkills?: GeneratedSkillInfo[]): Promise<{
|
|
24
25
|
files: string[];
|
|
25
26
|
}>;
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
+
import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
|
|
11
12
|
// ESM equivalent of __dirname
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
@@ -24,13 +25,14 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
|
24
25
|
* - Exact tool commands with parameters — vague directives get ignored
|
|
25
26
|
* - Self-review checklist — forces model to verify its own work
|
|
26
27
|
*/
|
|
27
|
-
function generateGitNexusContent(projectName, stats, skillScope, generatedSkills) {
|
|
28
|
+
function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec, generatedSkills) {
|
|
28
29
|
const skillRoot = skillScope === 'global'
|
|
29
30
|
? '~/.agents/skills/gitnexus'
|
|
30
31
|
: '.agents/skills/gitnexus';
|
|
31
32
|
const generatedRows = (generatedSkills && generatedSkills.length > 0)
|
|
32
33
|
? `\n${generatedSkills.map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`).join('\n')}`
|
|
33
34
|
: '';
|
|
35
|
+
const reindexCmd = buildNpxCommand(cliPackageSpec, 'analyze');
|
|
34
36
|
return `${GITNEXUS_START_MARKER}
|
|
35
37
|
# GitNexus MCP
|
|
36
38
|
|
|
@@ -42,7 +44,7 @@ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} s
|
|
|
42
44
|
2. **Match your task to a skill below** and **read that skill file**
|
|
43
45
|
3. **Follow the skill's workflow and checklist**
|
|
44
46
|
|
|
45
|
-
> If step 1 warns the index is stale, ask user whether to rebuild index via \`
|
|
47
|
+
> If step 1 warns the index is stale, ask user whether to rebuild index via \`gitnexus analyze\` when local CLI exists; otherwise run \`${reindexCmd}\` (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
|
|
46
48
|
|
|
47
49
|
## Skills
|
|
48
50
|
|
|
@@ -173,7 +175,8 @@ Use GitNexus tools to accomplish this task.
|
|
|
173
175
|
*/
|
|
174
176
|
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, options, generatedSkills) {
|
|
175
177
|
const skillScope = options?.skillScope === 'global' ? 'global' : 'project';
|
|
176
|
-
const
|
|
178
|
+
const cliPackageSpec = options?.cliPackageSpec || resolveCliSpec().packageSpec;
|
|
179
|
+
const content = generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec, generatedSkills);
|
|
177
180
|
const createdFiles = [];
|
|
178
181
|
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Codex, Cline, etc.)
|
|
179
182
|
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
package/dist/cli/analyze.js
CHANGED
|
@@ -22,6 +22,7 @@ import { resolveEffectiveAnalyzeOptions } from './analyze-options.js';
|
|
|
22
22
|
import { formatFallbackSummary, formatUnityDiagnosticsSummary } from './analyze-summary.js';
|
|
23
23
|
import { resolveChildProcessExit } from './exit-code.js';
|
|
24
24
|
import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
|
|
25
|
+
import { resolveCliSpec } from '../config/cli-spec.js';
|
|
25
26
|
const HEAP_MB = 8192;
|
|
26
27
|
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
27
28
|
/** Re-exec the process with an 8GB heap if we're currently below that. */
|
|
@@ -98,6 +99,14 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
98
99
|
}
|
|
99
100
|
const currentCommit = getCurrentCommit(repoPath);
|
|
100
101
|
const existingMeta = await loadMeta(storagePath);
|
|
102
|
+
let hasLbugIndex = false;
|
|
103
|
+
try {
|
|
104
|
+
await fs.stat(lbugPath);
|
|
105
|
+
hasLbugIndex = true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
hasLbugIndex = false;
|
|
109
|
+
}
|
|
101
110
|
let includeExtensions = [];
|
|
102
111
|
let scopeRules = [];
|
|
103
112
|
let repoAlias;
|
|
@@ -121,7 +130,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
121
130
|
process.exitCode = 1;
|
|
122
131
|
return;
|
|
123
132
|
}
|
|
124
|
-
if (existingMeta && !
|
|
133
|
+
if (existingMeta && !hasLbugIndex && !options?.force) {
|
|
134
|
+
console.log(' Existing metadata found, but LadybugDB index file is missing — rebuilding index...\n');
|
|
135
|
+
}
|
|
136
|
+
if (existingMeta && hasLbugIndex && !options?.force && existingMeta.lastCommit === currentCommit && !options?.skills) {
|
|
125
137
|
const hasScopePrefixInput = Array.isArray(options?.scopePrefix)
|
|
126
138
|
? options.scopePrefix.length > 0
|
|
127
139
|
: Boolean(options?.scopePrefix);
|
|
@@ -368,6 +380,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
368
380
|
const skillResult = await generateSkillFiles(repoPath, projectName, pipelineForSkills);
|
|
369
381
|
generatedSkills = skillResult.skills;
|
|
370
382
|
}
|
|
383
|
+
const cliConfig = await loadCLIConfig();
|
|
371
384
|
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
372
385
|
files: pipelineRuntime.totalFileCount,
|
|
373
386
|
nodes: stats.nodes,
|
|
@@ -376,7 +389,8 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
376
389
|
clusters: aggregatedClusterCount,
|
|
377
390
|
processes: pipelineRuntime.processResult?.stats.totalProcesses,
|
|
378
391
|
}, {
|
|
379
|
-
skillScope: (
|
|
392
|
+
skillScope: (cliConfig.setupScope === 'global') ? 'global' : 'project',
|
|
393
|
+
cliPackageSpec: resolveCliSpec({ config: cliConfig }).packageSpec,
|
|
380
394
|
}, generatedSkills);
|
|
381
395
|
await closeLbug();
|
|
382
396
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,8 @@ program
|
|
|
17
17
|
.description('One-time setup: configure MCP for a selected coding agent (claude/opencode/codex)')
|
|
18
18
|
.option('--scope <scope>', 'Install target: global (default) or project')
|
|
19
19
|
.option('--agent <agent>', 'Target coding agent: claude, opencode, or codex')
|
|
20
|
+
.option('--cli-version <version>', 'Pin npx GitNexus version/tag for generated MCP commands (e.g. 1.4.7-rc)')
|
|
21
|
+
.option('--cli-spec <spec>', 'Pin full npx package spec for generated MCP commands (e.g. @veewo/gitnexus@1.4.7-rc)')
|
|
20
22
|
.action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
|
|
21
23
|
program
|
|
22
24
|
.command('analyze [path]')
|
package/dist/cli/setup.d.ts
CHANGED
package/dist/cli/setup.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* in either global or project scope.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
|
-
import { readFileSync } from 'node:fs';
|
|
10
9
|
import path from 'path';
|
|
11
10
|
import os from 'os';
|
|
12
11
|
import { execFile } from 'node:child_process';
|
|
@@ -15,10 +14,10 @@ import { fileURLToPath } from 'url';
|
|
|
15
14
|
import { getGlobalDir, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js';
|
|
16
15
|
import { getGitRoot } from '../storage/git.js';
|
|
17
16
|
import { glob } from 'glob';
|
|
17
|
+
import { buildNpxCommand, 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);
|
|
21
|
-
const FALLBACK_MCP_PACKAGE = '@veewo/gitnexus@latest';
|
|
22
21
|
const LEGACY_CURSOR_AGENT = 'cursor';
|
|
23
22
|
function resolveSetupScope(rawScope) {
|
|
24
23
|
if (!rawScope || rawScope.trim() === '')
|
|
@@ -48,43 +47,25 @@ async function installLegacyCursorSkills(result) {
|
|
|
48
47
|
result.errors.push(`Cursor skills: ${err.message}`);
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
|
|
52
|
-
* Resolve the package spec used by MCP commands.
|
|
53
|
-
* Defaults to @veewo/gitnexus@latest when package metadata is unavailable.
|
|
54
|
-
*/
|
|
55
|
-
function resolveMcpPackageSpec() {
|
|
56
|
-
try {
|
|
57
|
-
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
58
|
-
const raw = readFileSync(packageJsonPath, 'utf-8');
|
|
59
|
-
const parsed = JSON.parse(raw);
|
|
60
|
-
if (typeof parsed.name === 'string' && parsed.name.trim().length > 0) {
|
|
61
|
-
return `${parsed.name}@latest`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Fallback keeps behavior for unusual runtimes.
|
|
66
|
-
}
|
|
67
|
-
return FALLBACK_MCP_PACKAGE;
|
|
68
|
-
}
|
|
69
|
-
const MCP_PACKAGE_SPEC = resolveMcpPackageSpec();
|
|
50
|
+
const DEFAULT_MCP_PACKAGE_SPEC = resolveCliSpec().packageSpec;
|
|
70
51
|
/**
|
|
71
52
|
* The MCP server entry for all editors.
|
|
72
53
|
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
|
|
73
54
|
*/
|
|
74
|
-
function getMcpEntry() {
|
|
55
|
+
function getMcpEntry(mcpPackageSpec) {
|
|
75
56
|
if (process.platform === 'win32') {
|
|
76
57
|
return {
|
|
77
58
|
command: 'cmd',
|
|
78
|
-
args: ['/c', 'npx', '-y',
|
|
59
|
+
args: ['/c', 'npx', '-y', mcpPackageSpec, 'mcp'],
|
|
79
60
|
};
|
|
80
61
|
}
|
|
81
62
|
return {
|
|
82
63
|
command: 'npx',
|
|
83
|
-
args: ['-y',
|
|
64
|
+
args: ['-y', mcpPackageSpec, 'mcp'],
|
|
84
65
|
};
|
|
85
66
|
}
|
|
86
|
-
function getOpenCodeMcpEntry() {
|
|
87
|
-
const entry = getMcpEntry();
|
|
67
|
+
function getOpenCodeMcpEntry(mcpPackageSpec) {
|
|
68
|
+
const entry = getMcpEntry(mcpPackageSpec);
|
|
88
69
|
return {
|
|
89
70
|
type: 'local',
|
|
90
71
|
command: [entry.command, ...entry.args],
|
|
@@ -94,28 +75,28 @@ function getOpenCodeMcpEntry() {
|
|
|
94
75
|
* Merge gitnexus entry into an existing MCP config JSON object.
|
|
95
76
|
* Returns the updated config.
|
|
96
77
|
*/
|
|
97
|
-
function mergeMcpConfig(existing) {
|
|
78
|
+
function mergeMcpConfig(existing, mcpPackageSpec) {
|
|
98
79
|
if (!existing || typeof existing !== 'object') {
|
|
99
80
|
existing = {};
|
|
100
81
|
}
|
|
101
82
|
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
102
83
|
existing.mcpServers = {};
|
|
103
84
|
}
|
|
104
|
-
existing.mcpServers.gitnexus = getMcpEntry();
|
|
85
|
+
existing.mcpServers.gitnexus = getMcpEntry(mcpPackageSpec);
|
|
105
86
|
return existing;
|
|
106
87
|
}
|
|
107
88
|
/**
|
|
108
89
|
* Merge gitnexus entry into an OpenCode config JSON object.
|
|
109
90
|
* Returns the updated config.
|
|
110
91
|
*/
|
|
111
|
-
function mergeOpenCodeConfig(existing) {
|
|
92
|
+
function mergeOpenCodeConfig(existing, mcpPackageSpec) {
|
|
112
93
|
if (!existing || typeof existing !== 'object') {
|
|
113
94
|
existing = {};
|
|
114
95
|
}
|
|
115
96
|
if (!existing.mcp || typeof existing.mcp !== 'object') {
|
|
116
97
|
existing.mcp = {};
|
|
117
98
|
}
|
|
118
|
-
existing.mcp.gitnexus = getOpenCodeMcpEntry();
|
|
99
|
+
existing.mcp.gitnexus = getOpenCodeMcpEntry(mcpPackageSpec);
|
|
119
100
|
return existing;
|
|
120
101
|
}
|
|
121
102
|
/**
|
|
@@ -167,16 +148,16 @@ async function fileExists(filePath) {
|
|
|
167
148
|
function toTomlString(value) {
|
|
168
149
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
169
150
|
}
|
|
170
|
-
function buildCodexMcpTable() {
|
|
171
|
-
const entry = getMcpEntry();
|
|
151
|
+
function buildCodexMcpTable(mcpPackageSpec) {
|
|
152
|
+
const entry = getMcpEntry(mcpPackageSpec);
|
|
172
153
|
return [
|
|
173
154
|
'[mcp_servers.gitnexus]',
|
|
174
155
|
`command = ${toTomlString(entry.command)}`,
|
|
175
156
|
`args = [${entry.args.map(toTomlString).join(', ')}]`,
|
|
176
157
|
].join('\n');
|
|
177
158
|
}
|
|
178
|
-
function mergeCodexConfig(existingRaw) {
|
|
179
|
-
const table = buildCodexMcpTable();
|
|
159
|
+
function mergeCodexConfig(existingRaw, mcpPackageSpec) {
|
|
160
|
+
const table = buildCodexMcpTable(mcpPackageSpec);
|
|
180
161
|
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
181
162
|
const tablePattern = /^\[mcp_servers\.gitnexus\][\s\S]*?(?=^\[[^\]]+\]|(?![\s\S]))/m;
|
|
182
163
|
if (tablePattern.test(normalized)) {
|
|
@@ -198,7 +179,7 @@ async function resolveOpenCodeConfigPath(opencodeDir) {
|
|
|
198
179
|
return preferredPath;
|
|
199
180
|
}
|
|
200
181
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
201
|
-
async function setupCursor(result) {
|
|
182
|
+
async function setupCursor(result, mcpPackageSpec) {
|
|
202
183
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
203
184
|
if (!(await dirExists(cursorDir))) {
|
|
204
185
|
result.skipped.push('Cursor (not installed)');
|
|
@@ -207,7 +188,7 @@ async function setupCursor(result) {
|
|
|
207
188
|
const mcpPath = path.join(cursorDir, 'mcp.json');
|
|
208
189
|
try {
|
|
209
190
|
const existing = await readJsonFile(mcpPath);
|
|
210
|
-
const updated = mergeMcpConfig(existing);
|
|
191
|
+
const updated = mergeMcpConfig(existing, mcpPackageSpec);
|
|
211
192
|
await writeJsonFile(mcpPath, updated);
|
|
212
193
|
result.configured.push('Cursor');
|
|
213
194
|
}
|
|
@@ -215,7 +196,7 @@ async function setupCursor(result) {
|
|
|
215
196
|
result.errors.push(`Cursor: ${err.message}`);
|
|
216
197
|
}
|
|
217
198
|
}
|
|
218
|
-
async function setupClaudeCode(result) {
|
|
199
|
+
async function setupClaudeCode(result, mcpPackageSpec) {
|
|
219
200
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
220
201
|
const hasClaude = await dirExists(claudeDir);
|
|
221
202
|
if (!hasClaude) {
|
|
@@ -226,7 +207,7 @@ async function setupClaudeCode(result) {
|
|
|
226
207
|
console.log('');
|
|
227
208
|
console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
|
|
228
209
|
console.log('');
|
|
229
|
-
console.log(` claude mcp add gitnexus --
|
|
210
|
+
console.log(` claude mcp add gitnexus -- ${buildNpxCommand(mcpPackageSpec, 'mcp')}`);
|
|
230
211
|
console.log('');
|
|
231
212
|
result.configured.push('Claude Code (MCP manual step printed)');
|
|
232
213
|
}
|
|
@@ -262,7 +243,7 @@ async function installProjectAgentSkills(repoRoot, result) {
|
|
|
262
243
|
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
263
244
|
* Merges hook config without overwriting existing hooks.
|
|
264
245
|
*/
|
|
265
|
-
async function installClaudeCodeHooks(result) {
|
|
246
|
+
async function installClaudeCodeHooks(result, mcpPackageSpec) {
|
|
266
247
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
267
248
|
if (!(await dirExists(claudeDir)))
|
|
268
249
|
return;
|
|
@@ -283,6 +264,7 @@ async function installClaudeCodeHooks(result) {
|
|
|
283
264
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
284
265
|
const jsonCli = JSON.stringify(normalizedCli);
|
|
285
266
|
content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
|
|
267
|
+
content = content.replace("const DEFAULT_NPX_SPEC = '@veewo/gitnexus@latest';", `const DEFAULT_NPX_SPEC = ${JSON.stringify(mcpPackageSpec)};`);
|
|
286
268
|
await fs.writeFile(dest, content, 'utf-8');
|
|
287
269
|
}
|
|
288
270
|
catch {
|
|
@@ -314,7 +296,7 @@ async function installClaudeCodeHooks(result) {
|
|
|
314
296
|
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
315
297
|
}
|
|
316
298
|
}
|
|
317
|
-
async function setupOpenCode(result) {
|
|
299
|
+
async function setupOpenCode(result, mcpPackageSpec) {
|
|
318
300
|
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
319
301
|
if (!(await dirExists(opencodeDir))) {
|
|
320
302
|
result.skipped.push('OpenCode (not installed)');
|
|
@@ -323,7 +305,7 @@ async function setupOpenCode(result) {
|
|
|
323
305
|
const configPath = await resolveOpenCodeConfigPath(opencodeDir);
|
|
324
306
|
try {
|
|
325
307
|
const existing = await readJsonFile(configPath);
|
|
326
|
-
const config = mergeOpenCodeConfig(existing);
|
|
308
|
+
const config = mergeOpenCodeConfig(existing, mcpPackageSpec);
|
|
327
309
|
await writeJsonFile(configPath, config);
|
|
328
310
|
result.configured.push(`OpenCode (${path.basename(configPath)})`);
|
|
329
311
|
}
|
|
@@ -331,8 +313,8 @@ async function setupOpenCode(result) {
|
|
|
331
313
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
332
314
|
}
|
|
333
315
|
}
|
|
334
|
-
async function setupCodex(result) {
|
|
335
|
-
const entry = getMcpEntry();
|
|
316
|
+
async function setupCodex(result, mcpPackageSpec) {
|
|
317
|
+
const entry = getMcpEntry(mcpPackageSpec);
|
|
336
318
|
try {
|
|
337
319
|
await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { timeout: 15000 });
|
|
338
320
|
result.configured.push('Codex');
|
|
@@ -345,11 +327,11 @@ async function setupCodex(result) {
|
|
|
345
327
|
result.errors.push(`Codex: ${err.message}`);
|
|
346
328
|
}
|
|
347
329
|
}
|
|
348
|
-
async function setupProjectMcp(repoRoot, result) {
|
|
330
|
+
async function setupProjectMcp(repoRoot, result, mcpPackageSpec) {
|
|
349
331
|
const mcpPath = path.join(repoRoot, '.mcp.json');
|
|
350
332
|
try {
|
|
351
333
|
const existing = await readJsonFile(mcpPath);
|
|
352
|
-
const updated = mergeMcpConfig(existing);
|
|
334
|
+
const updated = mergeMcpConfig(existing, mcpPackageSpec);
|
|
353
335
|
await writeJsonFile(mcpPath, updated);
|
|
354
336
|
result.configured.push(`Project MCP (${path.relative(repoRoot, mcpPath)})`);
|
|
355
337
|
}
|
|
@@ -357,7 +339,7 @@ async function setupProjectMcp(repoRoot, result) {
|
|
|
357
339
|
result.errors.push(`Project MCP: ${err.message}`);
|
|
358
340
|
}
|
|
359
341
|
}
|
|
360
|
-
async function setupProjectCodex(repoRoot, result) {
|
|
342
|
+
async function setupProjectCodex(repoRoot, result, mcpPackageSpec) {
|
|
361
343
|
const codexConfigPath = path.join(repoRoot, '.codex', 'config.toml');
|
|
362
344
|
try {
|
|
363
345
|
let existingRaw = '';
|
|
@@ -368,7 +350,7 @@ async function setupProjectCodex(repoRoot, result) {
|
|
|
368
350
|
if (err?.code !== 'ENOENT')
|
|
369
351
|
throw err;
|
|
370
352
|
}
|
|
371
|
-
const merged = mergeCodexConfig(existingRaw);
|
|
353
|
+
const merged = mergeCodexConfig(existingRaw, mcpPackageSpec);
|
|
372
354
|
await fs.mkdir(path.dirname(codexConfigPath), { recursive: true });
|
|
373
355
|
await fs.writeFile(codexConfigPath, merged, 'utf-8');
|
|
374
356
|
result.configured.push(`Project Codex MCP (${path.relative(repoRoot, codexConfigPath)})`);
|
|
@@ -377,11 +359,11 @@ async function setupProjectCodex(repoRoot, result) {
|
|
|
377
359
|
result.errors.push(`Project Codex MCP: ${err.message}`);
|
|
378
360
|
}
|
|
379
361
|
}
|
|
380
|
-
async function setupProjectOpenCode(repoRoot, result) {
|
|
362
|
+
async function setupProjectOpenCode(repoRoot, result, mcpPackageSpec) {
|
|
381
363
|
const opencodePath = path.join(repoRoot, 'opencode.json');
|
|
382
364
|
try {
|
|
383
365
|
const existing = await readJsonFile(opencodePath);
|
|
384
|
-
const merged = mergeOpenCodeConfig(existing);
|
|
366
|
+
const merged = mergeOpenCodeConfig(existing, mcpPackageSpec);
|
|
385
367
|
await writeJsonFile(opencodePath, merged);
|
|
386
368
|
result.configured.push(`Project OpenCode MCP (${path.relative(repoRoot, opencodePath)})`);
|
|
387
369
|
}
|
|
@@ -389,11 +371,28 @@ async function setupProjectOpenCode(repoRoot, result) {
|
|
|
389
371
|
result.errors.push(`Project OpenCode MCP: ${err.message}`);
|
|
390
372
|
}
|
|
391
373
|
}
|
|
392
|
-
|
|
374
|
+
function extractVersionFromPackageSpec(packageSpec) {
|
|
375
|
+
const trimmed = packageSpec.trim();
|
|
376
|
+
if (!trimmed)
|
|
377
|
+
return undefined;
|
|
378
|
+
if (trimmed.startsWith('@')) {
|
|
379
|
+
const at = trimmed.indexOf('@', 1);
|
|
380
|
+
return at > 0 ? trimmed.slice(at + 1) : undefined;
|
|
381
|
+
}
|
|
382
|
+
const at = trimmed.lastIndexOf('@');
|
|
383
|
+
return at > 0 ? trimmed.slice(at + 1) : undefined;
|
|
384
|
+
}
|
|
385
|
+
async function saveSetupConfig(scope, packageSpec, result) {
|
|
393
386
|
try {
|
|
394
387
|
const existing = await loadCLIConfig();
|
|
395
|
-
await saveCLIConfig({
|
|
388
|
+
await saveCLIConfig({
|
|
389
|
+
...existing,
|
|
390
|
+
setupScope: scope,
|
|
391
|
+
cliPackageSpec: packageSpec,
|
|
392
|
+
cliVersion: extractVersionFromPackageSpec(packageSpec),
|
|
393
|
+
});
|
|
396
394
|
result.configured.push(`Default setup scope (${scope})`);
|
|
395
|
+
result.configured.push(`CLI package spec (${packageSpec})`);
|
|
397
396
|
}
|
|
398
397
|
catch (err) {
|
|
399
398
|
result.errors.push(`Persist setup scope: ${err.message}`);
|
|
@@ -477,6 +476,11 @@ export const setupCommand = async (options = {}) => {
|
|
|
477
476
|
console.log(' GitNexus Setup');
|
|
478
477
|
console.log(' ==============');
|
|
479
478
|
console.log('');
|
|
479
|
+
if (options.cliSpec && options.cliVersion) {
|
|
480
|
+
console.log(' Use either --cli-spec or --cli-version, not both.\n');
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
480
484
|
let scope;
|
|
481
485
|
let agent;
|
|
482
486
|
const legacyCursorMode = !options.agent || options.agent.trim() === '';
|
|
@@ -492,6 +496,13 @@ export const setupCommand = async (options = {}) => {
|
|
|
492
496
|
// Ensure global directory exists
|
|
493
497
|
const globalDir = getGlobalDir();
|
|
494
498
|
await fs.mkdir(globalDir, { recursive: true });
|
|
499
|
+
const existingConfig = await loadCLIConfig();
|
|
500
|
+
const resolvedCliSpec = resolveCliSpec({
|
|
501
|
+
explicitSpec: options.cliSpec,
|
|
502
|
+
explicitVersion: options.cliVersion,
|
|
503
|
+
config: existingConfig,
|
|
504
|
+
});
|
|
505
|
+
const mcpPackageSpec = resolvedCliSpec.packageSpec || DEFAULT_MCP_PACKAGE_SPEC;
|
|
495
506
|
const result = {
|
|
496
507
|
configured: [],
|
|
497
508
|
skipped: [],
|
|
@@ -499,27 +510,27 @@ export const setupCommand = async (options = {}) => {
|
|
|
499
510
|
};
|
|
500
511
|
if (scope === 'global') {
|
|
501
512
|
if (legacyCursorMode) {
|
|
502
|
-
await setupCursor(result);
|
|
513
|
+
await setupCursor(result, mcpPackageSpec);
|
|
503
514
|
await installLegacyCursorSkills(result);
|
|
504
|
-
await
|
|
515
|
+
await saveSetupConfig(scope, mcpPackageSpec, result);
|
|
505
516
|
agent = LEGACY_CURSOR_AGENT;
|
|
506
517
|
}
|
|
507
518
|
else {
|
|
508
519
|
// Configure only the selected agent MCP
|
|
509
520
|
if (agent === 'claude') {
|
|
510
|
-
await setupClaudeCode(result);
|
|
521
|
+
await setupClaudeCode(result, mcpPackageSpec);
|
|
511
522
|
// Claude-only hooks should only be installed when Claude is selected.
|
|
512
|
-
await installClaudeCodeHooks(result);
|
|
523
|
+
await installClaudeCodeHooks(result, mcpPackageSpec);
|
|
513
524
|
}
|
|
514
525
|
else if (agent === 'opencode') {
|
|
515
|
-
await setupOpenCode(result);
|
|
526
|
+
await setupOpenCode(result, mcpPackageSpec);
|
|
516
527
|
}
|
|
517
528
|
else if (agent === 'codex') {
|
|
518
|
-
await setupCodex(result);
|
|
529
|
+
await setupCodex(result, mcpPackageSpec);
|
|
519
530
|
}
|
|
520
531
|
// Install shared global skills once
|
|
521
532
|
await installGlobalAgentSkills(result);
|
|
522
|
-
await
|
|
533
|
+
await saveSetupConfig(scope, mcpPackageSpec, result);
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
else {
|
|
@@ -530,16 +541,16 @@ export const setupCommand = async (options = {}) => {
|
|
|
530
541
|
return;
|
|
531
542
|
}
|
|
532
543
|
if (agent === 'claude') {
|
|
533
|
-
await setupProjectMcp(repoRoot, result);
|
|
544
|
+
await setupProjectMcp(repoRoot, result, mcpPackageSpec);
|
|
534
545
|
}
|
|
535
546
|
else if (agent === 'codex') {
|
|
536
|
-
await setupProjectCodex(repoRoot, result);
|
|
547
|
+
await setupProjectCodex(repoRoot, result, mcpPackageSpec);
|
|
537
548
|
}
|
|
538
549
|
else if (agent === 'opencode') {
|
|
539
|
-
await setupProjectOpenCode(repoRoot, result);
|
|
550
|
+
await setupProjectOpenCode(repoRoot, result, mcpPackageSpec);
|
|
540
551
|
}
|
|
541
552
|
await installProjectAgentSkills(repoRoot, result);
|
|
542
|
-
await
|
|
553
|
+
await saveSetupConfig(scope, mcpPackageSpec, result);
|
|
543
554
|
}
|
|
544
555
|
// Print results
|
|
545
556
|
if (result.configured.length > 0) {
|
|
@@ -566,6 +577,7 @@ export const setupCommand = async (options = {}) => {
|
|
|
566
577
|
console.log(' Summary:');
|
|
567
578
|
console.log(` Scope: ${scope}`);
|
|
568
579
|
console.log(` Agent: ${legacyCursorMode ? LEGACY_CURSOR_AGENT : agent}`);
|
|
580
|
+
console.log(` CLI package spec: ${mcpPackageSpec}`);
|
|
569
581
|
console.log(` MCP configured for: ${result.configured.filter(c => !c.includes('skills')).join(', ') || 'none'}`);
|
|
570
582
|
console.log(` Skills installed to: ${result.configured.filter(c => c.includes('skills')).length > 0 ? result.configured.filter(c => c.includes('skills')).join(', ') : 'none'}`);
|
|
571
583
|
console.log('');
|
package/dist/cli/setup.test.js
CHANGED
|
@@ -61,6 +61,26 @@ test('setup rejects invalid --agent', async () => {
|
|
|
61
61
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
+
test('setup rejects using --cli-spec and --cli-version together', async () => {
|
|
65
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
66
|
+
try {
|
|
67
|
+
try {
|
|
68
|
+
await runSetup(['--agent', 'opencode', '--cli-spec', '@veewo/gitnexus@1.4.7-rc', '--cli-version', '1.4.7-rc'], {
|
|
69
|
+
...process.env,
|
|
70
|
+
HOME: fakeHome,
|
|
71
|
+
USERPROFILE: fakeHome,
|
|
72
|
+
});
|
|
73
|
+
assert.fail('expected setup with conflicting CLI options to fail');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
assert.equal(typeof err?.stdout, 'string');
|
|
77
|
+
assert.match(err.stdout, /Use either --cli-spec or --cli-version/);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
64
84
|
test('setup installs global skills under ~/.agents/skills/gitnexus', async () => {
|
|
65
85
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
66
86
|
try {
|
|
@@ -148,6 +168,30 @@ test('setup configures OpenCode MCP in ~/.config/opencode/opencode.json', async
|
|
|
148
168
|
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
149
169
|
}
|
|
150
170
|
});
|
|
171
|
+
test('setup --cli-version pins MCP package spec and persists it in config', async () => {
|
|
172
|
+
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
173
|
+
try {
|
|
174
|
+
const opencodeDir = path.join(fakeHome, '.config', 'opencode');
|
|
175
|
+
await fs.mkdir(opencodeDir, { recursive: true });
|
|
176
|
+
await runSetup(['--agent', 'opencode', '--cli-version', '1.4.7-rc'], {
|
|
177
|
+
...process.env,
|
|
178
|
+
HOME: fakeHome,
|
|
179
|
+
USERPROFILE: fakeHome,
|
|
180
|
+
});
|
|
181
|
+
const opencodeConfigPath = path.join(opencodeDir, 'opencode.json');
|
|
182
|
+
const opencodeRaw = await fs.readFile(opencodeConfigPath, 'utf-8');
|
|
183
|
+
const opencodeConfig = JSON.parse(opencodeRaw);
|
|
184
|
+
const configPath = path.join(fakeHome, '.gitnexus', 'config.json');
|
|
185
|
+
const savedConfigRaw = await fs.readFile(configPath, 'utf-8');
|
|
186
|
+
const savedConfig = JSON.parse(savedConfigRaw);
|
|
187
|
+
assert.deepEqual(opencodeConfig.mcp?.gitnexus?.command, ['npx', '-y', '@veewo/gitnexus@1.4.7-rc', 'mcp']);
|
|
188
|
+
assert.equal(savedConfig.cliPackageSpec, '@veewo/gitnexus@1.4.7-rc');
|
|
189
|
+
assert.equal(savedConfig.cliVersion, '1.4.7-rc');
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await fs.rm(fakeHome, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
151
195
|
test('setup keeps using legacy ~/.config/opencode/config.json when it already exists', async () => {
|
|
152
196
|
const fakeHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-setup-home-'));
|
|
153
197
|
try {
|
package/dist/cli/status.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shows the indexing status of the current repository.
|
|
5
5
|
*/
|
|
6
|
-
import { findRepo, getStoragePaths, hasKuzuIndex } from '../storage/repo-manager.js';
|
|
6
|
+
import { findRepo, getStoragePaths, hasKuzuIndex, hasLbugIndex } from '../storage/repo-manager.js';
|
|
7
7
|
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
8
8
|
export const statusCommand = async () => {
|
|
9
9
|
const cwd = process.cwd();
|
|
@@ -28,6 +28,13 @@ export const statusCommand = async () => {
|
|
|
28
28
|
}
|
|
29
29
|
const currentCommit = getCurrentCommit(repo.repoPath);
|
|
30
30
|
const isUpToDate = currentCommit === repo.meta.lastCommit;
|
|
31
|
+
const lbugReady = await hasLbugIndex(repo.storagePath);
|
|
32
|
+
if (!lbugReady) {
|
|
33
|
+
console.log(`Repository: ${repo.repoPath}`);
|
|
34
|
+
console.log('Status: ⚠️ index metadata exists but LadybugDB is missing');
|
|
35
|
+
console.log('Run: gitnexus analyze --force');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
31
38
|
console.log(`Repository: ${repo.repoPath}`);
|
|
32
39
|
console.log(`Indexed: ${new Date(repo.meta.indexedAt).toLocaleString()}`);
|
|
33
40
|
console.log(`Indexed commit: ${repo.meta.lastCommit?.slice(0, 7)}`);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const DEFAULT_GITNEXUS_PACKAGE_NAME = "@veewo/gitnexus";
|
|
2
|
+
export declare const DEFAULT_GITNEXUS_DIST_TAG = "latest";
|
|
3
|
+
export declare const CLI_SPEC_ENV_KEY = "GITNEXUS_CLI_SPEC";
|
|
4
|
+
export declare const CLI_VERSION_ENV_KEY = "GITNEXUS_CLI_VERSION";
|
|
5
|
+
type CliSpecSource = 'explicit-spec' | 'explicit-version' | 'env-spec' | 'env-version' | 'config-spec' | 'config-version' | 'default';
|
|
6
|
+
export interface CliSpecConfigLike {
|
|
7
|
+
cliPackageSpec?: string;
|
|
8
|
+
cliVersion?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolveCliSpecInput {
|
|
11
|
+
packageName?: string;
|
|
12
|
+
explicitSpec?: string;
|
|
13
|
+
explicitVersion?: string;
|
|
14
|
+
config?: CliSpecConfigLike;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
defaultDistTag?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedCliSpec {
|
|
19
|
+
packageName: string;
|
|
20
|
+
packageSpec: string;
|
|
21
|
+
source: CliSpecSource;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve package name from package.json with fallback for unusual runtimes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveGitNexusPackageName(): string;
|
|
27
|
+
export declare function resolveCliSpec(input?: ResolveCliSpecInput): ResolvedCliSpec;
|
|
28
|
+
export declare function buildNpxCommand(packageSpec: string, subcommand: string): string;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const DEFAULT_GITNEXUS_PACKAGE_NAME = '@veewo/gitnexus';
|
|
5
|
+
export const DEFAULT_GITNEXUS_DIST_TAG = 'latest';
|
|
6
|
+
export const CLI_SPEC_ENV_KEY = 'GITNEXUS_CLI_SPEC';
|
|
7
|
+
export const CLI_VERSION_ENV_KEY = 'GITNEXUS_CLI_VERSION';
|
|
8
|
+
let cachedPackageName = null;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve package name from package.json with fallback for unusual runtimes.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveGitNexusPackageName() {
|
|
13
|
+
if (cachedPackageName)
|
|
14
|
+
return cachedPackageName;
|
|
15
|
+
try {
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
const packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
|
|
19
|
+
const raw = readFileSync(packageJsonPath, 'utf-8');
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
const trimmed = typeof parsed.name === 'string' ? parsed.name.trim() : '';
|
|
22
|
+
cachedPackageName = trimmed || DEFAULT_GITNEXUS_PACKAGE_NAME;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
cachedPackageName = DEFAULT_GITNEXUS_PACKAGE_NAME;
|
|
26
|
+
}
|
|
27
|
+
return cachedPackageName;
|
|
28
|
+
}
|
|
29
|
+
function hasPinnedVersion(packageSpec) {
|
|
30
|
+
const trimmed = packageSpec.trim();
|
|
31
|
+
if (!trimmed)
|
|
32
|
+
return false;
|
|
33
|
+
if (trimmed.startsWith('@')) {
|
|
34
|
+
return trimmed.indexOf('@', 1) !== -1;
|
|
35
|
+
}
|
|
36
|
+
return trimmed.includes('@');
|
|
37
|
+
}
|
|
38
|
+
function looksLikePackageSpec(token) {
|
|
39
|
+
if (!token)
|
|
40
|
+
return false;
|
|
41
|
+
if (token.startsWith('@'))
|
|
42
|
+
return true;
|
|
43
|
+
if (token.includes('/'))
|
|
44
|
+
return true;
|
|
45
|
+
return token.includes('@');
|
|
46
|
+
}
|
|
47
|
+
function normalizePackageSpec(packageName, raw, defaultDistTag) {
|
|
48
|
+
const token = raw.trim();
|
|
49
|
+
if (!token)
|
|
50
|
+
return `${packageName}@${defaultDistTag}`;
|
|
51
|
+
if (looksLikePackageSpec(token)) {
|
|
52
|
+
return hasPinnedVersion(token) ? token : `${token}@${defaultDistTag}`;
|
|
53
|
+
}
|
|
54
|
+
return `${packageName}@${token}`;
|
|
55
|
+
}
|
|
56
|
+
export function resolveCliSpec(input = {}) {
|
|
57
|
+
const packageName = (input.packageName || resolveGitNexusPackageName()).trim() || DEFAULT_GITNEXUS_PACKAGE_NAME;
|
|
58
|
+
const env = input.env || process.env;
|
|
59
|
+
const config = input.config || {};
|
|
60
|
+
const defaultDistTag = (input.defaultDistTag || DEFAULT_GITNEXUS_DIST_TAG).trim() || DEFAULT_GITNEXUS_DIST_TAG;
|
|
61
|
+
const candidates = [
|
|
62
|
+
{ value: input.explicitSpec, source: 'explicit-spec' },
|
|
63
|
+
{ value: input.explicitVersion, source: 'explicit-version' },
|
|
64
|
+
{ value: env[CLI_SPEC_ENV_KEY], source: 'env-spec' },
|
|
65
|
+
{ value: env[CLI_VERSION_ENV_KEY], source: 'env-version' },
|
|
66
|
+
{ value: config.cliPackageSpec, source: 'config-spec' },
|
|
67
|
+
{ value: config.cliVersion, source: 'config-version' },
|
|
68
|
+
];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const trimmed = (candidate.value || '').trim();
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
continue;
|
|
73
|
+
return {
|
|
74
|
+
packageName,
|
|
75
|
+
packageSpec: normalizePackageSpec(packageName, trimmed, defaultDistTag),
|
|
76
|
+
source: candidate.source,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
packageName,
|
|
81
|
+
packageSpec: `${packageName}@${defaultDistTag}`,
|
|
82
|
+
source: 'default',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function buildNpxCommand(packageSpec, subcommand) {
|
|
86
|
+
return `npx -y ${packageSpec} ${subcommand}`.trim();
|
|
87
|
+
}
|
package/dist/mcp/resources.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* All resources use repo-scoped URIs: gitnexus://repo/{name}/context
|
|
6
6
|
*/
|
|
7
7
|
import { checkStaleness } from './staleness.js';
|
|
8
|
+
import { loadCLIConfig } from '../storage/repo-manager.js';
|
|
9
|
+
import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
|
|
8
10
|
/**
|
|
9
11
|
* Static resources — includes per-repo resources and the global repos list
|
|
10
12
|
*/
|
|
@@ -121,6 +123,11 @@ export async function readResource(uri, backend) {
|
|
|
121
123
|
throw new Error(`Unknown resource: ${uri}`);
|
|
122
124
|
}
|
|
123
125
|
}
|
|
126
|
+
async function resolveAnalyzeNpxCommand() {
|
|
127
|
+
const config = await loadCLIConfig();
|
|
128
|
+
const packageSpec = resolveCliSpec({ config }).packageSpec;
|
|
129
|
+
return buildNpxCommand(packageSpec, 'analyze');
|
|
130
|
+
}
|
|
124
131
|
// ─── Resource Implementations ─────────────────────────────────────────
|
|
125
132
|
/**
|
|
126
133
|
* Repos resource — list all indexed repositories
|
|
@@ -186,7 +193,8 @@ async function getContextResource(backend, repoName) {
|
|
|
186
193
|
lines.push(' - cypher: Raw graph queries');
|
|
187
194
|
lines.push(' - list_repos: Discover all indexed repositories');
|
|
188
195
|
lines.push('');
|
|
189
|
-
|
|
196
|
+
const analyzeCmd = await resolveAnalyzeNpxCommand();
|
|
197
|
+
lines.push(`re_index: If data is stale, ask user whether to run \`gitnexus analyze\` when local CLI exists; otherwise run \`${analyzeCmd}\` (reuses previous analyze scope/options unless \`--no-reuse-options\` is passed). If user declines, clearly state retrieval may not reflect current code. For build/analyze/test commands, use 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently switch to glob/grep fallback.`);
|
|
190
198
|
lines.push('');
|
|
191
199
|
lines.push('resources_available:');
|
|
192
200
|
lines.push(' - gitnexus://repos: All indexed repositories');
|
|
@@ -371,8 +379,9 @@ async function getProcessDetailResource(name, backend, repoName) {
|
|
|
371
379
|
*/
|
|
372
380
|
async function getSetupResource(backend) {
|
|
373
381
|
const repos = await backend.listRepos();
|
|
382
|
+
const analyzeCmd = await resolveAnalyzeNpxCommand();
|
|
374
383
|
if (repos.length === 0) {
|
|
375
|
-
return
|
|
384
|
+
return `# GitNexus\n\nNo repositories indexed. Run: \`gitnexus analyze\` when local CLI exists; otherwise \`${analyzeCmd}\` in a repository.`;
|
|
376
385
|
}
|
|
377
386
|
const sections = [];
|
|
378
387
|
for (const repo of repos) {
|
|
@@ -64,6 +64,10 @@ export declare const getStoragePaths: (repoPath: string) => {
|
|
|
64
64
|
* Non-destructive — safe to call from status commands.
|
|
65
65
|
*/
|
|
66
66
|
export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Check whether a LadybugDB index exists in the given storage path.
|
|
69
|
+
*/
|
|
70
|
+
export declare const hasLbugIndex: (storagePath: string) => Promise<boolean>;
|
|
67
71
|
/**
|
|
68
72
|
* Clean up stale KuzuDB files after migration to LadybugDB.
|
|
69
73
|
*
|
|
@@ -135,6 +139,8 @@ export interface CLIConfig {
|
|
|
135
139
|
model?: string;
|
|
136
140
|
baseUrl?: string;
|
|
137
141
|
setupScope?: 'global' | 'project';
|
|
142
|
+
cliPackageSpec?: string;
|
|
143
|
+
cliVersion?: string;
|
|
138
144
|
}
|
|
139
145
|
/**
|
|
140
146
|
* Get the path to the global CLI config file
|
|
@@ -52,6 +52,18 @@ export const hasKuzuIndex = async (storagePath) => {
|
|
|
52
52
|
return false;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Check whether a LadybugDB index exists in the given storage path.
|
|
57
|
+
*/
|
|
58
|
+
export const hasLbugIndex = async (storagePath) => {
|
|
59
|
+
try {
|
|
60
|
+
await fs.stat(path.join(storagePath, 'lbug'));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
55
67
|
/**
|
|
56
68
|
* Clean up stale KuzuDB files after migration to LadybugDB.
|
|
57
69
|
*
|
|
@@ -279,6 +291,7 @@ export const listRegisteredRepos = async (opts) => {
|
|
|
279
291
|
for (const entry of entries) {
|
|
280
292
|
try {
|
|
281
293
|
await fs.access(path.join(entry.storagePath, 'meta.json'));
|
|
294
|
+
await fs.access(path.join(entry.storagePath, 'lbug'));
|
|
282
295
|
valid.push(entry);
|
|
283
296
|
}
|
|
284
297
|
catch {
|
|
@@ -13,7 +13,41 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
16
17
|
const { spawnSync } = require('child_process');
|
|
18
|
+
const DEFAULT_NPX_SPEC = '@veewo/gitnexus@latest';
|
|
19
|
+
const DEFAULT_PACKAGE_NAME = '@veewo/gitnexus';
|
|
20
|
+
|
|
21
|
+
function normalizeNpxSpec(raw) {
|
|
22
|
+
const value = (raw || '').trim();
|
|
23
|
+
if (!value) return DEFAULT_NPX_SPEC;
|
|
24
|
+
if (value.startsWith('@') || value.includes('/') || value.includes('@')) {
|
|
25
|
+
if (value.startsWith('@') && value.indexOf('@', 1) === -1) return `${value}@latest`;
|
|
26
|
+
if (!value.startsWith('@') && !value.includes('/') && !value.includes('@')) return `${DEFAULT_PACKAGE_NAME}@${value}`;
|
|
27
|
+
if (!value.startsWith('@') && value.includes('@')) return value;
|
|
28
|
+
if (!value.includes('@')) return `${value}@latest`;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return `${DEFAULT_PACKAGE_NAME}@${value}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveNpxSpec() {
|
|
35
|
+
if (process.env.GITNEXUS_CLI_SPEC) return normalizeNpxSpec(process.env.GITNEXUS_CLI_SPEC);
|
|
36
|
+
if (process.env.GITNEXUS_CLI_VERSION) return normalizeNpxSpec(process.env.GITNEXUS_CLI_VERSION);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(path.join(os.homedir(), '.gitnexus', 'config.json'), 'utf-8');
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if (typeof parsed.cliPackageSpec === 'string' && parsed.cliPackageSpec.trim()) {
|
|
42
|
+
return normalizeNpxSpec(parsed.cliPackageSpec);
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed.cliVersion === 'string' && parsed.cliVersion.trim()) {
|
|
45
|
+
return normalizeNpxSpec(parsed.cliVersion);
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
49
|
+
return DEFAULT_NPX_SPEC;
|
|
50
|
+
}
|
|
17
51
|
|
|
18
52
|
/**
|
|
19
53
|
* Read JSON input from stdin synchronously.
|
|
@@ -107,7 +141,7 @@ function resolveCliPath() {
|
|
|
107
141
|
* Spawn a gitnexus CLI command synchronously.
|
|
108
142
|
* Returns the stderr output (KuzuDB captures stdout at OS level).
|
|
109
143
|
*/
|
|
110
|
-
function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
144
|
+
function runGitNexusCli(cliPath, args, cwd, timeout, npxSpec) {
|
|
111
145
|
const isWin = process.platform === 'win32';
|
|
112
146
|
if (cliPath) {
|
|
113
147
|
return spawnSync(
|
|
@@ -119,7 +153,7 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
|
119
153
|
// On Windows, invoke npx.cmd directly (no shell needed)
|
|
120
154
|
return spawnSync(
|
|
121
155
|
isWin ? 'npx.cmd' : 'npx',
|
|
122
|
-
['-y',
|
|
156
|
+
['-y', npxSpec, ...args],
|
|
123
157
|
{ encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
124
158
|
);
|
|
125
159
|
}
|
|
@@ -141,9 +175,10 @@ function handlePreToolUse(input) {
|
|
|
141
175
|
if (!pattern || pattern.length < 3) return;
|
|
142
176
|
|
|
143
177
|
const cliPath = resolveCliPath();
|
|
178
|
+
const npxSpec = resolveNpxSpec();
|
|
144
179
|
let result = '';
|
|
145
180
|
try {
|
|
146
|
-
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
181
|
+
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000, npxSpec);
|
|
147
182
|
if (!child.error && child.status === 0) {
|
|
148
183
|
result = child.stderr || '';
|
|
149
184
|
}
|
|
@@ -166,7 +201,7 @@ function sendHookResponse(hookEventName, message) {
|
|
|
166
201
|
/**
|
|
167
202
|
* PostToolUse handler — detect index staleness after git mutations.
|
|
168
203
|
*
|
|
169
|
-
* Instead of spawning a full
|
|
204
|
+
* Instead of spawning a full analyze command synchronously (which blocks
|
|
170
205
|
* the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
|
|
171
206
|
* lightweight staleness check: compare `git rev-parse HEAD` against the
|
|
172
207
|
* lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
|
|
@@ -210,10 +245,13 @@ function handlePostToolUse(input) {
|
|
|
210
245
|
// If HEAD matches last indexed commit, no reindex needed
|
|
211
246
|
if (currentHead && currentHead === lastCommit) return;
|
|
212
247
|
|
|
213
|
-
const
|
|
248
|
+
const npxSpec = resolveNpxSpec();
|
|
249
|
+
const analyzeArgs = `analyze${hadEmbeddings ? ' --embeddings' : ''}`;
|
|
250
|
+
const analyzeCmd = `gitnexus ${analyzeArgs}`;
|
|
251
|
+
const fallbackCmd = `npx -y ${npxSpec} ${analyzeArgs}`;
|
|
214
252
|
sendHookResponse('PostToolUse',
|
|
215
253
|
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
|
216
|
-
`Run \`${analyzeCmd}\` to update the knowledge graph.`
|
|
254
|
+
`Run \`${analyzeCmd}\` (or \`${fallbackCmd}\`) to update the knowledge graph.`
|
|
217
255
|
);
|
|
218
256
|
}
|
|
219
257
|
|
|
@@ -64,7 +64,11 @@ fi
|
|
|
64
64
|
|
|
65
65
|
# Run gitnexus augment — must be fast (<500ms target)
|
|
66
66
|
# augment writes to stderr (KuzuDB captures stdout at OS level), so capture stderr and discard stdout
|
|
67
|
-
|
|
67
|
+
GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
|
|
68
|
+
if [ -n "$GITNEXUS_CLI_VERSION" ]; then
|
|
69
|
+
GITNEXUS_CLI_SPEC="@veewo/gitnexus@$GITNEXUS_CLI_VERSION"
|
|
70
|
+
fi
|
|
71
|
+
RESULT=$(cd "$CWD" && npx -y "$GITNEXUS_CLI_SPEC" augment "$PATTERN" 2>&1 1>/dev/null)
|
|
68
72
|
|
|
69
73
|
if [ -n "$RESULT" ]; then
|
|
70
74
|
ESCAPED=$(echo "$RESULT" | jq -Rs .)
|
package/package.json
CHANGED
package/skills/gitnexus-cli.md
CHANGED
|
@@ -5,14 +5,25 @@ description: "Use when the user needs to run GitNexus CLI commands like analyze/
|
|
|
5
5
|
|
|
6
6
|
# GitNexus CLI Commands
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use one command alias in the session so every CLI/MCP call stays on one version line.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# If user prompt specifies a version (example: 1.4.7-rc), set it once:
|
|
12
|
+
# export GITNEXUS_CLI_SPEC="@veewo/gitnexus@1.4.7-rc"
|
|
13
|
+
GITNEXUS_CLI_SPEC="${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest}"
|
|
14
|
+
if command -v gitnexus >/dev/null 2>&1; then
|
|
15
|
+
GN="gitnexus"
|
|
16
|
+
else
|
|
17
|
+
GN="npx -y ${GITNEXUS_CLI_SPEC}"
|
|
18
|
+
fi
|
|
19
|
+
```
|
|
9
20
|
|
|
10
21
|
## Commands
|
|
11
22
|
|
|
12
23
|
### analyze — Build or refresh the index
|
|
13
24
|
|
|
14
25
|
```bash
|
|
15
|
-
|
|
26
|
+
$GN analyze
|
|
16
27
|
```
|
|
17
28
|
|
|
18
29
|
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
|
|
@@ -27,7 +38,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
|
|
|
27
38
|
### status — Check index freshness
|
|
28
39
|
|
|
29
40
|
```bash
|
|
30
|
-
|
|
41
|
+
$GN status
|
|
31
42
|
```
|
|
32
43
|
|
|
33
44
|
Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
|
|
@@ -35,7 +46,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
|
|
|
35
46
|
### clean — Delete the index
|
|
36
47
|
|
|
37
48
|
```bash
|
|
38
|
-
|
|
49
|
+
$GN clean
|
|
39
50
|
```
|
|
40
51
|
|
|
41
52
|
Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
|
|
@@ -48,7 +59,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
|
|
|
48
59
|
### wiki — Generate documentation from the graph
|
|
49
60
|
|
|
50
61
|
```bash
|
|
51
|
-
|
|
62
|
+
$GN wiki
|
|
52
63
|
```
|
|
53
64
|
|
|
54
65
|
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
|
|
@@ -65,7 +76,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
|
|
|
65
76
|
### list — Show all indexed repos
|
|
66
77
|
|
|
67
78
|
```bash
|
|
68
|
-
|
|
79
|
+
$GN list
|
|
69
80
|
```
|
|
70
81
|
|
|
71
82
|
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
|
|
@@ -75,11 +86,11 @@ Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_
|
|
|
75
86
|
For Unity resource retrieval:
|
|
76
87
|
|
|
77
88
|
```bash
|
|
78
|
-
|
|
89
|
+
$GN context DoorObj --repo neonnew-core --file Assets/NEON/Code/Game/Doors/DoorObj.cs --unity-resources on --unity-hydration compact
|
|
79
90
|
```
|
|
80
91
|
|
|
81
92
|
```bash
|
|
82
|
-
|
|
93
|
+
$GN query "DoorObj binding" --repo neonnew-core --unity-resources on --unity-hydration compact
|
|
83
94
|
```
|
|
84
95
|
|
|
85
96
|
Rules:
|
|
@@ -23,7 +23,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
|
|
23
23
|
5. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
> If "Index is stale" → run `npx -y
|
|
26
|
+
> If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
|
|
27
27
|
|
|
28
28
|
## Checklist
|
|
29
29
|
|
|
@@ -24,7 +24,7 @@ description: "Use when the user asks how code works, wants to understand archite
|
|
|
24
24
|
6. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
> If step 2 says "Index is stale" → run `npx -y
|
|
27
|
+
> If step 2 says "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
|
|
28
28
|
|
|
29
29
|
## Checklist
|
|
30
30
|
|
package/skills/gitnexus-guide.md
CHANGED
|
@@ -15,7 +15,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
|
|
15
15
|
2. **Match your task to a skill below** and **read that skill file**
|
|
16
16
|
3. **Follow the skill's workflow and checklist**
|
|
17
17
|
|
|
18
|
-
> If step 1 warns the index is stale, run `npx -y
|
|
18
|
+
> If step 1 warns the index is stale, run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
|
|
19
19
|
|
|
20
20
|
## Skills
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ description: "Use when the user wants to know what will break if they change som
|
|
|
23
23
|
4. Assess risk and report to user
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
> If "Index is stale" → run `npx -y
|
|
26
|
+
> If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
|
|
27
27
|
|
|
28
28
|
## Checklist
|
|
29
29
|
|
|
@@ -26,7 +26,7 @@ description: "Use when the user wants to review a pull request, understand what
|
|
|
26
26
|
6. Summarize findings with risk assessment
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
> If "Index is stale" → run `npx -y
|
|
29
|
+
> If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze` before reviewing.
|
|
30
30
|
|
|
31
31
|
## Checklist
|
|
32
32
|
|
|
@@ -23,7 +23,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
|
|
23
23
|
5. Plan update order: interfaces → implementations → callers → tests
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
> If "Index is stale" → run `npx -y
|
|
26
|
+
> If "Index is stale" → run `gitnexus analyze` when local CLI exists; otherwise run `npx -y ${GITNEXUS_CLI_SPEC:-@veewo/gitnexus@latest} analyze`.
|
|
27
27
|
|
|
28
28
|
## Checklists
|
|
29
29
|
|