Compare commits

...

37 Commits

Author SHA1 Message Date
janishutz 36d3c6f992 feat(rewrite): Config preparations 2026-05-16 15:51:10 +02:00
janishutz 217ccadc74 feat(rewrite): Start rewrite 2026-05-16 15:00:09 +02:00
janishutz d391c53c6c chore: notes 2026-05-16 14:28:24 +02:00
janishutz 24c52c1bba feat(schema): Add array templates 2026-05-16 12:01:40 +02:00
janishutz 80d7b3d86e feat(config): Clean up, add themes options 2026-05-16 11:41:00 +02:00
janishutz 72b477381f chore: more ideas 2026-05-15 17:00:41 +02:00
janishutz e8d8429bc9 feat(config): Prepare for more metadata 2026-05-15 16:58:19 +02:00
janishutz afe8d29340 feat(cli): More argument handling 2026-05-15 16:58:00 +02:00
janishutz 78eecfc81a feat(cli): Better organized CLI arg parsing 2026-05-14 17:36:06 +02:00
janishutz e24eb647ca feat(templates): More package templates, template management start 2026-05-12 14:15:18 +02:00
janishutz 72a8ceb741 feat(templates): pkg groups 2026-05-09 10:20:02 +02:00
janishutz ecb2952a7e feat(commit): individual packages from config respected 2026-05-07 18:25:19 +02:00
janishutz b7218c2a82 chore: some notes 2026-05-04 16:47:35 +02:00
janishutz 7b1dfe6ebc feat(config): Config loading and merging 2026-05-03 15:09:05 +02:00
janishutz 190fb86758 feat(typing): possibly complete 2026-05-03 14:24:03 +02:00
janishutz f9e6120910 feat(typing): More types, cleaner structure 2026-04-30 14:12:35 +02:00
janishutz 31426c006b feat(typing): Start adding python types for the configuration 2026-04-30 10:49:46 +02:00
janishutz 2fd69cc595 feat(init): Templates handling 2026-04-25 16:43:53 +02:00
janishutz 541a876307 feat(config): Schema validator, initial config merger setup 2026-04-23 11:31:01 +02:00
janishutz 5126e0373f feat(PKGBUILD): First setup 2026-04-23 11:30:31 +02:00
janishutz 299ac49b40 feat(schema): Mostly finish the config options 2026-04-18 14:48:52 +02:00
janishutz b6901b59e6 feat(schema): More described opts 2026-04-17 16:57:07 +02:00
janishutz bcd0339d88 feat: Start config schema 2026-04-17 15:41:07 +02:00
janishutz 4017e0263b chore: Note 2026-04-17 11:33:51 +02:00
janishutz b71d18cdc9 chore: Notes 2026-04-16 16:42:22 +02:00
janishutz 043b2618a3 chore: some improvements towards packaging 2026-04-16 16:02:39 +02:00
janishutz c264a5bea2 feat: Diff packages 2026-04-16 16:02:15 +02:00
janishutz bb123c23a1 refactor(pw): Improve handling of password input 2026-04-16 16:01:56 +02:00
janishutz 414c065df4 refactor(printing): Improve handling of list printing 2026-04-16 16:00:58 +02:00
janishutz 35c976fcac chore(notes): Add some ideas 2026-04-16 16:00:33 +02:00
janishutz b8e2d68469 feat: Improved package diff printing 2026-04-15 17:15:21 +02:00
janishutz 5734c0d524 feat: printing utils 2026-04-15 16:51:55 +02:00
janishutz f5386d0e98 feat: Add setup function 2026-04-15 15:59:36 +02:00
janishutz ead9e3a3f7 fix: no importing, catch errors, ascii art 2026-04-10 08:59:35 +02:00
janishutz 711b89a0d6 feat(pacman): Add needed commands 2026-04-10 08:58:54 +02:00
janishutz 0697bef7d5 feat(input): Password prompt, rename 2026-04-10 08:58:40 +02:00
janishutz 6018d6256d feat(git): all needed subcommands (commit, branches, push, pull) 2026-04-10 08:58:17 +02:00
52 changed files with 636 additions and 160 deletions
+28
View File
@@ -0,0 +1,28 @@
# Maintainer: Janis Hutz <development@janishutz.com>
pkgname=archmgr-git
pkgver=0.0.0
pkgrel=1
pkgdesc='A nixos-like declarative config and package manager for Arch Linux'
arch=('any')
url="https://github.com/janishutz/archmgr"
license=('GPL3')
depends=('python', 'python-pyaml')
makedepends=('git')
provides=('archmgr')
conflicts=('archmgr')
source=("$pkgname"::git+${url}.git)
sha256sums=('SKIP') # TODO: Add?
pkgver() {
cd "${pkgname}"
# TODO: For the non-git pkgbuild, need to use different output
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
package() {
cd "${pkgname}"
# TODO: Need to finish
}
+6
View File
@@ -3,3 +3,9 @@
A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks). A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks).
See the [Wiki](https://git.janishutz.com/janishutz/archmgr/wiki) See the [Wiki](https://git.janishutz.com/janishutz/archmgr/wiki)
## WIP
This project is very much Work In Progress.
The configs will likely move to python-based configs from the current yaml-based ones to provide you with more flexibility.
`archmgr` will automatically create a basic setup if you run `archmgr init`, so you can get up and running quickly
-38
View File
@@ -1,38 +0,0 @@
import cli.args as cliargs
import commands.commit as commit
import commands.config as config
import commands.init as init
import commands.pull as pull
import commands.push as push
args, ap = cliargs.add_cli_args()
if args.cmd == None:
ap.print_help()
print("\nSpecify a subcommand, '-h' or '-v'")
exit(64)
print(
"""
_
( )
_ _ _ __ ___| |__ ___ ___ __ _ __
/ _ ) __)/ ___) _ \\ _ _ \\/ _ \\ __)
( (_| | | ( (___| | | | ( ) ( ) | (_) | |
\\__ _)_) \\____)_) (_)_) (_) (_)\\__ |_)
( )_) |
\\___/
"""
)
if args.cmd == "commit":
commit.commit(args.dry_run, args.force, args.no_commit)
elif args.cmd == "config":
config.config()
elif args.cmd == "init":
init.init(args.force)
elif args.cmd == "pull":
pull.pull(args.rebase, args.apply)
elif args.cmd == "push":
push.push(args.force)
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# TODO: Re-export the config stuff
if __name__ == "__main__":
from app import run as _run
_run()
+52
View File
@@ -0,0 +1,52 @@
# The main app
from typing import cast
import cli as cliargs
import commands.commit as commit
import commands.config as config
import commands.init as init
import commands.pull as pull
import commands.push as push
import commands.prepare as setup
import commands.show as show
def run():
args, ap = cliargs.add_cli_args()
if args.cmd == None:
ap.print_help()
print("\nSpecify a subcommand, '-h' or '-v'")
exit(64)
print("""
_
( )
_ _ _ __ ___| |__ ___ ___ __ _ __
/ _ ) __)/ ___) _ \\ _ _ \\/ _ \\ __)
( (_| | | ( (___| | | | ( ) ( ) | (_) | |
\\__ _)_) \\____)_) (_)_) (_) (_)\\__ |_)
( )_) |
\\___/
""")
conf = {}
try:
if args.cmd == "commit":
commit.commit(conf, args.force, args.no_render)
elif args.cmd == "config":
config.config(args, conf)
elif args.cmd == "init":
init.init(args.force)
elif args.cmd == "setup":
setup.setup()
elif args.cmd == "pull":
pull.pull(conf, args.rebase, args.apply)
elif args.cmd == "push":
push.push(args.force)
elif args.cmd == "show":
show.show(conf, cast(ArchMgrTemplates, {}), args)
except KeyboardInterrupt as e:
exit(130)
except Exception as e:
raise e
+34
View File
@@ -0,0 +1,34 @@
# CLI argument parsing and setup for it
import argparse
import argcomplete
from args import commit, init, pull, push, show
def add_cli_args():
ap = argparse.ArgumentParser(
"archmgr",
description="A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks)",
usage="archmgr [command] [options]",
)
ap.add_argument("-v", "--version", action="version", version="%(prog)s V0.0.1")
sp = ap.add_subparsers(
title="commands",
metavar="Use 'archmgr [command] --help' to see help for each command",
dest="cmd",
)
# ┌ ┐
# │ Subcommands │
# └ ┘
sp.add_parser("setup", help="Do initial setup, like installing required tools")
commit.add_parser(sp)
sp.add_parser("config", help="prints information about your config")
init.add_parser(sp)
pull.add_parser(sp)
push.add_parser(sp)
show.add_parser(sp)
argcomplete.autocomplete(ap)
return ap.parse_args(), ap
+13
View File
@@ -0,0 +1,13 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
commit = sp.add_parser(
"commit", help="apply pending changes and commit to git repo"
)
commit.add_argument(
"-f", "--force", help="force apply (skips prompts)", action="store_true"
)
commit.add_argument(
"--no-render", "-r", help="do not re-render renderables", action="store_true"
)
+11
View File
@@ -0,0 +1,11 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
init = sp.add_parser("init", help="initialize a new archmgr repository")
init.add_argument(
"--force",
"-f",
help="resets the git repository and initializes new archmgr repo",
action="store_true",
)
+12
View File
@@ -0,0 +1,12 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
pull = sp.add_parser("pull", help="pull changes from git remote")
pull.add_argument("--rebase", "-r", help="execute rebase", action="store_true")
pull.add_argument(
"--apply",
"-a",
help="also apply the changes to the local system",
action="store_true",
)
+11
View File
@@ -0,0 +1,11 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
push = sp.add_parser("push", help="push pending changes to git remote")
push.add_argument(
"--force",
"-f",
help="force push (overriding possible remote changes)",
action="store_true",
)
+13
View File
@@ -0,0 +1,13 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
show = sp.add_parser("show", help="get information about configuration presets")
show_sp = show.add_subparsers(
title="config presets to show details on",
dest="show",
required=True,
)
show_sp.add_parser("config", help="show details about your configuration. Alias of config show")
pkgs = show_sp.add_parser("pkgs", help="show details on package presets")
pkgs.add_argument("show_pkg")
View File
View File
+1
View File
@@ -0,0 +1 @@
# Declare a file explicitly if it has different permissions
View File
+2
View File
@@ -0,0 +1,2 @@
def clone_git_repo(repo_url: str, clone_path: str, branch: str = "DEFAULT"):
pass
View File
+6
View File
@@ -0,0 +1,6 @@
def add_packages(pkgs: list[str]):
pass
def add_package_bundles(pkgs: list[str]):
pass
+2
View File
@@ -0,0 +1,2 @@
def enable_reflector():
pass
+2
View File
@@ -0,0 +1,2 @@
def enable_repo(name: str):
pass
+36
View File
@@ -0,0 +1,36 @@
def add_template_data(name: str, data: str):
"""Replace all occurrences of variable specified by `name` by
the specified data
Args:
name: The variable to replace
data: The data to replace it with
"""
pass
def add_template_array(name: str, template: str, data: list[str]):
"""Replace all occurrences of variable specified by `name` in the files with
the specified template, where {{ data }} is replaced with the each element
of the data argument
Args:
name: The name of the variable to replace
template:
Template syntax, with {{ data }} as variable, see example below or docs.
Recursive replacement is supported up to one layer deep
data: A list of data that is substituted into the {{ data }} variable in the template
Example:
```python
add_template_array("test", "- Hello World {{ data }}\\n", [0, 1])
```
for example replaces {{ test }} in a file with
```
- Hello World 0
- Hello World 1
```
"""
# TODO: Recursive replacement
pass
View File
+14
View File
@@ -0,0 +1,14 @@
from typing import Literal
def add_custom_theme():
pass
def custom_theme_color_source(src: Literal["wallpaper"] | Literal["default"]):
pass
# TODO: For desktop environments, what command??
def set_wallpaper(path: str):
pass
+2
View File
@@ -0,0 +1,2 @@
def set_gtk_theme():
pass
View File
View File
View File
View File
View File
+34
View File
@@ -0,0 +1,34 @@
from typing import List
def pkg_diff(target: List[str], actual: List[str]) -> tuple[List[str], List[str]]:
"""Compute a diff between target packages and installed packages
Args:
target: The target packages
actual: The actually installed packages
Returns:
A tuple with first the missing (not installed) packages
and second the extraneous (to be uninstalled) packages
"""
removed = []
diffed_out = []
for pkg in actual:
try:
diffed_out.append(target.index(pkg))
except Exception:
removed.append(pkg)
diffed_out.reverse()
new_pkgs = []
if len(diffed_out) > 0:
curr = diffed_out.pop()
for i, pkg in enumerate(target):
if i != curr:
new_pkgs.append(pkg)
else:
if len(diffed_out) > 0:
curr = diffed_out.pop()
return (new_pkgs, removed)
+105
View File
@@ -0,0 +1,105 @@
import os
import subprocess
def init(dirname: str) -> bool:
"""Initialize a new git repo in the directory
Args:
dirname: The directory to create it in
Returns:
True on success, False on error
"""
return subprocess.call("git init", cwd=dirname) == 0
def commit(msg: str):
"""Create a new commit
Args:
msg: The commit message
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call('git commit -am "' + msg + '"', cwd=dir) == 0
def branch_add(name: str):
"""Create a new git branch
Args:
name: The name of the branch to create
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git branch " + name, cwd=dir) == 0
def branch_switch(name: str):
"""Switch to a git branch
Args:
name: The name of the branch to switch to
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git checkout " + name, cwd=dir) == 0
def branch_show():
"""Create a new git branch
Args:
name: The name of the branch to create
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git branch", cwd=dir) == 0
def branch_delete(name: str):
"""Delete a git branch
Args:
name: The name of the branch to delete
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git branch -d " + name, cwd=dir) == 0
def pull(rebase: bool = False):
"""Pull git changes
Args:
rebase: Whether to rebase or not
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git pull" + (" --rebase" if rebase else ""), cwd=dir) == 0
def push(force: bool = False):
"""Push git changes
Args:
force: Whether to force push
Returns:
True on success, False on error
"""
dir = os.getcwd()
return subprocess.call("git push" + (" --force" if force else ""), cwd=dir) == 0
@@ -1,5 +1,7 @@
from typing import Optional from typing import Optional
from password_mgr import PasswordManager
def choice(default: str, options: str, msg: str) -> str: def choice(default: str, options: str, msg: str) -> str:
default = default.lower() default = default.lower()
@@ -28,3 +30,15 @@ def confirm_overwrite(msg: Optional[str] = ""):
return confirm( return confirm(
False, (msg if msg != "" else "Do you really want to overwrite your changes?") False, (msg if msg != "" else "Do you really want to overwrite your changes?")
) )
# Use class to store the password
pw_manager = PasswordManager()
def password():
return pw_manager.get()
def unlock_sudo():
return pw_manager.validate()
+70
View File
@@ -0,0 +1,70 @@
import getpass
import time
import colorama
import subprocess as sp
class PasswordManager:
_pw = ""
_wrong_cnt = 0
_valid = False
def get(self, msg: str = ""):
"""Get the user's password (uses cached password if PW is valid)
Otherwise prompts user
Args:
msg: The message to use for the password prompt
Returns:
The user's password
"""
if self._valid or self.validate():
return self._pw
if msg != "":
self._pw = getpass.getpass(msg)
else:
self._pw = getpass.getpass()
if self._pw != "":
if not self.validate():
print(
colorama.Fore.RED + "Error:",
colorama.Style.RESET_ALL + "Invalid Password. Please try again",
)
return self.get(msg)
return self._pw
else:
time.sleep(1)
print(
colorama.Fore.RED + "Error:",
colorama.Style.RESET_ALL + "Password cannot be empty",
)
return self.get(msg)
def validate(self) -> bool:
"""Validate that the password is correct by running sudo command
Returns:
True if password is valid, False otherwise
"""
def helper():
if self._pw == "":
return False
try:
sp.run(
["sudo", "-k"], capture_output=True, text=True
).check_returncode()
sp.run(
["sudo", "-S", "echo"],
capture_output=True,
input=self._pw,
text=True,
).check_returncode()
except Exception:
return False
return True
self._valid = helper()
return self._valid
+64
View File
@@ -0,0 +1,64 @@
import subprocess as sp
from typing import List
from util.input import password
pkg_manager = ["yay", "--noconfirm"]
def list_explicitly_installed() -> List[str]:
"""List all packages explicitly installed
Returns:
The package list
"""
return run_pkg_manager_cmd(["-Qeq"]).stdout.split()
def remove_orphans() -> bool:
"""Removes all orphan packages
Returns:
True if successful, False otherwise
"""
return (
sp.run(
["pacman", "-Qdtq", "|", "sudo", "pacman", "-Rns", "--noconfirm", "-"],
capture_output=True,
).returncode
== 0
)
def uninstall_package_list(pkgs: List[str]) -> bool:
"""Uninstall all packages in the list
Args:
pkgs: A list of packages to uninstall
Returns:
True if successful, False otherwise
"""
# TODO: Add guard to protect against uninstalling archmgr and confirm uninstalling crucial pkgs
# pkgs.index("archmgr")
# TODO: Consider if we want to print out stdout and stderr on err
return run_pkg_manager_cmd(["-Rs", *pkgs], True).returncode == 0
def install_package_list(pkgs: List[str]) -> bool:
"""Install all packages in the list
Args:
pkgs: A list of packages to install
"""
return run_pkg_manager_cmd(["-S", *pkgs], True).returncode == 0
def run_pkg_manager_cmd(
args: List[str], use_sudo: bool = False
) -> sp.CompletedProcess[str]:
if use_sudo:
pw = password()
return sp.run([*pkg_manager, *args], capture_output=True, text=True, input=pw)
else:
return sp.run([*pkg_manager, *args], capture_output=True, text=True)
+2
View File
@@ -0,0 +1,2 @@
import list
import diff
+64
View File
@@ -0,0 +1,64 @@
from typing import List
import colorama as cl
from list import print_list
def print_diff(add: List[str], remove: List[str]):
if len(add) == 0 and len(remove) == 0:
print(
cl.Fore.BLUE
+ "-->"
+ cl.Style.DIM
+ cl.Fore.GREEN
+ " No packages changes"
+ cl.Style.RESET_ALL,
)
# Packages to be installed
if len(add) == 0:
print(
cl.Fore.BLUE
+ "-->"
+ cl.Style.DIM
+ cl.Fore.GREEN
+ " No packages to be installed"
+ cl.Style.RESET_ALL,
)
else:
print(
cl.Fore.GREEN + "==>",
cl.Style.RESET_ALL + "Packages that will be",
cl.Fore.GREEN + "installed" + cl.Style.RESET_ALL,
)
print_list(add)
print()
# Packages to be removed
if len(remove) == 0:
print(
cl.Fore.BLUE
+ "-->"
+ cl.Style.DIM
+ cl.Fore.GREEN
+ " No packages to be uninstalled"
+ cl.Style.RESET_ALL,
)
else:
print(
cl.Fore.GREEN + "==>",
cl.Style.RESET_ALL + "Packages that will be",
cl.Fore.RED + "uninstalled" + cl.Style.RESET_ALL,
)
print_list(remove)
print()
# Ask user to confirm
print(
cl.Fore.GREEN + "==>",
cl.Style.RESET_ALL
+ f"Transaction (packages): {cl.Fore.BLUE + str(len(add)) + cl.Style.RESET_ALL}",
cl.Fore.GREEN + "installed",
cl.Style.RESET_ALL
+ f"and {cl.Fore.BLUE + str(len(remove)) + cl.Style.RESET_ALL}",
cl.Fore.RED + "uninstalled" + cl.Style.RESET_ALL,
)
+20
View File
@@ -0,0 +1,20 @@
from typing import Any, List
import colorama as cl
def count_digits(number: int):
return len(str(number))
def print_list(list: List[Any]):
digit_count = count_digits(len(list))
for i, pkg in enumerate(list):
print(
" "
+ cl.Fore.BLUE
+ cl.Style.DIM
+ (" " * (digit_count - count_digits(i + 1)))
+ str(1 + i)
+ cl.Style.RESET_ALL + " ",
pkg,
)
View File
View File
-64
View File
@@ -1,64 +0,0 @@
import argparse
import argcomplete
def add_cli_args():
ap = argparse.ArgumentParser(
"archmgr",
description="A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks)",
usage="archmgr [command] [options]",
)
ap.add_argument("-v", "--version", action="version", version="%(prog)s V0.0.1")
sp = ap.add_subparsers(
title="commands",
metavar="Use 'archmgr [command] --help' to see help for each command",
dest="cmd",
)
# ┌ ┐
# │ Subcommands │
# └ ┘
commit = sp.add_parser(
"commit", help="apply pending changes and commit to git repo"
)
commit.add_argument("-f", "--force", help="force apply", action="store_true")
commit.add_argument(
"--no-render", "-r", help="do not re-render renderables", action="store_true"
)
commit.add_argument(
"--dry-run",
"-d",
help="print out files that would be changed",
action="store_true",
)
sp.add_parser("config", help="prints information about your config")
init = sp.add_parser("init", help="initialize a new archmgr repository")
init.add_argument(
"--force",
"-f",
help="resets the git repository and initializes new archmgr repo",
action="store_true",
)
push = sp.add_parser("push", help="push pending changes to git remote")
push.add_argument(
"--force",
"-f",
help="force push (overriding possible remote changes)",
action="store_true",
)
pull = sp.add_parser("pull", help="pull changes from git remote")
pull.add_argument("--rebase", "-r", help="execute rebase", action="store_true")
pull.add_argument(
"--apply",
"-a",
help="also apply the changes to the local system",
action="store_true",
)
argcomplete.autocomplete(ap)
return ap.parse_args(), ap
-8
View File
@@ -1,8 +0,0 @@
from commands.util.choice import confirm_overwrite
def commit(dry_run: bool = False, force: bool = False):
if dry_run:
pass
print("Commit, force:", force)
print(confirm_overwrite())
-6
View File
@@ -1,6 +0,0 @@
def config():
print(
"""
Your config can be found at
"""
)
-14
View File
@@ -1,14 +0,0 @@
import os
import commands.util.git as git
def init(force: bool = False):
dir = os.getcwd()
if force:
[os.remove(dir) for dir in os.listdir(dir)]
git.init(dir)
os.mkdir(dir + "/config")
os.mkdir(dir + "/etc")
with open(dir + "/config.yaml") as file:
file.write("")
print("Initialized a new archmgr repository")
-2
View File
@@ -1,2 +0,0 @@
def pull(rebase: bool = False, apply: bool = False):
print("pull")
-2
View File
@@ -1,2 +0,0 @@
def push(force: bool = False):
print("push")
-13
View File
@@ -1,13 +0,0 @@
from typing import List
def compute_diff(paths: List[str]):
pass
def file_diff(path: str):
pass
def pkg_diff():
pass
-5
View File
@@ -1,5 +0,0 @@
import subprocess
def init(dirname: str):
subprocess.call("git init", cwd=dirname)
+7 -8
View File
@@ -1,8 +1,7 @@
# Concept for tracking changes # Concepts
- Create new commit (if possible) ## Config
- If force is unset: Diff system's files against current state by copying them into the current state branch In python, using functions and args for them
- Then, diff the package state against the state branch by dumping it
- Else, or if no diff, continue
- Copy normal config files into correct directories ## Init
- Render and copy renderables - Copy
- Retrieve explicitly installed packages and remove those that are not present in goal and install those that are not present in current state
+1
View File
@@ -1,3 +1,4 @@
pyyaml
pylette pylette
argcomplete argcomplete
inquirer inquirer
+1
View File
@@ -0,0 +1 @@
import archmgr