From 845db3396cd1316948b79c81086793fa8423c838 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Fri, 22 May 2026 12:04:14 +0300 Subject: [PATCH 1/5] fix(cli): fix switch args dropped by workflow/run dispatchers --- scripts/install-global.ps1 | 80 ++++++++++++++++-------------- tests/Test-WorkflowIntegration.ps1 | 34 +++++++++++++ 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/scripts/install-global.ps1 b/scripts/install-global.ps1 index 30c131d2..2a7d7d85 100644 --- a/scripts/install-global.ps1 +++ b/scripts/install-global.ps1 @@ -148,6 +148,34 @@ 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-SplatArgs { + 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] + } + 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 +321,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-SplatArgs -Tokens $wfRest -PositionalNames @('Name') + & $wfScript @wfSplat } else { Write-DotbotWarning "Usage: dotbot workflow [add|remove|list] [name] [--Force]" } @@ -323,36 +352,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-SplatArgs -Tokens $regRest -PositionalNames $regPositional & $regScript @regSplat } else { Write-DotbotWarning "Usage: dotbot registry [add|list|update|remove] ..." @@ -364,12 +370,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..bbbb4fdb 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 https://example.com/repo.git --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 # ═══════════════════════════════════════════════════════════════════ From f1dc196a4c29bcaf85ad8c0722b55de3c827ab15 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Fri, 22 May 2026 12:50:50 +0300 Subject: [PATCH 2/5] style: rename ConvertTo-SplatArgs to singular noun --- scripts/install-global.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/install-global.ps1 b/scripts/install-global.ps1 index 2a7d7d85..d0260998 100644 --- a/scripts/install-global.ps1 +++ b/scripts/install-global.ps1 @@ -150,7 +150,7 @@ $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-SplatArgs { +function ConvertTo-SplatArg { param( [string[]]$Tokens, [string[]]$PositionalNames = @() @@ -331,7 +331,7 @@ function Invoke-Workflow { } if ($wfScript -and (Test-Path $wfScript)) { - $wfSplat = ConvertTo-SplatArgs -Tokens $wfRest -PositionalNames @('Name') + $wfSplat = ConvertTo-SplatArg -Tokens $wfRest -PositionalNames @('Name') & $wfScript @wfSplat } else { Write-DotbotWarning "Usage: dotbot workflow [add|remove|list] [name] [--Force]" @@ -358,7 +358,7 @@ function Invoke-Registry { 'update' { @('Name') } default { @() } } - $regSplat = ConvertTo-SplatArgs -Tokens $regRest -PositionalNames $regPositional + $regSplat = ConvertTo-SplatArg -Tokens $regRest -PositionalNames $regPositional & $regScript @regSplat } else { Write-DotbotWarning "Usage: dotbot registry [add|list|update|remove] ..." From e8ad12739a751d729f9f568895e2f8463e165cce Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Fri, 22 May 2026 19:01:15 +0300 Subject: [PATCH 3/5] fix: address copilot review on pr #443 --- tests/Test-WorkflowIntegration.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Test-WorkflowIntegration.ps1 b/tests/Test-WorkflowIntegration.ps1 index bbbb4fdb..f35cad24 100644 --- a/tests/Test-WorkflowIntegration.ps1 +++ b/tests/Test-WorkflowIntegration.ps1 @@ -568,7 +568,7 @@ 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 https://example.com/repo.git --branch main --force" 2>&1 + $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) ` From c2e979e55aeac3a0e2a311a6d8cca3be8e622ff3 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Fri, 22 May 2026 19:09:46 +0300 Subject: [PATCH 4/5] fix: warn on unexpected positional args in ConvertTo-SplatArg --- scripts/install-global.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/install-global.ps1 b/scripts/install-global.ps1 index d0260998..84fc3adc 100644 --- a/scripts/install-global.ps1 +++ b/scripts/install-global.ps1 @@ -173,6 +173,10 @@ function ConvertTo-SplatArg { 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-DotbotWarning "Unexpected argument(s): $unexpected" + } return $splat } From f90a2c50c58b210e5c2276727019979b339f6872 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Fri, 22 May 2026 19:44:43 +0300 Subject: [PATCH 5/5] fix: error on unexpected positional args in ConvertTo-SplatArg --- scripts/install-global.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/install-global.ps1 b/scripts/install-global.ps1 index 84fc3adc..181c4a0d 100644 --- a/scripts/install-global.ps1 +++ b/scripts/install-global.ps1 @@ -175,7 +175,8 @@ function ConvertTo-SplatArg { } if ($positional.Count -gt $PositionalNames.Count) { $unexpected = $positional[$PositionalNames.Count..($positional.Count - 1)] -join ', ' - Write-DotbotWarning "Unexpected argument(s): $unexpected" + Write-DotbotError "Unexpected argument(s): $unexpected" + exit 1 } return $splat }