├── src ├── __init__.py ├── bin │ ├── __init__.py │ └── hacapt ├── cmd │ ├── __init__.py │ └── arguments.py ├── hacapt │ ├── __init__.py │ └── main.py ├── lib │ ├── __init__.py │ ├── errors.py │ ├── output.py │ └── settings.py └── manifests │ ├── __init__.py │ ├── install_package.py │ ├── manifest_generator.py │ └── install_dependencies.py ├── .gitignore ├── docs ├── _config.yml └── README.md ├── requirements.txt ├── hacapt.py ├── setup.py ├── README.md └── LICENSE.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/bin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cmd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hacapt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/manifests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/* 3 | venv/* -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-time-machine -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests<=2.19.1 2 | pyyaml==3.13 3 | pysocks==1.6.8 4 | beautifulsoup4 -------------------------------------------------------------------------------- /hacapt.py: -------------------------------------------------------------------------------- 1 | from src.hacapt.main import main 2 | 3 | if __name__ == "__main__": 4 | main() -------------------------------------------------------------------------------- /src/bin/hacapt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from src.hacapt.main import main 4 | 5 | 6 | if __name__ == "__main__": 7 | main() -------------------------------------------------------------------------------- /src/lib/errors.py: -------------------------------------------------------------------------------- 1 | class RootURLNotProvidedException(EnvironmentError): pass 2 | 3 | 4 | class LockFileExistsException(IOError): pass 5 | 6 | 7 | class NonRootUserException(EnvironmentError): pass -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Demo 6 | 7 | TODO:/ 8 | 9 | # Basic usage 10 | 11 | TODO:/ 12 | 13 | # Help links 14 | 15 | - [User manual]() 16 | - [Source code]() 17 | - [Interesting aspects]() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import ( 2 | setup, 3 | find_packages 4 | ) 5 | 6 | from src.lib.settings import VERSION 7 | 8 | 9 | setup( 10 | name='HacApt', 11 | version=VERSION, 12 | auth="Ekultek", 13 | author_email="staysalty@protonmail.com", 14 | license="General Public License", 15 | packages=find_packages(), 16 | scripts=['src/bin/hacapt'], 17 | install_requires=["requests<=2.19.1", "pyyaml==3.13", "pysocks==1.6.8"], 18 | keywords="package-manager hacking-tools information-security" 19 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | # HacApt 7 | 8 | HacApt is a package manager for hackers built by hackers that focuses on the installation of information security tools. HacApt is built with security in mind so everything is done over Tor connections. 9 | 10 | # First look 11 | 12 | Here's the first demo video of HacApt 13 | 14 | [![HacApt](https://user-images.githubusercontent.com/14183473/44936485-8cfe8900-ad3a-11e8-8337-63131d74d515.png)](https://vimeo.com/287728646 "#staysalty") 15 | 16 | # Helpful links 17 | 18 | - [Homepage](https://ekultek.github.io/HacApt/) 19 | - [User Manual]() -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ``` 2 | HacApt; Package manager for hackers built by hackers 3 | Copyright (C) 2018 Ekultek 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ``` -------------------------------------------------------------------------------- /src/manifests/install_package.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | import shlex 4 | import subprocess 5 | 6 | from src.lib.settings import ( 7 | PACKAGE_LOCATIONS, 8 | SCRIPT_LOCATIONS 9 | ) 10 | 11 | 12 | def install(package_name, root_url, language): 13 | ext = "py" if language.lower() == "python" else "rb" 14 | path = "{}/{}".format(PACKAGE_LOCATIONS, package_name) 15 | if not os.path.exists(path): 16 | os.makedirs(path) 17 | command = shlex.split("git clone {} {}".format(root_url, path)) 18 | try: 19 | proc = subprocess.check_output(command) 20 | except subprocess.CalledProcessError: 21 | proc = None 22 | 23 | if proc is None: 24 | return False 25 | else: 26 | script = "{}/{}".format(SCRIPT_LOCATIONS, package_name) 27 | text = "#!/bin/bash\n# this is the execution script for {}\n\ncd {}\nexec {} {}.{} $@".format( 28 | package_name, path, language, package_name, ext 29 | ) 30 | with open(script, "a+") as package: 31 | package.write(text) 32 | st = os.stat(script) 33 | os.chmod(script, st.st_mode | stat.S_IEXEC) 34 | return True 35 | -------------------------------------------------------------------------------- /src/lib/output.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def set_color(string, level=None): 5 | """ 6 | set the string color 7 | """ 8 | color_levels = { 9 | 10: "\033[36m{}\033[0m", 10 | 15: "\033[1m\033[32m{}\033[0m", 11 | 20: "\033[32m{}\033[0m", 12 | 30: "\033[1m\033[33m{}\033[0m", 13 | 35: "\033[33m{}\033[0m", 14 | 40: "\033[1m\033[31m{}\033[0m", 15 | 50: "\033[1m\033[30m{}\033[0m", 16 | 60: "\033[7;31;31m{}\033[0m" 17 | } 18 | if level is None: 19 | return color_levels[20].format(string) 20 | else: 21 | return color_levels[int(level)].format(string) 22 | 23 | 24 | def info(string): 25 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[INFO] {}".format(string), level=20)) 26 | 27 | 28 | def debug(string): 29 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[DEBUG] {}".format(string), level=10)) 30 | 31 | 32 | def warn(string, minor=False): 33 | if not minor: 34 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[WARN] {}".format(string), level=30)) 35 | else: 36 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[WARN] {}".format(string), level=35)) 37 | 38 | 39 | def error(string): 40 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[ERROR] {}".format(string), level=40)) 41 | 42 | 43 | def fatal(string): 44 | print("\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[FATAL] {}".format(string), level=60)) 45 | 46 | 47 | def prompt(string): 48 | try: 49 | question = raw_input( 50 | "\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[PROMPT] {}: ".format(string), level=50) 51 | ) 52 | except: 53 | quetion = input( 54 | "\033[38m[{}]\033[0m".format(time.strftime("%H:%M:%S")) + set_color("[PROMPT] {}: ".format(string), level=50) 55 | ) 56 | return question -------------------------------------------------------------------------------- /src/cmd/arguments.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | class HacaptParser(argparse.ArgumentParser): 5 | 6 | def __init__(self): 7 | super(HacaptParser, self).__init__() 8 | 9 | @staticmethod 10 | def optparse(): 11 | parser = argparse.ArgumentParser() 12 | mandatory = parser.add_argument_group("mandatory") 13 | mandatory.add_argument("-i", "--install", dest="install", nargs="?", metavar="PACKAGE-NAME", help="install this package") 14 | mandatory.add_argument("-r", "--root", metavar="GITHUB-URL", dest="githubManifest", help="pass a Github URL to create a package manifest and install the repo") 15 | info = parser.add_argument_group("info") 16 | info.add_argument("-R", "--readme-link", metavar="README-URL", dest="readMeLink", default="n/a", help="pass the README link to add to the manifest") 17 | info.add_argument("-V", "--repo-version", metavar="REPO-VERSION-#", dest="repoVersion", default="n/a", help="pass the version of the repo") 18 | info.add_argument("-t", "--tar-download", metavar="LINK", dest="tarDownloadLink", default="self-extract", help="pass the link to the tar download") 19 | info.add_argument("-d", "--dependencies", metavar="DEPENDENCY=-FILE-LINK", dest="dependencyFile", default="self-extract", help="pass the link to the raw dependency file") 20 | etc = parser.add_argument_group("misc") 21 | etc.add_argument("--check-tor", action="store_true", dest="checkTor") 22 | etc.add_argument("-v", "--verbose", action="store_true", dest="runVerbose", help="run in verbose mode") 23 | etc.add_argument("-F", "--full-clean", action="store_true", default=False, dest="fullclean", help="clean the manifest files and all the cache") 24 | etc.add_argument("--force", action="store_true", default=False, dest="forceInstall", help="force an installation of a package") 25 | etc.add_argument("-l", "--list-manifests", action="store_true", default=False, dest="listManifestFiles", help="show a list of all installed manifest files") 26 | return parser.parse_args() 27 | -------------------------------------------------------------------------------- /src/hacapt/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from src.cmd.arguments import HacaptParser 5 | from src.manifests.manifest_generator import generate_manifest_file 6 | from src.manifests.install_dependencies import install_dependencies 7 | from src.lib.settings import IS_LOCKED, LOCKFILE_PATH, HOME, safe_delete, check_if_run, parse_manifest, output_infomation, MANIFEST_FILES_PATH 8 | from src.lib.errors import LockFileExistsException, NonRootUserException 9 | from src.lib.output import info, error, fatal, prompt, warn 10 | 11 | 12 | def main(): 13 | 14 | try: 15 | 16 | check_if_run() 17 | 18 | # if not os.getuid() == 0: 19 | # raise NonRootUserException( 20 | # 'must be run as root' 21 | # ) 22 | 23 | opt = HacaptParser().optparse() 24 | 25 | if IS_LOCKED: 26 | raise LockFileExistsException( 27 | 'you will need to delete the lock file out of \'{}\' this usually occurs when there is an issue ' 28 | 'downloading a package'.format(LOCKFILE_PATH)) 29 | else: 30 | open(LOCKFILE_PATH, "a+").close() 31 | 32 | if opt.listManifestFiles: 33 | files = os.listdir(MANIFEST_FILES_PATH) 34 | info("there is a total of {} manifest file(s) installed".format(len(files))) 35 | print("{}\n{}\n{}".format("-" * 30, '\n'.join([f for f in files]), "-" * 30)) 36 | 37 | if opt.fullclean: 38 | print("cleaning home folder") 39 | files = [] 40 | for root, _, filenames in os.walk(HOME): 41 | for f in filenames: 42 | files.append(os.path.join(root, f)) 43 | for f in files: 44 | safe_delete(f, verbose=opt.runVerbose) 45 | exit(-1) 46 | 47 | if opt.install is None and opt.githubManifest is None: 48 | error("there's nothing left to do..?") 49 | elif opt.install is None: 50 | info('generating manifest file for Github repo \'{}\''.format(opt.githubManifest)) 51 | manifest_file, language_used = generate_manifest_file( 52 | root_url=opt.githubManifest, 53 | readme=opt.readMeLink, 54 | version=opt.repoVersion, 55 | tar_link=opt.tarDownloadLink, 56 | dependencies=opt.dependencyFile 57 | ) 58 | info("manifest file generated and stored in '{}'".format(manifest_file)) 59 | data = parse_manifest(manifest_file) 60 | if opt.runVerbose: 61 | output_infomation(data) 62 | info("attempting to install dependencies") 63 | install_dependencies(manifest_file, language_used) 64 | except AttributeError as e: 65 | print(e) 66 | 67 | 68 | try: 69 | os.unlink(LOCKFILE_PATH) 70 | except: 71 | pass -------------------------------------------------------------------------------- /src/manifests/manifest_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import yaml 4 | import requests 5 | 6 | import src.lib.settings 7 | import src.lib.errors 8 | import src.lib.output 9 | 10 | 11 | def generate_manifest_file(**kwargs): 12 | readme_link = kwargs.get("readme", "n/a") 13 | version = kwargs.get("version", "n/a") 14 | root_url = kwargs.get("root_url", None) 15 | tar_download_link = kwargs.get("tar_link", "self-extract") 16 | dependencies = kwargs.get("dependencies", "self-extract") 17 | force = kwargs.get("force", False) 18 | 19 | try: 20 | split = root_url.split("/") 21 | username = split[-2] 22 | project_name = split[-1] 23 | except Exception: 24 | raise src.lib.errors.RootURLNotProvidedException 25 | 26 | project_language = src.lib.settings.determine_project_language(root_url) 27 | filename = "{}.manifest.yaml".format(project_name) 28 | file_path = "{}/{}".format(src.lib.settings.MANIFEST_FILES_PATH, filename) 29 | 30 | if os.path.exists(file_path) and not force: 31 | src.lib.output.info("manifest file exists using stored one") 32 | elif os.path.exists(file_path) and force or not os.path.exists(file_path): 33 | for arg in kwargs: 34 | if kwargs[arg] is None: 35 | root_url = src.lib.output.prompt("enter the root URL to the Github repo") 36 | elif kwargs[arg].lower() == "self-extract": 37 | if arg == "tar_link": 38 | tar_download_link = "{}/tarball/master".format(root_url) 39 | else: 40 | if project_language == "python": 41 | dependencies = "https://raw.githubusercontent.com/{}/{}/master/requirements.txt".format( 42 | username, project_name 43 | ) 44 | elif project_language == "ruby": 45 | dependencies = "https://raw.githubusercontent.com/{}/{}/master/Gemfile".format( 46 | username, project_name 47 | ) 48 | try: 49 | req = requests.get(dependencies, proxies=src.lib.settings.REQUESTS_PROXY) 50 | if req.status_code == 200: 51 | dependencies = req.content.split("\n") 52 | else: 53 | dependencies = "n/a" 54 | except Exception: 55 | dependencies = "unknown" 56 | 57 | with open(file_path, "a+") as manifest: 58 | template = src.lib.settings.MANIFEST_TEMPLATE.format( 59 | package_name=project_name, root_url=root_url, 60 | package_version=version, readme_link=readme_link, 61 | download_link=tar_download_link, requirements=dependencies, 62 | language=project_language 63 | ) 64 | template = yaml.safe_load(template) 65 | manifest.write(yaml.safe_dump(template)) 66 | 67 | return file_path, project_language 68 | -------------------------------------------------------------------------------- /src/lib/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import random 4 | import string 5 | import getpass 6 | 7 | import requests 8 | import yaml 9 | from bs4 import BeautifulSoup 10 | 11 | import output 12 | 13 | 14 | MANIFEST_TEMPLATE = """package: 15 | {package_name}: 16 | root url: {root_url} 17 | version: {package_version} 18 | readme: {readme_link} 19 | tar download: {download_link} 20 | dependencies: {requirements} 21 | language: {language}""" 22 | INIT_CONFIG_TEMPLATE = """config: 23 | username: {username} 24 | uid: {uuid} 25 | gid: {guid} 26 | home: {home_location}""" 27 | HOME = "/usr/local/etc/hacapt" 28 | SCRIPT_LOCATIONS = "/usr/local/bin" 29 | CONFIG_PATH = "{}/.conf.yaml".format(HOME) 30 | MANIFEST_FILES_PATH = "{}/manifests".format(HOME) 31 | PACKAGE_LOCATIONS = "{}/packages".format(HOME) 32 | TOR_PROXY = "socks5://127.0.0.1:9050" 33 | REQUESTS_PROXY = {"http": TOR_PROXY, "https": TOR_PROXY} 34 | LOCKFILE_PATH = "{}/.haclock".format(HOME) 35 | IS_LOCKED = os.path.exists(LOCKFILE_PATH) 36 | VERSION = "0.0.1" 37 | 38 | 39 | def safe_delete(path, passes=3, verbose=False): 40 | import struct 41 | 42 | length = os.path.getsize(path) 43 | data = open(path, "w") 44 | # fill with random printable characters 45 | if verbose: 46 | print("filling '{}' with random printable".format(path)) 47 | for _ in xrange(passes): 48 | data.seek(0) 49 | data.write(''.join(random.choice(string.printable) for _ in range(length))) 50 | # fill with random data from the OS 51 | if verbose: 52 | print("filling '{}' with urandom".format(path)) 53 | for _ in xrange(passes): 54 | data.seek(0) 55 | data.write(os.urandom(length)) 56 | # fill with null bytes 57 | if verbose: 58 | print("filling '{}' with null bytes".format(path)) 59 | for _ in xrange(passes): 60 | data.seek(0) 61 | data.write(struct.pack("B", 0) * length) 62 | data.close() 63 | if verbose: 64 | print("removing file") 65 | os.remove(path) 66 | 67 | 68 | def check_if_run(): 69 | paths = [HOME, MANIFEST_FILES_PATH] 70 | if not os.path.exists(HOME): 71 | output.info("initializing hacapt") 72 | if os.getuid() == 0: 73 | output.error("initializing as root") 74 | current_uid = os.getuid() 75 | current_gid = os.getgid() 76 | current_user = getpass.getuser() 77 | for path in paths: 78 | if not os.path.exists(path): 79 | os.makedirs(path) 80 | with open(CONFIG_PATH, "a+") as conf: 81 | conf.write(INIT_CONFIG_TEMPLATE.format( 82 | username=current_user, 83 | uuid=current_uid, 84 | guid=current_gid, 85 | home_location=HOME 86 | )) 87 | output.info("initialized successfully") 88 | exit(1) 89 | 90 | 91 | def parse_manifest(file_path): 92 | with open(file_path) as manifest: 93 | return yaml.safe_load(manifest.read()) 94 | 95 | 96 | def output_infomation(data): 97 | seperator = "-" * 50 98 | package_information = [] 99 | for item in data["package"]: 100 | package_information.append(("package name", item)) 101 | for key in data["package"][item]: 102 | package_information.append((key, data["package"][item][key])) 103 | print(seperator) 104 | for item in package_information: 105 | print("{}: {}".format(item[0].title(), item[1])) 106 | print(seperator) 107 | 108 | 109 | def determine_project_language(root_url): 110 | temp, cleaned = [], [] 111 | __clean_entity = lambda html: html.split(">")[1].split("<")[0] 112 | identifier = re.compile(r"\w+(\S+)?", re.I) 113 | req = requests.get(root_url, proxies=REQUESTS_PROXY) 114 | soup = BeautifulSoup(req.content, "html.parser") 115 | language_stats = str(soup.findAll("div", {"class": "repository-lang-stats"})[0]).split("\n") 116 | for i, entity in enumerate(language_stats): 117 | if identifier.search(entity) is not None: 118 | temp.append((entity, language_stats[i+1])) 119 | for item in temp: 120 | lang = __clean_entity(item[0]) 121 | amount = __clean_entity(item[1]) 122 | cleaned.append((lang, amount)) 123 | most_used_language = cleaned[0][0] 124 | return most_used_language.lower() 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/manifests/install_dependencies.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | import shlex 5 | import string 6 | import random 7 | import subprocess 8 | 9 | import yaml 10 | 11 | from .install_package import install 12 | import src.lib.output 13 | import src.lib.settings 14 | 15 | 16 | def run_install(args, project_lang, user): 17 | # give the files time to catch up 18 | time.sleep(1) 19 | if project_lang.lower() == "python": 20 | command = shlex.split("pip install") 21 | elif project_lang.lower() == "ruby": 22 | os.setuid(user) 23 | command = shlex.split("bundle install") 24 | if args is not None: 25 | for arg in args: 26 | command.append(arg) 27 | src.lib.output.info("running command '{}'".format(" ".join(command))) 28 | try: 29 | proc = subprocess.check_output(command) 30 | except subprocess.CalledProcessError: 31 | return False 32 | failed_identifier = re.compile("failed|error", re.I) 33 | for item in proc: 34 | if failed_identifier.search(item) is not None: 35 | return False 36 | return True 37 | 38 | 39 | def generate_temporary_req_file(data_to_add, gemfile=False, requirements_file=False): 40 | if gemfile: 41 | filename = "{}/{}".format(os.getcwd(), "Gemfile") 42 | if requirements_file: 43 | filename = [] 44 | for _ in range(5): 45 | filename.append(random.choice(string.ascii_letters)) 46 | filename = ''.join(filename) + ".txt" 47 | with open(filename, "a+") as req: 48 | req.write(data_to_add) 49 | return filename 50 | 51 | 52 | def install_dependencies(manifest_filename, language, tries=3): 53 | package_name = manifest_filename.split("/")[-1].split(".")[0] 54 | with open(manifest_filename) as manifest: 55 | data = yaml.safe_load(manifest.read()) 56 | __dependency_check = lambda d: d["package"][manifest_filename.split("/")[-1]]["dependencies"] == "n/a" 57 | if language == "ruby": 58 | conf = yaml.safe_load(open(src.lib.settings.CONFIG_PATH).read()) 59 | uid = conf["config"]["uid"] 60 | src.lib.output.warn("bundler will not be run behind Tor connection") 61 | set_opts = (["-V"], language, uid) 62 | if not __dependency_check: 63 | dependencies = data["package"][manifest_filename.split(".")[0].split("/")[-1]]["dependencies"] 64 | dependencies = "\n".join(dependencies) 65 | gemfile = generate_temporary_req_file(dependencies, gemfile=True) 66 | else: 67 | dependencies = None 68 | elif language == "python": 69 | set_opts = (["-vv", "--proxy", src.lib.settings.TOR_PROXY], language, None) 70 | if not __dependency_check: 71 | dependencies = data["package"][manifest_filename.split(".")[0].split("/")[-1]]["dependencies"] 72 | dependencies = "\n".join(dependencies) 73 | requirements_file = generate_temporary_req_file(dependencies, language) 74 | else: 75 | dependencies = None 76 | requirements_file = "" 77 | set_opts[0].append("-r") 78 | set_opts[0].append(requirements_file) 79 | if dependencies is not None: 80 | results = run_install(*set_opts) 81 | else: 82 | results = True 83 | if results: 84 | src.lib.output.info("dependencies installed successsully") 85 | else: 86 | if tries != 0: 87 | src.lib.output.error("failed to install dependencies, trying again ({})".format(tries)) 88 | install_dependencies(manifest_filename, language, tries=tries-1) 89 | else: 90 | src.lib.output.fatal("attempted to install dependencies multiple times and failed, giving up") 91 | try: 92 | os.unlink(gemfile) 93 | except: 94 | pass 95 | try: 96 | os.unlink(requirements_file) 97 | except: 98 | pass 99 | 100 | src.lib.output.info("installing package now") 101 | installation_results = install(package_name, data["package"][package_name]["root url"], language) 102 | if installation_results: 103 | src.lib.output.info("installed successfully! just run `{} `".format(package_name)) 104 | else: 105 | src.lib.output.error("failed to install package") --------------------------------------------------------------------------------