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.
This commit is contained in:
Aleksey Shakhmatov
2026-08-10 12:11:53 +03:00
parent 0c6e590273
commit 3c44853cde
3 changed files with 244 additions and 80 deletions
+127 -70
View File
@@ -7,9 +7,8 @@
# # or, from a checkout:
# ./install.sh
#
# Profile selection:
# Interactive by default (prompts via /dev/tty, so it works under curl | bash).
# Non-interactive: set PI_KIT_PROFILE=frontend|backend|qa|mobile before running.
# Profile selection (multiple allowed, merged):
# Non-interactive: set PI_KIT_PROFILE=frontend or PI_KIT_PROFILE=frontend,qa before running.
#
set -euo pipefail
@@ -80,70 +79,132 @@ info "Устанавливаю корпоративный пакет: ${PI_KIT_R
pi install "$PI_KIT_REPO"
info "Пакет установлен. Обновляться потом: pi update --extensions"
# --- 4. Resolve profile -----------------------------------------------------
# Precedence: PI_KIT_PROFILE env > interactive prompt > "all" (no gating).
PROFILE="${PI_KIT_PROFILE:-}"
case "$PROFILE" in
frontend | backend | qa | mobile | pm) ;; # valid, keep
"") # ask if we have a terminal, even under `curl | bash` (read from /dev/tty)
if [ -r /dev/tty ]; then
{
printf 'Выбери профиль:\n'
printf ' 1) frontend — TS/UI\n'
printf ' 2) backend — Go/Rust/Python\n'
printf ' 3) qa — тестирование\n'
printf ' 4) mobile — Android/iOS\n'
printf ' 5) pm — постановка задач продакт-менеджера\n'
printf 'Номер или имя [1-5]: '
} >/dev/tty
read -r ans </dev/tty || ans=""
case "$ans" in
1 | frontend) PROFILE=frontend ;;
2 | backend) PROFILE=backend ;;
3 | qa) PROFILE=qa ;;
4 | mobile) PROFILE=mobile ;;
5 | pm) PROFILE=pm ;;
*) PROFILE=all ;;
# --- 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
PROFILE=all
warn "Неизвестный профиль/номер '${p}' — пропущен."
fi
;;
*)
warn "Неизвестный PI_KIT_PROFILE='${PROFILE}', использую 'all' (без гейтинга скилов)."
PROFILE=all
;;
esac
info "Профиль: ${PROFILE}"
done
}
# Bundled language skills per profile. Common skills (jira-workflow/repo-map/
# docs-map) are always enabled. Prompts and guardrail extensions are never gated.
COMMON_BUNDLED="jira-workflow repo-map docs-map"
# Extra bundled skills beyond language standards, added per profile (e.g. product/PM tooling).
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=()
case "$PROFILE" in
frontend)
LANG_BUNDLED="typescript-standards"
PROFILE_PUBLIC=("anthropics/skills:frontend-design")
;;
backend)
LANG_BUNDLED="go-standards rust-standards python-standards"
;;
qa) # TODO: уточнить стек тестировщиков; по умолчанию — Python и TS (автотесты)
LANG_BUNDLED="python-standards typescript-standards"
;;
mobile) # Android (Kotlin) + iOS (Swift)
LANG_BUNDLED="kotlin-standards swift-standards"
;;
pm) # Продакт-менеджер: постановка задач, без языковых стандартов кода
LANG_BUNDLED=""
EXTRA_BUNDLED="pm-task-spec"
;;
*) # all: не гейтим, включаем все языковые скилы
LANG_BUNDLED="go-standards rust-standards python-standards typescript-standards kotlin-standards swift-standards"
;;
esac
# 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:[...]}
@@ -176,12 +237,8 @@ console.log(patched ? "gated bundled skills -> " + allowed.join(", ") : "pi-kit
NODE
}
if [ "$PROFILE" = "all" ]; then
info "Профиль 'all' — оставляю все скилы пакета без гейтинга."
else
info "Гейчу скилы пакета под профиль '${PROFILE}'..."
gate_bundled_skills "${COMMON_BUNDLED} ${LANG_BUNDLED} ${EXTRA_BUNDLED}" || warn "не удалось отфильтровать скилы (см. выше)."
fi
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.
@@ -200,7 +257,7 @@ for entry in "${COMMON_PUBLIC_SKILLS[@]}"; do
add_public_skill "${entry%%:*}" "${entry##*:}"
done
if [ "${#PROFILE_PUBLIC[@]}" -gt 0 ]; then
info "Ставлю публичные скилы профиля '${PROFILE}'..."
info "Ставлю публичные скилы профилей '${PROFILES[*]}'..."
for entry in "${PROFILE_PUBLIC[@]}"; do
add_public_skill "${entry%%:*}" "${entry##*:}"
done
@@ -379,13 +436,13 @@ fi
cat <<EOF
────────────────────────────────────────────────────────
Готово! Профиль: ${PROFILE} Канал: ${PI_KIT_CHANNEL}
Готово! Профили: ${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=backend ./install.sh (или перезапусти и выбери; есть и 'pm')
Сменить профиль: PI_KIT_PROFILE=frontend,qa ./install.sh (или перезапусти и выбери несколько)
Сменить канал: PI_KIT_CHANNEL=beta ./install.sh
Команды: /bugfix <тикет> /feature <тикет> /review [фокус] /rfc <тема>
/kit-config /kit-doctor /kit-help