machineconfig 3.92__py3-none-any.whl → 3.94__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 (74) hide show
  1. machineconfig/jobs/{python_custom_installers → installer/custom}/gh.py +22 -7
  2. machineconfig/jobs/{python_custom_installers → installer/custom}/hx.py +13 -4
  3. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/alacritty.py +11 -5
  4. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/brave.py +17 -14
  5. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/bypass_paywall.py +7 -9
  6. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/code.py +13 -13
  7. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/cursor.py +7 -7
  8. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/espanso.py +21 -17
  9. machineconfig/jobs/installer/custom_dev/goes.py +63 -0
  10. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/lvim.py +10 -14
  11. machineconfig/jobs/installer/custom_dev/nerdfont.py +87 -0
  12. machineconfig/{setup_windows/wt_and_pwsh/install_nerd_fonts.py → jobs/installer/custom_dev/nerfont_windows_helper.py} +68 -25
  13. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/redis.py +13 -8
  14. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/wezterm.py +13 -7
  15. machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/winget.py +1 -3
  16. machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/docker.sh +1 -1
  17. machineconfig/jobs/installer/packages_custom_dev.json +226 -0
  18. machineconfig/jobs/installer/packages_custom_essential.json +39 -0
  19. machineconfig/jobs/installer/packages_github_dev.json +1110 -0
  20. machineconfig/jobs/installer/packages_github_essential.json +804 -0
  21. machineconfig/jobs/linux/msc/cli_agents.sh +5 -0
  22. machineconfig/scripts/python/ai/solutions/gemini/settings.json +1 -1
  23. machineconfig/scripts/python/devops_devapps_install.py +31 -20
  24. machineconfig/utils/installer.py +17 -80
  25. machineconfig/utils/installer_utils/github_release_bulk.py +198 -0
  26. machineconfig/utils/installer_utils/installer_class.py +223 -210
  27. machineconfig/utils/schemas/installer/installer_types.py +29 -6
  28. {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/METADATA +1 -1
  29. {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/RECORD +45 -67
  30. machineconfig/jobs/python_custom_installers/archive/ngrok.py +0 -63
  31. machineconfig/jobs/python_custom_installers/dev/aider.py +0 -37
  32. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +0 -78
  33. machineconfig/jobs/python_custom_installers/dev/goes.py +0 -55
  34. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +0 -68
  35. machineconfig/jobs/python_custom_installers/dev/reverse_proxy.md +0 -31
  36. machineconfig/jobs/python_custom_installers/docker.py +0 -74
  37. machineconfig/jobs/python_custom_installers/warp-cli.py +0 -71
  38. machineconfig/jobs/python_generic_installers/config.json +0 -603
  39. machineconfig/jobs/python_generic_installers/config.json.bak +0 -414
  40. machineconfig/jobs/python_generic_installers/dev/config.archive.json +0 -18
  41. machineconfig/jobs/python_generic_installers/dev/config.json +0 -825
  42. machineconfig/jobs/python_generic_installers/dev/config.json.bak +0 -565
  43. machineconfig/jobs/python_linux_installers/__init__.py +0 -0
  44. machineconfig/jobs/python_linux_installers/archive/config.json +0 -18
  45. machineconfig/jobs/python_linux_installers/archive/config.json.bak +0 -10
  46. machineconfig/jobs/python_linux_installers/config.json +0 -145
  47. machineconfig/jobs/python_linux_installers/config.json.bak +0 -110
  48. machineconfig/jobs/python_linux_installers/dev/__init__.py +0 -0
  49. machineconfig/jobs/python_linux_installers/dev/config.json +0 -276
  50. machineconfig/jobs/python_linux_installers/dev/config.json.bak +0 -206
  51. machineconfig/jobs/python_windows_installers/__init__.py +0 -0
  52. machineconfig/jobs/python_windows_installers/archive/__init__.py +0 -0
  53. machineconfig/jobs/python_windows_installers/archive/file.json +0 -11
  54. machineconfig/jobs/python_windows_installers/config.json +0 -82
  55. machineconfig/jobs/python_windows_installers/config.json.bak +0 -56
  56. machineconfig/jobs/python_windows_installers/dev/__init__.py +0 -0
  57. machineconfig/jobs/python_windows_installers/dev/config.json +0 -4
  58. machineconfig/jobs/python_windows_installers/dev/config.json.bak +0 -3
  59. /machineconfig/jobs/{python_custom_installers → installer}/__init__.py +0 -0
  60. /machineconfig/jobs/{python_generic_installers → installer/custom_dev}/__init__.py +0 -0
  61. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/brave.sh +0 -0
  62. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/docker_start.sh +0 -0
  63. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/edge.sh +0 -0
  64. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/nerdfont.sh +0 -0
  65. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/pgsql.sh +0 -0
  66. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/redis.sh +0 -0
  67. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/timescaledb.sh +0 -0
  68. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/vscode.sh +0 -0
  69. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/warp-cli.sh +0 -0
  70. /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/wezterm.sh +0 -0
  71. /machineconfig/{setup_windows/wt_and_pwsh → jobs/installer/powershell_scripts}/install_fonts.ps1 +0 -0
  72. {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/WHEEL +0 -0
  73. {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/entry_points.txt +0 -0
  74. {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,14 @@
2
2
 
3
3
 
4
4
  # Terminal-based CLI agents and tools for productivity and coding.
5
+ npm install -g @github/copilot
5
6
  npm install -g @google/gemini-cli
6
7
  npm install -g @charmland/crush
7
8
  npm install -g opencode-ai@latest # or curl -fsSL https://opencode.ai/install | bash
8
9
  uv tool install --force --python python3.12 --with pip aider-chat@latest
10
+ curl -fsSL https://app.factory.ai/cli | sh
11
+ # WARP TERMINAL CLI
12
+ # droid
9
13
 
10
14
 
11
15
  # cursor-cli
@@ -29,4 +33,5 @@ rm -rfd q
29
33
  # Editors based on AI
30
34
  # Kiro
31
35
  # Cursor
36
+ # Warp
32
37
 
@@ -69,7 +69,7 @@
69
69
  "customThemes": {
70
70
  "MyCustomTheme": {
71
71
  "name": "MyCustomTheme",
72
- "type": "custom",
72
+ "type": "CMD",
73
73
  "Background": "#181818",
74
74
  "Foreground": "#F8F8F2",
75
75
  "AccentBlue": "#61AFEF",
@@ -6,10 +6,11 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
6
6
  from machineconfig.utils.source_of_truth import LIBRARY_ROOT
7
7
  from machineconfig.utils.options import choose_from_options
8
8
  from machineconfig.utils.installer import get_installers, install_all
9
+ from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
9
10
  from platform import system
10
11
  from typing import Any, Optional, Literal, TypeAlias, get_args, Annotated
11
12
 
12
- WHICH_CAT: TypeAlias = Literal["essentials", "essentialsPlus", "systymPackages", "precheckedPackages"]
13
+ WHICH_CAT: TypeAlias = Literal["essentials", "essentialsDev", "systymPackages", "precheckedPackages"]
13
14
 
14
15
 
15
16
  def main_with_parser():
@@ -26,8 +27,7 @@ def main(which: Annotated[Optional[str], typer.Argument(help=f"Choose a category
26
27
  if which is not None: # install by name
27
28
  total_messages: list[str] = []
28
29
  for a_which in which.split(",") if type(which) == str else which:
29
- # Use get_installers to get properly converted installer objects
30
- all_installers = get_installers(system=system(), dev=False) + get_installers(system=system(), dev=True)
30
+ all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL", "GITHUB_DEV", "CUSTOM_DEV"])
31
31
 
32
32
  # Find installer by exe_name or name
33
33
  selected_installer = None
@@ -53,33 +53,43 @@ def main(which: Annotated[Optional[str], typer.Argument(help=f"Choose a category
53
53
  print(a_message)
54
54
  return None
55
55
 
56
- # interactive installation - get all installers including dev ones
57
- installers = get_installers(system=system(), dev=True)
58
-
56
+ installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL", "GITHUB_DEV", "CUSTOM_DEV"])
59
57
  # Check installed programs with progress indicator
60
58
  with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
61
59
  task = progress.add_task("✅ Checking installed programs...", total=len(installers))
62
- options = []
60
+ installer_options = []
63
61
  for x in installers:
64
- options.append(x.get_description())
62
+ installer_options.append(x.get_description())
65
63
  progress.update(task, advance=1)
66
64
 
67
- options += list(get_args(WHICH_CAT))
68
- # print("s"*1000)
69
- program_names = choose_from_options(multi=True, msg="", options=options, header="🚀 CHOOSE DEV APP", default="essentials", fzf=True)
65
+ # Add category options at the beginning for better visibility
66
+ category_options = [f"📦 {cat}" for cat in get_args(WHICH_CAT)]
67
+ options = category_options + ["" * 50] + installer_options
68
+
69
+ program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", default="📦 essentials", fzf=True)
70
70
 
71
71
  total_commands = ""
72
72
  installation_messages: list[str] = []
73
73
  for _an_idx, a_program_name in enumerate(program_names):
74
+ # Skip separator lines
75
+ if a_program_name.startswith("─"):
76
+ continue
77
+
74
78
  print(f"""
75
79
  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76
80
  ┃ 🔄 Processing: {a_program_name}
77
81
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━""")
78
- if a_program_name in get_args(WHICH_CAT):
79
- shell_commands = get_programs_by_category(program_name=a_program_name) # type: ignore
80
- total_commands += "\n" + shell_commands
82
+
83
+ # Handle category options (remove emoji prefix)
84
+ if a_program_name.startswith("📦 "):
85
+ category_name = a_program_name[2:] # Remove "📦 " prefix
86
+ if category_name in get_args(WHICH_CAT):
87
+ shell_commands = get_programs_by_category(program_name=category_name) # type: ignore
88
+ total_commands += "\n" + shell_commands
81
89
  else:
82
- an_installer = installers[options.index(a_program_name)]
90
+ # Handle individual installer options
91
+ installer_idx = installer_options.index(a_program_name)
92
+ an_installer = installers[installer_idx]
83
93
  status_message = an_installer.install_robust(version=None) # finish the task - this returns a status message, not a command
84
94
  installation_messages.append(status_message)
85
95
 
@@ -103,13 +113,14 @@ def get_programs_by_category(program_name: WHICH_CAT):
103
113
  ┃ 📦 Installing Category: {program_name}
104
114
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━""")
105
115
  match program_name:
106
- case "essentials" | "essentialsPlus":
107
- installers_ = get_installers(dev=False, system=system())
108
- if program_name == "essentialsPlus":
109
- installers_ += get_installers(dev=True, system=system())
116
+ case "essentials":
117
+ installers_ = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL"])
118
+ install_all(installers=installers_)
119
+ program = ""
120
+ case "essentialsDev":
121
+ installers_ = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["GITHUB_DEV", "CUSTOM_DEV", "GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL"])
110
122
  install_all(installers=installers_)
111
123
  program = ""
112
-
113
124
  case "systymPackages":
114
125
  if system() == "Windows":
115
126
  options_system = parse_apps_installer_windows(LIBRARY_ROOT.joinpath("setup_windows/apps.ps1").read_text(encoding="utf-8"))
@@ -2,7 +2,7 @@
2
2
 
3
3
  from machineconfig.utils.installer_utils.installer_abc import LINUX_INSTALL_PATH
4
4
  from machineconfig.utils.installer_utils.installer_class import Installer
5
- from machineconfig.utils.schemas.installer.installer_types import APP_INSTALLER_CATEGORY, InstallerData, InstallerDataFiles
5
+ from machineconfig.utils.schemas.installer.installer_types import APP_INSTALLER_CATEGORY, InstallerData, InstallerDataFiles, get_normalized_arch, get_os_name, OPERATING_SYSTEMS, CPU_ARCHITECTURES
6
6
  from rich.console import Console
7
7
  from rich.panel import Panel # Added import
8
8
 
@@ -18,8 +18,7 @@ from joblib import Parallel, delayed
18
18
  def check_latest():
19
19
  console = Console() # Added console initialization
20
20
  console.print(Panel("🔍 CHECKING FOR LATEST VERSIONS", title="Status", expand=False)) # Replaced print with Panel
21
- installers = get_installers(system=platform.system(), dev=False)
22
- # installers += get_installers(system=platform.system(), dev=True)
21
+ installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL"])
23
22
  installers_github = []
24
23
  for inst__ in installers:
25
24
  app_name = inst__.installer_data.get("appName", "unknown")
@@ -92,92 +91,30 @@ def get_installed_cli_apps():
92
91
  return apps
93
92
 
94
93
 
95
- def get_installers(system: str, dev: bool) -> list[Installer]:
94
+ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: list[APP_INSTALLER_CATEGORY]) -> list[Installer]:
96
95
  print(f"\n{'=' * 80}\n🔍 LOADING INSTALLER CONFIGURATIONS 🔍\n{'=' * 80}")
97
- res_all = get_all_installer_data_files(system=system)
98
- if not dev:
99
- print("ℹ️ Excluding development installers...")
100
- del res_all["CUSTOM_DEV"]
101
- del res_all["OS_SPECIFIC_DEV"]
102
- del res_all["OS_GENERIC_DEV"]
103
-
104
- # Flatten the installer data from all categories
96
+ res_all = get_all_installer_data_files(which_cats=which_cats)
105
97
  all_installers: list[InstallerData] = []
106
98
  for _category, installer_data_files in res_all.items():
107
- all_installers.extend(installer_data_files["installers"])
108
-
99
+ suitable_installers = []
100
+ for an_installer in installer_data_files["installers"]:
101
+ if an_installer["fileNamePattern"][arch][os] is None:
102
+ continue
103
+ suitable_installers.append(an_installer)
104
+ all_installers.extend(suitable_installers)
109
105
  print(f"✅ Loaded {len(all_installers)} installer configurations\n{'=' * 80}")
110
106
  return [Installer(installer_data=installer_data) for installer_data in all_installers]
111
107
 
112
108
 
113
- def get_all_installer_data_files(system: str) -> dict[APP_INSTALLER_CATEGORY, InstallerDataFiles]:
109
+ def get_all_installer_data_files(which_cats: list[APP_INSTALLER_CATEGORY]) -> dict[APP_INSTALLER_CATEGORY, InstallerDataFiles]:
114
110
  print(f"\n{'=' * 80}\n📂 LOADING CONFIGURATION FILES 📂\n{'=' * 80}")
115
-
116
- print(f"🔍 Importing OS-specific installers for {system}...")
117
- if system == "Windows":
118
- import machineconfig.jobs.python_windows_installers as os_specific_installer
119
- else:
120
- import machineconfig.jobs.python_linux_installers as os_specific_installer
121
-
122
- print("🔍 Importing generic installers...")
123
- import machineconfig.jobs.python_generic_installers as generic_installer
124
-
125
- path_os_specific = PathExtended(os_specific_installer.__file__).parent
126
- path_os_generic = PathExtended(generic_installer.__file__).parent
127
-
128
- path_os_specific_dev = path_os_specific.joinpath("dev")
129
- path_os_generic_dev = path_os_generic.joinpath("dev")
130
-
111
+ import machineconfig.jobs.installer as module
112
+ from pathlib import Path
131
113
  print("📂 Loading configuration files...")
132
- res_final: dict[APP_INSTALLER_CATEGORY, InstallerDataFiles] = {}
133
-
134
- print(f"""📄 Loading OS-specific config from: {path_os_specific.joinpath("config.json")}""")
135
- os_specific_data = read_json(path=path_os_specific.joinpath("config.json"))
136
- res_final["OS_SPECIFIC"] = InstallerDataFiles(os_specific_data)
137
-
138
- print(f"""📄 Loading OS-generic config from: {path_os_generic.joinpath("config.json")}""")
139
- os_generic_data = read_json(path=path_os_generic.joinpath("config.json"))
140
- res_final["OS_GENERIC"] = InstallerDataFiles(os_generic_data)
141
-
142
- print(f"""📄 Loading OS-specific dev config from: {path_os_specific_dev.joinpath("config.json")}""")
143
- os_specific_dev_data = read_json(path=path_os_specific_dev.joinpath("config.json"))
144
- res_final["OS_SPECIFIC_DEV"] = InstallerDataFiles(os_specific_dev_data)
145
-
146
- print(f"""📄 Loading OS-generic dev config from: {path_os_generic_dev.joinpath("config.json")}""")
147
- os_generic_dev_data = read_json(path=path_os_generic_dev.joinpath("config.json"))
148
- res_final["OS_GENERIC_DEV"] = InstallerDataFiles(os_generic_dev_data)
149
-
150
- path_custom_installer = path_os_generic.with_name("python_custom_installers")
151
- path_custom_installer_dev = path_custom_installer.joinpath("dev")
152
-
153
- print(f"🔍 Loading custom installers from: {path_custom_installer}")
154
- import runpy
155
-
156
- res_custom_installers: list[InstallerData] = []
157
- for item in path_custom_installer.search("*.py", r=False, not_in=["__init__"]):
158
- try:
159
- print(f"📄 Loading custom installer: {item.name}")
160
- installer_data: InstallerData = runpy.run_path(str(item), run_name=None)["config_dict"]
161
- res_custom_installers.append(installer_data)
162
- except Exception as ex:
163
- print(f"❌ Failed to load {item}: {ex}")
164
-
165
- print(f"🔍 Loading custom dev installers from: {path_custom_installer_dev}")
166
- res_custom_dev_installers: list[InstallerData] = []
167
- for item in path_custom_installer_dev.search("*.py", r=False, not_in=["__init__"]):
168
- try:
169
- print(f"📄 Loading custom dev installer: {item.name}")
170
- installer_data: InstallerData = runpy.run_path(str(item), run_name=None)["config_dict"]
171
- res_custom_dev_installers.append(installer_data)
172
- except Exception as ex:
173
- print(f"❌ Failed to load {item}: {ex}")
174
-
175
- res_final["CUSTOM"] = InstallerDataFiles({"version": "1", "installers": res_custom_installers})
176
- res_final["CUSTOM_DEV"] = InstallerDataFiles({"version": "1", "installers": res_custom_dev_installers})
177
-
178
- print(
179
- f"✅ Configuration loading complete:\n - OS_SPECIFIC: {len(res_final['OS_SPECIFIC']['installers'])} items\n - OS_GENERIC: {len(res_final['OS_GENERIC']['installers'])} items\n - CUSTOM: {len(res_final['CUSTOM']['installers'])} items\n{'=' * 80}"
180
- )
114
+ res_final: dict[APP_INSTALLER_CATEGORY, InstallerDataFiles] = {key: read_json(Path(module.__file__).parent.joinpath(f"packages_{key.lower()}.json")) for key in which_cats}
115
+ print(f"Loaded: {len(res_final)} installer categories")
116
+ for k, v in res_final.items():
117
+ print(f" - {k}: {len(v['installers'])} items")
181
118
  return res_final
182
119
 
183
120
 
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to fetch GitHub release information from installer JSON files.
4
+ Extracts GitHub repository URLs and fetches latest release data with rate limiting.
5
+ """
6
+
7
+ import json
8
+ import time
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional, Set
12
+ from urllib.parse import urlparse
13
+
14
+
15
+ def is_github_repo(url: str) -> bool:
16
+ """Check if URL is a GitHub repository URL."""
17
+ try:
18
+ parsed = urlparse(url)
19
+ return parsed.netloc == "github.com" and len(parsed.path.split("/")) >= 3
20
+ except Exception:
21
+ return False
22
+
23
+
24
+ def extract_github_repos_from_json(json_file_path: Path) -> Set[str]:
25
+ """Extract GitHub repository URLs from installer JSON file."""
26
+ github_repos: Set[str] = set()
27
+
28
+ try:
29
+ with open(json_file_path, 'r', encoding='utf-8') as file:
30
+ data = json.load(file)
31
+
32
+ for installer in data.get("installers", []):
33
+ repo_url = installer.get("repoURL", "")
34
+ if is_github_repo(repo_url):
35
+ github_repos.add(repo_url)
36
+
37
+ except (json.JSONDecodeError, FileNotFoundError) as e:
38
+ print(f"Error reading {json_file_path}: {e}")
39
+
40
+ return github_repos
41
+
42
+
43
+ def get_repo_name_from_url(repo_url: str) -> str:
44
+ """Extract owner/repo from GitHub URL."""
45
+ try:
46
+ parsed = urlparse(repo_url)
47
+ path_parts = parsed.path.strip("/").split("/")
48
+ return f"{path_parts[0]}/{path_parts[1]}"
49
+ except (IndexError, AttributeError):
50
+ return ""
51
+
52
+
53
+ def fetch_github_release_data(repo_name: str) -> Optional[Dict[str, Any]]:
54
+ """Fetch latest release data from GitHub API using curl."""
55
+ 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}")
70
+ 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}")
78
+ return None
79
+ elif "Not Found" in response_data.get("message", ""):
80
+ print(f"🔍 No releases found for {repo_name}")
81
+ return None
82
+
83
+ return response_data
84
+
85
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, subprocess.SubprocessError) as e:
86
+ print(f"❌ Error fetching {repo_name}: {e}")
87
+ return None
88
+
89
+
90
+ def extract_release_info(release_data: Dict[str, Any]) -> Dict[str, Any]:
91
+ """Extract relevant information from GitHub release data."""
92
+ if not release_data:
93
+ return {}
94
+
95
+ asset_names = [asset["name"] for asset in release_data.get("assets", [])]
96
+
97
+ return {
98
+ "tag_name": release_data.get("tag_name", ""),
99
+ "name": release_data.get("name", ""),
100
+ "published_at": release_data.get("published_at", ""),
101
+ "assets": asset_names,
102
+ "assets_count": len(asset_names)
103
+ }
104
+
105
+
106
+ def main() -> None:
107
+ """Main function to process installer JSON files and fetch GitHub release data."""
108
+ # Define paths
109
+ current_dir = Path(__file__).parent
110
+ installer_dir = current_dir.parent.parent / "jobs" / "installer"
111
+
112
+ standard_json = installer_dir / "packages_standard.json"
113
+ dev_json = installer_dir / "packages_dev.json"
114
+ output_json = current_dir / "github_releases.json"
115
+
116
+ print("🔍 Starting GitHub release data extraction...")
117
+ print(f"📁 Processing files from: {installer_dir}")
118
+
119
+ # Extract GitHub repositories from both files
120
+ all_github_repos: Set[str] = set()
121
+
122
+ if standard_json.exists():
123
+ print(f"📄 Reading {standard_json.name}...")
124
+ repos = extract_github_repos_from_json(standard_json)
125
+ all_github_repos.update(repos)
126
+ print(f" Found {len(repos)} GitHub repos")
127
+ else:
128
+ print(f"⚠️ File not found: {standard_json}")
129
+
130
+ if dev_json.exists():
131
+ print(f"📄 Reading {dev_json.name}...")
132
+ repos = extract_github_repos_from_json(dev_json)
133
+ all_github_repos.update(repos)
134
+ print(f" Found {len(repos)} GitHub repos")
135
+ else:
136
+ print(f"⚠️ File not found: {dev_json}")
137
+
138
+ print(f"🎯 Total unique GitHub repositories found: {len(all_github_repos)}")
139
+
140
+ if not all_github_repos:
141
+ print("❌ No GitHub repositories found. Exiting.")
142
+ return
143
+
144
+ # Fetch release data with rate limiting
145
+ release_mapping: Dict[str, Any] = {}
146
+ total_repos = len(all_github_repos)
147
+
148
+ print(f"\n🚀 Fetching release data for {total_repos} repositories...")
149
+ print("⏰ Rate limiting: 5 seconds between requests")
150
+ print("-" * 60)
151
+
152
+ for i, repo_url in enumerate(sorted(all_github_repos), 1):
153
+ repo_name = get_repo_name_from_url(repo_url)
154
+
155
+ if not repo_name:
156
+ print(f"⚠️ [{i:3d}/{total_repos}] Invalid repo URL: {repo_url}")
157
+ continue
158
+
159
+ print(f"📡 [{i:3d}/{total_repos}] Fetching: {repo_name}", end=" ... ")
160
+
161
+ release_data = fetch_github_release_data(repo_name)
162
+
163
+ if release_data:
164
+ release_info = extract_release_info(release_data)
165
+ release_mapping[repo_url] = release_info
166
+ assets_count = release_info.get("assets_count", 0)
167
+ tag = release_info.get("tag_name", "unknown")
168
+ print(f"✅ {tag} ({assets_count} assets)")
169
+ else:
170
+ release_mapping[repo_url] = {}
171
+ print("❌ No data")
172
+
173
+ # Rate limiting - wait 5 seconds between requests (except for the last one)
174
+ if i < total_repos:
175
+ time.sleep(5)
176
+
177
+ # Save results
178
+ output_data = {
179
+ "generated_at": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
180
+ "total_repositories": len(all_github_repos),
181
+ "successful_fetches": len([v for v in release_mapping.values() if v]),
182
+ "releases": release_mapping
183
+ }
184
+
185
+ with open(output_json, 'w', encoding='utf-8') as f:
186
+ json.dump(output_data, f, indent=2, ensure_ascii=False)
187
+
188
+ successful = len([v for v in release_mapping.values() if v])
189
+ print("\n📊 Summary:")
190
+ print(f" Total repositories processed: {len(all_github_repos)}")
191
+ print(f" Successful fetches: {successful}")
192
+ print(f" Failed fetches: {len(all_github_repos) - successful}")
193
+ print(f" Output saved to: {output_json}")
194
+ print("✅ Done!")
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()