From 035bededb0c22519a1bff0c147fe427dc4bb64f9 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Thu, 6 Aug 2026 11:45:50 +0300 Subject: [PATCH] feat(uninstall): one-command full uninstall with confirmation - uninstall.sh: removes pi-kit from settings.json (string + gated object entries), the package clone, kit-installed public skills, ~/.config/pi-kit (env.sh with tokens, audit.jsonl) and the shell-rc sourcing block - confirmation prompt by default; PI_KIT_UNINSTALL=1 for non-interactive, PI_KIT_KEEP_CONFIG=1 / PI_KIT_KEEP_SKILLS=1 to retain parts - works without the pi binary (settings edited directly); notes how to remove pi itself - tests: test/uninstall.test.sh against a throwaway $HOME (14 checks: full/partial removal, refusal without flag, idempotent re-run), wired into npm test --- CHANGELOG.md | 4 + README.md | 5 +- package.json | 2 +- test/uninstall.test.sh | 99 +++++++++++++++++++++ uninstall.sh | 196 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 test/uninstall.test.sh create mode 100644 uninstall.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f207b..e53af0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ - Пример конфига в README приведён в соответствие с реальным `config/company.json`. ### Added +- `uninstall.sh` — полное удаление кита одной командой с подтверждением + (пакет+клон, публичные скилы, `~/.config/pi-kit` с токенами, блок в shell-rc; + флаги `PI_KIT_UNINSTALL`/`PI_KIT_KEEP_CONFIG`/`PI_KIT_KEEP_SKILLS`); покрыт тестами + (`test/uninstall.test.sh`, изолированный `$HOME`). - Тесты: company-context (normalize/fetch/truncation), mcp-bridge (disabled-путь), табличные кейсы permission-gate, shell-тест `create-mr.sh` (парсинг remote, dry-run). - `package-lock.json` закоммичен (воспроизводимая установка). diff --git a/README.md b/README.md index d9fe6c6..39cec02 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,10 @@ pi-kit/ │ └── lib/ # общий код (secrets, audit, company-config) ├── skills/ # SKILL.md-скиллы (jira-workflow/repo-map/docs-map + -standards) ├── prompts/ # шаблоны команд (/bugfix, /feature, /review, /rfc) -├── test/ # guardrails.test.ts (node) + shell.test.sh (create-mr.sh) +├── test/ # guardrails.test.ts (node) + shell/uninstall shell-тесты ├── docs/mcp.md # документация MCP-интеграций -└── install.sh # bootstrap для новых сотрудников +├── install.sh # bootstrap для новых сотрудников +└── uninstall.sh # полное удаление кита (с подтверждением) ``` Формат: [packages](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md), diff --git a/package.json b/package.json index 13e2d65..d8920fe 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "test": "node --experimental-strip-types test/guardrails.test.ts && bash test/shell.test.sh" + "test": "node --experimental-strip-types test/guardrails.test.ts && bash test/shell.test.sh && bash test/uninstall.test.sh" }, "peerDependencies": { "@earendil-works/pi-ai": "*", diff --git a/test/uninstall.test.sh b/test/uninstall.test.sh new file mode 100644 index 0000000..ed51094 --- /dev/null +++ b/test/uninstall.test.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Tests for uninstall.sh against a throwaway $HOME (no real machine state touched). +# Run via `npm test` or directly: bash test/uninstall.test.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +UNINSTALL="$ROOT/uninstall.sh" +FAILS=0 + +check() { # + local name="$1" + if bash -c "$2"; then + echo "PASS $name" + else + echo "FAIL $name" + FAILS=$((FAILS + 1)) + fi +} + +# Fresh fake home with everything the kit can leave behind. +setup_fake_home() { + export HOME="$(mktemp -d)" + export SHELL=/bin/bash + export PI_KIT_UNINSTALL=1 + export PI_KIT_KEEP_CONFIG=0 + export PI_KIT_KEEP_SKILLS=0 + + mkdir -p "$HOME/.pi/agent/git/git.codelab.vc/ai/pi-kit" + mkdir -p "$HOME/.agents/skills" + mkdir -p "$HOME/.config/pi-kit" + + cat >"$HOME/.pi/agent/settings.json" <<'JSON' +{ + "packages": [ + "git:github.com/someone/other", + { "source": "git:git.codelab.vc/ai/pi-kit@stable", "skills": ["skills/go-standards"] }, + "git:git.codelab.vc/ai/pi-kit@beta" + ] +} +JSON + echo "export JIRA_TOKEN='dummy'" >"$HOME/.config/pi-kit/env.sh" + cat >"$HOME/.agents/skills/grill-me" <<'EOF' +skill content +EOF + cat >"$HOME/.bashrc" <<'RC' +# user content +export FOO=1 + +# >>> pi-kit >>> +[ -f "$HOME/.config/pi-kit/env.sh" ] && . "$HOME/.config/pi-kit/env.sh" +# <<< pi-kit <<< + +# more user content +RC +} + +# --- 1. Full uninstall -------------------------------------------------------- +setup_fake_home +bash "$UNINSTALL" >/dev/null +check "uninstall: settings.json keeps other packages" "grep -q 'git:github.com/someone/other' \"$HOME/.pi/agent/settings.json\"" +check "uninstall: settings.json drops object-form pi-kit" "! grep -q 'pi-kit' \"$HOME/.pi/agent/settings.json\"" +check "uninstall: clone dir removed" "test ! -e \"$HOME/.pi/agent/git/git.codelab.vc/ai/pi-kit\"" +check "uninstall: config dir removed" "test ! -e \"$HOME/.config/pi-kit\"" +check "uninstall: rc block removed" "! grep -q 'pi-kit' \"$HOME/.bashrc\"" +check "uninstall: rc user content preserved" "grep -q 'export FOO=1' \"$HOME/.bashrc\" && grep -q 'more user content' \"$HOME/.bashrc\"" +check "uninstall: public skill removed" "test ! -e \"$HOME/.agents/skills/grill-me\"" + +# --- 2. Keep config (secrets + rc) -------------------------------------------- +setup_fake_home +PI_KIT_KEEP_CONFIG=1 bash "$UNINSTALL" >/dev/null +check "keep-config: env.sh kept" "test -e \"$HOME/.config/pi-kit/env.sh\"" +check "keep-config: rc block kept" "grep -q 'pi-kit' \"$HOME/.bashrc\"" +check "keep-config: package still removed" "! grep -q 'pi-kit' \"$HOME/.pi/agent/settings.json\"" + +# --- 3. Keep skills ------------------------------------------------------------ +setup_fake_home +PI_KIT_KEEP_SKILLS=1 bash "$UNINSTALL" >/dev/null +check "keep-skills: skill kept" "test -e \"$HOME/.agents/skills/grill-me\"" + +# --- 4. Non-interactive without flag must refuse --------------------------------- +setup_fake_home +PI_KIT_UNINSTALL=0 bash "$UNINSTALL" >/dev/null 2>&1 && RC=0 || RC=$? +check "uninstall: refuses without confirmation flag in non-interactive mode" "test \"$RC\" -ne 0" +check "uninstall: nothing removed on refusal" "grep -q 'pi-kit' \"$HOME/.pi/agent/settings.json\" && test -e \"$HOME/.config/pi-kit/env.sh\"" + +# --- 5. Idempotent re-run ------------------------------------------------------- +setup_fake_home +bash "$UNINSTALL" >/dev/null +bash "$UNINSTALL" >/dev/null 2>&1 || true +check "uninstall: re-run is harmless" "test ! -e \"$HOME/.config/pi-kit\" && ! grep -q 'pi-kit' \"$HOME/.pi/agent/settings.json\"" + +echo "" +if [ "$FAILS" -eq 0 ]; then + echo "uninstall tests: ALL PASS" + exit 0 +else + echo "uninstall tests: $FAILS FAILED" + exit 1 +fi diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..44413f6 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# +# pi-kit uninstall — removes the corporate kit from this machine completely. +# Idempotent: safe to re-run; missing pieces are skipped with a note. +# +# Usage (one command, asks for confirmation): +# curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/uninstall.sh | bash +# # or, from a checkout: +# ./uninstall.sh +# +# What it removes: +# 1. the pi-kit package entry from ~/.pi/agent/settings.json and its clone +# (~/.pi/agent/git/git.codelab.vc/ai/pi-kit); +# 2. public skills the kit installed into ~/.agents/skills/ +# (grill-me, grill-with-docs, code-review, diagnosing-bugs, frontend-design); +# 3. ~/.config/pi-kit (env.sh with tokens, audit.jsonl) and the sourcing block +# it added to your shell rc — only if you confirm (or unless kept). +# +# Non-interactive / skip confirmation: PI_KIT_UNINSTALL=1 +# Keep secrets & rc block: PI_KIT_KEEP_CONFIG=1 +# Keep public skills: PI_KIT_KEEP_SKILLS=1 +# +# Pi itself is NOT removed (it may be used outside the kit). See the final note. +# +set -euo pipefail + +# Mirrors the list in install.sh (COMMON_PUBLIC_SKILLS + PROFILE_PUBLIC). +PUBLIC_SKILLS=("grill-me" "grill-with-docs" "code-review" "diagnosing-bugs" "frontend-design") + +SETTINGS="$HOME/.pi/agent/settings.json" +CONFIG_DIR="$HOME/.config/pi-kit" +ENV_FILE="$CONFIG_DIR/env.sh" +RC_MARK=">>> pi-kit >>>" +RC_MARK_END="<<< pi-kit <<<" + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; } + +case "${1:-}" in + -h | --help) + cat <<'EOF' +Удаляет pi-kit полностью: пакет из настроек pi, его клон, публичные скилы, +~/.config/pi-kit (env.sh с токенами) и блок в shell-rc. + +Использование: curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/uninstall.sh | bash + +Флаги: + PI_KIT_UNINSTALL=1 не спрашивать подтверждение (для CI/автоматизации) + PI_KIT_KEEP_CONFIG=1 оставить ~/.config/pi-kit (токены) и блок в rc + PI_KIT_KEEP_SKILLS=1 оставить публичные скилы в ~/.agents/skills +EOF + exit 0 + ;; +esac + +detect_rc() { + case "$(basename "${SHELL:-}")" in + zsh) echo "$HOME/.zshrc" ;; + bash) echo "$HOME/.bashrc" ;; + *) echo "$HOME/.profile" ;; + esac +} + +# --- 1. Confirmation --------------------------------------------------------- +summary_parts=("пакет pi-kit из настроек pi (settings.json + клон)") +if [ "${PI_KIT_KEEP_SKILLS:-0}" != "1" ]; then + summary_parts+=("публичные скилы кита в ~/.agents/skills: ${PUBLIC_SKILLS[*]}") +fi +if [ "${PI_KIT_KEEP_CONFIG:-0}" != "1" ]; then + summary_parts+=("~/.config/pi-kit (env.sh с токенами, audit.jsonl)") + summary_parts+=("блок '${RC_MARK}' в $(detect_rc)") +fi + +if [ "${PI_KIT_UNINSTALL:-0}" != "1" ]; then + if [ ! -r /dev/tty ]; then + err "Нет терминала для подтверждения. Для неинтерактивного удаления задай PI_KIT_UNINSTALL=1." + exit 1 + fi + { + printf 'Будет удалено:\n' + for s in "${summary_parts[@]}"; do printf ' • %s\n' "$s"; done + printf '\nУдалить pi-kit полностью? [y/N] ' + } >/dev/tty + read -r ans /dev/null 2>&1; then + SETTINGS_FILE="$SETTINGS" node <<'NODE' +const fs = require("fs"); +const file = process.env.SETTINGS_FILE; +const s = JSON.parse(fs.readFileSync(file, "utf8")); +if (!Array.isArray(s.packages)) process.exit(0); +const before = s.packages.length; +s.packages = s.packages.filter((p) => { + const src = typeof p === "string" ? p : (p && p.source); + return !(typeof src === "string" && src.includes("pi-kit")); +}); +if (s.packages.length === before) process.exit(0); +fs.writeFileSync(file, JSON.stringify(s, null, 2) + "\n"); +console.log("removed pi-kit entry from " + file); +NODE + else + warn "node не найден — пропускаю правку settings.json. Удали пакет вручную:" + warn " pi remove git:git.codelab.vc/ai/pi-kit@<твой канал>" + fi +else + warn "settings.json не найден ($SETTINGS) — пропускаю." +fi + +# --- 3. Remove the pi-kit clone ------------------------------------------------- +# install.sh installs `git:git.codelab.vc/ai/pi-kit@` → clone at +# ~/.pi/agent/git/git.codelab.vc/ai/pi-kit. Also catch any other pi-kit clone. +CLONE_DIR="$HOME/.pi/agent/git/git.codelab.vc/ai/pi-kit" +if [ -d "$CLONE_DIR" ]; then + rm -rf "$CLONE_DIR" + info "Удалён клон пакета: $CLONE_DIR" +else + warn "Клон пакета не найден ($CLONE_DIR) — пропускаю." +fi +if [ -d "$HOME/.pi/agent/git" ]; then + while IFS= read -r d; do + if [ "$d" != "$CLONE_DIR" ] && [ -d "$d" ]; then + rm -rf "$d" + info "Удалён дополнительный клон: $d" + fi + done < <(find "$HOME/.pi/agent/git" -maxdepth 4 -type d -name pi-kit 2>/dev/null || true) +fi + +# --- 4. Public skills ---------------------------------------------------------- +if [ "${PI_KIT_KEEP_SKILLS:-0}" != "1" ]; then + for skill in "${PUBLIC_SKILLS[@]}"; do + dir="$HOME/.agents/skills/$skill" + if [ -e "$dir" ]; then + rm -rf "$dir" + info "Удалён публичный скилл: $dir" + else + info "Скилл не был установлен (пропускаю): $skill" + fi + done +else + info "PI_KIT_KEEP_SKILLS=1 — публичные скилы оставлены." +fi + +# --- 5. Config dir (secrets) + shell rc block --------------------------------- +if [ "${PI_KIT_KEEP_CONFIG:-0}" != "1" ]; then + if [ -d "$CONFIG_DIR" ]; then + rm -rf "$CONFIG_DIR" + info "Удалён ~/.config/pi-kit (env.sh с токенами, audit.jsonl)." + else + warn "~/.config/pi-kit не найден — пропускаю." + fi + + rc="$(detect_rc)" + if [ -f "$rc" ] && grep -qF "$RC_MARK" "$rc" 2>/dev/null; then + awk -v start="$RC_MARK" -v end="$RC_MARK_END" ' + $0 ~ start { skip = 1 } + !skip { print } + $0 ~ end { skip = 0 } + ' "$rc" >"$rc.tmp" && mv "$rc.tmp" "$rc" + info "Убран блок pi-kit из $rc." + else + warn "Блок pi-kit в $(detect_rc) не найден — пропускаю." + fi +else + info "PI_KIT_KEEP_CONFIG=1 — ~/.config/pi-kit и rc-блок оставлены." +fi + +# --- 6. Final note -------------------------------------------------------------- +cat <