gitnexus 1.6.9 → 1.6.10-rc.1
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 +60 -28
- 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 +33 -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 +171 -4
- 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/)
|
|
@@ -45,6 +45,8 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|
|
45
45
|
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
|
|
46
46
|
| **Codex** | Yes | Yes | — | MCP + Skills |
|
|
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
52
|
> **Claude Code** gets 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.
|
|
@@ -105,6 +107,36 @@ Add to `~/.config/opencode/config.json`:
|
|
|
105
107
|
}
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
### CodeBuddy
|
|
111
|
+
|
|
112
|
+
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`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"gitnexus": {
|
|
118
|
+
"command": "npx",
|
|
119
|
+
"args": ["-y", "gitnexus@latest", "mcp"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Qoder
|
|
126
|
+
|
|
127
|
+
Add to `~/.qoder.json`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"gitnexus": {
|
|
133
|
+
"command": "npx",
|
|
134
|
+
"args": ["-y", "gitnexus@latest", "mcp"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
108
140
|
## How It Works
|
|
109
141
|
|
|
110
142
|
GitNexus builds a complete knowledge graph of your codebase through a multi-phase indexing pipeline:
|
|
@@ -125,24 +157,24 @@ The result is a **LadybugDB graph database** stored locally in `.gitnexus/` with
|
|
|
125
157
|
Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
|
|
126
158
|
|
|
127
159
|
| 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
|
|
160
|
+
| ---------------- | ---------------------------------------------------------------------- |
|
|
161
|
+
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) |
|
|
162
|
+
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) |
|
|
163
|
+
| `context` | 360-degree symbol view — categorized refs, process participation |
|
|
164
|
+
| `impact` | Blast radius analysis with depth grouping and confidence |
|
|
165
|
+
| `trace` | Shortest directed path between two symbols (call + class-member edges) |
|
|
166
|
+
| `detect_changes` | Git-diff impact — maps changed lines to affected processes |
|
|
167
|
+
| `check` | Read-only structural checks against the indexed graph |
|
|
168
|
+
| `rename` | Multi-file coordinated rename with graph + text search |
|
|
169
|
+
| `cypher` | Raw Cypher graph queries |
|
|
170
|
+
| `route_map` | API route map — which components fetch which endpoints, and handlers |
|
|
171
|
+
| `tool_map` | MCP/RPC tool definitions — where they're defined and handled |
|
|
172
|
+
| `shape_check` | Validate API response shapes against consumers' property accesses |
|
|
173
|
+
| `api_impact` | Pre-change impact report for an API route handler |
|
|
174
|
+
| `explain` | Explain persisted taint findings (source→sink flows, `--pdg` indexes) |
|
|
175
|
+
| `pdg_query` | Query control/data dependence at statement level (`--pdg` indexes) |
|
|
176
|
+
| `group_list` | List configured repository groups |
|
|
177
|
+
| `group_sync` | Rebuild a group's Contract Registry and cross-repo links |
|
|
146
178
|
|
|
147
179
|
> 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
180
|
|
|
@@ -383,13 +415,13 @@ GitNexus uses optional DuckDB extensions for BM25 and vector search. The `gitnex
|
|
|
383
415
|
|
|
384
416
|
Configure the behavior with these environment variables:
|
|
385
417
|
|
|
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.
|
|
418
|
+
| Variable | Values | Default | Effect |
|
|
419
|
+
| -------------------------------------------- | ---------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
420
|
+
| `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. |
|
|
421
|
+
| `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
|
|
422
|
+
| `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
423
|
| `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.
|
|
424
|
+
| `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
425
|
|
|
394
426
|
```bash
|
|
395
427
|
# Offline/airgapped: never reach the network for extensions
|
|
@@ -464,9 +496,9 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
|
|
|
464
496
|
|
|
465
497
|
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
498
|
|
|
467
|
-
| Variable
|
|
468
|
-
|
|
|
469
|
-
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS`
|
|
499
|
+
| Variable | Default | Effect |
|
|
500
|
+
| ----------------------------------- | ------- | ---------------------------------------------------------------------------------- |
|
|
501
|
+
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
|
|
470
502
|
|
|
471
503
|
Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
|
|
472
504
|
|
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
|
];
|
|
@@ -110,6 +134,15 @@ export function hookTarget(id, home) {
|
|
|
110
134
|
throw new Error(`No hook target for editor "${id}"`);
|
|
111
135
|
return t;
|
|
112
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* True when err is a Node fs error with code ENOENT (file/dir absent).
|
|
139
|
+
* Shared by setup and uninstall: both must swallow ONLY absence when reading
|
|
140
|
+
* editor configs — any other read/stat failure (EACCES, EIO) is surfaced so an
|
|
141
|
+
* unreadable config is never treated as empty and rewritten gitnexus-only.
|
|
142
|
+
*/
|
|
143
|
+
export function isEnoent(err) {
|
|
144
|
+
return err?.code === 'ENOENT';
|
|
145
|
+
}
|
|
113
146
|
/**
|
|
114
147
|
* Detect indentation style from file content so JSONC edits preserve the file's
|
|
115
148
|
* 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) {
|
|
@@ -660,6 +702,118 @@ async function setupOpenCode(result) {
|
|
|
660
702
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
661
703
|
}
|
|
662
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Resolve which config file in a target's [file, ...legacyFiles] priority
|
|
707
|
+
* chain setup should write into: the first that exists, else the recommended
|
|
708
|
+
* `file`. CodeBuddy reads only the first existing file in its chain, so
|
|
709
|
+
* creating the recommended file above a populated deprecated one would shadow
|
|
710
|
+
* the user's existing MCP servers.
|
|
711
|
+
*/
|
|
712
|
+
async function resolveMcpConfigFile(target) {
|
|
713
|
+
for (const candidate of [target.file, ...(target.legacyFiles ?? [])]) {
|
|
714
|
+
try {
|
|
715
|
+
const stat = await fs.stat(candidate);
|
|
716
|
+
// Non-empty regular files only: a 0-byte recommended file must not
|
|
717
|
+
// shadow a populated deprecated one (mergeJsoncFile treats empty as a
|
|
718
|
+
// fresh document anyway), and directories are never config candidates.
|
|
719
|
+
if (stat.isFile() && stat.size > 0)
|
|
720
|
+
return candidate;
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
// ENOENT = candidate absent — try the next one. Anything else (EACCES
|
|
724
|
+
// on the file or a parent) is surfaced: silently skipping could route
|
|
725
|
+
// the write to a lower-priority file the editor never reads.
|
|
726
|
+
if (!isEnoent(err))
|
|
727
|
+
throw err;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return target.file;
|
|
731
|
+
}
|
|
732
|
+
async function setupCodeBuddy(result) {
|
|
733
|
+
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
|
734
|
+
const target = mcpTarget('codebuddy');
|
|
735
|
+
// Installed = the config dir exists OR any registered MCP config file does.
|
|
736
|
+
// A user whose only trace is a root-level config (e.g. a legacy
|
|
737
|
+
// ~/.codebuddy.json) still gets configured — uninstall already handles that
|
|
738
|
+
// shape, so setup skipping it was an asymmetry (PR #2368 review I4).
|
|
739
|
+
if (!(await dirExists(codebuddyDir)) && !(await anyChainConfigFile(target))) {
|
|
740
|
+
result.skipped.push('CodeBuddy (not installed)');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const configFile = await resolveMcpConfigFile(target);
|
|
745
|
+
const ok = await mergeJsoncFile(configFile, target.keyPath, getMcpEntry());
|
|
746
|
+
if (ok) {
|
|
747
|
+
result.configured.push('CodeBuddy');
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
result.errors.push(`CodeBuddy: ${path.basename(configFile)} is corrupt — skipping to preserve existing content`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
result.errors.push(`CodeBuddy: ${err instanceof Error ? err.message : String(err)}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function setupQoder(result) {
|
|
758
|
+
const qoderDir = path.join(os.homedir(), '.qoder');
|
|
759
|
+
const target = mcpTarget('qoder');
|
|
760
|
+
const { file: mcpPath, keyPath } = target;
|
|
761
|
+
// Same chain-aware detection as CodeBuddy: ~/.qoder.json alone counts.
|
|
762
|
+
if (!(await dirExists(qoderDir)) && !(await anyChainConfigFile(target))) {
|
|
763
|
+
result.skipped.push('Qoder (not installed)');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
|
768
|
+
if (ok) {
|
|
769
|
+
result.configured.push('Qoder');
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
result.errors.push('Qoder: .qoder.json is corrupt — skipping to preserve existing content');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
result.errors.push(`Qoder: ${err instanceof Error ? err.message : String(err)}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Install global CodeBuddy skills to ~/.codebuddy/skills/
|
|
781
|
+
* (https://www.codebuddy.ai/docs/cli/skills — same SKILL.md layout as Claude Code).
|
|
782
|
+
*/
|
|
783
|
+
async function installCodeBuddySkills(result) {
|
|
784
|
+
const codebuddyDir = path.join(os.homedir(), '.codebuddy');
|
|
785
|
+
if (!(await dirExists(codebuddyDir)))
|
|
786
|
+
return;
|
|
787
|
+
const skillsDir = skillTarget('codebuddy').dir;
|
|
788
|
+
try {
|
|
789
|
+
const installed = await installSkillsTo(skillsDir);
|
|
790
|
+
if (installed.length > 0) {
|
|
791
|
+
result.configured.push(`CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
result.errors.push(`CodeBuddy skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Install global Qoder skills to ~/.qoder/skills/
|
|
800
|
+
* (https://docs.qoder.com/extensions/skills — same SKILL.md layout as Claude Code).
|
|
801
|
+
*/
|
|
802
|
+
async function installQoderSkills(result) {
|
|
803
|
+
const qoderDir = path.join(os.homedir(), '.qoder');
|
|
804
|
+
if (!(await dirExists(qoderDir)))
|
|
805
|
+
return;
|
|
806
|
+
const skillsDir = skillTarget('qoder').dir;
|
|
807
|
+
try {
|
|
808
|
+
const installed = await installSkillsTo(skillsDir);
|
|
809
|
+
if (installed.length > 0) {
|
|
810
|
+
result.configured.push(`Qoder skills (${installed.length} skills → ~/.qoder/skills/)`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
result.errors.push(`Qoder skills: ${err instanceof Error ? err.message : String(err)}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
663
817
|
/**
|
|
664
818
|
* Build a TOML section for Codex MCP config (~/.codex/config.toml).
|
|
665
819
|
*/
|
|
@@ -677,7 +831,12 @@ async function upsertCodexConfigToml(configPath) {
|
|
|
677
831
|
try {
|
|
678
832
|
existing = await fs.readFile(configPath, 'utf-8');
|
|
679
833
|
}
|
|
680
|
-
catch {
|
|
834
|
+
catch (err) {
|
|
835
|
+
// TOML variant of the mergeJsoncFile contract: treating a non-ENOENT read
|
|
836
|
+
// failure as an empty config would rewrite config.toml with only the
|
|
837
|
+
// gitnexus section. Rethrow into setupCodex's catch.
|
|
838
|
+
if (!isEnoent(err))
|
|
839
|
+
throw err;
|
|
681
840
|
existing = '';
|
|
682
841
|
}
|
|
683
842
|
if (existing.includes(`[${getEditorTargets().codex.tomlSection}]`)) {
|
|
@@ -885,6 +1044,10 @@ export const setupCommand = async (options) => {
|
|
|
885
1044
|
await setupAntigravity(result);
|
|
886
1045
|
if (selected.has('opencode'))
|
|
887
1046
|
await setupOpenCode(result);
|
|
1047
|
+
if (selected.has('codebuddy'))
|
|
1048
|
+
await setupCodeBuddy(result);
|
|
1049
|
+
if (selected.has('qoder'))
|
|
1050
|
+
await setupQoder(result);
|
|
888
1051
|
if (selected.has('codex'))
|
|
889
1052
|
await setupCodex(result);
|
|
890
1053
|
// Install global skills for platforms that support them
|
|
@@ -900,6 +1063,10 @@ export const setupCommand = async (options) => {
|
|
|
900
1063
|
await installCursorSkills(result);
|
|
901
1064
|
if (selected.has('opencode'))
|
|
902
1065
|
await installOpenCodeSkills(result);
|
|
1066
|
+
if (selected.has('codebuddy'))
|
|
1067
|
+
await installCodeBuddySkills(result);
|
|
1068
|
+
if (selected.has('qoder'))
|
|
1069
|
+
await installQoderSkills(result);
|
|
903
1070
|
if (selected.has('codex'))
|
|
904
1071
|
await installCodexSkills(result);
|
|
905
1072
|
// Print results
|
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',
|