diff --git a/scripts/install-global.ps1 b/scripts/install-global.ps1 index 30c131d2..181c4a0d 100644 --- a/scripts/install-global.ps1 +++ b/scripts/install-global.ps1 @@ -148,6 +148,39 @@ try { } catch { Write-DotbotCommand "Parse skipped: $_" } $env:DOTBOT_VERSION = $DotbotVersion +# Parses raw CLI tokens into a named-parameter hashtable. +# $PositionalNames maps positional index to param name (e.g. @('Name','Source')). +function ConvertTo-SplatArg { + param( + [string[]]$Tokens, + [string[]]$PositionalNames = @() + ) + $splat = @{} + $positional = @() + $i = 0 + while ($i -lt $Tokens.Count) { + if ($Tokens[$i] -match '^--?(.+)$') { + $pname = $Matches[1] + if (($i + 1) -lt $Tokens.Count -and $Tokens[$i+1] -notmatch '^--?') { + $splat[$pname] = $Tokens[$i+1]; $i += 2 + } else { + $splat[$pname] = $true; $i++ + } + } else { + $positional += $Tokens[$i]; $i++ + } + } + for ($j = 0; $j -lt [Math]::Min($positional.Count, $PositionalNames.Count); $j++) { + $splat[$PositionalNames[$j]] = $positional[$j] + } + if ($positional.Count -gt $PositionalNames.Count) { + $unexpected = $positional[$PositionalNames.Count..($positional.Count - 1)] -join ', ' + Write-DotbotError "Unexpected argument(s): $unexpected" + exit 1 + } + return $splat +} + function Show-Help { Write-DotbotBanner -Title "D O T B O T v$DotbotVersion" -Subtitle "Autonomous Development System" Write-DotbotSection "COMMANDS" @@ -293,17 +326,18 @@ function Invoke-Update { function Invoke-Workflow { $wfSubCmd = if ($SubArgs.Count -gt 0) { $SubArgs[0] } else { 'list' } - $wfName = if ($SubArgs.Count -gt 1) { $SubArgs[1] } else { '' } - [string[]]$wfExtra = @() - if ($SubArgs.Count -gt 2) { $wfExtra = @($SubArgs[2..($SubArgs.Count-1)]) } + $wfRest = if ($SubArgs.Count -gt 1) { @($SubArgs[1..($SubArgs.Count-1)]) } else { @() } + $wfScript = switch ($wfSubCmd) { 'add' { Join-Path $ScriptsDir 'workflow-add.ps1' } 'remove' { Join-Path $ScriptsDir 'workflow-remove.ps1' } 'list' { Join-Path $ScriptsDir 'workflow-list.ps1' } default { $null } } + if ($wfScript -and (Test-Path $wfScript)) { - if ($wfExtra.Count -gt 0) { & $wfScript $wfName @wfExtra } else { & $wfScript $wfName } + $wfSplat = ConvertTo-SplatArg -Tokens $wfRest -PositionalNames @('Name') + & $wfScript @wfSplat } else { Write-DotbotWarning "Usage: dotbot workflow [add|remove|list] [name] [--Force]" } @@ -323,36 +357,13 @@ function Invoke-Registry { } if ($regScript -and (Test-Path $regScript)) { - # Separate positional args from named flags - $regSplat = @{} - $positional = @() - $ri = 0 - while ($ri -lt $regRest.Count) { - if ($regRest[$ri] -match '^--?(.+)$') { - $pname = $Matches[1] - if (($ri + 1) -lt $regRest.Count -and $regRest[$ri + 1] -notmatch '^--?') { - $regSplat[$pname] = $regRest[$ri + 1] - $ri += 2 - } else { - $regSplat[$pname] = $true - $ri++ - } - } else { - $positional += $regRest[$ri] - $ri++ - } - } - - # Map positional args to named parameters - if ($regSubCmd -eq 'add') { - if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] } - if ($positional.Count -ge 2) { $regSplat['Source'] = $positional[1] } - } elseif ($regSubCmd -eq 'remove') { - if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] } - } elseif ($regSubCmd -eq 'update') { - if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] } + $regPositional = switch ($regSubCmd) { + 'add' { @('Name', 'Source') } + 'remove' { @('Name') } + 'update' { @('Name') } + default { @() } } - + $regSplat = ConvertTo-SplatArg -Tokens $regRest -PositionalNames $regPositional & $regScript @regSplat } else { Write-DotbotWarning "Usage: dotbot registry [add|list|update|remove] ..." @@ -364,12 +375,10 @@ function Invoke-Registry { } function Invoke-Run { - $wfName = if ($SplatArgs.Count -gt 0) { $SplatArgs.Values | Select-Object -First 1 } else { '' } - # Get workflow name from positional args - $raw = if ($args.Count -gt 1) { $args[1] } else { $wfName } + $wfName = if ($SubArgs.Count -gt 0) { $SubArgs[0] } else { '' } $runScript = Join-Path $ScriptsDir 'workflow-run.ps1' - if ($raw -and (Test-Path $runScript)) { - & $runScript -WorkflowName $raw + if ($wfName -and (Test-Path $runScript)) { + & $runScript -WorkflowName $wfName } else { Write-DotbotWarning "Usage: dotbot run " } diff --git a/tests/Test-WorkflowIntegration.ps1 b/tests/Test-WorkflowIntegration.ps1 index 65776c4d..f35cad24 100644 --- a/tests/Test-WorkflowIntegration.ps1 +++ b/tests/Test-WorkflowIntegration.ps1 @@ -524,6 +524,13 @@ if ((Test-Path $cliScript) -and (Test-Path $startFromPromptWf)) { $installedDir = Join-Path $testProjectCli ".bot\workflows\start-from-prompt" Assert-PathExists -Name "CLI 'workflow add' installs workflow directory" -Path $installedDir + # Test: workflow add --Force via CLI dispatcher (regression: array splatting dropped switches) + $forceOutput = & pwsh -NoProfile -ExecutionPolicy Bypass -Command "Set-Location '$testProjectCli'; & '$cliScript' workflow add start-from-prompt --Force" 2>&1 + $forceFailed = $forceOutput | Where-Object { $_ -match 'positional parameter cannot be found' -or $_ -match 'cannot be found that accepts argument' } + Assert-True -Name "CLI 'workflow add --Force' dispatches without splatting error" ` + -Condition ($null -eq $forceFailed -or $forceFailed.Count -eq 0) ` + -Message "Switch --Force not bound via CLI dispatcher: $forceFailed" + # Test: workflow remove also dispatches cleanly $removeOutput = & pwsh -NoProfile -ExecutionPolicy Bypass -Command "Set-Location '$testProjectCli'; & '$cliScript' workflow remove start-from-prompt" 2>&1 $removeFailed = $removeOutput | Where-Object { $_ -match 'positional parameter cannot be found' -or $_ -match 'cannot be found that accepts argument' } @@ -548,6 +555,33 @@ if ((Test-Path $cliScript) -and (Test-Path $startFromPromptWf)) { Write-Host "" +# ═══════════════════════════════════════════════════════════════════ +# CLI REGISTRY DISPATCH +# ═══════════════════════════════════════════════════════════════════ + +Write-Host " CLI REGISTRY DISPATCH" -ForegroundColor Cyan +Write-Host " ────────────────────────────────────────────" -ForegroundColor DarkGray + +# Regression: Invoke-Registry used inline parse loop; ConvertTo-SplatArgs helper must bind +# named flags (--branch, --force) without "positional parameter cannot be found" error. +if (Test-Path $cliScript) { + $regCliProj = New-TestProjectFromGolden -Flavor 'default' + try { + # registry add with named flags via CLI dispatcher — must not throw splatting error + $regAddOutput = & pwsh -NoProfile -ExecutionPolicy Bypass -Command "& '$cliScript' registry add TestReg not-a-url --branch main --force" 2>&1 + $regAddFailed = $regAddOutput | Where-Object { $_ -match 'positional parameter cannot be found' -or $_ -match 'cannot be found that accepts argument' } + Assert-True -Name "CLI 'registry add --branch --force' dispatches without splatting error" ` + -Condition ($null -eq $regAddFailed -or $regAddFailed.Count -eq 0) ` + -Message "Named flags not bound via CLI dispatcher: $regAddFailed" + } finally { + Remove-TestProject -Path $regCliProj.ProjectRoot + } +} else { + Write-TestResult -Name "CLI registry dispatch tests" -Status Skip -Message "dotbot CLI not found" +} + +Write-Host "" + # ═══════════════════════════════════════════════════════════════════ # WORKFLOW ADD FUNCTIONALITY # ═══════════════════════════════════════════════════════════════════