machineconfig 7.57__py3-none-any.whl → 7.79__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 +21 -9
  2. machineconfig/jobs/installer/custom/boxes.py +2 -2
  3. machineconfig/jobs/installer/custom/hx.py +3 -3
  4. machineconfig/jobs/installer/custom_dev/cloudflare_warp_cli.py +23 -0
  5. machineconfig/jobs/installer/custom_dev/dubdb_adbc.py +1 -1
  6. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +1 -1
  7. machineconfig/jobs/installer/custom_dev/sysabc.py +36 -28
  8. machineconfig/jobs/installer/custom_dev/wezterm.py +0 -4
  9. machineconfig/jobs/installer/installer_data.json +127 -25
  10. machineconfig/jobs/installer/package_groups.py +20 -13
  11. machineconfig/profile/create_links_export.py +2 -2
  12. machineconfig/scripts/__init__.py +0 -4
  13. machineconfig/scripts/linux/wrap_mcfg +1 -1
  14. machineconfig/scripts/python/agents.py +22 -17
  15. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +3 -0
  16. machineconfig/scripts/python/croshell.py +22 -17
  17. machineconfig/scripts/python/devops.py +3 -4
  18. machineconfig/scripts/python/devops_navigator.py +0 -4
  19. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  20. machineconfig/scripts/python/fire_jobs.py +19 -18
  21. machineconfig/scripts/python/ftpx.py +36 -12
  22. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  23. machineconfig/scripts/python/helpers/qr_code.py +166 -0
  24. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  25. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  26. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  27. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  28. machineconfig/scripts/python/helpers_cloud/cloud_mount.py +19 -17
  29. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  30. machineconfig/scripts/python/helpers_croshell/crosh.py +2 -2
  31. machineconfig/scripts/python/helpers_croshell/start_slidev.py +6 -7
  32. machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +4 -5
  33. machineconfig/scripts/python/helpers_devops/cli_nw.py +88 -7
  34. machineconfig/scripts/python/helpers_devops/cli_self.py +7 -6
  35. machineconfig/scripts/python/helpers_devops/cli_share_file.py +9 -9
  36. machineconfig/scripts/python/helpers_devops/cli_share_server.py +13 -12
  37. machineconfig/scripts/python/helpers_devops/cli_terminal.py +7 -6
  38. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -73
  39. machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
  40. machineconfig/scripts/python/helpers_devops/devops_status.py +7 -19
  41. machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +2 -3
  42. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +23 -13
  43. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  44. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +7 -4
  45. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  46. machineconfig/scripts/python/helpers_repos/entrypoint.py +2 -1
  47. machineconfig/scripts/python/helpers_repos/record.py +2 -1
  48. machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +5 -5
  49. machineconfig/scripts/python/helpers_utils/download.py +152 -0
  50. machineconfig/scripts/python/helpers_utils/path.py +81 -31
  51. machineconfig/scripts/python/interactive.py +2 -2
  52. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  53. machineconfig/scripts/python/msearch.py +21 -2
  54. machineconfig/scripts/python/nw/address.py +132 -0
  55. machineconfig/scripts/python/nw/devops_add_ssh_key.py +8 -5
  56. machineconfig/scripts/python/nw/ssh_debug_linux.py +7 -7
  57. machineconfig/scripts/python/nw/ssh_debug_windows.py +4 -4
  58. machineconfig/scripts/python/nw/wsl_windows_transfer.py +3 -2
  59. machineconfig/scripts/python/sessions.py +35 -20
  60. machineconfig/scripts/python/terminal.py +2 -2
  61. machineconfig/scripts/python/utils.py +12 -10
  62. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  63. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  64. machineconfig/settings/shells/nushell/config.nu +2 -2
  65. machineconfig/settings/shells/nushell/env.nu +45 -6
  66. machineconfig/settings/shells/nushell/init.nu +282 -95
  67. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  68. machineconfig/settings/shells/zsh/init.sh +0 -7
  69. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  70. machineconfig/setup_windows/uv.ps1 +8 -1
  71. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  72. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +3 -2
  73. machineconfig/utils/accessories.py +7 -4
  74. machineconfig/utils/code.py +6 -4
  75. machineconfig/utils/files/headers.py +2 -2
  76. machineconfig/utils/installer_utils/install_from_url.py +180 -0
  77. machineconfig/utils/installer_utils/installer_class.py +53 -47
  78. machineconfig/utils/installer_utils/{installer.py → installer_cli.py} +71 -65
  79. machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +1 -25
  80. machineconfig/utils/links.py +2 -2
  81. machineconfig/utils/meta.py +30 -16
  82. machineconfig/utils/options.py +4 -4
  83. machineconfig/utils/path_extended.py +3 -3
  84. machineconfig/utils/path_helper.py +33 -31
  85. machineconfig/utils/schemas/layouts/layout_types.py +1 -1
  86. machineconfig/utils/ssh.py +143 -409
  87. machineconfig/utils/ssh_utils/abc.py +8 -0
  88. machineconfig/utils/ssh_utils/copy_from_here.py +110 -0
  89. machineconfig/utils/ssh_utils/copy_to_here.py +302 -0
  90. machineconfig/utils/ssh_utils/utils.py +141 -0
  91. machineconfig/utils/ssh_utils/wsl.py +168 -0
  92. machineconfig/utils/upgrade_packages.py +2 -1
  93. machineconfig/utils/ve.py +11 -4
  94. {machineconfig-7.57.dist-info → machineconfig-7.79.dist-info}/METADATA +1 -1
  95. {machineconfig-7.57.dist-info → machineconfig-7.79.dist-info}/RECORD +102 -92
  96. {machineconfig-7.57.dist-info → machineconfig-7.79.dist-info}/entry_points.txt +2 -2
  97. machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
  98. machineconfig/scripts/python/explore.py +0 -49
  99. /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
  100. /machineconfig/{settings/shells/pwsh/profile.ps1 → scripts/python/helpers_fire_command/f.py} +0 -0
  101. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  102. /machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +0 -0
  103. {machineconfig-7.57.dist-info → machineconfig-7.79.dist-info}/WHEEL +0 -0
  104. {machineconfig-7.57.dist-info → machineconfig-7.79.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
1
+
2
+
3
+ import platform
4
+ from urllib.parse import urlparse
5
+
6
+ import typer
7
+ from typing import Optional, TypeAlias, cast
8
+
9
+ from machineconfig.utils.installer_utils.installer_class 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.path_extended import DECOMPRESS_SUPPORTED_FORMATS, PathExtended
12
+ from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
13
+
14
+ SUPPORTED_GITHUB_HOSTS = {"github.com", "www.github.com"}
15
+
16
+ GitHubAsset: TypeAlias = dict[str, object]
17
+ GitHubRelease: TypeAlias = dict[str, object]
18
+
19
+
20
+ def _extract_repo_name(github_url: str) -> str:
21
+ parsed = urlparse(github_url)
22
+ parts = [part for part in parsed.path.strip("/").split("/") if part]
23
+ if len(parts) < 2:
24
+ return ""
25
+ owner, repo = parts[0], parts[1]
26
+ if repo == "":
27
+ return ""
28
+ return f"{owner}/{repo}"
29
+
30
+
31
+ def _fetch_latest_release(repo_name: str) -> Optional[GitHubRelease]:
32
+ import json
33
+ import requests
34
+ try:
35
+ response = requests.get(f"https://api.github.com/repos/{repo_name}/releases/latest", timeout=30)
36
+ except requests.RequestException:
37
+ return None
38
+ if response.status_code != 200:
39
+ return None
40
+ try:
41
+ data = response.json()
42
+ except json.JSONDecodeError:
43
+ return None
44
+ if not isinstance(data, dict):
45
+ return None
46
+ return cast(GitHubRelease, data)
47
+
48
+
49
+ def _format_size(size_bytes: int) -> str:
50
+ if size_bytes <= 0:
51
+ return "0 B"
52
+ units = ("B", "KiB", "MiB", "GiB", "TiB")
53
+ value = float(size_bytes)
54
+ index = 0
55
+ while value >= 1024 and index < len(units) - 1:
56
+ value /= 1024
57
+ index += 1
58
+ return f"{value:.1f} {units[index]}"
59
+
60
+
61
+ def _derive_tool_name(repo_name: str, asset_name: str) -> str:
62
+ repo_segment = repo_name.split("/", maxsplit=1)[-1]
63
+ repo_clean = repo_segment.replace(".git", "").lower()
64
+ repo_filtered = "".join(char for char in repo_clean if char.isalnum())
65
+ if repo_filtered:
66
+ return repo_filtered
67
+ asset_clean = asset_name.lower()
68
+ asset_filtered = "".join(char for char in asset_clean if char.isalnum())
69
+ if asset_filtered:
70
+ return asset_filtered
71
+ return "githubapp"
72
+
73
+
74
+ def install_from_github_url(github_url: str) -> None:
75
+ from machineconfig.utils.options import choose_from_options
76
+ from rich.console import Console
77
+ from rich.panel import Panel
78
+
79
+ console = Console()
80
+ repo_name = _extract_repo_name(github_url)
81
+ if repo_name == "":
82
+ console.print(Panel(f"Invalid GitHub URL: {github_url}", title="❌ Error", border_style="red"))
83
+ raise typer.Exit(1)
84
+ console.print(Panel(f"Fetching latest release for [green]{repo_name}[/green]", title="🌐 GitHub", border_style="blue"))
85
+ release_raw = _fetch_latest_release(repo_name)
86
+ if not release_raw:
87
+ console.print(Panel("No releases available for this repository.", title="❌ Error", border_style="red"))
88
+ raise typer.Exit(1)
89
+ release = release_raw
90
+ assets_value = release.get("assets", [])
91
+ assets: list[GitHubAsset] = []
92
+ if isinstance(assets_value, list):
93
+ for asset in assets_value:
94
+ if isinstance(asset, dict):
95
+ typed_asset: GitHubAsset = {}
96
+ name_value = asset.get("name")
97
+ url_value = asset.get("browser_download_url")
98
+ size_value = asset.get("size")
99
+ content_value = asset.get("content_type")
100
+ if isinstance(name_value, str):
101
+ typed_asset["name"] = name_value
102
+ if isinstance(url_value, str):
103
+ typed_asset["browser_download_url"] = url_value
104
+ if isinstance(size_value, int):
105
+ typed_asset["size"] = size_value
106
+ if isinstance(content_value, str):
107
+ typed_asset["content_type"] = content_value
108
+ assets.append(typed_asset)
109
+ if not assets:
110
+ console.print(Panel("No downloadable assets found in the latest release.", title="❌ Error", border_style="red"))
111
+ raise typer.Exit(1)
112
+ binary_assets = assets
113
+ selection_pool = binary_assets if binary_assets else assets
114
+ if not selection_pool:
115
+ console.print(Panel("No assets available for installation.", title="❌ Error", border_style="red"))
116
+ raise typer.Exit(1)
117
+ options_map: dict[str, GitHubAsset] = {}
118
+ for asset in selection_pool:
119
+ name = asset.get("name")
120
+ download_url = asset.get("browser_download_url")
121
+ if not isinstance(name, str) or not isinstance(download_url, str) or name == "" or download_url == "":
122
+ continue
123
+ size_value = asset.get("size")
124
+ size = size_value if isinstance(size_value, int) else 0
125
+ label = f"{name} [{_format_size(size)}]"
126
+ options_map[label] = asset
127
+ if not options_map:
128
+ console.print(Panel("Release assets lack download URLs.", title="❌ Error", border_style="red"))
129
+ raise typer.Exit(1)
130
+ selection_label = choose_from_options(options=list(options_map.keys()), msg="Select a release asset", multi=False, header="📦 GitHub Release Assets", fzf=True)
131
+ selected_asset = options_map[selection_label]
132
+ download_url_value = selected_asset.get("browser_download_url")
133
+ asset_name_value = selected_asset.get("name")
134
+ if not isinstance(download_url_value, str) or download_url_value == "":
135
+ console.print(Panel("Selected asset lacks a download URL.", title="❌ Error", border_style="red"))
136
+ raise typer.Exit(1)
137
+ asset_name = asset_name_value if isinstance(asset_name_value, str) else "github_binary"
138
+ version_value = release.get("tag_name")
139
+ version = version_value if isinstance(version_value, str) and version_value != "" else "latest"
140
+ console.print(Panel(f"Downloading [cyan]{asset_name}[/cyan]", title="⬇️ Download", border_style="magenta"))
141
+ archive_path = PathExtended(download_url_value).download(folder=INSTALL_TMP_DIR)
142
+ extracted_path = archive_path
143
+ if extracted_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
144
+ extracted_path = archive_path.decompress()
145
+ archive_path.delete(sure=True)
146
+ if extracted_path.is_dir():
147
+ nested_items = list(extracted_path.glob("*"))
148
+ if len(nested_items) == 1:
149
+ nested_path = PathExtended(nested_items[0])
150
+ if nested_path.suffix in DECOMPRESS_SUPPORTED_FORMATS:
151
+ extracted_path = nested_path.decompress()
152
+ nested_path.delete(sure=True)
153
+ if extracted_path.suffix == ".deb":
154
+ install_deb_package(extracted_path)
155
+ tool_name_deb = _derive_tool_name(repo_name, asset_name)
156
+ INSTALL_VERSION_ROOT.joinpath(tool_name_deb).parent.mkdir(parents=True, exist_ok=True)
157
+ INSTALL_VERSION_ROOT.joinpath(tool_name_deb).write_text(version, encoding="utf-8")
158
+ console.print(Panel(f"Installed Debian package for [green]{tool_name_deb}[/green]", title="✅ Complete", border_style="green"))
159
+ return
160
+ system_name = platform.system()
161
+ tool_name = _derive_tool_name(repo_name, asset_name)
162
+ rename_target = f"{tool_name}.exe" if system_name == "Windows" else tool_name
163
+ try:
164
+ if system_name == "Windows":
165
+ installed_path = find_move_delete_windows(downloaded_file_path=extracted_path, exe_name=tool_name, delete=True, rename_to=rename_target)
166
+ elif system_name in {"Linux", "Darwin"}:
167
+ installed_path = find_move_delete_linux(downloaded=extracted_path, tool_name=tool_name, delete=True, rename_to=rename_target)
168
+ else:
169
+ console.print(Panel(f"Unsupported operating system: {system_name}", title="❌ Error", border_style="red"))
170
+ raise typer.Exit(1)
171
+ except IndexError:
172
+ if system_name == "Windows":
173
+ installed_path = find_move_delete_windows(downloaded_file_path=extracted_path, exe_name=None, delete=True, rename_to=rename_target)
174
+ elif system_name in {"Linux", "Darwin"}:
175
+ installed_path = find_move_delete_linux(downloaded=extracted_path, tool_name="", delete=True, rename_to=rename_target)
176
+ else:
177
+ raise
178
+ INSTALL_VERSION_ROOT.joinpath(tool_name).parent.mkdir(parents=True, exist_ok=True)
179
+ INSTALL_VERSION_ROOT.joinpath(tool_name).write_text(version, encoding="utf-8")
180
+ console.print(Panel(f"Installed [green]{tool_name}[/green] to {installed_path}\nVersion: {version}", title="✅ Complete", border_style="green"))
@@ -1,7 +1,6 @@
1
- from machineconfig.utils.path_extended import PathExtended
2
- from machineconfig.utils.installer_utils.installer_abc import find_move_delete_linux, find_move_delete_windows
1
+ from machineconfig.utils.path_extended import PathExtended, DECOMPRESS_SUPPORTED_FORMATS
3
2
  from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
4
- from machineconfig.utils.installer_utils.installer_abc import check_tool_exists
3
+ from machineconfig.utils.installer_utils.installer_locator_utils import find_move_delete_linux, find_move_delete_windows, check_tool_exists
5
4
  from machineconfig.utils.schemas.installer.installer_types import InstallerData, get_os_name, get_normalized_arch
6
5
 
7
6
  import platform
@@ -11,6 +10,28 @@ from typing import Optional, Any
11
10
  from urllib.parse import urlparse
12
11
 
13
12
 
13
+
14
+ def install_deb_package(downloaded: PathExtended) -> None:
15
+ from rich import print as rprint
16
+ from rich.panel import Panel
17
+ print(f"📦 Installing .deb package: {downloaded}")
18
+ assert platform.system() == "Linux"
19
+ result = subprocess.run(f"sudo nala install -y {downloaded}", shell=True, capture_output=True, text=True)
20
+ success = result.returncode == 0 and result.stderr == ""
21
+ if not success:
22
+ from rich.console import Group
23
+ desc = "Installing .deb"
24
+ sub_panels = []
25
+ if result.stdout:
26
+ sub_panels.append(Panel(result.stdout, title="STDOUT", style="blue"))
27
+ if result.stderr:
28
+ sub_panels.append(Panel(result.stderr, title="STDERR", style="red"))
29
+ group_content = Group(f"❌ {desc} failed\nReturn code: {result.returncode}", *sub_panels)
30
+ rprint(Panel(group_content, title=desc, style="red"))
31
+ print("🗑️ Cleaning up .deb package...")
32
+ downloaded.delete(sure=True)
33
+
34
+
14
35
  class Installer:
15
36
  def __init__(self, installer_data: InstallerData):
16
37
  self.installer_data: InstallerData = installer_data
@@ -42,10 +63,8 @@ class Installer:
42
63
  result_new = subprocess.run(f"{exe_name} --version", shell=True, capture_output=True, text=True)
43
64
  new_version_cli = result_new.stdout.strip()
44
65
  if old_version_cli == new_version_cli:
45
- # print(f"ℹ️ Same version detected: {old_version_cli}")
46
66
  return f"""📦️ 😑 {exe_name}, same version: {old_version_cli}"""
47
67
  else:
48
- # print(f"🚀 Update successful: {old_version_cli} ➡️ {new_version_cli}")
49
68
  return f"""📦️ 🤩 {exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}"""
50
69
  except Exception as ex:
51
70
  exe_name = self._get_exe_name()
@@ -64,23 +83,24 @@ class Installer:
64
83
  version_to_be_installed: str = "unknown" # Initialize to ensure it's always bound
65
84
  if repo_url == "CMD":
66
85
  if any(pm in installer_arch_os for pm in ["npm ", "pip ", "winget ", "brew ", "curl "]):
86
+ from rich import print as rprint
87
+ from rich.panel import Panel
88
+ from rich.console import Group
67
89
  package_manager = installer_arch_os.split(" ", maxsplit=1)[0]
68
90
  print(f"📦 Using package manager: {installer_arch_os}")
69
91
  desc = package_manager + " installation"
70
92
  version_to_be_installed = package_manager + "Latest"
71
- result = subprocess.run(installer_arch_os, shell=True, capture_output=True, text=False)
72
- # from machineconfig.utils.code import run_shell_script
73
- # result = run_shell_script(installer_arch_os)
74
- success = result.returncode == 0 and result.stderr == "".encode()
93
+ result = subprocess.run(installer_arch_os, shell=True, capture_output=False, text=True)
94
+ success = result.returncode == 0 and result.stderr == ""
75
95
  if not success:
76
- print(f"❌ {desc} failed")
96
+ sub_panels = []
77
97
  if result.stdout:
78
- print(f"STDOUT: {result.stdout}")
98
+ sub_panels.append(Panel(result.stdout, title="STDOUT", style="blue"))
79
99
  if result.stderr:
80
- print(f"STDERR: {result.stderr}")
81
- print(f"Return code: {result.returncode}")
100
+ sub_panels.append(Panel(result.stderr, title="STDERR", style="red"))
101
+ group_content = Group(f" {desc} failed\nReturn code: {result.returncode}", *sub_panels)
102
+ rprint(Panel(group_content, title=desc, style="red"))
82
103
  elif installer_arch_os.endswith((".sh", ".py", ".ps1")):
83
- # search for the script, see which path ends with the script name
84
104
  import machineconfig.jobs.installer as module
85
105
  from pathlib import Path
86
106
  search_root = Path(module.__file__).parent
@@ -106,9 +126,14 @@ class Installer:
106
126
  runpy.run_path(str(installer_path), run_name=None)["main"](self.installer_data, version=version)
107
127
  version_to_be_installed = str(version)
108
128
  elif installer_arch_os.startswith("https://"): # its a url to be downloaded
109
- downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
129
+ # downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
130
+ from machineconfig.scripts.python.helpers_utils.download import download
131
+ downloaded_object = download(installer_arch_os, output_dir=str(INSTALL_TMP_DIR))
132
+ if downloaded_object is None:
133
+ raise ValueError(f"Failed to download from URL: {installer_arch_os}")
110
134
  # object is either a zip containing a binary or a straight out binary.
111
- if downloaded_object.suffix in [".zip", ".tar.gz"]:
135
+ downloaded_object = PathExtended(downloaded_object)
136
+ if downloaded_object.suffix in DECOMPRESS_SUPPORTED_FORMATS:
112
137
  downloaded_object = downloaded_object.decompress()
113
138
  if downloaded_object.suffix in [".exe", ""]: # likely an executable
114
139
  if platform.system() == "Windows":
@@ -131,26 +156,17 @@ class Installer:
131
156
  print(f"🔄 Renaming to correct name: {new_exe_name}")
132
157
  exe.with_name(name=new_exe_name, inplace=True, overwrite=True)
133
158
  version_to_be_installed = "downloaded_binary"
159
+ elif downloaded_object.suffix in [".deb"]:
160
+ install_deb_package(downloaded_object)
161
+ version_to_be_installed = "downloaded_deb"
162
+ else:
163
+ raise ValueError(f"Downloaded file is not an executable: {downloaded_object}")
134
164
  else:
135
165
  raise NotImplementedError(f"CMD installation method not implemented for: {installer_arch_os}")
136
166
  else:
137
167
  assert repo_url.startswith("https://github.com/"), f"repoURL must be a GitHub URL, got {repo_url}"
138
- downloaded, version_to_be_installed = self.download(version=version)
139
- if str(downloaded).endswith(".deb"):
140
- print(f"📦 Installing .deb package: {downloaded}")
141
- assert platform.system() == "Linux"
142
- result = subprocess.run(f"sudo nala install -y {downloaded}", shell=True, capture_output=True, text=True)
143
- success = result.returncode == 0 and result.stderr == ""
144
- if not success:
145
- desc = "Installing .deb"
146
- print(f"❌ {desc} failed")
147
- if result.stdout:
148
- print(f"STDOUT: {result.stdout}")
149
- if result.stderr:
150
- print(f"STDERR: {result.stderr}")
151
- print(f"Return code: {result.returncode}")
152
- print("🗑️ Cleaning up .deb package...")
153
- downloaded.delete(sure=True)
168
+ downloaded, version_to_be_installed = self.binary_download(version=version)
169
+ if str(downloaded).endswith(".deb"): install_deb_package(downloaded)
154
170
  else:
155
171
  if platform.system() == "Windows":
156
172
  exe = find_move_delete_windows(downloaded_file_path=downloaded, exe_name=exe_name, delete=True, rename_to=exe_name.replace(".exe", "") + ".exe")
@@ -173,13 +189,13 @@ class Installer:
173
189
  exe.with_name(name=new_exe_name, inplace=True, overwrite=True)
174
190
  INSTALL_VERSION_ROOT.joinpath(exe_name).parent.mkdir(parents=True, exist_ok=True)
175
191
  INSTALL_VERSION_ROOT.joinpath(exe_name).write_text(version_to_be_installed or "unknown", encoding="utf-8")
176
- def download(self, version: Optional[str]) -> tuple[PathExtended, str]:
192
+ def binary_download(self, version: Optional[str]) -> tuple[PathExtended, str]:
177
193
  exe_name = self._get_exe_name()
178
194
  repo_url = self.installer_data["repoURL"]
179
195
  # app_name = self.installer_data["appName"]
180
196
  download_link: Optional[str] = None
181
197
  version_to_be_installed: Optional[str] = None
182
- if "github" not in repo_url or ".zip" in repo_url or ".tar.gz" in repo_url:
198
+ if "github" not in repo_url or (any(ext in repo_url for ext in DECOMPRESS_SUPPORTED_FORMATS)):
183
199
  # Direct download URL
184
200
  download_link = repo_url
185
201
  version_to_be_installed = "predefined_url"
@@ -202,12 +218,9 @@ class Installer:
202
218
  downloaded = PathExtended(download_link).download(folder=INSTALL_TMP_DIR).decompress()
203
219
  if downloaded.is_dir() and len(downloaded.search("*", r=True)) == 1:
204
220
  only_file_in = next(downloaded.glob("*"))
205
- if only_file_in.is_file() and only_file_in.suffix in [".7z", ".zip", ".tar.gz", ".tar"]: # further decompress
221
+ if only_file_in.is_file() and only_file_in.suffix in DECOMPRESS_SUPPORTED_FORMATS: # further decompress
206
222
  downloaded = only_file_in.decompress()
207
223
  return downloaded, version_to_be_installed
208
-
209
- # --------------------------- Arch / template helpers ---------------------------
210
-
211
224
  @staticmethod
212
225
  def _get_repo_name_from_url(repo_url: str) -> str:
213
226
  """Extract owner/repo from GitHub URL."""
@@ -222,23 +235,16 @@ class Installer:
222
235
  def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
223
236
  """Fetch release data from GitHub API using requests."""
224
237
  import requests
225
-
226
238
  try:
227
- if version and version.lower() != "latest":
228
- # Fetch specific version
239
+ if version and version.lower() != "latest": # Fetch specific version
229
240
  url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
230
- else:
231
- # Fetch latest release
241
+ else: # Fetch latest release
232
242
  url = f"https://api.github.com/repos/{repo_name}/releases/latest"
233
-
234
243
  response = requests.get(url, timeout=30)
235
-
236
244
  if response.status_code != 200:
237
245
  print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
238
246
  return None
239
-
240
247
  response_data = response.json()
241
-
242
248
  # Check if API returned an error
243
249
  if "message" in response_data:
244
250
  if "API rate limit exceeded" in response_data.get("message", ""):
@@ -1,55 +1,16 @@
1
1
  """Devops Devapps Install"""
2
2
 
3
3
  import typer
4
- from rich.console import Console
5
- from rich.panel import Panel
6
- from rich.table import Table
7
- from typing import Optional, Annotated
4
+ from typing import Annotated, Optional
8
5
  from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
6
+ from machineconfig.utils.installer_utils.installer_class import Installer
9
7
 
10
- console = Console()
11
8
 
12
9
 
13
- def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None: # type: ignore
14
- """Handle installer not found with friendly suggestions using fuzzy matching."""
15
- from difflib import get_close_matches
16
- close_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
17
- console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
18
- if close_matches:
19
- console.print("🤔 Did you mean one of these?", style="yellow")
20
- table = Table(show_header=False, box=None, pad_edge=False)
21
- for i, match in enumerate(close_matches, 1):
22
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{match}[/green]")
23
- console.print(table)
24
- else:
25
- console.print("📋 Here are some available options:", style="blue")
26
- # Show first 10 installers as examples
27
- if len(all_names) > 10:
28
- sample_names = all_names[:10]
29
- else:
30
- sample_names = all_names
31
- table = Table(show_header=False, box=None, pad_edge=False)
32
- for i, name in enumerate(sample_names, 1):
33
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{name}[/green]")
34
- console.print(table)
35
- if len(all_names) > 10:
36
- console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
37
-
38
- panel = Panel(f"[bold blue]💡 Use 'ia' to interactively browse all available installers.[/bold blue]\n[bold blue]💡 Use one of the categories: {list(PACKAGE_GROUP2NAMES.keys())}[/bold blue]", title="[yellow]Helpful Tips[/yellow]", border_style="yellow")
39
- console.print(panel)
40
-
41
-
42
- def main_with_parser():
43
- import typer
44
- app = typer.Typer()
45
- app.command()(main)
46
- app()
47
-
48
-
49
- def main(
10
+ def main_installer_cli(
50
11
  which: Annotated[Optional[str], typer.Argument(..., help="Comma-separated list of program/groups names to install (if --group flag is set).")] = None,
51
12
  group: Annotated[bool, typer.Option(..., "--group", "-g", help="Treat 'which' as a group name. A group is bundle of apps.")] = False,
52
- interactive: Annotated[bool, typer.Option(..., "--interactive", "-ia", help="Interactive selection of programs to install.")] = False,
13
+ interactive: Annotated[bool, typer.Option(..., "--interactive", "-i", help="Interactive selection of programs to install.")] = False,
53
14
  ) -> None:
54
15
  if interactive:
55
16
  return install_interactively()
@@ -61,6 +22,10 @@ def main(
61
22
  return install_clis(clis_names=[x.strip() for x in which.split(",") if x.strip() != ""])
62
23
  else:
63
24
  if group:
25
+ from rich.console import Console
26
+ from rich.table import Table
27
+ console = Console()
28
+
64
29
  typer.echo("❌ You must provide a group name when using the --group/-g option.")
65
30
  res = get_group_name_to_repr()
66
31
  console.print("[bold blue]Here are the available groups:[/bold blue]")
@@ -98,20 +63,18 @@ def get_group_name_to_repr() -> dict[str, str]:
98
63
  def install_interactively():
99
64
  from machineconfig.utils.options import choose_from_options
100
65
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
101
- from machineconfig.utils.installer import get_installers
66
+ from machineconfig.utils.installer_utils.installer_runner import get_installers
102
67
  from machineconfig.utils.installer_utils.installer_class import Installer
68
+ from rich.console import Console
69
+ from rich.panel import Panel
70
+ # from rich.table import Table
103
71
  installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
104
- installer_options = []
105
- for x in installers:
106
- installer_options.append(Installer(installer_data=x).get_description())
107
-
72
+ installer_options = [Installer(installer_data=x).get_description() for x in installers]
108
73
  category_display_to_name = get_group_name_to_repr()
109
- options = list(category_display_to_name.keys()) + ["─" * 50] + installer_options
110
- program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", default="📦 termabc", fzf=True)
74
+ options = list(category_display_to_name.keys()) + installer_options
75
+ program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", fzf=True)
111
76
  installation_messages: list[str] = []
112
77
  for _an_idx, a_program_name in enumerate(program_names):
113
- if a_program_name.startswith("─"): # 50 dashes separator
114
- continue
115
78
  if a_program_name.startswith("📦 "):
116
79
  category_name = category_display_to_name.get(a_program_name)
117
80
  if category_name:
@@ -122,59 +85,102 @@ def install_interactively():
122
85
  status_message = Installer(an_installer_data).install_robust(version=None) # finish the task - this returns a status message, not a command
123
86
  installation_messages.append(status_message)
124
87
  if installation_messages:
88
+ console = Console()
89
+
125
90
  panel = Panel("\n".join([f"[blue]• {message}[/blue]" for message in installation_messages]), title="[bold green]📊 Installation Summary[/bold green]", border_style="green", padding=(1, 2))
126
91
  console.print(panel)
127
92
 
128
93
 
129
94
  def install_group(package_group: str):
130
- from machineconfig.utils.installer import get_installers, install_bulk
95
+ from machineconfig.utils.installer_utils.installer_runner import get_installers, install_bulk
131
96
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
97
+ from rich.console import Console
98
+ from rich.panel import Panel
99
+ # from rich.table import Table
132
100
  if package_group in PACKAGE_GROUP2NAMES:
133
101
  panel = Panel(f"[bold yellow]Installing programs from category: [green]{package_group}[/green][/bold yellow]", title="[bold blue]📦 Category Installation[/bold blue]", border_style="blue", padding=(1, 2))
102
+ console = Console()
134
103
  console.print(panel)
135
104
  installers_ = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=[package_group])
136
105
  install_bulk(installers_data=installers_)
137
106
  return
138
- print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
107
+ console = Console()
108
+ console.print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
109
+ def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None: # type: ignore
110
+ """Handle installer not found with friendly suggestions using fuzzy matching."""
111
+ from difflib import get_close_matches
112
+ from rich.console import Console
113
+ from rich.panel import Panel
114
+ from rich.table import Table
115
+ close_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
116
+ console = Console()
117
+
118
+ console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
119
+ if close_matches:
120
+ console.print("🤔 Did you mean one of these?", style="yellow")
121
+ table = Table(show_header=False, box=None, pad_edge=False)
122
+ for i, match in enumerate(close_matches, 1):
123
+ table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{match}[/green]")
124
+ console.print(table)
125
+ else:
126
+ console.print("📋 Here are some available options:", style="blue")
127
+ # Show first 10 installers as examples
128
+ if len(all_names) > 10:
129
+ sample_names = all_names[:10]
130
+ else:
131
+ sample_names = all_names
132
+ table = Table(show_header=False, box=None, pad_edge=False)
133
+ for i, name in enumerate(sample_names, 1):
134
+ table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{name}[/green]")
135
+ console.print(table)
136
+ if len(all_names) > 10:
137
+ console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
138
+
139
+ panel = Panel(f"[bold blue]💡 Use 'ia' to interactively browse all available installers.[/bold blue]\n[bold blue]💡 Use one of the categories: {list(PACKAGE_GROUP2NAMES.keys())}[/bold blue]", title="[yellow]Helpful Tips[/yellow]", border_style="yellow")
140
+ console.print(panel)
141
+
139
142
  def install_clis(clis_names: list[str]):
140
143
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
141
- from machineconfig.utils.installer import get_installers
144
+ from machineconfig.utils.installer_utils.installer_runner import get_installers
142
145
  from machineconfig.utils.installer_utils.installer_class import Installer
146
+ from rich.console import Console
147
+ all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
143
148
  total_messages: list[str] = []
144
- for a_which in clis_names:
145
- all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
149
+ for a_cli_name in clis_names:
150
+ if "github.com" in a_cli_name.lower():
151
+ from machineconfig.utils.installer_utils.install_from_url import install_from_github_url
152
+ install_from_github_url(github_url=a_cli_name)
153
+ continue
146
154
  selected_installer = None
147
155
  for installer in all_installers:
148
156
  app_name = installer["appName"]
149
- if app_name.lower() == a_which.lower():
157
+ if app_name.lower() == a_cli_name.lower():
150
158
  selected_installer = installer
151
159
  break
152
160
  if selected_installer is None:
153
- _handle_installer_not_found(a_which, all_names=[inst["appName"] for inst in all_installers])
161
+ _handle_installer_not_found(a_cli_name, all_names=[inst["appName"] for inst in all_installers])
154
162
  return None
155
163
  message = Installer(selected_installer).install_robust(version=None) # finish the task
156
164
  total_messages.append(message)
157
165
  if total_messages:
166
+ console = Console()
158
167
  console.print("\n[bold green]📊 Installation Results:[/bold green]")
159
168
  for a_message in total_messages:
160
169
  console.print(f"[blue]• {a_message}[/blue]")
161
170
  return None
162
-
163
-
164
171
  def install_if_missing(which: str):
165
- from machineconfig.utils.installer_utils.installer_abc import check_tool_exists
172
+ from machineconfig.utils.installer_utils.installer_locator_utils import check_tool_exists
166
173
  exists = check_tool_exists(which)
167
174
  if exists:
168
175
  print(f"✅ {which} is already installed.")
169
176
  return
170
177
  print(f"⏳ {which} not found. Installing...")
171
- from machineconfig.utils.installer_utils.installer import main
172
- main(which=which, interactive=False)
178
+ from machineconfig.utils.installer_utils.installer_cli import main_installer_cli
179
+ main_installer_cli(which=which, interactive=False)
173
180
 
174
181
 
175
182
  if __name__ == "__main__":
176
183
  from machineconfig.utils.schemas.installer.installer_types import InstallerData
177
184
  from machineconfig.utils.installer_utils.installer_class import Installer
178
-
179
185
  _ = InstallerData, Installer
180
186
  pass
@@ -1,6 +1,6 @@
1
1
  """package manager"""
2
2
 
3
- from machineconfig.utils.installer_utils.installer_abc import check_if_installed_already
3
+ from machineconfig.utils.installer_utils.installer_locator_utils import check_if_installed_already
4
4
  from machineconfig.utils.installer_utils.installer_class import Installer
5
5
  from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles, get_normalized_arch, get_os_name, OPERATING_SYSTEMS, CPU_ARCHITECTURES
6
6
  from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
@@ -132,30 +132,6 @@ def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs:
132
132
  print("✅ Version cache cleared")
133
133
  if safe:
134
134
  pass
135
- # print("⚠️ Safe installation mode activated...")
136
- # from machineconfig.jobs.python.check_installations import APP_SUMMARY_PATH
137
- # if platform.system().lower() == "windows":
138
- # print("🪟 Moving applications to Windows Apps folder...")
139
- # # PathExtended.get_env().WindowsPaths().WindowsApps)
140
- # folder = PathExtended.home().joinpath("AppData/Local/Microsoft/WindowsApps")
141
- # apps_dir.search("*").apply(lambda app: app.move(folder=folder))
142
- # elif platform.system().lower() in ["linux", "darwin"]:
143
- # system_name = "Linux" if platform.system().lower() == "linux" else "macOS"
144
- # print(f"🐧 Moving applications to {system_name} bin folder...")
145
- # if platform.system().lower() == "linux":
146
- # install_path = LINUX_INSTALL_PATH
147
- # else: # Darwin/macOS
148
- # install_path = "/usr/local/bin"
149
- # Terminal().run(f"sudo mv {apps_dir.as_posix()}/* {install_path}/").capture().print_if_unsuccessful(desc=f"MOVING executable to {install_path}", strict_err=True, strict_returncode=True)
150
- # else:
151
- # error_msg = f"❌ ERROR: System {platform.system()} not supported"
152
- # print(error_msg)
153
- # raise NotImplementedError(error_msg)
154
-
155
- # apps_dir.delete(sure=True)
156
- # print(f"✅ Safe installation completed\n{'='*80}")
157
- # return None
158
-
159
135
  print(f"🚀 Starting installation of {len(installers_data)} packages...")
160
136
  print("📦 INSTALLING FIRST PACKAGE 📦")
161
137
  Installer(installers_data[0]).install(version=None)
@@ -164,7 +164,7 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
164
164
  else:
165
165
  # Files are different, use on_conflict strategy
166
166
  import subprocess
167
- command = f"""delta --side-by-side "{config_file_default_path}" "{self_managed_config_file_path}" """
167
+ command = f"""delta --paging never --side-by-side "{config_file_default_path}" "{self_managed_config_file_path}" """
168
168
  try:
169
169
  console.print(Panel(f"🆘 CONFLICT DETECTED | Showing diff between {config_file_default_path} and {self_managed_config_file_path}", title="Conflict Detected", expand=False))
170
170
  subprocess.run(command, shell=True, check=True)
@@ -293,7 +293,7 @@ def copy_map(config_file_default_path: PathExtended, self_managed_config_file_pa
293
293
  else:
294
294
  # Files are different, use on_conflict strategy
295
295
  import subprocess
296
- command = f"""delta --side-by-side "{config_file_default_path}" "{self_managed_config_file_path}" """
296
+ command = f"""delta --paging never --side-by-side "{config_file_default_path}" "{self_managed_config_file_path}" """
297
297
  try:
298
298
  console.print(Panel(f"🆘 CONFLICT DETECTED | Showing diff between {config_file_default_path} and {self_managed_config_file_path}", title="Conflict Detected", expand=False))
299
299
  subprocess.run(command, shell=True, check=True)