feat(qwen): add Qwen Code CLI — install, patcher, docs
- qwen/qwen_patcher.py: 12 patch targets (--settings-only for npm users) - qwen/qwen_config.json: proxy config with model list - qwen/README.md: install, usage, models, troubleshooting - README.md: add Qwen install section + update products table - README_ru.md: update products table Install: npm config set @qwen-code:registry https://npm.sensey24.ru/ && npm install -g @qwen-code/qwen-code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
32
README.md
32
README.md
@@ -11,7 +11,7 @@ Patched AI coding tools for use with custom API endpoints.
|
||||
| [claude/](claude/) | Claude Code | Active (v2.1.71) |
|
||||
| [codex/](codex/) | OpenAI Codex CLI | **Active (v0.111.0)** |
|
||||
| [gemini/](gemini/) | Gemini CLI | **Active (v0.32.1)** |
|
||||
| qwen/ | Qwen Code | Planned |
|
||||
| [qwen/](qwen/) | Qwen Code | **Active (v0.11.1)** |
|
||||
| antigravity/ | Antigravity | Planned |
|
||||
|
||||
## Quick Start
|
||||
@@ -182,6 +182,36 @@ sudo bash update-codex.sh && sudo python3 codex_patcher.py --apply
|
||||
|
||||
See [codex/README.md](codex/README.md) for details, troubleshooting, and configuration.
|
||||
|
||||
### Qwen Code — Install
|
||||
|
||||
**Step 1 — Install patched CLI:**
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
npm config set @qwen-code:registry https://npm.sensey24.ru/
|
||||
npm install -g @qwen-code/qwen-code
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
npm config set "@qwen-code:registry" "https://npm.sensey24.ru/"
|
||||
npm install -g @qwen-code/qwen-code
|
||||
```
|
||||
|
||||
> Node.js required. Install from https://nodejs.org/ if not present.
|
||||
|
||||
**Step 2 — Configure settings and environment:**
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://x-token:cadffcb0a6a3be728ac1ff619bb40c86588f6837@git.sensey24.ru/aibot777/unlimitedcoding.git
|
||||
cd unlimitedcoding/qwen
|
||||
python3 qwen_patcher.py --settings-only
|
||||
```
|
||||
|
||||
**Step 3 — Verify:** `qwen -p "Hello"`
|
||||
|
||||
See [qwen/README.md](qwen/README.md) for details, models, and troubleshooting.
|
||||
|
||||
### Manual install from release
|
||||
|
||||
Clone repo and run platform installer:
|
||||
|
||||
2
README_ru.md
Normal file → Executable file
2
README_ru.md
Normal file → Executable file
@@ -11,7 +11,7 @@
|
||||
| [claude/](claude/) | Claude Code | Активен (v2.1.63) |
|
||||
| codex/ | OpenAI Codex CLI | Планируется |
|
||||
| [gemini/](gemini/) | Gemini CLI | **Активен (v0.29.5)** |
|
||||
| qwen/ | Qwen Code | Планируется |
|
||||
| [qwen/](qwen/) | Qwen Code | **Активен (v0.11.1)** |
|
||||
| antigravity/ | Antigravity | Планируется |
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
99
qwen/README.md
Normal file
99
qwen/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Qwen Code Patcher
|
||||
|
||||
Patches [QwenCode CLI](https://github.com/QwenLM/qwen-code) (`@qwen-code/qwen-code`) to route all API requests through a custom AI proxy, disable telemetry, and auto-configure settings.
|
||||
|
||||
**[RU]** Патчер для QwenCode CLI — перенаправляет API запросы через пользовательский AI прокси, отключает телеметрию, автоматически настраивает окружение.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install from private registry
|
||||
npm config set @qwen-code:registry https://npm.sensey24.ru/
|
||||
npm install -g @qwen-code/qwen-code
|
||||
|
||||
# 2. Apply settings (env vars + settings.json)
|
||||
python3 qwen_patcher.py --settings-only
|
||||
|
||||
# 3. Verify
|
||||
qwen -p "Say hello"
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
```powershell
|
||||
npm config set "@qwen-code:registry" "https://npm.sensey24.ru/"
|
||||
npm install -g @qwen-code/qwen-code
|
||||
python3 qwen_patcher.py --settings-only
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
Same two commands — npm will pull the latest patched version:
|
||||
|
||||
```bash
|
||||
npm config set @qwen-code:registry https://npm.sensey24.ru/
|
||||
npm install -g @qwen-code/qwen-code
|
||||
```
|
||||
|
||||
## What Gets Patched
|
||||
|
||||
| # | Target | Description |
|
||||
|---|--------|-------------|
|
||||
| 1 | telemetry_flag | Force `getTelemetryEnabled()` -> false |
|
||||
| 2 | telemetry_log_prompts | Force `getTelemetryLogPromptsEnabled()` -> false |
|
||||
| 3 | telemetry_init_guard | Early return in `initializeTelemetry()` |
|
||||
| 4 | dashscope_base_url | `DEFAULT_DASHSCOPE_BASE_URL` -> proxy |
|
||||
| 5 | coding_plan_urls | `coding.dashscope.aliyuncs.com` -> proxy |
|
||||
| 6 | default_model | Validate `DEFAULT_QWEN_MODEL = "coder-model"` |
|
||||
| 7 | mainline_model | Validate `MAINLINE_CODER_MODEL = "qwen3.5-plus"` |
|
||||
| 8 | auto_update_registry | `registry.npmjs.org` -> private registry |
|
||||
| 9 | auto_update_command | Add `--registry` to update commands |
|
||||
| 10 | user_settings | Auth type=openai, telemetry=false, model |
|
||||
| 11 | trusted_folders | Trust /home, /root, /tmp |
|
||||
| 12 | system_env | OPENAI_API_KEY, OPENAI_BASE_URL, telemetry vars |
|
||||
|
||||
Targets 1-9 are pre-patched in the npm package. Targets 10-12 require running `qwen_patcher.py --settings-only`.
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `qwen3.5-plus` | Qwen 3.5 Plus — default |
|
||||
| `coder-model` | Direct OAuth model name |
|
||||
| `qwen3-coder-plus` | Qwen3 Coder Plus |
|
||||
| `qwen3-coder-flash` | Qwen3 Coder Flash (fast) |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Detection
|
||||
python3 qwen_patcher.py --detect
|
||||
|
||||
# Validation (GREEN/YELLOW/RED for each target)
|
||||
python3 qwen_patcher.py --validate
|
||||
|
||||
# Full patch (cli.js + settings + env)
|
||||
python3 qwen_patcher.py --apply
|
||||
|
||||
# Settings only (no cli.js modification)
|
||||
python3 qwen_patcher.py --settings-only
|
||||
|
||||
# Rollback cli.js from backup
|
||||
python3 qwen_patcher.py --rollback
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "model not supported" error
|
||||
|
||||
Make sure your proxy has `qwen3.5-plus` mapped to `coder-model` in the OAuth model alias config. The Qwen OAuth endpoint only accepts `coder-model` as the model name.
|
||||
|
||||
### CLI doesn't start after patching
|
||||
|
||||
If you applied `--apply` and the CLI fails to start, run `--rollback` to restore from backup, then use the pre-patched npm package instead:
|
||||
|
||||
```bash
|
||||
python3 qwen_patcher.py --rollback
|
||||
npm install -g @qwen-code/qwen-code # Re-installs pre-patched version
|
||||
python3 qwen_patcher.py --settings-only
|
||||
```
|
||||
15
qwen/qwen_config.json
Normal file
15
qwen/qwen_config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"base_url": "https://ai.37-187-136-86.sslip.io",
|
||||
"api_key": "ClauderAPI",
|
||||
"default_model": "qwen3.5-plus",
|
||||
"models": [
|
||||
"qwen3.5-plus",
|
||||
"coder-model",
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-coder-flash"
|
||||
],
|
||||
"target_version": "0.11.1",
|
||||
"telemetry_enabled": false,
|
||||
"npm_package": "@qwen-code/qwen-code",
|
||||
"npm_registry": "https://npm.sensey24.ru"
|
||||
}
|
||||
492
qwen/qwen_patcher.py
Executable file
492
qwen/qwen_patcher.py
Executable file
@@ -0,0 +1,492 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Qwen Code Patcher — patches QwenCode CLI to route through custom AI proxy.
|
||||
|
||||
Targets:
|
||||
1. telemetry_flag — force getTelemetryEnabled() → false
|
||||
2. telemetry_log_prompts — force getTelemetryLogPromptsEnabled() → false
|
||||
3. telemetry_init_guard — early return in initializeTelemetry()
|
||||
4. dashscope_base_url — DEFAULT_DASHSCOPE_BASE_URL → proxy
|
||||
5. coding_plan_urls — coding.dashscope.aliyuncs.com → proxy
|
||||
6. default_model — validate DEFAULT_QWEN_MODEL (no change)
|
||||
7. mainline_model — validate MAINLINE_CODER_MODEL (no change)
|
||||
8. auto_update_registry — registry.npmjs.org → npm.sensey24.ru
|
||||
9. auto_update_command — add --registry to update commands
|
||||
10. user_settings — ~/.qwen/settings.json (auth + telemetry)
|
||||
11. trusted_folders — ~/.qwen/trustedFolders.json
|
||||
12. system_env — env vars injection
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# ─── Constants ──────────────────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
CONFIG_PATH = SCRIPT_DIR / "qwen_config.json"
|
||||
|
||||
IS_WINDOWS = platform.system() == "Windows"
|
||||
IS_MACOS = platform.system() == "Darwin"
|
||||
|
||||
NPM_PACKAGE = "@qwen-code/qwen-code"
|
||||
CLI_JS_FILENAME = "cli.js"
|
||||
|
||||
PATCH_MARKER = "/* QWEN_PATCHED */"
|
||||
|
||||
# ANSI colors
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
RED = "\033[91m"
|
||||
CYAN = "\033[96m"
|
||||
BOLD = "\033[1m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
# ─── Utilities ──────────────────────────────────────────────────────────
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
def load_config(config_path=None):
|
||||
"""Load patcher configuration from JSON file."""
|
||||
path = Path(config_path) if config_path else CONFIG_PATH
|
||||
if not path.is_file():
|
||||
eprint(f"{RED}Config not found: {path}{RESET}")
|
||||
sys.exit(1)
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def read_version(qwen_root):
|
||||
"""Read version from package.json."""
|
||||
pkg = Path(qwen_root) / "package.json"
|
||||
if pkg.is_file():
|
||||
with open(pkg) as f:
|
||||
return json.load(f).get("version", "unknown")
|
||||
return "unknown"
|
||||
|
||||
|
||||
# ─── Detection ──────────────────────────────────────────────────────────
|
||||
|
||||
def _candidate_paths():
|
||||
"""Generate candidate paths for QwenCode CLI installation."""
|
||||
if IS_WINDOWS:
|
||||
appdata = os.environ.get("APPDATA", "")
|
||||
if appdata:
|
||||
yield Path(appdata) / "npm" / "node_modules" / "@qwen-code" / "qwen-code"
|
||||
localappdata = os.environ.get("LOCALAPPDATA", "")
|
||||
if localappdata:
|
||||
yield Path(localappdata) / "npm" / "node_modules" / "@qwen-code" / "qwen-code"
|
||||
else:
|
||||
yield Path("/usr/lib/node_modules/@qwen-code/qwen-code")
|
||||
yield Path("/usr/local/lib/node_modules/@qwen-code/qwen-code")
|
||||
if IS_MACOS:
|
||||
yield Path("/opt/homebrew/lib/node_modules/@qwen-code/qwen-code")
|
||||
home = Path.home()
|
||||
yield home / ".local" / "lib" / "node_modules" / "@qwen-code" / "qwen-code"
|
||||
yield home / ".npm-global" / "lib" / "node_modules" / "@qwen-code" / "qwen-code"
|
||||
# nvm
|
||||
nvm_dir = os.environ.get("NVM_DIR", str(home / ".nvm"))
|
||||
nvm_path = Path(nvm_dir)
|
||||
if nvm_path.is_dir():
|
||||
for ver_dir in sorted(nvm_path.glob("versions/node/v*"), reverse=True):
|
||||
yield ver_dir / "lib" / "node_modules" / "@qwen-code" / "qwen-code"
|
||||
|
||||
|
||||
def detect_qwen():
|
||||
"""Find QwenCode CLI installation. Returns (qwen_root, cli_js_path) or (None, None)."""
|
||||
for root in _candidate_paths():
|
||||
cli_js = root / CLI_JS_FILENAME
|
||||
if cli_js.is_file():
|
||||
return str(root), str(cli_js)
|
||||
return None, None
|
||||
|
||||
|
||||
# ─── Patching: cli.js targets (1-9) ────────────────────────────────────
|
||||
|
||||
def _backup_file(filepath):
|
||||
"""Create backup of a file."""
|
||||
backup = filepath + ".backup"
|
||||
if not os.path.isfile(backup):
|
||||
shutil.copy2(filepath, backup)
|
||||
print(f" {CYAN}Backup:{RESET} {backup}")
|
||||
|
||||
|
||||
def _already_patched(content):
|
||||
"""Check if file already has patch marker."""
|
||||
return PATCH_MARKER in content
|
||||
|
||||
|
||||
def patch_cli_js(cli_js_path, config):
|
||||
"""Apply all 9 cli.js patch targets. Returns dict of {target: status}."""
|
||||
_backup_file(cli_js_path)
|
||||
|
||||
with open(cli_js_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
|
||||
if _already_patched(content):
|
||||
print(f" {GREEN}cli.js already patched (marker found){RESET}")
|
||||
return {"cli_js": "already_patched"}
|
||||
|
||||
base_url = config["base_url"]
|
||||
npm_registry = config.get("npm_registry", "https://npm.sensey24.ru")
|
||||
results = {}
|
||||
original = content
|
||||
|
||||
# Target 1: TELEMETRY_FLAG
|
||||
pat1 = r'(getTelemetryEnabled\(\)\s*\{)\s*return\s+this\.telemetrySettings\.enabled\s*\?\?\s*false;'
|
||||
rep1 = r'\1 return false; /* QWEN_PATCHED */'
|
||||
content, n = re.subn(pat1, rep1, content)
|
||||
results["telemetry_flag"] = f"OK ({n})" if n > 0 else "SKIP"
|
||||
|
||||
# Target 2: TELEMETRY_LOG_PROMPTS
|
||||
pat2 = r'(getTelemetryLogPromptsEnabled\(\)\s*\{)\s*return\s+this\.telemetrySettings\.logPrompts\s*\?\?\s*true;'
|
||||
rep2 = r'\1 return false; /* QWEN_PATCHED */'
|
||||
content, n = re.subn(pat2, rep2, content)
|
||||
results["telemetry_log_prompts"] = f"OK ({n})" if n > 0 else "SKIP"
|
||||
|
||||
# Target 3: TELEMETRY_INIT_GUARD
|
||||
pat3 = r'(function initializeTelemetry\(config2\)\s*\{)\s*\n(\s*)if\s*\(telemetryInitialized'
|
||||
rep3 = r'\1\n\2return; /* QWEN_PATCHED: telemetry disabled */\n\2if (telemetryInitialized'
|
||||
content, n = re.subn(pat3, rep3, content)
|
||||
results["telemetry_init_guard"] = f"OK ({n})" if n > 0 else "SKIP"
|
||||
|
||||
# Target 4: DASHSCOPE_BASE_URL
|
||||
pat4 = r'(DEFAULT_DASHSCOPE_BASE_URL\s*=\s*)"https://dashscope\.aliyuncs\.com/compatible-mode/v1"'
|
||||
rep4 = rf'\1"{base_url}/v1"'
|
||||
content, n = re.subn(pat4, rep4, content)
|
||||
results["dashscope_base_url"] = f"OK ({n})" if n > 0 else "SKIP"
|
||||
|
||||
# Target 5: CODING_PLAN_URLS (string replace, not regex)
|
||||
count5 = 0
|
||||
for old_url in [
|
||||
"https://coding.dashscope.aliyuncs.com/v1",
|
||||
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
]:
|
||||
new_url = f"{base_url}/v1"
|
||||
c = content.count(old_url)
|
||||
content = content.replace(old_url, new_url)
|
||||
count5 += c
|
||||
results["coding_plan_urls"] = f"OK ({count5})" if count5 > 0 else "SKIP"
|
||||
|
||||
# Target 6: DEFAULT_MODEL (validate only)
|
||||
if re.search(r'DEFAULT_QWEN_MODEL\s*=\s*"coder-model"', content):
|
||||
results["default_model"] = "OK (validated)"
|
||||
else:
|
||||
results["default_model"] = "WARN (unexpected value)"
|
||||
|
||||
# Target 7: MAINLINE_MODEL (validate only)
|
||||
if re.search(r'MAINLINE_CODER_MODEL\s*=\s*"qwen3\.5-plus"', content):
|
||||
results["mainline_model"] = "OK (validated)"
|
||||
else:
|
||||
results["mainline_model"] = "WARN (unexpected value)"
|
||||
|
||||
# Target 8: AUTO_UPDATE_REGISTRY
|
||||
count8 = 0
|
||||
old_reg = '"https://registry.npmjs.org/"'
|
||||
new_reg = f'"{npm_registry}/"'
|
||||
c = content.count(old_reg)
|
||||
content = content.replace(old_reg, new_reg)
|
||||
count8 += c
|
||||
# Also handle single-quoted variant
|
||||
old_reg_sq = "'https://registry.npmjs.org/'"
|
||||
new_reg_sq = f"'{npm_registry}/'"
|
||||
c = content.count(old_reg_sq)
|
||||
content = content.replace(old_reg_sq, new_reg_sq)
|
||||
count8 += c
|
||||
results["auto_update_registry"] = f"OK ({count8})" if count8 > 0 else "SKIP"
|
||||
|
||||
# Target 9: AUTO_UPDATE_COMMAND
|
||||
old_cmd = '"npm install -g @qwen-code/qwen-code@latest"'
|
||||
new_cmd = f'"npm install -g @qwen-code/qwen-code@latest --registry {npm_registry}"'
|
||||
c = content.count(old_cmd)
|
||||
content = content.replace(old_cmd, new_cmd)
|
||||
results["auto_update_command"] = f"OK ({c})" if c > 0 else "SKIP"
|
||||
|
||||
# Add patch marker after shebang line (preserve shebang on line 1)
|
||||
if content != original:
|
||||
if content.startswith("#!"):
|
||||
first_nl = content.index("\n")
|
||||
content = content[:first_nl + 1] + PATCH_MARKER + "\n" + content[first_nl + 1:]
|
||||
else:
|
||||
content = PATCH_MARKER + "\n" + content
|
||||
|
||||
# Write patched file
|
||||
with open(cli_js_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ─── Patching: settings targets (10-12) ────────────────────────────────
|
||||
|
||||
def patch_user_settings(config):
|
||||
"""Configure ~/.qwen/settings.json (Target 10)."""
|
||||
qwen_dir = Path.home() / ".qwen"
|
||||
qwen_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings_path = qwen_dir / "settings.json"
|
||||
|
||||
existing = {}
|
||||
if settings_path.is_file():
|
||||
try:
|
||||
with open(settings_path) as f:
|
||||
existing = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# Deep merge
|
||||
if "security" not in existing:
|
||||
existing["security"] = {}
|
||||
if "auth" not in existing["security"]:
|
||||
existing["security"]["auth"] = {}
|
||||
existing["security"]["auth"]["selectedType"] = "openai"
|
||||
|
||||
if "telemetry" not in existing:
|
||||
existing["telemetry"] = {}
|
||||
existing["telemetry"]["enabled"] = False
|
||||
existing["telemetry"]["logPrompts"] = False
|
||||
|
||||
if "model" not in existing:
|
||||
existing["model"] = {}
|
||||
existing["model"]["name"] = config.get("default_model", "qwen3.5-plus")
|
||||
|
||||
with open(settings_path, "w") as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
|
||||
print(f" {GREEN}Settings:{RESET} {settings_path}")
|
||||
return "OK"
|
||||
|
||||
|
||||
def patch_trusted_folders(config):
|
||||
"""Create/update ~/.qwen/trustedFolders.json (Target 11)."""
|
||||
qwen_dir = Path.home() / ".qwen"
|
||||
qwen_dir.mkdir(parents=True, exist_ok=True)
|
||||
tf_path = qwen_dir / "trustedFolders.json"
|
||||
|
||||
existing = {}
|
||||
if tf_path.is_file():
|
||||
try:
|
||||
with open(tf_path) as f:
|
||||
existing = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
trust_paths = config.get("trust_paths", ["/home", "/root", "/tmp"])
|
||||
for p in trust_paths:
|
||||
if p not in existing:
|
||||
existing[p] = "TRUST_PARENT"
|
||||
|
||||
# Also trust home directory
|
||||
home = str(Path.home())
|
||||
if home not in existing:
|
||||
existing[home] = "TRUST_PARENT"
|
||||
|
||||
with open(tf_path, "w") as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
|
||||
print(f" {GREEN}Trusted folders:{RESET} {tf_path}")
|
||||
return "OK"
|
||||
|
||||
|
||||
def setup_env_vars(config):
|
||||
"""Set environment variables (Target 12)."""
|
||||
base_url = config["base_url"]
|
||||
api_key = config.get("api_key", "")
|
||||
default_model = config.get("default_model", "qwen3.5-plus")
|
||||
|
||||
env_vars = {
|
||||
"OPENAI_API_KEY": api_key,
|
||||
"OPENAI_BASE_URL": f"{base_url}/v1",
|
||||
"OPENAI_MODEL": default_model,
|
||||
"GEMINI_TELEMETRY_ENABLED": "false",
|
||||
"GEMINI_TELEMETRY_LOG_PROMPTS": "false",
|
||||
}
|
||||
|
||||
if IS_WINDOWS:
|
||||
import subprocess
|
||||
for k, v in env_vars.items():
|
||||
subprocess.run(["setx", k, v], capture_output=True)
|
||||
print(f" {GREEN}Env vars:{RESET} Set via setx (Windows)")
|
||||
return "OK"
|
||||
|
||||
# Linux/macOS: write to /etc/environment
|
||||
env_file = Path("/etc/environment")
|
||||
if not env_file.is_file():
|
||||
# Try creating it
|
||||
try:
|
||||
env_file.touch()
|
||||
except PermissionError:
|
||||
eprint(f" {YELLOW}Cannot write /etc/environment (no root){RESET}")
|
||||
_print_env_export(env_vars)
|
||||
return "MANUAL"
|
||||
|
||||
try:
|
||||
existing = env_file.read_text()
|
||||
except PermissionError:
|
||||
_print_env_export(env_vars)
|
||||
return "MANUAL"
|
||||
|
||||
lines = existing.splitlines()
|
||||
updated = False
|
||||
|
||||
for key, value in env_vars.items():
|
||||
found = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith(f"{key}="):
|
||||
lines[i] = f'{key}="{value}"'
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
lines.append(f'{key}="{value}"')
|
||||
updated = True
|
||||
|
||||
new_content = "\n".join(lines)
|
||||
if not new_content.endswith("\n"):
|
||||
new_content += "\n"
|
||||
|
||||
try:
|
||||
env_file.write_text(new_content)
|
||||
print(f" {GREEN}Env vars:{RESET} Written to /etc/environment")
|
||||
return "OK"
|
||||
except PermissionError:
|
||||
eprint(f" {YELLOW}Cannot write /etc/environment (no root){RESET}")
|
||||
_print_env_export(env_vars)
|
||||
return "MANUAL"
|
||||
|
||||
|
||||
def _print_env_export(env_vars):
|
||||
"""Print export commands for manual setup."""
|
||||
print(f"\n {YELLOW}Add these to your shell profile:{RESET}")
|
||||
for k, v in env_vars.items():
|
||||
print(f' export {k}="{v}"')
|
||||
print()
|
||||
|
||||
|
||||
# ─── Orchestration ─────────────────────────────────────────────────────
|
||||
|
||||
def apply_all_patches(cli_js_path, config, settings_only=False):
|
||||
"""Apply all patches. Returns overall results dict."""
|
||||
results = {}
|
||||
|
||||
if not settings_only:
|
||||
print(f"\n{BOLD}Patching cli.js...{RESET}")
|
||||
cli_results = patch_cli_js(cli_js_path, config)
|
||||
results.update(cli_results)
|
||||
|
||||
print(f"\n{BOLD}Configuring settings...{RESET}")
|
||||
results["user_settings"] = patch_user_settings(config)
|
||||
results["trusted_folders"] = patch_trusted_folders(config)
|
||||
results["system_env"] = setup_env_vars(config)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def rollback(cli_js_path):
|
||||
"""Restore cli.js from backup."""
|
||||
backup = cli_js_path + ".backup"
|
||||
if os.path.isfile(backup):
|
||||
shutil.copy2(backup, cli_js_path)
|
||||
print(f" {GREEN}Restored:{RESET} {cli_js_path}")
|
||||
return True
|
||||
else:
|
||||
eprint(f" {RED}No backup found:{RESET} {backup}")
|
||||
return False
|
||||
|
||||
|
||||
# ─── Validation (standalone) ───────────────────────────────────────────
|
||||
|
||||
def run_validation(cli_js_path):
|
||||
"""Run pattern validation on detected installation."""
|
||||
from updater.pattern_validator import validate_all, print_validation_report, get_summary
|
||||
|
||||
user_settings = str(Path.home() / ".qwen" / "settings.json")
|
||||
trusted_folders = str(Path.home() / ".qwen" / "trustedFolders.json")
|
||||
|
||||
results = validate_all(cli_js_path, user_settings, trusted_folders)
|
||||
counts = print_validation_report(results)
|
||||
return counts, get_summary(results)
|
||||
|
||||
|
||||
# ─── CLI ───────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Qwen Code Patcher — patches QwenCode CLI for custom AI proxy"
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--detect", action="store_true", help="Find QwenCode CLI installation")
|
||||
group.add_argument("--apply", action="store_true", help="Apply all patches")
|
||||
group.add_argument("--settings-only", action="store_true", help="Only settings + env (no cli.js)")
|
||||
group.add_argument("--rollback", action="store_true", help="Restore from backup")
|
||||
group.add_argument("--validate", action="store_true", help="Validate all targets")
|
||||
parser.add_argument("--config", type=str, help="Path to custom config file")
|
||||
|
||||
args = parser.parse_args()
|
||||
config = load_config(args.config)
|
||||
|
||||
# Detection
|
||||
qwen_root, cli_js_path = detect_qwen()
|
||||
|
||||
if args.detect:
|
||||
if qwen_root:
|
||||
version = read_version(qwen_root)
|
||||
print(f"\n {GREEN}Found QwenCode CLI{RESET}")
|
||||
print(f" Root: {qwen_root}")
|
||||
print(f" cli.js: {cli_js_path}")
|
||||
print(f" Version: {version}")
|
||||
else:
|
||||
eprint(f"\n {RED}QwenCode CLI not found{RESET}")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if not qwen_root:
|
||||
eprint(f"{RED}QwenCode CLI not found. Install: npm install -g @qwen-code/qwen-code{RESET}")
|
||||
sys.exit(1)
|
||||
|
||||
version = read_version(qwen_root)
|
||||
print(f"\n{BOLD}QwenCode CLI v{version}{RESET} — {qwen_root}")
|
||||
|
||||
if args.validate:
|
||||
counts, summary = run_validation(cli_js_path)
|
||||
# Save report
|
||||
report_dir = SCRIPT_DIR / "reports"
|
||||
report_dir.mkdir(exist_ok=True)
|
||||
report_path = report_dir / f"validation_{version}.json"
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
print(f"\n Report saved: {report_path}")
|
||||
if counts.get("RED", 0) > 0:
|
||||
sys.exit(2)
|
||||
return
|
||||
|
||||
if args.rollback:
|
||||
rollback(cli_js_path)
|
||||
return
|
||||
|
||||
if args.settings_only:
|
||||
results = apply_all_patches(cli_js_path, config, settings_only=True)
|
||||
else:
|
||||
results = apply_all_patches(cli_js_path, config, settings_only=False)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{BOLD}Results:{RESET}")
|
||||
for target, status in results.items():
|
||||
if "OK" in str(status) or status == "already_patched":
|
||||
print(f" {GREEN}[OK]{RESET} {target}: {status}")
|
||||
elif "SKIP" in str(status):
|
||||
print(f" {YELLOW}[SKIP]{RESET} {target}: {status}")
|
||||
else:
|
||||
print(f" {CYAN}[INFO]{RESET} {target}: {status}")
|
||||
|
||||
print(f"\n{GREEN}Done!{RESET} Restart QwenCode CLI to apply changes.\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user