gitnexus 1.6.6-rc.113 → 1.6.6-rc.115
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 -10
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +32 -5
- package/dist/cli/analyze.js +5 -0
- package/dist/cli/resolve-invocation.d.ts +21 -0
- package/dist/cli/resolve-invocation.js +68 -0
- package/dist/cli/setup.d.ts +33 -0
- package/dist/cli/setup.js +96 -35
- package/dist/core/ingestion/languages/python/query.js +40 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +2 -1
- package/hooks/claude/gitnexus-hook.cjs +2 -1
- package/hooks/claude/resolve-analyze-cmd.cjs +295 -0
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +2 -0
- package/skills/gitnexus-cli.md +8 -6
- 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
|
@@ -24,6 +24,14 @@ npx gitnexus analyze
|
|
|
24
24
|
|
|
25
25
|
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
26
|
|
|
27
|
+
> **On npm 11.x?** `npx` can crash during install (`Cannot destructure property 'package' of 'node.target'`). Use the pnpm form instead:
|
|
28
|
+
>
|
|
29
|
+
> ```bash
|
|
30
|
+
> pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze
|
|
31
|
+
> ```
|
|
32
|
+
>
|
|
33
|
+
> See [Troubleshooting → `npx gitnexus` crashes with `node.target is null` (npm 11)](#cannot-destructure-property-package-of-nodetarget-as-it-is-null) for the full matrix (global install, npm downgrade).
|
|
34
|
+
|
|
27
35
|
To configure MCP for your editor, run `npx gitnexus setup` once — or set it up manually below.
|
|
28
36
|
|
|
29
37
|
`gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
|
|
@@ -273,22 +281,27 @@ for the full list; stable `latest` is unaffected.
|
|
|
273
281
|
|
|
274
282
|
### `Cannot destructure property 'package' of 'node.target' as it is null`
|
|
275
283
|
|
|
276
|
-
This
|
|
277
|
-
certain npm/arborist versions ([npm/cli#8126](https://github.com/npm/cli/issues/8126)).
|
|
278
|
-
It is fixed in **gitnexus v1.6.2+**. Upgrade to the latest version:
|
|
284
|
+
This error comes from **npm 11.x's arborist** while installing gitnexus (often via `npx`), before gitnexus code runs. It is triggered by platform-filtered `optionalDependencies` in native packages such as `onnxruntime-node` / `@huggingface/transformers` (used when indexing with `--embeddings`). GitNexus cannot catch it at runtime — use one of these workarounds:
|
|
279
285
|
|
|
280
286
|
```bash
|
|
281
|
-
|
|
282
|
-
#
|
|
283
|
-
|
|
287
|
+
pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze # auto-selected when pnpm + npm 11+
|
|
288
|
+
npm install -g gitnexus@latest # global install avoids per-run npx reify
|
|
289
|
+
gitnexus analyze # if already installed globally
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
On **pnpm 10+**, lifecycle scripts are blocked unless explicitly allowed — the resolver adds `--allow-build` for `@ladybugdb/core`, `gitnexus`, and `tree-sitter` automatically when it picks `pnpm dlx`.
|
|
293
|
+
|
|
294
|
+
If you must stay on npm 11.x without pnpm, downgrade npm toolchain-wide (last resort):
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npm install -g npm@10.9.0
|
|
284
298
|
```
|
|
285
299
|
|
|
286
|
-
|
|
287
|
-
may help:
|
|
300
|
+
See [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939) and the original [#819](https://github.com/abhigyanpatwari/GitNexus/issues/819) thread. An older variant of this crash (tree-sitter-dart tarball URL) was fixed in gitnexus v1.6.2+ ([#820](https://github.com/abhigyanpatwari/GitNexus/pull/820)); if you still see install failures after upgrading, clear cache:
|
|
288
301
|
|
|
289
302
|
```bash
|
|
290
|
-
npm
|
|
291
|
-
|
|
303
|
+
npm cache clean --force
|
|
304
|
+
npx gitnexus@latest analyze
|
|
292
305
|
```
|
|
293
306
|
|
|
294
307
|
### `ERR_DLOPEN_FAILED` / `lbugjs.node` missing (pnpm dlx, pnpx)
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -19,10 +19,11 @@ export interface AIContextOptions {
|
|
|
19
19
|
noStats?: boolean;
|
|
20
20
|
skipSkills?: boolean;
|
|
21
21
|
}
|
|
22
|
+
export declare function generateGitNexusContent(projectName: string, stats: RepoStats, generatedSkills?: GeneratedSkillInfo[], groupNames?: string[], noStats?: boolean, skipSkills?: boolean, runnerPath?: string): string;
|
|
22
23
|
/**
|
|
23
24
|
* Generate AI context files after indexing
|
|
24
25
|
*/
|
|
25
|
-
export declare function generateAIContextFiles(repoPath: string,
|
|
26
|
+
export declare function generateAIContextFiles(repoPath: string, storagePath: string, projectName: string, stats: RepoStats, generatedSkills?: GeneratedSkillInfo[], options?: AIContextOptions): Promise<{
|
|
26
27
|
files: string[];
|
|
27
28
|
}>;
|
|
28
29
|
export {};
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -68,7 +68,11 @@ async function findGroupsContainingRegistryName(registryName) {
|
|
|
68
68
|
}
|
|
69
69
|
return hits;
|
|
70
70
|
}
|
|
71
|
-
function generateGitNexusContent(projectName, stats, generatedSkills, groupNames, noStats, skipSkills
|
|
71
|
+
export function generateGitNexusContent(projectName, stats, generatedSkills, groupNames, noStats, skipSkills,
|
|
72
|
+
// Project-relative path to the runner `gitnexus analyze` drops next to the
|
|
73
|
+
// index (#1945). Referenced by docs so a single CLI-neutral command resolves
|
|
74
|
+
// the available runner (global `gitnexus` → `pnpm dlx` → `npx`) at call time.
|
|
75
|
+
runnerPath = '.gitnexus/run.cjs') {
|
|
72
76
|
const generatedRows = generatedSkills && generatedSkills.length > 0
|
|
73
77
|
? generatedSkills
|
|
74
78
|
.map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`)
|
|
@@ -93,12 +97,20 @@ function generateGitNexusContent(projectName, stats, generatedSkills, groupNames
|
|
|
93
97
|
|------|---------------------|
|
|
94
98
|
${tableBody}`
|
|
95
99
|
: '';
|
|
100
|
+
// Docs reference the project-local runner `gitnexus analyze` writes (#1945):
|
|
101
|
+
// a single, CLI-neutral, machine-independent command (no per-machine churn,
|
|
102
|
+
// #1706) that auto-selects the available runner at call time. Kept terse to
|
|
103
|
+
// stay under the CLAUDE.md block token budget (#856); the cli skill carries the
|
|
104
|
+
// full bootstrap + npm-11 fallback (`node.target is null` npx install crash).
|
|
105
|
+
const runner = `node ${runnerPath}`;
|
|
106
|
+
const bootstrapNote = `No \`${runnerPath}\` yet? \`npx gitnexus analyze\` ` +
|
|
107
|
+
'(npm 11 crash → `npm i -g gitnexus`; #1939).';
|
|
96
108
|
return `${GITNEXUS_START_MARKER}
|
|
97
109
|
# GitNexus — Code Intelligence
|
|
98
110
|
|
|
99
111
|
This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows)`}. Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
|
100
112
|
|
|
101
|
-
>
|
|
113
|
+
> Index stale? Run \`${runner} analyze\` from the project root — it auto-selects an available runner. ${bootstrapNote}
|
|
102
114
|
|
|
103
115
|
## Always Do
|
|
104
116
|
|
|
@@ -127,7 +139,7 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
|
|
|
127
139
|
${groupNames && groupNames.length > 0
|
|
128
140
|
? `## Cross-Repo Groups
|
|
129
141
|
|
|
130
|
-
This repository is listed under GitNexus **group(s): ${groupNames.join(', ')}** (see \`~/.gitnexus/groups/\`). For cross-repo analysis, use MCP tools \`impact\`, \`query\`, and \`context\` with \`repo\` set to \`@<groupName>\` or \`@<groupName>/<memberPath>\` (paths match keys in that group’s \`group.yaml\`). Use \`group_list\` / \`group_sync\` for membership and sync. From the
|
|
142
|
+
This repository is listed under GitNexus **group(s): ${groupNames.join(', ')}** (see \`~/.gitnexus/groups/\`). For cross-repo analysis, use MCP tools \`impact\`, \`query\`, and \`context\` with \`repo\` set to \`@<groupName>\` or \`@<groupName>/<memberPath>\` (paths match keys in that group’s \`group.yaml\`). Use \`group_list\` / \`group_sync\` for membership and sync. From the project root: \`${runner} group list\`, \`${runner} group sync <name>\`, \`${runner} group impact <name> --target <symbol> --repo <group-path>\` (the \`${runnerPath}\` path is repo-root-relative).
|
|
131
143
|
|
|
132
144
|
`
|
|
133
145
|
: ''}${skillsTable
|
|
@@ -302,9 +314,24 @@ Use GitNexus tools to accomplish this task.
|
|
|
302
314
|
/**
|
|
303
315
|
* Generate AI context files after indexing
|
|
304
316
|
*/
|
|
305
|
-
export async function generateAIContextFiles(repoPath,
|
|
317
|
+
export async function generateAIContextFiles(repoPath, storagePath, projectName, stats, generatedSkills, options) {
|
|
306
318
|
const groupNames = await findGroupsContainingRegistryName(projectName);
|
|
307
|
-
|
|
319
|
+
// Drop a project-local runner next to the index (#1945) so the generated docs
|
|
320
|
+
// can reference one CLI-neutral command that resolves the available runner at
|
|
321
|
+
// call time. It is a copy of the canonical self-contained resolver, which the
|
|
322
|
+
// CLI and hooks already share; failure to copy is non-fatal (docs carry a
|
|
323
|
+
// bootstrap fallback). `runnerPath` is project-relative with POSIX separators
|
|
324
|
+
// so the emitted command is identical across platforms.
|
|
325
|
+
const runnerPath = path.relative(repoPath, path.join(storagePath, 'run.cjs')).replace(/\\/g, '/');
|
|
326
|
+
try {
|
|
327
|
+
const runnerSrc = path.join(__dirname, '..', '..', 'hooks', 'claude', 'resolve-analyze-cmd.cjs');
|
|
328
|
+
await fs.mkdir(storagePath, { recursive: true });
|
|
329
|
+
await fs.copyFile(runnerSrc, path.join(storagePath, 'run.cjs'));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
logger.warn(`Could not write GitNexus runner to ${runnerPath}: ${String(err)}`);
|
|
333
|
+
}
|
|
334
|
+
const content = generateGitNexusContent(projectName, stats, generatedSkills, groupNames, options?.noStats, options?.skipSkills, runnerPath);
|
|
308
335
|
const createdFiles = [];
|
|
309
336
|
if (!options?.skipAgentsMd) {
|
|
310
337
|
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
|
package/dist/cli/analyze.js
CHANGED
|
@@ -23,6 +23,7 @@ import fs from 'fs/promises';
|
|
|
23
23
|
import { cliError } from './cli-message.js';
|
|
24
24
|
import { formatElapsed } from './format-elapsed.js';
|
|
25
25
|
import { isHfDownloadFailure } from '../core/embeddings/hf-env.js';
|
|
26
|
+
import { warnIfNpm11NpxRisk } from './resolve-invocation.js';
|
|
26
27
|
// Capture stderr.write at module load BEFORE anything (LadybugDB native
|
|
27
28
|
// init, progress bar, console redirection) can monkey-patch it. The
|
|
28
29
|
// fatal handlers below MUST reach the user even when the analyze path
|
|
@@ -433,6 +434,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
433
434
|
// async error that escapes the try/catch below (#1169) surfaces with
|
|
434
435
|
// a stack trace and a non-zero exit code instead of a silent exit 0.
|
|
435
436
|
installFatalHandlers();
|
|
437
|
+
// npm-11 npx-crash nudge (#1939). Runs here, after the heap re-exec guard,
|
|
438
|
+
// so it fires once in the working process and never on the lazy-startup path
|
|
439
|
+
// of other commands (e.g. `gitnexus mcp`).
|
|
440
|
+
warnIfNpm11NpxRisk();
|
|
436
441
|
// Snapshot the GITNEXUS_* env vars that the impl writes for downstream
|
|
437
442
|
// consumption, so they don't leak across `analyzeCommand` invocations in
|
|
438
443
|
// programmatic callers (tests, long-running hosts). `process.exit(0)` on
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm 11.x npx-install-crash nudge for the `analyze` command (#1939).
|
|
3
|
+
*
|
|
4
|
+
* The gitnexus/pnpm/npx selection itself lives in the canonical hook helper
|
|
5
|
+
* (hooks/claude/resolve-analyze-cmd.cjs) — self-contained CJS because the copied
|
|
6
|
+
* hook runtime cannot import from the package. We reuse it here via createRequire
|
|
7
|
+
* instead of re-implementing it, so there is one source of truth for the
|
|
8
|
+
* invocation decision. This module adds only the npm-version probe and the
|
|
9
|
+
* warning, which are CLI-only. The relative path resolves identically from
|
|
10
|
+
* src/cli/ (tsx, vitest) and dist/cli/ (shipped), since both sit one level under
|
|
11
|
+
* the package root and `hooks/` is published.
|
|
12
|
+
*/
|
|
13
|
+
declare const NPX_REF: string;
|
|
14
|
+
export { NPX_REF };
|
|
15
|
+
export declare function getNpmMajorVersion(): number | null;
|
|
16
|
+
/**
|
|
17
|
+
* One-line stderr nudge when an npm 11+ user is on the npx install path (#1939).
|
|
18
|
+
* Skipped when a global `gitnexus` or `pnpm` is already preferred, so it never
|
|
19
|
+
* nags users who are not exposed to the npx/arborist crash.
|
|
20
|
+
*/
|
|
21
|
+
export declare function warnIfNpm11NpxRisk(): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm 11.x npx-install-crash nudge for the `analyze` command (#1939).
|
|
3
|
+
*
|
|
4
|
+
* The gitnexus/pnpm/npx selection itself lives in the canonical hook helper
|
|
5
|
+
* (hooks/claude/resolve-analyze-cmd.cjs) — self-contained CJS because the copied
|
|
6
|
+
* hook runtime cannot import from the package. We reuse it here via createRequire
|
|
7
|
+
* instead of re-implementing it, so there is one source of truth for the
|
|
8
|
+
* invocation decision. This module adds only the npm-version probe and the
|
|
9
|
+
* warning, which are CLI-only. The relative path resolves identically from
|
|
10
|
+
* src/cli/ (tsx, vitest) and dist/cli/ (shipped), since both sit one level under
|
|
11
|
+
* the package root and `hooks/` is published.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
const { resolveInvocationMode, formatDocumentationDlxCommand, NPX_REF } = createRequire(import.meta.url)('../../hooks/claude/resolve-analyze-cmd.cjs');
|
|
16
|
+
// Fail loud at module load if the canonical cjs export shape drifts (e.g. a
|
|
17
|
+
// renamed export), rather than as a late TypeError inside warnIfNpm11NpxRisk.
|
|
18
|
+
if (typeof resolveInvocationMode !== 'function' ||
|
|
19
|
+
typeof formatDocumentationDlxCommand !== 'function' ||
|
|
20
|
+
typeof NPX_REF !== 'string') {
|
|
21
|
+
throw new Error('resolve-analyze-cmd.cjs must export resolveInvocationMode (function), formatDocumentationDlxCommand (function), and NPX_REF (string)');
|
|
22
|
+
}
|
|
23
|
+
export { NPX_REF };
|
|
24
|
+
// Re-implemented here (rather than reusing the cjs export) so vitest's
|
|
25
|
+
// `vi.mock('node:child_process')` intercepts it — the cjs uses bare
|
|
26
|
+
// `require('child_process')`, which the mock cannot reach. Timeout matches the
|
|
27
|
+
// cjs PROBE_TIMEOUT_MS (1s) so this CLI probe shares the same hook-budget cap;
|
|
28
|
+
// `npm --version` is a sub-second local call.
|
|
29
|
+
export function getNpmMajorVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const output = execFileSync('npm', ['--version'], {
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
timeout: 1000,
|
|
34
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
// Windows `npm` is a `.cmd` shim; without a shell execFileSync ENOENTs
|
|
37
|
+
// (CVE-2024-27980) and the npm-11 npx-crash warning below would never
|
|
38
|
+
// fire on Windows. Mirrors probeVersion in resolve-analyze-cmd.cjs.
|
|
39
|
+
shell: process.platform === 'win32',
|
|
40
|
+
});
|
|
41
|
+
// Read the first version-shaped line so a Corepack/update banner on stdout
|
|
42
|
+
// doesn't defeat the parse (mirrors the cjs probeVersion hardening).
|
|
43
|
+
const major = output
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((l) => l.trim())
|
|
46
|
+
.find((l) => /^v?\d+\./.test(l))
|
|
47
|
+
?.match(/^v?(\d+)\./);
|
|
48
|
+
return major ? Number(major[1]) : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One-line stderr nudge when an npm 11+ user is on the npx install path (#1939).
|
|
56
|
+
* Skipped when a global `gitnexus` or `pnpm` is already preferred, so it never
|
|
57
|
+
* nags users who are not exposed to the npx/arborist crash.
|
|
58
|
+
*/
|
|
59
|
+
export function warnIfNpm11NpxRisk() {
|
|
60
|
+
if (resolveInvocationMode() !== 'npx')
|
|
61
|
+
return;
|
|
62
|
+
const major = getNpmMajorVersion();
|
|
63
|
+
if (major === null || major < 11)
|
|
64
|
+
return;
|
|
65
|
+
process.stderr.write(`Warning: npm ${major}.x can crash while installing gitnexus via npx ` +
|
|
66
|
+
`(npm/arborist "node.target is null"). Prefer: ${formatDocumentationDlxCommand('analyze')} ` +
|
|
67
|
+
`or npm install -g ${NPX_REF}. See https://github.com/abhigyanpatwari/GitNexus/issues/1939\n`);
|
|
68
|
+
}
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -5,4 +5,37 @@
|
|
|
5
5
|
* Detects installed AI editors and writes the appropriate MCP config
|
|
6
6
|
* so the GitNexus MCP server is available in all projects.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Build the `command` string written into an editor's hook settings, which the
|
|
10
|
+
* editor shell-evaluates. `hookPath` is already forward-slash-normalized.
|
|
11
|
+
*
|
|
12
|
+
* On POSIX, single-quote the path: a single-quoted shell string expands nothing,
|
|
13
|
+
* so spaces and metacharacters ($, backtick, ;, |, &, newline, parens) in the
|
|
14
|
+
* install path cannot run as commands. The only character needing escaping
|
|
15
|
+
* inside single quotes is the single quote, via the standard `'\''` idiom
|
|
16
|
+
* (close, literal-quote, reopen). The previous double-quoted `node "..."` form
|
|
17
|
+
* left $/backtick live — a code-execution risk for an adversarial $HOME.
|
|
18
|
+
*
|
|
19
|
+
* On Windows, filenames cannot contain these POSIX metacharacters and the path
|
|
20
|
+
* is forward-slashed, so keep the double-quoted form with backslash-then-quote
|
|
21
|
+
* escaping (CodeQL js/incomplete-sanitization safe ordering).
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatHookCommand(hookPath: string, isWindows?: boolean): string;
|
|
24
|
+
interface SetupResult {
|
|
25
|
+
configured: string[];
|
|
26
|
+
skipped: string[];
|
|
27
|
+
errors: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Copy the shared hook helpers from `srcDir` into `destDir`. The adapters
|
|
31
|
+
* top-level `require()` the `.cjs` helpers, so a missing required helper makes
|
|
32
|
+
* the installed hook crash with MODULE_NOT_FOUND. A failed copy is recorded as a
|
|
33
|
+
* setup error, and the names of any failed REQUIRED helpers are returned so the
|
|
34
|
+
* caller can fail closed (skip hook registration) instead of registering a hook
|
|
35
|
+
* that crashes at runtime. `win-rm-list-json.ps1` is best-effort — its absence is
|
|
36
|
+
* recorded but does not gate registration. Both the Claude and Antigravity
|
|
37
|
+
* install paths copy this same list from hooks/claude/ (the canonical source).
|
|
38
|
+
*/
|
|
39
|
+
export declare function copyHookHelpers(srcDir: string, destDir: string, label: string, result: SetupResult): Promise<string[]>;
|
|
8
40
|
export declare const setupCommand: () => Promise<void>;
|
|
41
|
+
export {};
|
package/dist/cli/setup.js
CHANGED
|
@@ -28,7 +28,38 @@ const _pkg = _require('../../package.json');
|
|
|
28
28
|
if (typeof _pkg.version !== 'string' || !_pkg.version) {
|
|
29
29
|
throw new Error('gitnexus/package.json#version is missing or not a string — cannot generate MCP fallback config.');
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// Version-pinned ref for the persisted MCP entry — deliberately distinct from
|
|
32
|
+
// the cjs's exported `gitnexus@latest` hint ref (resolve-analyze-cmd.cjs); the
|
|
33
|
+
// two are not unified (see the comment above and that file's MCP_PINNED_REF).
|
|
34
|
+
const MCP_PINNED_REF = `gitnexus@${_pkg.version}`;
|
|
35
|
+
/**
|
|
36
|
+
* Build the `command` string written into an editor's hook settings, which the
|
|
37
|
+
* editor shell-evaluates. `hookPath` is already forward-slash-normalized.
|
|
38
|
+
*
|
|
39
|
+
* On POSIX, single-quote the path: a single-quoted shell string expands nothing,
|
|
40
|
+
* so spaces and metacharacters ($, backtick, ;, |, &, newline, parens) in the
|
|
41
|
+
* install path cannot run as commands. The only character needing escaping
|
|
42
|
+
* inside single quotes is the single quote, via the standard `'\''` idiom
|
|
43
|
+
* (close, literal-quote, reopen). The previous double-quoted `node "..."` form
|
|
44
|
+
* left $/backtick live — a code-execution risk for an adversarial $HOME.
|
|
45
|
+
*
|
|
46
|
+
* On Windows, filenames cannot contain these POSIX metacharacters and the path
|
|
47
|
+
* is forward-slashed, so keep the double-quoted form with backslash-then-quote
|
|
48
|
+
* escaping (CodeQL js/incomplete-sanitization safe ordering).
|
|
49
|
+
*/
|
|
50
|
+
export function formatHookCommand(hookPath, isWindows = process.platform === 'win32') {
|
|
51
|
+
if (isWindows) {
|
|
52
|
+
const escaped = hookPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
53
|
+
return `node "${escaped}"`;
|
|
54
|
+
}
|
|
55
|
+
return `node '${hookPath.replace(/'/g, "'\\''")}'`;
|
|
56
|
+
}
|
|
57
|
+
// The exact source line each hook adapter ships, rewritten at install time to
|
|
58
|
+
// point cliPath at the installed CLI. Kept as a named constant so the install
|
|
59
|
+
// patch and its drift guard reference one string — if the adapter source ever
|
|
60
|
+
// changes this literal, the guard records an actionable error instead of
|
|
61
|
+
// silently shipping a hook with an unresolved relative cliPath.
|
|
62
|
+
const CLI_PATH_SOURCE_LITERAL = "let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');";
|
|
32
63
|
/**
|
|
33
64
|
* Resolve the absolute path to the `gitnexus` binary if it's installed
|
|
34
65
|
* globally (or via npm -g / yarn global). Returns null when not found.
|
|
@@ -83,12 +114,12 @@ function getMcpEntry() {
|
|
|
83
114
|
if (process.platform === 'win32') {
|
|
84
115
|
return {
|
|
85
116
|
command: 'cmd',
|
|
86
|
-
args: ['/c', 'npx', '-y',
|
|
117
|
+
args: ['/c', 'npx', '-y', MCP_PINNED_REF, 'mcp'],
|
|
87
118
|
};
|
|
88
119
|
}
|
|
89
120
|
return {
|
|
90
121
|
command: 'npx',
|
|
91
|
-
args: ['-y',
|
|
122
|
+
args: ['-y', MCP_PINNED_REF, 'mcp'],
|
|
92
123
|
};
|
|
93
124
|
}
|
|
94
125
|
/**
|
|
@@ -101,9 +132,9 @@ function getOpenCodeMcpEntry() {
|
|
|
101
132
|
return { type: 'local', command: [bin, 'mcp'] };
|
|
102
133
|
}
|
|
103
134
|
if (process.platform === 'win32') {
|
|
104
|
-
return { type: 'local', command: ['cmd', '/c', 'npx', '-y',
|
|
135
|
+
return { type: 'local', command: ['cmd', '/c', 'npx', '-y', MCP_PINNED_REF, 'mcp'] };
|
|
105
136
|
}
|
|
106
|
-
return { type: 'local', command: ['npx', '-y',
|
|
137
|
+
return { type: 'local', command: ['npx', '-y', MCP_PINNED_REF, 'mcp'] };
|
|
107
138
|
}
|
|
108
139
|
/**
|
|
109
140
|
* Detect indentation style from file content.
|
|
@@ -281,6 +312,42 @@ async function mergeHooksJsonc(filePath, entries) {
|
|
|
281
312
|
await fs.writeFile(filePath, current, 'utf-8');
|
|
282
313
|
return true;
|
|
283
314
|
}
|
|
315
|
+
const HOOK_HELPERS = [
|
|
316
|
+
'hook-lock.cjs',
|
|
317
|
+
'hook-db-lock-probe.cjs',
|
|
318
|
+
'win-rm-list-json.ps1',
|
|
319
|
+
'resolve-analyze-cmd.cjs',
|
|
320
|
+
];
|
|
321
|
+
// win-rm-list-json.ps1 is best-effort: it is read (not require()'d) by
|
|
322
|
+
// hook-db-lock-probe.cjs only on Windows, and that probe fails open when the
|
|
323
|
+
// script is absent. Every other helper is top-level require()'d by the adapters,
|
|
324
|
+
// so its absence crashes the installed hook — those are the ones a failed copy
|
|
325
|
+
// must gate hook registration on (see copyHookHelpers' return value).
|
|
326
|
+
const BEST_EFFORT_HOOK_HELPERS = new Set(['win-rm-list-json.ps1']);
|
|
327
|
+
/**
|
|
328
|
+
* Copy the shared hook helpers from `srcDir` into `destDir`. The adapters
|
|
329
|
+
* top-level `require()` the `.cjs` helpers, so a missing required helper makes
|
|
330
|
+
* the installed hook crash with MODULE_NOT_FOUND. A failed copy is recorded as a
|
|
331
|
+
* setup error, and the names of any failed REQUIRED helpers are returned so the
|
|
332
|
+
* caller can fail closed (skip hook registration) instead of registering a hook
|
|
333
|
+
* that crashes at runtime. `win-rm-list-json.ps1` is best-effort — its absence is
|
|
334
|
+
* recorded but does not gate registration. Both the Claude and Antigravity
|
|
335
|
+
* install paths copy this same list from hooks/claude/ (the canonical source).
|
|
336
|
+
*/
|
|
337
|
+
export async function copyHookHelpers(srcDir, destDir, label, result) {
|
|
338
|
+
const failedRequired = [];
|
|
339
|
+
for (const helper of HOOK_HELPERS) {
|
|
340
|
+
try {
|
|
341
|
+
await fs.copyFile(path.join(srcDir, helper), path.join(destDir, helper));
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
result.errors.push(`${label}: failed to copy ${helper} — hook may crash at runtime`);
|
|
345
|
+
if (!BEST_EFFORT_HOOK_HELPERS.has(helper))
|
|
346
|
+
failedRequired.push(helper);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return failedRequired;
|
|
350
|
+
}
|
|
284
351
|
/**
|
|
285
352
|
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
286
353
|
* Merges hook config without overwriting existing hooks, preserving
|
|
@@ -304,37 +371,32 @@ async function installClaudeCodeHooks(result) {
|
|
|
304
371
|
const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
|
|
305
372
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
306
373
|
const jsonCli = JSON.stringify(normalizedCli);
|
|
307
|
-
|
|
374
|
+
if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
|
|
375
|
+
result.errors.push('Claude Code hooks: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.');
|
|
376
|
+
}
|
|
377
|
+
content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
|
|
308
378
|
await fs.writeFile(dest, content, 'utf-8');
|
|
309
379
|
}
|
|
310
380
|
catch {
|
|
311
381
|
// Script not found in source — skip
|
|
312
382
|
}
|
|
383
|
+
// Fail closed: registering the hook without its adapter would crash on every
|
|
384
|
+
// tool invocation. Mirrors the Antigravity adapter guard below (this path
|
|
385
|
+
// previously registered regardless of whether the adapter wrote).
|
|
313
386
|
try {
|
|
314
|
-
await fs.
|
|
387
|
+
await fs.access(dest);
|
|
315
388
|
}
|
|
316
389
|
catch {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
await fs.copyFile(path.join(pluginHooksPath, 'hook-db-lock-probe.cjs'), path.join(destHooksDir, 'hook-db-lock-probe.cjs'));
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
// Helper not found in source — skip
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
await fs.copyFile(path.join(pluginHooksPath, 'win-rm-list-json.ps1'), path.join(destHooksDir, 'win-rm-list-json.ps1'));
|
|
390
|
+
result.errors.push('Claude Code hooks: adapter script was not installed — skipping hook registration');
|
|
391
|
+
return;
|
|
327
392
|
}
|
|
328
|
-
|
|
329
|
-
|
|
393
|
+
const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, 'Claude Code hooks', result);
|
|
394
|
+
if (failedRequired.length > 0) {
|
|
395
|
+
result.errors.push(`Claude Code hooks: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
|
|
396
|
+
return;
|
|
330
397
|
}
|
|
331
398
|
const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
|
|
332
|
-
|
|
333
|
-
// The previous shape `replace(/"/g, '\\"')` alone would let `path\with"quote`
|
|
334
|
-
// become `path\with\"quote`, where the trailing `\` before `"` could
|
|
335
|
-
// unescape the quote inside the surrounding double-quoted shell context.
|
|
336
|
-
const escapedHookPath = hookPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
337
|
-
const hookCmd = `node "${escapedHookPath}"`;
|
|
399
|
+
const hookCmd = formatHookCommand(hookPath);
|
|
338
400
|
// Check which hook events need entries (idempotent: skip if already registered)
|
|
339
401
|
const parsed = await (async () => {
|
|
340
402
|
try {
|
|
@@ -483,7 +545,10 @@ async function installAntigravityHooks(result) {
|
|
|
483
545
|
const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
|
|
484
546
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
485
547
|
const jsonCli = JSON.stringify(normalizedCli);
|
|
486
|
-
|
|
548
|
+
if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
|
|
549
|
+
result.errors.push('Antigravity hooks: gitnexus-antigravity-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.');
|
|
550
|
+
}
|
|
551
|
+
content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
|
|
487
552
|
await fs.writeFile(adapterDest, content, 'utf-8');
|
|
488
553
|
}
|
|
489
554
|
catch {
|
|
@@ -503,17 +568,13 @@ async function installAntigravityHooks(result) {
|
|
|
503
568
|
// required by hook-db-lock-probe.cjs on Windows — without it, the MCP
|
|
504
569
|
// server ownership probe silently fails open and the hook may contend
|
|
505
570
|
// with the MCP server on the LadybugDB.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
catch {
|
|
511
|
-
result.errors.push(`Antigravity hooks: failed to copy ${helper} — hook may crash at runtime`);
|
|
512
|
-
}
|
|
571
|
+
const failedRequired = await copyHookHelpers(pluginClaudeDir, destHooksDir, 'Antigravity hooks', result);
|
|
572
|
+
if (failedRequired.length > 0) {
|
|
573
|
+
result.errors.push(`Antigravity hooks: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
|
|
574
|
+
return;
|
|
513
575
|
}
|
|
514
576
|
const hookPath = path.join(destHooksDir, 'gitnexus-antigravity-hook.cjs').replace(/\\/g, '/');
|
|
515
|
-
const
|
|
516
|
-
const hookCmd = `node "${escapedHookPath}"`;
|
|
577
|
+
const hookCmd = formatHookCommand(hookPath);
|
|
517
578
|
const parsed = await (async () => {
|
|
518
579
|
try {
|
|
519
580
|
const r = await fs.readFile(settingsPath, 'utf-8');
|
|
@@ -11,11 +11,35 @@ const PYTHON_SCOPE_QUERY = `
|
|
|
11
11
|
(module) @scope.module
|
|
12
12
|
(class_definition) @scope.class
|
|
13
13
|
(function_definition) @scope.function
|
|
14
|
+
(lambda) @scope.function
|
|
14
15
|
|
|
15
16
|
;; Declarations
|
|
16
17
|
(class_definition
|
|
17
18
|
name: (identifier) @declaration.name) @declaration.class
|
|
18
19
|
|
|
20
|
+
;; Heritage — bare identifier
|
|
21
|
+
;; NOTE: captures.ts on main already synthesizes @reference.inherits for
|
|
22
|
+
;; qualified bases via #1951/#1956. These @heritage.* patterns are redundant
|
|
23
|
+
;; with that synthesis but kept as documentation and a safety net for the
|
|
24
|
+
;; generic heritage extractor path. They produce topicOf edges that the
|
|
25
|
+
;; resolution pipeline ignores when the synthesis path wins.
|
|
26
|
+
(class_definition
|
|
27
|
+
name: (identifier) @heritage.class
|
|
28
|
+
superclasses: (argument_list
|
|
29
|
+
(identifier) @heritage.extends)) @heritage
|
|
30
|
+
|
|
31
|
+
;; Heritage — qualified base (module.Class)
|
|
32
|
+
(class_definition
|
|
33
|
+
name: (identifier) @heritage.class
|
|
34
|
+
superclasses: (argument_list
|
|
35
|
+
(attribute) @heritage.extends)) @heritage
|
|
36
|
+
|
|
37
|
+
;; Heritage — subscripted/generic base (Generic[T])
|
|
38
|
+
(class_definition
|
|
39
|
+
name: (identifier) @heritage.class
|
|
40
|
+
superclasses: (argument_list
|
|
41
|
+
(subscript) @heritage.extends)) @heritage
|
|
42
|
+
|
|
19
43
|
(function_definition
|
|
20
44
|
name: (identifier) @declaration.name) @declaration.function
|
|
21
45
|
|
|
@@ -232,6 +256,22 @@ const PYTHON_SCOPE_QUERY = `
|
|
|
232
256
|
name: (identifier) @type-binding.name
|
|
233
257
|
return_type: (type) @type-binding.type) @type-binding.return
|
|
234
258
|
|
|
259
|
+
;; Decorators — simple @decorator
|
|
260
|
+
(decorator
|
|
261
|
+
(identifier) @reference.name) @reference.call.free
|
|
262
|
+
|
|
263
|
+
;; Decorators — @obj.decorator (single attribute, identifier receiver)
|
|
264
|
+
(decorator
|
|
265
|
+
(attribute
|
|
266
|
+
object: (identifier) @reference.receiver
|
|
267
|
+
attribute: (identifier) @reference.name)) @reference.call.member
|
|
268
|
+
|
|
269
|
+
;; Decorators — @a.b.decorator (nested attributes)
|
|
270
|
+
(decorator
|
|
271
|
+
(attribute
|
|
272
|
+
object: (attribute) @reference.receiver
|
|
273
|
+
attribute: (identifier) @reference.name)) @reference.call.member
|
|
274
|
+
|
|
235
275
|
;; References — calls
|
|
236
276
|
(call
|
|
237
277
|
function: (identifier) @reference.name) @reference.call.free
|
|
@@ -25,6 +25,7 @@ const path = require('path');
|
|
|
25
25
|
const { spawnSync } = require('child_process');
|
|
26
26
|
const { acquireHookSlot } = require('./hook-lock.cjs');
|
|
27
27
|
const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
|
|
28
|
+
const { formatAnalyzeCommand } = require('./resolve-analyze-cmd.cjs');
|
|
28
29
|
|
|
29
30
|
function readInput() {
|
|
30
31
|
try {
|
|
@@ -315,7 +316,7 @@ function buildStaleIndexHint(gitNexusDir, cwd) {
|
|
|
315
316
|
|
|
316
317
|
if (currentHead === lastCommit) return '';
|
|
317
318
|
|
|
318
|
-
const analyzeCmd =
|
|
319
|
+
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
|
|
319
320
|
return (
|
|
320
321
|
`[GitNexus] index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
|
321
322
|
`Run \`${analyzeCmd}\` to refresh the knowledge graph.`
|
|
@@ -16,6 +16,7 @@ const path = require('path');
|
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
17
|
const { acquireHookSlot } = require('./hook-lock.cjs');
|
|
18
18
|
const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
|
|
19
|
+
const { formatAnalyzeCommand } = require('./resolve-analyze-cmd.cjs');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Read JSON input from stdin synchronously.
|
|
@@ -340,7 +341,7 @@ function handlePostToolUse(input) {
|
|
|
340
341
|
// If HEAD matches last indexed commit, no reindex needed
|
|
341
342
|
if (currentHead && currentHead === lastCommit) return;
|
|
342
343
|
|
|
343
|
-
const analyzeCmd =
|
|
344
|
+
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
|
|
344
345
|
sendHookResponse(
|
|
345
346
|
'PostToolUse',
|
|
346
347
|
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for how docs, hooks, and warnings invoke gitnexus.
|
|
3
|
+
*
|
|
4
|
+
* Automatically selects a working invocation path:
|
|
5
|
+
* 1. Global `gitnexus` on PATH (best — no install step)
|
|
6
|
+
* 2. npm 11+ with pnpm on PATH → `pnpm --allow-build=… dlx` (avoids the npx
|
|
7
|
+
* arborist crash *and* pnpm 10+ ignored-build-script failures, #1939)
|
|
8
|
+
* 3. npm < 11 with npm on PATH → `npx` (works; simpler than pnpm dlx)
|
|
9
|
+
* 4. pnpm-only → `pnpm --allow-build=… dlx`
|
|
10
|
+
* 5. Last resort → `npx` (warned on npm 11+ from analyze.ts)
|
|
11
|
+
*
|
|
12
|
+
* The `--allow-build` flags MUST precede the `dlx` token. pnpm < 10.14 keeps
|
|
13
|
+
* `dlx` in its argv escape list, so flags placed *after* `dlx` are parsed as
|
|
14
|
+
* package specs (ERR_PNPM_SPEC_NOT_SUPPORTED). The pre-`dlx` position parses
|
|
15
|
+
* into dlx's allow-build option and has been honored since pnpm 10.2.0 (#1939).
|
|
16
|
+
*
|
|
17
|
+
* This stays self-contained CJS because the Claude/Antigravity hooks run as
|
|
18
|
+
* standalone files copied into the user's hook dir, where no package import is
|
|
19
|
+
* available. The CLI reuses this module from src/cli/resolve-invocation.ts via
|
|
20
|
+
* createRequire rather than re-implementing it. Two committed copies must stay
|
|
21
|
+
* byte-identical (enforced by resolve-invocation.test.ts) — edit both together:
|
|
22
|
+
* gitnexus/hooks/claude/ (the canonical copy the CLI and `gitnexus setup` read)
|
|
23
|
+
* and gitnexus-claude-plugin/hooks/. A THIRD copy is written at runtime to
|
|
24
|
+
* `<repo>/.gitnexus/run.cjs` by `gitnexus analyze` (ai-context.ts) so docs can
|
|
25
|
+
* reference it directly via the `require.main === module` exec tail below; that
|
|
26
|
+
* copy is gitignored and refreshed on every analyze, so it cannot drift for long.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { execFileSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
const NPX_REF = 'gitnexus@latest';
|
|
32
|
+
|
|
33
|
+
// Native packages whose postinstall must run under pnpm 10+ (blocked by default).
|
|
34
|
+
const PNPM_ALLOW_BUILD_BASE = ['@ladybugdb/core', 'gitnexus', 'tree-sitter'];
|
|
35
|
+
const PNPM_ALLOW_BUILD_EMBEDDINGS = ['onnxruntime-node'];
|
|
36
|
+
|
|
37
|
+
// Probe timeout, kept under Claude Code's 10s hook budget. In a linked worktree
|
|
38
|
+
// the stale-index hook first runs `git rev-parse --git-common-dir` (~2s) and
|
|
39
|
+
// `git rev-parse HEAD` (~3s); the pnpm path then adds up to four 1s probes
|
|
40
|
+
// (which gitnexus, npm --version, which pnpm, pnpm --version), so the worst case
|
|
41
|
+
// is ~9s — within budget but tight. A healthy `which`/`where`/`--version`
|
|
42
|
+
// returns in well under a second, so the realistic cost is far lower.
|
|
43
|
+
const PROBE_TIMEOUT_MS = 1000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pick the best match from `where`/`which` output. A global `gitnexus` may be a
|
|
47
|
+
* `.cmd`/`.bat` (npm), a `.exe`, or an extensionless shim (Volta, scoop), so on
|
|
48
|
+
* Windows we prefer a recognized executable extension but accept any hit — the
|
|
49
|
+
* emitted hint is `gitnexus analyze` regardless of which shim resolves it. Pure
|
|
50
|
+
* and exported so the shim-matching can be unit-tested without spawning.
|
|
51
|
+
*/
|
|
52
|
+
function pickPathMatch(output, { isWin, gitnexusWrapper } = {}) {
|
|
53
|
+
const lines = output
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((l) => l.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
if (isWin && gitnexusWrapper) {
|
|
58
|
+
return lines.find((l) => /\.(cmd|bat|exe)$/i.test(l)) || lines[0] || null;
|
|
59
|
+
}
|
|
60
|
+
return lines[0] || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Absolute path to `command` on PATH, or null. `gitnexusWrapper` enables the Windows shim match. */
|
|
64
|
+
function resolveOnPath(command, gitnexusWrapper = false) {
|
|
65
|
+
const isWin = process.platform === 'win32';
|
|
66
|
+
try {
|
|
67
|
+
const output = execFileSync(isWin ? 'where' : 'which', [command], {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
70
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
return pickPathMatch(output, { isWin, gitnexusWrapper });
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// One spawn of `<command> --version` → { major, minor } (each null when
|
|
80
|
+
// unreadable). Version injection happens at the resolver seam (getNpmMajorVersion
|
|
81
|
+
// / formatPnpmAllowBuildArgs), so this stays a pure real-process probe.
|
|
82
|
+
function probeVersion(command) {
|
|
83
|
+
try {
|
|
84
|
+
const output = execFileSync(command, ['--version'], {
|
|
85
|
+
encoding: 'utf-8',
|
|
86
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
87
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
// On Windows, npm/pnpm resolve to `.cmd` shims; execFileSync does no
|
|
90
|
+
// PATHEXT resolution and Node refuses to spawn `.cmd`/`.bat` without a
|
|
91
|
+
// shell (CVE-2024-27980), so a bare `<command> --version` ENOENTs and the
|
|
92
|
+
// probe would wrongly report a present tool as absent. A shell lets the OS
|
|
93
|
+
// resolve the shim. POSIX needs no shell (direct PATH lookup works).
|
|
94
|
+
shell: process.platform === 'win32',
|
|
95
|
+
});
|
|
96
|
+
// Find the first line that starts with a version token (`MAJOR.MINOR`,
|
|
97
|
+
// optional `v` prefix) rather than splitting the whole output — pnpm/npm
|
|
98
|
+
// under Corepack or with an update notice can print a banner line on stdout
|
|
99
|
+
// before the version (stderr is already dropped via the stdio config).
|
|
100
|
+
const versionLine = output
|
|
101
|
+
.split('\n')
|
|
102
|
+
.map((l) => l.trim())
|
|
103
|
+
.find((l) => /^v?\d+\.\d+/.test(l));
|
|
104
|
+
const match = versionLine ? versionLine.match(/^v?(\d+)\.(\d+)/) : null;
|
|
105
|
+
return {
|
|
106
|
+
major: match ? Number(match[1]) : null,
|
|
107
|
+
minor: match ? Number(match[2]) : null,
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return { major: null, minor: null };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `deps` is the single injection seam: an explicitly provided key — including a
|
|
115
|
+
// `null` value, detected via `in` — is honored as-is so tests can simulate an
|
|
116
|
+
// absent tool without spawning; an absent key falls through to the real probe.
|
|
117
|
+
function getNpmMajorVersion(deps = {}) {
|
|
118
|
+
return 'npmMajor' in deps ? deps.npmMajor : probeVersion('npm').major;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `--allow-build` flags for the pre-`dlx` position. Emitted for pnpm >= 10.2
|
|
123
|
+
* (where the flag exists, and pnpm 10+ blocks build scripts by default). Omitted
|
|
124
|
+
* below 10.2: pnpm < 10 runs build scripts anyway, and pnpm 10.0/10.1 lack the
|
|
125
|
+
* flag (it would be rejected as an unknown option). `alwaysAllowBuild` forces the
|
|
126
|
+
* flags for committed documentation, which cannot probe the reader's pnpm.
|
|
127
|
+
*/
|
|
128
|
+
function formatPnpmAllowBuildArgs(options = {}, deps = {}) {
|
|
129
|
+
if (!options.alwaysAllowBuild) {
|
|
130
|
+
const { major, minor } =
|
|
131
|
+
'pnpmMajor' in deps
|
|
132
|
+
? { major: deps.pnpmMajor, minor: 'pnpmMinor' in deps ? deps.pnpmMinor : null }
|
|
133
|
+
: probeVersion('pnpm');
|
|
134
|
+
const lacksAllowBuild =
|
|
135
|
+
major !== null && (major < 10 || (major === 10 && minor !== null && minor < 2));
|
|
136
|
+
if (lacksAllowBuild) return [];
|
|
137
|
+
}
|
|
138
|
+
const pkgs = [...PNPM_ALLOW_BUILD_BASE];
|
|
139
|
+
if (options.embeddings) pkgs.push(...PNPM_ALLOW_BUILD_EMBEDDINGS);
|
|
140
|
+
return pkgs.map((p) => `--allow-build=${p}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Fixed install-free command for committed AGENTS.md / SKILL.md (pnpm >= 10.2). */
|
|
144
|
+
function formatDocumentationDlxCommand(gitnexusArgs, options = {}) {
|
|
145
|
+
const flags = formatPnpmAllowBuildArgs({ ...options, alwaysAllowBuild: true }).join(' ');
|
|
146
|
+
const prefix = flags ? `${flags} ` : '';
|
|
147
|
+
return `pnpm ${prefix}dlx ${NPX_REF} ${gitnexusArgs}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve `gitnexus` | `pnpm` | `npx`. `GITNEXUS_INVOCATION` forces a mode
|
|
152
|
+
* (test/escape hatch). `probe` is injectable so the preference order can be
|
|
153
|
+
* unit-tested without spawning; it defaults to the real PATH probe. `deps` can
|
|
154
|
+
* inject `{ npmMajor, pnpmMajor }` for tests.
|
|
155
|
+
*/
|
|
156
|
+
function resolveInvocationMode(probe = resolveOnPath, deps = {}) {
|
|
157
|
+
const forced = process.env.GITNEXUS_INVOCATION?.trim().toLowerCase();
|
|
158
|
+
if (forced === 'gitnexus' || forced === 'pnpm' || forced === 'npx') {
|
|
159
|
+
return forced;
|
|
160
|
+
}
|
|
161
|
+
if (probe('gitnexus', true)) return 'gitnexus';
|
|
162
|
+
|
|
163
|
+
const npmMajor = getNpmMajorVersion(deps);
|
|
164
|
+
// pnpm presence: prefer an explicit `pnpmPresent` flag (set by
|
|
165
|
+
// formatAnalyzeCommand, which falls back to a PATH probe when the version is
|
|
166
|
+
// unreadable) so a present-but-unparseable pnpm — slow probe, Corepack
|
|
167
|
+
// banner — still selects pnpm instead of the npx crash path. Otherwise an
|
|
168
|
+
// injected version (a successful `pnpm --version` proves presence)
|
|
169
|
+
// short-circuits the `which pnpm` probe; failing both, fall back to PATH.
|
|
170
|
+
const hasPnpm =
|
|
171
|
+
'pnpmPresent' in deps
|
|
172
|
+
? deps.pnpmPresent
|
|
173
|
+
: 'pnpmMajor' in deps
|
|
174
|
+
? deps.pnpmMajor !== null
|
|
175
|
+
: Boolean(probe('pnpm'));
|
|
176
|
+
|
|
177
|
+
// npm 11+ npx install crash (#1939) — prefer pnpm dlx when available.
|
|
178
|
+
if (hasPnpm && npmMajor !== null && npmMajor >= 11) return 'pnpm';
|
|
179
|
+
// npm 10 and earlier: npx works; prefer it over pnpm dlx when npm is present.
|
|
180
|
+
if (npmMajor !== null && npmMajor < 11) return 'npx';
|
|
181
|
+
// npm absent or unreadable — use pnpm if present (with allow-build flags).
|
|
182
|
+
if (hasPnpm) return 'pnpm';
|
|
183
|
+
|
|
184
|
+
return 'npx';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatPnpmDlxCommand(gitnexusArgs, options = {}, deps = {}) {
|
|
188
|
+
const flags = formatPnpmAllowBuildArgs(options, deps).join(' ');
|
|
189
|
+
const prefix = flags ? `${flags} ` : '';
|
|
190
|
+
return `pnpm ${prefix}dlx ${NPX_REF} ${gitnexusArgs}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatAnalyzeCommand(options = {}, deps = {}) {
|
|
194
|
+
const suffix = options.embeddings ? ' --embeddings' : '';
|
|
195
|
+
// Keep the stale-index hook budget tight by querying each tool at most once.
|
|
196
|
+
// A memoized PATH probe is shared with resolveInvocationMode (so `gitnexus`
|
|
197
|
+
// isn't probed twice), and pnpm's version is captured by a single
|
|
198
|
+
// `pnpm --version` that proves both presence (for mode resolution) and
|
|
199
|
+
// version (for the allow-build gate) — replacing the former `which pnpm` +
|
|
200
|
+
// `pnpm --version` double spawn. Injected deps (tests) and forced/global
|
|
201
|
+
// modes skip the pnpm probe.
|
|
202
|
+
const cache = new Map();
|
|
203
|
+
const probe = (command, gitnexusWrapper) => {
|
|
204
|
+
const key = `${command}:${gitnexusWrapper ? 1 : 0}`;
|
|
205
|
+
if (!cache.has(key)) cache.set(key, resolveOnPath(command, gitnexusWrapper));
|
|
206
|
+
return cache.get(key);
|
|
207
|
+
};
|
|
208
|
+
let resolved = deps;
|
|
209
|
+
if (!('pnpmMajor' in deps)) {
|
|
210
|
+
const forced = process.env.GITNEXUS_INVOCATION?.trim().toLowerCase();
|
|
211
|
+
// pnpm is only consulted when no non-pnpm mode is already certain: forced
|
|
212
|
+
// gitnexus/npx never use pnpm, and a present global gitnexus wins outright.
|
|
213
|
+
const mightUsePnpm = forced === 'pnpm' || (forced !== 'gitnexus' && forced !== 'npx');
|
|
214
|
+
if (mightUsePnpm && (forced === 'pnpm' || !probe('gitnexus', true))) {
|
|
215
|
+
const { major, minor } = probeVersion('pnpm');
|
|
216
|
+
// Carry presence separately from version: when the version probe fails
|
|
217
|
+
// (timeout, Corepack banner) but pnpm is on PATH, still treat it as
|
|
218
|
+
// present so mode resolution picks pnpm over the npx crash path. The
|
|
219
|
+
// PATH probe is memoized and only runs when the version is unreadable.
|
|
220
|
+
const pnpmPresent = major !== null || Boolean(probe('pnpm'));
|
|
221
|
+
resolved = { ...deps, pnpmMajor: major, pnpmMinor: minor, pnpmPresent };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const mode = resolveInvocationMode(probe, resolved);
|
|
225
|
+
if (mode === 'gitnexus') return `gitnexus analyze${suffix}`;
|
|
226
|
+
if (mode === 'pnpm') return `${formatPnpmDlxCommand(`analyze${suffix}`, options, resolved)}`;
|
|
227
|
+
return `npx ${NPX_REF} analyze${suffix}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolve `mode` into a concrete { program, args } pair for a set of gitnexus
|
|
232
|
+
* subcommand arguments. Shared by the direct-exec entrypoint below; pure (no
|
|
233
|
+
* spawn) so it is unit-testable. `--embeddings` widens the pnpm allow-build set.
|
|
234
|
+
*/
|
|
235
|
+
function buildRunnerArgv(mode, gitnexusArgs, deps = {}) {
|
|
236
|
+
// Match both the space form (`--embeddings`) and the equals form
|
|
237
|
+
// (`--embeddings=5000`) Commander accepts, so the pnpm allow-build set still
|
|
238
|
+
// widens to onnxruntime-node when a user hand-types the equals form.
|
|
239
|
+
const embeddings = gitnexusArgs.some(
|
|
240
|
+
(a) => a === '--embeddings' || a.startsWith('--embeddings='),
|
|
241
|
+
);
|
|
242
|
+
if (mode === 'gitnexus') return { program: 'gitnexus', args: [...gitnexusArgs] };
|
|
243
|
+
if (mode === 'pnpm') {
|
|
244
|
+
return {
|
|
245
|
+
program: 'pnpm',
|
|
246
|
+
args: [...formatPnpmAllowBuildArgs({ embeddings }, deps), 'dlx', NPX_REF, ...gitnexusArgs],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { program: 'npx', args: [NPX_REF, ...gitnexusArgs] };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
formatAnalyzeCommand,
|
|
254
|
+
formatDocumentationDlxCommand,
|
|
255
|
+
formatPnpmAllowBuildArgs,
|
|
256
|
+
formatPnpmDlxCommand,
|
|
257
|
+
resolveInvocationMode,
|
|
258
|
+
buildRunnerArgv,
|
|
259
|
+
pickPathMatch,
|
|
260
|
+
getNpmMajorVersion,
|
|
261
|
+
NPX_REF,
|
|
262
|
+
PNPM_ALLOW_BUILD_BASE,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Direct-exec entrypoint (#1945): `node run.cjs <gitnexus args…>` resolves the
|
|
266
|
+
// best available runner (global `gitnexus` → `pnpm dlx` → `npx`) at call time and
|
|
267
|
+
// runs it, inheriting stdio and propagating the child's exit code. This lets the
|
|
268
|
+
// committed skills and generated AGENTS.md/CLAUDE.md reference ONE stable,
|
|
269
|
+
// CLI-neutral command without baking in a package-manager assumption. `gitnexus
|
|
270
|
+
// analyze` drops a copy of this file at `.gitnexus/run.cjs`. Skipped on require()
|
|
271
|
+
// (the CLI and tests reuse the exports above), so it runs only when invoked as a
|
|
272
|
+
// script.
|
|
273
|
+
if (require.main === module) {
|
|
274
|
+
const gitnexusArgs = process.argv.slice(2);
|
|
275
|
+
const { program, args } = buildRunnerArgv(resolveInvocationMode(), gitnexusArgs);
|
|
276
|
+
try {
|
|
277
|
+
execFileSync(program, args, {
|
|
278
|
+
stdio: 'inherit',
|
|
279
|
+
windowsHide: true,
|
|
280
|
+
// On Windows, `npx`/`pnpm`/`gitnexus` resolve to `.cmd`/`.ps1`/`.exe`
|
|
281
|
+
// shims (npm, Volta, Corepack, scoop). execFileSync does not do PATHEXT
|
|
282
|
+
// resolution and Node refuses to spawn `.cmd`/`.bat` without a shell
|
|
283
|
+
// (CVE-2024-27980), so a bare program name ENOENTs. A shell lets the OS
|
|
284
|
+
// resolve the shim; POSIX needs no shell (direct PATH lookup works).
|
|
285
|
+
shell: process.platform === 'win32',
|
|
286
|
+
});
|
|
287
|
+
} catch (err) {
|
|
288
|
+
// Make spawn failures (resolved program absent from PATH) self-explanatory
|
|
289
|
+
// instead of a silent exit 1, then propagate the runner's own exit code.
|
|
290
|
+
if (typeof err.status !== 'number') {
|
|
291
|
+
process.stderr.write(`gitnexus runner: could not launch \`${program}\` — ${err.message}\n`);
|
|
292
|
+
}
|
|
293
|
+
process.exit(typeof err.status === 'number' ? err.status : 1);
|
|
294
|
+
}
|
|
295
|
+
}
|
package/package.json
CHANGED
|
@@ -30,6 +30,7 @@ const PLATFORM_LOGIC = [
|
|
|
30
30
|
'test/unit/setup-jsonc.test.ts',
|
|
31
31
|
'test/unit/setup-codex.test.ts',
|
|
32
32
|
'test/unit/setup-antigravity.test.ts',
|
|
33
|
+
'test/unit/resolve-invocation.test.ts',
|
|
33
34
|
'test/unit/platform-capabilities.test.ts',
|
|
34
35
|
'test/unit/worker-pool-windows-quarantine.test.ts',
|
|
35
36
|
'test/unit/lbug-pool-win-fts-probe.test.ts',
|
|
@@ -84,6 +85,7 @@ const SPAWN_CLI = [
|
|
|
84
85
|
'test/integration/setup-antigravity.test.ts',
|
|
85
86
|
'test/integration/antigravity-hook-e2e.test.ts',
|
|
86
87
|
'test/unit/local-cli-subprocess.test.ts',
|
|
88
|
+
'test/unit/runner-exec-tail.test.ts',
|
|
87
89
|
];
|
|
88
90
|
|
|
89
91
|
// Worker threads tests — exercise real worker_threads which have
|
package/skills/gitnexus-cli.md
CHANGED
|
@@ -5,14 +5,16 @@ description: "Use when the user needs to run GitNexus CLI commands like analyze/
|
|
|
5
5
|
|
|
6
6
|
# GitNexus CLI Commands
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Commands below use `node .gitnexus/run.cjs <command>` — the project-local runner `gitnexus analyze` drops next to the index. It auto-selects an available runner at call time (global `gitnexus`, else `pnpm dlx`, else `npx`), so no package-manager assumption and no global install is required.
|
|
9
|
+
|
|
10
|
+
> **Not analyzed yet, or `node .gitnexus/run.cjs` reports `Cannot find module`** (the gitignored runner is absent — e.g. a fresh clone or `git clean`)? (Re)generate it with `npx gitnexus analyze` from the project root. On **npm 11.x**, if `npx` crashes during install (`node.target is null`), install once with `npm i -g gitnexus` (then `gitnexus analyze`) or use `pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze`. See [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939).
|
|
9
11
|
|
|
10
12
|
## Commands
|
|
11
13
|
|
|
12
14
|
### analyze — Build or refresh the index
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
|
-
|
|
17
|
+
node .gitnexus/run.cjs analyze
|
|
16
18
|
```
|
|
17
19
|
|
|
18
20
|
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.
|
|
@@ -28,7 +30,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
|
|
|
28
30
|
### status — Check index freshness
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
|
|
33
|
+
node .gitnexus/run.cjs status
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
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.
|
|
@@ -36,7 +38,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
|
|
|
36
38
|
### clean — Delete the index
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
|
|
41
|
+
node .gitnexus/run.cjs clean
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
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.
|
|
@@ -49,7 +51,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
|
|
|
49
51
|
### wiki — Generate documentation from the graph
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
|
|
54
|
+
node .gitnexus/run.cjs wiki
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
|
|
@@ -66,7 +68,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
|
|
|
66
68
|
### list — Show all indexed repos
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
|
-
|
|
71
|
+
node .gitnexus/run.cjs list
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
|
|
@@ -22,7 +22,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
|
|
22
22
|
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
> If "Index is stale" → run `
|
|
25
|
+
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
26
26
|
|
|
27
27
|
## Checklist
|
|
28
28
|
|
|
@@ -23,7 +23,7 @@ description: "Use when the user asks how code works, wants to understand archite
|
|
|
23
23
|
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
> If step 2 says "Index is stale" → run `
|
|
26
|
+
> If step 2 says "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
27
27
|
|
|
28
28
|
## Checklist
|
|
29
29
|
|
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 `
|
|
18
|
+
> If step 1 warns the index is stale, run `node .gitnexus/run.cjs analyze` in the terminal first.
|
|
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 `
|
|
26
|
+
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
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 `
|
|
29
|
+
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal before reviewing.
|
|
30
30
|
|
|
31
31
|
## Checklist
|
|
32
32
|
|
|
@@ -22,7 +22,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
|
|
22
22
|
4. Plan update order: interfaces → implementations → callers → tests
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
> If "Index is stale" → run `
|
|
25
|
+
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
|
|
26
26
|
|
|
27
27
|
## Checklists
|
|
28
28
|
|