@duytransipher/gitnexus 1.0.0 → 1.1.0

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/index.js CHANGED
@@ -14,6 +14,10 @@ program
14
14
  program
15
15
  .command('setup')
16
16
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
17
+ .option('--unreal', 'Also install the GitNexus Unreal Engine plugin into a UE project')
18
+ .option('--project <path>', 'UE project root (default: current directory, used with --unreal)')
19
+ .option('--editor-cmd <path>', 'Path to UnrealEditor-Cmd.exe (auto-detected if omitted)')
20
+ .option('--force', 'Overwrite existing Unreal plugin/config if present')
17
21
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
18
22
  program
19
23
  .command('analyze [path]')
@@ -5,4 +5,9 @@
5
5
  * Detects installed AI editors and writes the appropriate MCP config
6
6
  * so the GitNexus MCP server is available in all projects.
7
7
  */
8
- export declare const setupCommand: () => Promise<void>;
8
+ export declare const setupCommand: (options: {
9
+ unreal?: boolean;
10
+ project?: string;
11
+ editorCmd?: string;
12
+ force?: boolean;
13
+ }) => Promise<void>;
package/dist/cli/setup.js CHANGED
@@ -6,8 +6,10 @@
6
6
  * so the GitNexus MCP server is available in all projects.
7
7
  */
8
8
  import fs from 'fs/promises';
9
+ import fsSync from 'fs';
9
10
  import path from 'path';
10
11
  import os from 'os';
12
+ import { execSync } from 'child_process';
11
13
  import { fileURLToPath } from 'url';
12
14
  import { glob } from 'glob';
13
15
  import { getGlobalDir } from '../storage/repo-manager.js';
@@ -310,8 +312,164 @@ async function installOpenCodeSkills(result) {
310
312
  result.errors.push(`OpenCode skills: ${err.message}`);
311
313
  }
312
314
  }
315
+ async function findUProjectFile(projectRoot) {
316
+ const entries = await fs.readdir(projectRoot);
317
+ const uprojects = entries.filter(e => e.endsWith('.uproject'));
318
+ if (uprojects.length === 0) {
319
+ throw new Error(`No .uproject file found in ${projectRoot}. Use --project to specify the UE project root.`);
320
+ }
321
+ if (uprojects.length > 1) {
322
+ throw new Error(`Multiple .uproject files in ${projectRoot}: ${uprojects.join(', ')}. Move into the correct project directory or use --project.`);
323
+ }
324
+ return path.join(projectRoot, uprojects[0]);
325
+ }
326
+ function resolveEditorCmd(explicitPath, engineAssociation) {
327
+ if (explicitPath) {
328
+ const resolved = path.resolve(explicitPath);
329
+ try {
330
+ fsSync.accessSync(resolved);
331
+ }
332
+ catch {
333
+ throw new Error(`Editor command not found: ${resolved}`);
334
+ }
335
+ return resolved;
336
+ }
337
+ if (process.platform !== 'win32') {
338
+ throw new Error('Auto-detection of UnrealEditor-Cmd is only supported on Windows. Use --editor-cmd.');
339
+ }
340
+ const tryCandidates = (rootOrExe) => {
341
+ const candidates = [
342
+ rootOrExe,
343
+ path.join(rootOrExe, 'Engine', 'Binaries', 'Win64', 'UnrealEditor-Cmd.exe'),
344
+ path.join(rootOrExe, 'Engine', 'Windows', 'Engine', 'Binaries', 'Win64', 'UnrealEditor-Cmd.exe'),
345
+ ];
346
+ for (const c of candidates) {
347
+ try {
348
+ const stat = fsSync.statSync(c);
349
+ if (stat.isFile())
350
+ return c;
351
+ }
352
+ catch { /* next */ }
353
+ }
354
+ return null;
355
+ };
356
+ // Try registry lookup for source builds (GUID engine association)
357
+ if (engineAssociation) {
358
+ try {
359
+ const regQuery = `reg query "HKCU\\Software\\Epic Games\\Unreal Engine\\Builds" /v "${engineAssociation}" 2>nul`;
360
+ const output = execSync(regQuery, { encoding: 'utf-8' });
361
+ const match = output.match(/REG_SZ\s+(.+)/);
362
+ if (match) {
363
+ const found = tryCandidates(match[1].trim());
364
+ if (found)
365
+ return found;
366
+ }
367
+ }
368
+ catch { /* registry key not found */ }
369
+ // Try standard install path
370
+ const found = tryCandidates(path.join('C:\\Program Files\\Epic Games', `UE_${engineAssociation}`));
371
+ if (found)
372
+ return found;
373
+ }
374
+ throw new Error(`Could not auto-detect UnrealEditor-Cmd.exe for EngineAssociation '${engineAssociation}'. Use --editor-cmd to specify it.`);
375
+ }
376
+ async function setupUnreal(options, result) {
377
+ const projectRoot = path.resolve(options.project || process.cwd());
378
+ try {
379
+ const stat = await fs.stat(projectRoot);
380
+ if (!stat.isDirectory())
381
+ throw new Error('not a directory');
382
+ }
383
+ catch {
384
+ result.errors.push(`Unreal: project root not found: ${projectRoot}`);
385
+ return;
386
+ }
387
+ let uprojectPath;
388
+ try {
389
+ uprojectPath = await findUProjectFile(projectRoot);
390
+ }
391
+ catch (err) {
392
+ result.errors.push(`Unreal: ${err.message}`);
393
+ return;
394
+ }
395
+ // Read engine association from .uproject
396
+ let engineAssociation = '';
397
+ try {
398
+ const uproject = JSON.parse(await fs.readFile(uprojectPath, 'utf-8'));
399
+ engineAssociation = uproject.EngineAssociation || '';
400
+ }
401
+ catch (err) {
402
+ result.errors.push(`Unreal: failed to read ${path.basename(uprojectPath)}: ${err.message}`);
403
+ return;
404
+ }
405
+ // Resolve editor command
406
+ let editorCmd;
407
+ try {
408
+ editorCmd = resolveEditorCmd(options.editorCmd, engineAssociation);
409
+ }
410
+ catch (err) {
411
+ result.errors.push(`Unreal: ${err.message}`);
412
+ return;
413
+ }
414
+ // Paths
415
+ const pluginDest = path.join(projectRoot, 'Plugins', 'GitNexusUnreal');
416
+ const gitnexusDir = path.join(projectRoot, '.gitnexus');
417
+ const unrealDir = path.join(gitnexusDir, 'unreal');
418
+ const configPath = path.join(unrealDir, 'config.json');
419
+ // Check existing installation
420
+ const pluginExists = await dirExists(pluginDest);
421
+ const configExists = await fileExists(configPath);
422
+ if ((pluginExists || configExists) && !options.force) {
423
+ result.errors.push(`Unreal: plugin or config already exists in ${projectRoot}. Use --force to overwrite.`);
424
+ return;
425
+ }
426
+ // Copy bundled plugin
427
+ const bundledPlugin = path.join(__dirname, '..', '..', 'vendor', 'GitNexusUnreal');
428
+ if (!(await dirExists(bundledPlugin))) {
429
+ result.errors.push('Unreal: bundled plugin not found in vendor/GitNexusUnreal');
430
+ return;
431
+ }
432
+ try {
433
+ if (pluginExists) {
434
+ await fs.rm(pluginDest, { recursive: true, force: true });
435
+ }
436
+ await fs.mkdir(path.join(projectRoot, 'Plugins'), { recursive: true });
437
+ await copyDirRecursive(bundledPlugin, pluginDest);
438
+ }
439
+ catch (err) {
440
+ result.errors.push(`Unreal: failed to copy plugin: ${err.message}`);
441
+ return;
442
+ }
443
+ // Write config
444
+ try {
445
+ await fs.mkdir(unrealDir, { recursive: true });
446
+ const config = {
447
+ editor_cmd: editorCmd.replace(/\\/g, '/'),
448
+ project_path: uprojectPath.replace(/\\/g, '/'),
449
+ commandlet: 'GitNexusBlueprintAnalyzer',
450
+ timeout_ms: 300000,
451
+ };
452
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
453
+ }
454
+ catch (err) {
455
+ result.errors.push(`Unreal: failed to write config: ${err.message}`);
456
+ return;
457
+ }
458
+ result.configured.push(`Unreal plugin → ${pluginDest}`);
459
+ result.configured.push(`Unreal config → ${configPath}`);
460
+ console.log(` Editor: ${editorCmd}`);
461
+ }
462
+ async function fileExists(filePath) {
463
+ try {
464
+ const stat = await fs.stat(filePath);
465
+ return stat.isFile();
466
+ }
467
+ catch {
468
+ return false;
469
+ }
470
+ }
313
471
  // ─── Main command ──────────────────────────────────────────────────
314
- export const setupCommand = async () => {
472
+ export const setupCommand = async (options) => {
315
473
  console.log('');
316
474
  console.log(' GitNexus Setup');
317
475
  console.log(' ==============');
@@ -333,6 +491,12 @@ export const setupCommand = async () => {
333
491
  await installClaudeCodeHooks(result);
334
492
  await installCursorSkills(result);
335
493
  await installOpenCodeSkills(result);
494
+ // Unreal Engine plugin setup (optional)
495
+ if (options.unreal) {
496
+ console.log(' Unreal Engine Setup');
497
+ console.log(' -------------------');
498
+ await setupUnreal({ project: options.project, editorCmd: options.editorCmd, force: options.force }, result);
499
+ }
336
500
  // Print results
337
501
  if (result.configured.length > 0) {
338
502
  console.log(' Configured:');
@@ -363,5 +527,12 @@ export const setupCommand = async () => {
363
527
  console.log(' 1. cd into any git repo');
364
528
  console.log(' 2. Run: gitnexus analyze');
365
529
  console.log(' 3. Open the repo in your editor — MCP is ready!');
530
+ if (options.unreal) {
531
+ console.log('');
532
+ console.log(' Unreal next steps:');
533
+ console.log(' 1. Build your Unreal editor target (so the commandlet compiles)');
534
+ console.log(' 2. Run: gitnexus analyze (index the C++ codebase)');
535
+ console.log(' 3. Run: gitnexus unreal-sync (scan Blueprint assets)');
536
+ }
366
537
  console.log('');
367
538
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duytransipher/gitnexus",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Sipher-maintained fork of GitNexus for graph-powered code intelligence via MCP and CLI.",
5
5
  "author": "DuyTranSipher",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",