gitnexus 1.6.9 → 1.6.10-rc.2
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 +74 -31
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/editor-targets.d.ts +17 -2
- package/dist/cli/editor-targets.js +44 -0
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/setup.js +204 -28
- package/dist/cli/uninstall.js +70 -16
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
|
|
4
4
|
|
|
5
|
-
Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
|
|
5
|
+
Works with **Cursor**, **Claude Code**, **Antigravity** (Google), **Codex**, **Windsurf**, **Cline**, **OpenCode**, **CodeBuddy** (Tencent), **Qoder** (Alibaba), and any MCP-compatible tool.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/gitnexus)
|
|
8
8
|
[](https://polyformproject.org/licenses/noncommercial/1.0.0/)
|
|
@@ -43,11 +43,13 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|
|
43
43
|
| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
|
|
44
44
|
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
|
|
45
45
|
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
|
|
46
|
-
| **Codex** | Yes | Yes |
|
|
46
|
+
| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** |
|
|
47
47
|
| **OpenCode** | Yes | Yes | — | MCP + Skills |
|
|
48
|
+
| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills |
|
|
49
|
+
| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills |
|
|
48
50
|
| **Windsurf** | Yes | — | — | MCP |
|
|
49
51
|
|
|
50
|
-
> **Claude Code**
|
|
52
|
+
> **Claude Code** and **Codex** get the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
|
|
51
53
|
|
|
52
54
|
### Community Integrations
|
|
53
55
|
|
|
@@ -69,12 +71,23 @@ claude mcp add gitnexus -- npx -y gitnexus@latest mcp
|
|
|
69
71
|
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
### Codex (full support — MCP + skills)
|
|
74
|
+
### Codex (full support — MCP + skills + hooks)
|
|
73
75
|
|
|
74
76
|
```bash
|
|
75
77
|
codex mcp add gitnexus -- npx -y gitnexus@latest mcp
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
Codex hooks (PreToolUse graph enrichment + PostToolUse stale-index detection in `~/.codex/hooks.json`, [same schema as Claude Code](https://developers.openai.com/codex/hooks)) need the bundled adapter script, so they are installed by `gitnexus setup -c codex` rather than manually.
|
|
81
|
+
|
|
82
|
+
Alternatively, install everything as a [Codex plugin](https://developers.openai.com/codex/plugins/build) (MCP + skills + hooks in one step):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
codex plugin marketplace add abhigyanpatwari/GitNexus
|
|
86
|
+
# then inside Codex: /plugins → install "GitNexus"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> **Codex notes:** SessionStart is intentionally not registered — Codex reads [AGENTS.md natively](https://developers.openai.com/codex/guides/agents-md), which already carries the GitNexus context block. Newly installed hooks need a one-time approval in Codex via `/hooks` before they run. Pick **one** install route (`gitnexus setup -c codex` **or** the plugin): plugin hooks load alongside `~/.codex/hooks.json`, so installing both can fire duplicate hooks per tool call.
|
|
90
|
+
|
|
78
91
|
### Cursor / Windsurf
|
|
79
92
|
|
|
80
93
|
Add to `~/.cursor/mcp.json` (global — works for all projects):
|
|
@@ -105,6 +118,36 @@ Add to `~/.config/opencode/config.json`:
|
|
|
105
118
|
}
|
|
106
119
|
```
|
|
107
120
|
|
|
121
|
+
### CodeBuddy
|
|
122
|
+
|
|
123
|
+
CodeBuddy reads only the **first existing file** in its config priority chain: `~/.codebuddy/.mcp.json` (recommended) → `~/.codebuddy/mcp.json` (deprecated) → `~/.codebuddy.json` (legacy). Edit the first non-empty file that exists — creating a higher-priority file would hide the servers in the ones below it. If none exist, create `~/.codebuddy/.mcp.json`:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"mcpServers": {
|
|
128
|
+
"gitnexus": {
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "gitnexus@latest", "mcp"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Qoder
|
|
137
|
+
|
|
138
|
+
Add to `~/.qoder.json`:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"gitnexus": {
|
|
144
|
+
"command": "npx",
|
|
145
|
+
"args": ["-y", "gitnexus@latest", "mcp"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
108
151
|
## How It Works
|
|
109
152
|
|
|
110
153
|
GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:
|
|
@@ -125,24 +168,24 @@ The result is a **LadybugDB graph database** stored locally in `.gitnexus/` with
|
|
|
125
168
|
Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
|
|
126
169
|
|
|
127
170
|
| Tool | What It Does |
|
|
128
|
-
| ---------------- |
|
|
129
|
-
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`)
|
|
130
|
-
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF)
|
|
131
|
-
| `context` | 360-degree symbol view — categorized refs, process participation
|
|
132
|
-
| `impact` | Blast radius analysis with depth grouping and confidence
|
|
133
|
-
| `trace` | Shortest directed path between two symbols (call + class-member edges)
|
|
134
|
-
| `detect_changes` | Git-diff impact — maps changed lines to affected processes
|
|
135
|
-
| `check` | Read-only structural checks against the indexed graph
|
|
136
|
-
| `rename` | Multi-file coordinated rename with graph + text search
|
|
137
|
-
| `cypher` | Raw Cypher graph queries
|
|
138
|
-
| `route_map` | API route map — which components fetch which endpoints, and handlers
|
|
139
|
-
| `tool_map` | MCP/RPC tool definitions — where they're defined and handled
|
|
140
|
-
| `shape_check` | Validate API response shapes against consumers' property accesses
|
|
141
|
-
| `api_impact` | Pre-change impact report for an API route handler
|
|
142
|
-
| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes)
|
|
143
|
-
| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes)
|
|
144
|
-
| `group_list` | List configured repository groups
|
|
145
|
-
| `group_sync` | Rebuild a group's Contract Registry and cross-repo links
|
|
171
|
+
| ---------------- | ---------------------------------------------------------------------- |
|
|
172
|
+
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) |
|
|
173
|
+
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) |
|
|
174
|
+
| `context` | 360-degree symbol view — categorized refs, process participation |
|
|
175
|
+
| `impact` | Blast radius analysis with depth grouping and confidence |
|
|
176
|
+
| `trace` | Shortest directed path between two symbols (call + class-member edges) |
|
|
177
|
+
| `detect_changes` | Git-diff impact — maps changed lines to affected processes |
|
|
178
|
+
| `check` | Read-only structural checks against the indexed graph |
|
|
179
|
+
| `rename` | Multi-file coordinated rename with graph + text search |
|
|
180
|
+
| `cypher` | Raw Cypher graph queries |
|
|
181
|
+
| `route_map` | API route map — which components fetch which endpoints, and handlers |
|
|
182
|
+
| `tool_map` | MCP/RPC tool definitions — where they're defined and handled |
|
|
183
|
+
| `shape_check` | Validate API response shapes against consumers' property accesses |
|
|
184
|
+
| `api_impact` | Pre-change impact report for an API route handler |
|
|
185
|
+
| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) |
|
|
186
|
+
| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) |
|
|
187
|
+
| `group_list` | List configured repository groups |
|
|
188
|
+
| `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
|
|
146
189
|
|
|
147
190
|
> With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({search_query: "auth", repo: "my-app"})`. Per-repo tools also take an optional `branch` for indexes pinned with `gitnexus analyze --branch`; omitting it queries the workspace index, which follows your checked-out working tree. `explain` and `pdg_query` need an index built with `gitnexus analyze --pdg`.
|
|
148
191
|
|
|
@@ -383,13 +426,13 @@ GitNexus uses optional DuckDB extensions for BM25 and vector search. The `gitnex
|
|
|
383
426
|
|
|
384
427
|
Configure the behavior with these environment variables:
|
|
385
428
|
|
|
386
|
-
| Variable | Values | Default | Effect
|
|
387
|
-
| -------------------------------------------- | ---------------------------- | ------------------- |
|
|
388
|
-
| `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely.
|
|
389
|
-
| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed.
|
|
390
|
-
| `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it.
|
|
429
|
+
| Variable | Values | Default | Effect |
|
|
430
|
+
| -------------------------------------------- | ---------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
|
+
| `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
|
|
432
|
+
| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
|
|
433
|
+
| `GITNEXUS_FTS_STEMMER` | supported LadybugDB stemmer | `porter` | Stemmer used when rebuilding BM25/FTS indexes. Use `none` for CJK-heavy repositories, or a language stemmer such as `german`, `french`, or `spanish` when that better matches repository comments and identifiers. Re-run `gitnexus analyze --repair-fts` after changing it. |
|
|
391
434
|
| `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. |
|
|
392
|
-
| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments.
|
|
435
|
+
| `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
|
|
393
436
|
|
|
394
437
|
```bash
|
|
395
438
|
# Offline/airgapped: never reach the network for extensions
|
|
@@ -464,9 +507,9 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
|
|
|
464
507
|
|
|
465
508
|
After scope resolution, analyze prunes inert block-local value symbols (a function-local `const`/`let`/`var` that ends up with only its structural `File→DEFINES` edge) to keep the graph focused on cross-symbol relationships. Module/file-scope symbols, class members, and any local with a real edge are always kept.
|
|
466
509
|
|
|
467
|
-
| Variable
|
|
468
|
-
|
|
|
469
|
-
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS`
|
|
510
|
+
| Variable | Default | Effect |
|
|
511
|
+
| ----------------------------------- | ------- | ---------------------------------------------------------------------------------- |
|
|
512
|
+
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
|
|
470
513
|
|
|
471
514
|
Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
|
|
472
515
|
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI Context Generator
|
|
3
3
|
*
|
|
4
4
|
* Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
|
|
5
|
-
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
|
|
5
|
+
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, CodeBuddy, Qoder, etc.
|
|
6
6
|
* CLAUDE.md is for Claude Code which only reads that file.
|
|
7
7
|
*/
|
|
8
8
|
import { type GeneratedSkillInfo } from './skill-gen.js';
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI Context Generator
|
|
3
3
|
*
|
|
4
4
|
* Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
|
|
5
|
-
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
|
|
5
|
+
* AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, CodeBuddy, Qoder, etc.
|
|
6
6
|
* CLAUDE.md is for Claude Code which only reads that file.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs/promises';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* implementations remain behaviourally symmetrical on top of this shared
|
|
16
16
|
* structure.
|
|
17
17
|
*/
|
|
18
|
-
export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codex';
|
|
18
|
+
export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codebuddy' | 'qoder' | 'codex';
|
|
19
19
|
/** An editor whose MCP config is a JSONC document (server keyed by name). */
|
|
20
20
|
export interface McpJsoncTarget {
|
|
21
21
|
id: EditorId;
|
|
@@ -29,6 +29,14 @@ export interface McpJsoncTarget {
|
|
|
29
29
|
* without either side needing a cast.
|
|
30
30
|
*/
|
|
31
31
|
keyPath: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Older config locations the editor still reads when `file` is absent
|
|
34
|
+
* (CodeBuddy reads only the FIRST existing file in its priority chain).
|
|
35
|
+
* Setup writes into the first existing candidate of [file, ...legacyFiles]
|
|
36
|
+
* so it never shadows a user's servers living in a deprecated file;
|
|
37
|
+
* uninstall sweeps all of them.
|
|
38
|
+
*/
|
|
39
|
+
legacyFiles?: string[];
|
|
32
40
|
}
|
|
33
41
|
/** Codex stores MCP config as a TOML table, not JSONC. */
|
|
34
42
|
export interface CodexMcpTarget {
|
|
@@ -58,7 +66,7 @@ export interface HookTarget {
|
|
|
58
66
|
scriptDir: string;
|
|
59
67
|
}
|
|
60
68
|
export interface EditorTargets {
|
|
61
|
-
/** JSONC-format MCP entries: Cursor, Claude Code, Antigravity, OpenCode. */
|
|
69
|
+
/** JSONC-format MCP entries: Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder. */
|
|
62
70
|
mcpJsonc: McpJsoncTarget[];
|
|
63
71
|
/** Codex MCP (TOML). */
|
|
64
72
|
codex: CodexMcpTarget;
|
|
@@ -80,6 +88,13 @@ export declare function mcpTarget(id: EditorId, home?: string): McpJsoncTarget;
|
|
|
80
88
|
export declare function skillTarget(id: EditorId, home?: string): SkillTarget;
|
|
81
89
|
/** Look up a single hook target by editor id (throws if unknown). */
|
|
82
90
|
export declare function hookTarget(id: EditorId, home?: string): HookTarget;
|
|
91
|
+
/**
|
|
92
|
+
* True when err is a Node fs error with code ENOENT (file/dir absent).
|
|
93
|
+
* Shared by setup and uninstall: both must swallow ONLY absence when reading
|
|
94
|
+
* editor configs — any other read/stat failure (EACCES, EIO) is surfaced so an
|
|
95
|
+
* unreadable config is never treated as empty and rewritten gitnexus-only.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isEnoent(err: unknown): boolean;
|
|
83
98
|
/**
|
|
84
99
|
* Detect indentation style from file content so JSONC edits preserve the file's
|
|
85
100
|
* existing formatting. Shared by setup (writes) and uninstall (removes).
|
|
@@ -50,6 +50,26 @@ export function getEditorTargets(home = os.homedir()) {
|
|
|
50
50
|
// OpenCode nests servers under `mcp`, not `mcpServers`.
|
|
51
51
|
keyPath: ['mcp', 'gitnexus'],
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
id: 'codebuddy',
|
|
55
|
+
label: 'CodeBuddy',
|
|
56
|
+
// Recommended user-scope path per https://www.codebuddy.ai/docs/cli/mcp;
|
|
57
|
+
// CodeBuddy reads only the first existing file in this priority chain.
|
|
58
|
+
file: path.join(home, '.codebuddy', '.mcp.json'),
|
|
59
|
+
legacyFiles: [
|
|
60
|
+
path.join(home, '.codebuddy', 'mcp.json'), // deprecated
|
|
61
|
+
path.join(home, '.codebuddy.json'), // legacy
|
|
62
|
+
],
|
|
63
|
+
keyPath: ['mcpServers', 'gitnexus'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'qoder',
|
|
67
|
+
label: 'Qoder',
|
|
68
|
+
// Qoder's documented user-scope MCP config (https://docs.qoder.com/cli/using-cli);
|
|
69
|
+
// the IDE manages MCP via its Settings UI with no documented file path.
|
|
70
|
+
file: path.join(home, '.qoder.json'),
|
|
71
|
+
keyPath: ['mcpServers', 'gitnexus'],
|
|
72
|
+
},
|
|
53
73
|
];
|
|
54
74
|
const codex = {
|
|
55
75
|
id: 'codex',
|
|
@@ -66,6 +86,10 @@ export function getEditorTargets(home = os.homedir()) {
|
|
|
66
86
|
},
|
|
67
87
|
{ id: 'cursor', label: 'Cursor', dir: path.join(home, '.cursor', 'skills') },
|
|
68
88
|
{ id: 'opencode', label: 'OpenCode', dir: path.join(home, '.config', 'opencode', 'skills') },
|
|
89
|
+
{ id: 'codebuddy', label: 'CodeBuddy', dir: path.join(home, '.codebuddy', 'skills') },
|
|
90
|
+
// Qoder skills live at ~/.qoder/skills/{name}/SKILL.md
|
|
91
|
+
// (https://docs.qoder.com/extensions/skills).
|
|
92
|
+
{ id: 'qoder', label: 'Qoder', dir: path.join(home, '.qoder', 'skills') },
|
|
69
93
|
// Codex reads skills from ~/.agents/skills (not ~/.codex).
|
|
70
94
|
{ id: 'codex', label: 'Codex', dir: path.join(home, '.agents', 'skills') },
|
|
71
95
|
];
|
|
@@ -78,6 +102,17 @@ export function getEditorTargets(home = os.homedir()) {
|
|
|
78
102
|
needle: 'gitnexus-hook',
|
|
79
103
|
scriptDir: path.join(home, '.claude', 'hooks', 'gitnexus'),
|
|
80
104
|
},
|
|
105
|
+
{
|
|
106
|
+
id: 'codex',
|
|
107
|
+
label: 'Codex',
|
|
108
|
+
// Codex hooks use Claude Code's exact {hooks: {Event: [...]}} JSON shape
|
|
109
|
+
// and hookSpecificOutput response contract, in a dedicated hooks.json
|
|
110
|
+
// (https://developers.openai.com/codex/hooks).
|
|
111
|
+
settingsFile: path.join(home, '.codex', 'hooks.json'),
|
|
112
|
+
events: ['PreToolUse', 'PostToolUse'],
|
|
113
|
+
needle: 'gitnexus-hook',
|
|
114
|
+
scriptDir: path.join(home, '.codex', 'hooks', 'gitnexus'),
|
|
115
|
+
},
|
|
81
116
|
{
|
|
82
117
|
id: 'antigravity',
|
|
83
118
|
label: 'Antigravity',
|
|
@@ -110,6 +145,15 @@ export function hookTarget(id, home) {
|
|
|
110
145
|
throw new Error(`No hook target for editor "${id}"`);
|
|
111
146
|
return t;
|
|
112
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* True when err is a Node fs error with code ENOENT (file/dir absent).
|
|
150
|
+
* Shared by setup and uninstall: both must swallow ONLY absence when reading
|
|
151
|
+
* editor configs — any other read/stat failure (EACCES, EIO) is surfaced so an
|
|
152
|
+
* unreadable config is never treated as empty and rewritten gitnexus-only.
|
|
153
|
+
*/
|
|
154
|
+
export function isEnoent(err) {
|
|
155
|
+
return err?.code === 'ENOENT';
|
|
156
|
+
}
|
|
113
157
|
/**
|
|
114
158
|
* Detect indentation style from file content so JSONC edits preserve the file's
|
|
115
159
|
* existing formatting. Shared by setup (writes) and uninstall (removes).
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export declare const en: {
|
|
|
108
108
|
readonly 'help.command.help.description': "display help for command";
|
|
109
109
|
readonly 'help.option.help': "display help for command";
|
|
110
110
|
readonly 'help.option.version': "output the version number";
|
|
111
|
-
readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
|
|
111
|
+
readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
|
|
112
112
|
readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
|
|
113
113
|
readonly 'help.command.analyze.description': "Index a repository (full analysis)";
|
|
114
114
|
readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -108,7 +108,7 @@ export const en = {
|
|
|
108
108
|
'help.command.help.description': 'display help for command',
|
|
109
109
|
'help.option.help': 'display help for command',
|
|
110
110
|
'help.option.version': 'output the version number',
|
|
111
|
-
'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex',
|
|
111
|
+
'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex',
|
|
112
112
|
'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
|
|
113
113
|
'help.command.analyze.description': 'Index a repository (full analysis)',
|
|
114
114
|
'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
|
|
@@ -109,7 +109,7 @@ export declare const cliResources: {
|
|
|
109
109
|
readonly 'help.command.help.description': "display help for command";
|
|
110
110
|
readonly 'help.option.help': "display help for command";
|
|
111
111
|
readonly 'help.option.version': "output the version number";
|
|
112
|
-
readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
|
|
112
|
+
readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
|
|
113
113
|
readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
|
|
114
114
|
readonly 'help.command.analyze.description': "Index a repository (full analysis)";
|
|
115
115
|
readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -108,7 +108,7 @@ export const zhCN = {
|
|
|
108
108
|
'help.command.help.description': '显示命令帮助',
|
|
109
109
|
'help.option.help': '显示命令帮助',
|
|
110
110
|
'help.option.version': '输出版本号',
|
|
111
|
-
'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、OpenCode、Codex 配置 MCP',
|
|
111
|
+
'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、Antigravity、OpenCode、CodeBuddy、Qoder、Codex 配置 MCP',
|
|
112
112
|
'help.command.uninstall.description': '撤销 `setup`:从所有检测到的编辑器中移除 GitNexus 的 MCP 配置、技能和钩子',
|
|
113
113
|
'help.command.analyze.description': '索引仓库(完整分析)',
|
|
114
114
|
'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)',
|
package/dist/cli/index.js
CHANGED
|
@@ -17,7 +17,7 @@ function collectCodingAgents(value, previous) {
|
|
|
17
17
|
program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
|
|
18
18
|
program
|
|
19
19
|
.command('setup')
|
|
20
|
-
.description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, Codex')
|
|
20
|
+
.description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex')
|
|
21
21
|
.option('-c, --coding-agent <agents>', 'Configure only these coding agents (comma-separated or repeatable)', collectCodingAgents)
|
|
22
22
|
.action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
|
|
23
23
|
program
|
package/dist/cli/setup.js
CHANGED
|
@@ -14,7 +14,7 @@ import { promisify } from 'util';
|
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
|
|
16
16
|
import { getGlobalDir } from '../storage/repo-manager.js';
|
|
17
|
-
import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, } from './editor-targets.js';
|
|
17
|
+
import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, isEnoent, } from './editor-targets.js';
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = path.dirname(__filename);
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
@@ -66,6 +66,8 @@ const CODING_AGENT_IDS = {
|
|
|
66
66
|
claude: 'claude',
|
|
67
67
|
antigravity: 'antigravity',
|
|
68
68
|
opencode: 'opencode',
|
|
69
|
+
codebuddy: 'codebuddy',
|
|
70
|
+
qoder: 'qoder',
|
|
69
71
|
codex: 'codex',
|
|
70
72
|
};
|
|
71
73
|
const SUPPORTED_CODING_AGENTS = Object.values(CODING_AGENT_IDS);
|
|
@@ -173,7 +175,13 @@ async function mergeJsoncFile(filePath, keyPath, value) {
|
|
|
173
175
|
try {
|
|
174
176
|
raw = await fs.readFile(filePath, 'utf-8');
|
|
175
177
|
}
|
|
176
|
-
catch {
|
|
178
|
+
catch (err) {
|
|
179
|
+
// Only an absent file means "start fresh". Any other read failure (EACCES,
|
|
180
|
+
// EIO, cloud-placeholder faults) must not be treated as empty — the write
|
|
181
|
+
// below would replace the user's existing config with a gitnexus-only
|
|
182
|
+
// document and report success. Rethrow into the per-editor catch instead.
|
|
183
|
+
if (!isEnoent(err))
|
|
184
|
+
throw err;
|
|
177
185
|
raw = '';
|
|
178
186
|
}
|
|
179
187
|
if (raw.trim().length === 0) {
|
|
@@ -207,6 +215,35 @@ async function dirExists(dirPath) {
|
|
|
207
215
|
return false;
|
|
208
216
|
}
|
|
209
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Detection probe: is there a non-empty regular file at this path?
|
|
220
|
+
* Swallows ALL errors (like dirExists) — detection gates run outside the
|
|
221
|
+
* per-editor try blocks, so a rethrowing probe would abort setup for every
|
|
222
|
+
* remaining editor. Size > 0 keeps detection aligned with the config-chain
|
|
223
|
+
* resolver: an empty config file is not evidence of an install, and treating
|
|
224
|
+
* it as one would route the write to a fresh file whose mkdir manufactures
|
|
225
|
+
* the editor's directory.
|
|
226
|
+
*/
|
|
227
|
+
async function isNonEmptyFile(filePath) {
|
|
228
|
+
try {
|
|
229
|
+
const stat = await fs.stat(filePath);
|
|
230
|
+
return stat.isFile() && stat.size > 0;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Detection probe: does any file in the target's MCP config chain look like an
|
|
238
|
+
* install trace? Always walks [file, ...legacyFiles] so an editor gaining
|
|
239
|
+
* legacyFiles later is automatically covered (CodeBuddy and Qoder share this —
|
|
240
|
+
* a per-editor copy is how the root-config-only detection gap crept in, see
|
|
241
|
+
* PR #2368 review I4).
|
|
242
|
+
*/
|
|
243
|
+
async function anyChainConfigFile(target) {
|
|
244
|
+
const hits = await Promise.all([target.file, ...(target.legacyFiles ?? [])].map(isNonEmptyFile));
|
|
245
|
+
return hits.includes(true);
|
|
246
|
+
}
|
|
210
247
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
211
248
|
async function setupCursor(result) {
|
|
212
249
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
@@ -287,7 +324,12 @@ async function mergeHooksJsonc(filePath, entries) {
|
|
|
287
324
|
try {
|
|
288
325
|
raw = await fs.readFile(filePath, 'utf-8');
|
|
289
326
|
}
|
|
290
|
-
catch {
|
|
327
|
+
catch (err) {
|
|
328
|
+
// Same contract as mergeJsoncFile: an unreadable (non-ENOENT) settings
|
|
329
|
+
// file must not be rewritten as hooks-only — that would destroy every
|
|
330
|
+
// user setting in it. Rethrow into the hook installer's catch.
|
|
331
|
+
if (!isEnoent(err))
|
|
332
|
+
throw err;
|
|
291
333
|
raw = '';
|
|
292
334
|
}
|
|
293
335
|
if (raw.trim().length === 0) {
|
|
@@ -365,20 +407,26 @@ export async function copyHookHelpers(srcDir, destDir, label, result) {
|
|
|
365
407
|
return failedRequired;
|
|
366
408
|
}
|
|
367
409
|
/**
|
|
368
|
-
* Install GitNexus hooks
|
|
369
|
-
*
|
|
370
|
-
*
|
|
410
|
+
* Install GitNexus hooks for editors that use Claude Code's hooks schema.
|
|
411
|
+
*
|
|
412
|
+
* Claude Code registers hooks in ~/.claude/settings.json; Codex uses a
|
|
413
|
+
* dedicated ~/.codex/hooks.json with the identical {hooks: {Event: [...]}}
|
|
414
|
+
* JSON shape, stdin payload, and hookSpecificOutput response contract
|
|
415
|
+
* (https://developers.openai.com/codex/hooks), so both runtimes share this
|
|
416
|
+
* installer and the same bundled adapter script. Merges hook config without
|
|
417
|
+
* overwriting existing hooks, preserving comments and formatting.
|
|
371
418
|
*/
|
|
372
|
-
async function
|
|
373
|
-
const
|
|
374
|
-
|
|
419
|
+
async function installClaudeSchemaHooks(result, id) {
|
|
420
|
+
const hookCfg = hookTarget(id);
|
|
421
|
+
const settingsPath = hookCfg.settingsFile;
|
|
422
|
+
const label = `${hookCfg.label} hooks`;
|
|
423
|
+
// Gate on the editor's own config dir (~/.claude, ~/.codex) existing.
|
|
424
|
+
if (!(await dirExists(path.dirname(settingsPath))))
|
|
375
425
|
return;
|
|
376
|
-
const claudeHook = hookTarget('claude');
|
|
377
|
-
const settingsPath = claudeHook.settingsFile;
|
|
378
426
|
// Source hooks bundled within the gitnexus package (hooks/claude/)
|
|
379
427
|
const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
|
|
380
|
-
// Copy unified hook script to
|
|
381
|
-
const destHooksDir =
|
|
428
|
+
// Copy unified hook script to the editor's hooks/gitnexus/ dir
|
|
429
|
+
const destHooksDir = hookCfg.scriptDir;
|
|
382
430
|
try {
|
|
383
431
|
await fs.mkdir(destHooksDir, { recursive: true });
|
|
384
432
|
const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
|
|
@@ -389,7 +437,7 @@ async function installClaudeCodeHooks(result) {
|
|
|
389
437
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
390
438
|
const jsonCli = JSON.stringify(normalizedCli);
|
|
391
439
|
if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
|
|
392
|
-
result.errors.push(
|
|
440
|
+
result.errors.push(`${label}: 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.`);
|
|
393
441
|
}
|
|
394
442
|
content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
|
|
395
443
|
await fs.writeFile(dest, content, 'utf-8');
|
|
@@ -404,12 +452,12 @@ async function installClaudeCodeHooks(result) {
|
|
|
404
452
|
await fs.access(dest);
|
|
405
453
|
}
|
|
406
454
|
catch {
|
|
407
|
-
result.errors.push(
|
|
455
|
+
result.errors.push(`${label}: adapter script was not installed — skipping hook registration`);
|
|
408
456
|
return;
|
|
409
457
|
}
|
|
410
|
-
const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir,
|
|
458
|
+
const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, label, result);
|
|
411
459
|
if (failedRequired.length > 0) {
|
|
412
|
-
result.errors.push(
|
|
460
|
+
result.errors.push(`${label}: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
|
|
413
461
|
return;
|
|
414
462
|
}
|
|
415
463
|
const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
|
|
@@ -425,9 +473,10 @@ async function installClaudeCodeHooks(result) {
|
|
|
425
473
|
}
|
|
426
474
|
})();
|
|
427
475
|
const hookEntries = [];
|
|
428
|
-
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576)
|
|
429
|
-
// Session context is delivered via
|
|
430
|
-
|
|
476
|
+
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576),
|
|
477
|
+
// and Codex reads AGENTS.md natively. Session context is delivered via
|
|
478
|
+
// CLAUDE.md / AGENTS.md / skills instead.
|
|
479
|
+
if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', hookCfg.needle)) {
|
|
431
480
|
hookEntries.push({
|
|
432
481
|
eventName: 'PreToolUse',
|
|
433
482
|
value: {
|
|
@@ -443,7 +492,7 @@ async function installClaudeCodeHooks(result) {
|
|
|
443
492
|
},
|
|
444
493
|
});
|
|
445
494
|
}
|
|
446
|
-
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse',
|
|
495
|
+
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', hookCfg.needle)) {
|
|
447
496
|
hookEntries.push({
|
|
448
497
|
eventName: 'PostToolUse',
|
|
449
498
|
value: {
|
|
@@ -460,19 +509,19 @@ async function installClaudeCodeHooks(result) {
|
|
|
460
509
|
});
|
|
461
510
|
}
|
|
462
511
|
if (hookEntries.length === 0) {
|
|
463
|
-
result.configured.push(
|
|
512
|
+
result.configured.push(`${label} (already configured)`);
|
|
464
513
|
return;
|
|
465
514
|
}
|
|
466
515
|
const ok = await mergeHooksJsonc(settingsPath, hookEntries);
|
|
467
516
|
if (ok) {
|
|
468
|
-
result.configured.push(
|
|
517
|
+
result.configured.push(`${label} (PreToolUse, PostToolUse)`);
|
|
469
518
|
}
|
|
470
519
|
else {
|
|
471
|
-
result.errors.push(
|
|
520
|
+
result.errors.push(`${label}: ${path.basename(settingsPath)} is corrupt — skipping to preserve existing content`);
|
|
472
521
|
}
|
|
473
522
|
}
|
|
474
523
|
catch (err) {
|
|
475
|
-
result.errors.push(
|
|
524
|
+
result.errors.push(`${label}: ${err.message}`);
|
|
476
525
|
}
|
|
477
526
|
}
|
|
478
527
|
// ─── Antigravity (Google) ──────────────────────────────────────────
|
|
@@ -660,6 +709,118 @@ async function setupOpenCode(result) {
|
|
|
660
709
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
661
710
|
}
|
|
662
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Resolve which config file in a target's [file, ...legacyFiles] priority
|
|
714
|
+
* chain setup should write into: the first that exists, else the recommended
|
|
715
|
+
* `file`. CodeBuddy reads only the first existing file in its chain, so
|
|
716
|
+
* creating the recommended file above a populated deprecated one would shadow
|
|
717
|
+
* the user's existing MCP servers.
|
|
718
|
+
*/
|
|
719
|
+
async function resolveMcpConfigFile(target) {
|
|
720
|
+
for (const candidate of [target.file, ...(target.legacyFiles ?? [])]) {
|
|
721
|
+
try {
|
|
722
|
+
const stat = await fs.stat(candidate);
|
|
723
|
+
// Non-empty regular files only: a 0-byte recommended file must not
|
|
724
|
+
// shadow a populated deprecated one (mergeJsoncFile treats empty as a
|
|
725
|
+
// fresh document anyway), and directories are never config candidates.
|
|
726
|
+
if (stat.isFile() && stat.size > 0)
|
|
727
|
+
return candidate;
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
// ENOENT = candidate absent — try the next one. Anything else (EACCES
|
|
731
|
+
// on the file or a parent) is surfaced: silently skipping could route
|
|
732
|
+
// the write to a lower-priority file the editor never reads.
|
|
733
|
+
if (!isEnoent(err))
|
|
734
|
+
throw err;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return target.file;
|
|
738
|
+
}
|
|
739
|
+
async function setupCodeBuddy(result) {
|
|
740
|
+
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
|
741
|
+
const target = mcpTarget('codebuddy');
|
|
742
|
+
// Installed = the config dir exists OR any registered MCP config file does.
|
|
743
|
+
// A user whose only trace is a root-level config (e.g. a legacy
|
|
744
|
+
// ~/.codebuddy.json) still gets configured — uninstall already handles that
|
|
745
|
+
// shape, so setup skipping it was an asymmetry (PR #2368 review I4).
|
|
746
|
+
if (!(await dirExists(codebuddyDir)) && !(await anyChainConfigFile(target))) {
|
|
747
|
+
result.skipped.push('CodeBuddy (not installed)');
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
const configFile = await resolveMcpConfigFile(target);
|
|
752
|
+
const ok = await mergeJsoncFile(configFile, target.keyPath, getMcpEntry());
|
|
753
|
+
if (ok) {
|
|
754
|
+
result.configured.push('CodeBuddy');
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
result.errors.push(`CodeBuddy: ${path.basename(configFile)} is corrupt — skipping to preserve existing content`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
result.errors.push(`CodeBuddy: ${err instanceof Error ? err.message : String(err)}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function setupQoder(result) {
|
|
765
|
+
const qoderDir = path.join(os.homedir(), '.qoder');
|
|
766
|
+
const target = mcpTarget('qoder');
|
|
767
|
+
const { file: mcpPath, keyPath } = target;
|
|
768
|
+
// Same chain-aware detection as CodeBuddy: ~/.qoder.json alone counts.
|
|
769
|
+
if (!(await dirExists(qoderDir)) && !(await anyChainConfigFile(target))) {
|
|
770
|
+
result.skipped.push('Qoder (not installed)');
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
|
775
|
+
if (ok) {
|
|
776
|
+
result.configured.push('Qoder');
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
result.errors.push('Qoder: .qoder.json is corrupt — skipping to preserve existing content');
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
result.errors.push(`Qoder: ${err instanceof Error ? err.message : String(err)}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Install global CodeBuddy skills to ~/.codebuddy/skills/
|
|
788
|
+
* (https://www.codebuddy.ai/docs/cli/skills — same SKILL.md layout as Claude Code).
|
|
789
|
+
*/
|
|
790
|
+
async function installCodeBuddySkills(result) {
|
|
791
|
+
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
|
792
|
+
if (!(await dirExists(codebuddyDir)))
|
|
793
|
+
return;
|
|
794
|
+
const skillsDir = skillTarget('codebuddy').dir;
|
|
795
|
+
try {
|
|
796
|
+
const installed = await installSkillsTo(skillsDir);
|
|
797
|
+
if (installed.length > 0) {
|
|
798
|
+
result.configured.push(`CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
result.errors.push(`CodeBuddy skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Install global Qoder skills to ~/.qoder/skills/
|
|
807
|
+
* (https://docs.qoder.com/extensions/skills — same SKILL.md layout as Claude Code).
|
|
808
|
+
*/
|
|
809
|
+
async function installQoderSkills(result) {
|
|
810
|
+
const qoderDir = path.join(os.homedir(), '.qoder');
|
|
811
|
+
if (!(await dirExists(qoderDir)))
|
|
812
|
+
return;
|
|
813
|
+
const skillsDir = skillTarget('qoder').dir;
|
|
814
|
+
try {
|
|
815
|
+
const installed = await installSkillsTo(skillsDir);
|
|
816
|
+
if (installed.length > 0) {
|
|
817
|
+
result.configured.push(`Qoder skills (${installed.length} skills → ~/.qoder/skills/)`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
result.errors.push(`Qoder skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
663
824
|
/**
|
|
664
825
|
* Build a TOML section for Codex MCP config (~/.codex/config.toml).
|
|
665
826
|
*/
|
|
@@ -677,7 +838,12 @@ async function upsertCodexConfigToml(configPath) {
|
|
|
677
838
|
try {
|
|
678
839
|
existing = await fs.readFile(configPath, 'utf-8');
|
|
679
840
|
}
|
|
680
|
-
catch {
|
|
841
|
+
catch (err) {
|
|
842
|
+
// TOML variant of the mergeJsoncFile contract: treating a non-ENOENT read
|
|
843
|
+
// failure as an empty config would rewrite config.toml with only the
|
|
844
|
+
// gitnexus section. Rethrow into setupCodex's catch.
|
|
845
|
+
if (!isEnoent(err))
|
|
846
|
+
throw err;
|
|
681
847
|
existing = '';
|
|
682
848
|
}
|
|
683
849
|
if (existing.includes(`[${getEditorTargets().codex.tomlSection}]`)) {
|
|
@@ -885,12 +1051,16 @@ export const setupCommand = async (options) => {
|
|
|
885
1051
|
await setupAntigravity(result);
|
|
886
1052
|
if (selected.has('opencode'))
|
|
887
1053
|
await setupOpenCode(result);
|
|
1054
|
+
if (selected.has('codebuddy'))
|
|
1055
|
+
await setupCodeBuddy(result);
|
|
1056
|
+
if (selected.has('qoder'))
|
|
1057
|
+
await setupQoder(result);
|
|
888
1058
|
if (selected.has('codex'))
|
|
889
1059
|
await setupCodex(result);
|
|
890
1060
|
// Install global skills for platforms that support them
|
|
891
1061
|
if (selected.has('claude')) {
|
|
892
1062
|
await installClaudeCodeSkills(result);
|
|
893
|
-
await
|
|
1063
|
+
await installClaudeSchemaHooks(result, 'claude');
|
|
894
1064
|
}
|
|
895
1065
|
if (selected.has('antigravity')) {
|
|
896
1066
|
await installAntigravitySkills(result);
|
|
@@ -900,8 +1070,14 @@ export const setupCommand = async (options) => {
|
|
|
900
1070
|
await installCursorSkills(result);
|
|
901
1071
|
if (selected.has('opencode'))
|
|
902
1072
|
await installOpenCodeSkills(result);
|
|
903
|
-
if (selected.has('
|
|
1073
|
+
if (selected.has('codebuddy'))
|
|
1074
|
+
await installCodeBuddySkills(result);
|
|
1075
|
+
if (selected.has('qoder'))
|
|
1076
|
+
await installQoderSkills(result);
|
|
1077
|
+
if (selected.has('codex')) {
|
|
904
1078
|
await installCodexSkills(result);
|
|
1079
|
+
await installClaudeSchemaHooks(result, 'codex');
|
|
1080
|
+
}
|
|
905
1081
|
// Print results
|
|
906
1082
|
if (result.configured.length > 0) {
|
|
907
1083
|
console.log(' Configured:');
|
package/dist/cli/uninstall.js
CHANGED
|
@@ -33,7 +33,7 @@ import { execFile } from 'child_process';
|
|
|
33
33
|
import { promisify } from 'util';
|
|
34
34
|
import { fileURLToPath } from 'url';
|
|
35
35
|
import { parseTree, modify, applyEdits, findNodeAtLocation, parse as parseJsonc, } from 'jsonc-parser';
|
|
36
|
-
import { getEditorTargets, detectIndentation } from './editor-targets.js';
|
|
36
|
+
import { getEditorTargets, detectIndentation, isEnoent } from './editor-targets.js';
|
|
37
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
38
38
|
const __dirname = path.dirname(__filename);
|
|
39
39
|
const execFileAsync = promisify(execFile);
|
|
@@ -50,7 +50,13 @@ async function removeJsoncKey(filePath, keyPath, dryRun) {
|
|
|
50
50
|
try {
|
|
51
51
|
raw = await fs.readFile(filePath, 'utf-8');
|
|
52
52
|
}
|
|
53
|
-
catch {
|
|
53
|
+
catch (err) {
|
|
54
|
+
// ENOENT = genuinely not configured. Any other read failure (EACCES,
|
|
55
|
+
// locks) must not report 'missing' — the file may hold a real gitnexus
|
|
56
|
+
// entry the dry-run would then deny exists. Rethrow into the caller's
|
|
57
|
+
// per-file catch.
|
|
58
|
+
if (!isEnoent(err))
|
|
59
|
+
throw err;
|
|
54
60
|
return 'missing';
|
|
55
61
|
}
|
|
56
62
|
if (raw.trim().length === 0)
|
|
@@ -88,7 +94,12 @@ async function removeHookEntries(filePath, eventNames, commandNeedle, dryRun) {
|
|
|
88
94
|
try {
|
|
89
95
|
raw = await fs.readFile(filePath, 'utf-8');
|
|
90
96
|
}
|
|
91
|
-
catch {
|
|
97
|
+
catch (err) {
|
|
98
|
+
// Masking a non-ENOENT read failure as 'missing' would also let the
|
|
99
|
+
// caller delete the hook scriptDir while the unreadable settings file
|
|
100
|
+
// still references it. Rethrow into the hook uninstaller's catch.
|
|
101
|
+
if (!isEnoent(err))
|
|
102
|
+
throw err;
|
|
92
103
|
return { status: 'missing', count: 0 };
|
|
93
104
|
}
|
|
94
105
|
if (raw.trim().length === 0)
|
|
@@ -323,8 +334,15 @@ async function uninstallCodex(result, dryRun, configPath, tomlSection) {
|
|
|
323
334
|
try {
|
|
324
335
|
raw = await fs.readFile(configPath, 'utf-8');
|
|
325
336
|
}
|
|
326
|
-
catch {
|
|
327
|
-
|
|
337
|
+
catch (err) {
|
|
338
|
+
// Catch locally: this call site has no surrounding try, so a rethrow
|
|
339
|
+
// would abort the hooks/skills cleanup that runs after Codex.
|
|
340
|
+
if (isEnoent(err)) {
|
|
341
|
+
result.skipped.push('Codex MCP (not configured)');
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
result.errors.push(`Codex: ${err instanceof Error ? err.message : String(err)}`);
|
|
345
|
+
}
|
|
328
346
|
return;
|
|
329
347
|
}
|
|
330
348
|
if (!raw.includes(`[${tomlSection}]`)) {
|
|
@@ -369,20 +387,50 @@ export const uninstallCommand = async (options) => {
|
|
|
369
387
|
console.log(' Dry run — nothing will be changed. Re-run with --force to apply.');
|
|
370
388
|
console.log('');
|
|
371
389
|
}
|
|
372
|
-
const result = { removed: [], skipped: [], errors: [] };
|
|
390
|
+
const result = { removed: [], skipped: [], errors: [], corruptLegacy: false };
|
|
373
391
|
// ─── MCP server entries (JSONC editors) ──────────────────────────
|
|
392
|
+
// Sweep legacyFiles too: setup writes into the first existing file of the
|
|
393
|
+
// editor's priority chain, so the gitnexus entry may live in a deprecated
|
|
394
|
+
// location (e.g. CodeBuddy's ~/.codebuddy/mcp.json).
|
|
374
395
|
for (const target of targets.mcpJsonc) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
396
|
+
let removedAny = false;
|
|
397
|
+
let erroredAny = false;
|
|
398
|
+
let corruptLegacyAny = false;
|
|
399
|
+
for (const file of [target.file, ...(target.legacyFiles ?? [])]) {
|
|
400
|
+
try {
|
|
401
|
+
const status = await removeJsoncKey(file, target.keyPath, dryRun);
|
|
402
|
+
if (status === 'removed') {
|
|
403
|
+
removedAny = true;
|
|
404
|
+
result.removed.push(`${target.label} MCP server — ${target.keyPath.join('.')} in ${file}`);
|
|
405
|
+
}
|
|
406
|
+
else if (status === 'corrupt') {
|
|
407
|
+
if (file === target.file) {
|
|
408
|
+
// The primary path is where gitnexus itself writes — corruption
|
|
409
|
+
// there is an error worth failing the command over.
|
|
410
|
+
erroredAny = true;
|
|
411
|
+
result.errors.push(`${target.label}: ${path.basename(file)} is corrupt — left untouched`);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Legacy chain files are vendor locations gitnexus may never have
|
|
415
|
+
// touched (e.g. a corrupt ~/.codebuddy.json from an old install).
|
|
416
|
+
// Report informationally without failing uninstall. Deliberate
|
|
417
|
+
// asymmetry: an UNREADABLE (non-ENOENT) legacy file still errors —
|
|
418
|
+
// that's an environmental problem worth surfacing, while corrupt-
|
|
419
|
+
// but-readable proves there is no removable gitnexus entry.
|
|
420
|
+
corruptLegacyAny = true;
|
|
421
|
+
result.corruptLegacy = true;
|
|
422
|
+
result.skipped.push(`${target.label} MCP (legacy ${path.basename(file)} is corrupt — left untouched)`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
erroredAny = true;
|
|
428
|
+
result.errors.push(`${target.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
429
|
+
}
|
|
383
430
|
}
|
|
384
|
-
|
|
385
|
-
|
|
431
|
+
// A corrupt legacy file makes "not configured" unknowable — suppress it.
|
|
432
|
+
if (!removedAny && !erroredAny && !corruptLegacyAny) {
|
|
433
|
+
result.skipped.push(`${target.label} MCP (not configured)`);
|
|
386
434
|
}
|
|
387
435
|
}
|
|
388
436
|
await uninstallCodex(result, dryRun, targets.codex.configFile, targets.codex.tomlSection);
|
|
@@ -425,6 +473,12 @@ export const uninstallCommand = async (options) => {
|
|
|
425
473
|
for (const name of result.removed)
|
|
426
474
|
console.log(` - ${name}`);
|
|
427
475
|
}
|
|
476
|
+
else if (result.errors.length > 0 || result.corruptLegacy) {
|
|
477
|
+
// Errors (corrupt primary files, unreadable configs) or corrupt legacy
|
|
478
|
+
// configs make "not configured" unknowable — claiming it right above an
|
|
479
|
+
// Errors block would be a contradiction users learn to distrust.
|
|
480
|
+
console.log(' Nothing removed.');
|
|
481
|
+
}
|
|
428
482
|
else {
|
|
429
483
|
console.log(' Nothing to remove — GitNexus is not configured in any detected editor.');
|
|
430
484
|
}
|
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/integration/setup-uninstall-roundtrip.test.ts',
|
|
33
34
|
'test/unit/resolve-invocation.test.ts',
|
|
34
35
|
'test/unit/platform-capabilities.test.ts',
|
|
35
36
|
'test/unit/worker-pool-windows-quarantine.test.ts',
|