← Files NightshiftARCHIVED FILE

runtime/windows/provision.ps1

9.08 KB · Oct 2, 2026 · 00:30 UTC

↓ Download file

<#
.SYNOPSIS
  Thin auto-add seatbelt on native Windows. No recipe engine.

.DESCRIPTION
  Mirrors runtime/provision.sh. The skill tells the model to inspect the package
  manager, choose a compatible tool, install, smoke, and record. This helper
  only captures a write-surface baseline, prints the diff, and restores.
  Refuse symlink or reparse escape. Unknown flags do not mutate.
  recover of a leftover provision-transaction.json still settles that file.
  Exit: 0 ok - 1 usage/runtime - 2 refused - 3 unproven restore
#>
param(
    [string]$Project = [Environment]::CurrentDirectory,
    [Parameter(Position = 0)]
    [string]$Command = '',
    [string[]]$Surface = @(),
    [switch]$Rollback,
    [switch]$Diagnose
)

Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'

$pluginRoot = Resolve-Path (Join-Path $PSScriptRoot '../..')
Import-Module (Join-Path $pluginRoot 'lib/Nightshift.psm1') -Force -DisableNameChecking
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)

function Write-NSLf {
    param([string]$Text)
    [Console]::Out.Write($Text + "`n")
}

function Write-NSSeatbeltUsage {
    Write-Error @'
provision.ps1 -Project DIR -Command baseline -Surface PATH[,PATH...]
provision.ps1 -Project DIR -Command diff
provision.ps1 -Project DIR -Command rollback
provision.ps1 -Project DIR -Command recover
'@
    return 1
}

if ([string]::IsNullOrEmpty($Command)) { exit (Write-NSSeatbeltUsage) }

$allowed = @('baseline', 'diff', 'rollback', 'recover')
if ($allowed -notcontains $Command) {
    # An unrecognized command prints the usage and mutates nothing.
    exit (Write-NSSeatbeltUsage)
}

$workspace = Get-NSAbsolutePath $Project
if (-not (Test-Path -LiteralPath $workspace -PathType Container)) {
    Write-Error ('provision: not a directory: ' + $workspace)
    exit 1
}

$ns = Join-NSPath $workspace '.nightshift'
$manifest = Get-NSLayoutPath $ns 'provision-surface'
$tx = Get-NSLayoutPath $ns 'provision-transaction'

# Leftover engine transaction: the native recover still settles it.
if (($Command -eq 'recover' -or $Command -eq 'rollback') -and
    (Test-Path -LiteralPath $tx) -and -not (Test-Path -LiteralPath $manifest)) {
    exit (Invoke-NSProvisionCommand -Project $Project -Command $Command `
            -Rollback:($Command -eq 'rollback' -or $Rollback) -Diagnose:$Diagnose)
}

# Thin seatbelt for the new surface format.
$target = $workspace
$wt = Get-NSLayoutPath $ns 'work-target'
if (Test-Path -LiteralPath $wt -PathType Leaf) {
    $line = (Get-Content -LiteralPath $wt -TotalCount 1)
    if (-not [string]::IsNullOrEmpty($line)) {
        if ($line -match '^[\\/]' -or $line -match '^[A-Za-z]:[\\/]') {
            $target = $line
        } else {
            $target = Join-NSPath $workspace $line
        }
    }
}

function Test-NSSurfaceEscape {
    param([string]$Rel, [string]$Root)
    if ([string]::IsNullOrEmpty($Rel)) { return $true }
    if ($Rel.Contains('..')) { return $true }
    if ($Rel.StartsWith('/') -or $Rel.StartsWith('\')) { return $true }
    $locked = @(
        'punch-list.md', 'parking-lot.md', 'drafting-table.md', 'work-orders.md',
        'capability-policy.json', 'shift-policy.json', 'shift-defaults.json', 'rules.json'
    )
    if ($locked -contains $Rel) { return $true }
    if ($Rel -eq '.nightshift' -or $Rel.StartsWith('.nightshift/') -or
        $Rel -eq '.git' -or $Rel.StartsWith('.git/')) { return $true }
    $full = Join-Path $Root $Rel
    if (Test-Path -LiteralPath $full) {
        $item = Get-Item -LiteralPath $full -Force
        if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { return $true }
        if ($item.LinkType) { return $true }
    }
    return $false
}

switch ($Command) {
    'baseline' {
        if ($Surface.Count -lt 1) { exit (Write-NSSeatbeltUsage) }
        New-Item -ItemType Directory -Force -Path $ns | Out-Null
        $base = Get-NSLayoutPath $ns 'provision-baseline'
        if (Test-Path -LiteralPath $base) { Remove-Item -LiteralPath $base -Recurse -Force }
        New-Item -ItemType Directory -Force -Path $base | Out-Null
        $rows = New-Object System.Collections.Generic.List[string]
        foreach ($rel in $Surface) {
            if (Test-NSSurfaceEscape -Rel $rel -Root $target) {
                if (Test-Path -LiteralPath $base) { Remove-Item -LiteralPath $base -Recurse -Force }
                Write-NSLf ('{"ok":false,"refused":true,"reason":"surface-escape:' + $rel + '"}')
                exit 2
            }
            $path = Join-Path $target $rel
            $existed = 0
            $digest = '-'
            if (Test-Path -LiteralPath $path -PathType Leaf) {
                $existed = 1
                $sha = Get-FileHash -LiteralPath $path -Algorithm SHA256
                $digest = $sha.Hash.ToLowerInvariant()
                Copy-Item -LiteralPath $path -Destination (Join-Path $base $digest) -Force
            } elseif (Test-Path -LiteralPath $path) {
                if (Test-Path -LiteralPath $base) { Remove-Item -LiteralPath $base -Recurse -Force }
                Write-NSLf ('{"ok":false,"refused":true,"reason":"surface-not-file:' + $rel + '"}')
                exit 2
            }
            $rows.Add(($rel + "`t" + $existed + "`t" + $digest))
        }
        Set-Content -LiteralPath $manifest -Value $rows -Encoding utf8
        Set-Content -LiteralPath $tx -Value '{"schemaVersion":1,"stage":"baseline"}' -Encoding utf8
        Write-NSLf '{"ok":true,"refused":false,"rolledBack":false,"command":"baseline"}'
        exit 0
    }
    'diff' {
        if (-not (Test-Path -LiteralPath $manifest)) {
            Write-Error 'provision: no provision-surface; run baseline first'
            exit 1
        }
        $touched = New-Object System.Collections.Generic.List[string]
        Get-Content -LiteralPath $manifest | ForEach-Object {
            if ([string]::IsNullOrEmpty($_)) { return }
            $parts = $_ -split "`t", 3
            $rel = $parts[0]
            $existed = $parts[1]
            $digest = $parts[2]
            $path = Join-Path $target $rel
            $now = 'absent'
            if (Test-Path -LiteralPath $path -PathType Leaf) {
                $item = Get-Item -LiteralPath $path -Force
                if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
                    $now = 'symlink'
                } else {
                    $now = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
                }
            } elseif (Test-Path -LiteralPath $path) {
                $now = 'other'
            }
            $changed = $false
            if ($existed -eq '1') {
                if ($now -ne $digest) { $changed = $true }
            } else {
                if ($now -ne 'absent') { $changed = $true }
            }
            if ($changed) { $touched.Add($rel) }
        }
        $json = ($touched | ForEach-Object { '"' + $_ + '"' }) -join ','
        Write-NSLf ('{"ok":true,"touched":[' + $json + ']}')
        exit 0
    }
    { $_ -in @('rollback', 'recover') } {
        if (-not (Test-Path -LiteralPath $manifest)) {
            Write-NSLf '{"detail":"no transaction","ok":true,"recovered":false}'
            exit 0
        }
        $base = Get-NSLayoutPath $ns 'provision-baseline'
        Get-Content -LiteralPath $manifest | ForEach-Object {
            if ([string]::IsNullOrEmpty($_)) { return }
            $parts = $_ -split "`t", 3
            $rel = $parts[0]
            $existed = $parts[1]
            $digest = $parts[2]
            if (Test-NSSurfaceEscape -Rel $rel -Root $target) {
                Write-NSLf '{"ok":false,"proven":false,"rolledBack":false,"reason":"surface-escape"}'
                exit 3
            }
            $path = Join-Path $target $rel
            if (Test-Path -LiteralPath $path) {
                $item = Get-Item -LiteralPath $path -Force
                if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
                    Remove-Item -LiteralPath $path -Force
                } elseif (-not $item.PSIsContainer) {
                    Remove-Item -LiteralPath $path -Force
                } else {
                    Write-NSLf '{"ok":false,"proven":false,"rolledBack":false,"reason":"restore-failed"}'
                    exit 3
                }
            }
            if ($existed -eq '1') {
                $blob = Join-Path $base $digest
                if (-not (Test-Path -LiteralPath $blob)) {
                    Write-NSLf '{"ok":false,"proven":false,"rolledBack":false,"reason":"restore-failed"}'
                    exit 3
                }
                $dir = Split-Path -Parent $path
                if (-not (Test-Path -LiteralPath $dir)) {
                    New-Item -ItemType Directory -Force -Path $dir | Out-Null
                }
                Copy-Item -LiteralPath $blob -Destination $path -Force
            }
        }
        Remove-Item -LiteralPath $manifest -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $tx -Force -ErrorAction SilentlyContinue
        if (Test-Path -LiteralPath $base) { Remove-Item -LiteralPath $base -Recurse -Force }
        Write-NSLf '{"ok":true,"rolledBack":true,"recovered":true}'
        exit 0
    }
}

exit (Write-NSSeatbeltUsage)

SHA-256: cf80d365343885ca2bd98f8b5d565394c8663f611d84e5da04867a2cd1de3e34