machineconfig 7.64__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 (104) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +4 -2
  2. machineconfig/jobs/installer/custom/yazi.py +120 -0
  3. machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
  4. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
  5. machineconfig/jobs/installer/custom_dev/sysabc.py +26 -5
  6. machineconfig/jobs/installer/installer_data.json +232 -96
  7. machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
  8. machineconfig/profile/create_helper.py +0 -12
  9. machineconfig/profile/create_links_export.py +2 -2
  10. machineconfig/profile/mapper.toml +2 -2
  11. machineconfig/scripts/__init__.py +0 -4
  12. machineconfig/scripts/python/agents.py +22 -17
  13. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +4 -0
  14. machineconfig/scripts/python/croshell.py +22 -17
  15. machineconfig/scripts/python/devops.py +1 -1
  16. machineconfig/scripts/python/devops_navigator.py +0 -4
  17. machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
  18. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  19. machineconfig/scripts/python/fire_jobs.py +13 -13
  20. machineconfig/scripts/python/ftpx.py +36 -12
  21. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  22. machineconfig/scripts/python/helpers/qr_code.py +166 -0
  23. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  24. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  25. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  26. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  27. machineconfig/scripts/python/helpers_cloud/cloud_mount.py +19 -17
  28. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  29. machineconfig/scripts/python/helpers_croshell/start_slidev.py +6 -7
  30. machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
  31. machineconfig/scripts/python/helpers_devops/cli_nw.py +90 -10
  32. machineconfig/scripts/python/helpers_devops/cli_self.py +8 -7
  33. machineconfig/scripts/python/helpers_devops/cli_share_file.py +7 -7
  34. machineconfig/scripts/python/helpers_devops/cli_share_server.py +12 -11
  35. machineconfig/scripts/python/helpers_devops/cli_terminal.py +8 -10
  36. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -1
  37. machineconfig/scripts/python/helpers_devops/devops_status.py +7 -19
  38. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +20 -9
  39. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
  40. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
  41. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  42. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +5 -3
  43. machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
  44. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  45. machineconfig/scripts/python/helpers_utils/download.py +4 -3
  46. machineconfig/scripts/python/helpers_utils/path.py +87 -34
  47. machineconfig/scripts/python/interactive.py +1 -1
  48. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  49. machineconfig/scripts/python/msearch.py +55 -6
  50. machineconfig/scripts/python/nw/address.py +132 -0
  51. machineconfig/scripts/python/nw/devops_add_ssh_key.py +8 -5
  52. machineconfig/scripts/python/terminal.py +2 -2
  53. machineconfig/scripts/python/utils.py +12 -11
  54. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  55. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  56. machineconfig/settings/shells/nushell/config.nu +2 -2
  57. machineconfig/settings/shells/nushell/env.nu +45 -6
  58. machineconfig/settings/shells/nushell/init.nu +282 -95
  59. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  60. machineconfig/settings/yazi/init.lua +4 -0
  61. machineconfig/settings/yazi/keymap_linux.toml +11 -4
  62. machineconfig/settings/yazi/theme.toml +4 -0
  63. machineconfig/settings/yazi/yazi_linux.toml +84 -0
  64. machineconfig/settings/yazi/yazi_windows.toml +58 -0
  65. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  66. machineconfig/setup_windows/uv.ps1 +8 -1
  67. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  68. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +3 -2
  69. machineconfig/utils/accessories.py +7 -4
  70. machineconfig/utils/code.py +4 -2
  71. machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
  72. machineconfig/utils/installer_utils/install_from_url.py +200 -0
  73. machineconfig/utils/installer_utils/installer_class.py +25 -74
  74. machineconfig/utils/installer_utils/installer_cli.py +40 -50
  75. machineconfig/utils/installer_utils/installer_helper.py +100 -0
  76. machineconfig/utils/installer_utils/installer_runner.py +5 -8
  77. machineconfig/utils/links.py +2 -2
  78. machineconfig/utils/meta.py +2 -2
  79. machineconfig/utils/options.py +3 -3
  80. machineconfig/utils/path_extended.py +1 -1
  81. machineconfig/utils/path_helper.py +0 -1
  82. machineconfig/utils/ssh.py +143 -409
  83. machineconfig/utils/ssh_utils/abc.py +8 -0
  84. machineconfig/utils/ssh_utils/copy_from_here.py +110 -0
  85. machineconfig/utils/ssh_utils/copy_to_here.py +302 -0
  86. machineconfig/utils/ssh_utils/utils.py +141 -0
  87. machineconfig/utils/ssh_utils/wsl.py +210 -0
  88. machineconfig/utils/upgrade_packages.py +2 -1
  89. machineconfig/utils/ve.py +11 -4
  90. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
  91. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/RECORD +96 -89
  92. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +2 -2
  93. machineconfig/scripts/python/explore.py +0 -49
  94. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
  95. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
  96. machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
  97. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
  98. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
  99. machineconfig/settings/yazi/yazi.toml +0 -17
  100. machineconfig/setup_linux/others/cli_installation.sh +0 -137
  101. /machineconfig/{settings/shells/pwsh/profile.ps1 → scripts/python/helpers_fire_command/f.py} +0 -0
  102. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  103. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
  104. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,58 @@
1
+
2
+ [mgr]
3
+ show_hidden = true
4
+ ratio = [1, 3, 5]
5
+
6
+ [opener]
7
+ system_default = [
8
+ { run = "cmd /C start \"\" \"%1\"", desc = "System Default (Windows)", for = "windows", block = false, orphan = false }
9
+ ]
10
+ # Edit files in your editor (replace "code" with your editor if you like)
11
+ edit = [
12
+ { run = "code \"%*\"", desc = "VS Code (Editor)", block = true, orphan = false, for = "windows" }
13
+ ]
14
+ helix = [
15
+ { run = "hx \"%*\"", desc = "Helix (Editor)", block = true, orphan = false, for = "windows" }
16
+ ]
17
+
18
+ # Play media files
19
+ play = [
20
+ { run = "start \"\" \"%*\"", desc = "System Default (Media)", block = false, orphan = false, for = "windows" }
21
+ ]
22
+
23
+ # Open files/folders with system default program/explorer
24
+ open = [
25
+ { run = "start \"\" \"%1\"", desc = "System Default (Open)", block = false, orphan = false, for = "windows" }
26
+ ]
27
+
28
+
29
+ [open]
30
+ prepend_rules = [
31
+ # Text/Code files - open with VSCode by default, Helix as alternative
32
+ { name = "*.py", use = ["edit", "helix"] },
33
+ { name = "*.{rs,toml,yaml,yml,json,js,ts,tsx,jsx,html,css,md,txt,sh,ps1,bat,cmd}", use = ["edit", "helix"] },
34
+ { mime = "text/*", use = ["edit", "helix"] },
35
+ ]
36
+ rules = [
37
+ { mime = "video/*", use = "play" },
38
+ { mime = "application/pdf", use = ["system_default"] },
39
+ { mime = "image/*", use = ["system_default"] },
40
+ { name = "*", use = ["system_default"] }
41
+ ]
42
+
43
+
44
+
45
+ [plugin]
46
+ # dir = "~/AppData/Roaming/yazi/config/plugins"
47
+
48
+ [[plugin.load]]
49
+ name = "toggle-pane"
50
+ path = "toggle-pane.yazi"
51
+
52
+
53
+ [preview]
54
+ # Change them to your desired values
55
+ max_width = 1000
56
+ max_height = 1000
57
+
58
+
@@ -2,16 +2,16 @@
2
2
  . <( curl -sSL "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_linux/uv.sh")
3
3
  . <( curl -sSL "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/scripts/linux/wrap_mcfg")
4
4
 
5
- alias devops='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" devops'
6
- alias cloud='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" cloud'
7
- alias agents='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" agents'
8
- alias sessions='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" sessions'
9
- alias ftpx='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" ftpx'
10
- alias fire='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" fire'
11
- alias croshell='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" croshell'
12
- alias utils='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" utils'
13
- alias terminal='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" terminal'
14
- alias msearch='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.64" msearch'
5
+ alias devops='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" devops'
6
+ alias cloud='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" cloud'
7
+ alias agents='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" agents'
8
+ alias sessions='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" sessions'
9
+ alias ftpx='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" ftpx'
10
+ alias fire='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" fire'
11
+ alias croshell='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" croshell'
12
+ alias utils='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" utils'
13
+ alias terminal='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" terminal'
14
+ alias msearch='$HOME/.local/bin/uvx --python 3.14 --from "machineconfig>=7.83" msearch'
15
15
 
16
16
  alias d='wrap_in_shell_script devops'
17
17
  alias c='wrap_in_shell_script cloud'
@@ -7,4 +7,11 @@ if (-not (Test-Path -Path "$HOME\.local\bin\uv.exe")) {
7
7
  Write-Output "uv binary found, updating..."
8
8
  & "$HOME\.local\bin\uv.exe" self update
9
9
  }
10
- & "$HOME\.local\bin\uv.exe" python install 3.14
10
+
11
+ # `C:\Users\aalsaf01\.local\bin` is not on your PATH. To use installed Python executables, run `$env:PATH = "C:\Users\aalsaf01\.local\bin;$env:PATH"` or `uv python update-shell`.
12
+ & "$env:USERPROFILE\.local\bin\uv.exe" python update-shell
13
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
14
+
15
+ # & "$HOME\.local\bin\uv.exe" python install 3.14
16
+ uv python install 3.14
17
+
@@ -3,16 +3,16 @@
3
3
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/uv.ps1").Content
4
4
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/scripts/windows/wrap_mcfg.ps1").Content
5
5
 
6
- function devops { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" devops $args }
7
- function cloud { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" cloud $args }
8
- function agents { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" agents $args }
9
- function sessions { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" sessions $args }
10
- function ftpx { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" ftpx $args }
11
- function fire { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" fire $args }
12
- function croshell { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" croshell $args }
13
- function utils { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" utils $args }
14
- function terminal { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" terminal $args }
15
- function msearch { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.64" msearch $args }
6
+ function devops { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" devops $args }
7
+ function cloud { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" cloud $args }
8
+ function agents { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" agents $args }
9
+ function sessions { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" sessions $args }
10
+ function ftpx { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" ftpx $args }
11
+ function fire { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" fire $args }
12
+ function croshell { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" croshell $args }
13
+ function utils { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" utils $args }
14
+ function terminal { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" terminal $args }
15
+ function msearch { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.83" msearch $args }
16
16
 
17
17
  function d { wrap_in_shell_script devops @args }
18
18
  function c { wrap_in_shell_script cloud @args }
@@ -1,7 +1,8 @@
1
1
 
2
2
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/uv.ps1").Content
3
3
  # iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/scripts/windows/wrap_mcfg.ps1").Content
4
- uv tool install --upgrade --python 3.14 machineconfig both
4
+
5
+ uv tool install --upgrade --python 3.14 machineconfig
5
6
 
6
7
  devops install --group sysabc
7
8
 
@@ -12,5 +13,5 @@ devops config shell
12
13
  devops config shell --which nushell
13
14
 
14
15
  wt # start Windows Terminal to pick up config changes
15
- devops install --group termabc
16
+ devops install --group termabc
16
17
  devops install --group gui
@@ -1,4 +1,3 @@
1
- from pathlib import Path
2
1
  from typing import Optional, Any
3
2
 
4
3
  from datetime import datetime, timezone, timedelta
@@ -96,14 +95,18 @@ def human_friendly_dict(d: dict[str, Any]) -> dict[str, Any]:
96
95
  return result
97
96
 
98
97
 
99
- def get_repo_root(path: Path) -> Optional[Path]:
98
+ def get_repo_root(path: "Path") -> Optional["Path"]:
100
99
  from git import Repo, InvalidGitRepositoryError
101
-
102
100
  try:
103
- repo = Repo(str(path), search_parent_directories=True)
101
+ repo = Repo(path, search_parent_directories=True)
104
102
  root = repo.working_tree_dir
105
103
  if root is not None:
104
+ from pathlib import Path
106
105
  return Path(root)
107
106
  except InvalidGitRepositoryError:
108
107
  pass
109
108
  return None
109
+
110
+
111
+ if __name__ == "__main__":
112
+ from pathlib import Path
@@ -38,14 +38,16 @@ def get_uv_command_executing_python_script(python_script: str, uv_with: Optional
38
38
  uv_project_dir_arg = "--project" + f' "{uv_project_dir}"'
39
39
  else:
40
40
  uv_project_dir_arg = ""
41
- print_code_string = lambda_to_python_script(lambda: print_code(code=python_script, lexer="python", desc="Temporary Python Script", subtitle="Executing via shell script"), in_global=True, import_module=False)
41
+ print_code_string = lambda_to_python_script(lambda: print_code(code=python_script, lexer="python", desc="Temporary Python Script", subtitle="Executing via shell script"),
42
+ in_global=True, import_module=False)
42
43
  python_file.write_text(print_code_string + "\n" + python_script, encoding="utf-8")
43
44
  shell_script = f"""uv run {uv_with_arg} {uv_project_dir_arg} {str(python_file)} """
44
45
  return shell_script, python_file
45
46
 
46
47
 
47
48
  def run_lambda_function(lmb: Callable[[], Any], uv_with: Optional[list[str]], uv_project_dir: Optional[str]) -> None:
48
- code = lambda_to_python_script(lmb, in_global=True, import_module=False)
49
+ code = lambda_to_python_script(lmb,
50
+ in_global=True, import_module=False)
49
51
  uv_command, _py_file = get_uv_command_executing_python_script(python_script=code, uv_with=uv_with, uv_project_dir=uv_project_dir)
50
52
  run_shell_script(uv_command)
51
53
  def run_python_script_in_marimo(py_script: str, uv_project_with: Optional[str]):
@@ -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]),
@@ -0,0 +1,200 @@
1
+
2
+
3
+ import platform
4
+ from urllib.parse import urlparse
5
+
6
+ import typer
7
+ from typing import TYPE_CHECKING
8
+
9
+ from machineconfig.utils.installer_utils.installer_helper import install_deb_package
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
+ )
17
+ from machineconfig.utils.path_extended import DECOMPRESS_SUPPORTED_FORMATS, PathExtended
18
+ from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
19
+
20
+ if TYPE_CHECKING:
21
+ from rich.console import Console
22
+
23
+ SUPPORTED_GITHUB_HOSTS = {"github.com", "www.github.com"}
24
+
25
+
26
+ def _format_size(size_bytes: int) -> str:
27
+ if size_bytes <= 0:
28
+ return "0 B"
29
+ units = ("B", "KiB", "MiB", "GiB", "TiB")
30
+ value = float(size_bytes)
31
+ index = 0
32
+ while value >= 1024 and index < len(units) - 1:
33
+ value /= 1024
34
+ index += 1
35
+ return f"{value:.1f} {units[index]}"
36
+
37
+
38
+ def _derive_tool_name(repo_name: str, asset_name: str) -> str:
39
+ repo_segment = repo_name.split("/", maxsplit=1)[-1]
40
+ repo_clean = repo_segment.replace(".git", "").lower()
41
+ repo_filtered = "".join(char for char in repo_clean if char.isalnum())
42
+ if repo_filtered:
43
+ return repo_filtered
44
+ asset_clean = asset_name.lower()
45
+ asset_filtered = "".join(char for char in asset_clean if char.isalnum())
46
+ if asset_filtered:
47
+ return asset_filtered
48
+ return "githubapp"
49
+
50
+
51
+ def _download_and_prepare(download_url: str) -> PathExtended:
52
+ archive_path = PathExtended(download_url).download(folder=INSTALL_TMP_DIR)
53
+ extracted_path = archive_path
54
+ if extracted_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
55
+ extracted_path = archive_path.decompress()
56
+ archive_path.delete(sure=True)
57
+ if extracted_path.is_dir():
58
+ nested_items = list(extracted_path.glob("*"))
59
+ if len(nested_items) == 1:
60
+ nested_path = PathExtended(nested_items[0])
61
+ if nested_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
62
+ extracted_path = nested_path.decompress()
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
+
70
+ if extracted_path.suffix == ".deb":
71
+ install_deb_package(extracted_path)
72
+ tool_name_deb = _derive_tool_name(repo_name, asset_name)
73
+ INSTALL_VERSION_ROOT.joinpath(tool_name_deb).parent.mkdir(parents=True, exist_ok=True)
74
+ INSTALL_VERSION_ROOT.joinpath(tool_name_deb).write_text(version, encoding="utf-8")
75
+ console.print(Panel(f"Installed Debian package for [green]{tool_name_deb}[/green]", title="✅ Complete", border_style="green"))
76
+ return
77
+ system_name = platform.system()
78
+ tool_name = _derive_tool_name(repo_name, asset_name)
79
+ rename_target = f"{tool_name}.exe" if system_name == "Windows" else tool_name
80
+ try:
81
+ if system_name == "Windows":
82
+ installed_path = find_move_delete_windows(downloaded_file_path=extracted_path, exe_name=tool_name, delete=True, rename_to=rename_target)
83
+ elif system_name in {"Linux", "Darwin"}:
84
+ installed_path = find_move_delete_linux(downloaded=extracted_path, tool_name=tool_name, delete=True, rename_to=rename_target)
85
+ else:
86
+ console.print(Panel(f"Unsupported operating system: {system_name}", title="❌ Error", border_style="red"))
87
+ raise typer.Exit(1)
88
+ except IndexError:
89
+ if system_name == "Windows":
90
+ installed_path = find_move_delete_windows(downloaded_file_path=extracted_path, exe_name=None, delete=True, rename_to=rename_target)
91
+ elif system_name in {"Linux", "Darwin"}:
92
+ installed_path = find_move_delete_linux(downloaded=extracted_path, tool_name="", delete=True, rename_to=rename_target)
93
+ else:
94
+ raise
95
+ INSTALL_VERSION_ROOT.joinpath(tool_name).parent.mkdir(parents=True, exist_ok=True)
96
+ INSTALL_VERSION_ROOT.joinpath(tool_name).write_text(version, encoding="utf-8")
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)