diff --git a/AGENTS.md b/AGENTS.md index 25ea4cc..dd7163b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ This repository is the control-plane CLI for deterministic `C:\dev` workspace or ## CLI Orchestration Contract - CLI entrypoint: `scripts/Invoke-CdevCli.ps1`. +- Windows invocation contract: `powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 ...`. +- Linux invocation contract: `pwsh -NoProfile -File ./scripts/Invoke-CdevCli.ps1 ...`. - CLI is the preferred operator interface for: - repo topology inspection (`repos list`) - governance checks (`repos doctor`) diff --git a/README.md b/README.md index efd0c13..8439bcf 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ Control-plane CLI for deterministic `C:\dev` workspace operations. ## Entrypoint ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 [options] +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 [options] ``` +On Linux, invoke the same entrypoint with `pwsh -NoProfile -File`. + ## Commands - `help [topic]` @@ -26,23 +28,23 @@ pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 [options] ## Quick Start ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 repos list -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1 -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 ci integration-gate --repo svelderrainruiz/labview-cdev-surface --branch main +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 repos list +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1 +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 ci integration-gate --repo svelderrainruiz/labview-cdev-surface --branch main ``` ## Linux Flow (Docker Desktop Linux) ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux --image nationalinstruments/labview:latest-linux +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux --image nationalinstruments/labview:latest-linux ``` ## Release Packaging ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 release package --output-root .\artifacts\release\cli +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 release package --output-root .\artifacts\release\cli ``` Release artifacts: diff --git a/scripts/Invoke-CdevCli.ps1 b/scripts/Invoke-CdevCli.ps1 index 017075f..2544273 100644 --- a/scripts/Invoke-CdevCli.ps1 +++ b/scripts/Invoke-CdevCli.ps1 @@ -1,4 +1,3 @@ -#Requires -Version 7.0 [CmdletBinding()] param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] @@ -8,12 +7,24 @@ param( [string]$SurfaceRoot, [Parameter()] - [string]$ReportPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'artifacts\cli\cdev-cli-last-run.json') + [string]$ReportPath = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +if ([string]::IsNullOrWhiteSpace([string]$ReportPath)) { + $scriptDirectory = if (-not [string]::IsNullOrWhiteSpace([string]$PSScriptRoot)) { + $PSScriptRoot + } elseif (-not [string]::IsNullOrWhiteSpace([string]$PSCommandPath)) { + Split-Path -Parent $PSCommandPath + } else { + (Get-Location).Path + } + + $ReportPath = Join-Path (Split-Path -Parent $scriptDirectory) 'artifacts\cli\cdev-cli-last-run.json' +} + $libRoot = Join-Path $PSScriptRoot 'lib' foreach ($libFile in @( 'Common.ps1', @@ -37,7 +48,7 @@ function Show-CdevHelp { 'cdev control-plane CLI', '', 'Usage:', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 [options]', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 [options]', '', 'Groups and commands:', ' help [topic]', @@ -54,12 +65,12 @@ function Show-CdevHelp { ' release package', '', 'Examples:', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 repos list', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 postactions collect --report-path C:\dev\artifacts\workspace-install-latest.json', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux' + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 repos list', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 postactions collect --report-path C:\dev\artifacts\workspace-install-latest.json', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux' ) if ([string]::IsNullOrWhiteSpace($Topic)) { @@ -81,7 +92,7 @@ function Show-CdevHelp { } } -Assert-CdevCommand -Name 'pwsh' +Resolve-CdevPowerShellHost | Out-Null $cliRepoRoot = Get-CdevRepoRoot -ScriptPath $PSCommandPath $argsMap = Convert-CdevArgsToMap -InputArgs $CommandArgs $script:resolvedSurfaceRoot = $null diff --git a/scripts/lib/Common.ps1 b/scripts/lib/Common.ps1 index 828bba5..16670c0 100644 --- a/scripts/lib/Common.ps1 +++ b/scripts/lib/Common.ps1 @@ -1,5 +1,26 @@ Set-StrictMode -Version Latest +function Resolve-CdevPowerShellHost { + [CmdletBinding()] + param() + + $isWindowsHost = [string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase) + $candidates = if ($isWindowsHost) { + @('powershell.exe', 'powershell', 'pwsh.exe', 'pwsh') + } else { + @('pwsh', 'pwsh.exe', 'powershell', 'powershell.exe') + } + + foreach ($candidate in $candidates) { + $command = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($null -ne $command -and -not [string]::IsNullOrWhiteSpace($command.Source)) { + return $command.Source + } + } + + throw "Unable to resolve a PowerShell host executable (tried: $($candidates -join ', '))." +} + function Get-CdevRepoRoot { param([Parameter(Mandatory = $true)][string]$ScriptPath) return (Resolve-Path -Path (Join-Path (Split-Path -Parent $ScriptPath) '..')).Path @@ -78,16 +99,22 @@ function Invoke-CdevPwshScript { throw "Script not found: $ScriptPath" } - $argList = @('-NoProfile', '-File', $ScriptPath) + $powerShellHost = Resolve-CdevPowerShellHost + $argList = @('-NoProfile') + if ([string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase)) { + $argList += @('-ExecutionPolicy', 'RemoteSigned') + } + $argList += @('-File', $ScriptPath) if ($null -ne $Arguments -and $Arguments.Count -gt 0) { $argList += $Arguments } - & pwsh @argList - $exitCode = $LASTEXITCODE + & $powerShellHost @argList + $exitCode = if ($LASTEXITCODE -ne $null) { [int]$LASTEXITCODE } else { 0 } return [pscustomobject]@{ script = $ScriptPath + host = $powerShellHost arguments = @($Arguments) exit_code = $exitCode status = if ($exitCode -eq 0) { 'succeeded' } else { 'failed' } diff --git a/tests/CdevCliContract.Tests.ps1 b/tests/CdevCliContract.Tests.ps1 index cd24671..cf3c662 100644 --- a/tests/CdevCliContract.Tests.ps1 +++ b/tests/CdevCliContract.Tests.ps1 @@ -1,10 +1,32 @@ -#Requires -Version 7.0 #Requires -Modules Pester $ErrorActionPreference = 'Stop' Describe 'cdev CLI command contract' { BeforeAll { + $isWindowsHost = [string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase) + $candidateHosts = if ($isWindowsHost) { + @('powershell.exe', 'powershell', 'pwsh.exe', 'pwsh') + } else { + @('pwsh', 'pwsh.exe', 'powershell', 'powershell.exe') + } + $script:powerShellHost = $null + foreach ($candidate in $candidateHosts) { + $cmd = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($null -ne $cmd -and -not [string]::IsNullOrWhiteSpace($cmd.Source)) { + $script:powerShellHost = $cmd.Source + break + } + } + if ([string]::IsNullOrWhiteSpace([string]$script:powerShellHost)) { + throw "Unable to resolve a PowerShell host executable (tried: $($candidateHosts -join ', '))." + } + + $script:powerShellInvocationArgs = @('-NoProfile') + if ($isWindowsHost) { + $script:powerShellInvocationArgs += @('-ExecutionPolicy', 'RemoteSigned') + } + $script:repoRoot = (Resolve-Path -Path (Join-Path $PSScriptRoot '..')).Path $script:entrypoint = Join-Path $script:repoRoot 'scripts/Invoke-CdevCli.ps1' $script:contractPath = Join-Path $script:repoRoot 'cli-contract.json' @@ -56,7 +78,8 @@ Describe 'cdev CLI command contract' { try { Remove-Item Env:CDEV_SURFACE_ROOT -ErrorAction SilentlyContinue - & pwsh -NoProfile -File $script:entrypoint -ReportPath $reportPath help + $invokeArgs = @($script:powerShellInvocationArgs + @('-File', $script:entrypoint, '-ReportPath', $reportPath, 'help')) + & $script:powerShellHost @invokeArgs $LASTEXITCODE | Should -Be 0 (Test-Path -LiteralPath $reportPath -PathType Leaf) | Should -BeTrue } finally { @@ -76,7 +99,8 @@ Describe 'cdev CLI command contract' { $resolvedOutputRoot = [System.IO.Path]::GetFullPath($outputRoot) try { - & pwsh -NoProfile -File $script:entrypoint -ReportPath $reportPath release package --output-root $outputRoot + $invokeArgs = @($script:powerShellInvocationArgs + @('-File', $script:entrypoint, '-ReportPath', $reportPath, 'release', 'package', '--output-root', $outputRoot)) + & $script:powerShellHost @invokeArgs $LASTEXITCODE | Should -Be 0 $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json -ErrorAction Stop