diff --git a/scripts/validate-config.py b/scripts/checks/validate-config.py similarity index 94% rename from scripts/validate-config.py rename to scripts/checks/validate-config.py index b840be6..c2f40a8 100644 --- a/scripts/validate-config.py +++ b/scripts/checks/validate-config.py @@ -11,7 +11,7 @@ from pathlib import Path CLAUDE_TOP_LEVEL = {"permissions", "sandbox", "env", "statusLine", "hooks"} CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"} -CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands"} +CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands", "failIfUnavailable"} CLAUDE_STATUS_LINE_KEYS = {"type", "command"} CLAUDE_HOOK_KEYS = {"type", "command", "timeout"} CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"} @@ -40,8 +40,8 @@ def validate_claude(path: Path) -> None: if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]): fail(f"permissions.{key} must be an array of strings") sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox") - if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False: - fail("sandbox must be enabled without unsandboxed command retries") + if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False or sandbox.get("failIfUnavailable") is not True: + fail("sandbox must be enabled, fail closed, and disallow unsandboxed command retries") env = settings.get("env") if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()): fail("env must be an object of string values") diff --git a/scripts/build.ps1 b/scripts/commands/build.ps1 similarity index 98% rename from scripts/build.ps1 rename to scripts/commands/build.ps1 index 99e9089..5f403ea 100644 --- a/scripts/build.ps1 +++ b/scripts/commands/build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $shared = Join-Path $root 'shared' $output = Join-Path $root 'generated' $pluginManifest = Join-Path $root 'adapters/plugins.tsv' @@ -202,7 +202,7 @@ foreach ($shell in @('powershell','bash')) { $tomlPath = Join-Path $output "codex-$shell/config.toml" $settingsPath = Join-Path $output "claude-$shell/settings.json" - & python (Join-Path $root 'scripts/validate-config.py') $tomlPath $settingsPath + & python (Join-Path $root 'scripts/checks/validate-config.py') $tomlPath $settingsPath if ($LASTEXITCODE -ne 0) { throw "Generated configuration validation failed for $shell." } Test-CodexSchema $tomlPath } diff --git a/scripts/build.sh b/scripts/commands/build.sh old mode 100755 new mode 100644 similarity index 97% rename from scripts/build.sh rename to scripts/commands/build.sh index 4025f62..884bd18 --- a/scripts/build.sh +++ b/scripts/commands/build.sh @@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) shared="$root/shared" output="$root/generated" plugin_manifest="$root/adapters/plugins.tsv" @@ -85,7 +85,7 @@ quote_toml() { printf '"%s"' "$value" } -session_output=$(bash "$shared/hooks/scripts/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; } +session_output=$(bash "$shared/hooks/scripts/tests/test-session-config.sh" "$root") || { printf '%s\n' "$session_output" >&2; exit 1; } if [ "$summary" = true ]; then printf '%s\n' "$session_output" | sed '/^PASS session configuration$/d' else @@ -213,7 +213,7 @@ done toml_path="$output/codex-$shell/config.toml" settings_path="$output/claude-$shell/settings.json" -python3 "$root/scripts/validate-config.py" "$toml_path" "$settings_path" +python3 "$root/scripts/checks/validate-config.py" "$toml_path" "$settings_path" if command -v codex >/dev/null; then schema_home=$(mktemp -d) cp "$toml_path" "$schema_home/config.toml" diff --git a/scripts/check-capabilities.ps1 b/scripts/commands/check-capabilities.ps1 similarity index 100% rename from scripts/check-capabilities.ps1 rename to scripts/commands/check-capabilities.ps1 diff --git a/scripts/check-capabilities.sh b/scripts/commands/check-capabilities.sh old mode 100755 new mode 100644 similarity index 100% rename from scripts/check-capabilities.sh rename to scripts/commands/check-capabilities.sh diff --git a/scripts/doctor.ps1 b/scripts/commands/doctor.ps1 similarity index 98% rename from scripts/doctor.ps1 rename to scripts/commands/doctor.ps1 index 24b0322..5815049 100644 --- a/scripts/doctor.ps1 +++ b/scripts/commands/doctor.ps1 @@ -1,8 +1,8 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/manifest.ps1') $homePath = [Environment]::GetFolderPath('UserProfile') $script:fail = $false $script:checkCount = 0 diff --git a/scripts/doctor.sh b/scripts/commands/doctor.sh old mode 100755 new mode 100644 similarity index 98% rename from scripts/doctor.sh rename to scripts/commands/doctor.sh index f76f69f..d13296d --- a/scripts/doctor.sh +++ b/scripts/commands/doctor.sh @@ -9,7 +9,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/manifest.sh" home_path=${HOME:?HOME is required} failed=false diff --git a/scripts/install.ps1 b/scripts/commands/install.ps1 similarity index 87% rename from scripts/install.ps1 rename to scripts/commands/install.ps1 index d06ac2c..5ecac57 100644 --- a/scripts/install.ps1 +++ b/scripts/commands/install.ps1 @@ -6,13 +6,13 @@ param( [switch]$Summary ) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/plugins.ps1') -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') -. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') -. (Join-Path $PSScriptRoot 'lib/selection-state.ps1') -. (Join-Path $PSScriptRoot 'lib/install-options.ps1') -. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/plugins.ps1') +. (Join-Path $root 'scripts/lib/manifest.ps1') +. (Join-Path $root 'scripts/lib/install-targets.ps1') +. (Join-Path $root 'scripts/lib/selection-state.ps1') +. (Join-Path $root 'scripts/lib/install-options.ps1') +. (Join-Path $root 'scripts/lib/semantic-retrieval.ps1') $Shell = Read-InstallShell -Shell $Shell $shell = $Shell.ToLowerInvariant() @@ -26,7 +26,7 @@ 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 $PSScriptRoot 'build.ps1' +$buildScript = Join-Path $root 'scripts/commands/build.ps1' & $buildScript -Summary:$Summary if (-not $?) { throw 'Build failed. Installation was not started.' } $generated = Join-Path $root 'generated' diff --git a/scripts/install.sh b/scripts/commands/install.sh old mode 100755 new mode 100644 similarity index 96% rename from scripts/install.sh rename to scripts/commands/install.sh index c8cc467..ab796dd --- a/scripts/install.sh +++ b/scripts/commands/install.sh @@ -15,7 +15,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/install-targets.sh" @@ -40,7 +40,7 @@ reinstall_via_npm=$(read_cli_reinstall_option "$client" "$dry_run") build_args=() [ "$summary" = false ] || build_args+=(--summary) -bash "$root/scripts/build.sh" "${build_args[@]}" +bash "$root/scripts/commands/build.sh" "${build_args[@]}" generated="$root/generated" [ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; } diff --git a/scripts/prompt-budget.ps1 b/scripts/commands/prompt-budget.ps1 similarity index 95% rename from scripts/prompt-budget.ps1 rename to scripts/commands/prompt-budget.ps1 index feca023..93d370e 100644 --- a/scripts/prompt-budget.ps1 +++ b/scripts/commands/prompt-budget.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $generated = Join-Path $root 'generated' if (-not (Test-Path (Join-Path $generated 'claude-bash/CLAUDE.md'))) { throw 'Generated output is missing; run build first.' } function Measure-Tree([string]$path) { @@ -30,7 +30,7 @@ if ($old) { } } if ($Summary) { Write-Output 'Prompt Budget'; foreach ($row in $rows | Select-Object -First 2) { $limits = $baselineValues[$row.Name]; $base = if($limits){$limits[0]}else{'-'}; $absolute = if($limits){$limits[1]}else{'-'}; Write-Output ('{0,-28} {1,8} bytes baseline {2} relative 115% absolute {3}' -f $row.Name,$row.Bytes,$base,$absolute) }; $rows | Select-Object -Skip 2 | ForEach-Object { Write-Output ('{0,-28} {1,8} bytes {2,6} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } else { $rows | ForEach-Object { Write-Output ('{0}: {1} bytes / {2} tokens' -f $_.Name,$_.Bytes,$_.Tokens) } } -& python (Join-Path $PSScriptRoot 'lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile')) +& python (Join-Path $root 'scripts/lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile')) if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' } if ($fail) { exit 1 } exit 0 diff --git a/scripts/prompt-budget.sh b/scripts/commands/prompt-budget.sh old mode 100755 new mode 100644 similarity index 97% rename from scripts/prompt-budget.sh rename to scripts/commands/prompt-budget.sh index 14b1456..f7dda63 --- a/scripts/prompt-budget.sh +++ b/scripts/commands/prompt-budget.sh @@ -2,7 +2,7 @@ set -euo pipefail summary=false for arg in "$@"; do case "$arg" in --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) generated="$root/generated" [ -f "$generated/claude-bash/CLAUDE.md" ] || { printf 'Generated output is missing; run build first.\n' >&2; exit 1; } bytes() { wc -c < "$1" | tr -d ' '; } diff --git a/scripts/repository-context.ps1 b/scripts/commands/repository-context.ps1 similarity index 98% rename from scripts/repository-context.ps1 rename to scripts/commands/repository-context.ps1 index 0412f0d..bb45318 100644 --- a/scripts/repository-context.ps1 +++ b/scripts/commands/repository-context.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param([switch]$Force,[switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $cacheDir = Join-Path $root '.ai-session' $cachePath = Join-Path $cacheDir 'repository-context.json' $safeRoot = $root.Replace('\','/') diff --git a/scripts/repository-context.sh b/scripts/commands/repository-context.sh old mode 100755 new mode 100644 similarity index 93% rename from scripts/repository-context.sh rename to scripts/commands/repository-context.sh index bc2ed65..94b1a16 --- a/scripts/repository-context.sh +++ b/scripts/commands/repository-context.sh @@ -2,7 +2,7 @@ set -euo pipefail force=false; summary=false for arg in "$@"; do case "$arg" in --force) force=true;; --summary) summary=true;; *) printf 'Unknown argument: %s\n' "$arg" >&2; exit 1;; esac; done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json" +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd); cache_dir="$root/.ai-session"; cache_path="$cache_dir/repository-context.json" head=$(git -c "safe.directory=$root" -C "$root" rev-parse HEAD 2>/dev/null || true); key_input=$head while IFS= read -r file; do key_input+="\n$file:$(sha256sum "$file" | awk '{print $1}')"; done < <(find "$root" -type f \( -name AI-Instructions.md -o -path '*/adapters/*' \) ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/.ai-session/*' | sort) key=$(printf '%s' "$key_input" | sha256sum | awk '{print $1}') diff --git a/scripts/session-state.ps1 b/scripts/commands/session-state.ps1 similarity index 92% rename from scripts/session-state.ps1 rename to scripts/commands/session-state.ps1 index 077358b..332f02a 100644 --- a/scripts/session-state.ps1 +++ b/scripts/commands/session-state.ps1 @@ -12,7 +12,7 @@ param( [switch]$Reset ) $ErrorActionPreference = 'Stop' -$helper = Join-Path (Split-Path $PSScriptRoot -Parent) 'shared/hooks/scripts/session-state.py' +$helper = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'shared/hooks/scripts/session-state.py' $values = @{ workspace = $Workspace } $fields = @{ Goal = 'goal'; Decisions = 'decisions'; ChangedFiles = 'changed'; Verified = 'verified'; Pending = 'open'; Risks = 'risks' } foreach ($parameter in $fields.Keys) { diff --git a/scripts/session-state.sh b/scripts/commands/session-state.sh old mode 100755 new mode 100644 similarity index 76% rename from scripts/session-state.sh rename to scripts/commands/session-state.sh index d13e9e0..b725bf3 --- a/scripts/session-state.sh +++ b/scripts/commands/session-state.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) arguments=() if [ "${AI_TASK_GOAL+x}" = x ]; then arguments+=(--goal "$AI_TASK_GOAL"); fi python3 "$root/shared/hooks/scripts/session-state.py" "${arguments[@]}" "$@" diff --git a/scripts/telemetry.ps1 b/scripts/commands/telemetry.ps1 similarity index 92% rename from scripts/telemetry.ps1 rename to scripts/commands/telemetry.ps1 index 8b81eaf..6b08b14 100644 --- a/scripts/telemetry.ps1 +++ b/scripts/commands/telemetry.ps1 @@ -2,7 +2,7 @@ param([ValidateSet('record','summary','rotate')][string]$Action='record',[string]$Event,[string]$Agent,[string]$Role,[Nullable[int]]$DurationMs,[Nullable[int]]$ToolCalls,[string]$CommandClass,[Nullable[int64]]$Bytes,[Nullable[int]]$Count,[Nullable[int]]$HandoffBytes,[Nullable[int]]$FilesRead,[Nullable[int]]$FilesChanged,[string]$Outcome,[Nullable[int]]$InputTokens,[Nullable[int]]$CachedTokens,[Nullable[int]]$OutputTokens,[Nullable[int]]$ReasoningTokens) $ErrorActionPreference = 'Stop'; if ($env:AI_CONFIG_TELEMETRY -eq '0') { exit 0 } try { - $root = Split-Path $PSScriptRoot -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null + $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent; $dir = Join-Path $root '.ai-session'; $path = Join-Path $dir 'telemetry.jsonl'; New-Item $dir -ItemType Directory -Force | Out-Null if ($Action -eq 'rotate') { if (Test-Path $path) { Move-Item $path ($path + '.1') -Force }; exit 0 } if ($Action -eq 'summary') { if (-not (Test-Path $path)) { Write-Output 'Telemetry: no events'; exit 0 }; $events=@(Get-Content $path | ForEach-Object { try { $_ | ConvertFrom-Json } catch {} }); Write-Output ('Telemetry: {0} events' -f $events.Count); foreach($group in @($events | Where-Object role | Group-Object role | Sort-Object Name)){ $inputs=@($group.Group|Where-Object tokenUsage|ForEach-Object {$_.tokenUsage.input}|Where-Object {$_ -is [int] -or $_ -is [long]}|Sort-Object); $median=if($inputs.Count){$inputs[[int][Math]::Floor($inputs.Count/2)]}else{'-'}; Write-Output ('{0}: runs {1} findings {2} median input {3}' -f $group.Name,$group.Count,@($group.Group|Where-Object outcome -eq findings).Count,$median)}; exit 0 } if ([string]::IsNullOrWhiteSpace($Event)) { throw 'Event is required.' } diff --git a/scripts/telemetry.sh b/scripts/commands/telemetry.sh old mode 100755 new mode 100644 similarity index 93% rename from scripts/telemetry.sh rename to scripts/commands/telemetry.sh index 14b305e..cad1088 --- a/scripts/telemetry.sh +++ b/scripts/commands/telemetry.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail [ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] && exit 0 -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir" +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd); dir="$root/.ai-session"; path="$dir/telemetry.jsonl"; mkdir -p "$dir" action=${1:-record}; shift || true case "$action" in rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;; diff --git a/scripts/uninstall.ps1 b/scripts/commands/uninstall.ps1 similarity index 93% rename from scripts/uninstall.ps1 rename to scripts/commands/uninstall.ps1 index 237f819..497366e 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/commands/uninstall.ps1 @@ -6,11 +6,11 @@ param( [switch]$Summary ) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/plugins.ps1') -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') -. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') -. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/plugins.ps1') +. (Join-Path $root 'scripts/lib/manifest.ps1') +. (Join-Path $root 'scripts/lib/install-targets.ps1') +. (Join-Path $root 'scripts/lib/semantic-retrieval.ps1') $Client = Read-InstallClient -Client $Client $homePath = [Environment]::GetFolderPath('UserProfile') diff --git a/scripts/uninstall.sh b/scripts/commands/uninstall.sh similarity index 98% rename from scripts/uninstall.sh rename to scripts/commands/uninstall.sh index bcc96b0..517c3d8 100644 --- a/scripts/uninstall.sh +++ b/scripts/commands/uninstall.sh @@ -15,7 +15,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/install-targets.sh" diff --git a/scripts/update.ps1 b/scripts/commands/update.ps1 similarity index 88% rename from scripts/update.ps1 rename to scripts/commands/update.ps1 index 2860974..fdedfda 100644 --- a/scripts/update.ps1 +++ b/scripts/commands/update.ps1 @@ -7,13 +7,13 @@ param( [switch]$Summary ) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/plugins.ps1') -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') -. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') -. (Join-Path $PSScriptRoot 'lib/selection-state.ps1') -. (Join-Path $PSScriptRoot 'lib/install-options.ps1') -. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/plugins.ps1') +. (Join-Path $root 'scripts/lib/manifest.ps1') +. (Join-Path $root 'scripts/lib/install-targets.ps1') +. (Join-Path $root 'scripts/lib/selection-state.ps1') +. (Join-Path $root 'scripts/lib/install-options.ps1') +. (Join-Path $root 'scripts/lib/semantic-retrieval.ps1') $homePath = [Environment]::GetFolderPath('UserProfile') if ($Quick) { @@ -32,7 +32,7 @@ if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -H $options = if ($Quick) { $saved } else { Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun } -$buildScript = Join-Path $PSScriptRoot 'build.ps1' +$buildScript = Join-Path $root 'scripts/commands/build.ps1' & $buildScript -Summary:$Summary if (-not $?) { throw 'Build failed. Update was not started.' } $generated = Join-Path $root 'generated' diff --git a/scripts/update.sh b/scripts/commands/update.sh old mode 100755 new mode 100644 similarity index 96% rename from scripts/update.sh rename to scripts/commands/update.sh index a7a5e9e..ef784f5 --- a/scripts/update.sh +++ b/scripts/commands/update.sh @@ -17,7 +17,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/plugins.sh" . "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/install-targets.sh" @@ -45,7 +45,7 @@ if [ "$quick" = false ]; then read_install_options; fi build_args=() [ "$summary" = false ] || build_args+=(--summary) -bash "$root/scripts/build.sh" "${build_args[@]}" +bash "$root/scripts/commands/build.sh" "${build_args[@]}" generated="$root/generated" [ -d "$generated" ] || { printf 'Generated output is missing after a successful build.\n' >&2; exit 1; } diff --git a/scripts/test-build-validation.ps1 b/scripts/tests/test-build-validation.ps1 similarity index 92% rename from scripts/test-build-validation.ps1 rename to scripts/tests/test-build-validation.ps1 index eec008f..90b0e29 100644 --- a/scripts/test-build-validation.ps1 +++ b/scripts/tests/test-build-validation.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid()) New-Item $work -ItemType Directory -Force | Out-Null @@ -18,14 +18,14 @@ function Test-BuildFails([string]$Label, [scriptblock]$Mutate) { $copy = New-RepoCopy & $Mutate $copy $failed = $false - try { & (Join-Path $copy 'scripts/build.ps1') 2>&1 | Out-Null } + try { & (Join-Path $copy 'scripts/commands/build.ps1') 2>&1 | Out-Null } catch { $failed = $true } Remove-Item $copy -Recurse -Force -ErrorAction SilentlyContinue if (-not $failed) { throw "Expected build to fail for case '$Label' but it succeeded." } } $baseline = New-RepoCopy -& (Join-Path $baseline 'scripts/build.ps1') -Summary | Out-Null +& (Join-Path $baseline 'scripts/commands/build.ps1') -Summary | Out-Null Test-BuildFails 'invalid Claude settings.json' { param($copy) diff --git a/scripts/test-build-validation.sh b/scripts/tests/test-build-validation.sh old mode 100755 new mode 100644 similarity index 91% rename from scripts/test-build-validation.sh rename to scripts/tests/test-build-validation.sh index 27e570d..0d15c50 --- a/scripts/test-build-validation.sh +++ b/scripts/tests/test-build-validation.sh @@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) work=$(mktemp -d) cleanup() { rm -rf -- "$work"; } trap cleanup EXIT @@ -24,7 +24,7 @@ test_build_fails() { local label=$1 mutate_fn=$2 copy copy=$(new_repo_copy) "$mutate_fn" "$copy" - if bash "$copy/scripts/build.sh" >/dev/null 2>&1; then + if bash "$copy/scripts/commands/build.sh" >/dev/null 2>&1; then printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2 exit 1 fi @@ -32,7 +32,7 @@ test_build_fails() { } baseline=$(new_repo_copy) -bash "$baseline/scripts/build.sh" --summary >/dev/null +bash "$baseline/scripts/commands/build.sh" --summary >/dev/null mutate_invalid_json() { printf '} this is not json {' >> "$1/adapters/claude/config/settings.json" diff --git a/scripts/test-install-guards.ps1 b/scripts/tests/test-install-guards.ps1 similarity index 87% rename from scripts/test-install-guards.ps1 rename to scripts/tests/test-install-guards.ps1 index 3bed575..c7212f4 100644 --- a/scripts/test-install-guards.ps1 +++ b/scripts/tests/test-install-guards.ps1 @@ -1,9 +1,9 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') -. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/manifest.ps1') +. (Join-Path $root 'scripts/lib/install-targets.ps1') $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid()) try { diff --git a/scripts/test-install-guards.sh b/scripts/tests/test-install-guards.sh old mode 100755 new mode 100644 similarity index 94% rename from scripts/test-install-guards.sh rename to scripts/tests/test-install-guards.sh index 888786f..e87d562 --- a/scripts/test-install-guards.sh +++ b/scripts/tests/test-install-guards.sh @@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/install-targets.sh" diff --git a/scripts/test-install-options.py b/scripts/tests/test-install-options.py similarity index 99% rename from scripts/test-install-options.py rename to scripts/tests/test-install-options.py index cd7ef13..fe6f34e 100644 --- a/scripts/test-install-options.py +++ b/scripts/tests/test-install-options.py @@ -9,7 +9,7 @@ import tomllib import unittest -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.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) diff --git a/scripts/test-managed-extensions.py b/scripts/tests/test-managed-extensions.py similarity index 97% rename from scripts/test-managed-extensions.py rename to scripts/tests/test-managed-extensions.py index 9b220fd..b2423c0 100644 --- a/scripts/test-managed-extensions.py +++ b/scripts/tests/test-managed-extensions.py @@ -172,19 +172,19 @@ class ManagedExtensionTests(unittest.TestCase): self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'}) self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue()) - def test_qmd_installs_once_and_registers_codex_mcp(self): + def test_qmd_installs_once_as_a_cli_without_registering_mcp(self): entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd') self.manager().sync([entry], ['codex'], {'QMD'}) - self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd'], ['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp']], self.commands) + self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd']], self.commands) self.manager().sync([], ['codex'], set()) - self.assertEqual(['codex', 'mcp', 'remove', 'qmd'], self.commands[-1]) + self.assertEqual(['npm', 'uninstall', '--global', '@tobilu/qmd'], self.commands[-1]) def test_qmd_installs_once_for_both_clients(self): entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd') self.manager().sync([entry], ['claude', 'codex'], {'QMD'}) self.assertEqual(1, self.commands.count(['npm', 'install', '--global', '@tobilu/qmd'])) self.assertIn(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands) - self.assertIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands) + self.assertNotIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands) def test_foreign_empty_directory_survives_skill_removal(self): self.manager().sync([skill()], ['codex'], {'Humanizer'}) diff --git a/scripts/test-managed-install.ps1 b/scripts/tests/test-managed-install.ps1 similarity index 98% rename from scripts/test-managed-install.ps1 rename to scripts/tests/test-managed-install.ps1 index 93cd30f..65be785 100644 --- a/scripts/test-managed-install.ps1 +++ b/scripts/tests/test-managed-install.ps1 @@ -1,9 +1,9 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent -. (Join-Path $PSScriptRoot 'lib/manifest.ps1') -. (Join-Path $PSScriptRoot 'lib/install-targets.ps1') +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +. (Join-Path $root 'scripts/lib/manifest.ps1') +. (Join-Path $root 'scripts/lib/install-targets.ps1') $work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-manifest-test-" + [Guid]::NewGuid()) New-Item $work -ItemType Directory -Force | Out-Null diff --git a/scripts/test-managed-install.sh b/scripts/tests/test-managed-install.sh old mode 100755 new mode 100644 similarity index 99% rename from scripts/test-managed-install.sh rename to scripts/tests/test-managed-install.sh index 1d40262..72684a2 --- a/scripts/test-managed-install.sh +++ b/scripts/tests/test-managed-install.sh @@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/manifest.sh" . "$root/scripts/lib/install-targets.sh" diff --git a/scripts/test-plugin-selection.ps1 b/scripts/tests/test-plugin-selection.ps1 similarity index 86% rename from scripts/test-plugin-selection.ps1 rename to scripts/tests/test-plugin-selection.ps1 index 409c217..59b2657 100644 --- a/scripts/test-plugin-selection.ps1 +++ b/scripts/tests/test-plugin-selection.ps1 @@ -1,8 +1,9 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } } -. (Join-Path $PSScriptRoot 'lib/plugins.ps1') +. (Join-Path $root 'scripts/lib/plugins.ps1') $script:answers = [System.Collections.Generic.Queue[string]]::new() $script:answers.Enqueue('1') @@ -43,22 +44,22 @@ $null = Get-InstalledPlugins -Client Codex -HomePath 'C:/temporary profile' if ($observedCodexHome -ne (Join-Path 'C:/temporary profile' '.codex') -or $observedProfile -ne 'C:/temporary profile') { throw 'Plugin inspection must use the requested user home.' } if ($env:CODEX_HOME -ne $previousCodexHome -or $env:USERPROFILE -ne $previousProfile) { throw 'Plugin inspection must restore the process environment.' } $script:answers.Enqueue('done') -$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update +$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update if (@($selection.Selected | Where-Object { $_.codex_method -eq 'plugin' }).Count -ne 5) { throw 'All installed Codex plugins must start checked when updating.' } $script:answers.Enqueue('4') $script:answers.Enqueue('done') -$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Mode Update +$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'Only the explicitly unchecked installed plugin must be deselected.' } $script:managedArguments = @() function python { $script:managedArguments = @($args); $global:LASTEXITCODE = 0 } $entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' } -Sync-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -HomePath 'C:/temporary home' -Update -Summary -Entries @($entry) +Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -HomePath 'C:/temporary home' -Update -Summary -Entries @($entry) if ($managedArguments -notcontains 'sync' -or $managedArguments -notcontains '--update' -or $managedArguments -notcontains 'Test' -or $managedArguments -notcontains 'C:/temporary home') { throw 'Managed reconciliation must receive selection, home, and update mode.' } -Sync-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Codex -Entries @() +Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Entries @() if ($managedArguments[-1] -ne '--selected') { throw 'Empty selection must reach reconciliation for managed cleanup.' } function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' } function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' } -Install-ConfiguredPlugins -RepositoryRoot (Split-Path $PSScriptRoot -Parent) -Client Both -Entries @() +Install-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Both -Entries @() Uninstall-DeselectedPlugins -Client Both -Entries @() if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' } diff --git a/scripts/test-plugin-selection.sh b/scripts/tests/test-plugin-selection.sh old mode 100755 new mode 100644 similarity index 97% rename from scripts/test-plugin-selection.sh rename to scripts/tests/test-plugin-selection.sh index 113148a..1f2b97c --- a/scripts/test-plugin-selection.sh +++ b/scripts/tests/test-plugin-selection.sh @@ -8,7 +8,7 @@ while [ $# -gt 0 ]; do esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/lib/plugins.sh" # shellcheck disable=SC2034 diff --git a/scripts/test-prompt-inventory.py b/scripts/tests/test-prompt-inventory.py similarity index 100% rename from scripts/test-prompt-inventory.py rename to scripts/tests/test-prompt-inventory.py diff --git a/scripts/test-quick-update.ps1 b/scripts/tests/test-quick-update.ps1 similarity index 100% rename from scripts/test-quick-update.ps1 rename to scripts/tests/test-quick-update.ps1 diff --git a/scripts/test-quick-update.py b/scripts/tests/test-quick-update.py similarity index 97% rename from scripts/test-quick-update.py rename to scripts/tests/test-quick-update.py index 3401552..ab45493 100644 --- a/scripts/test-quick-update.py +++ b/scripts/tests/test-quick-update.py @@ -8,7 +8,7 @@ import tempfile import unittest -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.parent.parent HELPER = ROOT / 'scripts/lib/selection-state.py' @@ -112,7 +112,7 @@ class SelectionTests(unittest.TestCase): env=environment, capture_output=True, text=True, timeout=10) self.assertEqual(save.returncode, 0, save.stdout + save.stderr) original = self.path.read_bytes() - source = (ROOT / 'scripts/update.ps1').read_text(encoding='utf-8-sig') + source = (ROOT / 'scripts/commands/update.ps1').read_text(encoding='utf-8-sig') source = source.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_SELECTION_HOME') with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp.ps1', dir=ROOT / 'scripts', encoding='utf-8', delete=False) as stream: @@ -148,7 +148,7 @@ class SelectionTests(unittest.TestCase): self.assertEqual(save.returncode, 0, save.stdout + save.stderr) original = self.path.read_bytes() environment = dict(os.environ, HOME=self.home.as_posix()) - result = subprocess.run([shell, str(ROOT / 'scripts/update.sh'), '--quick', '--dry-run'], + result = subprocess.run([shell, str(ROOT / 'scripts/commands/update.sh'), '--quick', '--dry-run'], env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertNotIn('Select plugins', result.stdout) diff --git a/scripts/test-quick-update.sh b/scripts/tests/test-quick-update.sh old mode 100755 new mode 100644 similarity index 61% rename from scripts/test-quick-update.sh rename to scripts/tests/test-quick-update.sh index 60db52b..b36d330 --- a/scripts/test-quick-update.sh +++ b/scripts/tests/test-quick-update.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail case "${1:-}" in ''|--summary) ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) -python3 "$root/scripts/test-quick-update.py" +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +python3 "$root/scripts/tests/test-quick-update.py" printf 'Tests: PASS | quick update selection\n' diff --git a/scripts/test-retrieval-server.py b/scripts/tests/test-retrieval-server.py similarity index 99% rename from scripts/test-retrieval-server.py rename to scripts/tests/test-retrieval-server.py index 194d3ab..50a7656 100644 --- a/scripts/test-retrieval-server.py +++ b/scripts/tests/test-retrieval-server.py @@ -11,7 +11,7 @@ import unittest from unittest.mock import patch -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.parent.parent SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py') MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) diff --git a/scripts/test-semantic-retrieval.py b/scripts/tests/test-semantic-retrieval.py similarity index 98% rename from scripts/test-semantic-retrieval.py rename to scripts/tests/test-semantic-retrieval.py index 77ef6fa..3ef3475 100644 --- a/scripts/test-semantic-retrieval.py +++ b/scripts/tests/test-semantic-retrieval.py @@ -9,7 +9,7 @@ import unittest from unittest.mock import patch -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.parent.parent SPEC = importlib.util.spec_from_file_location('semantic_retrieval', ROOT / 'scripts/lib/semantic-retrieval.py') MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) diff --git a/scripts/test-session-state.py b/scripts/tests/test-session-state.py similarity index 92% rename from scripts/test-session-state.py rename to scripts/tests/test-session-state.py index 05d3805..da10fd4 100644 --- a/scripts/test-session-state.py +++ b/scripts/tests/test-session-state.py @@ -8,7 +8,7 @@ import tempfile import unittest -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.parent.parent SHELLS = [ ('powershell', shutil.which('powershell') or shutil.which('pwsh')), ('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe') @@ -88,16 +88,16 @@ class SessionStateHookTests(unittest.TestCase): if shell == 'powershell': prefix = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command'] commands = [ - '& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")', - '& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")', - '& ./scripts/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer', + '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")', + '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")', + '& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer', ] else: prefix = [executable, '-c'] commands = [ - 'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check', - 'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed', - 'bash ./scripts/session-state.sh --workspace "$TASK_WORKSPACE" --pointer', + 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check', + 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed', + 'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --pointer', ] for command in commands: result = subprocess.run(prefix + [command], cwd=repository, capture_output=True, diff --git a/scripts/test-shell-packages.ps1 b/scripts/tests/test-shell-packages.ps1 similarity index 94% rename from scripts/test-shell-packages.ps1 rename to scripts/tests/test-shell-packages.ps1 index e1816e3..0e5c9e2 100644 --- a/scripts/test-shell-packages.ps1 +++ b/scripts/tests/test-shell-packages.ps1 @@ -1,13 +1,13 @@ [CmdletBinding()] param([switch]$Summary) $ErrorActionPreference = 'Stop' -$root = Split-Path $PSScriptRoot -Parent +$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw $bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on PowerShell and Bash.' } $statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' $branch = & git -C $root branch --show-current -$buildSummaryOutput = & (Join-Path $root 'scripts/build.ps1') -Summary +$buildSummaryOutput = & (Join-Path $root 'scripts/commands/build.ps1') -Summary if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" } $statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n" $plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '') @@ -48,7 +48,7 @@ foreach ($shell in @('powershell','bash')) { throw "Claude finish hook is incorrect in $package" } elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') { throw "Claude auto permission mode is missing in $package" - } elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false) { + } elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true) { throw "Claude strict sandbox is missing in $package" } elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") { throw "Claude status line is incorrect in $package" @@ -70,17 +70,17 @@ foreach ($shellSelection in @('1','2')) { $script:answers.Enqueue($shellSelection) $script:answers.Enqueue($clientSelection) function Read-Host { param($Prompt) $answers.Dequeue() } - $output = & "$PSScriptRoot/install.ps1" -DryRun 6>$null + $output = & (Join-Path $root 'scripts/commands/install.ps1') -DryRun 6>$null if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' } $shell = if ($shellSelection -eq '1') { 'powershell' } else { 'bash' } if (($output -join "`n") -notmatch "-$shell") { throw 'Incorrect selected package.' } } } -$doctorSummaryOutput = & (Join-Path $root 'scripts/doctor.ps1') -Summary +$doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' } if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" } foreach ($entryPoint in @('install','update')) { - $preview = @(& "$PSScriptRoot/$entryPoint.ps1" -Summary -DryRun -Client Both -Shell PowerShell) + $preview = @(& (Join-Path $root "scripts/commands/$entryPoint.ps1") -Summary -DryRun -Client Both -Shell PowerShell) if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." } if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." } } diff --git a/scripts/test-shell-packages.sh b/scripts/tests/test-shell-packages.sh old mode 100755 new mode 100644 similarity index 93% rename from scripts/test-shell-packages.sh rename to scripts/tests/test-shell-packages.sh index d0a3a6c..b02edb8 --- a/scripts/test-shell-packages.sh +++ b/scripts/tests/test-shell-packages.sh @@ -7,12 +7,12 @@ while [ $# -gt 0 ]; do *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac done -root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1" [ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ] branch=$(git -C "$root" branch --show-current) -build_summary_output=$(bash "$root/scripts/build.sh" --summary) +build_summary_output=$(bash "$root/scripts/commands/build.sh" --summary) if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2 exit 1 @@ -65,7 +65,7 @@ for shell in powershell bash; do ! grep -q 'flashbang-if-input' "$package/$file" ! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file" grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file" - python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False}' "$package/$file" + python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}' "$package/$file" statusline_extension='sh' [ "$shell" != powershell ] || statusline_extension=ps1 test -f "$package/statusline/statusline.$statusline_extension" @@ -100,7 +100,7 @@ for shell in powershell bash; do done for shell_selection in 1 2; do for client_selection in 1 2 3; do - output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; } + output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/commands/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; } printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true [[ "$output" == *'PASS install dry-run'* ]] shell=powershell @@ -108,7 +108,7 @@ for shell_selection in 1 2; do [[ "$output" == *"-$shell"* ]] done done -doctor_summary_output=$(bash "$root/scripts/doctor.sh" --summary) +doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary) if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then printf 'Doctor summary mode must not print individual PASS lines.\n' >&2 exit 1 @@ -118,7 +118,7 @@ if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+ exit 1 fi for entry_point in install update; do - preview=$(bash "$root/scripts/$entry_point.sh" --summary --dry-run --client both --shell bash) + preview=$(bash "$root/scripts/commands/$entry_point.sh" --summary --dry-run --client both --shell bash) if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then printf '%s summary leaked per-item output.\n' "$entry_point" >&2 exit 1 diff --git a/scripts/test-telemetry.py b/scripts/tests/test-telemetry.py similarity index 88% rename from scripts/test-telemetry.py rename to scripts/tests/test-telemetry.py index e3cdffa..fedec74 100644 --- a/scripts/test-telemetry.py +++ b/scripts/tests/test-telemetry.py @@ -7,7 +7,7 @@ import tempfile import unittest -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parent.parent.parent class TelemetryTests(unittest.TestCase): @@ -17,9 +17,9 @@ class TelemetryTests(unittest.TestCase): self.skipTest('Bash unavailable') with tempfile.TemporaryDirectory() as directory: root = Path(directory) - script = root / 'scripts/telemetry.sh' + script = root / 'scripts/commands/telemetry.sh' script.parent.mkdir() - shutil.copy(ROOT / 'scripts/telemetry.sh', script) + shutil.copy(ROOT / 'scripts/commands/telemetry.sh', script) env = dict(os.environ, AI_CONFIG_TELEMETRY='1') result = subprocess.run([bash, str(script), 'record', 'subagent-stop', '--role', 'reviewer', '--filesRead', '3', '--outcome', 'findings'], env=env, capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr)