feat(installer): stop auto-indexing on install + ship opt-in front-load prompt hook
`codegraph install` no longer indexes the current directory — it wires up agents only, and building a project's graph is always the explicit `codegraph init` / `index`. Removes the global-vs-local inconsistency (a local install silently indexed, a global one didn't) and the docs/behavior mismatch (#826). README updated to match; the stale `init --index` note (indexing is default now) fixed. Adds an opt-in Claude Code front-load hook: a `UserPromptSubmit` hook that runs the new hidden `codegraph prompt-hook`, which injects codegraph_explore context for structural ("how / where / trace / impact") prompts so the agent answers from the graph instead of grepping to rebuild it. Prompted at install (default-yes; Claude-only — the only agent with prompt hooks), removed on uninstall, and `codegraph upgrade` self-heals it onto an already-configured global Claude install. Strictly additive + degradable: non-structural prompts, un-indexed projects, and any failure are silent no-ops. Disable without uninstalling via CODEGRAPH_NO_PROMPT_HOOK=1. 7 new installer-targets contract tests (write / idempotent / opt-out round-trip / sibling-preserved / uninstall / legacy-independent). Full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
212dfc4b6a
commit
bd4814d8c1
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
|
- **Claude Code:** an optional front-load hook makes your agent reach for CodeGraph automatically. When you ask a structural question — "how does X work", "what calls Y", "trace the flow from A to B" — CodeGraph injects the relevant source and call paths into the prompt up front, so the agent answers from the graph instead of grepping around to rebuild it. You're asked during `codegraph install` (default yes; Claude Code only, since it's the agent with prompt hooks), it's removed by `codegraph uninstall`, and `codegraph upgrade` turns it on for existing Claude setups. It's strictly additive and degradable — non-structural prompts and un-indexed projects are left alone — and you can switch it off any time without uninstalling by setting `CODEGRAPH_NO_PROMPT_HOOK=1`.
|
||||||
- Vue store actions, mutations, and getters are now indexed as symbols you can find and read. Whether your store is **Vuex** (`mutations` / `actions` objects in a module) or **Pinia** — both the options form (`defineStore({ actions: { … } })`) and the setup form (`defineStore('id', () => { … })`, where actions are local functions) — each action, mutation, and getter is now a real node. So `codegraph search` finds `login` or `getSessionList`, and `codegraph_explore` / `codegraph_node` show its body and what it calls, instead of "not found" because the function only existed as an object-literal property.
|
- Vue store actions, mutations, and getters are now indexed as symbols you can find and read. Whether your store is **Vuex** (`mutations` / `actions` objects in a module) or **Pinia** — both the options form (`defineStore({ actions: { … } })`) and the setup form (`defineStore('id', () => { … })`, where actions are local functions) — each action, mutation, and getter is now a real node. So `codegraph search` finds `login` or `getSessionList`, and `codegraph_explore` / `codegraph_node` show its body and what it calls, instead of "not found" because the function only existed as an object-literal property.
|
||||||
- `codegraph_explore` now connects a Vue component to the **Pinia** store action it calls. When code does `const store = useUserStore()` and then `store.fetchUser()`, that call now links through to the `fetchUser` action in the store module — so "what happens when this view loads its data?" traces from the component into the action's body instead of stopping at the `store.fetchUser()` line. Works for both Pinia store styles (options and setup), and stays precise (a built-in like `store.$patch()` or an unrelated same-named method isn't mislinked).
|
- `codegraph_explore` now connects a Vue component to the **Pinia** store action it calls. When code does `const store = useUserStore()` and then `store.fetchUser()`, that call now links through to the `fetchUser` action in the store module — so "what happens when this view loads its data?" traces from the component into the action's body instead of stopping at the `store.fetchUser()` line. Works for both Pinia store styles (options and setup), and stays precise (a built-in like `store.$patch()` or an unrelated same-named method isn't mislinked).
|
||||||
- `codegraph_explore` now follows **Vuex** string dispatch. A `dispatch('user/login')` or `commit('SET_TOKEN')` call — namespaced `'module/action'` keys included — now links to the action or mutation it names, resolved to the correct store module even when several modules share an action name (and without being fooled by a same-named `api/` helper). So "what runs when this dispatches?" traces from the call into the store handler and on to the mutations it commits. Vuex's canonical `export default { namespaced, actions, mutations }` module shape is now indexed too, so those handlers are findable symbols.
|
- `codegraph_explore` now follows **Vuex** string dispatch. A `dispatch('user/login')` or `commit('SET_TOKEN')` call — namespaced `'module/action'` keys included — now links to the action or mutation it names, resolved to the correct store module even when several modules share an action name (and without being fooled by a same-named `api/` helper). So "what runs when this dispatches?" traces from the call into the store handler and on to the mutations it commits. Vuex's canonical `export default { namespaced, actions, mutations }` module shape is now indexed too, so those handlers are findable symbols.
|
||||||
@@ -30,6 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- `codegraph install` now wires up your agents and stops there — it no longer indexes the current directory. Building a project's graph is always the explicit `codegraph init` (or `codegraph index`), so you decide what gets indexed and when, and the steps are the same whether you installed globally or just for one project. This clears up the confusion where a project-local install silently indexed but a global one didn't, and where the docs and the tool disagreed about whether you still had to run `init`. (#826)
|
||||||
- React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `<Button/>` usage links back to the component, so callers and blast radius are complete. This is the standard shadcn/ui declaration style, so for typical React design systems the whole UI layer is no longer invisible to impact analysis. Thanks @Arlandaren for the report and @maxmilian for the root-cause. (#841)
|
- React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `<Button/>` usage links back to the component, so callers and blast radius are complete. This is the standard shadcn/ui declaration style, so for typical React design systems the whole UI layer is no longer invisible to impact analysis. Thanks @Arlandaren for the report and @maxmilian for the root-cause. (#841)
|
||||||
- React Router and Next.js routes defined in `.tsx` / `.jsx` files are now indexed. Routes written as JSX — `<Route path="/users" element={<UsersPage/>}/>`, `createBrowserRouter([...])`, and Next.js `app/`/`pages/` page files — were being skipped entirely (only routes that happened to live in plain `.ts`/`.js` were picked up), so "what renders at this path?" and the route → page-component link were missing for most React apps. Now those routes show up in `codegraph search`/`codegraph_explore` and connect to the component they render, just like the backend route → handler links on other frameworks.
|
- React Router and Next.js routes defined in `.tsx` / `.jsx` files are now indexed. Routes written as JSX — `<Route path="/users" element={<UsersPage/>}/>`, `createBrowserRouter([...])`, and Next.js `app/`/`pages/` page files — were being skipped entirely (only routes that happened to live in plain `.ts`/`.js` were picked up), so "what renders at this path?" and the route → page-component link were missing for most React apps. Now those routes show up in `codegraph search`/`codegraph_explore` and connect to the component they render, just like the backend route → handler links on other frameworks.
|
||||||
- `codegraph sync` (and the file-watcher auto-sync behind always-on / MCP use) no longer drops a function's callers when you edit the file that function lives in. Re-indexing a file after an edit — even a docstring-only change — was severing the `calls` / `references` edges coming from *other, unchanged* files (e.g. `from pkg import mod; mod.fn()` callers, or any cross-file reference), so `codegraph callers` / `impact` would abruptly report "no callers" for a function that's used throughout the codebase, until the next full re-index. Sync now preserves those incoming cross-file edges across the re-index, re-matching them to the symbol even when the edit shifted its line number. Most visible in large Python package trees, but it applies to every language. Thanks @JosefAschauer. (#899)
|
- `codegraph sync` (and the file-watcher auto-sync behind always-on / MCP use) no longer drops a function's callers when you edit the file that function lives in. Re-indexing a file after an edit — even a docstring-only change — was severing the `calls` / `references` edges coming from *other, unchanged* files (e.g. `from pkg import mod; mod.fn()` callers, or any cross-file reference), so `codegraph callers` / `impact` would abruptly report "no callers" for a function that's used throughout the codebase, until the next full re-index. Sync now preserves those incoming cross-file edges across the re-index, re-matching them to the symbol even when the edit shifted its line number. Most visible in large Python package trees, but it applies to every language. Thanks @JosefAschauer. (#899)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you
|
|||||||
codegraph install
|
codegraph install
|
||||||
```
|
```
|
||||||
|
|
||||||
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
|
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
|
||||||
|
|
||||||
### 3. Initialize each project
|
### 3. Initialize each project
|
||||||
|
|
||||||
@@ -341,7 +341,8 @@ The installer will:
|
|||||||
- Ask whether configs apply to all your projects or just this one
|
- Ask whether configs apply to all your projects or just this one
|
||||||
- Write each chosen agent's MCP server config, plus a small marker-fenced CodeGraph section in the agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) — that's how subagents and non-MCP agents learn the `codegraph explore` command, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
|
- Write each chosen agent's MCP server config, plus a small marker-fenced CodeGraph section in the agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) — that's how subagents and non-MCP agents learn the `codegraph explore` command, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
|
||||||
- Set up auto-allow permissions when Claude Code is one of the targets
|
- Set up auto-allow permissions when Claude Code is one of the targets
|
||||||
- Initialize your current project (local installs only)
|
|
||||||
|
The installer **wires up your agents only — it does not index your code.** After it finishes, build each project's graph yourself with `codegraph init` (step 3). One global `codegraph install` covers every project; you run `codegraph init` once per project.
|
||||||
|
|
||||||
**Non-interactive (scripting / CI):**
|
**Non-interactive (scripting / CI):**
|
||||||
|
|
||||||
@@ -466,7 +467,7 @@ The exact text is `src/mcp/server-instructions.ts` — the single source of trut
|
|||||||
codegraph # Run interactive installer
|
codegraph # Run interactive installer
|
||||||
codegraph install # Run installer (explicit)
|
codegraph install # Run installer (explicit)
|
||||||
codegraph uninstall # Remove CodeGraph from your agents (inverse of install)
|
codegraph uninstall # Remove CodeGraph from your agents (inverse of install)
|
||||||
codegraph init [path] # Initialize in a project (--index to also index)
|
codegraph init [path] # Initialize a project + build its graph (one step)
|
||||||
codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
|
codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
|
||||||
codegraph index [path] # Full index (--force to re-index, --quiet for less output)
|
codegraph index [path] # Full index (--force to re-index, --quiet for less output)
|
||||||
codegraph sync [path] # Incremental update
|
codegraph sync [path] # Incremental update
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import * as os from 'os';
|
|||||||
import { ALL_TARGETS, getTarget, resolveTargetFlag } from '../src/installer/targets/registry';
|
import { ALL_TARGETS, getTarget, resolveTargetFlag } from '../src/installer/targets/registry';
|
||||||
import { uninstallTargets } from '../src/installer';
|
import { uninstallTargets } from '../src/installer';
|
||||||
import { upsertTomlTable, removeTomlTable, buildTomlTable } from '../src/installer/targets/toml';
|
import { upsertTomlTable, removeTomlTable, buildTomlTable } from '../src/installer/targets/toml';
|
||||||
import { cleanupLegacyHooks } from '../src/installer/targets/claude';
|
import { cleanupLegacyHooks, writePromptHookEntry, removePromptHookEntry } from '../src/installer/targets/claude';
|
||||||
|
|
||||||
function mkTmpDir(label: string): string {
|
function mkTmpDir(label: string): string {
|
||||||
return fs.mkdtempSync(path.join(os.tmpdir(), `cg-targets-${label}-`));
|
return fs.mkdtempSync(path.join(os.tmpdir(), `cg-targets-${label}-`));
|
||||||
@@ -1097,6 +1097,84 @@ describe('Installer targets — partial-state idempotency', () => {
|
|||||||
// Both events emptied → the whole `hooks` object is removed.
|
// Both events emptied → the whole `hooks` object is removed.
|
||||||
expect(after.hooks).toBeUndefined();
|
expect(after.hooks).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Front-load prompt hook (UserPromptSubmit) — #841 follow-up ----
|
||||||
|
// Opt-in (default-yes in the installer) UserPromptSubmit hook that runs
|
||||||
|
// `codegraph prompt-hook`. Must write/remove surgically, be idempotent, and
|
||||||
|
// round-trip an opt-out — without disturbing the user's own hooks.
|
||||||
|
const promptCommands = (s: any): string[] =>
|
||||||
|
(s.hooks?.UserPromptSubmit ?? []).flatMap((g: any) => (g.hooks ?? []).map((h: any) => h.command));
|
||||||
|
|
||||||
|
it('claude: install with promptHook:true writes the UserPromptSubmit hook (alongside permissions)', () => {
|
||||||
|
const claude = getTarget('claude')!;
|
||||||
|
claude.install('global', { autoAllow: true, promptHook: true });
|
||||||
|
const s = JSON.parse(fs.readFileSync(path.join(tmpHome, '.claude', 'settings.json'), 'utf-8'));
|
||||||
|
expect(promptCommands(s)).toContain('codegraph prompt-hook');
|
||||||
|
expect(s.permissions?.allow).toContain('mcp__codegraph__*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: install without promptHook does NOT add the hook', () => {
|
||||||
|
const claude = getTarget('claude')!;
|
||||||
|
claude.install('global', { autoAllow: true });
|
||||||
|
const s = JSON.parse(fs.readFileSync(path.join(tmpHome, '.claude', 'settings.json'), 'utf-8'));
|
||||||
|
expect(promptCommands(s)).not.toContain('codegraph prompt-hook');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: install with promptHook:true is idempotent (no duplicate, byte-identical re-run)', () => {
|
||||||
|
const claude = getTarget('claude')!;
|
||||||
|
const file = path.join(tmpHome, '.claude', 'settings.json');
|
||||||
|
claude.install('global', { autoAllow: true, promptHook: true });
|
||||||
|
const first = fs.readFileSync(file, 'utf-8');
|
||||||
|
claude.install('global', { autoAllow: true, promptHook: true });
|
||||||
|
expect(fs.readFileSync(file, 'utf-8')).toBe(first);
|
||||||
|
const s = JSON.parse(first);
|
||||||
|
expect(promptCommands(s).filter((c: string) => c === 'codegraph prompt-hook')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: install with promptHook:false strips a hook a prior install wrote (opt-out round-trips)', () => {
|
||||||
|
const claude = getTarget('claude')!;
|
||||||
|
claude.install('global', { autoAllow: true, promptHook: true });
|
||||||
|
claude.install('global', { autoAllow: true, promptHook: false });
|
||||||
|
const s = JSON.parse(fs.readFileSync(path.join(tmpHome, '.claude', 'settings.json'), 'utf-8'));
|
||||||
|
expect(promptCommands(s)).not.toContain('codegraph prompt-hook');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: writePromptHookEntry preserves a sibling UserPromptSubmit hook', () => {
|
||||||
|
const file = seedSettings('global', {
|
||||||
|
hooks: { UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'my-own-hook' }] }] },
|
||||||
|
});
|
||||||
|
expect(writePromptHookEntry('global').action).toBe('updated');
|
||||||
|
const s = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
expect(promptCommands(s)).toEqual(['my-own-hook', 'codegraph prompt-hook']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: uninstall removes the prompt hook but keeps the user\'s sibling', () => {
|
||||||
|
const file = seedSettings('global', {
|
||||||
|
hooks: {
|
||||||
|
UserPromptSubmit: [
|
||||||
|
{ hooks: [{ type: 'command', command: 'codegraph prompt-hook' }] },
|
||||||
|
{ hooks: [{ type: 'command', command: 'my-own-hook' }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getTarget('claude')!.uninstall('global');
|
||||||
|
const s = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
expect(promptCommands(s)).toEqual(['my-own-hook']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('claude: removePromptHookEntry leaves the legacy auto-sync hook untouched', () => {
|
||||||
|
const file = seedSettings('global', {
|
||||||
|
hooks: {
|
||||||
|
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'codegraph prompt-hook' }] }],
|
||||||
|
Stop: [{ hooks: [{ type: 'command', command: 'codegraph sync-if-dirty' }] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(removePromptHookEntry('global').action).toBe('removed');
|
||||||
|
const s = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||||
|
expect(promptCommands(s)).not.toContain('codegraph prompt-hook');
|
||||||
|
const stopCmds = (s.hooks?.Stop ?? []).flatMap((g: any) => (g.hooks ?? []).map((h: any) => h.command));
|
||||||
|
expect(stopCmds).toContain('codegraph sync-if-dirty');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Installer targets — registry', () => {
|
describe('Installer targets — registry', () => {
|
||||||
|
|||||||
@@ -1017,6 +1017,82 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* codegraph prompt-hook (hidden)
|
||||||
|
*
|
||||||
|
* A Claude Code `UserPromptSubmit` hook entry point. Reads `{prompt, cwd}` JSON
|
||||||
|
* on stdin; for a structural/flow/impact prompt it runs `codegraph_explore` on
|
||||||
|
* the indexed project and prints the result to stdout, which Claude injects into
|
||||||
|
* the agent's context — so the agent's reflex grep/read has nothing left to find
|
||||||
|
* and reliably uses CodeGraph (the adoption problem). Installed by the installer
|
||||||
|
* into Claude's settings.json (opt-in, default-yes).
|
||||||
|
*
|
||||||
|
* LOAD-BEARING: this must NEVER break the user's prompt. Every failure path —
|
||||||
|
* kill-switch, non-structural prompt, no index, engine error — exits 0 with no
|
||||||
|
* output. The only effect is additive context when it can confidently provide it.
|
||||||
|
*/
|
||||||
|
program
|
||||||
|
.command('prompt-hook', { hidden: true })
|
||||||
|
.description('Claude UserPromptSubmit hook: inject CodeGraph context for structural prompts (reads {prompt,cwd} JSON on stdin)')
|
||||||
|
.action(async () => {
|
||||||
|
try {
|
||||||
|
// Kill-switch: lets a user disable the nudge without uninstalling /
|
||||||
|
// editing settings.json (CI, low-power machines, personal preference).
|
||||||
|
if (process.env.CODEGRAPH_NO_PROMPT_HOOK === '1' || process.env.CODEGRAPH_PROMPT_HOOK === '0') return;
|
||||||
|
if (process.stdin.isTTY) return; // invoked by hand, no piped payload
|
||||||
|
|
||||||
|
const raw = await new Promise<string>((resolve) => {
|
||||||
|
let data = '';
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
process.stdin.on('data', (c) => { data += c; });
|
||||||
|
process.stdin.on('end', () => resolve(data));
|
||||||
|
process.stdin.on('error', () => resolve(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
let input: { prompt?: string; cwd?: string } = {};
|
||||||
|
try { input = JSON.parse(raw); } catch { return; }
|
||||||
|
const prompt = String(input.prompt || '');
|
||||||
|
|
||||||
|
// Gate: only structural / flow / impact / where-how prompts get context.
|
||||||
|
// A cheap regex keeps every other prompt ("fix this typo") a zero-cost
|
||||||
|
// no-op so we never add latency where there's no structural answer to give.
|
||||||
|
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(?:es|ed)?|call(?:s|ed|er|ers|ee)?|depend|impact|affect|wired?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
|
||||||
|
if (!prompt || !STRUCTURAL.test(prompt)) return;
|
||||||
|
|
||||||
|
// Find an indexed project: cwd, then walk up a few levels.
|
||||||
|
let root: string | null = null;
|
||||||
|
let dir = path.resolve(String(input.cwd || process.cwd()));
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
if (isInitialized(dir)) { root = dir; break; }
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (parent === dir) break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
if (!root) return; // not indexed — the agent's normal tools apply
|
||||||
|
|
||||||
|
const { default: CodeGraph } = await loadCodeGraph();
|
||||||
|
const cg = await CodeGraph.open(root);
|
||||||
|
try {
|
||||||
|
const { ToolHandler } = await import('../mcp/tools');
|
||||||
|
const handler = new ToolHandler(cg);
|
||||||
|
const result = await handler.execute('codegraph_explore', { query: prompt });
|
||||||
|
const text = result.content[0]?.text ?? '';
|
||||||
|
if (!result.isError && text.trim()) {
|
||||||
|
// Cap the injection so a large-repo explore can't flood the prompt.
|
||||||
|
const MAX = 16000;
|
||||||
|
const body = text.length > MAX ? `${text.slice(0, MAX)}\n…(truncated; call codegraph_explore for the rest)` : text;
|
||||||
|
process.stdout.write(
|
||||||
|
`<codegraph_context note="Structural context from CodeGraph for this prompt — treat returned source as already read; call codegraph_explore for more.">\n${body}\n</codegraph_context>\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cg.destroy();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Degradable by contract: never surface an error to the prompt pipeline.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* codegraph node <name>
|
* codegraph node <name>
|
||||||
*
|
*
|
||||||
|
|||||||
+39
-82
@@ -22,14 +22,13 @@ import {
|
|||||||
resolveTargetFlag,
|
resolveTargetFlag,
|
||||||
} from './targets/registry';
|
} from './targets/registry';
|
||||||
import type { AgentTarget, Location, TargetId } from './targets/types';
|
import type { AgentTarget, Location, TargetId } from './targets/types';
|
||||||
import { getGlyphs } from '../ui/glyphs';
|
|
||||||
// Import the lightweight submodules directly (not the ../sync barrel, which
|
// Import the lightweight submodules directly (not the ../sync barrel, which
|
||||||
// re-exports FileWatcher and would transitively pull in ../extraction — the
|
// re-exports FileWatcher and would transitively pull in ../extraction — the
|
||||||
// installer must stay importable even when native modules can't load).
|
// installer must stay importable even when native modules can't load).
|
||||||
import { watchDisabledReason } from '../sync/watch-policy';
|
import { watchDisabledReason } from '../sync/watch-policy';
|
||||||
import { isGitRepo, isSyncHookInstalled, installGitSyncHook } from '../sync/git-hooks';
|
import { isGitRepo, isSyncHookInstalled, installGitSyncHook } from '../sync/git-hooks';
|
||||||
import { getCodeGraphDir, codeGraphDirName, unsafeIndexRootReason } from '../directory';
|
import { getCodeGraphDir, codeGraphDirName } from '../directory';
|
||||||
import { getTelemetry, recordIndexEvent, TELEMETRY_DOCS } from '../telemetry';
|
import { getTelemetry, TELEMETRY_DOCS } from '../telemetry';
|
||||||
|
|
||||||
// Backwards-compat: keep these named exports — downstream code may
|
// Backwards-compat: keep these named exports — downstream code may
|
||||||
// import them. The shim in `config-writer.ts` continues to re-export
|
// import them. The shim in `config-writer.ts` continues to re-export
|
||||||
@@ -48,9 +47,6 @@ export type { InstallLocation } from './config-writer';
|
|||||||
const importESM = new Function('specifier', 'return import(specifier)') as
|
const importESM = new Function('specifier', 'return import(specifier)') as
|
||||||
(specifier: string) => Promise<typeof import('@clack/prompts')>;
|
(specifier: string) => Promise<typeof import('@clack/prompts')>;
|
||||||
|
|
||||||
function formatNumber(n: number): string {
|
|
||||||
return n.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersion(): string {
|
function getVersion(): string {
|
||||||
try {
|
try {
|
||||||
@@ -205,6 +201,31 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 4¾: front-load prompt hook (Claude Code only). A UserPromptSubmit hook
|
||||||
|
// that runs `codegraph prompt-hook` — it injects codegraph_explore context on
|
||||||
|
// structural ("how / where / trace / impact") prompts so the agent reliably
|
||||||
|
// reaches for the graph instead of grepping. Opt-in, default-yes. Only Claude
|
||||||
|
// Code has UserPromptSubmit, so it's offered only when Claude is a target;
|
||||||
|
// other targets ignore the option. `undefined` (no Claude / not asked) leaves
|
||||||
|
// any existing hook untouched.
|
||||||
|
let promptHook: boolean | undefined;
|
||||||
|
if (targets.some((t) => t.id === 'claude')) {
|
||||||
|
if (useDefaults) {
|
||||||
|
promptHook = true; // --yes → on
|
||||||
|
} else {
|
||||||
|
const ans = await clack.confirm({
|
||||||
|
message:
|
||||||
|
'Front-load CodeGraph on “how / where / trace” prompts? Auto-injects structural context so answers need fewer steps (adds a moment to those prompts; Claude Code only).',
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (clack.isCancel(ans)) {
|
||||||
|
clack.cancel('Installation cancelled.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
promptHook = ans; // false → opt out; install() strips any prior hook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: per-target install loop.
|
// Step 5: per-target install loop.
|
||||||
const installedIds: TargetId[] = [];
|
const installedIds: TargetId[] = [];
|
||||||
let sawCreated = false;
|
let sawCreated = false;
|
||||||
@@ -216,7 +237,7 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const result = target.install(location, { autoAllow });
|
const result = target.install(location, { autoAllow, promptHook });
|
||||||
installedIds.push(target.id);
|
installedIds.push(target.id);
|
||||||
for (const file of result.files) {
|
for (const file of result.files) {
|
||||||
if (file.action === 'created') sawCreated = true;
|
if (file.action === 'created') sawCreated = true;
|
||||||
@@ -243,14 +264,17 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: for local install, initialize the project.
|
// Step 6: install wires up agents only — it deliberately does NOT index.
|
||||||
if (location === 'local') {
|
// Building the per-project graph is the user's explicit `codegraph init`
|
||||||
await initializeLocalProject(clack, useDefaults);
|
// (or `index`), so they choose what gets indexed and when, and we never
|
||||||
}
|
// index a surprise directory (e.g. a shell sitting in $HOME). Same next step
|
||||||
|
// regardless of global/local scope.
|
||||||
if (location === 'global') {
|
clack.note(
|
||||||
clack.note('cd your-project\ncodegraph init -i', 'Quick start');
|
location === 'local'
|
||||||
}
|
? 'codegraph init # build this project’s graph (one time; auto-syncs after)'
|
||||||
|
: 'cd <your-project>\ncodegraph init # build a project’s graph (one time; auto-syncs after)',
|
||||||
|
'Next: index a project',
|
||||||
|
);
|
||||||
|
|
||||||
// Deliver buffered telemetry while we're already in a long interactive
|
// Deliver buffered telemetry while we're already in a long interactive
|
||||||
// command — bounded (~1.5s worst case), invisible after a multi-second install.
|
// command — bounded (~1.5s worst case), invisible after a multi-second install.
|
||||||
@@ -490,73 +514,6 @@ async function resolveTargets(
|
|||||||
.filter((t): t is AgentTarget => t !== undefined);
|
.filter((t): t is AgentTarget => t !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize CodeGraph in the current project (for local installs), then
|
|
||||||
* offer the watch fallback when the live watcher won't run here (see
|
|
||||||
* offerWatchFallback). Agent-agnostic by nature.
|
|
||||||
*/
|
|
||||||
async function initializeLocalProject(
|
|
||||||
clack: typeof import('@clack/prompts'),
|
|
||||||
useDefaults = false,
|
|
||||||
): Promise<void> {
|
|
||||||
const projectPath = process.cwd();
|
|
||||||
|
|
||||||
// Never auto-index the home directory or a filesystem root. Running the
|
|
||||||
// installer from `$HOME` would otherwise index the entire home tree — a
|
|
||||||
// multi-GB index, constant watcher churn, and (pre-1.0 on macOS) fd
|
|
||||||
// exhaustion that crashed the machine (#845). The install itself still
|
|
||||||
// completes; we just skip the auto-index and point them at a real project.
|
|
||||||
const unsafe = unsafeIndexRootReason(projectPath);
|
|
||||||
if (unsafe) {
|
|
||||||
clack.log.warn(`Skipping automatic indexing — ${projectPath} looks like ${unsafe}.`);
|
|
||||||
clack.log.info('Indexing it would pull in caches, other projects, and your whole tree. Run "codegraph init" inside a specific project instead.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let CodeGraph: typeof import('../index').default;
|
|
||||||
try {
|
|
||||||
CodeGraph = (await import('../index')).default;
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
clack.log.error(`Could not load native modules: ${msg}`);
|
|
||||||
clack.log.info('Skipping project initialization. Run "codegraph init -i" later.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already initialized
|
|
||||||
if (CodeGraph.isInitialized(projectPath)) {
|
|
||||||
clack.log.info('CodeGraph already initialized in this project');
|
|
||||||
await offerWatchFallback(clack, projectPath, { yes: useDefaults });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
const cg = await CodeGraph.init(projectPath);
|
|
||||||
clack.log.success('Created .codegraph/ directory');
|
|
||||||
|
|
||||||
// Index the project with shimmer progress (worker thread for smooth animation)
|
|
||||||
const { createShimmerProgress } = await import('../ui/shimmer-progress');
|
|
||||||
process.stdout.write(`\x1b[2m${getGlyphs().rail}\x1b[0m\n`);
|
|
||||||
const progress = createShimmerProgress();
|
|
||||||
|
|
||||||
const result = await cg.indexAll({
|
|
||||||
onProgress: progress.onProgress,
|
|
||||||
});
|
|
||||||
|
|
||||||
await progress.stop();
|
|
||||||
|
|
||||||
if (result.filesErrored > 0) {
|
|
||||||
clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} failed, ${formatNumber(result.nodesCreated)} symbols)`);
|
|
||||||
} else {
|
|
||||||
clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.nodesCreated)} symbols)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
recordIndexEvent(cg, result); // buffered; the installer flushes at the end
|
|
||||||
|
|
||||||
cg.close();
|
|
||||||
|
|
||||||
await offerWatchFallback(clack, projectPath, { yes: useDefaults });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the live file watcher will be disabled for this project (e.g. WSL2
|
* When the live file watcher will be disabled for this project (e.g. WSL2
|
||||||
|
|||||||
@@ -121,6 +121,18 @@ class ClaudeCodeTarget implements AgentTarget {
|
|||||||
const hookCleanup = cleanupLegacyHooks(loc);
|
const hookCleanup = cleanupLegacyHooks(loc);
|
||||||
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
||||||
|
|
||||||
|
// 2c. Front-load prompt hook (Claude UserPromptSubmit). Opt-in via the
|
||||||
|
// installer prompt (default-yes): `promptHook === true` writes it;
|
||||||
|
// `=== false` strips any a prior install wrote so opting out round-trips
|
||||||
|
// (and an upgrade re-run honors the new choice); `undefined` leaves it
|
||||||
|
// untouched for callers that don't manage it.
|
||||||
|
if (opts.promptHook === true) {
|
||||||
|
files.push(writePromptHookEntry(loc));
|
||||||
|
} else if (opts.promptHook === false) {
|
||||||
|
const removed = removePromptHookEntry(loc);
|
||||||
|
if (removed.action === 'removed') files.push(removed);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. CLAUDE.md instructions — the short marker-fenced CodeGraph
|
// 3. CLAUDE.md instructions — the short marker-fenced CodeGraph
|
||||||
// block (#704). The MCP initialize instructions reach only the main
|
// block (#704). The MCP initialize instructions reach only the main
|
||||||
// agent; CLAUDE.md is what Task-tool subagents (and non-MCP
|
// agent; CLAUDE.md is what Task-tool subagents (and non-MCP
|
||||||
@@ -187,6 +199,10 @@ class ClaudeCodeTarget implements AgentTarget {
|
|||||||
const hookCleanup = cleanupLegacyHooks(loc);
|
const hookCleanup = cleanupLegacyHooks(loc);
|
||||||
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
if (hookCleanup.action === 'removed') files.push(hookCleanup);
|
||||||
|
|
||||||
|
// 2c. Remove the front-load prompt hook this installer may have written.
|
||||||
|
const promptHookCleanup = removePromptHookEntry(loc);
|
||||||
|
if (promptHookCleanup.action === 'removed') files.push(promptHookCleanup);
|
||||||
|
|
||||||
// 3. Instructions — strip the legacy CodeGraph block if present.
|
// 3. Instructions — strip the legacy CodeGraph block if present.
|
||||||
files.push(removeInstructionsEntry(loc));
|
files.push(removeInstructionsEntry(loc));
|
||||||
|
|
||||||
@@ -278,6 +294,16 @@ function isLegacyCodegraphHookCommand(command: unknown): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The front-load prompt-hook command the installer writes into Claude's
|
||||||
|
* `UserPromptSubmit` (see writePromptHookEntry). Matched by substring so an
|
||||||
|
* `npx @colbymchenry/codegraph prompt-hook` form is recognized too.
|
||||||
|
*/
|
||||||
|
const PROMPT_HOOK_COMMAND = 'codegraph prompt-hook';
|
||||||
|
function isPromptHookCommand(command: unknown): boolean {
|
||||||
|
return typeof command === 'string' && command.includes(PROMPT_HOOK_COMMAND);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove stale codegraph auto-sync hooks from Claude `settings.json`.
|
* Remove stale codegraph auto-sync hooks from Claude `settings.json`.
|
||||||
*
|
*
|
||||||
@@ -293,7 +319,10 @@ function isLegacyCodegraphHookCommand(command: unknown): boolean {
|
|||||||
* Exported so it can be unit-tested directly and reused by both
|
* Exported so it can be unit-tested directly and reused by both
|
||||||
* `install` (an upgrade self-heals) and `uninstall`.
|
* `install` (an upgrade self-heals) and `uninstall`.
|
||||||
*/
|
*/
|
||||||
export function cleanupLegacyHooks(loc: Location): WriteResult['files'][number] {
|
function removeHookCommandsMatching(
|
||||||
|
loc: Location,
|
||||||
|
match: (command: unknown) => boolean,
|
||||||
|
): WriteResult['files'][number] {
|
||||||
const file = settingsJsonPath(loc);
|
const file = settingsJsonPath(loc);
|
||||||
if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
|
if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
|
||||||
|
|
||||||
@@ -303,7 +332,7 @@ export function cleanupLegacyHooks(loc: Location): WriteResult['files'][number]
|
|||||||
return { path: file, action: 'unchanged' };
|
return { path: file, action: 'unchanged' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: drop the legacy command(s) from inside every matcher group.
|
// Pass 1: drop matching command(s) from inside every matcher group.
|
||||||
let removedAny = false;
|
let removedAny = false;
|
||||||
for (const event of Object.keys(hooks)) {
|
for (const event of Object.keys(hooks)) {
|
||||||
const groups = hooks[event];
|
const groups = hooks[event];
|
||||||
@@ -311,18 +340,17 @@ export function cleanupLegacyHooks(loc: Location): WriteResult['files'][number]
|
|||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (!group || !Array.isArray(group.hooks)) continue;
|
if (!group || !Array.isArray(group.hooks)) continue;
|
||||||
const before = group.hooks.length;
|
const before = group.hooks.length;
|
||||||
group.hooks = group.hooks.filter(
|
group.hooks = group.hooks.filter((h: any) => !match(h?.command));
|
||||||
(h: any) => !isLegacyCodegraphHookCommand(h?.command),
|
|
||||||
);
|
|
||||||
if (group.hooks.length !== before) removedAny = true;
|
if (group.hooks.length !== before) removedAny = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!removedAny) return { path: file, action: 'unchanged' };
|
if (!removedAny) return { path: file, action: 'unchanged' };
|
||||||
|
|
||||||
// Pass 2: prune empty matcher groups, then events with no groups
|
// Pass 2: prune empty matcher groups, then events with no groups left,
|
||||||
// left, then an empty top-level `hooks`. Guarded by `removedAny` so
|
// then an empty top-level `hooks`. Guarded by `removedAny` so we never
|
||||||
// we never restructure a settings.json that had no codegraph hooks.
|
// restructure a settings.json that had no matching hooks. Sibling hooks
|
||||||
|
// (a different command in the group, or a different event) survive.
|
||||||
for (const event of Object.keys(hooks)) {
|
for (const event of Object.keys(hooks)) {
|
||||||
const groups = hooks[event];
|
const groups = hooks[event];
|
||||||
if (!Array.isArray(groups)) continue;
|
if (!Array.isArray(groups)) continue;
|
||||||
@@ -337,6 +365,24 @@ export function cleanupLegacyHooks(loc: Location): WriteResult['files'][number]
|
|||||||
return { path: file, action: 'removed' };
|
return { path: file, action: 'removed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove stale codegraph auto-sync hooks (`mark-dirty` / `sync-if-dirty`) that a
|
||||||
|
* pre-0.8 install wrote. Exported for direct unit-testing; reused by both
|
||||||
|
* `install` (an upgrade self-heals) and `uninstall`.
|
||||||
|
*/
|
||||||
|
export function cleanupLegacyHooks(loc: Location): WriteResult['files'][number] {
|
||||||
|
return removeHookCommandsMatching(loc, isLegacyCodegraphHookCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the front-load `UserPromptSubmit` hook this installer writes (see
|
||||||
|
* writePromptHookEntry). Used by `uninstall`, and by `install` when the user
|
||||||
|
* opts out, so the choice round-trips.
|
||||||
|
*/
|
||||||
|
export function removePromptHookEntry(loc: Location): WriteResult['files'][number] {
|
||||||
|
return removeHookCommandsMatching(loc, isPromptHookCommand);
|
||||||
|
}
|
||||||
|
|
||||||
export function writePermissionsEntry(loc: Location): WriteResult['files'][number] {
|
export function writePermissionsEntry(loc: Location): WriteResult['files'][number] {
|
||||||
const file = settingsJsonPath(loc);
|
const file = settingsJsonPath(loc);
|
||||||
const settings = readJsonFile(file);
|
const settings = readJsonFile(file);
|
||||||
@@ -359,6 +405,37 @@ export function writePermissionsEntry(loc: Location): WriteResult['files'][numbe
|
|||||||
return { path: file, action: created ? 'created' : 'updated' };
|
return { path: file, action: created ? 'created' : 'updated' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the front-load `UserPromptSubmit` hook into Claude `settings.json` —
|
||||||
|
* a `command` hook that runs `codegraph prompt-hook`, which injects
|
||||||
|
* codegraph_explore context for structural prompts so the agent reliably uses
|
||||||
|
* the graph. Idempotent: if our command is already wired under UserPromptSubmit
|
||||||
|
* the file is left byte-for-byte untouched and reported `unchanged`. Sibling
|
||||||
|
* hooks (the user's own, or other events) are preserved. Opt-in — the installer
|
||||||
|
* only calls this when the user accepts the prompt (default-yes).
|
||||||
|
*/
|
||||||
|
export function writePromptHookEntry(loc: Location): WriteResult['files'][number] {
|
||||||
|
const file = settingsJsonPath(loc);
|
||||||
|
const created = !fs.existsSync(file);
|
||||||
|
const settings = readJsonFile(file);
|
||||||
|
|
||||||
|
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
|
||||||
|
settings.hooks = {};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(settings.hooks.UserPromptSubmit)) settings.hooks.UserPromptSubmit = [];
|
||||||
|
|
||||||
|
const already = settings.hooks.UserPromptSubmit.some(
|
||||||
|
(g: any) => g && Array.isArray(g.hooks) && g.hooks.some((h: any) => isPromptHookCommand(h?.command)),
|
||||||
|
);
|
||||||
|
if (already) return { path: file, action: 'unchanged' };
|
||||||
|
|
||||||
|
settings.hooks.UserPromptSubmit.push({
|
||||||
|
hooks: [{ type: 'command', command: PROMPT_HOOK_COMMAND }],
|
||||||
|
});
|
||||||
|
writeJsonFile(file, settings);
|
||||||
|
return { path: file, action: created ? 'created' : 'updated' };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip the marker-delimited CodeGraph block from CLAUDE.md if a prior
|
* Strip the marker-delimited CodeGraph block from CLAUDE.md if a prior
|
||||||
* install wrote one. Codegraph no longer maintains an instructions file
|
* install wrote one. Codegraph no longer maintains an instructions file
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ export interface InstallOptions {
|
|||||||
* target has no permissions concept this option is a no-op.
|
* target has no permissions concept this option is a no-op.
|
||||||
*/
|
*/
|
||||||
autoAllow: boolean;
|
autoAllow: boolean;
|
||||||
|
/**
|
||||||
|
* Front-load prompt hook (Claude `UserPromptSubmit`) that injects
|
||||||
|
* codegraph_explore context for structural prompts. `true` installs it,
|
||||||
|
* `false` removes any prior install (so opt-out round-trips), `undefined`
|
||||||
|
* leaves it untouched. Targets without a prompt-hook concept ignore it.
|
||||||
|
*/
|
||||||
|
promptHook?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentTarget {
|
export interface AgentTarget {
|
||||||
|
|||||||
+41
-4
@@ -340,16 +340,21 @@ export async function runUpgrade(opts: UpgradeOptions, deps: UpgradeDeps): Promi
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch by install method.
|
// Dispatch by install method. bundle/npm perform a real binary update, so
|
||||||
|
// after they succeed we self-heal the front-load hook (below); npx/source/
|
||||||
|
// unknown don't update anything here, so they return directly.
|
||||||
|
let code: number;
|
||||||
switch (method.kind) {
|
switch (method.kind) {
|
||||||
case 'bundle':
|
case 'bundle':
|
||||||
return method.os === 'windows'
|
code = await (method.os === 'windows'
|
||||||
? upgradeWindowsBundle(method, latest, deps)
|
? upgradeWindowsBundle(method, latest, deps)
|
||||||
: upgradeUnixBundle(method, opts.version ? latest : undefined, deps);
|
: upgradeUnixBundle(method, opts.version ? latest : undefined, deps));
|
||||||
|
break;
|
||||||
case 'npm':
|
case 'npm':
|
||||||
// npm version specs have no leading "v" (`@0.9.8`, not `@v0.9.8` — the
|
// npm version specs have no leading "v" (`@0.9.8`, not `@v0.9.8` — the
|
||||||
// latter resolves as a nonexistent dist-tag).
|
// latter resolves as a nonexistent dist-tag).
|
||||||
return upgradeNpm(method, opts.version ? stripV(latest) : 'latest', deps);
|
code = await upgradeNpm(method, opts.version ? stripV(latest) : 'latest', deps);
|
||||||
|
break;
|
||||||
case 'npx':
|
case 'npx':
|
||||||
deps.log(c.green('npx always runs the latest version on demand — nothing to upgrade.'));
|
deps.log(c.green('npx always runs the latest version on demand — nothing to upgrade.'));
|
||||||
deps.log(c.dim(`Force a fresh fetch with: npx ${NPM_PACKAGE}@latest`));
|
deps.log(c.dim(`Force a fresh fetch with: npx ${NPM_PACKAGE}@latest`));
|
||||||
@@ -363,6 +368,38 @@ export async function runUpgrade(opts: UpgradeOptions, deps: UpgradeDeps): Promi
|
|||||||
deps.log(c.dim(`Reinstall manually — see https://github.com/${REPO}#install`));
|
deps.log(c.dim(`Reinstall manually — see https://github.com/${REPO}#install`));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After a successful update, ensure the front-load prompt hook is wired for an
|
||||||
|
// already-configured global Claude install — so existing users pick it up on
|
||||||
|
// upgrade, not only on a fresh `install` (the hook config is version-agnostic,
|
||||||
|
// so the still-running old binary can write it safely). Idempotent + gated on
|
||||||
|
// an existing Claude config, and skipped entirely by the kill-switch. Never
|
||||||
|
// fatal to the upgrade.
|
||||||
|
if (code === 0) {
|
||||||
|
try {
|
||||||
|
await selfHealPromptHook(deps);
|
||||||
|
} catch {
|
||||||
|
/* a hook-wiring hiccup must not fail the upgrade */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire the Claude `UserPromptSubmit` front-load hook on upgrade for an
|
||||||
|
* already-configured global Claude install. No-op when Claude isn't configured,
|
||||||
|
* when the hook is already present, or when the kill-switch is set.
|
||||||
|
*/
|
||||||
|
async function selfHealPromptHook(deps: UpgradeDeps): Promise<void> {
|
||||||
|
if (process.env.CODEGRAPH_NO_PROMPT_HOOK === '1' || process.env.CODEGRAPH_PROMPT_HOOK === '0') return;
|
||||||
|
const { claudeTarget, writePromptHookEntry } = await import('../installer/targets/claude');
|
||||||
|
if (!claudeTarget.detect('global').alreadyConfigured) return;
|
||||||
|
const res = writePromptHookEntry('global');
|
||||||
|
if (res.action === 'created' || res.action === 'updated') {
|
||||||
|
deps.log(
|
||||||
|
c.dim('Enabled the CodeGraph front-load hook for Claude Code (structural prompts). Disable any time: CODEGRAPH_NO_PROMPT_HOOK=1'),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeUnixBundle(
|
function upgradeUnixBundle(
|
||||||
|
|||||||
Reference in New Issue
Block a user