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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (104) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +4 -2
  2. machineconfig/jobs/installer/custom/yazi.py +120 -0
  3. machineconfig/jobs/installer/custom_dev/nerdfont.py +1 -1
  4. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +26 -12
  5. machineconfig/jobs/installer/custom_dev/sysabc.py +26 -5
  6. machineconfig/jobs/installer/installer_data.json +232 -96
  7. machineconfig/jobs/installer/powershell_scripts/install_fonts.ps1 +129 -34
  8. machineconfig/profile/create_helper.py +0 -12
  9. machineconfig/profile/create_links_export.py +2 -2
  10. machineconfig/profile/mapper.toml +2 -2
  11. machineconfig/scripts/__init__.py +0 -4
  12. machineconfig/scripts/python/agents.py +22 -17
  13. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +4 -0
  14. machineconfig/scripts/python/croshell.py +22 -17
  15. machineconfig/scripts/python/devops.py +1 -1
  16. machineconfig/scripts/python/devops_navigator.py +0 -4
  17. machineconfig/scripts/python/env_manager/env_manager_tui.py +204 -0
  18. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  19. machineconfig/scripts/python/fire_jobs.py +13 -13
  20. machineconfig/scripts/python/ftpx.py +36 -12
  21. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  22. machineconfig/scripts/python/helpers/qr_code.py +166 -0
  23. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  24. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  25. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  26. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  27. machineconfig/scripts/python/helpers_cloud/cloud_mount.py +19 -17
  28. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  29. machineconfig/scripts/python/helpers_croshell/start_slidev.py +6 -7
  30. machineconfig/scripts/python/helpers_devops/cli_config.py +10 -0
  31. machineconfig/scripts/python/helpers_devops/cli_nw.py +90 -10
  32. machineconfig/scripts/python/helpers_devops/cli_self.py +8 -7
  33. machineconfig/scripts/python/helpers_devops/cli_share_file.py +7 -7
  34. machineconfig/scripts/python/helpers_devops/cli_share_server.py +12 -11
  35. machineconfig/scripts/python/helpers_devops/cli_terminal.py +8 -10
  36. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -1
  37. machineconfig/scripts/python/helpers_devops/devops_status.py +7 -19
  38. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +20 -9
  39. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +2 -2
  40. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfg.ps1 +58 -1
  41. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  42. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +5 -3
  43. machineconfig/scripts/python/helpers_repos/count_lines.py +40 -11
  44. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  45. machineconfig/scripts/python/helpers_utils/download.py +4 -3
  46. machineconfig/scripts/python/helpers_utils/path.py +87 -34
  47. machineconfig/scripts/python/interactive.py +1 -1
  48. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  49. machineconfig/scripts/python/msearch.py +55 -6
  50. machineconfig/scripts/python/nw/address.py +132 -0
  51. machineconfig/scripts/python/nw/devops_add_ssh_key.py +8 -5
  52. machineconfig/scripts/python/terminal.py +2 -2
  53. machineconfig/scripts/python/utils.py +12 -11
  54. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  55. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  56. machineconfig/settings/shells/nushell/config.nu +2 -2
  57. machineconfig/settings/shells/nushell/env.nu +45 -6
  58. machineconfig/settings/shells/nushell/init.nu +282 -95
  59. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  60. machineconfig/settings/yazi/init.lua +4 -0
  61. machineconfig/settings/yazi/keymap_linux.toml +11 -4
  62. machineconfig/settings/yazi/theme.toml +4 -0
  63. machineconfig/settings/yazi/yazi_linux.toml +84 -0
  64. machineconfig/settings/yazi/yazi_windows.toml +58 -0
  65. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  66. machineconfig/setup_windows/uv.ps1 +8 -1
  67. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  68. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +3 -2
  69. machineconfig/utils/accessories.py +7 -4
  70. machineconfig/utils/code.py +4 -2
  71. machineconfig/utils/installer_utils/github_release_bulk.py +104 -62
  72. machineconfig/utils/installer_utils/install_from_url.py +200 -0
  73. machineconfig/utils/installer_utils/installer_class.py +25 -74
  74. machineconfig/utils/installer_utils/installer_cli.py +40 -50
  75. machineconfig/utils/installer_utils/installer_helper.py +100 -0
  76. machineconfig/utils/installer_utils/installer_runner.py +5 -8
  77. machineconfig/utils/links.py +2 -2
  78. machineconfig/utils/meta.py +2 -2
  79. machineconfig/utils/options.py +3 -3
  80. machineconfig/utils/path_extended.py +1 -1
  81. machineconfig/utils/path_helper.py +0 -1
  82. machineconfig/utils/ssh.py +143 -409
  83. machineconfig/utils/ssh_utils/abc.py +8 -0
  84. machineconfig/utils/ssh_utils/copy_from_here.py +110 -0
  85. machineconfig/utils/ssh_utils/copy_to_here.py +302 -0
  86. machineconfig/utils/ssh_utils/utils.py +141 -0
  87. machineconfig/utils/ssh_utils/wsl.py +210 -0
  88. machineconfig/utils/upgrade_packages.py +2 -1
  89. machineconfig/utils/ve.py +11 -4
  90. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/METADATA +2 -2
  91. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/RECORD +96 -89
  92. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/entry_points.txt +2 -2
  93. machineconfig/scripts/python/explore.py +0 -49
  94. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfag +0 -17
  95. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfrga +0 -21
  96. machineconfig/scripts/python/helpers_msearch/scripts_linux/skrg +0 -4
  97. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfb.ps1 +0 -3
  98. machineconfig/scripts/python/helpers_msearch/scripts_windows/fzfrga.bat +0 -20
  99. machineconfig/settings/yazi/yazi.toml +0 -17
  100. machineconfig/setup_linux/others/cli_installation.sh +0 -137
  101. /machineconfig/{settings/shells/pwsh/profile.ps1 → scripts/python/helpers_fire_command/f.py} +0 -0
  102. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  103. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/WHEEL +0 -0
  104. {machineconfig-7.64.dist-info → machineconfig-7.83.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,16 @@
1
+ from machineconfig.utils.installer_utils.installer_helper import install_deb_package
1
2
  from machineconfig.utils.path_extended import PathExtended, DECOMPRESS_SUPPORTED_FORMATS
2
3
  from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT
3
4
  from machineconfig.utils.installer_utils.installer_locator_utils import find_move_delete_linux, find_move_delete_windows, check_tool_exists
4
5
  from machineconfig.utils.schemas.installer.installer_types import InstallerData, get_os_name, get_normalized_arch
6
+ from machineconfig.utils.installer_utils.github_release_bulk import (
7
+ get_repo_name_from_url,
8
+ get_release_info,
9
+ )
5
10
 
6
11
  import platform
7
12
  import subprocess
8
- import json
9
- from typing import Optional, Any
10
- from urllib.parse import urlparse
11
-
12
-
13
-
14
- def install_deb_package(downloaded: PathExtended) -> None:
15
- print(f"📦 Installing .deb package: {downloaded}")
16
- assert platform.system() == "Linux"
17
- result = subprocess.run(f"sudo nala install -y {downloaded}", shell=True, capture_output=True, text=True)
18
- success = result.returncode == 0 and result.stderr == ""
19
- if not success:
20
- desc = "Installing .deb"
21
- print(f"❌ {desc} failed")
22
- if result.stdout:
23
- print(f"STDOUT: {result.stdout}")
24
- if result.stderr:
25
- print(f"STDERR: {result.stderr}")
26
- print(f"Return code: {result.returncode}")
27
- print("🗑️ Cleaning up .deb package...")
28
- downloaded.delete(sure=True)
13
+ from typing import Optional
29
14
 
30
15
 
31
16
  class Installer:
@@ -79,19 +64,23 @@ class Installer:
79
64
  version_to_be_installed: str = "unknown" # Initialize to ensure it's always bound
80
65
  if repo_url == "CMD":
81
66
  if any(pm in installer_arch_os for pm in ["npm ", "pip ", "winget ", "brew ", "curl "]):
67
+ from rich import print as rprint
68
+ from rich.panel import Panel
69
+ from rich.console import Group
82
70
  package_manager = installer_arch_os.split(" ", maxsplit=1)[0]
83
71
  print(f"📦 Using package manager: {installer_arch_os}")
84
72
  desc = package_manager + " installation"
85
73
  version_to_be_installed = package_manager + "Latest"
86
- result = subprocess.run(installer_arch_os, shell=True, capture_output=True, text=False)
87
- success = result.returncode == 0 and result.stderr == "".encode()
74
+ result = subprocess.run(installer_arch_os, shell=True, capture_output=False, text=True)
75
+ success = result.returncode == 0 and result.stderr == ""
88
76
  if not success:
89
- print(f"❌ {desc} failed")
77
+ sub_panels = []
90
78
  if result.stdout:
91
- print(f"STDOUT: {result.stdout}")
79
+ sub_panels.append(Panel(result.stdout, title="STDOUT", style="blue"))
92
80
  if result.stderr:
93
- print(f"STDERR: {result.stderr}")
94
- print(f"Return code: {result.returncode}")
81
+ sub_panels.append(Panel(result.stderr, title="STDERR", style="red"))
82
+ group_content = Group(f" {desc} failed\nReturn code: {result.returncode}", *sub_panels)
83
+ rprint(Panel(group_content, title=desc, style="red"))
95
84
  elif installer_arch_os.endswith((".sh", ".py", ".ps1")):
96
85
  import machineconfig.jobs.installer as module
97
86
  from pathlib import Path
@@ -117,7 +106,7 @@ class Installer:
117
106
  import runpy
118
107
  runpy.run_path(str(installer_path), run_name=None)["main"](self.installer_data, version=version)
119
108
  version_to_be_installed = str(version)
120
- elif installer_arch_os.startswith("https://"): # its a url to be downloaded
109
+ elif installer_arch_os.startswith("https://") or installer_arch_os.startswith("http://"):
121
110
  # downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
122
111
  from machineconfig.scripts.python.helpers_utils.download import download
123
112
  downloaded_object = download(installer_arch_os, output_dir=str(INSTALL_TMP_DIR))
@@ -213,44 +202,6 @@ class Installer:
213
202
  if only_file_in.is_file() and only_file_in.suffix in DECOMPRESS_SUPPORTED_FORMATS: # further decompress
214
203
  downloaded = only_file_in.decompress()
215
204
  return downloaded, version_to_be_installed
216
- @staticmethod
217
- def _get_repo_name_from_url(repo_url: str) -> str:
218
- """Extract owner/repo from GitHub URL."""
219
- try:
220
- parsed = urlparse(repo_url)
221
- path_parts = parsed.path.strip("/").split("/")
222
- return f"{path_parts[0]}/{path_parts[1]}"
223
- except (IndexError, AttributeError):
224
- return ""
225
-
226
- @staticmethod
227
- def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
228
- """Fetch release data from GitHub API using requests."""
229
- import requests
230
- try:
231
- if version and version.lower() != "latest": # Fetch specific version
232
- url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
233
- else: # Fetch latest release
234
- url = f"https://api.github.com/repos/{repo_name}/releases/latest"
235
- response = requests.get(url, timeout=30)
236
- if response.status_code != 200:
237
- print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
238
- return None
239
- response_data = response.json()
240
- # Check if API returned an error
241
- if "message" in response_data:
242
- if "API rate limit exceeded" in response_data.get("message", ""):
243
- print(f"🚫 Rate limit exceeded for {repo_name}")
244
- return None
245
- elif "Not Found" in response_data.get("message", ""):
246
- print(f"🔍 No releases found for {repo_name}")
247
- return None
248
-
249
- return response_data
250
-
251
- except (requests.RequestException, requests.Timeout, json.JSONDecodeError) as e:
252
- print(f"❌ Error fetching {repo_name}: {e}")
253
- return None
254
205
 
255
206
  def get_github_release(self, repo_url: str, version: Optional[str]) -> tuple[Optional[str], Optional[str]]:
256
207
  """
@@ -262,20 +213,20 @@ class Installer:
262
213
  filename_pattern = self.installer_data["fileNamePattern"][arch][os_name]
263
214
  if filename_pattern is None:
264
215
  raise ValueError(f"No fileNamePattern for {self._get_exe_name()} on {os_name} {arch}")
265
- repo_name = self._get_repo_name_from_url(repo_url)
266
- if not repo_name:
216
+ repo_info = get_repo_name_from_url(repo_url)
217
+ if not repo_info:
267
218
  print(f"❌ Invalid repository URL: {repo_url}")
268
219
  return None, None
269
- release_data = self._fetch_github_release_data(repo_name, version)
270
- if not release_data:
220
+ username, repository = repo_info
221
+ release_info = get_release_info(username, repository, version)
222
+ if not release_info:
271
223
  return None, None
272
- # print(release_data)
273
- actual_version = release_data.get("tag_name", "unknown")
224
+ actual_version = release_info.get("tag_name", "unknown") or "unknown"
274
225
  filename = filename_pattern.format(version=actual_version)
275
226
 
276
227
  available_filenames: list[str] = []
277
- for asset in release_data.get("assets", []):
278
- an_dl = asset.get("browser_download_url", "NA")
228
+ for asset in release_info["assets"]:
229
+ an_dl = asset["browser_download_url"]
279
230
  available_filenames.append(an_dl.split("/")[-1])
280
231
  if filename not in available_filenames:
281
232
  candidates = [
@@ -1,11 +1,27 @@
1
- """Devops Devapps Install"""
1
+ """Devops Devapps Install
2
2
 
3
+
4
+ sudo apt update && sudo apt install -y \
5
+ git gcc g++ clang \
6
+ yasm nasm pkg-config \
7
+ meson ninja-build \
8
+ autoconf automake libtool \
9
+ libx11-dev libxext-dev libxrandr-dev libxrender-dev libxss-dev \
10
+ libvdpau-dev libgl1-mesa-dev libegl1-mesa-dev libxv-dev \
11
+ libasound2-dev libpulse-dev \
12
+ libfribidi-dev libfreetype-dev libfontconfig1-dev libharfbuzz-dev \
13
+ libjpeg-dev libssl-dev zlib1g-dev python3-pip
14
+
15
+
16
+ """
17
+
18
+ from machineconfig.utils.installer_utils.installer_helper import get_group_name_to_repr
3
19
  import typer
4
- from typing import Optional, Annotated
5
- from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
20
+ from typing import Annotated, Optional
6
21
 
7
22
 
8
- def main(
23
+
24
+ def main_installer_cli(
9
25
  which: Annotated[Optional[str], typer.Argument(..., help="Comma-separated list of program/groups names to install (if --group flag is set).")] = None,
10
26
  group: Annotated[bool, typer.Option(..., "--group", "-g", help="Treat 'which' as a group name. A group is bundle of apps.")] = False,
11
27
  interactive: Annotated[bool, typer.Option(..., "--interactive", "-i", help="Interactive selection of programs to install.")] = False,
@@ -25,6 +41,7 @@ def main(
25
41
  console = Console()
26
42
 
27
43
  typer.echo("❌ You must provide a group name when using the --group/-g option.")
44
+ from machineconfig.utils.installer_utils.installer_helper import get_group_name_to_repr
28
45
  res = get_group_name_to_repr()
29
46
  console.print("[bold blue]Here are the available groups:[/bold blue]")
30
47
  table = Table(show_header=True, header_style="bold magenta")
@@ -49,13 +66,7 @@ def main(
49
66
  raise typer.Exit(1)
50
67
 
51
68
 
52
- def get_group_name_to_repr() -> dict[str, str]:
53
- # Build category options and maintain a mapping from display text to actual category name
54
- category_display_to_name: dict[str, str] = {}
55
- for group_name, group_values in PACKAGE_GROUP2NAMES.items():
56
- display = f"📦 {group_name:<20}" + " -- " + f"{'|'.join(group_values):<60}"
57
- category_display_to_name[display] = group_name
58
- return category_display_to_name
69
+
59
70
 
60
71
 
61
72
  def install_interactively():
@@ -95,6 +106,7 @@ def install_group(package_group: str):
95
106
  from rich.console import Console
96
107
  from rich.panel import Panel
97
108
  # from rich.table import Table
109
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
98
110
  if package_group in PACKAGE_GROUP2NAMES:
99
111
  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))
100
112
  console = Console()
@@ -104,55 +116,34 @@ def install_group(package_group: str):
104
116
  return
105
117
  console = Console()
106
118
  console.print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
107
- def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None: # type: ignore
108
- """Handle installer not found with friendly suggestions using fuzzy matching."""
109
- from difflib import get_close_matches
110
- from rich.console import Console
111
- from rich.panel import Panel
112
- from rich.table import Table
113
- close_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
114
- console = Console()
115
-
116
- console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
117
- if close_matches:
118
- console.print("🤔 Did you mean one of these?", style="yellow")
119
- table = Table(show_header=False, box=None, pad_edge=False)
120
- for i, match in enumerate(close_matches, 1):
121
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{match}[/green]")
122
- console.print(table)
123
- else:
124
- console.print("📋 Here are some available options:", style="blue")
125
- # Show first 10 installers as examples
126
- if len(all_names) > 10:
127
- sample_names = all_names[:10]
128
- else:
129
- sample_names = all_names
130
- table = Table(show_header=False, box=None, pad_edge=False)
131
- for i, name in enumerate(sample_names, 1):
132
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{name}[/green]")
133
- console.print(table)
134
- if len(all_names) > 10:
135
- console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
136
119
 
137
- 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")
138
- console.print(panel)
139
120
 
140
121
  def install_clis(clis_names: list[str]):
141
122
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
142
123
  from machineconfig.utils.installer_utils.installer_runner import get_installers
143
124
  from machineconfig.utils.installer_utils.installer_class import Installer
144
125
  from rich.console import Console
126
+ all_installers_data = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
145
127
  total_messages: list[str] = []
146
- for a_which in clis_names:
147
- all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
128
+ for a_cli_name in clis_names:
129
+ if "github.com" in a_cli_name.lower():
130
+ from machineconfig.utils.installer_utils.install_from_url import install_from_github_url
131
+ install_from_github_url(github_url=a_cli_name)
132
+ continue
133
+ elif a_cli_name.startswith("https://") or a_cli_name.startswith("http://"):
134
+ print(f"⏳ Installing from binary URL: {a_cli_name} ...")
135
+ from machineconfig.utils.installer_utils.install_from_url import install_from_binary_url
136
+ install_from_binary_url(a_cli_name)
137
+ continue
148
138
  selected_installer = None
149
- for installer in all_installers:
139
+ for installer in all_installers_data:
150
140
  app_name = installer["appName"]
151
- if app_name.lower() == a_which.lower():
141
+ if app_name.lower() == a_cli_name.lower():
152
142
  selected_installer = installer
153
143
  break
154
144
  if selected_installer is None:
155
- _handle_installer_not_found(a_which, all_names=[inst["appName"] for inst in all_installers])
145
+ from machineconfig.utils.installer_utils.installer_helper import handle_installer_not_found
146
+ handle_installer_not_found(a_cli_name, all_installers_data)
156
147
  return None
157
148
  message = Installer(selected_installer).install_robust(version=None) # finish the task
158
149
  total_messages.append(message)
@@ -169,13 +160,12 @@ def install_if_missing(which: str):
169
160
  print(f"✅ {which} is already installed.")
170
161
  return
171
162
  print(f"⏳ {which} not found. Installing...")
172
- from machineconfig.utils.installer_utils.installer_cli import main
173
- main(which=which, interactive=False)
163
+ from machineconfig.utils.installer_utils.installer_cli import main_installer_cli
164
+ main_installer_cli(which=which, interactive=False)
174
165
 
175
166
 
176
167
  if __name__ == "__main__":
177
168
  from machineconfig.utils.schemas.installer.installer_types import InstallerData
178
169
  from machineconfig.utils.installer_utils.installer_class import Installer
179
-
180
170
  _ = InstallerData, Installer
181
171
  pass
@@ -0,0 +1,100 @@
1
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
2
+ from machineconfig.utils.schemas.installer.installer_types import InstallerData
3
+ from pathlib import Path
4
+
5
+
6
+ def get_group_name_to_repr() -> dict[str, str]:
7
+ # Build category options and maintain a mapping from display text to actual category name
8
+ category_display_to_name: dict[str, str] = {}
9
+ for group_name, group_values in PACKAGE_GROUP2NAMES.items():
10
+ display = f"📦 {group_name:<20}" + " -- " + f"{'|'.join(group_values):<60}"
11
+ category_display_to_name[display] = group_name
12
+ return category_display_to_name
13
+
14
+
15
+ def handle_installer_not_found(search_term: str, app_apps: list[InstallerData]) -> None: # type: ignore
16
+ """Handle installer not found with friendly suggestions using fuzzy matching."""
17
+ from difflib import get_close_matches
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+ from rich.table import Table
21
+ all_names = sorted([inst["appName"] for inst in app_apps])
22
+ name_to_doc = {inst["appName"]: inst["doc"] for inst in app_apps}
23
+ all_descriptions = {f"{inst['appName']}: {inst['doc']}": inst["appName"] for inst in app_apps}
24
+
25
+ close_name_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
26
+ close_description_matches = get_close_matches(search_term, list(all_descriptions.keys()), n=5, cutoff=0.4)
27
+
28
+ search_lower = search_term.lower()
29
+ substring_matches = [
30
+ inst["appName"]
31
+ for inst in app_apps
32
+ if search_lower in inst["appName"].lower() or search_lower in inst["doc"].lower()
33
+ ]
34
+
35
+ ordered_matches: list[str] = list(
36
+ dict.fromkeys(
37
+ close_name_matches
38
+ + [all_descriptions[desc] for desc in close_description_matches]
39
+ + substring_matches
40
+ )
41
+ )
42
+ top_matches = ordered_matches[:10]
43
+ console = Console()
44
+
45
+ console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
46
+ if top_matches:
47
+ console.print("🤔 Did you mean one of these?", style="yellow")
48
+ table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
49
+ table.add_column("#", justify="right", width=3)
50
+ table.add_column("Installer", style="green")
51
+ table.add_column("Description", style="dim", overflow="fold")
52
+ for i, match in enumerate(top_matches, 1):
53
+ table.add_row(f"[cyan]{i}[/cyan]", match, name_to_doc.get(match, ""))
54
+ console.print(table)
55
+ else:
56
+ console.print("📋 Here are some available options:", style="blue")
57
+ # Show first 10 installers as examples
58
+ if len(all_names) > 10:
59
+ sample_names = all_names[:10]
60
+ else:
61
+ sample_names = all_names
62
+ table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
63
+ table.add_column("#", justify="right", width=3)
64
+ table.add_column("Installer", style="green")
65
+ table.add_column("Description", style="dim", overflow="fold")
66
+ for i, name in enumerate(sample_names, 1):
67
+ table.add_row(f"[cyan]{i}[/cyan]", name, name_to_doc.get(name, ""))
68
+ console.print(table)
69
+ if len(all_names) > 10:
70
+ console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
71
+
72
+ 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")
73
+ console.print(panel)
74
+
75
+
76
+ def install_deb_package(downloaded: Path) -> None:
77
+ from rich import print as rprint
78
+ from rich.panel import Panel
79
+ print(f"📦 Installing .deb package: {downloaded}")
80
+ import platform
81
+ import subprocess
82
+ assert platform.system() == "Linux"
83
+ result = subprocess.run(f"sudo nala install -y {downloaded}", shell=True, capture_output=True, text=True)
84
+ success = result.returncode == 0 and result.stderr == ""
85
+ if not success:
86
+ from rich.console import Group
87
+ desc = "Installing .deb"
88
+ sub_panels = []
89
+ if result.stdout:
90
+ sub_panels.append(Panel(result.stdout, title="STDOUT", style="blue"))
91
+ if result.stderr:
92
+ sub_panels.append(Panel(result.stderr, title="STDERR", style="red"))
93
+ group_content = Group(f"❌ {desc} failed\nReturn code: {result.returncode}", *sub_panels)
94
+ rprint(Panel(group_content, title=desc, style="red"))
95
+ print("🗑️ Cleaning up .deb package...")
96
+ if downloaded.is_file():
97
+ downloaded.unlink(missing_ok=True)
98
+ elif downloaded.is_dir():
99
+ import shutil
100
+ shutil.rmtree(downloaded, ignore_errors=True)
@@ -92,7 +92,11 @@ def get_installed_cli_apps():
92
92
 
93
93
 
94
94
  def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: Optional[list[str]]) -> list[InstallerData]:
95
- res_all = get_all_installer_data_files()
95
+ import machineconfig.jobs.installer as module
96
+ from pathlib import Path
97
+ res_raw: InstallerDataFiles = read_json(Path(module.__file__).parent.joinpath("installer_data.json"))
98
+ res_all: list[InstallerData] = res_raw["installers"]
99
+
96
100
  acceptable_apps_names: list[str] | None = None
97
101
  if which_cats is not None:
98
102
  acceptable_apps_names = []
@@ -116,13 +120,6 @@ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: O
116
120
  return all_installers
117
121
 
118
122
 
119
- def get_all_installer_data_files() -> list[InstallerData]:
120
- import machineconfig.jobs.installer as module
121
- from pathlib import Path
122
- res_raw: InstallerDataFiles = read_json(Path(module.__file__).parent.joinpath("installer_data.json"))
123
- res_final: list[InstallerData] = res_raw["installers"]
124
- return res_final
125
-
126
123
 
127
124
  def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs: int = 10, fresh: bool = False):
128
125
  print("🚀 BULK INSTALLATION PROCESS 🚀")
@@ -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)
@@ -29,7 +29,8 @@ except (ImportError, ModuleNotFoundError) as ex:
29
29
  return txt
30
30
 
31
31
 
32
- def lambda_to_python_script(lmb: Callable[[], Any], in_global: bool, import_module: bool) -> str:
32
+ def lambda_to_python_script(lmb: Callable[[], Any],
33
+ in_global: bool, import_module: bool) -> str:
33
34
  """
34
35
  caveats: always use keyword arguments in the lambda call for best results.
35
36
  return statement not allowed in the wrapped function (otherwise it can be put in the global space)
@@ -250,7 +251,6 @@ if __name__ == "__main__":
250
251
  import_code_robust = "<import_code_robust>"
251
252
  res = lambda_to_python_script(
252
253
  lambda: print_code(code=import_code_robust, lexer="python", desc="import as module code"),
253
- # in_global=True, import_module=False
254
254
  in_global=True, import_module=False
255
255
  )
256
256
  print(res)
@@ -14,10 +14,10 @@ from typing import Optional, Union, Iterable, overload, Literal, cast
14
14
 
15
15
 
16
16
  @overload
17
- def choose_from_options[T](msg: str, options: Iterable[T], multi: Literal[False], custom_input: bool = False, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False) -> T: ...
17
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[False], custom_input: bool = False, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False) -> T: ...
18
18
  @overload
19
- def choose_from_options[T](msg: str, options: Iterable[T], multi: Literal[True], custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> list[T]: ...
20
- def choose_from_options[T](msg: str, options: Iterable[T], multi: bool, custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> Union[T, list[T]]:
19
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[True], custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> list[T]: ...
20
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: bool, custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> Union[T, list[T]]:
21
21
  # TODO: replace with https://github.com/tmbo/questionary
22
22
  # # also see https://github.com/charmbracelet/gum
23
23
  options_strings: list[str] = [str(x) for x in options]
@@ -711,7 +711,7 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
711
711
  :return: pathlib.Path pointing to the destination directory where contents were extracted
712
712
  :raises: FileNotFoundError if archive does not exist; py7zr.Bad7zFile or other error if extraction fails
713
713
  """
714
- import py7zr
714
+ import py7zr # type: ignore
715
715
  import tempfile
716
716
  from pathlib import Path
717
717
  archive_path_obj = Path(archive_path)
@@ -1,4 +1,3 @@
1
- # from machineconfig.utils.path_extended import Path
2
1
  from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
3
2
  from rich.console import Console
4
3
  from rich.panel import Panel