#!/usr/bin/env pwsh # Git extension: auto-commit.ps1 # Automatically commit changes after a Spec Kit command completes. # Checks per-command config keys in git-config.yml before committing. # # Usage: auto-commit.ps1 # e.g.: auto-commit.ps1 after_specify param( [Parameter(Position = 0, Mandatory = $true)] [string]$EventName ) $ErrorActionPreference = 'Stop' function Find-ProjectRoot { param([string]$StartDir) $current = Resolve-Path $StartDir while ($true) { foreach ($marker in @('.specify', '.git')) { if (Test-Path (Join-Path $current $marker)) { return $current } } $parent = Split-Path $current -Parent if ($parent -eq $current) { return $null } $current = $parent } } $repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot if (-not $repoRoot) { $repoRoot = Get-Location } Set-Location $repoRoot # Check if git is available if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Warning "[specify] Warning: Git not found; skipped auto-commit" exit 0 } try { git rev-parse --is-inside-work-tree 2>$null | Out-Null if ($LASTEXITCODE -ne 0) { throw "not a repo" } } catch { Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit" exit 0 } # Read per-command config from git-config.yml $configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml" $enabled = $false $commitMsg = "" if (Test-Path $configFile) { # Parse YAML to find auto_commit section $inAutoCommit = $false $inEvent = $false $defaultEnabled = $false foreach ($line in Get-Content $configFile) { # Detect auto_commit: section if ($line -match '^auto_commit:') { $inAutoCommit = $true $inEvent = $false continue } # Exit auto_commit section on next top-level key if ($inAutoCommit -and $line -match '^[a-z]') { break } if ($inAutoCommit) { # Check default key if ($line -match '^\s+default:\s*(.+)$') { $val = $matches[1].Trim().ToLower() if ($val -eq 'true') { $defaultEnabled = $true } } # Detect our event subsection if ($line -match "^\s+${EventName}:") { $inEvent = $true continue } # Inside our event subsection if ($inEvent) { # Exit on next sibling key (2-space indent, not 4+) if ($line -match '^\s{2}[a-z]' -and $line -notmatch '^\s{4}') { $inEvent = $false continue } if ($line -match '\s+enabled:\s*(.+)$') { $val = $matches[1].Trim().ToLower() if ($val -eq 'true') { $enabled = $true } if ($val -eq 'false') { $enabled = $false } } if ($line -match '\s+message:\s*(.+)$') { $commitMsg = $matches[1].Trim() -replace '^["'']' -replace '["'']$' } } } } # If event-specific key not found, use default if (-not $enabled -and $defaultEnabled) { $hasEventKey = Select-String -Path $configFile -Pattern "^\s*${EventName}:" -Quiet if (-not $hasEventKey) { $enabled = $true } } } else { # No config file — auto-commit disabled by default exit 0 } if (-not $enabled) { exit 0 } # Check if there are changes to commit $diffHead = git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE $diffCached = git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE $untracked = git ls-files --others --exclude-standard 2>$null if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) { Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray exit 0 } # Derive a human-readable command name from the event $commandName = $EventName -replace '^after_', '' -replace '^before_', '' $phase = if ($EventName -match '^before_') { 'before' } else { 'after' } # Use custom message if configured, otherwise default if (-not $commitMsg) { $commitMsg = "[Spec Kit] Auto-commit $phase $commandName" } # Stage and commit try { $out = git add . 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" } $out = git commit -q -m $commitMsg 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" } } catch { Write-Warning "[specify] Error: $_" exit 1 } Write-Host "✓ Changes committed $phase $commandName"