diff --git a/setup.ps1 b/setup.ps1 index a5e327e..c1ce41f 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -12,6 +12,13 @@ # в приложение Claude и перезапускает его. Ничего не скачивает # и не устанавливает. Терминальный Claude Code не затрагивается. # +# Понимает обе сборки приложения Claude для Windows: +# · классическую: %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe; +# · MSIX (Microsoft Store и установщик с claude.com): +# C:\Program Files\WindowsApps\Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe. +# Сторонний режим у обеих живёт в %LOCALAPPDATA%\Claude-3p (так в коде приложения), +# родная папка данных зависит от сборки, см. Get-ClaudeInstall. +# # ВНИМАНИЕ при правке: сохранять в UTF-8 БЕЗ BOM. Gitea отдаёт файл # с заголовком charset=utf-8, irm декодирует кириллицу правильно, # а BOM в конвейере irm | iex становится лишней командой и роняет разбор. @@ -23,9 +30,6 @@ $Mode = "" if ($env:AK) { $Mode = $env:AK; $env:AK = $null } $ErrorActionPreference = "Stop" -$C1P = Join-Path $env:LOCALAPPDATA "Claude" -$C3P = "$C1P-3p" -$LIB = Join-Path $C3P "configLibrary" $ID_OLLAMA = "a11a0000-0000-4000-8000-0000000000aa" $ID_ZAI = "a11a0000-0000-4000-8000-0000000000bb" $ID_DEFAULT = "a11a0000-0000-4000-8000-0000000000dd" @@ -45,21 +49,110 @@ function Read-Json($path) { try { Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $null } } +# ---------- Где приложение и где его данные ---------- +# Сборок две, и ведут они себя по-разному. +# · Классическая (Squirrel): %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe, +# родные данные в %APPDATA%\Claude, логи в %LOCALAPPDATA%\Claude\logs. +# · MSIX (Microsoft Store и установщик с claude.com): +# C:\Program Files\WindowsApps\Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe. +# Папка WindowsApps закрыта системой, exe оттуда напрямую не запускается, а всё, +# что приложение пишет в %APPDATA%, Windows подменяет на +# %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude. +# Сторонний режим у ОБЕИХ сборок живёт в %LOCALAPPDATA%\Claude-3p: так прописано +# в коде приложения, а в манифесте MSIX эта папка исключена из подмены. function Find-ClaudeExe { @("$env:LOCALAPPDATA\AnthropicClaude", "$env:LOCALAPPDATA\Programs") | + Where-Object { Test-Path $_ } | ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1 } +function Find-ClaudeMsix { + $pkg = $null + try { + if (-not (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) -and $PSVersionTable.PSVersion.Major -ge 7) { + Import-Module Appx -UseWindowsPowerShell -ErrorAction SilentlyContinue + } + $all = @(Get-AppxPackage -Name "*Claude*" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq "Claude" -or $_.Name -like "*ClaudeDesktop*" -or $_.Publisher -like "*Anthropic*" }) + $pkg = $all | Where-Object { $_.Name -eq "Claude" } | Select-Object -First 1 + if (-not $pkg) { $pkg = $all | Select-Object -First 1 } + } catch { $pkg = $null } + if ($pkg) { + $appId = "Claude" + try { + $ids = @((Get-AppxPackageManifest $pkg).Package.Applications.Application.Id) + if ($ids.Count -gt 0 -and $ids[0]) { $appId = "$($ids[0])" } + } catch { } + return [pscustomobject]@{ Family = "$($pkg.PackageFamilyName)"; Aumid = "$($pkg.PackageFamilyName)!$appId"; Version = "$($pkg.Version)" } + } + # Запасной путь: Get-AppxPackage бывает недоступен (PowerShell 7 без модуля Appx), + # но папка данных пакета остаётся на месте после первого запуска приложения. + $d = Get-ChildItem (Join-Path $env:LOCALAPPDATA "Packages") -Directory -Filter "Claude_*" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($d) { return [pscustomobject]@{ Family = $d.Name; Aumid = "$($d.Name)!Claude"; Version = "" } } + return $null +} + +function Get-ClaudeInstall { + $msix = Find-ClaudeMsix + $exe = Find-ClaudeExe + if ($msix) { + $home1p = Join-Path $env:LOCALAPPDATA "Packages\$($msix.Family)\LocalCache\Roaming\Claude" + $launched = Test-Path $home1p + # Если рядом осталась и классическая сборка, верим той, которой пользовались. + if ($launched -or -not $exe) { + $label = "сборка MSIX (Microsoft Store или установщик с claude.com)" + if ($msix.Version) { $label += ", версия $($msix.Version)" } + return [pscustomobject]@{ Kind = "msix"; Label = $label; Aumid = $msix.Aumid; Exe = $null; Home = $home1p; Launched = $launched } + } + } + if ($exe) { + $home1p = Join-Path $env:APPDATA "Claude" + return [pscustomobject]@{ Kind = "classic"; Label = "классическая сборка"; Aumid = $null; Exe = $exe.FullName; Home = $home1p; Launched = (Test-Path $home1p) } + } + return $null +} + +$INST = Get-ClaudeInstall + +# Сторонний режим: конфиги, библиотека, флаг разработчика для стороннего режима. +$C3P = Join-Path $env:LOCALAPPDATA "Claude-3p" +$LIB = Join-Path $C3P "configLibrary" + +# Родные папки: сюда кладём только флаг разработчика и чистим кэши. Их несколько, +# потому что настоящая зависит от сборки, а лишний флаг ничему не вредит. +$P1_DIRS = @() +if ($INST) { $P1_DIRS += $INST.Home } +foreach ($d in @((Join-Path $env:APPDATA "Claude"), (Join-Path $env:LOCALAPPDATA "Claude"), (Join-Path $env:LOCALAPPDATA "Claude-Data"))) { + if ($P1_DIRS -notcontains $d -and ((Test-Path $d) -or $P1_DIRS.Count -eq 0)) { $P1_DIRS += $d } +} +if ($P1_DIRS.Count -eq 0) { $P1_DIRS += (Join-Path $env:APPDATA "Claude") } +$ALL_DIRS = @($P1_DIRS) + @($C3P) + +# Пакетное приложение запускают по идентификатору, а не по пути к exe. +function Start-Claude { + if ($INST.Kind -eq "msix") { + $target = "shell:AppsFolder\$($INST.Aumid)" + try { Start-Process $target -ErrorAction Stop; return } catch { } + try { Start-Process explorer.exe $target -ErrorAction Stop; return } catch { } + $alias = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\Claude.exe" + if (Test-Path $alias) { try { Start-Process $alias -ErrorAction Stop; return } catch { } } + try { Start-Process "claude://" -ErrorAction Stop } catch { } + return + } + if ($INST.Exe) { Start-Process $INST.Exe } +} + # ВАЖНО. Приложение читает developer_settings.json из ТЕКУЩЕЙ папки данных, # а она переключается на -3p только когда сторонний режим уже взлетел. -# Кладём флаг в ОБЕ папки, иначе пункт Developer не появится. +# Кладём флаг и в родные папки, и в -3p, иначе пункт Developer не появится. function Write-DevFlag { - foreach ($d in @($C1P, $C3P)) { + foreach ($d in $ALL_DIRS) { New-Item -ItemType Directory -Force -Path $d | Out-Null Write-Utf8 (Join-Path $d "developer_settings.json") '{"allowDevTools": true}' } - Ok "Режим разработчика включён в обеих папках приложения." + Ok "Режим разработчика включён во всех папках приложения." } # "deploymentMode": "1p" намертво запрещает сторонний режим. @@ -81,7 +174,7 @@ function Clear-DeploymentPin { function Repair-JsonBom { $files = @() if (Test-Path $LIB) { $files += Get-ChildItem $LIB -Filter *.json -ErrorAction SilentlyContinue } - foreach ($d in @($C1P, $C3P)) { + foreach ($d in $ALL_DIRS) { foreach ($n in @("developer_settings.json", "claude_desktop_config.json")) { $f = Join-Path $d $n if (Test-Path $f) { $files += Get-Item $f } @@ -159,7 +252,7 @@ function Check-Applied { # скиллы и наши конфиги остаются на месте. Делать только при закрытом приложении. # ВАЖНО: не путать с соседним пунктом Reset App Data, тот сносит папку целиком. function Clear-Caches { - foreach ($d in @($C1P, $C3P)) { + foreach ($d in $ALL_DIRS) { if (-not (Test-Path $d)) { continue } foreach ($c in @("Cache", "Code Cache", "GPUCache", "DawnGraphiteCache", "DawnWebGPUCache", "Service Worker")) { Remove-Item (Join-Path $d $c) -Recurse -Force -ErrorAction SilentlyContinue @@ -174,12 +267,12 @@ function Restart-Claude { $i = 0 while ((Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $i -lt 12) { Start-Sleep 1; $i++ } Clear-Caches - $exe = Find-ClaudeExe - if (-not $exe) { Warn "Не нашёл claude.exe, открой Claude вручную."; return } - Start-Process $exe.FullName + if (-not $INST) { Warn "Не нашёл приложение Claude, открой его вручную."; return } + Start-Claude # Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя. + # Пакетной сборке на старт нужно больше времени. $j = 0 - while (-not (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $j -lt 8) { Start-Sleep 1; $j++ } + while (-not (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) -and $j -lt 12) { Start-Sleep 1; $j++ } if (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) { Ok "Claude перезапущен." } else { @@ -205,10 +298,16 @@ if ($Mode -eq "restore") { # ---------- 1. Приложение Claude ---------- Say "[1/4] Ищу приложение Claude..." -if (Find-ClaudeExe) { - Ok "Claude на месте." +if ($INST) { + Ok "Claude на месте: $($INST.Label)." + Ok "Конфиги стороннего режима: $C3P" + if (-not $INST.Launched) { + Warn "Похоже, приложение ещё ни разу не запускалось. Это не страшно, но если после" + Write-Host " запуска пункта Developer не будет, запусти эту же команду ещё раз." + } } else { Warn "Приложение Claude не найдено, а без него настраивать нечего." + Write-Host " Смотрел классическую сборку в AppData\Local и пакеты Microsoft Store." Write-Host "" Write-Host " Поставь его руками, это одна минута:" Write-Host " 1. Открой https://claude.com/download и скачай версию для Windows." diff --git a/setup.sh b/setup.sh index bb0fa26..b848e52 100644 --- a/setup.sh +++ b/setup.sh @@ -26,13 +26,70 @@ case "$(uname -s)" in *) echo "Эта система не поддерживается (нужен Мак или Windows с Git Bash)."; exit 1 ;; esac +# ---------- Обнаружение приложения (Windows) ---------- +# Сборок две, и ведут они себя по-разному. +# · Классическая (Squirrel): %LOCALAPPDATA%\AnthropicClaude\app-<версия>\claude.exe, +# родные данные в %APPDATA%\Claude. +# · MSIX (Microsoft Store и установщик с claude.com): C:\Program Files\WindowsApps\ +# Claude_<версия>_x64__pzs8sxrjxfjjc\app\Claude.exe. Папка закрыта системой, exe +# оттуда напрямую не запускается, приложение поднимают по идентификатору пакета, +# а родные данные Windows подменяет на +# %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude. +# Сторонний режим у ОБЕИХ сборок живёт в %LOCALAPPDATA%\Claude-3p: так прописано +# в коде приложения, а в манифесте MSIX эта папка исключена из подмены. +WIN_KIND=""; WIN_AUMID=""; WIN_PFN=""; WIN_EXE=""; WIN_VER="" +WIN_PROBE=' +$ErrorActionPreference = "SilentlyContinue" +$exe = @("$env:LOCALAPPDATA\AnthropicClaude", "$env:LOCALAPPDATA\Programs") | Where-Object { Test-Path $_ } | + ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1 +$pkg = $null +try { + if (-not (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) -and $PSVersionTable.PSVersion.Major -ge 7) { Import-Module Appx -UseWindowsPowerShell -ErrorAction SilentlyContinue } + $all = @(Get-AppxPackage -Name "*Claude*" | Where-Object { $_.Name -eq "Claude" -or $_.Name -like "*ClaudeDesktop*" -or $_.Publisher -like "*Anthropic*" }) + $pkg = $all | Where-Object { $_.Name -eq "Claude" } | Select-Object -First 1 + if (-not $pkg) { $pkg = $all | Select-Object -First 1 } +} catch { } +$pfn = ""; $aumid = ""; $ver = "" +if ($pkg) { + $pfn = "$($pkg.PackageFamilyName)"; $ver = "$($pkg.Version)"; $id = "Claude" + try { $ids = @((Get-AppxPackageManifest $pkg).Package.Applications.Application.Id); if ($ids.Count -gt 0 -and $ids[0]) { $id = "$($ids[0])" } } catch { } + $aumid = "$pfn!$id" +} else { + $d = Get-ChildItem (Join-Path $env:LOCALAPPDATA "Packages") -Directory -Filter "Claude_*" | Select-Object -First 1 + if ($d) { $pfn = $d.Name; $aumid = "$pfn!Claude" } +} +$kind = "" +if ($pfn) { $launched = Test-Path (Join-Path $env:LOCALAPPDATA "Packages\$pfn\LocalCache\Roaming\Claude"); if ($launched -or -not $exe) { $kind = "msix" } } +if (-not $kind -and $exe) { $kind = "classic" } +$exePath = ""; if ($exe) { $exePath = $exe.FullName } +Write-Output "$kind|$aumid|$pfn|$exePath|$ver" +' +win_probe() { + local out + out="$(powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$WIN_PROBE" 2>/dev/null | tr -d '\r' | tail -n 1)" || out="" + IFS='|' read -r WIN_KIND WIN_AUMID WIN_PFN WIN_EXE WIN_VER </dev/null 2>&1; } # Developer не будет. Поэтому пишем в ОБЕ папки. write_dev_flag() { local d - for d in "$C1P" "$C3P"; do + for d in "${P1_DIRS[@]}" "$C3P"; do mkdir -p "$d" printf '{"allowDevTools": true}\n' > "$d/developer_settings.json" done - ok "Режим разработчика включён в обеих папках приложения." + ok "Режим разработчика включён во всех папках приложения." } # ---------- deploymentMode ---------- @@ -107,13 +164,14 @@ PY # Windows (он по умолчанию сохраняет UTF-8 именно так) и из старых скриптов. repair_json_bom() { [ -n "$PY" ] || return 0 - "$PY" - "$LIB" "$C1P" "$C3P" <<'PY' + "$PY" - "$LIB" "${P1_DIRS[@]}" "$C3P" <<'PY' import os, sys -lib, c1p, c3p = sys.argv[1:4] +lib = sys.argv[1] +dirs = sys.argv[2:] targets = [] if os.path.isdir(lib): targets += [os.path.join(lib, f) for f in os.listdir(lib) if f.endswith(".json")] -for d in (c1p, c3p): +for d in dirs: for n in ("developer_settings.json", "claude_desktop_config.json"): p = os.path.join(d, n) if os.path.isfile(p): targets.append(p) @@ -225,7 +283,7 @@ PY # ВАЖНО: не путать с соседним пунктом Reset App Data, тот сносит папку целиком. clear_caches() { local d - for d in "$C1P" "$C3P"; do + for d in "${P1_DIRS[@]}" "$C3P"; do [ -d "$d" ] || continue rm -rf "$d/Cache" "$d/Code Cache" "$d/GPUCache" \ "$d/DawnGraphiteCache" "$d/DawnWebGPUCache" "$d/Service Worker" 2>/dev/null || true @@ -283,10 +341,33 @@ restart_claude() { Get-Process -Name "Claude*" -ErrorAction SilentlyContinue | Stop-Process -Force Start-Sleep 3' 2>/dev/null || true clear_caches - powershell.exe -NoProfile -Command ' - $exe=@("$env:LOCALAPPDATA\AnthropicClaude","$env:LOCALAPPDATA\Programs") | ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1 - if($exe){ Start-Process $exe.FullName } else { exit 1 } - ' || { warn "Не смог перезапустить Claude, закрой и открой его вручную."; return 0; } + if [ "$WIN_KIND" = "msix" ]; then + # Пакетное приложение запускают по идентификатору, а не по пути к exe. + AK_AUMID="$WIN_AUMID" powershell.exe -NoProfile -Command ' + $t = "shell:AppsFolder\" + $env:AK_AUMID + try { Start-Process $t -ErrorAction Stop; exit 0 } catch { } + try { Start-Process explorer.exe $t -ErrorAction Stop; exit 0 } catch { } + $a = Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\Claude.exe" + if (Test-Path $a) { try { Start-Process $a -ErrorAction Stop; exit 0 } catch { } } + try { Start-Process "claude://" -ErrorAction Stop; exit 0 } catch { } + exit 1 + ' || { warn "Не смог запустить Claude, открой его вручную из меню Пуск."; return 0; } + elif [ -n "$WIN_EXE" ]; then + AK_EXE="$WIN_EXE" powershell.exe -NoProfile -Command 'Start-Process $env:AK_EXE' \ + || { warn "Не смог перезапустить Claude, закрой и открой его вручную."; return 0; } + else + warn "Не нашёл приложение Claude, открой его вручную."; return 0 + fi + # Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя. + j=0 + while [ "$j" -lt 12 ]; do + if powershell.exe -NoProfile -Command 'if (Get-Process -Name "Claude*" -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }' 2>/dev/null; then break; fi + sleep 1; j=$((j+1)) + done + if [ "$j" -ge 12 ]; then + warn "Claude не поднялся автоматически. Конфиги на месте, открой приложение вручную." + return 0 + fi fi ok "Claude перезапущен." } @@ -339,11 +420,17 @@ fi say "[1/4] Ищу приложение Claude..." CLAUDE_OK=false if [ "$OS" = "mac" ] && [ -d "/Applications/Claude.app" ]; then CLAUDE_OK=true; fi -if [ "$OS" = "win" ] && find "$LOCALAPPDATA/AnthropicClaude" "$LOCALAPPDATA/Programs" -maxdepth 3 -iname "claude.exe" 2>/dev/null | grep -q .; then CLAUDE_OK=true; fi +if [ "$OS" = "win" ] && [ -n "$WIN_KIND" ]; then CLAUDE_OK=true; fi if [ "$CLAUDE_OK" = "true" ]; then - ok "Claude на месте." + case "$WIN_KIND" in + msix) ok "Claude на месте: сборка MSIX (Microsoft Store или установщик с claude.com)${WIN_VER:+, версия $WIN_VER}." ;; + classic) ok "Claude на месте: классическая сборка." ;; + *) ok "Claude на месте." ;; + esac + ok "Конфиги стороннего режима: $C3P" else warn "Приложение Claude не найдено, а без него настраивать нечего." + [ "$OS" = "win" ] && echo " Смотрел классическую сборку в AppData\\Local и пакеты Microsoft Store." echo echo " Поставь его руками, это одна минута:" echo " 1. Открой https://claude.com/download и скачай версию для своей системы."