monitorify 1.0.3__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {monitorify-1.0.3 → monitorify-1.1.0}/PKG-INFO +4 -1
  2. {monitorify-1.0.3 → monitorify-1.1.0}/README.md +1 -0
  3. {monitorify-1.0.3 → monitorify-1.1.0}/pyproject.toml +4 -1
  4. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/__init__.py +1 -1
  5. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config.py +1 -1
  6. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/main.py +28 -34
  7. monitorify-1.1.0/src/monitorify/remote/__init__.py +4 -0
  8. monitorify-1.1.0/src/monitorify/remote/addRemote.py +157 -0
  9. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/remote/connector.py +82 -8
  10. monitorify-1.1.0/src/monitorify/remote/install.sh +81 -0
  11. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/storage/db.py +148 -2
  12. monitorify-1.1.0/src/monitorify/storage/keyring_helper.py +48 -0
  13. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/components/menu.py +95 -54
  14. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/css/tui.css +64 -6
  15. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/tui.py +86 -1
  16. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/programmListWidget.py +71 -8
  17. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/PKG-INFO +4 -1
  18. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/SOURCES.txt +7 -1
  19. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/requires.txt +2 -0
  20. monitorify-1.1.0/tests/test_menu.py +75 -0
  21. monitorify-1.1.0/tests/test_programm_list_search.py +138 -0
  22. monitorify-1.1.0/tests/test_remote_db.py +207 -0
  23. monitorify-1.0.3/src/monitorify/remote/__init__.py +0 -3
  24. {monitorify-1.0.3 → monitorify-1.1.0}/setup.cfg +0 -0
  25. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/__main__.py +0 -0
  26. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/__init__.py +0 -0
  27. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/manager.py +0 -0
  28. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/schema.py +0 -0
  29. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/collector/worker.py +0 -0
  30. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config/__init__.py +0 -0
  31. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config/configManager.py +0 -0
  32. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/config_manager.py +0 -0
  33. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/daemon.py +0 -0
  34. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/DiskStats.py +0 -0
  35. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/__init__.py +0 -0
  36. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/cpuStats.py +0 -0
  37. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/networkStats.py +0 -0
  38. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/programmList.py +0 -0
  39. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/stats/ramStats.py +0 -0
  40. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/storage/__init__.py +0 -0
  41. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/__init__.py +0 -0
  42. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/__init__.py +0 -0
  43. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/components/widgetManager.py +0 -0
  44. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/__init__.py +0 -0
  45. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/brailleGraph.py +0 -0
  46. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/cpuWidget.py +0 -0
  47. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/diskWidget.py +0 -0
  48. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/networkWidget.py +0 -0
  49. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/procInfoWidget.py +0 -0
  50. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/ramWidget.py +0 -0
  51. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/statusWidget.py +0 -0
  52. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/dependency_links.txt +0 -0
  53. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/entry_points.txt +0 -0
  54. {monitorify-1.0.3 → monitorify-1.1.0}/src/monitorify.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: monitorify
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: A lightweight Linux terminal system monitor built with Textual
5
5
  Author: Leon
6
6
  License: MIT
@@ -9,6 +9,8 @@ Description-Content-Type: text/markdown
9
9
  Requires-Dist: textual>=0.80.0
10
10
  Requires-Dist: platformdirs>=4.0.0
11
11
  Requires-Dist: rich>=13.0.0
12
+ Requires-Dist: paramiko>=3.0.0
13
+ Requires-Dist: keyring>=24.0.0
12
14
 
13
15
  # Monitorify
14
16
  ### A terminal montioring tool for linux
@@ -123,6 +125,7 @@ To use Monitorify remotely over SSH on your server, you can set the following pa
123
125
  - `--username` – Set the username for the remote host.
124
126
  - `--password` – Set the password for the SSH server.
125
127
  - `--key_filename` – Set the path to an SSH key *(optional)*.
128
+ - `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
126
129
 
127
130
  ## Tech Stack
128
131
 
@@ -111,6 +111,7 @@ To use Monitorify remotely over SSH on your server, you can set the following pa
111
111
  - `--username` – Set the username for the remote host.
112
112
  - `--password` – Set the password for the SSH server.
113
113
  - `--key_filename` – Set the path to an SSH key *(optional)*.
114
+ - `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
114
115
 
115
116
  ## Tech Stack
116
117
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "monitorify"
7
- version = "1.0.3"
7
+ version = "1.1.0"
8
8
  description = "A lightweight Linux terminal system monitor built with Textual"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -16,6 +16,8 @@ dependencies = [
16
16
  "textual>=0.80.0",
17
17
  "platformdirs>=4.0.0",
18
18
  "rich>=13.0.0",
19
+ "paramiko>=3.0.0",
20
+ "keyring>=24.0.0",
19
21
  ]
20
22
 
21
23
  [project.scripts]
@@ -27,3 +29,4 @@ where = ["src"]
27
29
 
28
30
  [tool.setuptools.package-data]
29
31
  "monitorify.ui.tui" = ["css/*.css"]
32
+ "monitorify.remote" = ["*.sh"]
@@ -1,3 +1,3 @@
1
1
  """Monitorify - A lightweight Linux terminal system monitor."""
2
2
 
3
- __version__ = "1.0.3"
3
+ __version__ = "1.1.0"
@@ -22,7 +22,7 @@ CONFIG_PATH = Path(os.getenv("MONITORIFY_CONFIG_PATH", str(DATA_DIR / "config.js
22
22
  # Daemon PID file path
23
23
  DAEMON_PIDFILE = Path(os.getenv("MONITORIFY_PID_PATH", str(RUNTIME_DIR / "daemon.pid")))
24
24
 
25
- # Collection settings (seconds)
25
+ # Collection settings
26
26
  DEFAULT_COLLECTION_INTERVAL = float(os.getenv("MONITORIFY_INTERVAL", "1.0"))
27
27
 
28
28
  # Retention settings (default: 7 days in seconds)
@@ -13,9 +13,17 @@ def parse_args():
13
13
  parser.add_argument("--username", type=str, default=None, help="Username for the remote machine")
14
14
  parser.add_argument("--password", type=str, default=None, help="Password for the remote machine")
15
15
  parser.add_argument("--key_filename", type=str, default=None, help="Path to the key file for the remote machine")
16
+ parser.add_argument("--command", type=str, default=None, help="Command to run on the remote machine")
17
+ parser.add_argument(
18
+ "--host",
19
+ type=str,
20
+ choices=["add", "list", "remove", "delete"],
21
+ default=None,
22
+ help="Manage remote hosts (e.g. add, list, remove)",
23
+ )
16
24
  args = parser.parse_args()
17
25
 
18
- if not args.ip and (args.port is not None or args.username or args.password or args.key_filename):
26
+ if not args.ip and not args.host and (args.port is not None or args.username or args.password or args.key_filename):
19
27
  parser.error("--ip is required when specifying remote connection arguments")
20
28
 
21
29
  return args
@@ -50,42 +58,28 @@ def start_daemon() -> subprocess.Popen | None:
50
58
 
51
59
 
52
60
  def cli():
53
- """Main CLI entrypoint"""
61
+ """Main CLI entrypoint."""
54
62
  args = parse_args()
55
63
 
56
- if args.ip:
57
- from monitorify.remote.connector import RemoteConnector
58
-
59
- try:
60
- connector = RemoteConnector(
61
- host=args.ip,
62
- port=args.port or 22,
63
- username=args.username,
64
- password=args.password,
65
- key_filename=args.key_filename,
66
- )
67
- client = connector.connect()
68
- try:
69
- if not args.command and not connector.is_installed(client):
70
- print(f"Error: Monitorify is not installed on the remote host ({args.ip}).")
71
- response = input("Do you want to install it? (y/n): ").strip().lower()
72
- if response == "y":
73
- success = connector.install(client)
74
- if not success:
75
- print("Installation failed.")
76
- sys.exit(1)
77
- else:
78
- sys.exit(1)
79
-
80
- remote_cmd = args.command or "bash -l -c 'monitorify || python3 -m monitorify'"
81
- connector.run_interactive(client, command=remote_cmd)
82
- finally:
83
- client.close()
84
- except ConnectionError as e:
85
- print(f"Connection failed: {e}")
64
+ if args.host:
65
+ from monitorify.remote.addRemote import handle_host_action
66
+
67
+ handle_host_action(args.host, args)
68
+ return
69
+ elif args.ip:
70
+ from monitorify.remote.connector import connect_and_run
71
+
72
+ success = connect_and_run(
73
+ host=args.ip,
74
+ port=args.port or 22,
75
+ username=args.username,
76
+ password=args.password,
77
+ key_filename=args.key_filename,
78
+ command=args.command,
79
+ pause_on_exit=False,
80
+ )
81
+ if not success:
86
82
  sys.exit(1)
87
- except KeyboardInterrupt:
88
- pass
89
83
  return
90
84
 
91
85
  # Local mode
@@ -0,0 +1,4 @@
1
+ from monitorify.remote.connector import RemoteConnector
2
+ from monitorify.remote.addRemote import add_remote, list_hosts, remove_host, handle_host_action
3
+
4
+ __all__ = ["RemoteConnector", "add_remote", "list_hosts", "remove_host", "handle_host_action"]
@@ -0,0 +1,157 @@
1
+ import getpass
2
+ import os
3
+ from typing import Optional
4
+ from monitorify.storage.db import Database
5
+
6
+
7
+ def add_remote(
8
+ ip: Optional[str] = None,
9
+ port: Optional[int] = None,
10
+ username: Optional[str] = None,
11
+ password: Optional[str] = None,
12
+ key_filename: Optional[str] = None,
13
+ db: Optional[Database] = None,
14
+ ) -> int:
15
+ """Prompt for remote host details if not provided, and save to database."""
16
+ if not ip:
17
+ ip = input("Enter IP address: ").strip()
18
+ if not ip:
19
+ print("Error: IP address cannot be empty.")
20
+ return -1
21
+
22
+ if port is None:
23
+ port_input = input("Enter port (default 22): ").strip()
24
+ port = int(port_input) if port_input.isdigit() else 22
25
+ elif isinstance(port, str):
26
+ port = int(port) if port.isdigit() else 22
27
+
28
+ if not (1 <= port <= 65535):
29
+ port = 22
30
+
31
+ if username is None:
32
+ username_input = input("Enter username (default root): ").strip()
33
+ username = username_input if username_input else "root"
34
+ else:
35
+ username = username.strip() or "root"
36
+
37
+ if password is None and key_filename is None:
38
+ password_input = getpass.getpass("Enter password (leave blank to use SSH keys): ").strip()
39
+ if password_input:
40
+ password = password_input
41
+ key_filename = None
42
+ else:
43
+ password = None
44
+ key_input = input("Enter key filename: ").strip()
45
+ key_filename = key_input if key_input else None
46
+ else:
47
+ password = password if password else None
48
+ key_filename = key_filename if key_filename else None
49
+
50
+ if key_filename:
51
+ key_filename = os.path.expanduser(key_filename)
52
+
53
+ close_db = False
54
+ if db is None:
55
+ db = Database()
56
+ close_db = True
57
+
58
+ try:
59
+ host_id = db.save_host(
60
+ ip=ip,
61
+ port=port,
62
+ username=username,
63
+ password=password,
64
+ key_filename=key_filename,
65
+ )
66
+ if host_id != -1:
67
+ print(f"Host '{username}@{ip}:{port}' saved to database (ID: {host_id}).")
68
+ else:
69
+ print("Failed to save host to database.")
70
+ return host_id
71
+ finally:
72
+ if close_db:
73
+ db.disconnect()
74
+
75
+
76
+ def list_hosts(db: Optional[Database] = None) -> list[dict]:
77
+ """List all saved remote hosts."""
78
+ close_db = False
79
+ if db is None:
80
+ db = Database()
81
+ close_db = True
82
+
83
+ try:
84
+ hosts = db.get_hosts()
85
+ if not hosts:
86
+ print("No saved hosts found.")
87
+ return []
88
+
89
+ print(f"{'ID':<5} {'Host':<35} {'Auth':<20}")
90
+ print("-" * 65)
91
+ for h in hosts:
92
+ host_str = f"{h['username']}@{h['ip']}:{h['port']}"
93
+ auth_str = f"Key: {h['key_filename']}" if h['key_filename'] else ("Password: ***" if h['password'] else "None")
94
+ print(f"{h['id']:<5} {host_str:<35} {auth_str:<20}")
95
+ return hosts
96
+ finally:
97
+ if close_db:
98
+ db.disconnect()
99
+
100
+
101
+ def remove_host(host_id: Optional[int] = None, db: Optional[Database] = None) -> bool:
102
+ """Remove a saved remote host by ID."""
103
+ close_db = False
104
+ if db is None:
105
+ db = Database()
106
+ close_db = True
107
+
108
+ try:
109
+ if host_id is None:
110
+ hosts = db.get_hosts()
111
+ if not hosts:
112
+ print("No saved hosts to remove.")
113
+ return False
114
+ list_hosts(db=db)
115
+ id_input = input("Enter host ID to remove: ").strip()
116
+ if not id_input.isdigit():
117
+ print("Invalid host ID.")
118
+ return False
119
+ host_id = int(id_input)
120
+
121
+ if db.delete_host(host_id):
122
+ print(f"Host ID {host_id} successfully deleted.")
123
+ return True
124
+ else:
125
+ print(f"Failed to delete host ID {host_id}.")
126
+ return False
127
+ finally:
128
+ if close_db:
129
+ db.disconnect()
130
+
131
+
132
+ def handle_host_action(action: str, args=None) -> None:
133
+ """Handle --host subcommands."""
134
+ action = (action or "").lower()
135
+ if action == "add":
136
+ ip = getattr(args, "ip", None)
137
+ port = getattr(args, "port", None)
138
+ username = getattr(args, "username", None)
139
+ password = getattr(args, "password", None)
140
+ key_filename = getattr(args, "key_filename", None)
141
+ add_remote(
142
+ ip=ip,
143
+ port=port,
144
+ username=username,
145
+ password=password,
146
+ key_filename=key_filename,
147
+ )
148
+ elif action == "list":
149
+ list_hosts()
150
+ elif action in ("remove", "delete"):
151
+ remove_host()
152
+ else:
153
+ print(f"Unknown host action: '{action}'. Supported actions: add, list, remove")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ add_remote()
@@ -1,5 +1,7 @@
1
+ import os
1
2
  import paramiko
2
3
 
4
+
3
5
  class RemoteConnector:
4
6
  def __init__(
5
7
  self,
@@ -8,18 +10,27 @@ class RemoteConnector:
8
10
  username: str | None = None,
9
11
  password: str | None = None,
10
12
  key_filename: str | None = None,
13
+ timeout: float = 10.0,
11
14
  ):
12
15
  self.host = host
13
16
  self.port = port
14
17
  self.username = username
15
18
  self.password = password
16
- self.key_filename = key_filename
19
+ self.key_filename = os.path.expanduser(key_filename) if key_filename else None
20
+ self.timeout = timeout
17
21
 
18
- def connect(self):
22
+ def connect(self) -> paramiko.SSHClient:
19
23
  client = paramiko.SSHClient()
20
24
  client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
21
- try:
22
- client.connect(hostname=self.host, port=self.port, username=self.username, password=self.password, key_filename=self.key_filename)
25
+ try:
26
+ client.connect(
27
+ hostname=self.host,
28
+ port=self.port,
29
+ username=self.username,
30
+ password=self.password,
31
+ key_filename=self.key_filename,
32
+ timeout=self.timeout,
33
+ )
23
34
  except paramiko.AuthenticationException as e:
24
35
  raise ConnectionError(f"Authentication failed: {e}")
25
36
  except paramiko.SSHException as e:
@@ -32,7 +43,7 @@ class RemoteConnector:
32
43
  """Check if monitorify is installed on the remote host."""
33
44
  try:
34
45
  _, stdout, _ = client.exec_command(
35
- "bash -l -c 'command -v monitorify >/dev/null 2>&1 || python3 -c \"import monitorify\" >/dev/null 2>&1'"
46
+ "bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; command -v monitorify >/dev/null 2>&1 || [ -x \"$HOME/.local/bin/monitorify\" ] || python3 -c \"import monitorify\" >/dev/null 2>&1'"
36
47
  )
37
48
  return stdout.channel.recv_exit_status() == 0
38
49
  except Exception:
@@ -51,7 +62,6 @@ class RemoteConnector:
51
62
 
52
63
  def run_interactive(self, client: paramiko.SSHClient, command: str = "monitorify") -> None:
53
64
  """Run a command interactively on the remote host with a PTY."""
54
- import os
55
65
  import select
56
66
  import shutil
57
67
  import signal
@@ -81,8 +91,9 @@ class RemoteConnector:
81
91
  except Exception:
82
92
  pass
83
93
 
94
+ old_winch_handler = None
84
95
  try:
85
- signal.signal(signal.SIGWINCH, handle_resize)
96
+ old_winch_handler = signal.signal(signal.SIGWINCH, handle_resize)
86
97
  except Exception:
87
98
  pass
88
99
 
@@ -110,6 +121,69 @@ class RemoteConnector:
110
121
  except Exception:
111
122
  break
112
123
  finally:
124
+ if old_winch_handler is not None:
125
+ try:
126
+ signal.signal(signal.SIGWINCH, old_winch_handler)
127
+ except Exception:
128
+ pass
113
129
  if is_atty and old_tty is not None:
114
130
  termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
115
- channel.close()
131
+ channel.close()
132
+
133
+
134
+ def connect_and_run(
135
+ host: str,
136
+ port: int = 22,
137
+ username: str | None = None,
138
+ password: str | None = None,
139
+ key_filename: str | None = None,
140
+ command: str | None = None,
141
+ pause_on_exit: bool = False,
142
+ timeout: float = 10.0,
143
+ ) -> bool:
144
+ """Connects to a remote host and runs the interactive session."""
145
+ connector = RemoteConnector(
146
+ host=host,
147
+ port=port or 22,
148
+ username=username,
149
+ password=password,
150
+ key_filename=key_filename,
151
+ timeout=timeout,
152
+ )
153
+ try:
154
+ print(f"\nConnecting to {username or 'root'}@{host}:{port or 22}...")
155
+ client = connector.connect()
156
+ try:
157
+ if not command and not connector.is_installed(client):
158
+ print(f"Error: Monitorify is not installed on the remote host ({host}).")
159
+ response = input("Do you want to install it? (y/n): ").strip().lower()
160
+ if response == "y":
161
+ success = connector.install(client)
162
+ if not success:
163
+ print("Installation failed.")
164
+ if pause_on_exit:
165
+ input("Press Enter to return...")
166
+ return False
167
+ else:
168
+ if pause_on_exit:
169
+ input("Press Enter to return...")
170
+ return False
171
+
172
+ remote_cmd = command or "bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; monitorify || python3 -m monitorify'"
173
+ connector.run_interactive(client, command=remote_cmd)
174
+ return True
175
+ finally:
176
+ client.close()
177
+ except ConnectionError as e:
178
+ print(f"Connection failed: {e}")
179
+ if pause_on_exit:
180
+ input("Press Enter to return...")
181
+ return False
182
+ except KeyboardInterrupt:
183
+ print("\nSession cancelled.")
184
+ return False
185
+ except Exception as e:
186
+ print(f"Unexpected error: {e}")
187
+ if pause_on_exit:
188
+ input("Press Enter to return...")
189
+ return False
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ REPO_URL="https://github.com/leonst036/Monitorify.git"
6
+ export PATH="$HOME/.local/bin:$PATH"
7
+
8
+ install_via_pipx() {
9
+ if command -v pipx >/dev/null 2>&1; then
10
+ echo "[1/3] Attempting installation via pipx..."
11
+ pipx ensurepath >/dev/null 2>&1 || true
12
+ if pipx install --force "git+${REPO_URL}"; then
13
+ echo "Successfully installed via pipx"
14
+ return 0
15
+ fi
16
+ fi
17
+ return 1
18
+ }
19
+
20
+ install_via_pip() {
21
+ echo "[2/3] Attempting installation via pip..."
22
+ if command -v pip >/dev/null 2>&1; then
23
+ if pip install --upgrade "git+${REPO_URL}" || pip install --break-system-packages --upgrade "git+${REPO_URL}"; then
24
+ echo "Successfully installed via pip"
25
+ return 0
26
+ fi
27
+ fi
28
+ if python3 -m pip --version >/dev/null 2>&1; then
29
+ if python3 -m pip install --upgrade "git+${REPO_URL}" || python3 -m pip install --break-system-packages --upgrade "git+${REPO_URL}"; then
30
+ echo "Successfully installed via python3 -m pip"
31
+ return 0
32
+ fi
33
+ fi
34
+ return 1
35
+ }
36
+
37
+ install_deps() {
38
+ echo "[3/3] pipx and pip not found. Attempting to install package manager dependencies..."
39
+ SUDO_CMD=""
40
+ if [ "$(id -u)" -ne 0 ]; then
41
+ if command -v sudo >/dev/null 2>&1; then
42
+ SUDO_CMD="sudo"
43
+ fi
44
+ fi
45
+
46
+ if command -v apt-get >/dev/null 2>&1; then
47
+ $SUDO_CMD apt-get update && $SUDO_CMD apt-get install -y pipx git || $SUDO_CMD apt-get install -y python3-pip python3-venv git
48
+ elif command -v dnf >/dev/null 2>&1; then
49
+ $SUDO_CMD dnf install -y pipx git || $SUDO_CMD dnf install -y python3-pip git
50
+ elif command -v pacman >/dev/null 2>&1; then
51
+ $SUDO_CMD pacman -Sy --noconfirm python-pipx git || $SUDO_CMD pacman -Sy --noconfirm python-pip git
52
+ elif command -v zypper >/dev/null 2>&1; then
53
+ $SUDO_CMD zypper install -y python3-pipx git || $SUDO_CMD zypper install -y python3-pip git
54
+ elif command -v apk >/dev/null 2>&1; then
55
+ $SUDO_CMD apk add pipx git || $SUDO_CMD apk add py3-pip git
56
+ else
57
+ echo "No supported package manager found."
58
+ return 1
59
+ fi
60
+ }
61
+
62
+ if install_via_pipx; then
63
+ exit 0
64
+ fi
65
+
66
+ if install_via_pip; then
67
+ exit 0
68
+ fi
69
+
70
+ if install_deps; then
71
+ export PATH="$HOME/.local/bin:$PATH"
72
+ if install_via_pipx; then
73
+ exit 0
74
+ fi
75
+ if install_via_pip; then
76
+ exit 0
77
+ fi
78
+ fi
79
+
80
+ echo "All installation methods failed."
81
+ exit 1