refactor(scripts): organize script entry points
This commit is contained in:
1 parent
793eee5b61
commit
11c2b6dfcb
41 files changed
+102
-101
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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[@]}" "$@"
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user