machineconfig 2.8__py3-none-any.whl → 2.94__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (34) hide show
  1. machineconfig/cluster/sessions_managers/zellij_local_manager.py +4 -20
  2. machineconfig/jobs/python/python_ve_symlink.py +1 -1
  3. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  4. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  5. machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
  6. machineconfig/scripts/python/__pycache__/repos_helper_update.cpython-313.pyc +0 -0
  7. machineconfig/scripts/python/ai/mcinit.py +2 -14
  8. machineconfig/scripts/python/devops.py +18 -64
  9. machineconfig/scripts/python/devops_add_identity.py +6 -2
  10. machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
  11. machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
  12. machineconfig/scripts/python/devops_devapps_install.py +8 -6
  13. machineconfig/scripts/python/devops_update_repos.py +122 -226
  14. machineconfig/scripts/python/fire_agents.py +111 -198
  15. machineconfig/scripts/python/fire_agents_help_launch.py +142 -0
  16. machineconfig/scripts/python/fire_agents_help_search.py +82 -0
  17. machineconfig/scripts/python/fire_agents_load_balancer.py +52 -0
  18. machineconfig/scripts/python/fire_jobs.py +2 -1
  19. machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
  20. machineconfig/scripts/python/repos.py +1 -1
  21. machineconfig/scripts/python/repos_helper_update.py +265 -0
  22. machineconfig/utils/installer_utils/installer_class.py +3 -3
  23. machineconfig/utils/notifications.py +24 -4
  24. machineconfig/utils/path.py +2 -1
  25. machineconfig/utils/procs.py +7 -7
  26. machineconfig/utils/schemas/fire_agents/fire_agents_input.py +70 -0
  27. machineconfig/utils/schemas/layouts/layout_types.py +0 -1
  28. machineconfig/utils/source_of_truth.py +2 -0
  29. machineconfig/utils/ve.py +9 -5
  30. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/METADATA +7 -10
  31. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/RECORD +34 -27
  32. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/WHEEL +0 -0
  33. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/entry_points.txt +0 -0
  34. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,265 @@
1
+ from pathlib import Path
2
+ from typing import TypedDict
3
+ import subprocess
4
+ import git
5
+
6
+
7
+
8
+ class RepositoryUpdateResult(TypedDict):
9
+ """Result of updating a single repository."""
10
+ repo_path: str
11
+ status: str # "success", "error", "skipped", "auth_failed"
12
+ had_uncommitted_changes: bool
13
+ uncommitted_files: list[str]
14
+ commit_before: str
15
+ commit_after: str
16
+ commits_changed: bool
17
+ pyproject_changed: bool
18
+ dependencies_changed: bool
19
+ uv_sync_ran: bool
20
+ uv_sync_success: bool
21
+ remotes_processed: list[str]
22
+ remotes_skipped: list[str]
23
+ error_message: str | None
24
+ is_machineconfig_repo: bool
25
+ permissions_updated: bool
26
+
27
+
28
+ def set_permissions_recursive(path: Path, executable: bool = True) -> None:
29
+ """Set permissions recursively for a directory."""
30
+ if not path.exists():
31
+ return
32
+ if path.is_file():
33
+ if executable:
34
+ path.chmod(0o755)
35
+ else:
36
+ path.chmod(0o644)
37
+ elif path.is_dir():
38
+ path.chmod(0o755)
39
+ for item in path.rglob("*"):
40
+ set_permissions_recursive(item, executable)
41
+
42
+
43
+ def run_uv_sync(repo_path: Path) -> bool:
44
+ """Run uv sync in the given repository path. Returns True if successful."""
45
+ try:
46
+ print(f"🔄 Running uv sync in {repo_path}")
47
+ # Run uv sync with output directly to terminal (no capture)
48
+ subprocess.run(["uv", "sync"], cwd=repo_path, check=True)
49
+ print("✅ uv sync completed successfully")
50
+ return True
51
+ except subprocess.CalledProcessError as e:
52
+ print(f"❌ uv sync failed with return code {e.returncode}")
53
+ return False
54
+ except FileNotFoundError:
55
+ print("⚠️ uv command not found. Please install uv first.")
56
+ return False
57
+
58
+
59
+ def get_file_hash(file_path: Path) -> str | None:
60
+ """Get SHA256 hash of a file, return None if file doesn't exist."""
61
+ if not file_path.exists():
62
+ return None
63
+ import hashlib
64
+ return hashlib.sha256(file_path.read_bytes()).hexdigest()
65
+
66
+
67
+ def update_repository(repo: git.Repo, auto_sync: bool, allow_password_prompt: bool) -> RepositoryUpdateResult:
68
+ """Update a single repository and return detailed information about what happened."""
69
+ repo_path = Path(repo.working_dir)
70
+ print(f"🔄 {'Updating ' + str(repo_path):.^80}")
71
+
72
+ # Initialize result dict
73
+ result: RepositoryUpdateResult = {
74
+ "repo_path": str(repo_path),
75
+ "status": "success",
76
+ "had_uncommitted_changes": False,
77
+ "uncommitted_files": [],
78
+ "commit_before": "",
79
+ "commit_after": "",
80
+ "commits_changed": False,
81
+ "pyproject_changed": False,
82
+ "dependencies_changed": False,
83
+ "uv_sync_ran": False,
84
+ "uv_sync_success": False,
85
+ "remotes_processed": [],
86
+ "remotes_skipped": [],
87
+ "error_message": None,
88
+ "is_machineconfig_repo": "machineconfig" in str(repo_path),
89
+ "permissions_updated": False,
90
+ }
91
+
92
+ # Check git status first
93
+ print("📊 Checking git status...")
94
+ if repo.is_dirty():
95
+ # Get the list of modified files
96
+ changed_files_raw = [item.a_path for item in repo.index.diff(None)]
97
+ changed_files_raw.extend([item.a_path for item in repo.index.diff("HEAD")])
98
+ # Filter out None values and remove duplicates
99
+ changed_files = list(set(file for file in changed_files_raw if file is not None))
100
+
101
+ result["had_uncommitted_changes"] = True
102
+ result["uncommitted_files"] = changed_files
103
+ print(f"⚠️ Repository has uncommitted changes: {', '.join(changed_files)}")
104
+
105
+ # Repository has uncommitted changes - cannot update
106
+ result["status"] = "error"
107
+ result["error_message"] = f"Cannot update repository - there are pending changes in: {', '.join(changed_files)}. Please commit or stash your changes first."
108
+ raise RuntimeError(result["error_message"])
109
+ else:
110
+ print("✅ Repository is clean")
111
+
112
+ # Check if this repo has pyproject.toml
113
+ pyproject_path = repo_path / "pyproject.toml"
114
+
115
+ # Get hashes before pull
116
+ pyproject_hash_before = get_file_hash(pyproject_path)
117
+
118
+ # Get current commit hash before pull
119
+ result["commit_before"] = repo.head.commit.hexsha
120
+
121
+ try:
122
+ # Use subprocess for git pull to get better output control
123
+
124
+ # Get list of remotes
125
+ remotes = list(repo.remotes)
126
+ if not remotes:
127
+ print("⚠️ No remotes configured for this repository")
128
+ result["status"] = "skipped"
129
+ result["error_message"] = "No remotes configured for this repository"
130
+ return result
131
+
132
+ for remote in remotes:
133
+ try:
134
+ print(f"📥 Fetching from {remote.name}...")
135
+
136
+ # Set up environment for git commands
137
+ env = None
138
+ if not allow_password_prompt:
139
+ # Disable interactive prompts
140
+ import os
141
+
142
+ env = os.environ.copy()
143
+ env["GIT_TERMINAL_PROMPT"] = "0"
144
+ env["GIT_ASKPASS"] = "echo" # Returns empty string for any credential request
145
+
146
+ # First fetch to see what's available
147
+ fetch_result = subprocess.run(
148
+ ["git", "fetch", remote.name, "--verbose"],
149
+ cwd=repo_path,
150
+ capture_output=True,
151
+ text=True,
152
+ env=env,
153
+ timeout=30, # Add timeout to prevent hanging
154
+ )
155
+
156
+ # Check if fetch failed due to authentication
157
+ if fetch_result.returncode != 0 and not allow_password_prompt:
158
+ auth_error_indicators = [
159
+ "Authentication failed",
160
+ "Password for",
161
+ "Username for",
162
+ "could not read Username",
163
+ "could not read Password",
164
+ "fatal: Authentication failed",
165
+ "fatal: could not read Username",
166
+ "fatal: could not read Password",
167
+ ]
168
+
169
+ error_output = (fetch_result.stderr or "") + (fetch_result.stdout or "")
170
+ if any(indicator in error_output for indicator in auth_error_indicators):
171
+ print(f"⚠️ Skipping {remote.name} - authentication required but password prompts are disabled")
172
+ continue
173
+
174
+ if fetch_result.stdout:
175
+ print(f"📡 Fetch output: {fetch_result.stdout.strip()}")
176
+ if fetch_result.stderr:
177
+ print(f"📡 Fetch info: {fetch_result.stderr.strip()}")
178
+
179
+ # Now pull with verbose output
180
+ print(f"📥 Pulling from {remote.name}/{repo.active_branch.name}...")
181
+ pull_result = subprocess.run(["git", "pull", remote.name, repo.active_branch.name, "--verbose"], cwd=repo_path, capture_output=True, text=True, env=env, timeout=30)
182
+
183
+ # Check if pull failed due to authentication
184
+ if pull_result.returncode != 0 and not allow_password_prompt:
185
+ auth_error_indicators = [
186
+ "Authentication failed",
187
+ "Password for",
188
+ "Username for",
189
+ "could not read Username",
190
+ "could not read Password",
191
+ "fatal: Authentication failed",
192
+ "fatal: could not read Username",
193
+ "fatal: could not read Password",
194
+ ]
195
+
196
+ error_output = (pull_result.stderr or "") + (pull_result.stdout or "")
197
+ if any(indicator in error_output for indicator in auth_error_indicators):
198
+ print(f"⚠️ Skipping pull from {remote.name} - authentication required but password prompts are disabled")
199
+ continue
200
+
201
+ if pull_result.stdout:
202
+ print(f"📦 Pull output: {pull_result.stdout.strip()}")
203
+ if pull_result.stderr:
204
+ print(f"📦 Pull info: {pull_result.stderr.strip()}")
205
+
206
+ # Check if pull was successful
207
+ if pull_result.returncode == 0:
208
+ result["remotes_processed"].append(remote.name)
209
+ # Check if commits changed
210
+ result["commit_after"] = repo.head.commit.hexsha
211
+ if result["commit_before"] != result["commit_after"]:
212
+ result["commits_changed"] = True
213
+ print(f"✅ Repository updated: {result['commit_before'][:8]} → {result['commit_after'][:8]}")
214
+ else:
215
+ print("✅ Already up to date")
216
+ else:
217
+ result["remotes_skipped"].append(remote.name)
218
+ print(f"❌ Pull failed with return code {pull_result.returncode}")
219
+
220
+ except Exception as e:
221
+ result["remotes_skipped"].append(remote.name)
222
+ print(f"⚠️ Failed to pull from {remote.name}: {e}")
223
+ continue
224
+
225
+ # Check if pyproject.toml changed after pull
226
+ pyproject_hash_after = get_file_hash(pyproject_path)
227
+
228
+ if pyproject_hash_before != pyproject_hash_after:
229
+ print("📋 pyproject.toml has changed")
230
+ result["pyproject_changed"] = True
231
+ result["dependencies_changed"] = True
232
+
233
+ # Special handling for machineconfig repository
234
+ if result["is_machineconfig_repo"]:
235
+ print("🛠 Special handling for machineconfig repository...")
236
+ scripts_path = Path.home() / "scripts"
237
+ if scripts_path.exists():
238
+ set_permissions_recursive(scripts_path)
239
+ result["permissions_updated"] = True
240
+ print(f"✅ Set permissions for {scripts_path}")
241
+
242
+ linux_jobs_path = repo_path / "src" / "machineconfig" / "jobs" / "linux"
243
+ if linux_jobs_path.exists():
244
+ set_permissions_recursive(linux_jobs_path)
245
+ result["permissions_updated"] = True
246
+ print(f"✅ Set permissions for {linux_jobs_path}")
247
+
248
+ lf_exe_path = repo_path / "src" / "machineconfig" / "settings" / "lf" / "linux" / "exe"
249
+ if lf_exe_path.exists():
250
+ set_permissions_recursive(lf_exe_path)
251
+ result["permissions_updated"] = True
252
+ print(f"✅ Set permissions for {lf_exe_path}")
253
+
254
+ # Run uv sync if dependencies changed and auto_sync is enabled
255
+ if result["dependencies_changed"] and auto_sync:
256
+ result["uv_sync_ran"] = True
257
+ result["uv_sync_success"] = run_uv_sync(repo_path)
258
+
259
+ return result
260
+
261
+ except Exception as e:
262
+ result["status"] = "error"
263
+ result["error_message"] = str(e)
264
+ print(f"❌ Error updating repository {repo_path}: {e}")
265
+ return result
@@ -94,14 +94,14 @@ class Installer:
94
94
 
95
95
  if old_version_cli == new_version_cli:
96
96
  print(f"ℹ️ Same version detected: {old_version_cli}")
97
- return f"""echo "📦️ 😑 {self.exe_name}, same version: {old_version_cli}" """
97
+ return f"""📦️ 😑 {self.exe_name}, same version: {old_version_cli}"""
98
98
  else:
99
99
  print(f"🚀 Update successful: {old_version_cli} ➡️ {new_version_cli}")
100
- return f"""echo "📦️ 🤩 {self.exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}" """
100
+ return f"""📦️ 🤩 {self.exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}"""
101
101
 
102
102
  except Exception as ex:
103
103
  print(f"❌ ERROR: Installation failed for {self.exe_name}: {ex}")
104
- return f"""echo "📦️ ❌ Failed to install `{self.name}` with error: {ex}" """
104
+ return f"""📦️ ❌ Failed to install `{self.name}` with error: {ex}"""
105
105
 
106
106
  def install(self, version: Optional[str]):
107
107
  print(f"\n{'=' * 80}\n🔧 INSTALLATION PROCESS: {self.exe_name} 🔧\n{'=' * 80}")
@@ -14,7 +14,8 @@ import imaplib
14
14
  from email.mime.text import MIMEText
15
15
  from email.mime.multipart import MIMEMultipart
16
16
  from typing import Optional, Any, Union
17
- from markdown import markdown
17
+ from rich.console import Console
18
+ from rich.markdown import Markdown
18
19
 
19
20
 
20
21
  def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> "Any":
@@ -30,8 +31,27 @@ def get_github_markdown_css() -> str:
30
31
  return download_to_memory(Path(pp)).text
31
32
 
32
33
 
33
- def md2html(body: str):
34
- gh_style = Path(__file__).parent.joinpath("gh_style.css").read_text()
34
+ def md2html(body: str) -> str:
35
+ """Convert markdown to HTML using Rich library."""
36
+ # Use Rich's HTML export functionality to convert markdown to HTML
37
+ console = Console(record=True, width=120)
38
+ markdown_obj = Markdown(body)
39
+ console.print(markdown_obj)
40
+ html_output = console.export_html(inline_styles=True)
41
+
42
+ # Try to load GitHub CSS style, fallback to basic style if not found
43
+ gh_style_path = Path(__file__).parent.joinpath("gh_style.css")
44
+ if gh_style_path.exists():
45
+ gh_style = gh_style_path.read_text()
46
+ else:
47
+ # Fallback basic styling
48
+ gh_style = """
49
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
50
+ h1, h2, h3, h4, h5, h6 { color: #0366d6; }
51
+ code { background-color: #f6f8fa; padding: 2px 4px; border-radius: 3px; }
52
+ pre { background-color: #f6f8fa; padding: 16px; border-radius: 6px; overflow: auto; }
53
+ """
54
+
35
55
  return f"""
36
56
  <!DOCTYPE html>
37
57
  <html>
@@ -51,7 +71,7 @@ def md2html(body: str):
51
71
  </style>
52
72
  <body>
53
73
  <div class="markdown-body">
54
- {markdown(body)}
74
+ {html_output}
55
75
  </div>
56
76
  </body>
57
77
  </html>"""
@@ -1,5 +1,6 @@
1
1
  from machineconfig.utils.path_reduced import PathExtended as PathExtended
2
2
  from machineconfig.utils.options import choose_one_option
3
+ from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
3
4
  from rich.console import Console
4
5
  from rich.panel import Panel
5
6
  import platform
@@ -54,7 +55,7 @@ def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[l
54
55
  partial_path_matches = []
55
56
  for entry in root.iterdir():
56
57
  if entry.is_dir():
57
- if entry.name in {".links", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".mypy_cache"}:
58
+ if entry.name in set(EXCLUDE_DIRS):
58
59
  # prune this entire subtree
59
60
  continue
60
61
  tmp1, tmp2 = find_scripts(entry, name_substring, suffixes)
@@ -2,12 +2,12 @@
2
2
 
3
3
  import psutil
4
4
  from rich.progress import Progress, SpinnerColumn, TextColumn
5
- from pytz import timezone
5
+ from zoneinfo import ZoneInfo
6
6
  from machineconfig.utils.options import display_options
7
7
  from typing import Optional, Any
8
8
  from rich.console import Console
9
9
  from rich.panel import Panel
10
- from datetime import datetime
10
+ from datetime import datetime, timezone
11
11
  from machineconfig.utils.utils2 import pprint
12
12
 
13
13
  console = Console()
@@ -72,8 +72,8 @@ class ProcessManager:
72
72
  try:
73
73
  mem_usage_mb = proc.memory_info().rss / (1024 * 1024)
74
74
  # Convert create_time to local timezone
75
- create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone("UTC"))
76
- create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
75
+ create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone.utc)
76
+ create_time_local = create_time_utc.astimezone(ZoneInfo("Australia/Adelaide"))
77
77
 
78
78
  process_info.append(
79
79
  {
@@ -222,13 +222,13 @@ def get_age(create_time: Any) -> str:
222
222
  try:
223
223
  if isinstance(create_time, (int, float)):
224
224
  # Handle timestampz
225
- create_time_utc = datetime.fromtimestamp(create_time, tz=timezone("UTC"))
226
- create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
225
+ create_time_utc = datetime.fromtimestamp(create_time, tz=timezone.utc)
226
+ create_time_local = create_time_utc.astimezone(ZoneInfo("Australia/Adelaide"))
227
227
  else:
228
228
  # Already a datetime object
229
229
  create_time_local = create_time
230
230
 
231
- now_local = datetime.now(tz=timezone("Australia/Adelaide"))
231
+ now_local = datetime.now(tz=ZoneInfo("Australia/Adelaide"))
232
232
  age = now_local - create_time_local
233
233
  return str(age)
234
234
  except Exception as e:
@@ -0,0 +1,70 @@
1
+ """TypedDict definitions for fire_agents.py inputs.
2
+
3
+ This module defines the structured input types for the fire_agents main function,
4
+ capturing all user inputs collected during interactive execution.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import TypedDict, Literal, NotRequired
9
+ from machineconfig.scripts.python.fire_agents_load_balancer import SPLITTING_STRATEGY
10
+ from machineconfig.scripts.python.fire_agents_help_launch import AGENTS
11
+
12
+ SEARCH_STRATEGIES = Literal["file_path", "keyword_search", "filename_pattern"]
13
+
14
+
15
+ class FilePathSearchInput(TypedDict):
16
+ """Input for file_path search strategy."""
17
+ file_path: str
18
+ separator: str # Default: "\n"
19
+
20
+
21
+ class KeywordSearchInput(TypedDict):
22
+ """Input for keyword_search strategy."""
23
+ keyword: str
24
+
25
+
26
+ class FilenamePatternSearchInput(TypedDict):
27
+ """Input for filename_pattern search strategy."""
28
+ pattern: str # e.g., '*.py', '*test*', 'config.*'
29
+
30
+
31
+ class AgentCapSplittingInput(TypedDict):
32
+ """Input for agent_cap splitting strategy."""
33
+ agent_cap: int # Default: 6
34
+
35
+
36
+ class TaskRowsSplittingInput(TypedDict):
37
+ """Input for task_rows splitting strategy."""
38
+ task_rows: int # Default: 13
39
+
40
+
41
+ class FireAgentsMainInput(TypedDict):
42
+ """Complete input structure for fire_agents main function."""
43
+
44
+ # Core configuration
45
+ repo_root: Path
46
+ search_strategy: SEARCH_STRATEGIES
47
+ splitting_strategy: SPLITTING_STRATEGY
48
+ agent_selected: AGENTS
49
+ prompt_prefix: str
50
+ job_name: str # Default: "AI_Agents"
51
+ keep_material_in_separate_file: bool # Default: False
52
+ max_agents: int # Default: 25
53
+
54
+ # Search strategy specific inputs (only one will be present based on search_strategy)
55
+ file_path_input: NotRequired[FilePathSearchInput]
56
+ keyword_search_input: NotRequired[KeywordSearchInput]
57
+ filename_pattern_input: NotRequired[FilenamePatternSearchInput]
58
+
59
+ # Splitting strategy specific inputs (only one will be present based on splitting_strategy)
60
+ agent_cap_input: NotRequired[AgentCapSplittingInput]
61
+ task_rows_input: NotRequired[TaskRowsSplittingInput]
62
+
63
+
64
+ class FireAgentsRuntimeData(TypedDict):
65
+ """Runtime data derived from inputs during execution."""
66
+
67
+ prompt_material: str
68
+ separator: str
69
+ prompt_material_re_splitted: list[str]
70
+ random_name: str # 3-character random string for session naming
@@ -17,7 +17,6 @@ class TabConfig(TypedDict):
17
17
 
18
18
  class LayoutConfig(TypedDict):
19
19
  """Configuration for a complete layout with its tabs."""
20
-
21
20
  layoutName: str
22
21
  layoutTabs: List[TabConfig]
23
22
 
@@ -5,6 +5,8 @@ Utils
5
5
  import machineconfig
6
6
  from pathlib import Path
7
7
 
8
+ EXCLUDE_DIRS = [".links", ".ai", ".scripts", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".mypy_cache"]
9
+
8
10
  LIBRARY_ROOT = Path(machineconfig.__file__).resolve().parent
9
11
  REPO_ROOT = LIBRARY_ROOT.parent.parent
10
12
 
machineconfig/utils/ve.py CHANGED
@@ -1,7 +1,9 @@
1
+
1
2
  from machineconfig.utils.path_reduced import PathExtended as PathExtended
2
3
  from machineconfig.utils.utils2 import read_ini
3
4
  import platform
4
5
  from typing import Optional
6
+ from pathlib import Path
5
7
 
6
8
 
7
9
  def get_ve_path_and_ipython_profile(init_path: PathExtended) -> tuple[Optional[str], Optional[str]]:
@@ -38,15 +40,17 @@ def get_ve_path_and_ipython_profile(init_path: PathExtended) -> tuple[Optional[s
38
40
  return ve_path, ipy_profile
39
41
 
40
42
 
41
- def get_repo_root(choice_file: str) -> Optional[str]:
43
+ def get_repo_root(path: Path) -> Optional[Path]:
42
44
  from git import Repo, InvalidGitRepositoryError
43
45
 
44
46
  try:
45
- repo = Repo(PathExtended(choice_file), search_parent_directories=True)
46
- repo_root = str(repo.working_tree_dir) if repo.working_tree_dir else None
47
+ repo = Repo(str(path), search_parent_directories=True)
48
+ root = repo.working_tree_dir
49
+ if root is not None:
50
+ return Path(root)
47
51
  except InvalidGitRepositoryError:
48
- repo_root = None
49
- return repo_root
52
+ pass
53
+ return None
50
54
 
51
55
 
52
56
  def get_ve_activate_line(ve_root: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 2.8
3
+ Version: 2.94
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -13,21 +13,18 @@ Requires-Python: >=3.13
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: cryptography>=44.0.2
15
15
  Requires-Dist: fire>=0.7.0
16
- Requires-Dist: gitpython>=3.1.44
17
16
  Requires-Dist: joblib>=1.5.2
18
- Requires-Dist: markdown>=3.9
19
17
  Requires-Dist: paramiko>=3.5.1
20
- Requires-Dist: psutil>=7.0.0
21
- Requires-Dist: pydantic>=2.11.3
22
- Requires-Dist: pyfzf>=0.3.1
23
- Requires-Dist: pyjson5>=1.6.9
24
- Requires-Dist: pytz>=2025.2
25
- Requires-Dist: pyyaml>=6.0.2
26
18
  Requires-Dist: randomname>=0.2.1
27
- Requires-Dist: rclone-python>=0.1.23
28
19
  Requires-Dist: requests>=2.32.5
29
20
  Requires-Dist: rich>=14.0.0
30
21
  Requires-Dist: tenacity>=9.1.2
22
+ Requires-Dist: psutil>=7.0.0
23
+ Requires-Dist: gitpython>=3.1.44
24
+ Requires-Dist: pyfzf>=0.3.1
25
+ Requires-Dist: rclone-python>=0.1.23
26
+ Requires-Dist: pyjson5>=1.6.9
27
+ Requires-Dist: pyyaml>=6.0.2
31
28
  Requires-Dist: toml>=0.10.2
32
29
  Requires-Dist: tomli>=2.2.1
33
30
  Provides-Extra: windows