feat(install): configure custom status line
This commit is contained in:
1 parent
0d034f4299
commit
ee19c64beb
14 files changed
+101
-315
No files matched your search
@@ -24,7 +24,6 @@ if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePat
|
||||
}
|
||||
|
||||
$options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun
|
||||
$reinstallViaNpm = Read-CliReinstallOption -Client $Client -DryRun:$DryRun
|
||||
|
||||
$buildScript = Join-Path $root 'scripts/commands/build.ps1'
|
||||
& $buildScript -Summary:$Summary
|
||||
@@ -39,16 +38,14 @@ $claudeConcurrency = if ($env:CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY) { $env:CLAUD
|
||||
if ($claudeConcurrency -notmatch '^[1-9][0-9]?$' -or [int]$claudeConcurrency -gt 10) { throw 'CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY must be an integer from 1 to 10.' }
|
||||
|
||||
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -Mode 'Install' -DryRun:$DryRun
|
||||
if ($options.update_agents) { Update-SelectedAgentClis -Client $Client -DryRun:$DryRun -Summary:$Summary }
|
||||
if ($reinstallViaNpm) { Invoke-CliReinstall -Client $Client -DryRun:$DryRun -Summary:$Summary }
|
||||
|
||||
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Shell $shell -Client $Client)) {
|
||||
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -ClaudeConcurrency $claudeConcurrency -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
|
||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -ClaudeConcurrency $claudeConcurrency -FlashbangEnabled $options.flashbang -StatusLineEnabled $options.statusline -DryRun:$DryRun -Summary:$Summary
|
||||
}
|
||||
|
||||
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
||||
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Summary:$Summary
|
||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
|
||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -Flashbang $options.flashbang -StatusLine $options.statusline -SemanticRetrieval $options.semantic_retrieval }
|
||||
if ($Summary) { Write-Output "Install: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
||||
exit 0
|
||||
@@ -36,7 +36,6 @@ if [ "$dry_run" = false ]; then
|
||||
fi
|
||||
|
||||
read_install_options
|
||||
reinstall_via_npm=$(read_cli_reinstall_option "$client" "$dry_run")
|
||||
|
||||
build_args=()
|
||||
[ "$summary" = false ] || build_args+=(--summary)
|
||||
@@ -55,13 +54,11 @@ claude_concurrency=${CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY:-5}
|
||||
selected_plugins=()
|
||||
deselected_plugins=()
|
||||
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
|
||||
[ "$update_agents" = false ] || update_selected_agent_clis
|
||||
[ "$reinstall_via_npm" = false ] || invoke_cli_reinstall "$client" "$dry_run" "$summary"
|
||||
|
||||
while IFS='|' read -r source destination; do
|
||||
ai_config_root=$destination
|
||||
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang" "$claude_concurrency"
|
||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang" "$claude_concurrency" "$statusline"
|
||||
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
||||
|
||||
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
|
||||
|
||||
@@ -49,11 +49,9 @@ $plugins = if ($Quick) {
|
||||
Select-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -Mode 'Update' -DryRun:$DryRun
|
||||
}
|
||||
|
||||
if ($options.update_agents) { Update-SelectedAgentClis -Client $Client -DryRun:$DryRun -Summary:$Summary }
|
||||
|
||||
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Shell $shell -Client $Client)) {
|
||||
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
|
||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -FlashbangEnabled $options.flashbang -StatusLineEnabled $options.statusline -DryRun:$DryRun -Summary:$Summary
|
||||
}
|
||||
|
||||
$previousPluginNonInteractive = $script:PluginNonInteractive
|
||||
@@ -67,6 +65,6 @@ try {
|
||||
$env:CI = $previousCI
|
||||
}
|
||||
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Update -Summary:$Summary
|
||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
|
||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -Flashbang $options.flashbang -StatusLine $options.statusline -SemanticRetrieval $options.semantic_retrieval }
|
||||
if ($Summary) { Write-Output "Update: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
|
||||
exit 0
|
||||
@@ -59,12 +59,11 @@ if [ "$quick" = false ]; then
|
||||
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
|
||||
fi
|
||||
|
||||
[ "$update_agents" = false ] || update_selected_agent_clis
|
||||
|
||||
while IFS='|' read -r source destination; do
|
||||
ai_config_root=$destination
|
||||
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang"
|
||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang" 5 "$statusline"
|
||||
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
||||
|
||||
if [ "$quick" = true ]; then
|
||||
|
||||
@@ -15,15 +15,15 @@ function Read-InstallBoolean {
|
||||
|
||||
function Read-InstallOptions {
|
||||
param([string]$HomePath, [string]$RepositoryRoot, [string]$Client, [switch]$DryRun)
|
||||
$defaults = @{ update_agents = $false; flashbang = $true; semantic_retrieval = $false }
|
||||
$defaults = @{ flashbang = $true; statusline = $true; semantic_retrieval = $false }
|
||||
if (Test-Path -LiteralPath (Join-Path $HomePath '.my-ai-configuration/selection.json')) {
|
||||
try { $defaults = Read-UpdateSelection -HomePath $HomePath -RepositoryRoot $RepositoryRoot -AllowLegacy }
|
||||
catch { Write-Warning 'Saved options could not be read; confirm new options below.' }
|
||||
}
|
||||
if ($DryRun) { return $defaults }
|
||||
return @{
|
||||
update_agents = Read-InstallBoolean -Prompt "Update selected agent CLIs ($Client)?" -Default $defaults.update_agents
|
||||
flashbang = Read-InstallBoolean -Prompt 'Enable the Flashbang notification hook?' -Default $defaults.flashbang
|
||||
statusline = Read-InstallBoolean -Prompt 'Apply the custom status line?' -Default $defaults.statusline
|
||||
semantic_retrieval = Read-SemanticRetrievalOption -Default $defaults.semantic_retrieval -RepositoryRoot $RepositoryRoot -HomePath $HomePath
|
||||
}
|
||||
}
|
||||
@@ -43,102 +43,3 @@ function Read-SemanticRetrievalOption {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Test-CodexProcessActive {
|
||||
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { return $false }
|
||||
return [bool](Get-Process -Name 'codex' -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Test-GlobalNpmCodex {
|
||||
$npm = Get-Command npm -ErrorAction SilentlyContinue
|
||||
if (-not $npm) { return $false }
|
||||
$previousPreference = $ErrorActionPreference
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $npm list -g '@openai/codex' --depth=0 --json 1>$null 2>$null
|
||||
return $LASTEXITCODE -eq 0
|
||||
} finally {
|
||||
$ErrorActionPreference = $previousPreference
|
||||
}
|
||||
}
|
||||
|
||||
function Test-GlobalNpmPackage {
|
||||
param([Parameter(Mandatory=$true)][string]$Package)
|
||||
$npm = Get-Command npm -ErrorAction SilentlyContinue
|
||||
if (-not $npm) { return $false }
|
||||
$previousPreference = $ErrorActionPreference
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $npm list -g $Package --depth=0 --json 1>$null 2>$null
|
||||
return $LASTEXITCODE -eq 0
|
||||
} finally {
|
||||
$ErrorActionPreference = $previousPreference
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CliNpmPackage {
|
||||
param([ValidateSet('codex','claude')][Parameter(Mandatory=$true)][string]$Client)
|
||||
if ($Client -eq 'codex') { return '@openai/codex' }
|
||||
return '@anthropic-ai/claude-code'
|
||||
}
|
||||
|
||||
function Read-CliReinstallOption {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Asked once per install run, never persisted; a dry run never prompts and never previews it.
|
||||
#>
|
||||
param([string]$Client, [switch]$DryRun)
|
||||
if ($DryRun) { return $false }
|
||||
return Read-InstallBoolean -Prompt "Uninstall $Client and reinstall via npm?" -Default $false
|
||||
}
|
||||
|
||||
function Invoke-CliReinstall {
|
||||
param([string]$Client, [switch]$DryRun, [switch]$Summary)
|
||||
$clients = if ($Client -eq 'Both') { @('codex', 'claude') } else { @($Client.ToLowerInvariant()) }
|
||||
$previousAutoUpdate = $env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE
|
||||
try {
|
||||
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = '1'
|
||||
foreach ($selectedClient in $clients) {
|
||||
if ($selectedClient -eq 'codex' -and -not $DryRun -and (Test-CodexProcessActive)) {
|
||||
Write-Warning 'Codex CLI reinstall skipped because codex.exe is running. Close all Codex sessions and run install again to reinstall the CLI.'
|
||||
continue
|
||||
}
|
||||
$package = Get-CliNpmPackage -Client $selectedClient
|
||||
if (Test-GlobalNpmPackage -Package $package) {
|
||||
Invoke-PluginCommand -Command 'npm' -Arguments @('uninstall', '-g', $package) -DryRun:$DryRun -Summary:$Summary
|
||||
} else {
|
||||
Write-Warning "$selectedClient CLI was not found as a global npm package; a non-npm installation cannot be removed automatically. Installing $package via npm alongside it."
|
||||
}
|
||||
Invoke-PluginCommand -Command 'npm' -Arguments @('install', '-g', "$package@latest") -DryRun:$DryRun -Summary:$Summary
|
||||
if ($Summary) { Write-Output "CLI $selectedClient reinstall: $(if ($DryRun) { 'dry-run' } else { 'PASS' })" }
|
||||
}
|
||||
} finally {
|
||||
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = $previousAutoUpdate
|
||||
}
|
||||
}
|
||||
|
||||
function Update-SelectedAgentClis {
|
||||
param([string]$Client, [switch]$DryRun, [switch]$Summary)
|
||||
$clients = if ($Client -eq 'Both') { @('codex', 'claude') } else { @($Client.ToLowerInvariant()) }
|
||||
$previousNonInteractive = $script:PluginNonInteractive
|
||||
$previousAutoUpdate = $env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE
|
||||
try {
|
||||
$script:PluginNonInteractive = $true
|
||||
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = '1'
|
||||
foreach ($selectedClient in $clients) {
|
||||
if ($selectedClient -eq 'codex' -and -not $DryRun -and (Test-CodexProcessActive)) {
|
||||
Write-Warning 'Codex CLI update skipped because codex.exe is running. Close all Codex sessions and run the update again to update the CLI.'
|
||||
continue
|
||||
}
|
||||
if ($selectedClient -eq 'codex' -and (Test-GlobalNpmCodex)) {
|
||||
Invoke-PluginCommand -Command 'npm' -Arguments @('install','-g','@openai/codex@latest') -DryRun:$DryRun -Summary:$Summary
|
||||
} else {
|
||||
Invoke-PluginCommand -Command $selectedClient -Arguments @('update') -DryRun:$DryRun -Summary:$Summary
|
||||
}
|
||||
if ($Summary) { Write-Output "CLI $selectedClient update: $(if ($DryRun) { 'dry-run' } else { 'PASS' })" }
|
||||
}
|
||||
} finally {
|
||||
$script:PluginNonInteractive = $previousNonInteractive
|
||||
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = $previousAutoUpdate
|
||||
}
|
||||
}
|
||||
@@ -11,47 +11,65 @@ def is_flashbang(hook):
|
||||
for key in ('command', 'command_windows'))
|
||||
|
||||
|
||||
def filter_flashbang(path):
|
||||
def filter_options(path, flashbang_enabled=True, statusline_enabled=True):
|
||||
content = path.read_text(encoding='utf-8-sig')
|
||||
if path.name == 'settings.json':
|
||||
settings = json.loads(content)
|
||||
groups = settings.get('hooks', {}).get('Stop', [])
|
||||
remaining = []
|
||||
for group in groups:
|
||||
hooks = [hook for hook in group.get('hooks', []) if not is_flashbang(hook)]
|
||||
if hooks:
|
||||
remaining.append(dict(group, hooks=hooks))
|
||||
if remaining:
|
||||
settings['hooks']['Stop'] = remaining
|
||||
elif 'Stop' in settings.get('hooks', {}):
|
||||
del settings['hooks']['Stop']
|
||||
if not flashbang_enabled:
|
||||
groups = settings.get('hooks', {}).get('Stop', [])
|
||||
remaining = []
|
||||
for group in groups:
|
||||
hooks = [hook for hook in group.get('hooks', []) if not is_flashbang(hook)]
|
||||
if hooks:
|
||||
remaining.append(dict(group, hooks=hooks))
|
||||
if remaining:
|
||||
settings['hooks']['Stop'] = remaining
|
||||
elif 'Stop' in settings.get('hooks', {}):
|
||||
del settings['hooks']['Stop']
|
||||
if not statusline_enabled:
|
||||
settings.pop('statusLine', None)
|
||||
return json.dumps(settings, indent=2, ensure_ascii=False) + '\n'
|
||||
if path.name != 'config.toml':
|
||||
raise ValueError('Flashbang filtering only supports client configuration files.')
|
||||
raise ValueError('Install option filtering only supports client configuration files.')
|
||||
tomllib.loads(content)
|
||||
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', content)
|
||||
filtered = []
|
||||
for section in sections:
|
||||
if re.match(r'\s*\[\[hooks\.Stop\.hooks\]\]', section):
|
||||
hook = tomllib.loads(section)['hooks']['Stop']['hooks'][0]
|
||||
if is_flashbang(hook):
|
||||
continue
|
||||
filtered.append(section)
|
||||
result = ''.join(filtered)
|
||||
result = re.sub(r'(?m)^\[\[hooks\.Stop\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.Stop\.hooks\]\])', '', result)
|
||||
result = content
|
||||
if not flashbang_enabled:
|
||||
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
||||
filtered = []
|
||||
for section in sections:
|
||||
if re.match(r'\s*\[\[hooks\.Stop\.hooks\]\]', section):
|
||||
hook = tomllib.loads(section)['hooks']['Stop']['hooks'][0]
|
||||
if is_flashbang(hook):
|
||||
continue
|
||||
filtered.append(section)
|
||||
result = ''.join(filtered)
|
||||
result = re.sub(r'(?m)^\[\[hooks\.Stop\]\][ \t]*\r?\n\s*(?=\[|\Z)(?!\[\[hooks\.Stop\.hooks\]\])', '', result)
|
||||
if not statusline_enabled:
|
||||
sections = re.split(r'(?m)(?=^\s*\[\[?[^\r\n]+\]\]?\s*$)', result)
|
||||
for index, section in enumerate(sections):
|
||||
header = re.match(r'\s*\[\[?([^\]\r\n]+)\]\]?\s*(?:#.*)?(?:\r?\n|$)', section)
|
||||
if header and header.group(1).strip() == 'tui':
|
||||
sections[index] = re.sub(r'(?m)^[ \t]*status_line[ \t]*=.*(?:\r?\n|$)', '', section)
|
||||
break
|
||||
result = ''.join(sections)
|
||||
tomllib.loads(result)
|
||||
return result
|
||||
|
||||
|
||||
def filter_flashbang(path):
|
||||
return filter_options(path, flashbang_enabled=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('action', choices=('filter',))
|
||||
parser.add_argument('--path', type=Path, required=True)
|
||||
parser.add_argument('--flashbang', choices=('true', 'false'), required=True)
|
||||
parser.add_argument('--statusline', choices=('true', 'false'), default='true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
print(filter_flashbang(args.path) if args.flashbang == 'false' else args.path.read_text(encoding='utf-8-sig'), end='')
|
||||
print(filter_options(args.path, args.flashbang == 'true', args.statusline == 'true'), end='')
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(f'Install options: {error}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -17,23 +17,23 @@ read_install_boolean() {
|
||||
}
|
||||
|
||||
read_install_options() {
|
||||
update_agents=false
|
||||
flashbang=true
|
||||
statusline=true
|
||||
semantic_retrieval=false
|
||||
local saved key value
|
||||
if [ -f "$home_path/.my-ai-configuration/selection.json" ]; then
|
||||
if saved=$(python3 "$root/scripts/lib/selection-state.py" read --home "$home_path" --manifest "$root/adapters/plugins.tsv" --format tsv --allow-legacy); then
|
||||
while IFS=$'\t' read -r key value; do
|
||||
value=${value%$'\r'}
|
||||
case "$key" in update_agents) update_agents=$value ;; flashbang) flashbang=$value ;; semantic_retrieval) semantic_retrieval=$value ;; esac
|
||||
case "$key" in flashbang) flashbang=$value ;; statusline) statusline=$value ;; semantic_retrieval) semantic_retrieval=$value ;; esac
|
||||
done <<< "$saved"
|
||||
else
|
||||
printf 'WARN Saved options could not be read; confirm new options below.\n' >&2
|
||||
fi
|
||||
fi
|
||||
[ "$dry_run" = false ] || return 0
|
||||
update_agents=$(read_install_boolean "Update selected agent CLIs ($client)?" "$update_agents") || return
|
||||
flashbang=$(read_install_boolean 'Enable the Flashbang notification hook?' "$flashbang") || return
|
||||
statusline=$(read_install_boolean 'Apply the custom status line?' "$statusline") || return
|
||||
semantic_retrieval=$(read_semantic_retrieval_option "$semantic_retrieval") || return
|
||||
}
|
||||
|
||||
@@ -56,78 +56,3 @@ read_semantic_retrieval_option() {
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
global_npm_package() {
|
||||
command -v npm >/dev/null 2>&1 || return 1
|
||||
npm list -g "$1" --depth=0 --json >/dev/null 2>&1
|
||||
}
|
||||
|
||||
cli_npm_package() {
|
||||
case "$1" in
|
||||
codex) printf '@openai/codex\n' ;;
|
||||
claude) printf '@anthropic-ai/claude-code\n' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# read_cli_reinstall_option <client> <dry_run> -- asked once per install run, never persisted;
|
||||
# a dry run never prompts and never previews it.
|
||||
read_cli_reinstall_option() {
|
||||
local client=$1 dry_run=$2
|
||||
if [ "$dry_run" = true ]; then printf 'false\n'; return; fi
|
||||
read_install_boolean "Uninstall $client and reinstall via npm?" false
|
||||
}
|
||||
|
||||
invoke_cli_reinstall() {
|
||||
local client=$1 dry_run=$2 summary=${3:-false}
|
||||
local selected_client clients=() package
|
||||
case "$client" in codex|both) clients+=(codex) ;; esac
|
||||
case "$client" in claude|both) clients+=(claude) ;; esac
|
||||
for selected_client in "${clients[@]}"; do
|
||||
if [ "$selected_client" = codex ] && [ "$dry_run" = false ] && codex_process_active; then
|
||||
printf 'WARN Codex CLI reinstall skipped because codex.exe is running. Close all Codex sessions and run install again to reinstall the CLI.\n' >&2
|
||||
continue
|
||||
fi
|
||||
package=$(cli_npm_package "$selected_client")
|
||||
if global_npm_package "$package"; then
|
||||
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 run_plugin_command "$dry_run" npm uninstall -g "$package" </dev/null || return
|
||||
else
|
||||
printf 'WARN %s CLI was not found as a global npm package; a non-npm installation cannot be removed automatically. Installing %s via npm alongside it.\n' "$selected_client" "$package" >&2
|
||||
fi
|
||||
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 run_plugin_command "$dry_run" npm install -g "${package}@latest" </dev/null || return
|
||||
if [ "$summary" = true ]; then
|
||||
if [ "$dry_run" = true ]; then printf 'CLI %s reinstall: dry-run\n' "$selected_client"; else printf 'CLI %s reinstall: PASS\n' "$selected_client"; fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
is_windows_host() {
|
||||
case "${OSTYPE:-}" in msys*|cygwin*|win32*) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
|
||||
codex_process_active() {
|
||||
is_windows_host || return 1
|
||||
command -v powershell.exe >/dev/null 2>&1 || return 1
|
||||
powershell.exe -NoProfile -Command "if (Get-Process -Name codex -ErrorAction SilentlyContinue) { exit 0 }; exit 1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
global_npm_codex() {
|
||||
command -v npm >/dev/null 2>&1 || return 1
|
||||
npm list -g '@openai/codex' --depth=0 --json >/dev/null 2>&1
|
||||
}
|
||||
|
||||
update_selected_agent_clis() {
|
||||
local selected_client clients=()
|
||||
case "$client" in codex|both) clients+=(codex) ;; esac
|
||||
case "$client" in claude|both) clients+=(claude) ;; esac
|
||||
for selected_client in "${clients[@]}"; do
|
||||
if [ "$selected_client" = codex ] && [ "$dry_run" = false ] && codex_process_active; then
|
||||
printf 'WARN Codex CLI update skipped because codex.exe is running. Close all Codex sessions and run the update again to update the CLI.\n' >&2
|
||||
continue
|
||||
fi
|
||||
if [ "$selected_client" = codex ] && global_npm_codex; then
|
||||
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 CI=true run_plugin_command "$dry_run" npm install -g '@openai/codex@latest' </dev/null || return
|
||||
else
|
||||
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 CI=true run_plugin_command "$dry_run" "$selected_client" update </dev/null || return
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -79,6 +79,7 @@ function Sync-ManagedDestination {
|
||||
[string]$PowerShellCommand,
|
||||
[string]$ClaudeConcurrency = '5',
|
||||
[bool]$FlashbangEnabled = $true,
|
||||
[bool]$StatusLineEnabled = $true,
|
||||
[switch]$DryRun,
|
||||
[switch]$Summary
|
||||
)
|
||||
@@ -93,14 +94,17 @@ function Sync-ManagedDestination {
|
||||
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
||||
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
||||
if ((Split-Path $Destination -Leaf) -eq '.codex' -and $relative.StartsWith('skills/')) { return }
|
||||
if ((Split-Path $Destination -Leaf) -eq '.claude' -and -not $StatusLineEnabled -and $relative.StartsWith('statusline/')) { return }
|
||||
$target = Resolve-ManagedPath -Destination $Destination -Relative $relative
|
||||
$rawContent = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8
|
||||
$filterFlashbang = -not $FlashbangEnabled -and $relative -in @('settings.json', 'config.toml')
|
||||
if ($filterFlashbang) {
|
||||
$rawContent = (& python (Join-Path $PSScriptRoot 'install-options.py') filter --path $_.FullName --flashbang false | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not configure Flashbang.' }
|
||||
$filterOptions = ((-not $FlashbangEnabled) -or (-not $StatusLineEnabled)) -and $relative -in @('settings.json', 'config.toml')
|
||||
if ($filterOptions) {
|
||||
$flashbangOption = if ($FlashbangEnabled) { 'true' } else { 'false' }
|
||||
$statusLineOption = if ($StatusLineEnabled) { 'true' } else { 'false' }
|
||||
$rawContent = (& python (Join-Path $PSScriptRoot 'install-options.py') filter --path $_.FullName --flashbang $flashbangOption --statusline $statusLineOption | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not configure install options.' }
|
||||
}
|
||||
$needsSubstitution = $filterFlashbang -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
||||
$needsSubstitution = $filterOptions -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__') -or $rawContent.Contains('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__')
|
||||
if ($needsSubstitution) {
|
||||
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__POWERSHELL_HOOK_COMMAND__', $PowerShellCommand).Replace('__POWERSHELL_COMMAND__', $PowerShellCommand).Replace('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', $ClaudeConcurrency).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__POWERSHELL_HOOK_SCRIPT__', 'flashbang.ps1')
|
||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||
|
||||
@@ -70,9 +70,9 @@ write_managed_manifest() {
|
||||
} > "$path"
|
||||
}
|
||||
|
||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <powershell-command> <dry-run: true|false> [summary: true|false] [flashbang] [claude-concurrency]
|
||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <powershell-command> <dry-run: true|false> [summary: true|false] [flashbang] [claude-concurrency] [statusline]
|
||||
sync_managed_destination() {
|
||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 powershell_command=$6 dry_run=$7 summary=${8:-false} flashbang_enabled=${9:-true} claude_concurrency=${10:-5}
|
||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 powershell_command=$6 dry_run=$7 summary=${8:-false} flashbang_enabled=${9:-true} claude_concurrency=${10:-5} statusline_enabled=${11:-true}
|
||||
[ "$summary" = true ] || printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
||||
local manifest
|
||||
manifest=$(manifest_path "$destination")
|
||||
@@ -87,12 +87,13 @@ sync_managed_destination() {
|
||||
local relative target content needs_sub new_hash exists current_hash action backup
|
||||
relative=${source_file#"$source/"}
|
||||
if [ "${destination##*/}" = .codex ] && [[ "$relative" = skills/* ]]; then continue; fi
|
||||
if [ "${destination##*/}" = .claude ] && [ "$statusline_enabled" = false ] && [[ "$relative" = statusline/* ]]; then continue; fi
|
||||
target=$(managed_target "$destination" "$relative") || return 1
|
||||
content=$(cat -- "$source_file" && printf '\034') || return
|
||||
content=${content%$'\034'}
|
||||
needs_sub=false
|
||||
if [ "$flashbang_enabled" = false ] && [[ "$relative" = settings.json || "$relative" = config.toml ]]; then
|
||||
content=$(python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" filter --path "$source_file" --flashbang false && printf '\034') || return
|
||||
if { [ "$flashbang_enabled" = false ] || [ "$statusline_enabled" = false ]; } && [[ "$relative" = settings.json || "$relative" = config.toml ]]; then
|
||||
content=$(python3 "$(dirname -- "${BASH_SOURCE[0]}")/install-options.py" filter --path "$source_file" --flashbang "$flashbang_enabled" --statusline "$statusline_enabled" && printf '\034') || return
|
||||
content=${content%$'\034'}
|
||||
needs_sub=true
|
||||
fi
|
||||
|
||||
@@ -9,8 +9,8 @@ function Read-UpdateSelection {
|
||||
|
||||
function Save-UpdateSelection {
|
||||
param([string]$HomePath, [string]$RepositoryRoot, [string]$Shell, [string]$Client, [hashtable]$Plugins,
|
||||
[Parameter(Mandatory=$true)][bool]$UpdateAgents, [Parameter(Mandatory=$true)][bool]$Flashbang, [bool]$SemanticRetrieval = $false)
|
||||
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--shell', $Shell.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--update-agents', $UpdateAgents.ToString().ToLowerInvariant(), '--flashbang', $Flashbang.ToString().ToLowerInvariant(), '--semantic-retrieval', $SemanticRetrieval.ToString().ToLowerInvariant(), '--selected')
|
||||
[Parameter(Mandatory=$true)][bool]$Flashbang, [bool]$StatusLine = $true, [bool]$SemanticRetrieval = $false)
|
||||
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--shell', $Shell.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--flashbang', $Flashbang.ToString().ToLowerInvariant(), '--statusline', $StatusLine.ToString().ToLowerInvariant(), '--semantic-retrieval', $SemanticRetrieval.ToString().ToLowerInvariant(), '--selected')
|
||||
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
|
||||
$arguments += '--deselected'
|
||||
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
|
||||
|
||||
@@ -12,7 +12,7 @@ def validate(state, allow_legacy=False):
|
||||
if not allow_legacy:
|
||||
raise ValueError('Saved selection predates install options. Run update once without Quick.')
|
||||
state = dict(state, version=2, shell={'windows': 'powershell', 'linux': 'bash'}.get(state.get('platform')),
|
||||
update_agents=False, flashbang=True)
|
||||
flashbang=True, statusline=True)
|
||||
state.pop('platform', None)
|
||||
if not isinstance(state, dict) or state.get('version') != 2:
|
||||
raise ValueError('Unsupported saved selection.')
|
||||
@@ -20,7 +20,10 @@ def validate(state, allow_legacy=False):
|
||||
raise ValueError('Invalid saved shell or client.')
|
||||
if 'semantic_retrieval' not in state:
|
||||
state['semantic_retrieval'] = False
|
||||
if any(type(state.get(key)) is not bool for key in ('update_agents', 'flashbang', 'semantic_retrieval')):
|
||||
if 'statusline' not in state:
|
||||
state['statusline'] = True
|
||||
state.pop('update_agents', None)
|
||||
if any(type(state.get(key)) is not bool for key in ('flashbang', 'statusline', 'semantic_retrieval')):
|
||||
raise ValueError('Invalid saved install options. Run update once without Quick.')
|
||||
for key in ('selected', 'deselected'):
|
||||
values = state.get(key)
|
||||
@@ -41,8 +44,8 @@ def main():
|
||||
parser.add_argument('--format', choices=('json', 'tsv'), default='json')
|
||||
parser.add_argument('--shell')
|
||||
parser.add_argument('--allow-legacy', action='store_true')
|
||||
parser.add_argument('--update-agents', choices=('true', 'false'))
|
||||
parser.add_argument('--flashbang', choices=('true', 'false'))
|
||||
parser.add_argument('--statusline', choices=('true', 'false'))
|
||||
parser.add_argument('--semantic-retrieval', choices=('true', 'false'))
|
||||
parser.add_argument('--client')
|
||||
parser.add_argument('--selected', nargs='*', default=[])
|
||||
@@ -62,15 +65,15 @@ def main():
|
||||
else:
|
||||
print('shell\t' + state['shell'])
|
||||
print('client\t' + state['client'])
|
||||
for key in ('update_agents', 'flashbang', 'semantic_retrieval'):
|
||||
for key in ('flashbang', 'statusline', 'semantic_retrieval'):
|
||||
print(key + '\t' + str(state[key]).lower())
|
||||
for key in ('selected', 'deselected'):
|
||||
for name in state[key]:
|
||||
print(key + '\t' + name)
|
||||
return
|
||||
state = validate(dict(version=2, shell=args.shell, client=args.client,
|
||||
update_agents=None if args.update_agents is None else args.update_agents == 'true',
|
||||
flashbang=None if args.flashbang is None else args.flashbang == 'true',
|
||||
flashbang=True if args.flashbang is None else args.flashbang == 'true',
|
||||
statusline=True if args.statusline is None else args.statusline == 'true',
|
||||
semantic_retrieval=False if args.semantic_retrieval is None else args.semantic_retrieval == 'true',
|
||||
selected=args.selected, deselected=args.deselected))
|
||||
if (set(state['selected']) | set(state['deselected'])) - names:
|
||||
|
||||
@@ -11,8 +11,8 @@ read_update_selection() {
|
||||
case "$key" in
|
||||
shell) [ -n "$shell" ] || shell=$value ;;
|
||||
client) [ -n "$client" ] || client=$value ;;
|
||||
update_agents) update_agents=$value ;;
|
||||
flashbang) flashbang=$value ;;
|
||||
statusline) statusline=$value ;;
|
||||
semantic_retrieval) semantic_retrieval=$value ;;
|
||||
selected) selected_plugins+=("$value") ;;
|
||||
deselected) deselected_plugins+=("$value") ;;
|
||||
@@ -22,6 +22,6 @@ read_update_selection() {
|
||||
|
||||
save_update_selection() {
|
||||
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
|
||||
--shell "$shell" --client "$client" --update-agents "$update_agents" --flashbang "$flashbang" --semantic-retrieval "${semantic_retrieval:-false}" \
|
||||
--shell "$shell" --client "$client" --flashbang "$flashbang" --statusline "${statusline:-true}" --semantic-retrieval "${semantic_retrieval:-false}" \
|
||||
--selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
|
||||
}
|
||||
@@ -40,71 +40,6 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
self.assertEqual(result['hooks']['Stop'][0]['hooks'], [{'command': 'echo keep'}])
|
||||
self.assertEqual(result['tui']['status_line'], ['model'])
|
||||
|
||||
def test_native_option_helpers_select_clients_and_restore_environment(self):
|
||||
shells = [('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))]
|
||||
for kind, executable in shells:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=kind):
|
||||
if kind == 'powershell':
|
||||
command = [executable, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $false }; function Test-GlobalNpmCodex { $false }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) "
|
||||
"Write-Output ($Command + ':' + ($Arguments -join ' ')) }; "
|
||||
"$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE='original'; "
|
||||
"Update-SelectedAgentClis -Client Both; "
|
||||
"if ($env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE -ne 'original') { throw 'Environment leaked' }"]
|
||||
else:
|
||||
command = [executable, '-c',
|
||||
'. ./scripts/lib/install-options.sh; '
|
||||
'codex_process_active() { return 1; }; global_npm_codex() { return 1; }; '
|
||||
'run_plugin_command() { shift; printf "%s:%s\\n" "$1" "$2"; }; '
|
||||
'client=both; dry_run=false; summary=false; '
|
||||
'CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=original; update_selected_agent_clis; '
|
||||
'[ "$CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE" = original ]']
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['codex:update', 'claude:update'])
|
||||
|
||||
def test_running_codex_is_skipped_and_npm_install_is_used_when_available(self):
|
||||
shell = shutil.which('powershell') or shutil.which('pwsh')
|
||||
if shell:
|
||||
skipped = subprocess.run([
|
||||
shell, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $true }; function Test-GlobalNpmCodex { $true }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) Write-Output $Command }; "
|
||||
"Update-SelectedAgentClis -Client Both"
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(skipped.returncode, 0, skipped.stdout + skipped.stderr)
|
||||
self.assertEqual(skipped.stdout.splitlines()[-1], 'claude')
|
||||
self.assertIn('Codex CLI update skipped', skipped.stdout + skipped.stderr)
|
||||
npm = subprocess.run([
|
||||
shell, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $false }; function Test-GlobalNpmCodex { $true }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) "
|
||||
"Write-Output ($Command + ':' + ($Arguments -join ' ')) }; Update-SelectedAgentClis -Client Codex"
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(npm.returncode, 0, npm.stdout + npm.stderr)
|
||||
self.assertEqual(npm.stdout.strip(), 'npm:install -g @openai/codex@latest')
|
||||
|
||||
bash = (str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))
|
||||
if bash and Path(bash).exists():
|
||||
result = subprocess.run([
|
||||
bash, '-c',
|
||||
'. ./scripts/lib/install-options.sh; codex_process_active() { return 0; }; '
|
||||
'global_npm_codex() { return 0; }; run_plugin_command() { shift; printf "%s\\n" "$1"; }; '
|
||||
'client=both; dry_run=false; summary=false; update_selected_agent_clis'
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['claude'])
|
||||
self.assertIn('Codex CLI update skipped', result.stderr)
|
||||
|
||||
def test_flashbang_toggle_is_applied_by_both_manifest_writers(self):
|
||||
shells = [('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
@@ -165,8 +100,13 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
package = root / 'generated' / (client + '-bash')
|
||||
(package / 'skills').mkdir(parents=True)
|
||||
(package / 'skills' / 'test.txt').write_text('managed')
|
||||
if client == 'claude':
|
||||
(package / 'statusline').mkdir()
|
||||
(package / 'statusline' / 'statusline.sh').write_text('managed status line')
|
||||
config = ('[[hooks.Stop]]\n[[hooks.Stop.hooks]]\ncommand = \'bash "__AI_CONFIG_ROOT__/flashbang.sh"\'\n'
|
||||
if client == 'codex' else '{"hooks":{"Stop":[{"hooks":[{"command":"bash flashbang.sh"}]}]}}')
|
||||
'[tui]\nstatus_line = ["model"]\n' if client == 'codex' else
|
||||
'{"hooks":{"Stop":[{"hooks":[{"command":"bash flashbang.sh"}]}]},'
|
||||
'"statusLine":{"type":"command","command":"bash statusline.sh"}}')
|
||||
(package / ('config.toml' if client == 'codex' else 'settings.json')).write_text(config)
|
||||
suffix = 'ps1' if kind == 'powershell' else 'sh'
|
||||
for name in ('install', 'update'):
|
||||
@@ -186,7 +126,7 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
|
||||
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
|
||||
environment = dict(os.environ, HOME=home.as_posix(), TEST_INSTALL_HOME=str(home))
|
||||
for name, quick, answers in [('install', False, 'y\nn\nn\nn\n'), ('update', True, '')]:
|
||||
for name, quick, answers in [('install', False, 'n\nn\nn\n'), ('update', True, '')]:
|
||||
entry = str(commands / (name + '.' + suffix))
|
||||
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
|
||||
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
|
||||
@@ -194,16 +134,18 @@ class InstallOptionsTests(unittest.TestCase):
|
||||
command.append('-Quick' if kind == 'powershell' else '--quick')
|
||||
result = subprocess.run(command, env=environment, input=answers, capture_output=True, text=True, timeout=25)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertTrue('CLI-MOCK codex' in result.stdout or
|
||||
'Codex CLI update skipped' in result.stdout + result.stderr)
|
||||
self.assertIn('CLI-MOCK claude', result.stdout)
|
||||
self.assertNotIn('CLI-MOCK', result.stdout)
|
||||
if quick:
|
||||
self.assertNotIn('Enable the Flashbang', result.stdout + result.stderr)
|
||||
self.assertNotIn('Apply the custom status line', result.stdout + result.stderr)
|
||||
state = json.loads((home / '.my-ai-configuration/selection.json').read_text())
|
||||
self.assertTrue(state['update_agents'])
|
||||
self.assertFalse(state['flashbang'])
|
||||
self.assertFalse(state['statusline'])
|
||||
self.assertEqual(state['shell'], 'bash')
|
||||
self.assertNotIn('Stop', json.loads((home / '.claude/settings.json').read_text()).get('hooks', {}))
|
||||
self.assertNotIn('statusLine', json.loads((home / '.claude/settings.json').read_text()))
|
||||
self.assertFalse((home / '.claude/statusline/statusline.sh').exists())
|
||||
self.assertNotIn('status_line', tomllib.loads((home / '.codex/config.toml').read_text()).get('tui', {}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -25,7 +25,7 @@ class SelectionTests(unittest.TestCase):
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
def save(self, *arguments):
|
||||
result = self.run_state('write', '--shell', 'powershell', '--update-agents', 'false', '--flashbang', 'false', '--client', 'both', *arguments)
|
||||
result = self.run_state('write', '--shell', 'powershell', '--flashbang', 'false', '--statusline', 'false', '--client', 'both', *arguments)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_missing_selection_fails_without_creating_state(self):
|
||||
@@ -59,30 +59,31 @@ class SelectionTests(unittest.TestCase):
|
||||
self.assertIn('without Quick', result.stderr)
|
||||
migrated = json.loads(self.run_state('read', '--allow-legacy').stdout)
|
||||
self.assertEqual(migrated['shell'], 'powershell')
|
||||
self.assertFalse(migrated['update_agents'])
|
||||
self.assertTrue(migrated['flashbang'])
|
||||
self.assertTrue(migrated['statusline'])
|
||||
|
||||
def test_boolean_options_are_required_and_preserved(self):
|
||||
self.save()
|
||||
state = json.loads(self.run_state('read').stdout)
|
||||
self.assertFalse(state['update_agents'])
|
||||
self.assertFalse(state['flashbang'])
|
||||
self.assertFalse(state['statusline'])
|
||||
state['flashbang'] = 'false'
|
||||
self.path.write_text(json.dumps(state))
|
||||
self.assertNotEqual(self.run_state('read').returncode, 0)
|
||||
|
||||
def test_semantic_retrieval_option_round_trips(self):
|
||||
result = self.run_state('write', '--shell', 'bash', '--client', 'both', '--update-agents', 'false',
|
||||
'--flashbang', 'true', '--semantic-retrieval', 'true')
|
||||
result = self.run_state('write', '--shell', 'bash', '--client', 'both',
|
||||
'--flashbang', 'true', '--statusline', 'true', '--semantic-retrieval', 'true')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
state = json.loads(self.run_state('read').stdout)
|
||||
self.assertTrue(state['semantic_retrieval'])
|
||||
self.assertTrue(state['statusline'])
|
||||
self.assertIn('semantic_retrieval\ttrue', self.run_state('read', '--format', 'tsv').stdout)
|
||||
|
||||
def test_invalid_write_preserves_last_selection(self):
|
||||
self.save('--selected', 'Superpowers')
|
||||
original = self.path.read_bytes()
|
||||
result = self.run_state('write', '--shell', 'bash', '--update-agents', 'false', '--flashbang', 'true', '--client', 'codex', '--selected', 'Unknown')
|
||||
result = self.run_state('write', '--shell', 'bash', '--flashbang', 'true', '--client', 'codex', '--selected', 'Unknown')
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(self.path.read_bytes(), original)
|
||||
|
||||
@@ -108,7 +109,7 @@ class SelectionTests(unittest.TestCase):
|
||||
save = subprocess.run([shell, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference = 'Stop'; . (Join-Path $env:TEST_REPOSITORY_ROOT 'scripts/lib/selection-state.ps1'); "
|
||||
"Save-UpdateSelection -HomePath $env:TEST_SELECTION_HOME -RepositoryRoot $env:TEST_REPOSITORY_ROOT "
|
||||
"-Shell PowerShell -Client Both -UpdateAgents $false -Flashbang $false -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
|
||||
"-Shell PowerShell -Client Both -Flashbang $false -StatusLine $false -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
|
||||
env=environment, capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
|
||||
original = self.path.read_bytes()
|
||||
@@ -140,7 +141,7 @@ class SelectionTests(unittest.TestCase):
|
||||
if not shell:
|
||||
self.skipTest('Bash unavailable')
|
||||
save = subprocess.run([shell, '-c',
|
||||
'set -euo pipefail; root=$1; home_path=$2; shell=powershell; client=both; update_agents=false; flashbang=false; '
|
||||
'set -euo pipefail; root=$1; home_path=$2; shell=powershell; client=both; flashbang=false; statusline=false; '
|
||||
'selected_plugins=(Superpowers); deselected_plugins=(); '
|
||||
'. "$root/scripts/lib/selection-state.sh"; save_update_selection',
|
||||
'test', ROOT.as_posix(), self.home.as_posix()],
|
||||
|
||||
Reference in new issue
Block a user