├── .gitignore ├── LICENSE ├── README.md ├── config.nims ├── install.sh ├── jitter.nim ├── jitter.nimble └── jitter ├── begin.nim ├── finish.nim ├── github.nim ├── log.nim ├── parse.nim └── update.nim /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /TODO.md 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sharpcdf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jitter 2 | A repository-oriented binary manager for Linux 3 | 4 | # Notice 5 | I may revist this project and rework it in the future. 6 | 7 | ## How it works 8 | Jitter searches through GitHub(and hopefully soon more sources) for releases with `.tar.gz`, `.tgz`, `.zip` or `.AppImage` assets. Unlike Homebrew or similar package managers, Jitter does not require a brewfile or nixfile in order to recognize the project. 9 | 10 | ## Installing 11 | Before installing, make sure you have glibc installed on your distro. 12 | 13 | Using the `install.sh` script (recommended): 14 | ``` 15 | wget -qO- https://github.com/sharpcdf/jitter/raw/main/install.sh | bash 16 | ``` 17 | To pass flags such as `--force` or `--uninstall` use: 18 | ``` 19 | wget -qO- https://github.com/sharpcdf/jitter/raw/main/install.sh | bash -s -- --flag 20 | ``` 21 | Through Nimble: 22 | ``` 23 | nimble install https://github.com/sharpcdf/jitter 24 | ``` 25 | Manually (versions above 0.3.0): 26 | Download the latest release and run 27 | ``` 28 | ./jtr setup 29 | ``` 30 | ## Uninstalling 31 | Through the install.sh script: 32 | ``` 33 | wget -qO- https://github.com/sharpcdf/jitter/raw/main/install.sh | bash -s -- --uninstall 34 | ``` 35 | ## Notes 36 | - Right now, Jitter only supports GitHub as a download source. 37 | - You may encounter bugs as this project is still in development, please create an issue if you encounter anything wrong with jitter :) 38 | - On Ubuntu or other distros, you may need to run `sudo apt install glibc-source` or similiar in order to use jitter. 39 | - Because Jitter has no way of knowing what executable is meant to be run by the end user, it uses a loose method of finding the executables, and therefore there may be irrelavant executables added to the bin. 40 | - Jitter requires git to be installed when using the -g flag 41 | ## Building 42 | Clone the repository and run `nimble build` to create a release version, or `nim debug` to debug the code after making changes. 43 | (You need to have Nim and Nimble installed). 44 | ``` 45 | git clone https://github.com/sharpcdf/jitter 46 | cd jitter 47 | nimble build 48 | ``` 49 | 50 | ## Usage 51 | ``` 52 | ❯ jtr -h 53 | A repository-oriented binary manager for Linux 54 | 55 | Usage: 56 | [options] COMMAND 57 | 58 | Commands: 59 | 60 | install Installs the given repository, if avaliable. [gh:][user/]repo[@tag] 61 | update Updates the specified package, Jitter itself, or all packages if specified. [user/repo[@tag]][all][this|jitter|jtr] 62 | remove Removes the specified package from your system. user/repo[@tag] 63 | search Searches for repositories that match the given term, returning them if found. [user/]repo 64 | list Lists all executables downloaded. 65 | catalog Lists all installed packages. 66 | setup Creates needed directories if they do not exist 67 | 68 | Options: 69 | -h, --help 70 | -v, --version 71 | --no-make If makefiles are found in the downloaded package, Jitter ignores them. By default, Jitter runs all found makefiles. 72 | --exactmatch When searching for a repository, only repositories with the query AS THEIR NAME will be shown. Jitter shows any repository returned by the query. 73 | -g Clones the repo, and looks for makefiles or supported file types to build, then adds built executables to the bin 74 | ``` 75 | 76 | ### Example Usage 77 | 1. `jtr install gh:VSCodium/vscodium` - installs repository VSCodium/vscodium from github. 78 | 2. `jtr install vscodium` - searches for all repositories that have the name `vscodium`, and then installs the chosen one 79 | 3. `jtr search vscodium` - searches and lists all repositories that have `vscodium` in their name. 80 | 4. `jtr search VSCodium/vscodium` - searches and lists all release tags of repository `VSCodium/vscodium` 81 | 5. `jtr list` - lists all executables in jitter's bin. 82 | 6. `jtr catalog` - lists all downloaded repositories 83 | 7. `jtr remove VSCodium/vscodium` - removes VSCodium/vscodium from your system 84 | 8. `jtr install VSCodium/vscodium@1.69.0` - installs VSCodium/vscodium release with the tag `1.69.0` 85 | 9. `jtr update VSCodium/vscodium` - updates vscodium to the latest version 86 | 10. ~~`jtr update (this|jitter|jtr)` - updates jitter to the latest release~~ broken in the code revamp, being worked on 87 | 11. `jtr update all` - updates all installed packages 88 | 89 | Note: repositories are case insensitive, and all AppImage file names are converted to the name of the repository. `jtr install VSCodium/vscodium` is equivalent to `jtr install vscodium/vscodium`. 90 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | var mainfile = "jitter.nim" 4 | var version = "0.4.2-dev" 5 | var nimble = getHomeDir() & ".nimble/pkgs" 6 | 7 | 8 | switch("NimblePath", nimble) 9 | switch("define", "version:" & version) 10 | switch("define", "ssl") 11 | 12 | task debug, "Builds the debug version of jitter": 13 | echo "Setting arguments" 14 | switch("verbosity", "3") 15 | switch("define", "debug") 16 | switch("out", "bin/jtr") 17 | switch("opt", "speed") 18 | echo "Done\nCompiling.." 19 | setCommand("c", mainfile) 20 | 21 | task release, "Builds the release version of jitter": 22 | echo "Setting arguments" 23 | switch("verbosity", "0") 24 | switch("define", "release") 25 | switch("out", "bin/jtr") 26 | switch("opt", "size") 27 | switch("hints", "off") 28 | switch("warnings", "off") 29 | setCommand("c", mainfile) 30 | 31 | task setup, "Installs required nimble libraries": 32 | exec("nimble install zippy") 33 | exec("nimble install argpase") -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() { 4 | echo " 5 | Usage: $0 [-f --force] [--uninstall] 6 | 7 | Options: 8 | -f --force: Force installation. 9 | --uninstall: Uninstall Jitter 10 | " 11 | exit 1 12 | } 13 | 14 | while [ "$1" != "" ]; do 15 | case $1 in 16 | -f | --force) 17 | FORCE=true 18 | ;; 19 | --uninstall) 20 | shift 21 | UNINSTALL=true 22 | ;; 23 | -h | --help) 24 | usage 25 | ;; 26 | *) 27 | usage 28 | ;; 29 | esac 30 | shift 31 | done 32 | 33 | if [[ $UNINSTALL ]]; then 34 | if [[ -d "$HOME/.jitter" ]]; then 35 | echo "You will delete all installed packages and Jitter itself." 36 | rm -rf "$HOME/.jitter" 37 | echo "Successfully uninstalled" 38 | exit 0 39 | else 40 | echo "Jitter is not installed" 41 | exit 1 42 | fi 43 | else 44 | if [[ ! -d "$HOME/.jitter" || $FORCE ]]; then 45 | echo "Creating $HOME/.jitter directory" 46 | 47 | if [[ ! -d "$HOME/.jitter" ]]; then mkdir $HOME/.jitter; fi 48 | if [[ ! -d "$HOME/.jitter/bin" ]]; then mkdir $HOME/.jitter/bin; fi 49 | if [[ ! -d "$HOME/.jitter/nerve" ]]; then mkdir $HOME/.jitter/nerve; fi 50 | if [[ ! -d "$HOME/.jitter/config" ]]; then mkdir $HOME/.jitter/config; fi 51 | 52 | echo "Downloading latest Jitter release to $HOME/.jitter/bin" 53 | wget -qO $HOME/.jitter/bin/jtr.tar.gz https://github.com/sharpcdf/jitter/releases/latest/download/jtr.tar.gz 54 | echo "Extracting jtr" 55 | tar -xf $HOME/.jitter/bin/jtr.tar.gz -C $HOME/.jitter/bin 56 | echo "Adding executable permissions" 57 | chmod +x $HOME/.jitter/bin/jtr 58 | echo "Cleaning up" 59 | rm -rf $HOME/.jitter/bin/jtr.tar.gz 60 | echo "Consider adding $HOME/.jitter/bin to your PATH running the following command: " 61 | echo "echo 'export PATH=\$PATH:$HOME/.jitter/bin' >> $HOME/.bashrc" 62 | exit 0 63 | 64 | else 65 | echo "Jitter is already installed, force an installation using the --force flag or uninstall it using the --uninstall flag." 66 | exit 1 67 | fi 68 | fi 69 | -------------------------------------------------------------------------------- /jitter.nim: -------------------------------------------------------------------------------- 1 | import std/[sequtils, strutils, terminal, os] 2 | 3 | import argparse 4 | 5 | import jitter/[begin, log, update] 6 | 7 | #TODO add 'jtr update all' to update all packages 8 | #TODO add config file to manage bin & download directory 9 | 10 | const version {.strdefine.} = "undefined" 11 | when not defined(version): 12 | raise newException(ValueError, "Version has to be specified, -d:version=x.y.z") 13 | 14 | 15 | const parser = newParser: 16 | help("A repository-oriented binary manager for Linux") ## Help message 17 | flag("-v", "--version") ## Create a version flag 18 | flag("--no-make", help = "If makefiles are found in the downloaded package, Jitter ignores them. By default, Jitter runs all found makefiles.") ## Create a no-make flag 19 | flag("--exactmatch", help = "When searching for a repository, only repositories with the query AS THEIR NAME will be shown. Jitter shows any repository returned by the query.") 20 | flag("-g", help = "Clones the repo, and looks for makefiles or supported file types to build, then adds built executables to the bin.") 21 | flag("-q", "--quiet", help = "Runs jitter, logging only fatals and errors.") 22 | flag("--upgrade") 23 | run: 24 | log.setQuiet(opts.quiet) 25 | if opts.version: ## If the version flag was passed 26 | styledEcho(fgCyan, "Jitter version ", fgYellow, version) 27 | styledEcho("For more information visit ", fgGreen, "https://github.com/sharpcdf/jitter") 28 | 29 | command("install"): ## Create an install command 30 | help("Installs the given repository, if avaliable. [gh:][user/]repo[@tag]") ## Help message 31 | arg("input") ## Positional argument called input 32 | run: 33 | opts.input.install(not opts.parentOpts.nomake, opts.parentOpts.g) 34 | 35 | command("update"): ## Create an update command 36 | help("Updates the specified package, Jitter itself, or all packages if specified. [user/repo[@tag]][all][this|jitter|jtr]") ## Help message 37 | arg("input") ## Positional argument called input 38 | run: 39 | if opts.input == "upgrade": 40 | upgrade() 41 | elif opts.input != "jtr": 42 | opts.input.update(not opts.parentOpts.nomake) 43 | else: 44 | selfUpdate() 45 | command("remove"): ## Create a remove command 46 | help("Removes the specified package from your system. user/repo[@tag]") ## Help message 47 | arg("input") ## Positional arugment called input 48 | run: 49 | opts.input.toLowerAscii().remove() 50 | command("search"): ## Create a search command 51 | help("Searches for repositories that match the given term, returning them if found. [user/]repo") ## Help message 52 | arg("query") ## Positional argument called query 53 | run: 54 | opts.query.search(opts.parentOpts.exactmatch) 55 | command("list"): ## Create a list command 56 | help("Lists all executables downloaded.") ## Help message 57 | run: 58 | list() 59 | command("catalog"): ## Create a catalog command 60 | help("Lists all installed packages.") ## Help message 61 | run: 62 | catalog() 63 | command("setup"): 64 | help("Creates needed directories if they do not exist") 65 | run: 66 | setup() 67 | 68 | when isMainModule: 69 | if commandLineParams().len == 0: 70 | parser.run(@["--help"]) 71 | else: 72 | try: 73 | if dirExists(getHomeDir() / ".jitter"): 74 | parser.run() 75 | else: 76 | error "Jitter is not installed" 77 | let yes = prompt("Do you want to install jitter?") 78 | if yes: 79 | parser.run(@["setup"]) 80 | else: 81 | info "Check https://github.com/sharpcdf/jitter for more information on installing jitter." 82 | 83 | except ShortCircuit, UsageError: 84 | error "Error parsing arguments. Make sure to dot your Ts and cross your Is and try again. Oh, wait." 85 | -------------------------------------------------------------------------------- /jitter.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.4.2-dev" 4 | author = "sharpcdf" 5 | description = "A git-based binary manager for linux." 6 | license = "MIT" 7 | namedBin["jitter"] = "jtr" 8 | binDir = "bin" 9 | # Dependencies 10 | 11 | requires "nim >= 1.6.5" 12 | requires "zippy >= 0.10.3" 13 | requires "argparse >= 3.0.0" 14 | 15 | task release, "Build Jitter for release": 16 | exec "nimble build -d:release -d:version=" & version 17 | -------------------------------------------------------------------------------- /jitter/begin.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, sequtils, strutils, terminal, os, httpclient, json] 2 | import parse, github, log 3 | 4 | let baseDir = getHomeDir() / ".jitter" 5 | let nerveDir = baseDir / "nerve" 6 | let binDir = baseDir / "bin" 7 | 8 | proc getInstalledPkgs*(): seq[Package] = 9 | for kind, path in walkDir(nerveDir): 10 | if kind == pcDir and (let (ok, pkg) = path.splitPath.tail.parsePkgFormat(); ok): 11 | result.add(pkg) 12 | 13 | proc search*(query: string, exactmatch = false) = 14 | if '/' in query: 15 | let (ok, pkg) = query.parsePkgFormat() 16 | if not ok: 17 | fatal fmt"Couldn't parse package {query}" 18 | 19 | discard pkg.ghListReleases() 20 | else: 21 | for pkg in query.ghSearch(exactmatch): 22 | list fmt"(Github) {pkg.pkg.gitFormat()}: {pkg.description}" 23 | 24 | proc setup*() = 25 | if dirExists(getHomeDir() / ".jitter"): 26 | fatal "Jitter is already set up!" 27 | info "Creating directories" 28 | createDir(getHomeDir() / ".jitter") 29 | createDir(getHomeDir() / ".jitter/bin") 30 | createDir(getHomeDir() / ".jitter/nerve") 31 | createDir(getHomeDir() / ".jitter/config") 32 | success "Done!" 33 | let yes = prompt(&"Do you want to add {getAppDir()} to your path?") 34 | if yes: 35 | if &"export PATH=$PATH:{getAppDir()}" in readFile(getHomeDir() / ".bashrc"): 36 | error "Jitter is already in your .bashrc file!" 37 | else: 38 | let f = open(getHomeDir() / ".bashrc", fmAppend) 39 | f.writeLine(&"export PATH=$PATH:{getAppDir()}") 40 | f.close() 41 | success "Added to bash path!" 42 | if getEnv("SHELL") == "/usr/bin/fish": 43 | info "Adding jitter to fish user paths via config.fish file" 44 | if &"set -U fish_user_paths $fish_user_paths {getAppDir()}" in readFile( 45 | &"{getHomeDir()}.config/fish/config.fish"): 46 | error "Jitter is already in your config.fish file!" 47 | else: 48 | let f = open(getHomeDir() / ".config/fish/config.fish", fmAppend) 49 | f.writeLine(&"set -U fish_user_paths $fish_user_paths {getAppDir()}") 50 | f.close() 51 | success "Added to fish path!" 52 | else: 53 | info &"Consider running 'echo \"export PATH=$PATH:{getAppDir()}\" >> {getHomeDir()}.bashrc' to add it to your bash path." 54 | 55 | 56 | proc install*(input: string, make = true, build = false) = 57 | let (srctype, input) = input.parseInputSource() 58 | if '/' notin input: 59 | info fmt"Searching for {input}" 60 | input.ghDownload(make, build) 61 | return 62 | 63 | let (ok, pkg) = input.parsePkgFormat() 64 | 65 | if not ok: 66 | fatal fmt"Couldn't parse package {input}" 67 | 68 | var success = true 69 | case srctype: 70 | of GitHub: 71 | pkg.ghDownload(make, build) 72 | of GitLab: 73 | #pkg.glDownload(make) 74 | discard 75 | of SourceHut: 76 | # pkg.shDownload(make) 77 | discard 78 | of CodeBerg: 79 | #pkg.cbDownload(make) 80 | discard 81 | of Undefined: 82 | 83 | pkg.ghDownload(make, build) 84 | #pkg.glDownload(make) 85 | #pkg.cbDownload(make) 86 | #pkg.shDownload(make) 87 | 88 | if success: 89 | success "Binaries successfully installed" 90 | 91 | proc remove*(pkg: Package) = 92 | for kind, path in walkDir(binDir): 93 | if kind == pcLinkToFile and pkg.pkgFormat() in path.expandSymlink(): 94 | info fmt"Removing symlink {path}" 95 | path.removeFile() 96 | 97 | removeDir(nerveDir / pkg.pkgFormat().toLowerAscii()) 98 | 99 | proc remove*(input: string) = 100 | let (ok, pkg) = input.parsePkgFormat() 101 | if not ok: 102 | fatal fmt"Couldn't parse package {input}" 103 | 104 | let installedPkgs = getInstalledPkgs() 105 | 106 | if not installedPkgs.anyIt(it.owner == pkg.owner and it.repo == pkg.repo): 107 | fatal fmt"{input} package is not installed" 108 | 109 | if pkg.tag.len == 0: 110 | ask "Which tag would you like to remove?" 111 | #goes through all installed package versions and lists them 112 | for instPkg in installedPkgs: 113 | if instPkg.owner == pkg.owner and instPkg.repo == pkg.repo: 114 | list instPkg.tag 115 | #lists the option to remove all versions 116 | list "All" 117 | 118 | let answer = stdin.readLine().strip() 119 | case answer.toLowerAscii(): 120 | of "all": 121 | for instPkg in installedPkgs: 122 | if instPkg.owner == pkg.owner and instPkg.repo == pkg.repo: 123 | instPkg.remove() 124 | else: 125 | var valid = false 126 | for instPkg in installedPkgs: 127 | if instPkg.tag == answer: 128 | valid = true 129 | if not valid: 130 | fatal "Invalid tag" 131 | else: 132 | package(pkg.owner, pkg.repo, answer).remove() 133 | else: 134 | pkg.remove() 135 | 136 | success "Done" 137 | 138 | proc update*(input: string, make = true) = 139 | if input.toLowerAscii() == "all": 140 | for pkg in getInstalledPkgs(): 141 | pkg.remove() 142 | pkg.ghDownload(make) 143 | return 144 | let (ok, pkg) = input.parsePkgFormat() 145 | if not ok: 146 | fatal fmt"Couldn't parse package {input}" 147 | 148 | input.remove() 149 | pkg.ghDownload(make) 150 | 151 | success fmt"Successfully updated {pkg.owner}/{pkg.repo}" 152 | 153 | proc list*() = 154 | for kind, path in walkDir(binDir): 155 | if path.hasExecPerms() and path.extractFilename() != "jtr": 156 | list path.extractFilename() 157 | 158 | proc catalog*() = 159 | for pkg in getInstalledPkgs(): 160 | list pkg.gitFormat() 161 | -------------------------------------------------------------------------------- /jitter/finish.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, strutils, osproc, os] 2 | 3 | import zippy/tarballs 4 | import zippy/ziparchives 5 | 6 | import log, parse 7 | 8 | let baseDir = getHomeDir() & ".jitter/" 9 | let nerveDir = baseDir / "nerve" 10 | let binDir = baseDir / "bin" 11 | var dest: string 12 | var dup: bool 13 | proc makeSymlink(file:string, pkg:Package) = 14 | case file.splitFile().ext: 15 | of "": 16 | if not file.hasExecPerms() and file.isExecFile(): 17 | file.setFilePermissions({fpUserExec, fpOthersExec, fpUserRead, fpUserWrite, fpOthersRead, fpOthersWrite}) 18 | if file.hasExecPerms() and file.isExecFile(): 19 | let name = 20 | if not dup: 21 | file.splitFile().name 22 | else: 23 | fmt"{file.splitFile().name}@{pkg.tag}" 24 | try: 25 | file.createSymlink(binDir / name) 26 | except: 27 | error fmt"Failed to create symlink for {file}" 28 | success fmt"Created symlink {name} from {file}" 29 | of ".AppImage": 30 | let name = 31 | if not dup: 32 | pkg.repo 33 | else: 34 | fmt"{pkg.repo}@{pkg.tag}" 35 | try: 36 | file.createSymlink(binDir / name) 37 | except: 38 | fatal fmt"Failed to create symlink for {pkg.repo}" 39 | success fmt"Created symlink {pkg.repo}" 40 | 41 | proc walkForExec*(pkg: Package) = 42 | #Creates symlinks for executables and adds them to the bin 43 | for file in walkDirRec(nerveDir / dest): 44 | if ".git" in file: 45 | continue 46 | makeSymlink(file, pkg) 47 | 48 | proc make*() = 49 | for path in walkDirRec(nerveDir / dest): 50 | if path.extractFilename().toLowerAscii() == "makefile": 51 | info fmt"Attempting to make {path}" 52 | if (let ex = execCmdEx(fmt"make -C {path.splitFile.dir}"); ex.exitCode != 0): 53 | error fmt"Failed to make {path}: {ex.output}" 54 | else: 55 | success fmt"Successfully made makefile {path}" 56 | 57 | proc build*(pkg: Package, dupl = false) = 58 | dest = pkg.pkgFormat() 59 | dup = dupl 60 | var built = false 61 | for p in walkDir(nerveDir / dest): 62 | let path = p.path 63 | #checks if file is a makefile or sh file with build in the name 64 | if path.extractFilename().toLowerAscii() == "makefile": 65 | info fmt"Attempting to build package with file {path}..." 66 | if (let ex = execCmdEx(fmt"make -C {path.splitFile.dir}"); ex.exitCode != 0): 67 | error fmt"Failed to make {path}: {ex.output}" 68 | else: 69 | success fmt"Successfully made makefile {path}" 70 | built = true 71 | elif "build" in path.extractFilename().toLowerAscii() and path.splitFile().ext == ".sh": 72 | info fmt"Attempting to build package with file {path}..." 73 | if (let ex = execCmdEx(fmt"sh {path}"); ex.exitCode != 0): 74 | error fmt"Failed to run shell script {path}: {ex.output}" 75 | else: 76 | success fmt"Successfully ran shell script {path}" 77 | built = true 78 | if not built: 79 | error "No build files could be made." 80 | var i = prompt("Would you like to remove the repository?") 81 | if i: 82 | removeDir(nerveDir / pkg.pkgFormat().toLowerAscii()) 83 | return 84 | pkg.walkForExec() 85 | 86 | proc extract*(pkg: Package, path, toDir: string, make = true) = 87 | dest = toDir 88 | dup = pkg.duplicate() 89 | ## Extracts `path` inside `toDir` directory. 90 | info "Extracting files" 91 | try: 92 | #toDir should be called a package version of the repo name, in nerve directory 93 | #path is downloadPath in github.nim 94 | if path.splitFile().ext == ".zip": 95 | ziparchives.extractAll(path, nerveDir / toDir) 96 | elif path.splitFile().ext == ".AppImage": 97 | createDir(nerveDir / toDir) 98 | path.setFilePermissions({fpUserExec, fpUserRead, fpUserWrite, fpOthersExec, fpOthersRead, fpOthersWrite, fpGroupExec}) 99 | moveFile(path, nerveDir / toDir / path.extractFilename()) 100 | else: 101 | tarballs.extractAll(path, nerveDir / toDir) 102 | except ZippyError: 103 | for f in walkDir(nerveDir): 104 | if f.path == path: 105 | removeFile(path) 106 | break 107 | fatal "Failed to extract archive [ZippyError]" 108 | except IOError: 109 | for f in walkDir(nerveDir): 110 | if f.path == path: 111 | removeFile(path) 112 | break 113 | fatal "Failed to extract archive [IOError]" 114 | 115 | path.removeFile() 116 | success "Files extracted" 117 | 118 | #if the --no-make flag isnt passed than this happens 119 | if make: 120 | make() 121 | else: 122 | info "--no-make flag found, skipping make process" 123 | info "Adding executables to bin" 124 | 125 | walkForExec(pkg) 126 | -------------------------------------------------------------------------------- /jitter/github.nim: -------------------------------------------------------------------------------- 1 | import std/[httpclient, strformat, strutils, json, uri, os, osproc] 2 | 3 | import finish, parse, log 4 | 5 | #TODO add support for appimage downloads 6 | #TODO prefer appimages -> .tar.gz -> .tgz -> .zip 7 | 8 | let baseDir = getHomeDir() & ".jitter/" 9 | let nerveDir = baseDir / "nerve" 10 | 11 | 12 | #Clones and builds the repo 13 | proc ghBuild*(pkg: Package) = 14 | let p = package(pkg.owner, pkg.repo, "current") 15 | info fmt"Attempting to build {p.owner}/{p.repo}" 16 | let dup = pkg.duplicate() 17 | info "Cloning repository..." 18 | let url = fmt"https://github.com/{p.owner}/{p.repo}" 19 | if (let ex = execCmdEx(&"git clone {url} {nerveDir}/{p.pkgFormat()}/"); ex.exitCode != 0): 20 | fatal fmt"Failed to clone git repository: {ex.output}" 21 | p.build(dup) 22 | 23 | proc ghListReleases*(pkg: Package): seq[string] = 24 | ## List and return pkg release tags. 25 | let url = fmt"https://api.github.com/repos/{pkg.owner}/{pkg.repo}/releases" 26 | let client = newHttpClient() 27 | var content: string 28 | 29 | try: 30 | content = client.getContent(url) 31 | except HttpRequestError: 32 | fatal "Failed to find repository" 33 | finally: 34 | client.close() 35 | 36 | let data = content.parseJson() 37 | 38 | if data.kind != JArray: 39 | fatal fmt"Failed to find {pkg.gitFormat} releases" 40 | 41 | info fmt"Listing release tags for {pkg.gitFormat}" 42 | 43 | for release in data.getElems(): 44 | list release["tag_name"].getStr() 45 | result.add(release["tag_name"].getStr()) 46 | 47 | proc ghSearch*(repo: string, exactmatch: bool = false): seq[Repository] = 48 | let url = "https://api.github.com/search/repositories?" & encodeQuery({"q": repo}) 49 | let client = newHttpClient() 50 | var content: string 51 | 52 | try: 53 | content = client.getContent(url) 54 | except HttpRequestError: 55 | fatal "Failed to find repositories" 56 | finally: 57 | client.close() 58 | 59 | for r in content.parseJson()["items"]: 60 | if not exactmatch: 61 | result.add(repo(parsePkgFormat(r["full_name"].getStr()).pkg, r["description"].getStr())) 62 | else: 63 | if r["name"].getStr().toLowerAscii() == repo.toLowerAscii(): 64 | result.add(repo(parsePkgFormat(r["full_name"].getStr()).pkg, r["description"].getStr())) 65 | else: 66 | continue 67 | 68 | proc downloadRelease(pkg: Package, make = true) = 69 | 70 | let url = 71 | if pkg.tag == "": 72 | fmt"https://api.github.com/repos/{pkg.owner}/{pkg.repo}/releases/latest" 73 | else: 74 | fmt"https://api.github.com/repos/{pkg.owner}/{pkg.repo}/releases/tags/{pkg.tag}" 75 | 76 | let client = newHttpClient(headers = newHttpHeaders([("accept", "application/vnd.github+json")])) 77 | var content: string 78 | 79 | try: 80 | content = client.getContent(url) 81 | except HttpRequestError: 82 | fatal &"Failed to download {pkg.gitFormat()}." 83 | finally: 84 | client.close() 85 | 86 | let data = content.parseJson() 87 | let pkg = package(pkg.owner, pkg.repo, data["tag_name"].getStr()) 88 | 89 | info "Looking for compatible archives" 90 | #TODO make download specific to cpu type 91 | 92 | var downloadUrl, downloadPath: string 93 | for asset in data["assets"].getElems(): 94 | let name = asset["name"].getStr() 95 | #Checks if asset has extension .tar.gz, .tar.xz, .tgz, is not ARM 96 | if name.isCompatibleExt() and name.isCompatibleCPU() and name.isCompatibleOS(): 97 | downloadUrl = asset["browser_download_url"].getStr() 98 | downloadPath = name 99 | success fmt"Archive found: {name}" 100 | let yes = prompt("Are you sure you want to download this archive? There might be other compatible assets.") 101 | if yes: 102 | break 103 | else: 104 | downloadUrl = "" 105 | downloadPath = "" 106 | continue 107 | 108 | if downloadUrl.len == 0: 109 | fatal fmt"No archives found for {pkg.gitFormat()}" 110 | for f in walkDir(nerveDir): 111 | #f.path.splitFile().name is just the repository in package format, this checks if that repository is the same repository AND the same version as the queued one 112 | if f.path.splitFile().name == pkg.pkgFormat(): 113 | fatal "Repository is already installed, try installing a different version" 114 | info fmt"Downloading {downloadUrl}" 115 | #downloadPath should be ~/.jitter/nerve/repo-release.tar.gz or similar 116 | client.downloadFile(downloadUrl, nerveDir / downloadPath) 117 | success fmt"Downloaded {pkg.gitFormat}" 118 | pkg.extract(nerveDir / downloadPath, pkg.pkgFormat(), make) 119 | 120 | proc ghDownload*(pkg: Package, make = true, build = false) = 121 | if not build: 122 | pkg.downloadRelease(make) 123 | else: 124 | pkg.ghBuild() 125 | 126 | #Downloads repo without owner 127 | proc ghDownload*(repo: string, make = true, build = false) = 128 | let pkgs = repo.ghSearch(true) 129 | for pkg in pkgs: 130 | if pkg.pkg.repo.toLowerAscii() == repo.toLowerAscii(): 131 | success fmt"Repository found: {pkg.pkg.gitFormat()}" 132 | let yes = prompt("Are you sure you want to install this repository?") 133 | if yes: 134 | if not build: 135 | pkg.pkg.ghDownload(make, build) 136 | else: 137 | pkg.pkg.ghBuild() 138 | return 139 | else: 140 | continue 141 | if pkgs.len == 0: 142 | fatal "No repositories found" -------------------------------------------------------------------------------- /jitter/log.nim: -------------------------------------------------------------------------------- 1 | import std/[terminal, strformat, strutils] 2 | 3 | var quiet: bool 4 | 5 | proc info*(s: string) = 6 | if not quiet: 7 | styledEcho({styleBright}, fgCyan, "[I]", resetStyle, " ", fgBlue, styleUnderscore, s) 8 | 9 | proc fatal*(s: string) = 10 | styledEcho({styleBright}, fgRed, "[F]", resetStyle, " ", fgRed, styleUnderscore, s) 11 | quit(1) 12 | 13 | proc success*(s: string) = 14 | if not quiet: 15 | styledEcho({styleBright}, fgGreen, "[S]", resetStyle, " ", fgGreen, styleItalic, s) 16 | 17 | proc list*(s: string) = 18 | styledEcho({styleBright, styleItalic}, fgMagenta, s) 19 | 20 | proc ask*(s: string) = 21 | styledEcho({styleBright}, fgBlue, "[Q]", resetStyle, " ", fgYellow, s) 22 | 23 | proc error*(s: string) = 24 | styledEcho({styleBright}, fgRed, "[E]", resetStyle, " ", fgRed, s) 25 | 26 | proc prompt*(question: string): bool = 27 | ask fmt"{question} [y/n]" 28 | let r = stdin.readLine().strip() 29 | case r.toLowerAscii(): 30 | of "n", "no": 31 | return false 32 | of "y", "yes": 33 | return true 34 | else: 35 | error "Invalid answer given." 36 | prompt(question) 37 | 38 | proc setQuiet*(q: bool) = 39 | if q: 40 | quiet = true 41 | else: 42 | quiet = false -------------------------------------------------------------------------------- /jitter/parse.nim: -------------------------------------------------------------------------------- 1 | import std/[strformat, strutils, strscans, os] 2 | 3 | let baseDir = getHomeDir() & ".jitter/" 4 | let nerveDir = baseDir / "nerve" 5 | 6 | type 7 | Package* = object 8 | owner*, repo*, tag*: string 9 | 10 | SourceType* = enum 11 | Undefined, 12 | GitHub, 13 | GitLab, 14 | SourceHut, 15 | CodeBerg 16 | 17 | Repository* = object 18 | pkg*: Package 19 | description*: string 20 | 21 | const 22 | # Supported 23 | extensions = [".tar.gz", ".tgz", ".zip", ".AppImage"] 24 | # Not supported 25 | unsupportedCPU = ["arm32", "arm64", "-arm", "arm-"] 26 | unsupportedOS = ["darwin", "windows", "osx", "macos", "win"] 27 | 28 | proc package*(owner, repo, tag: string): Package = 29 | return Package(owner: owner, repo: repo, tag: tag) 30 | 31 | proc repo*(pkg: Package, d: string): Repository = 32 | return Repository(pkg: pkg, description: d) 33 | 34 | proc parseInputSource*(input: string): tuple[source: SourceType, output: string] = 35 | ## Parses the source prefix and returns the (source, input without the preffix). 36 | result.source = 37 | if input.startsWith("gh:"): GitHub 38 | elif input.startsWith("gl:"): GitLab 39 | elif input.startsWith("cb:"): CodeBerg 40 | elif input.startsWith("sh:"): SourceHut 41 | else: Undefined 42 | 43 | if result.source != Undefined: 44 | result.output = input[3..^1] 45 | else: 46 | result.output = input 47 | 48 | proc isCompatibleExt*(file: string): bool = 49 | result = false 50 | for ext in extensions: 51 | if file.endsWith(ext): 52 | return true 53 | 54 | proc isCompatibleCPU*(file: string): bool = 55 | result = true 56 | for cpu in unsupportedCPU: 57 | if cpu in file: 58 | return false 59 | 60 | proc isCompatibleOS*(file: string): bool = 61 | result = true 62 | for os in unsupportedOS: 63 | if os in file: 64 | return false 65 | 66 | proc isExecFile*(file:string): bool = 67 | if file.splitFile().name.startsWith(".") or file.splitFile().name.toLowerAscii() == "makefile" or "license" in file.splitFile().name.toLowerAscii() or file.splitFile().ext != "": 68 | return false 69 | else: 70 | if file.getFileInfo().kind == pcDir: 71 | return false 72 | else: 73 | return true 74 | 75 | proc hasExecPerms*(file: string): bool = 76 | let perms = getFilePermissions(file) 77 | if fpUserExec in perms or fpGroupExec in perms or fpOthersExec in perms: 78 | return true 79 | else: 80 | return false 81 | 82 | proc validIdent(input: string, strVal: var string, start: int, validChars = IdentChars + {'.', '-'}): int = 83 | while start + result < input.len and input[start + result] in validChars: 84 | strVal.add(input[start + result]) 85 | inc result 86 | 87 | proc parsePkgFormat*(pkg: string): tuple[ok: bool, pkg: Package] = 88 | ## Parses packages in two formats: 89 | ## - `owner__repo__tag` 90 | ## - `owner/repo[@tag]` Tag is optional 91 | 92 | var (success, owner, repo, tag) = scanTuple(pkg, "${validIdent()}/${validIdent()}@$+$.", string, string) 93 | 94 | if not success: 95 | if owner.len > 0 and repo.len > 0 and tag.len == 0: # No tag 96 | success = true 97 | else: 98 | (success, owner, repo, tag) = scanTuple(pkg, "${validIdent()}::${validIdent()}::$+$.", string, string) 99 | 100 | if success: 101 | result.ok = success 102 | result.pkg = package(owner, repo, tag) 103 | 104 | proc gitFormat*(pkg: Package): string = 105 | if pkg.owner.len > 0 and pkg.repo.len > 0: 106 | if pkg.tag.len > 0: 107 | return fmt"{pkg.owner}/{pkg.repo}@{pkg.tag}" 108 | else: 109 | return fmt"{pkg.owner}/{pkg.repo}" 110 | 111 | proc pkgFormat*(pkg: Package): string = 112 | return fmt"{pkg.owner.toLowerAscii()}::{pkg.repo.toLowerAscii()}::{pkg.tag}" 113 | 114 | proc duplicate*(pkg:Package): bool = 115 | for p in walkDir(nerveDir): 116 | if &"{pkg.owner}::{pkg.repo}" in p.path.splitPath().tail: 117 | #echo fmt"for some reason its true: {pkg.owner}::{pkg.repo} is in {p.path.splitPath().tail}" 118 | return true 119 | -------------------------------------------------------------------------------- /jitter/update.nim: -------------------------------------------------------------------------------- 1 | import std/[httpclient, json, strformat, os, osproc] 2 | import log, zippy/tarballs 3 | 4 | let baseDir = getHomeDir() / ".jitter" 5 | let binDir = baseDir / "bin" 6 | 7 | #* super important to match release asset name (currently jtr.tar.gz) to this 8 | proc selfUpdate*() = 9 | let url = "https://api.github.com/repos/sharpcdf/jitter/releases/latest" 10 | let client = newHttpClient(headers = newHttpHeaders([("accept", "application/vnd.github+json")])) 11 | var content: string 12 | try: 13 | content = client.getContent(url) 14 | except HttpRequestError: 15 | fatal "Failed to download latest release of Jitter." 16 | finally: 17 | client.close() 18 | 19 | let ar = content.parseJson()["assets"][0]["browser_download_url"].getStr() 20 | echo ar 21 | try: 22 | client.downloadFile(ar, binDir) 23 | except HttpRequestError: 24 | fatal "Failed to download latest release of Jitter." 25 | finally: 26 | client.close() 27 | 28 | try: 29 | tarballs.extractAll(binDir / "jtr.tar.gz", binDir / "new/") 30 | except: 31 | removeFile(binDir / "jtr.tar.gz") 32 | fatal "Failed to extract" 33 | removeFile(binDir / "jtr.tar.gz") 34 | discard startProcess(fmt"./jtr", "{binDir}/jtr/new/", ["update", "upgrade"]) 35 | 36 | proc upgrade*() = 37 | if getAppDir().splitPath().tail == "new/": 38 | removeFile(binDir / "jtr") 39 | (getAppDir()/getAppFilename()).moveFile(binDir/"jtr") --------------------------------------------------------------------------------