machineconfig 2.6__py3-none-any.whl → 2.8__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.
- machineconfig/cluster/remote/remote_machine.py +0 -1
- machineconfig/cluster/sessions_managers/wt_local.py +1 -1
- machineconfig/cluster/sessions_managers/wt_local_manager.py +1 -1
- machineconfig/cluster/sessions_managers/wt_remote.py +1 -1
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +1 -1
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +1 -1
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +1 -1
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_local.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_remote.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +1 -1
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python/check_installations.py +0 -2
- machineconfig/jobs/python/vscode/sync_code.py +0 -1
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/profile/create.py +8 -16
- machineconfig/profile/shell.py +140 -179
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/linux/choose_wezterm_theme +1 -1
- machineconfig/scripts/linux/cloud_copy +1 -1
- machineconfig/scripts/linux/cloud_mount +1 -1
- machineconfig/scripts/linux/cloud_repo_sync +1 -1
- machineconfig/scripts/linux/cloud_sync +1 -1
- machineconfig/scripts/linux/croshell +2 -1
- machineconfig/scripts/linux/devops +1 -1
- machineconfig/scripts/linux/fire +1 -1
- machineconfig/scripts/linux/fire_agents +1 -1
- machineconfig/scripts/linux/ftpx +1 -1
- machineconfig/scripts/linux/gh_models +1 -1
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/linux/mcinit +1 -1
- machineconfig/scripts/linux/mount_nfs +1 -1
- machineconfig/scripts/linux/mount_nw_drive +1 -11
- machineconfig/scripts/linux/repos +1 -1
- machineconfig/scripts/linux/scheduler +1 -1
- machineconfig/scripts/linux/start_slidev +1 -1
- machineconfig/scripts/linux/start_terminals +1 -1
- machineconfig/scripts/linux/wifi_conn +1 -1
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_repo_sync.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos_helper_record.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/mcinit.py +16 -2
- machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +1 -1
- machineconfig/scripts/python/croshell.py +0 -1
- machineconfig/scripts/python/devops.py +1 -13
- machineconfig/scripts/python/devops_devapps_install.py +14 -2
- machineconfig/scripts/python/devops_update_repos.py +39 -19
- machineconfig/scripts/python/fire_agents.py +1 -1
- machineconfig/scripts/python/fire_jobs.py +8 -3
- machineconfig/scripts/python/fire_jobs_layout_helper.py +1 -1
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-313.pyc +0 -0
- machineconfig/scripts/python/repos.py +10 -227
- machineconfig/scripts/python/repos_helper_record.py +270 -0
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -1
- machineconfig/scripts/windows/cloud_copy.ps1 +1 -1
- machineconfig/scripts/windows/cloud_mount.ps1 +1 -1
- machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -1
- machineconfig/scripts/windows/cloud_sync.ps1 +1 -1
- machineconfig/scripts/windows/croshell.ps1 +1 -1
- machineconfig/scripts/windows/devops.ps1 +1 -29
- machineconfig/scripts/windows/dotfile.ps1 +1 -1
- machineconfig/scripts/windows/fire.ps1 +1 -45
- machineconfig/scripts/windows/ftpx.ps1 +1 -1
- machineconfig/scripts/windows/gpt.ps1 +1 -23
- machineconfig/scripts/windows/kill_process.ps1 +1 -1
- machineconfig/scripts/windows/mcinit.ps1 +1 -1
- machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/pomodoro.ps1 +1 -1
- machineconfig/scripts/windows/repos.ps1 +1 -1
- machineconfig/scripts/windows/scheduler.ps1 +1 -1
- machineconfig/scripts/windows/snapshot.ps1 +1 -1
- machineconfig/scripts/windows/start_slidev.ps1 +1 -1
- machineconfig/scripts/windows/start_terminals.ps1 +1 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +1 -2
- machineconfig/settings/shells/pwsh/init.ps1 +0 -4
- machineconfig/setup_linux/web_shortcuts/croshell.sh +1 -1
- machineconfig/setup_linux/web_shortcuts/interactive.sh +7 -13
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +9 -18
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -66
- machineconfig/utils/links.py +1 -2
- machineconfig/utils/options.py +8 -5
- machineconfig/utils/procs.py +50 -31
- machineconfig/utils/scheduling.py +0 -1
- machineconfig/{cluster/sessions_managers → utils/schemas/layouts}/layout_types.py +0 -1
- machineconfig/utils/schemas/repos/repos_types.py +28 -0
- machineconfig/utils/source_of_truth.py +1 -4
- machineconfig/utils/ssh.py +30 -8
- {machineconfig-2.6.dist-info → machineconfig-2.8.dist-info}/METADATA +1 -2
- {machineconfig-2.6.dist-info → machineconfig-2.8.dist-info}/RECORD +105 -112
- {machineconfig-2.6.dist-info → machineconfig-2.8.dist-info}/entry_points.txt +1 -1
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/profile/create_hardlinks.py +0 -140
- machineconfig/scripts/linux/checkout_versions +0 -2
- machineconfig/scripts/linux/cloud_manager +0 -2
- machineconfig/scripts/linux/url2md +0 -2
- machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
- machineconfig/scripts/windows/checkout_version.ps1 +0 -1
- machineconfig/scripts/windows/cloud_manager.ps1 +0 -1
- machineconfig/scripts/windows/neofetch.ps1 +0 -2
- machineconfig/scripts/windows/wsl_windows_transfer.ps1 +0 -1
- machineconfig/settings/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/settings/shells/ipy/profiles/default/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/settings/shells/ipy/profiles/default/startup/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/utils/ai/browser_user_wrapper.py +0 -66
- machineconfig/utils/ai/url2md.py +0 -85
- /machineconfig/setup_windows/wt_and_pwsh/{set_pwsh_theme.py → install_nerd_fonts.py} +0 -0
- {machineconfig-2.6.dist-info → machineconfig-2.8.dist-info}/WHEEL +0 -0
- {machineconfig-2.6.dist-info → machineconfig-2.8.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.
|
|
13
|
-
from machineconfig.
|
|
14
|
-
from machineconfig.scripts.python.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,270 @@
|
|
|
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
|
+
from rich.progress import Progress, TaskID, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn, MofNCompleteColumn
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_tree_structure(repos: list[RepoRecordDict], repos_root: PathExtended) -> str:
|
|
16
|
+
"""Build a tree structure representation of all repositories."""
|
|
17
|
+
if not repos:
|
|
18
|
+
return "No repositories found."
|
|
19
|
+
|
|
20
|
+
# Group repos by their parent directories relative to repos_root
|
|
21
|
+
tree_dict: dict[str, list[RepoRecordDict]] = {}
|
|
22
|
+
repos_root_abs = repos_root.expanduser().absolute()
|
|
23
|
+
|
|
24
|
+
for repo in repos:
|
|
25
|
+
parent_path = PathExtended(repo["parentDir"]).expanduser().absolute()
|
|
26
|
+
try:
|
|
27
|
+
relative_path = parent_path.relative_to(repos_root_abs)
|
|
28
|
+
relative_str = str(relative_path) if str(relative_path) != "." else ""
|
|
29
|
+
except ValueError:
|
|
30
|
+
# If the path is not relative to repos_root, use the full path
|
|
31
|
+
relative_str = str(parent_path)
|
|
32
|
+
|
|
33
|
+
if relative_str not in tree_dict:
|
|
34
|
+
tree_dict[relative_str] = []
|
|
35
|
+
tree_dict[relative_str].append(repo)
|
|
36
|
+
|
|
37
|
+
# Sort directories for consistent output
|
|
38
|
+
sorted_dirs = sorted(tree_dict.keys())
|
|
39
|
+
|
|
40
|
+
tree_lines: list[str] = []
|
|
41
|
+
tree_lines.append(f"📂 {repos_root.name}/ ({repos_root_abs})")
|
|
42
|
+
|
|
43
|
+
for i, dir_path in enumerate(sorted_dirs):
|
|
44
|
+
is_last_dir = i == len(sorted_dirs) - 1
|
|
45
|
+
dir_prefix = "└── " if is_last_dir else "├── "
|
|
46
|
+
|
|
47
|
+
if dir_path:
|
|
48
|
+
tree_lines.append(f"│ {dir_prefix}📁 {dir_path}/")
|
|
49
|
+
repo_prefix_base = "│ │ " if not is_last_dir else " "
|
|
50
|
+
else:
|
|
51
|
+
repo_prefix_base = "│ "
|
|
52
|
+
|
|
53
|
+
repos_in_dir = tree_dict[dir_path]
|
|
54
|
+
# Sort repos by name
|
|
55
|
+
repos_in_dir.sort(key=lambda x: x["name"])
|
|
56
|
+
|
|
57
|
+
for j, repo in enumerate(repos_in_dir):
|
|
58
|
+
is_last_repo = j == len(repos_in_dir) - 1
|
|
59
|
+
repo_prefix = f"{repo_prefix_base}└── " if is_last_repo else f"{repo_prefix_base}├── "
|
|
60
|
+
|
|
61
|
+
# Create status indicators
|
|
62
|
+
status_indicators = []
|
|
63
|
+
if repo["isDirty"]:
|
|
64
|
+
status_indicators.append("🔶 DIRTY")
|
|
65
|
+
if not repo["remotes"]:
|
|
66
|
+
status_indicators.append("⚠️ NO_REMOTE")
|
|
67
|
+
if repo["currentBranch"] == "DETACHED":
|
|
68
|
+
status_indicators.append("🔀 DETACHED")
|
|
69
|
+
|
|
70
|
+
status_str = f"[{' | '.join(status_indicators)}]" if status_indicators else "[✅ CLEAN]"
|
|
71
|
+
branch_info = f" ({repo['currentBranch']})" if repo['currentBranch'] != "DETACHED" else ""
|
|
72
|
+
|
|
73
|
+
# Build the base string without status
|
|
74
|
+
base_str = f"{repo_prefix}📦 {repo['name']}{branch_info}"
|
|
75
|
+
|
|
76
|
+
# Calculate padding to align status at 75 characters
|
|
77
|
+
target_width = 45
|
|
78
|
+
current_length = len(base_str)
|
|
79
|
+
padding = max(1, target_width - current_length) # At least 1 space
|
|
80
|
+
|
|
81
|
+
tree_lines.append(f"{base_str}{' ' * padding}{status_str}")
|
|
82
|
+
|
|
83
|
+
return "\n".join(tree_lines)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def record_a_repo(path: PathExtended, search_parent_directories: bool, preferred_remote: Optional[str]) -> RepoRecordDict:
|
|
87
|
+
from git.repo import Repo
|
|
88
|
+
|
|
89
|
+
repo = Repo(path, search_parent_directories=search_parent_directories) # get list of remotes using git python
|
|
90
|
+
repo_root = PathExtended(repo.working_dir).absolute()
|
|
91
|
+
# remotes: = {remote.name: remote.url for remote in repo.remotes}
|
|
92
|
+
remotes: list[RepoRemote] = [{"name": remote.name, "url": remote.url} for remote in repo.remotes]
|
|
93
|
+
if preferred_remote is not None:
|
|
94
|
+
if preferred_remote in [remote["name"] for remote in remotes]:
|
|
95
|
+
remotes = [remote for remote in remotes if remote["name"] == preferred_remote]
|
|
96
|
+
else:
|
|
97
|
+
print(f"⚠️ `{preferred_remote=}` not found in {remotes}.")
|
|
98
|
+
preferred_remote = None
|
|
99
|
+
try:
|
|
100
|
+
commit = repo.head.commit.hexsha
|
|
101
|
+
except ValueError: # look at https://github.com/gitpython-developers/GitPython/issues/1016
|
|
102
|
+
print(f"⚠️ Failed to get latest commit of {repo}")
|
|
103
|
+
commit = "UNKNOWN"
|
|
104
|
+
try:
|
|
105
|
+
current_branch = repo.head.reference.name # same as repo.active_branch.name
|
|
106
|
+
except TypeError:
|
|
107
|
+
print(f"⁉️ Failed to get current branch of {repo}. It is probably in a detached state.")
|
|
108
|
+
# current_branch = None
|
|
109
|
+
current_branch = "DETACHED"
|
|
110
|
+
|
|
111
|
+
# Check if repo is dirty (has uncommitted changes)
|
|
112
|
+
is_dirty = repo.is_dirty(untracked_files=True)
|
|
113
|
+
|
|
114
|
+
version_info: GitVersionInfo = {
|
|
115
|
+
"branch": current_branch,
|
|
116
|
+
"commit": commit
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
res: RepoRecordDict = {
|
|
120
|
+
"name": repo_root.name,
|
|
121
|
+
"parentDir": repo_root.parent.collapseuser().as_posix(),
|
|
122
|
+
"currentBranch": current_branch,
|
|
123
|
+
"remotes": remotes,
|
|
124
|
+
"version": version_info,
|
|
125
|
+
"isDirty": is_dirty
|
|
126
|
+
}
|
|
127
|
+
return res
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def count_git_repositories(repos_root: str, r: bool) -> int:
|
|
131
|
+
"""Count total git repositories for accurate progress tracking."""
|
|
132
|
+
path_obj = PathExtended(repos_root).expanduser().absolute()
|
|
133
|
+
if path_obj.is_file():
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
search_res = path_obj.search("*", files=False, folders=True)
|
|
137
|
+
count = 0
|
|
138
|
+
|
|
139
|
+
for a_search_res in search_res:
|
|
140
|
+
if a_search_res.joinpath(".git").exists():
|
|
141
|
+
count += 1
|
|
142
|
+
elif r:
|
|
143
|
+
count += count_git_repositories(str(a_search_res), r=r)
|
|
144
|
+
|
|
145
|
+
return count
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def count_total_directories(repos_root: str, r: bool) -> int:
|
|
149
|
+
"""Count total directories to scan for accurate progress tracking."""
|
|
150
|
+
path_obj = PathExtended(repos_root).expanduser().absolute()
|
|
151
|
+
if path_obj.is_file():
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
search_res = path_obj.search("*", files=False, folders=True)
|
|
155
|
+
count = len(search_res)
|
|
156
|
+
|
|
157
|
+
if r:
|
|
158
|
+
for a_search_res in search_res:
|
|
159
|
+
if not a_search_res.joinpath(".git").exists():
|
|
160
|
+
count += count_total_directories(str(a_search_res), r=r)
|
|
161
|
+
|
|
162
|
+
return count
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def record_repos_recursively(repos_root: str, r: bool, progress: Progress | None, scan_task_id: TaskID | None, process_task_id: TaskID | None) -> list[RepoRecordDict]:
|
|
166
|
+
path_obj = PathExtended(repos_root).expanduser().absolute()
|
|
167
|
+
if path_obj.is_file():
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
search_res = path_obj.search("*", files=False, folders=True)
|
|
171
|
+
res: list[RepoRecordDict] = []
|
|
172
|
+
|
|
173
|
+
for a_search_res in search_res:
|
|
174
|
+
if progress and scan_task_id:
|
|
175
|
+
progress.update(scan_task_id, description=f"Scanning: {a_search_res.name}")
|
|
176
|
+
|
|
177
|
+
if a_search_res.joinpath(".git").exists():
|
|
178
|
+
try:
|
|
179
|
+
if progress and process_task_id:
|
|
180
|
+
progress.update(process_task_id, description=f"Recording: {a_search_res.name}")
|
|
181
|
+
|
|
182
|
+
repo_record = record_a_repo(a_search_res, search_parent_directories=False, preferred_remote=None)
|
|
183
|
+
res.append(repo_record)
|
|
184
|
+
|
|
185
|
+
if progress and process_task_id:
|
|
186
|
+
progress.update(process_task_id, advance=1, description=f"Recorded: {repo_record['name']}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"⚠️ Failed to record {a_search_res}: {e}")
|
|
189
|
+
else:
|
|
190
|
+
if r:
|
|
191
|
+
res += record_repos_recursively(str(a_search_res), r=r, progress=progress, scan_task_id=scan_task_id, process_task_id=process_task_id)
|
|
192
|
+
|
|
193
|
+
if progress and scan_task_id:
|
|
194
|
+
progress.update(scan_task_id, advance=1)
|
|
195
|
+
|
|
196
|
+
return res
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main(repos_root: PathExtended):
|
|
200
|
+
print("\n📝 Recording repositories...")
|
|
201
|
+
|
|
202
|
+
# Count total directories and repositories for accurate progress tracking
|
|
203
|
+
print("🔍 Analyzing directory structure...")
|
|
204
|
+
total_dirs = count_total_directories(str(repos_root), r=True)
|
|
205
|
+
total_repos = count_git_repositories(str(repos_root), r=True)
|
|
206
|
+
print(f"📊 Found {total_dirs} directories to scan and {total_repos} git repositories to record")
|
|
207
|
+
|
|
208
|
+
# Setup progress bars
|
|
209
|
+
with Progress(
|
|
210
|
+
SpinnerColumn(),
|
|
211
|
+
TextColumn("[progress.description]{task.description}"),
|
|
212
|
+
BarColumn(),
|
|
213
|
+
MofNCompleteColumn(),
|
|
214
|
+
TimeElapsedColumn(),
|
|
215
|
+
) as progress:
|
|
216
|
+
scan_task = progress.add_task("Scanning directories...", total=total_dirs)
|
|
217
|
+
process_task = progress.add_task("Recording repositories...", total=total_repos)
|
|
218
|
+
|
|
219
|
+
repo_records = record_repos_recursively(
|
|
220
|
+
repos_root=str(repos_root),
|
|
221
|
+
r=True,
|
|
222
|
+
progress=progress,
|
|
223
|
+
scan_task_id=scan_task,
|
|
224
|
+
process_task_id=process_task
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
res: RepoRecordFile = {"version": "0.1", "repos": repo_records}
|
|
228
|
+
|
|
229
|
+
# Summary with warnings
|
|
230
|
+
total_repos = len(repo_records)
|
|
231
|
+
repos_with_no_remotes = [repo for repo in repo_records if len(repo["remotes"]) == 0]
|
|
232
|
+
repos_with_remotes = [repo for repo in repo_records if len(repo["remotes"]) > 0]
|
|
233
|
+
dirty_repos = [repo for repo in repo_records if repo["isDirty"]]
|
|
234
|
+
clean_repos = [repo for repo in repo_records if not repo["isDirty"]]
|
|
235
|
+
|
|
236
|
+
print("\n📊 Repository Summary:")
|
|
237
|
+
print(f" Total repositories found: {total_repos}")
|
|
238
|
+
print(f" Repositories with remotes: {len(repos_with_remotes)}")
|
|
239
|
+
print(f" Repositories without remotes: {len(repos_with_no_remotes)}")
|
|
240
|
+
print(f" Clean repositories: {len(clean_repos)}")
|
|
241
|
+
print(f" Dirty repositories: {len(dirty_repos)}")
|
|
242
|
+
|
|
243
|
+
if repos_with_no_remotes:
|
|
244
|
+
print(f"\n⚠️ WARNING: {len(repos_with_no_remotes)} repositories have no remotes configured:")
|
|
245
|
+
for repo in repos_with_no_remotes:
|
|
246
|
+
repo_path = PathExtended(repo["parentDir"]).joinpath(repo["name"])
|
|
247
|
+
print(f" • {repo['name']} ({repo_path})")
|
|
248
|
+
print(" These repositories may be local-only or have configuration issues.")
|
|
249
|
+
else:
|
|
250
|
+
print("\n✅ All repositories have remote configurations.")
|
|
251
|
+
|
|
252
|
+
if dirty_repos:
|
|
253
|
+
print(f"\n⚠️ WARNING: {len(dirty_repos)} repositories have uncommitted changes:")
|
|
254
|
+
for repo in dirty_repos:
|
|
255
|
+
repo_path = PathExtended(repo["parentDir"]).joinpath(repo["name"])
|
|
256
|
+
print(f" • {repo['name']} ({repo_path}) [branch: {repo['currentBranch']}]")
|
|
257
|
+
print(" These repositories have uncommitted changes that may need attention.")
|
|
258
|
+
else:
|
|
259
|
+
print("\n✅ All repositories are clean (no uncommitted changes).")
|
|
260
|
+
|
|
261
|
+
# Display repository tree structure
|
|
262
|
+
print("\n🌳 Repository Tree Structure:")
|
|
263
|
+
tree_structure = build_tree_structure(repo_records, repos_root)
|
|
264
|
+
print(tree_structure)
|
|
265
|
+
|
|
266
|
+
save_path = CONFIG_PATH.joinpath("repos").joinpath(repos_root.rel2home()).joinpath("repos.json")
|
|
267
|
+
save_json(obj=res, path=save_path, indent=4)
|
|
268
|
+
pprint(f"📁 Result saved at {PathExtended(save_path)}")
|
|
269
|
+
print(">>>>>>>>> Finished Recording")
|
|
270
|
+
return save_path
|
|
@@ -1 +1 @@
|
|
|
1
|
-
uv run --python 3.13 --
|
|
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 --
|
|
1
|
+
uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_copy $args
|
|
@@ -1 +1 @@
|
|
|
1
|
-
uv run --python 3.13 --
|
|
1
|
+
uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_mount $args
|
|
@@ -1 +1 @@
|
|
|
1
|
-
uv run --python 3.13 --
|
|
1
|
+
uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_repo_sync $args
|
|
@@ -1 +1 @@
|
|
|
1
|
-
uv run --python 3.13 --
|
|
1
|
+
uv run --python 3.13 --no-dev --project $HOME/code/machineconfig cloud_sync $args
|
|
@@ -1 +1 @@
|
|
|
1
|
-
uv run --python 3.13 --
|
|
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 --
|
|
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 --
|
|
1
|
+
uv run --python 3.13 --no-dev --project $HOME/code/machineconfig ftpx $args
|