#!/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() { # -> 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() { # — 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() { # 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 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() { # 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() { # 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 — 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 read -r ans /dev/tty read -rs ans /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 [ -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 [ -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 read -rs prov_val /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 < /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