Skip to content

Commit 2af3774

Browse files
committed
pkgz is born
0 parents  commit 2af3774

File tree

6 files changed

+296
-0
lines changed

6 files changed

+296
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 roguehashrate <roguehashrate@gmail.com>
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.

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Pkgz
2+
3+
A simple Crystal CLI tool to install, remove, and update applications from APT and Flatpak on Debian-based GNU/Linux systems.
4+
5+
Supports interactive selection when apps are available from multiple sources and manages privilege escalation via system commands.
6+
7+
---
8+
9+
## Features
10+
11+
- Install apps from APT or Flatpak
12+
- Remove apps from all supported sources
13+
- Update system and Flatpak packages
14+
- Interactive choice when multiple sources provide the same app
15+
- Extensible design for adding more package sources
16+
17+
---
18+
19+
## Requirements
20+
21+
- Crystal programming language installed (for building from source)
22+
- Debian-based Linux (Ubuntu, Debian, etc.)
23+
- APT and Flatpak installed and configured
24+
- Optional: sudo or doas for privilege escalation
25+
26+
---
27+
28+
## Installation
29+
30+
### Option 1: Use the Precompiled Binary
31+
32+
Download the `pkgz` binary from the [Releases](https://github.com/yourusername/pkgz/releases) section.
33+
34+
Move it to a directory in your PATH (e.g., `/usr/local/bin`):
35+
36+
```bash
37+
sudo mv pkgz /usr/local/bin/
38+
sudo chmod +x /usr/local/bin/pkgz
39+
```
40+
41+
### Option 2: Build from Source
42+
43+
Clone this repository or download the `pkgz.cr` file.
44+
45+
Compile the program:
46+
47+
```bash
48+
crystal build src/pkgz.cr --release -o pkgz
49+
```
50+
51+
(Optional) Move the compiled binary to your PATH:
52+
53+
```bash
54+
sudo mv pkgz /usr/local/bin/
55+
```
56+
57+
---
58+
59+
## Usage
60+
61+
```bash
62+
pkgz <command> [app-name]
63+
```
64+
65+
You can run commands like:
66+
67+
- `install <app-name>` — install an app
68+
- `remove <app-name>` — remove an app from all sources
69+
- `update` — update all package sources
70+
71+
If multiple sources provide the app, you’ll be prompted to choose.
72+
73+
---
74+
75+
## Example
76+
77+
```bash
78+
$ pkgz install gimp
79+
🔍 Searching for 'gimp' in sources...
80+
📦 Found 'gimp' in multiple sources:
81+
1. APT
82+
2. Flatpak
83+
Which one would you like to use? [1-2]: 2
84+
🚀 Installing with Flatpak...
85+
```
86+
87+
```bash
88+
$ pkgz remove tmux
89+
❌ Trying to remove 'tmux' from APT...
90+
❌ Trying to remove 'tmux' from Flatpak...
91+
```
92+
93+
```bash
94+
$ pkgz update
95+
⬆️ Updating APT packages...
96+
⬆️ Updating Flatpak packages...
97+
```
98+
99+
---
100+
101+
## License
102+
103+
MIT License
104+
105+
Created with Crystal 💎 by roguehashrate

shard.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: pkgz
2+
version: 0.1.0
3+
4+
authors:
5+
- roguehashrate <roguehashrate@gmail.com>
6+
7+
targets:
8+
pkgz:
9+
main: src/pkgz.cr
10+
11+
crystal: '>= 1.16.3'
12+
13+
license: MIT

spec/pkgz_spec.cr

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require "./spec_helper"
2+
3+
describe Pkgz do
4+
# TODO: Write tests
5+
6+
it "works" do
7+
false.should eq(true)
8+
end
9+
end

spec/spec_helper.cr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require "spec"
2+
require "../src/pkgz"

src/pkgz.cr

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# src/pkgz.cr
2+
module Pkgz
3+
VERSION = "0.1.0"
4+
5+
# Auto-detect whether to use doas or sudo
6+
@@elevator = nil
7+
8+
def self.privileged(cmd : String) : Nil
9+
if @@elevator.nil?
10+
@@elevator = system("which doas > /dev/null 2>&1") ? "doas" : "sudo"
11+
end
12+
system("#{@@elevator} #{cmd}")
13+
end
14+
15+
# Abstract blueprint for a package source
16+
abstract class Source
17+
abstract def name : String
18+
abstract def available?(app : String) : Bool
19+
abstract def install(app : String) : Nil
20+
abstract def remove(app : String) : Nil
21+
abstract def update : Nil
22+
end
23+
24+
# APT Source implementation
25+
class AptSource < Source
26+
def name : String
27+
"APT"
28+
end
29+
30+
def available?(app : String) : Bool
31+
output = `apt-cache search #{app}`
32+
output.includes?(app)
33+
end
34+
35+
def install(app : String) : Nil
36+
Pkgz.privileged("apt install -y #{app}")
37+
end
38+
39+
def remove(app : String) : Nil
40+
Pkgz.privileged("apt remove -y #{app}")
41+
end
42+
43+
def update : Nil
44+
Pkgz.privileged("apt update && apt upgrade -y")
45+
end
46+
end
47+
48+
# Flatpak Source implementation
49+
class FlatpakSource < Source
50+
def name : String
51+
"Flatpak"
52+
end
53+
54+
def available?(app : String) : Bool
55+
output = `flatpak search #{app}`
56+
output.includes?(app)
57+
end
58+
59+
def install(app : String) : Nil
60+
system("flatpak install -y #{app}")
61+
end
62+
63+
def remove(app : String) : Nil
64+
system("flatpak uninstall -y #{app}")
65+
end
66+
67+
def update : Nil
68+
system("flatpak update -y")
69+
end
70+
end
71+
72+
# Interactive app install logic
73+
def self.find_and_install(app : String, sources : Array(Source))
74+
puts "🔍 Searching for '#{app}' in sources..."
75+
76+
available_sources = sources.select { |s| s.available?(app) }
77+
78+
if available_sources.empty?
79+
puts "❌ App '#{app}' not found in any source."
80+
return
81+
end
82+
83+
if available_sources.size == 1
84+
source = available_sources.first
85+
puts "✅ Found '#{app}' in #{source.name}. Installing..."
86+
source.install(app)
87+
return
88+
end
89+
90+
puts "📦 Found '#{app}' in multiple sources:"
91+
available_sources.each_with_index do |source, i|
92+
puts "#{i + 1}. #{source.name}"
93+
end
94+
95+
print "Which one would you like to use? [1-#{available_sources.size}]: "
96+
choice = gets.try &.to_i || 0
97+
selected = available_sources[choice - 1]?
98+
99+
if selected
100+
puts "🚀 Installing with #{selected.name}..."
101+
selected.install(app)
102+
else
103+
puts "❌ Invalid choice."
104+
end
105+
end
106+
end
107+
108+
# ─────────────────────────────────────────────
109+
# CLI Entry Point (outside the module)
110+
# ─────────────────────────────────────────────
111+
112+
if ARGV.size < 1
113+
puts "Usage: pkgz <install|remove|update> [app-name]"
114+
exit
115+
end
116+
117+
command = ARGV[0]
118+
app_name = ARGV[1]? # may be nil
119+
120+
sources = [Pkgz::AptSource.new, Pkgz::FlatpakSource.new]
121+
122+
case command
123+
when "install"
124+
if app_name
125+
Pkgz.find_and_install(app_name, sources)
126+
else
127+
puts "Usage: pkgz install <app-name>"
128+
end
129+
when "remove"
130+
if app_name
131+
sources.each do |source|
132+
puts "❌ Trying to remove '#{app_name}' from #{source.name}..."
133+
source.remove(app_name)
134+
end
135+
else
136+
puts "Usage: pkgz remove <app-name>"
137+
end
138+
when "update"
139+
sources.each do |source|
140+
puts "⬆️ Updating #{source.name} packages..."
141+
source.update
142+
end
143+
else
144+
puts "❓ Unknown command: #{command}"
145+
puts "Available commands: install, remove, update"
146+
end

0 commit comments

Comments
 (0)