69 lines
2.4 KiB
PowerShell
69 lines
2.4 KiB
PowerShell
# planning-with-files: resolve active plan directory (PowerShell mirror).
|
|
#
|
|
# Resolution order matches scripts/resolve-plan-dir.sh:
|
|
# 1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
# 2. .\.planning\.active_plan content
|
|
# 3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
# 4. Empty (legacy fallback to .\task_plan.md handled by caller)
|
|
|
|
param(
|
|
[string]$PlanRoot = (Join-Path (Get-Location) ".planning")
|
|
)
|
|
|
|
$projectRoot = (Get-Location).Path
|
|
|
|
# Containment guard (security A1.3): a resolved plan dir must canonicalize to a
|
|
# path under the project root. A directory symlink/junction inside a valid slug
|
|
# pointing outside the workspace would otherwise let the hooks hash and inject
|
|
# an arbitrary file. Resolve-Path follows reparse points; we compare the real
|
|
# paths. If canonicalization fails for either side we fail open (return $true)
|
|
# to keep legacy behavior intact on minimal hosts.
|
|
function Test-WithinRoot {
|
|
param([string]$Candidate)
|
|
try {
|
|
$rootReal = (Resolve-Path -LiteralPath $projectRoot -ErrorAction Stop).Path
|
|
$candReal = (Resolve-Path -LiteralPath $Candidate -ErrorAction Stop).Path
|
|
} catch {
|
|
return $true
|
|
}
|
|
if (-not $rootReal -or -not $candReal) { return $true }
|
|
$rootNorm = $rootReal.TrimEnd('\', '/')
|
|
$candNorm = $candReal.TrimEnd('\', '/')
|
|
if ($candNorm -eq $rootNorm) { return $true }
|
|
return $candNorm.StartsWith($rootNorm + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
|
}
|
|
|
|
$activeFile = Join-Path $PlanRoot ".active_plan"
|
|
|
|
if ($env:PLAN_ID) {
|
|
$candidate = Join-Path $PlanRoot $env:PLAN_ID
|
|
if ((Test-Path $candidate -PathType Container) -and (Test-WithinRoot $candidate)) {
|
|
Write-Output $candidate
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
if (Test-Path $activeFile) {
|
|
$planId = (Get-Content $activeFile -Raw).Trim()
|
|
if ($planId) {
|
|
$candidate = Join-Path $PlanRoot $planId
|
|
if ((Test-Path $candidate -PathType Container) -and (Test-WithinRoot $candidate)) {
|
|
Write-Output $candidate
|
|
exit 0
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Test-Path $PlanRoot -PathType Container) {
|
|
$latest = Get-ChildItem -Path $PlanRoot -Directory |
|
|
Where-Object { -not $_.Name.StartsWith('.') } |
|
|
Where-Object { Test-WithinRoot $_.FullName } |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Select-Object -First 1
|
|
if ($latest) {
|
|
Write-Output $latest.FullName
|
|
}
|
|
}
|
|
|
|
exit 0
|