machineconfig 7.53__py3-none-any.whl → 7.69__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 (90) 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 +15 -12
  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 +39 -34
  8. machineconfig/jobs/installer/custom_dev/wezterm.py +0 -4
  9. machineconfig/jobs/installer/installer_data.json +103 -35
  10. machineconfig/jobs/installer/package_groups.py +28 -13
  11. machineconfig/scripts/__init__.py +0 -4
  12. machineconfig/scripts/linux/wrap_mcfg +1 -1
  13. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +3 -0
  14. machineconfig/scripts/python/croshell.py +22 -17
  15. machineconfig/scripts/python/devops.py +3 -4
  16. machineconfig/scripts/python/devops_navigator.py +0 -4
  17. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  18. machineconfig/scripts/python/fire_jobs.py +17 -15
  19. machineconfig/scripts/python/ftpx.py +13 -11
  20. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  21. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  22. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  23. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  24. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  25. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  26. machineconfig/scripts/python/helpers_croshell/crosh.py +2 -2
  27. machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +22 -13
  28. machineconfig/scripts/python/helpers_devops/cli_self.py +7 -6
  29. machineconfig/scripts/python/helpers_devops/cli_share_file.py +2 -2
  30. machineconfig/scripts/python/helpers_devops/cli_share_server.py +1 -1
  31. machineconfig/scripts/python/helpers_devops/cli_terminal.py +1 -1
  32. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -73
  33. machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
  34. machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +2 -3
  35. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +3 -4
  36. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  37. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +13 -5
  38. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  39. machineconfig/scripts/python/helpers_repos/entrypoint.py +2 -1
  40. machineconfig/scripts/python/helpers_repos/record.py +2 -1
  41. machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +5 -5
  42. machineconfig/scripts/python/helpers_utils/download.py +152 -0
  43. machineconfig/scripts/python/helpers_utils/path.py +4 -2
  44. machineconfig/scripts/python/interactive.py +11 -14
  45. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  46. machineconfig/scripts/python/msearch.py +21 -2
  47. machineconfig/scripts/python/nw/devops_add_ssh_key.py +21 -5
  48. machineconfig/scripts/python/nw/ssh_debug_linux.py +7 -7
  49. machineconfig/scripts/python/nw/ssh_debug_windows.py +4 -4
  50. machineconfig/scripts/python/nw/wsl_windows_transfer.py +3 -2
  51. machineconfig/scripts/python/sessions.py +35 -20
  52. machineconfig/scripts/python/terminal.py +2 -2
  53. machineconfig/scripts/python/utils.py +12 -10
  54. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  55. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  56. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  57. machineconfig/settings/shells/wezterm/wezterm.lua +2 -0
  58. machineconfig/settings/shells/zsh/init.sh +0 -7
  59. machineconfig/settings/yazi/shell/yazi_cd.ps1 +29 -5
  60. machineconfig/setup_linux/web_shortcuts/interactive.sh +12 -11
  61. machineconfig/setup_windows/uv.ps1 +8 -1
  62. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +12 -11
  63. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +4 -2
  64. machineconfig/utils/accessories.py +7 -4
  65. machineconfig/utils/code.py +6 -4
  66. machineconfig/utils/files/headers.py +2 -2
  67. machineconfig/utils/installer_utils/install_from_url.py +180 -0
  68. machineconfig/utils/installer_utils/installer_class.py +56 -46
  69. machineconfig/utils/installer_utils/{installer.py → installer_cli.py} +71 -65
  70. machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +1 -25
  71. machineconfig/utils/meta.py +28 -15
  72. machineconfig/utils/options.py +4 -4
  73. machineconfig/utils/path_extended.py +40 -19
  74. machineconfig/utils/path_helper.py +33 -31
  75. machineconfig/utils/schemas/layouts/layout_types.py +1 -1
  76. machineconfig/utils/ssh.py +330 -99
  77. machineconfig/utils/ve.py +11 -4
  78. machineconfig-7.69.dist-info/METADATA +124 -0
  79. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/RECORD +85 -83
  80. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/entry_points.txt +2 -2
  81. machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
  82. machineconfig/scripts/python/explore.py +0 -49
  83. machineconfig/scripts/python/nw/add_ssh_key.py +0 -148
  84. machineconfig/settings/lf/linux/exe/fzf_nano.sh +0 -16
  85. machineconfig-7.53.dist-info/METADATA +0 -94
  86. /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
  87. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  88. /machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +0 -0
  89. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/WHEEL +0 -0
  90. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/top_level.txt +0 -0
@@ -7,4 +7,11 @@ if (-not (Test-Path -Path "$HOME\.local\bin\uv.exe")) {
7
7
  Write-Output "uv binary found, updating..."
8
8
  & "$HOME\.local\bin\uv.exe" self update
9
9
  }
10
- & "$HOME\.local\bin\uv.exe" python install 3.14
10
+
11
+ # `C:\Users\aalsaf01\.local\bin` is not on your PATH. To use installed Python executables, run `$env:PATH = "C:\Users\aalsaf01\.local\bin;$env:PATH"` or `uv python update-shell`.
12
+ & "$env:USERPROFILE\.local\bin\uv.exe" python update-shell
13
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
14
+
15
+ # & "$HOME\.local\bin\uv.exe" python install 3.14
16
+ uv python install 3.14
17
+
@@ -3,16 +3,16 @@
3
3
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/uv.ps1").Content
4
4
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/scripts/windows/wrap_mcfg.ps1").Content
5
5
 
6
- function devops { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" devops $args }
7
- function cloud { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" cloud $args }
8
- function agents { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" agents $args }
9
- function sessions { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" sessions $args }
10
- function ftpx { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" ftpx $args }
11
- function fire { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" fire $args }
12
- function croshell { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" croshell $args }
13
- function utils { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" utils $args }
14
- function terminal { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" terminal $args }
15
- function msearch { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.53" msearch $args }
6
+ function devops { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" devops $args }
7
+ function cloud { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" cloud $args }
8
+ function agents { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" agents $args }
9
+ function sessions { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" sessions $args }
10
+ function ftpx { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" ftpx $args }
11
+ function fire { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" fire $args }
12
+ function croshell { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" croshell $args }
13
+ function utils { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" utils $args }
14
+ function terminal { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" terminal $args }
15
+ function msearch { & "$HOME\.local\bin\uvx.exe" --python 3.14 --from "machineconfig>=7.69" msearch $args }
16
16
 
17
17
  function d { wrap_in_shell_script devops @args }
18
18
  function c { wrap_in_shell_script cloud @args }
@@ -25,5 +25,6 @@ function u { wrap_in_shell_script utils @args }
25
25
  function t { wrap_in_shell_script terminal @args }
26
26
  function ms { wrap_in_shell_script msearch @args }
27
27
 
28
- devops self
29
28
  Write-Host "mcfg command aliases are now defined in this PowerShell session."
29
+
30
+ devops self interactive
@@ -1,7 +1,8 @@
1
1
 
2
2
  iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/uv.ps1").Content
3
3
  # iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/scripts/windows/wrap_mcfg.ps1").Content
4
- uv tool install --upgrade --python 3.14 machineconfig both
4
+
5
+ uv tool install --upgrade --python 3.14 machineconfig
5
6
 
6
7
  devops install --group sysabc
7
8
 
@@ -12,4 +13,5 @@ devops config shell
12
13
  devops config shell --which nushell
13
14
 
14
15
  wt # start Windows Terminal to pick up config changes
15
- devops install --group termabc
16
+ devops install --group termabc
17
+ devops install --group gui
@@ -1,4 +1,3 @@
1
- from pathlib import Path
2
1
  from typing import Optional, Any
3
2
 
4
3
  from datetime import datetime, timezone, timedelta
@@ -96,14 +95,18 @@ def human_friendly_dict(d: dict[str, Any]) -> dict[str, Any]:
96
95
  return result
97
96
 
98
97
 
99
- def get_repo_root(path: Path) -> Optional[Path]:
98
+ def get_repo_root(path: "Path") -> Optional["Path"]:
100
99
  from git import Repo, InvalidGitRepositoryError
101
-
102
100
  try:
103
- repo = Repo(str(path), search_parent_directories=True)
101
+ repo = Repo(path, search_parent_directories=True)
104
102
  root = repo.working_tree_dir
105
103
  if root is not None:
104
+ from pathlib import Path
106
105
  return Path(root)
107
106
  except InvalidGitRepositoryError:
108
107
  pass
109
108
  return None
109
+
110
+
111
+ if __name__ == "__main__":
112
+ from pathlib import Path
@@ -38,14 +38,16 @@ def get_uv_command_executing_python_script(python_script: str, uv_with: Optional
38
38
  uv_project_dir_arg = "--project" + f' "{uv_project_dir}"'
39
39
  else:
40
40
  uv_project_dir_arg = ""
41
- print_code_string = lambda_to_python_script(lambda: print_code(code=python_script, lexer="python", desc="Temporary Python Script", subtitle="Executing via shell script"), in_global=True, import_module=False)
41
+ print_code_string = lambda_to_python_script(lambda: print_code(code=python_script, lexer="python", desc="Temporary Python Script", subtitle="Executing via shell script"),
42
+ in_global=True, import_module=False)
42
43
  python_file.write_text(print_code_string + "\n" + python_script, encoding="utf-8")
43
44
  shell_script = f"""uv run {uv_with_arg} {uv_project_dir_arg} {str(python_file)} """
44
45
  return shell_script, python_file
45
46
 
46
47
 
47
48
  def run_lambda_function(lmb: Callable[[], Any], uv_with: Optional[list[str]], uv_project_dir: Optional[str]) -> None:
48
- code = lambda_to_python_script(lmb, in_global=True, import_module=False)
49
+ code = lambda_to_python_script(lmb,
50
+ in_global=True, import_module=False)
49
51
  uv_command, _py_file = get_uv_command_executing_python_script(python_script=code, uv_with=uv_with, uv_project_dir=uv_project_dir)
50
52
  run_shell_script(uv_command)
51
53
  def run_python_script_in_marimo(py_script: str, uv_project_with: Optional[str]):
@@ -132,7 +134,7 @@ def exit_then_run_shell_script(script: str, strict: bool = False):
132
134
  op_program_path = Path.home().joinpath("tmp_results", "tmp_scripts", "manual_run", f"manual_script_{randstr()}{suffix}")
133
135
  op_program_path.parent.mkdir(parents=True, exist_ok=True)
134
136
  op_program_path.write_text(script, encoding="utf-8")
135
- print_code(script, lexer=lexer, desc="script to run manually")
137
+ print_code(code=script, lexer=lexer, desc="script to run manually")
136
138
  console.print("[bold yellow]⚠️ STRICT MODE:[/bold yellow] [cyan]Please run the script manually via your shell by executing:[/cyan]")
137
139
  console.print(f"[green]{str(op_program_path)}[/green]")
138
140
  console.print("[red]❌ OP_PROGRAM_PATH environment variable is not set in strict mode.[/red]")
@@ -145,7 +147,7 @@ def exit_then_run_shell_script(script: str, strict: bool = False):
145
147
  op_program_path.write_text(script, encoding="utf-8")
146
148
  console.print("[cyan]🚀 Handing over to shell script runner via OP_PROGRAM_PATH:[/cyan]")
147
149
  console.print(f"[bold green]{str(op_program_path)}[/bold green]")
148
- print_code(script, lexer="shell", desc="script to run via OP_PROGRAM_PATH")
150
+ print_code(code=script, lexer="shell", desc="script to run via OP_PROGRAM_PATH")
149
151
  else:
150
152
  if op_program_path is not None and exists:
151
153
  console.print(f"[yellow]⚠️ OP_PROGRAM_PATH @ {str(op_program_path)} already exists.[/yellow] [cyan]Falling back to direct execution.[/cyan]")
@@ -25,7 +25,7 @@ def print_header():
25
25
  table.add_row("Virtual Environment", os.getenv('VIRTUAL_ENV', 'None'))
26
26
  table.add_row("Running @", str(Path.cwd()))
27
27
 
28
- from machineconfig.utils.installer import get_machineconfig_version
28
+ from machineconfig.utils.installer_utils.installer_runner import get_machineconfig_version
29
29
 
30
30
  console.print(Panel(table, title=f"[bold blue]✨ 🐊 Machineconfig Shell {get_machineconfig_version()} ✨ Made with 🐍 | Built with ❤️[/bold blue]", border_style="blue"))
31
31
  def print_logo(logo: str):
@@ -44,7 +44,7 @@ def print_logo(logo: str):
44
44
  _default_art = Path(random.choice(glob.glob(str(Path(__file__).parent.joinpath("art", "*")))))
45
45
  print(_default_art.read_text())
46
46
  elif platform.system() in ["Linux", "Darwin"]: # Explicitly handle both Linux and macOS
47
- from machineconfig.utils.installer_utils.installer_abc import is_executable_in_path
47
+ from machineconfig.utils.installer_utils.installer_locator_utils import is_executable_in_path
48
48
  avail_cowsay = is_executable_in_path("cowsay")
49
49
  avail_lolcat = is_executable_in_path("lolcat")
50
50
  avail_boxes = is_executable_in_path("boxes")
@@ -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"
@@ -200,10 +216,11 @@ class Installer:
200
216
  assert download_link is not None, "download_link must be set"
201
217
  assert version_to_be_installed is not None, "version_to_be_installed must be set"
202
218
  downloaded = PathExtended(download_link).download(folder=INSTALL_TMP_DIR).decompress()
219
+ if downloaded.is_dir() and len(downloaded.search("*", r=True)) == 1:
220
+ only_file_in = next(downloaded.glob("*"))
221
+ if only_file_in.is_file() and only_file_in.suffix in DECOMPRESS_SUPPORTED_FORMATS: # further decompress
222
+ downloaded = only_file_in.decompress()
203
223
  return downloaded, version_to_be_installed
204
-
205
- # --------------------------- Arch / template helpers ---------------------------
206
-
207
224
  @staticmethod
208
225
  def _get_repo_name_from_url(repo_url: str) -> str:
209
226
  """Extract owner/repo from GitHub URL."""
@@ -218,23 +235,16 @@ class Installer:
218
235
  def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
219
236
  """Fetch release data from GitHub API using requests."""
220
237
  import requests
221
-
222
238
  try:
223
- if version and version.lower() != "latest":
224
- # Fetch specific version
239
+ if version and version.lower() != "latest": # Fetch specific version
225
240
  url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
226
- else:
227
- # Fetch latest release
241
+ else: # Fetch latest release
228
242
  url = f"https://api.github.com/repos/{repo_name}/releases/latest"
229
-
230
243
  response = requests.get(url, timeout=30)
231
-
232
244
  if response.status_code != 200:
233
245
  print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
234
246
  return None
235
-
236
247
  response_data = response.json()
237
-
238
248
  # Check if API returned an error
239
249
  if "message" in response_data:
240
250
  if "API rate limit exceeded" in response_data.get("message", ""):