From a23ef0007e6bafdca5f7740199908127099e9027 Mon Sep 17 00:00:00 2001 From: delta-cloud-208e Date: Sat, 7 Mar 2026 13:52:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(qwen):=20add=20Qwen=20Code=20CLI=20?= =?UTF-8?q?=E2=80=94=20install,=20patcher,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 32 ++- README_ru.md | 2 +- qwen/README.md | 99 +++++++++ qwen/qwen_config.json | 15 ++ qwen/qwen_patcher.py | 492 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 638 insertions(+), 2 deletions(-) mode change 100644 => 100755 README_ru.md create mode 100644 qwen/README.md create mode 100644 qwen/qwen_config.json create mode 100755 qwen/qwen_patcher.py diff --git a/README.md b/README.md index 8699d47..a2f1102 100755 --- a/README.md +++ b/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: diff --git a/README_ru.md b/README_ru.md old mode 100644 new mode 100755 index 56a5091..e0d3db0 --- a/README_ru.md +++ b/README_ru.md @@ -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 | Планируется | ## Быстрый старт diff --git a/qwen/README.md b/qwen/README.md new file mode 100644 index 0000000..05e6c0e --- /dev/null +++ b/qwen/README.md @@ -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 +``` diff --git a/qwen/qwen_config.json b/qwen/qwen_config.json new file mode 100644 index 0000000..5146be6 --- /dev/null +++ b/qwen/qwen_config.json @@ -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" +} diff --git a/qwen/qwen_patcher.py b/qwen/qwen_patcher.py new file mode 100755 index 0000000..0e768cb --- /dev/null +++ b/qwen/qwen_patcher.py @@ -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()