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.
- package/dist/cli/setup.js +137 -74
- 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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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