feat(update): persist interactive install choices
This commit is contained in:
1 parent
80e1ab440e
commit
9a2a606410
14 files changed
+557
-82
No files matched your search
+16
-12
@@ -2,7 +2,7 @@
|
|||||||
param(
|
param(
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||||
[ValidateSet('Windows','Linux')][string]$Platform,
|
[ValidateSet('PowerShell','Bash')][string]$Shell,
|
||||||
[switch]$Summary
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -11,9 +11,10 @@ $root = Split-Path $PSScriptRoot -Parent
|
|||||||
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
||||||
|
|
||||||
$Platform = Read-InstallPlatform -Platform $Platform
|
$Shell = Read-InstallShell -Shell $Shell
|
||||||
$platform = $Platform.ToLowerInvariant()
|
$shell = $Shell.ToLowerInvariant()
|
||||||
$Client = Read-InstallClient -Client $Client
|
$Client = Read-InstallClient -Client $Client
|
||||||
|
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
@@ -21,6 +22,8 @@ if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePat
|
|||||||
throw 'Already installed, use the update script.'
|
throw 'Already installed, use the update script.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun
|
||||||
|
|
||||||
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
||||||
& $buildScript -Summary:$Summary
|
& $buildScript -Summary:$Summary
|
||||||
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
if (-not $?) { throw 'Build failed. Installation was not started.' }
|
||||||
@@ -28,17 +31,18 @@ $generated = Join-Path $root 'generated'
|
|||||||
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
||||||
|
|
||||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
$powerShellCommand = if (Get-Command powershell -ErrorAction SilentlyContinue) { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'pwsh -NoProfile -ExecutionPolicy Bypass -File' }
|
||||||
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
|
$shellCommand = if ($Shell -eq 'PowerShell') { $powerShellCommand } else { 'bash' }
|
||||||
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
|
|
||||||
|
|
||||||
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
|
$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 }
|
||||||
|
|
||||||
|
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Shell $shell -Client $Client)) {
|
||||||
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
|
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
|
|
||||||
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Install' -DryRun:$DryRun
|
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
||||||
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
|
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang }
|
||||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Platform $Platform -Client $Client -Plugins $plugins }
|
if ($Summary) { Write-Output "Install: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
||||||
if ($Summary) { Write-Output "Install: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
|
|
||||||
exit 0
|
exit 0
|
||||||
+19
-12
@@ -3,14 +3,14 @@ set -euo pipefail
|
|||||||
|
|
||||||
dry_run=false
|
dry_run=false
|
||||||
client=""
|
client=""
|
||||||
platform=""
|
shell=""
|
||||||
summary=false
|
summary=false
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--dry-run) dry_run=true; shift ;;
|
--dry-run) dry_run=true; shift ;;
|
||||||
--summary) summary=true; shift ;;
|
--summary) summary=true; shift ;;
|
||||||
--client) client=${2:?--client requires a value}; shift 2 ;;
|
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||||
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
|
--shell) shell=${2:?--shell requires a value}; shift 2 ;;
|
||||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -20,8 +20,9 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|||||||
. "$root/scripts/lib/manifest.sh"
|
. "$root/scripts/lib/manifest.sh"
|
||||||
. "$root/scripts/lib/install-targets.sh"
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
. "$root/scripts/lib/selection-state.sh"
|
. "$root/scripts/lib/selection-state.sh"
|
||||||
|
. "$root/scripts/lib/install-options.sh"
|
||||||
|
|
||||||
platform=$(read_install_platform "$platform")
|
shell=$(read_install_shell "$shell")
|
||||||
client=$(read_install_client "$client")
|
client=$(read_install_client "$client")
|
||||||
|
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
@@ -33,6 +34,8 @@ if [ "$dry_run" = false ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
read_install_options
|
||||||
|
|
||||||
build_args=()
|
build_args=()
|
||||||
[ "$summary" = false ] || build_args+=(--summary)
|
[ "$summary" = false ] || build_args+=(--summary)
|
||||||
bash "$root/scripts/build.sh" "${build_args[@]}"
|
bash "$root/scripts/build.sh" "${build_args[@]}"
|
||||||
@@ -40,23 +43,27 @@ generated="$root/generated"
|
|||||||
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
||||||
|
|
||||||
stamp=$(date +%Y%m%d-%H%M%S)
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
powershell_command='pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
if command -v powershell >/dev/null; then powershell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'; fi
|
||||||
shell_command=bash
|
shell_command=bash
|
||||||
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
[ "$shell" != powershell ] || shell_command=$powershell_command
|
||||||
|
|
||||||
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" "$windows_shell_command" "$dry_run" "$summary"
|
|
||||||
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
|
|
||||||
|
|
||||||
selected_plugins=()
|
selected_plugins=()
|
||||||
deselected_plugins=()
|
deselected_plugins=()
|
||||||
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
|
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
|
||||||
install_configured_plugins "$root" "$client" "$dry_run" false selected_plugins "$summary"
|
[ "$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"
|
||||||
|
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
||||||
|
|
||||||
|
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
|
||||||
if [ "$dry_run" = false ]; then save_update_selection; fi
|
if [ "$dry_run" = false ]; then save_update_selection; fi
|
||||||
|
|
||||||
if [ "$summary" = true ]; then
|
if [ "$summary" = true ]; then
|
||||||
suffix=
|
suffix=
|
||||||
[ "$dry_run" = false ] || suffix=', dry-run'
|
[ "$dry_run" = false ] || suffix=', dry-run'
|
||||||
printf 'Install: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
|
printf 'Install: PASS | %s, %s%s\n' "$client" "$shell" "$suffix"
|
||||||
elif "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
elif "$dry_run"; then printf 'PASS install dry-run\n'; else printf 'PASS install\n'; fi
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
function Read-InstallBoolean {
|
||||||
|
param([string]$Prompt, [bool]$Default)
|
||||||
|
$hint = if ($Default) { 'Y/n' } else { 'y/N' }
|
||||||
|
while ($true) {
|
||||||
|
Write-Host "$Prompt [$hint] " -NoNewline
|
||||||
|
$answer = [Console]::ReadLine()
|
||||||
|
if ($null -eq $answer) { throw 'Input ended before install options were confirmed.' }
|
||||||
|
switch ($answer.Trim().ToLowerInvariant()) {
|
||||||
|
'' { return $Default }
|
||||||
|
{ $_ -in @('y', 'yes') } { return $true }
|
||||||
|
{ $_ -in @('n', 'no') } { return $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-InstallOptions {
|
||||||
|
param([string]$HomePath, [string]$RepositoryRoot, [string]$Client, [switch]$DryRun)
|
||||||
|
$defaults = @{ update_agents = $false; flashbang = $true }
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
|
||||||
|
def is_flashbang(hook):
|
||||||
|
return any(re.search(r'(?:[/\\\s"\x27]|^)flashbang\.(?:ps1|sh)(?:[\s"\x27]|$)', str(hook.get(key, '')), re.I)
|
||||||
|
for key in ('command', 'command_windows'))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_flashbang(path):
|
||||||
|
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']
|
||||||
|
return json.dumps(settings, indent=2, ensure_ascii=False) + '\n'
|
||||||
|
if path.name != 'config.toml':
|
||||||
|
raise ValueError('Flashbang 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)
|
||||||
|
tomllib.loads(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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='')
|
||||||
|
except (OSError, ValueError, KeyError) as error:
|
||||||
|
print(f'Install options: {error}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
read_install_boolean() {
|
||||||
|
local prompt=$1 default=$2 hint=y/N answer
|
||||||
|
[ "$default" = false ] || hint=Y/n
|
||||||
|
while :; do
|
||||||
|
printf '%s [%s] ' "$prompt" "$hint" >&2
|
||||||
|
IFS= read -r answer || { printf 'Input ended before install options were confirmed.\n' >&2; return 1; }
|
||||||
|
answer=${answer%$'\r'}
|
||||||
|
case "$answer" in
|
||||||
|
'') printf '%s\n' "$default"; return ;;
|
||||||
|
y|Y|yes|Yes|YES) printf 'true\n'; return ;;
|
||||||
|
n|N|no|No|NO) printf 'false\n'; return ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
read_install_options() {
|
||||||
|
update_agents=false
|
||||||
|
flashbang=true
|
||||||
|
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 ;; 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ function Get-Sha256Hash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Get-Sha256HashOfBytes {
|
function Get-Sha256HashOfBytes {
|
||||||
param([Parameter(Mandatory=$true)][byte[]]$Bytes)
|
param([Parameter(Mandatory=$true)][AllowEmptyCollection()][byte[]]$Bytes)
|
||||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||||
try { return ([System.BitConverter]::ToString($sha256.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
|
try { return ([System.BitConverter]::ToString($sha256.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
|
||||||
finally { $sha256.Dispose() }
|
finally { $sha256.Dispose() }
|
||||||
@@ -55,7 +55,8 @@ function Sync-ManagedDestination {
|
|||||||
[Parameter(Mandatory=$true)][string]$Stamp,
|
[Parameter(Mandatory=$true)][string]$Stamp,
|
||||||
[string]$AiConfigRoot,
|
[string]$AiConfigRoot,
|
||||||
[string]$ShellCommand,
|
[string]$ShellCommand,
|
||||||
[string]$WindowsShellCommand,
|
[string]$PowerShellCommand,
|
||||||
|
[bool]$FlashbangEnabled = $true,
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Summary
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
@@ -69,10 +70,15 @@ function Sync-ManagedDestination {
|
|||||||
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
Get-ChildItem $Source -File -Recurse | ForEach-Object {
|
||||||
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
$relative = $_.FullName.Substring($Source.Length).TrimStart([char[]]@('\','/')).Replace('\','/')
|
||||||
$target = Join-Path $Destination $relative
|
$target = Join-Path $Destination $relative
|
||||||
$rawContent = Get-Content -LiteralPath $_.FullName -Raw
|
$rawContent = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8
|
||||||
$needsSubstitution = $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__WINDOWS_HOOK_COMMAND__')
|
$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.' }
|
||||||
|
}
|
||||||
|
$needsSubstitution = $filterFlashbang -or $rawContent.Contains('__AI_CONFIG_ROOT__') -or $rawContent.Contains('__HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_HOOK_COMMAND__') -or $rawContent.Contains('__POWERSHELL_COMMAND__')
|
||||||
if ($needsSubstitution) {
|
if ($needsSubstitution) {
|
||||||
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__WINDOWS_HOOK_COMMAND__', $WindowsShellCommand).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__WINDOWS_HOOK_SCRIPT__', 'flashbang.ps1')
|
$finalContent = $rawContent.Replace('__AI_CONFIG_ROOT__', $AiConfigRoot).Replace('__HOOK_COMMAND__', $ShellCommand).Replace('__POWERSHELL_HOOK_COMMAND__', $PowerShellCommand).Replace('__POWERSHELL_COMMAND__', $PowerShellCommand).Replace('__HOOK_SCRIPT__', 'flashbang.ps1').Replace('__POWERSHELL_HOOK_SCRIPT__', 'flashbang.ps1')
|
||||||
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
$newBytes = [System.Text.Encoding]::UTF8.GetBytes($finalContent)
|
||||||
} else {
|
} else {
|
||||||
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
$newBytes = [System.IO.File]::ReadAllBytes($_.FullName)
|
||||||
|
|||||||
+10
-5
@@ -49,9 +49,9 @@ write_managed_manifest() {
|
|||||||
} > "$path"
|
} > "$path"
|
||||||
}
|
}
|
||||||
|
|
||||||
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <windows-shell-command> <dry-run: true|false> [summary: true|false]
|
# sync_managed_destination <source-dir> <destination-dir> <stamp> <ai-config-root> <shell-command> <powershell-command> <dry-run: true|false> [summary: true|false]
|
||||||
sync_managed_destination() {
|
sync_managed_destination() {
|
||||||
local source=$1 destination=$2 stamp=$3 ai_config_root=$4 shell_command=$5 windows_shell_command=$6 dry_run=$7 summary=${8:-false}
|
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}
|
||||||
[ "$summary" = true ] || printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
[ "$summary" = true ] || printf 'SOURCE %s -> %s\n' "$source" "$destination"
|
||||||
local manifest
|
local manifest
|
||||||
manifest=$(manifest_path "$destination")
|
manifest=$(manifest_path "$destination")
|
||||||
@@ -67,13 +67,18 @@ sync_managed_destination() {
|
|||||||
target="$destination/$relative"
|
target="$destination/$relative"
|
||||||
content=$(<"$source_file")
|
content=$(<"$source_file")
|
||||||
needs_sub=false
|
needs_sub=false
|
||||||
if [[ "$content" == *'__AI_CONFIG_ROOT__'* || "$content" == *'__HOOK_COMMAND__'* || "$content" == *'__WINDOWS_HOOK_COMMAND__'* ]]; then
|
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) || return
|
||||||
|
needs_sub=true
|
||||||
|
fi
|
||||||
|
if [[ "$needs_sub" = true || "$content" == *'__AI_CONFIG_ROOT__'* || "$content" == *'__HOOK_COMMAND__'* || "$content" == *'__POWERSHELL_HOOK_COMMAND__'* || "$content" == *'__POWERSHELL_COMMAND__'* ]]; then
|
||||||
needs_sub=true
|
needs_sub=true
|
||||||
content=${content//__AI_CONFIG_ROOT__/$ai_config_root}
|
content=${content//__AI_CONFIG_ROOT__/$ai_config_root}
|
||||||
content=${content//__HOOK_COMMAND__/$shell_command}
|
content=${content//__HOOK_COMMAND__/$shell_command}
|
||||||
content=${content//__WINDOWS_HOOK_COMMAND__/$windows_shell_command}
|
content=${content//__POWERSHELL_HOOK_COMMAND__/$powershell_command}
|
||||||
|
content=${content//__POWERSHELL_COMMAND__/$powershell_command}
|
||||||
content=${content//__HOOK_SCRIPT__/flashbang.sh}
|
content=${content//__HOOK_SCRIPT__/flashbang.sh}
|
||||||
content=${content//__WINDOWS_HOOK_SCRIPT__/flashbang.sh}
|
content=${content//__POWERSHELL_HOOK_SCRIPT__/flashbang.sh}
|
||||||
new_hash=$(sha256_of_string "$content")
|
new_hash=$(sha256_of_string "$content")
|
||||||
else
|
else
|
||||||
new_hash=$(sha256_of_file "$source_file")
|
new_hash=$(sha256_of_file "$source_file")
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
function Read-UpdateSelection {
|
function Read-UpdateSelection {
|
||||||
param([string]$HomePath, [string]$RepositoryRoot)
|
param([string]$HomePath, [string]$RepositoryRoot, [switch]$AllowLegacy)
|
||||||
$output = & python (Join-Path $PSScriptRoot 'selection-state.py') read --home $HomePath --manifest (Join-Path $RepositoryRoot 'adapters/plugins.tsv')
|
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'read', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'))
|
||||||
|
if ($AllowLegacy) { $arguments += '--allow-legacy' }
|
||||||
|
$output = & python @arguments
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Could not load the saved selection.' }
|
if ($LASTEXITCODE -ne 0) { throw 'Could not load the saved selection.' }
|
||||||
return ($output | Out-String | ConvertFrom-Json)
|
return ($output | Out-String | ConvertFrom-Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Save-UpdateSelection {
|
function Save-UpdateSelection {
|
||||||
param([string]$HomePath, [string]$RepositoryRoot, [string]$Platform, [string]$Client, [hashtable]$Plugins)
|
param([string]$HomePath, [string]$RepositoryRoot, [string]$Shell, [string]$Client, [hashtable]$Plugins,
|
||||||
$arguments = @((Join-Path $PSScriptRoot 'selection-state.py'), 'write', '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--platform', $Platform.ToLowerInvariant(), '--client', $Client.ToLowerInvariant(), '--selected')
|
[Parameter(Mandatory=$true)][bool]$UpdateAgents, [Parameter(Mandatory=$true)][bool]$Flashbang)
|
||||||
|
$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(), '--selected')
|
||||||
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
|
$arguments += @($Plugins.Selected | ForEach-Object { $_.name })
|
||||||
$arguments += '--deselected'
|
$arguments += '--deselected'
|
||||||
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
|
$arguments += @($Plugins.Deselected | ForEach-Object { $_.name })
|
||||||
|
|||||||
@@ -7,11 +7,19 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def validate(state):
|
def validate(state, allow_legacy=False):
|
||||||
if not isinstance(state, dict) or state.get('version') != 1:
|
if isinstance(state, dict) and state.get('version') == 1:
|
||||||
|
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)
|
||||||
|
state.pop('platform', None)
|
||||||
|
if not isinstance(state, dict) or state.get('version') != 2:
|
||||||
raise ValueError('Unsupported saved selection.')
|
raise ValueError('Unsupported saved selection.')
|
||||||
if state.get('platform') not in ('windows', 'linux') or state.get('client') not in ('codex', 'claude', 'both'):
|
if state.get('shell') not in ('powershell', 'bash') or state.get('client') not in ('codex', 'claude', 'both'):
|
||||||
raise ValueError('Invalid saved platform or client.')
|
raise ValueError('Invalid saved shell or client.')
|
||||||
|
if any(type(state.get(key)) is not bool for key in ('update_agents', 'flashbang')):
|
||||||
|
raise ValueError('Invalid saved install options. Run update once without Quick.')
|
||||||
for key in ('selected', 'deselected'):
|
for key in ('selected', 'deselected'):
|
||||||
values = state.get(key)
|
values = state.get(key)
|
||||||
if not isinstance(values, list) or any(not isinstance(value, str) or not value or any(ord(c) < 32 for c in value) for value in values):
|
if not isinstance(values, list) or any(not isinstance(value, str) or not value or any(ord(c) < 32 for c in value) for value in values):
|
||||||
@@ -29,7 +37,10 @@ def main():
|
|||||||
parser.add_argument('--home', required=True)
|
parser.add_argument('--home', required=True)
|
||||||
parser.add_argument('--manifest', required=True)
|
parser.add_argument('--manifest', required=True)
|
||||||
parser.add_argument('--format', choices=('json', 'tsv'), default='json')
|
parser.add_argument('--format', choices=('json', 'tsv'), default='json')
|
||||||
parser.add_argument('--platform')
|
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('--client')
|
parser.add_argument('--client')
|
||||||
parser.add_argument('--selected', nargs='*', default=[])
|
parser.add_argument('--selected', nargs='*', default=[])
|
||||||
parser.add_argument('--deselected', nargs='*', default=[])
|
parser.add_argument('--deselected', nargs='*', default=[])
|
||||||
@@ -40,19 +51,23 @@ def main():
|
|||||||
if args.action == 'read':
|
if args.action == 'read':
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise ValueError('No saved selection. Run install or update once without Quick.')
|
raise ValueError('No saved selection. Run install or update once without Quick.')
|
||||||
state = validate(json.loads(path.read_text(encoding='utf-8-sig')))
|
state = validate(json.loads(path.read_text(encoding='utf-8-sig')), args.allow_legacy)
|
||||||
for key in ('selected', 'deselected'):
|
for key in ('selected', 'deselected'):
|
||||||
state[key] = [name for name in state[key] if name in names]
|
state[key] = [name for name in state[key] if name in names]
|
||||||
if args.format == 'json':
|
if args.format == 'json':
|
||||||
print(json.dumps(state))
|
print(json.dumps(state))
|
||||||
else:
|
else:
|
||||||
print('platform\t' + state['platform'])
|
print('shell\t' + state['shell'])
|
||||||
print('client\t' + state['client'])
|
print('client\t' + state['client'])
|
||||||
|
for key in ('update_agents', 'flashbang'):
|
||||||
|
print(key + '\t' + str(state[key]).lower())
|
||||||
for key in ('selected', 'deselected'):
|
for key in ('selected', 'deselected'):
|
||||||
for name in state[key]:
|
for name in state[key]:
|
||||||
print(key + '\t' + name)
|
print(key + '\t' + name)
|
||||||
return
|
return
|
||||||
state = validate(dict(version=1, platform=args.platform, client=args.client,
|
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',
|
||||||
selected=args.selected, deselected=args.deselected))
|
selected=args.selected, deselected=args.deselected))
|
||||||
if (set(state['selected']) | set(state['deselected'])) - names:
|
if (set(state['selected']) | set(state['deselected'])) - names:
|
||||||
raise ValueError('Unknown plugin in selection.')
|
raise ValueError('Unknown plugin in selection.')
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ read_update_selection() {
|
|||||||
while IFS=$'\t' read -r key value; do
|
while IFS=$'\t' read -r key value; do
|
||||||
value=${value%$'\r'}
|
value=${value%$'\r'}
|
||||||
case "$key" in
|
case "$key" in
|
||||||
platform) [ -n "$platform" ] || platform=$value ;;
|
shell) [ -n "$shell" ] || shell=$value ;;
|
||||||
client) [ -n "$client" ] || client=$value ;;
|
client) [ -n "$client" ] || client=$value ;;
|
||||||
|
update_agents) update_agents=$value ;;
|
||||||
|
flashbang) flashbang=$value ;;
|
||||||
selected) selected_plugins+=("$value") ;;
|
selected) selected_plugins+=("$value") ;;
|
||||||
deselected) deselected_plugins+=("$value") ;;
|
deselected) deselected_plugins+=("$value") ;;
|
||||||
esac
|
esac
|
||||||
@@ -18,5 +20,6 @@ read_update_selection() {
|
|||||||
|
|
||||||
save_update_selection() {
|
save_update_selection() {
|
||||||
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
|
python3 "$root/scripts/lib/selection-state.py" write --home "$home_path" --manifest "$root/adapters/plugins.tsv" \
|
||||||
--platform "$platform" --client "$client" --selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
|
--shell "$shell" --client "$client" --update-agents "$update_agents" --flashbang "$flashbang" \
|
||||||
|
--selected "${selected_plugins[@]}" --deselected "${deselected_plugins[@]}"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import tomllib
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SPEC = importlib.util.spec_from_file_location('install_options', ROOT / 'scripts/lib/install-options.py')
|
||||||
|
OPTIONS = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(OPTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
class InstallOptionsTests(unittest.TestCase):
|
||||||
|
def test_filter_preserves_other_claude_hooks_and_status(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / 'settings.json'
|
||||||
|
settings = {'hooks': {'Stop': [{'hooks': [
|
||||||
|
{'command': 'bash "root/hooks/flashbang.sh"'}, {'command': 'echo keep'}]}],
|
||||||
|
'PreCompact': [{'hooks': [{'command': 'echo compact'}]}]},
|
||||||
|
'statusLine': {'command': 'echo status'}}
|
||||||
|
path.write_text(json.dumps(settings))
|
||||||
|
result = json.loads(OPTIONS.filter_flashbang(path))
|
||||||
|
self.assertEqual(result['hooks']['Stop'][0]['hooks'], [{'command': 'echo keep'}])
|
||||||
|
self.assertEqual(result['hooks']['PreCompact'], settings['hooks']['PreCompact'])
|
||||||
|
self.assertEqual(result['statusLine'], settings['statusLine'])
|
||||||
|
|
||||||
|
def test_filter_preserves_other_codex_stop_hooks_and_settings(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / 'config.toml'
|
||||||
|
path.write_text('approval_policy = "on-request"\n[[hooks.Stop]]\n'
|
||||||
|
'[[hooks.Stop.hooks]]\ncommand = \'bash "root/flashbang.sh"\'\n'
|
||||||
|
'[[hooks.Stop.hooks]]\ncommand = "echo keep"\n'
|
||||||
|
'[tui]\nstatus_line = ["model"]\n')
|
||||||
|
result = tomllib.loads(OPTIONS.filter_flashbang(path))
|
||||||
|
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')
|
||||||
|
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), tempfile.TemporaryDirectory() as directory:
|
||||||
|
source = Path(directory) / 'source'
|
||||||
|
source.mkdir()
|
||||||
|
destination = Path(directory) / 'destination'
|
||||||
|
settings = {'hooks': {'Stop': [{'hooks': [{'command': 'bash "__AI_CONFIG_ROOT__/flashbang.sh"'}]}],
|
||||||
|
'PreCompact': [{'hooks': [{'command': 'echo keep'}]}]}}
|
||||||
|
(source / 'settings.json').write_text(json.dumps(settings))
|
||||||
|
(source / 'config.toml').write_text('[[hooks.Stop]]\n[[hooks.Stop.hooks]]\n'
|
||||||
|
'command = \'bash "__AI_CONFIG_ROOT__/flashbang.sh"\'\n')
|
||||||
|
environment = dict(os.environ, OPTION_SOURCE=str(source), OPTION_DESTINATION=str(destination))
|
||||||
|
for enabled in (False, True):
|
||||||
|
if kind == 'powershell':
|
||||||
|
command = [executable, '-NoProfile', '-Command',
|
||||||
|
"$ErrorActionPreference='Stop'; . ./scripts/lib/manifest.ps1; "
|
||||||
|
"Sync-ManagedDestination -Source $env:OPTION_SOURCE -Destination $env:OPTION_DESTINATION "
|
||||||
|
"-Stamp test -AiConfigRoot root -ShellCommand bash -PowerShellCommand pwsh "
|
||||||
|
"-Summary -FlashbangEnabled $" + str(enabled).lower()]
|
||||||
|
else:
|
||||||
|
command = [executable, '-c',
|
||||||
|
'set -euo pipefail; . ./scripts/lib/manifest.sh; '
|
||||||
|
'sync_managed_destination "$OPTION_SOURCE" "$OPTION_DESTINATION" test root bash pwsh false true '
|
||||||
|
+ str(enabled).lower()]
|
||||||
|
environment.update(OPTION_SOURCE=source.as_posix(), OPTION_DESTINATION=destination.as_posix())
|
||||||
|
result = subprocess.run(command, cwd=ROOT, env=environment, capture_output=True, text=True, timeout=20)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
installed = json.loads((destination / 'settings.json').read_text(encoding='utf-8-sig'))
|
||||||
|
self.assertEqual('Stop' in installed['hooks'], enabled)
|
||||||
|
self.assertIn('PreCompact', installed['hooks'])
|
||||||
|
codex = tomllib.loads((destination / 'config.toml').read_text(encoding='utf-8-sig'))
|
||||||
|
self.assertEqual(bool(codex.get('hooks', {}).get('Stop')), enabled)
|
||||||
|
self.assertTrue((destination / '.ai-config-manifest.tsv').is_file())
|
||||||
|
|
||||||
|
def test_entrypoints_save_normal_choices_and_replay_quick_without_prompts(self):
|
||||||
|
executables = [('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 executables:
|
||||||
|
if not executable or not Path(executable).exists():
|
||||||
|
continue
|
||||||
|
with self.subTest(shell=kind), tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory) / 'repo'
|
||||||
|
scripts = root / 'scripts'
|
||||||
|
shutil.copytree(ROOT / 'scripts/lib', scripts / 'lib', ignore=shutil.ignore_patterns('__pycache__'))
|
||||||
|
home = Path(directory) / 'home'
|
||||||
|
home.mkdir()
|
||||||
|
(root / 'adapters').mkdir()
|
||||||
|
shutil.copyfile(ROOT / 'adapters/plugins.tsv', root / 'adapters/plugins.tsv')
|
||||||
|
for client in ('codex', 'claude'):
|
||||||
|
package = root / 'generated' / (client + '-bash')
|
||||||
|
(package / 'skills').mkdir(parents=True)
|
||||||
|
(package / 'skills' / 'test.txt').write_text('managed')
|
||||||
|
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"}]}]}}')
|
||||||
|
(package / ('config.toml' if client == 'codex' else 'settings.json')).write_text(config)
|
||||||
|
suffix = 'ps1' if kind == 'powershell' else 'sh'
|
||||||
|
for name in ('install', 'update'):
|
||||||
|
content = (ROOT / 'scripts' / (name + '.' + suffix)).read_text(encoding='utf-8-sig')
|
||||||
|
content = content.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_INSTALL_HOME')
|
||||||
|
(scripts / (name + '.' + suffix)).write_text(content)
|
||||||
|
(scripts / ('build.' + suffix)).write_text('param([switch]$Summary)' if kind == 'powershell' else 'exit 0\n')
|
||||||
|
if kind == 'powershell':
|
||||||
|
(scripts / 'lib/plugins.ps1').write_text(
|
||||||
|
'function Select-ConfiguredPlugins { @{Selected=@(); Deselected=@()} }\n'
|
||||||
|
'function Get-ConfiguredPluginEntries { @() }\n'
|
||||||
|
'function Sync-ConfiguredPlugins {}\n'
|
||||||
|
'function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) '
|
||||||
|
'Write-Output "CLI-MOCK $Command" }\n')
|
||||||
|
else:
|
||||||
|
(scripts / 'lib/plugins.sh').write_text(
|
||||||
|
'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\n'), ('update', True, '')]:
|
||||||
|
entry = str(scripts / (name + '.' + suffix))
|
||||||
|
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
|
||||||
|
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
|
||||||
|
if quick:
|
||||||
|
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)
|
||||||
|
if quick:
|
||||||
|
self.assertNotIn('Enable the Flashbang', 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.assertEqual(state['shell'], 'bash')
|
||||||
|
self.assertNotIn('Stop', json.loads((home / '.claude/settings.json').read_text()).get('hooks', {}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -25,7 +25,7 @@ class SelectionTests(unittest.TestCase):
|
|||||||
capture_output=True, text=True, timeout=10)
|
capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
def save(self, *arguments):
|
def save(self, *arguments):
|
||||||
result = self.run_state('write', '--platform', 'windows', '--client', 'both', *arguments)
|
result = self.run_state('write', '--shell', 'powershell', '--update-agents', 'false', '--flashbang', 'false', '--client', 'both', *arguments)
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
def test_missing_selection_fails_without_creating_state(self):
|
def test_missing_selection_fails_without_creating_state(self):
|
||||||
@@ -38,7 +38,7 @@ class SelectionTests(unittest.TestCase):
|
|||||||
self.save('--selected', 'Superpowers', 'Anthropic Frontend Design', '--deselected', 'Context7')
|
self.save('--selected', 'Superpowers', 'Anthropic Frontend Design', '--deselected', 'Context7')
|
||||||
original = self.path.read_bytes()
|
original = self.path.read_bytes()
|
||||||
state = json.loads(self.run_state('read').stdout)
|
state = json.loads(self.run_state('read').stdout)
|
||||||
self.assertEqual(state['platform'], 'windows')
|
self.assertEqual(state['shell'], 'powershell')
|
||||||
self.assertEqual(state['client'], 'both')
|
self.assertEqual(state['client'], 'both')
|
||||||
self.assertEqual(state['selected'], ['Superpowers', 'Anthropic Frontend Design'])
|
self.assertEqual(state['selected'], ['Superpowers', 'Anthropic Frontend Design'])
|
||||||
self.assertEqual(state['deselected'], ['Context7'])
|
self.assertEqual(state['deselected'], ['Context7'])
|
||||||
@@ -51,10 +51,30 @@ class SelectionTests(unittest.TestCase):
|
|||||||
self.save()
|
self.save()
|
||||||
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], [])
|
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], [])
|
||||||
|
|
||||||
|
def test_legacy_state_requires_normal_update_and_can_supply_defaults(self):
|
||||||
|
self.path.parent.mkdir()
|
||||||
|
self.path.write_text(json.dumps(dict(version=1, platform='windows', client='both', selected=[], deselected=[])))
|
||||||
|
result = self.run_state('read')
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
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'])
|
||||||
|
|
||||||
|
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'])
|
||||||
|
state['flashbang'] = 'false'
|
||||||
|
self.path.write_text(json.dumps(state))
|
||||||
|
self.assertNotEqual(self.run_state('read').returncode, 0)
|
||||||
|
|
||||||
def test_invalid_write_preserves_last_selection(self):
|
def test_invalid_write_preserves_last_selection(self):
|
||||||
self.save('--selected', 'Superpowers')
|
self.save('--selected', 'Superpowers')
|
||||||
original = self.path.read_bytes()
|
original = self.path.read_bytes()
|
||||||
result = self.run_state('write', '--platform', 'linux', '--client', 'codex', '--selected', 'Unknown')
|
result = self.run_state('write', '--shell', 'bash', '--update-agents', 'false', '--flashbang', 'true', '--client', 'codex', '--selected', 'Unknown')
|
||||||
self.assertNotEqual(result.returncode, 0)
|
self.assertNotEqual(result.returncode, 0)
|
||||||
self.assertEqual(self.path.read_bytes(), original)
|
self.assertEqual(self.path.read_bytes(), original)
|
||||||
|
|
||||||
@@ -80,7 +100,7 @@ class SelectionTests(unittest.TestCase):
|
|||||||
save = subprocess.run([shell, '-NoProfile', '-Command',
|
save = subprocess.run([shell, '-NoProfile', '-Command',
|
||||||
"$ErrorActionPreference = 'Stop'; . (Join-Path $env:TEST_REPOSITORY_ROOT 'scripts/lib/selection-state.ps1'); "
|
"$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 "
|
"Save-UpdateSelection -HomePath $env:TEST_SELECTION_HOME -RepositoryRoot $env:TEST_REPOSITORY_ROOT "
|
||||||
"-Platform Windows -Client Both -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
|
"-Shell PowerShell -Client Both -UpdateAgents $false -Flashbang $false -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
|
||||||
env=environment, capture_output=True, text=True, timeout=10)
|
env=environment, capture_output=True, text=True, timeout=10)
|
||||||
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
|
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
|
||||||
original = self.path.read_bytes()
|
original = self.path.read_bytes()
|
||||||
@@ -95,8 +115,8 @@ class SelectionTests(unittest.TestCase):
|
|||||||
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
||||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
self.assertNotIn('Select plugins', result.stdout)
|
self.assertNotIn('Select plugins', result.stdout)
|
||||||
self.assertIn('codex plugin add superpowers@openai-curated-remote', result.stdout)
|
self.assertIn('DRYRUN ensure codex plugin: Superpowers', result.stdout)
|
||||||
self.assertNotIn('codex plugin add context7', result.stdout)
|
self.assertNotIn('DRYRUN ensure codex plugin: Context7', result.stdout)
|
||||||
self.assertEqual(self.path.read_bytes(), original)
|
self.assertEqual(self.path.read_bytes(), original)
|
||||||
self.assertFalse((self.home / '.codex').exists())
|
self.assertFalse((self.home / '.codex').exists())
|
||||||
failed = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick'], env=environment,
|
failed = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick'], env=environment,
|
||||||
@@ -112,7 +132,7 @@ class SelectionTests(unittest.TestCase):
|
|||||||
if not shell:
|
if not shell:
|
||||||
self.skipTest('Bash unavailable')
|
self.skipTest('Bash unavailable')
|
||||||
save = subprocess.run([shell, '-c',
|
save = subprocess.run([shell, '-c',
|
||||||
'set -euo pipefail; root=$1; home_path=$2; platform=windows; client=both; '
|
'set -euo pipefail; root=$1; home_path=$2; shell=powershell; client=both; update_agents=false; flashbang=false; '
|
||||||
'selected_plugins=(Superpowers); deselected_plugins=(); '
|
'selected_plugins=(Superpowers); deselected_plugins=(); '
|
||||||
'. "$root/scripts/lib/selection-state.sh"; save_update_selection',
|
'. "$root/scripts/lib/selection-state.sh"; save_update_selection',
|
||||||
'test', ROOT.as_posix(), self.home.as_posix()],
|
'test', ROOT.as_posix(), self.home.as_posix()],
|
||||||
@@ -124,8 +144,8 @@ class SelectionTests(unittest.TestCase):
|
|||||||
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
||||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
self.assertNotIn('Select plugins', result.stdout)
|
self.assertNotIn('Select plugins', result.stdout)
|
||||||
self.assertIn('codex plugin add superpowers@openai-curated-remote', result.stdout)
|
self.assertIn('DRYRUN ensure codex plugin: Superpowers', result.stdout)
|
||||||
self.assertNotIn('codex plugin add context7', result.stdout)
|
self.assertNotIn('DRYRUN ensure codex plugin: Context7', result.stdout)
|
||||||
self.assertEqual(self.path.read_bytes(), original)
|
self.assertEqual(self.path.read_bytes(), original)
|
||||||
self.assertFalse((self.home / '.codex').exists())
|
self.assertFalse((self.home / '.codex').exists())
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -3,7 +3,7 @@ param(
|
|||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Quick,
|
[switch]$Quick,
|
||||||
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||||
[ValidateSet('Windows','Linux')][string]$Platform,
|
[ValidateSet('PowerShell','Bash')][string]$Shell,
|
||||||
[switch]$Summary
|
[switch]$Summary
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -12,22 +12,25 @@ $root = Split-Path $PSScriptRoot -Parent
|
|||||||
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
|
||||||
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
. (Join-Path $PSScriptRoot 'lib/selection-state.ps1')
|
||||||
|
. (Join-Path $PSScriptRoot 'lib/install-options.ps1')
|
||||||
|
|
||||||
$homePath = [Environment]::GetFolderPath('UserProfile')
|
$homePath = [Environment]::GetFolderPath('UserProfile')
|
||||||
if ($Quick) {
|
if ($Quick) {
|
||||||
$saved = Read-UpdateSelection -HomePath $homePath -RepositoryRoot $root
|
$saved = Read-UpdateSelection -HomePath $homePath -RepositoryRoot $root
|
||||||
if (-not $Platform) { $Platform = $saved.platform }
|
if (-not $Shell) { $Shell = $saved.shell }
|
||||||
if (-not $Client) { $Client = $saved.client }
|
if (-not $Client) { $Client = $saved.client }
|
||||||
}
|
}
|
||||||
|
|
||||||
$Platform = Read-InstallPlatform -Platform $Platform
|
$Shell = Read-InstallShell -Shell $Shell
|
||||||
$platform = $Platform.ToLowerInvariant()
|
$shell = $Shell.ToLowerInvariant()
|
||||||
$Client = Read-InstallClient -Client $Client
|
$Client = Read-InstallClient -Client $Client
|
||||||
|
|
||||||
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
|
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
|
||||||
throw 'Not installed, use the install script.'
|
throw 'Not installed, use the install script.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$options = if ($Quick) { $saved } else { Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun }
|
||||||
|
|
||||||
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
|
||||||
& $buildScript -Summary:$Summary
|
& $buildScript -Summary:$Summary
|
||||||
if (-not $?) { throw 'Build failed. Update was not started.' }
|
if (-not $?) { throw 'Build failed. Update was not started.' }
|
||||||
@@ -35,20 +38,21 @@ $generated = Join-Path $root 'generated'
|
|||||||
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
|
||||||
|
|
||||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
$shellCommand = 'pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
$powerShellCommand = if (Get-Command powershell -ErrorAction SilentlyContinue) { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'pwsh -NoProfile -ExecutionPolicy Bypass -File' }
|
||||||
$windowsShellCommand = 'powershell -NoProfile -ExecutionPolicy Bypass -File'
|
$shellCommand = if ($Shell -eq 'PowerShell') { $powerShellCommand } else { 'bash' }
|
||||||
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $shellCommand = $windowsShellCommand }
|
|
||||||
|
|
||||||
$plugins = if ($Quick) {
|
$plugins = if ($Quick) {
|
||||||
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $root)
|
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $root)
|
||||||
@{ Selected = @($entries | Where-Object { $saved.selected -contains $_.name }); Deselected = @($entries | Where-Object { $saved.deselected -contains $_.name }) }
|
@{ Selected = @($entries | Where-Object { $saved.selected -contains $_.name }); Deselected = @($entries | Where-Object { $saved.deselected -contains $_.name }) }
|
||||||
} else {
|
} else {
|
||||||
Select-ConfiguredPlugins -RepositoryRoot $root -Client $Client -Mode 'Update' -DryRun:$DryRun
|
Select-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -Mode 'Update' -DryRun:$DryRun
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Platform $platform -Client $Client)) {
|
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 `
|
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
|
||||||
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -WindowsShellCommand $windowsShellCommand -DryRun:$DryRun -Summary:$Summary
|
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
|
||||||
}
|
}
|
||||||
|
|
||||||
$previousPluginNonInteractive = $script:PluginNonInteractive
|
$previousPluginNonInteractive = $script:PluginNonInteractive
|
||||||
@@ -56,12 +60,11 @@ $previousCI = $env:CI
|
|||||||
try {
|
try {
|
||||||
$script:PluginNonInteractive = [bool]$Quick
|
$script:PluginNonInteractive = [bool]$Quick
|
||||||
if ($Quick) { $env:CI = 'true' }
|
if ($Quick) { $env:CI = 'true' }
|
||||||
Install-ConfiguredPlugins -RepositoryRoot $root -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected
|
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected
|
||||||
Uninstall-DeselectedPlugins -Entries $plugins.Deselected -Client $Client -DryRun:$DryRun -Summary:$Summary
|
|
||||||
} finally {
|
} finally {
|
||||||
$script:PluginNonInteractive = $previousPluginNonInteractive
|
$script:PluginNonInteractive = $previousPluginNonInteractive
|
||||||
$env:CI = $previousCI
|
$env:CI = $previousCI
|
||||||
}
|
}
|
||||||
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Platform $Platform -Client $Client -Plugins $plugins }
|
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang }
|
||||||
if ($Summary) { Write-Output "Update: PASS | $Client, $Platform$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS update dry-run' } else { 'PASS update' })) }
|
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
|
exit 0
|
||||||
+16
-11
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
dry_run=false
|
dry_run=false
|
||||||
quick=false
|
quick=false
|
||||||
client=""
|
client=""
|
||||||
platform=""
|
shell=""
|
||||||
summary=false
|
summary=false
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -12,7 +12,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--quick) quick=true; shift ;;
|
--quick) quick=true; shift ;;
|
||||||
--summary) summary=true; shift ;;
|
--summary) summary=true; shift ;;
|
||||||
--client) client=${2:?--client requires a value}; shift 2 ;;
|
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||||
--platform) platform=${2:?--platform requires a value}; shift 2 ;;
|
--shell) shell=${2:?--shell requires a value}; shift 2 ;;
|
||||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -22,13 +22,14 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|||||||
. "$root/scripts/lib/manifest.sh"
|
. "$root/scripts/lib/manifest.sh"
|
||||||
. "$root/scripts/lib/install-targets.sh"
|
. "$root/scripts/lib/install-targets.sh"
|
||||||
. "$root/scripts/lib/selection-state.sh"
|
. "$root/scripts/lib/selection-state.sh"
|
||||||
|
. "$root/scripts/lib/install-options.sh"
|
||||||
|
|
||||||
home_path=${HOME:?HOME is required}
|
home_path=${HOME:?HOME is required}
|
||||||
selected_plugins=()
|
selected_plugins=()
|
||||||
deselected_plugins=()
|
deselected_plugins=()
|
||||||
if [ "$quick" = true ]; then read_update_selection; fi
|
if [ "$quick" = true ]; then read_update_selection; fi
|
||||||
|
|
||||||
platform=$(read_install_platform "$platform")
|
shell=$(read_install_shell "$shell")
|
||||||
client=$(read_install_client "$client")
|
client=$(read_install_client "$client")
|
||||||
|
|
||||||
if [ "$dry_run" = false ]; then
|
if [ "$dry_run" = false ]; then
|
||||||
@@ -39,6 +40,8 @@ if [ "$dry_run" = false ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$quick" = false ]; then read_install_options; fi
|
||||||
|
|
||||||
build_args=()
|
build_args=()
|
||||||
[ "$summary" = false ] || build_args+=(--summary)
|
[ "$summary" = false ] || build_args+=(--summary)
|
||||||
bash "$root/scripts/build.sh" "${build_args[@]}"
|
bash "$root/scripts/build.sh" "${build_args[@]}"
|
||||||
@@ -46,30 +49,32 @@ generated="$root/generated"
|
|||||||
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
[ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; }
|
||||||
|
|
||||||
stamp=$(date +%Y%m%d-%H%M%S)
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
powershell_command='pwsh -NoProfile -ExecutionPolicy Bypass -File'
|
||||||
|
if command -v powershell >/dev/null; then powershell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'; fi
|
||||||
shell_command=bash
|
shell_command=bash
|
||||||
windows_shell_command='powershell -NoProfile -ExecutionPolicy Bypass -File'
|
[ "$shell" != powershell ] || shell_command=$powershell_command
|
||||||
|
|
||||||
if [ "$quick" = false ]; then
|
if [ "$quick" = false ]; then
|
||||||
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
|
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
[ "$update_agents" = false ] || update_selected_agent_clis
|
||||||
|
|
||||||
while IFS='|' read -r source destination; do
|
while IFS='|' read -r source destination; do
|
||||||
ai_config_root=$destination
|
ai_config_root=$destination
|
||||||
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
|
||||||
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$windows_shell_command" "$dry_run" "$summary"
|
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang"
|
||||||
done < <(get_install_targets "$generated" "$home_path" "$platform" "$client")
|
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
|
||||||
|
|
||||||
if [ "$quick" = true ]; then
|
if [ "$quick" = true ]; then
|
||||||
CI=true install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary" </dev/null
|
CI=true sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary" </dev/null
|
||||||
CI=true uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary" </dev/null
|
|
||||||
else
|
else
|
||||||
install_configured_plugins "$root" "$client" "$dry_run" true selected_plugins "$summary"
|
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary"
|
||||||
uninstall_deselected_plugins "$root" "$client" "$dry_run" deselected_plugins "$summary"
|
|
||||||
fi
|
fi
|
||||||
if [ "$dry_run" = false ]; then save_update_selection; fi
|
if [ "$dry_run" = false ]; then save_update_selection; fi
|
||||||
|
|
||||||
if [ "$summary" = true ]; then
|
if [ "$summary" = true ]; then
|
||||||
suffix=
|
suffix=
|
||||||
[ "$dry_run" = false ] || suffix=', dry-run'
|
[ "$dry_run" = false ] || suffix=', dry-run'
|
||||||
printf 'Update: PASS | %s, %s%s\n' "$client" "$platform" "$suffix"
|
printf 'Update: PASS | %s, %s%s\n' "$client" "$shell" "$suffix"
|
||||||
elif "$dry_run"; then printf 'PASS update dry-run\n'; else printf 'PASS update\n'; fi
|
elif "$dry_run"; then printf 'PASS update dry-run\n'; else printf 'PASS update\n'; fi
|
||||||
Reference in new issue
Block a user