@@ -322,10 +322,7 @@ function Format-SpecKitCommand {
322322function Get-Python3Command {
323323 if ($env: SPECKIT_PYTHON -and (Get-Command $env: SPECKIT_PYTHON - ErrorAction SilentlyContinue)) {
324324 $ver = & $env: SPECKIT_PYTHON -- version 2>&1
325- if ($ver -match ' Python 3' ) {
326- & $env: SPECKIT_PYTHON - c ' import yaml' * > $null
327- if ($LASTEXITCODE -eq 0 ) { return @ ($env: SPECKIT_PYTHON ) }
328- }
325+ if ($ver -match ' Python 3' ) { return @ ($env: SPECKIT_PYTHON ) }
329326 }
330327 if (Get-Command python3 - ErrorAction SilentlyContinue) { return @ (' python3' ) }
331328 if (Get-Command python - ErrorAction SilentlyContinue) {
@@ -339,6 +336,58 @@ function Get-Python3Command {
339336 return $null
340337}
341338
339+ # SPECKIT_YAML_RUNTIME_FALLBACK=1
340+ function Get-Python3WithYamlCommand {
341+ $candidates = @ ()
342+ if ($env: SPECKIT_PYTHON -and (Get-Command $env: SPECKIT_PYTHON - ErrorAction SilentlyContinue)) {
343+ $candidates += , @ ($env: SPECKIT_PYTHON )
344+ }
345+ if (Get-Command python3 - ErrorAction SilentlyContinue) { $candidates += , @ (' python3' ) }
346+ if (Get-Command python - ErrorAction SilentlyContinue) { $candidates += , @ (' python' ) }
347+ if (Get-Command py - ErrorAction SilentlyContinue) { $candidates += , @ (' py' , ' -3' ) }
348+ if (Get-Command uv - ErrorAction SilentlyContinue) {
349+ $candidates += , @ (
350+ ' uv' ,
351+ ' run' ,
352+ ' --isolated' ,
353+ ' --no-project' ,
354+ ' --with' ,
355+ ' pyyaml==6.0.3' ,
356+ ' python'
357+ )
358+ }
359+
360+ $previousPythonPath = $env: PYTHONPATH
361+ $previousPythonSafePath = $env: PYTHONSAFEPATH
362+ try {
363+ $env: PYTHONSAFEPATH = ' 1'
364+ foreach ($command in $candidates ) {
365+ if ($command [0 ] -eq ' uv' ) {
366+ Remove-Item Env:PYTHONPATH - ErrorAction SilentlyContinue
367+ } elseif ($null -eq $previousPythonPath ) {
368+ Remove-Item Env:PYTHONPATH - ErrorAction SilentlyContinue
369+ } else {
370+ $env: PYTHONPATH = $previousPythonPath
371+ }
372+ [array ]$commandArgs = if ($command.Count -gt 1 ) { $command [1 .. ($command.Count - 1 )] } else { @ () }
373+ & $command [0 ] @commandArgs - c ' import sys, yaml; raise SystemExit(sys.version_info.major != 3)' * > $null
374+ if ($LASTEXITCODE -eq 0 ) { return $command }
375+ }
376+ } finally {
377+ if ($null -eq $previousPythonPath ) {
378+ Remove-Item Env:PYTHONPATH - ErrorAction SilentlyContinue
379+ } else {
380+ $env: PYTHONPATH = $previousPythonPath
381+ }
382+ if ($null -eq $previousPythonSafePath ) {
383+ Remove-Item Env:PYTHONSAFEPATH - ErrorAction SilentlyContinue
384+ } else {
385+ $env: PYTHONSAFEPATH = $previousPythonSafePath
386+ }
387+ }
388+ return $null
389+ }
390+
342391function Get-NormalizedPriority {
343392 param ($Value )
344393
@@ -598,22 +647,34 @@ function Resolve-TemplateContent {
598647 ForEach-Object { $_.Name }
599648 }
600649
601- $pyCmd = @ (Get-Python3Command )
650+ $yamlCmd = $null
651+ $yamlCommandResolved = $false
602652 foreach ($presetId in $sortedPresets ) {
603653 # Read strategy and file path from preset manifest
604654 $strategy = ' replace'
605655 $manifestFilePath = ' '
606656 $manifestDeclared = $false
607657 $manifest = Join-Path $presetsDir " $presetId /preset.yml"
608- if ((Test-Path $manifest ) -and -not $pyCmd ) {
609- throw " Python 3 and PyYAML are required to resolve preset template composition"
610- }
611658 if (Test-Path $manifest ) {
659+ if (-not $yamlCommandResolved ) {
660+ $yamlCmd = @ (Get-Python3WithYamlCommand )
661+ $yamlCommandResolved = $true
662+ }
663+ if (-not $yamlCmd ) {
664+ throw " Python 3 and PyYAML are required to resolve preset template composition"
665+ }
612666 try {
613667 # Use Python to parse YAML manifest for strategy and file path
614- $pyArgs = if ($pyCmd .Count -gt 1 ) { $pyCmd [1 .. ($pyCmd .Count - 1 )] } else { @ () }
668+ [ array ] $pyArgs = if ($yamlCmd .Count -gt 1 ) { $yamlCmd [1 .. ($yamlCmd .Count - 1 )] } else { @ () }
615669 $pyStderrFile = [System.IO.Path ]::GetTempFileName()
616- $stratResult = & $pyCmd [0 ] @pyArgs - c @"
670+ $previousPythonPath = $env: PYTHONPATH
671+ $previousPythonSafePath = $env: PYTHONSAFEPATH
672+ try {
673+ $env: PYTHONSAFEPATH = ' 1'
674+ if ($yamlCmd [0 ] -eq ' uv' ) {
675+ Remove-Item Env:PYTHONPATH - ErrorAction SilentlyContinue
676+ }
677+ $stratResult = & $yamlCmd [0 ] @pyArgs - c @"
617678import sys
618679try:
619680 import yaml
@@ -668,8 +729,21 @@ except Exception as exc:
668729 print(f'manifest_invalid: {exc}', file=sys.stderr)
669730 sys.exit(3)
670731"@ $manifest $TemplateName 2> $pyStderrFile
671- if ($LASTEXITCODE -ne 0 ) {
672- if ($LASTEXITCODE -eq 2 ) {
732+ $parserExitCode = $LASTEXITCODE
733+ } finally {
734+ if ($null -eq $previousPythonPath ) {
735+ Remove-Item Env:PYTHONPATH - ErrorAction SilentlyContinue
736+ } else {
737+ $env: PYTHONPATH = $previousPythonPath
738+ }
739+ if ($null -eq $previousPythonSafePath ) {
740+ Remove-Item Env:PYTHONSAFEPATH - ErrorAction SilentlyContinue
741+ } else {
742+ $env: PYTHONSAFEPATH = $previousPythonSafePath
743+ }
744+ }
745+ if ($parserExitCode -ne 0 ) {
746+ if ($parserExitCode -eq 2 ) {
673747 throw " PyYAML is required to resolve preset template composition"
674748 }
675749 throw " Invalid preset manifest $manifest "
0 commit comments