zscripts-token-savers/zchecksums.ps1
KellyMichels d013dc79f1 feat: two blank lines after every z-script run
zdeploy ended with two blank lines and nothing else did, so its output
was the only one that did not butt up against the next prompt. Applied
everywhere.

Implemented in Stop-ZTracking rather than per script, because every
tracked script ends by calling it - including the usage and guard paths
that do `Stop-ZTracking; exit 1`. One place therefore covers every exit
of sixteen scripts.

  * Write-ZTrailer emits the two lines. Stop-ZTracking calls it on both
    paths, including the early return when tracking never started - a
    script that printed output still deserves the separation.
  * -FinalNote prints one last line AFTER the tracking footer and BEFORE
    the blanks. zdeploy's "Last deployed at ..." now goes through it, so
    it stays the last thing on screen instead of being followed by the
    token count.
  * The standalone tools (zchecksums, zversion, zrelease) get a local
    three-line copy at each exit rather than dot-sourcing ZHelpers -
    they are deliberately dependency-free so they run from an extracted
    release zip with nothing beside them.
  * Redundant trailing blanks were removed where a script already
    printed one before exiting, so the count is exactly two, not three.

Also fixes a related gap found while testing: the error exits inside
Get-ZConfig and Get-ZProject bypassed Stop-ZTracking entirely, so a run
that died on a missing zconfig.json printed no trailer AND no token
footer. Those three exits now route through Stop-ZTracking.

zkill/zrestart are one-line wrappers around scripts that already trail,
so they are untouched - adding it would double the blanks.

Verified by running each script and counting the trailing blank lines in
its captured output: 2 on every success path, every usage path, and the
config-error path. Suite 231/231. CHECKSUMS.txt refreshed.
2026-08-25 15:34:55 -05:00

153 lines
5.9 KiB
PowerShell

# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
# Created by Kelly Michels · dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
# Version: v1.0.0.0.14
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
#
# Usage:
# zchecksums verify every script against CHECKSUMS.txt
# zchecksums -Update regenerate CHECKSUMS.txt after changing a script
# zchecksums -Quiet verify, print only the summary line
#
# Exit codes: 0 = everything matches, 1 = a mismatch, missing or unlisted file.
#
# WHAT THIS DOES AND DOES NOT PROTECT AGAINST
# -------------------------------------------
# CHECKSUMS.txt lives in the same repo as the scripts, so anyone able to modify
# a script can also modify the manifest. This is an integrity check, not a
# signature. It reliably catches:
#
# * a truncated or corrupted download / clone
# * a file edited locally that you forgot about
# * a script added or removed without going through a commit
#
# It does NOT prove the code came from this project - only a signature (GPG,
# Sigstore) or a hash published somewhere outside this repo can do that. Compare
# against the copy on GitHub if you need that assurance.
#
# Only *.ps1 and *.cmd are covered: they are what you actually execute, and
# .gitattributes pins them to CRLF on every platform, so their hashes are
# identical on Windows, Linux and macOS. Files without that pin would hash
# differently per platform and are deliberately left out.
#
# RUN -Update ON A CLEAN CHECKOUT
# ------------------------------
# Hash whatever is on disk. That is only the same as what a user clones if the
# working copy matches git's normalised form. An editor that writes LF leaves a
# file git still considers unchanged (it normalises to LF in the index either
# way), so the file sits there with LF while every clone gets CRLF - and the
# manifest generated from it fails for everyone else. If in doubt:
#
# git status --short # must be clean
# git rm -r --cached . ; git reset --hard # or delete the scripts and
# # `git checkout -- .`
#
# then re-run -Update. The surest check is to clone the repo somewhere else and
# run `sha256sum -c CHECKSUMS.txt` there.
[CmdletBinding()]
param(
[switch]$Update,
[switch]$Quiet
)
$ErrorActionPreference = "Stop"
# Two blank lines after this script's output, matching every other z-script, so
# a run is visually separated from the next prompt. Local rather than from
# ZHelpers: this script is deliberately standalone, so it can run from an
# extracted release zip with nothing beside it.
function Write-ZTrailer { Write-Host ""; Write-Host "" }
$ManifestName = "CHECKSUMS.txt"
$Manifest = Join-Path $PSScriptRoot $ManifestName
# The covered set: executable scripts, top level only. Sorted for a stable file.
function Get-CoveredFiles {
Get-ChildItem -LiteralPath $PSScriptRoot -File |
Where-Object { $_.Extension -in @('.ps1', '.cmd') } |
Sort-Object Name
}
function Get-Sha256([string]$Path) {
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
# sha256sum-compatible: "<hash> <name>", LF endings, so `sha256sum -c` works on
# Linux/macOS as well as this script on Windows.
function Write-Manifest($Files) {
$lines = foreach ($f in $Files) { "{0} {1}" -f (Get-Sha256 $f.FullName), $f.Name }
$text = ($lines -join "`n") + "`n"
[IO.File]::WriteAllText($Manifest, $text, (New-Object Text.UTF8Encoding($false)))
}
function Read-Manifest {
if (-not (Test-Path -LiteralPath $Manifest)) { return $null }
$map = [ordered]@{}
foreach ($line in [IO.File]::ReadAllLines($Manifest)) {
$t = $line.Trim()
if (-not $t -or $t.StartsWith('#')) { continue }
# "<64 hex> <name>" - two spaces is the sha256sum convention, but accept
# any run of whitespace so a hand-edited file still parses.
if ($t -match '^([0-9a-fA-F]{64})\s+(.+)$') {
$map[$Matches[2].Trim()] = $Matches[1].ToLowerInvariant()
}
}
return $map
}
$files = @(Get-CoveredFiles)
if ($Update) {
Write-Manifest $files
Write-Host ""
Write-Host "=== zchecksums (updated) ===" -ForegroundColor Cyan
Write-Host (" Wrote {0} with {1} entries." -f $ManifestName, $files.Count) -ForegroundColor Green
Write-Host " Commit it alongside the script change, or verification will fail." -ForegroundColor DarkGray
Write-ZTrailer
exit 0
}
$expected = Read-Manifest
if ($null -eq $expected) {
Write-Host "ERROR: $ManifestName not found. Run 'zchecksums -Update' to create it." -ForegroundColor Red
Write-ZTrailer
exit 1
}
$ok = 0
$changed = @()
$missing = @()
$unlisted = @()
foreach ($f in $files) {
if (-not $expected.Contains($f.Name)) { $unlisted += $f.Name; continue }
if ((Get-Sha256 $f.FullName) -eq $expected[$f.Name]) { $ok++ } else { $changed += $f.Name }
}
foreach ($name in $expected.Keys) {
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot $name))) { $missing += $name }
}
$bad = $changed.Count + $missing.Count + $unlisted.Count
if (-not $Quiet) {
Write-Host ""
Write-Host "=== zchecksums ===" -ForegroundColor Cyan
foreach ($n in $changed) { Write-Host " CHANGED $n" -ForegroundColor Red }
foreach ($n in $missing) { Write-Host " MISSING $n (listed but not on disk)" -ForegroundColor Red }
foreach ($n in $unlisted) { Write-Host " UNLISTED $n (on disk but not in $ManifestName)" -ForegroundColor Yellow }
}
if ($bad -eq 0) {
Write-Host (" OK - {0} file(s) match {1}." -f $ok, $ManifestName) -ForegroundColor Green
Write-ZTrailer
exit 0
}
Write-Host (" FAILED - {0} ok, {1} changed, {2} missing, {3} unlisted." -f $ok, $changed.Count, $missing.Count, $unlisted.Count) -ForegroundColor Red
Write-Host " If you changed a script on purpose, run: zchecksums -Update" -ForegroundColor DarkGray
Write-ZTrailer
exit 1