Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4408f23c68 | ||
|
|
c4676e9a2f | ||
|
|
110842b9a4 | ||
|
|
fd707539d5 | ||
|
|
953eb34e21 | ||
|
|
670c907375 | ||
|
|
60205169f4 | ||
|
|
c2a79cf415 | ||
|
|
f7230dd84f | ||
|
|
1bc59ca881 | ||
|
|
fe9ca9552f | ||
|
|
fb926e9877 | ||
|
|
b7bac4c9cc | ||
|
|
b77114ad90 | ||
|
|
dc71fb4cc4 | ||
|
|
5ae8e0cff5 | ||
|
|
8379e29eb6 | ||
|
|
ed8dd89ba7 | ||
|
|
6c3538f888 | ||
|
|
769af0d206 | ||
|
|
6bf3cc63d7 | ||
|
|
942288eeda |
No files matched your search
+10
-10
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" '
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2154
|
||||
|
||||
read_update_selection() {
|
||||
local saved key value
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
Reference in new issue
Block a user