machineconfig 7.51__py3-none-any.whl → 7.53__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 (55) hide show
  1. machineconfig/jobs/installer/custom_dev/brave.py +1 -1
  2. machineconfig/jobs/installer/custom_dev/code.py +4 -1
  3. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +0 -9
  4. machineconfig/jobs/installer/custom_dev/sysabc.py +140 -0
  5. machineconfig/jobs/installer/custom_dev/wezterm.py +2 -15
  6. machineconfig/jobs/installer/installer_data.json +689 -9
  7. machineconfig/jobs/installer/linux_scripts/redis.sh +1 -0
  8. machineconfig/jobs/installer/package_groups.py +23 -72
  9. machineconfig/logger.py +0 -1
  10. machineconfig/profile/create_links_export.py +8 -3
  11. machineconfig/profile/mapper.toml +1 -4
  12. machineconfig/scripts/python/croshell.py +20 -43
  13. machineconfig/scripts/python/devops.py +1 -1
  14. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  15. machineconfig/scripts/python/fire_jobs.py +52 -39
  16. machineconfig/scripts/python/helpers_croshell/crosh.py +1 -1
  17. machineconfig/scripts/python/helpers_devops/cli_config.py +3 -19
  18. machineconfig/scripts/python/helpers_devops/cli_self.py +12 -6
  19. machineconfig/scripts/python/helpers_devops/cli_utils.py +1 -80
  20. machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +0 -17
  21. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +1 -1
  22. machineconfig/scripts/python/helpers_repos/clone.py +0 -1
  23. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
  24. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  25. machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +2 -2
  26. machineconfig/scripts/python/helpers_utils/path.py +106 -0
  27. machineconfig/scripts/python/interactive.py +9 -15
  28. machineconfig/scripts/python/sessions.py +2 -2
  29. machineconfig/scripts/python/utils.py +7 -3
  30. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  31. machineconfig/settings/yazi/init.lua +45 -24
  32. machineconfig/setup_linux/__init__.py +0 -1
  33. machineconfig/setup_linux/web_shortcuts/interactive.sh +11 -10
  34. machineconfig/setup_mac/__init__.py +2 -3
  35. machineconfig/setup_windows/__init__.py +0 -3
  36. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +11 -10
  37. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +15 -0
  38. machineconfig/utils/installer.py +11 -27
  39. machineconfig/utils/installer_utils/installer.py +9 -50
  40. machineconfig/utils/installer_utils/installer_abc.py +0 -68
  41. machineconfig/utils/io.py +0 -1
  42. machineconfig/utils/path_helper.py +57 -6
  43. machineconfig/utils/ssh.py +3 -3
  44. {machineconfig-7.51.dist-info → machineconfig-7.53.dist-info}/METADATA +5 -3
  45. {machineconfig-7.51.dist-info → machineconfig-7.53.dist-info}/RECORD +50 -52
  46. machineconfig/jobs/installer/linux_scripts/timescaledb.sh +0 -71
  47. machineconfig/jobs/installer/powershell_scripts/archive_pygraphviz.ps1 +0 -12
  48. machineconfig/setup_linux/apps.sh +0 -66
  49. machineconfig/setup_mac/apps.sh +0 -73
  50. machineconfig/setup_windows/apps.ps1 +0 -62
  51. /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_add_key.ps1 +0 -0
  52. /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_copy-ssh-id.ps1 +0 -0
  53. {machineconfig-7.51.dist-info → machineconfig-7.53.dist-info}/WHEEL +0 -0
  54. {machineconfig-7.51.dist-info → machineconfig-7.53.dist-info}/entry_points.txt +0 -0
  55. {machineconfig-7.51.dist-info → machineconfig-7.53.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  """package manager"""
2
2
 
3
- from machineconfig.utils.installer_utils.installer_abc import check_if_installed_already, parse_apps_installer_linux, parse_apps_installer_windows
3
+ from machineconfig.utils.installer_utils.installer_abc 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
- from machineconfig.jobs.installer.package_groups import PACKAGE_GROUPS, PACKAGE_GROUP2NAMES
6
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
7
7
  from machineconfig.utils.path_extended import PathExtended
8
8
  from machineconfig.utils.source_of_truth import INSTALL_VERSION_ROOT, LINUX_INSTALL_PATH
9
9
  from machineconfig.utils.io import read_json
@@ -18,7 +18,7 @@ from joblib import Parallel, delayed
18
18
  def check_latest():
19
19
  console = Console() # Added console initialization
20
20
  console.print(Panel("🔍 CHECKING FOR LATEST VERSIONS", title="Status", expand=False)) # Replaced print with Panel
21
- installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["ESSENTIAL"])
21
+ installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["termabc"])
22
22
  installers_github = []
23
23
  for inst__ in installers:
24
24
  app_name = inst__["appName"]
@@ -91,7 +91,7 @@ def get_installed_cli_apps():
91
91
  return apps
92
92
 
93
93
 
94
- def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: Optional[list[PACKAGE_GROUPS]]) -> list[InstallerData]:
94
+ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: Optional[list[str]]) -> list[InstallerData]:
95
95
  res_all = get_all_installer_data_files()
96
96
  acceptable_apps_names: list[str] | None = None
97
97
  if which_cats is not None:
@@ -105,8 +105,13 @@ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: O
105
105
  if acceptable_apps_names is not None:
106
106
  if installer_data["appName"] not in acceptable_apps_names:
107
107
  continue
108
- if installer_data["fileNamePattern"][arch][os] is None:
109
- continue
108
+ try:
109
+ if installer_data["fileNamePattern"][arch][os] is None:
110
+ continue
111
+ except KeyError as ke:
112
+ print(f"❌ ERROR: Missing key in installer data: {ke}")
113
+ print(f"Installer data: {installer_data}")
114
+ raise KeyError(f"Missing key in installer data: {ke}")
110
115
  all_installers.append(installer_data)
111
116
  return all_installers
112
117
 
@@ -119,27 +124,6 @@ def get_all_installer_data_files() -> list[InstallerData]:
119
124
  return res_final
120
125
 
121
126
 
122
- def dynamically_extract_installers_system_groups_from_scripts():
123
- res_final: list[InstallerData] = []
124
- from platform import system
125
- if system() == "Windows":
126
- from machineconfig.setup_windows import APPS
127
- options_system = parse_apps_installer_windows(APPS.read_text(encoding="utf-8"))
128
- elif system() == "Linux":
129
- from machineconfig.setup_linux import APPS
130
- options_system = parse_apps_installer_linux(APPS.read_text(encoding="utf-8"))
131
- elif system() == "Darwin":
132
- from machineconfig.setup_mac import APPS
133
- options_system = parse_apps_installer_linux(APPS.read_text(encoding="utf-8"))
134
- else:
135
- raise NotImplementedError(f"❌ System {system()} not supported")
136
- os_name = get_os_name()
137
- for group_name, (docs, script) in options_system.items():
138
- item: InstallerData = {"appName": group_name, "doc": docs, "repoURL": "CMD", "fileNamePattern": {"amd64": {os_name: script}, "arm64": {os_name: script}}}
139
- res_final.append(item)
140
- return res_final
141
-
142
-
143
127
  def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs: int = 10, fresh: bool = False):
144
128
  print("🚀 BULK INSTALLATION PROCESS 🚀")
145
129
  if fresh:
@@ -1,12 +1,11 @@
1
1
  """Devops Devapps Install"""
2
2
 
3
- from machineconfig.utils.installer import dynamically_extract_installers_system_groups_from_scripts
4
3
  import typer
5
4
  from rich.console import Console
6
5
  from rich.panel import Panel
7
6
  from rich.table import Table
8
- from typing import Optional, cast, get_args, Annotated
9
- from machineconfig.jobs.installer.package_groups import PACKAGE_GROUPS, PACKAGE_GROUP2NAMES
7
+ from typing import Optional, Annotated
8
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
10
9
 
11
10
  console = Console()
12
11
 
@@ -36,7 +35,7 @@ def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None:
36
35
  if len(all_names) > 10:
37
36
  console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
38
37
 
39
- panel = Panel(f"[bold blue]💡 Use 'ia' to interactively browse all available installers.[/bold blue]\n[bold blue]💡 Use one of the categories: {list(get_args(PACKAGE_GROUPS))}[/bold blue]", title="[yellow]Helpful Tips[/yellow]", border_style="yellow")
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")
40
39
  console.print(panel)
41
40
 
42
41
 
@@ -63,7 +62,7 @@ def main(
63
62
  else:
64
63
  if group:
65
64
  typer.echo("❌ You must provide a group name when using the --group/-g option.")
66
- res = get_static_groups_combined_with_dynamic_groups_extracted()
65
+ res = get_group_name_to_repr()
67
66
  console.print("[bold blue]Here are the available groups:[/bold blue]")
68
67
  table = Table(show_header=True, header_style="bold magenta")
69
68
  table.add_column("Group", style="cyan", no_wrap=True)
@@ -87,16 +86,12 @@ def main(
87
86
  raise typer.Exit(1)
88
87
 
89
88
 
90
- def get_static_groups_combined_with_dynamic_groups_extracted():
89
+ def get_group_name_to_repr() -> dict[str, str]:
91
90
  # Build category options and maintain a mapping from display text to actual category name
92
91
  category_display_to_name: dict[str, str] = {}
93
92
  for group_name, group_values in PACKAGE_GROUP2NAMES.items():
94
93
  display = f"📦 {group_name:<20}" + " -- " + f"{'|'.join(group_values):<60}"
95
94
  category_display_to_name[display] = group_name
96
- options_system = dynamically_extract_installers_system_groups_from_scripts()
97
- for item in options_system:
98
- display = f"📦 {item['appName']:<20} -- {item['doc']:<60}"
99
- category_display_to_name[display] = item['appName']
100
95
  return category_display_to_name
101
96
 
102
97
 
@@ -110,9 +105,9 @@ def install_interactively():
110
105
  for x in installers:
111
106
  installer_options.append(Installer(installer_data=x).get_description())
112
107
 
113
- category_display_to_name = get_static_groups_combined_with_dynamic_groups_extracted()
108
+ category_display_to_name = get_group_name_to_repr()
114
109
  options = list(category_display_to_name.keys()) + ["─" * 50] + installer_options
115
- program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", default="📦 essentials", fzf=True)
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)
116
111
  installation_messages: list[str] = []
117
112
  for _an_idx, a_program_name in enumerate(program_names):
118
113
  if a_program_name.startswith("─"): # 50 dashes separator
@@ -120,7 +115,7 @@ def install_interactively():
120
115
  if a_program_name.startswith("📦 "):
121
116
  category_name = category_display_to_name.get(a_program_name)
122
117
  if category_name:
123
- install_group(package_group=cast(PACKAGE_GROUPS, category_name))
118
+ install_group(package_group=category_name)
124
119
  else:
125
120
  installer_idx = installer_options.index(a_program_name)
126
121
  an_installer_data = installers[installer_idx]
@@ -140,43 +135,7 @@ def install_group(package_group: str):
140
135
  installers_ = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=[package_group])
141
136
  install_bulk(installers_data=installers_)
142
137
  return
143
- options_system = dynamically_extract_installers_system_groups_from_scripts()
144
- from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
145
- from machineconfig.utils.code import run_shell_script
146
- for an_item in options_system:
147
- if an_item["appName"] == package_group:
148
- 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))
149
- console.print(panel)
150
- program = an_item["fileNamePattern"][get_normalized_arch()][get_os_name()]
151
- if program is not None:
152
- run_shell_script(program)
153
- break
154
- else:
155
- console.print(f"❌ [red]Group '{package_group}' not found.[/red]", style="bold")
156
- _handle_installer_not_found(package_group, all_names=list(PACKAGE_GROUP2NAMES.keys()) + [item['appName'] for item in options_system])
157
-
158
-
159
- def choose_from_system_package_groups(options_system: dict[str, tuple[str, str]]) -> str:
160
- from machineconfig.utils.options import choose_from_options
161
- display_options = []
162
- for group_name, (description, _) in options_system.items():
163
- if description:
164
- display_options.append(f"{group_name:<20} - {description}")
165
- else:
166
- display_options.append(group_name)
167
- program_names = choose_from_options(multi=True, msg="", options=sorted(display_options), header="🚀 CHOOSE DEV APP", fzf=True)
168
- program = ""
169
- for display_name in program_names:
170
- # Extract the actual group name (everything before " - " if present)
171
- group_name = display_name.split(" - ")[0].strip() if " - " in display_name else display_name.strip()
172
- console.print(f"\n[bold cyan]⚙️ Installing: [yellow]{group_name}[/yellow][/bold cyan]", style="bold")
173
- _, sub_program = options_system[group_name] # Extract content from tuple
174
- if sub_program.startswith("#winget"):
175
- sub_program = sub_program[1:]
176
- program += "\n" + sub_program
177
- return program
178
-
179
-
138
+ print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
180
139
  def install_clis(clis_names: list[str]):
181
140
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
182
141
  from machineconfig.utils.installer import get_installers
@@ -191,71 +191,3 @@ def check_if_installed_already(exe_name: str, version: Optional[str], use_cache:
191
191
  return ("⚠️ NotInstalled", "None", version or "unknown")
192
192
 
193
193
 
194
- def parse_apps_installer_linux(txt: str) -> dict[str, tuple[str, str]]:
195
- """Parse Linux shell installation scripts into logical chunks.
196
-
197
- Splits scripts by # --GROUP:<name>:<description> comment signatures into a dictionary
198
- mapping block names to (description, shell script content) tuples.
199
-
200
- Returns:
201
- dict[str, tuple[str, str]]: Dictionary mapping block/section names to (description, installation_script) tuples
202
- """
203
- chunks = txt.split('# --GROUP:')
204
- res: dict[str, tuple[str, str]] = {}
205
-
206
- for chunk in chunks[1:]: # Skip first empty chunk before first group
207
- lines = chunk.split('\n')
208
- # First line contains the group name and description in format "NAME:DESCRIPTION"
209
- group_line = lines[0].strip()
210
-
211
- # Extract group name and description
212
- if ':' in group_line:
213
- parts = group_line.split(':', 1) # Split only on first colon
214
- group_name = parts[0].strip()
215
- group_description = parts[1].strip() if len(parts) > 1 else ""
216
- else:
217
- group_name = group_line
218
- group_description = ""
219
-
220
- # Rest is the content
221
- content = '\n'.join(lines[1:]).strip()
222
-
223
- if group_name and content:
224
- res[group_name] = (group_description, content)
225
-
226
- return res
227
-
228
-
229
- def parse_apps_installer_windows(txt: str) -> dict[str, tuple[str, str]]:
230
- """Parse Windows PowerShell installation scripts into logical chunks.
231
-
232
- Splits scripts by # --GROUP:<name>:<description> comment signatures into a dictionary
233
- mapping block names to (description, PowerShell script content) tuples.
234
-
235
- Returns:
236
- dict[str, tuple[str, str]]: Dictionary mapping block/section names to (description, installation_script) tuples
237
- """
238
- chunks = txt.split('# --GROUP:')
239
- res: dict[str, tuple[str, str]] = {}
240
-
241
- for chunk in chunks[1:]: # Skip first chunk before first group
242
- lines = chunk.split('\n')
243
- # First line contains the group name and description in format "NAME:DESCRIPTION"
244
- group_line = lines[0].strip()
245
-
246
- # Extract group name and description
247
- if ':' in group_line:
248
- parts = group_line.split(':', 1) # Split only on first colon
249
- group_name = parts[0].strip()
250
- group_description = parts[1].strip() if len(parts) > 1 else ""
251
- else:
252
- group_name = group_line
253
- group_description = ""
254
-
255
- # Rest is the content
256
- content = '\n'.join(lines[1:]).strip()
257
-
258
- if group_name and content:
259
- res[group_name] = (group_description, content)
260
-
261
- return res
machineconfig/utils/io.py CHANGED
@@ -1,4 +1,3 @@
1
- from __future__ import annotations
2
1
 
3
2
  from typing import Any, Union, Optional, Mapping
4
3
  from pathlib import Path
@@ -1,12 +1,11 @@
1
1
  from machineconfig.utils.path_extended import PathExtended
2
- from machineconfig.utils.options import choose_from_options
3
2
  from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
4
3
  from rich.console import Console
5
4
  from rich.panel import Panel
6
5
  import platform
7
6
  import subprocess
8
7
  from pathlib import Path
9
-
8
+ from typing import Optional
10
9
 
11
10
  console = Console()
12
11
 
@@ -83,6 +82,7 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
83
82
  if len(reduced_scripts) == 1:
84
83
  return PathExtended(reduced_scripts[0])
85
84
  elif len(reduced_scripts) > 1:
85
+ from machineconfig.utils.options import choose_from_options
86
86
  choice = choose_from_options(multi=False, msg="Multiple matches found", options=reduced_scripts, fzf=True)
87
87
  return PathExtended(choice)
88
88
  print(f"Result: This still generated {len(reduced_scripts)} results.")
@@ -97,19 +97,27 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
97
97
  reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
98
98
  if len(reduced_scripts) == 1:
99
99
  return PathExtended(reduced_scripts[0])
100
- print(f"Result: This still generated {len(reduced_scripts)} results.")
100
+ print(f"Result: This still generated {len(reduced_scripts)} results.")
101
+
101
102
  try:
102
- fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
103
+
104
+ if len(partial_path_matches) == 0:
105
+ print("No partial path matches found, trying to do fd with --no-ignore ...")
106
+ fzf_cmd = f"cd '{search_root_obj}'; fd --no-ignore --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
107
+ else:
108
+ fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
103
109
  console.print(Panel(f"🔍 Second attempt: SEARCH STRATEGY | Using fd to search for '{sub_string}' in '{search_root_obj}' ...\n{fzf_cmd}", title="Search Strategy", expand=False))
104
110
  search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout
105
- search_res = search_res_raw.strip().split("\\n")[:-1]
111
+ search_res = search_res_raw.strip().split("\n")
106
112
  except subprocess.CalledProcessError as cpe:
107
113
  console.print(Panel(f"❌ ERROR | FZF search failed with '{sub_string}' in '{search_root_obj}'.\n{cpe}", title="Error", expand=False))
108
114
  import sys
109
-
110
115
  sys.exit(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results.")
111
116
  if len(search_res) == 1:
112
117
  return search_root_obj.joinpath(search_res_raw)
118
+ elif len(search_res) == 0:
119
+ msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
120
+ raise FileNotFoundError(msg)
113
121
 
114
122
  print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`:\n'{search_res}'")
115
123
  cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"
@@ -121,3 +129,46 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
121
129
  msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
122
130
  raise FileNotFoundError(msg) from cpe
123
131
  return search_root_obj.joinpath(res)
132
+
133
+
134
+ def search_for_files_of_interest(path_obj: PathExtended, suffixes: set[str]):
135
+ if path_obj.joinpath(".venv").exists():
136
+ path_objects = path_obj.search("*", not_in=[".venv"])
137
+ files: list[PathExtended] = []
138
+ for a_path_obj in path_objects:
139
+ files += search_for_files_of_interest(path_obj=a_path_obj, suffixes=suffixes)
140
+ return files
141
+ if path_obj.is_file():
142
+ return [path_obj]
143
+ files: list[PathExtended] = []
144
+ for a_suffix in suffixes:
145
+ if a_suffix == ".py":
146
+ files += path_obj.search(pattern="*.py", r=True, not_in=["__init__.py"])
147
+ else:
148
+ files += path_obj.search(pattern=f"*{a_suffix}", r=True)
149
+ return files
150
+
151
+
152
+ def get_choice_file(path: str, suffixes: Optional[set[str]]):
153
+ path_obj = sanitize_path(path)
154
+ if suffixes is None:
155
+ import platform
156
+ if platform.system() == "Windows":
157
+ suffixes = {".py", ".ps1", ".sh"}
158
+ elif platform.system() in ["Linux", "Darwin"]:
159
+ suffixes = {".py", ".sh"}
160
+ else:
161
+ suffixes = {".py"}
162
+ if not path_obj.exists():
163
+ print(f"🔍 Searching for file matching `{path}` under `{PathExtended.cwd()}`, but only if suffix matches {suffixes}")
164
+ choice_file = match_file_name(sub_string=path, search_root=PathExtended.cwd(), suffixes=suffixes)
165
+ elif path_obj.is_dir():
166
+ print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
167
+ files = search_for_files_of_interest(path_obj, suffixes=suffixes)
168
+ print(f"🔍 Got #{len(files)} results.")
169
+ from machineconfig.utils.options import choose_from_options
170
+ choice_file = choose_from_options(multi=False, options=files, fzf=True, msg="Choose one option")
171
+ choice_file = PathExtended(choice_file)
172
+ else:
173
+ choice_file = path_obj
174
+ return choice_file
@@ -2,13 +2,13 @@ from typing import Callable, Optional, Any, Union, cast
2
2
  import os
3
3
  from pathlib import Path
4
4
  import platform
5
- from machineconfig.scripts.python.helpers_devops.cli_utils import MachineSpecs
5
+ from machineconfig.scripts.python.helpers_utils.path import MachineSpecs
6
6
  import rich.console
7
7
  from machineconfig.utils.terminal import Response
8
8
  from machineconfig.utils.accessories import pprint, randstr
9
9
  from machineconfig.utils.meta import lambda_to_python_script
10
10
  UV_RUN_CMD = "$HOME/.local/bin/uv run" if platform.system() != "Windows" else """& "$env:USERPROFILE/.local/bin/uv" run"""
11
- MACHINECONFIG_VERSION = "machineconfig>=7.51"
11
+ MACHINECONFIG_VERSION = "machineconfig>=7.53"
12
12
  DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
13
13
 
14
14
  class SSH:
@@ -113,7 +113,7 @@ class SSH:
113
113
  if self.progress and self.task is not None:
114
114
  self.progress.update(self.task, completed=transferred, total=total)
115
115
  self.tqdm_wrap = RichProgressWrapper
116
- from machineconfig.scripts.python.helpers_devops.cli_utils import get_machine_specs
116
+ from machineconfig.scripts.python.helpers_utils.path import get_machine_specs
117
117
  self.local_specs: MachineSpecs = get_machine_specs()
118
118
  resp = self.run_shell(command="""~/.local/bin/utils get-machine-specs """, verbose_output=False, description="Getting remote machine specs", strict_stderr=False, strict_return_code=False)
119
119
  json_str = resp.op
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 7.51
3
+ Version: 7.53
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -65,7 +65,9 @@ iex (iwr bit.ly/cfgwindows).Content
65
65
  iex (uvx machineconfig define)
66
66
  # Permanent install:
67
67
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Skip if UV is already installed
68
- uvx install --upgrade machineconfig
68
+ uv tool install --upgrade --python 3.14 machineconfig both
69
+ devops config copy-assets
70
+
69
71
  ```
70
72
 
71
73
  # Install On Linux and MacOS
@@ -77,7 +79,7 @@ uvx install --upgrade machineconfig
77
79
  . <(uvx machineconfig define)
78
80
  # Permanent install:
79
81
  curl -LsSf https://astral.sh/uv/install.sh | sh # Skip if UV is already installed
80
- uvx install --upgrade machineconfig
82
+ uv tool install --upgrade --upgrade 3.14 machineconfig
81
83
  ```
82
84
 
83
85