diff --git a/.gitignore b/.gitignore index bbebd405d..2c370d2d0 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,4 @@ local_docs/ .tmp/ .agents/ .context/ +gitnexus/web/ diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 3d7faae78..2c100228d 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -218,21 +218,19 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption let repoPath: string; if (inputPath) { repoPath = path.resolve(inputPath); + } else if (options?.skipGit) { + // --skip-git: treat cwd as the index root, do not walk up to a parent git repo. + repoPath = path.resolve(process.cwd()); } else { const gitRoot = getGitRoot(process.cwd()); if (!gitRoot) { - if (!options?.skipGit) { - console.log( - ' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n', - ); - process.exitCode = 1; - return; - } - // --skip-git: fall back to cwd as the root - repoPath = path.resolve(process.cwd()); - } else { - repoPath = gitRoot; + console.log( + ' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n', + ); + process.exitCode = 1; + return; } + repoPath = gitRoot; } const repoHasGit = hasGitDir(repoPath); diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index dcee1339a..ca536dc80 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -32,7 +32,10 @@ program .option('--skills', 'Generate repo-specific skill files from detected communities') .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md') .option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md') - .option('--skip-git', 'Index a folder without requiring a .git directory') + .option( + '--skip-git', + 'Treat the provided path/cwd as the index root and skip parent git-root discovery', + ) .option( '--name ', 'Register this repo under a custom name in ~/.gitnexus/registry.json ' + diff --git a/gitnexus/test/unit/skip-git-cli.test.ts b/gitnexus/test/unit/skip-git-cli.test.ts index 740c8265a..c8c433f35 100644 --- a/gitnexus/test/unit/skip-git-cli.test.ts +++ b/gitnexus/test/unit/skip-git-cli.test.ts @@ -36,4 +36,140 @@ describe('--skip-git CLI flag', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); + + describe('--skip-git does not walk up to parent git repo (#1232)', () => { + const cliPath = path.resolve(__dirname, '../../dist/cli/index.js'); + let parentDir: string; + + function testEnv() { + return { + ...process.env, + HOME: parentDir, + GITNEXUS_HOME: path.join(parentDir, '.gitnexus-home'), + GITNEXUS_LBUG_EXTENSION_INSTALL: 'never', + }; + } + + function readRegistry(): Array<{ name: string; path: string }> { + const registryPath = path.join(parentDir, '.gitnexus-home', 'registry.json'); + expect(fs.existsSync(registryPath)).toBe(true); + return JSON.parse(fs.readFileSync(registryPath, 'utf8')); + } + + function canonicalPath(filePath: string): string { + return fs.realpathSync(filePath); + } + + function expectCoolioRegistryEntry() { + const registry = readRegistry(); + const entry = registry.find((e) => e.name === 'COOLIO'); + expect(entry).toBeTruthy(); + if (!entry) throw new Error('Expected COOLIO registry entry'); + expect(canonicalPath(entry.path)).toBe(canonicalPath(path.join(parentDir, 'COOLIO'))); + expect( + registry.find((e) => canonicalPath(e.path) === canonicalPath(parentDir)), + ).toBeUndefined(); + expect( + registry.find( + (e) => canonicalPath(e.path) === canonicalPath(path.join(parentDir, 'SubWooder')), + ), + ).toBeUndefined(); + return entry; + } + + function initParentGitRepo() { + execSync('git init', { cwd: parentDir, stdio: 'ignore' }); + execSync( + 'git -c user.name=test -c user.email=test@example.com commit --allow-empty -m init', + { + cwd: parentDir, + stdio: 'ignore', + }, + ); + } + + function createTestStructure() { + // Create structure: + // parentDir/ + // .git/ (parent is a git repo) + // COOLIO/ + // package.json + // src/index.ts + // SubWooder/ + // package.json + // src/index.ts + parentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-skip-git-')); + initParentGitRepo(); + fs.mkdirSync(path.join(parentDir, 'COOLIO', 'src'), { recursive: true }); + fs.writeFileSync( + path.join(parentDir, 'COOLIO', 'package.json'), + JSON.stringify({ name: 'coolio' }), + ); + fs.writeFileSync( + path.join(parentDir, 'COOLIO', 'src', 'index.ts'), + 'export const hello = "world";', + ); + fs.mkdirSync(path.join(parentDir, 'SubWooder', 'src'), { recursive: true }); + fs.writeFileSync( + path.join(parentDir, 'SubWooder', 'package.json'), + JSON.stringify({ name: 'subwooder' }), + ); + fs.writeFileSync( + path.join(parentDir, 'SubWooder', 'src', 'index.ts'), + 'export const bass = 42;', + ); + return parentDir; + } + + function cleanup() { + if (parentDir) { + fs.rmSync(parentDir, { recursive: true, force: true }); + } + } + + it('from subdir inside parent git repo, indexes subdir not parent', () => { + createTestStructure(); + try { + // Run analyze from COOLIO with --skip-git + const output = execSync(`node "${cliPath}" analyze --skip-git --skip-agents-md`, { + cwd: path.join(parentDir, 'COOLIO'), + encoding: 'utf8', + timeout: 60000, + env: testEnv(), + }); + // Should mention COOLIO not the parent dir name + expect(output).toContain('COOLIO'); + + expectCoolioRegistryEntry(); + + const siblingQuery = execSync(`node "${cliPath}" query bass --repo COOLIO`, { + cwd: path.join(parentDir, 'COOLIO'), + encoding: 'utf8', + timeout: 60000, + env: testEnv(), + }); + expect(siblingQuery).not.toContain('SubWooder'); + expect(siblingQuery).not.toContain('bass'); + } finally { + cleanup(); + } + }); + + it('explicit input path with --skip-git indexes subdir', () => { + createTestStructure(); + try { + const output = execSync(`node "${cliPath}" analyze ./COOLIO --skip-git --skip-agents-md`, { + cwd: parentDir, + encoding: 'utf8', + timeout: 60000, + env: testEnv(), + }); + expect(output).toContain('COOLIO'); + + expectCoolioRegistryEntry(); + } finally { + cleanup(); + } + }); + }); });