machineconfig 3.0__py3-none-any.whl → 3.2__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 (59) hide show
  1. machineconfig/__init__.py +16 -17
  2. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +6 -6
  3. machineconfig/cluster/sessions_managers/wt_local.py +7 -7
  4. machineconfig/cluster/sessions_managers/wt_local_manager.py +8 -8
  5. machineconfig/cluster/sessions_managers/wt_remote.py +8 -8
  6. machineconfig/cluster/sessions_managers/wt_remote_manager.py +6 -6
  7. machineconfig/cluster/sessions_managers/zellij_local.py +156 -77
  8. machineconfig/cluster/sessions_managers/zellij_local_manager.py +18 -15
  9. machineconfig/cluster/sessions_managers/zellij_remote.py +14 -61
  10. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +15 -12
  11. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +9 -3
  12. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +2 -2
  13. machineconfig/cluster/sessions_managers/zellij_utils/monitoring_types.py +40 -67
  14. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +19 -17
  15. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -2
  16. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +1 -1
  17. machineconfig/jobs/python/python_ve_symlink.py +1 -1
  18. machineconfig/profile/create.py +8 -0
  19. machineconfig/scripts/linux/kill_process +1 -1
  20. machineconfig/scripts/python/ai/mcinit.py +1 -2
  21. machineconfig/scripts/python/cloud_mount.py +1 -1
  22. machineconfig/scripts/python/cloud_repo_sync.py +1 -0
  23. machineconfig/scripts/python/cloud_sync.py +1 -0
  24. machineconfig/scripts/python/croshell.py +1 -0
  25. machineconfig/scripts/python/devops.py +11 -1
  26. machineconfig/scripts/python/devops_add_identity.py +1 -0
  27. machineconfig/scripts/python/devops_add_ssh_key.py +1 -0
  28. machineconfig/scripts/python/devops_backup_retrieve.py +1 -0
  29. machineconfig/scripts/python/devops_devapps_install.py +5 -7
  30. machineconfig/scripts/python/devops_update_repos.py +21 -20
  31. machineconfig/scripts/python/fire_agents.py +21 -11
  32. machineconfig/scripts/python/fire_agents_help_launch.py +4 -3
  33. machineconfig/scripts/python/fire_agents_help_search.py +1 -2
  34. machineconfig/scripts/python/fire_agents_load_balancer.py +8 -10
  35. machineconfig/scripts/python/fire_jobs.py +1 -0
  36. machineconfig/scripts/python/helpers/cloud_helpers.py +1 -0
  37. machineconfig/scripts/python/mount_nfs.py +2 -0
  38. machineconfig/scripts/python/mount_nw_drive.py +1 -1
  39. machineconfig/scripts/python/mount_ssh.py +1 -0
  40. machineconfig/scripts/python/repos.py +0 -2
  41. machineconfig/scripts/python/repos_helper_record.py +43 -66
  42. machineconfig/scripts/python/repos_helper_update.py +3 -2
  43. machineconfig/scripts/python/start_slidev.py +1 -0
  44. machineconfig/scripts/python/start_terminals.py +1 -1
  45. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -0
  46. machineconfig/utils/code.py +0 -1
  47. machineconfig/utils/notifications.py +2 -2
  48. machineconfig/utils/procs.py +10 -15
  49. machineconfig/utils/schemas/fire_agents/fire_agents_input.py +9 -4
  50. machineconfig/utils/schemas/layouts/layout_types.py +2 -0
  51. machineconfig/utils/schemas/repos/repos_types.py +0 -3
  52. machineconfig/utils/ssh.py +9 -11
  53. machineconfig/utils/utils2.py +1 -2
  54. machineconfig/utils/ve.py +0 -1
  55. {machineconfig-3.0.dist-info → machineconfig-3.2.dist-info}/METADATA +1 -1
  56. {machineconfig-3.0.dist-info → machineconfig-3.2.dist-info}/RECORD +59 -59
  57. {machineconfig-3.0.dist-info → machineconfig-3.2.dist-info}/entry_points.txt +1 -0
  58. {machineconfig-3.0.dist-info → machineconfig-3.2.dist-info}/WHEEL +0 -0
  59. {machineconfig-3.0.dist-info → machineconfig-3.2.dist-info}/top_level.txt +0 -0
machineconfig/__init__.py CHANGED
@@ -6,25 +6,24 @@ import tomllib
6
6
 
7
7
 
8
8
  def _get_version() -> str:
9
- name: str = "machineconfig"
10
- try:
11
- return _pkg_version(name)
12
- except PackageNotFoundError:
13
- pass
9
+ name: str = "machineconfig"
10
+ try:
11
+ return _pkg_version(name)
12
+ except PackageNotFoundError:
13
+ pass
14
14
 
15
- root: Path = Path(__file__).resolve().parents[2]
16
- pyproject: Path = root / "pyproject.toml"
17
- if pyproject.is_file():
18
- with pyproject.open("rb") as f:
19
- data: dict[str, object] = tomllib.load(f)
20
- project = data.get("project")
21
- if isinstance(project, dict):
22
- version = project.get("version")
23
- if isinstance(version, str) and version:
24
- return version
25
- return "0.0.0"
15
+ root: Path = Path(__file__).resolve().parents[2]
16
+ pyproject: Path = root / "pyproject.toml"
17
+ if pyproject.is_file():
18
+ with pyproject.open("rb") as f:
19
+ data: dict[str, object] = tomllib.load(f)
20
+ project = data.get("project")
21
+ if isinstance(project, dict):
22
+ version = project.get("version")
23
+ if isinstance(version, str) and version:
24
+ return version
25
+ return "0.0.0"
26
26
 
27
27
 
28
28
  __version__: str = _get_version()
29
29
  __all__: list[str] = ["__version__"]
30
-
@@ -14,7 +14,7 @@ from rich import box
14
14
  console = Console()
15
15
 
16
16
 
17
- def run_enhanced_command(command: str, description: Optional[str] = None, show_progress: bool = True, timeout: Optional[int] = None) -> Dict[str, Any]:
17
+ def run_enhanced_command(command: str, description: Optional[str], show_progress: bool, timeout: Optional[int]) -> Dict[str, Any]:
18
18
  """
19
19
  Run a command with enhanced Rich formatting and user feedback.
20
20
 
@@ -89,16 +89,16 @@ def enhanced_zellij_session_start(session_name: str, layout_path: str) -> Dict[s
89
89
  run_enhanced_command(
90
90
  delete_cmd,
91
91
  f"Cleaning up existing session '{session_name}'",
92
- show_progress=False,
93
- timeout=5, # Quick timeout for cleanup
92
+ False,
93
+ 5, # Quick timeout for cleanup
94
94
  )
95
95
  # Start new session (use -b for background to avoid hanging)
96
96
  start_cmd = f"zellij --layout {layout_path} a -b {session_name}"
97
97
  start_result = run_enhanced_command(
98
98
  start_cmd,
99
99
  f"Starting session '{session_name}' with layout",
100
- show_progress=False,
101
- timeout=10, # Add timeout to prevent hanging
100
+ False,
101
+ 10, # Add timeout to prevent hanging
102
102
  )
103
103
 
104
104
  if start_result["success"]:
@@ -114,5 +114,5 @@ if __name__ == "__main__":
114
114
  console.print(Panel.fit("🎨 Enhanced Command Execution Demo", style="bold cyan"))
115
115
 
116
116
  # Test with a simple command
117
- result = run_enhanced_command("echo 'Hello, Rich world!'", "Testing enhanced output")
117
+ result = run_enhanced_command("echo 'Hello, Rich world!'", "Testing enhanced output", True, None)
118
118
  console.print(f"Result: {result}")
@@ -27,7 +27,7 @@ class WTLayoutGenerator:
27
27
  self.script_path: Optional[str] = None # Store the full path to the PowerShell script
28
28
 
29
29
  @staticmethod
30
- def _generate_random_suffix(length: int = 8) -> str:
30
+ def _generate_random_suffix(length: int) -> str:
31
31
  """Generate a random string suffix for unique PowerShell script names."""
32
32
  return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
33
33
 
@@ -53,7 +53,7 @@ class WTLayoutGenerator:
53
53
  return text
54
54
 
55
55
  @staticmethod
56
- def _create_tab_command(tab_config: TabConfig, is_first_tab: bool = False) -> str:
56
+ def _create_tab_command(tab_config: TabConfig, is_first_tab: bool) -> str:
57
57
  """Create a Windows Terminal tab command string from tab config."""
58
58
  tab_name = tab_config["tabName"]
59
59
  cwd = tab_config["startDir"]
@@ -95,7 +95,7 @@ class WTLayoutGenerator:
95
95
  if not tab["startDir"].strip():
96
96
  raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
97
97
 
98
- def create_wt_layout(self, layout_config: LayoutConfig, output_dir: Optional[str] = None) -> str:
98
+ def create_wt_layout(self, layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
99
99
  WTLayoutGenerator._validate_layout_config(layout_config)
100
100
  logger.info(f"Creating Windows Terminal layout '{layout_config['layoutName']}' with {len(layout_config['layoutTabs'])} tabs")
101
101
 
@@ -107,7 +107,7 @@ class WTLayoutGenerator:
107
107
  wt_command = self._generate_wt_command_string(layout_config, self.session_name)
108
108
 
109
109
  try:
110
- random_suffix = WTLayoutGenerator._generate_random_suffix()
110
+ random_suffix = WTLayoutGenerator._generate_random_suffix(8)
111
111
  if output_dir:
112
112
  output_path = Path(output_dir)
113
113
  output_path.mkdir(parents=True, exist_ok=True)
@@ -367,7 +367,7 @@ Get-Process | ForEach-Object {{
367
367
  print("=" * 80)
368
368
 
369
369
 
370
- def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str] = None) -> str:
370
+ def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
371
371
  generator = WTLayoutGenerator()
372
372
  return generator.create_wt_layout(layout_config, output_dir)
373
373
 
@@ -375,7 +375,7 @@ def create_wt_layout(layout_config: LayoutConfig, output_dir: Optional[str] = No
375
375
  def run_wt_layout(layout_config: LayoutConfig) -> str:
376
376
  """Create and run a Windows Terminal layout."""
377
377
  generator = WTLayoutGenerator()
378
- script_path = generator.create_wt_layout(layout_config)
378
+ script_path = generator.create_wt_layout(layout_config, None)
379
379
 
380
380
  # Execute the script
381
381
  cmd = f'powershell -ExecutionPolicy Bypass -File "{script_path}"'
@@ -406,7 +406,7 @@ if __name__ == "__main__":
406
406
  try:
407
407
  # Create layout using the generator
408
408
  generator = WTLayoutGenerator()
409
- script_path = generator.create_wt_layout(sample_layout)
409
+ script_path = generator.create_wt_layout(sample_layout, None)
410
410
  print(f"✅ Windows Terminal layout created: {script_path}")
411
411
 
412
412
  # Show preview
@@ -35,7 +35,7 @@ class WTLocalManager:
35
35
  # Create a WTLayoutGenerator for each session
36
36
  for layout_config in session_layouts:
37
37
  manager = WTLayoutGenerator()
38
- manager.create_wt_layout(layout_config=layout_config)
38
+ manager.create_wt_layout(layout_config=layout_config, output_dir=None)
39
39
  self.managers.append(manager)
40
40
 
41
41
  logger.info(f"Initialized WTLocalManager with {len(self.managers)} sessions")
@@ -324,16 +324,16 @@ class WTLocalManager:
324
324
  if not config_file.exists():
325
325
  raise FileNotFoundError(f"Configuration file not found: {config_file}")
326
326
 
327
- with open(config_file, "r", encoding="utf-8") as f:
328
- session_layouts = json.load(f)
327
+ text = config_file.read_text(encoding="utf-8")
328
+ session_layouts = json.loads(text)
329
329
 
330
330
  # Load metadata
331
331
  metadata_file = session_dir / "metadata.json"
332
332
  session_name_prefix = "LocalWTMgr" # default fallback
333
333
  if metadata_file.exists():
334
- with open(metadata_file, "r", encoding="utf-8") as f:
335
- metadata = json.load(f)
336
- session_name_prefix = metadata.get("session_name_prefix", "LocalWTMgr")
334
+ text = metadata_file.read_text(encoding="utf-8")
335
+ metadata = json.loads(text)
336
+ session_name_prefix = metadata.get("session_name_prefix", "LocalWTMgr")
337
337
 
338
338
  # Create new instance
339
339
  instance = cls(session_layouts=session_layouts, session_name_prefix=session_name_prefix)
@@ -346,8 +346,8 @@ class WTLocalManager:
346
346
 
347
347
  for manager_file in manager_files:
348
348
  try:
349
- with open(manager_file, "r", encoding="utf-8") as f:
350
- manager_data = json.load(f)
349
+ text = manager_file.read_text(encoding="utf-8")
350
+ manager_data = json.loads(text)
351
351
 
352
352
  # Recreate the manager
353
353
  manager = WTLayoutGenerator()
@@ -34,7 +34,7 @@ class WTRemoteLayoutGenerator:
34
34
 
35
35
  # Tabs are stored and used as List[TabConfig]; no legacy dict compatibility
36
36
 
37
- def create_wt_layout(self, tabs: List[TabConfig], output_dir: Optional[str] = None) -> str:
37
+ def create_wt_layout(self, tabs: List[TabConfig], output_dir: Optional[str]) -> str:
38
38
  logger.info(f"Creating Windows Terminal layout with {len(tabs)} tabs for remote '{self.remote_name}'")
39
39
  self.tabs = tabs
40
40
  if output_dir:
@@ -49,7 +49,7 @@ class WTRemoteLayoutGenerator:
49
49
  def to_dict(self) -> Dict[str, Any]:
50
50
  return {"remote_name": self.remote_name, "session_name": self.session_name, "tabs": self.tabs, "script_path": self.script_path, "created_at": datetime.now().isoformat(), "class_name": self.__class__.__name__}
51
51
 
52
- def to_json(self, file_path: Optional[str] = None) -> str:
52
+ def to_json(self, file_path: Optional[str]) -> str:
53
53
  # Generate file path if not provided
54
54
  if file_path is None:
55
55
  random_id = str(uuid.uuid4())[:8]
@@ -87,8 +87,8 @@ class WTRemoteLayoutGenerator:
87
87
  raise FileNotFoundError(f"JSON file not found: {path_obj}")
88
88
 
89
89
  # Load JSON data
90
- with open(path_obj, "r", encoding="utf-8") as f:
91
- data = json.load(f)
90
+ text = path_obj.read_text(encoding="utf-8")
91
+ data = json.loads(text)
92
92
 
93
93
  # Validate that it's the correct class
94
94
  if data.get("class_name") != cls.__name__:
@@ -117,7 +117,7 @@ class WTRemoteLayoutGenerator:
117
117
  return instance
118
118
 
119
119
  @staticmethod
120
- def list_saved_sessions(directory_path: Optional[str] = None) -> List[str]:
120
+ def list_saved_sessions(directory_path: Optional[str]) -> List[str]:
121
121
  if directory_path is None:
122
122
  dir_path = Path.home() / "tmp_results" / "wt_sessions" / "serialized"
123
123
  else:
@@ -146,7 +146,7 @@ if __name__ == "__main__":
146
146
  try:
147
147
  # Create layout using the remote generator
148
148
  generator = WTRemoteLayoutGenerator(remote_name=remote_name, session_name_prefix=session_name)
149
- script_path = generator.create_wt_layout(sample_tabs)
149
+ script_path = generator.create_wt_layout(sample_tabs, None)
150
150
  print(f"✅ Remote layout created successfully: {script_path}")
151
151
 
152
152
  # Check if Windows Terminal is available on remote
@@ -160,11 +160,11 @@ if __name__ == "__main__":
160
160
 
161
161
  # Demonstrate serialization
162
162
  print("\n💾 Demonstrating serialization...")
163
- saved_path = generator.to_json()
163
+ saved_path = generator.to_json(None)
164
164
  print(f"✅ Session saved to: {saved_path}")
165
165
 
166
166
  # List all saved sessions
167
- saved_sessions = WTRemoteLayoutGenerator.list_saved_sessions()
167
+ saved_sessions = WTRemoteLayoutGenerator.list_saved_sessions(None)
168
168
  print(f"📋 Available saved sessions: {saved_sessions}")
169
169
 
170
170
  # Demonstrate loading (using the full path)
@@ -24,7 +24,7 @@ class WTSessionManager:
24
24
  an_m = WTRemoteLayoutGenerator(remote_name=machine, session_name_prefix=self.session_name_prefix)
25
25
  # Convert legacy dict[str, tuple[str,str]] to List[TabConfig]
26
26
  tabs: list[TabConfig] = [{"tabName": name, "startDir": cwd, "command": cmd} for name, (cwd, cmd) in tab_config.items()]
27
- an_m.create_wt_layout(tabs=tabs)
27
+ an_m.create_wt_layout(tabs=tabs, output_dir=None)
28
28
  self.managers.append(an_m)
29
29
 
30
30
  def ssh_to_all_machines(self) -> str:
@@ -124,16 +124,16 @@ class WTSessionManager:
124
124
  config_file = session_dir / "machine2wt_tabs.json"
125
125
  if not config_file.exists():
126
126
  raise FileNotFoundError(f"Configuration file not found: {config_file}")
127
- with open(config_file, "r", encoding="utf-8") as f:
128
- machine2wt_tabs = json.load(f)
127
+ text = config_file.read_text(encoding="utf-8")
128
+ machine2wt_tabs = json.loads(text)
129
129
 
130
130
  # Load metadata
131
131
  metadata_file = session_dir / "metadata.json"
132
132
  session_name_prefix = "WTJobMgr" # default fallback
133
133
  if metadata_file.exists():
134
- with open(metadata_file, "r", encoding="utf-8") as f:
135
- metadata = json.load(f)
136
- session_name_prefix = metadata.get("session_name_prefix", "WTJobMgr")
134
+ text = metadata_file.read_text(encoding="utf-8")
135
+ metadata = json.loads(text)
136
+ session_name_prefix = metadata.get("session_name_prefix", "WTJobMgr")
137
137
  # Create new instance (this will create new managers)
138
138
  instance = cls(machine2wt_tabs=machine2wt_tabs, session_name_prefix=session_name_prefix)
139
139
  # Load saved managers to restore their states
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  import shlex
3
3
  import subprocess
4
- from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import CommandStatusResult, ZellijSessionStatus, ComprehensiveStatus, ProcessInfo
4
+ from machineconfig.cluster.sessions_managers.zellij_utils.monitoring_types import CommandStatus, ZellijSessionStatus, ComprehensiveStatus, ProcessInfo
5
5
  import psutil
6
6
  import random
7
7
  import string
@@ -14,7 +14,6 @@ from rich.console import Console
14
14
  from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig, TabConfig
15
15
 
16
16
 
17
-
18
17
  logging.basicConfig(level=logging.INFO)
19
18
  logger = logging.getLogger(__name__)
20
19
  console = Console()
@@ -37,7 +36,7 @@ class ZellijLayoutGenerator:
37
36
  """
38
37
 
39
38
  @staticmethod
40
- def _generate_random_suffix(length: int = 8) -> str:
39
+ def _generate_random_suffix(length: int) -> str:
41
40
  """Generate a random string suffix for unique layout file names."""
42
41
  return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
43
42
 
@@ -71,7 +70,6 @@ class ZellijLayoutGenerator:
71
70
  tab_name = tab_config["tabName"]
72
71
  cwd = tab_config["startDir"]
73
72
  command = tab_config["command"]
74
-
75
73
  cmd, args = ZellijLayoutGenerator._parse_command(command)
76
74
  args_str = ZellijLayoutGenerator._format_args_for_kdl(args)
77
75
  tab_cwd = cwd or "~"
@@ -95,7 +93,7 @@ class ZellijLayoutGenerator:
95
93
  if not tab["startDir"].strip():
96
94
  raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
97
95
 
98
- def create_zellij_layout(self, layout_config: LayoutConfig, output_dir: Optional[str] = None, session_name: Optional[str] = None) -> str:
96
+ def create_zellij_layout(self, layout_config: LayoutConfig, output_dir: Optional[str], session_name: Optional[str]) -> str:
99
97
  ZellijLayoutGenerator._validate_layout_config(layout_config)
100
98
 
101
99
  # Enhanced Rich logging
@@ -117,7 +115,7 @@ class ZellijLayoutGenerator:
117
115
  layout_content += "\n}\n"
118
116
 
119
117
  try:
120
- random_suffix = ZellijLayoutGenerator._generate_random_suffix()
118
+ random_suffix = ZellijLayoutGenerator._generate_random_suffix(8)
121
119
  if output_dir:
122
120
  output_path = Path(output_dir)
123
121
  output_path.mkdir(parents=True, exist_ok=True)
@@ -139,7 +137,7 @@ class ZellijLayoutGenerator:
139
137
  raise
140
138
 
141
139
  @staticmethod
142
- def get_layout_preview(layout_config: LayoutConfig, layout_template: str | None = None) -> str:
140
+ def get_layout_preview(layout_config: LayoutConfig, layout_template: str | None) -> str:
143
141
  if layout_template is None:
144
142
  layout_template = """layout {
145
143
  default_tab_template {
@@ -157,7 +155,7 @@ class ZellijLayoutGenerator:
157
155
  return layout_content + "\n}\n"
158
156
 
159
157
  @staticmethod
160
- def check_command_status(tab_name: str, layout_config: LayoutConfig) -> CommandStatusResult:
158
+ def check_command_status(tab_name: str, layout_config: LayoutConfig) -> CommandStatus:
161
159
  # Find the tab with the given name
162
160
  tab_config = None
163
161
  for tab in layout_config["layoutTabs"]:
@@ -171,94 +169,176 @@ class ZellijLayoutGenerator:
171
169
  command = tab_config["command"]
172
170
  cwd = tab_config["startDir"]
173
171
  cmd, args = ZellijLayoutGenerator._parse_command(command)
174
-
175
172
  try:
176
- # Look for processes matching the command more accurately
173
+ shells = {"bash", "sh", "zsh", "fish"}
177
174
  matching_processes: list[ProcessInfo] = []
178
- for proc in psutil.process_iter(["pid", "name", "cmdline", "status", "ppid"]):
175
+ for proc in psutil.process_iter(["pid", "name", "cmdline", "status", "ppid", "create_time", "memory_info"]):
179
176
  try:
180
- if not proc.info["cmdline"] or len(proc.info["cmdline"]) == 0:
177
+ info = proc.info
178
+ proc_cmdline: list[str] | None = info.get("cmdline") # type: ignore[assignment]
179
+ if not proc_cmdline:
181
180
  continue
182
-
183
- # Skip processes that are already dead/zombie/stopped
184
- if proc.info["status"] in ["zombie", "dead", "stopped"]:
181
+ if info.get("status") in ["zombie", "dead", "stopped"]:
185
182
  continue
186
-
187
- # Get the actual command from cmdline
188
- proc_cmdline = proc.info["cmdline"]
189
- proc_name = proc.info["name"]
190
-
191
- # More precise matching logic
183
+ proc_name = info.get("name", "")
192
184
  is_match = False
193
-
194
- # Check if this is the exact command we're looking for
195
- if proc_name == cmd:
196
- # For exact name matches, also check if arguments match or if it's a reasonable match
197
- if len(args) == 0 or any(arg in " ".join(proc_cmdline) for arg in args):
185
+ joined_cmdline = " ".join(proc_cmdline)
186
+ # Primary matching heuristics - more precise matching
187
+ if proc_name == cmd and cmd not in shells:
188
+ # For non-shell commands, match if args appear in cmdline
189
+ if not args or any(arg in joined_cmdline for arg in args):
198
190
  is_match = True
199
-
200
- # Check if the command appears in the command line
201
- elif cmd in proc_cmdline[0]:
191
+ elif proc_name == cmd and cmd in shells:
192
+ # For shell commands, require more precise matching to avoid false positives
193
+ if args:
194
+ # Check if all args appear as separate cmdline arguments (not just substrings)
195
+ args_found = 0
196
+ for arg in args:
197
+ for cmdline_arg in proc_cmdline[1:]: # Skip shell name
198
+ if arg == cmdline_arg or (len(arg) > 3 and arg in cmdline_arg):
199
+ args_found += 1
200
+ break
201
+ # Require at least as many args found as we're looking for
202
+ if args_found >= len(args):
203
+ is_match = True
204
+ elif cmd in proc_cmdline[0] and cmd not in shells:
205
+ # Non-shell command in first argument
202
206
  is_match = True
203
207
 
204
- # For script-based commands, check if the script name appears
205
- elif any(cmd in arg for arg in proc_cmdline):
206
- is_match = True
207
-
208
- # Skip shell processes that are just wrappers unless they're running our specific command
209
- if is_match and proc_name in ["bash", "sh", "zsh", "fish"]:
210
- # Only count shell processes if they contain our specific command
211
- full_cmdline = " ".join(proc_cmdline)
212
- if cmd not in full_cmdline and not any(arg in full_cmdline for arg in args):
208
+ # Additional shell wrapper filter - be more restrictive for shells
209
+ if is_match and proc_name in shells and args:
210
+ # For shell processes, ensure the match is actually meaningful
211
+ # Don't match generic shell sessions just because they contain common paths
212
+ meaningful_match = False
213
+ for arg in args:
214
+ # Only consider it meaningful if the arg is substantial (not just a common path)
215
+ if len(arg) > 10 and any(arg == cmdline_arg for cmdline_arg in proc_cmdline[1:]):
216
+ meaningful_match = True
217
+ break
218
+ # Or if it's an exact script name match
219
+ elif arg.endswith('.py') or arg.endswith('.sh') or arg.endswith('.rb'):
220
+ if any(arg in cmdline_arg for cmdline_arg in proc_cmdline[1:]):
221
+ meaningful_match = True
222
+ break
223
+ if not meaningful_match:
213
224
  is_match = False
214
-
215
- if is_match:
216
- # Additional check: make sure the process is actually running something meaningful
225
+ if not is_match:
226
+ continue
227
+ try:
228
+ proc_obj = psutil.Process(info["pid"]) # type: ignore[index]
229
+ if proc_obj.status() not in ["running", "sleeping"]:
230
+ continue
231
+ mem_info = None
217
232
  try:
218
- # Check if process has been running for a reasonable time and is active
219
- proc_obj = psutil.Process(proc.info["pid"])
220
- if proc_obj.status() not in ["running", "sleeping"]:
221
- continue # Skip inactive processes
222
-
223
- matching_processes.append({
224
- "pid": proc.info["pid"],
225
- "name": proc.info["name"],
226
- "cmdline": proc.info["cmdline"],
227
- "status": proc.info["status"]
228
- })
233
+ mem = proc_obj.memory_info()
234
+ mem_info = mem.rss / (1024 * 1024)
229
235
  except (psutil.NoSuchProcess, psutil.AccessDenied):
230
- continue
231
-
236
+ pass
237
+ matching_processes.append(
238
+ {
239
+ "pid": info["pid"], # type: ignore[index]
240
+ "name": proc_name,
241
+ "cmdline": proc_cmdline,
242
+ "status": info.get("status", "unknown"),
243
+ "cmdline_str": joined_cmdline,
244
+ "create_time": info.get("create_time", 0.0),
245
+ **({"memory_mb": float(mem_info)} if mem_info is not None else {}),
246
+ }
247
+ )
248
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
249
+ continue
232
250
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
233
251
  continue
234
252
 
235
- # Filter out clearly finished processes or parent shells
236
- active_processes = []
253
+ # Second-pass filtering: remove idle wrapper shells that have no meaningful (non-shell) descendants
254
+ filtered_active: list[ProcessInfo] = []
237
255
  for proc_info in matching_processes:
238
256
  try:
239
- proc = psutil.Process(proc_info["pid"])
240
- # Double-check the process is still active
241
- if proc.status() in ["running", "sleeping"] and proc.is_running():
242
- active_processes.append(proc_info)
257
+ proc_obj = psutil.Process(proc_info["pid"]) # type: ignore[index]
258
+ if not proc_obj.is_running():
259
+ continue
260
+ status_val = proc_obj.status()
261
+ if status_val not in ["running", "sleeping"]:
262
+ continue
263
+ proc_name = proc_info.get("name", "")
264
+ if proc_name in shells:
265
+ descendants = proc_obj.children(recursive=True)
266
+ # Keep shell only if there exists a non-shell alive descendant OR descendant cmdline still includes our command token
267
+ meaningful = False
268
+ for child in descendants:
269
+ try:
270
+ if not child.is_running():
271
+ continue
272
+ child_name = child.name()
273
+ child_cmdline = " ".join(child.cmdline())
274
+ if child_name not in shells:
275
+ meaningful = True
276
+ break
277
+ if cmd in child_cmdline or any(arg in child_cmdline for arg in args):
278
+ meaningful = True
279
+ break
280
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
281
+ continue
282
+ if not meaningful:
283
+ continue # discard idle wrapper shell
284
+ filtered_active.append(proc_info)
243
285
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
244
- # Process is gone, don't count it
245
286
  continue
246
287
 
247
- if active_processes:
248
- return {"status": "running", "running": True, "processes": active_processes, "command": command, "cwd": cwd, "tab_name": tab_name}
249
- else:
250
- return {"status": "not_running", "running": False, "processes": [], "command": command, "cwd": cwd, "tab_name": tab_name}
288
+ if filtered_active:
289
+ # Heuristic: if the only remaining processes are wrapper shells invoking a script that already completed, mark as not running.
290
+ # Case: layout launches 'bash <script.sh>' where script finishes and leaves an idle shell whose cmdline still shows the script path.
291
+ try:
292
+ if all(p.get("name") in shells for p in filtered_active):
293
+ script_paths = [arg for arg in args if arg.endswith(".sh")]
294
+ shell_only = True
295
+ stale_script_overall = False
296
+ for p in filtered_active:
297
+ proc_shell = psutil.Process(p["pid"]) # type: ignore[index]
298
+ create_time = getattr(proc_shell, "create_time", lambda: None)()
299
+ cmdline_joined = " ".join(p.get("cmdline", []))
300
+ stale_script = False
301
+ for spath in script_paths:
302
+ script_file = Path(spath)
303
+ if script_file.exists():
304
+ try:
305
+ # If script mtime older than process start AND no non-shell descendants -> likely finished
306
+ if create_time and script_file.stat().st_mtime < create_time:
307
+ stale_script = True
308
+ except OSError:
309
+ pass
310
+ if spath not in cmdline_joined:
311
+ stale_script = False
312
+ # If shell has any alive non-shell descendants, treat as running
313
+ descendants = proc_shell.children(recursive=True)
314
+ for d in descendants:
315
+ try:
316
+ if d.is_running() and d.name() not in shells:
317
+ shell_only = False
318
+ break
319
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
320
+ continue
321
+ if not shell_only:
322
+ break
323
+ if stale_script:
324
+ stale_script_overall = True
325
+ if shell_only and stale_script_overall:
326
+ return {"status": "not_running", "running": False, "processes": [], "command": command, "cwd": cwd, "tab_name": tab_name}
327
+ except Exception:
328
+ pass
329
+ return {"status": "running", "running": True, "processes": filtered_active, "command": command, "cwd": cwd, "tab_name": tab_name}
330
+ return {"status": "not_running", "running": False, "processes": [], "command": command, "cwd": cwd, "tab_name": tab_name}
251
331
 
252
332
  except Exception as e:
253
333
  logger.error(f"Error checking command status for tab '{tab_name}': {e}")
254
334
  return {"status": "error", "error": str(e), "running": False, "command": command, "cwd": cwd, "tab_name": tab_name, "processes": []}
255
335
 
256
- def check_all_commands_status(self) -> dict[str, CommandStatusResult]:
336
+ def check_all_commands_status(self) -> dict[str, CommandStatus]:
257
337
  if not self.layout_config:
258
338
  logger.warning("No layout config tracked. Make sure to create a layout first.")
259
339
  return {}
260
340
 
261
- status_report: dict[str, CommandStatusResult] = {}
341
+ status_report: dict[str, CommandStatus] = {}
262
342
  for tab in self.layout_config["layoutTabs"]:
263
343
  tab_name = tab["tabName"]
264
344
  status_report[tab_name] = ZellijLayoutGenerator.check_command_status(tab_name, self.layout_config)
@@ -277,22 +357,20 @@ class ZellijLayoutGenerator:
277
357
 
278
358
  return {"zellij_running": True, "session_exists": session_running, "session_name": session_name, "all_sessions": sessions}
279
359
  else:
280
- return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": result.stderr}
360
+ return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": result.stderr}
281
361
 
282
362
  except subprocess.TimeoutExpired:
283
- return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": "Timeout while checking Zellij sessions"}
363
+ return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": "Timeout while checking Zellij sessions"}
284
364
  except FileNotFoundError:
285
- return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": "Zellij not found in PATH"}
365
+ return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": "Zellij not found in PATH"}
286
366
  except Exception as e:
287
- return {"zellij_running": False, "session_name": session_name, "all_sessions": [], "error": str(e)}
367
+ return {"zellij_running": False, "session_exists": False, "session_name": session_name, "all_sessions": [], "error": str(e)}
288
368
 
289
369
  def get_comprehensive_status(self) -> ComprehensiveStatus:
290
370
  zellij_status = ZellijLayoutGenerator.check_zellij_session_status(self.session_name or "default")
291
371
  commands_status = self.check_all_commands_status()
292
-
293
372
  running_count = sum(1 for status in commands_status.values() if status.get("running", False))
294
373
  total_count = len(commands_status)
295
-
296
374
  return {
297
375
  "zellij_session": zellij_status,
298
376
  "commands": commands_status,
@@ -369,16 +447,17 @@ class ZellijLayoutGenerator:
369
447
  console.print(Panel(summary_text, title="📊 Summary", style="blue"))
370
448
 
371
449
 
372
- def created_zellij_layout(layout_config: LayoutConfig, output_dir: Optional[str] = None) -> str:
450
+ def created_zellij_layout(layout_config: LayoutConfig, output_dir: Optional[str]) -> str:
373
451
  generator = ZellijLayoutGenerator()
374
- return generator.create_zellij_layout(layout_config, output_dir)
452
+ return generator.create_zellij_layout(layout_config, output_dir, None)
375
453
 
376
454
 
377
455
  def run_zellij_layout(layout_config: LayoutConfig):
378
- layout_path = created_zellij_layout(layout_config)
456
+ layout_path = created_zellij_layout(layout_config, None)
379
457
  session_name = layout_config["layoutName"]
380
458
  try:
381
459
  from machineconfig.cluster.sessions_managers.enhanced_command_runner import enhanced_zellij_session_start
460
+
382
461
  enhanced_zellij_session_start(session_name, layout_path)
383
462
  except ImportError:
384
463
  # Fallback to original implementation
@@ -425,7 +504,7 @@ if __name__ == "__main__":
425
504
  try:
426
505
  # Create layout using the generator directly to access status methods
427
506
  generator = ZellijLayoutGenerator()
428
- layout_path = generator.create_zellij_layout(sample_layout, session_name="test_session")
507
+ layout_path = generator.create_zellij_layout(sample_layout, None, "test_session")
429
508
  print(f"✅ Layout created successfully: {layout_path}")
430
509
 
431
510
  # Demonstrate status checking