refactor(scripts): organize script entry points

This commit is contained in:
fraujulian committed 2026-09-18 11:35:38 +02:00
1 parent 793eee5b61
commit 11c2b6dfcb
41 files changed
+102 -101

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$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'
$ruleSkillManifest = Join-Path $root 'adapters/rule-skills.tsv'
$capabilityManifest = Join-Path $root 'adapters/claude/capabilities.tsv'
if (-not (Test-Path -LiteralPath $pluginManifest)) { throw "Missing plugin manifest: $pluginManifest" }
$pluginEntries = @(Import-Csv -LiteralPath $pluginManifest -Delimiter ([char]9))
if ($pluginEntries.Count -eq 0) { throw 'Plugin manifest must define at least one plugin.' }
$pluginNames = @()
foreach ($pluginEntry in $pluginEntries) {
foreach ($field in @('name','claude_plugin','codex_method')) {
if ([string]::IsNullOrWhiteSpace($pluginEntry.$field)) { throw "Missing $field for plugin $($pluginEntry.name)." }
}
if ($pluginNames -contains $pluginEntry.name) { throw "Duplicate plugin name in manifest: $($pluginEntry.name)" }
$pluginNames += $pluginEntry.name
}
if (-not (Test-Path -LiteralPath $ruleSkillManifest)) { throw "Missing rule-skill manifest: $ruleSkillManifest" }
if (-not (Test-Path -LiteralPath $capabilityManifest)) { throw "Missing capability manifest: $capabilityManifest" }
$ruleSkillEntries = @(Import-Csv -LiteralPath $ruleSkillManifest -Delimiter ([char]9) | Sort-Object skill_name)
if ($ruleSkillEntries.Count -eq 0) { throw 'Rule-skill manifest must define at least one entry.' }
$ruleSkillNames = @()
foreach ($entry in $ruleSkillEntries) {
foreach ($field in @('rule_file','skill_name','trigger')) {
if ([string]::IsNullOrWhiteSpace($entry.$field)) { throw "Missing $field in rule-skill manifest row for $($entry.rule_file)." }
}
if (-not (Test-Path -LiteralPath (Join-Path $shared "rules/$($entry.rule_file)"))) { throw "Rule-skill manifest references a missing rule file: $($entry.rule_file)" }
if ($ruleSkillNames -contains $entry.skill_name) { throw "Duplicate rule-skill name in manifest: $($entry.skill_name)" }
$ruleSkillNames += $entry.skill_name
}
$agentTools = @{}
foreach ($entry in @(Import-Csv -LiteralPath $capabilityManifest -Delimiter ([char]9))) {
if ([string]::IsNullOrWhiteSpace($entry.role) -or [string]::IsNullOrWhiteSpace($entry.tools)) { throw 'Invalid capability manifest row.' }
$agentTools[$entry.role] = $entry.tools
}
$codexRuleLoadingLines = @()
foreach ($entry in $ruleSkillEntries) {
$rulePath = $entry.rule_file
if (Test-Path -LiteralPath (Join-Path $shared "rules/$($entry.rule_file)") -PathType Container) { $rulePath = "$($entry.rule_file)/index.md" }
$codexRuleLoadingLines += "- rules/$rulePath for $($entry.trigger)."
}
$codexRuleLoadingList = $codexRuleLoadingLines -join "`r`n"
Remove-Item $output -Recurse -Force -ErrorAction SilentlyContinue
New-Item $output -ItemType Directory -Force | Out-Null
function Copy-Directory($source, $destination) {
New-Item $destination -ItemType Directory -Force | Out-Null
Get-ChildItem $source -File -Recurse | ForEach-Object {
$relative = $_.FullName.Substring($source.Length).TrimStart([char[]]@('\','/'))
$target = Join-Path $destination $relative
New-Item (Split-Path $target -Parent) -ItemType Directory -Force | Out-Null
Copy-Item $_.FullName $target -Force
}
}
function Read-Field($path, $name) {
$line = Get-Content $path | Where-Object { $_ -match ('^' + [regex]::Escape($name) + ':\s*(.*)$') } | Select-Object -First 1
if (-not $line) { throw "Missing $name in $path" }
return ([regex]::Match($line, '^' + [regex]::Escape($name) + ':\s*(.*)$')).Groups[1].Value.Trim()
}
function Quote-Toml($value) {
return ('"' + $value.Replace('\','\\').Replace('"','\"').Replace("`r",'').Replace("`n",'\n') + '"')
}
function Test-CodexSchema($configPath) {
if (-not (Get-Command codex -ErrorAction SilentlyContinue)) {
if ($env:REQUIRE_CODEX_SCHEMA -eq 'true') { throw 'Codex CLI is required for schema validation.' }
Write-Warning 'Codex CLI unavailable; skipped Codex schema validation.'
return
}
$tempHome = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-schema-" + [Guid]::NewGuid())
$previousCodexHome = $env:CODEX_HOME
try {
New-Item $tempHome -ItemType Directory -Force | Out-Null
Copy-Item $configPath (Join-Path $tempHome 'config.toml') -Force
$env:CODEX_HOME = $tempHome
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
$schemaOutput = @(& codex --strict-config --help 2>&1)
$schemaExitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
if ($schemaExitCode -ne 0) { throw "Codex schema validation failed: $configPath`n$($schemaOutput -join [Environment]::NewLine)" }
} finally {
$env:CODEX_HOME = $previousCodexHome
Remove-Item $tempHome -Recurse -Force -ErrorAction SilentlyContinue
}
}
& (Join-Path $root 'shared/hooks/scripts/Test-SessionConfig.ps1') -RepositoryRoot $root | Where-Object { -not $Summary -or $_ -ne 'PASS session configuration' }
$sourceAgentDirs = @(Get-ChildItem (Join-Path $shared 'agents') -Directory | Sort-Object Name)
if ($sourceAgentDirs.Count -eq 0) { throw 'No agents found under shared/agents.' }
$agentNames = @()
foreach ($agentDir in $sourceAgentDirs) {
$name = Read-Field (Join-Path $agentDir.FullName 'agent.yml') 'name'
if ($agentNames -contains $name) { throw "Duplicate agent name: $name" }
$agentNames += $name
}
# Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
# it into CLAUDE.md, instead of only being referenced by path — guaranteed present
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
$codexRuleLoading = @'
General rules are embedded below.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
Load rule files when their subject applies:
__CODEX_RULE_LOADING__
'@
$codexRuleLoading = $codexRuleLoading.Replace('__CODEX_RULE_LOADING__', $codexRuleLoadingList)
foreach ($shell in @('powershell','bash')) {
New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
New-Item (Join-Path $output "claude-$shell") -ItemType Directory -Force | Out-Null
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$shell/skills")
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$shell/skills")
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$shell/rules")
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$shell/hooks")
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$shell/hooks")
Copy-Directory (Join-Path $shared 'statusline') (Join-Path $output "claude-$shell/statusline")
$claudeRulesDir = Join-Path $output "claude-$shell/rules"
New-Item $claudeRulesDir -ItemType Directory -Force | Out-Null
$claudeRuleSkillLines = @()
foreach ($entry in $ruleSkillEntries) {
$ruleSourcePath = Join-Path $shared "rules/$($entry.rule_file)"
$isDirectory = Test-Path -LiteralPath $ruleSourcePath -PathType Container
$ruleBodyPath = if ($isDirectory) { Join-Path $ruleSourcePath 'index.md' } else { $ruleSourcePath }
$skillDir = Join-Path $output "claude-$shell/skills/rules/$($entry.skill_name)"
New-Item $skillDir -ItemType Directory -Force | Out-Null
$ruleContent = Get-Content $ruleBodyPath -Raw
$skillBody = "---`r`nname: $($entry.skill_name)`r`ndescription: $(Quote-Toml "Use for $($entry.trigger).")`r`n---`r`n`r`n$ruleContent"
Set-Content (Join-Path $skillDir 'SKILL.md') $skillBody -Encoding UTF8
if ($isDirectory) {
$referencesSource = Join-Path $ruleSourcePath 'references'
if (Test-Path -LiteralPath $referencesSource) { Copy-Directory $referencesSource (Join-Path $skillDir 'references') }
}
$claudeRuleSkillLines += "- $($entry.skill_name) for $($entry.trigger)."
}
$claudeRuleLoading = "General rules are embedded below.`r`n`r`nDetect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.`r`n`r`nInvoke rule skills when their subject applies:`r`n`r`n" + ($claudeRuleSkillLines -join "`r`n")
$sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw
$generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)
$agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "codex-$shell/AGENTS.md") $agentsContent -Encoding UTF8
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
Set-Content (Join-Path $output "claude-$shell/CLAUDE.md") $claudeContent -Encoding UTF8
Copy-Item (Join-Path $root 'adapters/codex/config/config.toml') (Join-Path $output "codex-$shell/config.toml") -Force
Copy-Item (Join-Path $root 'adapters/claude/config/settings.json') (Join-Path $output "claude-$shell/settings.json") -Force
foreach ($client in @('codex','claude')) {
$agentsOutput = Join-Path $output "$client-$shell/agents"
New-Item $agentsOutput -ItemType Directory -Force | Out-Null
foreach ($agentDir in $sourceAgentDirs) {
$metadata = Join-Path $agentDir.FullName 'agent.yml'
$name = Read-Field $metadata 'name'
$description = Read-Field $metadata 'description'
$loader = (Get-Content (Join-Path $root "adapters/$client/orchestration-loader.txt") -Raw -Encoding UTF8).Trim()
$instructions = $loader + "`n`n" + (Get-Content (Join-Path $agentDir.FullName 'instructions.md') -Raw -Encoding UTF8)
if ($client -eq 'codex') {
Set-Content (Join-Path $agentsOutput "$name.toml") "name = $(Quote-Toml $name)`r`ndescription = $(Quote-Toml $description)`r`ndeveloper_instructions = $(Quote-Toml $instructions)" -Encoding UTF8
} else {
$shellTool = if ($shell -eq 'powershell') { 'PowerShell' } else { 'Bash' }
$tools = $agentTools[$name]
if ($tools) { $tools = $tools.Replace('__SHELL__', $shellTool) }
$frontmatter = "---`r`nname: $name`r`ndescription: $description`r`n"
if ($tools) { $frontmatter += "tools: $tools`r`n" }
Set-Content (Join-Path $agentsOutput "$name.md") ($frontmatter + "---`r`n`r`n$instructions") -Encoding UTF8
}
}
}
foreach ($client in @('codex','claude')) {
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
$path = Join-Path $output "$client-$shell/$file"
$command = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__' } else { 'bash' }
$script = if ($shell -eq 'powershell') { 'flashbang.ps1' } else { 'flashbang.sh' }
$statusLineScript = if ($shell -eq 'powershell') { 'statusline.ps1' } else { 'statusline.sh' }
$content = Get-Content -LiteralPath $path -Raw
$compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' }
$pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' }
$content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript)
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
}
$tomlPath = Join-Path $output "codex-$shell/config.toml"
$settingsPath = Join-Path $output "claude-$shell/settings.json"
& 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
}
$leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object {
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output. -cmatch keeps this case-sensitive so Python
# dunder names (__main__, __name__) in flashbang.sh are not mistaken for placeholders.
$content = (Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', ''
if ($content -cmatch '__[A-Z0-9_]+__') { $_.FullName }
})
if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" }
if ($Summary) { Write-Output 'Build: PASS | 4 packages' } else { Write-Output 'PASS build: codex-powershell, claude-powershell, codex-bash, claude-bash' }
exit 0
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env bash
set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
shared="$root/shared"
output="$root/generated"
plugin_manifest="$root/adapters/plugins.tsv"
rule_skill_manifest="$root/adapters/rule-skills.tsv"
capability_manifest="$root/adapters/claude/capabilities.tsv"
[ -f "$plugin_manifest" ] || { printf 'Missing plugin manifest: %s\n' "$plugin_manifest" >&2; exit 1; }
declare -A plugin_seen
plugin_count=0
while IFS=$'\t' read -r name _ claude_plugin _ codex_plugin; do
[ "$name" != name ] || continue
[ -n "$name" ] || continue
plugin_count=$((plugin_count + 1))
[ -n "$claude_plugin" ] || { printf 'Missing claude_plugin for plugin %s.\n' "$name" >&2; exit 1; }
[ -n "$codex_plugin" ] || { printf 'Missing codex_plugin for plugin %s.\n' "$name" >&2; exit 1; }
[ -z "${plugin_seen[$name]+x}" ] || { printf 'Duplicate plugin name in manifest: %s\n' "$name" >&2; exit 1; }
plugin_seen[$name]=1
done < "$plugin_manifest"
[ "$plugin_count" -gt 0 ] || { printf 'Plugin manifest must define at least one plugin.\n' >&2; exit 1; }
[ -f "$rule_skill_manifest" ] || { printf 'Missing rule-skill manifest: %s\n' "$rule_skill_manifest" >&2; exit 1; }
[ -f "$capability_manifest" ] || { printf 'Missing capability manifest: %s\n' "$capability_manifest" >&2; exit 1; }
declare -A rule_skill_seen
rule_skill_files=()
rule_skill_names=()
rule_skill_triggers=()
while IFS=$'\t' read -r rule_file skill_name trigger; do
trigger=${trigger%$'\r'}
[ "$rule_file" != rule_file ] || continue
[ -n "$rule_file" ] || continue
[ -n "$skill_name" ] || { printf 'Missing skill_name in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
[ -n "$trigger" ] || { printf 'Missing trigger in rule-skill manifest row for %s.\n' "$rule_file" >&2; exit 1; }
[ -f "$shared/rules/$rule_file" ] || [ -d "$shared/rules/$rule_file" ] || { printf 'Rule-skill manifest references a missing rule file: %s\n' "$rule_file" >&2; exit 1; }
[ -z "${rule_skill_seen[$skill_name]+x}" ] || { printf 'Duplicate rule-skill name in manifest: %s\n' "$skill_name" >&2; exit 1; }
rule_skill_seen[$skill_name]=1
rule_skill_files+=("$rule_file")
rule_skill_names+=("$skill_name")
rule_skill_triggers+=("$trigger")
done < <(tail -n +2 "$rule_skill_manifest" | sort -t$'\t' -k2,2)
[ "${#rule_skill_names[@]}" -gt 0 ] || { printf 'Rule-skill manifest must define at least one entry.\n' >&2; exit 1; }
codex_rule_loading_list=""
for i in "${!rule_skill_names[@]}"; do
rule_file=${rule_skill_files[$i]}
rule_path="$rule_file"
[ -d "$shared/rules/$rule_file" ] && rule_path="$rule_file/index.md"
codex_rule_loading_list="${codex_rule_loading_list}- rules/${rule_path} for ${rule_skill_triggers[$i]}."$'\n'
done
declare -A agent_tools
while IFS=$'\t' read -r role tools; do
[ "$role" != role ] || continue
[ -n "$role" ] && [ -n "$tools" ] || { printf 'Invalid capability manifest row.\n' >&2; exit 1; }
agent_tools[$role]="$tools"
done < "$capability_manifest"
copy_directory() {
local source=$1 destination=$2
mkdir -p "$destination"
cp -R "$source"/. "$destination"/
}
read_field() {
local path=$1 name=$2 value
value=$(sed -n "s/^${name}:[[:space:]]*//p" "$path" | head -n 1)
[ -n "$value" ] || { printf 'Missing %s in %s\n' "$name" "$path" >&2; exit 1; }
printf '%s' "$value"
}
quote_toml() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\r'/}
value=${value//$'\n'/\\n}
printf '"%s"' "$value"
}
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
printf '%s\n' "$session_output"
fi
rm -rf -- "$output"
agent_dirs=("$shared"/agents/*/)
declare -A agent_seen
for agent_dir in "${agent_dirs[@]}"; do
name=$(read_field "${agent_dir}agent.yml" name)
[ -z "${agent_seen[$name]+x}" ] || { printf 'Duplicate agent name: %s\n' "$name" >&2; exit 1; }
agent_seen[$name]=1
done
# Codex: general.md is embedded directly into AGENTS.md, the same way Claude embeds
# it into CLAUDE.md, instead of only being referenced by path — guaranteed present
# either way, and safe even if a subagent's AGENTS.md inheritance is not guaranteed.
read -r -d '' codex_rule_loading <<'BLOCK' || true
General rules are embedded below.
When programming, always load and apply `rules/security.md`. This includes implementing, modifying, debugging, reviewing, testing, and configuring software, scripts, hooks, infrastructure, and integrations.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Load every applicable rule file before editing. Load all matching files when multiple technologies apply.
Load rule files when their subject applies:
BLOCK
codex_rule_loading="${codex_rule_loading}"$'\n\n'"${codex_rule_loading_list%$'\n'}"
for shell in powershell bash; do
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
copy_directory "$shared/skills" "$output/codex-$shell/skills"
copy_directory "$shared/skills" "$output/claude-$shell/skills"
copy_directory "$shared/rules" "$output/codex-$shell/rules"
copy_directory "$shared/hooks" "$output/codex-$shell/hooks"
copy_directory "$shared/hooks" "$output/claude-$shell/hooks"
copy_directory "$shared/statusline" "$output/claude-$shell/statusline"
mkdir -p "$output/claude-$shell/rules"
claude_rule_loading_list=""
for i in "${!rule_skill_names[@]}"; do
rule_file=${rule_skill_files[$i]}
skill_name=${rule_skill_names[$i]}
trigger=${rule_skill_triggers[$i]}
rule_source="$shared/rules/$rule_file"
rule_body="$rule_source"
[ -d "$rule_source" ] && rule_body="$rule_source/index.md"
skill_dir="$output/claude-$shell/skills/rules/$skill_name"
mkdir -p "$skill_dir"
{ printf -- '---\nname: %s\ndescription: %s\n---\n\n' "$skill_name" "$(quote_toml "Use for $trigger.")"; cat "$rule_body"; } > "$skill_dir/SKILL.md"
if [ -d "$rule_source/references" ]; then
copy_directory "$rule_source/references" "$skill_dir/references"
fi
claude_rule_loading_list="${claude_rule_loading_list}- ${skill_name} for ${trigger}.
"
done
claude_rule_loading="General rules are embedded below.
Detect the languages, frameworks, tools, and change areas from the repository and the requested work. Invoke every matching rule skill before editing. Invoke all matching rule skills when multiple technologies apply.
Invoke rule skills when their subject applies:
${claude_rule_loading_list%$'\n'}"
shared_template=$(<"$shared/global-instructions.md")
general_content=$(<"$shared/rules/general.md")
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$shell/AGENTS.md"
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$shell/CLAUDE.md"
cp "$root/adapters/codex/config/config.toml" "$output/codex-$shell/config.toml"
cp "$root/adapters/claude/config/settings.json" "$output/claude-$shell/settings.json"
for client in codex claude; do
agents_output="$output/$client-$shell/agents"
mkdir -p "$agents_output"
for agent_dir in "${agent_dirs[@]}"; do
metadata="${agent_dir}agent.yml"
name=$(read_field "$metadata" name)
description=$(read_field "$metadata" description)
instructions=$(<"$root/adapters/$client/orchestration-loader.txt")$'\n\n'$(<"${agent_dir}instructions.md")
if [ "$client" = codex ]; then
printf 'name = %s\ndescription = %s\ndeveloper_instructions = %s\n' "$(quote_toml "$name")" "$(quote_toml "$description")" "$(quote_toml "$instructions")" > "$agents_output/$name.toml"
else
tools=${agent_tools[$name]:-}
shell_tool=Bash
[ "$shell" != powershell ] || shell_tool=PowerShell
tools=${tools//__SHELL__/$shell_tool}
if [ -n "$tools" ]; then printf '%s\nname: %s\ndescription: %s\ntools: %s\n%s\n\n%s\n' '---' "$name" "$description" "$tools" '---' "$instructions" > "$agents_output/$name.md"; else printf '%s\nname: %s\ndescription: %s\n%s\n\n%s\n' '---' "$name" "$description" '---' "$instructions" > "$agents_output/$name.md"; fi
fi
done
done
for client in codex claude; do
file=config.toml
[ "$client" != claude ] || file=settings.json
command=bash
script=flashbang.sh
statusline_script=statusline.sh
compact_script=record-compact.sh
pointer_script=show-session-state-pointer.sh
if [ "$shell" = powershell ]; then
command='__POWERSHELL_COMMAND__'
script=flashbang.ps1
statusline_script=statusline.ps1
compact_script=Record-Compact.ps1
pointer_script=Show-SessionStatePointer.ps1
fi
path="$output/$client-$shell/$file"
content=$(<"$path")
content=${content//__HOOK_COMMAND__/$command}
content=${content//__POWERSHELL_HOOK_COMMAND__/$command}
content=${content//__HOOK_SCRIPT__/$script}
content=${content//__POWERSHELL_HOOK_SCRIPT__/$script}
content=${content//__COMPACT_SCRIPT__/$compact_script}
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
content=${content//__STATUSLINE_COMMAND__/$command}
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
printf '%s\n' "$content" > "$path"
done
toml_path="$output/codex-$shell/config.toml"
settings_path="$output/claude-$shell/settings.json"
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"
CODEX_HOME="$schema_home" codex --strict-config --help >/dev/null
rm -rf -- "$schema_home"
elif [ "${REQUIRE_CODEX_SCHEMA:-false}" = true ]; then
printf 'Codex CLI is required for schema validation.\n' >&2
exit 1
else
printf 'WARN Codex CLI unavailable; skipped Codex schema validation.\n' >&2
fi
done
# __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is
# expected to remain in generated output, so it is excluded from the leftover check. A
# single tree-wide grep (rather than one process per file) keeps this fast.
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$output" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__)$' || true)
if [ -n "$leftover_tokens" ]; then
offending_files=$(grep -RlE '__[A-Z0-9_]+__' "$output" 2>/dev/null | tr '\n' ' ')
printf 'Unresolved template placeholders (%s) in: %s\n' "$(printf '%s' "$leftover_tokens" | tr '\n' ' ')" "$offending_files" >&2
exit 1
fi
if [ "$summary" = true ]; then printf 'Build: PASS | 4 packages\n'; else printf 'PASS build: codex-powershell, claude-powershell, codex-bash, claude-bash\n'; fi
+19
View File
@@ -0,0 +1,19 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Continue'
$failed = $false
function Pass([string]$message) { Write-Output "PASS $message" }
function Fail([string]$message) { $script:failed = $true; Write-Error "FAIL $message" -ErrorAction Continue }
function Require-Command([string]$name) { if (Get-Command $name -ErrorAction SilentlyContinue) { Pass "$name available" } else { Fail "$name is required" } }
function Version-AtLeast([string]$version, [version]$minimum) { try { return ([version]$version -ge $minimum) } catch { return $false } }
function Check-Version([string]$name, [string]$version, [version]$minimum) { if (Version-AtLeast $version $minimum) { Pass "$name $version (minimum $minimum)" } else { Fail "$name $version is below minimum $minimum" } }
Check-Version 'PowerShell' $PSVersionTable.PSVersion.ToString() ([version]'5.1')
Require-Command jq; Require-Command python
if (Get-Command python -ErrorAction SilentlyContinue) {
$pythonVersion = (& python --version 2>$null | Select-Object -First 1) -replace '^Python\s+', ''
if ($LASTEXITCODE -eq 0) { Check-Version 'Python' $pythonVersion ([version]'3.11') } else { Fail 'Python 3.11 or newer is required' }
}
if ($failed) { Write-Error 'Capability check: FAIL'; exit 1 }
Write-Output 'Capability check: PASS'
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
failed=false
pass() { printf 'PASS %s\n' "$1"; }
fail() { printf 'FAIL %s\n' "$1" >&2; failed=true; }
require_command() {
if command -v "$1" >/dev/null 2>&1; then pass "$1 available"; else fail "$1 is required"; fi
}
version_at_least() {
awk -F. -v required="$2" 'BEGIN { split(required,r) } { split($1,v); if (v[1]+0 > r[1]+0 || (v[1]+0 == r[1]+0 && v[2]+0 >= r[2]+0)) exit 0; exit 1 }' <<<"$1"
}
check_version() {
local name=$1 version=$2 required=$3
if version_at_least "$version" "$required"; then pass "$name $version (minimum $required)"; else fail "$name $version is below minimum $required"; fi
}
check_version Bash "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" 4.3
require_command awk; require_command sed; require_command sha256sum; require_command jq
require_command python3
if command -v python3 >/dev/null 2>&1 && python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 11))' >/dev/null 2>&1; then
pass "Python $(python3 --version 2>&1 | awk '{print $2}') (minimum 3.11)"
else
fail 'Python 3.11 or newer is required'
fi
if [ "$failed" = true ]; then printf 'Capability check: FAIL\n' >&2; exit 1; fi
printf 'Capability check: PASS\n'
+101
View File
@@ -0,0 +1,101 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$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
$script:passBuffer = @()
function Result($state, $message) {
$script:checkCount++
if ($state -eq 'FAIL') { $script:fail = $true }
if ($Summary -and $state -eq 'PASS') {
$script:passBuffer += "$state $message"
} else {
Write-Output ("$state $message")
}
}
foreach ($tool in @('codex','claude')) { if (Get-Command $tool -ErrorAction SilentlyContinue) { $version = & $tool --version 2>&1 | Select-Object -First 1; Result 'PASS' "$tool available ($version)" } else { Result 'WARN' "$tool unavailable" } }
if (Get-Command jq -ErrorAction SilentlyContinue) { Result 'PASS' 'jq available for Claude Bash status line' } else { Result 'WARN' 'jq unavailable; Claude Bash status line is disabled' }
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory -ErrorAction SilentlyContinue).Count
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
$ruleSkillManifestPath = Join-Path $root 'adapters/rule-skills.tsv'
$ruleSkillCount = if (Test-Path -LiteralPath $ruleSkillManifestPath) { @(Import-Csv -LiteralPath $ruleSkillManifestPath -Delimiter ([char]9)).Count } else { 0 }
foreach ($shell in @('powershell','bash')) {
if (Test-Path (Join-Path $root "generated/codex-$shell/AGENTS.md")) { Result 'PASS' "generated output present ($shell)" } else { Result 'FAIL' "generated output missing ($shell); run build" }
if ((Test-Path (Join-Path $root "generated/codex-$shell/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/codex-$shell/hooks/scripts/validate-command-safety.sh")) -and (Test-Path (Join-Path $root "generated/claude-$shell/hooks/scripts/Validate-CommandSafety.ps1")) -and (Test-Path (Join-Path $root "generated/claude-$shell/hooks/scripts/validate-command-safety.sh"))) { Result 'PASS' "generated hooks present ($shell)" } else { Result 'FAIL' "generated hooks missing ($shell); run build" }
foreach ($client in @('codex','claude')) {
$package = Join-Path $root "generated/$client-$shell"
if (-not (Test-Path $package)) { continue }
$agentCount = @(Get-ChildItem (Join-Path $package 'agents') -File -ErrorAction SilentlyContinue).Count
if ($agentCount -eq $sourceAgentCount) { Result 'PASS' "$client-$shell agent count matches source ($agentCount)" } else { Result 'FAIL' "$client-$shell agent count $agentCount does not match source ($sourceAgentCount)" }
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
$skillCount = @(Get-ChildItem (Join-Path $package 'skills') -Filter 'SKILL.md' -Recurse -ErrorAction SilentlyContinue).Count
if ($skillCount -eq $expectedSkillCount) { Result 'PASS' "$client-$shell skill count matches source ($skillCount)" } else { Result 'FAIL' "$client-$shell skill count $skillCount does not match source ($expectedSkillCount)" }
$placeholderHits = @(Get-ChildItem $package -File -Recurse | Where-Object { ((Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', '') -cmatch '__[A-Z0-9_]+__' })
if ($placeholderHits.Count -eq 0) { Result 'PASS' "$client-$shell has no unresolved placeholders" } else { Result 'FAIL' "$client-$shell has unresolved placeholders: $($placeholderHits.FullName -join ', ')" }
}
}
foreach ($path in @('.codex/AGENTS.md','.codex/config.toml','.claude/CLAUDE.md','.claude/settings.json')) {
if (Test-Path (Join-Path $homePath $path)) { Result 'PASS' "installed $path" } else { Result 'WARN' "not installed $path" }
}
$settingsPath = Join-Path $homePath '.claude/settings.json'
if (Test-Path -LiteralPath $settingsPath) {
try { Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json | Out-Null; Result 'PASS' 'installed .claude/settings.json is valid JSON' }
catch { Result 'FAIL' 'installed .claude/settings.json is not valid JSON' }
}
$configTomlPath = Join-Path $homePath '.codex/config.toml'
if (Test-Path -LiteralPath $configTomlPath) {
$parseResult = & python -c "import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8-sig'))" $configTomlPath 2>&1
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml parses as TOML' } else { Result 'FAIL' "installed .codex/config.toml is invalid TOML: $parseResult" }
if (Get-Command codex -ErrorAction SilentlyContinue) {
$previousCodexHome = $env:CODEX_HOME
try {
$env:CODEX_HOME = Split-Path $configTomlPath -Parent
$schemaResult = & codex --strict-config --help 2>&1
if ($LASTEXITCODE -eq 0) { Result 'PASS' 'installed .codex/config.toml matches the installed Codex schema' } else { Result 'FAIL' "installed .codex/config.toml has unsupported Codex settings: $schemaResult" }
} finally { $env:CODEX_HOME = $previousCodexHome }
} else { Result 'WARN' 'Codex CLI unavailable; skipped installed Codex schema validation' }
}
foreach ($destination in @((Join-Path $homePath '.codex'), (Join-Path $homePath '.agents/skills'), (Join-Path $homePath '.claude'))) {
$manifestPath = Get-ManagedManifestPath $destination
if (-not (Test-Path -LiteralPath $manifestPath)) { continue }
$manifest = Read-ManagedManifest $manifestPath
$missing = @()
$modified = @()
foreach ($relative in $manifest.Keys) {
$target = Join-Path $destination $relative
if (-not (Test-Path -LiteralPath $target)) { $missing += $relative; continue }
if ((Get-Sha256Hash $target) -ne $manifest[$relative]) { $modified += $relative }
}
if ($missing.Count) { Result 'WARN' "$destination is missing managed files: $($missing -join ', ')" } else { Result 'PASS' "$destination has no missing managed files" }
if ($modified.Count) { Result 'WARN' "$destination has locally modified managed files: $($modified -join ', ')" } else { Result 'PASS' "$destination has no locally modified managed files" }
}
foreach ($path in @('shared/rules','shared/skills','shared/agents','shared/hooks/scripts')) { if (Test-Path (Join-Path $root $path)) { Result 'PASS' "source $path" } else { Result 'FAIL' "missing $path" } }
foreach ($client in @('codex','claude')) {
$config = if ($client -eq 'codex') { Join-Path $root 'adapters/codex/config/config.toml' } else { Join-Path $root 'adapters/claude/config/settings.json' }
$registered = if ($client -eq 'codex') { (Get-Content $config -Raw) -match '\[\[hooks\.Stop\]\]' } else { ((Get-Content $config -Raw | ConvertFrom-Json).hooks.PSObject.Properties.Name -contains 'Stop') }
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
}
if ($Summary) {
if ($script:fail) {
$script:passBuffer | ForEach-Object { Write-Output $_ }
Write-Output "Doctor: FAIL | $script:checkCount checks"
exit 1
}
Write-Output "Doctor: PASS | $script:checkCount checks"
} else {
if ($script:fail) { exit 1 }
Write-Output 'PASS doctor'
}
exit 0
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--summary) summary=true; shift ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/manifest.sh"
home_path=${HOME:?HOME is required}
failed=false
check_count=0
pass_buffer=()
result() {
check_count=$((check_count + 1))
[ "$1" != FAIL ] || failed=true
if [ "$summary" = true ] && [ "$1" = PASS ]; then
pass_buffer+=("$1 $2")
else
printf '%s %s\n' "$1" "$2"
fi
}
for tool in codex claude; do
if command -v "$tool" >/dev/null; then result PASS "$tool available ($($tool --version 2>&1 | head -n 1))"; else result WARN "$tool unavailable"; fi
done
command -v jq >/dev/null && result PASS 'jq available for Claude Bash status line' || result WARN 'jq unavailable; Claude Bash status line is disabled'
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
source_skill_count=$(find "$root/shared/skills" -name SKILL.md 2>/dev/null | wc -l)
rule_skill_manifest="$root/adapters/rule-skills.tsv"
rule_skill_count=0
[ -f "$rule_skill_manifest" ] && rule_skill_count=$(($(wc -l < "$rule_skill_manifest") - 1))
for shell in powershell bash; do
[ -f "$root/generated/codex-$shell/AGENTS.md" ] && result PASS "generated output present ($shell)" || result FAIL "generated output missing ($shell); run build"
[ -f "$root/generated/codex-$shell/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/codex-$shell/hooks/scripts/validate-command-safety.sh" ] && [ -f "$root/generated/claude-$shell/hooks/scripts/Validate-CommandSafety.ps1" ] && [ -f "$root/generated/claude-$shell/hooks/scripts/validate-command-safety.sh" ] && result PASS "generated hooks present ($shell)" || result FAIL "generated hooks missing ($shell); run build"
for client in codex claude; do
package="$root/generated/$client-$shell"
[ -d "$package" ] || continue
agent_count=$(find "$package/agents" -type f 2>/dev/null | wc -l)
if [ "$agent_count" -eq "$source_agent_count" ]; then result PASS "$client-$shell agent count matches source ($agent_count)"; else result FAIL "$client-$shell agent count $agent_count does not match source ($source_agent_count)"; fi
expected_skill_count=$source_skill_count
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
skill_count=$(find "$package/skills" -name SKILL.md 2>/dev/null | wc -l)
if [ "$skill_count" -eq "$expected_skill_count" ]; then result PASS "$client-$shell skill count matches source ($skill_count)"; else result FAIL "$client-$shell skill count $skill_count does not match source ($expected_skill_count)"; fi
leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$package" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__)$' || true)
if [ -n "$leftover_tokens" ]; then result FAIL "$client-$shell has unresolved placeholders"; else result PASS "$client-$shell has no unresolved placeholders"; fi
done
done
for path in .codex/AGENTS.md .codex/config.toml .claude/CLAUDE.md .claude/settings.json; do
[ -f "$home_path/$path" ] && result PASS "installed $path" || result WARN "not installed $path"
done
if [ -f "$home_path/.claude/settings.json" ]; then
if command -v jq >/dev/null 2>&1; then
if jq empty "$home_path/.claude/settings.json" >/dev/null 2>&1; then result PASS 'installed .claude/settings.json is valid JSON'; else result FAIL 'installed .claude/settings.json is not valid JSON'; fi
elif command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; then
if python3 -c 'import json,sys; json.load(open(sys.argv[1], encoding="utf-8"))' "$home_path/.claude/settings.json" 2>/dev/null; then
result PASS 'installed .claude/settings.json is valid JSON'
else
result FAIL 'installed .claude/settings.json is not valid JSON'
fi
fi
fi
if [ -f "$home_path/.codex/config.toml" ]; then
if python3 -c 'import sys,tomllib,pathlib; tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8-sig"))' "$home_path/.codex/config.toml"; then
result PASS 'installed .codex/config.toml parses as TOML'
else
result FAIL 'installed .codex/config.toml is invalid TOML'
fi
if command -v codex >/dev/null; then
if CODEX_HOME="$home_path/.codex" codex --strict-config --help >/dev/null 2>&1; then result PASS 'installed .codex/config.toml matches the installed Codex schema'; else result FAIL 'installed .codex/config.toml has unsupported Codex settings'; fi
else result WARN 'Codex CLI unavailable; skipped installed Codex schema validation'; fi
fi
for destination in "$home_path/.codex" "$home_path/.agents/skills" "$home_path/.claude"; do
manifest=$(manifest_path "$destination")
[ -f "$manifest" ] || continue
declare -A managed
read_managed_manifest "$manifest" managed
missing=()
modified=()
for relative in "${!managed[@]}"; do
target="$destination/$relative"
if [ ! -f "$target" ]; then missing+=("$relative"); continue; fi
[ "$(sha256_of_file "$target")" = "${managed[$relative]}" ] || modified+=("$relative")
done
if [ "${#missing[@]}" -eq 0 ]; then result PASS "$destination has no missing managed files"; else result WARN "$destination is missing managed files: ${missing[*]}"; fi
if [ "${#modified[@]}" -eq 0 ]; then result PASS "$destination has no locally modified managed files"; else result WARN "$destination has locally modified managed files: ${modified[*]}"; fi
done
for path in shared/rules shared/skills shared/agents shared/hooks/scripts; do
[ -e "$root/$path" ] && result PASS "source $path" || result FAIL "missing $path"
done
if [ "$summary" = true ]; then
if [ "$failed" = true ]; then
for line in "${pass_buffer[@]}"; do printf '%s\n' "$line"; done
printf 'Doctor: FAIL | %s checks\n' "$check_count"
exit 1
fi
printf 'Doctor: PASS | %s checks\n' "$check_count"
else
[ "$failed" = false ] || exit 1
printf 'PASS doctor\n'
fi
+54
View File
@@ -0,0 +1,54 @@
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('PowerShell','Bash')][string]$Shell,
[switch]$Summary
)
$ErrorActionPreference = 'Stop'
$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()
$Client = Read-InstallClient -Client $Client
$homePath = [Environment]::GetFolderPath('UserProfile')
if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
throw 'Already installed, use the update script.'
}
$options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun
$reinstallViaNpm = Read-CliReinstallOption -Client $Client -DryRun:$DryRun
$buildScript = Join-Path $root 'scripts/commands/build.ps1'
& $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Installation was not started.' }
$generated = Join-Path $root 'generated'
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$powerShellCommand = if (Get-Command powershell -ErrorAction SilentlyContinue) { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'pwsh -NoProfile -ExecutionPolicy Bypass -File' }
$shellCommand = if ($Shell -eq 'PowerShell') { $powerShellCommand } else { 'bash' }
$claudeConcurrency = if ($env:CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY) { $env:CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY } else { '5' }
if ($claudeConcurrency -notmatch '^[1-9][0-9]?$' -or [int]$claudeConcurrency -gt 10) { throw 'CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY must be an integer from 1 to 10.' }
$plugins = Select-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -Mode 'Install' -DryRun:$DryRun
if ($options.update_agents) { Update-SelectedAgentClis -Client $Client -DryRun:$DryRun -Summary:$Summary }
if ($reinstallViaNpm) { Invoke-CliReinstall -Client $Client -DryRun:$DryRun -Summary:$Summary }
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Shell $shell -Client $Client)) {
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -ClaudeConcurrency $claudeConcurrency -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
}
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries $plugins.Selected
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Summary:$Summary
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
if ($Summary) { Write-Output "Install: PASS | $Client, $Shell$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS install dry-run' } else { 'PASS install' })) }
exit 0
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
dry_run=false
client=""
shell=""
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--summary) summary=true; shift ;;
--client) client=${2:?--client requires a value}; shift 2 ;;
--shell) shell=${2:?--shell requires a value}; shift 2 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
. "$root/scripts/lib/install-options.sh"
. "$root/scripts/lib/semantic-retrieval.sh"
shell=$(read_install_shell "$shell")
client=$(read_install_client "$client")
home_path=${HOME:?HOME is required}
if [ "$dry_run" = false ]; then
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
if any_manifest_present "${destinations[@]}"; then
printf 'Already installed, use the update script.\n' >&2
exit 1
fi
fi
read_install_options
reinstall_via_npm=$(read_cli_reinstall_option "$client" "$dry_run")
build_args=()
[ "$summary" = false ] || build_args+=(--summary)
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; }
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" != powershell ] || shell_command=$powershell_command
claude_concurrency=${CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY:-5}
[[ "$claude_concurrency" =~ ^[1-9][0-9]?$ ]] && [ "$claude_concurrency" -le 10 ] || { printf 'CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY must be an integer from 1 to 10.\n' >&2; exit 1; }
selected_plugins=()
deselected_plugins=()
select_configured_plugins "$root" "$client" Install "$dry_run" selected_plugins deselected_plugins
[ "$update_agents" = false ] || update_selected_agent_clis
[ "$reinstall_via_npm" = false ] || invoke_cli_reinstall "$client" "$dry_run" "$summary"
while IFS='|' read -r source destination; do
ai_config_root=$destination
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang" "$claude_concurrency"
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
sync_semantic_retrieval "$root" "$client" "$home_path" "$semantic_retrieval" "$dry_run" false "$summary"
if [ "$dry_run" = false ]; then save_update_selection; fi
if [ "$summary" = true ]; then
suffix=
[ "$dry_run" = false ] || suffix=', dry-run'
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
+36
View File
@@ -0,0 +1,36 @@
[CmdletBinding()]
param([switch]$Summary)
$ErrorActionPreference = 'Stop'
$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) {
if (-not (Test-Path $path)) { return 0 }
return [int64]((Get-ChildItem $path -File -Recurse | ForEach-Object { $_.Length } | Measure-Object -Sum).Sum)
}
function Tokens([int64]$bytes) { return [int][Math]::Ceiling($bytes / 4.0) }
$claude = (Get-Item (Join-Path $generated 'claude-bash/CLAUDE.md')).Length
$codex = (Get-Item (Join-Path $generated 'codex-bash/AGENTS.md')).Length
$skills = Measure-Tree (Join-Path $generated 'claude-bash/skills')
$triggers = (Get-Item (Join-Path $root 'adapters/rule-skills.tsv')).Length
$lazyItem = Get-ChildItem (Join-Path $root 'shared/rules') -File -Recurse | Sort-Object Length -Descending | Select-Object -First 1
$lazy = if ($lazyItem) { [int64]$lazyItem.Length } else { 0 }
$baseline = Join-Path $root 'adapters/prompt-budget-baseline.json'
$old = if (Test-Path $baseline) { Get-Content $baseline -Raw | ConvertFrom-Json } else { $null }
$rows = @([pscustomobject]@{Name='Claude permanent context';Bytes=$claude;Tokens=(Tokens $claude)},[pscustomobject]@{Name='Codex permanent context';Bytes=$codex;Tokens=(Tokens $codex)},[pscustomobject]@{Name='Claude skill files (lazy)';Bytes=$skills;Tokens=(Tokens $skills)},[pscustomobject]@{Name='Rule trigger metadata';Bytes=$triggers;Tokens=(Tokens $triggers)},[pscustomobject]@{Name='Largest lazy rule';Bytes=$lazy;Tokens=(Tokens $lazy)})
$fail = $false
$baselineValues = @{}
if ($old) {
foreach ($row in $rows | Select-Object -First 2) {
$previous = [double]$old.($row.Name)
$absolute = [double]$old.(($row.Name + ' absolute limit'))
$baselineValues[$row.Name] = @($previous, $absolute)
if ($previous -gt 0 -and $row.Bytes -gt $previous * 1.15) { $fail = $true; Write-Error "Prompt budget relative limit failed: $($row.Name) $($row.Bytes) bytes (baseline $previous, limit 15%)." }
if ($absolute -gt 0 -and $row.Bytes -gt $absolute) { $fail = $true; Write-Error "Prompt budget absolute limit failed: $($row.Name) $($row.Bytes) bytes (absolute limit $absolute)." }
}
}
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 $root 'scripts/lib/prompt-inventory.py') --home ([Environment]::GetFolderPath('UserProfile'))
if ($LASTEXITCODE -ne 0) { throw 'Prompt inventory failed.' }
if ($fail) { exit 1 }
exit 0
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
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)
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 ' '; }
tree_bytes() { find "$1" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END {print s+0}'; }
tokens() { awk -v b="$1" 'BEGIN { printf "%d", (b + 3) / 4 }'; }
claude=$(bytes "$generated/claude-bash/CLAUDE.md"); codex=$(bytes "$generated/codex-bash/AGENTS.md"); skills=$(tree_bytes "$generated/claude-bash/skills"); triggers=$(bytes "$root/adapters/rule-skills.tsv"); lazy=$(find "$root/shared/rules" -type f -printf '%s %p\n' | sort -nr | awk 'NR==1 {print $1+0}'); fail=false
claude_baseline=0; codex_baseline=0; claude_absolute=0; codex_absolute=0
if [ -f "$root/adapters/prompt-budget-baseline.tsv" ]; then
while IFS=$'\t' read -r name old absolute; do
[ "$name" = Claude ] || [ "$name" = Codex ] || continue
current=$([ "$name" = Claude ] && printf '%s' "$claude" || printf '%s' "$codex")
[ "$name" = Claude ] && claude_baseline=$old && claude_absolute=$absolute
[ "$name" = Codex ] && codex_baseline=$old && codex_absolute=$absolute
if awk -v n="$current" -v o="$old" 'BEGIN { exit !(o > 0 && n > o * 1.15) }'; then printf 'Prompt budget relative limit failed: %s %s bytes (baseline %s, limit 15%%).\n' "$name" "$current" "$old" >&2; fail=true; fi
if awk -v n="$current" -v a="$absolute" 'BEGIN { exit !(a > 0 && n > a) }'; then printf 'Prompt budget absolute limit failed: %s %s bytes (absolute limit %s).\n' "$name" "$current" "$absolute" >&2; fail=true; fi
done < "$root/adapters/prompt-budget-baseline.tsv"
fi
if [ "$summary" = true ]; then printf 'Prompt Budget\nClaude permanent context %8s bytes baseline %s relative 115%% absolute %s\nCodex permanent context %8s bytes baseline %s relative 115%% absolute %s\nClaude skill files (lazy) %8s bytes %6s tokens\nRule trigger metadata %8s bytes %6s tokens\nLargest lazy rule %8s bytes %6s tokens\n' "$claude" "$claude_baseline" "$claude_absolute" "$codex" "$codex_baseline" "$codex_absolute" "$skills" "$(tokens "$skills")" "$triggers" "$(tokens "$triggers")" "$lazy" "$(tokens "$lazy")"; else printf 'Prompt budget: Claude %s bytes, Codex %s bytes, skills %s bytes, triggers %s bytes, largest lazy rule %s bytes\n' "$claude" "$codex" "$skills" "$triggers" "$lazy"; fi
python3 "$root/scripts/lib/prompt-inventory.py" --home "${HOME:?HOME is required}"
$fail && { printf 'Prompt budget: permanent context increased by more than 15 percent.\n' >&2; exit 1; }
exit 0
+23
View File
@@ -0,0 +1,23 @@
[CmdletBinding()]
param([switch]$Force,[switch]$Summary)
$ErrorActionPreference = 'Stop'
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$cacheDir = Join-Path $root '.ai-session'
$cachePath = Join-Path $cacheDir 'repository-context.json'
$safeRoot = $root.Replace('\','/')
$head = (& git -c "safe.directory=$safeRoot" -C $root rev-parse HEAD 2>$null).Trim()
$inputs = @('AI-Instructions.md','adapters/plugins.tsv','adapters/rule-skills.tsv','adapters/claude/capabilities.tsv','adapters/prompt-budget-baseline.json','adapters/prompt-budget-baseline.tsv','adapters/codex/config/config.toml','adapters/claude/config/settings.json')
$inputs += @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' -and $_.Extension -in @('.sln','.csproj','.fsproj','.ts','.tsx','.json','.toml','.yaml','.yml','.cs','.fs','.py','.sh','.ps1') } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } | Sort-Object)
$keyText = $head + "`n" + (($inputs | Sort-Object -Unique | ForEach-Object { $_ + ':' + (Get-FileHash (Join-Path $root $_) -Algorithm SHA256).Hash }) -join "`n")
$key = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($keyText)).TrimEnd('=')
if (-not $Force -and (Test-Path $cachePath)) { try { $existing = Get-Content $cachePath -Raw | ConvertFrom-Json; if ($existing.invalidationKey -eq $key) { if ($Summary) { Write-Output 'Repository context: unchanged' }; exit 0 } } catch {} }
$extensions = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj|vendor|\.ai-session)\\' } | Group-Object Extension | Sort-Object Count -Descending)
$languages = @(); foreach ($item in $extensions) { switch ($item.Name.ToLowerInvariant()) { '.cs' { $languages += 'csharp' }; '.ts' { $languages += 'typescript' }; '.tsx' { $languages += 'typescript' }; '.py' { $languages += 'python' }; '.ps1' { $languages += 'powershell' }; '.sh' { $languages += 'bash' } } }
$languages = @($languages | Sort-Object -Unique)
$solutions = @(Get-ChildItem $root -Filter *.sln -File -Recurse | Where-Object { $_.FullName -notmatch '\\(generated|\.git)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
$tests = @(Get-ChildItem $root -File -Recurse | Where-Object { $_.Name -match '(?i)test' -and $_.FullName -notmatch '\\(generated|\.git|node_modules|bin|obj)\\' } | ForEach-Object { $_.BaseName } | Sort-Object -Unique)
$packageManager = if (Test-Path (Join-Path $root 'pnpm-lock.yaml')) { 'pnpm' } elseif (Test-Path (Join-Path $root 'package-lock.json')) { 'npm' } elseif (Test-Path (Join-Path $root 'yarn.lock')) { 'yarn' } else { $null }
$data = [ordered]@{ invalidationKey=$key; head=$head; languages=$languages; frameworks=@(); solutions=$solutions; testProjects=$tests; packageManager=$packageManager; buildCommands=[ordered]@{}; generatedAt=(Get-Date).ToUniversalTime().ToString('o') }
New-Item $cacheDir -ItemType Directory -Force | Out-Null
$data | ConvertTo-Json -Depth 5 | Set-Content $cachePath -Encoding UTF8
if ($Summary) { Write-Output ('Repository context: refreshed | {0} languages | {1} solutions | {2} tests' -f $languages.Count,$solutions.Count,$tests.Count) } else { Write-Output $cachePath }
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
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"
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}')
if [ "$force" = false ] && [ -f "$cache_path" ] && grep -Fq "\"invalidationKey\": \"$key\"" "$cache_path"; then [ "$summary" = true ] && printf 'Repository context: unchanged\n'; exit 0; fi
mapfile -t languages < <(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/.ai-session/*' | awk -F. 'NF>1 {print $NF}' | sort -u | awk 'BEGIN{a["cs"]="csharp";a["ts"]="typescript";a["tsx"]="typescript";a["py"]="python";a["ps1"]="powershell";a["sh"]="bash"} a[$1]!=""{print a[$1]}' | sort -u)
solutions=$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' -printf '"%f",' | sed 's/,$//'); tests=$(find "$root" -type f ! -path '*/generated/*' ! -path '*/.git/*' -iname '*test*' -printf '"%f",' | sed 's/,$//'); manager=null; [ -f "$root/pnpm-lock.yaml" ] && manager='"pnpm"'; [ -f "$root/package-lock.json" ] && manager='"npm"'; [ -f "$root/yarn.lock" ] && manager='"yarn"'
mkdir -p "$cache_dir"; printf '{\n "invalidationKey": "%s",\n "head": "%s",\n "languages": [%s],\n "frameworks": [],\n "solutions": [%s],\n "testProjects": [%s],\n "packageManager": %s,\n "buildCommands": {}\n}\n' "$key" "$head" "$(printf '"%s",' "${languages[@]}" | sed 's/,$//')" "$solutions" "$tests" "$manager" > "$cache_path"
if [ "$summary" = true ]; then printf 'Repository context: refreshed | %s languages | %s solutions\n' "${#languages[@]}" "$(find "$root" -type f -name '*.sln' ! -path '*/generated/*' | wc -l)"; else printf '%s\n' "$cache_path"; fi
+31
View File
@@ -0,0 +1,31 @@
[CmdletBinding()]
param(
[string]$Workspace = (Get-Location).Path,
[AllowEmptyString()][string]$Goal,
[AllowEmptyCollection()][string[]]$Decisions,
[AllowEmptyCollection()][string[]]$ChangedFiles,
[AllowEmptyCollection()][string[]]$Verified,
[AllowEmptyCollection()][string[]]$Pending,
[AllowEmptyCollection()][string[]]$Risks,
[AllowEmptyCollection()][string[]]$ImportantFindings,
[switch]$Pointer,
[switch]$Reset
)
$ErrorActionPreference = 'Stop'
$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) {
if ($PSBoundParameters.ContainsKey($parameter)) { $values[$fields[$parameter]] = $PSBoundParameters[$parameter] }
}
if ($PSBoundParameters.ContainsKey('ImportantFindings') -and -not $PSBoundParameters.ContainsKey('Decisions')) {
$values.decisions = $ImportantFindings
}
$OutputEncoding = New-Object System.Text.UTF8Encoding($false)
$arguments = @($helper, '--update-json')
if ($Pointer) { $arguments += '--pointer' }
if ($Reset) { $arguments += '--reset' }
$payload = $values | ConvertTo-Json -Depth 4 -Compress
$payload = [regex]::Replace($payload, '[^\x00-\x7F]', { param($match) '\u{0:x4}' -f [int][char]$match.Value })
$payload | & python @arguments
exit $LASTEXITCODE
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
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[@]}" "$@"
+10
View File
@@ -0,0 +1,10 @@
[CmdletBinding()]
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 (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.' }
$record=[ordered]@{event=$Event;timestamp=(Get-Date).ToUniversalTime().ToString('o')}; foreach($pair in @(@('agent',$Agent),@('role',$Role),@('durationMs',$DurationMs),@('toolCalls',$ToolCalls),@('commandClass',$CommandClass),@('bytes',$Bytes),@('count',$Count),@('handoffBytes',$HandoffBytes),@('filesRead',$FilesRead),@('filesChanged',$FilesChanged),@('outcome',$Outcome))){if($null -ne $pair[1] -and "$($pair[1])" -ne ''){$record[$pair[0]]=$pair[1]}}; $tokens=[ordered]@{}; foreach($pair in @(@('input',$InputTokens),@('cached',$CachedTokens),@('output',$OutputTokens),@('reasoning',$ReasoningTokens))){if($null -ne $pair[1]){$tokens[$pair[0]]=$pair[1]}}; if($tokens.Count){$record.tokenUsage=$tokens}; ($record|ConvertTo-Json -Compress)|Add-Content $path; if((Get-Item $path).Length -gt 1048576){Move-Item $path ($path+'.1') -Force}
} catch { exit 0 }
+40
View File
@@ -0,0 +1,40 @@
#!/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"
action=${1:-record}; shift || true
case "$action" in
rotate) [ -f "$path" ] && mv -f "$path" "$path.1"; exit 0;;
summary) [ -f "$path" ] || { printf 'Telemetry: no events\n'; exit 0; }; python3 - "$path" <<'PY' || exit 0
import json,sys,statistics
rows=[]
for line in open(sys.argv[1],encoding='utf-8',errors='replace'):
try: rows.append(json.loads(line))
except Exception: pass
print(f'Telemetry: {len(rows)} events')
for role in sorted({r.get('role') for r in rows if r.get('role')}):
subset=[r for r in rows if r.get('role')==role]
inputs=[r['tokenUsage']['input'] for r in subset if isinstance(r.get('tokenUsage'),dict) and isinstance(r['tokenUsage'].get('input'),(int,float))]
print(f"{role}: runs {len(subset)} findings {sum(r.get('outcome')=='findings' for r in subset)} median input {int(statistics.median(inputs)) if inputs else '-'}")
PY
exit 0;;
record) event=${1:?event required}; shift;; *) printf 'Unknown action: %s\n' "$action" >&2; exit 1;;
esac
python3 - "$path" "$event" "$@" <<'PY' || exit 0
import json,sys,datetime
path,event=sys.argv[1:3]; args=sys.argv[3:]
record={'event':event,'timestamp':datetime.datetime.now(datetime.timezone.utc).isoformat()}
keys=['role','durationMs','handoffBytes','filesRead','filesChanged','outcome','input','cached','output','reasoning']
if any(a.startswith('--') for a in args):
parsed={}; i=0
while i < len(args):
if args[i].startswith('--') and i+1 < len(args): parsed[args[i][2:]]=args[i+1]; i+=2
else: i+=1
args=[parsed.get(k,'') for k in keys]
for key,value in zip(keys,args):
if value: record[key]=int(value) if value.lstrip('-').isdigit() else value
tokens={k:record.pop(k) for k in ('input','cached','output','reasoning') if k in record}
if tokens: record['tokenUsage']=tokens
with open(path,'a',encoding='utf-8') as out: out.write(json.dumps(record,separators=(',',':'))+'\n')
PY
if [ "$(wc -c < "$path")" -gt 1048576 ]; then mv -f "$path" "$path.1"; fi
+75
View File
@@ -0,0 +1,75 @@
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[switch]$Force,
[switch]$Summary
)
$ErrorActionPreference = 'Stop'
$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')
$generated = Join-Path $root 'generated'
$targets = @(Get-InstallTargets -Generated $generated -HomePath $homePath -Shell 'bash' -Client $Client)
$installedTargets = @($targets | Where-Object { Test-Path -LiteralPath (Get-ManagedManifestPath $_.Destination) })
if (-not $DryRun -and $installedTargets.Count -eq 0) { throw 'Not installed, nothing to uninstall.' }
if (-not $DryRun -and -not $Force) {
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') {
throw 'Refusing to uninstall without confirmation in a non-interactive session. Pass -Force to proceed.'
}
Write-Host "This removes the managed configuration, extensions, and semantic retrieval setup for $Client under $homePath."
Write-Host 'Backups already on disk are kept; anything this setup never installed is left untouched.'
$confirm = Read-Host -Prompt 'Continue? [y/N]'
if ($null -eq $confirm) { $confirm = '' }
if ($confirm.Trim().ToLowerInvariant() -notin @('y', 'yes')) {
Write-Output 'Cancelled, nothing was removed.'
exit 1
}
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$emptySource = Join-Path ([System.IO.Path]::GetTempPath()) "ai-config-uninstall-$stamp"
New-Item $emptySource -ItemType Directory -Force | Out-Null
try {
foreach ($item in $targets) {
if (-not (Test-Path -LiteralPath (Get-ManagedManifestPath $item.Destination))) {
if (-not $Summary) { Write-Output "SKIP $($item.Destination) (not installed)" }
continue
}
Sync-ManagedDestination -Source $emptySource -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand '' -PowerShellCommand '' -DryRun:$DryRun -Summary:$Summary
if (-not $DryRun) {
Remove-Item -LiteralPath (Get-ManagedManifestPath $item.Destination) -Force -ErrorAction SilentlyContinue
if (@(Get-ChildItem -LiteralPath $item.Destination -Force -ErrorAction SilentlyContinue).Count -eq 0) {
Remove-Item -LiteralPath $item.Destination -Force -ErrorAction SilentlyContinue
}
}
}
} finally {
Remove-Item -LiteralPath $emptySource -Recurse -Force -ErrorAction SilentlyContinue
}
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Summary:$Summary -Entries @()
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $false -DryRun:$DryRun -Summary:$Summary
if (-not $DryRun) {
$remaining = @($targets | Where-Object { Test-Path -LiteralPath (Get-ManagedManifestPath $_.Destination) })
if ($remaining.Count -eq 0) {
$configRoot = Join-Path $homePath '.my-ai-configuration'
$selectionPath = Join-Path $configRoot 'selection.json'
if (Test-Path -LiteralPath $selectionPath) { Remove-Item -LiteralPath $selectionPath -Force }
if ((Test-Path -LiteralPath $configRoot) -and @(Get-ChildItem -LiteralPath $configRoot -Force -ErrorAction SilentlyContinue).Count -eq 0) {
Remove-Item -LiteralPath $configRoot -Force -ErrorAction SilentlyContinue
}
}
}
if ($Summary) { Write-Output "Uninstall: PASS | $Client$(if ($DryRun) { ', dry-run' })" } else { Write-Output ($(if ($DryRun) { 'PASS uninstall dry-run' } else { 'PASS uninstall' })) }
exit 0
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
dry_run=false
client=""
force=false
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--force) force=true; shift ;;
--summary) summary=true; shift ;;
--client) client=${2:?--client requires a value}; shift 2 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/semantic-retrieval.sh"
client=$(read_install_client "$client")
home_path=${HOME:?HOME is required}
generated="$root/generated"
mapfile -t targets < <(get_install_targets "$generated" "$home_path" bash "$client")
installed_count=0
for target_pair in "${targets[@]}"; do
[ -f "$(manifest_path "${target_pair#*|}")" ] && installed_count=$((installed_count + 1))
done
if [ "$dry_run" = false ] && [ "$installed_count" -eq 0 ]; then
printf 'Not installed, nothing to uninstall.\n' >&2
exit 1
fi
if [ "$dry_run" = false ] && [ "$force" = false ]; then
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then
printf 'Refusing to uninstall without confirmation in a non-interactive session. Pass --force to proceed.\n' >&2
exit 1
fi
printf 'This removes the managed configuration, extensions, and semantic retrieval setup for %s under %s.\n' "$client" "$home_path"
printf 'Backups already on disk are kept; anything this setup never installed is left untouched.\n'
read -r -p "Continue? [y/N]: " confirm || confirm=""
case "$confirm" in
[Yy]|[Yy][Ee][Ss]) ;;
*)
printf 'Cancelled, nothing was removed.\n'
exit 1
;;
esac
fi
stamp=$(date +%Y%m%d-%H%M%S)
empty_source=$(mktemp -d)
cleanup() { rm -rf -- "$empty_source"; }
trap cleanup EXIT
for target_pair in "${targets[@]}"; do
destination=${target_pair#*|}
if [ ! -f "$(manifest_path "$destination")" ]; then
[ "$summary" = true ] || printf 'SKIP %s (not installed)\n' "$destination"
continue
fi
ai_config_root=$destination
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
sync_managed_destination "$empty_source" "$destination" "$stamp" "$ai_config_root" '' '' "$dry_run" "$summary"
if [ "$dry_run" = false ]; then
rm -f -- "$(manifest_path "$destination")"
[ -n "$(find "$destination" -mindepth 1 -maxdepth 1 2>/dev/null)" ] || rmdir -- "$destination" 2>/dev/null || true
fi
done
# shellcheck disable=SC2034
selected_plugins=()
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" false selected_plugins "$summary"
sync_semantic_retrieval "$root" "$client" "$home_path" false "$dry_run" false "$summary"
if [ "$dry_run" = false ]; then
remaining=0
for target_pair in "${targets[@]}"; do
[ -f "$(manifest_path "${target_pair#*|}")" ] && remaining=$((remaining + 1))
done
if [ "$remaining" -eq 0 ]; then
config_root="$home_path/.my-ai-configuration"
rm -f -- "$config_root/selection.json"
[ -n "$(find "$config_root" -mindepth 1 -maxdepth 1 2>/dev/null)" ] || rmdir -- "$config_root" 2>/dev/null || true
fi
fi
if [ "$summary" = true ]; then
suffix=
[ "$dry_run" = false ] || suffix=', dry-run'
printf 'Uninstall: PASS | %s%s\n' "$client" "$suffix"
elif "$dry_run"; then printf 'PASS uninstall dry-run\n'; else printf 'PASS uninstall\n'; fi
+72
View File
@@ -0,0 +1,72 @@
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$Quick,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[ValidateSet('PowerShell','Bash')][string]$Shell,
[switch]$Summary
)
$ErrorActionPreference = 'Stop'
$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) {
$saved = Read-UpdateSelection -HomePath $homePath -RepositoryRoot $root
if (-not $Shell) { $Shell = $saved.shell }
if (-not $Client) { $Client = $saved.client }
}
$Shell = Read-InstallShell -Shell $Shell
$shell = $Shell.ToLowerInvariant()
$Client = Read-InstallClient -Client $Client
if (-not $DryRun -and -not (Test-AllManifestsPresent (Get-InstallDestinations -HomePath $homePath -Client $Client))) {
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 $root 'scripts/commands/build.ps1'
& $buildScript -Summary:$Summary
if (-not $?) { throw 'Build failed. Update was not started.' }
$generated = Join-Path $root 'generated'
if (-not (Test-Path $generated)) { throw 'Generated output is missing after a successful build.' }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$powerShellCommand = if (Get-Command powershell -ErrorAction SilentlyContinue) { 'powershell -NoProfile -ExecutionPolicy Bypass -File' } else { 'pwsh -NoProfile -ExecutionPolicy Bypass -File' }
$shellCommand = if ($Shell -eq 'PowerShell') { $powerShellCommand } else { 'bash' }
$plugins = if ($Quick) {
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $root)
@{ Selected = @($entries | Where-Object { $saved.selected -contains $_.name }); Deselected = @($entries | Where-Object { $saved.deselected -contains $_.name }) }
} else {
Select-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -Mode 'Update' -DryRun:$DryRun
}
if ($options.update_agents) { Update-SelectedAgentClis -Client $Client -DryRun:$DryRun -Summary:$Summary }
foreach ($item in (Get-InstallTargets -Generated $generated -HomePath $homePath -Shell $shell -Client $Client)) {
Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp $stamp `
-AiConfigRoot $item.Destination.Replace('\','/') -ShellCommand $shellCommand -PowerShellCommand $powerShellCommand -FlashbangEnabled $options.flashbang -DryRun:$DryRun -Summary:$Summary
}
$previousPluginNonInteractive = $script:PluginNonInteractive
$previousCI = $env:CI
try {
$script:PluginNonInteractive = [bool]$Quick
if ($Quick) { $env:CI = 'true' }
Sync-ConfiguredPlugins -RepositoryRoot $root -HomePath $homePath -Client $Client -DryRun:$DryRun -Update -Summary:$Summary -Entries $plugins.Selected
} finally {
$script:PluginNonInteractive = $previousPluginNonInteractive
$env:CI = $previousCI
}
Sync-SemanticRetrieval -RepositoryRoot $root -HomePath $homePath -Client $Client -Enabled $options.semantic_retrieval -DryRun:$DryRun -Update -Summary:$Summary
if (-not $DryRun) { Save-UpdateSelection -HomePath $homePath -RepositoryRoot $root -Shell $Shell -Client $Client -Plugins $plugins -UpdateAgents $options.update_agents -Flashbang $options.flashbang -SemanticRetrieval $options.semantic_retrieval }
if ($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
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
dry_run=false
quick=false
client=""
shell=""
summary=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true; shift ;;
--quick) quick=true; shift ;;
--summary) summary=true; shift ;;
--client) client=${2:?--client requires a value}; shift 2 ;;
--shell) shell=${2:?--shell requires a value}; shift 2 ;;
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
. "$root/scripts/lib/plugins.sh"
. "$root/scripts/lib/manifest.sh"
. "$root/scripts/lib/install-targets.sh"
. "$root/scripts/lib/selection-state.sh"
. "$root/scripts/lib/install-options.sh"
. "$root/scripts/lib/semantic-retrieval.sh"
home_path=${HOME:?HOME is required}
selected_plugins=()
deselected_plugins=()
if [ "$quick" = true ]; then read_update_selection; fi
shell=$(read_install_shell "$shell")
client=$(read_install_client "$client")
if [ "$dry_run" = false ]; then
mapfile -t destinations < <(get_install_destinations "$home_path" "$client")
if ! all_manifests_present "${destinations[@]}"; then
printf 'Not installed, use the install script.\n' >&2
exit 1
fi
fi
if [ "$quick" = false ]; then read_install_options; fi
build_args=()
[ "$summary" = false ] || build_args+=(--summary)
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; }
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" != powershell ] || shell_command=$powershell_command
if [ "$quick" = false ]; then
select_configured_plugins "$root" "$client" Update "$dry_run" selected_plugins deselected_plugins
fi
[ "$update_agents" = false ] || update_selected_agent_clis
while IFS='|' read -r source destination; do
ai_config_root=$destination
command -v cygpath >/dev/null && ai_config_root=$(cygpath -m "$destination")
sync_managed_destination "$source" "$destination" "$stamp" "$ai_config_root" "$shell_command" "$powershell_command" "$dry_run" "$summary" "$flashbang"
done < <(get_install_targets "$generated" "$home_path" "$shell" "$client")
if [ "$quick" = true ]; then
CI=true sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary" </dev/null
else
sync_configured_plugins "$root" "$client" "$home_path" "$dry_run" true selected_plugins "$summary"
fi
sync_semantic_retrieval "$root" "$client" "$home_path" "$semantic_retrieval" "$dry_run" true "$summary"
if [ "$dry_run" = false ]; then save_update_selection; fi
if [ "$summary" = true ]; then
suffix=
[ "$dry_run" = false ] || suffix=', dry-run'
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