refactor: перенос файлов в claude/ + мульти-продуктовая структура
- Все claude-файлы перенесены в claude/ (uclaude_updater.py, скрипты, config) - claude/README.md с инструкцией для Claude Code - Корневой README — общий для всех продуктов (claude, codex, gemini, qwen, antigravity) - Node.js v24.13+ автоустановка через nodesource - Sparse checkout: клиент скачивает только latest версию cli.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
61
claude/README.md
Normal file
61
claude/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Claude Code — Patched CLI
|
||||
|
||||
Patched Claude Code CLI for use with custom API endpoints. 15 patches applied.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js v24.13+ ([nodejs.org](https://nodejs.org/)) — updater will auto-install if missing or outdated
|
||||
- Python 3
|
||||
- Git
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Install Claude Code
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# One-line install (from repo root):
|
||||
sudo bash claude/uclaude_update.sh --force
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
```bash
|
||||
sudo bash claude/uclaude_update.sh
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```bash
|
||||
sudo bash claude/uclaude_update.sh --check # check for updates
|
||||
sudo bash claude/uclaude_update.sh --force # force reinstall
|
||||
sudo bash claude/uclaude_update.sh --settings-only # only patch settings
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
```cmd
|
||||
claude\uclaude_update.bat
|
||||
```
|
||||
|
||||
## What's Patched
|
||||
|
||||
- Custom API endpoint (base URL, auth token)
|
||||
- Custom model picker (configurable model list)
|
||||
- Auth/OAuth bypass for custom endpoints
|
||||
- Telemetry disabled (Datadog, Segment)
|
||||
- Permission prompts auto-accepted
|
||||
- Root/sudo check removed
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `uclaude_updater.py` | Main updater — version check, cli.js install, settings patch |
|
||||
| `uclaude_update.sh` | Linux/macOS wrapper |
|
||||
| `uclaude_update.bat` | Windows CMD wrapper |
|
||||
| `uclaude_update.ps1` | Windows PowerShell wrapper |
|
||||
| `uclaude_install.sh` | One-line installer (curl-friendly) |
|
||||
| `patcher.config.json` | API endpoint config (base_url, api_key, models) |
|
||||
| `releases/index.json` | Version index |
|
||||
| `releases/v*/cli.js` | Patched cli.js per version |
|
||||
26
claude/patcher.config.json
Normal file
26
claude/patcher.config.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"base_url": "https://ai.37-187-136-86.sslip.io",
|
||||
"api_key": "ClauderAPI",
|
||||
"model": "claude-opus-4-6",
|
||||
"models": [
|
||||
"claude-opus-4-6",
|
||||
"claude-sonnet-4-6",
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.2-codex",
|
||||
"claude-opus-4-5-20251101",
|
||||
"claude-sonnet-4-5-20250929",
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview",
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-coder-flash",
|
||||
"qwen3.5-coder-plus",
|
||||
"glm-5",
|
||||
"glm-4.7"
|
||||
],
|
||||
"default_sonnet_model": "claude-sonnet-4-6",
|
||||
"default_opus_model": "claude-opus-4-6",
|
||||
"timeout_ms": 3000000,
|
||||
"theme": "dark",
|
||||
"complete_onboarding": true,
|
||||
"target_version": "2.1.50"
|
||||
}
|
||||
57
claude/uclaude_install.sh
Executable file
57
claude/uclaude_install.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
# UClaude — one-line installer
|
||||
# Usage: bash <(curl -s https://git.sensey24.ru/aibot777/unlimitedcoding/raw/branch/master/claude/uclaude_install.sh)
|
||||
set -e
|
||||
|
||||
REPO_URL="https://git.sensey24.ru/aibot777/unlimitedcoding.git"
|
||||
INSTALL_DIR="${UCLAUDE_DIR:-$HOME/unlimitedcoding}"
|
||||
|
||||
echo "=== UClaude Installer ==="
|
||||
echo " Install dir: $INSTALL_DIR"
|
||||
|
||||
# Check prerequisites
|
||||
command -v git >/dev/null 2>&1 || { echo "ERROR: git not found. Install git first."; exit 1; }
|
||||
command -v python3 >/dev/null 2>&1 || { echo "ERROR: python3 not found. Install Python 3 first."; exit 1; }
|
||||
# Node.js check — will be auto-installed by updater if needed
|
||||
command -v node >/dev/null 2>&1 || echo "WARNING: node not found. Updater will attempt auto-install."
|
||||
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
echo " Already cloned, updating..."
|
||||
cd "$INSTALL_DIR"
|
||||
git fetch --depth 1 origin master 2>/dev/null
|
||||
git reset --hard origin/master 2>/dev/null
|
||||
else
|
||||
echo " Cloning (shallow, sparse — only latest version)..."
|
||||
|
||||
# Shallow clone without checkout
|
||||
git clone --depth 1 --no-checkout "$REPO_URL" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Enable sparse checkout: root + claude core files + index.json (first pass)
|
||||
git sparse-checkout init --no-cone
|
||||
git sparse-checkout set '/*' 'claude/*' '!claude/releases/v*' 'claude/releases/index.json'
|
||||
git checkout 2>/dev/null
|
||||
|
||||
# Read latest version from index.json and add only that release dir
|
||||
if [ -f claude/releases/index.json ]; then
|
||||
VER=$(python3 -c "import json; print(json.load(open('claude/releases/index.json'))['latest'])")
|
||||
echo " Latest version: v${VER}"
|
||||
git sparse-checkout add "claude/releases/v${VER}"
|
||||
git checkout 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Running updater..."
|
||||
|
||||
# Run updater (needs root for cli.js replacement)
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
python3 claude/uclaude_updater.py --force
|
||||
else
|
||||
echo " Root privileges required to install cli.js."
|
||||
sudo python3 claude/uclaude_updater.py --force
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Installation complete ==="
|
||||
echo " To update later: cd $INSTALL_DIR && sudo bash claude/uclaude_update.sh"
|
||||
18
claude/uclaude_update.bat
Normal file
18
claude/uclaude_update.bat
Normal file
@@ -0,0 +1,18 @@
|
||||
@echo off
|
||||
REM UClaude Updater — automatic Claude Code patch updater
|
||||
REM Usage: claude\uclaude_update.bat [--check] [--force] [--settings-only]
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
cd /d "%SCRIPT_DIR%\.."
|
||||
|
||||
REM Pull latest artifacts
|
||||
git pull --quiet 2>nul
|
||||
|
||||
REM Run updater
|
||||
python claude\uclaude_updater.py %*
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo If you see permission errors, run this script as Administrator.
|
||||
pause
|
||||
)
|
||||
21
claude/uclaude_update.ps1
Normal file
21
claude/uclaude_update.ps1
Normal file
@@ -0,0 +1,21 @@
|
||||
# UClaude Updater — automatic Claude Code patch updater
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File claude\uclaude_update.ps1 [--check] [--force] [--settings-only]
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = Split-Path -Parent $ScriptDir
|
||||
Set-Location $RepoRoot
|
||||
|
||||
# Pull latest artifacts
|
||||
git pull --quiet 2>$null
|
||||
|
||||
# Check admin
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host "Re-running as Administrator..." -ForegroundColor Yellow
|
||||
Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" $args" -Verb RunAs
|
||||
exit
|
||||
}
|
||||
|
||||
# Run updater
|
||||
& python claude\uclaude_updater.py @args
|
||||
28
claude/uclaude_update.sh
Executable file
28
claude/uclaude_update.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# UClaude Updater — automatic Claude Code patch updater
|
||||
# Usage: sudo bash claude/uclaude_update.sh [--check] [--force] [--settings-only]
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Fetch latest commit (shallow — no history)
|
||||
git fetch --depth 1 origin master 2>/dev/null && git reset --hard origin/master 2>/dev/null || git pull --quiet 2>/dev/null || true
|
||||
|
||||
# Update sparse checkout to include latest version directory
|
||||
if [ -f claude/releases/index.json ] && git config core.sparseCheckout 2>/dev/null | grep -q true; then
|
||||
VER=$(python3 -c "import json; print(json.load(open('claude/releases/index.json'))['latest'])" 2>/dev/null)
|
||||
if [ -n "$VER" ]; then
|
||||
git sparse-checkout add "claude/releases/v${VER}" 2>/dev/null
|
||||
git checkout 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run updater
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
python3 claude/uclaude_updater.py "$@"
|
||||
else
|
||||
echo "Root privileges required. Re-running with sudo..."
|
||||
sudo python3 claude/uclaude_updater.py "$@"
|
||||
fi
|
||||
693
claude/uclaude_updater.py
Normal file
693
claude/uclaude_updater.py
Normal file
@@ -0,0 +1,693 @@
|
||||
#!/usr/bin/env python3
|
||||
"""UClaude Updater — automatic Claude Code patch updater.
|
||||
|
||||
Usage:
|
||||
sudo python3 uclaude_updater.py # Check and update if new version available
|
||||
sudo python3 uclaude_updater.py --check # Only check, don't install
|
||||
sudo python3 uclaude_updater.py --force # Update even if version matches
|
||||
sudo python3 uclaude_updater.py --settings-only # Only patch settings, don't touch cli.js
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
# ============================================================
|
||||
# Platform detection
|
||||
# ============================================================
|
||||
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
IS_MACOS = sys.platform == "darwin"
|
||||
|
||||
try:
|
||||
import pwd
|
||||
HAS_PWD = True
|
||||
except ImportError:
|
||||
HAS_PWD = False
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # claude/
|
||||
REPO_ROOT = os.path.dirname(SCRIPT_DIR) # unlimitedcoding/
|
||||
|
||||
# ANSI colors
|
||||
G = "\033[92m" # green
|
||||
Y = "\033[93m" # yellow
|
||||
R = "\033[91m" # red
|
||||
W = "\033[97m" # white/bold
|
||||
D = "\033[0m" # default
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers
|
||||
# ============================================================
|
||||
|
||||
def is_admin():
|
||||
if IS_WINDOWS:
|
||||
try:
|
||||
import ctypes
|
||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
except Exception:
|
||||
return False
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def safe_chown(path, uid, gid):
|
||||
if not IS_WINDOWS and is_admin():
|
||||
os.chown(path, uid, gid)
|
||||
|
||||
|
||||
def safe_chmod(path, mode):
|
||||
if not IS_WINDOWS:
|
||||
os.chmod(path, mode)
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Node.js check and auto-install
|
||||
# ============================================================
|
||||
|
||||
MIN_NODE_VERSION = (24, 13, 0)
|
||||
|
||||
|
||||
def get_node_version():
|
||||
"""Get installed Node.js version as tuple, or None."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["node", "--version"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", result.stdout.strip())
|
||||
if m:
|
||||
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def install_node():
|
||||
"""Auto-install Node.js v24+ using the official nodesource setup."""
|
||||
print(f" {Y}Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+ required.{D}")
|
||||
|
||||
if IS_WINDOWS:
|
||||
print(f" {R}Please install Node.js manually: https://nodejs.org/{D}")
|
||||
return False
|
||||
|
||||
print(f" Installing Node.js v24 via nodesource...")
|
||||
try:
|
||||
# Use nodesource setup script
|
||||
result = subprocess.run(
|
||||
["bash", "-c", "curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt-get install -y nodejs"],
|
||||
timeout=120, capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
ver = get_node_version()
|
||||
if ver:
|
||||
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
|
||||
return True
|
||||
|
||||
# Fallback: try nvm-like approach or direct download
|
||||
eprint(f" {R}Auto-install failed. Install manually: https://nodejs.org/{D}")
|
||||
if result.stderr:
|
||||
eprint(f" {result.stderr.strip()[:200]}")
|
||||
return False
|
||||
except Exception as e:
|
||||
eprint(f" {R}Auto-install error: {e}{D}")
|
||||
return False
|
||||
|
||||
|
||||
def ensure_node():
|
||||
"""Check Node.js version, auto-install/update if needed. Returns True if OK."""
|
||||
ver = get_node_version()
|
||||
|
||||
if ver is None:
|
||||
print(f" {Y}Node.js not found.{D}")
|
||||
if is_admin():
|
||||
return install_node()
|
||||
else:
|
||||
eprint(f" {R}Install Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+: https://nodejs.org/{D}")
|
||||
return False
|
||||
|
||||
if ver < MIN_NODE_VERSION:
|
||||
print(f" {Y}Node.js v{'.'.join(map(str, ver))} found, need v{'.'.join(map(str, MIN_NODE_VERSION))}+{D}")
|
||||
if is_admin():
|
||||
return install_node()
|
||||
else:
|
||||
eprint(f" {R}Update Node.js: https://nodejs.org/{D}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Version detection
|
||||
# ============================================================
|
||||
|
||||
def find_cli_js():
|
||||
"""Find installed Claude Code cli.js path."""
|
||||
candidates = []
|
||||
if IS_WINDOWS:
|
||||
for env_key in ("APPDATA", "LOCALAPPDATA", "PROGRAMFILES"):
|
||||
base = os.environ.get(env_key, "")
|
||||
if base:
|
||||
candidates.append(os.path.join(base, "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"))
|
||||
else:
|
||||
candidates = [
|
||||
"/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js",
|
||||
"/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js",
|
||||
"/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js",
|
||||
]
|
||||
|
||||
for path in candidates:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def get_installed_version():
|
||||
"""Get currently installed Claude Code version."""
|
||||
cli_js = find_cli_js()
|
||||
if not cli_js:
|
||||
return None, None
|
||||
|
||||
# Try package.json first (same directory as cli.js)
|
||||
pkg_json = os.path.join(os.path.dirname(cli_js), "package.json")
|
||||
if os.path.isfile(pkg_json):
|
||||
try:
|
||||
with open(pkg_json, "r") as f:
|
||||
data = json.load(f)
|
||||
return data.get("version"), cli_js
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: claude --version
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
m = re.search(r"(\d+\.\d+\.\d+)", result.stdout)
|
||||
if m:
|
||||
return m.group(1), cli_js
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None, cli_js
|
||||
|
||||
|
||||
def get_latest_version():
|
||||
"""Read latest version from local index.json."""
|
||||
index_path = os.path.join(SCRIPT_DIR, "releases", "index.json")
|
||||
if not os.path.isfile(index_path):
|
||||
return None
|
||||
try:
|
||||
with open(index_path, "r") as f:
|
||||
data = json.load(f)
|
||||
return data.get("latest")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def ver_tuple(v):
|
||||
"""Parse version string to tuple for comparison."""
|
||||
m = re.match(r"(\d+)\.(\d+)\.(\d+)", v or "")
|
||||
return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else (0, 0, 0)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Git pull
|
||||
# ============================================================
|
||||
|
||||
def git_pull():
|
||||
"""Pull latest changes from remote (shallow fetch for minimal download)."""
|
||||
try:
|
||||
# Shallow fetch + reset — downloads only latest commit, not full history
|
||||
result = subprocess.run(
|
||||
["git", "fetch", "--depth", "1", "origin", "master"],
|
||||
cwd=REPO_ROOT, capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Fallback to regular pull
|
||||
result = subprocess.run(
|
||||
["git", "pull", "--quiet"],
|
||||
cwd=REPO_ROOT, capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
eprint(f" {Y}git pull warning: {result.stderr.strip()}{D}")
|
||||
return True
|
||||
|
||||
# Reset to fetched state
|
||||
subprocess.run(
|
||||
["git", "reset", "--hard", "origin/master"],
|
||||
cwd=REPO_ROOT, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
|
||||
# Setup sparse checkout to download only latest version's cli.js
|
||||
_setup_sparse_checkout()
|
||||
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
eprint(f" {Y}git fetch timed out{D}")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
eprint(f" {Y}git not found, skipping pull{D}")
|
||||
return True
|
||||
|
||||
|
||||
def _setup_sparse_checkout():
|
||||
"""Configure sparse checkout to only include root files + latest release.
|
||||
|
||||
This avoids downloading cli.js for ALL versions (each ~12MB).
|
||||
Only the latest version's cli.js is checked out.
|
||||
"""
|
||||
index_path = os.path.join(SCRIPT_DIR, "releases", "index.json")
|
||||
if not os.path.isfile(index_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(index_path, "r") as f:
|
||||
latest = json.load(f).get("latest")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if not latest:
|
||||
return
|
||||
|
||||
# Enable sparse checkout
|
||||
subprocess.run(
|
||||
["git", "config", "core.sparseCheckout", "true"],
|
||||
cwd=REPO_ROOT, capture_output=True,
|
||||
)
|
||||
|
||||
sparse_file = os.path.join(REPO_ROOT, ".git", "info", "sparse-checkout")
|
||||
os.makedirs(os.path.dirname(sparse_file), exist_ok=True)
|
||||
|
||||
patterns = [
|
||||
"/*", # root files (updater, config, README)
|
||||
"/claude/releases/index.json", # version index
|
||||
f"/claude/releases/v{latest}/", # latest release (cli.js + changelog + install)
|
||||
]
|
||||
|
||||
with open(sparse_file, "w") as f:
|
||||
f.write("\n".join(patterns) + "\n")
|
||||
|
||||
# Apply sparse checkout
|
||||
subprocess.run(
|
||||
["git", "checkout", "HEAD", "--", "."],
|
||||
cwd=REPO_ROOT, capture_output=True, timeout=30,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CLI.js installation
|
||||
# ============================================================
|
||||
|
||||
def install_cli_js(version, cli_js_path):
|
||||
"""Install patched cli.js for the given version."""
|
||||
release_cli = os.path.join(SCRIPT_DIR, "releases", f"v{version}", "cli.js")
|
||||
if not os.path.isfile(release_cli):
|
||||
eprint(f" {R}Release cli.js not found: {release_cli}{D}")
|
||||
return False
|
||||
|
||||
# Backup
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
backup_path = f"{cli_js_path}.bak.{timestamp}"
|
||||
|
||||
try:
|
||||
shutil.copy2(cli_js_path, backup_path)
|
||||
shutil.copy2(release_cli, cli_js_path)
|
||||
|
||||
# Syntax check
|
||||
result = subprocess.run(
|
||||
["node", "--check", cli_js_path],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
eprint(f" {R}Syntax check FAILED, rolling back...{D}")
|
||||
shutil.copy2(backup_path, cli_js_path)
|
||||
return False
|
||||
|
||||
print(f" {G}cli.js installed successfully{D}")
|
||||
print(f" Backup: {backup_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
eprint(f" {R}Installation error: {e}{D}")
|
||||
if os.path.isfile(backup_path):
|
||||
shutil.copy2(backup_path, cli_js_path)
|
||||
eprint(f" Rolled back to backup")
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Settings patching
|
||||
# ============================================================
|
||||
|
||||
def load_config():
|
||||
"""Load patcher.config.json from repo root."""
|
||||
config_path = os.path.join(SCRIPT_DIR, "patcher.config.json")
|
||||
if not os.path.isfile(config_path):
|
||||
eprint(f" {R}patcher.config.json not found{D}")
|
||||
return None
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def ensure_dir(path, uid, gid):
|
||||
os.makedirs(path, mode=0o700, exist_ok=True)
|
||||
safe_chmod(path, 0o700)
|
||||
safe_chown(path, uid, gid)
|
||||
|
||||
|
||||
def read_settings(path):
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
data = json.load(handle)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||
backup_path = f"{path}.bak.{timestamp}"
|
||||
os.rename(path, backup_path)
|
||||
print(f" Backed up invalid settings to {backup_path}")
|
||||
return {}
|
||||
|
||||
|
||||
def write_settings(path, data, uid, gid):
|
||||
tmp_path = f"{path}.tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, indent=2, ensure_ascii=True)
|
||||
handle.write("\n")
|
||||
os.replace(tmp_path, path)
|
||||
safe_chmod(path, 0o600)
|
||||
safe_chown(path, uid, gid)
|
||||
|
||||
|
||||
def patch_user(user_home, user_name, uid, gid, config):
|
||||
"""Patch settings for a single user."""
|
||||
settings_dir = os.path.join(user_home, ".claude")
|
||||
settings_path = os.path.join(settings_dir, "settings.json")
|
||||
|
||||
ensure_dir(settings_dir, uid, gid)
|
||||
data = read_settings(settings_path)
|
||||
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict):
|
||||
env = {}
|
||||
|
||||
env["ANTHROPIC_AUTH_TOKEN"] = config["api_key"]
|
||||
env["ANTHROPIC_BASE_URL"] = config["base_url"]
|
||||
env.pop("ANTHROPIC_MODEL", None)
|
||||
if "timeout_ms" in config and config["timeout_ms"] is not None:
|
||||
env["API_TIMEOUT_MS"] = str(config["timeout_ms"])
|
||||
if config.get("models"):
|
||||
env["CLAUDE_CUSTOM_MODELS"] = ",".join(config["models"])
|
||||
if config.get("default_sonnet_model"):
|
||||
env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = config["default_sonnet_model"]
|
||||
if config.get("default_opus_model"):
|
||||
env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = config["default_opus_model"]
|
||||
env["DISABLE_AUTOUPDATER"] = "1"
|
||||
env["DISABLE_TELEMETRY"] = "1"
|
||||
env["DISABLE_ERROR_REPORTING"] = "1"
|
||||
env["CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"] = "1"
|
||||
env["CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY"] = "1"
|
||||
env["CLAUDE_CODE_EFFORT_LEVEL"] = "medium"
|
||||
|
||||
data["env"] = env
|
||||
data["model"] = config["model"]
|
||||
data.pop("models", None)
|
||||
data.pop("availableModels", None)
|
||||
data["effortLevel"] = "medium"
|
||||
|
||||
theme = config.get("theme")
|
||||
if theme:
|
||||
data["theme"] = theme
|
||||
|
||||
if config.get("complete_onboarding"):
|
||||
data["hasCompletedOnboarding"] = True
|
||||
version = config.get("target_version") or "2.1.50"
|
||||
data["lastOnboardingVersion"] = {
|
||||
"ISSUES_EXPLAINER": "report the issue at https://github.com/anthropics/claude-code/issues",
|
||||
"PACKAGE_URL": "@anthropic-ai/claude-code",
|
||||
"README_URL": "https://code.claude.com/docs/en/overview",
|
||||
"VERSION": version,
|
||||
}
|
||||
|
||||
perms = data.get("permissions")
|
||||
if not isinstance(perms, dict):
|
||||
perms = {}
|
||||
perms["defaultMode"] = "bypassPermissions"
|
||||
data["permissions"] = perms
|
||||
|
||||
write_settings(settings_path, data, uid, gid)
|
||||
|
||||
# --- settings.local.json ---
|
||||
local_path = os.path.join(settings_dir, "settings.local.json")
|
||||
local_data = read_settings(local_path)
|
||||
local_perms = local_data.get("permissions")
|
||||
if not isinstance(local_perms, dict):
|
||||
local_perms = {}
|
||||
local_perms["defaultMode"] = "bypassPermissions"
|
||||
|
||||
base_allow = [
|
||||
"Bash", "Edit", "Write", "Read", "Glob", "Grep",
|
||||
"NotebookEdit", "WebFetch", "WebSearch",
|
||||
"mcp__memory-bank__list_projects",
|
||||
"mcp__memory-bank__list_project_files",
|
||||
"mcp__memory-bank__memory_bank_read",
|
||||
"mcp__memory-bank__memory_bank_write",
|
||||
"mcp__memory-bank__memory_bank_update",
|
||||
]
|
||||
bash_cmds = [
|
||||
"git", "python3", "python", "node", "npm", "npx", "bash", "sh",
|
||||
"ls", "cat", "wc", "ln", "cp", "mv", "rm", "mkdir", "chmod",
|
||||
"chown", "tail", "head", "touch", "tee", "echo", "printf",
|
||||
"date", "sleep", "sort", "uniq", "tr", "cut", "xargs", "find",
|
||||
"grep", "sed", "awk", "jq", "diff", "curl", "wget", "tar",
|
||||
"gzip", "gunzip", "unzip", "sha256sum", "md5sum", "du", "df",
|
||||
"free", "ps", "kill", "whoami", "hostname", "uname", "go",
|
||||
"make", "systemctl", "journalctl", "docker", "docker-compose",
|
||||
"ssh", "scp", "rsync", "pip", "pip3", "gh", "claude", "entire",
|
||||
"cd",
|
||||
]
|
||||
for cmd in bash_cmds:
|
||||
base_allow.append(f"Bash({cmd}:*)")
|
||||
|
||||
existing_allow = local_perms.get("allow", [])
|
||||
existing_set = set(existing_allow)
|
||||
for item in base_allow:
|
||||
if item not in existing_set:
|
||||
existing_allow.append(item)
|
||||
local_perms["allow"] = existing_allow
|
||||
if "deny" not in local_perms:
|
||||
local_perms["deny"] = []
|
||||
if "ask" not in local_perms:
|
||||
local_perms["ask"] = []
|
||||
|
||||
local_data["permissions"] = local_perms
|
||||
write_settings(local_path, local_data, uid, gid)
|
||||
|
||||
return settings_path
|
||||
|
||||
|
||||
def discover_users():
|
||||
"""Find all users with home directories."""
|
||||
if IS_WINDOWS or not HAS_PWD:
|
||||
home = os.path.expanduser("~")
|
||||
try:
|
||||
username = os.getlogin()
|
||||
except Exception:
|
||||
username = os.environ.get("USER", "user")
|
||||
# Return as simple namespace
|
||||
class User:
|
||||
def __init__(self, name, home, uid, gid):
|
||||
self.name = name
|
||||
self.home = home
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
return [User(username, home, os.getuid() if not IS_WINDOWS else 0, os.getgid() if not IS_WINDOWS else 0)]
|
||||
|
||||
users = []
|
||||
for entry in pwd.getpwall():
|
||||
if entry.pw_uid < 500 and entry.pw_name != "root":
|
||||
continue
|
||||
if entry.pw_shell in ("/usr/sbin/nologin", "/bin/false", "/sbin/nologin"):
|
||||
continue
|
||||
if not os.path.isdir(entry.pw_dir):
|
||||
continue
|
||||
class User:
|
||||
def __init__(self, name, home, uid, gid):
|
||||
self.name = name
|
||||
self.home = home
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
users.append(User(entry.pw_name, entry.pw_dir, entry.pw_uid, entry.pw_gid))
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def patch_all_users(config):
|
||||
"""Patch settings for all discovered users."""
|
||||
users = discover_users()
|
||||
if not users:
|
||||
eprint(f" {Y}No users found{D}")
|
||||
return
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
path = patch_user(user.home, user.name, user.uid, user.gid, config)
|
||||
print(f" {G}Patched {user.name}{D}: {path}")
|
||||
except Exception as e:
|
||||
eprint(f" {R}Failed to patch {user.name}: {e}{D}")
|
||||
|
||||
# Windows extras
|
||||
if IS_WINDOWS:
|
||||
_set_user_env_windows(config)
|
||||
|
||||
|
||||
def _set_user_env_windows(config):
|
||||
"""Set user-level environment variables via setx (Windows only)."""
|
||||
env_vars = {
|
||||
"ANTHROPIC_BASE_URL": config["base_url"],
|
||||
"ANTHROPIC_AUTH_TOKEN": config["api_key"],
|
||||
"DISABLE_TELEMETRY": "1",
|
||||
"DISABLE_ERROR_REPORTING": "1",
|
||||
"DISABLE_AUTOUPDATER": "1",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
||||
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY": "1",
|
||||
}
|
||||
if config.get("models"):
|
||||
env_vars["CLAUDE_CUSTOM_MODELS"] = ",".join(config["models"])
|
||||
if config.get("timeout_ms"):
|
||||
env_vars["API_TIMEOUT_MS"] = str(config["timeout_ms"])
|
||||
|
||||
for key, val in env_vars.items():
|
||||
try:
|
||||
subprocess.run(["setx", key, val], capture_output=True, check=True)
|
||||
os.environ[key] = val
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main commands
|
||||
# ============================================================
|
||||
|
||||
def cmd_check():
|
||||
"""Check for updates without installing."""
|
||||
installed, cli_js = get_installed_version()
|
||||
latest = get_latest_version()
|
||||
|
||||
print(f"\n{W}=== UClaude Update Check ==={D}")
|
||||
print(f" Installed: {installed or 'not found'}")
|
||||
print(f" Latest: {latest or 'unknown'}")
|
||||
|
||||
if not installed:
|
||||
print(f" {Y}Claude Code not found. Install it first: npm install -g @anthropic-ai/claude-code{D}")
|
||||
return 1
|
||||
|
||||
if not latest:
|
||||
print(f" {R}Cannot determine latest version. Run 'git pull' first.{D}")
|
||||
return 1
|
||||
|
||||
if ver_tuple(latest) > ver_tuple(installed):
|
||||
print(f" {Y}Update available: {installed} → {latest}{D}")
|
||||
return 0
|
||||
else:
|
||||
print(f" {G}Up to date.{D}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_update(force=False, settings_only=False):
|
||||
"""Full update: git pull → install cli.js → patch settings."""
|
||||
print(f"\n{W}=== UClaude Updater ==={D}")
|
||||
|
||||
# Check Node.js version
|
||||
if not ensure_node():
|
||||
return 1
|
||||
|
||||
installed, cli_js = get_installed_version()
|
||||
|
||||
# Git pull to get latest artifacts
|
||||
print(f"\n Pulling latest updates...")
|
||||
git_pull()
|
||||
|
||||
latest = get_latest_version()
|
||||
print(f" Installed: {installed or 'not found'}")
|
||||
print(f" Latest: {latest or 'unknown'}")
|
||||
|
||||
if not latest:
|
||||
eprint(f" {R}Cannot determine latest version.{D}")
|
||||
return 1
|
||||
|
||||
needs_update = force or not installed or ver_tuple(latest) > ver_tuple(installed)
|
||||
|
||||
if not needs_update and not settings_only:
|
||||
print(f"\n {G}Already up to date.{D}")
|
||||
# Still patch settings in case config changed
|
||||
config = load_config()
|
||||
if config:
|
||||
print(f"\n{W}--- Patching settings ---{D}")
|
||||
patch_all_users(config)
|
||||
return 0
|
||||
|
||||
# Install cli.js
|
||||
if not settings_only:
|
||||
if not cli_js:
|
||||
cli_js = find_cli_js()
|
||||
if not cli_js:
|
||||
eprint(f" {R}Claude Code cli.js not found. Install Claude Code first.{D}")
|
||||
return 1
|
||||
|
||||
if not is_admin():
|
||||
eprint(f" {R}Root/admin privileges required to update cli.js.{D}")
|
||||
eprint(f" Run with: sudo python3 {sys.argv[0]}")
|
||||
return 1
|
||||
|
||||
print(f"\n{W}--- Installing cli.js v{latest} ---{D}")
|
||||
ok = install_cli_js(latest, cli_js)
|
||||
if not ok:
|
||||
return 1
|
||||
|
||||
# Patch settings
|
||||
config = load_config()
|
||||
if config:
|
||||
print(f"\n{W}--- Patching settings ---{D}")
|
||||
patch_all_users(config)
|
||||
else:
|
||||
eprint(f" {Y}No config found, skipping settings patch{D}")
|
||||
|
||||
# Verify
|
||||
new_ver, _ = get_installed_version()
|
||||
print(f"\n{W}=== Done ==={D}")
|
||||
print(f" Version: {new_ver or 'unknown'}")
|
||||
print(f" {G}Update complete.{D}")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="UClaude Updater — automatic Claude Code patch updater.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument("--check", action="store_true", help="Only check for updates")
|
||||
parser.add_argument("--force", action="store_true", help="Force update even if version matches")
|
||||
parser.add_argument("--settings-only", action="store_true", help="Only patch settings, don't touch cli.js")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.check:
|
||||
return cmd_check()
|
||||
else:
|
||||
return cmd_update(force=args.force, settings_only=args.settings_only)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user