feat(git): /git overview page and diff viewer

Add git module with repo status overview (/git) and per-repo detail page
(/git/[repo]) featuring a color-coded diff viewer (+ green, - red).
Reads repo paths from REPO_PATHS env var, calls ops-agent git_status and
git_diff commands. Status badges: clean (green), dirty (orange),
behind-origin (blue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-13 17:35:11 +02:00
parent 4821d29670
commit 9e08a7c31f
5 changed files with 513 additions and 0 deletions

46
lib/parse-git.ts Normal file
View file

@ -0,0 +1,46 @@
export interface RepoStatus {
branch: string
/** Number of commits ahead of upstream (undefined if no upstream) */
ahead?: number
/** Number of commits behind upstream (undefined if no upstream) */
behind?: number
/** True when there are uncommitted changes */
dirty: boolean
}
/**
* Parses `git status --short --branch` output into a RepoStatus.
*
* First line format: ## main...origin/main [ahead N, behind M]
* Remaining lines: XY path (presence means dirty)
*/
export function parseGitStatus(output: string): RepoStatus {
const lines = output.trim().split('\n').filter(Boolean)
let branch = 'unknown'
let ahead: number | undefined
let behind: number | undefined
let dirty = false
for (const line of lines) {
if (line.startsWith('## ')) {
const rest = line.slice(3)
// "No commits yet on main" or "HEAD (no branch)"
const trackMatch = rest.match(/^([^.]+)\.\.\.(\S+)/)
if (trackMatch) {
branch = trackMatch[1]
} else {
branch = rest.split(' ')[0]
}
const aheadMatch = rest.match(/ahead (\d+)/)
const behindMatch = rest.match(/behind (\d+)/)
if (aheadMatch) ahead = parseInt(aheadMatch[1], 10)
if (behindMatch) behind = parseInt(behindMatch[1], 10)
} else {
// Any non-header line means there are changes
dirty = true
}
}
return { branch, ahead, behind, dirty }
}