<# Установка ccv на Windows одной командой: irm https://claudevisual.com/install.ps1 | iex Ставит пакет `@i14ks/ccv` из npm глобально — то же самое, что `npm install --global @i14ks/ccv`, плюс три вещи: проверка версии Node, проверка PATH (без них голая команда на Windows чаще всего и не срабатывает) и выбор движка — качать ли 254 МБ Claude Code в комплекте или хватит того, что уже стоит в системе. Про PATH подробнее: команду прописывает npm, а не мы, — пакет объявляет `bin`, и глобальная установка кладёт `ccv.cmd` в каталог глобальных команд npm. Но сам этот каталог (`%APPDATA%\npm`) в PATH попадает только установщиком Node, и на машине, где его оттуда убрали или где Node ставили портативно, все глобальные команды npm молча не находятся. Проверяем и, если каталога в пользовательском PATH нет, дописываем — один раз и без прав администратора. Своего каталога установки у нас нет: `ccv` — не бинарник, а пакет npm, и удаляется тем же npm (`npm uninstall --global @i14ks/ccv`). Скрипт лежит открытым текстом по тому же адресу, откуда качается, — его можно прочитать до запуска. ВАЖНО: файл сохраняется в UTF-8 БЕЗ BOM. Соблазн добавить BOM есть — с ним Windows PowerShell 5.1 правильно читает русский текст, когда скрипт запускают сохранённым файлом. Но BOM ломает ровно тот способ, который написан на сайте: `irm | iex` с ним не разбирается ни в 5.1, ни в 7 — проверено. Кодировку из ответа сервера (`text/plain; charset=utf-8`) `irm` учитывает сам, и без BOM одна команда работает в обеих версиях PowerShell. Переменная CCV_VERSION ставит конкретную версию вместо последней. #> # Тело в блоке `& { }` намеренно: скрипт исполняется через `iex` прямо в сессии # пользователя, и без своей области видимости он оставил бы в ней и свои # переменные, и $ErrorActionPreference. По той же причине выход отсюда — # `return`, а не `exit`: `exit` закрыл бы интерактивную консоль целиком. & { $ErrorActionPreference = 'Stop' $package = '@i14ks/ccv' $version = if ($env:CCV_VERSION) { $env:CCV_VERSION } else { 'latest' } $nodeMin = [version]'22.5' function Say ($m) { Write-Host "[ccv] $m" -ForegroundColor DarkGray } function Good ($m) { Write-Host "[ccv] $m" -ForegroundColor Green } function Bad ($m) { Write-Host "[ccv] $m" -ForegroundColor Red } # ------------------------------------------------------------ Node и npm if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Bad "нужен Node.js $nodeMin или новее — поставьте его и повторите: https://nodejs.org" return } if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { Bad 'Node есть, а npm нет — поставьте Node с https://nodejs.org, npm приходит вместе с ним' return } # `node -v` даёт «v22.14.0», у нестабильных сборок бывает и «v23.0.0-rc.1». $raw = (node -v).Trim() $parsed = $null if (-not [version]::TryParse(($raw.TrimStart('v') -replace '-.*$', ''), [ref]$parsed)) { Bad "не разобрал версию Node ($raw) — поставьте Node $nodeMin или новее: https://nodejs.org" return } if ($parsed -lt $nodeMin) { Bad "Node $raw слишком старый, нужен $nodeMin или новее — обновите: https://nodejs.org" return } 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` # задают платформу, под которую отбираются платформенные пакеты, и # несуществующая не подходит ни одному из восьми. Других пакетов с полями # `os`/`cpu` в дереве ccv нет — иначе они бы тоже отсеялись. Say "ставлю $package ($version) глобально" npm install --global --os=none --cpu=none "$package@$version" if ($LASTEXITCODE -ne 0) { Bad 'установка не прошла — смотрите вывод npm выше' return } # --------------------------------------------------------- движок сессий # Своего Claude Code ищет сам ccv, а не мы: он разбирает PATH с учётом # PATHEXT, заглядывает внутрь шимов npm за путём к `cli.js` и проверяет # `~/.local/bin`, куда кладёт нативный установщик, — `Get-Command claude` # не умеет ни первого, ни третьего. Отсюда и порядок: сначала пакет, потом # вопрос к нему. Ответ он печатает сам, а нам отдаёт кодом возврата. # # Зовём по полному пути: каталог глобальных команд npm может быть ещё не в # PATH — это чинится ниже, а ответ нужен сейчас. # `npm prefix -g`, а не `npm config get prefix`: второй умеет отказать — # «The prefix option is protected, and can not be retrieved in this way», # если prefix задан переменной окружения `npm_config_prefix` (законный # способ настроить npm). Отказ здесь стоил бы дорого: $prefix оказывается # пустым, Join-Path бросает, и скрипт умирает целиком — уже после # установки, но не дойдя ни до движка, ни до PATH. $prefix = (npm prefix -g 2>$null | Select-Object -Last 1) if ($prefix) { $prefix = $prefix.Trim() } $haveClaude = $false if ($prefix) { # Спрашиваем именно тот ccv, который только что поставили. Первый # попавшийся из PATH может быть другим — копией из прежнего prefix или # установкой прошлых времён; такой ответит на `--claude` «неизвестная # опция», и мы зря выкачаем 254 МБ. $ccvCmd = Join-Path $prefix 'ccv.cmd' if (Test-Path -LiteralPath $ccvCmd) { & $ccvCmd --claude $haveClaude = ($LASTEXITCODE -eq 0) } } if (-not $haveClaude) { Say 'нужен движок Claude Code — доставляю (254 МБ, это дольше)' npm install --global "$package@$version" if ($LASTEXITCODE -ne 0) { Bad 'движок не поставился — смотрите вывод npm выше' return } Say 'поставьте Claude Code (https://claude.com/claude-code) — тогда' Say 'переустановка ccv обойдётся без этих 254 МБ' } # ------------------------------------------------------------------ PATH # Каталога команд мы не знаем — сказать про PATH нечего. Проверяем то # единственное, что здесь важно: находится ли команда вообще. if (-not $prefix) { if (Get-Command ccv -ErrorAction SilentlyContinue) { Good 'готово. Запустить: ccv' Good 'поднимет хост и откроет браузер. Опции: ccv --help' } else { Bad 'пакет поставлен, но команда ccv не находится, а каталог команд npm не определился' Say 'подскажет: npm prefix -g — этот каталог должен быть в PATH' } return } # На Windows команды лежат в самом prefix, каталога bin внутри него нет. $binDir = $prefix $inPath = ($env:PATH -split [IO.Path]::PathSeparator | ForEach-Object { $_.TrimEnd('\', '/') }) -contains $binDir.TrimEnd('\', '/') if ($inPath) { Good 'готово. Запустить: ccv' Good 'поднимет хост и откроет браузер. Опции: ccv --help' return } Say "каталог $binDir отсутствует в PATH — дописываю в пользовательский" # Читаем PATH сырым: Get-ItemProperty разворачивает %ПЕРЕМЕННЫЕ%, и запись # развёрнутого значения обратно превратила бы `%USERPROFILE%\bin` в путь, # намертво привязанный к нынешнему пользователю. По той же причине # сохраняем тип значения: был REG_EXPAND_SZ — останется им. try { $key = Get-Item 'HKCU:\Environment' $current = $key.GetValue('Path', $null, 'DoNotExpandEnvironmentNames') $kind = if ($null -eq $current) { 'String' } else { $key.GetValueKind('Path') } $updated = if ([string]::IsNullOrWhiteSpace($current)) { $binDir } else { $current.TrimEnd(';') + ';' + $binDir } Set-ItemProperty -Path 'HKCU:\Environment' -Name Path -Value $updated -Type $kind # Чтобы `ccv` работал и в этом окне тоже, а не только в следующем. $env:PATH = $env:PATH.TrimEnd(';') + ';' + $binDir Good "PATH дополнен: $binDir" } catch { Bad "не смог дописать PATH ($_)" Say "добавьте вручную в переменную PATH пользователя: $binDir" return } Good 'готово. Запустить: ccv' Good 'в уже открытых терминалах PATH прежний — там понадобится новое окно' }