Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+326
@@ -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("<Button-1>", 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()
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user