├── 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 | [](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")
--------------------------------------------------------------------------------