machineconfig 6.83__py3-none-any.whl → 6.85__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 (42) hide show
  1. machineconfig/cluster/sessions_managers/wt_local.py +16 -221
  2. machineconfig/cluster/sessions_managers/wt_local_manager.py +33 -174
  3. machineconfig/cluster/sessions_managers/wt_remote_manager.py +39 -197
  4. machineconfig/cluster/sessions_managers/wt_utils/manager_persistence.py +52 -0
  5. machineconfig/cluster/sessions_managers/wt_utils/monitoring_helpers.py +50 -0
  6. machineconfig/cluster/sessions_managers/wt_utils/status_reporting.py +76 -0
  7. machineconfig/cluster/sessions_managers/wt_utils/wt_helpers.py +199 -0
  8. machineconfig/scripts/linux/mcfgs +1 -1
  9. machineconfig/scripts/linux/term +39 -0
  10. machineconfig/scripts/python/ai/vscode_tasks.py +7 -2
  11. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  12. machineconfig/scripts/python/fire_jobs.py +30 -65
  13. machineconfig/scripts/python/helpers_devops/cli_config.py +1 -1
  14. machineconfig/scripts/python/helpers_devops/cli_nw.py +50 -0
  15. machineconfig/scripts/python/helpers_devops/cli_self.py +3 -3
  16. machineconfig/scripts/python/helpers_devops/cli_utils.py +1 -1
  17. machineconfig/scripts/python/helpers_fire/helpers4.py +15 -0
  18. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
  19. machineconfig/scripts/python/nw/mount_nfs +1 -1
  20. machineconfig/scripts/python/nw/wifi_conn.py +1 -53
  21. machineconfig/scripts/python/terminal.py +110 -0
  22. machineconfig/scripts/python/utils.py +2 -0
  23. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  24. machineconfig/scripts/windows/term.ps1 +48 -0
  25. machineconfig/settings/shells/bash/init.sh +2 -0
  26. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  27. machineconfig/setup_linux/web_shortcuts/interactive.sh +2 -1
  28. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +2 -1
  29. machineconfig/utils/code.py +21 -14
  30. machineconfig/utils/installer.py +0 -1
  31. machineconfig/utils/options.py +12 -2
  32. machineconfig/utils/path_helper.py +1 -1
  33. machineconfig/utils/scheduling.py +0 -2
  34. machineconfig/utils/ssh.py +2 -2
  35. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/METADATA +1 -1
  36. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/RECORD +39 -35
  37. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/entry_points.txt +1 -0
  38. machineconfig/scripts/linux/other/share_smb +0 -1
  39. machineconfig/scripts/linux/warp-cli.sh +0 -122
  40. machineconfig/scripts/linux/z_ls +0 -104
  41. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/WHEEL +0 -0
  42. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/top_level.txt +0 -0
@@ -7,11 +7,7 @@ https://github.com/ruby9455/app_management/tree/main/app_management
7
7
 
8
8
  """
9
9
 
10
- import shlex
11
10
  import subprocess
12
- import random
13
- import string
14
- import json
15
11
  import platform
16
12
  from typing import Optional, Any
17
13
  from pathlib import Path
@@ -22,6 +18,14 @@ from rich.panel import Panel
22
18
  from rich.table import Table
23
19
 
24
20
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
21
+ from machineconfig.cluster.sessions_managers.wt_utils.wt_helpers import (
22
+ generate_random_suffix,
23
+ # escape_for_wt,
24
+ validate_layout_config,
25
+ generate_wt_command_string,
26
+ check_wt_session_status,
27
+ check_command_status,
28
+ )
25
29
 
26
30
  logging.basicConfig(level=logging.INFO)
27
31
  logger = logging.getLogger(__name__)
@@ -37,52 +41,11 @@ class WTLayoutGenerator:
37
41
  def __init__(self, layout_config: LayoutConfig, session_name: str):
38
42
  self.session_name: str = session_name
39
43
  self.layout_config: LayoutConfig = layout_config.copy()
40
- self.script_path: Optional[str] = None # Store the full path to the PowerShell script
41
-
42
- @staticmethod
43
- def _generate_random_suffix(length: int) -> str:
44
- """Generate a random string suffix for unique PowerShell script names."""
45
- return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
46
-
47
- @staticmethod
48
- def _parse_command(command: str) -> tuple[str, list[str]]:
49
- try:
50
- parts = shlex.split(command)
51
- if not parts:
52
- raise ValueError("Empty command provided")
53
- return parts[0], parts[1:] if len(parts) > 1 else []
54
- except ValueError as e:
55
- logger.error(f"Error parsing command '{command}': {e}")
56
- parts = command.split()
57
- return parts[0] if parts else "", parts[1:] if len(parts) > 1 else []
58
-
59
- @staticmethod
60
- def _escape_for_wt(text: str) -> str:
61
- """Escape text for use in Windows Terminal commands."""
62
- # Windows Terminal uses PowerShell-style escaping
63
- text = text.replace('"', '""') # Escape quotes for PowerShell
64
- if " " in text or ";" in text or "&" in text or "|" in text:
65
- return f'"{text}"'
66
- return text
67
-
68
-
69
-
70
- @staticmethod
71
- def _validate_layout_config(layout_config: LayoutConfig) -> None:
72
- """Validate layout configuration format and content."""
73
- if not layout_config["layoutTabs"]:
74
- raise ValueError("Layout must contain at least one tab")
75
- for tab in layout_config["layoutTabs"]:
76
- if not tab["tabName"].strip():
77
- raise ValueError(f"Invalid tab name: {tab['tabName']}")
78
- if not tab["command"].strip():
79
- raise ValueError(f"Invalid command for tab '{tab['tabName']}': {tab['command']}")
80
- if not tab["startDir"].strip():
81
- raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
44
+ self.script_path: Optional[str] = None
82
45
 
83
46
  def create_layout_file(self) -> bool:
84
47
  """Create Windows Terminal layout file and return success status."""
85
- WTLayoutGenerator._validate_layout_config(self.layout_config)
48
+ validate_layout_config(self.layout_config)
86
49
  tab_count = len(self.layout_config['layoutTabs'])
87
50
  layout_name = self.layout_config['layoutName']
88
51
  console.print(f"[bold cyan]📋 Creating Windows Terminal layout[/bold cyan] [bright_green]'{layout_name}' with {tab_count} tabs[/bright_green]")
@@ -90,17 +53,13 @@ class WTLayoutGenerator:
90
53
  for tab in self.layout_config['layoutTabs']:
91
54
  console.print(f" [yellow]→[/yellow] [bold]{tab['tabName']}[/bold] [dim]in[/dim] [blue]{tab['startDir']}[/blue]")
92
55
 
93
- # Generate Windows Terminal command
94
- wt_command = self._generate_wt_command_string(self.layout_config, self.session_name)
56
+ wt_command = generate_wt_command_string(self.layout_config, self.session_name)
95
57
 
96
- random_suffix = WTLayoutGenerator._generate_random_suffix(8)
97
- # Create PowerShell script content
58
+ random_suffix = generate_random_suffix(8)
98
59
  script_content = f"""# Windows Terminal layout for {self.session_name}
99
60
  # Generated with random suffix: {random_suffix}
100
61
  {wt_command}
101
62
  """
102
- # Write to file
103
- random_suffix = WTLayoutGenerator._generate_random_suffix(8)
104
63
  tmp_dir = Path(TMP_LAYOUT_DIR)
105
64
  tmp_dir.mkdir(parents=True, exist_ok=True)
106
65
  script_file = tmp_dir / f"wt_layout_{self.session_name}_{random_suffix}.ps1"
@@ -110,46 +69,10 @@ class WTLayoutGenerator:
110
69
  console.print(f"[bold green]✅ Layout created successfully:[/bold green] [cyan]{self.script_path}[/cyan]")
111
70
  return True
112
71
 
113
- def _generate_wt_command_string(self, layout_config: LayoutConfig, window_name: str) -> str:
114
- """Generate complete Windows Terminal command string."""
115
- # Build the complete Windows Terminal command
116
- command_parts = []
117
-
118
- for i, tab in enumerate(layout_config["layoutTabs"]):
119
- is_first = i == 0
120
-
121
- if is_first:
122
- # First tab: start with wt command and window name
123
- tab_parts = ["wt", "-w", WTLayoutGenerator._escape_for_wt(window_name)]
124
- else:
125
- # Subsequent tabs: use new-tab
126
- tab_parts = ["new-tab"]
127
-
128
- # Add common tab arguments
129
- tab_name = tab["tabName"]
130
- cwd = tab["startDir"]
131
- command = tab["command"]
132
-
133
- # Convert paths to Windows format if needed
134
- if cwd.startswith("~/"):
135
- cwd = cwd.replace("~/", f"{Path.home()}/")
136
- elif cwd == "~":
137
- cwd = str(Path.home())
138
-
139
- # Add arguments in the correct order
140
- tab_parts.extend(["-d", WTLayoutGenerator._escape_for_wt(cwd)])
141
- tab_parts.extend(["--title", WTLayoutGenerator._escape_for_wt(tab_name)])
142
- tab_parts.append(WTLayoutGenerator._escape_for_wt(command))
143
-
144
- command_parts.append(" ".join(tab_parts))
145
-
146
- # Join all tab commands with escaped semicolons for PowerShell
147
- return " `; ".join(command_parts)
148
-
149
72
  def get_wt_layout_preview(self, layout_config: LayoutConfig) -> str:
150
73
  """Generate preview of the Windows Terminal command that would be created."""
151
- WTLayoutGenerator._validate_layout_config(layout_config)
152
- return self._generate_wt_command_string(layout_config, "preview")
74
+ validate_layout_config(layout_config)
75
+ return generate_wt_command_string(layout_config, "preview")
153
76
 
154
77
  def check_all_commands_status(self) -> dict[str, dict[str, Any]]:
155
78
  if not self.layout_config:
@@ -159,137 +82,13 @@ class WTLayoutGenerator:
159
82
  status_report = {}
160
83
  for tab in self.layout_config["layoutTabs"]:
161
84
  tab_name = tab["tabName"]
162
- status_report[tab_name] = WTLayoutGenerator.check_command_status(tab_name, self.layout_config)
85
+ status_report[tab_name] = check_command_status(tab_name, self.layout_config)
163
86
 
164
87
  return status_report
165
88
 
166
- @staticmethod
167
- def check_wt_session_status(session_name: str) -> dict[str, Any]:
168
- try:
169
- # Simplified Windows Terminal process check
170
- ps_script = """
171
- try {
172
- $wtProcesses = Get-Process -Name 'WindowsTerminal' -ErrorAction SilentlyContinue
173
- if ($wtProcesses) {
174
- $processInfo = @()
175
- $wtProcesses | ForEach-Object {
176
- $info = @{
177
- "Id" = $_.Id
178
- "ProcessName" = $_.ProcessName
179
- "StartTime" = $_.StartTime.ToString()
180
- }
181
- $processInfo += $info
182
- }
183
- $processInfo | ConvertTo-Json -Depth 2
184
- }
185
- } catch {
186
- # No Windows Terminal processes found
187
- }
188
- """
189
-
190
- result = subprocess.run([POWERSHELL_CMD, "-Command", ps_script], capture_output=True, text=True, timeout=5)
191
-
192
- if result.returncode == 0:
193
- output = result.stdout.strip()
194
- if output and output != "":
195
- try:
196
- processes = json.loads(output)
197
- if not isinstance(processes, list):
198
- processes = [processes]
199
-
200
- # For simplicity, assume session exists if WT is running
201
- return {
202
- "wt_running": True,
203
- "session_exists": len(processes) > 0,
204
- "session_name": session_name,
205
- "all_windows": processes,
206
- "session_windows": processes # Simplified - assume all windows could be session windows
207
- }
208
- except Exception as e:
209
- return {"wt_running": True, "session_exists": False, "error": f"Failed to parse process info: {e}", "session_name": session_name}
210
- else:
211
- return {"wt_running": False, "session_exists": False, "session_name": session_name, "all_windows": []}
212
- else:
213
- return {"wt_running": False, "error": result.stderr, "session_name": session_name}
214
-
215
- except subprocess.TimeoutExpired:
216
- return {"wt_running": False, "error": "Timeout while checking Windows Terminal processes", "session_name": session_name}
217
- except FileNotFoundError:
218
- return {"wt_running": False, "error": f"PowerShell ({POWERSHELL_CMD}) not found in PATH", "session_name": session_name}
219
- except Exception as e:
220
- return {"wt_running": False, "error": str(e), "session_name": session_name}
221
-
222
- @staticmethod
223
- def check_command_status(tab_name: str, layout_config: LayoutConfig) -> dict[str, Any]:
224
- """Check if a command is running by looking for processes."""
225
- # Find the tab with the given name
226
- tab_config = None
227
- for tab in layout_config["layoutTabs"]:
228
- if tab["tabName"] == tab_name:
229
- tab_config = tab
230
- break
231
-
232
- if tab_config is None:
233
- return {"status": "unknown", "error": f"Tab '{tab_name}' not found in layout config", "running": False, "pid": None, "command": None}
234
-
235
- command = tab_config["command"]
236
-
237
- try:
238
- # Extract the primary executable name from command
239
- primary_cmd = command.split()[0] if command.strip() else ""
240
- if not primary_cmd:
241
- return {"status": "error", "error": "Empty command", "running": False, "command": command, "tab_name": tab_name}
242
-
243
- # Use a much simpler PowerShell script that just checks for process names
244
- ps_script = f"""
245
- try {{
246
- $processes = Get-Process -Name '{primary_cmd}' -ErrorAction SilentlyContinue
247
- if ($processes) {{
248
- $processes | ForEach-Object {{
249
- $procInfo = @{{
250
- "pid" = $_.Id
251
- "name" = $_.ProcessName
252
- "start_time" = $_.StartTime.ToString()
253
- }}
254
- Write-Output ($procInfo | ConvertTo-Json -Compress)
255
- }}
256
- }}
257
- }} catch {{
258
- # No processes found or other error
259
- }}
260
- """
261
-
262
- result = subprocess.run([POWERSHELL_CMD, "-Command", ps_script], capture_output=True, text=True, timeout=5)
263
-
264
- if result.returncode == 0:
265
- output_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
266
- matching_processes = []
267
-
268
- for line in output_lines:
269
- if line.startswith("{") and line.endswith("}"):
270
- try:
271
- proc_info = json.loads(line)
272
- matching_processes.append(proc_info)
273
- except json.JSONDecodeError:
274
- continue
275
-
276
- if matching_processes:
277
- return {"status": "running", "running": True, "processes": matching_processes, "command": command, "tab_name": tab_name}
278
- else:
279
- return {"status": "not_running", "running": False, "processes": [], "command": command, "tab_name": tab_name}
280
- else:
281
- return {"status": "error", "error": f"Command failed: {result.stderr}", "running": False, "command": command, "tab_name": tab_name}
282
-
283
- except subprocess.TimeoutExpired:
284
- logger.error(f"Timeout checking command status for tab '{tab_name}'")
285
- return {"status": "timeout", "error": "Timeout checking process status", "running": False, "command": command, "tab_name": tab_name}
286
- except Exception as e:
287
- logger.error(f"Error checking command status for tab '{tab_name}': {e}")
288
- return {"status": "error", "error": str(e), "running": False, "command": command, "tab_name": tab_name}
289
-
290
89
  def get_status_report(self) -> dict[str, Any]:
291
90
  """Get status report for this layout including Windows Terminal and commands."""
292
- wt_status = WTLayoutGenerator.check_wt_session_status(self.session_name or "default")
91
+ wt_status = check_wt_session_status(self.session_name or "default")
293
92
  commands_status = self.check_all_commands_status()
294
93
 
295
94
  running_count = sum(1 for status in commands_status.values() if status.get("running", False))
@@ -382,14 +181,11 @@ def run_wt_layout(layout_config: LayoutConfig) -> None:
382
181
  session_name = layout_config["layoutName"]
383
182
  generator = WTLayoutGenerator(layout_config=layout_config, session_name=session_name)
384
183
  generator.create_layout_file()
385
-
386
184
  if generator.script_path is None:
387
185
  raise RuntimeError("Script path was not set after creating layout file")
388
-
389
186
  # Execute the script
390
187
  cmd = f'powershell -ExecutionPolicy Bypass -File "{generator.script_path}"'
391
188
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
392
-
393
189
  if result.returncode == 0:
394
190
  print(f"Windows Terminal layout is running @ {layout_config['layoutName']}")
395
191
  else:
@@ -400,7 +196,6 @@ def run_wt_layout(layout_config: LayoutConfig) -> None:
400
196
  def run_command_in_wt_tab(command: str, tab_name: str, cwd: Optional[str]) -> str:
401
197
  """Create a command to run in a new Windows Terminal tab."""
402
198
  cwd_part = f'-d "{cwd}"' if cwd else ""
403
-
404
199
  return f"""
405
200
  echo "Creating new Windows Terminal tab: {tab_name}"
406
201
  wt new-tab --title "{tab_name}" {cwd_part} "{command}"
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  from datetime import datetime
3
- import json
4
- import uuid
5
3
  import logging
6
4
  import subprocess
7
5
  from pathlib import Path
@@ -9,9 +7,14 @@ from typing import Optional, Any
9
7
  from rich.console import Console
10
8
  from machineconfig.utils.scheduler import Scheduler
11
9
  from machineconfig.cluster.sessions_managers.wt_local import WTLayoutGenerator
10
+ from machineconfig.cluster.sessions_managers.wt_utils.wt_helpers import check_wt_session_status
12
11
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
13
- from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import (
14
- StartResult, GlobalSummary, ActiveSessionInfo
12
+ from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import StartResult, ActiveSessionInfo
13
+ from machineconfig.cluster.sessions_managers.wt_utils.manager_persistence import (
14
+ generate_session_id, save_json_file, load_json_file, list_saved_sessions_in_dir, delete_session_dir, ensure_session_dir_exists
15
+ )
16
+ from machineconfig.cluster.sessions_managers.wt_utils.status_reporting import (
17
+ print_global_summary, print_session_health_status, print_commands_status, calculate_session_summary, calculate_global_summary_from_status
15
18
  )
16
19
 
17
20
 
@@ -129,104 +132,29 @@ class WTLocalManager:
129
132
  return "\n".join(commands)
130
133
 
131
134
  def check_all_sessions_status(self) -> dict[str, dict[str, Any]]:
132
- """Check the status of all sessions and their commands."""
133
135
  status_report = {}
134
-
135
136
  for manager in self.managers:
136
137
  session_name = manager.session_name or "default"
137
-
138
- # Get session status
139
- session_status = WTLayoutGenerator.check_wt_session_status(session_name)
140
-
141
- # Get commands status for this session
138
+ session_status = check_wt_session_status(session_name)
142
139
  commands_status = manager.check_all_commands_status()
143
-
144
- # Calculate summary for this session
145
- running_count = sum(1 for status in commands_status.values() if status.get("running", False))
146
- total_count = len(commands_status)
147
-
148
- status_report[session_name] = {
149
- "session_status": session_status,
150
- "commands_status": commands_status,
151
- "summary": {"total_commands": total_count, "running_commands": running_count, "stopped_commands": total_count - running_count, "session_healthy": session_status.get("session_exists", False)},
152
- }
153
-
140
+ summary = calculate_session_summary(commands_status, session_status.get("session_exists", False))
141
+ status_report[session_name] = {"session_status": session_status, "commands_status": commands_status, "summary": summary}
154
142
  return status_report
155
143
 
156
- def get_global_summary(self) -> GlobalSummary:
157
- """Get a global summary across all sessions."""
144
+ def get_global_summary(self) -> dict[str, Any]:
158
145
  all_status = self.check_all_sessions_status()
159
-
160
- total_sessions = len(all_status)
161
- healthy_sessions = sum(1 for status in all_status.values() if status["summary"]["session_healthy"])
162
- total_commands = sum(status["summary"]["total_commands"] for status in all_status.values())
163
- total_running = sum(status["summary"]["running_commands"] for status in all_status.values())
164
-
165
- return {
166
- "total_sessions": total_sessions,
167
- "healthy_sessions": healthy_sessions,
168
- "unhealthy_sessions": total_sessions - healthy_sessions,
169
- "total_commands": total_commands,
170
- "running_commands": total_running,
171
- "stopped_commands": total_commands - total_running,
172
- "all_sessions_healthy": healthy_sessions == total_sessions,
173
- "all_commands_running": total_running == total_commands,
174
- }
146
+ return calculate_global_summary_from_status(all_status, include_remote_machines=False)
175
147
 
176
148
  def print_status_report(self) -> None:
177
- """Print a comprehensive status report for all sessions."""
178
149
  all_status = self.check_all_sessions_status()
179
150
  global_summary = self.get_global_summary()
180
-
181
- print("=" * 80)
182
- print("🖥️ WINDOWS TERMINAL LOCAL MANAGER STATUS REPORT")
183
- print("=" * 80)
184
-
185
- # Global summary
186
- print("🌐 GLOBAL SUMMARY:")
187
- print(f" Total sessions: {global_summary['total_sessions']}")
188
- print(f" Healthy sessions: {global_summary['healthy_sessions']}")
189
- print(f" Total commands: {global_summary['total_commands']}")
190
- print(f" Running commands: {global_summary['running_commands']}")
191
- print(f" All healthy: {'✅' if global_summary['all_sessions_healthy'] else '❌'}")
192
- print()
193
-
194
- # Per-session details
151
+ print_global_summary(global_summary, "WINDOWS TERMINAL LOCAL MANAGER STATUS REPORT")
195
152
  for session_name, status in all_status.items():
196
- session_status = status["session_status"]
197
- commands_status = status["commands_status"]
198
- summary = status["summary"]
199
-
200
153
  print(f"🪟 SESSION: {session_name}")
201
154
  print("-" * 60)
202
-
203
- # Session health
204
- if session_status.get("wt_running", False):
205
- if session_status.get("session_exists", False):
206
- session_windows = session_status.get("session_windows", [])
207
- all_windows = session_status.get("all_windows", [])
208
- print("✅ Windows Terminal is running")
209
- print(f" Session windows: {len(session_windows)}")
210
- print(f" Total WT windows: {len(all_windows)}")
211
- else:
212
- print("⚠️ Windows Terminal is running but no session windows found")
213
- else:
214
- print(f"❌ Windows Terminal session issue: {session_status.get('error', 'Unknown error')}")
215
-
216
- # Commands in this session
217
- print(f" Commands ({summary['running_commands']}/{summary['total_commands']} running):")
218
- for tab_name, cmd_status in commands_status.items():
219
- status_icon = "✅" if cmd_status.get("running", False) else "❌"
220
- cmd_text = cmd_status.get("command", "Unknown")[:50]
221
- if len(cmd_status.get("command", "")) > 50:
222
- cmd_text += "..."
223
- console.print(f" {status_icon} {tab_name}: {cmd_text}")
224
-
225
- if cmd_status.get("processes"):
226
- for proc in cmd_status["processes"][:2]: # Show first 2 processes
227
- console.print(f" [dim]└─[/dim] PID {proc['pid']}: {proc['name']}")
155
+ print_session_health_status(status["session_status"], remote_name=None)
156
+ print_commands_status(status["commands_status"], status["summary"])
228
157
  print()
229
-
230
158
  print("=" * 80)
231
159
 
232
160
  def run_monitoring_routine(self, wait_ms: int = 30000) -> None:
@@ -288,126 +216,59 @@ class WTLocalManager:
288
216
  sched.run(max_cycles=None)
289
217
 
290
218
  def save(self, session_id: Optional[str] = None) -> str:
291
- """Save the manager state to disk."""
292
219
  if session_id is None:
293
- session_id = str(uuid.uuid4())[:8]
294
-
295
- # Create session directory
220
+ session_id = generate_session_id()
296
221
  session_dir = TMP_SERIALIZATION_DIR / session_id
297
- session_dir.mkdir(parents=True, exist_ok=True)
298
-
299
- # Save the session2wt_tabs configuration
300
- config_file = session_dir / "session_layouts.json"
301
- text = json.dumps(self.session_layouts, indent=2, ensure_ascii=False)
302
- config_file.write_text(text, encoding="utf-8")
303
-
304
- # Save metadata
222
+ ensure_session_dir_exists(session_dir)
223
+ save_json_file(session_dir / "session_layouts.json", self.session_layouts, "session layouts")
305
224
  metadata = {"session_name_prefix": self.session_name_prefix, "created_at": str(datetime.now()), "num_managers": len(self.managers), "sessions": [item["layoutName"] for item in self.session_layouts], "manager_type": "WTLocalManager"}
306
- metadata_file = session_dir / "metadata.json"
307
- text = json.dumps(metadata, indent=2, ensure_ascii=False)
308
- metadata_file.write_text(text, encoding="utf-8")
309
-
310
- # Save each manager's state
225
+ save_json_file(session_dir / "metadata.json", metadata, "metadata")
311
226
  managers_dir = session_dir / "managers"
312
227
  managers_dir.mkdir(exist_ok=True)
313
-
314
228
  for i, manager in enumerate(self.managers):
315
229
  manager_data = {"session_name": manager.session_name, "layout_config": manager.layout_config, "script_path": manager.script_path}
316
- manager_file = managers_dir / f"manager_{i}_{manager.session_name}.json"
317
- text = json.dumps(manager_data, indent=2, ensure_ascii=False)
318
- manager_file.write_text(text, encoding="utf-8")
319
-
230
+ save_json_file(managers_dir / f"manager_{i}_{manager.session_name}.json", manager_data, f"manager {i}")
320
231
  logger.info(f"✅ Saved WTLocalManager session to: {session_dir}")
321
232
  return session_id
322
233
 
323
234
  @classmethod
324
235
  def load(cls, session_id: str) -> "WTLocalManager":
325
- """Load a saved manager state from disk."""
326
236
  session_dir = TMP_SERIALIZATION_DIR / session_id
327
-
328
237
  if not session_dir.exists():
329
238
  raise FileNotFoundError(f"Session directory not found: {session_dir}")
330
-
331
- # Load configuration
332
- config_file = session_dir / "session_layouts.json"
333
- if not config_file.exists():
334
- raise FileNotFoundError(f"Configuration file not found: {config_file}")
335
-
336
- text = config_file.read_text(encoding="utf-8")
337
- session_layouts = json.loads(text)
338
-
339
- # Load metadata
340
- metadata_file = session_dir / "metadata.json"
341
- session_name_prefix = "LocalWTMgr" # default fallback
342
- if metadata_file.exists():
343
- text = metadata_file.read_text(encoding="utf-8")
344
- metadata = json.loads(text)
345
- session_name_prefix = metadata.get("session_name_prefix", "LocalWTMgr")
346
-
347
- # Create new instance
239
+ loaded_data = load_json_file(session_dir / "session_layouts.json", "Configuration file")
240
+ session_layouts = loaded_data if isinstance(loaded_data, list) else [] # type: ignore[arg-type]
241
+ metadata_data = load_json_file(session_dir / "metadata.json", "Metadata file") if (session_dir / "metadata.json").exists() else {}
242
+ metadata = metadata_data if isinstance(metadata_data, dict) else {} # type: ignore[arg-type]
243
+ session_name_prefix = metadata.get("session_name_prefix", "LocalWTMgr") # type: ignore[union-attr]
348
244
  instance = cls(session_layouts=session_layouts, session_name_prefix=session_name_prefix)
349
-
350
- # Load saved manager states
351
245
  managers_dir = session_dir / "managers"
352
246
  if managers_dir.exists():
353
247
  instance.managers = []
354
- manager_files = sorted(managers_dir.glob("manager_*.json"))
355
-
356
- for manager_file in manager_files:
248
+ for manager_file in sorted(managers_dir.glob("manager_*.json")):
357
249
  try:
358
- text = manager_file.read_text(encoding="utf-8")
359
- manager_data = json.loads(text)
360
-
361
- # Recreate the manager
362
- manager = WTLayoutGenerator(layout_config=manager_data["layout_config"], session_name=manager_data["session_name"])
363
- manager.script_path = manager_data["script_path"]
364
-
250
+ loaded_manager_data = load_json_file(manager_file, "Manager data")
251
+ manager_data = loaded_manager_data if isinstance(loaded_manager_data, dict) else {} # type: ignore[arg-type]
252
+ manager = WTLayoutGenerator(layout_config=manager_data["layout_config"], session_name=manager_data["session_name"]) # type: ignore[typeddict-item]
253
+ manager.script_path = manager_data["script_path"] # type: ignore[typeddict-item]
365
254
  instance.managers.append(manager)
366
-
367
255
  except Exception as e:
368
256
  logger.warning(f"Failed to load manager from {manager_file}: {e}")
369
-
370
257
  logger.info(f"✅ Loaded WTLocalManager session from: {session_dir}")
371
258
  return instance
372
259
 
373
260
  @staticmethod
374
261
  def list_saved_sessions() -> list[str]:
375
- """List all saved session IDs."""
376
- if not TMP_SERIALIZATION_DIR.exists():
377
- return []
378
-
379
- sessions = []
380
- for item in TMP_SERIALIZATION_DIR.iterdir():
381
- if item.is_dir() and (item / "metadata.json").exists():
382
- sessions.append(item.name)
383
-
384
- return sorted(sessions)
262
+ return list_saved_sessions_in_dir(TMP_SERIALIZATION_DIR)
385
263
 
386
264
  @staticmethod
387
265
  def delete_session(session_id: str) -> bool:
388
- """Delete a saved session."""
389
- session_dir = TMP_SERIALIZATION_DIR / session_id
390
-
391
- if not session_dir.exists():
392
- logger.warning(f"Session directory not found: {session_dir}")
393
- return False
394
-
395
- try:
396
- import shutil
397
-
398
- shutil.rmtree(session_dir)
399
- logger.info(f"✅ Deleted session: {session_id}")
400
- return True
401
- except Exception as e:
402
- logger.error(f"Failed to delete session {session_id}: {e}")
403
- return False
266
+ return delete_session_dir(TMP_SERIALIZATION_DIR / session_id, session_id)
404
267
 
405
268
  def list_active_sessions(self) -> list[ActiveSessionInfo]:
406
- """List currently active Windows Terminal sessions managed by this instance."""
407
- active_sessions = []
269
+ active_sessions: list[ActiveSessionInfo] = []
408
270
 
409
271
  try:
410
- # Get all running Windows Terminal processes
411
272
  result = subprocess.run(
412
273
  ["powershell", "-Command", 'Get-Process -Name "WindowsTerminal" -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, StartTime, MainWindowTitle | ConvertTo-Json -Depth 2'], capture_output=True, text=True, timeout=10
413
274
  )
@@ -419,7 +280,6 @@ class WTLocalManager:
419
280
  if not isinstance(all_processes, list):
420
281
  all_processes = [all_processes]
421
282
 
422
- # Filter to only our managed sessions
423
283
  for manager in self.managers:
424
284
  session_name = manager.session_name
425
285
  session_windows = []
@@ -435,7 +295,6 @@ class WTLocalManager:
435
295
  "is_active": len(session_windows) > 0,
436
296
  "tab_count": len(manager.layout_config["layoutTabs"]) if manager.layout_config else 0,
437
297
  "tabs": [tab["tabName"] for tab in manager.layout_config["layoutTabs"]] if manager.layout_config else [],
438
- "windows": session_windows,
439
298
  }
440
299
  )
441
300