#!/bin/sh # Astrum Shop · подключение к партнёрскому API одной командой. # # curl -fsSL https://astrum.shop/connect | sh # # В папке проекта: примет ваш ключ или выдаст ключ песочницы, проверит его, # поставит скилл ИИ-агентам, запишет ключ в .env и прогонит тестовый заказ. # Без sudo и без установки программ: нужны curl и unzip (или python3). # Каждый шаг разобран здесь: https://astrum.shop/docs/connect # # Ключ читается скрытым вводом с терминала. В историю команд, в аргументы # процессов и в вывод он не попадает — только в .env проекта с правами 600. # Без терминала (CI) ключ берётся из переменной ASTRUM_KEY. # # Весь код — внутри main(): оборванная загрузка не исполнит полскрипта. CONNECT_VERSION="2.0.0" LIVE_BASE="https://api.astrum.shop/v1" SANDBOX_BASE="https://sandbox-api.astrum.shop/v1" SKILL_NAME="astrum-partner-api" SKILL_ZIP="https://api.astrum.shop/v1/skill/astrum-partner-api-latest.zip" DOCS="https://astrum.shop/docs" LLMS_FULL="https://api.astrum.shop/llms-full.txt" AGENTS_MARK="" if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then G=$(printf '\033[32m'); R=$(printf '\033[31m'); Y=$(printf '\033[33m') D=$(printf '\033[2m'); B=$(printf '\033[1m'); N=$(printf '\033[0m') else G=""; R=""; Y=""; D=""; B=""; N="" fi # Колонка меток — по символам, а не байтам: у кириллицы их вдвое больше. pad() { n=$(printf '%s' "$1" | wc -m | tr -d ' ') printf '%s' "$1" while [ "$n" -lt 21 ]; do printf ' '; n=$((n + 1)); done } ok() { printf ' %s✓%s %s %s%s%s\n' "$G" "$N" "$(pad "$1")" "$D" "$2" "$N"; } warn() { printf ' %s!%s %s %s\n' "$Y" "$N" "$(pad "$1")" "$2"; } skip() { printf ' %s–%s %s %s%s%s\n' "$D" "$N" "$(pad "$1")" "$D" "$2" "$N"; } fail() { printf ' %s✗%s %s\n' "$R" "$N" "$1"; } more() { printf ' %s\n' "$1"; } sub() { printf ' %s%s%s\n' "$D" "$1" "$N"; } # plural N один два пять → «1 ключ», «3 ключа», «5 ключей». plural() { m10=$(($1 % 10)); m100=$(($1 % 100)) if [ "$m10" -eq 1 ] && [ "$m100" -ne 11 ]; then printf '%s %s' "$1" "$2" elif [ "$m10" -ge 2 ] && [ "$m10" -le 4 ] && { [ "$m100" -lt 12 ] || [ "$m100" -gt 14 ]; }; then printf '%s %s' "$1" "$3" else printf '%s %s' "$1" "$4"; fi } # «18720» → «5 ч 12 мин». human_wait() { s=${1:-0} h=$((s / 3600)); m=$(((s % 3600 + 59) / 60)) if [ "$m" -ge 60 ]; then h=$((h + 1)); m=0; fi if [ "$h" -gt 0 ] && [ "$m" -gt 0 ]; then printf '%s ч %s мин' "$h" "$m" elif [ "$h" -gt 0 ]; then printf '%s ч' "$h" else printf '%s мин' "$m"; fi } # http МЕТОД АДРЕС [ТЕЛО] → тело в $TMP/body, заголовки в $TMP/head, код в $CODE. # ⚠️ Ключ уходит в curl конфигом через stdin (-K -), а не аргументом: аргументы # процессов видит любой пользователь машины. http() { auth="" if [ -n "${KEY:-}" ]; then auth="header = \"Authorization: Bearer $KEY\""; fi if [ -n "${3:-}" ]; then CODE=$(printf '%s\n' "$auth" | curl -sS -K - -X "$1" --max-time 60 \ -H "User-Agent: astrum-connect/$CONNECT_VERSION" -H "Content-Type: application/json" \ --data "$3" -D "$TMP/head" -o "$TMP/body" -w '%{http_code}' "$2" 2>"$TMP/err") || CODE=000 else CODE=$(printf '%s\n' "$auth" | curl -sS -K - -X "$1" --max-time 60 \ -H "User-Agent: astrum-connect/$CONNECT_VERSION" \ -D "$TMP/head" -o "$TMP/body" -w '%{http_code}' "$2" 2>"$TMP/err") || CODE=000 fi } field() { sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" "$TMP/body" | head -n 1; } # Имя ошибки (`name`): номер ошибки (`code`) — число, ветвимся по имени — оно читается. error_code() { sed -n 's/.*"error":{"code":[0-9]*,"name":"\([a-z_]*\)".*/\1/p' "$TMP/body" | head -n 1; } retry_after() { tr -d '\r' <"$TMP/head" | awk -F': *' 'tolower($1) == "retry-after" { print $2 }' | tail -n 1; } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{ print $1 }' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{ print $1 }' elif command -v python3 >/dev/null 2>&1; then python3 -c 'import hashlib, sys; print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' "$1" else return 1; fi } unzip_to() { if command -v unzip >/dev/null 2>&1; then unzip -q -o "$1" -d "$2" elif command -v python3 >/dev/null 2>&1; then python3 -m zipfile -e "$1" "$2" else return 1; fi } restore_tty() { if [ -n "${STTY_SAVED:-}" ]; then stty "$STTY_SAVED" /dev/null; STTY_SAVED=""; fi } short_path() { case "$1" in "$HOME") printf '~' ;; "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac } main() { if ! command -v curl >/dev/null 2>&1; then printf 'Нужен curl: он и скачал этот скрипт.\n' >&2 exit 1 fi TMP=$(mktemp -d 2>/dev/null || mktemp -d -t astrum) || exit 1 trap 'restore_tty; rm -rf "$TMP"' EXIT trap 'restore_tty; rm -rf "$TMP"; exit 130' INT TERM PROJECT=$(pwd -P) HOME_REAL=$(cd "$HOME" 2>/dev/null && pwd -P || printf '%s' "$HOME") printf '\n %sAstrum Shop · подключение к API%s %sconnect %s%s\n\n' "$B" "$N" "$D" "$CONNECT_VERSION" "$N" printf ' Папка проекта %s\n\n' "$(short_path "$PROJECT")" # ── до ключа: всё, из-за чего ключ пришлось бы выбросить ── if [ "$PROJECT" = "$HOME_REAL" ] || [ "$PROJECT" = "/" ]; then warn "Это домашняя папка" ".env ляжет в $(short_path "$PROJECT")" answer="" if (: /dev/null; then printf ' Продолжить? [y/N] ' IFS= read -r answer /dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then IN_GIT=1 if git ls-files --error-unmatch .env >/dev/null 2>&1; then fail ".env отслеживается git — ключ туда не пишем" more "Уберите его из индекса и запустите снова:" more "\$ git rm --cached .env" more "\$ echo .env >> .gitignore" exit 1 fi fi # ── ключ ── KEY="" if [ -n "${ASTRUM_KEY:-}" ]; then KEY=$ASTRUM_KEY printf ' Ключ API — из переменной ASTRUM_KEY\n' elif (: /dev/null; then printf ' %sКлюч API%s — ask_test_… или ask_live_…\n' "$B" "$N" printf ' %sНет ключа? Нажмите Enter — выдадим ключ песочницы.%s\n' "$D" "$N" # Эхо — выключить ДО подсказки: набранное раньше, чем сработает stty, ушло бы на экран. STTY_SAVED=$(stty -g /dev/null) || STTY_SAVED="" stty -echo /dev/null printf ' › ' IFS= read -r KEY /dev/null; then want=$(curl -fsSL --max-time 30 "$SKILL_ZIP.sha256" 2>/dev/null | awk '{ print $1 }') got=$(sha256_of "$TMP/skill.zip" 2>/dev/null) if [ -n "$want" ] && [ "$want" = "$got" ] && unzip_to "$TMP/skill.zip" "$TMP/skill" >/dev/null 2>&1 \ && [ -f "$TMP/skill/$SKILL_NAME/SKILL.md" ]; then SKILL_OK=1 SKILL_VERSION=$(sed -n 's/^ skill-version: "\(.*\)"$/\1/p' "$TMP/skill/$SKILL_NAME/SKILL.md" | head -n 1) fi fi CLAUDE=""; CODEX=""; CURSOR="" if command -v claude >/dev/null 2>&1 || [ -d "$HOME/.claude" ]; then CLAUDE=1; fi if command -v codex >/dev/null 2>&1 || [ -d "$HOME/.codex" ]; then CODEX=1; fi if command -v cursor >/dev/null 2>&1 || [ -d "$HOME/.cursor" ]; then CURSOR=1; fi SKILL_DIR="" CHECK_PY="" if [ -z "$SKILL_OK" ]; then skip "Скилл" "не скачан — возьмите его позже: $DOCS/skill" else if [ -n "$CLAUDE" ]; then SKILL_DIR=".claude/skills"; else SKILL_DIR="astrum-skill"; fi mkdir -p "$PROJECT/$SKILL_DIR" \ && rm -rf "${PROJECT:?}/$SKILL_DIR/$SKILL_NAME" \ && cp -R "$TMP/skill/$SKILL_NAME" "$PROJECT/$SKILL_DIR/" CHECK_PY="$PROJECT/$SKILL_DIR/$SKILL_NAME/scripts/astrum_check.py" if [ -n "$CLAUDE" ]; then ok "Claude Code" "скилл $SKILL_NAME ${SKILL_VERSION:-} → .claude/skills/" else skip "Claude Code" "не найден" fi if [ -n "$CODEX" ] || [ -n "$CURSOR" ]; then if [ -f "$PROJECT/AGENTS.md" ] && grep -qF "$AGENTS_MARK" "$PROJECT/AGENTS.md"; then note="абзац про Astrum уже в AGENTS.md" else cat >>"$PROJECT/AGENTS.md" <"$PROJECT/.env"; fi chmod 600 "$PROJECT/.env" grep -v -e '^ASTRUM_KEY=' -e '^ASTRUM_BASE_URL=' "$PROJECT/.env" >"$TMP/env" || true { cat "$TMP/env" printf 'ASTRUM_KEY=%s\nASTRUM_BASE_URL=%s\n' "$KEY" "$BASE" } >"$PROJECT/.env" umask "$OLD_UMASK" if [ -n "$IN_GIT" ]; then if git check-ignore -q .env 2>/dev/null; then ignore=".env уже в .gitignore" else printf '\n.env\n' >>"$PROJECT/.gitignore" ignore=".env добавлен в .gitignore" fi else ignore="не git-репозиторий — не коммитьте .env" fi ok ".env" "ASTRUM_KEY (права 600) и ASTRUM_BASE_URL · $ignore" # ── прогон ── if [ "$ENV_NAME" = "бой" ]; then warn "Боевой ключ" "прогон с заказом не запускаем: он тратит баланс" sub "проверили без трат — GET /account и каталог: ok" http GET "$BASE/webhook" if [ "$CODE" = 200 ] && grep -q '"url":null' "$TMP/body"; then warn "Вебхук не задан" "изменения заказов — только через GET /v1/orders" sub "задать — $DOCS/webhooks" fi printf '\n Готово. Сначала — песочница: та же команда без ключа выдаст тестовый.\n\n' return 0 fi rc=0 if [ -n "$CHECK_PY" ] && command -v python3 >/dev/null 2>&1; then printf '\n %sПрогон песочницы — astrum_check.py sandbox%s\n' "$B" "$N" { ASTRUM_KEY=$KEY ASTRUM_BASE_URL=$BASE python3 -u "$CHECK_PY" sandbox 2>&1; echo "$?" >"$TMP/rc"; } \ | sed 's/^./ &/' rc=$(cat "$TMP/rc" 2>/dev/null || echo 1) elif [ -n "$CHECK_PY" ]; then skip "Прогон" "нужен python3 — потом: python3 $SKILL_DIR/$SKILL_NAME/scripts/astrum_check.py sandbox" else skip "Прогон" "без скилла не запускаем" fi if [ "$rc" = 0 ]; then printf '\n Готово. Скажите своему ИИ-агенту:\n' printf ' %s› Подключи заказ подписок Astrum к моему боту%s\n' "$B" "$N" else printf '\n Прогон не прошёл целиком — строки FAIL выше. Разбор: %s/sandbox\n' "$DOCS" fi printf ' %sРуками — %s/start%s\n\n' "$D" "$DOCS" "$N" return 0 } main "$@"