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
@@ -3,12 +3,14 @@ from machineconfig.utils.installer_utils.installer_abc import find_move_delete_l
3
3
  from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT, LIBRARY_ROOT
4
4
  from machineconfig.utils.options import check_tool_exists
5
5
  from machineconfig.utils.io import read_json
6
- from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles
6
+ from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles, get_os_name, get_normalized_arch
7
7
 
8
8
  import platform
9
9
  import subprocess
10
- from typing import Optional
10
+ import json
11
+ from typing import Optional, Any
11
12
  from pathlib import Path
13
+ from urllib.parse import urlparse
12
14
 
13
15
 
14
16
  class Installer:
@@ -16,23 +18,21 @@ class Installer:
16
18
  self.installer_data: InstallerData = installer_data
17
19
 
18
20
  def __repr__(self) -> str:
19
- exe_name = self.installer_data.get("exeName", "unknown")
20
- app_name = self.installer_data.get("appName", "unknown")
21
- repo_url = self.installer_data.get("repoURL", "unknown")
22
- return f"Installer of {exe_name} {app_name} @ {repo_url}"
23
-
24
- def get_description(self):
25
- # old_version_cli = Terminal().run(f"{self.exe_name} --version").op.replace("\n", "")
26
- # old_version_cli = os.system(f"{self.exe_name} --version").replace("\n", "")
27
- exe_name = self.installer_data.get("exeName", "")
28
- if not exe_name:
29
- return "Invalid installer: missing exeName"
21
+ app_name = self.installer_data["appName"]
22
+ repo_url = self.installer_data["repoURL"]
23
+ return f"Installer of {app_name} @ {repo_url}"
30
24
 
25
+ def get_description(self) -> str:
26
+ exe_name = self._get_exe_name()
27
+
31
28
  old_version_cli: bool = check_tool_exists(tool_name=exe_name)
32
29
  old_version_cli_str = "✅" if old_version_cli else "❌"
33
- # name_version = f"{self.exe_name} {old_version_cli_str}"
34
- doc = self.installer_data.get("doc", "No description")
30
+ doc = self.installer_data["doc"]
35
31
  return f"{exe_name:<12} {old_version_cli_str} {doc}"
32
+
33
+ def _get_exe_name(self) -> str:
34
+ """Derive executable name from app name by converting to lowercase and removing spaces."""
35
+ return self.installer_data["appName"].lower().replace(" ", "").replace("-", "")
36
36
 
37
37
  @staticmethod
38
38
  def choose_app_and_install():
@@ -63,14 +63,15 @@ class Installer:
63
63
  raise ValueError(f"Could not find installer data for {app_name}")
64
64
 
65
65
  installer = Installer(installer_data=selected_installer_data)
66
- print(f"📦 Selected application: {selected_installer_data.get('exeName', 'unknown')}")
67
- version = input(f"📝 Enter version to install for {selected_installer_data.get('exeName', 'unknown')} [latest]: ") or None
68
- print(f"\n{'=' * 80}\n🚀 INSTALLING {selected_installer_data.get('exeName', 'UNKNOWN').upper()} 🚀\n{'=' * 80}")
66
+ exe_name = installer._get_exe_name()
67
+ print(f"📦 Selected application: {exe_name}")
68
+ version = input(f"📝 Enter version to install for {exe_name} [latest]: ") or None
69
+ print(f"\n{'=' * 80}\n🚀 INSTALLING {exe_name.upper()} 🚀\n{'=' * 80}")
69
70
  installer.install(version=version)
70
71
 
71
- def install_robust(self, version: Optional[str]):
72
+ def install_robust(self, version: Optional[str]) -> str:
72
73
  try:
73
- exe_name = self.installer_data.get("exeName", "unknown")
74
+ exe_name = self._get_exe_name()
74
75
  print(f"\n{'=' * 80}\n🚀 INSTALLING {exe_name.upper()} 🚀\n{'=' * 80}")
75
76
  result_old = subprocess.run(f"{exe_name} --version", shell=True, capture_output=True, text=True)
76
77
  old_version_cli = result_old.stdout.strip()
@@ -90,67 +91,106 @@ class Installer:
90
91
  return f"""📦️ 🤩 {exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}"""
91
92
 
92
93
  except Exception as ex:
93
- exe_name = self.installer_data.get("exeName", "unknown")
94
- app_name = self.installer_data.get("appName", "unknown")
94
+ exe_name = self._get_exe_name()
95
+ app_name = self.installer_data["appName"]
95
96
  print(f"❌ ERROR: Installation failed for {exe_name}: {ex}")
96
97
  return f"""📦️ ❌ Failed to install `{app_name}` with error: {ex}"""
97
98
 
98
- def install(self, version: Optional[str]):
99
- exe_name = self.installer_data.get("exeName", "unknown")
100
- repo_url = self.installer_data.get("repoURL", "")
99
+ def install(self, version: Optional[str]) -> None:
100
+ exe_name = self._get_exe_name()
101
+ repo_url = self.installer_data["repoURL"]
102
+ os_name = get_os_name()
103
+ arch = get_normalized_arch()
104
+ installer_arch_os = self.installer_data["fileNamePattern"][arch][os_name]
105
+ if installer_arch_os is None:
106
+ raise ValueError(f"No installation pattern for {exe_name} on {os_name} {arch}")
101
107
 
102
108
  print(f"\n{'=' * 80}\n🔧 INSTALLATION PROCESS: {exe_name} 🔧\n{'=' * 80}")
103
- if repo_url == "CUSTOM":
104
- print(f"🧩 Using custom installer for {exe_name}")
105
- import machineconfig.jobs.python_custom_installers as python_custom_installers
106
-
107
- installer_path = Path(python_custom_installers.__file__).parent.joinpath(exe_name + ".py")
108
- if not installer_path.exists():
109
- installer_path = Path(python_custom_installers.__file__).parent.joinpath("dev", exe_name + ".py")
110
- print(f"🔍 Looking for installer in dev folder: {installer_path}")
111
- else:
112
- print(f"🔍 Found installer at: {installer_path}")
113
-
114
- import runpy
115
-
116
- print(f"⚙️ Executing function 'main' from '{installer_path}'...")
117
- program: str = runpy.run_path(str(installer_path), run_name=None)["main"](version=version)
118
- # print(program)
119
- print("🚀 Running installation script...")
120
- if platform.system() == "Linux":
121
- script = "#!/bin/bash" + "\n" + program
122
- else:
123
- script = program
124
- script_file = PathExtended.tmpfile(name="tmp_shell_script", suffix=".ps1" if platform.system() == "Windows" else ".sh", folder="tmp_scripts")
125
- script_file.write_text(script, newline=None if platform.system() == "Windows" else "\n")
126
- if platform.system() == "Windows":
127
- start_cmd = "powershell"
128
- full_command = f"{start_cmd} {script_file}"
109
+ version_to_be_installed: str = "unknown" # Initialize to ensure it's always bound
110
+ if repo_url == "CMD":
111
+ if "npm " in installer_arch_os or "pip " in installer_arch_os or "winget " in installer_arch_os:
112
+ package_manager = installer_arch_os.split(" ", maxsplit=1)[0]
113
+ print(f"📦 Using package manager: {package_manager}")
114
+ desc = package_manager + " installation"
115
+ version_to_be_installed = package_manager + "Latest"
116
+ print(f"🚀 Running: {installer_arch_os}")
117
+ result = subprocess.run(installer_arch_os, shell=True, capture_output=True, text=True)
118
+ success = result.returncode == 0 and result.stderr == ""
119
+ if not success:
120
+ print(f"❌ {desc} failed")
121
+ if result.stdout:
122
+ print(f"STDOUT: {result.stdout}")
123
+ if result.stderr:
124
+ print(f"STDERR: {result.stderr}")
125
+ print(f"Return code: {result.returncode}")
126
+ print(f"✅ Package manager installation completed\n{'=' * 80}")
127
+ elif installer_arch_os.endswith((".sh", ".py", ".ps1")):
128
+ # search for the script, see which path ends with the script name
129
+ import machineconfig.jobs.installer as module
130
+ from pathlib import Path
131
+ search_root = Path(module.__file__).parent
132
+ search_results = list(search_root.rglob(installer_arch_os))
133
+ if len(search_results) == 0:
134
+ raise FileNotFoundError(f"Could not find installation script: {installer_arch_os}")
135
+ elif len(search_results) > 1:
136
+ raise ValueError(f"Multiple installation scripts found for {installer_arch_os}: {search_results}")
137
+ installer_path = search_results[0]
138
+ print(f"📄 Found installation script: {installer_path}")
139
+ if installer_arch_os.endswith(".sh"):
140
+ if platform.system() not in ["Linux", "Darwin"]:
141
+ raise NotImplementedError(f"Shell script installation not supported on {platform.system()}")
142
+ print(f"🚀 Running shell script: {installer_path}")
143
+ subprocess.run(f"bash {installer_path}", shell=True, check=True)
144
+ version_to_be_installed = "scripted_installation"
145
+ print(f"✅ Shell script installation completed\n{'=' * 80}")
146
+ elif installer_arch_os.endswith(".ps1"):
147
+ if platform.system() != "Windows":
148
+ raise NotImplementedError(f"PowerShell script installation not supported on {platform.system()}")
149
+ print(f"🚀 Running PowerShell script: {installer_path}")
150
+ subprocess.run(f"powershell -ExecutionPolicy Bypass -File {installer_path}", shell=True, check=True)
151
+ version_to_be_installed = "scripted_installation"
152
+ print(f"✅ PowerShell script installation completed\n{'=' * 80}")
153
+ elif installer_arch_os.endswith(".py"):
154
+ import runpy
155
+ runpy.run_path(str(installer_path), run_name=None)["main"](self.installer_data, version=version)
156
+ version_to_be_installed = str(version)
157
+ print(f"✅ Custom installation completed\n{'=' * 80}")
158
+ elif installer_arch_os.startswith("https://"): # its a url to be downloaded
159
+ print(f"📥 Downloading object from URL: {installer_arch_os}")
160
+ downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
161
+ # object is either a zip containing a binary or a straight out binary.
162
+ if downloaded_object.suffix in [".zip", ".tar.gz"]:
163
+ print(f"📦 Decompressing downloaded archive: {downloaded_object}")
164
+ downloaded_object = downloaded_object.decompress()
165
+ print(f"✅ Decompression completed to: {downloaded_object}")
166
+ if downloaded_object.suffix in [".exe", ""]: # likely an executable
167
+ if platform.system() == "Windows":
168
+ print("🪟 Installing on Windows...")
169
+ exe = find_move_delete_windows(downloaded_file_path=downloaded_object, exe_name=exe_name, delete=True, rename_to=exe_name.replace(".exe", "") + ".exe")
170
+ elif platform.system() in ["Linux", "Darwin"]:
171
+ system_name = "Linux" if platform.system() == "Linux" else "macOS"
172
+ print(f"🐧 Installing on {system_name}...")
173
+ exe = find_move_delete_linux(downloaded=downloaded_object, tool_name=exe_name, delete=True, rename_to=exe_name)
174
+ else:
175
+ error_msg = f"❌ ERROR: System {platform.system()} not supported"
176
+ print(error_msg)
177
+ raise NotImplementedError(error_msg)
178
+
179
+ _ = exe
180
+ if exe.name.replace(".exe", "") != exe_name.replace(".exe", ""):
181
+ from rich import print as pprint
182
+ from rich.panel import Panel
183
+ print("⚠️ Warning: Executable name mismatch")
184
+ pprint(Panel(f"Expected exe name: [red]{exe_name}[/red] \nAttained name: [red]{exe.name.replace('.exe', '')}[/red]", title="exe name mismatch", subtitle=repo_url))
185
+ new_exe_name = exe_name + ".exe" if platform.system() == "Windows" else exe_name
186
+ print(f"🔄 Renaming to correct name: {new_exe_name}")
187
+ exe.with_name(name=new_exe_name, inplace=True, overwrite=True)
188
+ version_to_be_installed = "downloaded_binary"
189
+ print(f"✅ Downloaded binary installation completed\n{'=' * 80}")
129
190
  else:
130
- start_cmd = "bash"
131
- full_command = f"{start_cmd} {script_file}"
132
- subprocess.run(full_command, stdin=None, stdout=None, stderr=None, shell=True, text=True)
133
- version_to_be_installed = str(version)
134
- print(f"✅ Custom installation completed\n{'=' * 80}")
135
-
136
- elif "npm " in repo_url or "pip " in repo_url or "winget " in repo_url:
137
- package_manager = repo_url.split(" ", maxsplit=1)[0]
138
- print(f"📦 Using package manager: {package_manager}")
139
- desc = package_manager + " installation"
140
- version_to_be_installed = package_manager + "Latest"
141
- print(f"🚀 Running: {repo_url}")
142
- result = subprocess.run(repo_url, shell=True, capture_output=True, text=True)
143
- success = result.returncode == 0 and result.stderr == ""
144
- if not success:
145
- print(f"❌ {desc} failed")
146
- if result.stdout:
147
- print(f"STDOUT: {result.stdout}")
148
- if result.stderr:
149
- print(f"STDERR: {result.stderr}")
150
- print(f"Return code: {result.returncode}")
151
- print(f"✅ Package manager installation completed\n{'=' * 80}")
152
-
191
+ raise NotImplementedError(f"CMD installation method not implemented for: {installer_arch_os}")
153
192
  else:
193
+ assert repo_url.startswith("https://github.com/"), f"repoURL must be a GitHub URL, got {repo_url}"
154
194
  print("📥 Downloading from repository...")
155
195
  downloaded, version_to_be_installed = self.download(version=version)
156
196
  if str(downloaded).endswith(".deb"):
@@ -195,55 +235,39 @@ class Installer:
195
235
 
196
236
  print(f"💾 Saving version information to: {INSTALL_VERSION_ROOT.joinpath(exe_name)}")
197
237
  INSTALL_VERSION_ROOT.joinpath(exe_name).parent.mkdir(parents=True, exist_ok=True)
198
- INSTALL_VERSION_ROOT.joinpath(exe_name).write_text(version_to_be_installed, encoding="utf-8")
238
+ INSTALL_VERSION_ROOT.joinpath(exe_name).write_text(version_to_be_installed or "unknown", encoding="utf-8")
199
239
  print(f"✅ Installation completed successfully!\n{'=' * 80}")
200
240
 
201
- def download(self, version: Optional[str]):
202
- exe_name = self.installer_data.get("exeName", "unknown")
203
- repo_url = self.installer_data.get("repoURL", "")
204
- app_name = self.installer_data.get("appName", "unknown")
205
- strip_v = self.installer_data.get("stripVersion", False)
206
-
241
+ def download(self, version: Optional[str]) -> tuple[PathExtended, str]:
242
+ exe_name = self._get_exe_name()
243
+ repo_url = self.installer_data["repoURL"]
244
+ app_name = self.installer_data["appName"]
207
245
  print(f"\n{'=' * 80}\n📥 DOWNLOADING: {exe_name} 📥\n{'=' * 80}")
208
- download_link: Optional[Path] = None
246
+
247
+ download_link: Optional[str] = None
209
248
  version_to_be_installed: Optional[str] = None
249
+
210
250
  if "github" not in repo_url or ".zip" in repo_url or ".tar.gz" in repo_url:
211
- download_link = Path(repo_url)
251
+ # Direct download URL
252
+ download_link = repo_url
212
253
  version_to_be_installed = "predefined_url"
213
254
  print(f"🔗 Using direct download URL: {download_link}")
214
255
  print(f"📦 Version to be installed: {version_to_be_installed}")
215
- elif self._any_direct_http_template():
216
- template, arch = self._select_template()
217
- if not template.startswith("http"):
218
- # Fall back to github-style handling below
219
- pass
220
- else:
221
- download_link = Path(template)
222
- version_to_be_installed = "predefined_url"
223
- system_name = self._system_name()
224
- print(f"🧭 Detected system={system_name} arch={arch}")
225
- print(f"🔗 Using architecture-specific direct URL: {download_link}")
226
- print(f"📦 Version to be installed: {version_to_be_installed}")
227
- # continue to unified download logic below
228
-
229
256
  else:
257
+ # GitHub repository
230
258
  print("🌐 Retrieving release information from GitHub...")
231
- release_url, version_to_be_installed = Installer.get_github_release(repo_url=repo_url, version=version)
259
+ arch = get_normalized_arch()
260
+ os_name = get_os_name()
261
+ print(f"🧭 Detected system={os_name} arch={arch}")
262
+
263
+ # Use existing get_github_release method to get download link and version
264
+ download_link, version_to_be_installed = self.get_github_release(repo_url, version)
265
+
266
+ if download_link is None:
267
+ raise ValueError(f"Could not retrieve download link for {exe_name} version {version or 'latest'}")
268
+
232
269
  print(f"📦 Version to be installed: {version_to_be_installed}")
233
- print(f"📦 Release URL: {release_url}")
234
-
235
- version_to_be_installed_stripped = version_to_be_installed.replace("v", "") if strip_v else version_to_be_installed
236
- version_to_be_installed_stripped = version_to_be_installed_stripped.replace("ipinfo-", "")
237
-
238
- template, arch = self._select_template()
239
- system_name = self._system_name()
240
- file_name = template.format(version_to_be_installed_stripped)
241
- print(f"🧭 Detected system={system_name} arch={arch}")
242
- print(f"📄 Using template: {template}")
243
- print(f"🗂️ Resolved file name: {file_name}")
244
-
245
- print(f"📄 File name: {file_name}")
246
- download_link = release_url.joinpath(file_name)
270
+ print(f"🔗 Download URL: {download_link}")
247
271
 
248
272
  assert download_link is not None, "download_link must be set"
249
273
  assert version_to_be_installed is not None, "version_to_be_installed must be set"
@@ -253,100 +277,89 @@ class Installer:
253
277
  return downloaded, version_to_be_installed
254
278
 
255
279
  # --------------------------- Arch / template helpers ---------------------------
256
- def _normalized_arch(self) -> str:
257
- arch_raw = platform.machine().lower()
258
- if arch_raw in ("x86_64", "amd64"):
259
- return "amd64"
260
- if arch_raw in ("aarch64", "arm64", "armv8", "armv8l"):
261
- return "arm64"
262
- return arch_raw
263
-
264
- def _system_name(self) -> str:
265
- sys_ = platform.system()
266
- if sys_ == "Darwin":
267
- return "macOS"
268
- return sys_
269
-
270
- def _any_direct_http_template(self) -> bool:
271
- filename_templates = self.installer_data.get("filenameTemplate", {})
272
- templates: list[str] = []
273
-
274
- for arch_templates in filename_templates.values():
275
- templates.extend([t for t in arch_templates.values() if t])
276
-
277
- return any(t for t in templates if t.startswith("http"))
278
-
279
- def _select_template(self) -> tuple[str, str]:
280
- sys_name = platform.system()
281
- arch = self._normalized_arch()
282
-
283
- filename_templates = self.installer_data.get("filenameTemplate", {})
284
-
285
- # Get templates for each architecture
286
- amd64_templates = filename_templates.get("amd64", {})
287
- arm64_templates = filename_templates.get("arm64", {})
288
-
289
- # mapping logic
290
- candidates: list[str] = []
291
- template: Optional[str] = None
292
-
293
- if sys_name == "Windows":
294
- if arch == "arm64" and arm64_templates.get("windows"):
295
- template = arm64_templates["windows"]
296
- else:
297
- template = amd64_templates.get("windows", "")
298
- candidates = ["arm64.windows", "amd64.windows"]
299
- elif sys_name == "Linux":
300
- if arch == "arm64" and arm64_templates.get("linux"):
301
- template = arm64_templates["linux"]
302
- else:
303
- template = amd64_templates.get("linux", "")
304
- candidates = ["arm64.linux", "amd64.linux"]
305
- elif sys_name == "Darwin":
306
- if arch == "arm64" and arm64_templates.get("macos"):
307
- template = arm64_templates["macos"]
308
- elif arch == "amd64" and amd64_templates.get("macos"):
309
- template = amd64_templates["macos"]
310
- else:
311
- # fallback between available mac templates
312
- template = arm64_templates.get("macos") or amd64_templates.get("macos") or ""
313
- candidates = ["arm64.macos", "amd64.macos"]
314
- else:
315
- raise NotImplementedError(f"System {sys_name} not supported")
316
-
317
- if not template:
318
- raise ValueError(f"No filename template available for system={sys_name} arch={arch}. Checked {candidates}")
319
-
320
- return template, arch
321
280
 
322
281
  @staticmethod
323
- def get_github_release(repo_url: str, version: Optional[str] = None):
324
- print(f"\n{'=' * 80}\n🔍 GITHUB RELEASE DETECTION 🔍\n{'=' * 80}")
325
- print(f"🌐 Inspecting releases at: {repo_url}")
326
- # with console.status("Installing..."): # makes troubles on linux when prompt asks for password to move file to /usr/bin
327
- if version is None:
328
- # see this: https://api.github.com/repos/cointop-sh/cointop/releases/latest
329
- print("🔍 Finding latest version...")
330
- import requests # https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
331
-
332
- _latest_version = requests.get(str(repo_url) + "/releases/latest", timeout=10).url.split("/")[
333
- -1
334
- ] # this is to resolve the redirection that occures: https://stackoverflow.com/questions/36070821/how-to-get-redirect-url-using-python-requests
335
- version_to_be_installed = _latest_version
336
- print(f"✅ Latest version detected: {version_to_be_installed}")
337
- # print(version_to_be_installed)
338
- else:
339
- version_to_be_installed = version
340
- print(f"📝 Using specified version: {version_to_be_installed}")
282
+ def _get_repo_name_from_url(repo_url: str) -> str:
283
+ """Extract owner/repo from GitHub URL."""
284
+ try:
285
+ parsed = urlparse(repo_url)
286
+ path_parts = parsed.path.strip("/").split("/")
287
+ return f"{path_parts[0]}/{path_parts[1]}"
288
+ except (IndexError, AttributeError):
289
+ return ""
341
290
 
342
- release_url = Path(repo_url + "/releases/download/" + version_to_be_installed)
343
- print(f"🔗 Release download URL: {release_url}\n{'=' * 80}")
344
- return release_url, version_to_be_installed
291
+ @staticmethod
292
+ def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
293
+ """Fetch release data from GitHub API using requests."""
294
+ import requests
295
+
296
+ try:
297
+ if version and version.lower() != "latest":
298
+ # Fetch specific version
299
+ url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
300
+ else:
301
+ # Fetch latest release
302
+ url = f"https://api.github.com/repos/{repo_name}/releases/latest"
303
+
304
+ response = requests.get(url, timeout=30)
305
+
306
+ if response.status_code != 200:
307
+ print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
308
+ return None
309
+
310
+ response_data = response.json()
311
+
312
+ # Check if API returned an error
313
+ if "message" in response_data:
314
+ if "API rate limit exceeded" in response_data.get("message", ""):
315
+ print(f"🚫 Rate limit exceeded for {repo_name}")
316
+ return None
317
+ elif "Not Found" in response_data.get("message", ""):
318
+ print(f"🔍 No releases found for {repo_name}")
319
+ return None
320
+
321
+ return response_data
322
+
323
+ except (requests.RequestException, requests.Timeout, json.JSONDecodeError) as e:
324
+ print(f"❌ Error fetching {repo_name}: {e}")
325
+ return None
326
+
327
+ def get_github_release(self, repo_url: str, version: Optional[str]) -> tuple[Optional[str], Optional[str]]:
328
+ """
329
+ Get download link and version from GitHub release based on fileNamePattern.
330
+ Returns (download_url, actual_version)
331
+ """
332
+ arch = get_normalized_arch()
333
+ os_name = get_os_name()
334
+ filename_pattern = self.installer_data["fileNamePattern"][arch][os_name]
335
+ if filename_pattern is None:
336
+ raise ValueError(f"No fileNamePattern for {self._get_exe_name()} on {os_name} {arch}")
337
+ repo_name = self._get_repo_name_from_url(repo_url)
338
+ if not repo_name:
339
+ print(f"❌ Invalid repository URL: {repo_url}")
340
+ return None, None
341
+ release_data = self._fetch_github_release_data(repo_name, version)
342
+ if not release_data:
343
+ return None, None
344
+ # print(release_data)
345
+ actual_version = release_data.get("tag_name", "unknown")
346
+ filename = filename_pattern.format(version=actual_version)
347
+
348
+ available_filenames: list[str] = []
349
+ for asset in release_data.get("assets", []):
350
+ an_dl = asset.get("browser_download_url", "NA")
351
+ available_filenames.append(an_dl.split("/")[-1])
352
+ if filename not in available_filenames:
353
+ filename = filename_pattern.format(version=actual_version.replace("v", ""))
354
+ if filename not in available_filenames:
355
+ print(f"❌ Filename {filename} not found in assets: {available_filenames}")
356
+ return None, None
357
+ browser_download_url = f"{repo_url}/releases/download/{actual_version}/{filename}"
358
+ return browser_download_url, actual_version
345
359
 
346
360
  @staticmethod
347
- def check_if_installed_already(exe_name: str, version: str, use_cache: bool):
361
+ def check_if_installed_already(exe_name: str, version: Optional[str], use_cache: bool) -> tuple[str, str, str]:
348
362
  print(f"\n{'=' * 80}\n🔍 CHECKING INSTALLATION STATUS: {exe_name} 🔍\n{'=' * 80}")
349
- version_to_be_installed = version
350
363
  INSTALL_VERSION_ROOT.joinpath(exe_name).parent.mkdir(parents=True, exist_ok=True)
351
364
  tmp_path = INSTALL_VERSION_ROOT.joinpath(exe_name)
352
365
 
@@ -368,18 +381,18 @@ class Installer:
368
381
  existing_version = result.stdout.strip()
369
382
  print(f"📄 Detected installed version: {existing_version}")
370
383
 
371
- if existing_version is not None:
372
- if existing_version == version_to_be_installed:
373
- print(f"✅ {exe_name} is up to date (version {version_to_be_installed})")
384
+ if existing_version is not None and version is not None:
385
+ if existing_version == version:
386
+ print(f"✅ {exe_name} is up to date (version {version})")
374
387
  print(f"📂 Version information stored at: {INSTALL_VERSION_ROOT}")
375
- return ("✅ Up to date", version.strip(), version_to_be_installed.strip())
388
+ return ("✅ Up to date", version.strip(), version.strip())
376
389
  else:
377
- print(f"🔄 {exe_name} needs update: {existing_version.rstrip()} → {version_to_be_installed}")
378
- tmp_path.write_text(version_to_be_installed, encoding="utf-8")
379
- return ("❌ Outdated", existing_version.strip(), version_to_be_installed.strip())
390
+ print(f"🔄 {exe_name} needs update: {existing_version.rstrip()} → {version}")
391
+ tmp_path.write_text(version, encoding="utf-8")
392
+ return ("❌ Outdated", existing_version.strip(), version.strip())
380
393
  else:
381
- print(f"📦 {exe_name} is not installed. Will install version: {version_to_be_installed}")
382
- tmp_path.write_text(version_to_be_installed, encoding="utf-8")
394
+ print(f"📦 {exe_name} is not installed. Will install version: {version}")
395
+ # tmp_path.write_text(version, encoding="utf-8")
383
396
 
384
397
  print(f"{'=' * 80}")
385
- return ("⚠️ NotInstalled", "None", version_to_be_installed.strip())
398
+ return ("⚠️ NotInstalled", "None", version or "unknown")
@@ -1,20 +1,43 @@
1
- from typing import TypedDict, Literal, TypeAlias
1
+ from typing import TypedDict, Literal, TypeAlias, Optional
2
+ import platform
2
3
 
3
4
 
4
- APP_INSTALLER_CATEGORY: TypeAlias = Literal["OS_SPECIFIC", "OS_GENERIC", "CUSTOM", "OS_SPECIFIC_DEV", "OS_GENERIC_DEV", "CUSTOM_DEV"]
5
+ APP_INSTALLER_CATEGORY: TypeAlias = Literal["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL", "GITHUB_DEV", "CUSTOM_DEV"]
5
6
  CPU_ARCHITECTURES: TypeAlias = Literal["amd64", "arm64"]
6
7
  OPERATING_SYSTEMS: TypeAlias = Literal["windows", "linux", "macos"]
7
8
 
8
9
 
9
10
  class InstallerData(TypedDict):
10
11
  appName: str
11
- repoURL: str
12
12
  doc: str
13
- filenameTemplate: dict[CPU_ARCHITECTURES, dict[OPERATING_SYSTEMS, str]]
14
- stripVersion: bool
15
- exeName: str
13
+ repoURL: str
14
+ fileNamePattern: dict[CPU_ARCHITECTURES, dict[OPERATING_SYSTEMS, Optional[str]]]
16
15
 
17
16
 
18
17
  class InstallerDataFiles(TypedDict):
19
18
  version: str
20
19
  installers: list[InstallerData]
20
+
21
+
22
+ def get_os_name() -> OPERATING_SYSTEMS:
23
+ """Get the operating system name in the format expected by the github parser."""
24
+ sys_name = platform.system()
25
+ if sys_name == "Windows":
26
+ return "windows"
27
+ elif sys_name == "Linux":
28
+ return "linux"
29
+ elif sys_name == "Darwin":
30
+ return "macos"
31
+ else:
32
+ raise NotImplementedError(f"System {sys_name} not supported")
33
+
34
+
35
+ def get_normalized_arch() -> CPU_ARCHITECTURES:
36
+ """Get the normalized CPU architecture."""
37
+ arch_raw = platform.machine().lower()
38
+ if arch_raw in ("x86_64", "amd64"):
39
+ return "amd64"
40
+ if arch_raw in ("aarch64", "arm64", "armv8", "armv8l"):
41
+ return "arm64"
42
+ # Default to amd64 if unknown architecture
43
+ return "amd64"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 3.92
3
+ Version: 3.94
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0