Compare commits

...
22 Commits
Author SHA1 Message Date
Julian lechner 4408f23c68 chore(shellcheck): annotate nameref inputs
CI / linux (push) Successful in 6s
2026-09-17 23:04:44 +02:00
Julian lechner c4676e9a2f chore(shellcheck): allow shared script globals 2026-09-17 23:04:24 +02:00
Julian lechner 110842b9a4 refactor(shell): remove unused local fields 2026-09-17 23:04:05 +02:00
Julian lechner fd707539d5 refactor(hooks): drop unused workspace variable 2026-09-17 23:03:44 +02:00
Julian lechner 953eb34e21 fix(retrieval): declare Bash interpreter 2026-09-17 23:03:23 +02:00
Julian lechner 670c907375 fix(plugins): clarify indirect shell fields 2026-09-17 23:02:59 +02:00
Julian lechner 60205169f4 fix(manifest): clarify dynamic shell references 2026-09-17 23:02:36 +02:00
Julian lechner c2a79cf415 fix(statusline): delimit ANSI escape variables 2026-09-17 23:01:43 +02:00
Julian lechner f7230dd84f ci: refresh pinned GitHub actions 2026-09-17 23:00:43 +02:00
Julian lechner 1bc59ca881 perf(retrieval): gate semantic repository search 2026-09-17 23:00:18 +02:00
Julian lechner fe9ca9552f test(config): enforce sandbox defaults 2026-09-17 23:00:01 +02:00
Julian lechner fb926e9877 feat(claude): enable strict command sandbox 2026-09-17 22:59:33 +02:00
Julian lechner b7bac4c9cc feat(codex): constrain sandbox and agent depth 2026-09-17 22:59:13 +02:00
Julian lechnerandClaude Sonnet 5 b77114ad90 docs(readme): document the npm CLI reinstall install option
Explains the new "Uninstall Codex/Claude/Both and reinstall via npm?"
install-time prompt, its npm package mapping, and its dry-run behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:59 +02:00
Julian lechnerandClaude Sonnet 5 dc71fb4cc4 test(install-options): feed the new reinstall prompt answer
The entrypoint test now supplies a fourth answer for the "Uninstall
... and reinstall via npm?" prompt install.ps1/install.sh ask after
the existing three install options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:49 +02:00
Julian lechnerandClaude Sonnet 5 5ae8e0cff5 feat(install): wire the npm CLI reinstall prompt (Bash)
install.sh now asks whether to uninstall and reinstall the selected
client CLI(s) via npm, using the helpers added to install-options.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:43 +02:00
Julian lechnerandClaude Sonnet 5 8379e29eb6 feat(install): wire the npm CLI reinstall prompt (PowerShell)
install.ps1 now asks whether to uninstall and reinstall the selected
client CLI(s) via npm, using the helpers added to install-options.ps1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:39 +02:00
Julian lechnerandClaude Sonnet 5 ed8dd89ba7 feat(install-options): add npm CLI reinstall helpers (Bash)
global_npm_package, cli_npm_package, read_cli_reinstall_option, and
invoke_cli_reinstall are the Bash equivalent of the PowerShell npm
reinstall helpers. Not wired into install.sh yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:34 +02:00
Julian lechnerandClaude Sonnet 5 6c3538f888 feat(install-options): add npm CLI reinstall helpers (PowerShell)
Test-GlobalNpmPackage, Get-CliNpmPackage, Read-CliReinstallOption, and
Invoke-CliReinstall let install.ps1 offer uninstalling and reinstalling
a client CLI through npm. Not wired into install.ps1 yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:29 +02:00
Julian lechnerandClaude Sonnet 5 769af0d206 docs(readme): document the uninstall script
Adds an Uninstall section describing usage, the confirmation guard,
and its removal/backup semantics, and mentions it in the feature list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:23 +02:00
Julian lechnerandClaude Sonnet 5 6bf3cc63d7 feat(uninstall): add uninstall.sh script
Bash equivalent of uninstall.ps1: removes managed files, extension/
skill ledger entries, semantic retrieval, and the saved Quickupdate
selection for a selected client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:48:03 +02:00
Julian lechnerandClaude Sonnet 5 942288eeda feat(uninstall): add uninstall.ps1 script
Removes managed files, extension/skill ledger entries, semantic
retrieval, and the saved Quickupdate selection for a selected client,
reusing the same manifest-sync backup and warn-and-retain semantics as
install and update.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:47:59 +02:00
25 changed files with 373 additions and 31 deletions

No files matched your search

+10 -10
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install shell tools
run: |
sudo apt-get update
@@ -42,8 +42,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.6.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Compile Python sources
@@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check PowerShell runtime
shell: bash
run: |
@@ -93,7 +93,7 @@ jobs:
timeout-minutes: 15
needs: [capabilities, python]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build packages
env:
REQUIRE_CODEX_SCHEMA: 'false'
@@ -113,7 +113,7 @@ jobs:
timeout-minutes: 25
needs: [build]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run integration tests
run: |
set -euo pipefail
@@ -137,15 +137,15 @@ jobs:
timeout-minutes: 30
needs: [python]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.6.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
cache: pip
cache-dependency-path: shared/retrieval/requirements.txt
- name: Install retrieval dependencies
run: python -m pip install -r shared/retrieval/requirements.txt
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.2.3
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .cache/huggingface
key: qwen-retrieval-${{ runner.os }}-${{ hashFiles('shared/retrieval/requirements.txt') }}
@@ -159,7 +159,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Reject unsafe workflow settings
run: |
set -euo pipefail
+48 -2
View File
@@ -108,8 +108,9 @@ Install and update also rebuild internally; the explicit build above lets you ch
files are retained with a warning.
- Dry-run support (no destination writes, backups, or plugin changes) and timestamped backup directories.
- Finish notifications, Claude session-state hooks, optional safety utilities, and compact status displays.
- Separate install and update scripts: install refuses to run against an already-installed destination, update refuses
to run against one that is not installed yet.
- Separate install, update, and uninstall scripts: install refuses to run against an already-installed destination,
update refuses to run against one that is not installed yet, and uninstall refuses to run against one that was
never installed.
- User-level plugin installation for Ponytail, i-have-adhd, Superpowers, Context7, Caveman, QMD, Humanizer, Impeccable, and
Anthropic Frontend Design, with an interactive extension toggle during both install and update. Only extensions
integrations installed and recorded by this setup can be removed through deselection.
@@ -220,6 +221,15 @@ the selected client CLIs, enable Flashbang, and enable Qwen3 semantic retrieval.
Disabling retrieval on a later update removes its setup-owned runtime and index. Dry runs preview
changes without updating CLIs, extensions, or retrieval. See [Plugins](docs/plugins.md) for client-specific behavior.
Install also always asks `Uninstall Codex/Claude/Both and reinstall via npm?` (default no), scoped to whatever client
was selected. This choice is independent of the "update selected agent CLIs" choice above, is never saved, and is not
asked again by Quickupdate or by the update script. Answering yes uninstalls and reinstalls, per selected client, the
matching CLI (`@openai/codex` for Codex, `@anthropic-ai/claude-code` for Claude) through `npm uninstall -g`
followed by `npm install -g <package>@latest`, when that CLI is currently found as a global npm package. When it is
not (for example, installed through a native installer or another package manager), that prior installation is left
in place with a warning, and only a fresh `npm install -g` is run alongside it. Codex is skipped with a warning while
`codex.exe` is running. A dry run never asks this question and never previews it, since the choice is not persisted.
## Update
Once a destination is installed, use the update script (not the installer) to refresh its files and plugins. It refuses
@@ -241,6 +251,42 @@ installer, re-syncs every managed file, and lets you revise the CLI update, Flas
Selected extensions are installed or updated. Deselection removes only extensions recorded as installed by this
setup; pre-existing or manually installed extensions are retained. Quickupdate reuses the saved choices.
## Uninstall
Remove everything this setup previously installed for the selected client(s): managed files tracked in each
destination's manifest, extensions and skills recorded in the extension ledger, an enabled semantic retrieval setup,
and the saved Quickupdate selection (once no destination remains installed for any client).
PowerShell:
```powershell
.\scripts\uninstall.ps1
```
Bash:
```bash
./scripts/uninstall.sh
```
Omitting `-Client`/`--client` opens the same selection prompt as install and update. Uninstall refuses to continue
if none of the selected destinations are installed (`Not installed, nothing to uninstall.`), unless previewing with
`-DryRun`/`--dry-run`, which always works and never changes anything.
Because this removes configuration rather than adding it, a real run asks for confirmation (type `yes`) before
touching anything, unless `-Force`/`--force` is passed; running without a terminal attached and without `-Force`/`--force`
fails instead of hanging. Removal reuses the same manifest-sync logic as install and update: every managed file is
backed up under `backups/<timestamp>/` before removal, and a file changed locally since the last install/update is
backed up but left in place with a warning instead of being deleted. Files this setup never installed are never
touched. A destination directory is only deleted once it is fully empty; backups already on disk are always kept.
Extensions and skills recorded in the ledger are removed the same way `update` removes a deselected extension:
pre-existing or manually installed ones outside the ledger are left alone, and a skill file modified since it was
installed is kept with a warning. An enabled semantic retrieval setup is disabled and its setup-owned runtime and
index removed for the selected client(s). The saved Quickupdate selection (`~/.my-ai-configuration/selection.json`)
is only removed once uninstalling leaves no client installed at all, so uninstalling just one client out of a
`Both` installation keeps the saved choices for the client that remains.
Uninstall does not remove the CLI tools themselves (`codex`, `claude`), any plugin marketplace registrations they
keep outside this setup's ledger, or files this setup never installed.
## Doctor
Check client availability, generated packages, source directories, hooks, and installed managed files:
+4
View File
@@ -11,6 +11,10 @@
"Bash(Remove-Item -Recurse*)"
]
},
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false
},
"env": {
"CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__"
},
+4
View File
@@ -2,12 +2,16 @@ approval_policy = "on-request"
approvals_reviewer = "auto_review"
sandbox_mode = "workspace-write"
[windows]
sandbox = "unelevated"
[tui]
status_line = ["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]
[agents]
enabled = true
max_concurrent_threads_per_session = 5
max_depth = 1
[features]
hooks = true
+1 -1
View File
@@ -18,7 +18,7 @@ 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_marketplace claude_plugin codex_marketplace codex_plugin; do
while IFS=$'\t' read -r name _ claude_plugin _ codex_plugin; do
[ "$name" != name ] || continue
[ -n "$name" ] || continue
plugin_count=$((plugin_count + 1))
+2
View File
@@ -24,6 +24,7 @@ if (-not $DryRun -and (Test-AnyManifestPresent (Get-InstallDestinations -HomePat
}
$options = Read-InstallOptions -HomePath $homePath -RepositoryRoot $root -Client $Client -DryRun:$DryRun
$reinstallViaNpm = Read-CliReinstallOption -Client $Client -DryRun:$DryRun
$buildScript = Join-Path $PSScriptRoot 'build.ps1'
& $buildScript -Summary:$Summary
@@ -39,6 +40,7 @@ if ($claudeConcurrency -notmatch '^[1-9][0-9]?$' -or [int]$claudeConcurrency -gt
$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 `
+2
View File
@@ -36,6 +36,7 @@ if [ "$dry_run" = false ]; then
fi
read_install_options
reinstall_via_npm=$(read_cli_reinstall_option "$client" "$dry_run")
build_args=()
[ "$summary" = false ] || build_args+=(--summary)
@@ -55,6 +56,7 @@ 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
+55
View File
@@ -46,6 +46,61 @@ function Test-GlobalNpmCodex {
}
}
function Test-GlobalNpmPackage {
param([Parameter(Mandatory=$true)][string]$Package)
$npm = Get-Command npm -ErrorAction SilentlyContinue
if (-not $npm) { return $false }
$previousPreference = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $npm list -g $Package --depth=0 --json 1>$null 2>$null
return $LASTEXITCODE -eq 0
} finally {
$ErrorActionPreference = $previousPreference
}
}
function Get-CliNpmPackage {
param([ValidateSet('codex','claude')][Parameter(Mandatory=$true)][string]$Client)
if ($Client -eq 'codex') { return '@openai/codex' }
return '@anthropic-ai/claude-code'
}
function Read-CliReinstallOption {
<#
.SYNOPSIS
Asked once per install run, never persisted; a dry run never prompts and never previews it.
#>
param([string]$Client, [switch]$DryRun)
if ($DryRun) { return $false }
return Read-InstallBoolean -Prompt "Uninstall $Client and reinstall via npm?" -Default $false
}
function Invoke-CliReinstall {
param([string]$Client, [switch]$DryRun, [switch]$Summary)
$clients = if ($Client -eq 'Both') { @('codex', 'claude') } else { @($Client.ToLowerInvariant()) }
$previousAutoUpdate = $env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE
try {
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = '1'
foreach ($selectedClient in $clients) {
if ($selectedClient -eq 'codex' -and -not $DryRun -and (Test-CodexProcessActive)) {
Write-Warning 'Codex CLI reinstall skipped because codex.exe is running. Close all Codex sessions and run install again to reinstall the CLI.'
continue
}
$package = Get-CliNpmPackage -Client $selectedClient
if (Test-GlobalNpmPackage -Package $package) {
Invoke-PluginCommand -Command 'npm' -Arguments @('uninstall', '-g', $package) -DryRun:$DryRun -Summary:$Summary
} else {
Write-Warning "$selectedClient CLI was not found as a global npm package; a non-npm installation cannot be removed automatically. Installing $package via npm alongside it."
}
Invoke-PluginCommand -Command 'npm' -Arguments @('install', '-g', "$package@latest") -DryRun:$DryRun -Summary:$Summary
if ($Summary) { Write-Output "CLI $selectedClient reinstall: $(if ($DryRun) { 'dry-run' } else { 'PASS' })" }
}
} finally {
$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE = $previousAutoUpdate
}
}
function Update-SelectedAgentClis {
param([string]$Client, [switch]$DryRun, [switch]$Summary)
$clients = if ($Client -eq 'Both') { @('codex', 'claude') } else { @($Client.ToLowerInvariant()) }
+44
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
# shellcheck disable=SC2154
read_install_boolean() {
local prompt=$1 default=$2 hint=y/N answer
@@ -36,6 +37,49 @@ read_install_options() {
semantic_retrieval=$(read_install_boolean 'Enable Qwen3 embedding and reranking semantic retrieval?' "$semantic_retrieval") || return
}
global_npm_package() {
command -v npm >/dev/null 2>&1 || return 1
npm list -g "$1" --depth=0 --json >/dev/null 2>&1
}
cli_npm_package() {
case "$1" in
codex) printf '@openai/codex\n' ;;
claude) printf '@anthropic-ai/claude-code\n' ;;
esac
}
# read_cli_reinstall_option <client> <dry_run> -- asked once per install run, never persisted;
# a dry run never prompts and never previews it.
read_cli_reinstall_option() {
local client=$1 dry_run=$2
if [ "$dry_run" = true ]; then printf 'false\n'; return; fi
read_install_boolean "Uninstall $client and reinstall via npm?" false
}
invoke_cli_reinstall() {
local client=$1 dry_run=$2 summary=${3:-false}
local selected_client clients=() package
case "$client" in codex|both) clients+=(codex) ;; esac
case "$client" in claude|both) clients+=(claude) ;; esac
for selected_client in "${clients[@]}"; do
if [ "$selected_client" = codex ] && [ "$dry_run" = false ] && codex_process_active; then
printf 'WARN Codex CLI reinstall skipped because codex.exe is running. Close all Codex sessions and run install again to reinstall the CLI.\n' >&2
continue
fi
package=$(cli_npm_package "$selected_client")
if global_npm_package "$package"; then
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 run_plugin_command "$dry_run" npm uninstall -g "$package" </dev/null || return
else
printf 'WARN %s CLI was not found as a global npm package; a non-npm installation cannot be removed automatically. Installing %s via npm alongside it.\n' "$selected_client" "$package" >&2
fi
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 run_plugin_command "$dry_run" npm install -g "${package}@latest" </dev/null || return
if [ "$summary" = true ]; then
if [ "$dry_run" = true ]; then printf 'CLI %s reinstall: dry-run\n' "$selected_client"; else printf 'CLI %s reinstall: PASS\n' "$selected_client"; fi
fi
done
}
is_windows_host() {
case "${OSTYPE:-}" in msys*|cygwin*|win32*) return 0 ;; *) return 1 ;; esac
}
+1 -1
View File
@@ -27,7 +27,7 @@ read_install_client() {
clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H' >&2; }
read_single_selection() {
local title=$1; shift; local labels=() values=() label value index=0 key
local title=$1; shift; local labels=() values=() value index=0 key
while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi
local selected=0
+2 -1
View File
@@ -32,6 +32,7 @@ read_managed_manifest() {
fi
[ -n "$rel" ] || continue
hash=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]') # tolerate uppercase hashes from older manifests
# shellcheck disable=SC2034
out_ref[$rel]=$hash
done < "$path"
}
@@ -96,7 +97,7 @@ sync_managed_destination() {
while IFS= read -r setting; do
setting_name=${setting%%=*}
setting_name=${setting_name//[[:space:]]/}
content=$(printf '%s\n' "$content" | sed "/^[[:space:]]*$setting_name[[:space:]]*=/d")
content=$(printf '%s\n' "$content" | sed "/^[[:space:]]*${setting_name}[[:space:]]*=/d")
done <<< "$local_settings"
if printf '%s\n' "$content" | grep -qE '^[[:blank:]]*\['; then
content=$(printf '%s\n' "$content" | awk -v settings="$local_settings" '
+3 -1
View File
@@ -43,7 +43,9 @@ get_plugin_names() {
# plugin_field <root> <name> <field> -- field is one of the plugins.tsv column names
plugin_field() {
local root=$1 target=$2 field=$3
# shellcheck disable=SC2034
local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill
# shellcheck disable=SC2034
while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do
[ "$name" = "$target" ] || continue
printf '%s\n' "${!field}"
@@ -72,7 +74,7 @@ read_plugin_toggle_selection() {
printf ' %d) [%s] %s\n' "$((i + 1))" "$mark" "${names_ref[$i]}"
done
read -r -p 'Toggle number or "done" ' answer || { printf 'Plugin selection cancelled: input closed.\n' >&2; return 1; }
{ [ -z "$answer" ] || [ "$answer" = done ]; } && return 0
{ [ -z "$answer" ] || [ "$answer" = "done" ]; } && return 0
if [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "${#names_ref[@]}" ]; then
i=$((10#$answer - 1))
if [ "${checked_ref[$i]}" = true ]; then checked_ref[$i]=false; else checked_ref[$i]=true; fi
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
# shellcheck disable=SC2154
read_update_selection() {
local saved key value
+2
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env bash
sync_semantic_retrieval() {
local root=$1 client=$2 home_path=$3 enabled=$4 dry_run=$5 update=$6 summary=${7:-false}
[ "$enabled" = true ] || [ -f "$home_path/.my-ai-configuration/semantic-retrieval.json" ] || return 0
+1 -1
View File
@@ -184,7 +184,7 @@ class InstallOptionsTests(unittest.TestCase):
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
environment = dict(os.environ, HOME=home.as_posix(), TEST_INSTALL_HOME=str(home))
for name, quick, answers in [('install', False, 'y\nn\nn\n'), ('update', True, '')]:
for name, quick, answers in [('install', False, 'y\nn\nn\nn\n'), ('update', True, '')]:
entry = str(scripts / (name + '.' + suffix))
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
+2
View File
@@ -96,7 +96,9 @@ cp "$package/skills/example/SKILL.md" "$legacy/skills/example/SKILL.md"
printf 'customized' > "$legacy/skills/example/custom.md"
printf 'foreign' > "$legacy/skills/example/foreign.md"
declare -A legacy_manifest
# shellcheck disable=SC2034
legacy_manifest[skills/example/SKILL.md]=$(sha256_of_file "$legacy/skills/example/SKILL.md")
# shellcheck disable=SC2034
legacy_manifest[skills/example/custom.md]=$(printf '0%.0s' {1..64})
write_managed_manifest "$(manifest_path "$legacy")" legacy_manifest
mapfile -t targets < <(get_install_targets "$generated" "$test_home" bash codex)
+2
View File
@@ -11,6 +11,7 @@ done
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
. "$root/scripts/lib/plugins.sh"
# shellcheck disable=SC2034
names=(A B C)
checked=(true false true)
toggle_log=$(mktemp)
@@ -35,6 +36,7 @@ if diagnostics=$(summary=true run_plugin_command false test_plugin_output fail 2
exit 1
fi
[[ "$diagnostics" == *'download progress'* ]] && [[ "$diagnostics" == *'WARN test warning'* ]]
# shellcheck disable=SC2034
selected=()
plugin_installed() { printf 'Empty selection must not query installed plugins.\n' >&2; exit 1; }
run_plugin_command() { printf 'Empty selection must not run commands.\n' >&2; exit 1; }
+3
View File
@@ -41,11 +41,14 @@ foreach ($shell in @('powershell','bash')) {
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
if ($content -notmatch '(?m)^max_depth\s*=\s*1\s*$') { throw "Codex agent depth limit is missing in $package" }
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
throw "Claude finish hook is incorrect in $package"
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
throw "Claude auto permission mode is missing in $package"
} elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false) {
throw "Claude strict sandbox is missing in $package"
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") {
throw "Claude status line is incorrect in $package"
}
+4 -2
View File
@@ -32,7 +32,7 @@ for shell in powershell bash; do
[ "$client" != claude ] || file=settings.json
test -f "$package/$file"
if [ "$client" = codex ]; then
while IFS=$'\t' read -r rule_file skill_name trigger; do
while IFS=$'\t' read -r rule_file _ trigger; do
[ "$rule_file" = rule_file ] && continue
[ -n "$rule_file" ] || continue
rule_path=$rule_file
@@ -56,13 +56,15 @@ for shell in powershell bash; do
! grep -q 'flashbang-if-input' "$package/$file"
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
grep -Eq '^max_depth[[:space:]]*=[[:space:]]*1[[:space:]]*$' "$package/$file"
grep -Fq 'status_line = ["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
else
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert set(settings["hooks"]) == {"PreCompact", "SessionStart", "Stop"}' "$package/$file"
! grep -q 'flashbang-if-input' "$package/$file"
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
statusline_extension=sh
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False}' "$package/$file"
statusline_extension='sh'
[ "$shell" != powershell ] || statusline_extension=ps1
test -f "$package/statusline/statusline.$statusline_extension"
grep -q "statusline.$statusline_extension" "$package/$file"
+74
View File
@@ -0,0 +1,74 @@
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('Codex','Claude','Both')][string]$Client,
[switch]$Force,
[switch]$Summary
)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
. (Join-Path $PSScriptRoot 'lib/plugins.ps1')
. (Join-Path $PSScriptRoot 'lib/manifest.ps1')
. (Join-Path $PSScriptRoot 'lib/install-targets.ps1')
. (Join-Path $PSScriptRoot 'lib/semantic-retrieval.ps1')
$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 = [Console]::ReadLine()
if ($null -eq $confirm -or $confirm.Trim().ToLowerInvariant() -ne 'yes') {
Write-Output "Type 'yes' to continue: 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
+93
View File
@@ -0,0 +1,93 @@
#!/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 "Type 'yes' to continue: " confirm || confirm=""
if [ "$confirm" != yes ]; then
printf 'Cancelled, nothing was removed.\n'
exit 1
fi
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
+5 -1
View File
@@ -9,8 +9,9 @@ import tomllib
from pathlib import Path
CLAUDE_TOP_LEVEL = {"permissions", "env", "statusLine", "hooks"}
CLAUDE_TOP_LEVEL = {"permissions", "sandbox", "env", "statusLine", "hooks"}
CLAUDE_PERMISSION_KEYS = {"defaultMode", "allow", "deny"}
CLAUDE_SANDBOX_KEYS = {"enabled", "allowUnsandboxedCommands"}
CLAUDE_STATUS_LINE_KEYS = {"type", "command"}
CLAUDE_HOOK_KEYS = {"type", "command", "timeout"}
CLAUDE_PERMISSION_MODES = {"acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"}
@@ -37,6 +38,9 @@ def validate_claude(path: Path) -> None:
for key in ("allow", "deny"):
if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]):
fail(f"permissions.{key} must be an array of strings")
sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox")
if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False:
fail("sandbox must be enabled without unsandboxed command retries")
env = settings.get("env")
if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
fail("env must be an object of string values")
@@ -2,7 +2,6 @@
set -euo pipefail
command_text=${1:?Command is required}
workspace=${2:-$PWD}
for pattern in 'git reset --hard' 'git clean -fd' 'git clean -fx' 'rm -rf' 'Remove-Item -Recurse' 'format c:' 'del /s /q'; do
[[ ${command_text,,} != *"${pattern,,}"* ]] || { printf 'Blocked potentially destructive command: %s\n' "$pattern" >&2; exit 1; }
done
+1 -1
View File
@@ -6,7 +6,7 @@ Preserve security, data, compatibility, and unrelated user work. Ask only when b
Use one applicable workflow per task. Treat skills and plugins as guidance subordinate to these rules. Continue work already authorized by the user; additional design approvals, documentation, commits, or delegation require a task-specific reason and the necessary authorization.
For conceptual repository questions, unfamiliar code, or finding behavior and implementation patterns, first run the installed semantic retrieval CLI when available: `python ~/.my-ai-configuration/semantic-retrieval/server.py search --root . --data-dir ~/.my-ai-configuration/semantic-retrieval/data --model-cache ~/.my-ai-configuration/semantic-retrieval/model-cache --query "..." --top-k 5`. Each uncached search uses Qwen3-Embedding-0.6B for candidate retrieval and Qwen3-Reranker-0.6B for final ranking; use it whenever repository discovery would help, and reuse the cache for repeated unchanged queries. Verify results against current files. Use direct reads and `rg` for known paths, symbols, and exact strings. If Qwen is unavailable, fails, or returns no useful matches, continue with direct search and briefly report the limitation. Skip retrieval for tasks that do not need repository discovery.
Start repository discovery with direct reads and `rg` for known paths, symbols, exact strings, localized changes, configuration edits, and routine checks. Use the installed semantic retrieval CLI only when terminology is unknown, the question spans multiple components, or direct search did not locate the relevant flow: `python ~/.my-ai-configuration/semantic-retrieval/server.py search --root . --data-dir ~/.my-ai-configuration/semantic-retrieval/data --model-cache ~/.my-ai-configuration/semantic-retrieval/model-cache --query "..." --top-k 5`. Each uncached search uses Qwen3-Embedding-0.6B for candidate retrieval and Qwen3-Reranker-0.6B for final ranking; reuse the cache for repeated unchanged queries and verify results against current files. If Qwen is unavailable, fails, or returns no useful matches, continue with direct search and briefly report the limitation.
Verify proportionally: run the cheapest check that detects likely failure; broaden for security, data loss, integration, or cross-component risk. Never claim unrun checks passed. Report failures and material gaps.
+9 -9
View File
@@ -47,23 +47,23 @@ used_tokens_display=$(format_token_count "$used_tokens")
context_window_display=$(format_token_count "$context_window")
esc=$'\033'
reset="$esc[0m"
dim="$esc[90m"
cyan="$esc[96m"
magenta="$esc[95m"
blue="$esc[94m"
green="$esc[92m"
reset="${esc}[0m"
dim="${esc}[90m"
cyan="${esc}[96m"
magenta="${esc}[95m"
blue="${esc}[94m"
green="${esc}[92m"
if [ "$used_percentage" = - ]; then
context_color=$dim
elif [ "$used_percentage" -ge 85 ]; then
context_color="$esc[31m"
context_color="${esc}[31m"
elif [ "$used_percentage" -ge 60 ]; then
context_color="$esc[33m"
context_color="${esc}[33m"
else
context_color=$green
fi
if [ -n "${NO_COLOR:-}" ]; then
reset= dim= cyan= magenta= blue= green= context_color=
reset='' dim='' cyan='' magenta='' blue='' green='' context_color=''
fi
sep="${dim}·${reset}"