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

@@ -4,6 +4,7 @@ This script Takes away all config files from the computer, place them in one dir
4
4
 
5
5
  """
6
6
 
7
+ from machineconfig.utils.links import OperationRecord
7
8
  from rich.console import Console
8
9
  from rich.panel import Panel
9
10
  from rich.pretty import Pretty
@@ -12,7 +13,7 @@ from rich.table import Table
12
13
 
13
14
  from machineconfig.utils.path_extended import PathExtended
14
15
  from machineconfig.utils.links import symlink_map, copy_map
15
- from machineconfig.utils.source_of_truth import LIBRARY_ROOT
16
+ from machineconfig.utils.source_of_truth import LIBRARY_ROOT, CONFIG_PATH
16
17
 
17
18
  import platform
18
19
  import subprocess
@@ -40,7 +41,6 @@ class Base(TypedDict):
40
41
  to_this: str
41
42
  contents: Optional[bool]
42
43
  copy: Optional[bool]
43
-
44
44
  class ConfigMapper(TypedDict):
45
45
  file_name: str
46
46
  config_file_default_path: str
@@ -74,31 +74,6 @@ def read_mapper() -> MapperFileData:
74
74
  return {"public": public, "private": private}
75
75
 
76
76
 
77
- class OperationRecord(TypedDict):
78
- program: str
79
- file_key: str
80
- source: str
81
- target: str
82
- operation: str
83
- action: Literal[
84
- "already_linked",
85
- "relinking",
86
- "fixing_broken_link",
87
- "identical_files",
88
- "backing_up_source",
89
- "backing_up_target",
90
- "relinking_to_new_target",
91
- "moving_to_target",
92
- "new_link",
93
- "new_link_and_target",
94
- "linking",
95
- "copying",
96
- "error"
97
- ]
98
- details: str
99
- status: str
100
-
101
-
102
77
  def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
103
78
  on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"],
104
79
  method: Literal["symlink", "copy"]
@@ -135,80 +110,104 @@ def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
135
110
  self_managed_config_file_path = PathExtended(a_mapper["self_managed_config_file_path"].replace("LIBRARY_ROOT", LIBRARY_ROOT.as_posix()))
136
111
 
137
112
  # Determine whether to use copy or symlink
138
- use_copy = method == "copy" or "copy" in a_mapper
113
+ use_copy = method == "copy" or a_mapper.get("copy", False)
139
114
 
140
115
  if "contents" in a_mapper:
141
- try:
142
- targets = list(self_managed_config_file_path.expanduser().search("*"))
143
- for a_target in targets:
116
+ targets = list(self_managed_config_file_path.expanduser().search("*"))
117
+ for a_target in targets:
118
+ operation_type = "contents_copy" if use_copy else "contents_symlink"
119
+ try:
144
120
  if use_copy:
145
121
  result = copy_map(config_file_default_path=config_file_default_path.joinpath(a_target.name), self_managed_config_file_path=a_target, on_conflict=on_conflict)
146
- operation_type = "contents_copy"
147
122
  else:
148
123
  result = symlink_map(config_file_default_path=config_file_default_path.joinpath(a_target.name), self_managed_config_file_path=a_target, on_conflict=on_conflict)
149
- operation_type = "contents_symlink"
150
124
  operation_records.append({
151
125
  "program": program_name,
152
126
  "file_key": a_mapper["file_name"],
153
- "source": str(config_file_default_path.joinpath(a_target.name)),
154
- "target": str(a_target),
127
+ "defaultPath": str(config_file_default_path.joinpath(a_target.name)),
128
+ "selfManaged": str(a_target),
155
129
  "operation": operation_type,
156
130
  "action": result["action"],
157
131
  "details": result["details"],
158
132
  "status": "success"
159
133
  })
160
- except Exception as ex:
161
- console.print(f" [red]Config error[/red]: {program_name} | {a_mapper['file_name']} | missing keys 'config_file_default_path ==> self_managed_config_file_path'. {ex}")
162
- operation_records.append({
163
- "program": program_name,
164
- "file_key": a_mapper["file_name"],
165
- "source": str(config_file_default_path),
166
- "target": str(self_managed_config_file_path),
167
- "operation": "contents_symlink" if not use_copy else "contents_copy",
168
- "action": "error",
169
- "details": f"Failed to process contents: {str(ex)}",
170
- "status": f"error: {str(ex)}"
171
- })
134
+ except ValueError as ex:
135
+ if "resolve to the same location" in str(ex):
136
+ operation_records.append({
137
+ "program": program_name,
138
+ "file_key": a_mapper["file_name"],
139
+ "defaultPath": str(config_file_default_path.joinpath(a_target.name)),
140
+ "selfManaged": str(a_target),
141
+ "operation": operation_type,
142
+ "action": "already_linked",
143
+ "details": "defaultPath and selfManaged resolve to same location - already correctly configured",
144
+ "status": "success"
145
+ })
146
+ else:
147
+ raise
148
+ except Exception as ex:
149
+ console.print(f"❌ [red]Config error[/red]: {program_name} | {a_mapper['file_name']} | {a_target.name}. {ex}")
150
+ operation_records.append({
151
+ "program": program_name,
152
+ "file_key": a_mapper["file_name"],
153
+ "defaultPath": str(config_file_default_path.joinpath(a_target.name)),
154
+ "selfManaged": str(a_target),
155
+ "operation": operation_type,
156
+ "action": "error",
157
+ "details": f"Failed to process contents: {str(ex)}",
158
+ "status": f"error: {str(ex)}"
159
+ })
172
160
  else:
161
+ operation_type = "copy" if use_copy else "symlink"
173
162
  try:
174
163
  if use_copy:
175
164
  result = copy_map(config_file_default_path=config_file_default_path, self_managed_config_file_path=self_managed_config_file_path, on_conflict=on_conflict)
176
- operation_type = "copy"
177
165
  else:
178
166
  result = symlink_map(config_file_default_path=config_file_default_path, self_managed_config_file_path=self_managed_config_file_path, on_conflict=on_conflict)
179
- operation_type = "symlink"
180
167
  operation_records.append({
181
168
  "program": program_name,
182
169
  "file_key": a_mapper["file_name"],
183
- "source": str(config_file_default_path),
184
- "target": str(self_managed_config_file_path),
170
+ "defaultPath": str(config_file_default_path),
171
+ "selfManaged": str(self_managed_config_file_path),
185
172
  "operation": operation_type,
186
173
  "action": result["action"],
187
174
  "details": result["details"],
188
175
  "status": "success"
189
176
  })
177
+ except ValueError as ex:
178
+ if "resolve to the same location" in str(ex):
179
+ operation_records.append({
180
+ "program": program_name,
181
+ "file_key": a_mapper["file_name"],
182
+ "defaultPath": str(config_file_default_path),
183
+ "selfManaged": str(self_managed_config_file_path),
184
+ "operation": operation_type,
185
+ "action": "already_linked",
186
+ "details": "defaultPath and selfManaged resolve to same location - already correctly configured",
187
+ "status": "success"
188
+ })
189
+ else:
190
+ raise
190
191
  except Exception as ex:
191
- console.print(f"❌ [red]Config error[/red]: {program_name} | {a_mapper['file_name']} | missing keys 'config_file_default_path ==> self_managed_config_file_path'. {ex}")
192
+ console.print(f"❌ [red]Config error[/red]: {program_name} | {a_mapper['file_name']} | {ex}")
192
193
  operation_records.append({
193
194
  "program": program_name,
194
195
  "file_key": a_mapper["file_name"],
195
- "source": str(config_file_default_path),
196
- "target": str(self_managed_config_file_path),
197
- "operation": "symlink" if not use_copy else "copy",
196
+ "defaultPath": str(config_file_default_path),
197
+ "selfManaged": str(self_managed_config_file_path),
198
+ "operation": operation_type,
198
199
  "action": "error",
199
- "details": f"Failed to create {'symlink' if not use_copy else 'copy'}: {str(ex)}",
200
+ "details": f"Failed to create {operation_type}: {str(ex)}",
200
201
  "status": f"error: {str(ex)}"
201
202
  })
202
203
 
203
204
  if program_name == "ssh" and system == "Linux": # permissions of ~/dotfiles/.ssh should be adjusted
204
205
  try:
205
206
  console.print("\n[bold]🔒 Setting secure permissions for SSH files...[/bold]")
206
- # run_shell_script("sudo chmod 600 $HOME/.ssh/*")
207
- # run_shell_script("sudo chmod 700 $HOME/.ssh")
208
-
209
- subprocess.run("chmod 700 ~/.ssh/", check=True)
210
- subprocess.run("chmod 700 ~/dotfiles/creds/.ssh/", check=True) # may require sudo
211
- subprocess.run("chmod 600 ~/dotfiles/creds/.ssh/*", check=True)
207
+ subprocess.run("chmod 700 $HOME/.ssh/", shell=True, check=True)
208
+ subprocess.run("chmod 700 $HOME/dotfiles/creds/.ssh/", shell=True, check=True)
209
+ subprocess.run("chmod 600 $HOME/dotfiles/creds/.ssh/*", shell=True, check=True)
210
+ subprocess.run("chmod 600 $HOME/.ssh/*", shell=True, check=True)
212
211
  console.print("[green]✅ SSH permissions set successfully[/green]")
213
212
  except Exception as e:
214
213
  ERROR_LIST.append(e)
@@ -224,8 +223,8 @@ def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
224
223
  table = Table(title="🔗 Symlink Operations Summary", show_header=True, header_style="bold magenta")
225
224
  table.add_column("Program", style="cyan", no_wrap=True)
226
225
  table.add_column("File Key", style="blue", no_wrap=True)
227
- table.add_column("Source", style="green")
228
- table.add_column("Target", style="yellow")
226
+ table.add_column("Default Path", style="green")
227
+ table.add_column("Self Managed", style="yellow")
229
228
  table.add_column("Operation", style="magenta", no_wrap=True)
230
229
  table.add_column("Action", style="red", no_wrap=True)
231
230
  table.add_column("Details", style="white")
@@ -237,8 +236,8 @@ def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
237
236
  table.add_row(
238
237
  record["program"],
239
238
  record["file_key"],
240
- record["source"],
241
- record["target"],
239
+ record["defaultPath"],
240
+ record["selfManaged"],
242
241
  record["operation"],
243
242
  f"[{action_style}]{record['action']}[/{action_style}]",
244
243
  record["details"],
@@ -247,6 +246,25 @@ def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
247
246
 
248
247
  console.print("\n")
249
248
  console.print(table)
249
+
250
+ # Export operation records to CSV
251
+ import csv
252
+ from datetime import datetime
253
+
254
+ csv_dir = PathExtended(CONFIG_PATH).joinpath("symlink_operations")
255
+ csv_dir.mkdir(parents=True, exist_ok=True)
256
+
257
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
258
+ csv_filename = f"symlink_operations_{timestamp}.csv"
259
+ csv_path = csv_dir.joinpath(csv_filename)
260
+
261
+ with open(csv_path, "w", newline="", encoding="utf-8") as csvfile:
262
+ fieldnames = ["program", "file_key", "defaultPath", "selfManaged", "operation", "action", "details", "status"]
263
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
264
+ writer.writeheader()
265
+ writer.writerows(operation_records)
266
+
267
+ console.print(f"\n[bold green]📊 Operations exported to CSV:[/bold green] [cyan]{csv_path}[/cyan]")
250
268
 
251
269
  if len(ERROR_LIST) > 0:
252
270
  console.print(
@@ -1,33 +1,32 @@
1
1
  """devops with emojis"""
2
2
 
3
- import machineconfig.utils.installer_utils.installer as installer_entry_point
4
3
  import machineconfig.scripts.python.share_terminal as share_terminal
5
4
  import machineconfig.scripts.python.repos as repos
6
-
7
- import machineconfig.profile.create_frontend as create_frontend
5
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUPS
8
6
  # import machineconfig.scripts.python.dotfile as dotfile_module
9
7
  import typer
10
- from typing import Literal, Annotated
8
+ from typing import Literal, Annotated, Optional, get_args
11
9
 
12
10
 
13
11
  app = typer.Typer(help="🛠️ DevOps operations", no_args_is_help=True)
14
- app.command(name="install", help="📦 Install essential packages")(installer_entry_point.main)
15
- app.add_typer(repos.app, name="repos", help="📁 Manage git repositories")
12
+ @app.command(no_args_is_help=True)
13
+ def install( which: Optional[str] = typer.Option(None, "--which", "-w", help="Comma-separated list of program names to install."),
14
+ group: Optional[PACKAGE_GROUPS] = typer.Option(None, "--group", "-g", help=f"Group name (one of {list(get_args(PACKAGE_GROUPS))})"),
15
+ interactive: bool = typer.Option(False, "--interactive", "-ia", help="Interactive selection of programs to install."),
16
+ ) -> None:
17
+ """📦 Install essential packages"""
18
+ import machineconfig.utils.installer_utils.installer as installer_entry_point
19
+ installer_entry_point.main(which=which, group=group, interactive=interactive)
16
20
 
17
21
 
22
+ app.add_typer(repos.app, name="repos", help="📁 Manage git repositories")
18
23
  config_apps = typer.Typer(help="⚙️ Configuration subcommands", no_args_is_help=True)
19
24
  app.add_typer(config_apps, name="config")
20
-
21
-
22
-
23
25
  app_data = typer.Typer(help="💾 Data subcommands", no_args_is_help=True)
24
26
  app.add_typer(app_data, name="data")
25
-
26
27
  nw_apps = typer.Typer(help="🔐 Network subcommands", no_args_is_help=True)
27
28
  nw_apps.command(name="share-terminal", help="📡 Share terminal via web browser")(share_terminal.main)
28
29
  app.add_typer(nw_apps, name="network")
29
-
30
-
31
30
  self_app = typer.Typer(help="🔄 SELF operations subcommands", no_args_is_help=True)
32
31
  app.add_typer(self_app, name="self")
33
32
 
@@ -46,17 +45,35 @@ def interactive():
46
45
  def status():
47
46
  """📊 STATUS of machine, shell profile, apps, symlinks, dotfiles, etc."""
48
47
  pass
48
+ @self_app.command()
49
+ def clone():
50
+ """📋 CLONE machienconfig locally for faster execution and nightly updates. """
51
+ import platform
52
+ from machineconfig.utils.code import run_shell_script
53
+ if platform.system() == "Windows":
54
+ from machineconfig.setup_windows import REPOS
55
+ else:
56
+ from machineconfig.setup_linux import REPOS
57
+ run_shell_script(REPOS.read_text(encoding="utf-8"))
49
58
 
50
59
 
51
60
  @config_apps.command(no_args_is_help=True)
52
- def private():
61
+ def private(method: Literal["symlink", "copy"] = typer.Option(..., "--method", "-m", help="Method to use for linking files"),
62
+ on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"] = typer.Option("throwError", "--on-conflict", "-o", help="Action to take on conflict"),
63
+ which: Optional[str] = typer.Option(None, "--which", "-w", help="Specific items to process"),
64
+ interactive: bool = typer.Option(False, "--interactive", "-ia", help="Run in interactive mode")):
53
65
  """🔗 Manage private configuration files."""
54
- create_frontend.main_private_from_parser()
66
+ import machineconfig.profile.create_frontend as create_frontend
67
+ create_frontend.main_private_from_parser(method=method, on_conflict=on_conflict, which=which, interactive=interactive)
55
68
 
56
69
  @config_apps.command(no_args_is_help=True)
57
- def public():
70
+ def public(method: Literal["symlink", "copy"] = typer.Option(..., "--method", "-m", help="Method to use for setting up the config file."),
71
+ on_conflict: Literal["throwError", "overwriteDefaultPath", "backupDefaultPath"] = typer.Option("throwError", "--on-conflict", "-o", help="Action to take on conflict"),
72
+ which: Optional[str] = typer.Option(None, "--which", "-w", help="Specific items to process"),
73
+ interactive: bool = typer.Option(False, "--interactive", "-ia", help="Run in interactive mode")):
58
74
  """🔗 Manage public configuration files."""
59
- create_frontend.main_public_from_parser()
75
+ import machineconfig.profile.create_frontend as create_frontend
76
+ create_frontend.main_public_from_parser(method=method, on_conflict=on_conflict, which=which, interactive=interactive)
60
77
 
61
78
  # @config_apps.command(no_args_is_help=True)
62
79
  # def dotfile():
@@ -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
@@ -142,7 +140,7 @@ Set-Service -Name sshd -StartupType 'Automatic'"""
142
140
  console.print("🔧 Configuring shell profile", style="bold cyan")
143
141
  try:
144
142
  from machineconfig.profile.shell import create_default_shell_profile
145
- create_default_shell_profile()
143
+ create_default_shell_profile(method="copy")
146
144
  console.print("✅ Shell profile configured successfully", style="bold green")
147
145
  except Exception as e:
148
146
  console.print(f"❌ Error configuring shell profile: {e}", style="bold red")
@@ -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:
@@ -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.18
3
+ Version: 5.20
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -90,7 +90,7 @@ machineconfig/jobs/windows/archive/openssh-server_copy-ssh-id.ps1,sha256=-7pElYi
90
90
  machineconfig/jobs/windows/msc/cli_agents.bat,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
91
  machineconfig/jobs/windows/msc/cli_agents.ps1,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  machineconfig/profile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
- machineconfig/profile/create.py,sha256=sV97Zl6Pag1CkOjaQS8OpCcBrbR87KQz6c_bYP7fiOY,12066
93
+ machineconfig/profile/create.py,sha256=n8AEpwWrGmzyWyIcFxTXQp8Ztw6MTHTjt1aPlsw7yhc,13974
94
94
  machineconfig/profile/create_frontend.py,sha256=RlrQgsAzvJA3KQInPpavLJ6XIEayIjjq4it6dSLQR08,3352
95
95
  machineconfig/profile/shell.py,sha256=B2dg2eV9fPMlMLlqjL3W91E0ZrEEg5qpJXmBax-Kx_M,10958
96
96
  machineconfig/profile/records/generic/shares.toml,sha256=FduDztfyQtZcr5bfx-RSKhEEweweQSWfVXkKWnx8hCY,143
@@ -146,7 +146,7 @@ machineconfig/scripts/python/cloud_sync.py,sha256=RWGpAfJ9fnN18yNBSgN44dzA38Hmd4
146
146
  machineconfig/scripts/python/count_lines.py,sha256=ZexMRsV70pe9fhLbGuens9EP5gCf078EwTDRHRZo5A0,15960
147
147
  machineconfig/scripts/python/count_lines_frontend.py,sha256=HlzPLU9_oJYqPNbnoQ0Hm4CuYy1UUlkZPcE5tFBSEbo,545
148
148
  machineconfig/scripts/python/croshell.py,sha256=zHUhOqWG81AOTeawZoDkpURnV1fAisY2lyZ0apvlmVY,6547
149
- machineconfig/scripts/python/devops.py,sha256=3EjUUOglL2DWYM1IuZJ6Eusn_FGyTa4GVDx8FKNCDcI,4407
149
+ machineconfig/scripts/python/devops.py,sha256=GeHPeRvQGajtgWZZ4U1itF4bPh0tFTQ49YYg7iPnLCk,6767
150
150
  machineconfig/scripts/python/devops_add_identity.py,sha256=wvjNgqsLmqD2SxbNCW_usqfp0LI-TDvcJJKGOWt2oFw,3775
151
151
  machineconfig/scripts/python/devops_add_ssh_key.py,sha256=BXB-9RvuSZO0YTbnM2azeABW2ngLW4SKhhAGAieMzfw,6873
152
152
  machineconfig/scripts/python/devops_backup_retrieve.py,sha256=JLJHmi8JmZ_qVTeMW-qBEAYGt1fmfWXzZ7Gm-Q-GDcU,5585
@@ -163,13 +163,13 @@ machineconfig/scripts/python/fire_jobs_streamlit_helper.py,sha256=47DEQpj8HBSa-_
163
163
  machineconfig/scripts/python/ftpx.py,sha256=QfQTp-6jQP6yxfbLc5sKxiMtTgAgc8sjN7d17_uLiZc,9400
164
164
  machineconfig/scripts/python/get_zellij_cmd.py,sha256=e35-18hoXM9N3PFbvbizfkNY_-63iMicieWE3TbGcCQ,576
165
165
  machineconfig/scripts/python/gh_models.py,sha256=3BLfW25mBRiPO5VKtVm-nMlKLv-PaZDw7mObajq6F6M,5538
166
- machineconfig/scripts/python/interactive.py,sha256=d4zI-3pvT66y9sJEgH3qXPt3BTh12Mr91bJFhjVv_CA,10125
166
+ machineconfig/scripts/python/interactive.py,sha256=FYb-bhn5GYDSER_l_w8YS_LwEI1aeczp3pF-3U2e3aQ,10038
167
167
  machineconfig/scripts/python/mount_nfs.py,sha256=aECrL64j9g-9rF49sVJAjGmzaoGgcMnl3g9v17kQF4c,3239
168
168
  machineconfig/scripts/python/mount_nw_drive.py,sha256=iru6AtnTyvyuk6WxlK5R4lDkuliVpPV5_uBTVVhXtjQ,1550
169
169
  machineconfig/scripts/python/mount_ssh.py,sha256=k2fKq3f5dKq_7anrFOlqvJoI_3U4EWNHLRZ1o3Lsy6M,2268
170
170
  machineconfig/scripts/python/onetimeshare.py,sha256=bmGsNnskym5OWfIhpOfZG5jq3m89FS0a6dF5Sb8LaZM,2539
171
171
  machineconfig/scripts/python/pomodoro.py,sha256=SPkfeoZGv8rylGiOyzQ7UK3aXZ3G2FIOuGkSuBUggOI,2019
172
- machineconfig/scripts/python/repos.py,sha256=IidAfUx6jFs4dB8Wjq8ems8mS8X8jYFgvEhtCYdLs-A,4917
172
+ machineconfig/scripts/python/repos.py,sha256=7OwnQVedmLVgDnIY-OJ9epa1AcmRoUptu8ihyRNe_u4,5105
173
173
  machineconfig/scripts/python/repos_helper.py,sha256=3jLdnNf1canpzi3JXiz5VA6UTUmLeNHuhjOWVl_thP0,3006
174
174
  machineconfig/scripts/python/repos_helper_action.py,sha256=sXeOw5uHaK2GJixYW8qU_PD24mruGcQ59uf68ELC76A,14846
175
175
  machineconfig/scripts/python/repos_helper_clone.py,sha256=9vGb9NCXT0lkerPzOJjmFfhU8LSzE-_1LDvjkhgnal0,5461
@@ -387,7 +387,7 @@ machineconfig/utils/accessories.py,sha256=W_9dLzjwNTW5JQk_pe3B2ijQ1nA2-8Kdg2r7VB
387
387
  machineconfig/utils/code.py,sha256=CaDMxAoOKkjeMCr0Zw-yH0ghfz-4yvf3q7dPHKvHzrg,5634
388
388
  machineconfig/utils/installer.py,sha256=FitnRR2xOekkAygXgptKWdHHGdbXImKR8PcbgPmiPXw,10738
389
389
  machineconfig/utils/io.py,sha256=ZXB3aataS1IZ_0WMcCRSmoN1nbkvEO-bWYcs-TpngqU,2872
390
- machineconfig/utils/links.py,sha256=pbMlxElovS6aMfjYx2pd8MAHjQtFuD13-RPTJYbh5xI,20067
390
+ machineconfig/utils/links.py,sha256=GQExBsMoxewOhwIrNdERuzk9HVKcmWgNUGO-RzPMS6M,22588
391
391
  machineconfig/utils/notifications.py,sha256=vvdsY5IX6XEiILTnt5lNyHxhCi0ljdGX2T_67VRfrG4,9009
392
392
  machineconfig/utils/options.py,sha256=vUO4Kej-vDOv64wHr2HNDyu6PATURpjd7xp6N8OOoJg,7083
393
393
  machineconfig/utils/path_extended.py,sha256=Xjdn2AVnB8p1jfNMNe2kJutVa5zGnFFJVGZbw-Bp_hg,53200
@@ -417,8 +417,8 @@ machineconfig/utils/schemas/fire_agents/fire_agents_input.py,sha256=pTxvLzIpD5RF
417
417
  machineconfig/utils/schemas/installer/installer_types.py,sha256=QClRY61QaduBPJoSpdmTIdgS9LS-RvE-QZ-D260tD3o,1214
418
418
  machineconfig/utils/schemas/layouts/layout_types.py,sha256=TcqlZdGVoH8htG5fHn1KWXhRdPueAcoyApppZsPAPto,2020
419
419
  machineconfig/utils/schemas/repos/repos_types.py,sha256=ECVr-3IVIo8yjmYmVXX2mnDDN1SLSwvQIhx4KDDQHBQ,405
420
- machineconfig-5.18.dist-info/METADATA,sha256=RRQwB-lj3IAjGHQM3sjVT1R8YoHzF0mbXglHpUb3Mjc,8030
421
- machineconfig-5.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
422
- machineconfig-5.18.dist-info/entry_points.txt,sha256=2afE1mw-o4MUlfxyX73SV02XaQI4SV_LdL2r6_CzhPU,1074
423
- machineconfig-5.18.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
424
- machineconfig-5.18.dist-info/RECORD,,
420
+ machineconfig-5.20.dist-info/METADATA,sha256=1nL_AlCqeKOVeHhyQXWYzCUg6dXrutMRVaYqL6bbE2I,8030
421
+ machineconfig-5.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
422
+ machineconfig-5.20.dist-info/entry_points.txt,sha256=2afE1mw-o4MUlfxyX73SV02XaQI4SV_LdL2r6_CzhPU,1074
423
+ machineconfig-5.20.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
424
+ machineconfig-5.20.dist-info/RECORD,,