#!/bin/sh # # Установка ccv на macOS и Linux одной командой: # # curl -fsSL https://claudevisual.com/install.sh | sh # # Ставит пакет `@i14ks/ccv` из npm глобально — то же самое, что # `npm install --global @i14ks/ccv`, но с проверками, из-за которых # голая команда чаще всего и не срабатывает: нет Node, Node слишком старый, # каталог глобальных команд npm не в PATH. Своего каталога установки у нас нет # и не будет: `ccv` — не бинарник, а пакет npm, и удалять его надо тем же npm # (`npm uninstall --global @i14ks/ccv`), а не поиском по диску. # # Скрипт читается: он лежит открытым текстом по тому же адресу, откуда его # качают. Если исполнять из сети не хочется, всё, что он делает, — команда # `npm install --global @i14ks/ccv`, проверка движка (см. ниже) и совет # про PATH. # # Переменные: CCV_VERSION — поставить конкретную версию вместо последней. set -eu PACKAGE="@i14ks/ccv" VERSION="${CCV_VERSION:-latest}" NODE_MIN="22.5" # Цвет только в терминал: в файле или в конвейере escape-последовательности # превратились бы в мусор. NO_COLOR — общепринятый способ их отключить. if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then B="$(printf '\033[1m')"; DIM="$(printf '\033[2m')"; R="$(printf '\033[0m')" OK="$(printf '\033[32m')"; ERR="$(printf '\033[31m')" else B=''; DIM=''; R=''; OK=''; ERR='' fi say() { printf '%s[ccv]%s %s\n' "$DIM" "$R" "$1"; } good() { printf '%s[ccv]%s %s\n' "$OK" "$R" "$1"; } die() { printf '%s[ccv]%s %s\n' "$ERR" "$R" "$1" >&2; exit 1; } # ---------------------------------------------------------------- Node и npm if ! command -v node >/dev/null 2>&1; then die "нужен Node.js $NODE_MIN или новее — поставьте его и повторите: ${B}https://nodejs.org${R}" fi if ! command -v npm >/dev/null 2>&1; then die "Node есть, а npm нет — поставьте Node с сайта ${B}https://nodejs.org${R}, npm приходит вместе с ним" fi # Сравниваем числами, а не строкой: «22.9» новее «22.10» только в лексикографике. raw="$(node -v)" # v22.14.0 raw="${raw#v}" major="${raw%%.*}" rest="${raw#*.}" minor="${rest%%.*}" if [ "$major" -lt 22 ] || { [ "$major" -eq 22 ] && [ "$minor" -lt 5 ]; }; then die "Node $raw слишком старый, нужен $NODE_MIN или новее — обновите: ${B}https://nodejs.org${R}" fi say "Node $raw — годится" # ------------------------------------------------------------------ установка # Ставим сначала без движка. Сессии поднимает Claude Code, а он у пользователя # ccv стоит заведомо — ccv к нему интерфейс. Копия того же движка приезжает с # Agent SDK (`optionalDependencies`) и весит 254 МБ при 600 кБ самого пакета; # качать её вслепую значит четверть часа ожидания ради файла, которым в # большинстве установок никто не воспользуется. Нашёлся свой Claude Code или # нет — спрашиваем ниже у самого ccv и, если не нашёлся, доставляем движок. # # Отбирает движок пара `--os`/`--cpu`, а не `--omit=optional`, как просится: # при глобальной установке npm флаг omit молча игнорирует (проверено на npm 10 # и 11, установка остаётся 293 МБ). `--os`/`--cpu` задают платформу, под # которую отбираются платформенные пакеты, и несуществующая не подходит ни # одному из восьми. В дереве ccv никаких других пакетов с полями `os`/`cpu` # нет — иначе они бы тоже отсеялись. LIGHT="--os=none --cpu=none" say "ставлю $PACKAGE ($VERSION) глобально" # shellcheck disable=SC2086 # два отдельных флага, а не один аргумент if ! npm install --global $LIGHT "$PACKAGE@$VERSION"; then # Самая частая причина — системный Node, у которого каталог глобальных # пакетов принадлежит root. Молча звать sudo из скрипта, скачанного из сети, # нельзя: это ровно та команда, которую человек должен набрать сам. prefix="$(npm prefix -g 2>/dev/null || echo '')" if [ -n "$prefix" ] && [ ! -w "$prefix" ]; then printf '\n' say "каталог $prefix закрыт на запись. Два выхода:" say " 1) поставить от администратора: ${B}sudo npm install --global $PACKAGE${R}" say " 2) увести глобальные пакеты в свой каталог (лучше, sudo больше не нужен):" say " ${B}npm config set prefix ~/.npm-global${R}" say " ${B}export PATH=\"\$HOME/.npm-global/bin:\$PATH\"${R} ${DIM}(и в ~/.zshrc или ~/.bashrc)${R}" fi die "установка не прошла" fi # ------------------------------------------------------- движок для сессий # Своего Claude Code ищет сам ccv, а не мы: он смотрит и мимо PATH — в # `~/.local/bin`, куда кладёт нативный установщик, и в `/usr/local/bin`, — а # `command -v claude` этого не умеет. Отсюда и порядок: сначала пакет, потом # вопрос к нему. Ответ он печатает сам, а нам отдаёт кодом возврата. # Спрашиваем именно тот ccv, который только что поставили, — по пути из # prefix, а не первый попавшийся из PATH. Разница не теоретическая: другой ccv # в PATH ((пере)установка в свой prefix, оставшаяся с прежних времён копия) на # `--claude` ответит «неизвестная опция», и скрипт решит, что Claude Code в # системе нет, — то есть выкачает 254 МБ, которые не нужны. # # Раскладок в prefix две: на macOS и Linux команды лежат в `bin` внутри него, а # npm под Windows кладёт их в сам prefix. Целимся мы в первую, но вторую тоже # проверяем — иначе этот скрипт нельзя прогнать в Git Bash, а другого способа # его проверить до живого Mac у нас нет. ccv_bin="" prefix="$(npm prefix -g 2>/dev/null || echo '')" if [ -n "$prefix" ]; then for candidate in "$prefix/bin/ccv" "$prefix/ccv"; do [ -z "$ccv_bin" ] && [ -x "$candidate" ] && ccv_bin="$candidate" done fi [ -z "$ccv_bin" ] && ccv_bin="$(command -v ccv 2>/dev/null || true)" if [ -z "$ccv_bin" ] || ! "$ccv_bin" --claude; then say "нужен движок Claude Code — доставляю (254 МБ, это дольше)" npm install --global "$PACKAGE@$VERSION" || die "движок не поставился" say "поставьте Claude Code (${B}https://claude.com/claude-code${R}) — тогда" say "переустановка ccv обойдётся без этих 254 МБ" fi # --------------------------------------------------------------------- PATH # Команду в PATH прописывает npm, а не мы: пакет объявляет `bin`, и глобальная # установка кладёт `ccv` в свой каталог команд. Проверяем только, виден ли этот # каталог оболочке: на портативном Node и после `npm config set prefix` он # запросто оказывается вне PATH, и тогда установка «прошла», а команды нет. if command -v ccv >/dev/null 2>&1; then good "готово. Запустить: ${B}ccv${R}" good "поднимет хост и откроет браузер. Опции: ${B}ccv --help${R}" exit 0 fi bin="$(npm prefix -g 2>/dev/null)/bin" good "пакет поставлен, но каталог $bin не в PATH" say "добавьте строку в ~/.zshrc или ~/.bashrc и откройте новый терминал:" printf '\n %sexport PATH="%s:$PATH"%s\n\n' "$B" "$bin" "$R" say "проверить: ${B}ccv --help${R}"