fix(core): retarget versioned shims to base dispatcher on interpreter… #583
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Windows Path Detection Deep Debug - Fixed | |
| on: | |
| push: | |
| branches: | |
| - development | |
| workflow_dispatch: | |
| inputs: | |
| branch: | |
| description: 'The branch to test' | |
| required: true | |
| default: 'development' | |
| jobs: | |
| windows-path-forensics: | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.inputs.branch || 'development' }} | |
| - name: Clean slate - remove any previous omnipkg config | |
| run: | | |
| if (Test-Path ~\.config\omnipkg) { | |
| Remove-Item ~\.config\omnipkg -Recurse -Force | |
| Write-Host "Removed previous omnipkg config" | |
| } else { | |
| Write-Host "No previous omnipkg config found" | |
| } | |
| shell: pwsh | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| # ============================================================================= | |
| # PHASE 1: PRE-INSTALLATION ENVIRONMENT FORENSICS | |
| # ============================================================================= | |
| - name: "PHASE 1: Python Environment Forensics (Pre-Installation)" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 1: PYTHON ENVIRONMENT FORENSICS (PRE-INSTALLATION)" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Python Executable Locations ---" | |
| Write-Host "which python: $(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)" | |
| Write-Host "which python3: $(Get-Command python3 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)" | |
| Write-Host "sys.executable via python -c: $((python -c 'import sys; print(sys.executable)') 2>$null)" | |
| Write-Host "`n--- Python Path Information ---" | |
| python -c " | |
| import sys, os, site | |
| print('sys.executable:', repr(sys.executable)) | |
| print('sys.prefix:', repr(sys.prefix)) | |
| print('sys.base_prefix:', repr(sys.base_prefix)) | |
| print('sys.path[0]:', repr(sys.path[0] if sys.path else 'EMPTY')) | |
| print('site.getsitepackages():', site.getsitepackages() if hasattr(site, 'getsitepackages') else 'NOT_AVAILABLE') | |
| print('site.getusersitepackages():', site.getusersitepackages() if hasattr(site, 'getusersitepackages') else 'NOT_AVAILABLE') | |
| print('os.getcwd():', repr(os.getcwd())) | |
| print('__file__ paths in sys.path:') | |
| for i, p in enumerate(sys.path): | |
| print(f' [{i}]: {repr(p)}') | |
| " | |
| Write-Host "`n--- Windows Environment Variables ---" | |
| Write-Host "PYTHONPATH: $($env:PYTHONPATH)" | |
| Write-Host "PYTHONHOME: $($env:PYTHONHOME)" | |
| Write-Host "PATH (python related): $($env:PATH.Split(';') | Where-Object { $_ -like '*python*' -or $_ -like '*Python*' })" | |
| shell: pwsh | |
| # ============================================================================= | |
| # PHASE 2: INSTALLATION AND DEPENDENCY ANALYSIS | |
| # ============================================================================= | |
| - name: Install omnipkg and dependencies | |
| run: pip install -e . | |
| shell: pwsh | |
| - name: "PHASE 2: Post-Installation Package Analysis" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 2: POST-INSTALLATION PACKAGE ANALYSIS" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Pip List (All Installed Packages) ---" | |
| pip list | |
| Write-Host "`n--- Critical Package Locations (pip show) ---" | |
| $critical_packages = @('packaging', 'omnipkg', 'setuptools', 'pip') | |
| foreach ($pkg in $critical_packages) { | |
| Write-Host "`n--- Package: $pkg ---" | |
| $show_output = pip show $pkg 2>$null | |
| if ($show_output) { | |
| $show_output | |
| } else { | |
| Write-Host "Package $pkg not found or error occurred" | |
| } | |
| } | |
| Write-Host "`n--- Site-packages Directory Analysis ---" | |
| python -c " | |
| import site, os, glob | |
| print('=== SITE-PACKAGES ANALYSIS ===') | |
| # Get all possible site-packages locations | |
| locations = [] | |
| if hasattr(site, 'getsitepackages'): | |
| locations.extend(site.getsitepackages()) | |
| if hasattr(site, 'getusersitepackages'): | |
| locations.append(site.getusersitepackages()) | |
| print('All detected site-packages locations:') | |
| for i, loc in enumerate(locations): | |
| print(f' [{i}]: {repr(loc)}') | |
| exists = os.path.exists(loc) | |
| print(f' Exists: {exists}') | |
| if exists: | |
| try: | |
| contents = os.listdir(loc) | |
| print(f' Contents count: {len(contents)}') | |
| # Look for omnipkg specifically | |
| omnipkg_items = [c for c in contents if 'omnipkg' in c.lower()] | |
| if omnipkg_items: | |
| print(f' omnipkg related: {omnipkg_items}') | |
| except Exception as e: | |
| print(f' Error listing contents: {e}') | |
| # Also check sys.path for site-packages | |
| print('\nsys.path entries containing site-packages:') | |
| import sys | |
| for i, path in enumerate(sys.path): | |
| if 'site-packages' in path: | |
| print(f' [{i}]: {repr(path)}') | |
| print(f' Exists: {os.path.exists(path)}') | |
| " | |
| shell: pwsh | |
| # ============================================================================= | |
| # PHASE 3: OMNIPKG CONFIGURATION FORENSICS | |
| # ============================================================================= | |
| - name: "PHASE 3: Run omnipkg with detailed tracing" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 3: OMNIPKG CONFIGURATION FORENSICS" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Python Module Import Test ---" | |
| python -c " | |
| try: | |
| import omnipkg | |
| print('omnipkg import: SUCCESS') | |
| print('omnipkg.__file__:', repr(omnipkg.__file__)) | |
| print('omnipkg.__version__:', getattr(omnipkg, '__version__', 'NO_VERSION')) | |
| except Exception as e: | |
| print('omnipkg import: FAILED -', e) | |
| " | |
| Write-Host "`n--- Pre-config Directory State ---" | |
| if (Test-Path ~\.config) { | |
| Write-Host "~\.config exists" | |
| if (Test-Path ~\.config\omnipkg) { | |
| Write-Host "~\.config\omnipkg exists" | |
| Get-ChildItem ~\.config\omnipkg -Recurse | ForEach-Object { Write-Host " $($_.FullName)" } | |
| } else { | |
| Write-Host "~\.config\omnipkg does not exist" | |
| } | |
| } else { | |
| Write-Host "~\.config does not exist" | |
| } | |
| Write-Host "`n--- Running omnipkg status (FIRST RUN) ---" | |
| shell: pwsh | |
| - name: Run omnipkg status with output capture | |
| id: omnipkg_run | |
| run: | | |
| $output = omnipkg status 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| Write-Host "Exit Code: $exitCode" | |
| Write-Host "Output:" | |
| Write-Host $output | |
| # Store for later analysis | |
| $output | Out-File -FilePath omnipkg_output.txt -Encoding UTF8 | |
| echo "OMNIPKG_EXIT_CODE=$exitCode" >> $env:GITHUB_OUTPUT | |
| shell: pwsh | |
| continue-on-error: true | |
| - name: "PHASE 4: Configuration File Analysis - FIXED" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 4: CONFIGURATION FILE ANALYSIS - FIXED" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Post-run Directory State ---" | |
| if (Test-Path ~\.config\omnipkg) { | |
| Write-Host "~\.config\omnipkg contents:" | |
| Get-ChildItem ~\.config\omnipkg -Recurse | ForEach-Object { | |
| Write-Host " $($_.FullName) ($(if($_.PSIsContainer){'DIR'}else{'FILE'}) - $($_.Length) bytes)" | |
| } | |
| } else { | |
| Write-Host "~\.config\omnipkg still does not exist!" | |
| } | |
| Write-Host "`n--- Configuration File Contents ---" | |
| if (Test-Path ~\.config\omnipkg\config.json) { | |
| Write-Host "config.json contents:" | |
| $config_content = Get-Content ~\.config\omnipkg\config.json -Raw | |
| Write-Host $config_content | |
| # Parse and analyze - FIXED JSON PARSING | |
| try { | |
| $config = $config_content | ConvertFrom-Json | |
| Write-Host "`n--- Parsed Configuration Analysis ---" | |
| Write-Host "Root level properties: $($config.PSObject.Properties.Name -join ', ')" | |
| if ($config.environments) { | |
| Write-Host "Environments found: $($config.environments.PSObject.Properties.Name.Count)" | |
| # FIXED: Properly access the first environment | |
| $env_ids = @($config.environments.PSObject.Properties.Name) | |
| foreach ($env_id in $env_ids) { | |
| $env_config = $config.environments.$env_id | |
| Write-Host "`nEnvironment ID: $env_id" | |
| # FIXED: Properly check for null/empty values | |
| $python_exe = if ($env_config.python_executable) { $env_config.python_executable } else { "" } | |
| $site_path = if ($env_config.site_packages_path) { $env_config.site_packages_path } else { "" } | |
| $version = if ($env_config.version) { $env_config.version } else { "" } | |
| $created_at = if ($env_config.created_at) { $env_config.created_at } else { "" } | |
| Write-Host " python_executable: '$python_exe'" | |
| Write-Host " site_packages_path: '$site_path'" | |
| Write-Host " version: '$version'" | |
| Write-Host " created_at: '$created_at'" | |
| # Check if paths exist | |
| if ($python_exe) { | |
| Write-Host " python_executable exists: $(Test-Path $python_exe)" | |
| } else { | |
| Write-Host " python_executable is EMPTY!" | |
| } | |
| if ($site_path) { | |
| Write-Host " site_packages_path exists: $(Test-Path $site_path)" | |
| } else { | |
| Write-Host " site_packages_path is EMPTY!" | |
| } | |
| } | |
| } else { | |
| Write-Host "No environments found in config!" | |
| } | |
| } catch { | |
| Write-Host "Failed to parse config.json: $($_.Exception.Message)" | |
| Write-Host "Full error: $($_ | Out-String)" | |
| } | |
| } else { | |
| Write-Host "config.json does not exist!" | |
| } | |
| shell: pwsh | |
| - name: "PHASE 5: Ground Truth Discovery and Comparison - FIXED" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 5: GROUND TRUTH vs OMNIPKG COMPARISON - FIXED" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Multiple Ground Truth Methods ---" | |
| # Method 1: pip show | |
| $pip_location = "" | |
| try { | |
| $pip_output = pip show packaging 2>$null | |
| $location_line = $pip_output | Select-String -Pattern "Location:" | |
| if ($location_line) { | |
| $pip_location = $location_line.Line.Split(' ', 2)[1].Trim() | |
| Write-Host "Method 1 (pip show packaging): '$pip_location'" | |
| } else { | |
| Write-Host "Method 1 (pip show packaging): FAILED - No Location line found" | |
| } | |
| } catch { | |
| Write-Host "Method 1 (pip show packaging): FAILED - $($_.Exception.Message)" | |
| } | |
| # Method 2: Python site module | |
| Write-Host "`nMethod 2 (Python site module):" | |
| python -c " | |
| import site | |
| try: | |
| locations = site.getsitepackages() | |
| print('site.getsitepackages():', locations) | |
| for i, loc in enumerate(locations): | |
| print(f' [{i}]: {repr(loc)}') | |
| except Exception as e: | |
| print('site.getsitepackages() failed:', e) | |
| " | |
| # Method 3: Find actual package file | |
| Write-Host "`nMethod 3 (Find actual packaging module):" | |
| python -c " | |
| import packaging | |
| import os | |
| print('packaging.__file__:', repr(packaging.__file__)) | |
| pkg_dir = os.path.dirname(packaging.__file__) | |
| print('packaging directory:', repr(pkg_dir)) | |
| # Go up to site-packages | |
| possible_site_packages = os.path.dirname(pkg_dir) | |
| print('possible site-packages:', repr(possible_site_packages)) | |
| print('possible site-packages exists:', os.path.exists(possible_site_packages)) | |
| " | |
| # Method 4: sys.path analysis | |
| Write-Host "`nMethod 4 (sys.path site-packages detection):" | |
| python -c " | |
| import sys, os | |
| for path in sys.path: | |
| if 'site-packages' in path and os.path.exists(path): | |
| print('Found site-packages in sys.path:', repr(path)) | |
| # Check if it contains packaging | |
| packaging_exists = os.path.exists(os.path.join(path, 'packaging')) | |
| packaging_dist = any('packaging' in f for f in os.listdir(path) if f.endswith('.dist-info')) | |
| print(' Contains packaging module:', packaging_exists) | |
| print(' Contains packaging .dist-info:', packaging_dist) | |
| " | |
| Write-Host "`n--- omnipkg Configuration Comparison - FIXED ---" | |
| if (Test-Path ~\.config\omnipkg\config.json) { | |
| $config = Get-Content ~\.config\omnipkg\config.json | ConvertFrom-Json | |
| # FIXED: Properly extract the site_packages_path | |
| $env_ids = @($config.environments.PSObject.Properties.Name) | |
| if ($env_ids.Count -gt 0) { | |
| $first_env_id = $env_ids[0] | |
| $first_env = $config.environments.$first_env_id | |
| $omnipkg_path = if ($first_env.site_packages_path) { $first_env.site_packages_path } else { "" } | |
| Write-Host "Ground Truth (pip show): '$pip_location'" | |
| Write-Host "omnipkg Config: '$omnipkg_path'" | |
| Write-Host "Paths match: $(if ($pip_location -eq $omnipkg_path) { 'YES' } else { 'NO' })" | |
| Write-Host "omnipkg path empty: $(if ([string]::IsNullOrEmpty($omnipkg_path)) { 'YES' } else { 'NO' })" | |
| # Case sensitivity check | |
| if (![string]::IsNullOrEmpty($pip_location) -and ![string]::IsNullOrEmpty($omnipkg_path)) { | |
| Write-Host "Case-insensitive match: $(if ($pip_location.ToLower() -eq $omnipkg_path.ToLower()) { 'YES' } else { 'NO' })" | |
| } | |
| } else { | |
| Write-Host "No environments found in omnipkg config" | |
| } | |
| } else { | |
| Write-Host "Cannot compare - omnipkg config.json not found" | |
| } | |
| shell: pwsh | |
| - name: "PHASE 6: Path Construction Logic Debugging" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 6: PATH CONSTRUCTION LOGIC DEBUGGING" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Manual Path Construction Test ---" | |
| python -c " | |
| import sys, os, pathlib | |
| executable = sys.executable | |
| print('sys.executable:', repr(executable)) | |
| print('os.path.normpath(executable):', repr(os.path.normpath(executable))) | |
| print('pathlib.Path(executable):', repr(str(pathlib.Path(executable)))) | |
| # Method 1: Simple dirname approach | |
| exe_dir = os.path.dirname(executable) | |
| method1 = os.path.join(exe_dir, 'Lib', 'site-packages') | |
| print('Method 1 (exe_dir/Lib/site-packages):', repr(method1)) | |
| print('Method 1 exists:', os.path.exists(method1)) | |
| # Method 2: lib (lowercase) | |
| method2 = os.path.join(exe_dir, 'lib', 'site-packages') | |
| print('Method 2 (exe_dir/lib/site-packages):', repr(method2)) | |
| print('Method 2 exists:', os.path.exists(method2)) | |
| # Method 3: Python version specific | |
| version = f'python{sys.version_info.major}.{sys.version_info.minor}' | |
| method3 = os.path.join(exe_dir, 'lib', version, 'site-packages') | |
| print('Method 3 (version specific):', repr(method3)) | |
| print('Method 3 exists:', os.path.exists(method3)) | |
| # Method 4: Use sys.prefix | |
| prefix_method = os.path.join(sys.prefix, 'Lib', 'site-packages') | |
| print('Method 4 (sys.prefix/Lib):', repr(prefix_method)) | |
| print('Method 4 exists:', os.path.exists(prefix_method)) | |
| # Method 5: Use sys.prefix lowercase | |
| prefix_method_lower = os.path.join(sys.prefix, 'lib', 'site-packages') | |
| print('Method 5 (sys.prefix/lib):', repr(prefix_method_lower)) | |
| print('Method 5 exists:', os.path.exists(prefix_method_lower)) | |
| # List actual directory structure around executable | |
| print('\\nActual directory structure around executable:') | |
| try: | |
| for root, dirs, files in os.walk(exe_dir): | |
| level = root.replace(exe_dir, '').count(os.sep) | |
| if level < 3: # Limit depth | |
| indent = ' ' * 2 * level | |
| print(f'{indent}{os.path.basename(root)}/') | |
| sub_indent = ' ' * 2 * (level + 1) | |
| for file in files[:5]: # Limit files shown | |
| print(f'{sub_indent}{file}') | |
| if len(files) > 5: | |
| print(f'{sub_indent}... and {len(files)-5} more files') | |
| if level > 2: | |
| break | |
| except Exception as e: | |
| print(f'Error walking directory: {e}') | |
| " | |
| shell: pwsh | |
| - name: "PHASE 7: Final Diagnosis and Summary - FIXED" | |
| run: | | |
| Write-Host "=" * 80 | |
| Write-Host "PHASE 7: FINAL DIAGNOSIS AND SUMMARY - FIXED" | |
| Write-Host "=" * 80 | |
| Write-Host "`n--- Summary of Findings ---" | |
| Write-Host "1. omnipkg exit code: ${{ steps.omnipkg_run.outputs.OMNIPKG_EXIT_CODE }}" | |
| if (Test-Path ~\.config\omnipkg\config.json) { | |
| $config = Get-Content ~\.config\omnipkg\config.json | ConvertFrom-Json | |
| # FIXED: Properly extract environment data | |
| $env_ids = @($config.environments.PSObject.Properties.Name) | |
| if ($env_ids.Count -gt 0) { | |
| $first_env_id = $env_ids[0] | |
| $first_env = $config.environments.$first_env_id | |
| $omnipkg_path = if ($first_env.site_packages_path) { $first_env.site_packages_path } else { "" } | |
| Write-Host "2. omnipkg detected site-packages: '$omnipkg_path'" | |
| Write-Host "3. Config path is empty: $(if ([string]::IsNullOrEmpty($omnipkg_path)) { 'YES - THIS IS THE PROBLEM' } else { 'NO - PATH DETECTED CORRECTLY' })" | |
| # Check other important fields | |
| $python_exe = if ($first_env.python_executable) { $first_env.python_executable } else { "" } | |
| $version = if ($first_env.version) { $first_env.version } else { "" } | |
| $created_at = if ($first_env.created_at) { $first_env.created_at } else { "" } | |
| Write-Host "4. Python executable: '$python_exe'" | |
| Write-Host "5. Version field: '$version' $(if ([string]::IsNullOrEmpty($version)) { '(MISSING)' } else { '(OK)' })" | |
| Write-Host "6. Created at field: '$created_at' $(if ([string]::IsNullOrEmpty($created_at)) { '(MISSING)' } else { '(OK)' })" | |
| } else { | |
| Write-Host "2. omnipkg environments: NONE FOUND" | |
| Write-Host "3. This indicates omnipkg failed during environment creation" | |
| } | |
| } else { | |
| Write-Host "2. omnipkg config.json: NOT CREATED" | |
| Write-Host "3. This indicates omnipkg failed during initialization" | |
| } | |
| # Get pip's answer one more time for final comparison | |
| $pip_location = "" | |
| try { | |
| $pip_output = pip show packaging 2>$null | |
| $location_line = $pip_output | Select-String -Pattern "Location:" | |
| if ($location_line) { | |
| $pip_location = $location_line.Line.Split(' ', 2)[1].Trim() | |
| } | |
| } catch {} | |
| Write-Host "7. Ground truth (pip): '$pip_location'" | |
| Write-Host "`n--- Status Assessment ---" | |
| if (Test-Path ~\.config\omnipkg\config.json) { | |
| $config = Get-Content ~\.config\omnipkg\config.json | ConvertFrom-Json | |
| $env_ids = @($config.environments.PSObject.Properties.Name) | |
| if ($env_ids.Count -gt 0) { | |
| $first_env = $config.environments.$($env_ids[0]) | |
| $has_site_path = ![string]::IsNullOrEmpty($first_env.site_packages_path) | |
| $has_python_exe = ![string]::IsNullOrEmpty($first_env.python_executable) | |
| if ($has_site_path -and $has_python_exe) { | |
| Write-Host "STATUS: PATH DETECTION WORKING - Core functionality appears intact" | |
| } else { | |
| Write-Host "STATUS: PARTIAL SUCCESS - Some fields missing" | |
| } | |
| } else { | |
| Write-Host "STATUS: INITIALIZATION FAILED - No environments created" | |
| } | |
| } else { | |
| Write-Host "STATUS: COMPLETE FAILURE - No config file created" | |
| } | |
| shell: pwsh | |
| # ============================================================================= | |
| # ARTIFACTS AND CLEANUP - FIXED | |
| # ============================================================================= | |
| - name: Copy omnipkg config to workspace for upload | |
| run: | | |
| # Create a directory in the workspace to copy config files | |
| New-Item -ItemType Directory -Path "./debug-artifacts" -Force | |
| if (Test-Path ~\.config\omnipkg) { | |
| # Copy the entire omnipkg config directory to workspace | |
| Copy-Item -Path ~\.config\omnipkg -Destination "./debug-artifacts/" -Recurse -Force | |
| Write-Host "Copied omnipkg config to workspace" | |
| } else { | |
| Write-Host "No omnipkg config to copy" | |
| # Create an empty marker file | |
| New-Item -ItemType File -Path "./debug-artifacts/no-config-created.txt" -Force | |
| } | |
| # Also copy the output file if it exists | |
| if (Test-Path "./omnipkg_output.txt") { | |
| Copy-Item -Path "./omnipkg_output.txt" -Destination "./debug-artifacts/" -Force | |
| } | |
| # List what we're about to upload | |
| Write-Host "`nFiles to be uploaded:" | |
| Get-ChildItem -Path "./debug-artifacts" -Recurse | ForEach-Object { | |
| Write-Host " $($_.FullName)" | |
| } | |
| shell: pwsh | |
| if: always() | |
| - name: Upload omnipkg debug artifacts - FIXED | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: omnipkg-debug-output-fixed | |
| path: ./debug-artifacts/ | |
| if: always() | |
| - name: Final Status | |
| run: | | |
| if ("${{ steps.omnipkg_run.outputs.OMNIPKG_EXIT_CODE }}" -ne "0") { | |
| Write-Host "INVESTIGATION COMPLETE: omnipkg failed with exit code ${{ steps.omnipkg_run.outputs.OMNIPKG_EXIT_CODE }}" | |
| exit 1 | |
| } else { | |
| Write-Host "INVESTIGATION COMPLETE: omnipkg ran successfully" | |
| # Additional check for path detection | |
| if (Test-Path ~\.config\omnipkg\config.json) { | |
| $config = Get-Content ~\.config\omnipkg\config.json | ConvertFrom-Json | |
| $env_ids = @($config.environments.PSObject.Properties.Name) | |
| if ($env_ids.Count -gt 0) { | |
| $first_env = $config.environments.$($env_ids[0]) | |
| if ([string]::IsNullOrEmpty($first_env.site_packages_path)) { | |
| Write-Host "WARNING: Path detection failed - site_packages_path is empty" | |
| exit 1 | |
| } else { | |
| Write-Host "SUCCESS: Path detection working correctly" | |
| } | |
| } | |
| } | |
| } | |
| shell: pwsh |