machineconfig 5.19__py3-none-any.whl → 5.21__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.

@@ -19,8 +19,8 @@ for better user experience with checkbox selections.
19
19
 
20
20
  import sys
21
21
  from pathlib import Path
22
- from platform import system
23
22
  from typing import cast
23
+ import platform
24
24
 
25
25
  import questionary
26
26
  from questionary import Choice
@@ -80,14 +80,14 @@ def get_installation_choices() -> list[str]:
80
80
  Choice(value="retrieve_data", title="💾 Retrieve Data - Backup restoration", checked=False),
81
81
  ]
82
82
  # Add Windows-specific options
83
- if system() == "Windows":
83
+ if platform.system() == "Windows":
84
84
  choices.append(Choice(value="install_windows_desktop", title="💻 Install Windows Desktop Apps - Brave, Windows Terminal, PowerShell, VSCode (Windows only)", checked=False))
85
85
  selected = questionary.checkbox("Select the installation options you want to execute:", choices=choices, show_description=True).ask()
86
86
  return selected or []
87
87
 
88
88
 
89
89
  def execute_installations(selected_options: list[str]) -> None:
90
- if system() == "Windows":
90
+ if platform.system() == "Windows":
91
91
  from machineconfig import setup_windows as module
92
92
  script_path = Path(module.__file__).parent / "ve.ps1"
93
93
  run_shell_script(script_path.read_text(encoding="utf-8"))
@@ -109,26 +109,24 @@ def execute_installations(selected_options: list[str]) -> None:
109
109
  run_shell_script(". $HOME/.bashrc")
110
110
 
111
111
  if "upgrade_system" in selected_options:
112
- if system() == "Windows":
112
+ if platform.system() == "Windows":
113
113
  console.print("❌ System upgrade is not applicable on Windows via this script.", style="bold red")
114
- elif system() == "Linux":
114
+ elif platform.system() == "Linux":
115
115
  console.print(Panel("🔄 [bold magenta]SYSTEM UPDATE[/bold magenta]\n[italic]Package management[/italic]", border_style="magenta"))
116
116
  run_shell_script("sudo nala upgrade -y")
117
117
  else:
118
- console.print(f"❌ System upgrade not supported on {system()}.", style="bold red")
118
+ console.print(f"❌ System upgrade not supported on {platform.system()}.", style="bold red")
119
119
  if "install_repos" in selected_options:
120
120
  console.print(Panel("🐍 [bold green]PYTHON ENVIRONMENT[/bold green]\n[italic]Virtual environment setup[/italic]", border_style="green"))
121
- if system() == "Windows":
122
- from machineconfig import setup_windows as module
123
- script_path = Path(module.__file__).parent / "repos.ps1"
121
+ if platform.system() == "Windows":
122
+ from machineconfig.setup_windows import REPOS
124
123
  else:
125
- from machineconfig import setup_linux as module
126
- script_path = Path(module.__file__).parent / "repos.sh"
127
- run_shell_script(script_path.read_text(encoding="utf-8"))
124
+ from machineconfig.setup_linux import REPOS
125
+ run_shell_script(REPOS.read_text(encoding="utf-8"))
128
126
 
129
127
  if "install_ssh_server" in selected_options:
130
128
  console.print(Panel("🔒 [bold red]SSH SERVER[/bold red]\n[italic]Remote access setup[/italic]", border_style="red"))
131
- if system() == "Windows":
129
+ if platform.system() == "Windows":
132
130
  powershell_script = """Write-Host "🔧 Installing and configuring SSH server..."
133
131
  Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
134
132
  Start-Service sshd
@@ -35,7 +35,7 @@ CloudOption = Annotated[
35
35
 
36
36
 
37
37
 
38
- @app.command()
38
+ @app.command(no_args_is_help=True)
39
39
  def push(directory: DirectoryArgument = None,
40
40
  recursive: RecursiveOption = False,
41
41
  no_sync: NoSyncOption = False,
@@ -43,7 +43,7 @@ def push(directory: DirectoryArgument = None,
43
43
  """🚀 Push changes across repositories."""
44
44
  from machineconfig.scripts.python.repos_helper import git_operations
45
45
  git_operations(directory, pull=False, commit=False, push=True, recursive=recursive, no_sync=no_sync)
46
- @app.command()
46
+ @app.command(no_args_is_help=True)
47
47
  def pull(
48
48
  directory: DirectoryArgument = None,
49
49
  recursive: RecursiveOption = False,
@@ -52,7 +52,7 @@ def pull(
52
52
  """⬇️ Pull changes across repositories."""
53
53
  from machineconfig.scripts.python.repos_helper import git_operations
54
54
  git_operations(directory, pull=True, commit=False, push=False, recursive=recursive, no_sync=no_sync)
55
- @app.command()
55
+ @app.command(no_args_is_help=True)
56
56
  def commit(
57
57
  directory: DirectoryArgument = None,
58
58
  recursive: RecursiveOption = False,
@@ -61,8 +61,8 @@ def commit(
61
61
  """💾 Commit changes across repositories."""
62
62
  from machineconfig.scripts.python.repos_helper import git_operations
63
63
  git_operations(directory, pull=False, commit=True, push=False, recursive=recursive, no_sync=no_sync)
64
- @app.command()
65
- def all(
64
+ @app.command(no_args_is_help=True)
65
+ def cleanup(
66
66
  directory: DirectoryArgument = None,
67
67
  recursive: RecursiveOption = False,
68
68
  no_sync: NoSyncOption = False,
@@ -72,7 +72,7 @@ def all(
72
72
  git_operations(directory, pull=True, commit=True, push=True, recursive=recursive, no_sync=no_sync)
73
73
 
74
74
 
75
- @sync_app.command()
75
+ @sync_app.command(no_args_is_help=True)
76
76
  def capture(
77
77
  directory: DirectoryArgument = None,
78
78
  cloud: CloudOption = None,
@@ -86,7 +86,7 @@ def capture(
86
86
  from machineconfig.utils.path_extended import PathExtended
87
87
  if cloud is not None:
88
88
  PathExtended(save_path).to_cloud(rel2home=True, cloud=cloud)
89
- @sync_app.command()
89
+ @sync_app.command(no_args_is_help=True)
90
90
  def clone(
91
91
  directory: DirectoryArgument = None,
92
92
  cloud: CloudOption = None,
@@ -97,7 +97,7 @@ def clone(
97
97
  clone_from_specs(directory, cloud, checkout_branch_flag=False, checkout_commit_flag=False)
98
98
 
99
99
 
100
- @sync_app.command(name="checkout-to-commit")
100
+ @sync_app.command(name="checkout-to-commit", no_args_is_help=True)
101
101
  def checkout_command(
102
102
  directory: DirectoryArgument = None,
103
103
  cloud: CloudOption = None,
@@ -108,7 +108,7 @@ def checkout_command(
108
108
  clone_from_specs(directory, cloud, checkout_branch_flag=False, checkout_commit_flag=True)
109
109
 
110
110
 
111
- @sync_app.command(name="checkout-to-branch")
111
+ @sync_app.command(name="checkout-to-branch", no_args_is_help=True)
112
112
  def checkout_to_branch_command(
113
113
  directory: DirectoryArgument = None,
114
114
  cloud: CloudOption = None,
@@ -119,7 +119,7 @@ def checkout_to_branch_command(
119
119
  clone_from_specs(directory, cloud, checkout_branch_flag=True, checkout_commit_flag=False)
120
120
 
121
121
 
122
- @app.command()
122
+ @app.command(no_args_is_help=True)
123
123
  def analyze(
124
124
  directory: DirectoryArgument = None,
125
125
  ) -> None:
@@ -2,7 +2,7 @@
2
2
  from pathlib import Path
3
3
  from typing import Optional, Literal
4
4
  import typer
5
-
5
+ from machineconfig.scripts.python.sessions_multiprocess import create_from_function
6
6
 
7
7
  def balance_load(layout_path: Path = typer.Argument(..., help="Path to the layout.json file"),
8
8
  max_thresh: int = typer.Option(..., help="Maximum tabs per layout"),
@@ -69,7 +69,7 @@ def find_layout_file(layout_path: str, ) -> Path:
69
69
  return choice_file
70
70
 
71
71
 
72
- def launch(ctx: typer.Context,
72
+ def run(ctx: typer.Context,
73
73
  layout_path: Optional[str] = typer.Argument(None, help="Path to the layout.json file"),
74
74
  max_tabs: int = typer.Option(10, help="A Sanity checker that throws an error if any layout exceeds the maximum number of tabs to launch."),
75
75
  max_layouts: int = typer.Option(10, help="A Sanity checker that throws an error if the total number of layouts exceeds this number."),
@@ -133,14 +133,11 @@ def launch(ctx: typer.Context,
133
133
 
134
134
 
135
135
  def main_from_parser():
136
- layouts_app = typer.Typer(help="Layouts management subcommands")
137
- layouts_app.command("run")(launch)
138
- layouts_app.command("balance-load")(balance_load)
139
- import sys
140
- if len(sys.argv) == 1:
141
- layouts_app(["--help"])
142
- else:
143
- layouts_app()
136
+ layouts_app = typer.Typer(help="Layouts management subcommands", no_args_is_help=True)
137
+ layouts_app.command("create-from-function", no_args_is_help=True)(create_from_function)
138
+ layouts_app.command("run", no_args_is_help=True)(run)
139
+ layouts_app.command("balance-load", no_args_is_help=True)(balance_load)
140
+ layouts_app()
144
141
 
145
142
 
146
143
  if __name__ == "__main__":
@@ -0,0 +1,56 @@
1
+
2
+
3
+ from typing import Optional
4
+ from pathlib import Path
5
+ import typer
6
+
7
+
8
+ def create_from_function(
9
+ num_process: int = typer.Option(..., "--num-process", "-n", help="Number of parallel processes to run"),
10
+ path: str = typer.Option(".", "--path", "-p", help="Path to a Python or Shell script file or a directory containing such files"),
11
+ function: Optional[str] = typer.Option(None, "--function", "-f", help="Function to run from the Python file. If not provided, you will be prompted to choose."),
12
+ ):
13
+ from machineconfig.utils.ve import get_ve_activate_line, get_ve_path_and_ipython_profile
14
+ from machineconfig.utils.options import choose_from_options
15
+ from machineconfig.utils.path_helper import match_file_name, sanitize_path
16
+ from machineconfig.utils.path_extended import PathExtended
17
+ from machineconfig.utils.accessories import get_repo_root
18
+
19
+ path_obj = sanitize_path(path)
20
+ if not path_obj.exists():
21
+ suffixes = {".py"}
22
+ choice_file = match_file_name(sub_string=path, search_root=PathExtended.cwd(), suffixes=suffixes)
23
+ elif path_obj.is_dir():
24
+ from machineconfig.scripts.python.helpers.helpers4 import search_for_files_of_interest
25
+ print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
26
+ files = search_for_files_of_interest(path_obj)
27
+ print(f"🔍 Got #{len(files)} results.")
28
+ choice_file = choose_from_options(multi=False, options=files, fzf=True, msg="Choose one option")
29
+ choice_file = PathExtended(choice_file)
30
+ else:
31
+ choice_file = path_obj
32
+
33
+
34
+ repo_root = get_repo_root(Path(choice_file))
35
+ print(f"💾 Selected file: {choice_file}.\nRepo root: {repo_root}")
36
+ ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
37
+ if ipy_profile is None:
38
+ ipy_profile = "default"
39
+
40
+ _activate_ve_line = get_ve_activate_line(ve_root=ve_root_from_file or "$HOME/code/machineconfig/.venv")
41
+
42
+ # ========================= choosing function to run
43
+ if function is None or function.strip() == "":
44
+ from machineconfig.scripts.python.fire_jobs_route_helper import choose_function_or_lines
45
+ choice_function, choice_file, _kwargs_dict = choose_function_or_lines(choice_file, kwargs_dict={})
46
+ else:
47
+ choice_function = function
48
+
49
+ from machineconfig.cluster.sessions_managers.zellij_local import run_zellij_layout
50
+ from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
51
+ layout: LayoutConfig = {"layoutName": "fireNprocess", "layoutTabs": []}
52
+ for an_arg in range(num_process):
53
+ layout["layoutTabs"].append({"tabName": f"tab{an_arg}", "startDir": str(PathExtended.cwd()), "command": f"uv run -m fire {choice_file} {choice_function} --idx={an_arg} --idx_max={num_process}"})
54
+ print(layout)
55
+ run_zellij_layout(layout_config=layout)
56
+
@@ -8,39 +8,40 @@ from typing import TypedDict, Literal
8
8
  console = Console()
9
9
 
10
10
 
11
- class SymlinkResult(TypedDict):
12
- action: Literal[
13
- "already_linked",
14
- "relinking",
15
- "fixing_broken_link",
16
- "identical_files",
17
- "backing_up_source",
18
- "backing_up_target",
19
- "relinking_to_new_target",
20
- "moving_to_target",
21
- "new_link",
22
- "new_link_and_target",
23
- "linking",
24
- "error"
25
- ]
26
- details: str
11
+ ActionType = Literal[
12
+ "already_linked",
13
+ "relinking",
14
+ "fixing_broken_link",
15
+ "identical_files",
16
+ "backupConfigDefaultPath",
17
+ "backing_up_source",
18
+ "backing_up_target",
19
+ "relink2newSelfManagedPath",
20
+ "relinking_to_new_target",
21
+ "move2selfManagedPath",
22
+ "moving_to_target",
23
+ "new_link",
24
+ "newLinkAndSelfManagedPath",
25
+ "new_link_and_target",
26
+ "linking",
27
+ "copying",
28
+ "error"
29
+ ]
30
+
27
31
 
32
+ class OperationResult(TypedDict):
33
+ action: ActionType
34
+ details: str
28
35
 
29
- class CopyResult(TypedDict):
30
- action: Literal[
31
- "already_linked",
32
- "relinking",
33
- "fixing_broken_link",
34
- "backing_up_source",
35
- "backing_up_target",
36
- "relinking_to_new_target",
37
- "moving_to_target",
38
- "new_link",
39
- "new_link_and_target",
40
- "copying",
41
- "error"
42
- ]
36
+ class OperationRecord(TypedDict):
37
+ action: ActionType
43
38
  details: str
39
+ program: str
40
+ file_key: str
41
+ defaultPath: str
42
+ selfManaged: str
43
+ operation: str
44
+ status: str
44
45
 
45
46
 
46
47
  def files_are_identical(file1: PathExtended, file2: PathExtended) -> bool:
@@ -79,14 +80,24 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
79
80
  tmp_results_root.mkdir(parents=True, exist_ok=True)
80
81
  target_dirs_filtered.append((tmp_results_root, "tmp_results"))
81
82
 
83
+ links_dir = repo_root_obj.joinpath("links")
84
+ links_dir.mkdir(parents=True, exist_ok=True)
85
+
82
86
  for a_target_path, a_name in target_dirs_filtered:
83
- links_path = repo_root_obj.joinpath("links", a_name)
84
- links_path.symlink_to(target=a_target_path)
87
+ links_path = links_dir.joinpath(a_name)
88
+ if links_path.exists() or links_path.is_symlink():
89
+ if links_path.is_symlink() and links_path.resolve() == a_target_path.resolve():
90
+ continue
91
+ links_path.unlink(missing_ok=True)
92
+ try:
93
+ links_path.symlink_to(target=a_target_path)
94
+ except OSError as ex:
95
+ console.print(Panel(f"❌ Failed to create symlink {links_path} -> {a_target_path}: {ex}", title="Symlink Error", expand=False))
85
96
 
86
97
 
87
98
  def symlink_map(config_file_default_path: PathExtended, self_managed_config_file_path: PathExtended,
88
99
  on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"]
89
- ) -> SymlinkResult:
100
+ ) -> OperationResult:
90
101
  """helper function. creates a symlink from `config_file_default_path` to `self_managed_config_file_path`.
91
102
 
92
103
  Returns a dict with 'action' and 'details' keys describing what was done.
@@ -103,6 +114,10 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
103
114
  """
104
115
  config_file_default_path = PathExtended(config_file_default_path).expanduser().absolute()
105
116
  self_managed_config_file_path = PathExtended(self_managed_config_file_path).expanduser().absolute()
117
+
118
+ if config_file_default_path.resolve() == self_managed_config_file_path.resolve():
119
+ raise ValueError(f"config_file_default_path and self_managed_config_file_path resolve to the same location: {config_file_default_path.resolve()}")
120
+
106
121
  action_taken = ""
107
122
  details = ""
108
123
 
@@ -112,7 +127,7 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
112
127
  if config_file_default_path.is_symlink():
113
128
  # Check if symlink already points to correct target
114
129
  try:
115
- if config_file_default_path.readlink().resolve() == self_managed_config_file_path.resolve():
130
+ if config_file_default_path.resolve() == self_managed_config_file_path.resolve():
116
131
  # Case: config_file_default_path exists AND self_managed_config_file_path exists AND config_file_default_path is a symlink pointing to self_managed_config_file_path
117
132
  action_taken = "already_linked"
118
133
  details = "Symlink already correctly points to target"
@@ -135,7 +150,7 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
135
150
  if files_are_identical(config_file_default_path, self_managed_config_file_path):
136
151
  # Files are identical, just delete this and create symlink
137
152
  action_taken = "identical_files"
138
- details = "Files identical, removed source and will create symlink"
153
+ details = "Files identical, removed config_file_default_path and will create symlink"
139
154
  console.print(Panel(f"🔗 IDENTICAL FILES | Files are identical, deleting {config_file_default_path} and creating symlink to {self_managed_config_file_path}", title="Identical Files", expand=False))
140
155
  config_file_default_path.delete(sure=True)
141
156
  else:
@@ -156,13 +171,13 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
156
171
  self_managed_config_file_path.move(path=backup_name)
157
172
  config_file_default_path.move(path=self_managed_config_file_path)
158
173
  elif on_conflict == "overwriteDefaultPath":
159
- action_taken = "backing_up_source"
174
+ action_taken = "backupConfigDefaultPath"
160
175
  details = "Overwriting default path, creating symlink to self-managed config"
161
176
  console.print(Panel(f"📦 OVERWRITE DEFAULT | Deleting {config_file_default_path}, creating symlink to {self_managed_config_file_path}", title="Overwrite Default", expand=False))
162
177
  config_file_default_path.delete(sure=True)
163
178
  elif on_conflict == "backupDefaultPath":
164
179
  backup_name = f"{config_file_default_path}.orig_{randstr()}"
165
- action_taken = "backing_up_source"
180
+ action_taken = "backupConfigDefaultPath"
166
181
  details = f"Backed up default path to {backup_name}"
167
182
  console.print(Panel(f"📦 BACKUP DEFAULT | Moving {config_file_default_path} to {backup_name}, creating symlink to {self_managed_config_file_path}", title="Backup Default", expand=False))
168
183
  config_file_default_path.move(path=backup_name)
@@ -170,8 +185,8 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
170
185
  # self_managed_config_file_path doesn't exist
171
186
  if config_file_default_path.is_symlink():
172
187
  # Case: config_file_default_path exists AND self_managed_config_file_path doesn't exist AND config_file_default_path is a symlink (pointing anywhere)
173
- action_taken = "relinking_to_new_target"
174
- details = "Removed existing symlink, will create target and new symlink"
188
+ action_taken = "relink2newSelfManagedPath"
189
+ details = "Removed existing symlink, will create self_managed_config_file_path and new symlink"
175
190
  console.print(Panel(f"🔄 RELINKING | Updating symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Relinking", expand=False))
176
191
  config_file_default_path.delete(sure=True)
177
192
  # Create self_managed_config_file_path
@@ -179,8 +194,8 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
179
194
  self_managed_config_file_path.touch()
180
195
  else:
181
196
  # Case: config_file_default_path exists AND self_managed_config_file_path doesn't exist AND config_file_default_path is a concrete path
182
- action_taken = "moving_to_target"
183
- details = "Moved source to target location, will create symlink"
197
+ action_taken = "move2selfManagedPath"
198
+ details = "Moved config_file_default_path to self_managed_config_file_path location, will create symlink"
184
199
  console.print(Panel(f"📁 MOVING | Moving {config_file_default_path} to {self_managed_config_file_path}, then creating symlink", title="Moving", expand=False))
185
200
  config_file_default_path.move(path=self_managed_config_file_path)
186
201
  else:
@@ -188,22 +203,23 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
188
203
  if self_managed_config_file_path.exists():
189
204
  # Case: config_file_default_path doesn't exist AND self_managed_config_file_path exists
190
205
  action_taken = "new_link"
191
- details = "Creating new symlink to existing target"
206
+ details = "Creating new symlink to existing self_managed_config_file_path"
192
207
  console.print(Panel(f"🆕 NEW LINK | Creating new symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="New Link", expand=False))
193
208
  else:
194
209
  # Case: config_file_default_path doesn't exist AND self_managed_config_file_path doesn't exist
195
- action_taken = "new_link_and_target"
196
- details = "Creating target file and new symlink"
210
+ action_taken = "newLinkAndSelfManagedPath"
211
+ details = "Creating self_managed_config_file_path file and new symlink"
197
212
  console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {self_managed_config_file_path} and symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="New Link & Target", expand=False))
198
213
  self_managed_config_file_path.parent.mkdir(parents=True, exist_ok=True)
199
214
  self_managed_config_file_path.touch()
200
215
 
201
216
  # Create the symlink
202
217
  try:
203
- action_taken = action_taken or "linking"
204
- details = details or "Creating symlink"
205
- console.print(Panel(f"🔗 LINKING | Creating symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Linking", expand=False))
206
- PathExtended(config_file_default_path).symlink_to(target=self_managed_config_file_path, verbose=True, overwrite=True)
218
+ if not action_taken:
219
+ action_taken = "linking"
220
+ details = "Creating symlink"
221
+ console.print(Panel(f"🔗 LINKING | Creating symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Linking", expand=False))
222
+ PathExtended(config_file_default_path).symlink_to(target=self_managed_config_file_path, verbose=True, overwrite=False)
207
223
  return {"action": action_taken, "details": details}
208
224
  except Exception as ex:
209
225
  action_taken = "error"
@@ -212,87 +228,105 @@ def symlink_map(config_file_default_path: PathExtended, self_managed_config_file
212
228
  return {"action": action_taken, "details": details}
213
229
 
214
230
 
215
- def copy_map(config_file_default_path: PathExtended, self_managed_config_file_path: PathExtended, on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"]) -> CopyResult:
231
+ def copy_map(config_file_default_path: PathExtended, self_managed_config_file_path: PathExtended, on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"]) -> OperationResult:
216
232
  config_file_default_path = PathExtended(config_file_default_path).expanduser().absolute()
217
233
  self_managed_config_file_path = PathExtended(self_managed_config_file_path).expanduser().absolute()
234
+
235
+ if config_file_default_path.resolve() == self_managed_config_file_path.resolve():
236
+ raise ValueError(f"config_file_default_path and self_managed_config_file_path resolve to the same location: {config_file_default_path.resolve()}")
237
+
218
238
  action_taken = ""
219
239
  details = ""
220
240
 
221
- if config_file_default_path.exists():
222
- if self_managed_config_file_path.exists():
241
+ match (config_file_default_path.exists(), self_managed_config_file_path.exists()):
242
+ case (True, True):
243
+ # Both files exist
223
244
  if config_file_default_path.is_symlink():
224
245
  try:
225
- if config_file_default_path.readlink().resolve() == self_managed_config_file_path.resolve():
246
+ if config_file_default_path.resolve() == self_managed_config_file_path.resolve():
226
247
  action_taken = "already_linked"
227
- details = "Symlink already correctly points to target"
228
- console.print(Panel(f"✅ ALREADY LINKED | {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Already Linked", expand=False))
248
+ details = "File at default path is already a symlink to self-managed config"
249
+ console.print(Panel(f"✅ ALREADY CORRECT | {config_file_default_path} already points to {self_managed_config_file_path}", title="Already Correct", expand=False))
229
250
  return {"action": action_taken, "details": details}
230
251
  else:
231
252
  action_taken = "relinking"
232
- details = "Updated existing symlink to point to new target"
233
- console.print(Panel(f"🔄 RELINKING | Updating symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Relinking", expand=False))
253
+ details = "Removing symlink at default path that points elsewhere"
254
+ console.print(Panel(f"🔄 REMOVING SYMLINK | Removing symlink {config_file_default_path} (points elsewhere), will copy from {self_managed_config_file_path}", title="Removing Symlink", expand=False))
234
255
  config_file_default_path.delete(sure=True)
235
256
  except OSError:
236
257
  action_taken = "fixing_broken_link"
237
- details = "Removed broken symlink and will create new one"
238
- console.print(Panel(f"🔄 FIXING BROKEN LINK | Fixing broken symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Fixing Broken Link", expand=False))
258
+ details = "Removed broken symlink at default path"
259
+ console.print(Panel(f"🔄 FIXING BROKEN SYMLINK | Removing broken symlink {config_file_default_path}, will copy from {self_managed_config_file_path}", title="Fixing Broken Symlink", expand=False))
239
260
  config_file_default_path.delete(sure=True)
240
261
  else:
241
- if on_conflict == "throwError":
242
- raise RuntimeError(f"Conflict detected: {config_file_default_path} and {self_managed_config_file_path} both exist with different content")
243
- elif on_conflict == "overwriteSelfManaged":
244
- action_taken = "backing_up_target"
245
- details = "Overwriting self-managed config, moving default path to self-managed location"
246
- console.print(Panel(f"📦 OVERWRITE SELF-MANAGED | Deleting {self_managed_config_file_path}, moving {config_file_default_path} to {self_managed_config_file_path}", title="Overwrite Self-Managed", expand=False))
247
- self_managed_config_file_path.delete(sure=True)
248
- config_file_default_path.move(path=self_managed_config_file_path)
249
- elif on_conflict == "backupSelfManaged":
250
- backup_name = f"{self_managed_config_file_path}.orig_{randstr()}"
251
- action_taken = "backing_up_target"
252
- details = f"Backed up self-managed config to {backup_name}"
253
- console.print(Panel(f"📦 BACKUP SELF-MANAGED | Moving {self_managed_config_file_path} to {backup_name}, moving {config_file_default_path} to {self_managed_config_file_path}", title="Backup Self-Managed", expand=False))
254
- self_managed_config_file_path.move(path=backup_name)
255
- config_file_default_path.move(path=self_managed_config_file_path)
256
- elif on_conflict == "overwriteDefaultPath":
257
- action_taken = "backing_up_source"
258
- details = "Overwriting default path, creating symlink to self-managed config"
259
- console.print(Panel(f"📦 OVERWRITE DEFAULT | Deleting {config_file_default_path}, copying {self_managed_config_file_path}", title="Overwrite Default", expand=False))
262
+ # Check if files are identical first
263
+ if files_are_identical(config_file_default_path, self_managed_config_file_path):
264
+ # Files are identical, just delete this and proceed with copy
265
+ action_taken = "identical_files"
266
+ details = "Files identical, removed config_file_default_path and will copy"
267
+ console.print(Panel(f"🔗 IDENTICAL FILES | Files are identical, deleting {config_file_default_path} and copying from {self_managed_config_file_path}", title="Identical Files", expand=False))
260
268
  config_file_default_path.delete(sure=True)
261
- elif on_conflict == "backupDefaultPath":
262
- backup_name = f"{config_file_default_path}.orig_{randstr()}"
263
- action_taken = "backing_up_source"
264
- details = f"Backed up default path to {backup_name}"
265
- console.print(Panel(f"📦 BACKUP DEFAULT | Moving {config_file_default_path} to {backup_name}, copying {self_managed_config_file_path}", title="Backup Default", expand=False))
266
- config_file_default_path.move(path=backup_name)
267
- else:
269
+ else:
270
+ # Files are different, use on_conflict strategy
271
+ match on_conflict:
272
+ case "throwError":
273
+ raise RuntimeError(f"Conflict detected: {config_file_default_path} and {self_managed_config_file_path} both exist with different content")
274
+ case "overwriteSelfManaged":
275
+ action_taken = "backing_up_target"
276
+ details = "Overwriting self-managed config with default path content"
277
+ console.print(Panel(f"📦 OVERWRITE SELF-MANAGED | Deleting {self_managed_config_file_path}, moving {config_file_default_path} to {self_managed_config_file_path}", title="Overwrite Self-Managed", expand=False))
278
+ self_managed_config_file_path.delete(sure=True)
279
+ config_file_default_path.move(path=self_managed_config_file_path)
280
+ case "backupSelfManaged":
281
+ backup_name = f"{self_managed_config_file_path}.orig_{randstr()}"
282
+ action_taken = "backing_up_target"
283
+ details = f"Backed up self-managed config to {backup_name}"
284
+ console.print(Panel(f"📦 BACKUP SELF-MANAGED | Moving {self_managed_config_file_path} to {backup_name}, moving {config_file_default_path} to {self_managed_config_file_path}", title="Backup Self-Managed", expand=False))
285
+ self_managed_config_file_path.move(path=backup_name)
286
+ config_file_default_path.move(path=self_managed_config_file_path)
287
+ case "overwriteDefaultPath":
288
+ action_taken = "backupConfigDefaultPath"
289
+ details = "Overwriting default path with self-managed config"
290
+ console.print(Panel(f"📦 OVERWRITE DEFAULT | Deleting {config_file_default_path}, will copy from {self_managed_config_file_path}", title="Overwrite Default", expand=False))
291
+ config_file_default_path.delete(sure=True)
292
+ case "backupDefaultPath":
293
+ backup_name = f"{config_file_default_path}.orig_{randstr()}"
294
+ action_taken = "backupConfigDefaultPath"
295
+ details = f"Backed up default path to {backup_name}"
296
+ console.print(Panel(f"📦 BACKUP DEFAULT | Moving {config_file_default_path} to {backup_name}, will copy from {self_managed_config_file_path}", title="Backup Default", expand=False))
297
+ config_file_default_path.move(path=backup_name)
298
+ case (True, False):
299
+ # config_file_default_path exists, self_managed_config_file_path doesn't
268
300
  if config_file_default_path.is_symlink():
269
- action_taken = "relinking_to_new_target"
270
- details = "Removed existing symlink, will create target and new symlink"
271
- console.print(Panel(f"🔄 RELINKING | Updating symlink from {config_file_default_path} ➡️ {self_managed_config_file_path}", title="Relinking", expand=False))
301
+ action_taken = "relink2newSelfManagedPath"
302
+ details = "Removed existing symlink, will create self_managed_config_file_path and copy"
303
+ console.print(Panel(f"🔄 REMOVING SYMLINK | Removing symlink {config_file_default_path}, creating {self_managed_config_file_path}", title="Removing Symlink", expand=False))
272
304
  config_file_default_path.delete(sure=True)
273
305
  self_managed_config_file_path.parent.mkdir(parents=True, exist_ok=True)
274
306
  self_managed_config_file_path.touch()
275
307
  else:
276
- action_taken = "moving_to_target"
277
- details = "Moved source to target location, will copy"
278
- console.print(Panel(f"📁 MOVING | Moving {config_file_default_path} to {self_managed_config_file_path}, then copying", title="Moving", expand=False))
308
+ action_taken = "move2selfManagedPath"
309
+ details = "Moved config_file_default_path to self_managed_config_file_path location, will copy back"
310
+ console.print(Panel(f"📁 MOVING | Moving {config_file_default_path} to {self_managed_config_file_path}, then copying back", title="Moving", expand=False))
279
311
  config_file_default_path.move(path=self_managed_config_file_path)
280
- else:
281
- if self_managed_config_file_path.exists():
312
+ case (False, True):
313
+ # config_file_default_path doesn't exist, self_managed_config_file_path does
282
314
  action_taken = "new_link"
283
- details = "Copying existing target to source location"
284
- console.print(Panel(f"🆕 NEW LINK | Copying {self_managed_config_file_path} to {config_file_default_path}", title="New Link", expand=False))
285
- else:
286
- action_taken = "new_link_and_target"
287
- details = "Creating target file and copying to source"
288
- console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {self_managed_config_file_path} and copying to {config_file_default_path}", title="New Link & Target", expand=False))
315
+ details = "Copying existing self_managed_config_file_path to config_file_default_path location"
316
+ console.print(Panel(f"🆕 NEW COPY | Copying {self_managed_config_file_path} to {config_file_default_path}", title="New Copy", expand=False))
317
+ case (False, False):
318
+ # Neither exists
319
+ action_taken = "newLinkAndSelfManagedPath"
320
+ details = "Creating self_managed_config_file_path file and copying to config_file_default_path"
321
+ console.print(Panel(f"🆕 NEW FILE & COPY | Creating {self_managed_config_file_path} and copying to {config_file_default_path}", title="New File & Copy", expand=False))
289
322
  self_managed_config_file_path.parent.mkdir(parents=True, exist_ok=True)
290
323
  self_managed_config_file_path.touch()
291
324
 
292
325
  try:
293
- action_taken = action_taken or "copying"
294
- details = details or "Copying file"
295
- console.print(Panel(f"📋 COPYING | Copying {self_managed_config_file_path} to {config_file_default_path}", title="Copying", expand=False))
326
+ if not action_taken:
327
+ action_taken = "copying"
328
+ details = "Copying file"
329
+ console.print(Panel(f"📋 COPYING | Copying {self_managed_config_file_path} to {config_file_default_path}", title="Copying", expand=False))
296
330
  self_managed_config_file_path.copy(path=config_file_default_path, overwrite=True, verbose=True)
297
331
  return {"action": action_taken, "details": details}
298
332
  except Exception as ex:
@@ -300,3 +334,5 @@ def copy_map(config_file_default_path: PathExtended, self_managed_config_file_pa
300
334
  details = f"Failed to copy file: {str(ex)}"
301
335
  console.print(Panel(f"❌ ERROR | Failed at copying {self_managed_config_file_path} to {config_file_default_path}. Reason: {ex}", title="Error", expand=False))
302
336
  return {"action": action_taken, "details": details}
337
+
338
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 5.19
3
+ Version: 5.21
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0