gitnexus 1.6.3-rc.45 → 1.6.3-rc.46

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.
Files changed (2) hide show
  1. package/dist/cli/setup.js +137 -74
  2. package/package.json +1 -1
package/dist/cli/setup.js CHANGED
@@ -12,7 +12,7 @@ import { execFile, execFileSync } from 'child_process';
12
12
  import { promisify } from 'util';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { glob } from 'glob';
15
- import { parseTree, modify, applyEdits } from 'jsonc-parser';
15
+ import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
16
16
  import { getGlobalDir } from '../storage/repo-manager.js';
17
17
  const __filename = fileURLToPath(import.meta.url);
18
18
  const __dirname = path.dirname(__filename);
@@ -78,39 +78,6 @@ function getOpenCodeMcpEntry() {
78
78
  }
79
79
  return { type: 'local', command: ['npx', '-y', 'gitnexus@latest', 'mcp'] };
80
80
  }
81
- /**
82
- * Merge gitnexus entry into an existing MCP config JSON object.
83
- * Returns the updated config.
84
- */
85
- function mergeMcpConfig(existing) {
86
- if (!existing || typeof existing !== 'object') {
87
- existing = {};
88
- }
89
- if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
90
- existing.mcpServers = {};
91
- }
92
- existing.mcpServers.gitnexus = getMcpEntry();
93
- return existing;
94
- }
95
- /**
96
- * Try to read a JSON file, returning null if it doesn't exist or is invalid.
97
- */
98
- async function readJsonFile(filePath) {
99
- try {
100
- const raw = await fs.readFile(filePath, 'utf-8');
101
- return JSON.parse(raw);
102
- }
103
- catch {
104
- return null;
105
- }
106
- }
107
- /**
108
- * Write JSON to a file, creating parent directories if needed.
109
- */
110
- async function writeJsonFile(filePath, data) {
111
- await fs.mkdir(path.dirname(filePath), { recursive: true });
112
- await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
113
- }
114
81
  /**
115
82
  * Detect indentation style from file content.
116
83
  * Returns formatting options matching the file's existing style.
@@ -136,18 +103,11 @@ async function mergeJsoncFile(filePath, keyPath, value) {
136
103
  raw = '';
137
104
  }
138
105
  if (raw.trim().length === 0) {
139
- const config = {};
140
- let parent = config;
141
- for (let i = 0; i < keyPath.length; i++) {
142
- if (i === keyPath.length - 1) {
143
- parent[keyPath[i]] = value;
144
- }
145
- else {
146
- parent[keyPath[i]] = {};
147
- parent = parent[keyPath[i]];
148
- }
149
- }
150
- await writeJsonFile(filePath, config);
106
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
107
+ const formattingOptions = { tabSize: 2, insertSpaces: true };
108
+ const edits = modify('{}', keyPath, value, { formattingOptions });
109
+ const result = applyEdits('{}', edits);
110
+ await fs.writeFile(filePath, result, 'utf-8');
151
111
  return true;
152
112
  }
153
113
  const parseErrors = [];
@@ -182,10 +142,13 @@ async function setupCursor(result) {
182
142
  }
183
143
  const mcpPath = path.join(cursorDir, 'mcp.json');
184
144
  try {
185
- const existing = await readJsonFile(mcpPath);
186
- const updated = mergeMcpConfig(existing);
187
- await writeJsonFile(mcpPath, updated);
188
- result.configured.push('Cursor');
145
+ const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
146
+ if (ok) {
147
+ result.configured.push('Cursor');
148
+ }
149
+ else {
150
+ result.errors.push('Cursor: mcp.json is corrupt — skipping to preserve existing content');
151
+ }
189
152
  }
190
153
  catch (err) {
191
154
  result.errors.push(`Cursor: ${err.message}`);
@@ -200,10 +163,13 @@ async function setupClaudeCode(result) {
200
163
  // Claude Code stores MCP config in ~/.claude.json
201
164
  const mcpPath = path.join(os.homedir(), '.claude.json');
202
165
  try {
203
- const existing = await readJsonFile(mcpPath);
204
- const updated = mergeMcpConfig(existing);
205
- await writeJsonFile(mcpPath, updated);
206
- result.configured.push('Claude Code');
166
+ const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
167
+ if (ok) {
168
+ result.configured.push('Claude Code');
169
+ }
170
+ else {
171
+ result.errors.push('Claude Code: .claude.json is corrupt — skipping to preserve existing content');
172
+ }
207
173
  }
208
174
  catch (err) {
209
175
  result.errors.push(`Claude Code: ${err.message}`);
@@ -227,9 +193,71 @@ async function installClaudeCodeSkills(result) {
227
193
  result.errors.push(`Claude Code skills: ${err.message}`);
228
194
  }
229
195
  }
196
+ /**
197
+ * Check whether an event array already contains a gitnexus-hook entry.
198
+ */
199
+ function hasGitnexusHook(hooksObj, eventName) {
200
+ const entries = hooksObj?.[eventName];
201
+ if (!Array.isArray(entries))
202
+ return false;
203
+ return entries.some((h) => Array.isArray(h.hooks) &&
204
+ h.hooks.some((hh) => typeof hh.command === 'string' && hh.command.includes('gitnexus-hook')));
205
+ }
206
+ /**
207
+ * Merge hook entries into a JSONC settings file, preserving comments and formatting.
208
+ * Uses chained modify()+applyEdits() calls to append to arrays without a full
209
+ * JSON.stringify roundtrip that would strip comments.
210
+ */
211
+ async function mergeHooksJsonc(filePath, entries) {
212
+ let raw;
213
+ try {
214
+ raw = await fs.readFile(filePath, 'utf-8');
215
+ }
216
+ catch {
217
+ raw = '';
218
+ }
219
+ if (raw.trim().length === 0) {
220
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
221
+ const hooks = {};
222
+ for (const { eventName, value } of entries) {
223
+ hooks[eventName] = [value];
224
+ }
225
+ const formattingOptions = { tabSize: 2, insertSpaces: true };
226
+ const edits = modify('{}', ['hooks'], hooks, { formattingOptions });
227
+ await fs.writeFile(filePath, applyEdits('{}', edits), 'utf-8');
228
+ return true;
229
+ }
230
+ const parseErrors = [];
231
+ const tree = parseTree(raw, parseErrors);
232
+ if (!tree || tree.type !== 'object' || parseErrors.length > 0) {
233
+ return false;
234
+ }
235
+ const formattingOptions = detectIndentation(raw);
236
+ let current = raw;
237
+ for (const { eventName, value } of entries) {
238
+ // Re-parse after each edit to get a fresh insertion index.
239
+ const currentTree = parseTree(current, []);
240
+ const hooksNode = currentTree?.children?.find((c) => c.type === 'property' && c.children?.[0]?.value === 'hooks');
241
+ const eventNode = hooksNode?.children?.[1]?.children?.find((c) => c.type === 'property' && c.children?.[0]?.value === eventName);
242
+ let insertIndex;
243
+ if (eventNode?.children?.[1] && Array.isArray(eventNode.children[1].children)) {
244
+ insertIndex = eventNode.children[1].children.length;
245
+ }
246
+ else {
247
+ insertIndex = 0;
248
+ }
249
+ const edits = modify(current, ['hooks', eventName, insertIndex], value, {
250
+ formattingOptions,
251
+ });
252
+ current = applyEdits(current, edits);
253
+ }
254
+ await fs.writeFile(filePath, current, 'utf-8');
255
+ return true;
256
+ }
230
257
  /**
231
258
  * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
232
- * Merges hook config without overwriting existing hooks.
259
+ * Merges hook config without overwriting existing hooks, preserving
260
+ * comments and formatting in the JSONC file.
233
261
  */
234
262
  async function installClaudeCodeHooks(result) {
235
263
  const claudeDir = path.join(os.homedir(), '.claude');
@@ -246,8 +274,6 @@ async function installClaudeCodeHooks(result) {
246
274
  const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
247
275
  try {
248
276
  let content = await fs.readFile(src, 'utf-8');
249
- // Inject resolved CLI path so the copied hook can find the CLI
250
- // even when it's no longer inside the npm package tree
251
277
  const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
252
278
  const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
253
279
  const jsonCli = JSON.stringify(normalizedCli);
@@ -259,25 +285,62 @@ async function installClaudeCodeHooks(result) {
259
285
  }
260
286
  const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
261
287
  const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
262
- // Merge hook config into ~/.claude/settings.json
263
- const existing = (await readJsonFile(settingsPath)) || {};
264
- if (!existing.hooks)
265
- existing.hooks = {};
266
- function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
267
- if (!existing.hooks[eventName])
268
- existing.hooks[eventName] = [];
269
- const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus-hook')));
270
- if (!hasHook) {
271
- existing.hooks[eventName].push({
272
- matcher,
273
- hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
274
- });
288
+ // Check which hook events need entries (idempotent: skip if already registered)
289
+ const parsed = await (async () => {
290
+ try {
291
+ const r = await fs.readFile(settingsPath, 'utf-8');
292
+ return parseJsonc(r);
275
293
  }
294
+ catch {
295
+ return null;
296
+ }
297
+ })();
298
+ const hookEntries = [];
299
+ // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
300
+ // Session context is delivered via CLAUDE.md / skills instead.
301
+ if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse')) {
302
+ hookEntries.push({
303
+ eventName: 'PreToolUse',
304
+ value: {
305
+ matcher: 'Grep|Glob|Bash',
306
+ hooks: [
307
+ {
308
+ type: 'command',
309
+ command: hookCmd,
310
+ timeout: 10,
311
+ statusMessage: 'Enriching with GitNexus graph context...',
312
+ },
313
+ ],
314
+ },
315
+ });
316
+ }
317
+ if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse')) {
318
+ hookEntries.push({
319
+ eventName: 'PostToolUse',
320
+ value: {
321
+ matcher: 'Bash',
322
+ hooks: [
323
+ {
324
+ type: 'command',
325
+ command: hookCmd,
326
+ timeout: 10,
327
+ statusMessage: 'Checking GitNexus index freshness...',
328
+ },
329
+ ],
330
+ },
331
+ });
332
+ }
333
+ if (hookEntries.length === 0) {
334
+ result.configured.push('Claude Code hooks (already configured)');
335
+ return;
336
+ }
337
+ const ok = await mergeHooksJsonc(settingsPath, hookEntries);
338
+ if (ok) {
339
+ result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
340
+ }
341
+ else {
342
+ result.errors.push('Claude Code hooks: settings.json is corrupt — skipping to preserve existing content');
276
343
  }
277
- ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
278
- ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
279
- await writeJsonFile(settingsPath, existing);
280
- result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
281
344
  }
282
345
  catch (err) {
283
346
  result.errors.push(`Claude Code hooks: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.3-rc.45",
3
+ "version": "1.6.3-rc.46",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",