machineconfig 5.17__py3-none-any.whl → 5.18__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (53) hide show
  1. machineconfig/cluster/sessions_managers/wt_local.py +6 -1
  2. machineconfig/cluster/sessions_managers/wt_local_manager.py +4 -2
  3. machineconfig/cluster/sessions_managers/wt_remote_manager.py +4 -2
  4. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +4 -2
  5. machineconfig/cluster/sessions_managers/zellij_local_manager.py +1 -1
  6. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +3 -1
  7. machineconfig/profile/create.py +108 -140
  8. machineconfig/profile/create_frontend.py +58 -0
  9. machineconfig/profile/shell.py +45 -9
  10. machineconfig/scripts/python/ai/solutions/_shared.py +9 -1
  11. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +1 -1
  12. machineconfig/scripts/python/ai/solutions/generic.py +12 -2
  13. machineconfig/scripts/python/count_lines_frontend.py +1 -1
  14. machineconfig/scripts/python/devops.py +78 -49
  15. machineconfig/scripts/python/dotfile.py +14 -8
  16. machineconfig/scripts/python/interactive.py +3 -21
  17. machineconfig/scripts/python/share_terminal.py +1 -1
  18. machineconfig/setup_linux/__init__.py +11 -0
  19. machineconfig/setup_linux/{openssh_all.sh → ssh/openssh_all.sh} +1 -0
  20. machineconfig/setup_linux/web_shortcuts/interactive.sh +1 -1
  21. machineconfig/setup_windows/__init__.py +12 -0
  22. machineconfig/setup_windows/apps.ps1 +1 -0
  23. machineconfig/setup_windows/{openssh_all.ps1 → ssh/openssh_all.ps1} +5 -5
  24. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +1 -1
  25. machineconfig/utils/code.py +7 -4
  26. machineconfig/utils/files/dbms.py +355 -0
  27. machineconfig/utils/files/read.py +2 -2
  28. machineconfig/utils/installer.py +5 -5
  29. machineconfig/utils/links.py +128 -104
  30. machineconfig/utils/procs.py +4 -4
  31. machineconfig/utils/scheduler.py +10 -14
  32. {machineconfig-5.17.dist-info → machineconfig-5.18.dist-info}/METADATA +1 -1
  33. {machineconfig-5.17.dist-info → machineconfig-5.18.dist-info}/RECORD +41 -51
  34. machineconfig/scripts/windows/dotfile.ps1 +0 -1
  35. machineconfig/setup_linux/others/openssh-server_add_pub_key.sh +0 -57
  36. machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -11
  37. machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -52
  38. machineconfig/setup_windows/symlinks.ps1 +0 -5
  39. machineconfig/setup_windows/symlinks2linux.ps1 +0 -1
  40. machineconfig/setup_windows/web_shortcuts/all.ps1 +0 -18
  41. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +0 -36
  42. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +0 -16
  43. machineconfig/setup_windows/web_shortcuts/ssh.ps1 +0 -11
  44. machineconfig/setup_windows/wsl_refresh.ps1 +0 -8
  45. machineconfig/setup_windows/wt_and_pwsh.ps1 +0 -9
  46. /machineconfig/setup_linux/{openssh_wsl.sh → ssh/openssh_wsl.sh} +0 -0
  47. /machineconfig/setup_windows/{quirks.ps1 → others/power_options.ps1} +0 -0
  48. /machineconfig/setup_windows/{openssh-server.ps1 → ssh/openssh-server.ps1} +0 -0
  49. /machineconfig/setup_windows/{openssh-server_add-sshkey.ps1 → ssh/openssh-server_add-sshkey.ps1} +0 -0
  50. /machineconfig/setup_windows/{openssh-server_add_identity.ps1 → ssh/openssh-server_add_identity.ps1} +0 -0
  51. {machineconfig-5.17.dist-info → machineconfig-5.18.dist-info}/WHEEL +0 -0
  52. {machineconfig-5.17.dist-info → machineconfig-5.18.dist-info}/entry_points.txt +0 -0
  53. {machineconfig-5.17.dist-info → machineconfig-5.18.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,9 @@
2
2
  """
3
3
  Windows Terminal local layout generator and session manager.
4
4
  Equivalent to zellij_local.py but for Windows Terminal.
5
+
6
+ https://github.com/ruby9455/app_management/tree/main/app_management
7
+
5
8
  """
6
9
 
7
10
  import shlex
@@ -13,11 +16,13 @@ import platform
13
16
  from typing import Dict, List, Optional, Any
14
17
  from pathlib import Path
15
18
  import logging
19
+ from rich.console import Console
16
20
 
17
21
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
18
22
 
19
23
  logging.basicConfig(level=logging.INFO)
20
24
  logger = logging.getLogger(__name__)
25
+ console = Console()
21
26
  TMP_LAYOUT_DIR = Path.home().joinpath("tmp_results", "session_manager", "wt", "layout_manager")
22
27
 
23
28
  # Check if we're on Windows
@@ -341,7 +346,7 @@ try {{
341
346
  for proc in cmd_status["processes"][:2]: # Show first 2 processes
342
347
  pid = proc.get("pid", "Unknown")
343
348
  name = proc.get("name", "Unknown")
344
- print(f" └─ PID {pid}: {name}")
349
+ console.print(f" [dim]└─[/dim] PID {pid}: {name}")
345
350
  print()
346
351
 
347
352
  print("=" * 80)
@@ -6,6 +6,7 @@ import logging
6
6
  import subprocess
7
7
  from pathlib import Path
8
8
  from typing import TypedDict, Optional, Dict, List, Any
9
+ from rich.console import Console
9
10
  from machineconfig.utils.scheduler import Scheduler
10
11
  from machineconfig.cluster.sessions_managers.wt_local import WTLayoutGenerator
11
12
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
@@ -21,6 +22,7 @@ class WTSessionReport(TypedDict):
21
22
 
22
23
  logging.basicConfig(level=logging.INFO)
23
24
  logger = logging.getLogger(__name__)
25
+ console = Console()
24
26
 
25
27
  TMP_SERIALIZATION_DIR = Path.home().joinpath("tmp_results", "session_manager", "wt", "local_manager")
26
28
 
@@ -220,11 +222,11 @@ class WTLocalManager:
220
222
  cmd_text = cmd_status.get("command", "Unknown")[:50]
221
223
  if len(cmd_status.get("command", "")) > 50:
222
224
  cmd_text += "..."
223
- print(f" {status_icon} {tab_name}: {cmd_text}")
225
+ console.print(f" {status_icon} {tab_name}: {cmd_text}")
224
226
 
225
227
  if cmd_status.get("processes"):
226
228
  for proc in cmd_status["processes"][:2]: # Show first 2 processes
227
- print(f" └─ PID {proc['pid']}: {proc['name']}")
229
+ console.print(f" [dim]└─[/dim] PID {proc['pid']}: {proc['name']}")
228
230
  print()
229
231
 
230
232
  print("=" * 80)
@@ -4,6 +4,7 @@ import uuid
4
4
  import logging
5
5
  from pathlib import Path
6
6
  from typing import Optional, Any
7
+ from rich.console import Console
7
8
  from machineconfig.utils.scheduler import Scheduler
8
9
  from machineconfig.cluster.sessions_managers.wt_local import run_command_in_wt_tab
9
10
  from machineconfig.cluster.sessions_managers.wt_remote import WTRemoteLayoutGenerator
@@ -13,6 +14,7 @@ TMP_SERIALIZATION_DIR = Path.home().joinpath("tmp_results", "session_manager", "
13
14
 
14
15
  # Module-level logger to be used throughout this module
15
16
  logger = logging.getLogger(__name__)
17
+ console = Console()
16
18
 
17
19
 
18
20
  class WTSessionManager:
@@ -315,11 +317,11 @@ class WTSessionManager:
315
317
  cmd_text = cmd_status.get("command", "Unknown")[:50]
316
318
  if len(cmd_status.get("command", "")) > 50:
317
319
  cmd_text += "..."
318
- print(f" {status_icon} {tab_name}: {cmd_text}")
320
+ console.print(f" {status_icon} {tab_name}: {cmd_text}")
319
321
 
320
322
  if cmd_status.get("processes"):
321
323
  for proc in cmd_status["processes"][:2]: # Show first 2 processes
322
- print(f" └─ PID {proc.get('pid', 'Unknown')}: {proc.get('name', 'Unknown')}")
324
+ console.print(f" [dim]└─[/dim] PID {proc.get('pid', 'Unknown')}: {proc.get('name', 'Unknown')}")
323
325
  print()
324
326
 
325
327
  print("=" * 80)
@@ -5,11 +5,13 @@ Status reporting utilities for Windows Terminal layouts and sessions.
5
5
 
6
6
  import logging
7
7
  from typing import Dict, Any, List
8
+ from rich.console import Console
8
9
  from machineconfig.cluster.sessions_managers.wt_utils.process_monitor import WTProcessMonitor
9
10
  from machineconfig.cluster.sessions_managers.wt_utils.session_manager import WTSessionManager
10
11
  from machineconfig.utils.schemas.layouts.layout_types import TabConfig
11
12
 
12
13
  logger = logging.getLogger(__name__)
14
+ console = Console()
13
15
 
14
16
 
15
17
  class WTStatusReporter:
@@ -82,11 +84,11 @@ class WTStatusReporter:
82
84
  for proc in processes:
83
85
  pid = proc.get("pid", "Unknown")
84
86
  name = proc.get("name", "Unknown")
85
- print(f" └─ PID {pid}: {name}")
87
+ console.print(f" [dim]└─[/dim] PID {pid}: {name}")
86
88
 
87
89
  if len(cmd_status["processes"]) > 3:
88
90
  remaining = len(cmd_status["processes"]) - 3
89
- print(f" └─ ... and {remaining} more processes")
91
+ console.print(f" [dim]└─[/dim] ... and {remaining} more processes")
90
92
 
91
93
  if cmd_status.get("error"):
92
94
  print(f" Error: {cmd_status['error']}")
@@ -245,7 +245,7 @@ class ZellijLocalManager:
245
245
 
246
246
  if cmd_status.get("processes"):
247
247
  for proc in cmd_status["processes"][:2]: # Show first 2 processes
248
- print(f" └─ PID {proc['pid']}: {proc['name']} ({proc['status']})")
248
+ console.print(f" [dim]└─[/dim] PID {proc['pid']}: {proc['name']} ({proc['status']})")
249
249
  print()
250
250
 
251
251
  print("=" * 80)
@@ -5,11 +5,13 @@ Status reporting utilities for Zellij remote layouts.
5
5
 
6
6
  import logging
7
7
  from typing import Dict, Any
8
+ from rich.console import Console
8
9
  from machineconfig.cluster.sessions_managers.zellij_utils.process_monitor import ProcessMonitor
9
10
  from machineconfig.cluster.sessions_managers.zellij_utils.session_manager import SessionManager
10
11
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
11
12
 
12
13
  logger = logging.getLogger(__name__)
14
+ console = Console()
13
15
 
14
16
 
15
17
  class StatusReporter:
@@ -70,7 +72,7 @@ class StatusReporter:
70
72
  print(f"✅ {tab_name}: Running on {remote_name}")
71
73
  if cmd_status.get("processes"):
72
74
  for proc in cmd_status["processes"][:2]: # Show first 2 processes
73
- print(f" └─ PID {proc['pid']}: {proc['name']} ({proc['status']})")
75
+ console.print(f" [dim]└─[/dim] PID {proc['pid']}: {proc['name']} ({proc['status']})")
74
76
  else:
75
77
  print(f"❌ {tab_name}: Not running on {remote_name}")
76
78
  print(f" Command: {cmd_status.get('command', 'Unknown')}")
@@ -11,18 +11,15 @@ from rich.text import Text
11
11
  from rich.table import Table
12
12
 
13
13
  from machineconfig.utils.path_extended import PathExtended
14
- from machineconfig.utils.links import symlink_func, symlink_copy
15
- from machineconfig.utils.options import choose_from_options
16
- from machineconfig.utils.source_of_truth import LIBRARY_ROOT, REPO_ROOT
17
- from machineconfig.profile.shell import create_default_shell_profile
14
+ from machineconfig.utils.links import symlink_map, copy_map
15
+ from machineconfig.utils.source_of_truth import LIBRARY_ROOT
18
16
 
19
17
  import platform
20
- import os
21
- import ctypes
22
18
  import subprocess
23
19
  import tomllib
24
20
  from typing import Optional, Any, TypedDict, Literal
25
21
 
22
+
26
23
  system = platform.system() # Linux or Windows
27
24
  ERROR_LIST: list[Any] = [] # append to this after every exception captured.
28
25
  SYSTEM = system.lower()
@@ -38,10 +35,43 @@ def get_other_systems(current_system: str) -> list[str]:
38
35
  OTHER_SYSTEMS = get_other_systems(SYSTEM)
39
36
 
40
37
 
41
- class SymlinkMapper(TypedDict):
38
+ class Base(TypedDict):
42
39
  this: str
43
40
  to_this: str
44
41
  contents: Optional[bool]
42
+ copy: Optional[bool]
43
+
44
+ class ConfigMapper(TypedDict):
45
+ file_name: str
46
+ config_file_default_path: str
47
+ self_managed_config_file_path: str
48
+ contents: Optional[bool]
49
+ copy: Optional[bool]
50
+ class MapperFileData(TypedDict):
51
+ public: dict[str, list[ConfigMapper]]
52
+ private: dict[str, list[ConfigMapper]]
53
+ def read_mapper() -> MapperFileData:
54
+ mapper_data: dict[str, dict[str, Base]] = tomllib.loads(LIBRARY_ROOT.joinpath("profile/mapper.toml").read_text(encoding="utf-8"))
55
+ public: dict[str, list[ConfigMapper]] = {}
56
+ private: dict[str, list[ConfigMapper]] = {}
57
+ for program_key, program_map in mapper_data.items():
58
+ for file_name, file_base in program_map.items():
59
+ file_map: ConfigMapper = {
60
+ "file_name": file_name,
61
+ "config_file_default_path": file_base["this"],
62
+ "self_managed_config_file_path": file_base["to_this"],
63
+ "contents": file_base.get("contents"),
64
+ "copy": file_base.get("copy"),
65
+ }
66
+ if "LIBRARY_ROOT" in file_map["self_managed_config_file_path"]:
67
+ if program_key not in public:
68
+ public[program_key] = []
69
+ public[program_key].append(file_map)
70
+ else:
71
+ if program_key not in private:
72
+ private[program_key] = []
73
+ private[program_key].append(file_map)
74
+ return {"public": public, "private": private}
45
75
 
46
76
 
47
77
  class OperationRecord(TypedDict):
@@ -69,156 +99,113 @@ class OperationRecord(TypedDict):
69
99
  status: str
70
100
 
71
101
 
72
- def apply_mapper(choice: Optional[str], prioritize_to_this: bool):
73
- symlink_mapper: dict[str, dict[str, SymlinkMapper]] = tomllib.loads(LIBRARY_ROOT.joinpath("profile/mapper.toml").read_text(encoding="utf-8"))
74
- exclude: list[str] = [] # "wsl_linux", "wsl_windows"
102
+ def apply_mapper(mapper_data: dict[str, list[ConfigMapper]],
103
+ on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"],
104
+ method: Literal["symlink", "copy"]
105
+ ):
75
106
  operation_records: list[OperationRecord] = []
76
107
 
77
- program_keys_raw: list[str] = list(symlink_mapper.keys())
78
- program_keys: list[str] = []
79
- for program_key in program_keys_raw:
80
- if program_key in exclude or any([another_system in program_key for another_system in OTHER_SYSTEMS]):
81
- continue
82
- else:
83
- program_keys.append(program_key)
84
-
85
- program_keys.sort()
86
- if choice is None:
87
- choice_selected = choose_from_options(msg="Which symlink to create?", options=program_keys + ["all", "none(EXIT)"], default="none(EXIT)", fzf=True, multi=True)
88
- assert isinstance(choice_selected, list)
89
- if len(choice_selected) == 1 and choice_selected[0] == "none(EXIT)":
90
- return # terminate function.
91
- elif len(choice_selected) == 1 and choice_selected[0] == "all":
92
- choice_selected = "all" # i.e. program_keys = program_keys
93
- else:
94
- choice_selected = choice
95
-
96
- if isinstance(choice_selected, str):
97
- if str(choice_selected) == "all" and system == "Windows":
98
- if os.name == "nt":
99
- try:
100
- is_admin = ctypes.windll.shell32.IsUserAnAdmin()
101
- except Exception:
102
- is_admin = False
103
- else:
104
- is_admin = False
105
- if not is_admin:
106
- warning_body = "\n".join([
107
- "[bold yellow]Administrator privileges required[/]",
108
- "Run the terminal as admin and try again to avoid repeated elevation prompts.",
109
- ])
110
- console.print(
111
- Panel.fit(
112
- warning_body,
113
- title="⚠️ Permission Needed",
114
- border_style="yellow",
115
- padding=(1, 2),
116
- )
117
- )
118
- raise RuntimeError("Run terminal as admin and try again, otherwise, there will be too many popups for admin requests and no chance to terminate the program.")
119
- elif choice_selected == "all":
108
+ import os
109
+ if os.name == "nt":
110
+ import ctypes
111
+ try:
112
+ is_admin = ctypes.windll.shell32.IsUserAnAdmin()
113
+ except Exception:
114
+ is_admin = False
115
+ total_length = sum(len(item) for item in mapper_data.values())
116
+ if not is_admin and method == "symlink" and total_length > 5:
117
+ warning_body = "\n".join([
118
+ "[bold yellow]Administrator privileges required[/]",
119
+ "Run the terminal as admin and try again to avoid repeated elevation prompts.",
120
+ ])
120
121
  console.print(
121
- Panel(
122
- Pretty(program_keys),
123
- title="🔍 Processing All Program Keys",
124
- border_style="cyan",
122
+ Panel.fit(
123
+ warning_body,
124
+ title="⚠️ Permission Needed",
125
+ border_style="yellow",
125
126
  padding=(1, 2),
126
127
  )
127
128
  )
128
- pass # i.e. program_keys = program_keys
129
- else:
130
- program_keys = [choice_selected]
131
- else:
132
- program_keys = choice_selected
129
+ raise RuntimeError("Run terminal as admin and try again, otherwise, there will be too many popups for admin requests and no chance to terminate the program.")
133
130
 
134
- for program_key in program_keys:
135
- console.rule(f"🔄 Processing [bold]{program_key}[/] symlinks", style="cyan")
136
- for file_key, file_map in symlink_mapper[program_key].items():
137
- this = PathExtended(file_map["this"])
138
- to_this = PathExtended(file_map["to_this"].replace("REPO_ROOT", REPO_ROOT.as_posix()).replace("LIBRARY_ROOT", LIBRARY_ROOT.as_posix()))
131
+ for program_name, program_files in mapper_data.items():
132
+ console.rule(f"🔄 Processing [bold]{program_name}[/] symlinks", style="cyan")
133
+ for a_mapper in program_files:
134
+ config_file_default_path = PathExtended(a_mapper["config_file_default_path"])
135
+ self_managed_config_file_path = PathExtended(a_mapper["self_managed_config_file_path"].replace("LIBRARY_ROOT", LIBRARY_ROOT.as_posix()))
136
+
137
+ # Determine whether to use copy or symlink
138
+ use_copy = method == "copy" or "copy" in a_mapper
139
139
 
140
- if "contents" in file_map:
140
+ if "contents" in a_mapper:
141
141
  try:
142
- targets = list(to_this.expanduser().search("*"))
142
+ targets = list(self_managed_config_file_path.expanduser().search("*"))
143
143
  for a_target in targets:
144
- result = symlink_func(this=this.joinpath(a_target.name), to_this=a_target, prioritize_to_this=prioritize_to_this)
144
+ if use_copy:
145
+ 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
+ else:
148
+ 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"
145
150
  operation_records.append({
146
- "program": program_key,
147
- "file_key": file_key,
148
- "source": str(this.joinpath(a_target.name)),
151
+ "program": program_name,
152
+ "file_key": a_mapper["file_name"],
153
+ "source": str(config_file_default_path.joinpath(a_target.name)),
149
154
  "target": str(a_target),
150
- "operation": "contents_symlink",
155
+ "operation": operation_type,
151
156
  "action": result["action"],
152
157
  "details": result["details"],
153
158
  "status": "success"
154
159
  })
155
160
  except Exception as ex:
156
- console.print(f"❌ [red]Config error[/red]: {program_key} | {file_key} | missing keys 'this ==> to_this'. {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}")
157
162
  operation_records.append({
158
- "program": program_key,
159
- "file_key": file_key,
160
- "source": str(this),
161
- "target": str(to_this),
162
- "operation": "contents_symlink",
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",
163
168
  "action": "error",
164
169
  "details": f"Failed to process contents: {str(ex)}",
165
170
  "status": f"error: {str(ex)}"
166
- })
167
-
168
- elif "copy" in file_map:
169
- try:
170
- result = symlink_copy(this=this, to_this=to_this, prioritize_to_this=prioritize_to_this)
171
- operation_records.append({
172
- "program": program_key,
173
- "file_key": file_key,
174
- "source": str(this),
175
- "target": str(to_this),
176
- "operation": "copy",
177
- "action": result["action"],
178
- "details": result["details"],
179
- "status": "success"
180
- })
181
- except Exception as ex:
182
- console.print(f"❌ [red]Config error[/red]: {program_key} | {file_key} | {ex}")
183
- operation_records.append({
184
- "program": program_key,
185
- "file_key": file_key,
186
- "source": str(this),
187
- "target": str(to_this),
188
- "operation": "copy",
189
- "action": "error",
190
- "details": f"Failed to copy: {str(ex)}",
191
- "status": f"error: {str(ex)}"
192
- })
171
+ })
193
172
  else:
194
173
  try:
195
- result = symlink_func(this=this, to_this=to_this, prioritize_to_this=prioritize_to_this)
174
+ if use_copy:
175
+ 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
+ else:
178
+ 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"
196
180
  operation_records.append({
197
- "program": program_key,
198
- "file_key": file_key,
199
- "source": str(this),
200
- "target": str(to_this),
201
- "operation": "symlink",
181
+ "program": program_name,
182
+ "file_key": a_mapper["file_name"],
183
+ "source": str(config_file_default_path),
184
+ "target": str(self_managed_config_file_path),
185
+ "operation": operation_type,
202
186
  "action": result["action"],
203
187
  "details": result["details"],
204
188
  "status": "success"
205
189
  })
206
190
  except Exception as ex:
207
- console.print(f"❌ [red]Config error[/red]: {program_key} | {file_key} | missing keys 'this ==> to_this'. {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}")
208
192
  operation_records.append({
209
- "program": program_key,
210
- "file_key": file_key,
211
- "source": str(this),
212
- "target": str(to_this),
213
- "operation": "symlink",
193
+ "program": program_name,
194
+ "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",
214
198
  "action": "error",
215
- "details": f"Failed to create symlink: {str(ex)}",
199
+ "details": f"Failed to create {'symlink' if not use_copy else 'copy'}: {str(ex)}",
216
200
  "status": f"error: {str(ex)}"
217
201
  })
218
202
 
219
- if program_key == "ssh" and system == "Linux": # permissions of ~/dotfiles/.ssh should be adjusted
203
+ if program_name == "ssh" and system == "Linux": # permissions of ~/dotfiles/.ssh should be adjusted
220
204
  try:
221
205
  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
+
222
209
  subprocess.run("chmod 700 ~/.ssh/", check=True)
223
210
  subprocess.run("chmod 700 ~/dotfiles/creds/.ssh/", check=True) # may require sudo
224
211
  subprocess.run("chmod 600 ~/dotfiles/creds/.ssh/*", check=True)
@@ -280,24 +267,5 @@ def apply_mapper(choice: Optional[str], prioritize_to_this: bool):
280
267
  )
281
268
 
282
269
 
283
- def main_symlinks():
284
- console.print("")
285
- console.rule("[bold blue]🔗 CREATING SYMLINKS 🔗")
286
- apply_mapper(choice="all", prioritize_to_this=True)
287
-
288
-
289
- def main_profile():
290
- console.print("")
291
- console.rule("[bold green]🐚 CREATING SHELL PROFILE 🐚")
292
- create_default_shell_profile()
293
- console.print(
294
- Panel.fit(
295
- Text("✨ Configuration setup complete! ✨", justify="center"),
296
- title="Profile Setup",
297
- border_style="green",
298
- )
299
- )
300
-
301
-
302
270
  if __name__ == "__main__":
303
271
  pass
@@ -0,0 +1,58 @@
1
+
2
+ import typer
3
+ from typing import Optional, Literal
4
+ from pathlib import Path
5
+
6
+
7
+ def main_public_from_parser(method: Literal["symlink", "copy"] = typer.Option(..., help="Method to use for setting up the config file."),
8
+ on_conflict: Literal["throwError", "overwriteDefaultPath", "backupDefaultPath"] = typer.Option(..., help="Action to take on conflict"),
9
+ which: Optional[str] = typer.Option(None, help="Specific items to process"),
10
+ interactive: bool = typer.Option(False, help="Run in interactive mode")):
11
+ """Terminology:
12
+ SOURCE = Self-Managed-Config-File-Path
13
+ TARGET = Config-File-Default-Path
14
+ For public config files, the source always exists, because we know it comes from machineconfig repo."""
15
+ from machineconfig.profile.create import ConfigMapper, read_mapper
16
+ if method == "symlink":
17
+ machineconfig_repo_path = Path.home().joinpath("code/machineconfig")
18
+ if not machineconfig_repo_path.exists() or not machineconfig_repo_path.is_dir():
19
+ raise FileNotFoundError(f"machineconfig repo not found at {machineconfig_repo_path}. Cannot create symlinks to non-existing source files.")
20
+
21
+ mapper_full = read_mapper()["public"]
22
+ if which is None:
23
+ assert interactive is True
24
+ from machineconfig.utils.options import choose_from_options
25
+ items_chosen = choose_from_options(msg="Which symlink to create?", options=list(mapper_full.keys()), fzf=True, multi=True)
26
+ else:
27
+ assert interactive is False
28
+ if which == "all":
29
+ items_chosen = list(mapper_full.keys())
30
+ else:
31
+ items_chosen = which.split(",")
32
+ items_objections: dict[str, list[ConfigMapper]] = {item: mapper_full[item] for item in items_chosen if item in mapper_full}
33
+
34
+ from machineconfig.profile.create import apply_mapper
35
+ apply_mapper(mapper_data=items_objections, on_conflict=on_conflict, method=method)
36
+
37
+
38
+ def main_private_from_parser(method: Literal["symlink", "copy"] = typer.Option(..., help="Method to use for linking files"),
39
+ on_conflict: Literal["throwError", "overwriteSelfManaged", "backupSelfManaged", "overwriteDefaultPath", "backupDefaultPath"] = typer.Option("throwError", help="Action to take on conflict"),
40
+ which: Optional[str] = typer.Option(None, help="Specific items to process"),
41
+ interactive: bool = typer.Option(False, help="Run in interactive mode")):
42
+ from machineconfig.profile.create import ConfigMapper, read_mapper
43
+
44
+ mapper_full = read_mapper()["private"]
45
+ if which is None:
46
+ assert interactive is True
47
+ from machineconfig.utils.options import choose_from_options
48
+ items_chosen = choose_from_options(msg="Which symlink to create?", options=list(mapper_full.keys()), fzf=True, multi=True)
49
+ else:
50
+ assert interactive is False
51
+ if which == "all":
52
+ items_chosen = list(mapper_full.keys())
53
+ else:
54
+ items_chosen = which.split(",")
55
+ items_objections: dict[str, list[ConfigMapper]] = {item: mapper_full[item] for item in items_chosen if item in mapper_full}
56
+
57
+ from machineconfig.profile.create import apply_mapper
58
+ apply_mapper(mapper_data=items_objections, on_conflict=on_conflict, method=method)
@@ -1,7 +1,8 @@
1
1
  """shell"""
2
2
 
3
+ from typing import Literal
3
4
  from machineconfig.utils.path_extended import PathExtended
4
- from machineconfig.utils.source_of_truth import LIBRARY_ROOT
5
+ from machineconfig.utils.source_of_truth import LIBRARY_ROOT, CONFIG_PATH
5
6
 
6
7
  import platform
7
8
  import os
@@ -35,25 +36,60 @@ def get_shell_profile_path() -> PathExtended:
35
36
  return profile_path
36
37
 
37
38
 
38
- def create_default_shell_profile() -> None:
39
+ def create_default_shell_profile(method: Literal["copy", "reference"]) -> None:
40
+ if method == "reference":
41
+ machineconfig_repo_path = PathExtended.home().joinpath("code/machineconfig")
42
+ if not machineconfig_repo_path.exists() or not machineconfig_repo_path.is_dir():
43
+ raise FileNotFoundError(f"machineconfig repo not found at {machineconfig_repo_path}. Cannot create symlinks to non-existing source files.")
44
+
39
45
  shell_profile_path = get_shell_profile_path()
40
46
  shell_profile = shell_profile_path.read_text(encoding="utf-8")
47
+
41
48
  if system == "Windows":
42
- source = f""". {str(PathExtended(LIBRARY_ROOT).joinpath("settings/shells/pwsh/init.ps1").collapseuser()).replace("~", "$HOME")}"""
49
+ init_script = PathExtended(LIBRARY_ROOT).joinpath("settings/shells/pwsh/init.ps1")
50
+
51
+ init_script_copy_path = PathExtended(CONFIG_PATH).joinpath("profile/init.ps1").collapseuser()
52
+ init_script_copy_path.parent.mkdir(parents=True, exist_ok=True)
53
+ init_script.copy(path=init_script_copy_path, overwrite=True)
54
+
55
+ source_using_copy = f""". {str(init_script_copy_path).replace("~", "$HOME")}"""
56
+ source_using_reference = f""". {str(init_script.collapseuser()).replace("~", "$HOME")}"""
43
57
  else:
44
- source = f"""source {str(PathExtended(LIBRARY_ROOT).joinpath("settings/shells/bash/init.sh").collapseuser()).replace("~", "$HOME")}"""
45
- if source in shell_profile:
46
- console.print(Panel("🔄 PROFILE | Skipping init script sourcing - already present in profile", title="[bold blue]Profile[/bold blue]", border_style="blue"))
58
+ init_script = PathExtended(LIBRARY_ROOT).joinpath("settings/shells/bash/init.sh")
59
+ init_script_copy_path = PathExtended(CONFIG_PATH).joinpath("profile/init.sh").collapseuser()
60
+ init_script_copy_path.parent.mkdir(parents=True, exist_ok=True)
61
+ init_script.copy(path=init_script_copy_path, overwrite=True)
62
+
63
+ source_using_reference = f"""source {str(init_script.collapseuser()).replace("~", "$HOME")}"""
64
+ source_using_copy = f"""source {str(init_script_copy_path).replace("~", "$HOME")}"""
65
+
66
+ match method:
67
+ case "copy":
68
+ line_of_interest = source_using_copy
69
+ line_other = source_using_reference
70
+ case "reference":
71
+ line_of_interest = source_using_reference
72
+ line_other = source_using_copy
73
+
74
+ was_shell_updated = False
75
+ if line_other in shell_profile: # always remove this irrelevant line
76
+ shell_profile = shell_profile.replace(line_other, "")
77
+ was_shell_updated = True
78
+
79
+ if line_of_interest in shell_profile:
80
+ console.print(Panel("🔄 PROFILE | Skipping init script sourcing - already present in profile", title="[bold blue]Profile[/bold blue]", border_style="blue"))
47
81
  else:
48
82
  console.print(Panel("📝 PROFILE | Adding init script sourcing to profile", title="[bold blue]Profile[/bold blue]", border_style="blue"))
49
- shell_profile += "\n" + source + "\n"
83
+ shell_profile += "\n" + line_of_interest + "\n"
50
84
  if system == "Linux":
51
85
  result = subprocess.run(["cat", "/proc/version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
52
86
  if result.returncode == 0 and result.stdout:
53
87
  version_info = result.stdout.lower()
54
88
  if "microsoft" in version_info or "wsl" in version_info:
55
- shell_profile += "\ncd ~"
56
- console.print("📌 WSL detected - adding 'cd ~' to profile to avoid Windows filesystem")
89
+ shell_profile += "\ncd $HOME"
90
+ console.print("📌 WSL detected - adding 'cd $HOME' to profile to avoid Windows filesystem")
91
+ was_shell_updated = True
92
+ if was_shell_updated:
57
93
  shell_profile_path.parent.mkdir(parents=True, exist_ok=True)
58
94
  shell_profile_path.write_text(shell_profile, encoding="utf-8")
59
95
  console.print(Panel("✅ Profile updated successfully", title="[bold blue]Profile[/bold blue]", border_style="blue"))
@@ -2,4 +2,12 @@ from pathlib import Path
2
2
  from machineconfig.utils.source_of_truth import LIBRARY_ROOT
3
3
 
4
4
  def get_generic_instructions_path() -> Path:
5
- return LIBRARY_ROOT.joinpath("scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md")
5
+ path = LIBRARY_ROOT.joinpath("scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md")
6
+ text = path.read_text(encoding="utf-8")
7
+ import platform
8
+ if platform.system().lower() == "windows":
9
+ text = text.replace("bash", "powershell").replace(".sh", ".ps1")
10
+ import tempfile
11
+ temp_path = Path(tempfile.gettempdir()).joinpath("generic_instructions.md")
12
+ temp_path.write_text(data=text, encoding="utf-8")
13
+ return temp_path
@@ -34,7 +34,7 @@ applyTo: "**/*.py"
34
34
  * when finished, run a linting static analysis check against files you touched, Any fix any mistakes.
35
35
  * Please run `uv run -m pyright $file_touched` and address all issues. if `pyright is not there, first run `uv add pyright --dev`.
36
36
  * For all type checkers and linters, like mypy, pyright, pyrefly and pylint, there are config files at different levels of the repo all the way up to home directory level. You don't need to worry about them, just be mindful that they exist. The tools themselves will respect the configs therein.
37
- * If you want to run all linters and pycheckers agains the entire project to make sure everything is clean, I prepared a nice shell script, you can run it from the repo root as `./.scripts/lint_and_typecheck.sh`. It will produce markdown files that are you are meant to look at @ ./.linters/*.md
37
+ * If you want to run all linters and pycheckers agains the entire project to make sure everything is clean, I prepared a nice shell script, you can run it from the repo root as `./.scripts/lint_and_type_check.sh`. It will produce markdown files that are you are meant to look at @ ./.linters/*.md
38
38
 
39
39
  # General Programming Ethos:
40
40
  * Make sure all the code is rigorous, no lazy stuff.