machineconfig 7.79__py3-none-any.whl → 7.83__py3-none-any.whl

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.

Potentially problematic release.


This version of machineconfig might be problematic. Click here for more details.

Files changed (52) hide show
  1. machineconfig/jobs/installer/custom/yazi.py +120 -0
  2. machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
  3. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
  4. machineconfig/jobs/installer/custom_dev/sysabc.py +0 -5
  5. machineconfig/jobs/installer/installer_data.json +162 -94
  6. machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
  7. machineconfig/profile/create_helper.py +0 -12
  8. machineconfig/profile/mapper.toml +2 -2
  9. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +1 -0
  10. machineconfig/scripts/python/croshell.py +4 -4
  11. machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
  12. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  13. machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
  14. machineconfig/scripts/python/helpers_devops/cli_nw.py +15 -16
  15. machineconfig/scripts/python/helpers_devops/cli_self.py +4 -4
  16. machineconfig/scripts/python/helpers_devops/cli_terminal.py +2 -5
  17. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
  18. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
  19. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
  20. machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
  21. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  22. machineconfig/scripts/python/helpers_utils/path.py +7 -4
  23. machineconfig/scripts/python/msearch.py +37 -7
  24. machineconfig/scripts/python/utils.py +3 -3
  25. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  26. machineconfig/settings/yazi/init.lua +4 -0
  27. machineconfig/settings/yazi/keymap_linux.toml +11 -4
  28. machineconfig/settings/yazi/theme.toml +4 -0
  29. machineconfig/settings/yazi/yazi_linux.toml +84 -0
  30. machineconfig/settings/yazi/yazi_windows.toml +58 -0
  31. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  32. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  33. machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
  34. machineconfig/utils/installer_utils/install_from_url.py +122 -102
  35. machineconfig/utils/installer_utils/installer_class.py +15 -72
  36. machineconfig/utils/installer_utils/installer_cli.py +29 -44
  37. machineconfig/utils/installer_utils/installer_helper.py +100 -0
  38. machineconfig/utils/installer_utils/installer_runner.py +5 -8
  39. machineconfig/utils/ssh_utils/abc.py +2 -2
  40. machineconfig/utils/ssh_utils/wsl.py +44 -2
  41. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
  42. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/RECORD +45 -47
  43. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
  44. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
  45. machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
  46. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
  47. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
  48. machineconfig/settings/yazi/yazi.toml +0 -17
  49. machineconfig/setup_linux/others/cli_installation.sh +0 -137
  50. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
  51. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +0 -0
  52. {machineconfig-7.79.dist-info → machineconfig-7.83.dist-info}/top_level.txt +0 -0
@@ -6,12 +6,40 @@ Extracts GitHub repository URLs and fetches latest release data with rate limiti
6
6
 
7
7
  import json
8
8
  import time
9
- import subprocess
9
+ import requests
10
10
  from pathlib import Path
11
- from typing import Any, Dict, Optional, Set
11
+ from typing import Any, Dict, Optional, Set, TypedDict
12
12
  from urllib.parse import urlparse
13
13
 
14
14
 
15
+ class AssetInfo(TypedDict):
16
+ """Type definition for GitHub release asset information."""
17
+ name: str
18
+ size: int
19
+ download_count: int
20
+ content_type: str
21
+ created_at: str
22
+ updated_at: str
23
+ browser_download_url: str
24
+
25
+
26
+ class ReleaseInfo(TypedDict):
27
+ """Type definition for GitHub release information."""
28
+ tag_name: str
29
+ name: str
30
+ published_at: str
31
+ assets: list[AssetInfo]
32
+ assets_count: int
33
+
34
+
35
+ class OutputData(TypedDict):
36
+ """Type definition for the output JSON data structure."""
37
+ generated_at: str
38
+ total_repositories: int
39
+ successful_fetches: int
40
+ releases: Dict[str, Optional[ReleaseInfo]]
41
+
42
+
15
43
  def is_github_repo(url: str) -> bool:
16
44
  """Check if URL is a GitHub repository URL."""
17
45
  try:
@@ -19,87 +47,99 @@ def is_github_repo(url: str) -> bool:
19
47
  return parsed.netloc == "github.com" and len(parsed.path.split("/")) >= 3
20
48
  except Exception:
21
49
  return False
22
-
23
-
24
50
  def extract_github_repos_from_json(json_file_path: Path) -> Set[str]:
25
51
  """Extract GitHub repository URLs from installer JSON file."""
26
52
  github_repos: Set[str] = set()
27
-
28
53
  try:
29
54
  with open(json_file_path, 'r', encoding='utf-8') as file:
30
55
  data = json.load(file)
31
-
32
56
  for installer in data.get("installers", []):
33
57
  repo_url = installer.get("repoURL", "")
34
58
  if is_github_repo(repo_url):
35
59
  github_repos.add(repo_url)
36
-
37
60
  except (json.JSONDecodeError, FileNotFoundError) as e:
38
61
  print(f"Error reading {json_file_path}: {e}")
39
-
40
62
  return github_repos
41
-
42
-
43
- def get_repo_name_from_url(repo_url: str) -> str:
44
- """Extract owner/repo from GitHub URL."""
63
+ def get_repo_name_from_url(repo_url: str) -> Optional[tuple[str, str]]:
64
+ """Extract owner/repo from GitHub URL as a tuple (username, repo_name)."""
45
65
  try:
46
66
  parsed = urlparse(repo_url)
47
67
  path_parts = parsed.path.strip("/").split("/")
48
- return f"{path_parts[0]}/{path_parts[1]}"
68
+ return (path_parts[0], path_parts[1])
49
69
  except (IndexError, AttributeError):
50
- return ""
70
+ return None
51
71
 
52
72
 
53
- def fetch_github_release_data(repo_name: str) -> Optional[Dict[str, Any]]:
54
- """Fetch latest release data from GitHub API using curl."""
73
+ def fetch_github_release_data(
74
+ username: str,
75
+ repo_name: str,
76
+ version: Optional[str] = None,
77
+ ) -> Optional[Dict[str, Any]]:
78
+ """Fetch GitHub release data for the latest or a specific tag."""
79
+
55
80
  try:
56
- cmd = [
57
- "curl", "-s",
58
- f"https://api.github.com/repos/{repo_name}/releases/latest"
59
- ]
60
-
61
- result = subprocess.run(
62
- cmd,
63
- capture_output=True,
64
- text=True,
65
- timeout=30
66
- )
67
-
68
- if result.returncode != 0:
69
- print(f"❌ Failed to fetch data for {repo_name}: {result.stderr}")
81
+ requested_version = (version or "").strip()
82
+ if requested_version and requested_version.lower() != "latest":
83
+ url = f"https://api.github.com/repos/{username}/{repo_name}/releases/tags/{requested_version}"
84
+ else:
85
+ url = f"https://api.github.com/repos/{username}/{repo_name}/releases/latest"
86
+
87
+ response = requests.get(url, timeout=30)
88
+ if response.status_code != 200:
89
+ print(f"❌ Failed to fetch data for {username}/{repo_name}: HTTP {response.status_code}")
70
90
  return None
71
-
72
- response_data = json.loads(result.stdout)
73
-
74
- # Check if API returned an error
75
- if "message" in response_data:
76
- if "API rate limit exceeded" in response_data.get("message", ""):
77
- print(f"🚫 Rate limit exceeded for {repo_name}")
91
+
92
+ response_data = response.json()
93
+ message = response_data.get("message")
94
+ if isinstance(message, str):
95
+ if "API rate limit exceeded" in message:
96
+ print(f"🚫 Rate limit exceeded for {username}/{repo_name}")
78
97
  return None
79
- elif "Not Found" in response_data.get("message", ""):
80
- print(f"🔍 No releases found for {repo_name}")
98
+ if "Not Found" in message:
99
+ print(f"🔍 No releases found for {username}/{repo_name}")
81
100
  return None
82
-
101
+
83
102
  return response_data
84
-
85
- except (subprocess.TimeoutExpired, json.JSONDecodeError, subprocess.SubprocessError) as e:
86
- print(f"❌ Error fetching {repo_name}: {e}")
103
+
104
+ except (requests.RequestException, requests.Timeout, json.JSONDecodeError) as error:
105
+ print(f"❌ Error fetching {username}/{repo_name}: {error}")
87
106
  return None
88
107
 
89
108
 
90
- def extract_release_info(release_data: Dict[str, Any]) -> Dict[str, Any]:
109
+ def get_release_info(
110
+ username: str,
111
+ repo_name: str,
112
+ version: Optional[str] = None,
113
+ ) -> Optional[ReleaseInfo]:
114
+ """Return sanitized release information for the requested repository."""
115
+ release_data = fetch_github_release_data(username, repo_name, version)
116
+ if not release_data:
117
+ return None
118
+ return extract_release_info(release_data)
119
+
120
+
121
+ def extract_release_info(release_data: Dict[str, Any]) -> Optional[ReleaseInfo]:
91
122
  """Extract relevant information from GitHub release data."""
92
123
  if not release_data:
93
- return {}
94
-
95
- asset_names = [asset["name"] for asset in release_data.get("assets", [])]
96
-
124
+ return None
125
+ assets: list[AssetInfo] = []
126
+ for asset in release_data.get("assets", []):
127
+ asset_info: AssetInfo = {
128
+ "name": asset.get("name", ""),
129
+ "size": asset.get("size", 0),
130
+ "download_count": asset.get("download_count", 0),
131
+ "content_type": asset.get("content_type", ""),
132
+ "created_at": asset.get("created_at", ""),
133
+ "updated_at": asset.get("updated_at", ""),
134
+ "browser_download_url": asset.get("browser_download_url", "")
135
+ }
136
+ assets.append(asset_info)
97
137
  return {
98
138
  "tag_name": release_data.get("tag_name", ""),
99
139
  "name": release_data.get("name", ""),
100
140
  "published_at": release_data.get("published_at", ""),
101
- "assets": asset_names,
102
- "assets_count": len(asset_names)
141
+ "assets": assets,
142
+ "assets_count": len(assets)
103
143
  }
104
144
 
105
145
 
@@ -132,7 +172,7 @@ def main() -> None:
132
172
  return
133
173
 
134
174
  # Fetch release data with rate limiting
135
- release_mapping: Dict[str, Any] = {}
175
+ release_mapping: Dict[str, Optional[ReleaseInfo]] = {}
136
176
  total_repos = len(all_github_repos)
137
177
 
138
178
  print(f"\n🚀 Fetching release data for {total_repos} repositories...")
@@ -140,24 +180,26 @@ def main() -> None:
140
180
  print("-" * 60)
141
181
 
142
182
  for i, repo_url in enumerate(sorted(all_github_repos), 1):
143
- repo_name = get_repo_name_from_url(repo_url)
183
+ repo_info = get_repo_name_from_url(repo_url)
144
184
 
145
- if not repo_name:
185
+ if not repo_info:
146
186
  print(f"⚠️ [{i:3d}/{total_repos}] Invalid repo URL: {repo_url}")
147
187
  continue
148
-
149
- print(f"📡 [{i:3d}/{total_repos}] Fetching: {repo_name}", end=" ... ")
150
188
 
151
- release_data = fetch_github_release_data(repo_name)
189
+ username, repo_name = repo_info
190
+ repo_full_name = f"{username}/{repo_name}"
191
+
192
+ print(f"📡 [{i:3d}/{total_repos}] Fetching: {repo_full_name}", end=" ... ")
152
193
 
153
- if release_data:
154
- release_info = extract_release_info(release_data)
194
+ release_info = get_release_info(username, repo_name)
195
+
196
+ if release_info:
155
197
  release_mapping[repo_url] = release_info
156
- assets_count = release_info.get("assets_count", 0)
157
- tag = release_info.get("tag_name", "unknown")
198
+ assets_count = release_info["assets_count"]
199
+ tag = release_info["tag_name"]
158
200
  print(f"✅ {tag} ({assets_count} assets)")
159
201
  else:
160
- release_mapping[repo_url] = {}
202
+ release_mapping[repo_url] = None
161
203
  print("❌ No data")
162
204
 
163
205
  # Rate limiting - wait 5 seconds between requests (except for the last one)
@@ -165,7 +207,7 @@ def main() -> None:
165
207
  time.sleep(5)
166
208
 
167
209
  # Save results
168
- output_data = {
210
+ output_data: OutputData = {
169
211
  "generated_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
170
212
  "total_repositories": len(all_github_repos),
171
213
  "successful_fetches": len([v for v in release_mapping.values() if v]),
@@ -4,46 +4,23 @@ import platform
4
4
  from urllib.parse import urlparse
5
5
 
6
6
  import typer
7
- from typing import Optional, TypeAlias, cast
7
+ from typing import TYPE_CHECKING
8
8
 
9
- from machineconfig.utils.installer_utils.installer_class import install_deb_package
9
+ from machineconfig.utils.installer_utils.installer_helper import install_deb_package
10
10
  from machineconfig.utils.installer_utils.installer_locator_utils import find_move_delete_linux, find_move_delete_windows
11
+ from machineconfig.utils.installer_utils.github_release_bulk import (
12
+ get_repo_name_from_url,
13
+ fetch_github_release_data,
14
+ extract_release_info,
15
+ AssetInfo,
16
+ )
11
17
  from machineconfig.utils.path_extended import DECOMPRESS_SUPPORTED_FORMATS, PathExtended
12
18
  from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
13
19
 
14
- SUPPORTED_GITHUB_HOSTS = {"github.com", "www.github.com"}
15
-
16
- GitHubAsset: TypeAlias = dict[str, object]
17
- GitHubRelease: TypeAlias = dict[str, object]
18
-
19
-
20
- def _extract_repo_name(github_url: str) -> str:
21
- parsed = urlparse(github_url)
22
- parts = [part for part in parsed.path.strip("/").split("/") if part]
23
- if len(parts) < 2:
24
- return ""
25
- owner, repo = parts[0], parts[1]
26
- if repo == "":
27
- return ""
28
- return f"{owner}/{repo}"
29
-
20
+ if TYPE_CHECKING:
21
+ from rich.console import Console
30
22
 
31
- def _fetch_latest_release(repo_name: str) -> Optional[GitHubRelease]:
32
- import json
33
- import requests
34
- try:
35
- response = requests.get(f"https://api.github.com/repos/{repo_name}/releases/latest", timeout=30)
36
- except requests.RequestException:
37
- return None
38
- if response.status_code != 200:
39
- return None
40
- try:
41
- data = response.json()
42
- except json.JSONDecodeError:
43
- return None
44
- if not isinstance(data, dict):
45
- return None
46
- return cast(GitHubRelease, data)
23
+ SUPPORTED_GITHUB_HOSTS = {"github.com", "www.github.com"}
47
24
 
48
25
 
49
26
  def _format_size(size_bytes: int) -> str:
@@ -71,74 +48,8 @@ def _derive_tool_name(repo_name: str, asset_name: str) -> str:
71
48
  return "githubapp"
72
49
 
73
50
 
74
- def install_from_github_url(github_url: str) -> None:
75
- from machineconfig.utils.options import choose_from_options
76
- from rich.console import Console
77
- from rich.panel import Panel
78
-
79
- console = Console()
80
- repo_name = _extract_repo_name(github_url)
81
- if repo_name == "":
82
- console.print(Panel(f"Invalid GitHub URL: {github_url}", title="❌ Error", border_style="red"))
83
- raise typer.Exit(1)
84
- console.print(Panel(f"Fetching latest release for [green]{repo_name}[/green]", title="🌐 GitHub", border_style="blue"))
85
- release_raw = _fetch_latest_release(repo_name)
86
- if not release_raw:
87
- console.print(Panel("No releases available for this repository.", title="❌ Error", border_style="red"))
88
- raise typer.Exit(1)
89
- release = release_raw
90
- assets_value = release.get("assets", [])
91
- assets: list[GitHubAsset] = []
92
- if isinstance(assets_value, list):
93
- for asset in assets_value:
94
- if isinstance(asset, dict):
95
- typed_asset: GitHubAsset = {}
96
- name_value = asset.get("name")
97
- url_value = asset.get("browser_download_url")
98
- size_value = asset.get("size")
99
- content_value = asset.get("content_type")
100
- if isinstance(name_value, str):
101
- typed_asset["name"] = name_value
102
- if isinstance(url_value, str):
103
- typed_asset["browser_download_url"] = url_value
104
- if isinstance(size_value, int):
105
- typed_asset["size"] = size_value
106
- if isinstance(content_value, str):
107
- typed_asset["content_type"] = content_value
108
- assets.append(typed_asset)
109
- if not assets:
110
- console.print(Panel("No downloadable assets found in the latest release.", title="❌ Error", border_style="red"))
111
- raise typer.Exit(1)
112
- binary_assets = assets
113
- selection_pool = binary_assets if binary_assets else assets
114
- if not selection_pool:
115
- console.print(Panel("No assets available for installation.", title="❌ Error", border_style="red"))
116
- raise typer.Exit(1)
117
- options_map: dict[str, GitHubAsset] = {}
118
- for asset in selection_pool:
119
- name = asset.get("name")
120
- download_url = asset.get("browser_download_url")
121
- if not isinstance(name, str) or not isinstance(download_url, str) or name == "" or download_url == "":
122
- continue
123
- size_value = asset.get("size")
124
- size = size_value if isinstance(size_value, int) else 0
125
- label = f"{name} [{_format_size(size)}]"
126
- options_map[label] = asset
127
- if not options_map:
128
- console.print(Panel("Release assets lack download URLs.", title="❌ Error", border_style="red"))
129
- raise typer.Exit(1)
130
- selection_label = choose_from_options(options=list(options_map.keys()), msg="Select a release asset", multi=False, header="📦 GitHub Release Assets", fzf=True)
131
- selected_asset = options_map[selection_label]
132
- download_url_value = selected_asset.get("browser_download_url")
133
- asset_name_value = selected_asset.get("name")
134
- if not isinstance(download_url_value, str) or download_url_value == "":
135
- console.print(Panel("Selected asset lacks a download URL.", title="❌ Error", border_style="red"))
136
- raise typer.Exit(1)
137
- asset_name = asset_name_value if isinstance(asset_name_value, str) else "github_binary"
138
- version_value = release.get("tag_name")
139
- version = version_value if isinstance(version_value, str) and version_value != "" else "latest"
140
- console.print(Panel(f"Downloading [cyan]{asset_name}[/cyan]", title="⬇️ Download", border_style="magenta"))
141
- archive_path = PathExtended(download_url_value).download(folder=INSTALL_TMP_DIR)
51
+ def _download_and_prepare(download_url: str) -> PathExtended:
52
+ archive_path = PathExtended(download_url).download(folder=INSTALL_TMP_DIR)
142
53
  extracted_path = archive_path
143
54
  if extracted_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
144
55
  extracted_path = archive_path.decompress()
@@ -150,6 +61,12 @@ def install_from_github_url(github_url: str) -> None:
150
61
  if nested_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
151
62
  extracted_path = nested_path.decompress()
152
63
  nested_path.delete(sure=True)
64
+ return extracted_path
65
+
66
+
67
+ def _finalize_install(repo_name: str, asset_name: str, version: str, extracted_path: PathExtended, console: "Console") -> None:
68
+ from rich.panel import Panel
69
+
153
70
  if extracted_path.suffix == ".deb":
154
71
  install_deb_package(extracted_path)
155
72
  tool_name_deb = _derive_tool_name(repo_name, asset_name)
@@ -178,3 +95,106 @@ def install_from_github_url(github_url: str) -> None:
178
95
  INSTALL_VERSION_ROOT.joinpath(tool_name).parent.mkdir(parents=True, exist_ok=True)
179
96
  INSTALL_VERSION_ROOT.joinpath(tool_name).write_text(version, encoding="utf-8")
180
97
  console.print(Panel(f"Installed [green]{tool_name}[/green] to {installed_path}\nVersion: {version}", title="✅ Complete", border_style="green"))
98
+
99
+
100
+ def install_from_github_url(github_url: str) -> None:
101
+ from machineconfig.utils.options import choose_from_options
102
+ from rich.console import Console
103
+ from rich.panel import Panel
104
+
105
+ console = Console()
106
+ repo_info = get_repo_name_from_url(github_url)
107
+ if repo_info is None:
108
+ console.print(Panel(f"Invalid GitHub URL: {github_url}", title="❌ Error", border_style="red"))
109
+ raise typer.Exit(1)
110
+ owner, repo = repo_info
111
+ repo_name = f"{owner}/{repo}"
112
+ console.print(Panel(f"Fetching latest release for [green]{repo_name}[/green]", title="🌐 GitHub", border_style="blue"))
113
+ release_raw = fetch_github_release_data(owner, repo)
114
+ if not release_raw:
115
+ console.print(Panel("No releases available for this repository.", title="❌ Error", border_style="red"))
116
+ raise typer.Exit(1)
117
+
118
+ release_info = extract_release_info(release_raw)
119
+ if not release_info:
120
+ console.print(Panel("Failed to parse release information.", title="❌ Error", border_style="red"))
121
+ raise typer.Exit(1)
122
+
123
+ assets = release_info["assets"]
124
+ if not assets:
125
+ console.print(Panel("No downloadable assets found in the latest release.", title="❌ Error", border_style="red"))
126
+ raise typer.Exit(1)
127
+ binary_assets = assets
128
+ selection_pool = binary_assets if binary_assets else assets
129
+ if not selection_pool:
130
+ console.print(Panel("No assets available for installation.", title="❌ Error", border_style="red"))
131
+ raise typer.Exit(1)
132
+
133
+ # First pass: collect all formatted data and calculate column widths
134
+ asset_data = []
135
+ for asset in selection_pool:
136
+ name = asset["name"]
137
+ download_url = asset["browser_download_url"]
138
+ if name == "" or download_url == "":
139
+ continue
140
+ size = asset["size"]
141
+ download_count = asset.get("download_count", 0)
142
+ created_at = asset.get("created_at", "")
143
+
144
+ # Format each field
145
+ size_str = f"[{_format_size(size)}]"
146
+ downloads_str = f"{download_count:,}"
147
+ date_str = created_at.split("T")[0] if created_at else "N/A"
148
+
149
+ asset_data.append({
150
+ "name": name,
151
+ "size_str": size_str,
152
+ "downloads_str": downloads_str,
153
+ "date_str": date_str,
154
+ "asset": asset
155
+ })
156
+
157
+ # Calculate maximum widths for alignment
158
+ max_name_len = max(len(item["name"]) for item in asset_data) if asset_data else 0
159
+ max_size_len = max(len(item["size_str"]) for item in asset_data) if asset_data else 0
160
+ max_downloads_len = max(len(item["downloads_str"]) for item in asset_data) if asset_data else 0
161
+
162
+ # Second pass: build aligned labels
163
+ options_map: dict[str, AssetInfo] = {}
164
+ for item in asset_data:
165
+ name_padded = item["name"].ljust(max_name_len)
166
+ size_padded = item["size_str"].ljust(max_size_len)
167
+ downloads_padded = item["downloads_str"].rjust(max_downloads_len)
168
+
169
+ label = f"{name_padded} {size_padded} | ⬇ {downloads_padded} | 📅 {item['date_str']}"
170
+ options_map[label] = item["asset"]
171
+
172
+ if not options_map:
173
+ console.print(Panel("Release assets lack download URLs.", title="❌ Error", border_style="red"))
174
+ raise typer.Exit(1)
175
+ selection_label = choose_from_options(options=list(options_map.keys()), msg="Select a release asset", multi=False, header="📦 GitHub Release Assets", fzf=True)
176
+ selected_asset = options_map[selection_label]
177
+ download_url_value = selected_asset["browser_download_url"]
178
+ asset_name_value = selected_asset["name"]
179
+ if download_url_value == "":
180
+ console.print(Panel("Selected asset lacks a download URL.", title="❌ Error", border_style="red"))
181
+ raise typer.Exit(1)
182
+ asset_name = asset_name_value if asset_name_value != "" else "github_binary"
183
+ version = release_info["tag_name"] if release_info["tag_name"] != "" else "latest"
184
+ console.print(Panel(f"Downloading [cyan]{asset_name}[/cyan]", title="⬇️ Download", border_style="magenta"))
185
+ extracted_path = _download_and_prepare(download_url_value)
186
+ _finalize_install(repo_name=repo_name, asset_name=asset_name, version=version, extracted_path=extracted_path, console=console)
187
+
188
+
189
+ def install_from_binary_url(binary_url: str) -> None:
190
+ from rich.console import Console
191
+ from rich.panel import Panel
192
+
193
+ console = Console()
194
+ parsed = urlparse(binary_url)
195
+ asset_candidate = parsed.path.split("/")[-1] if parsed.path else ""
196
+ asset_name = asset_candidate if asset_candidate != "" else "binary_asset"
197
+ host = parsed.netloc if parsed.netloc != "" else "remote host"
198
+ console.print(Panel(f"Downloading [cyan]{asset_name}[/cyan] from [green]{host}[/green]", title="⬇️ Download", border_style="magenta"))
199
+ extracted_path = _download_and_prepare(binary_url)
200
+ _finalize_install(repo_name="", asset_name=asset_name, version="latest", extracted_path=extracted_path, console=console)
@@ -1,35 +1,16 @@
1
+ from machineconfig.utils.installer_utils.installer_helper import install_deb_package
1
2
  from machineconfig.utils.path_extended import PathExtended, DECOMPRESS_SUPPORTED_FORMATS
2
3
  from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
3
4
  from machineconfig.utils.installer_utils.installer_locator_utils import find_move_delete_linux, find_move_delete_windows, check_tool_exists
4
5
  from machineconfig.utils.schemas.installer.installer_types import InstallerData, get_os_name, get_normalized_arch
6
+ from machineconfig.utils.installer_utils.github_release_bulk import (
7
+ get_repo_name_from_url,
8
+ get_release_info,
9
+ )
5
10
 
6
11
  import platform
7
12
  import subprocess
8
- import json
9
- from typing import Optional, Any
10
- from urllib.parse import urlparse
11
-
12
-
13
-
14
- def install_deb_package(downloaded: PathExtended) -> None:
15
- from rich import print as rprint
16
- from rich.panel import Panel
17
- print(f"📦 Installing .deb package: {downloaded}")
18
- assert platform.system() == "Linux"
19
- result = subprocess.run(f"sudo nala install -y {downloaded}", shell=True, capture_output=True, text=True)
20
- success = result.returncode == 0 and result.stderr == ""
21
- if not success:
22
- from rich.console import Group
23
- desc = "Installing .deb"
24
- sub_panels = []
25
- if result.stdout:
26
- sub_panels.append(Panel(result.stdout, title="STDOUT", style="blue"))
27
- if result.stderr:
28
- sub_panels.append(Panel(result.stderr, title="STDERR", style="red"))
29
- group_content = Group(f"❌ {desc} failed\nReturn code: {result.returncode}", *sub_panels)
30
- rprint(Panel(group_content, title=desc, style="red"))
31
- print("🗑️ Cleaning up .deb package...")
32
- downloaded.delete(sure=True)
13
+ from typing import Optional
33
14
 
34
15
 
35
16
  class Installer:
@@ -125,7 +106,7 @@ class Installer:
125
106
  import runpy
126
107
  runpy.run_path(str(installer_path), run_name=None)["main"](self.installer_data, version=version)
127
108
  version_to_be_installed = str(version)
128
- elif installer_arch_os.startswith("https://"): # its a url to be downloaded
109
+ elif installer_arch_os.startswith("https://") or installer_arch_os.startswith("http://"):
129
110
  # downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
130
111
  from machineconfig.scripts.python.helpers_utils.download import download
131
112
  downloaded_object = download(installer_arch_os, output_dir=str(INSTALL_TMP_DIR))
@@ -221,44 +202,6 @@ class Installer:
221
202
  if only_file_in.is_file() and only_file_in.suffix in DECOMPRESS_SUPPORTED_FORMATS: # further decompress
222
203
  downloaded = only_file_in.decompress()
223
204
  return downloaded, version_to_be_installed
224
- @staticmethod
225
- def _get_repo_name_from_url(repo_url: str) -> str:
226
- """Extract owner/repo from GitHub URL."""
227
- try:
228
- parsed = urlparse(repo_url)
229
- path_parts = parsed.path.strip("/").split("/")
230
- return f"{path_parts[0]}/{path_parts[1]}"
231
- except (IndexError, AttributeError):
232
- return ""
233
-
234
- @staticmethod
235
- def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
236
- """Fetch release data from GitHub API using requests."""
237
- import requests
238
- try:
239
- if version and version.lower() != "latest": # Fetch specific version
240
- url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
241
- else: # Fetch latest release
242
- url = f"https://api.github.com/repos/{repo_name}/releases/latest"
243
- response = requests.get(url, timeout=30)
244
- if response.status_code != 200:
245
- print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
246
- return None
247
- response_data = response.json()
248
- # Check if API returned an error
249
- if "message" in response_data:
250
- if "API rate limit exceeded" in response_data.get("message", ""):
251
- print(f"🚫 Rate limit exceeded for {repo_name}")
252
- return None
253
- elif "Not Found" in response_data.get("message", ""):
254
- print(f"🔍 No releases found for {repo_name}")
255
- return None
256
-
257
- return response_data
258
-
259
- except (requests.RequestException, requests.Timeout, json.JSONDecodeError) as e:
260
- print(f"❌ Error fetching {repo_name}: {e}")
261
- return None
262
205
 
263
206
  def get_github_release(self, repo_url: str, version: Optional[str]) -> tuple[Optional[str], Optional[str]]:
264
207
  """
@@ -270,20 +213,20 @@ class Installer:
270
213
  filename_pattern = self.installer_data["fileNamePattern"][arch][os_name]
271
214
  if filename_pattern is None:
272
215
  raise ValueError(f"No fileNamePattern for {self._get_exe_name()} on {os_name} {arch}")
273
- repo_name = self._get_repo_name_from_url(repo_url)
274
- if not repo_name:
216
+ repo_info = get_repo_name_from_url(repo_url)
217
+ if not repo_info:
275
218
  print(f"❌ Invalid repository URL: {repo_url}")
276
219
  return None, None
277
- release_data = self._fetch_github_release_data(repo_name, version)
278
- if not release_data:
220
+ username, repository = repo_info
221
+ release_info = get_release_info(username, repository, version)
222
+ if not release_info:
279
223
  return None, None
280
- # print(release_data)
281
- actual_version = release_data.get("tag_name", "unknown")
224
+ actual_version = release_info.get("tag_name", "unknown") or "unknown"
282
225
  filename = filename_pattern.format(version=actual_version)
283
226
 
284
227
  available_filenames: list[str] = []
285
- for asset in release_data.get("assets", []):
286
- an_dl = asset.get("browser_download_url", "NA")
228
+ for asset in release_info["assets"]:
229
+ an_dl = asset["browser_download_url"]
287
230
  available_filenames.append(an_dl.split("/")[-1])
288
231
  if filename not in available_filenames:
289
232
  candidates = [