commit 3012b62f6a88477d160032b6dfc23f60e9932ad0 Author: dinlo Date: Sun May 31 18:33:31 2026 +0800 Initial commit Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/npm_manager.py b/npm_manager.py new file mode 100644 index 0000000..3a82681 --- /dev/null +++ b/npm_manager.py @@ -0,0 +1,326 @@ +import customtkinter as ctk +import tkinter as tk +from tkinter import filedialog, messagebox +import subprocess +import json +import os +import threading +import webbrowser + +# Настройки стиля под Windows 11 +ctk.set_appearance_mode("System") +ctk.set_default_color_theme("blue") + +class NpmManagerPro(ctk.CTk): + def __init__(self): + super().__init__() + self.title("Windows 11 NPM Package Manager Pro") + self.geometry("1100x750") + + # Состояние приложения + self.current_mode = "global" # "global" или "local" + self.current_path = None # Путь к локальному проекту + self.checkbox_vars = {} # Состояния чекбоксов + + self.setup_ui() + self.fetch_packages() + + def setup_ui(self): + # --- ВЕРХНЯЯ ПАНЕЛЬ: Режимы и Установка --- + self.header_frame = ctk.CTkFrame(self) + self.header_frame.pack(fill="x", padx=10, pady=(10, 5)) + + # Заголовок + self.title_label = ctk.CTkLabel(self.header_frame, text="🌍 Глобальные пакеты", font=("Segoe UI", 20, "bold")) + self.title_label.pack(side="left", padx=10, pady=10) + + # Кнопка смены режима + self.switch_mode_btn = ctk.CTkButton(self.header_frame, text="📁 Выбрать локальный проект", + fg_color="#5bc0de", hover_color="#31b0d5", text_color="black", + command=self.switch_to_local) + self.switch_mode_btn.pack(side="left", padx=10) + + self.global_mode_btn = ctk.CTkButton(self.header_frame, text="🌍 Глобальный режим", + command=self.switch_to_global, state="disabled") + self.global_mode_btn.pack(side="left", padx=5) + + # Установка новых пакетов + self.install_btn = ctk.CTkButton(self.header_frame, text="Установить", width=100, command=self.install_pkg) + self.install_btn.pack(side="right", padx=10) + + self.install_entry = ctk.CTkEntry(self.header_frame, placeholder_text="Имя пакета (напр. lodash)", width=200) + self.install_entry.pack(side="right", padx=5) + + # --- ПАНЕЛЬ МАССОВЫХ ДЕЙСТВИЙ --- + self.batch_frame = ctk.CTkFrame(self, fg_color="transparent") + self.batch_frame.pack(fill="x", padx=10, pady=5) + + self.select_all_var = ctk.BooleanVar(value=False) + self.select_all_cb = ctk.CTkCheckBox(self.batch_frame, text="Выбрать все", variable=self.select_all_var, command=self.toggle_select_all) + self.select_all_cb.pack(side="left", padx=10) + + self.batch_update_btn = ctk.CTkButton(self.batch_frame, text="Обновить выбранные", width=140, fg_color="#28a745", hover_color="#218838", command=self.batch_update) + self.batch_update_btn.pack(side="left", padx=5) + + self.batch_delete_btn = ctk.CTkButton(self.batch_frame, text="Удалить выбранные", width=140, fg_color="#D9534F", hover_color="#C9302C", command=self.batch_delete) + self.batch_delete_btn.pack(side="left", padx=5) + + self.refresh_btn = ctk.CTkButton(self.batch_frame, text="🔄 Обновить список", width=140, command=self.fetch_packages) + self.refresh_btn.pack(side="right", padx=10) + + # --- ОСНОВНОЙ СПИСОК ПАКЕТОВ --- + self.scroll_frame = ctk.CTkScrollableFrame(self) + self.scroll_frame.pack(fill="both", expand=True, padx=10, pady=5) + + # --- ТЕРМИНАЛ / ЛОГИ --- + self.log_box = ctk.CTkTextbox(self, height=130, font=("Consolas", 12)) + self.log_box.pack(fill="x", padx=10, pady=(5, 10)) + self.log("Приложение запущено. Готово к работе.") + + # --- УТИЛИТЫ --- + def log(self, message): + self.log_box.insert("end", message + "\n") + self.log_box.see("end") + + def run_command(self, cmd, cwd=None): + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="ignore", cwd=cwd) + return result.stdout, result.stderr, result.returncode + + def extract_json(self, text): + try: + start = text.find('{') + end = text.rfind('}') + 1 + if start != -1 and end != 0: + return json.loads(text[start:end]) + except Exception: + pass + return {} + + # --- ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ --- + def switch_to_local(self): + folder = filedialog.askdirectory(title="Выберите папку проекта с package.json") + if folder: + if not os.path.exists(os.path.join(folder, "package.json")): + messagebox.showwarning("Внимание", "В выбранной папке нет файла package.json!") + return + self.current_mode = "local" + self.current_path = folder + self.title_label.configure(text=f"📁 Локальные: {os.path.basename(folder)}") + self.global_mode_btn.configure(state="normal") + self.fetch_packages() + + def switch_to_global(self): + self.current_mode = "global" + self.current_path = None + self.title_label.configure(text="🌍 Глобальные пакеты") + self.global_mode_btn.configure(state="disabled") + self.fetch_packages() + + # --- ЗАГРУЗКА ДАННЫХ --- + def fetch_packages(self): + self.refresh_btn.configure(state="disabled") + self.select_all_var.set(False) + for widget in self.scroll_frame.winfo_children(): + widget.destroy() + self.log("Поиск пакетов и проверка обновлений (это может занять несколько секунд)...") + threading.Thread(target=self._fetch_thread, daemon=True).start() + + def _fetch_thread(self): + cwd = self.current_path if self.current_mode == "local" else None + g_flag = "" if self.current_mode == "local" else "-g" + + # 1. Получаем точный абсолютный путь до папки node_modules через 'npm root' + out_root, _, _ = self.run_command(f"npm root {g_flag}", cwd) + node_modules_path = out_root.strip() + + # Получаем установленные пакеты + out_ls, err_ls, _ = self.run_command(f"npm ls {g_flag} --depth=0 --json", cwd) + ls_data = self.extract_json(out_ls) + + # Получаем устаревшие пакеты + out_out, err_out, _ = self.run_command(f"npm outdated {g_flag} --json", cwd) + outdated_data = self.extract_json(out_out) + + self.after(0, lambda: self.populate_list(ls_data, outdated_data, node_modules_path)) + + def populate_list(self, ls_data, outdated_data, node_modules_path): + deps = ls_data.get("dependencies", {}) + self.checkbox_vars.clear() + + if not deps: + lbl = ctk.CTkLabel(self.scroll_frame, text="Пакеты не найдены.", font=("Segoe UI", 16)) + lbl.pack(pady=20) + self.log("Пакеты не найдены.") + else: + self.log(f"Отображено пакетов: {len(deps)}") + + for pkg, info in deps.items(): + current_version = info.get("version", "неизвестно") + + # 2. Формируем правильный абсолютный путь для Windows + pkg_path = os.path.normpath(os.path.join(node_modules_path, pkg)) + + # Проверка на наличие обновлений + is_outdated = pkg in outdated_data + latest_version = outdated_data[pkg].get("latest", "") if is_outdated else "" + + row = ctk.CTkFrame(self.scroll_frame) + row.pack(fill="x", pady=5, padx=5) + + # Чекбокс + var = ctk.BooleanVar(value=False) + self.checkbox_vars[pkg] = var + cb = ctk.CTkCheckBox(row, text="", variable=var, width=20) + cb.pack(side="left", padx=(10, 5)) + + # Кликабельное название пакета (Попап с инфо) + name_btn = ctk.CTkButton(row, text=pkg, width=150, anchor="w", fg_color="transparent", + text_color=("black", "white"), hover_color=("#e0e0e0", "#2a2d2e"), + font=("Segoe UI", 14, "bold"), command=lambda p=pkg: self.show_pkg_info(p)) + name_btn.pack(side="left", padx=5) + + # Версия (с подсветкой если устарела) + if is_outdated: + v_text = f"{current_version} ➔ {latest_version}" + v_color = "#ff9900" # Оранжевый + else: + v_text = f"v{current_version}" + v_color = "gray" + + version_lbl = ctk.CTkLabel(row, text=v_text, text_color=v_color, font=("Segoe UI", 13, "bold" if is_outdated else "normal"), width=120, anchor="w") + version_lbl.pack(side="left", padx=5) + + # Путь и кнопка "Открыть в проводнике" + path_lbl = ctk.CTkLabel(row, text=pkg_path, anchor="w", text_color="gray", font=("Segoe UI", 11)) + path_lbl.pack(side="left", padx=5, fill="x", expand=True) + + open_folder_btn = ctk.CTkButton(row, text="📂", width=30, fg_color="transparent", + hover_color="#cccccc", text_color=("black", "white"), + command=lambda p=pkg_path: self.open_in_explorer(p)) + open_folder_btn.pack(side="left", padx=5) + + # Кнопка обновления (зеленая, если есть апдейт) + upd_color = "#28a745" if is_outdated else ("#3b8ed0", "#1f6aa5") + update_btn = ctk.CTkButton(row, text="Обновить", width=80, fg_color=upd_color, + command=lambda p=pkg: self.update_pkg(p)) + update_btn.pack(side="left", padx=5) + + # Кнопка удаления + delete_btn = ctk.CTkButton(row, text="Удалить", width=80, fg_color="#D9534F", hover_color="#C9302C", + command=lambda p=pkg: self.delete_pkg(p)) + delete_btn.pack(side="left", padx=5) + + self.refresh_btn.configure(state="normal") + + # --- ИНФОРМАЦИЯ О ПАКЕТЕ (ПОПАП) --- + def show_pkg_info(self, pkg): + info_window = ctk.CTkToplevel(self) + info_window.title(f"Информация: {pkg}") + info_window.geometry("450x300") + info_window.transient(self) + + loading_lbl = ctk.CTkLabel(info_window, text="Загрузка данных из реестра NPM...", font=("Segoe UI", 14)) + loading_lbl.pack(expand=True) + + threading.Thread(target=self._fetch_info_thread, args=(pkg, info_window, loading_lbl), daemon=True).start() + + def _fetch_info_thread(self, pkg, window, loading_lbl): + out, _, _ = self.run_command(f"npm view {pkg} --json") + info = self.extract_json(out) + + self.after(0, lambda: loading_lbl.destroy()) + self.after(0, lambda: self._build_info_ui(window, pkg, info)) + + def _build_info_ui(self, window, pkg, info): + if not info: + ctk.CTkLabel(window, text="Не удалось загрузить информацию.").pack(pady=20) + return + + desc = info.get("description", "Описание отсутствует") + author = info.get("author", "Неизвестен") + if isinstance(author, dict): author = author.get("name", "Неизвестен") + license_ = info.get("license", "Не указана") + homepage = info.get("homepage", "") + + ctk.CTkLabel(window, text=pkg, font=("Segoe UI", 22, "bold")).pack(pady=(10, 5)) + + desc_box = ctk.CTkTextbox(window, height=60, wrap="word") + desc_box.pack(fill="x", padx=20, pady=5) + desc_box.insert("1.0", desc) + desc_box.configure(state="disabled") + + ctk.CTkLabel(window, text=f"👤 Автор: {author}", font=("Segoe UI", 14)).pack(anchor="w", padx=20, pady=2) + ctk.CTkLabel(window, text=f"📄 Лицензия: {license_}", font=("Segoe UI", 14)).pack(anchor="w", padx=20, pady=2) + + if homepage: + link_lbl = ctk.CTkLabel(window, text=f"🌐 Сайт: {homepage}", text_color="#1f538d", cursor="hand2", font=("Segoe UI", 14, "underline")) + link_lbl.pack(anchor="w", padx=20, pady=5) + link_lbl.bind("", lambda e: webbrowser.open(homepage)) + + # --- ДЕЙСТВИЯ (Открыть папку, Установить) --- + def open_in_explorer(self, path): + if os.path.exists(path): + os.startfile(path) + else: + messagebox.showwarning("Ошибка", f"Папка не найдена:\n{path}") + + def install_pkg(self): + pkg = self.install_entry.get().strip() + if not pkg: return + self.log(f"Установка нового пакета '{pkg}'...") + self.install_entry.delete(0, 'end') + threading.Thread(target=self._exec_npm_command, args=("install", [pkg]), daemon=True).start() + + # --- МАССОВЫЕ ДЕЙСТВИЯ --- + def toggle_select_all(self): + state = self.select_all_var.get() + for var in self.checkbox_vars.values(): + var.set(state) + + def get_selected_packages(self): + return[pkg for pkg, var in self.checkbox_vars.items() if var.get()] + + def batch_update(self): + selected = self.get_selected_packages() + if not selected: return + self.log(f"Массовое обновление: {', '.join(selected)}") + threading.Thread(target=self._exec_npm_command, args=("update", selected), daemon=True).start() + + def batch_delete(self): + selected = self.get_selected_packages() + if not selected: return + confirm = messagebox.askyesno("Подтверждение", f"Удалить {len(selected)} пакетов?") + if confirm: + self.log(f"Массовое удаление: {', '.join(selected)}") + threading.Thread(target=self._exec_npm_command, args=("uninstall", selected), daemon=True).start() + + # --- ИНДИВИДУАЛЬНЫЕ ДЕЙСТВИЯ --- + def update_pkg(self, pkg): + self.log(f"Обновление пакета {pkg}...") + threading.Thread(target=self._exec_npm_command, args=("update", [pkg]), daemon=True).start() + + def delete_pkg(self, pkg): + confirm = messagebox.askyesno("Подтверждение", f"Вы действительно хотите удалить '{pkg}'?") + if confirm: + self.log(f"Удаление {pkg}...") + threading.Thread(target=self._exec_npm_command, args=("uninstall", [pkg]), daemon=True).start() + + # --- ОБЩИЙ ИСПОЛНИТЕЛЬ КОМАНД --- + def _exec_npm_command(self, action, pkgs_list): + cwd = self.current_path if self.current_mode == "local" else None + g_flag = "" if self.current_mode == "local" else "-g" + pkgs_str = " ".join(pkgs_list) + + cmd = f"npm {action} {g_flag} {pkgs_str}" + out, err, code = self.run_command(cmd, cwd) + + if code == 0: + self.after(0, lambda: self.log(f"✅ Действие '{action}' успешно выполнено для {pkgs_str}.")) + else: + self.after(0, lambda: self.log(f"❌ Ошибка при '{action}' {pkgs_str}.\n{err}")) + + self.after(0, self.fetch_packages) + +if __name__ == "__main__": + app = NpmManagerPro() + app.mainloop() \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..923659b --- /dev/null +++ b/readme.md @@ -0,0 +1,25 @@ +# 📦 Windows 11 NPM Package Manager Pro + +Современное графическое приложение для удобного управления пакетами Node.js (NPM). Создано на Python с использованием библиотеки `CustomTkinter`, имеет современный интерфейс, адаптированный под дизайн Windows 11 (с поддержкой светлой/тёмной темы). + +## ✨ Возможности + +* **🌍 Два режима работы:** Управляйте глобальными пакетами системы или зависимостями конкретного локального проекта (выбор папки с `package.json`). +* **🔄 Умные обновления:** Автоматическая проверка устаревших версий. Программа покажет старую и новую версию пакета, а кнопка "Обновить" подсветится зелёным. +* **⚡ Массовые операции:** Выделяйте нужные пакеты чекбоксами для быстрого массового обновления или удаления в один клик. +* **📂 Быстрый доступ к файлам:** Кнопка-иконка открывает папку установленного пакета (исходный код) прямо в Проводнике Windows. +* **ℹ️ Информация о пакетах:** Кликните по названию пакета, чтобы получить данные из реестра NPM (описание, автор, лицензия) и перейти на сайт проекта. +* **➕ Установка:** Устанавливайте новые пакеты, просто введя их название в верхней панели. + +## 🛠 Требования + +Для запуска приложения на вашем компьютере должны быть установлены: +1. **[Node.js](https://nodejs.org/)** (включает в себя `npm`). +2. **[Python 3.8+](https://www.python.org/)** + +## 🚀 Установка и запуск + +1. Склонируйте репозиторий или скачайте файл `npm_manager.py`. +2. Установите библиотеку для графического интерфейса: + ```bash + pip install customtkinter \ No newline at end of file