mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat: full Codex support — hooks, plugin marketplace, and setup (#2328, supersedes #1131) (#2369)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(setup): install Codex PreToolUse/PostToolUse hooks (#2328) Codex CLI supports lifecycle hooks with Claude Code's exact {hooks: {Event: [...]}} JSON schema, stdin payload, and hookSpecificOutput response contract, registered in a dedicated ~/.codex/hooks.json (https://developers.openai.com/codex/hooks). Parameterize installClaudeCodeHooks into installClaudeSchemaHooks (claude | codex): both runtimes share the installer, the bundled gitnexus-hook.cjs adapter, and its helpers. A codex HookTarget in editor-targets.ts makes uninstall and the setup-uninstall round-trip tripwire cover the new surface with no uninstall.ts changes. SessionStart is deliberately not registered: Codex reads AGENTS.md natively, which already carries the GitNexus context block. Closes #2328. Closes #244 (Codex setup support is now complete: MCP + skills + hooks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(plugin): make the GitNexus plugin installable from Codex (#1131) Codex's plugin system (https://developers.openai.com/codex/plugins/build) reads a .codex-plugin/plugin.json manifest and a repo-root .agents/plugins/marketplace.json registry. The existing gitnexus-claude-plugin/ is already Codex-compatible as-is — Codex sets CLAUDE_PLUGIN_ROOT for hook-command compatibility, loads the same SKILL.md skills, hooks/hooks.json, and .mcp.json — so a second manifest in the same folder replaces PR #1131's duplicated plugin tree with zero copied skills or hooks. The .gitignore .agents/ scratch rule narrows to re-include only the registry file. Install: codex plugin marketplace add abhigyanpatwari/GitNexus Supersedes #1131. Co-authored-by: jublin <1799126+jublin@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: document Codex full support (MCP + skills + hooks + plugin) Promote Codex to Full in both editor tables, document the ~/.codex/hooks.json hook install, and add the Codex plugin marketplace install path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(release): extend the version-lockstep guard to the Codex manifests The always-on drift guard asserted only the Claude plugin manifests against gitnexus/package.json, so a release could ship stale versions in .codex-plugin/plugin.json and .agents/plugins/marketplace.json without CI noticing. Mirror the Claude lockstep test for the two Codex files and extend the CONTRIBUTING §Releases lockstep list to match. Verified guard semantics: a deliberate local version mutation of the Codex marketplace entry turns the new test red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(plugin): quote the hook command path for space-containing plugin roots Both plugin hook commands ran `node ${CLAUDE_PLUGIN_ROOT}/hooks/...` unquoted, which breaks whenever the substituted plugin root contains a space — the common case on Windows user profiles. Both Claude Code and Codex substitute the placeholder before shell execution, and Claude Code's plugin docs mandate the double-quoted form in shell-form hooks. No commandWindows entry: Codex source (codex-rs hooks engine) falls back to `command` on Windows with identical placeholder substitution, so an identical-content override would be pure duplication. Verified: space-in-root smoke test (old form exits 1 MODULE_NOT_FOUND, quoted form exits 0), `claude plugin validate` passes, and a local `codex plugin marketplace add` parses the marketplace + plugin cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(setup): pin fail-closed behavior for unreadable/corrupt Codex hooks.json The non-ENOENT suite covered Claude settings.json (EACCES) and Codex config.toml (EACCES) but not the new ~/.codex/hooks.json surface, and the mergeHooksJsonc "is corrupt" branch had zero coverage for either editor. A future refactor dropping the isEnoent rethrow or the parse gate could silently rewrite a user's hooks.json gitnexus-only with no CI tripwire. Two regression tests: EACCES leaves hooks.json byte-identical and reports "Codex hooks: EACCES"; corrupt content is preserved and reported via "Codex hooks: hooks.json is corrupt". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(readme): add the Codex plugin-marketplace install path to the npm README The root README documents the one-step plugin route but the package README (what npmjs.com renders) only showed the setup-CLI path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(setup): rename claudeHook to hookCfg in installClaudeSchemaHooks The local held a codex HookTarget on the codex branch since the installer was parameterized, so the claude-specific name misled. Pure local rename, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(readme): document Codex SessionStart exclusion, /hooks trust gate, and install-route choice Three behaviors were only recorded in code comments and the PR body: SessionStart is deliberately not registered (Codex reads AGENTS.md natively), setup-installed hooks need one-time /hooks approval in Codex, and the setup CLI and plugin are alternative install routes whose hooks load alongside each other if both are used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(test): share one logLines helper across setup.test.ts describes The corrupt-hooks.json test inlined the console.log-flattening expression that the non-ENOENT describe already defined locally. Hoist a single file-scope logLines so the two stay in sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6252aa745f
commit
187c162fd8
11 changed files with 285 additions and 47 deletions
21
.agents/plugins/marketplace.json
Normal file
21
.agents/plugins/marketplace.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "gitnexus-marketplace",
|
||||
"interface": {
|
||||
"displayName": "GitNexus"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.9",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./gitnexus-claude-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Developer Tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -106,7 +106,12 @@ gitnexus/vendor/**/node_modules/
|
|||
local_docs/
|
||||
|
||||
# Local agent scratch / review prompts (never commit)
|
||||
# (.agents/plugins/marketplace.json is the checked-in Codex plugin
|
||||
# marketplace registry — the rest of .agents/ stays local scratch.)
|
||||
.tmp/
|
||||
.agents/
|
||||
.agents/*
|
||||
!.agents/plugins/
|
||||
.agents/plugins/*
|
||||
!.agents/plugins/marketplace.json
|
||||
.context/
|
||||
gitnexus/web/
|
||||
|
|
|
|||
|
|
@ -169,7 +169,9 @@ routes between two modes based on the triggering event:
|
|||
not enforce branch reachability. No Docker build (RC-only). Before cutting a
|
||||
stable release, keep `gitnexus/package.json`,
|
||||
`gitnexus-claude-plugin/.claude-plugin/plugin.json`,
|
||||
`.claude-plugin/marketplace.json`, and the matching `CHANGELOG.md` entry in
|
||||
`.claude-plugin/marketplace.json`,
|
||||
`gitnexus-claude-plugin/.codex-plugin/plugin.json`,
|
||||
`.agents/plugins/marketplace.json`, and the matching `CHANGELOG.md` entry in
|
||||
lockstep — the always-on `gitnexus` unit suite now fails if those manifest
|
||||
versions drift.
|
||||
- **Release-candidate mode** — runs on every push to `main` (typically a
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -199,13 +199,13 @@ flowchart TB
|
|||
| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
|
||||
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
|
||||
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/))[¹](#fn-antigravity-hooks) | **Full** |
|
||||
| **Codex** | Yes | Yes | — | MCP + Skills |
|
||||
| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** |
|
||||
| **OpenCode** | Yes | Yes | — | MCP + Skills |
|
||||
| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills |
|
||||
| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills |
|
||||
| **Windsurf** | Yes | — | — | MCP |
|
||||
|
||||
> **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
|
||||
> **Claude Code** and **Codex** get the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
|
||||
|
||||
<a id="fn-antigravity-hooks"></a>
|
||||
> ¹ **Antigravity hooks** follow the [Gemini CLI hooks reference](https://geminicli.com/docs/hooks/reference/) (Antigravity 2.0 is the documented successor to Gemini CLI). Augmentation runs in `AfterTool` because `BeforeTool` has no context-injection channel in the Gemini contract — the agent sees graph context appended to the tool result via `hookSpecificOutput.additionalContext`. Stale-index hints land in the same channel after a successful `git commit/merge/rebase/cherry-pick/pull`. The schema may evolve if Antigravity-specific hook docs diverge from Gemini CLI's; the implementation will track those changes.
|
||||
|
|
@ -223,7 +223,7 @@ claude mcp add gitnexus -- npx -y gitnexus@latest mcp
|
|||
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
|
||||
```
|
||||
|
||||
**Codex** (MCP + skills):
|
||||
**Codex** (full support — MCP + skills + hooks):
|
||||
|
||||
```bash
|
||||
codex mcp add gitnexus -- npx -y gitnexus@latest mcp
|
||||
|
|
@ -237,6 +237,17 @@ command = "npx"
|
|||
args = ["-y", "gitnexus@latest", "mcp"]
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Alternatively, install everything as a [Codex plugin](https://developers.openai.com/codex/plugins/build) (MCP + skills + hooks in one step):
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add abhigyanpatwari/GitNexus
|
||||
# then inside Codex: /plugins → install "GitNexus"
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
**Cursor** (`~/.cursor/mcp.json` — global, works for all projects):
|
||||
|
||||
```json
|
||||
|
|
|
|||
25
gitnexus-claude-plugin/.codex-plugin/plugin.json
Normal file
25
gitnexus-claude-plugin/.codex-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "gitnexus",
|
||||
"description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase.",
|
||||
"version": "1.6.9",
|
||||
"skills": "./skills",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"hooks": "./hooks/hooks.json",
|
||||
"interface": {
|
||||
"displayName": "GitNexus",
|
||||
"category": "Developer Tools",
|
||||
"capabilities": [
|
||||
"code-exploration",
|
||||
"impact-analysis",
|
||||
"debugging",
|
||||
"refactoring",
|
||||
"code-review"
|
||||
]
|
||||
},
|
||||
"author": {
|
||||
"name": "GitNexus"
|
||||
},
|
||||
"homepage": "https://github.com/abhigyanpatwari/GitNexus",
|
||||
"repository": "https://github.com/abhigyanpatwari/GitNexus",
|
||||
"keywords": ["code-intelligence", "knowledge-graph", "mcp", "static-analysis"]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js\"",
|
||||
"timeout": 10,
|
||||
"statusMessage": "Enriching with GitNexus graph context..."
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gitnexus-hook.js\"",
|
||||
"timeout": 10,
|
||||
"statusMessage": "Checking GitNexus index freshness..."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
|
|||
| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
|
||||
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](../gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
|
||||
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) | **Full** |
|
||||
| **Codex** | Yes | Yes | — | MCP + Skills |
|
||||
| **Codex** | Yes | Yes | Yes (PreToolUse + PostToolUse, [Codex hooks](https://developers.openai.com/codex/hooks)) | **Full** |
|
||||
| **OpenCode** | Yes | Yes | — | MCP + Skills |
|
||||
| **CodeBuddy** (Tencent) | Yes | Yes | — | MCP + Skills |
|
||||
| **Qoder** (Alibaba) | Yes | Yes | — | MCP + Skills |
|
||||
| **Windsurf** | Yes | — | — | MCP |
|
||||
|
||||
> **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.
|
||||
> **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.
|
||||
|
||||
### Community Integrations
|
||||
|
||||
|
|
@ -71,12 +71,23 @@ claude mcp add gitnexus -- npx -y gitnexus@latest mcp
|
|||
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
|
||||
```
|
||||
|
||||
### Codex (full support — MCP + skills)
|
||||
### Codex (full support — MCP + skills + hooks)
|
||||
|
||||
```bash
|
||||
codex mcp add gitnexus -- npx -y gitnexus@latest mcp
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Alternatively, install everything as a [Codex plugin](https://developers.openai.com/codex/plugins/build) (MCP + skills + hooks in one step):
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add abhigyanpatwari/GitNexus
|
||||
# then inside Codex: /plugins → install "GitNexus"
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
### Cursor / Windsurf
|
||||
|
||||
Add to `~/.cursor/mcp.json` (global — works for all projects):
|
||||
|
|
|
|||
|
|
@ -180,6 +180,17 @@ export function getEditorTargets(home: string = os.homedir()): EditorTargets {
|
|||
needle: 'gitnexus-hook',
|
||||
scriptDir: path.join(home, '.claude', 'hooks', 'gitnexus'),
|
||||
},
|
||||
{
|
||||
id: 'codex',
|
||||
label: 'Codex',
|
||||
// Codex hooks use Claude Code's exact {hooks: {Event: [...]}} JSON shape
|
||||
// and hookSpecificOutput response contract, in a dedicated hooks.json
|
||||
// (https://developers.openai.com/codex/hooks).
|
||||
settingsFile: path.join(home, '.codex', 'hooks.json'),
|
||||
events: ['PreToolUse', 'PostToolUse'],
|
||||
needle: 'gitnexus-hook',
|
||||
scriptDir: path.join(home, '.codex', 'hooks', 'gitnexus'),
|
||||
},
|
||||
{
|
||||
id: 'antigravity',
|
||||
label: 'Antigravity',
|
||||
|
|
|
|||
|
|
@ -487,22 +487,31 @@ export async function copyHookHelpers(
|
|||
}
|
||||
|
||||
/**
|
||||
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
||||
* Merges hook config without overwriting existing hooks, preserving
|
||||
* comments and formatting in the JSONC file.
|
||||
* Install GitNexus hooks for editors that use Claude Code's hooks schema.
|
||||
*
|
||||
* Claude Code registers hooks in ~/.claude/settings.json; Codex uses a
|
||||
* dedicated ~/.codex/hooks.json with the identical {hooks: {Event: [...]}}
|
||||
* JSON shape, stdin payload, and hookSpecificOutput response contract
|
||||
* (https://developers.openai.com/codex/hooks), so both runtimes share this
|
||||
* installer and the same bundled adapter script. Merges hook config without
|
||||
* overwriting existing hooks, preserving comments and formatting.
|
||||
*/
|
||||
async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
if (!(await dirExists(claudeDir))) return;
|
||||
async function installClaudeSchemaHooks(
|
||||
result: SetupResult,
|
||||
id: 'claude' | 'codex',
|
||||
): Promise<void> {
|
||||
const hookCfg = hookTarget(id);
|
||||
const settingsPath = hookCfg.settingsFile;
|
||||
const label = `${hookCfg.label} hooks`;
|
||||
|
||||
const claudeHook = hookTarget('claude');
|
||||
const settingsPath = claudeHook.settingsFile;
|
||||
// Gate on the editor's own config dir (~/.claude, ~/.codex) existing.
|
||||
if (!(await dirExists(path.dirname(settingsPath)))) return;
|
||||
|
||||
// Source hooks bundled within the gitnexus package (hooks/claude/)
|
||||
const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
|
||||
|
||||
// Copy unified hook script to ~/.claude/hooks/gitnexus/
|
||||
const destHooksDir = claudeHook.scriptDir;
|
||||
// Copy unified hook script to the editor's hooks/gitnexus/ dir
|
||||
const destHooksDir = hookCfg.scriptDir;
|
||||
|
||||
try {
|
||||
await fs.mkdir(destHooksDir, { recursive: true });
|
||||
|
|
@ -516,7 +525,7 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|||
const jsonCli = JSON.stringify(normalizedCli);
|
||||
if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
|
||||
result.errors.push(
|
||||
'Claude Code hooks: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.',
|
||||
`${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.`,
|
||||
);
|
||||
}
|
||||
content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
|
||||
|
|
@ -531,21 +540,14 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|||
try {
|
||||
await fs.access(dest);
|
||||
} catch {
|
||||
result.errors.push(
|
||||
'Claude Code hooks: adapter script was not installed — skipping hook registration',
|
||||
);
|
||||
result.errors.push(`${label}: adapter script was not installed — skipping hook registration`);
|
||||
return;
|
||||
}
|
||||
|
||||
const failedRequired = await copyHookHelpers(
|
||||
pluginHooksPath,
|
||||
destHooksDir,
|
||||
'Claude Code hooks',
|
||||
result,
|
||||
);
|
||||
const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, label, result);
|
||||
if (failedRequired.length > 0) {
|
||||
result.errors.push(
|
||||
`Claude Code hooks: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`,
|
||||
`${label}: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -565,10 +567,11 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|||
|
||||
const hookEntries: Array<{ eventName: string; value: unknown }> = [];
|
||||
|
||||
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
|
||||
// Session context is delivered via CLAUDE.md / skills instead.
|
||||
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576),
|
||||
// and Codex reads AGENTS.md natively. Session context is delivered via
|
||||
// CLAUDE.md / AGENTS.md / skills instead.
|
||||
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', claudeHook.needle)) {
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', hookCfg.needle)) {
|
||||
hookEntries.push({
|
||||
eventName: 'PreToolUse',
|
||||
value: {
|
||||
|
|
@ -584,7 +587,7 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|||
},
|
||||
});
|
||||
}
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', claudeHook.needle)) {
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', hookCfg.needle)) {
|
||||
hookEntries.push({
|
||||
eventName: 'PostToolUse',
|
||||
value: {
|
||||
|
|
@ -602,20 +605,20 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|||
}
|
||||
|
||||
if (hookEntries.length === 0) {
|
||||
result.configured.push('Claude Code hooks (already configured)');
|
||||
result.configured.push(`${label} (already configured)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await mergeHooksJsonc(settingsPath, hookEntries);
|
||||
if (ok) {
|
||||
result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
|
||||
result.configured.push(`${label} (PreToolUse, PostToolUse)`);
|
||||
} else {
|
||||
result.errors.push(
|
||||
'Claude Code hooks: settings.json is corrupt — skipping to preserve existing content',
|
||||
`${label}: ${path.basename(settingsPath)} is corrupt — skipping to preserve existing content`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
result.errors.push(`Claude Code hooks: ${err.message}`);
|
||||
result.errors.push(`${label}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1198,7 +1201,7 @@ export const setupCommand = async (options?: { codingAgent?: string[] | string }
|
|||
// Install global skills for platforms that support them
|
||||
if (selected.has('claude')) {
|
||||
await installClaudeCodeSkills(result);
|
||||
await installClaudeCodeHooks(result);
|
||||
await installClaudeSchemaHooks(result, 'claude');
|
||||
}
|
||||
if (selected.has('antigravity')) {
|
||||
await installAntigravitySkills(result);
|
||||
|
|
@ -1208,7 +1211,10 @@ export const setupCommand = async (options?: { codingAgent?: string[] | string }
|
|||
if (selected.has('opencode')) await installOpenCodeSkills(result);
|
||||
if (selected.has('codebuddy')) await installCodeBuddySkills(result);
|
||||
if (selected.has('qoder')) await installQoderSkills(result);
|
||||
if (selected.has('codex')) await installCodexSkills(result);
|
||||
if (selected.has('codex')) {
|
||||
await installCodexSkills(result);
|
||||
await installClaudeSchemaHooks(result, 'codex');
|
||||
}
|
||||
|
||||
// Print results
|
||||
if (result.configured.length > 0) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,26 @@ describe('CLI commands', () => {
|
|||
expect(pluginManifest.version).toBe(pkg.default.version);
|
||||
expect(gitnexusEntries[0]?.version).toBe(pkg.default.version);
|
||||
});
|
||||
|
||||
it('keeps Codex plugin manifests aligned with the gitnexus release version', async () => {
|
||||
const pkg = await import('../../package.json', { with: { type: 'json' } });
|
||||
const pluginManifest = await readRepoJson<{ version: string }>(
|
||||
'gitnexus-claude-plugin/.codex-plugin/plugin.json',
|
||||
);
|
||||
const marketplaceManifest = await readRepoJson<{
|
||||
plugins?: Array<{ name: string; version: string }>;
|
||||
}>('.agents/plugins/marketplace.json');
|
||||
|
||||
expect(Array.isArray(marketplaceManifest.plugins)).toBe(true);
|
||||
|
||||
const gitnexusEntries = (marketplaceManifest.plugins ?? []).filter(
|
||||
(plugin) => plugin.name === 'gitnexus',
|
||||
);
|
||||
|
||||
expect(gitnexusEntries).toHaveLength(1);
|
||||
expect(pluginManifest.version).toBe(pkg.default.version);
|
||||
expect(gitnexusEntries[0]?.version).toBe(pkg.default.version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('package.json scripts', () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ const PKG_VERSION = (createRequire(import.meta.url)('../../package.json') as { v
|
|||
.version;
|
||||
const MCP_PINNED_REF = `gitnexus@${PKG_VERSION}`;
|
||||
|
||||
/** Flatten the spied console.log calls into one searchable string. */
|
||||
const logLines = () =>
|
||||
vi
|
||||
.mocked(console.log)
|
||||
.mock.calls.map((call) => call.join(' '))
|
||||
.join('\n');
|
||||
|
||||
const execFileMock = vi.fn((...args: any[]) => {
|
||||
const callback = args.at(-1);
|
||||
if (typeof callback === 'function') {
|
||||
|
|
@ -708,6 +715,107 @@ describe('setupQoder', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Codex hooks (installClaudeSchemaHooks)', () => {
|
||||
let tempHome: string;
|
||||
let originalHome: string | undefined;
|
||||
let originalUserProfile: string | undefined;
|
||||
|
||||
const hooksJsonPath = () => path.join(tempHome, '.codex', 'hooks.json');
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
|
||||
originalHome = process.env.HOME;
|
||||
originalUserProfile = process.env.USERPROFILE;
|
||||
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-codex-hooks-'));
|
||||
process.env.HOME = tempHome;
|
||||
process.env.USERPROFILE = tempHome;
|
||||
|
||||
// Only create ~/.codex — no other editor directories so their
|
||||
// setup functions skip and don't pollute assertions.
|
||||
await fs.mkdir(path.join(tempHome, '.codex'), { recursive: true });
|
||||
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
process.env.HOME = originalHome;
|
||||
process.env.USERPROFILE = originalUserProfile;
|
||||
await fs.rm(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('registers PreToolUse + PostToolUse in ~/.codex/hooks.json and installs the adapter', async () => {
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const hooks = JSON.parse(await fs.readFile(hooksJsonPath(), 'utf-8')).hooks;
|
||||
expect(hooks).toMatchObject({
|
||||
PreToolUse: [{ matcher: 'Grep|Glob|Bash' }],
|
||||
PostToolUse: [{ matcher: 'Bash' }],
|
||||
});
|
||||
for (const event of ['PreToolUse', 'PostToolUse']) {
|
||||
expect(hooks[event][0].hooks[0].command).toContain('gitnexus-hook');
|
||||
}
|
||||
await expect(
|
||||
fs.access(path.join(tempHome, '.codex', 'hooks', 'gitnexus', 'gitnexus-hook.cjs')),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('is idempotent — a second setup run adds no duplicate entries', async () => {
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
await setupCommand();
|
||||
|
||||
const hooks = JSON.parse(await fs.readFile(hooksJsonPath(), 'utf-8')).hooks;
|
||||
expect(hooks.PreToolUse).toHaveLength(1);
|
||||
expect(hooks.PostToolUse).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('preserves a user-owned hook already present in hooks.json', async () => {
|
||||
await fs.writeFile(
|
||||
hooksJsonPath(),
|
||||
JSON.stringify({
|
||||
hooks: {
|
||||
PreToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'my-own-hook' }] }],
|
||||
},
|
||||
}),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
const hooks = JSON.parse(await fs.readFile(hooksJsonPath(), 'utf-8')).hooks;
|
||||
const commands: string[] = hooks.PreToolUse.flatMap((e: { hooks: { command: string }[] }) =>
|
||||
e.hooks.map((h) => h.command),
|
||||
);
|
||||
expect(commands).toContain('my-own-hook');
|
||||
expect(commands.some((c: string) => c.includes('gitnexus-hook'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not write hooks.json when ~/.codex is absent', async () => {
|
||||
await fs.rm(path.join(tempHome, '.codex'), { recursive: true, force: true });
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
await expect(fs.access(hooksJsonPath())).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('leaves a corrupt hooks.json untouched and reports it (fail closed)', async () => {
|
||||
const corrupt = '{ this is not valid json !!!';
|
||||
await fs.writeFile(hooksJsonPath(), corrupt, 'utf-8');
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
expect(await fs.readFile(hooksJsonPath(), 'utf-8')).toBe(corrupt);
|
||||
expect(logLines()).toContain('Codex hooks: hooks.json is corrupt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup — non-ENOENT read/stat failures are surfaced, not masked', () => {
|
||||
let tempHome: string;
|
||||
let originalHome: string | undefined;
|
||||
|
|
@ -716,12 +824,6 @@ describe('setup — non-ENOENT read/stat failures are surfaced, not masked', ()
|
|||
const errnoError = (code: string) =>
|
||||
Object.assign(new Error(`${code}: simulated failure`), { code });
|
||||
|
||||
const logLines = () =>
|
||||
vi
|
||||
.mocked(console.log)
|
||||
.mock.calls.map((call) => call.join(' '))
|
||||
.join('\n');
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -840,6 +942,30 @@ describe('setup — non-ENOENT read/stat failures are surfaced, not masked', ()
|
|||
expect(await fs.readFile(configPath, 'utf-8')).toBe(raw);
|
||||
expect(logLines()).toContain('Codex: EACCES');
|
||||
});
|
||||
|
||||
it('does not rewrite an unreadable ~/.codex/hooks.json as hooks-only (fail closed)', async () => {
|
||||
await fs.mkdir(path.join(tempHome, '.codex'), { recursive: true });
|
||||
const hooksPath = path.join(tempHome, '.codex', 'hooks.json');
|
||||
const raw = JSON.stringify({
|
||||
hooks: { PreToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'mine' }] }] },
|
||||
});
|
||||
await fs.writeFile(hooksPath, raw, 'utf-8');
|
||||
|
||||
const realReadFile = fs.readFile;
|
||||
vi.spyOn(fs, 'readFile').mockImplementation(((file: any, ...rest: any[]) => {
|
||||
if (String(file) === hooksPath) return Promise.reject(errnoError('EACCES'));
|
||||
return (realReadFile as any)(file, ...rest);
|
||||
}) as typeof fs.readFile);
|
||||
|
||||
const { setupCommand } = await import('../../src/cli/setup.js');
|
||||
await setupCommand();
|
||||
|
||||
vi.mocked(fs.readFile).mockRestore();
|
||||
// The user's hooks survive; the installer reports instead of replacing
|
||||
// the whole file with a gitnexus-only document.
|
||||
expect(await fs.readFile(hooksPath, 'utf-8')).toBe(raw);
|
||||
expect(logLines()).toContain('Codex hooks: EACCES');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatHookCommand (hook command escaping, #1945)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue