Files
CursorInitGeneral/.cursor/hooks/check-changelog.ps1
2026-07-27 15:09:13 +08:00

140 lines
4.7 KiB
PowerShell

# Changelog sync guard: require Git changes newer than this session and changelog.
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
function Write-EmptyHookResult {
[Console]::Out.WriteLine('{}')
}
$null = [Console]::In.ReadToEnd()
# sessionStart sets this variable outside agent mode.
if ($env:CURSOR_SKIP_CHANGELOG) {
Write-EmptyHookResult
exit 0
}
$cursorDir = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $cursorDir
$changelog = Join-Path $cursorDir 'changelog\changelog-headlines.md'
$ackFile = Join-Path $cursorDir 'changelog\.changelog-ack'
$sessionMarker = Join-Path $cursorDir 'changelog\.session-start'
$scanConfigPath = Join-Path $PSScriptRoot 'changelog-scan.json'
if (-not (Test-Path -LiteralPath $changelog -PathType Leaf) -or
-not (Test-Path -LiteralPath $scanConfigPath -PathType Leaf)) {
Write-EmptyHookResult
exit 0
}
if (-not (Test-Path -LiteralPath $sessionMarker -PathType Leaf)) {
Write-EmptyHookResult
exit 0
}
$changelogMtime = (Get-Item -LiteralPath $changelog).LastWriteTimeUtc
if ((Test-Path -LiteralPath $ackFile -PathType Leaf) -and
(Get-Item -LiteralPath $ackFile).LastWriteTimeUtc -ge $changelogMtime) {
Write-EmptyHookResult
exit 0
}
$gitStatus = @(& git -C $repoRoot status --porcelain=v1 --untracked-files=all -- 2>$null)
if ($LASTEXITCODE -ne 0 -or $gitStatus.Count -eq 0) {
Write-EmptyHookResult
exit 0
}
try {
$scanConfig = Get-Content -LiteralPath $scanConfigPath -Raw -Encoding UTF8 |
ConvertFrom-Json
if ($scanConfig.schema_version -ne 1) {
throw "Unsupported changelog scan schema."
}
$includedNames = @(
$scanConfig.included_names |
Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }
)
$includedExtensions = @(
$scanConfig.included_extensions |
Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }
)
$excludedDirectoryNames = @(
$scanConfig.excluded_directories |
Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }
)
} catch {
Write-EmptyHookResult
exit 0
}
if ($includedNames.Count -eq 0 -and $includedExtensions.Count -eq 0) {
Write-EmptyHookResult
exit 0
}
$comparisonMtime = $changelogMtime
$sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc
if ($sessionMtime -gt $comparisonMtime) {
$comparisonMtime = $sessionMtime
}
$newerFile = $null
$newerRelativePath = $null
$rootItem = Get-Item -LiteralPath $repoRoot
$rootPrefix = $repoRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
$directories = New-Object 'System.Collections.Generic.Stack[System.IO.DirectoryInfo]'
$directories.Push($rootItem)
while ($directories.Count -gt 0 -and -not $newerFile) {
$directory = $directories.Pop()
try {
$entries = Get-ChildItem -LiteralPath $directory.FullName -Force -ErrorAction Stop
} catch {
continue
}
foreach ($entry in $entries) {
if ($entry.PSIsContainer) {
$isReparsePoint = ($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
if (-not $isReparsePoint -and $excludedDirectoryNames -notcontains $entry.Name) {
$directories.Push($entry)
}
continue
}
$isIncluded = ($includedNames -contains $entry.Name) -or
($includedExtensions -contains $entry.Extension)
if ($isIncluded -and $entry.LastWriteTimeUtc -gt $comparisonMtime) {
$relativePath = if ($entry.FullName.StartsWith(
$rootPrefix,
[System.StringComparison]::OrdinalIgnoreCase
)) {
$entry.FullName.Substring($rootPrefix.Length)
} else {
$entry.Name
}
$gitPath = $relativePath -replace '\\', '/'
$pathStatus = @(
& git -C $repoRoot status --porcelain=v1 --untracked-files=all -- $gitPath 2>$null
)
if ($LASTEXITCODE -eq 0 -and $pathStatus.Count -gt 0) {
$newerFile = $entry
$newerRelativePath = $relativePath
break
}
}
}
}
if ($newerFile) {
$message = "[Hook] Project source/config has an uncommitted change newer than this session and the changelog (e.g. $newerRelativePath). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views."
$result = [ordered]@{ followup_message = $message }
[Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 3))
} else {
Write-EmptyHookResult
}
exit 0