"""Declarative global npm command packages for Decman profiles.""" from __future__ import annotations import json import shutil import decman import decman.core.error as errors import decman.core.module as module import decman.core.output as output import decman.core.store as store_module from decman import Plugin from decman.plugins import run_methods_with_attribute def packages(fn): """Annotate a method that returns globally installed npm package names.""" fn.__npm_packages__ = True return fn class Npm(Plugin): """Install declared global npm packages without pruning unrelated packages.""" NAME = "npm" def __init__(self) -> None: self.packages: set[str] = set() def process_modules( self, store: store_module.Store, modules: list[module.Module] ) -> None: store.ensure("npm_packages_for_module", {}) for mod in modules: store["npm_packages_for_module"].setdefault(mod.name, set()) declared = set().union( *run_methods_with_attribute(mod, "__npm_packages__") ) if store["npm_packages_for_module"][mod.name] != declared: mod._changed = True output.print_debug( f"Module '{mod.name}' changed due to modified global npm packages." ) self.packages |= declared store["npm_packages_for_module"][mod.name] = declared def apply( self, store: store_module.Store, dry_run: bool = False, params: list[str] | None = None, ) -> bool: if not self.packages: return True if shutil.which("npm") is None: if dry_run: output.print_list( "Installing global npm packages after npm becomes available:", sorted(self.packages), ) return True output.print_error("npm is unavailable after the pacman step.") return False try: installed = self._installed_packages() to_install = self.packages - installed output.print_list("Installing global npm packages:", sorted(to_install)) if to_install and not dry_run: decman.prg( [ "npm", "install", "--global", "--no-audit", "--no-fund", *sorted(to_install), ] ) except (errors.CommandFailedError, ValueError) as error: output.print_error(f"Managing global npm packages failed: {error}") return False return True @staticmethod def _installed_packages() -> set[str]: raw = decman.prg( ["npm", "list", "--global", "--depth=0", "--json"], pty=False ) try: result = json.loads(raw) except json.JSONDecodeError as error: raise ValueError("npm returned invalid JSON") from error dependencies = result.get("dependencies", {}) if not isinstance(dependencies, dict): raise ValueError("npm dependencies must be an object") return set(dependencies)