machineconfig 2.6__py3-none-any.whl → 2.7__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 (118) hide show
  1. machineconfig/cluster/remote/remote_machine.py +0 -1
  2. machineconfig/cluster/sessions_managers/wt_local.py +1 -1
  3. machineconfig/cluster/sessions_managers/wt_local_manager.py +1 -1
  4. machineconfig/cluster/sessions_managers/wt_remote.py +1 -1
  5. machineconfig/cluster/sessions_managers/wt_remote_manager.py +1 -1
  6. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +1 -1
  7. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +1 -1
  8. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +1 -1
  9. machineconfig/cluster/sessions_managers/zellij_local.py +1 -1
  10. machineconfig/cluster/sessions_managers/zellij_local_manager.py +1 -1
  11. machineconfig/cluster/sessions_managers/zellij_remote.py +1 -1
  12. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +1 -1
  13. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +1 -1
  14. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +1 -1
  15. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +1 -1
  16. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +1 -1
  17. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  18. machineconfig/jobs/python/vscode/sync_code.py +0 -1
  19. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  20. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  21. machineconfig/profile/create.py +8 -16
  22. machineconfig/profile/shell.py +140 -179
  23. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  24. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  25. machineconfig/scripts/linux/cloud_copy +1 -1
  26. machineconfig/scripts/linux/cloud_mount +1 -1
  27. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  28. machineconfig/scripts/linux/cloud_sync +1 -1
  29. machineconfig/scripts/linux/croshell +2 -1
  30. machineconfig/scripts/linux/devops +1 -1
  31. machineconfig/scripts/linux/fire +1 -1
  32. machineconfig/scripts/linux/fire_agents +1 -1
  33. machineconfig/scripts/linux/ftpx +1 -1
  34. machineconfig/scripts/linux/gh_models +1 -1
  35. machineconfig/scripts/linux/kill_process +1 -1
  36. machineconfig/scripts/linux/mcinit +1 -1
  37. machineconfig/scripts/linux/mount_nfs +1 -1
  38. machineconfig/scripts/linux/mount_nw_drive +1 -11
  39. machineconfig/scripts/linux/repos +1 -1
  40. machineconfig/scripts/linux/scheduler +1 -1
  41. machineconfig/scripts/linux/start_slidev +1 -1
  42. machineconfig/scripts/linux/start_terminals +1 -1
  43. machineconfig/scripts/linux/wifi_conn +1 -1
  44. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  45. machineconfig/scripts/python/__pycache__/cloud_repo_sync.cpython-313.pyc +0 -0
  46. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  47. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  48. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  49. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-313.pyc +0 -0
  50. machineconfig/scripts/python/__pycache__/repos.cpython-313.pyc +0 -0
  51. machineconfig/scripts/python/__pycache__/repos_helper_record.cpython-313.pyc +0 -0
  52. machineconfig/scripts/python/ai/mcinit.py +16 -2
  53. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +1 -1
  54. machineconfig/scripts/python/croshell.py +0 -1
  55. machineconfig/scripts/python/devops.py +1 -13
  56. machineconfig/scripts/python/devops_update_repos.py +39 -19
  57. machineconfig/scripts/python/fire_agents.py +1 -1
  58. machineconfig/scripts/python/fire_jobs.py +8 -3
  59. machineconfig/scripts/python/fire_jobs_layout_helper.py +1 -1
  60. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  61. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-313.pyc +0 -0
  62. machineconfig/scripts/python/repos.py +10 -227
  63. machineconfig/scripts/python/repos_helper_record.py +193 -0
  64. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -1
  65. machineconfig/scripts/windows/cloud_copy.ps1 +1 -1
  66. machineconfig/scripts/windows/cloud_mount.ps1 +1 -1
  67. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -1
  68. machineconfig/scripts/windows/cloud_sync.ps1 +1 -1
  69. machineconfig/scripts/windows/croshell.ps1 +1 -1
  70. machineconfig/scripts/windows/devops.ps1 +1 -29
  71. machineconfig/scripts/windows/dotfile.ps1 +1 -1
  72. machineconfig/scripts/windows/fire.ps1 +1 -45
  73. machineconfig/scripts/windows/ftpx.ps1 +1 -1
  74. machineconfig/scripts/windows/gpt.ps1 +1 -23
  75. machineconfig/scripts/windows/kill_process.ps1 +1 -1
  76. machineconfig/scripts/windows/mcinit.ps1 +1 -1
  77. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  78. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  79. machineconfig/scripts/windows/repos.ps1 +1 -1
  80. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  81. machineconfig/scripts/windows/snapshot.ps1 +1 -1
  82. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  83. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  84. machineconfig/scripts/windows/wifi_conn.ps1 +1 -2
  85. machineconfig/settings/shells/pwsh/init.ps1 +0 -4
  86. machineconfig/setup_linux/web_shortcuts/croshell.sh +1 -1
  87. machineconfig/setup_linux/web_shortcuts/interactive.sh +7 -13
  88. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +9 -18
  89. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -66
  90. machineconfig/utils/links.py +1 -2
  91. machineconfig/utils/options.py +8 -5
  92. machineconfig/utils/procs.py +5 -1
  93. machineconfig/utils/scheduling.py +0 -1
  94. machineconfig/{cluster/sessions_managers → utils/schemas/layouts}/layout_types.py +0 -1
  95. machineconfig/utils/schemas/repos/repos_types.py +28 -0
  96. machineconfig/utils/source_of_truth.py +1 -4
  97. machineconfig/utils/ssh.py +1 -1
  98. {machineconfig-2.6.dist-info → machineconfig-2.7.dist-info}/METADATA +1 -1
  99. {machineconfig-2.6.dist-info → machineconfig-2.7.dist-info}/RECORD +103 -110
  100. {machineconfig-2.6.dist-info → machineconfig-2.7.dist-info}/entry_points.txt +1 -1
  101. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  102. machineconfig/profile/create_hardlinks.py +0 -140
  103. machineconfig/scripts/linux/checkout_versions +0 -2
  104. machineconfig/scripts/linux/cloud_manager +0 -2
  105. machineconfig/scripts/linux/url2md +0 -2
  106. machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
  107. machineconfig/scripts/windows/checkout_version.ps1 +0 -1
  108. machineconfig/scripts/windows/cloud_manager.ps1 +0 -1
  109. machineconfig/scripts/windows/neofetch.ps1 +0 -2
  110. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +0 -1
  111. machineconfig/settings/__pycache__/__init__.cpython-313.pyc +0 -0
  112. machineconfig/settings/shells/ipy/profiles/default/__pycache__/__init__.cpython-313.pyc +0 -0
  113. machineconfig/settings/shells/ipy/profiles/default/startup/__pycache__/__init__.cpython-313.pyc +0 -0
  114. machineconfig/utils/ai/browser_user_wrapper.py +0 -66
  115. machineconfig/utils/ai/url2md.py +0 -85
  116. /machineconfig/setup_windows/wt_and_pwsh/{set_pwsh_theme.py → install_nerd_fonts.py} +0 -0
  117. {machineconfig-2.6.dist-info → machineconfig-2.7.dist-info}/WHEEL +0 -0
  118. {machineconfig-2.6.dist-info → machineconfig-2.7.dist-info}/top_level.txt +0 -0
@@ -5,17 +5,17 @@ in the event that username@github.com is not mentioned in the remote url.
5
5
 
6
6
  """
7
7
 
8
- import subprocess
9
- from rich import print as pprint
10
8
  from machineconfig.utils.source_of_truth import CONFIG_PATH, DEFAULTS_PATH
11
9
  from machineconfig.utils.path_reduced import PathExtended as PathExtended
12
- from machineconfig.utils.io_save import save_json
13
- from machineconfig.utils.utils2 import randstr, read_json, read_ini
14
- from machineconfig.scripts.python.devops_update_repos import run_uv_sync, update_repository
10
+ from machineconfig.utils.utils2 import randstr, read_ini
11
+ from machineconfig.scripts.python.devops_update_repos import update_repository
12
+ from machineconfig.scripts.python.repos_helper_record import main as record_repos
13
+
15
14
  import argparse
16
- from dataclasses import dataclass
17
15
  from enum import Enum
18
- from typing import Optional, Any
16
+ from typing import Optional
17
+
18
+ from rich import print as pprint
19
19
 
20
20
 
21
21
  class GitAction(Enum):
@@ -24,14 +24,6 @@ class GitAction(Enum):
24
24
  pull = "pull"
25
25
 
26
26
 
27
- @dataclass
28
- class RepoRecord:
29
- name: str
30
- parent_dir: str
31
- remotes: dict[str, str] # Fixed: should be dict mapping remote name to URL
32
- version: dict[str, str]
33
-
34
-
35
27
  def git_action(path: PathExtended, action: GitAction, mess: Optional[str] = None, r: bool = False, auto_sync: bool = True) -> bool:
36
28
  """Perform git actions using Python instead of shell scripts. Returns True if successful."""
37
29
  from git.exc import InvalidGitRepositoryError
@@ -78,7 +70,7 @@ def git_action(path: PathExtended, action: GitAction, mess: Optional[str] = None
78
70
 
79
71
  elif action == GitAction.pull:
80
72
  # Use the enhanced update function with uv sync support
81
- update_repository(repo, auto_sync=auto_sync)
73
+ update_repository(repo, auto_sync=auto_sync, allow_password_prompt=False)
82
74
  print("✅ Pull completed")
83
75
  return True
84
76
 
@@ -120,15 +112,10 @@ def main():
120
112
  auto_sync = not args.no_sync # Enable auto sync by default, disable with --no-sync
121
113
 
122
114
  if args.record:
123
- print("\n📝 Recording repositories...")
124
- res = record_repos(repos_root=str(repos_root))
125
- pprint("✅ Recorded repositories:\n", res)
126
- save_path = CONFIG_PATH.joinpath("repos").joinpath(repos_root.rel2home()).joinpath("repos.json")
127
- save_json(obj=res, path=save_path, indent=4)
128
- pprint(f"📁 Result saved at {PathExtended(save_path)}")
115
+ save_path = record_repos(repos_root=repos_root)
129
116
  if args.cloud is not None:
130
117
  PathExtended(save_path).to_cloud(rel2home=True, cloud=args.cloud)
131
- print(">>>>>>>>> Finished Recording")
118
+
132
119
 
133
120
  elif args.clone or args.checkout or args.checkout_to_branch:
134
121
  print("\n📥 Cloning or checking out repositories...")
@@ -144,11 +131,6 @@ def main():
144
131
  assert cloud is not None, f"Path {repos_root} does not exist and cloud was not passed. You can't clone without one of them."
145
132
  repos_root.from_cloud(cloud=cloud, rel2home=True)
146
133
  assert (repos_root.exists() and repos_root.name == "repos.json") or args.cloud is not None, f"Path {repos_root} does not exist and cloud was not passed. You can't clone without one of them."
147
- success = install_repos_python(specs_path=str(repos_root), clone=args.clone, checkout_to_recorded_commit=args.checkout, checkout_to_branch=args.checkout_to_branch, auto_sync=auto_sync)
148
- if success:
149
- print("✅ Repository operations completed successfully")
150
- else:
151
- print("⚠️ Some repository operations encountered issues")
152
134
 
153
135
  elif args.all or args.commit or args.pull or args.push:
154
136
  print(f"\n🔄 Performing Git actions on repositories @ `{repos_root}`...")
@@ -172,205 +154,6 @@ def main():
172
154
  print("❌ No action specified. Try passing --push, --pull, --commit, or --all.")
173
155
 
174
156
 
175
- def record_repos(repos_root: str, r: bool = True) -> list[dict[str, Any]]:
176
- path_obj = PathExtended(repos_root).expanduser().absolute()
177
- if path_obj.is_file():
178
- return []
179
- search_res = path_obj.search("*", files=False)
180
- res: list[dict[str, Any]] = []
181
- for a_search_res in search_res:
182
- if a_search_res.joinpath(".git").exists():
183
- try:
184
- res.append(record_a_repo(a_search_res))
185
- except Exception as e:
186
- print(f"⚠️ Failed to record {a_search_res}: {e}")
187
- else:
188
- if r:
189
- res += record_repos(str(a_search_res), r=r)
190
- return res
191
-
192
-
193
- def record_a_repo(path: PathExtended, search_parent_directories: bool = False, preferred_remote: Optional[str] = None):
194
- from git.repo import Repo
195
-
196
- repo = Repo(path, search_parent_directories=search_parent_directories) # get list of remotes using git python
197
- repo_root = PathExtended(repo.working_dir).absolute()
198
- remotes = {remote.name: remote.url for remote in repo.remotes}
199
- if preferred_remote is not None:
200
- if preferred_remote in remotes:
201
- remotes = {preferred_remote: remotes[preferred_remote]}
202
- else:
203
- print(f"⚠️ `{preferred_remote=}` not found in {remotes}.")
204
- preferred_remote = None
205
- try:
206
- commit = repo.head.commit.hexsha
207
- except ValueError: # look at https://github.com/gitpython-developers/GitPython/issues/1016
208
- print(f"⚠️ Failed to get latest commit of {repo}")
209
- commit = None
210
- try:
211
- current_branch = repo.head.reference.name # same as repo.active_branch.name
212
- except TypeError:
213
- print(f"⁉️ Failed to get current branch of {repo}. It is probably in a detached state.")
214
- current_branch = None
215
- res: dict[str, Any] = {"name": repo_root.name, "parent_dir": repo_root.parent.collapseuser().as_posix(), "current_branch": current_branch, "remotes": remotes, "version": {"branch": current_branch, "commit": commit}}
216
- return res
217
-
218
-
219
- def install_repos_python(specs_path: str, clone: bool = True, checkout_to_recorded_commit: bool = False, checkout_to_branch: bool = False, editable_install: bool = False, preferred_remote: Optional[str] = None, auto_sync: bool = True) -> bool:
220
- """Python-based repository installation with uv sync support. Returns True if all operations succeeded."""
221
- from git.repo import Repo
222
- from git.exc import GitCommandError
223
-
224
- path_obj = PathExtended(specs_path).expanduser().absolute()
225
- repos: list[dict[str, Any]] = read_json(path_obj)
226
- overall_success = True
227
-
228
- for repo in repos:
229
- repo_success = True
230
- parent_dir = PathExtended(repo["parent_dir"]).expanduser().absolute()
231
- parent_dir.mkdir(parents=True, exist_ok=True)
232
- repo_path = parent_dir / repo["name"]
233
-
234
- print(f"\n{'Processing ' + repo['name']:.^80}")
235
-
236
- # Handle cloning and remote setup
237
- if clone:
238
- # Select the remote to use for cloning
239
- if len(repo["remotes"]) == 0:
240
- print(f"⚠️ No remotes found for {repo['name']}. Skipping clone.")
241
- repo_success = False
242
- continue
243
-
244
- remote_name, remote_url = next(iter(repo["remotes"].items())) # Get first remote by default
245
- if preferred_remote is not None and preferred_remote in repo["remotes"]:
246
- remote_name = preferred_remote
247
- remote_url = repo["remotes"][preferred_remote]
248
- elif preferred_remote is not None:
249
- print(f"⚠️ `{preferred_remote=}` not found in {repo['remotes']}.")
250
-
251
- try:
252
- # Clone with the selected remote
253
- print(f"📥 Cloning {remote_url} to {repo_path}")
254
- cloned_repo = Repo.clone_from(remote_url, repo_path, origin=remote_name, depth=2)
255
- print(f"✅ Successfully cloned {repo['name']}")
256
-
257
- # Add any additional remotes
258
- for other_remote_name, other_remote_url in repo["remotes"].items():
259
- if other_remote_name != remote_name:
260
- try:
261
- cloned_repo.create_remote(other_remote_name, other_remote_url)
262
- print(f"✅ Added remote {other_remote_name}")
263
- except Exception as e:
264
- print(f"⚠️ Failed to add remote {other_remote_name}: {e}")
265
-
266
- except GitCommandError as e:
267
- print(f"❌ Failed to clone {repo['name']}: {e}")
268
- repo_success = False
269
- continue
270
- except Exception as e:
271
- print(f"❌ Unexpected error cloning {repo['name']}: {e}")
272
- repo_success = False
273
- continue
274
-
275
- # Handle checkout operations (after cloning/if repo exists)
276
- if repo_path.exists():
277
- try:
278
- existing_repo = Repo(repo_path)
279
-
280
- if checkout_to_recorded_commit:
281
- commit = repo["version"]["commit"]
282
- if isinstance(commit, str):
283
- print(f"🔀 Checking out to commit {commit[:8]}...")
284
- existing_repo.git.checkout(commit)
285
- print("✅ Checked out to recorded commit")
286
- else:
287
- print(f"⚠️ Skipping {repo['name']} because it doesn't have a commit recorded. Found {commit}")
288
-
289
- elif checkout_to_branch:
290
- if repo.get("current_branch"):
291
- print(f"🔀 Checking out to branch {repo['current_branch']}...")
292
- existing_repo.git.checkout(repo["current_branch"])
293
- print("✅ Checked out to recorded branch")
294
- else:
295
- print(f"⚠️ No current branch recorded for {repo['name']}")
296
-
297
- # Handle editable install
298
- if editable_install:
299
- pyproject_path = repo_path / "pyproject.toml"
300
- if pyproject_path.exists():
301
- print(f"📦 Installing {repo['name']} in editable mode...")
302
- result = subprocess.run(["uv", "pip", "install", "-e", "."], cwd=repo_path, capture_output=True, text=True)
303
- if result.returncode == 0:
304
- print("✅ Editable install completed")
305
- else:
306
- print(f"❌ Editable install failed: {result.stderr}")
307
- repo_success = False
308
- else:
309
- print(f"⚠️ No pyproject.toml found in {repo['name']}, skipping editable install")
310
-
311
- # Run uv sync if auto_sync is enabled and pyproject.toml exists
312
- if auto_sync and (repo_path / "pyproject.toml").exists():
313
- sync_success = run_uv_sync(repo_path)
314
- if not sync_success:
315
- repo_success = False
316
-
317
- except Exception as e:
318
- print(f"❌ Error processing existing repository {repo['name']}: {e}")
319
- repo_success = False
320
-
321
- overall_success = overall_success and repo_success
322
-
323
- return overall_success
324
-
325
-
326
- def install_repos(specs_path: str, clone: bool = True, checkout_to_recorded_commit: bool = False, checkout_to_branch: bool = False, editable_install: bool = False, preferred_remote: Optional[str] = None):
327
- program = ""
328
- path_obj = PathExtended(specs_path).expanduser().absolute()
329
- repos: list[dict[str, Any]] = read_json(path_obj)
330
- for repo in repos:
331
- parent_dir = PathExtended(repo["parent_dir"]).expanduser().absolute()
332
- parent_dir.mkdir(parents=True, exist_ok=True)
333
-
334
- # Handle cloning and remote setup
335
- if clone:
336
- # Select the remote to use for cloning
337
- if len(repo["remotes"]) == 0:
338
- print(f"⚠️ No remotes found for {repo['name']}. Skipping clone.")
339
- continue
340
- remote_name, remote_url = next(iter(repo["remotes"].items())) # Get first remote by default
341
- if preferred_remote is not None and preferred_remote in repo["remotes"]:
342
- remote_name = preferred_remote
343
- remote_url = repo["remotes"][preferred_remote]
344
- elif preferred_remote is not None:
345
- print(f"⚠️ `{preferred_remote=}` not found in {repo['remotes']}.")
346
-
347
- # Clone with the selected remote
348
- program += f"\ncd {parent_dir.collapseuser().as_posix()}; git clone {remote_url} --origin {remote_name} --depth 2"
349
- program += f"\ncd {parent_dir.collapseuser().as_posix()}/{repo['name']}; git remote set-url {remote_name} {remote_url}"
350
-
351
- # Add any additional remotes
352
- for other_remote_name, other_remote_url in repo["remotes"].items():
353
- if other_remote_name != remote_name:
354
- program += f"\ncd {parent_dir.collapseuser().as_posix()}/{repo['name']}; git remote add {other_remote_name} {other_remote_url}"
355
-
356
- # Handle checkout operations (after all remotes are set up)
357
- if checkout_to_recorded_commit:
358
- commit = repo["version"]["commit"]
359
- if isinstance(commit, str):
360
- program += f"\ncd {parent_dir.collapseuser().as_posix()}/{repo['name']}; git checkout {commit}"
361
- else:
362
- print(f"Skipping {repo['parent_dir']} because it doesn't have a commit recorded. Found {commit}")
363
- elif checkout_to_branch:
364
- program += f"\ncd {parent_dir.collapseuser().as_posix()}/{repo['name']}; git checkout {repo['current_branch']}"
365
-
366
- # Handle editable install
367
- if editable_install:
368
- program += f"\ncd {parent_dir.collapseuser().as_posix()}/{repo['name']}; uv pip install -e ."
369
-
370
- program += "\n"
371
- pprint(program)
372
- return program
373
-
374
157
 
375
158
  if __name__ == "__main__":
376
159
  main()
@@ -0,0 +1,193 @@
1
+
2
+ from machineconfig.utils.path_reduced import PathExtended as PathExtended
3
+ from machineconfig.utils.schemas.repos.repos_types import GitVersionInfo, RepoRecordDict, RepoRemote
4
+
5
+ from machineconfig.utils.schemas.repos.repos_types import RepoRecordFile
6
+ from machineconfig.utils.source_of_truth import CONFIG_PATH
7
+ from machineconfig.utils.io_save import save_json
8
+
9
+ from typing import Optional
10
+
11
+ from rich import print as pprint
12
+
13
+
14
+ def build_tree_structure(repos: list[RepoRecordDict], repos_root: PathExtended) -> str:
15
+ """Build a tree structure representation of all repositories."""
16
+ if not repos:
17
+ return "No repositories found."
18
+
19
+ # Group repos by their parent directories relative to repos_root
20
+ tree_dict: dict[str, list[RepoRecordDict]] = {}
21
+ repos_root_abs = repos_root.expanduser().absolute()
22
+
23
+ for repo in repos:
24
+ parent_path = PathExtended(repo["parentDir"]).expanduser().absolute()
25
+ try:
26
+ relative_path = parent_path.relative_to(repos_root_abs)
27
+ relative_str = str(relative_path) if str(relative_path) != "." else ""
28
+ except ValueError:
29
+ # If the path is not relative to repos_root, use the full path
30
+ relative_str = str(parent_path)
31
+
32
+ if relative_str not in tree_dict:
33
+ tree_dict[relative_str] = []
34
+ tree_dict[relative_str].append(repo)
35
+
36
+ # Sort directories for consistent output
37
+ sorted_dirs = sorted(tree_dict.keys())
38
+
39
+ tree_lines: list[str] = []
40
+ tree_lines.append(f"📂 {repos_root.name}/ ({repos_root_abs})")
41
+
42
+ for i, dir_path in enumerate(sorted_dirs):
43
+ is_last_dir = i == len(sorted_dirs) - 1
44
+ dir_prefix = "└── " if is_last_dir else "├── "
45
+
46
+ if dir_path:
47
+ tree_lines.append(f"│ {dir_prefix}📁 {dir_path}/")
48
+ repo_prefix_base = "│ │ " if not is_last_dir else " "
49
+ else:
50
+ repo_prefix_base = "│ "
51
+
52
+ repos_in_dir = tree_dict[dir_path]
53
+ # Sort repos by name
54
+ repos_in_dir.sort(key=lambda x: x["name"])
55
+
56
+ for j, repo in enumerate(repos_in_dir):
57
+ is_last_repo = j == len(repos_in_dir) - 1
58
+ repo_prefix = f"{repo_prefix_base}└── " if is_last_repo else f"{repo_prefix_base}├── "
59
+
60
+ # Create status indicators
61
+ status_indicators = []
62
+ if repo["isDirty"]:
63
+ status_indicators.append("🔶 DIRTY")
64
+ if not repo["remotes"]:
65
+ status_indicators.append("⚠️ NO_REMOTE")
66
+ if repo["currentBranch"] == "DETACHED":
67
+ status_indicators.append("🔀 DETACHED")
68
+
69
+ status_str = f"[{' | '.join(status_indicators)}]" if status_indicators else "[✅ CLEAN]"
70
+ branch_info = f" ({repo['currentBranch']})" if repo['currentBranch'] != "DETACHED" else ""
71
+
72
+ # Build the base string without status
73
+ base_str = f"{repo_prefix}📦 {repo['name']}{branch_info}"
74
+
75
+ # Calculate padding to align status at 75 characters
76
+ target_width = 45
77
+ current_length = len(base_str)
78
+ padding = max(1, target_width - current_length) # At least 1 space
79
+
80
+ tree_lines.append(f"{base_str}{' ' * padding}{status_str}")
81
+
82
+ return "\n".join(tree_lines)
83
+
84
+
85
+ def record_a_repo(path: PathExtended, search_parent_directories: bool = False, preferred_remote: Optional[str] = None) -> RepoRecordDict:
86
+ from git.repo import Repo
87
+
88
+ repo = Repo(path, search_parent_directories=search_parent_directories) # get list of remotes using git python
89
+ repo_root = PathExtended(repo.working_dir).absolute()
90
+ # remotes: = {remote.name: remote.url for remote in repo.remotes}
91
+ remotes: list[RepoRemote] = [{"name": remote.name, "url": remote.url} for remote in repo.remotes]
92
+ if preferred_remote is not None:
93
+ if preferred_remote in [remote["name"] for remote in remotes]:
94
+ remotes = [remote for remote in remotes if remote["name"] == preferred_remote]
95
+ else:
96
+ print(f"⚠️ `{preferred_remote=}` not found in {remotes}.")
97
+ preferred_remote = None
98
+ try:
99
+ commit = repo.head.commit.hexsha
100
+ except ValueError: # look at https://github.com/gitpython-developers/GitPython/issues/1016
101
+ print(f"⚠️ Failed to get latest commit of {repo}")
102
+ commit = "UNKNOWN"
103
+ try:
104
+ current_branch = repo.head.reference.name # same as repo.active_branch.name
105
+ except TypeError:
106
+ print(f"⁉️ Failed to get current branch of {repo}. It is probably in a detached state.")
107
+ # current_branch = None
108
+ current_branch = "DETACHED"
109
+
110
+ # Check if repo is dirty (has uncommitted changes)
111
+ is_dirty = repo.is_dirty(untracked_files=True)
112
+
113
+ version_info: GitVersionInfo = {
114
+ "branch": current_branch,
115
+ "commit": commit
116
+ }
117
+
118
+ res: RepoRecordDict = {
119
+ "name": repo_root.name,
120
+ "parentDir": repo_root.parent.collapseuser().as_posix(),
121
+ "currentBranch": current_branch,
122
+ "remotes": remotes,
123
+ "version": version_info,
124
+ "isDirty": is_dirty
125
+ }
126
+ return res
127
+
128
+
129
+ def record_repos_recursively(repos_root: str, r: bool = True) -> list[RepoRecordDict]:
130
+ path_obj = PathExtended(repos_root).expanduser().absolute()
131
+ if path_obj.is_file():
132
+ return []
133
+ search_res = path_obj.search("*", files=False, folders=True)
134
+ res: list[RepoRecordDict] = []
135
+ for a_search_res in search_res:
136
+ if a_search_res.joinpath(".git").exists():
137
+ try:
138
+ res.append(record_a_repo(a_search_res))
139
+ except Exception as e:
140
+ print(f"⚠️ Failed to record {a_search_res}: {e}")
141
+ else:
142
+ if r:
143
+ res += record_repos_recursively(str(a_search_res), r=r)
144
+ return res
145
+
146
+
147
+ def main(repos_root: PathExtended):
148
+ print("\n📝 Recording repositories...")
149
+ repo_records = record_repos_recursively(repos_root=str(repos_root))
150
+ res: RepoRecordFile = {"version": "0.1", "repos": repo_records}
151
+
152
+ # Summary with warnings
153
+ total_repos = len(repo_records)
154
+ repos_with_no_remotes = [repo for repo in repo_records if len(repo["remotes"]) == 0]
155
+ repos_with_remotes = [repo for repo in repo_records if len(repo["remotes"]) > 0]
156
+ dirty_repos = [repo for repo in repo_records if repo["isDirty"]]
157
+ clean_repos = [repo for repo in repo_records if not repo["isDirty"]]
158
+
159
+ print("\n📊 Repository Summary:")
160
+ print(f" Total repositories found: {total_repos}")
161
+ print(f" Repositories with remotes: {len(repos_with_remotes)}")
162
+ print(f" Repositories without remotes: {len(repos_with_no_remotes)}")
163
+ print(f" Clean repositories: {len(clean_repos)}")
164
+ print(f" Dirty repositories: {len(dirty_repos)}")
165
+
166
+ if repos_with_no_remotes:
167
+ print(f"\n⚠️ WARNING: {len(repos_with_no_remotes)} repositories have no remotes configured:")
168
+ for repo in repos_with_no_remotes:
169
+ repo_path = PathExtended(repo["parentDir"]).joinpath(repo["name"])
170
+ print(f" • {repo['name']} ({repo_path})")
171
+ print(" These repositories may be local-only or have configuration issues.")
172
+ else:
173
+ print("\n✅ All repositories have remote configurations.")
174
+
175
+ if dirty_repos:
176
+ print(f"\n⚠️ WARNING: {len(dirty_repos)} repositories have uncommitted changes:")
177
+ for repo in dirty_repos:
178
+ repo_path = PathExtended(repo["parentDir"]).joinpath(repo["name"])
179
+ print(f" • {repo['name']} ({repo_path}) [branch: {repo['currentBranch']}]")
180
+ print(" These repositories have uncommitted changes that may need attention.")
181
+ else:
182
+ print("\n✅ All repositories are clean (no uncommitted changes).")
183
+
184
+ # Display repository tree structure
185
+ print("\n🌳 Repository Tree Structure:")
186
+ tree_structure = build_tree_structure(repo_records, repos_root)
187
+ print(tree_structure)
188
+
189
+ save_path = CONFIG_PATH.joinpath("repos").joinpath(repos_root.rel2home()).joinpath("repos.json")
190
+ save_json(obj=res, path=save_path, indent=4)
191
+ pprint(f"📁 Result saved at {PathExtended(save_path)}")
192
+ print(">>>>>>>>> Finished Recording")
193
+ return save_path
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m fire machineconfig.scripts.python.choose_wezterm_theme main2 $args[0]
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig choose_wezterm_theme $args[0]
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.cloud_copy $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_copy $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.cloud_mount $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_mount $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.cloud_repo_sync $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_repo_sync $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.cloud_sync $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_sync $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.croshell $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig croshell $args
@@ -1,29 +1 @@
1
-
2
- # ensure that python-output-script is deleted.
3
- $op_script = "~/tmp_results/shells/python_return_command.ps1"
4
- if (Test-Path $op_script ) {
5
- Remove-Item $op_script
6
- }
7
-
8
-
9
- . "$HOME\code\machineconfig\.venv\Scripts\activate.ps1"
10
-
11
- # Locate the python script to run relative to the current directory (which might be a symlink)
12
- $script_root = (Get-Item $PSScriptRoot).Target # resolves symlink if any
13
- if ( $script_root -eq $null) { # this does happen if a virtual enviroment is activated before running this script (don't know why)
14
- $script_root = $PSScriptRoot
15
- }
16
-
17
- python $script_root/../python/devops.py $args
18
-
19
- if (Test-Path $op_script ) {
20
- . $op_script
21
- }
22
- else
23
- {
24
- Write-Host "🤷‍♂️ No output script to be executed @ $op_script"
25
- }
26
-
27
-
28
- deactivate -ErrorAction SilentlyContinue
29
-
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig devops $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.dotfile $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig dotfile $args
@@ -1,45 +1 @@
1
-
2
- # param(
3
- # [switch]$IgnoreKeyboardInterrupt
4
- # )
5
-
6
- # Generate a random string of 10 characters
7
- $random_str = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 10 | ForEach-Object {[char]$_})
8
- $op_script = "$HOME\tmp_results\shells\$random_str\python_return_command.ps1"
9
- # Export the op_script variable to environment so python can access it
10
- $env:op_script = $op_script
11
-
12
- # Create directory if it doesn't exist
13
- $script_dir = Split-Path -Path $op_script -Parent
14
- if (-not (Test-Path $script_dir)) {
15
- New-Item -ItemType Directory -Path $script_dir -Force | Out-Null
16
- }
17
-
18
- # if (Test-Path $op_script ) {
19
- # Remove-Item $op_script
20
- # }
21
-
22
-
23
- # try {
24
- # # $null = & chafa --version
25
- # # & chafa "$HOME\code\machineconfig\assets\aafire.webp" --speed 2 --duration 1
26
- # # Chafa.exe "$HOME\code\machineconfig\assets\aafire.webp" --speed 2 --duration 1 --symbols ascii
27
- # Write-Host "🔥"
28
- # } catch {
29
- # Write-Host "Chafa not found, skipping."
30
- # }
31
-
32
-
33
- . "$HOME\code\machineconfig\.venv\Scripts\activate.ps1"
34
- python -m machineconfig.scripts.python.fire_jobs $args
35
-
36
-
37
- if (Test-Path $op_script ) {
38
- . $op_script
39
- }
40
- else
41
- {
42
- Write-Host "No output script to be executed at $op_script."
43
- }
44
-
45
- deactivate -ErrorAction SilentlyContinue
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig fire $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python $PSScriptRoot/../python/ftpx.py $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig ftpx $args
@@ -1,23 +1 @@
1
-
2
-
3
- . "$HOME\code\machineconfig\.venv\Scripts\activate.ps1"
4
-
5
- $op_script = "~/tmp_results/shells/python_return_command.ps1"
6
- if (Test-Path $op_script ) {
7
- Remove-Item $op_script
8
- }
9
-
10
-
11
- python -m machineconfig.scripts.python.chatgpt $args
12
-
13
- if (Test-Path $op_script ) {
14
- cat $op_script
15
- . $op_script
16
- }
17
- else
18
- {
19
- Write-Host "No output script to be executed @ $op_script"
20
- }
21
-
22
-
23
- deactivate
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig chatgpt $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -c "from machineconfig.utils.procs import ProcessManager; ProcessManager().choose_and_kill()"
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig kill_process $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.ai.mcinit $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig mcinit $args
@@ -7,7 +7,7 @@ $user = ''
7
7
  $sharePath = ''
8
8
  $driveLetter = ''
9
9
 
10
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.mount_ssh
10
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig python -m machineconfig.scripts.python.mount_ssh
11
11
 
12
12
  net use T: \\sshfs.kr\$user@$host.local
13
13
  # this worked: net use T: \\sshfs\alex@alex-p51s-5.local
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig -m fire machineconfig.scripts.python.pomodoro pomodoro $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig pomodoro $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.repos $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig repos $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.scheduler $Args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig scheduler $Args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python $PSScriptRoot/../python/snapshot.py $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig snapshot $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.start_slidev $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig start_slidev $args
@@ -1 +1 @@
1
- uv run --python 3.13 --with machineconfig python -m machineconfig.scripts.python.start_terminals $args
1
+ uv run --python 3.13 --no-dev --project $HOME/code/machineconfig start_terminals $args