Files
pi-kit/install.sh
T
Aleksey Shakhmatov 3c44853cde fix(install): gate confluence for every profile and dedup PROFILE selection
- Add 'confluence' (always-enabled, per README) to COMMON_BUNDLED so a
  default install (no PI_KIT_PROFILE / no tty) no longer silently strips
  it from settings.json. The allowlist now covers every skill under skills/.
- Deduplicate PROFILES in profiles_from_text: skip a canonical name already
  present, so 'frontend,1' or 'backend backend' no longer causes a redundant
  'npx skills add' call or a misleading 'Профили: frontend frontend'.
- Ship the multi-profile refactor it builds on: merged profile sets,
  profile_skills()/add_word() helpers, README updates, and the pm-task-spec
  bundled skill.

Validated: bash -n; functional checks for dedup and allowlist coverage.
2026-08-10 12:11:53 +03:00

454 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# pi-kit bootstrap — one-shot setup for new engineers. Idempotent: safe to re-run.
#
# Usage:
# curl -fsSL https://git.codelab.vc/ai/pi-kit/raw/branch/stable/install.sh | bash
# # or, from a checkout:
# ./install.sh
#
# Profile selection (multiple allowed, merged):
# Non-interactive: set PI_KIT_PROFILE=frontend or PI_KIT_PROFILE=frontend,qa before running.
#
set -euo pipefail
# ---------------------------------------------------------------------------
# The ONLY place the repository address appears. Employees install a CHANNEL
# (a moving git ref): `stable` (default, vetted releases) or `beta`. Maintainers
# fast-forward the channel branch to a tagged release. `pi update --extensions`
# then follows that channel. Override with PI_KIT_CHANNEL=beta (or a tag like v1.2.3).
# ---------------------------------------------------------------------------
PI_KIT_HOST="git.codelab.vc/ai/pi-kit"
PI_KIT_CHANNEL="${PI_KIT_CHANNEL:-stable}"
PI_KIT_REPO="git:${PI_KIT_HOST}@${PI_KIT_CHANNEL}"
MIN_NODE_MAJOR=20
SETTINGS="$HOME/.pi/agent/settings.json"
# Public skills installed for EVERYONE (repo:skill), via `npx skills add`.
COMMON_PUBLIC_SKILLS=(
"mattpocock/skills:grill-me"
"mattpocock/skills:grill-with-docs"
"mattpocock/skills:code-review"
"mattpocock/skills:diagnosing-bugs" # диагностика багов (в mattpocock/skills скилл называется так)
"brianlovin/claude-config:simplify" # упрощение кода (skills.sh: /brianlovin/claude-config/simplify)
)
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; }
# --- 1. Node.js -------------------------------------------------------------
if ! command -v node >/dev/null 2>&1; then
err "Node.js не найден. Установи Node.js >= ${MIN_NODE_MAJOR} (https://nodejs.org) и запусти скрипт снова."
exit 1
fi
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$NODE_MAJOR" -lt "$MIN_NODE_MAJOR" ]; then
err "Нужен Node.js >= ${MIN_NODE_MAJOR}, а установлен $(node -v). Обнови Node.js и запусти скрипт снова."
exit 1
fi
info "Node.js $(node -v) — ок."
# --- 1b. git (required by `pi install git:...`) ------------------------------
if ! command -v git >/dev/null 2>&1; then
err "git не найден. Установи git (macOS: xcode-select --install; Ubuntu: apt install git) и запусти скрипт снова."
exit 1
fi
info "git $(git --version | awk '{print $3}') — ок."
# --- 2. Pi ------------------------------------------------------------------
if command -v pi >/dev/null 2>&1; then
info "Pi уже установлен ($(pi --version 2>/dev/null || echo 'версия неизвестна'))."
else
info "Ставлю Pi..."
curl -fsSL https://pi.dev/install.sh | sh
# Make pi available in the current shell if the installer added a new PATH dir.
if ! command -v pi >/dev/null 2>&1; then
export PATH="$HOME/.local/bin:$PATH"
fi
if ! command -v pi >/dev/null 2>&1; then
err "Pi установлен, но не найден в PATH. Открой новый терминал (или поправь PATH) и запусти скрипт снова."
exit 1
fi
fi
# --- 3. Install the corporate package --------------------------------------
info "Устанавливаю корпоративный пакет: ${PI_KIT_REPO}"
pi install "$PI_KIT_REPO"
info "Пакет установлен. Обновляться потом: pi update --extensions"
# --- 4. Resolve profiles --------------------------------------------------
# Several profiles can be selected at once; their skill sets are merged.
# Precedence: PI_KIT_PROFILE env > interactive prompt.
# Env is comma- or space-separated: PI_KIT_PROFILE="backend,qa" (a single
# value like `backend` still works). Empty env + no tty => all profiles.
profile_name() { # <token> -> canonical profile name, or empty if unknown
case "$1" in
1 | frontend) echo frontend ;;
2 | backend) echo backend ;;
3 | qa) echo qa ;;
4 | mobile) echo mobile ;;
5 | pm) echo pm ;;
esac
}
is_profile() { # <token> — 0 if it's a known profile name/number
[ -n "$(profile_name "$1")" ]
}
# Parse a comma/space separated selection into the PROFILES array (deduplicated).
# Unknown tokens are dropped with a warning.
profiles_from_text() { # <text>
local raw="$1" arr p name
PROFILES=()
IFS=', ' read -r -a arr <<<"$raw"
for p in "${arr[@]}"; do
[ -z "$p" ] && continue
if is_profile "$p"; then
name="$(profile_name "$p")"
case " ${PROFILES[*]} " in
*" $name "*) ;; # duplicate token/alias — already present
*) PROFILES+=("$name") ;;
esac
else
warn "Неизвестный профиль/номер '${p}' — пропущен."
fi
done
}
all_profiles() { PROFILES=(frontend backend qa mobile pm); }
PROFILES=()
if [ -n "${PI_KIT_PROFILE:-}" ]; then
profiles_from_text "$PI_KIT_PROFILE"
if [ "${#PROFILES[@]}" -eq 0 ]; then
warn "В PI_KIT_PROFILE нет распознанных профилей — ставлю все."
all_profiles
fi
elif [ -r /dev/tty ]; then
# ask if we have a terminal, even under `curl | bash` (read from /dev/tty)
{
printf 'Выбери один или несколько профилей (через пробел или запятую):\n'
printf ' frontend — TS/UI\n'
printf ' backend — Go/Rust/Python\n'
printf ' qa — тестирование\n'
printf ' mobile — Android/iOS\n'
printf ' pm — постановка задач продакт-менеджера\n'
printf 'Пример: backend qa | frontend,pm: '
} >/dev/tty
read -r ans </dev/tty || ans=""
if [ -z "$ans" ]; then
all_profiles
else
profiles_from_text "$ans"
if [ "${#PROFILES[@]}" -eq 0 ]; then
warn "Не распознаны профили — ставлю все."
all_profiles
fi
fi
else
all_profiles
fi
# Bundled language/extra skills and public skills per profile. Common skills
# (confluence/jira-workflow/repo-map/docs-map) are always enabled; prompts and
# guardrail extensions are never gated. For several selected profiles their sets are merged.
COMMON_BUNDLED="confluence jira-workflow repo-map docs-map"
LANG_BUNDLED=""
EXTRA_BUNDLED=""
PROFILE_PUBLIC=()
# Add word(s) to a space-separated variable, skipping duplicates.
# Accepts one space-separated word list ("a b c") and/or separate args.
add_word() { # <var-name> <words...>
local var="$1" w cur all new
cur="${!var}"
shift
all="$*"
new=""
for w in $all; do
case " $cur " in
*" $w "*) ;; # already present
*) new="$new $w" ;;
esac
done
[ -n "$new" ] && printf -v "$var" '%s%s' "$cur" "$new"
}
# Accumulate the skill sets of one selected profile.
profile_skills() { # <profile>
case "$1" in
frontend)
add_word LANG_BUNDLED "typescript-standards"
PROFILE_PUBLIC+=("anthropics/skills:frontend-design")
;;
backend)
add_word LANG_BUNDLED "go-standards rust-standards python-standards"
;;
qa) # TODO: уточнить стек тестировщиков; по умолчанию — Python и TS (автотесты)
add_word LANG_BUNDLED "python-standards typescript-standards"
;;
mobile) # Android (Kotlin) + iOS (Swift)
add_word LANG_BUNDLED "kotlin-standards swift-standards"
;;
pm) # Продакт-менеджер: постановка задач, без языковых стандартов кода
add_word EXTRA_BUNDLED "pm-task-spec"
;;
esac
}
for p in "${PROFILES[@]}"; do
profile_skills "$p"
done
LANG_BUNDLED="$(printf '%s' "$LANG_BUNDLED" | sed 's/^ *//;s/ *$//')"
info "Профили: ${PROFILES[*]}"
# --- 5. Gate bundled skills via settings object-form filter -----------------
# Rewrites the pi-kit package entry in settings.json to {source, skills:[...]}
# so only the profile-relevant skills load. Extensions/prompts stay untouched
# (their keys are omitted => all load).
gate_bundled_skills() {
local allowed="$1"
if [ ! -f "$SETTINGS" ]; then
warn "settings.json не найден ($SETTINGS) — пропускаю гейтинг скилов."
return 0
fi
SETTINGS_FILE="$SETTINGS" ALLOWED="$allowed" SRC_MATCH="pi-kit" node <<'NODE'
const fs = require("fs");
const file = process.env.SETTINGS_FILE;
const allowed = process.env.ALLOWED.trim().split(/\s+/).filter(Boolean).map((s) => "skills/" + s);
const s = JSON.parse(fs.readFileSync(file, "utf8"));
if (!Array.isArray(s.packages)) process.exit(0);
let patched = false;
s.packages = s.packages.map((p) => {
const src = typeof p === "string" ? p : (p && p.source);
if (typeof src === "string" && src.includes(process.env.SRC_MATCH)) {
patched = true;
const base = (p && typeof p === "object") ? p : {};
return Object.assign({}, base, { source: src, skills: allowed });
}
return p;
});
fs.writeFileSync(file, JSON.stringify(s, null, 2) + "\n");
console.log(patched ? "gated bundled skills -> " + allowed.join(", ") : "pi-kit entry not found; skipped gating");
NODE
}
info "Гейчу скилы пакета под профили '${PROFILES[*]}'..."
gate_bundled_skills "${COMMON_BUNDLED} ${LANG_BUNDLED} ${EXTRA_BUNDLED}" || warn "не удалось отфильтровать скилы (см. выше)."
# --- 6. Install public skills ----------------------------------------------
# Uses the skills.sh CLI (`npx skills add`). Failures are non-fatal.
add_public_skill() { # <owner/repo> <skill-name>
local repo="$1" name="$2"
info "Публичный скилл: ${name} (${repo})"
# --global installs into ~/.agents/skills (the universal dir Pi reads),
# not the current project. --yes skips the interactive scope prompt.
if ! npx --yes skills add "https://github.com/${repo}" --skill "${name}" --global --yes >/dev/null 2>&1; then
warn "не удалось поставить '${name}'. Вручную: npx skills add https://github.com/${repo} --skill ${name} -g -y"
fi
}
info "Ставлю общие публичные скилы..."
for entry in "${COMMON_PUBLIC_SKILLS[@]}"; do
add_public_skill "${entry%%:*}" "${entry##*:}"
done
if [ "${#PROFILE_PUBLIC[@]}" -gt 0 ]; then
info "Ставлю публичные скилы профилей '${PROFILES[*]}'..."
for entry in "${PROFILE_PUBLIC[@]}"; do
add_public_skill "${entry%%:*}" "${entry##*:}"
done
fi
# --- 7. Environment (URLs + secrets) ---------------------------------------
# Interactive setup: prompts (via /dev/tty so it works under curl | bash),
# stores chosen values in ~/.config/pi-kit/env.sh (chmod 600) and sources it
# from the shell rc. Secrets are read hidden and never echoed/logged.
# Default URLs are taken from the installed package's config/company.json
# (single source of truth) and offered for confirmation — the user can keep or
# override them. Skip with PI_KIT_SETUP_ENV=0.
CONFIG_DIR="$HOME/.config/pi-kit"
ENV_FILE="$CONFIG_DIR/env.sh"
RC_MARK=">>> pi-kit >>>"
# Locate the installed package's config/company.json (where `pi install` cloned it).
# Tries the git-based package dir used by pi, falling back to a local checkout.
PACKAGE_JSON=""
for cand in "$HOME/.pi/agent/git/${PI_KIT_HOST}/config/company.json" "$HOME/.config/pi-kit/company.json" "./config/company.json"; do
if [ -f "$cand" ]; then PACKAGE_JSON="$cand"; break; fi
done
# read_pkg_url <field> — get a default URL from config/company.json (or "").
read_pkg_url() {
[ -z "$PACKAGE_JSON" ] && { echo ""; return; }
node -e 'const f=process.argv[1];try{const c=require(f);const v=c[process.argv[2]]||"";process.stdout.write(typeof v==="string"&&v&&v!=="TODO"?v:"")}catch(e){process.stdout.write("")}' "$PACKAGE_JSON" "$1" 2>/dev/null
}
# Which provider key (if any) is already present in the environment.
PROVIDER_KEYS=(ANTHROPIC_API_KEY OPENAI_API_KEY OPENROUTER_API_KEY GEMINI_API_KEY GOOGLE_API_KEY GROQ_API_KEY)
found_key=""
for k in "${PROVIDER_KEYS[@]}"; do
if [ -n "${!k:-}" ]; then
found_key="$k"
break
fi
done
detect_rc() {
case "$(basename "${SHELL:-}")" in
zsh) echo "$HOME/.zshrc" ;;
bash) echo "$HOME/.bashrc" ;;
*) echo "$HOME/.profile" ;;
esac
}
# Escape a value for safe embedding inside single quotes.
esc_sq() { printf "%s" "$1" | sed "s/'/'\\\\''/g"; }
env_setup_interactive() {
info "Настройка окружения — значения сохранятся в ${ENV_FILE} (chmod 600)."
# Load previously stored values so re-runs keep them as defaults.
# shellcheck disable=SC1090
[ -f "$ENV_FILE" ] && . "$ENV_FILE" || true
# Package defaults from config/company.json (offer, user may override).
local pkg_tracker pkg_docs pkg_host
pkg_tracker="$(read_pkg_url trackerUrl)"
pkg_docs="$(read_pkg_url docsUrl)"
pkg_host="$(read_pkg_url gitHost)"
local ans=""
# Seed res from current env (keep on re-run) else package default.
local tracker="${TRACKER_URL:-${pkg_tracker}}"
local confluence="${CONFLUENCE_URL:-${pkg_docs}}"
local jira="${JIRA_TOKEN:-}"
local conf="${CONFLUENCE_TOKEN:-}"
local gitlab="${GITLAB_TOKEN:-}"
# ---- URLs (visible prompts; current/default value shown) ----
printf '\nURL-адреса корпоративных сервисов (Enter — оставить дефолт):\n'
printf ' TRACKER_URL (Jira)%s: ' "${tracker:+ [${tracker}]}" >/dev/tty
read -r ans </dev/tty || ans=""
[ -n "$ans" ] && tracker="$ans"
printf ' CONFLUENCE_URL (Confluence/wiki)%s: ' "${confluence:+ [${confluence}]}" >/dev/tty
read -r ans </dev/tty || ans=""
[ -n "$ans" ] && confluence="$ans"
# ---- Tokens (secret, hidden input; empty keeps current) ----
printf '\nТокены сервисов (скрытый ввод, Enter — оставить как есть / пропустить):\n'
printf 'JIRA_TOKEN: где брать — Jira: Профиль (аватар) → Безопасность → Personal access tokens → создать.\n'
printf 'JIRA_TOKEN (скрытый ввод): ' >/dev/tty
read -rs ans </dev/tty || ans=""
printf '\n' >/dev/tty
[ -n "$ans" ] && jira="$ans"
printf 'CONFLUENCE_TOKEN: где брать — Confluence: Профиль → Настройки → Personal access tokens (старые: API tokens).\n'
printf 'CONFLUENCE_TOKEN (скрытый ввод): ' >/dev/tty
read -rs ans </dev/tty || ans=""
printf '\n' >/dev/tty
[ -n "$ans" ] && conf="$ans"
printf 'GITLAB_TOKEN: где брать — GitLab: Настройки → Access Tokens → Personal; scope: api (или read_api).\n'
printf 'GITLAB_TOKEN (скрытый ввод): ' >/dev/tty
read -rs ans </dev/tty || ans=""
printf '\n' >/dev/tty
[ -n "$ans" ] && gitlab="$ans"
# LLM provider key — only ask if none is already set.
local prov_name="" prov_val=""
if [ -z "$found_key" ]; then
printf 'LLM-провайдер: 1) anthropic 2) openai 3) openrouter 4) пропустить [1-4]: ' >/dev/tty
read -r ans </dev/tty || ans=""
case "$ans" in
1 | anthropic) prov_name=ANTHROPIC_API_KEY ;;
2 | openai) prov_name=OPENAI_API_KEY ;;
3 | openrouter) prov_name=OPENROUTER_API_KEY ;;
*) prov_name="" ;;
esac
if [ -n "$prov_name" ]; then
printf '%s (скрытый ввод): ' "$prov_name" >/dev/tty
read -rs prov_val </dev/tty || prov_val=""
printf '\n' >/dev/tty
fi
fi
# Write env file with restrictive permissions (umask 077 => 600).
mkdir -p "$CONFIG_DIR"
(
umask 077
{
echo "# Managed by pi-kit install.sh — не коммить. chmod 600, plaintext."
[ -n "$tracker" ] && echo "export TRACKER_URL='$(esc_sq "$tracker")'"
[ -n "$confluence" ] && echo "export CONFLUENCE_URL='$(esc_sq "$confluence")'"
[ -n "$jira" ] && echo "export JIRA_TOKEN='$(esc_sq "$jira")'"
[ -n "$conf" ] && echo "export CONFLUENCE_TOKEN='$(esc_sq "$conf")'"
[ -n "$gitlab" ] && echo "export GITLAB_TOKEN='$(esc_sq "$gitlab")'"
[ -n "$prov_name" ] && [ -n "$prov_val" ] && echo "export ${prov_name}='$(esc_sq "$prov_val")'"
} >"$ENV_FILE"
)
chmod 600 "$ENV_FILE"
info "Сохранено: ${ENV_FILE}"
# Ensure the shell rc sources the env file (managed block, idempotent).
local rc
rc="$(detect_rc)"
if [ -f "$rc" ] && grep -qF "$RC_MARK" "$rc" 2>/dev/null; then
info "Подключение уже есть в ${rc}."
else
{
echo ""
echo "# ${RC_MARK}"
echo '[ -f "$HOME/.config/pi-kit/env.sh" ] && . "$HOME/.config/pi-kit/env.sh"'
echo "# <<< pi-kit <<<"
} >>"$rc"
info "Добавил подключение в ${rc}. Применить сейчас: source ${rc}"
fi
warn "Токены лежат в ${ENV_FILE} (chmod 600, plaintext). Для строгих требований — секрет-менеджер."
}
if [ "${PI_KIT_SETUP_ENV:-1}" != 0 ] && [ -r /dev/tty ]; then
env_setup_interactive
else
# Non-interactive fallback: just check for a provider key and print guidance.
if [ -n "$found_key" ]; then
info "Найден ключ LLM-провайдера: ${found_key} — ок."
else
warn "Не найден ключ LLM-провайдера (установка неинтерактивная — пропускаю настройку)."
fi
cat <<'EOF'
Задай нужные переменные вручную (например, в ~/.config/pi-kit/env.sh):
export ANTHROPIC_API_KEY="..." # или OPENAI_API_KEY / OPENROUTER_API_KEY
export TRACKER_URL="..." # адрес Jira (сейчас подхватится трекер из config/company.json)
export CONFLUENCE_URL="..." # адрес Confluence (подхватится из config/company.json)
export JIRA_TOKEN="..." # Jira personal access token
export CONFLUENCE_TOKEN="..." # Confluence personal access token
export GITLAB_TOKEN="..." # GitLab token (scope api) для MR/пайплайнов/поиска
Провайдер/модель — флагами --provider/--model или в настройках Pi (пакет не навязывает).
URL можно не задавать: инструменты берут дефолты из config/company.json.
EOF
fi
# --- 8. Cheat sheet ---------------------------------------------------------
cat <<EOF
────────────────────────────────────────────────────────
Готово! Профили: ${PROFILES[*]} Канал: ${PI_KIT_CHANNEL}
Запуск: pi
Обновление: pi update --extensions (следует за каналом ${PI_KIT_CHANNEL})
Окружение: ${ENV_FILE} (JIRA_TOKEN/CONFLUENCE_TOKEN/GITLAB_TOKEN/ключ провайдера)
URL-адреса — дефолты из config/company.json (можно переопределить в env.sh)
Сменить профиль: PI_KIT_PROFILE=frontend,qa ./install.sh (или перезапусти и выбери несколько)
Сменить канал: PI_KIT_CHANNEL=beta ./install.sh
Команды: /bugfix <тикет> /feature <тикет> /review [фокус] /rfc <тема>
/kit-config /kit-doctor /kit-help
Инструменты: jira_issue_* | gitlab_mr_create/gitlab_pipeline_status/gitlab_code_search
| confluence_search/confluence_page_* (нативные, для агента)
Проверка: /kit-doctor — health-check окружения
────────────────────────────────────────────────────────
EOF