218 lines
6.9 KiB
PowerShell
218 lines
6.9 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$RootPath = "C:\Users\stefan\git",
|
|
[string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards",
|
|
[string[]]$Exclude = @('project-standards'),
|
|
[switch]$CheckOnly,
|
|
[switch]$Watch,
|
|
[int]$IntervalSeconds = 60
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md'
|
|
$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md'
|
|
$gitIgnoreTemplate = Join-Path $StandardsRepoPath 'templates/.gitignore.base'
|
|
$gitAttributesTemplate = Join-Path $StandardsRepoPath 'templates/.gitattributes.base'
|
|
$editorConfigTemplate = Join-Path $StandardsRepoPath 'templates/.editorconfig.base'
|
|
$preCommitHookTemplate = Join-Path $StandardsRepoPath 'templates/pre-commit.base.sh'
|
|
$hooksReadmeTemplate = Join-Path $StandardsRepoPath 'templates/.githooks.README.base.md'
|
|
|
|
if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) {
|
|
throw "AGENTS template not found: $agentsTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) {
|
|
throw "DoD template not found: $dodTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $gitIgnoreTemplate -PathType Leaf)) {
|
|
throw "gitignore template not found: $gitIgnoreTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $gitAttributesTemplate -PathType Leaf)) {
|
|
throw "gitattributes template not found: $gitAttributesTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $editorConfigTemplate -PathType Leaf)) {
|
|
throw "editorconfig template not found: $editorConfigTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $preCommitHookTemplate -PathType Leaf)) {
|
|
throw "pre-commit hook template not found: $preCommitHookTemplate"
|
|
}
|
|
|
|
if (-not (Test-Path -Path $hooksReadmeTemplate -PathType Leaf)) {
|
|
throw "hooks readme template not found: $hooksReadmeTemplate"
|
|
}
|
|
|
|
if ($IntervalSeconds -lt 5) {
|
|
throw 'IntervalSeconds must be >= 5.'
|
|
}
|
|
|
|
function Get-ContentHashOrMissing {
|
|
param([string]$Path)
|
|
|
|
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
|
return '__MISSING__'
|
|
}
|
|
|
|
return (Get-FileHash -Path $Path -Algorithm SHA256).Hash
|
|
}
|
|
|
|
function Ensure-FileFromTemplate {
|
|
param(
|
|
[string]$Template,
|
|
[string]$Target,
|
|
[switch]$OnlyCheck
|
|
)
|
|
|
|
$templateHash = Get-ContentHashOrMissing -Path $Template
|
|
$targetHash = Get-ContentHashOrMissing -Path $Target
|
|
|
|
if ($templateHash -eq $targetHash) {
|
|
return 'ok'
|
|
}
|
|
|
|
if ($OnlyCheck) {
|
|
return 'drift'
|
|
}
|
|
|
|
$parent = Split-Path -Parent $Target
|
|
if (-not (Test-Path -Path $parent -PathType Container)) {
|
|
New-Item -ItemType Directory -Path $parent | Out-Null
|
|
}
|
|
|
|
Copy-Item -Path $Template -Destination $Target -Force
|
|
return 'updated'
|
|
}
|
|
|
|
function Ensure-GitIgnoreEntriesFromTemplate {
|
|
param(
|
|
[string]$TemplatePath,
|
|
[string]$GitIgnorePath,
|
|
[switch]$OnlyCheck
|
|
)
|
|
|
|
$requiredEntries = Get-Content -Path $TemplatePath | Where-Object {
|
|
$_.Trim() -and -not $_.Trim().StartsWith('#')
|
|
}
|
|
|
|
if (-not (Test-Path -Path $GitIgnorePath -PathType Leaf)) {
|
|
if ($OnlyCheck) {
|
|
return 'drift'
|
|
}
|
|
|
|
New-Item -ItemType File -Path $GitIgnorePath | Out-Null
|
|
}
|
|
|
|
$lines = Get-Content -Path $GitIgnorePath
|
|
|
|
$missingEntry = $false
|
|
foreach ($entry in $requiredEntries) {
|
|
if ($lines -contains $entry) {
|
|
continue
|
|
}
|
|
|
|
$missingEntry = $true
|
|
if (-not $OnlyCheck) {
|
|
Add-Content -Path $GitIgnorePath -Value $entry
|
|
$lines += $entry
|
|
}
|
|
}
|
|
|
|
if (-not $missingEntry) {
|
|
return 'ok'
|
|
}
|
|
|
|
if ($OnlyCheck) {
|
|
return 'drift'
|
|
}
|
|
|
|
return 'updated'
|
|
}
|
|
|
|
function Invoke-ReconcileOnce {
|
|
param([switch]$OnlyCheck)
|
|
|
|
$resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path
|
|
$repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object {
|
|
$Exclude -notcontains $_.Name
|
|
}
|
|
|
|
$summary = [ordered]@{
|
|
scanned = 0
|
|
updated = 0
|
|
drift = 0
|
|
}
|
|
|
|
foreach ($repo in $repos) {
|
|
$repoPath = $repo.FullName
|
|
$agentsTarget = Join-Path $repoPath 'AGENTS.md'
|
|
$dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md'
|
|
$gitIgnoreTarget = Join-Path $repoPath '.gitignore'
|
|
$gitAttributesTarget = Join-Path $repoPath '.gitattributes'
|
|
$editorConfigTarget = Join-Path $repoPath '.editorconfig'
|
|
$preCommitHookTarget = Join-Path (Join-Path $repoPath '.githooks') 'pre-commit'
|
|
$hooksReadmeTarget = Join-Path (Join-Path $repoPath '.githooks') 'README.md'
|
|
|
|
$summary.scanned++
|
|
|
|
$agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck
|
|
$dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck
|
|
$gitAttributesState = Ensure-FileFromTemplate -Template $gitAttributesTemplate -Target $gitAttributesTarget -OnlyCheck:$OnlyCheck
|
|
$editorConfigState = Ensure-FileFromTemplate -Template $editorConfigTemplate -Target $editorConfigTarget -OnlyCheck:$OnlyCheck
|
|
$preCommitHookState = Ensure-FileFromTemplate -Template $preCommitHookTemplate -Target $preCommitHookTarget -OnlyCheck:$OnlyCheck
|
|
$hooksReadmeState = Ensure-FileFromTemplate -Template $hooksReadmeTemplate -Target $hooksReadmeTarget -OnlyCheck:$OnlyCheck
|
|
$gitIgnoreState = Ensure-GitIgnoreEntriesFromTemplate -TemplatePath $gitIgnoreTemplate -GitIgnorePath $gitIgnoreTarget -OnlyCheck:$OnlyCheck
|
|
|
|
if (
|
|
$agentsState -eq 'updated' -or
|
|
$dodState -eq 'updated' -or
|
|
$gitAttributesState -eq 'updated' -or
|
|
$editorConfigState -eq 'updated' -or
|
|
$preCommitHookState -eq 'updated' -or
|
|
$hooksReadmeState -eq 'updated' -or
|
|
$gitIgnoreState -eq 'updated'
|
|
) {
|
|
$summary.updated++
|
|
Write-Host "UPDATED: $repoPath"
|
|
continue
|
|
}
|
|
|
|
if (
|
|
$agentsState -eq 'drift' -or
|
|
$dodState -eq 'drift' -or
|
|
$gitAttributesState -eq 'drift' -or
|
|
$editorConfigState -eq 'drift' -or
|
|
$preCommitHookState -eq 'drift' -or
|
|
$hooksReadmeState -eq 'drift' -or
|
|
$gitIgnoreState -eq 'drift'
|
|
) {
|
|
$summary.drift++
|
|
Write-Host "DRIFT: $repoPath"
|
|
continue
|
|
}
|
|
|
|
Write-Host "OK: $repoPath"
|
|
}
|
|
|
|
Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)"
|
|
return $summary
|
|
}
|
|
|
|
if ($Watch) {
|
|
while ($true) {
|
|
$now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
Write-Host "[$now] Reconciling project standards..."
|
|
[void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly)
|
|
Start-Sleep -Seconds $IntervalSeconds
|
|
}
|
|
}
|
|
|
|
$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly
|
|
if ($CheckOnly -and $result.drift -gt 0) {
|
|
exit 1
|
|
}
|