Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions gitnexus/src/cli/ai-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ async function fileExists(filePath: string): Promise<boolean> {
async function upsertGitNexusSection(
filePath: string,
content: string,
): Promise<'created' | 'updated' | 'appended'> {
projectName: string,
stats: RepoStats,
): Promise<'created' | 'updated' | 'appended' | 'preserved'> {
const exists = await fileExists(filePath);

if (!exists) {
Expand All @@ -223,7 +225,50 @@ async function upsertGitNexusSection(
);

if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
// Replace existing section
const existingSection = existingContent.substring(
startIdx,
endIdx + GITNEXUS_END_MARKER.length,
);

// If the existing section contains <!-- gitnexus:keep -->, preserve the user's
// custom layout and only update the stats line (node/edge/flow counts).
// This lets teams trim the verbose default template to a lean format without
// having it overwritten on every `gitnexus analyze`.
//
// Note: the keep-marker check operates on `existingSection` (the substring
// between valid section markers identified by findSectionMarkerIndex), so
// a keep marker in user prose OUTSIDE the GitNexus block has no effect.
if (existingSection.includes('<!-- gitnexus:keep -->')) {
// Build the new stats line from the caller-provided values directly.
// We do NOT re-extract from `content` because:
// (a) first-bold extraction is fragile if the template evolves
// (b) the parenthesized-text fallback can match unrelated tuples
// like `({target: "symbolName", direction: "upstream"})`
// when noStats is set
// Passing projectName + stats explicitly makes the contract obvious.
// noStats controls template generation, not keep-section stat updates — the user opted into a stats line by keeping it.
const newStatsInner = `${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows`;
const statsLine = `Indexed as **${projectName}** (${newStatsInner})`;

// Match either canonical phrasing at line start (`^` with `m` flag) so we
// cannot replace prose embedded mid-paragraph. Deliberately no `$`: text
// after the closing `)` on the same line (e.g. ". MCP tools.") stays intact.
const statsPattern = /^(?:Indexed as|indexed by GitNexus as) \*\*[^*]+\*\* \([^)]+\)/m;

if (statsPattern.test(existingSection)) {
const updatedSection = existingSection.replace(statsPattern, statsLine);
const before = existingContent.substring(0, startIdx);
const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
await fs.writeFile(filePath, (before + updatedSection + after).trim() + '\n', 'utf-8');
return 'updated';
}
// Keep marker present but no stats line matched. Section is preserved
// unchanged on disk; return a distinct status so callers/CLI output
// don't mis-report this as 'updated' (which would imply a write).
return 'preserved';
}

// No keep marker — replace existing section with full verbose content
const before = existingContent.substring(0, startIdx);
const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length);
const newContent = before + content + after;
Expand Down Expand Up @@ -344,12 +389,12 @@ export async function generateAIContextFiles(
if (!options?.skipAgentsMd) {
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
const agentsPath = path.join(repoPath, 'AGENTS.md');
const agentsResult = await upsertGitNexusSection(agentsPath, content);
const agentsResult = await upsertGitNexusSection(agentsPath, content, projectName, stats);
createdFiles.push(`AGENTS.md (${agentsResult})`);

// Create CLAUDE.md (for Claude Code)
const claudePath = path.join(repoPath, 'CLAUDE.md');
const claudeResult = await upsertGitNexusSection(claudePath, content);
const claudeResult = await upsertGitNexusSection(claudePath, content, projectName, stats);
createdFiles.push(`CLAUDE.md (${claudeResult})`);
} else {
createdFiles.push('AGENTS.md (skipped via --skip-agents-md)');
Expand Down
Loading
Loading