diff --git a/.SRCINFO b/.SRCINFO deleted file mode 100644 index dbf7678..0000000 --- a/.SRCINFO +++ /dev/null @@ -1,17 +0,0 @@ -pkgbase = kjspkg-git - pkgdesc = A package manager for KubeJS. - pkgver = 1.0 - pkgrel = 1 - url = https://www.github.com/Modern-Modpacks/kjspkg.git - arch = x86_64 - arch = i686 - license = MIT - makedepends = python-pip - makedepends = curl - makedepends = sudo - depends = python - depends = git - source = git+https://www.github.com/Modern-Modpacks/kjspkg.git - md5sums = SKIP - -pkgname = kjspkg-git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b0580b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +kubejs +mods diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 222e853..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,35 +0,0 @@ -# Maintainer: G_cat -pkgname=kjspkg-git -pkgver=1.0 -pkgrel=1 -epoch= -pkgdesc="A package manager for KubeJS." -arch=(x86_64 i686) -url="https://www.github.com/Modern-Modpacks/kjspkg.git" -license=('MIT') -groups=() -depends=(python git) -makedepends=(python-pip curl sudo) -checkdepends=() -optdepends=() -provides=() -conflicts=() -replaces=() -backup=() -options=() -install= -changelog= -source=("git+$url") -noextract=() -md5sums=("SKIP") -validpgpkeys=() - -pkgver() { - cd "kjspkg" - git rev-parse --short HEAD -} - -package() { - sudo -v - curl -s https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/install.sh | sh -} diff --git a/README.md b/README.md index 716e9e2..9ad7738 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,68 @@ -# KJSPKG +# icon KJSPKG -A simple package manager for KubeJS. +A simple package manager for KubeJS written in Go. -[![contributions](https://github.com/Modern-Modpacks/kjspkg/assets/79367505/d2519e70-ce96-4bbc-b35b-af3e674bf421)](https://github.com/Modern-Modpacks/kjspkg#adding-your-own-package) -[![lat](https://img.shields.io/badge/approved%20by-lat-c374e4?style=for-the-badge&labelColor=480066)](https://github.com/user-attachments/assets/0df64919-6333-447e-9869-c270138941bd) +[![Contributions welcome](https://github.com/Modern-Modpacks/kjspkg/assets/79367505/d2519e70-ce96-4bbc-b35b-af3e674bf421)](https://github.com/Modern-Modpacks/kjspkg#adding-your-own-package) +[![Approved by latvian.dev](https://img.shields.io/badge/approved%20by-lat-c374e4?style=for-the-badge&labelColor=480066)](https://github.com/user-attachments/assets/0df64919-6333-447e-9869-c270138941bd) -![logo](https://user-images.githubusercontent.com/79367505/227798123-5454e9b1-b39b-4c45-9e02-e18f2e807585.png) +**The Go API exposed at github.com/Modern-Modpacks/kjspkg/pkg/kjspkg is not +stable. It is not recommended to use it just yet.** ## Installation & Update -### Requirements +### Install script -* [Python 3.8](https://www.python.org/) (or higher) -* Pip -* [Git](https://git-scm.com/) -* [Curl](https://curl.se/) (probably pre-installed) - -### Linux +This script will install KJSPKG to your system and add it to your PATH. ```sh -curl -s https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/install.sh | sh +# On Linux +curl -fsSL https://g.tizu.dev/mm.kj/install.sh?r | bash +# On Windows +powershell -c "irm https://g.tizu.dev/mm.kj/install.ps1?r | iex" ``` -### Windows + -## Usage + -Removing packages: - -```sh -kjspkg remove [package] [package] -``` +### Using Go -Updating packages: +This requires a working Go installation of at least 1.23.2. ```sh -kjspkg update [package] [package] +go install github.com/Modern-Modpacks/kjspkg/cmd/kjspkg@latest ``` -More info in the help page: +## Usage + +KJSPKG comes with extensive help text, so you can just run `kjspkg` to see all +the commands and options available. You may also use `--help` after any command +to get more information about it. ```sh -kjspkg help +kjspkg install [package] [package] +kjspkg remove [package] [package] +kjspkg update [package] [package] ``` ## Adding your own package 1. Create a repository containing your scripts and assets 2. [Don't forget to license your code](https://choosealicense.com/) -3. Add a file to your repo named `.kjspkg` and format it like this: - - ```json - { - "author": "", - "description": "", - - "versions": [], - "modloaders": [. Can contain multiple modloaders], - "dependencies": [], - "incompatibilities": [] - } - ``` - +3. Create an empty directory and run `kjspkg dev init` +4. Do your thing and create a repository with the code 4. Fork this repo 5. Clone it 6. Add your package to `pkgs.json` file. Format it like this: `"your_package_id": "your_github_name/your_repo_name[$path/to/your/package/directory][@branch_name]",` @@ -83,6 +71,7 @@ kjspkg help * Branch is `main` by default 7. Create a pull request 8. Wait for it to be accepted +9. profit ### KJSPKG badges @@ -96,8 +85,6 @@ kjspkg help ![Version list](https://github.com/user-attachments/assets/5a3b8e3a-bd91-456e-8443-bbffa894a38f) -(Thanks tizu.dev on discord for the figma template) - Tested means that the version is confirmed to be working; Not tested means that the version should work, but hasn't been tested. Feel free to test it yourself and let us know so we'll update the readme. diff --git a/app.py b/app.py deleted file mode 100755 index c1935d6..0000000 --- a/app.py +++ /dev/null @@ -1,963 +0,0 @@ -#!/usr/bin/python3 - -# IMPORTS - -# Built-in modules -from os import path, remove, getcwd, makedirs, walk, chmod, chdir, system, getenv, listdir # Working with files and system stuff -from os import name as osname # Windows/linux -from shutil import rmtree, move, copy, copytree # More file stuff -from pathlib import Path # EVEN MORE FILE STUFF -from tempfile import gettempdir # Get tmp dir of current os -from subprocess import run, DEVNULL # Subprocesses -from json import dump, load, dumps, loads # Json -from multiprocessing import Process # Async threads -from signal import signal, SIGTERM # Signals in threads -from time import sleep # Honk mimimimimimimi -from zipfile import ZipFile # Working with .jars -from itertools import zip_longest # Ziiiiiiiiiiiiiiiiiiiiiip - -from http import server # Discord login stuff -from urllib.parse import urlparse, parse_qs # Parse url path - -from stat import S_IWRITE # Windows stuff -from warnings import filterwarnings # Disable the dumb fuzz warning - -from random import choice # Random splash - -# External libraries -from fire import Fire # CLI tool - -from requests import get, put, exceptions # Requests -from git import Repo, GitCommandNotFound, GitCommandError # Git cloning - -from psutil import process_iter # Get processes -from flatten_json import flatten # Flatten dicts -from toml import loads as tomlload # Read .tomls -from esprima import parse, error_handler # Parse .js files - -# CONSTANTS -VERSIONS = { # Version and version keys - "1.12.2": 2, - "1.12": 2, - - "1.16.5": 6, - "1.16": 6, - - "1.18.2": 8, - "1.18": 8, - - "1.19.2": 9, - "1.19.3": 9, - "1.19.4": 9, - "1.19": 9, - - "1.20.1": 10, - "1.20.2": 10, - "1.20.3": 10, - "1.20.4": 10, - "1.20": 10, - - "1.21.1": 11, - "1.21": 11 -} -SCRIPT_DIRS = ("server_scripts", "client_scripts", "startup_scripts") # Script directories -ASSET_DIRS = ("data", "assets") # Asset directories -CONFIG = { # Default config - "_": "Please, do not delete this file or any other file/directory labeled \".kjspkg\", even if they might seem empty and useless, they are still required. Thanks for your understanding!", - - "installed": {}, - "trustgithub": False -} -NL = "\n" # Bruh -LOGO = """ -⠀⠀⠀⠀⢀⣤⣶⣿⣿⣶⣤⡀⠀⠀⠀⠀ -⠀⠀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⠀⠀ -⢠⣄⡀⠉⠻⢿⣿⣿⣿⣿⡿⠟⠉⢀⣠⡄ -⢸⣿⣿⣷⣦⣀⠈⠙⠋⠁⣀⣴⣾⣿⣿⡇ -⢸⣿⣿⣿⣿⣿⣿⠀⠀⣿⣿⣿⣿⣿⣿⡇ -⢸⣿⣿⣿⣿⣿⣿⠀⠀⣿⣿⣿⣿⣿⣿⡇ -⠀⠙⠻⣿⣿⣿⣿⠀⠀⣿⣿⣿⣿⠟⠋⠀ -⠀⠀⠀⠀⠉⠻⢿⠀⠀⡿⠟⠉⠀⠀⠀⠀ -""" # Epic logo in ascii - -# VARIABLES -kjspkgfile = {} # .kjspkg file - -# CLASSES -# class HTTPDiscordLoginRequestHandler(server.BaseHTTPRequestHandler): -# def log_message(self, format, *args): return -# def do_GET(self): -# self.send_response(200) -# self.send_header("Content-type", "text/html") -# self.end_headers() -# self.wfile.write( -# b""" -# -# -# -# """ -# ) - -# token = post("https://discord.com/api/v10/oauth2/token", data={ -# "client_id": "1108295881247166496", -# "client_secret": "yopSwUCpisfib6RR3aqHyp293vOyG4F5", -# "grant_type": "authorization_code", -# "code": parse_qs(urlparse(self.path).query)["code"][0], -# "redirect_uri": "http://localhost:1337" -# }).json()["access_token"] - -# print(post("https://discord.com/api/v10/channels/303440391124942858/messages", headers={ -# "Authorization": token -# }).text) - -# HELPER FUNCTIONS -def _bold(s:str) -> str: return "\u001b[1m"+s+"\u001b[0m" # Make the text bold -def _textbg(s:str) -> str: return "\u001b[47m\u001b[30m"+s+"\u001b[0m" # Make the text black and have white background -def _purple(s:str) -> str: return "\u001b[35;1m"+s+"\u001b[0m" # Make the text bright ourple -def _err(err:str, dontquit:bool=False): # Handle errors - print("\u001b[31;1m"+err+"\u001b[0m") # Print error - if not dontquit: exit(1) # Quit -def _carbon_err(): _err("CarbonJS has been abandoned (https://github.com/malezjaa/carbonjs/blob/main/README.md), and thus the support for its package format was removed from KJSPKG.") # Print carbon removal error -def _remove_prefix(pkgname:str) -> str: return pkgname.split(":")[-1] # Remove prefix -def _format_github(pkgname:str) -> str: return _remove_prefix(pkgname).split('/')[-1].split("@")[0].split("$")[0] # Remove github author, path and branch -def _loading_anim(prefix:str=""): # Loading animation - loading = "⡆⠇⠋⠙⠸⢰⣠⣄" # Animation frames - i = 0 - - # Termination - def terminate(*args): print(" "*(len(prefix)+2), end="\r"); exit(0) - signal(SIGTERM, terminate) - - try: - while 1: - print(_bold(prefix+" "+loading[i % len(loading)]), end="\r") # Print the next anim frame - i += 1 - sleep(.1) - except KeyboardInterrupt: terminate() # Terminate on keyboard interrupt -def _loading_thread(*args) -> Process: # Loading animation thread - thread = Process(target=_loading_anim, args=args) # Create a thread - thread.start() # Start it - return thread # Return it -def _dumbass_windows_path_error(f, p:str, e): chmod(p, S_IWRITE) # Dumbass windows path error -def _check_for_fun(): return getenv("NO_FUN_ALLOWED") == None # Disable easter eggs - -# TMP HELPER FUNCTIONS -def _create_tmp(pathintmp:str) -> str: # Create a temp directory and return its path - tmppath = path.join("tmp", pathintmp) - makedirs(tmppath, exist_ok=True) - return tmppath -def _clear_tmp(): # Clear tmp directory - if path.exists("tmp"): rmtree("tmp", onerror=_dumbass_windows_path_error) # Delete if exists - -# PROJECT HELPER FUNCTIONS -def _check_project() -> bool: # Check if the current directory is a kubejs directory - for dir in SCRIPT_DIRS: - if path.exists(dir) and path.basename(getcwd())=="kubejs": return True - return False -def _create_project_directories(): - for dir in SCRIPT_DIRS+ASSET_DIRS: makedirs(dir, exist_ok=True) # Create asset and script directories - for dir in SCRIPT_DIRS: makedirs(path.join(dir, ".kjspkg"), exist_ok=True) # Create .kjspkg directories -def _project_exists() -> bool: return path.exists(".kjspkg") # Check if a kjspkg project exists -def _delete_project(): # Delete the project and all of the files - for pkg in list(kjspkgfile["installed"].keys()): _remove_pkg(pkg, True) # Remove all packages - for dir in SCRIPT_DIRS: rmtree(path.join(dir, ".kjspkg"), onerror=_dumbass_windows_path_error) # Remove .kjspkg dirs - remove(".kjspkg") # Remove .kjspkg file -def _update_manifest(): # Update .kjspkg file - global kjspkgfile - - # Update the config by adding keys that don't exist - for k, v in CONFIG.items(): - if k not in kjspkgfile.keys(): kjspkgfile[k] = v -def _enable_reflection(): # Enable reflection on 1.16 - with open(path.join("config", "common.properties"), "a+") as f: - if ("invertClassLoader=true" not in f.read().splitlines()): f.write("invertClassLoader=true") -def _check_for_forge(): return kjspkgfile["modloader"]=="forge" # Checks the project for forge -def _get_mod_manifest(modpath:str) -> dict: # Get a mod's mods.toml/fabric.mod.json - modfile = ZipFile(path.join(getcwd(), "..", "mods", modpath)) - - try: - if _check_for_forge(): return tomlload(modfile.open("META-INF/mods.toml").read().decode("utf-8")) - else: return loads(modfile.open("fabric.mod.json").read().decode("utf-8")) - except KeyError: return # Check for wierd mods with no mods.toml/fabric.mod.json -def _get_mod_version(modpath:str) -> str: - manifest = _get_mod_manifest(modpath) # Get manifest - if manifest==None: return # Return none if not found - - if _check_for_forge(): return manifest["mods"][0]["version"] - else: return manifest["version"] -def _get_versions() -> list: # Get all mod versions - modversions = {} - for i in listdir(path.join(getcwd(), "..", "mods")): - if i.endswith(".jar"): - modversion = _get_mod_version(i) - if modversion: modversions[_get_modid(i)] = modversion # For each mod file, get the mod version and add the mod id - mod version pair to the dict - - return modversions # Return the dict of mod versions -def _get_modid(modpath:str) -> str: # Get mod id from a mod file - manifest = _get_mod_manifest(modpath) # Get manifest - if manifest==None: return # Return none if not found - - if _check_for_forge(): return manifest["mods"][0]["modId"] - else: return manifest["id"] -def _get_modids() -> list: # Get all mod ids - modids = [] - for i in listdir(path.join(getcwd(), "..", "mods")): - if i.endswith(".jar"): - modid = _get_modid(i) - if modid: modids.append(modid) # For each mod file, get the mod id and append - - return modids # Return the list of modids - -# def _discord_login(): # Login with discord for discord prefixes -# server.HTTPServer(("", 1337), HTTPDiscordLoginRequestHandler).handle_request() - -# PKG HELPER FUNCTIONS -def _reload_pkgs(): # Reload package registry cache - with open(path.join(gettempdir(), "kjspkgs.json"), "w") as tmp: tmp.write(get(f"https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/pkgs.json").text) -def _pkgs_json() -> dict: # Get the pkgs.json file - pkgspath = path.join(gettempdir(), "kjspkgs.json") - if not path.exists(pkgspath): _reload_pkgs() - return load(open(pkgspath)) - -def _pkg_info(pkg:str, ghinfo:bool=True, refresh:bool=True) -> dict: # Get info about the pkg - if refresh: _reload_pkgs() # Refresh pkgs - - prefix = pkg.split(":")[0] if len(pkg.split(":"))>1 else "kjspkg" # kjspkg: - default prefix - packagename = _remove_prefix(pkg) # Get package name without the prefix - - # Call correct function based on prefix - if prefix=="kjspkg": info = _kjspkginfo(packagename) - elif prefix in ("carbon", "carbonjs"): _carbon_err() # info = _carbonpkginfo(packagename) - elif prefix in ("github", "external"): info = _githubpkginfo(packagename) - # elif prefix=="discord": - # _discord_login() - # exit() - else: _err("Unknown prefix: "+_bold(prefix)) - - # Get github repo info if requested - if info!=None and ghinfo: - req = get(f"https://api.github.com/repos/{info['repo']}?ref={info['branch']}", headers=({"Authorization": "Bearer "+getenv("GITHUB_API_KEY")} if getenv("GITHUB_API_KEY") else {})) - if req.status_code==200: info["ghdata"] = req.json() - - return info -def _kjspkginfo(pkg:str) -> dict: # Get info about a default kjspkg pkg - pkgregistry = _pkgs_json() # Get the pkgs.json file - if pkg not in pkgregistry.keys(): return # Return nothing if the pkg doesn't exist - - repo = pkgregistry[pkg] # Get the repo with branch - branch = "main" # Set the branch to main by default - if "@" in repo: # If the branch is specifed - branch = repo.split("@")[-1] # Set the branch - repo = repo.split("@")[0] # Remove the branch from the repo - path = "." - if "$" in repo: - path = repo.split("$")[-1] # Set the path - repo = repo.split("$")[0] # Remove the path from the repo - - package = get(f"https://raw.githubusercontent.com/{repo}/{branch}{'/'+path if path!='.' else ''}/.kjspkg").json() # Get package info - - package["repo"] = repo # Add the repo to info - package["branch"] = branch # Add the branch to info - package["path"] = path # Add the path to info - - return package # Return the json object -# def _carbonpkginfo(pkg:str) -> dict: # Get info about a carbonjs pkg (https://github.com/malezjaa/carbonjs) -# allpackages = get("https://carbon.beanstech.tech/api/packages").json() # Get all packages -# allpackages = [i for i in allpackages if i["name"]==pkg.lower()] # Find the package with the name provided -# if len(allpackages)==0: return # If not found, return nothing - -# repository = allpackages[0]['repository'].replace('https://github.com/', '') # Format the repository -# branch = get(f"https://api.github.com/repos/{repository}").json()["default_branch"] # Get the default branch -# info = get(f"https://raw.githubusercontent.com/{repository}/{branch}/carbon.config.json").json() # Request info about the package - -# return { # Return formatted info -# "author": info["author"], -# "description": info["description"], - -# "versions": list(dict.fromkeys([VERSIONS[i] for i in info["minecraftVersion"]])), -# "modloaders": info["modloaders"], -# "dependencies": [], -# "incompatibilities": [], - -# "repo": repository, -# "branch": branch, -# "path": "." -# } -def _githubpkginfo(pkg:str) -> dict: # Get dummy info about an external pkg - return { - "author": pkg.split("/")[0], - "description": "", - - "versions": [kjspkgfile["version"]], - "modloaders": [kjspkgfile["modloader"]], - "dependencies": [], - "incompatibilities": [], - - "repo": pkg, - "branch": "main" if "@" not in pkg else pkg.split("@")[-1] - } -def _move_pkg_contents(pkg:str, tmpdir:str, furtherpath:str): # Move the contents of the pkg to the .kjspkg folders - # Find the license - licensefile = path.join(tmpdir, "LICENSE") - if not path.exists(licensefile): licensefile = path.join(tmpdir, "LICENSE.txt") - if not path.exists(licensefile): licensefile = path.join(tmpdir, "LICENSE.md") - - for dir in SCRIPT_DIRS: # Clone scripts & licenses into the main kjs folders - tmppkgpath = path.join(tmpdir, furtherpath, dir) - finalpkgpath = path.join(dir, ".kjspkg", pkg) - if path.exists(tmppkgpath): - move(tmppkgpath, finalpkgpath) # Files - if path.exists(licensefile): copy(licensefile, finalpkgpath) # License - - assetfiles = [] # Pkg's asset files - for dir in ASSET_DIRS: # Clone assets - tmppkgpath = path.join(tmpdir, furtherpath, dir) # Get asset path - if not path.exists(tmppkgpath): continue - - for dirpath, _, files in walk(tmppkgpath): # For each file in assets/data - for name in files: - tmppath = path.join(dirpath, name) - patharray = tmppath.split(path.sep) - finalpath = path.sep.join(patharray[patharray.index(dir):]) - - makedirs(path.sep.join(finalpath.split(path.sep)[:-1]), exist_ok=True) # Create parent dirs - move(tmppath, finalpath) # Move it to the permanent dir - assetfiles.append(finalpath) # Add it to assetfiles - - kjspkgfile["installed"][pkg] = assetfiles # Add the pkg to installed -def _install_pkg(pkg:str, update:bool, quiet:bool, skipmissing:bool, reload:bool, *, _depmode:bool=False): # Install the pkg - if not update and _format_github(pkg) in kjspkgfile["installed"]: # If the pkg is already installed and the update parameter is false, notify the user and just return - if not quiet: print(_bold(f"Package \"{pkg}\" already installed ✓")) - return - if update: - if pkg=="*": # If updating all packages - for p in list(kjspkgfile["installed"].keys()): _install_pkg(p, True, quiet, skipmissing, reload, _depmode=True) # Update all packages - return - - _remove_pkg(pkg, False) # If update is true, remove the previous version of the pkg - - package = _pkg_info(pkg, False, reload) # Get pkg - if not package and reload: - _reload_pkgs() # Reload if not found - package = _pkg_info(pkg, False, reload) # Try to get the pkg again - - if not package: # If pkg doesn't exist after reload - if not skipmissing: _err(f"Package \"{pkg}\" does not exist") # Err - else: return # Or just ingore - - pkg = _remove_prefix(pkg) # Remove pkg prefix - - # Unsupported version/modloader errs - if _depmode and (kjspkgfile["version"] not in package["versions"] or kjspkgfile["modloader"] not in package["modloaders"]): return - if kjspkgfile["version"] not in package["versions"]: _err(f"Unsupported version 1.{10+kjspkgfile['version']} for package \"{pkg}\"") - if kjspkgfile["modloader"] not in package["modloaders"]: _err(f"Unsupported modloader \"{kjspkgfile['modloader'].title()}\" for package \"{pkg}\"") - - # Install dependencies & check for incompats - modids = [] - if (("dependencies" in package.keys() and any([i.startswith("mod:") for i in package["dependencies"]])) or ("incompatibilities" in package.keys() and any([i.startswith("mod:") for i in package["incompatibilities"]]))): modids = _get_modids() # Get a list of all mod ids - - if "dependencies" in package.keys(): - for dep in package["dependencies"]: - if dep.lower().startswith("mod:") and _remove_prefix(dep.lower()) not in modids: _err(f"Mod \"{_remove_prefix(dep.replace('_', ' ').replace('-', ' ')).title()}\" not found.") # Check for mod dependency - elif not dep.lower().startswith("mod:"): _install_pkg(dep.lower(), dep.lower() in kjspkgfile["installed"], quiet, skipmissing, reload) # Install/update package dependency - if "incompatibilities" in package.keys(): - for i in package["incompatibilities"]: - if i.lower().startswith("mod:") and _remove_prefix(i.lower()) in modids: _err(f"Incompatible mod: "+_remove_prefix(i.replace('_', ' ').replace('-', ' ')).title()) # Check for mod incompats - elif i in kjspkgfile["installed"].keys(): _err(f"Incompatible package: "+i) # Throw err if incompats detected - - if not quiet: loadthread = _loading_thread(f"{'Installing' if not update else 'Updating'} {_format_github(pkg)}...") # Start the loading animation - - tmpdir = _create_tmp(pkg) # Create a temp dir - try: Repo.clone_from(f"https://github.com/{package['repo']}.git", tmpdir, branch=package["branch"]) # Install the repo into the tmp dir - except GitCommandError: Repo.clone_from(f"https://github.com/{package['repo']}.git", tmpdir) # If the branch is not found, try to install from the default one - furtherpath = path.sep.join(package["path"].split("/")) # Set the furtherpath to the package path - - pkg = _format_github(pkg) # Remove github author and branch if present - - _move_pkg_contents(pkg, tmpdir, furtherpath) # Move the contents of the pkg to the kubejs folder - put(f"https://tizudev.vercel.app/automatin/api/1025316079226064966/kjspkg?stat=downloads&id={pkg}") # Add 1 to the download counter on tizu's backend - - if not quiet: - loadthread.terminate() # Kill the loading animation - if pkg=="*": print(_bold(f"All packages updated succesfully! ✓")) # Show message if all packages are updated - else: print(_bold(f"Package \"{_format_github(pkg)}\" {'installed' if not update else 'updated'} succesfully! ✓")) # Show message if one package is installed/updated - -def _remove_pkg(pkg:str, skipmissing:bool): # Remove the pkg - if pkg not in kjspkgfile["installed"].keys(): - if not skipmissing: _err(f"Package \"{pkg}\" is not installed") # If the pkg is not installed, err - else: return # Or just ignore - - for dir in SCRIPT_DIRS: # Remove script files - scriptpath = path.join(dir, ".kjspkg", pkg) - if path.exists(scriptpath): rmtree(scriptpath, onerror=_dumbass_windows_path_error) - for file in kjspkgfile["installed"][pkg]: # Remove asset files - if path.exists(file): remove(file) - - del kjspkgfile["installed"][pkg] # Remove the pkg from installed - -# MAIN COMMAND FUNCTIONS -def install(*pkgs:str, update:bool=False, quiet:bool=False, skipmissing:bool=False, reload:bool=True): # Install pkgs - if update and not pkgs: pkgs = ("*",) # If update is passed and not pkgs are, set them to all pkgs - - # Install each given package - for pkg in pkgs: - pkg = pkg.lower() # Format - - if (pkg.startswith("github:")): # If the package is external - if ( - ("trustgithub" in kjspkgfile.keys() and kjspkgfile["trustgithub"]) or # And external packages are trusted - (quiet or input(_bold(f"Package \"{_format_github(pkg)}\" uses the \"github:\" prefix, external packages coming from github are not tested and are not guaranteed to work, do you want to disable external packages in this project? (Y/n) ")).lower()!="n") # Or quiet mode is set/prompt is answered with y - ): kjspkgfile["trustgithub"]=True # Trust external packages - else: # Otherwise - kjspkgfile["trustgithub"]=False # Don't trust them - if skipmissing: continue # Skip if skipmissing - else: return # Stop if no skipmissing - - if update and pkg not in kjspkgfile["installed"].keys() and not skipmissing and pkg!="*": _err(f"Package \"{_format_github(pkg)}\" not found") # Err if package not found during update - _install_pkg(pkg, update, quiet, skipmissing, reload) # Install package - -def removepkg(*pkgs:str, quiet:bool=False, skipmissing:bool=False): # Remove pkgs - for pkg in pkgs: - pkg = _remove_prefix(pkg.lower()) - _remove_pkg(pkg, skipmissing) - if not quiet: print(_bold(f"Package \"{pkg}\" removed succesfully! ✓")) -def update(*pkgs:str, **kwargs): # Update pkgs - install(*pkgs, update=True, **kwargs) -def updateall(**kwargs): # Update all pkgs - update("*", **kwargs) -def listpkgs(*, count:bool=False): # List pkgs - if count: # Only show the pkg count if the "count" option is passed - print(len(kjspkgfile["installed"].keys())) - return - - if (len(kjspkgfile["installed"].keys())==0 and _check_for_fun()): # Easter egg if 0 pkg installed - print(_bold("*nothing here, noone around to help*")) - return - - print("\n".join(kjspkgfile["installed"].keys())) # Print the list -def pkginfo(pkg:str, *, script:bool=False, githubinfo:bool=True): # Print info about a pkg - if(pkg.startswith("github:")): _err(f"Can't show info about an external package \"{_format_github(pkg)}\"") # Err if pkg is external - - info = _pkg_info(pkg, githubinfo, True) # Get the info - if not info: _err(f"Package {pkg} not found") # Err if pkg not found - - # Tizu lookup view/download data - headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; Unactivated)"} if _check_for_fun() else None - downloaddata = get("https://tizudev.vercel.app/automatin/api/1025316079226064966/kjspkg?stat=downloads", headers=headers) - viewdata = get("https://tizudev.vercel.app/automatin/api/1025316079226064966/kjspkg?stat=views", headers=headers) - if viewdata.status_code==200: - info["lookupapi"] = { - "downloads": downloaddata.json()[pkg] if pkg in downloaddata.json().keys() else 0, - "views": viewdata.json()[pkg] if pkg in viewdata.json().keys() else 0 - } - - # Print it (scripty) - if script: - print(dumps(info)) - return - - # Print it (pretty) - print(f""" -{_bold(_remove_prefix(pkg).replace("-", " ").title())} by {_bold(info["author"])} - -{info["description"]} -{ - NL+_bold("KJSPKG Lookup")+": https://kjspkglookup.modernmodpacks.site/#"+pkg+NL if ":" not in pkg or pkg.split(":")[0]=="kjspkg" else "" -}"""+(f"""{_bold("Downloads")}: {info['lookupapi']['downloads']} -{_bold("Views")}: {info['lookupapi']['views']} -""" if "lookupapi" in info.keys() else "\n")+f""" -{_bold("Dependencies")}: {", ".join([_remove_prefix(i).title().replace("-", " ").replace("_", " ")+(" ("+i.split(":")[0].title()+")" if ":" in i else "") for i in info["dependencies"]]) if "dependencies" in info.keys() and len(info["dependencies"])>0 else "*nothing here*"} -{_bold("Incompatibilities")}: {", ".join([_remove_prefix(i).title().replace("-", " ").replace("_", " ")+(" ("+i.split(":")[0].title()+")" if ":" in i else "") for i in info["incompatibilities"]]) if "incompatibilities" in info.keys() and len(info["incompatibilities"])>0 else "*compatible with everything!*"} - -{_bold("Versions")}: {", ".join([f"1.{10+i}" for i in info["versions"]])} -{_bold("Modloaders")}: {", ".join([i.title() for i in info["modloaders"]])} - -{_bold("GitHub")}: https://github.com/{info["repo"]}/tree/{info["branch"]}"""+ -(f""" -{_bold('License')}: {info['ghdata']['license']['key'].upper() if info['ghdata']['license']!=None else 'ARR'} - -{_textbg(f" 👁️ {info['ghdata']['watchers_count']} ")} {_textbg(f" 🍴 {info['ghdata']['forks_count']} ")} {_textbg(f" ⭐ {info['ghdata']['stargazers_count']} ")} -""" if "ghdata" in info else "\n")) -def fetch(*, logo:bool=True, script:bool=False): # Fetch data about the project in a pfetch-esque format - versions = _get_versions() # Get mod versions - data = { # Compile all required data - "version": f"1.{10+kjspkgfile['version']}", - "loader": kjspkgfile["modloader"], - "pkgs": len(kjspkgfile["installed"].keys()), - "kube": versions["kubejs"] if "kubejs" in versions.keys() else "???", - "rhino": versions["rhino"] if "rhino" in versions.keys() else "???", - "arch": versions["architectury"] if "architectury" in versions.keys() else "???" - } - - # Print it (scripty) - if script: - print(dumps(data)) - return - - # Prepare it to look pretty - datastr = _bold(f"KJSPKG@{getcwd()}\n") - longeststr = len(max(data.keys(), key=len))+1 - for k,v in data.items(): datastr += f"{_purple(k)}{' '*(longeststr-len(k))}{v}\n" - selectedlogo = LOGO if logo else "" - - # Print it (pretty) - for l, d in zip_longest(selectedlogo.splitlines()[1:], datastr.splitlines()): print(f"{_purple(l)+' ' if l!=None else ''}{d if d!=None else ''}") - print() -def listall(*, count:bool=False, search:str="", reload:bool=True): # List all pkgs - if reload: _reload_pkgs() # Reload pkgs - allpkgs = list(_pkgs_json().keys()) # All package names - - if count: # If count is true - print(len(allpkgs)) # Print the pkg count - return - if not search: # If no search query - print("\n".join(sorted(allpkgs))) # Print all pkg names - return - - filterwarnings("ignore") # Ignore the warning thefuzz produces - from thefuzz import process # Fuzzy search - - # Get results and print the best ones - results = process.extract(search, allpkgs, limit=25) - for result, ratio in results: - if ratio>75: print(result) -def search(*query:str, **kwags): # Search for pkgs - listall(search="-".join(query), **kwags) # Call listall with joined spaces -def reload(): _reload_pkgs() # Reload packages -def init(*, quiet:bool=False, override:bool=False, cancreate:str=None, **configargs): # Init project - global kjspkgfile - - params = [ # Changable parameters - "version", - "modloader", - "trustgithub" - ] - for arg in configargs.keys(): # If a parameter is not found, raise key err - if arg not in params: raise TypeError() - - if cancreate: # Scriptable cancreate option - print(_check_project()) - return - - if not _check_project(): _err("Hmm... This directory doesn't look like a kubejs directory") # Wrong dir err - - if _project_exists() and not override: # Override - if not quiet and input("\u001b[31;1mA PROJECT ALREADY EXISTS IN THIS REPOSITORY, CREATING A NEW ONE OVERRIDES THE PREVIOUS ONE, ARE YOU SURE YOU WANT TO PROCEED? (y/N): \u001b[0m").lower()=="y" or override: _delete_project() - else: exit(0) - - # Ask for missing params - if "version" not in configargs.keys(): configargs["version"] = input(_bold("Input your minecraft version (1.12/1.16/1.18/1.19/1.20/1.21): ")) # Version - if configargs["version"] not in VERSIONS.keys(): _err("Unknown or unsupported version: "+str(configargs["version"])) - configargs["version"] = VERSIONS[configargs["version"]] - - if "modloader" not in configargs.keys(): configargs["modloader"] = input(_bold("Input your modloader (forge/neoforge/fabric/quilt): ")) # Modloader - configargs["modloader"] = configargs["modloader"].lower() - if configargs["modloader"] not in ("forge", "neoforge", "fabric", "quilt"): _err("Unknown or unsupported modloader: "+configargs["modloader"].title()) - - if configargs["modloader"] in ("forge", "neoforge"): configargs["modloader"] = "forge" - elif configargs["modloader"] in ("fabric", "quilt"): configargs["modloader"] = "fabric" - - _create_project_directories() # Create .kjspkg directories - if configargs["version"]==6: _enable_reflection() # Enable reflection in the config for 1.16.5 - - kjspkgfile = CONFIG # Set .kjspkg to default config - for k, v in configargs.items(): kjspkgfile[k] = v # Change config as needed - - with open(".kjspkg", "w+") as f: dump(kjspkgfile, f) # Create .kjspkg file - if not quiet: print(_bold("Project created!")) # Woo! -def uninit(*, confirm:bool=False): # Remove the project - if confirm or input("\u001b[31;1mDOING THIS WILL REMOVE ALL PACKAGES AND UNINSTALL KJSPKG COMPLETELY, ARE YOU SURE YOU WANT TO PROCEED? (y/N): \u001b[0m").lower()=="y": - _delete_project() - print("\u001b[31;1mProject deleted\u001b[0m") - else: print(_bold("Aborted.")) - -# DEV COMMAND FUNCTIONS -def devrun(launcher:str=None, version:int=None, modloader:str=None, ignoremoddeps:bool=False, quiet:bool=False): # Run a test instance - global kjspkgfile - - # Errs - if not path.exists(".kjspkg"): _err(".kjspkg file not found. Add one according to the KJSPKG README (https://github.com/Modern-Modpacks/kjspkg/blob/main/README.md) and then re-run.") # No .kjspkg file error - manifest = load(open(".kjspkg")) # Parse the json file - if "description" not in manifest.keys(): _err("Invalid manifest. You are probably running this command inside of an instance instead of a package.") # Invalid schema error - - if version==None: # If the version is not specified - if len(manifest["versions"])==1: version = str(manifest["versions"][0]) # Select the only one - else: version = input(_bold(f"What version would you like to test the package for? ({'/'.join([str(i) for i in manifest['versions']])}) ")).lower() # Or ask if multiple - if not (isinstance(version, int) or version.isnumeric()) or int(version) not in manifest['versions']: _err("Unknown version: "+version) # Err if the version is unknown - if version=="2": _err("Testing for 1.12 is not supported") # 1.12 not supported :shrug: - - if modloader==None: # If the modloader is not specified - if len(manifest["modloaders"])==1: modloader = manifest["modloaders"][0] # Select the only one - else: modloader = input(_bold(f"What modloader would you like to test the package for? ({'/'.join(manifest['modloaders'])}) ")).lower() # Or ask if multiple - if modloader not in manifest['modloaders']: _err("Unknown modloader: "+modloader) # Err if the modloader is unknown - - if osname=="nt": # Windows paths - LAUNCHERPATHS = { - "PrismLauncher": path.join(getenv("LOCALAPPDATA"), "Programs", "PrismLauncher", "prismlauncher.exe"), - "multimc": path.join(getenv("USERPROFILE"), "Downloads", "MultiMC", "MultiMC.exe") - } - else: # Posix paths - LAUNCHERPATHS = { - "PrismLauncher": "/usr/bin/prismlauncher", - "multimc": "/opt/multimc/run.sh" - } - - if launcher==None: # If the launcher is not specified - launcher = "" - for l, p in LAUNCHERPATHS.items(): - if path.exists(p): - launcher = l # Find the one that's installed - break - if not launcher: _err("Prism/MultiMC was not found. Please install one of these launchers.") - else: # If it is specified - if launcher.lower() in ("prism", "prismlauncher"): launcher = "PrismLauncher" # Replace prism with prismlauncher - if launcher.lower()=="multi": launcher = "multimc" # Replace multi with multimc - - if launcher.lower() not in [i.lower() for i in LAUNCHERPATHS.keys()]: _err("Launcher doesn't exist or not supported: "+launcher.title()) # Err if unknown launcher - elif not path.exists({k.lower(): v for k, v in LAUNCHERPATHS.items()}[launcher.lower()]): _err("Launcher not installed: "+launcher.title()) # Err if the launcher isn't installed - - # Kill all launcher windows - for i in process_iter(): - if launcher.lower() in i.name().lower(): i.kill() - - instancename = f"kjspkg{version}{manifest['modloaders'][0]}" # Instance name - if osname=="posix": instancepath = path.expanduser(f"~/.local/share/{launcher}/instances/{instancename}") # Linux instance path - else: - if launcher=="multimc": instancepath = path.join(path.sep.join(LAUNCHERPATHS[launcher].split(path.sep)[:-1]), "instances", instancename) # Windows multimc instance path - elif launcher=="PrismLauncher": instancepath = path.join(getenv("APPDATA"), "PrismLauncher", "instances", instancename) # Windows prism instance path - instancezippath = path.join(gettempdir(), instancename+".zip") # Path to instance's zipped file - if not path.exists(instancepath): # If instance doesn't exit - with open(instancezippath, "wb+") as f: - f.write(get(f"https://github.com/Modern-Modpacks/kjspkg/raw/main/instances/{instancename}.zip").content) # Downlaod the zip - - if not quiet: print(_bold("A launcher window should now appear, please import the instance, config it if you need and close the window.")) - run([LAUNCHERPATHS[launcher], "-I", instancezippath], stdout=DEVNULL, stderr=DEVNULL) # Import - - pkgpath = getcwd() # Save the package file - chdir(path.join(instancepath, ".minecraft", "kubejs")) # Change the cwd to the instance - kjspkgfile = load(open(".kjspkg")) # Load the .kjspkg file - - if path.exists("tmp"): rmtree("tmp", onerror=_dumbass_windows_path_error) # Remove the temp folder if exists - - # Install deps - if "dependencies" in manifest.keys(): - modids = _get_modids() # Get mod ids - for dep in manifest["dependencies"]: # For each dep - if dep.startswith("mod:") and _remove_prefix(dep.lower()) not in modids: # If a mod dep is not found - if ignoremoddeps and not quiet: print(_bold(f"!!! Mod `{_remove_prefix(dep)}` is not installed, but it's ignored since ignoremoddeps is specified.")) # Warn if ignoremoddeps is true - else: _err(f"Your package depends on `{_remove_prefix(dep)}`, which is not installed.") # Or err if it's false - elif not dep.startswith("mod:"): _install_pkg(dep, False, True, True, True) # Install package deps - - makedirs("tmp", exist_ok=True) # Create a tempdir - tmpdir = copytree(pkgpath, "tmp/test") # Copy the test package contents - _move_pkg_contents("test", tmpdir, ".") # Install - rmtree(tmpdir, onerror=_dumbass_windows_path_error) # Remove the temp folder - - if not quiet: loadthread = _loading_thread("Running test instance...") # Loading anim - try: run([LAUNCHERPATHS[launcher], "-l", instancename], stdout=DEVNULL, stderr=DEVNULL) # Run the instance - except KeyboardInterrupt: pass # Stop on keyboard interrupt - - if not quiet: - loadthread.terminate() # Terminate loading anim - loadthread.join() # Wait for it to finish - - _remove_pkg("test", True) # Remove the test package - for dep in manifest["dependencies"]: - if not dep.startswith("mod:"): _remove_pkg(dep, True) # Remove all deps - - chdir(pkgpath) # Change the cwd back to the package dir - if not quiet: print(_bold("Test instance killed ✓")) # 👍 -def devdist(description:str=None, author:str=None, dependencies:list=None, incompatibilities:list=None, versions:list=None, modloaders:list=None, distdir:str="dist", gitrepository:bool=True, generatemanifest:bool=True, quiet:bool=False): # Package the scripts automatically - if not _check_project(): _err("Current directory does not look like a KubeJS one...") # Check if the cwd is a kubejs one - - # Inputs - if generatemanifest: - if description==None: description = input(_bold("Input a description for your package: ")) - if author==None: author = input(_bold("Enter authors' names that worked on the package")+" (comma separated): ") - if dependencies==None: dependencies = input(_bold("Enter dependency names for your package")+" (comma separated, optional): ").lower().replace(" ", "").split(",") - if dependencies==[""]: dependencies=[] # Set the deps to empty if the input is empty - if incompatibilities==None: incompatibilities = input(_bold("Enter incompatibility names for your package")+" (comma separated, optional): ").lower().replace(" ", "").split(",") - if dependencies==[""]: dependencies=[] # Set the incompats to empty if the input is empty - - kjspkgfile = None - if (versions==None or modloaders==None) and path.exists(".kjspkg"): kjspkgfile = load(open(".kjspkg")) # Load .kjspkg if exists - - if kjspkgfile!=None: # Extract the version number and modloader from .kjspkg - if versions==None: versions = (kjspkgfile["version"],) - if modloaders==None: modloaders = (kjspkgfile["modloader"],) - else: # If .kjspkg is not found - if versions==None: - versions = input(_bold("Enter the version keys for your package")+" (6/8/9/10, comma separated): ").replace(" ", "").split(",") # Ask for version - for i in versions: - if not i.isnumeric or int(i) not in VERSIONS.values(): _err("Unknown version: "+i) - versions = [int(i) for i in versions] - if modloaders==None: - modloaders = input(_bold("Enter the modloaders for your package")+" (forge/fabric, comma separated): ").replace(" ", "").lower().split(",") # Ask for modloader - for i in modloaders: - if i not in ("forge", "fabric"): _err("Unknown modloader: "+i) - - # Confirmations - if not quiet: input(_bold("Only scripts and assets that start with `kjspkg_` will be added (prefix removed). Press enter to confirm ")) - if path.exists(distdir): - if input(_bold(f"The `{distdir}` folder already exists, remove it?")+" (Y/n): ").lower()!="n" or quiet: rmtree(distdir, onerror=_dumbass_windows_path_error) - else: _err("Aborted.") - - for dir in SCRIPT_DIRS+ASSET_DIRS: # For all script and asset dirs - makedirs(path.join(distdir, dir), exist_ok=True) # Make dirs in dist - for dirpath, _, files in walk(dir): # For all files - for name in files: - if name.startswith("kjspkg_"): # If the file starts with kjspkg_ - makedirs(path.join(distdir, dirpath), exist_ok=True) # Create parents - copy(path.join(dirpath, name), path.join(distdir, dirpath, name.removeprefix("kjspkg_"))) # Copy it - - # Write .kjspkg manifest - if generatemanifest: - with open(path.join(distdir, ".kjspkg"), "w+") as f: - dump({ - "author": author, - "description": description, - - "versions": versions, - "modloaders": modloaders, - "dependencies": dependencies, - "incompatibilities": incompatibilities - }, f) - - if not quiet: print(_bold("Package generated ✓")) # Woo - - # Set up git repo - if gitrepository: - repo = Repo.init(distdir) # Init - repo.git.add(all=True) # Add - repo.index.commit("Initial Commit") # Commit - if not quiet: print(_bold("Git repository initialized (don't forget to add a license) ✓")) # Cool -def devtest(legacychecks:bool=True): # Test scripts for errors - if not path.exists(".kjspkg"): _err(".kjspkg not found") # Check for .kjspkg - manifest = load(open(".kjspkg")) # Load it - errors = False # No errors at first - - for dir in SCRIPT_DIRS: # For each script dir - for dirpath, _, files in walk(dir): # For each nested file - for name in files: - if not name.endswith(".js"): continue # If the file is not a js file, skip - script = path.join(dirpath, name) # Get the path to the script - - try: parsedscript = parse(open(script).read()).toDict() # Read it and parse it - except error_handler.Error as e: # If any errors come up during parsing - errors = True # Set the errs to true - _err(f"[{script}] {e.message}", True) # Show the error - continue # Skip to the next script - - for k, v in flatten(parsedscript).items(): # For each key in flattened tree - if ("version" in manifest.keys() and manifest["version"]==9) or ("versions" in manifest.keys() and any([i>=9 for i in manifest["versions"]])): # If the version is 1.19+ - if legacychecks and k.endswith("_callee_name") and v in ("onEvent", "java"): # And the script uses onEvent/java (+legacy checks are on) - errors = True # Set the errs to true - _err(f"[{script}] You are using the {v} method, but your script is marked as KJS6+. Please use the compat layer (https://kjspkglookup.modernmodpacks.site/#kjspkg-compat-layer) or change your script according to the migration guide (https://wiki.latvian.dev/books/kubejs-legacy/page/migrating-to-kubejs-6).", True) # Show the legacy error - else: # If the version is 1.18/1.16 - if legacychecks and k.endswith("_callee_object_name") and v in ( # And the script uses KJS6 event syntax (+legacy checks are on) - "StartupEvents", - "ServerEvents", - "ClientEvents", - "LevelEvents", - "PlayerEvents", - "EntityEvents", - "BlockEvents", - "ItemEvents", - "NetworkEvents", - "JEIEvents", - "REIEvents" - ): - errors = True # Set the errs to true - _err(f"[{script}] You are using the {v} class, but your script is marked as KJS legacy. Please use the compat layer (https://kjspkglookup.modernmodpacks.site/#kjspkg-compat-layer) or change your script according to the migration guide (https://wiki.latvian.dev/books/kubejs-legacy/page/migrating-to-kubejs-6).", True) # Show the legacy error - - if not errors: print(_bold("No errors found ✓")) # If no errors were found, display this message - -# INFO COMMAND FUNCTIONS -def info(): # Print the help page - SPLASHES = [ # Splash list - "You should run `kjspkg uninit`, NOW!", - "Run `kjspkg mold` to brew kombucha", - "Thanks Lat 👍", - "Help, I'm locked in a basement packaging scripts!", - "kjspkg rm -rf / --no-preserve-root", - "Made in Python 3.whatever!", - "https://modernmodpacks.site", - "Made by Modern Modpacks!", - "gimme gimme gimme", - "`amogus` is a real package!", - "Supports 1.12!", - "Procrastinating doing one project by doing another project, genius!", - "Also try Magna!", - "No alternative for CraftTweaker!" - ] - - # Info string - INFO = f""" -{_bold("KJSPKG")}, a package manager for KubeJS. -{choice(SPLASHES)+NL if _check_for_fun() else ""} -{_bold("Commands:")} - -kjspkg install/download [pkgname1] [pkgname2] [--quiet/--skipmissing] [--update] [--noreload] - installs packages -kjspkg remove/uninstall [pkgname1] [pkgname2] [--quiet/--skipmissing] - removes packages -kjspkg update [pkgname1/*] [pkgname2] [--quiet/--skipmissing] - updates packages -kjspkg updateall [--quiet/--skipmissing] - updates all packages - -kjspkg install [pkgname] - installs packages from kjspkg's repo -kjspkg install kjspkg:[pkgname] - installs packages from kjspkg's repo -kjspkg install github:[author]/[name] - installs external packages from github - -kjspkg list [--count] - lists packages (or outputs the count of them) -kjspkg pkg [package] [--script] [--nogithubinfo] - shows info about the package -kjspkg fetch [--nologo] [--script] - prints out a neofetch-eqsue screen with info about the current KJSPKG instance -kjspkg listall/all [--count] [--search ""] [--noreload] - lists all packages -kjspkg search [query] - searches for packages with a similar name -kjspkg reload/refresh - reloads the cached package registry - -kjspkg init [--override/--quiet] [--version ""] [--modloader ""] [--cancreate ""] - inits a new project (will be run by default) -kjspkg uninit [--confirm] - removes all packages and the project - -kjspkg help/info - shows this message (default behavior) -kjspkg dev - shows info about dev commands -kjspkg gui - shows info about the GUI app - -{_bold("Credits:")} - -Modern Modpacks - Owner -G_cat101 - Coder -Tizu69 - Maintainer of KJSPKG Lookup -Juh9870 - Wanted to be here - """ - - print(INFO) # Print the info -def devinfo(): # Print info about the dev commands - # Info string - INFO = f""" -{_bold("Dev commands")} -Dev utils are experimental, use at your own risk. - -kjspkg dev run [--quiet] [--ignoremoddeps] [--launcher ""] [--version ""] [--modloader ""] - runs your package in a test minecraft instance (requires MultiMC/Prism to be installed) -kjspkg dev dist [--quiet] [--nogitrepository] [--nogeneratemanifest] [--description ""] [--author ""] [--dependencies ","] [--incompatibilities ","] [--versions ","] [--modloaders ","] [--distdir ""] - creates a package from your kubejs folder -kjspkg dev test [--nolegacychecks] - checks your code for syntax errors - -kjspkg dev help - shows this message (default behavior) - """ - - print(INFO) # Print the info -def guiinfo(): # Print info about the GUI app - print(f"{_bold('Did you know there is a GUI app for KJSPKG?')} Check it out at https://github.com/Modern-Modpacks/kjspkg-gui!") -def kombucha(): # Kombucha easter egg - RECIPE = f""" -{_bold("Ingredients")} - -* 2 organic green teabags (or 2 tsp loose leaf) -* 2 organic black teabags bags (or 2 tsp loose leaf) -* 100-200g granulated sugar, to taste -* 1 medium scoby, plus 100-200ml starter liquid - -{_bold("Method")} - -STEP 1 -For essential information on brewing safely, our top recipe tips and fun flavours to try, read our guide on how to make kombucha. Pour 1.8 litres boiled water into a saucepan, add the teabags and sugar (depending on how sweet you like it or the bitterness of your tea), stir to dissolve the sugar and leave for 6-10 mins to infuse. - -STEP 2 -Remove and discard the teabags without squeezing them. Leave the tea to cool completely before pouring into a large 2.5- to 3-litre glass jar. Add the scoby and its starter liquid, leaving a minimum of 5cm space at the top of the jar. - -STEP 3 -Cover the jar with a thin tea towel or muslin cloth so the scoby can 'breathe'. Secure with an elastic band and label the jar with the date and its contents. - -STEP 4 -Leave to ferment for one to two weeks at room temperature and away from radiators, the oven or direct sunlight. Do not put the jar in a cupboard, as air circulation is important. - -STEP 5 -After the first week, taste the kombucha daily – the longer you leave it, the more acidic the flavour will become. When ready, pour the kombucha into bottles, making sure to reserve the scoby and 100-200ml of starter fluid for the next batch. - -STEP 6 -The kombucha is ready to drink immediately, or you can start a ‘secondary fermentation’ by adding flavours such as fruit, herbs and spices to the drawn-off liquid and leaving it bottled for a few more days before drinking. Will keep in the fridge for up to three months. - """ - print(RECIPE) - -# PARSER FUNCTION -def _parser(func:str="help", *args, help:bool=False, **kwargs): - global kjspkgfile - - system("") # Enable color codes on windows, don't ask me why - - if help: func="help" - - devparser = False - if func=="dev": - devparser = True # If the func is equal to dev, set the parser to the dev parser - if args: # If the args aren't empty - func = args[0] # Set the func to the next argument - args = args[1:] # And move the args by 1 - else: func="help" # If the args are empty, default to dev help - - if not devparser: - FUNCTIONS = { # Non-dev command mappings - "install": install, - "download": install, - "add": install, - "remove": removepkg, - "uninstall": removepkg, - "rm": removepkg, - "update": update, - "updateall": updateall, - "list": listpkgs, - "pkg": pkginfo, - "pkginfo": pkginfo, - "fetch": fetch, - "listall": listall, - "all": listall, - "search": search, - "reload": reload, - "refresh": reload, - "init": init, - "uninit": uninit, - "help": info, - "info": info, - "gui": guiinfo - } - if _check_for_fun(): FUNCTIONS["mold"] = kombucha - else: - FUNCTIONS = { # Dev command mappings - "run": devrun, - "dist": devdist, - "package": devdist, - "test": devtest, - "check": devtest, - "validate": devtest, - "help": devinfo, - "info": devinfo - } - - if func not in FUNCTIONS.keys(): _err(f"Command \"{func}\" is not found. Run \"kjspkg {'dev ' if devparser else ''}help\" to see all of the available commands") # Wrong command err - - if not devparser: # Skip the .kjspkg file stuff if the parser is the dev parser - helperfuncs = (info, guiinfo, init, pkginfo, listall, search, kombucha) # Helper commands that don't require a project - if FUNCTIONS[func] not in helperfuncs and not _project_exists(): # If a project is not found, call init - print(_bold("Project not found, a new one will be created.\n")) - init() - if (FUNCTIONS[func]==init or FUNCTIONS[func] not in helperfuncs) and path.exists(".kjspkg"): kjspkgfile = load(open(".kjspkg")) # Open .kjspkg - - FUNCTIONS[func](*args, **kwargs) # Run the command - - # Clean up - if not devparser and path.exists(".kjspkg") and FUNCTIONS[func] not in helperfuncs: # If uninit wasn't called, the command isn't a helper command and the parser is not dev - _update_manifest() # Update .kjspkg - with open(".kjspkg", "w") as f: dump(kjspkgfile, f) # Save .kjspkg - -# RUN -if __name__=="__main__": # If not imported - _clear_tmp() # Remove tmp - - try: Fire(_parser) # Run parser with fire - except (KeyboardInterrupt, EOFError): exit(0) # Ignore some exceptions - # except TypeError: _err("Wrong syntax") # Wrong syntax err - except GitCommandNotFound: _err("Git not found. Install it here: https://git-scm.com/downloads") # Git not found err - except (exceptions.ConnectionError, exceptions.ReadTimeout): _err("Low internet connection") # Low internet connection err - - _clear_tmp() # Remove tmp again - -# Ok that's it bye \ No newline at end of file diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..8197bf5 --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,6 @@ +package assets + +import "embed" + +//go:embed instances/* +var Instances embed.FS diff --git a/instances/kjspkg10fabric.zip b/assets/instances/kjspkg10fabric.zip similarity index 100% rename from instances/kjspkg10fabric.zip rename to assets/instances/kjspkg10fabric.zip diff --git a/instances/kjspkg10forge.zip b/assets/instances/kjspkg10forge.zip similarity index 100% rename from instances/kjspkg10forge.zip rename to assets/instances/kjspkg10forge.zip diff --git a/instances/kjspkg11forge.zip b/assets/instances/kjspkg11forge.zip similarity index 100% rename from instances/kjspkg11forge.zip rename to assets/instances/kjspkg11forge.zip diff --git a/instances/kjspkg6fabric.zip b/assets/instances/kjspkg6fabric.zip similarity index 100% rename from instances/kjspkg6fabric.zip rename to assets/instances/kjspkg6fabric.zip diff --git a/instances/kjspkg6forge.zip b/assets/instances/kjspkg6forge.zip similarity index 100% rename from instances/kjspkg6forge.zip rename to assets/instances/kjspkg6forge.zip diff --git a/instances/kjspkg8fabric.zip b/assets/instances/kjspkg8fabric.zip similarity index 100% rename from instances/kjspkg8fabric.zip rename to assets/instances/kjspkg8fabric.zip diff --git a/instances/kjspkg8forge.zip b/assets/instances/kjspkg8forge.zip similarity index 100% rename from instances/kjspkg8forge.zip rename to assets/instances/kjspkg8forge.zip diff --git a/instances/kjspkg9fabric.zip b/assets/instances/kjspkg9fabric.zip similarity index 100% rename from instances/kjspkg9fabric.zip rename to assets/instances/kjspkg9fabric.zip diff --git a/instances/kjspkg9forge.zip b/assets/instances/kjspkg9forge.zip similarity index 100% rename from instances/kjspkg9forge.zip rename to assets/instances/kjspkg9forge.zip diff --git a/cmd/kjspkg/c.go b/cmd/kjspkg/c.go new file mode 100644 index 0000000..f5b3d23 --- /dev/null +++ b/cmd/kjspkg/c.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + + "g.tizu.dev/colr" +) + +type CNotImplemented struct{} + +func (c *CNotImplemented) Run(ctx *Context) error { + fmt.Print(colr.Red("not implemented\n")) + + return nil +} diff --git a/cmd/kjspkg/c_dev_dist.go b/cmd/kjspkg/c_dev_dist.go new file mode 100644 index 0000000..3f7b211 --- /dev/null +++ b/cmd/kjspkg/c_dev_dist.go @@ -0,0 +1,106 @@ +package main + +import ( + "io" + "os" + "path/filepath" + + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "github.com/charmbracelet/huh" +) + +type CDevDist struct { + kjspkg.Package + Scripts []string `help:"Scripts to include in the package"` + Assets []string `help:"Assets to include in the package"` + Target string `help:"Target directory name (will be placed in the kubejs dir)" default:"dist"` +} + +func (c *CDevDist) Run(ctx *Context) error { + cfg, err := kjspkg.GetConfig(ctx.Path, false) + if err != nil { + warn("dev dist migrates a kubejs modpack to a kjspkg package.") + return err + } + + targetDir := filepath.Join(ctx.Path, c.Target) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return err + } + + scriptFiles := kjspkg.GetStandaloneScripts(ctx.Path) + if err := NewMultiSelect("Select scripts to include in your package", func(opts *[]huh.Option[string]) { + for _, file := range scriptFiles { + path, err := filepath.Rel(ctx.Path, file) + if err != nil { + path = file + } + *opts = append(*opts, huh.NewOption(path, path)) + } + }, &c.Scripts, "You may scroll to discover more scripts!", "NONE", func(input []string) error { + return nil + }); err != nil { + return err + } + + assetFiles := kjspkg.GetAssets(ctx.Path) + if err := NewMultiSelect("Select assets to include in your package", func(opts *[]huh.Option[string]) { + for _, file := range assetFiles { + path, err := filepath.Rel(ctx.Path, file) + if err != nil { + path = file + } + *opts = append(*opts, huh.NewOption(path, path)) + } + }, &c.Assets, "You may scroll to discover more assets!", "NONE", func(input []string) error { + return nil + }); err != nil { + return err + } + + installed := []string{} + for dep := range cfg.Installed { + installed = append(installed, dep) + } + init := CDevInit{ + Package: c.Package, + DependencyFilter: installed, + UseFilter: true, + } + if err := init.Run(&Context{Path: c.Target}); err != nil { + return err + } + + info("Just a moment, migrating your package...") + + types := [][]string{c.Scripts, c.Assets} + for _, srcType := range types { + for _, file := range srcType { + srcPath := filepath.Join(ctx.Path, file) + dstPath := filepath.Join(targetDir, file) + + if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { + return err + } + + src, err := os.Open(srcPath) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(dstPath) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return err + } + } + } + + info("Package created at %s", targetDir) + return nil +} diff --git a/cmd/kjspkg/c_dev_init.go b/cmd/kjspkg/c_dev_init.go new file mode 100644 index 0000000..af9ebb7 --- /dev/null +++ b/cmd/kjspkg/c_dev_init.go @@ -0,0 +1,114 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + + "github.com/Modern-Modpacks/kjspkg/pkg/commons" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "github.com/charmbracelet/huh" +) + +type CDevInit struct { + kjspkg.Package + DependencyFilter []string `help:"Filter dependencies by ID"` + UseFilter bool `help:"Use dependency filter to filter listed dependencies?"` +} + +func (c *CDevInit) Run(ctx *Context) error { + info("Let's get you started with a new KJSPKG package!") + if empty, err := commons.IsEmpty(ctx.Path); err != nil || !empty { + return fmt.Errorf("the provided directory (current or --path: %s) is not empty", ctx.Path) + } + + loc, err := LoadLocators() + if err != nil { + return err + } + if !c.UseFilter { + c.DependencyFilter = []string{} + for dep := range loc { + c.DependencyFilter = append(c.DependencyFilter, dep) + } + } + + if err := NewInput("What does this package do?", func(input string) error { + if input == "" { + return fmt.Errorf("description cannot be empty") + } + return nil + }, &c.Description); err != nil { + return err + } + + if err := NewInput("What's your name?", func(input string) error { + if input == "" { + return fmt.Errorf("no authors?") + } + return nil + }, &c.Author); err != nil { + return err + } + + if err := NewMultiSelect("What versions of the game does this support?", func(opts *[]huh.Option[int]) { + for k, v := range kjspkg.Versions { + *opts = append(*opts, huh.NewOption(k, v)) + } + }, &c.Versions, "", -1, func(input []int) error { + if len(input) == 0 { + return fmt.Errorf("at least one version is required") + } + return nil + }); err != nil { + return err + } + + if err := NewMultiSelect("What modloaders does this support?", func(opts *[]huh.Option[kjspkg.ModLoader]) { + for _, l := range kjspkg.ModLoaders { + *opts = append(*opts, huh.NewOption(l.String(), l)) + } + }, &c.ModLoaders, "", "NONE", func(input []kjspkg.ModLoader) error { + if len(input) == 0 { + return fmt.Errorf("at least one modloader is required") + } + return nil + }); err != nil { + return err + } + + if err := NewMultiSelect("What does this depend on?", func(opts *[]huh.Option[string]) { + for _, dep := range c.DependencyFilter { + *opts = append(*opts, huh.NewOption(dep, dep)) + } + }, &c.Dependencies, "You may scroll to discover more!", "NONE", func(input []string) error { + return nil + }); err != nil { + return err + } + + if err := NewMultiSelect("What does this conflict with?", func(opts *[]huh.Option[string]) { + for dep := range loc { + if !slices.Contains(c.Dependencies, dep) { + *opts = append(*opts, huh.NewOption(dep, dep)) + } + } + }, &c.Incompatibilities, "You may scroll to discover more!", "NONE", func(input []string) error { + return nil + }); err != nil { + return err + } + + pkgBytes, err := json.MarshalIndent(c.Package, "", " ") + if err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(ctx.Path, ".kjspkg"), pkgBytes, 0644); err != nil { + return err + } + info("Package manifest created at %s", ctx.Path) + return nil +} diff --git a/cmd/kjspkg/c_dev_publish.go b/cmd/kjspkg/c_dev_publish.go new file mode 100644 index 0000000..28ee669 --- /dev/null +++ b/cmd/kjspkg/c_dev_publish.go @@ -0,0 +1,8 @@ +package main + +// TODO: imma do publish later. +// there are too many variables at play here. +// i'll need to think about it. +// thanks cursor ai for writing this. +// no problem, that's what i'm here for. +// :3 diff --git a/cmd/kjspkg/c_dev_run.go b/cmd/kjspkg/c_dev_run.go new file mode 100644 index 0000000..b7cd7e7 --- /dev/null +++ b/cmd/kjspkg/c_dev_run.go @@ -0,0 +1,170 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "strings" + + "github.com/Modern-Modpacks/kjspkg/assets" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" +) + +type CDevRun struct { + Loader kjspkg.ModLoader `arg:"" help:"Mod loader"` + Version string `arg:"" help:"Game version"` + Launcher string `help:"CLI name of launcher to use" default:"prismlauncher"` +} + +func (c *CDevRun) Run(ctx *Context) error { + warn("This command is experimental and may not work as expected.") + + ver, ok := kjspkg.Versions[c.Version] + if !ok { + return fmt.Errorf("not a game version, expected version like '1.18'") + } + + pkg, err := kjspkg.PackageFromPath(ctx.Path) + if err != nil { + return err + } + + if !slices.Contains(pkg.Versions, ver) { + return fmt.Errorf("package does not support version %s", c.Version) + } + if !slices.Contains(pkg.ModLoaders, c.Loader) { + return fmt.Errorf("package does not support loader %s", c.Loader) + } + + launcherPath, err := getLauncherPath(c.Launcher) + if err != nil { + return err + } + + instanceName := fmt.Sprintf("kjspkg%d%s", ver, c.Loader.Identifier()) + instancePath := getInstancePath(c.Launcher, instanceName) + + data, err := assets.Instances.ReadFile("instances/" + instanceName + ".zip") + if err != nil { + return fmt.Errorf("not supported for this version/loader combination: %s", instanceName) + } + + putPath := filepath.Join(ctx.Path, instanceName+".zip") + if _, err := os.Stat(instancePath); os.IsNotExist(err) { + info("Creating new test instance...") + info("Please name your instance '%s'!!!", instanceName) + info("Then configure the instance (if required) and CLOSE the launcher.") + info("Rerun the same command after.") + + if err := os.WriteFile(putPath, data, 0666); err != nil { + return err + } + + cmd := exec.Command(launcherPath, "-I", putPath) + return cmd.Run() + } else { + os.Remove(putPath) + } + + kubejsPath := filepath.Join(instancePath, ".minecraft", "kubejs") + if err := os.MkdirAll(kubejsPath, 0755); err != nil { + return err + } + + // we have to assume this pack contains other kjspkg packages + // this is really hacky, but it works :) + cu := CUninit{Confirm: true} + if err := cu.Run(&Context{Path: kubejsPath}); err != nil { + return err + } + ci := CInit{Version: ver, Modloader: c.Loader} + if err := ci.Run(&Context{Path: kubejsPath}); err != nil { + return err + } + if err := kjspkg.InstallEnsureKube(kubejsPath); err != nil { + return err + } + + cin := CInstall{Packages: pkg.Dependencies, TrustExternal: true, NoModCheck: true} + if err := cin.Run(&Context{Path: kubejsPath}); err != nil { + return err + } + + cfg, err := kjspkg.GetConfig(kubejsPath, false) + if err != nil { + return err + } + + pkgAssets, err := kjspkg.InstallCopy(kubejsPath, ctx.Path) + if err != nil { + return err + } + + cfg.Installed["kjspkg-dev-test"] = pkgAssets + if err := kjspkg.SetConfig(kubejsPath, cfg); err != nil { + return err + } + + // if dependencies contain mods, warn the user + for _, dep := range pkg.Dependencies { + if strings.HasPrefix(dep, "mod:") { + warn("This package depends on mods mods, which may have to be manually installed: %s", dep) + break + } + } + + info("Launching test instance...") + cmd := exec.Command(launcherPath, "-l", instanceName) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func getLauncherPath(launcher string) (string, error) { + launcher = strings.ToLower(launcher) + + if runtime.GOOS == "windows" { + switch launcher { + case "prismlauncher": + return filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "PrismLauncher", "prismlauncher.exe"), nil + case "multimc": + return filepath.Join(os.Getenv("USERPROFILE"), "Downloads", "MultiMC", "MultiMC.exe"), nil // wtf gcat + } + } else { + switch launcher { + case "prismlauncher": + return "/usr/bin/prismlauncher", nil + case "multimc": + return "/opt/multimc/run.sh", nil + } + } + + return "", fmt.Errorf("unsupported launcher: %s", launcher) +} + +func getInstancePath(launcher, instanceName string) string { + if runtime.GOOS == "windows" { + switch strings.ToLower(launcher) { + case "prismlauncher": + return filepath.Join(os.Getenv("APPDATA"), "PrismLauncher", "instances", instanceName) + case "multimc": + return filepath.Join(os.Getenv("USERPROFILE"), "Downloads", "MultiMC", "instances", instanceName) // wtf gcat + } + } else { + switch strings.ToLower(launcher) { + case "prismlauncher": + return filepath.Join(os.Getenv("HOME"), ".local", "share", "PrismLauncher", "instances", instanceName) + case "multimc": + return filepath.Join(os.Getenv("HOME"), ".local", "share", "MultiMC", "instances", instanceName) + } + } + return "" +} diff --git a/cmd/kjspkg/c_fetch.go b/cmd/kjspkg/c_fetch.go new file mode 100644 index 0000000..d0c245d --- /dev/null +++ b/cmd/kjspkg/c_fetch.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "strings" + "unicode/utf8" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" +) + +type CFetch struct{} + +func (c *CFetch) Run(ctx *Context) error { + cfg, err := kjspkg.GetConfig(ctx.Path, false) + if err != nil { + return err + } + + mods, err := kjspkg.GetMods(ctx.Path, true, true) + if err != nil { + return err + } + + logoLines := strings.Split(kjspkg.Logo, "\n") + type Line struct{ K, V string } + infoLines := []Line{ + {K: "version ", V: kjspkg.GetVersionString(cfg.Version)}, + {K: "loader ", V: cfg.ModLoader.String()}, + {K: "pkgs ", V: fmt.Sprint(len(cfg.Installed))}, + {K: "kube ", V: fmt.Sprint(mods["kubejs"])}, + {K: "rhino ", V: fmt.Sprint(mods["rhino"])}, + {K: "arch ", V: fmt.Sprint(mods["architectury"])}, + } + + for i := 0; i < max(len(logoLines), len(infoLines)+1); i++ { + if len(logoLines) > i { + fmt.Print(colr.Purple(logoLines[i] + " ")) + } else { + fmt.Print(colr.Purple(strings.Repeat(" ", utf8.RuneCountInString(logoLines[1])+2))) + } + if i == 0 { + fmt.Printf(colr.Bold("KJSPKG")+colr.Dim(" @ %s"), ctx.Path) + } else if len(infoLines) >= i { + info := infoLines[i-1] + fmt.Printf(colr.Purple("%s")+"%s", info.K, info.V) + } + fmt.Printf("\n") + } + return nil +} diff --git a/cmd/kjspkg/c_init.go b/cmd/kjspkg/c_init.go new file mode 100644 index 0000000..f7d4152 --- /dev/null +++ b/cmd/kjspkg/c_init.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "github.com/charmbracelet/huh" +) + +type CInit struct { + Version int `help:"Version to use (as number, 1.16 -> 16 - 10 -> 6)"` + Modloader kjspkg.ModLoader `help:"Mod loader (forge, fabric, quilt, neoforge)"` +} + +func (c *CInit) Run(ctx *Context) error { + if !kjspkg.IsKube(ctx.Path) { + return fmt.Errorf("are you sure this is the kubejs directory?") + } + if kjspkg.HasConfig(ctx.Path) { + return fmt.Errorf("config already exists, use 'uninit' instead") + } + + cfg := kjspkg.DefaultConfig() + + err := NewSelect("Pick a game version", func(opts *[]huh.Option[int]) { + for _, ver := range kjspkg.VersionsInOrder { + *opts = append(*opts, huh.NewOption(ver, kjspkg.Versions[ver])) + } + }, &c.Version, "") + if err != nil { + return err + } + info("Game version: %s", kjspkg.GetVersionString(c.Version)) + cfg.Version = c.Version + + err = NewSelect("Pick a mod loader", func(opts *[]huh.Option[kjspkg.ModLoader]) { + for _, loader := range kjspkg.ModLoaders { + *opts = append(*opts, huh.NewOption(loader.StringLong(), loader)) + } + }, &c.Modloader, "") + if err != nil { + return err + } + info("Mod loader: %s", c.Modloader.String()) + cfg.ModLoader = c.Modloader + + err = kjspkg.SetConfig(ctx.Path, cfg) + if err != nil { + return err + } + + info("Done! Use 'install' to install your first package.") + return nil +} diff --git a/cmd/kjspkg/c_install.go b/cmd/kjspkg/c_install.go new file mode 100644 index 0000000..f5155dc --- /dev/null +++ b/cmd/kjspkg/c_install.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "golang.org/x/sync/errgroup" +) + +type CInstall struct { + Packages []string `arg:"" help:"The packages to install ('github:author/repo$path@branch' syntax supported)"` + TrustExternal bool `help:"If GitHub packages should be trusted (experimental! updates are only partially supported)"` + NoModCheck bool `help:"If mod dependency check should be skipped"` + Skipmissing bool `help:"Skips dependencies that can't be found"` + Update bool `help:"If packages already downloaded should be updated (same as 'update')"` + NoInstall bool `help:"If new packages should not be installed (requires --update)" hidden:""` +} + +func (c *CInstall) Run(ctx *Context) error { + cfg, err := kjspkg.GetConfig(ctx.Path, false) + if err != nil { + return err + } + + if c.NoInstall && !c.Update { + return fmt.Errorf("--no-install requires --update") + } + + refs, err := LoadLocators() // this caches them + if err != nil { + return err + } + + info("Loading installed mods") + mods, err := kjspkg.GetMods(ctx.Path, false, true) + if err != nil { + return err + } + if c.NoModCheck { + mods = nil + } + + toInstall := map[string]kjspkg.PackageLocator{} + info("Resolving dependencies") + for _, id := range c.Packages { + list, err := kjspkg.CollectPackages(refs, cfg, id, c.TrustExternal, c.Update, mods, c.Skipmissing) + if err != nil { + return err + } + for dep, loc := range list { + if c.NoInstall { + if _, ok := cfg.Installed[loc.Id]; !ok { + return fmt.Errorf("package %s is not installed", loc.Id) + } + } + toInstall[dep] = loc + fmt.Printf(colr.Dim(" >")+" %s\n", loc.Id) + } + } + + errs, _ := errgroup.WithContext(context.Background()) + info(If(c.NoInstall, "Updating", "Installing")) + os.RemoveAll(filepath.Join(ctx.Path, "tmp")) + for _, ref := range toInstall { + errs.Go(func() error { + startTime := time.Now() + assets, err := kjspkg.Install(ctx.Path, ref, cfg, true, If(ctx.Verbose, os.Stdout, nil)) + tookTime := time.Since(startTime).Milliseconds() + fmt.Printf(colr.Green(" +")+" %s "+colr.Dim("(took %dms)\n"), ref.Id, tookTime) + cfg.Installed[ref.Id] = assets + return err + }) + } + if err := errs.Wait(); err != nil { + return err + } + + os.RemoveAll(filepath.Join(ctx.Path, "tmp")) + info("Successfully %s %d package(s)!", If(c.NoInstall, "updated", "installed"), len(toInstall)) + kjspkg.SetConfig(ctx.Path, cfg) + return nil +} diff --git a/cmd/kjspkg/c_list.go b/cmd/kjspkg/c_list.go new file mode 100644 index 0000000..df14956 --- /dev/null +++ b/cmd/kjspkg/c_list.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "github.com/lithammer/fuzzysearch/fuzzy" +) + +type CList struct { + // Count bool `optional:"" help:"Show package count instead?"` + All bool `help:"Also show uninstalled packages?"` + Search string `help:"The search query"` +} + +func (c *CList) Run(ctx *Context) error { + filtered := []string{} + if !c.All { + cfg, err := kjspkg.GetConfig(ctx.Path, false) + if err != nil { + return err + } + + info("Installed: %d", len(cfg.Installed)) + for id := range cfg.Installed { + if fuzzy.MatchNormalizedFold(c.Search, id) { + filtered = append(filtered, id) + } + } + } else { + loc, err := LoadLocators() + if err != nil { + return err + } + + info("Available: %d", len(loc)) + for id := range loc { + if fuzzy.MatchNormalizedFold(c.Search, id) { + filtered = append(filtered, id) + } + } + } + for _, id := range filtered { + fmt.Printf(colr.Dim(" -")+" %s\n", id) + } + return nil +} + +type CListall struct { + Search string `help:"The search query"` +} + +func (c *CListall) Run(ctx *Context) error { + cmd := CList{ + All: true, + Search: c.Search, + } + return cmd.Run(ctx) +} + +type CSearch struct { + Query string `arg:"" help:"The search query"` +} + +func (c *CSearch) Run(ctx *Context) error { + cmd := CListall{ + Search: c.Query, + } + return cmd.Run(ctx) +} diff --git a/cmd/kjspkg/c_pkg.go b/cmd/kjspkg/c_pkg.go new file mode 100644 index 0000000..de304b4 --- /dev/null +++ b/cmd/kjspkg/c_pkg.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/commons" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" +) + +type CPkg struct { + Package string `arg:"" help:"The package to inspect"` + Script bool `help:"Return JSON instead?"` +} + +func (c *CPkg) Run(ctx *Context) error { + if c.Script { + doLog = false + } + + pkg, loc, err := LoadPackageById(c.Package, true) + if err != nil { + return err + } + + loaders := []string{} + for _, l := range pkg.ModLoaders { + loaders = append(loaders, l.String()) + } + versions := []string{} + for _, i := range pkg.Versions { + versions = append(versions, kjspkg.GetVersionString(i)) + } + + if c.Script { + b, err := json.Marshal(pkg) + if err != nil { + return err + } + fmt.Printf("%s", string(b)) + return nil + } + + fmt.Printf(colr.Bold(colr.Blue("%s"))+" by "+colr.Blue("%s")+"\n", commons.TitleCase(c.Package), pkg.Author) + fmt.Printf("%s\n", pkg.Description) + fmt.Printf("\n") + fmt.Printf(colr.Blue("Lookup:")+" https://kjspkglookup.modernmodpacks.site/#%s\n", c.Package) + fmt.Printf(colr.Blue("GitHub:")+" %s\n", loc.URLFrontend()) + fmt.Printf(colr.Blue("Views:")+" %-6d "+colr.Blue("Downloads:")+" %-6d\n", pkg.Views, pkg.Downloads) + fmt.Printf("\n") + fmt.Printf(colr.Blue("Available for:") + "\n") + fmt.Printf(colr.Blue(" Modloaders:")+" %s\n", strings.Join(loaders, ", ")) + fmt.Printf(colr.Blue(" Versions:")+" %s\n", strings.Join(versions, ", ")) + fmt.Printf("\n") + fmt.Printf(colr.Blue("Dependencies:")+" %s\n", kjspkg.DepsJoin(pkg.Dependencies)) + fmt.Printf(colr.Blue("Incompatibilities:")+" %s\n", kjspkg.DepsJoin(pkg.Incompatibilities)) + + return nil +} diff --git a/cmd/kjspkg/c_remove.go b/cmd/kjspkg/c_remove.go new file mode 100644 index 0000000..df995a9 --- /dev/null +++ b/cmd/kjspkg/c_remove.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "golang.org/x/sync/errgroup" +) + +type CRemove struct { + Packages []string `arg:"" help:"The packages to remove"` + Skipmissing bool `help:"Skips removals of packages that can't be found"` +} + +func (c *CRemove) Run(ctx *Context) error { + cfg, err := kjspkg.GetConfig(ctx.Path, false) + if err != nil { + return err + } + + info("Removing") + for i, id := range c.Packages { + _, ok := cfg.Installed[id] + if !ok && c.Skipmissing { + c.Packages = remove(c.Packages, i) + } else if !ok { + return fmt.Errorf("package not installed: %s", id) + } + } + + errs, _ := errgroup.WithContext(context.Background()) + for _, id := range c.Packages { + errs.Go(func() error { + err := kjspkg.Remove(ctx.Path, id, cfg) + fmt.Printf(colr.Red(" -")+" %s\n", id) + delete(cfg.Installed, id) + return err + }) + } + if err := errs.Wait(); err != nil { + return err + } + + info("Successfully removed %d package(s)!", len(c.Packages)) + kjspkg.SetConfig(ctx.Path, cfg) + return nil +} diff --git a/cmd/kjspkg/c_uninit.go b/cmd/kjspkg/c_uninit.go new file mode 100644 index 0000000..c9e9693 --- /dev/null +++ b/cmd/kjspkg/c_uninit.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "golang.org/x/sync/errgroup" +) + +type CUninit struct { + Confirm bool +} + +func (c *CUninit) Run(ctx *Context) error { + if !c.Confirm { + warn(colr.Red("DOING THIS WILL REMOVE ALL PACKAGES AND UNINSTALL KJSPKG COMPLETELY!")) + confirm := false + if err := NewConfirm("Are you sure?", "You can't undo this, which may cause data loss!", &confirm, false); err != nil { + return err + } + if !confirm { + return fmt.Errorf("aborted") + } + } + + cfg, err := kjspkg.GetConfig(ctx.Path, true) + if err != nil { + warn("oopsie woopsie! %v", err) + return nil + } + + info("Removing packages") + errs, _ := errgroup.WithContext(context.Background()) + for id := range cfg.Installed { + errs.Go(func() error { + err := kjspkg.Remove(ctx.Path, id, cfg) + fmt.Printf(colr.Red(" -")+" %s\n", id) + delete(cfg.Installed, id) + return err + }) + } + if err := errs.Wait(); err != nil { + return err + } + + info("Deleting stuff") + for _, name := range kjspkg.ScriptDirs { + os.RemoveAll(filepath.Join(ctx.Path, name, ".kjspkg")) + } + os.Remove(filepath.Join(ctx.Path, ".kjspkg")) + + info("Uninitialized successfully!") + return nil +} diff --git a/cmd/kjspkg/c_update.go b/cmd/kjspkg/c_update.go new file mode 100644 index 0000000..6084e12 --- /dev/null +++ b/cmd/kjspkg/c_update.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "slices" + + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" +) + +type CUpdate struct { + Packages []string `optional:"" arg:"" help:"The packages to update ('github:author/repo$path@branch' syntax supported)"` + TrustExternal bool `help:"If GitHub packages should be trusted"` + NoModCheck bool `help:"If mod dependency check should be skipped (experimental)"` + Skipmissing bool `help:"Skips dependencies that can't be found"` + All bool `help:"Update all packages ('update *'/'updateall' use this)"` +} + +func (c *CUpdate) Run(ctx *Context) error { + packages := []string{} + if slices.Contains(c.Packages, "*") { + if len(c.Packages) > 1 { + return fmt.Errorf("cannot combine * with others") + } + + cfg, err := kjspkg.GetConfig(ctx.Path, true) + if err != nil { + return err + } + + for pkg := range cfg.Installed { + packages = append(packages, pkg) + } + } else { + packages = c.Packages + } + + cmd := CInstall{ + Packages: packages, + TrustExternal: c.TrustExternal, + NoModCheck: c.NoModCheck, + Skipmissing: c.Skipmissing, + Update: true, + NoInstall: true, + } + return cmd.Run(ctx) +} + +func (c *CUpdate) AfterApply() error { + if !c.All && len(c.Packages) == 0 { + return fmt.Errorf("no packages specified") + } + + if c.All && len(c.Packages) > 0 { + return fmt.Errorf("cannot upgrade specific packages if --all specified") + } + + if c.All { + c.Packages = []string{"*"} + } + + return nil +} + +type CUpdateAll struct { + TrustExternal bool `help:"If GitHub packages should be trusted"` + NoModCheck bool `help:"If mod dependency check should be skipped (experimental)"` + Skipmissing bool `help:"Skips dependencies that can't be found"` +} + +func (c *CUpdateAll) Run(ctx *Context) error { + cmd := CUpdate{ + Packages: []string{"*"}, + TrustExternal: c.TrustExternal, + NoModCheck: c.NoModCheck, + Skipmissing: c.Skipmissing, + } + return cmd.Run(ctx) +} diff --git a/cmd/kjspkg/main.go b/cmd/kjspkg/main.go new file mode 100644 index 0000000..6cef6f7 --- /dev/null +++ b/cmd/kjspkg/main.go @@ -0,0 +1,85 @@ +// Hello gcast! +// I hope you're well. +// I'm sorry for the mess. I'll try to clean it up. +// alas, it is what it is. thanks for your patience. +// the entire cmd/kjspkg directory is a shithole. +// all the best, +// - mr erik "tizu" erikson + +package main + +import ( + "fmt" + "os" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" + "github.com/alecthomas/kong" +) + +type Context struct { + Verbose bool + Path string +} + +var cli struct { + Verbose bool `help:"Print verbose"` + Quiet bool `help:"No non-error output" short:"q" hidden:""` + Path string `help:"Path to KubeJS directory (defaults to current)" default:"." type:"existingdir"` + Source string `help:"URL source to package list (takes preference over KJSPKG_REPO envvar)" type:"url"` + + Install CInstall `cmd:"" help:"Installs packages" aliases:"download"` + Remove CRemove `cmd:"" help:"Removes packages" aliases:"uninstall"` + Update CUpdate `cmd:"" help:"Update packages (same as 'install --update')"` + Updateall CUpdateAll `cmd:"" help:"Update all packages (same as 'update *')"` + List CList `cmd:"" help:"Lists packages (and the count of them)"` + Fetch CFetch `cmd:"" help:"neofetch but different"` + Pkg CPkg `cmd:"" help:"Shows info about the package"` + Listall CListall `cmd:"" help:"Lists online packages (same as 'list --all')" aliases:"all"` + Search CSearch `cmd:"" help:"Search online packages (same as 'listall --search')"` + Init CInit `cmd:"" help:"Initialize a KJSPKG env"` + Uninit CUninit `cmd:"" help:"Remove all KJSPKG-related things in your project"` + Reload CNotImplemented `cmd:"" hidden:"" aliases:"refresh"` + Dev struct { + Init CDevInit `cmd:"" help:"Initialize a new KJSPKG package"` + Run CDevRun `cmd:"" help:"Runs your package in a test Minecraft instance"` + Dist CDevDist `cmd:"" help:"Creates a package from your packs' KubeJS folder"` + Test CNotImplemented `cmd:"" hidden:""` + } `cmd:"" help:"Helper functions for developing KJSPKG packages"` + Gui CNotImplemented `cmd:"" hidden:""` +} + +func main() { + ctx := kong.Parse(&cli, + kong.Name("kjspkg"), + kong.Description("A KubeJS package manager"), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + Summary: false, + })) + + if cli.Quiet && cli.Verbose { + fmt.Printf(" "+colr.BlackOnRed(" :( ")+" %v\n", "Cannot use -q in combination with --verbose") + os.Exit(1) + } + if cli.Quiet { + os.Stdout.Close() + } + + if os.Getenv("KJSPKG_REPO") != "" { + kjspkg.PackageList = os.Getenv("KJSPKG_REPO") + } + if cli.Source != "" { + kjspkg.PackageList = cli.Source + } + + err := ctx.Run(&Context{ + Path: cli.Path, + Verbose: cli.Verbose, + }) + if err != nil { + fmt.Printf(" "+colr.BlackOnRed(" :( ")+" %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/kjspkg/selectors.go b/cmd/kjspkg/selectors.go new file mode 100644 index 0000000..619f590 --- /dev/null +++ b/cmd/kjspkg/selectors.go @@ -0,0 +1,112 @@ +package main + +import ( + "slices" + + "github.com/charmbracelet/huh" +) + +// NewSelect is a wrapper function that displays a selection prompt only if +// the provided variable doesn't already match one of the available options. +func NewSelect[T comparable]( + title string, + getOptions func(opts *[]huh.Option[T]), + selected *T, + description string, +) error { + options := []huh.Option[T]{} + getOptions(&options) + + for _, opt := range options { + if *selected == opt.Value { + return nil + } + } + + sel := huh.NewSelect[T](). + Title(title). + Options(options...). + Value(selected) + if description != "" { + sel.Description(description) + } + + return sel.WithTheme(huh.ThemeBase16()).Run() +} + +// NewMultiSelect is a wrapper function that displays a selection prompt only if +// the provided variable doesn't already match one of the available options. +func NewMultiSelect[T comparable]( + title string, + getOptions func(opts *[]huh.Option[T]), + selected *[]T, + description string, + preselectedEmpty T, + validate func(input []T) error, +) error { + options := []huh.Option[T]{} + getOptions(&options) + + if len(*selected) == 1 { + sel := *selected + if sel[0] == preselectedEmpty { + return nil + } + } + + if len(*selected) > 0 { + allValid := true + for _, opt := range *selected { + if !slices.ContainsFunc(options, func(e huh.Option[T]) bool { return e.Value == opt }) { + allValid = false + break + } + } + if allValid { + return nil + } + } + + sel := huh.NewMultiSelect[T](). + Title(title). + Options(options...). + Validate(validate). + Height(9). // 8 + Value(selected) + if description != "" { + sel.Description(description) + } + + return sel.WithTheme(huh.ThemeBase16()).Run() +} + +// NewConfirm is a wrapper function that displays a confirmation prompt. +func NewConfirm(title string, description string, selected *bool, askIf bool) error { + if !askIf == *selected { + return nil + } + return huh.NewConfirm(). + Title(title). + Description(description). + Value(selected). + WithTheme(huh.ThemeBase16()). + Run() +} + +// NewInput is a wrapper function that displays a selection prompt only if +// the provided variable isn't empty and can be validated. +func NewInput( + title string, + validate func(input string) error, + selected *string, +) error { + if *selected != "" && validate(*selected) == nil { + return nil + } + + sel := huh.NewInput(). + Title(title). + Value(selected). + Validate(validate) + return sel.WithTheme(huh.ThemeBase16()).Run() +} diff --git a/cmd/kjspkg/utils.go b/cmd/kjspkg/utils.go new file mode 100644 index 0000000..90bda08 --- /dev/null +++ b/cmd/kjspkg/utils.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + + "g.tizu.dev/colr" + "github.com/Modern-Modpacks/kjspkg/pkg/kjspkg" +) + +var doLog = true + +func info(format string, a ...any) { + if doLog { + fmt.Printf(colr.Blue(":: ")+format+"\n", a...) + } +} +func warn(format string, a ...any) { + if doLog { + fmt.Printf(colr.Yellow(":: ")+format+"\n", a...) + } +} + +func remove[T comparable](slice []T, s int) []T { + return append(slice[:s], slice[s+1:]...) +} + +func LoadLocators() (map[string]kjspkg.PackageLocator, error) { + var packages map[string]kjspkg.PackageLocator + packages, err := kjspkg.GetPackageList() + if cli.Verbose { + info("Parsed package list") + } + return packages, err +} + +func LoadPackage(ref kjspkg.PackageLocator, withStats bool) (kjspkg.Package, error) { + var pkg kjspkg.Package + pkg, err := kjspkg.GetPackage(ref, withStats) + if cli.Verbose { + info("Obtained package metadata") + } + return pkg, err +} + +func LoadPackageById(id string, withStats bool) (kjspkg.Package, kjspkg.PackageLocator, error) { + var pkg kjspkg.Package + var loc kjspkg.PackageLocator + + locs, err := LoadLocators() + if err != nil { + return pkg, loc, err + } + + loc, ok := locs[id] + if !ok { + return pkg, loc, fmt.Errorf("package does not exist: %s", id) + } + + pkg, err = LoadPackage(loc, withStats) + return pkg, loc, err +} + +func If[T any](cond bool, a, b T) T { + if cond { + return a + } + return b +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fe594af --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/Modern-Modpacks/kjspkg + +go 1.23.2 + +require ( + github.com/alecthomas/kong v1.4.0 + golang.org/x/text v0.20.0 +) + +require golang.org/x/sync v0.9.0 + +require ( + g.tizu.dev/colr v1.0.0 + github.com/charmbracelet/huh v0.6.0 + github.com/lithammer/fuzzysearch v1.1.8 +) + +require github.com/otiai10/copy v1.14.0 + +require ( + github.com/BurntSushi/toml v1.4.0 + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6d3e18d --- /dev/null +++ b/go.sum @@ -0,0 +1,102 @@ +g.tizu.dev/colr v1.0.0 h1:IoC8JwSB7C6amJztrN5HtYHXPUYj/uPoqzeldnTKMsY= +g.tizu.dev/colr v1.0.0/go.mod h1:t3EYIVRrg7w9eNmA2OGOdCOtiGPd5DyvockE9NRZCPs= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= +github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/install.bat b/install.bat deleted file mode 100644 index c597429..0000000 --- a/install.bat +++ /dev/null @@ -1,12 +0,0 @@ -@echo off - -def %localappdata%\Microsoft\WindowsApps\kjspkg >nul 2>&1 -def %localappdata%\Microsoft\WindowsApps\kjspkg.py >nul 2>&1 -curl https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/run.bat > %localappdata%\Microsoft\WindowsApps\kjspkg.bat -curl https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/app.py > %localappdata%\Microsoft\WindowsApps\kjspkg.py - -curl -s https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/requirements.txt > kjspkgreqs.txt -python -m pip -q install -r kjspkgreqs.txt >nul 2>&1 -del kjspkgreqs.txt - -msg "%username%" "KJSPKG install successful! Reload your terminal for the command to work" diff --git a/install.ps1 b/install.ps1 new file mode 100755 index 0000000..b3174e2 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,24 @@ +$INFO = "::" +$WARN = "::" + +Write-Host "$INFO Getting ready" + +$REPO = "Modern-Modpacks/kjspkg" +$LATEST_RELEASE_URL = (Invoke-RestMethod -Uri "https://api.github.com/repos/$REPO/releases/latest").assets | Where-Object { $_.browser_download_url -like "*kjspkg_windows_amd64.exe" } | Select-Object -ExpandProperty browser_download_url + +if (-not $LATEST_RELEASE_URL) { + Write-Host "$WARN Failed to fetch the latest release. Please check your internet connection!" + exit 1 +} + +Write-Host "$INFO Downloading KJSPKG" +$installPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps" + +try { + Invoke-WebRequest -Uri $LATEST_RELEASE_URL -OutFile "$installPath\kjspkg.exe" +} catch { + Write-Host "$WARN Download failed. Please try again." + exit 1 +} + +Write-Host "$INFO Done! Run 'kjspkg' to get started" diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 45613cc..942b228 --- a/install.sh +++ b/install.sh @@ -1,14 +1,32 @@ -if [ `id -u` != 0 ] -then +#!/bin/bash + +INFO="\033[34m::\033[0m" +WARN="\033[33m::\033[0m" + +if [ "$(id -u)" != 0 ]; then + echo -e "$INFO Escalation is required to install KJSPKG globally" sudo -v + if [ $? -ne 0 ]; then + echo -e "$WARN Failed to escalate privileges. Please run the script again." + exit 1 + fi fi -echo "Installation started..." +echo -e "$INFO Getting ready" + +REPO="Modern-Modpacks/kjspkg" +LATEST_RELEASE_URL=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep -oP '"browser_download_url": "\K[^"]*kjspkg_linux_amd64') +if [ -z "$LATEST_RELEASE_URL" ]; then + echo -e "$WARN Failed to fetch the latest release. Please check your internet connection!" + exit 1 +fi -sudo rm -f /usr/local/bin/kjspkg +echo -e "$INFO Downloading KJSPKG" +sudo curl --progress-bar -L -o /usr/local/bin/kjspkg "$LATEST_RELEASE_URL" | cat +if [ $? -ne 0 ]; then + echo -e "$WARN Download failed. Please try again." + exit 1 +fi -sudo sh -c "curl -s https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/app.py > /usr/local/bin/kjspkg" -python3 -m pip -q install $(curl -s https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/requirements.txt) > /dev/null sudo chmod +x /usr/local/bin/kjspkg - -echo "Done!" \ No newline at end of file +echo -e "$INFO Done! Run 'kjspkg' to get started" diff --git a/pkg/commons/commons.go b/pkg/commons/commons.go new file mode 100644 index 0000000..4442c0f --- /dev/null +++ b/pkg/commons/commons.go @@ -0,0 +1,39 @@ +package commons + +import ( + "fmt" + "io" + "os" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func TitleCase(input string) string { + input = strings.ReplaceAll(input, "-", " ") + input = strings.ReplaceAll(input, "_", " ") + titleCase := cases.Title(language.English) + return titleCase.String(input) +} + +func EN200(err error, ctx string) error { + if err != nil { + return err + } + return fmt.Errorf("request did not return 200: %s", ctx) +} + +func IsEmpty(name string) (bool, error) { + f, err := os.Open(name) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err // Either not empty or error, suits both cases +} diff --git a/pkg/kjspkg/collect.go b/pkg/kjspkg/collect.go new file mode 100644 index 0000000..2075e2e --- /dev/null +++ b/pkg/kjspkg/collect.go @@ -0,0 +1,94 @@ +package kjspkg + +import ( + "fmt" + "slices" + "strings" + + "github.com/Modern-Modpacks/kjspkg/pkg/commons" +) + +// *mods* may be nil, in which case mod check gets ignored +// skipMissing == true may return partial dependency trees or an empty array if they can't be resolved. +func CollectPackages(refs map[string]PackageLocator, cfg *Config, id string, trustExternal, update bool, mods map[string]string, skipMissing bool) (map[string]PackageLocator, error) { + toInstall := map[string]PackageLocator{} + + ref, err := PackageLocatorFromPointer(id, refs, trustExternal) + if err != nil { + if skipMissing { + return toInstall, nil + } + return nil, err + } + + if _, ok := cfg.Installed[ref.Id]; ok { + if !update { + return toInstall, nil + } + } + + pkg, err := GetPackage(ref, false) + if err != nil { + if skipMissing { + return toInstall, nil + } + return nil, err + } + + if err := CollectEnsureVersion(pkg, cfg); err != nil { + return nil, err + } + + for _, dep := range pkg.Incompatibilities { + if !strings.HasPrefix(dep, "mod:") { + if _, ok := cfg.Installed[dep]; ok { + return nil, fmt.Errorf("%s is incompatible with %s", ref.Id, dep) + } + } else if mods != nil { + dep = strings.TrimPrefix(dep, "mod:") + if _, ok := mods[dep]; ok { + return nil, fmt.Errorf("%s is incompatible with %s", ref.Id, dep) + } + } + } + for _, dep := range pkg.Dependencies { + if !strings.HasPrefix(dep, "mod:") { + if _, ok := cfg.Installed[dep]; !ok { + deps, err := CollectPackages(refs, cfg, dep, trustExternal, update, mods, skipMissing) + if err != nil { + return nil, err + } + for dep, loc := range deps { + toInstall[dep] = loc + } + } + } else if mods != nil { + dep = strings.TrimPrefix(dep, "mod:") + if _, ok := mods[dep]; !ok { + return nil, fmt.Errorf("%s requires mod %s, but not found", ref.Id, dep) + } + } + } + toInstall[ref.String()] = ref + + return toInstall, nil +} + +// intended to be used by CollectPackages! +func CollectEnsureVersion(pkg Package, cfg *Config) error { + if !slices.Contains(pkg.Versions, cfg.Version) { + versions := []string{} + for _, i := range pkg.Versions { + versions = append(versions, GetVersionString(i)) + } + return fmt.Errorf("not available for %s, only %s", GetVersionString(cfg.Version), strings.Join(versions, ", ")) + } + if !slices.Contains(pkg.ModLoaders, cfg.ModLoader) { + loaders := []string{} + for _, l := range pkg.ModLoaders { + loaders = append(loaders, commons.TitleCase(string(l))) + } + return fmt.Errorf("not available for %s, only %s", commons.TitleCase(string(cfg.ModLoader)), strings.Join(loaders, ", ")) + } + return nil +} diff --git a/pkg/kjspkg/config.go b/pkg/kjspkg/config.go new file mode 100644 index 0000000..70b39ab --- /dev/null +++ b/pkg/kjspkg/config.go @@ -0,0 +1,39 @@ +package kjspkg + +import "github.com/Modern-Modpacks/kjspkg/pkg/commons" + +type Config struct { + Installed map[string][]string `json:"installed"` + + Version int `json:"version"` + ModLoader ModLoader `json:"modloader"` +} + +type ModLoader string + +func (s ModLoader) String() string { + return commons.TitleCase(string(s)) +} +func (s ModLoader) StringLong() string { + return ModLoadersLong[s] +} +func (s ModLoader) Identifier() string { + return string(s) +} + +var ModLoaders = []ModLoader{MLForge, MLFabric} +var ModLoadersLong = map[ModLoader]string{ + MLForge: "Forge/NeoForge", + MLFabric: "Fabric/Quilt", +} + +const ( + MLForge ModLoader = "forge" + MLFabric ModLoader = "fabric" +) + +func DefaultConfig() *Config { + return &Config{ + Installed: map[string][]string{}, + } +} diff --git a/pkg/kjspkg/configfs.go b/pkg/kjspkg/configfs.go new file mode 100644 index 0000000..2478bef --- /dev/null +++ b/pkg/kjspkg/configfs.go @@ -0,0 +1,55 @@ +package kjspkg + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +func HasConfig(path string) bool { + configFilePath := filepath.Join(path, ".kjspkg") + if _, err := os.Stat(configFilePath); os.IsNotExist(err) { + return false + } + return true +} + +func GetConfig(path string, noKubeOk bool) (*Config, error) { + config := DefaultConfig() + configFilePath := filepath.Join(path, ".kjspkg") + + if !noKubeOk && !IsKube(path) { + return nil, fmt.Errorf("are you sure this is the kubejs directory?") + } + + if _, err := os.Stat(configFilePath); os.IsNotExist(err) { + return nil, fmt.Errorf("no KJSPKG instance, use 'init' to create one") + } + + data, err := os.ReadFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read .kjspkg file: %w", err) + } + + if err := json.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse .kjspkg file as JSON: %w", err) + } + + return config, nil +} + +func SetConfig(path string, config *Config) error { + configFilePath := filepath.Join(path, ".kjspkg") + + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config to JSON: %w", err) + } + + if err := os.WriteFile(configFilePath, data, 0644); err != nil { + return fmt.Errorf("failed to write .kjspkg: %w", err) + } + + return nil +} diff --git a/pkg/kjspkg/directories.go b/pkg/kjspkg/directories.go new file mode 100644 index 0000000..fe8cad9 --- /dev/null +++ b/pkg/kjspkg/directories.go @@ -0,0 +1,19 @@ +package kjspkg + +import ( + "os" + "path/filepath" +) + +var ScriptDirs = []string{"server_scripts", "client_scripts", "startup_scripts"} +var AssetDirs = []string{"data", "assets"} + +func IsKube(path string) bool { + for _, dir := range ScriptDirs { + dirPath := filepath.Join(path, dir) + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + return false + } + } + return true +} diff --git a/pkg/kjspkg/install.go b/pkg/kjspkg/install.go new file mode 100644 index 0000000..2a42bc6 --- /dev/null +++ b/pkg/kjspkg/install.go @@ -0,0 +1,183 @@ +// TODO: hey gcast pls migrate this off of chmod 0744 :3 +package kjspkg + +import ( + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + + cp "github.com/otiai10/copy" +) + +// This is the recommended way to install packages. +// This will also update/reinstall packages that have already been installed. +// If mass is provided, some actions won't be done that may cause other concurrent +// install calls to fail, like deleting the tmp directory. +func Install(path string, loc PackageLocator, cfg *Config, mass bool, stdout io.Writer) ([]string, error) { + if !mass { + os.RemoveAll(filepath.Join(path, "tmp")) + } + + err := InstallEnsureKube(path) + if err != nil { + return nil, err + } + + err = InstallDiscardExisting(path, loc, cfg) + if err != nil { + return nil, err + } + + err = InstallClone(path, loc, stdout) + if err != nil { + return nil, err + } + + if loc.Branch == nil { + err = InstallBranch(path, loc, stdout) + if err != nil { + return nil, err + } + } + + assets, err := InstallCopy(path, loc) + if err != nil { + return nil, err + } + + // if unsuccessful, this does not delete tmp. this is intentional. + if !mass { + os.RemoveAll(filepath.Join(path, "tmp")) + } + + return assets, nil +} + +func InstallEnsureKube(path string) error { + if !IsKube(path) { + return fmt.Errorf("are you sure this is the kubejs directory?") + } + + for _, name := range ScriptDirs { + err := os.MkdirAll(filepath.Join(path, name, ".kjspkg"), 0744) + if err != nil { + return err + } + } + for _, name := range AssetDirs { + err := os.MkdirAll(filepath.Join(path, name), 0744) + if err != nil { + return err + } + } + + return nil +} + +func InstallDiscardExisting(path string, loc PackageLocator, cfg *Config) error { + return Remove(path, loc.Id, cfg) +} + +func InstallClone(path string, loc PackageLocator, stdout io.Writer) error { + err := os.MkdirAll(filepath.Join(path, "tmp", loc.Id), 0744) + if err != nil { + return err + } + + cmd := exec.Command("git", "clone", loc.URLBase(), loc.Id) + cmd.Dir = filepath.Join(path, "tmp") + cmd.Stdout = stdout + cmd.Stderr = stdout + if err := cmd.Start(); err != nil { + return err + } + return cmd.Wait() +} + +func InstallBranch(path string, loc PackageLocator, stdout io.Writer) error { + if loc.Branch == nil { + return nil + } + + // TODO: migrate to 'git switch' + cmd := exec.Command("git", "checkout", *loc.Branch) + cmd.Dir = filepath.Join(path, "tmp", loc.Id) + cmd.Stdout = stdout + cmd.Stderr = stdout + return cmd.Run() +} + +type LocEsque interface { + PackageLocator | string +} + +func InstallCopy[L LocEsque](path string, loc L) ([]string, error) { + id, repoPath := "", "" + switch l := any(loc).(type) { + case PackageLocator: + id = l.Id + repoPath = filepath.Join(path, "tmp", l.Id) + if l.Path != nil && !strings.Contains(*l.Path, "..") { + repoPath = filepath.Join(repoPath, *l.Path) + } + case string: + id = filepath.Base(l) + repoPath = filepath.Join(path, "tmp", id) + os.MkdirAll(repoPath, 0744) + err := cp.Copy(l, repoPath) + if err != nil { + return nil, err + } + } + + for _, name := range ScriptDirs { + root, target := filepath.Join(repoPath, name), filepath.Join(path, name, ".kjspkg", id) + os.MkdirAll(root, 0744) // TODO: creates dir so that I don't have to check if it exists or not + err := os.Rename(root, target) + if err != nil { + return nil, err + } + } + + assets := []string{} + for _, name := range AssetDirs { + root := filepath.Join(repoPath, name) + os.MkdirAll(root, 0744) // TODO: creates dir so that I don't have to check if it exists or not + err := filepath.WalkDir(root, func(longpath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + relpath, err := filepath.Rel(root, longpath) + if err != nil { + return err + } + + virtpath := filepath.Join(path, name, relpath) + if _, err := os.Stat(virtpath); err == nil { + return fmt.Errorf("asset file exists: %s", virtpath) + } + + os.MkdirAll(filepath.Dir(virtpath), 0744) // TODO: idk why I need this here, but I do :) + err = os.Rename(longpath, virtpath) + if err != nil { + return err + } + + assets = append(assets, filepath.Join(name, relpath)) + return nil + }) + if err != nil { + return nil, err + } + } + + return assets, nil +} diff --git a/pkg/kjspkg/modlist.go b/pkg/kjspkg/modlist.go new file mode 100644 index 0000000..3b497cc --- /dev/null +++ b/pkg/kjspkg/modlist.go @@ -0,0 +1,97 @@ +package kjspkg + +import ( + "archive/zip" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" +) + +// Returns mod list (K/V id/version) from KUBEJS path (so ../mods will be applied!). +// modlessOk == false causes it to error if mods dir can't be found. +// corruptedOk == false causes it to error if a mod can't be parsed (recommended true). +func GetMods(path string, modlessOk bool, corruptedOk bool) (map[string]string, error) { + modDir := filepath.Join(path, "..", "mods") + files, err := os.ReadDir(modDir) + if err != nil { + if !modlessOk { + return nil, fmt.Errorf("mods directory not found: %w", err) + } + return nil, nil + } + + modVersions := make(map[string]string) + for _, file := range files { + if strings.HasSuffix(file.Name(), ".jar") { + zipPath := filepath.Join(modDir, file.Name()) + r, err := zip.OpenReader(zipPath) + if err != nil { + if !corruptedOk { + return nil, fmt.Errorf("failed to open mod file %s: %w", file.Name(), err) + } + continue + } + defer r.Close() + + var foundManifest bool + for _, f := range r.File { + forgeLike, fabricLike := strings.HasSuffix(f.Name, "mods.toml"), strings.HasSuffix(f.Name, ".mod.json") + if forgeLike || fabricLike { + file, err := f.Open() + if err != nil { + if !corruptedOk { + return nil, fmt.Errorf("failed to read manifest from mod file %s: %w", f.Name, err) + } + continue + } + defer file.Close() + + if forgeLike { + var manifest struct { + Mods []struct { + ModId string `toml:"modId"` + Version string `toml:"version"` + } `toml:"mods"` + } + if _, err := toml.NewDecoder(file).Decode(&manifest); err != nil { + if !corruptedOk { + return nil, fmt.Errorf("failed to parse mods.toml for mod %s: %w", f.Name, err) + } + continue + } + if len(manifest.Mods) > 0 { + for _, mod := range manifest.Mods { + modVersions[mod.ModId] = mod.Version + } + foundManifest = true + } + } else if fabricLike { + var manifest struct { + ID string `json:"id"` + Version string `json:"version"` + } + decoder := json.NewDecoder(file) + if err := decoder.Decode(&manifest); err != nil { + if !corruptedOk { + return nil, fmt.Errorf("failed to parse fabric.mod.json for mod %s: %w", f.Name, err) + } + continue + } + modVersions[manifest.ID] = manifest.Version + foundManifest = true + } + } + } + + if !foundManifest && !corruptedOk { + return nil, fmt.Errorf("manifest not found in mod file %s", file.Name()) + } + } + } + + return modVersions, nil +} diff --git a/pkg/kjspkg/pkg.go b/pkg/kjspkg/pkg.go new file mode 100644 index 0000000..2575ee0 --- /dev/null +++ b/pkg/kjspkg/pkg.go @@ -0,0 +1,109 @@ +package kjspkg + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/Modern-Modpacks/kjspkg/pkg/commons" +) + +var StatsViews = "https://tizudev.vercel.app/automatin/api/1025316079226064966/kjspkg?stat=views" +var StatsDownloads = "https://tizudev.vercel.app/automatin/api/1025316079226064966/kjspkg?stat=downloads" + +// Note: withStats will slow down load, and MAY not result in stats being propagated! +func GetPackage(ref PackageLocator, withStats bool) (Package, error) { + r, err := httpClient.Get(ref.URL()) + if err != nil || r.StatusCode != 200 { + return Package{}, commons.EN200(err, ref.URL()) + } + defer r.Body.Close() + + var pkg Package + err = json.NewDecoder(r.Body).Decode(&pkg) + if err != nil { + return Package{}, err + } + + /* func() { + r, err := httpClient.Get(ref.URLPath() + "/README.md") + if err != nil || r.StatusCode != 200 { + return + } + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + if err != nil { + return + } + + pkg.Readme = string(body) + }() */ + + if withStats { + getStat := func(url string) int { + r, err := httpClient.Get(url) + if err != nil || r.StatusCode != 200 { + return 0 + } + defer r.Body.Close() + + bbody, err := io.ReadAll(r.Body) + if err != nil { + return 0 + } + // ffs + body := strings.ReplaceAll(string(bbody), "\"@ZeldaLord\": \"Activate Windows\"", "\"@ZeldaLord\": 0") + + var counts map[string]int + if err := json.Unmarshal([]byte(body), &counts); err != nil { + return 0 + } + return counts[ref.Id] + } + pkg.Views, pkg.Downloads = getStat(StatsViews), getStat(StatsDownloads) + } + + return pkg, nil +} + +type Package struct { + Author string `json:"author" help:"Author(s) of the package"` + Description string `json:"description" help:"Description of the package"` + // Readme string `json:"readme"` + + Versions []int `json:"versions" help:"Game versions the package supports"` + ModLoaders []ModLoader `json:"modloaders" help:"Mod loaders the package supports"` + Dependencies []string `json:"dependencies" help:"Which dependencies to include in the package"` + Incompatibilities []string `json:"incompatabilities" help:"Which dependencies to include in the package"` + + Views int `json:"-" hidden:""` + Downloads int `json:"-" hidden:""` +} + +func PackageFromPath(path string) (Package, error) { + pkg := Package{} + filePath := filepath.Join(path, ".kjspkg") + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return pkg, fmt.Errorf(".kjspkg pkg file not found") + } + + data, err := os.ReadFile(filePath) + if err != nil { + return pkg, fmt.Errorf("failed to read pkg file: %w", err) + } + + if err := json.Unmarshal(data, &pkg); err != nil { + return pkg, fmt.Errorf("failed to parse pkg file as JSON: %w", err) + } + + if pkg.Description == "" { + return pkg, fmt.Errorf("package is invalid (is this an instance?)") + } + + return pkg, nil +} diff --git a/pkg/kjspkg/pkglist.go b/pkg/kjspkg/pkglist.go new file mode 100644 index 0000000..c1c3361 --- /dev/null +++ b/pkg/kjspkg/pkglist.go @@ -0,0 +1,147 @@ +package kjspkg + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/Modern-Modpacks/kjspkg/pkg/commons" +) + +var PackageList = "https://raw.githubusercontent.com/Modern-Modpacks/kjspkg/main/pkgs.json" +var httpClient = http.Client{Timeout: time.Second * 10} +var packageListCache map[string]PackageLocator + +func GetPackageList() (map[string]PackageLocator, error) { + if packageListCache != nil { + return packageListCache, nil + } + + r, err := httpClient.Get(PackageList) + if err != nil || r.StatusCode != 200 { + return nil, commons.EN200(err, PackageList) + } + defer r.Body.Close() + + var locators map[string]string + err = json.NewDecoder(r.Body).Decode(&locators) + if err != nil { + return nil, err + } + + var list = map[string]PackageLocator{} + for id, input := range locators { + loc, err := PackageLocatorFromString(id, input) + if err != nil { + return nil, err + } + list[id] = loc + } + + packageListCache = list + return list, nil +} + +type PackageLocator struct { + Id string `json:"id"` + User string `json:"user"` + Repository string `json:"repository"` + Branch *string `json:"branch"` + Path *string `json:"path"` +} + +func PackageLocatorFromString(id, input string) (PackageLocator, error) { + p := PackageLocator{} + + // I stole this from https://github.com/Modern-Modpacks/kjspkg-lookup/blob/main/src/lib/consts.ts#L7 + regex := regexp.MustCompile(`([^/@$]*)\/([^/@$]*)(\$[^@$]*)?(@[^/@$]*)?`) + match := regex.FindStringSubmatch(input) + if match == nil { + return p, fmt.Errorf("input string does not match the expected format: %s", input) + } + + p.User = match[1] + p.Repository = match[2] + if len(match) > 3 && match[3] != "" { + trimmed := strings.TrimPrefix(match[3], "$") + p.Path = &trimmed + } + if len(match) > 4 && match[4] != "" { + trimmed := strings.TrimPrefix(match[4], "@") + p.Branch = &trimmed + } + + if id != "" { + p.Id = id + } else { + p.Id = p.Repository + } + + return p, nil +} + +// Obtains a package id from a pointer (that is to say, a string like 'kjspkg:amogus' or 'github:...') +func PackageLocatorFromPointer(id string, refs map[string]PackageLocator, trustExternal bool) (PackageLocator, error) { + if strings.HasPrefix(id, "github:") { + l, err := PackageLocatorFromString("", strings.TrimPrefix(id, "github:")) + if err != nil { + return l, err + } + if !trustExternal { + return l, fmt.Errorf("you may not use external packages unless you trust them") + } + return l, nil + } else { + id = strings.TrimPrefix(id, "kjspkg:") + r, ok := refs[id] + if !ok { + return PackageLocator{}, fmt.Errorf("cannot find package: %s", id) + } + return r, nil + } +} + +func (p *PackageLocator) String() string { + str := p.User + "/" + p.Repository + if p.Path != nil { + str = str + "$" + *p.Path + } + if p.Branch != nil { + str = str + "@" + *p.Branch + } + return str +} + +func (p *PackageLocator) URL() string { + return p.URLPath() + "/.kjspkg" +} + +func (p *PackageLocator) URLPath() string { + str := "https://raw.githubusercontent.com/" + p.User + "/" + p.Repository + if p.Branch != nil { + str = str + "/" + *p.Branch + } else { + str = str + "/main" + } + if p.Path != nil { + str = str + "/" + *p.Path + } + return str +} + +func (p *PackageLocator) URLFrontend() string { + str := "https://github.com/" + p.User + "/" + p.Repository + "/tree/" + if p.Branch != nil { + str = str + *p.Branch + } else { + str = str + "main" + } + return str +} + +func (p *PackageLocator) URLBase() string { + return "https://github.com/" + p.User + "/" + p.Repository +} diff --git a/pkg/kjspkg/remove.go b/pkg/kjspkg/remove.go new file mode 100644 index 0000000..099e490 --- /dev/null +++ b/pkg/kjspkg/remove.go @@ -0,0 +1,27 @@ +package kjspkg + +import ( + "os" + "path/filepath" +) + +// This is the recommended way to remove packages. This will NOT error if the +// package wasn't installed. Note that this will also remove assets that were +// added by the package. +func Remove(path string, id string, cfg *Config) error { + for _, name := range ScriptDirs { + err := os.RemoveAll(filepath.Join(path, name, ".kjspkg", id)) + if err != nil { + return err + } + } + if assets, ok := cfg.Installed[id]; ok { + for _, name := range assets { + err := os.RemoveAll(filepath.Join(path, name)) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/kjspkg/scripts.go b/pkg/kjspkg/scripts.go new file mode 100644 index 0000000..bc5933e --- /dev/null +++ b/pkg/kjspkg/scripts.go @@ -0,0 +1,39 @@ +package kjspkg + +import ( + "io/fs" + "path/filepath" + "slices" + "strings" +) + +var ScriptFileTypes = []string{".js", ".ts", ".tsx"} + +func GetStandaloneScripts(path string) []string { + files := []string{} + for _, dir := range ScriptDirs { + _ = filepath.Walk(filepath.Join(path, dir), func(p string, info fs.FileInfo, err error) error { + if info.IsDir() || strings.HasPrefix(p, filepath.Join(path, dir, ".kjspkg")) { + return nil + } + if slices.Contains(ScriptFileTypes, filepath.Ext(p)) { + files = append(files, p) + } + return nil + }) + } + return files +} + +func GetAssets(path string) []string { + files := []string{} + for _, dir := range AssetDirs { + _ = filepath.Walk(filepath.Join(path, dir), func(path string, info fs.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + } + return files +} diff --git a/pkg/kjspkg/utils.go b/pkg/kjspkg/utils.go new file mode 100644 index 0000000..fc84a68 --- /dev/null +++ b/pkg/kjspkg/utils.go @@ -0,0 +1,26 @@ +package kjspkg + +import ( + "strings" + + "github.com/Modern-Modpacks/kjspkg/pkg/commons" +) + +var Logo = " ⢀⣤⣶⣿⣿⣶⣤⡀ \n ⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦ \n⢠⣄⡀⠉⠻⢿⣿⣿⣿⣿⡿⠟⠉⢀⣠⡄\n⢸⣿⣿⣷⣦⣀⠈⠙⠋⠁⣀⣴⣾⣿⣿⡇\n⢸⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⡇\n⢸⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⡇\n ⠙⠻⣿⣿⣿⣿ ⣿⣿⣿⣿⠟⠋ \n ⠉⠻⢿ ⡿⠟⠉ " + +func DepsJoin(deps []string) string { + out := "" + for _, dep := range deps { + if out != "" { + out = out + ", " + } + out = out + commons.TitleCase(strings.TrimPrefix(dep, "mod:")) + if strings.HasPrefix(dep, "mod:") { + out = out + " (mod)" + } + } + if out == "" { + return "none!" + } + return out +} diff --git a/pkg/kjspkg/versions.go b/pkg/kjspkg/versions.go new file mode 100644 index 0000000..71dc27f --- /dev/null +++ b/pkg/kjspkg/versions.go @@ -0,0 +1,24 @@ +package kjspkg + +import ( + "strconv" +) + +var VersionsInOrder = []string{"1.12", "1.16", "1.18", "1.19", "1.20", "1.21"} +var Versions = map[string]int{ + "1.12": 2, + "1.16": 6, + "1.18": 8, + "1.19": 9, + "1.20": 10, + "1.21": 11, +} + +func GetVersionString(id int) string { + for ver, thisid := range Versions { + if thisid == id { + return ver + } + } + return "??? " + strconv.Itoa(id) +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3aa7122..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -fire -requests -psutil -GitPython -thefuzz -python-Levenshtein -toml -esprima -flatten_json \ No newline at end of file diff --git a/run.bat b/run.bat deleted file mode 100644 index bab7735..0000000 --- a/run.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -python %~dp0\kjspkg.py %* \ No newline at end of file