jax.ps1
|
[CmdletBinding()] param ( [Parameter(Position = 0)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $choices = @( 'help', 'init', 'env', 'list-envs', 'list-tasks', 'run', 'r', 'invoke', 'plan', 'autocomplete', 'create-flavour', 'settings', 'skill', 'info' ) foreach ($c in $choices) { if ([string]::IsNullOrWhiteSpace($wordToComplete) -or $c -like "$wordToComplete*") { [System.Management.Automation.CompletionResult]::new($c, $c, 'ParameterValue', $c) } } })] [string] $Command, [Parameter(ValueFromRemainingArguments = $true)] [string[]] $CommandArgs, # Compatibility core flags MUST be static so DynamicParam can resolve the plan. [Alias('e')] [string] $env, [string] $client, [Alias('s')] [string] $scenario, [Alias('f')] [string] $from, [Alias('t')] [string] $to, [Alias('o')] [string[]] $only, [Alias('nb')] [switch] $noBuild, [Alias('bo')] [switch] $buildChainOnly, [Alias('nc')] [switch] $noCache, # Shortcut flags # -sc <name> : expand a saved shortcut (injects its saved args into the invocation) # -ssc / -csc / -createShortCut <name> : save the current command as a shortcut (validate, save, exit) [Alias('sc')] [string] $Shortcut, [Alias('ssc', 'csc', 'createShortCut')] [string] $SaveShortcut, [Alias('rsc')] [string] $RemoveShortcut, # Plan detail level [switch] $Detailed, # Quiet mode: minimal output for token-optimized usage (RTK-friendly). # Persisted via 'jax settings set quiet on'. Override saved-on for one run with -NoQuiet/-nq. # -Verbose/-Debug always force full output. [Alias('q')] [switch] $Quiet, [Alias('nq', 'noq')] [switch] $NoQuiet, # Target repository. The Jax installation remains rooted at $PSScriptRoot. [Alias('C')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [string] $RepoRoot ) dynamicparam { # Compatibility DynamicParam: # - also expose task/script parameters from the resolved plan as native parameters try { # IMPORTANT: import core into the *global* session state so plugin modules (imported as separate modules) # can resolve core functions like Register-JaxPlugin/Register-JaxScenarioResolver during their module init. Import-Module (Join-Path $PSScriptRoot 'core/Jax.Core.psm1') -Global -Force -ErrorAction Stop } catch { return (New-Object System.Management.Automation.RuntimeDefinedParameterDictionary) } $resolved = Resolve-JaxCliCommand -Command $Command -CommandArgs $CommandArgs $resolvedCmd = $resolved.Command if ($resolvedCmd) { $resolvedCmd = $resolvedCmd.ToLowerInvariant() } $hasRunSelectors = -not [string]::IsNullOrWhiteSpace($env) -or -not [string]::IsNullOrWhiteSpace($client) -or -not [string]::IsNullOrWhiteSpace($scenario) -or -not [string]::IsNullOrWhiteSpace([string]$only) -or -not [string]::IsNullOrWhiteSpace($from) -or -not [string]::IsNullOrWhiteSpace($to) # IMPORTANT: DynamicParam executes before the begin{} fallback that defaults to 'run'. # If the user invokes jax without an explicit command (e.g. `./jax/jax.ps1 -env X -only Y`), # we must still expose dynamic params for run/plan scenarios. if ([string]::IsNullOrWhiteSpace($resolvedCmd)) { if ($hasRunSelectors) { $resolvedCmd = 'run' } } # Keep the historic implicit task form `jax info -env <env>` working. The # diagnostic command is intentionally the bare form, without run selectors. if ($resolvedCmd -eq 'info' -and $hasRunSelectors) { $resolvedCmd = 'run' } $supportsRunParams = $resolvedCmd -eq 'run' -or $resolvedCmd -eq 'plan' if (-not $supportsRunParams) { return (New-Object System.Management.Automation.RuntimeDefinedParameterDictionary) } function New-JaxRuntimeParam { param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [Type] $Type, [string[]] $Aliases, [string] $HelpMessage, [string[]] $ValidateSetValues ) $attrs = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $paramAttr = New-Object System.Management.Automation.ParameterAttribute $paramAttr.Mandatory = $false if (-not [string]::IsNullOrWhiteSpace($HelpMessage)) { $paramAttr.HelpMessage = $HelpMessage } $attrs.Add($paramAttr) | Out-Null if ($Aliases -and $Aliases.Count -gt 0) { $attrs.Add([System.Management.Automation.AliasAttribute]::new($Aliases)) | Out-Null } if ($ValidateSetValues -and $ValidateSetValues.Count -gt 0) { $attrs.Add([System.Management.Automation.ValidateSetAttribute]::new($ValidateSetValues)) | Out-Null } return [System.Management.Automation.RuntimeDefinedParameter]::new($Name, $Type, $attrs) } $dict = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary # ---- task/script params (resolved from plan) ---- try { $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters $repoRootParams = @{} if (-not [string]::IsNullOrWhiteSpace($RepoRoot)) { $repoRootParams.StartPath = $RepoRoot $repoRootParams.Exact = $true } $repoRoot = Get-JaxRepoRoot @repoRootParams $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null # Use $resolved.Args so positional env/task shorthand (e.g. `jax operations/wtw PublishWtw`) # is visible to task/script param discovery during DynamicParam tab completion. $parsed = Resolve-JaxCliArgs -Args $resolved.Args @commonParams $cliOptions = $parsed.Values # Static core flags win over legacy parsing if ($PSBoundParameters.ContainsKey('env')) { $cliOptions['env'] = $PSBoundParameters['env'] } if ($PSBoundParameters.ContainsKey('client')) { $cliOptions['client'] = $PSBoundParameters['client'] } if ($PSBoundParameters.ContainsKey('scenario')) { $cliOptions['scenario'] = $PSBoundParameters['scenario'] } if ($PSBoundParameters.ContainsKey('from')) { $cliOptions['from'] = $PSBoundParameters['from'] } if ($PSBoundParameters.ContainsKey('to')) { $cliOptions['to'] = $PSBoundParameters['to'] } if ($PSBoundParameters.ContainsKey('only')) { $cliOptions['only'] = $PSBoundParameters['only'] } if ($PSBoundParameters.ContainsKey('noBuild')) { $cliOptions['noBuild'] = [bool]$PSBoundParameters['noBuild'] } if ($PSBoundParameters.ContainsKey('buildChainOnly')) { $cliOptions['buildChainOnly'] = [bool]$PSBoundParameters['buildChainOnly'] } if ($PSBoundParameters.ContainsKey('noCache')) { $cliOptions['noCache'] = [bool]$PSBoundParameters['noCache'] } if ($PSBoundParameters.ContainsKey('Quiet')) { $cliOptions['quiet'] = [bool]$PSBoundParameters['Quiet'] } if ($PSBoundParameters.ContainsKey('NoQuiet')) { $cliOptions['noQuiet'] = [bool]$PSBoundParameters['NoQuiet'] } # ---- plugin/core CLI params (always) ---- # Expose registered CLI params (including plugin-defined flags) as real PowerShell parameters # so standard parameter-name completion works. $staticParamNames = @() try { $staticParamNames = @($MyInvocation.MyCommand.Parameters.Keys) } catch { $staticParamNames = @() } $staticLower = @($staticParamNames | ForEach-Object { $_.ToLowerInvariant() }) foreach ($def in @(Get-JaxCliParameters)) { if ($null -eq $def) { continue } $pName = [string]$def.Name if ([string]::IsNullOrWhiteSpace($pName)) { continue } $pLower = $pName.ToLowerInvariant() if ($reservedLower -contains $pLower) { continue } if ($staticLower -contains $pLower) { continue } if ($dict.ContainsKey($pName)) { continue } $pType = if ($def.Type -eq 'switch') { [switch] } else { [string] } $aliases = @() try { if ($def.Aliases) { $aliases = @($def.Aliases) } } catch { $aliases = @() } $dict.Add($pName, (New-JaxRuntimeParam -Name $pName -Type $pType -Aliases $aliases -HelpMessage '' -ValidateSetValues @())) } # Only compute dynamic task/script parameters when the user # narrowed execution with -only/-from/-to (otherwise tab completion becomes slow/noisy). # IMPORTANT: we check BOTH static params AND the legacy parsed args because in scripts # PowerShell may still leave some flags in $CommandArgs. $hasNarrowing = -not [string]::IsNullOrWhiteSpace([string]$cliOptions['only']) -or -not [string]::IsNullOrWhiteSpace([string]$cliOptions['from']) -or -not [string]::IsNullOrWhiteSpace([string]$cliOptions['to']) -or -not [string]::IsNullOrWhiteSpace([string]$only) -or -not [string]::IsNullOrWhiteSpace([string]$from) -or -not [string]::IsNullOrWhiteSpace([string]$to) if (-not $hasNarrowing) { return $dict } $envs = Get-JaxEnvironments -RepoRoot $repoRoot -Config $config @commonParams $selection = Resolve-JaxEnvSelection -Environments $envs -Env $cliOptions['env'] -Client $cliOptions['client'] @commonParams $selectedEnv = $selection.Environment if ($null -ne $selectedEnv) { $skipEnvRoot = $false if ($selectedEnv.PSObject.Properties.Match('SkipEnvRoot').Count -gt 0 -and $selectedEnv.SkipEnvRoot) { $skipEnvRoot = $true } if ($selectedEnv.PSObject.Properties.Match('IsDummy').Count -gt 0 -and $selectedEnv.IsDummy) { $skipEnvRoot = $true } $flowConfigEntry = Resolve-JaxSelectedFlowConfig -Environment $selectedEnv -PreferredConfig $selection.FlowOverride @commonParams if ($null -eq $flowConfigEntry -and -not $skipEnvRoot) { return $dict } $flowConfig = $null if ($null -ne $flowConfigEntry) { $flowConfig = Get-JaxFlowConfig -Paths $flowConfigEntry.ConfigPaths -ExpandVariables:$false @commonParams } $envDir = $null if ($null -ne $flowConfigEntry -and $flowConfigEntry.PSObject.Properties.Match('EnvDirPath').Count -gt 0) { $envDir = $flowConfigEntry.EnvDirPath } elseif ($selectedEnv.PSObject.Properties.Match('EnvDir').Count -gt 0) { $envDir = $selectedEnv.EnvDir } $context = @{ RepoRoot = $repoRoot EnvDir = $envDir Config = $config NoCache = $true SkipEnvRoot = $skipEnvRoot # DynamicParam evaluation must not create run logs / side effects. SuppressRunConfigLog = $true } # Used by runners to resolve relative paths (scripts/psakefile) against the env folder. if (-not [string]::IsNullOrWhiteSpace($envDir)) { $context['WorkingDir'] = $envDir } else { $context['WorkingDir'] = $repoRoot } if ($null -ne $flowConfigEntry -and $flowConfigEntry.PSObject.Properties.Match('EnvDirPath').Count -gt 0) { $defaultPsakeFile = Join-Path $flowConfigEntry.EnvDirPath 'psakefile.ps1' if (Test-Path -LiteralPath $defaultPsakeFile -PathType Leaf) { $context['PsakeFile'] = $defaultPsakeFile } } # Propagate Debug/Verbose if present in PSBoundParameters if ($PSBoundParameters.ContainsKey('Debug') -and $PSBoundParameters['Debug']) { $context['Debug'] = $true $DebugPreference = 'Continue' } if ($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose']) { $context['Verbose'] = $true $VerbosePreference = 'Continue' } $discoveredEntities = Get-JaxDiscoveredRunEntities -RepoRoot $repoRoot -EnvDir $envDir -Config $config -SkipEnvRoot:$skipEnvRoot @commonParams $context['DiscoveredEntities'] = $discoveredEntities $context['DiscoveredEntityIndex'] = New-JaxRunEntityIndex -Entities $discoveredEntities $scenarioEntities = @() $buildEntities = @() if ($null -ne $flowConfig) { $scenarioEntities = Get-JaxScenarioRunEntities -FlowConfig $flowConfig -Scenario $cliOptions['scenario'] -Context $context -ProvenancePath $flowConfigEntry.ConfigPath @commonParams $context['ScenarioEntityIndex'] = New-JaxRunEntityIndex -Entities $scenarioEntities $buildEntities = Get-JaxBuildRunEntities -FlowConfig $flowConfig -Config $config -Context $context -ProvenancePath $flowConfigEntry.ConfigPath @commonParams } $plan = Resolve-JaxRunPlan -BuildEntities $buildEntities -ScenarioEntities $scenarioEntities -IndividualEntities $discoveredEntities -NoBuild:([bool]$cliOptions['noBuild']) -BuildChainOnly:([bool]$cliOptions['buildChainOnly']) -Only $cliOptions['only'] -From $cliOptions['from'] -To $cliOptions['to'] -Context $context @commonParams $reserved = @('Command', 'CommandArgs', 'env', 'client', 'scenario', 'from', 'to', 'only', 'noBuild', 'buildChainOnly', 'noCache', 'Detailed', 'Quiet', 'NoQuiet', 'RepoRoot') $reservedLower = @($reserved | ForEach-Object { $_.ToLowerInvariant() }) foreach ($entity in @($plan)) { if ($null -eq $entity -or $entity -isnot [System.Collections.IDictionary]) { continue } # psake task params from Definition.Parameters (properties{} parsing) if ($entity.Contains('Definition') -and $entity['Definition'] -is [System.Collections.IDictionary]) { $def = $entity['Definition'] if ($def.Contains('Parameters') -and $def['Parameters'] -is [System.Collections.IEnumerable]) { foreach ($p in @($def['Parameters'])) { if ($p -isnot [System.Collections.IDictionary]) { continue } $pName = [string]$p['Name'] if ([string]::IsNullOrWhiteSpace($pName)) { continue } if ($reservedLower -contains $pName.ToLowerInvariant()) { continue } if ($dict.ContainsKey($pName)) { continue } $typeName = [string]$p['Type'] $paramType = [object] if (-not [string]::IsNullOrWhiteSpace($typeName)) { try { $paramType = $typeName -as [type] } catch { $paramType = [object] } if ($null -eq $paramType) { $paramType = [object] } } $help = [string]$p['Description'] $dict.Add($pName, (New-JaxRuntimeParam -Name $pName -Type $paramType -Aliases @() -HelpMessage $help -ValidateSetValues @())) } } } # script params via Get-Command metadata (ps1 only) if ($entity.Contains('Script') -and $entity['Script'] -is [string] -and -not [string]::IsNullOrWhiteSpace($entity['Script'])) { $scriptPath = Resolve-JaxRepoRootedPath -Path $entity['Script'] -RepoRoot $repoRoot -WorkingDir $envDir @commonParams if (Test-Path -Path $scriptPath -PathType Leaf) { $cmdInfo = Get-Command -Name $scriptPath -ErrorAction SilentlyContinue if ($cmdInfo -and $cmdInfo.Parameters) { foreach ($kv in $cmdInfo.Parameters.GetEnumerator()) { $pName = [string]$kv.Key if ([string]::IsNullOrWhiteSpace($pName)) { continue } if ($reservedLower -contains $pName.ToLowerInvariant()) { continue } if ($dict.ContainsKey($pName)) { continue } $meta = $kv.Value $pType = [object] try { $pType = $meta.ParameterType } catch { $pType = [object] } $dict.Add($pName, (New-JaxRuntimeParam -Name $pName -Type $pType -Aliases @() -HelpMessage '' -ValidateSetValues @())) } } } } } } } catch { # Best-effort only (tab completion should never throw). } return $dict } begin { $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters $coreModulePath = Join-Path $PSScriptRoot 'core/Jax.Core.psm1' try { Import-Module $coreModulePath -Global -Force -ErrorAction Stop } catch { throw "Failed to import Jax core module '$coreModulePath': $($_.Exception.Message)" } $repoRootParams = @{} if (-not [string]::IsNullOrWhiteSpace($RepoRoot)) { $repoRootParams.StartPath = $RepoRoot $repoRootParams.Exact = $true } $jaxRepoRoot = Get-JaxRepoRoot @repoRootParams # ---- Shortcut: expand -Shortcut/-sc before command resolution ---- if (-not [string]::IsNullOrWhiteSpace($Shortcut)) { try { $expanded = Expand-JaxCliShortcut -Name $Shortcut -CommonParams $commonParams -Command $Command -CommandArgs $CommandArgs -RepoRoot $jaxRepoRoot $Command = $expanded.Command $CommandArgs = $expanded.CommandArgs } catch { throw "Failed to expand shortcut '$Shortcut': $_" } } $hasRunSelectors = -not [string]::IsNullOrWhiteSpace($env) -or -not [string]::IsNullOrWhiteSpace($client) -or -not [string]::IsNullOrWhiteSpace($scenario) -or -not [string]::IsNullOrWhiteSpace([string]$only) -or -not [string]::IsNullOrWhiteSpace($from) -or -not [string]::IsNullOrWhiteSpace($to) if ($Command -eq 'info' -and $hasRunSelectors) { $Command = 'run' $CommandArgs = @('info') + @($CommandArgs) } $resolvedCommand = Resolve-JaxCliCommand -Command $Command -CommandArgs $CommandArgs # ---- Shortcut: save -SaveShortcut/-ssc (validate-then-save, no execution) ---- if (-not [string]::IsNullOrWhiteSpace($SaveShortcut)) { try { $argsToSave = Get-JaxShortcutArgsToSave -Command $Command -env $env -client $client -scenario $scenario ` -only $only -from $from -to $to -noBuild:$noBuild -buildChainOnly:$buildChainOnly -noCache:$noCache ` -CommandArgs $CommandArgs Save-JaxShortcutToConfig -Name $SaveShortcut -CommonParams $commonParams -ArgsToSave $argsToSave -RepoRoot $jaxRepoRoot } catch { throw "Failed to save shortcut '$SaveShortcut': $_" } return } # ---- Shortcut: remove -RemoveShortcut/-rsc (no execution) ---- if (-not [string]::IsNullOrWhiteSpace($RemoveShortcut)) { try { Remove-JaxShortcutFromConfig -Name $RemoveShortcut -CommonParams $commonParams -RepoRoot $jaxRepoRoot } catch { throw "Failed to remove shortcut '$RemoveShortcut': $_" } return } # Handle implicit 'run' or default to 'list-envs' if ([string]::IsNullOrWhiteSpace($resolvedCommand.Command)) { if ($hasRunSelectors) { $resolvedCommand.Command = 'run' } else { $resolvedCommand.Command = 'list-envs' } } $Command = $resolvedCommand.Command $CommandArgs = $resolvedCommand.Args if ($Command -eq 'plan') { $CommandArgs = @('-plan') + @($CommandArgs) $Command = 'run' } function Show-JaxHelp { try { $repoRoot = $jaxRepoRoot $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null } catch { } Write-Host "Jax CLI" Write-Host "Usage: jax [-RepoRoot <path> | -C <path>] <command> [options]" Write-Host "Commands:" Write-Host " init Initialize repo scaffold" # Write-Host " init-compat Generate a compatibility config" # Write-Host " init-legacy Alias for init-compat" Write-Host " env init Initialize environment scaffold" Write-Host " list-envs List environments discovered under env/" Write-Host " run (r, invoke) Run flows and scenarios (explicit)" Write-Host " plan Print the run plan without executing" Write-Host " autocomplete Register jax tab completion for the current session" Write-Host " info Show installed runtime, repository, and configuration diagnostics" Write-Host " skill Install the bundled AI-agent skill into the target repository" Write-Host " create-flavour Create a flavour file under configs/jax-flavours" Write-Host " settings Manage persisted settings (.jax/state.yml)" Write-Host "" Write-Host "Output flags:" Write-Host " -q / -Quiet Minimal output (one-line banners, no framed boxes). Persist with 'jax settings set quiet on'." Write-Host " -nq / -NoQuiet Force full output for one run (overrides saved quiet)." Write-Host " -Verbose Always shows full output regardless of quiet setting." Write-Host " -o / -only A,B Run one selector, or comma/array selectors as an ad-hoc scenario." Write-Host " -C / -RepoRoot Run against an explicit repository without changing directory." Write-Host "" Write-Host "Shortcut flags:" Write-Host " -sc <name> Run a saved shortcut (does not update saved env in .jax/state.yml)" Write-Host " -ssc / -csc / -createShortCut Save shortcut to .jax/jax.shortcut.yml, exit (no run; state unchanged; jax.config.yml untouched)" Write-Host " -rsc <name> Remove a saved shortcut in .jax/jax.shortcut.yml (no run; jax.config.yml untouched)" $entries = Get-JaxHelpEntries if ($entries.Count -gt 0) { Write-Host "" Write-Host "Plugin commands:" foreach ($entry in $entries) { Write-Host (" {0} {1}" -f $entry.Name, $entry.Summary) } } } function Show-JaxSettingsHelp { Write-Host "Usage: jax settings <subcommand>" Write-Host "" Write-Host " show Print current persisted settings (.jax/state.yml)." Write-Host " reset Remove .jax/state.yml for the current repo." Write-Host " set <key> [<value>] Persist a setting (e.g. 'set quiet on', 'set quiet off')." Write-Host "" Write-Host "Known keys (core scope):" Write-Host " quiet on|off|true|false Minimal CLI output (RTK-friendly)." Write-Host " verbose on|off Default verbose preference." Write-Host " docker on|off Force container runner by default." } function Show-JaxInfo { $info = Get-JaxDiagnosticInfo -RuntimeRoot $PSScriptRoot -RepoRoot $jaxRepoRoot Write-Host 'Jax diagnostics' foreach ($entry in $info.Display.GetEnumerator()) { Write-Host (' {0}: {1}' -f $entry.Key, $entry.Value) } if ($info.Warnings.Count -gt 0) { Write-Host '' Write-Host 'Warnings:' -ForegroundColor Yellow foreach ($warning in $info.Warnings) { Write-Host " - $warning" -ForegroundColor Yellow } } } function ConvertTo-JaxBoolValue { param([string] $Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $true } switch ($Value.ToLowerInvariant()) { { $_ -in @('on', 'true', '1', 'yes', 'y', 'enable', 'enabled') } { return $true } { $_ -in @('off', 'false', '0', 'no', 'n', 'disable', 'disabled') } { return $false } default { throw "Invalid boolean value '$Value'. Expected on|off|true|false." } } } if ($Command -eq 'vault') { # Plugin command dispatch # jax vault set -> Connect-JaxVault # jax vault status -> Get-JaxVaultStatus # jax vault reset -> Reset-JaxVault $subCmd = if ($CommandArgs.Count -gt 0) { $CommandArgs[0].ToLowerInvariant() } else { 'status' } $subArgs = if ($CommandArgs.Count -gt 1) { $CommandArgs[1..($CommandArgs.Count - 1)] } else { @() } # Defensive: avoid passing empty positional args into plugin commands (can bind to VaultToken etc.) $subArgs = @($subArgs | Where-Object { $_ -ne $null -and -not [string]::IsNullOrWhiteSpace([string]$_) }) try { # Load core config $repoRoot = $jaxRepoRoot $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams # Try standard plugin load in case user configured it Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null # Force load Vault plugin if not loaded (it might be disabled in config but we need it for these commands) $vaultPluginPath = Get-JaxBuiltinPluginPath -Name 'vault' if (-not (Test-Path $vaultPluginPath)) { throw "Vault plugin not found at '$vaultPluginPath'" } Import-Module $vaultPluginPath -Global -Force -ErrorAction Stop switch ($subCmd) { 'set' { Connect-JaxVault @commonParams @subArgs } 'login' { Connect-JaxVault @commonParams @subArgs } 'info' { Get-JaxVaultStatus @commonParams } 'status' { Get-JaxVaultStatus @commonParams } 'reset' { Reset-JaxVault @commonParams } 'help' { Write-Host "Usage: jax vault [command]" Write-Host "Commands:" Write-Host " set (or login) Authenticate with Vault (interactive)" Write-Host " status (or info) Check Vault connection status" Write-Host " reset Clear local Vault token" Write-Host "" Write-Host "Run overrides:" Write-Host " -vaultTokenEnv <name> Use token from the named env var for one run" Write-Host " -useEnvVaultToken Use VAULT_TOKEN for one run" Write-Host " -vaultToken <token> Use literal token for one run (shell history risk)" } default { Write-Error "Unknown vault command '$subCmd'. Try 'jax vault help'." } } } catch { Write-Error "Failed to execute vault command: $_" } return } switch ($Command) { 'help' { Show-JaxHelp } '-h' { Show-JaxHelp } '--help' { Show-JaxHelp } 'init' { $params = @{} $params.RepoRoot = $jaxRepoRoot if ($PSBoundParameters.ContainsKey('client')) { $params.Client = $client } if ($PSBoundParameters.ContainsKey('env')) { $params.Env = $env } if ($CommandArgs) { for ($i = 0; $i -lt $CommandArgs.Count; $i++) { $arg = [string]$CommandArgs[$i] if ($arg -match '^-(FlowConfig|BossConfig)=(.+)$') { $params.FlowConfig = $Matches[2] } if ($arg -in @('-FlowConfig', '-BossConfig') -and $i + 1 -lt $CommandArgs.Count) { $i++ $params.FlowConfig = [string]$CommandArgs[$i] } if ($arg -eq '-IncludeBobfile') { $params.IncludeBobfile = $true } if ($arg -eq '-IncludeJaxfile') { $params.IncludeJaxfile = $true } if ($arg -eq '-Customize') { $params.Customize = $true } } } Invoke-JaxInit @params @commonParams } 'info' { Show-JaxInfo } 'skill' { $installer = Join-Path $PSScriptRoot 'Install-JaxSkill.ps1' & $installer -RepoRoot $jaxRepoRoot } # 'init-compat' { # $params = @{} # if ($CommandArgs) { # foreach ($arg in $CommandArgs) { # if ($arg -eq '-Force') { $params.Force = $true } # } # } # $path = Invoke-JaxInitCompatConfig @params # Write-Host "Compatibility config created: $path" # } # 'init-legacy' { # $params = @{} # if ($CommandArgs) { # foreach ($arg in $CommandArgs) { # if ($arg -eq '-Force') { $params.Force = $true } # } # } # $path = Invoke-JaxInitCompatConfig @params # Write-Host "Compatibility config created: $path" # } 'list-tasks' { $repoRoot = $jaxRepoRoot $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null $envs = Get-JaxEnvironments -RepoRoot $repoRoot -Config $config @commonParams # Default to current environment if not specified via -Env (though list-tasks typically needs env context) $parsed = Resolve-JaxCliArgs -Args $CommandArgs @commonParams $options = $parsed.Values if ($PSBoundParameters.ContainsKey('env')) { $options['env'] = $PSBoundParameters['env'] } if ($PSBoundParameters.ContainsKey('client')) { $options['client'] = $PSBoundParameters['client'] } $selection = Resolve-JaxEnvSelection -Environments $envs -Env $options['env'] -Client $options['client'] @commonParams $selectedEnv = $selection.Environment if ($null -eq $selectedEnv) { Write-Host "No environment selected. Use -Env or set a default environment to list tasks." return } $skipEnvRoot = $false if ($selectedEnv.PSObject.Properties.Match('SkipEnvRoot').Count -gt 0 -and $selectedEnv.SkipEnvRoot) { $skipEnvRoot = $true } if ($selectedEnv.PSObject.Properties.Match('IsDummy').Count -gt 0 -and $selectedEnv.IsDummy) { $skipEnvRoot = $true } $isComputedEnv = $false if ($selectedEnv.PSObject.Properties.Match('IsComputed').Count -gt 0 -and $selectedEnv.IsComputed) { $isComputedEnv = $true } $flowOverride = $selection.FlowOverride if ([string]::IsNullOrWhiteSpace($flowOverride)) { $flowConfigEntry = Resolve-JaxSelectedFlowConfig -Environment $selectedEnv @commonParams } else { $flowConfigEntry = Resolve-JaxSelectedFlowConfig -Environment $selectedEnv -PreferredConfig $flowOverride @commonParams } $allowMissingFlowConfig = $skipEnvRoot -or $isComputedEnv if ($null -eq $flowConfigEntry -and -not $allowMissingFlowConfig) { Write-Host "No flow configs found for environment '$($selectedEnv.Name)'." return } $flowConfig = $null if ($null -ne $flowConfigEntry) { $flowConfig = Get-JaxFlowConfig -Paths $flowConfigEntry.ConfigPaths -ExpandVariables:$false @commonParams } $envDir = $null if ($null -ne $flowConfigEntry -and $flowConfigEntry.PSObject.Properties.Match('EnvDirPath').Count -gt 0) { $envDir = $flowConfigEntry.EnvDirPath } elseif ($selectedEnv.PSObject.Properties.Match('EnvDir').Count -gt 0) { $envDir = $selectedEnv.EnvDir } $context = @{ RepoRoot = $repoRoot EnvDir = $envDir Config = $config SuppressRunConfigLog = $true SkipEnvRoot = $skipEnvRoot } $discoveredEntities = Get-JaxDiscoveredRunEntities -RepoRoot $repoRoot -EnvDir $envDir -Config $config -SkipEnvRoot:$skipEnvRoot @commonParams $context['DiscoveredEntities'] = $discoveredEntities $context['DiscoveredEntityIndex'] = New-JaxRunEntityIndex -Entities $discoveredEntities $entities = @() # Include default scenario tasks if ($null -ne $flowConfig) { $entities += Get-JaxScenarioRunEntities -FlowConfig $flowConfig -Scenario 'default' -Context $context -ProvenancePath $flowConfigEntry.ConfigPath -ErrorAction SilentlyContinue @commonParams } # Add discovered if ($discoveredEntities.Count -gt 0) { $entities += $discoveredEntities } $keys = Get-JaxRunEntityKeys -Entities $entities -IncludeAliases if ($keys.Count -eq 0) { Write-Host "No tasks found." return } $keys | Sort-Object | Format-Wide -AutoSize } 'list-envs' { $repoRoot = $jaxRepoRoot $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams $envs = Get-JaxEnvironments -RepoRoot $repoRoot -Config $config @commonParams if ($envs.Count -eq 0) { Write-Host "No environments found." return } $envs = @($envs | Sort-Object -Property Name -Unique) foreach ($environment in $envs) { $clientKey = ($environment.Name -split '/')[0] $clientIcon = Get-JaxAutocompleteIcon -Config $config -Type 'client' -Key $clientKey $configs = @() foreach ($cfg in @($environment.FlowConfigs)) { if ($null -ne $cfg -and $cfg.PSObject.Properties.Match('Configuration').Count -gt 0) { if (-not [string]::IsNullOrWhiteSpace([string]$cfg.Configuration)) { $configs += [string]$cfg.Configuration } } } $configs = @($configs | Sort-Object -Unique) $configsText = if ($configs.Count -gt 0) { $configs -join ',' } else { 'none' } $flowIcon = '⚙️' if ($configs.Count -gt 0) { $flowIcon = Get-JaxAutocompleteIcon -Config $config -Type 'flow' -Key $configs[0] } else { $fallbackKey = ($environment.Name -split '/')[-1] $flowIcon = Get-JaxAutocompleteIcon -Config $config -Type 'flow' -Key $fallbackKey -DefaultIcon '⚙️' } if ($environment.Name -match '^build/') { Write-Host ("{0} {1} {2} [{3}]" -f $clientIcon, $flowIcon, $environment.Name, $configsText) } else { Write-Host ("{0} {1} {2} [{3}]" -f $flowIcon, $clientIcon, $environment.Name, $configsText) } } } 'env' { # Dispatch subcommands for env management # Usage: jax env init $subCmd = if ($CommandArgs.Count -gt 0) { $CommandArgs[0] } else { 'help' } $subArgs = if ($CommandArgs.Count -gt 1) { $CommandArgs[1..($CommandArgs.Count - 1)] } else { @() } switch ($subCmd) { 'init' { # jax env init -> New-JaxEnvironment $envParams = @{ RepoRoot = $jaxRepoRoot } $positionNames = @('Name', 'Client', 'DefaultFlow', 'Template') $positionIndex = 0 for ($i = 0; $i -lt $subArgs.Count; $i++) { $arg = [string]$subArgs[$i] if ($arg -match '^-?(Name|Client|DefaultFlow|Template)=(.*)$') { $envParams[$Matches[1]] = $Matches[2] continue } if ($arg -match '^-(Name|Client|DefaultFlow|Template)$') { if ($i + 1 -ge $subArgs.Count) { throw "Missing value for environment option '$arg'." } $i++ $envParams[$Matches[1]] = [string]$subArgs[$i] continue } if ($positionIndex -ge $positionNames.Count) { throw "Unexpected environment argument: $arg" } $envParams[$positionNames[$positionIndex]] = $arg $positionIndex++ } New-JaxEnvironment @envParams @commonParams } default { Write-Host "Usage: jax env [command]" Write-Host "Commands:" Write-Host " init Initialize a new environment (interactive)" } } } 'create-flavour' { $params = @{ RepoRoot = $jaxRepoRoot } if ($CommandArgs) { foreach ($arg in $CommandArgs) { if ($arg -like '-Name=*') { $params.Name = $arg.Split('=')[1] } if ($arg -like '-Description=*') { $params.Description = $arg.Split('=')[1] } if ($arg -eq '-Force') { $params.Force = $true } } } $path = Invoke-JaxCreateFlavour @params @commonParams Write-Host "Created flavour file: $path" } 'autocomplete' { $completionModule = Join-Path $PSScriptRoot 'Jax.Autocomplete.psm1' if (-not (Test-Path -Path $completionModule -PathType Leaf)) { Write-Host "Autocomplete module not found: $completionModule" return } Import-Module $completionModule -Force Write-Host "Jax autocomplete registered for this session." } 'run' { $repoRoot = $jaxRepoRoot $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null $parsed = Resolve-JaxCliArgs -Args $CommandArgs @commonParams $options = $parsed.Values # Merge static core flags into options (static wins over CommandArgs parsing) if ($PSBoundParameters.ContainsKey('env')) { $options['env'] = $PSBoundParameters['env'] } if ($PSBoundParameters.ContainsKey('client')) { $options['client'] = $PSBoundParameters['client'] } if ($PSBoundParameters.ContainsKey('scenario')) { $options['scenario'] = $PSBoundParameters['scenario'] } if ($PSBoundParameters.ContainsKey('from')) { $options['from'] = $PSBoundParameters['from'] } if ($PSBoundParameters.ContainsKey('to')) { $options['to'] = $PSBoundParameters['to'] } if ($PSBoundParameters.ContainsKey('only')) { $options['only'] = $PSBoundParameters['only'] } if ($PSBoundParameters.ContainsKey('noBuild')) { $options['noBuild'] = [bool]$PSBoundParameters['noBuild'] } if ($PSBoundParameters.ContainsKey('buildChainOnly')) { $options['buildChainOnly'] = [bool]$PSBoundParameters['buildChainOnly'] } if ($PSBoundParameters.ContainsKey('noCache')) { $options['noCache'] = [bool]$PSBoundParameters['noCache'] } if ($PSBoundParameters.ContainsKey('Quiet')) { $options['quiet'] = [bool]$PSBoundParameters['Quiet'] } if ($PSBoundParameters.ContainsKey('NoQuiet')) { $options['noQuiet'] = [bool]$PSBoundParameters['NoQuiet'] } # Common parameters (-Verbose/-Debug) are bound by PowerShell and do NOT appear in CommandArgs parsing, # but we still want them to influence Jax's internal preference/diagnostic behavior. if ($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters['Verbose']) { $options['verbose'] = $true } if ($PSBoundParameters.ContainsKey('Debug') -and $PSBoundParameters['Debug']) { $options['debug'] = $true } # IMPORTANT: # DynamicParam now exposes plugin-defined CLI flags as native PowerShell parameters. # That means flags like -novault/-onlyLocalArch may NOT appear in $CommandArgs anymore, # so Resolve-JaxCliArgs won't see them. Merge known CLI params from PSBoundParameters. try { $cliDefs = @(Get-JaxCliParameters) $cliSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($d in $cliDefs) { if ($null -eq $d) { continue } if ($d.Name) { $cliSet.Add([string]$d.Name) | Out-Null } } foreach ($k in @($PSBoundParameters.Keys)) { if ($cliSet.Contains([string]$k)) { $def = $cliDefs | Where-Object { $_.Name -ieq $k } | Select-Object -First 1 if ($def -and $def.Type -eq 'switch') { $options[[string]$k] = [bool]$PSBoundParameters[$k] } else { $options[[string]$k] = $PSBoundParameters[$k] } } } } catch { # best effort } if ($parsed.Remaining.Count -gt 0 -and [string]::IsNullOrWhiteSpace($options['only'])) { $options['only'] = $parsed.Remaining[0] } # Also capture any unknown flags (including dynamic/extra args) so they can still override defaults. # CLI `-myData X` should override `args.myData` even if DynamicParam # couldn't materialize `-myData` as a bound parameter. $unknownCliArgs = Convert-JaxCliUnknownToHashtable -Unknown $parsed.Unknown if ($options.ContainsKey('forceReloadModules') -and $options.forceReloadModules) { Write-Verbose "--------------------------------" Write-Verbose "ForceReloadModules: Force reloading Jax modules" Write-Verbose "--------------------------------" Write-Verbose "Importing Jax core module" try { Import-Module $coreModulePath -Global -Force -ErrorAction Stop } catch { throw "Failed to re-import Jax core module '$coreModulePath' (force reload): $($_.Exception.Message)" } Write-Verbose "ForceReloadModules: Reloading Jax modules" Invoke-JaxModulesReload -RepoRoot $repoRoot -ExcludeModuleNames @('Jax.Core.psm1') -Config $config @commonParams # Reload can unload other Jax modules/plugins; re-import core to ensure exported functions are available. try { Import-Module $coreModulePath -Global -Force -ErrorAction Stop } catch { throw "Failed to import Jax core module '$coreModulePath' after module reload: $($_.Exception.Message)" } Write-Verbose "ForceReloadModules: Importing Jax plugins" Import-JaxPlugins -Config $config -RepoRoot $repoRoot @commonParams | Out-Null } if ($options.ContainsKey('dv') -and $options.dv) { $DebugPreference = 'Continue' $VerbosePreference = 'Continue' } else { if ($options.ContainsKey('debug') -and $options.debug) { $DebugPreference = 'Continue' } if ($options.ContainsKey('verbose') -and $options.verbose) { $VerbosePreference = 'Continue' } } if ($options.ContainsKey('clearOutput') -and $options.clearOutput) { Clear-Host } $noSavedSettings = $options.ContainsKey('noSavedSettings') -and $options.noSavedSettings # Shortcuts are self-contained (-env/-only in the saved args); do not persist env/client/scenario # to .jax/state.yml or we overwrite the user's "last explicit" context for the next normal run. if (-not [string]::IsNullOrWhiteSpace($Shortcut)) { $noSavedSettings = $true } $state = Get-JaxState -RepoRoot $repoRoot -NoSavedSettings:$noSavedSettings @commonParams $stateCore = @{} if ($state -is [System.Collections.IDictionary] -and $state.Contains('core') -and $state['core'] -is [System.Collections.IDictionary]) { $stateCore = $state['core'] } $resolved = Merge-JaxHashtable -Base $stateCore -Overlay $options # Ensure explicitly passed static core flags always win (even if state/options merging gets weird). if ($PSBoundParameters.ContainsKey('env')) { $resolved['env'] = $PSBoundParameters['env'] } if ($PSBoundParameters.ContainsKey('client')) { $resolved['client'] = $PSBoundParameters['client'] } if ($PSBoundParameters.ContainsKey('scenario')) { $resolved['scenario'] = $PSBoundParameters['scenario'] } if ($PSBoundParameters.ContainsKey('from')) { $resolved['from'] = $PSBoundParameters['from'] } if ($PSBoundParameters.ContainsKey('to')) { $resolved['to'] = $PSBoundParameters['to'] } if ($PSBoundParameters.ContainsKey('only')) { $resolved['only'] = $PSBoundParameters['only'] } if ($PSBoundParameters.ContainsKey('noBuild')) { $resolved['noBuild'] = [bool]$PSBoundParameters['noBuild'] } if ($PSBoundParameters.ContainsKey('buildChainOnly')) { $resolved['buildChainOnly'] = [bool]$PSBoundParameters['buildChainOnly'] } if ($PSBoundParameters.ContainsKey('noCache')) { $resolved['noCache'] = [bool]$PSBoundParameters['noCache'] } if ($PSBoundParameters.ContainsKey('Quiet')) { $resolved['quiet'] = [bool]$PSBoundParameters['Quiet'] } if ($PSBoundParameters.ContainsKey('NoQuiet')) { $resolved['noQuiet'] = [bool]$PSBoundParameters['NoQuiet'] } # Quiet mode is computed once and propagated through the run context so that all # output sites (run header, entity banner, plan log breadcrumb) gate uniformly. $quietMode = Test-JaxQuietMode -Resolved $resolved # Cross-process signal: child psake/scripts and devops/ps wrappers (Test-JaxQuietContext) # honor $env:JAX_QUIET. Scoped to the current jax.ps1 process — does not leak between runs. if ($quietMode) { $env:JAX_QUIET = '1' } else { Remove-Item Env:JAX_QUIET -ErrorAction SilentlyContinue } # NOTE: we intentionally do NOT persist from/to/only in state (see Update-JaxState). $envs = Get-JaxEnvironments -RepoRoot $repoRoot -Config $config @commonParams if ($envs.Count -eq 0) { Write-Host "No environments found." return } $selection = Resolve-JaxEnvSelection -Environments $envs -Env $resolved['env'] -Client $resolved['client'] @commonParams $selectedEnv = $selection.Environment $flowOverride = $selection.FlowOverride if ($resolved.ContainsKey('printCurrentEnv') -and [bool]$resolved.printCurrentEnv) { Write-Host $selectedEnv.Name return } $skipEnvRoot = $false if ($selectedEnv.PSObject.Properties.Match('SkipEnvRoot').Count -gt 0 -and $selectedEnv.SkipEnvRoot) { $skipEnvRoot = $true } if ($selectedEnv.PSObject.Properties.Match('IsDummy').Count -gt 0 -and $selectedEnv.IsDummy) { $skipEnvRoot = $true } $isComputedEnv = $false if ($selectedEnv.PSObject.Properties.Match('IsComputed').Count -gt 0 -and $selectedEnv.IsComputed) { $isComputedEnv = $true } if ([string]::IsNullOrWhiteSpace($flowOverride)) { $flowConfigEntry = Resolve-JaxSelectedFlowConfig -Environment $selectedEnv @commonParams } else { $flowConfigEntry = Resolve-JaxSelectedFlowConfig -Environment $selectedEnv -PreferredConfig $flowOverride @commonParams } $allowMissingFlowConfig = $skipEnvRoot -or $isComputedEnv if ($null -eq $flowConfigEntry -and -not $allowMissingFlowConfig) { Write-Host "No flow configs found for environment '$($selectedEnv.Name)'." return } $flowConfig = $null if ($null -ne $flowConfigEntry) { $flowConfig = Get-JaxFlowConfig -Paths $flowConfigEntry.ConfigPaths -ExpandVariables:$false @commonParams } $envDir = $null if ($null -ne $flowConfigEntry -and $flowConfigEntry.PSObject.Properties.Match('EnvDirPath').Count -gt 0) { $envDir = $flowConfigEntry.EnvDirPath } elseif ($selectedEnv.PSObject.Properties.Match('EnvDir').Count -gt 0) { $envDir = $selectedEnv.EnvDir } $context = @{ RepoRoot = $repoRoot EnvDir = $envDir Config = $config SkipEnvRoot = $skipEnvRoot Quiet = $quietMode } if ($quietMode) { # Honored by Invoke-JaxRunEntity to skip the framed entity banner. $context['SuppressEntityBanner'] = $true } $context['CommonParameters'] = $commonParams # Used by runners to resolve relative paths (scripts/psakefile) against the env folder. if (-not [string]::IsNullOrWhiteSpace($envDir)) { $context['WorkingDir'] = $envDir } else { $context['WorkingDir'] = $repoRoot } if ($null -ne $flowConfigEntry -and $flowConfigEntry.PSObject.Properties.Match('EnvDirPath').Count -gt 0) { $defaultPsakeFile = Join-Path $flowConfigEntry.EnvDirPath 'psakefile.ps1' if (Test-Path -LiteralPath $defaultPsakeFile -PathType Leaf) { $context['PsakeFile'] = $defaultPsakeFile } } if ($options.ContainsKey('debug') -and $options.debug) { $context['Debug'] = $true } if ($options.ContainsKey('verbose') -and $options.verbose) { $context['Verbose'] = $true } # Make state/options available to plugins (so they can implement persisted preferences without jax.ps1 hardcoding). $context['State'] = $state $context['NoSavedSettings'] = $noSavedSettings $context['SelectedEnv'] = $selectedEnv $context['FlowConfigEntry'] = $flowConfigEntry $context['ResolvedOptions'] = $resolved # Allow plugins to adjust header state (and update persisted settings) before we render the header. Invoke-JaxHooks -Name 'BeforeRunHeader' -Context $context -Data @{ SelectedEnv = $selectedEnv FlowConfigEntry = $flowConfigEntry Resolved = $resolved } @commonParams $headerFlowConfig = if ($null -ne $flowConfigEntry) { $flowConfigEntry } else { [pscustomobject]@{} } if (-not $quietMode) { Write-JaxRunHeader -RepoRoot $repoRoot -SelectedEnv $selectedEnv -FlowConfigEntry $headerFlowConfig -Config $config -Resolved $resolved } else { $envName = if ($selectedEnv -and $selectedEnv.PSObject.Properties.Match('Name').Count -gt 0) { [string]$selectedEnv.Name } else { '' } if (-not [string]::IsNullOrWhiteSpace($envName)) { Write-Host ("jax: env={0}" -f $envName) } } Invoke-JaxHooks -Name 'RunHeader' -Context $context -Data @{ SelectedEnv = $selectedEnv FlowConfigEntry = $flowConfigEntry Resolved = $resolved } @commonParams # Forward any additional DynamicParam-bound task/script parameters into the run context. # Runners will merge these into their args/properties (CLI overrides win). $reservedKeys = @( 'Command', 'CommandArgs', 'env', 'client', 'scenario', 'from', 'to', 'only', 'noBuild', 'buildChainOnly', 'noCache', 'Detailed', 'Shortcut', 'SaveShortcut', 'RemoveShortcut', 'vault', 'novault', 'vaultToken', 'vaultTokenEnv', 'useEnvVaultToken', 'coverage', 'nocoverage' ) $reservedLower = @($reservedKeys | ForEach-Object { $_.ToLowerInvariant() }) $commonKeys = @((Get-JaxCommonParameters -BoundParameters $PSBoundParameters).Keys | ForEach-Object { $_.ToLowerInvariant() }) $cliArgs = @{} foreach ($k in @($PSBoundParameters.Keys)) { if ([string]::IsNullOrWhiteSpace($k)) { continue } $lk = $k.ToLowerInvariant() if ($reservedLower -contains $lk) { continue } if ($commonKeys -contains $lk) { continue } $cliArgs[$k] = $PSBoundParameters[$k] } # Unknown CLI args (from CommandArgs parsing) are lower precedence than actual bound parameters. if ($null -ne $unknownCliArgs -and $unknownCliArgs.Count -gt 0) { $cliKnown = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($def in Get-JaxCliParameters) { if ($def -and $def.Name) { $cliKnown.Add([string]$def.Name) | Out-Null } foreach ($a in @($def.Aliases)) { if (-not [string]::IsNullOrWhiteSpace([string]$a)) { $cliKnown.Add([string]$a) | Out-Null } } } foreach ($k in @($unknownCliArgs.Keys)) { if ([string]::IsNullOrWhiteSpace([string]$k)) { continue } $lk = ([string]$k).ToLowerInvariant() if ($reservedLower -contains $lk) { continue } if ($commonKeys -contains $lk) { continue } if ($cliKnown.Contains([string]$k)) { continue } if (-not $cliArgs.ContainsKey([string]$k)) { $cliArgs[[string]$k] = $unknownCliArgs[$k] } } } if ($cliArgs.Count -gt 0) { $context['CliArgs'] = $cliArgs } if ($resolved.ContainsKey('noCache') -and $resolved.noCache) { $context['NoCache'] = $true } $overrideList = @() if ($resolved.ContainsKey('override')) { $overrideList += Convert-JaxCliListValue -Value $resolved.override } if ($resolved.ContainsKey('overrides')) { $overrideList += Convert-JaxCliListValue -Value $resolved.overrides } if ($overrideList.Count -gt 0) { $context['Overrides'] = @($overrideList | Sort-Object -Unique) } $noTasksDiscovery = $resolved.ContainsKey('noTasksDiscovery') -and [bool]$resolved['noTasksDiscovery'] $noScriptsDiscovery = $resolved.ContainsKey('noScriptsDiscovery') -and [bool]$resolved['noScriptsDiscovery'] $noScenarioDiscovery = $resolved.ContainsKey('noScenarioDiscovery') -and [bool]$resolved['noScenarioDiscovery'] $noCache = $resolved.ContainsKey('noCache') -and [bool]$resolved['noCache'] $discoveredEntities = @() if (-not $noTasksDiscovery -or -not $noScriptsDiscovery) { $discoveredEntities = Get-JaxDiscoveredRunEntities -RepoRoot $repoRoot -EnvDir $envDir -Config $config -NoTasksDiscovery:$noTasksDiscovery -NoScriptsDiscovery:$noScriptsDiscovery -NoCache:$noCache -SkipEnvRoot:$skipEnvRoot @commonParams $context['DiscoveredEntities'] = $discoveredEntities $context['DiscoveredEntityIndex'] = New-JaxRunEntityIndex -Entities $discoveredEntities } if ([bool]$resolved['listScenarios']) { if ($null -eq $flowConfig) { Write-Host "No scenarios found." return } $names = Get-JaxScenarioNames -FlowConfig $flowConfig @commonParams if ($names.Count -eq 0) { Write-Host "No scenarios found." return } $names | Format-Wide -AutoSize return } if ([bool]$resolved['listTasks']) { $entities = @() if ($null -ne $flowConfig -and -not $noScenarioDiscovery -and -not $noTasksDiscovery) { $entities += Get-JaxScenarioRunEntities -FlowConfig $flowConfig -Scenario $resolved['scenario'] -Context $context -ProvenancePath $flowConfigEntry.ConfigPath @commonParams } if ($discoveredEntities.Count -gt 0) { $entities += $discoveredEntities } $keys = Get-JaxRunEntityKeys -Entities $entities -IncludeAliases @commonParams if ($keys.Count -eq 0) { Write-Host "No tasks found." return } $keys | Format-Wide -AutoSize return } $scenarioEntities = @() if ($null -ne $flowConfig -and -not $noScenarioDiscovery -and -not $noTasksDiscovery) { $scenarioEntities = Get-JaxScenarioRunEntities -FlowConfig $flowConfig -Scenario $resolved['scenario'] -Context $context -ProvenancePath $flowConfigEntry.ConfigPath @commonParams } $context['ScenarioEntityIndex'] = New-JaxRunEntityIndex -Entities $scenarioEntities if ($noScriptsDiscovery) { $scenarioEntities = @($scenarioEntities | Where-Object { -not (Test-JaxRunEntityIsScript -Entity $_) }) } $buildEntities = @() if ($null -ne $flowConfig) { $buildEntities = Get-JaxBuildRunEntities -FlowConfig $flowConfig -Config $config -Context $context -ProvenancePath $flowConfigEntry.ConfigPath @commonParams } # Ensure run-config is available even when there is no flow config (dummy env or scenario discovery disabled). $skipScenarioResolution = ($null -eq $flowConfig) -or $noScenarioDiscovery -or $noTasksDiscovery if ($skipScenarioResolution -and -not ($context.ContainsKey('RunConfig'))) { if (-not ($context.ContainsKey('CommonParameters'))) { $context['CommonParameters'] = $commonParams } Invoke-JaxHooks -Name 'BeforeSequenceResolve' -Context $context -Data @{ FlowConfig = $flowConfig Scenario = $resolved['scenario'] FlowConfigPath = if ($flowConfigEntry) { $flowConfigEntry.ConfigPath } else { $null } } } $resolvedOnly = $resolved['only'] $plan = Resolve-JaxRunPlan ` -BuildEntities $buildEntities ` -ScenarioEntities $scenarioEntities ` -IndividualEntities $discoveredEntities ` -NoBuild:([bool]$resolved['noBuild']) ` -BuildChainOnly:([bool]$resolved['buildChainOnly']) ` -Only $resolvedOnly ` -From ([string]$resolved['from']) ` -To ([string]$resolved['to']) ` -Context $context @commonParams $hasNoPlan = ($null -eq $plan) -or ($plan.Count -eq 0) if ($hasNoPlan) { $selectorOnly = [string]$resolved['only'] $selectorFrom = [string]$resolved['from'] $selectorTo = [string]$resolved['to'] $selectorEnv = [string]$resolved['env'] $selectorScenario = [string]$resolved['scenario'] $selectorsSummary = @() if (-not [string]::IsNullOrWhiteSpace($selectorOnly)) { $selectorsSummary += "-only '$selectorOnly'" } if (-not [string]::IsNullOrWhiteSpace($selectorFrom)) { $selectorsSummary += "-from '$selectorFrom'" } if (-not [string]::IsNullOrWhiteSpace($selectorTo)) { $selectorsSummary += "-to '$selectorTo'" } if (-not [string]::IsNullOrWhiteSpace($selectorScenario)) { $selectorsSummary += "-scenario '$selectorScenario'" } $selectorsText = if ($selectorsSummary.Count -gt 0) { ($selectorsSummary -join ', ') } else { '(none)' } $hint = @() if ([string]::IsNullOrWhiteSpace($selectorOnly) -and -not [string]::IsNullOrWhiteSpace($selectorTo) -and [string]::IsNullOrWhiteSpace($selectorFrom)) { $hint += "It looks like you used -to without -from. Note: '-t' is an alias for '-to' (build-chain slicing), not 'task'." $hint += "If you meant to run a single task/entity, use: -only '$selectorTo' (or -o '$selectorTo')." $hint += "If you meant a build chain slice, use: -from <StartKey> -to '$selectorTo'." } $hint += "To see available tasks: jax -e $selectorEnv listTasks" $hint += "To preview what would run: jax -e $selectorEnv plan -only <TaskKey>" $hintText = ($hint | Where-Object { $_ } | ForEach-Object { " - $_" }) -join "`n" throw @" No run entities matched your selection. Env: $selectorEnv Selectors: $selectorsText Hints: $hintText "@ } if ([bool]$resolved['docker'] -or [bool]$resolved['container']) { $runConfig = $null if ($context.ContainsKey('RunConfig')) { $runConfig = $context['RunConfig'] } $defaultImage = Resolve-JaxDefaultContainerImage -RunConfig $runConfig if ([string]::IsNullOrWhiteSpace($defaultImage)) { Write-Host "⚠️ -docker requested but no container image was found in run-config." } else { $plan = Set-JaxContainerOverride -Entities $plan -Image $defaultImage $context['DefaultContainerImage'] = $defaultImage } } $planLog = $null if ($plan.Count -gt 0) { $planLog = Write-JaxPlanLog -Entities $plan -RepoRoot $repoRoot @commonParams if (-not $quietMode) { Write-Host ("🪵 → 📋 Run plan saved: {0}" -f (Format-JaxPathLink -Path $planLog @commonParams)) } } # Real execution (or dry-run/plan variants). We wrap in try/finally so that # even on exceptions, Ctrl-C, or psake crashes we reliably restore any terminal # / cmux surface titles we changed (e.g. "WTW Shell" or other wtw-named tabs). try { if ($options.ContainsKey('dryRunDeep') -and $options.dryRunDeep) { $plan | ConvertTo-Json -Depth 6 } elseif ($options.ContainsKey('plan') -and $options.plan) { Write-JaxPlanOutput -Entities $plan -Detailed:$Detailed -Context $context @commonParams } elseif ($options.ContainsKey('dryRun') -and $options.dryRun) { $plan | Format-List } else { $context['SuppressResults'] = $true Invoke-JaxRunEntities -Entities $plan -Context $context @commonParams } } finally { try { Complete-JaxTerminalTitle } catch { # Never let title finalization break anything on exit } } if (-not $noSavedSettings) { $coreUpdate = @{ env = $resolved['env'] client = $resolved['client'] scenario = $resolved['scenario'] } # IMPORTANT: never persist from/to/only. Those are execution selectors and # must remain ephemeral to avoid "stale slicing" surprises across runs. # Plugins may persist their own settings under their own namespace if needed # (e.g. plugins.bob.vault=true, plugins.test.enabled=true). Update-JaxState -RepoRoot $repoRoot -Updates @{ core = $coreUpdate } @commonParams | Out-Null } if ($options.ContainsKey('beep') -and $options.beep) { Invoke-JaxBeep } } 'settings' { $action = if ($CommandArgs -and $CommandArgs.Count -gt 0) { $CommandArgs[0] } else { $null } switch ($action) { 'reset' { Reset-JaxState -RepoRoot $jaxRepoRoot @commonParams Write-Host "Jax settings reset." } 'show' { $repoRoot = $jaxRepoRoot $state = Get-JaxState -RepoRoot $repoRoot @commonParams if ($null -ne $state) { $state | ConvertTo-Json -Depth 6 } } 'set' { if ($CommandArgs.Count -lt 2) { Write-Host "Usage: jax settings set <key> [<value>]" Show-JaxSettingsHelp return } $key = [string]$CommandArgs[1] $rawValue = if ($CommandArgs.Count -ge 3) { [string]$CommandArgs[2] } else { 'on' } $repoRoot = $jaxRepoRoot $boolKeys = @('quiet', 'verbose', 'docker', 'container', 'dryRun') if ($boolKeys -contains $key) { $val = ConvertTo-JaxBoolValue -Value $rawValue Update-JaxState -RepoRoot $repoRoot -Updates @{ core = @{ $key = $val } } @commonParams | Out-Null Write-Host ("ok: core.{0}={1}" -f $key, $val) } else { # Generic string setter under core.<key>. Update-JaxState -RepoRoot $repoRoot -Updates @{ core = @{ $key = $rawValue } } @commonParams | Out-Null Write-Host ("ok: core.{0}={1}" -f $key, $rawValue) } } default { Show-JaxSettingsHelp } } } default { Show-JaxHelp } } } |