domd 2.2.24__tar.gz → 2.2.26__tar.gz

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.
Files changed (125) hide show
  1. {domd-2.2.24 → domd-2.2.26}/PKG-INFO +1 -1
  2. {domd-2.2.24 → domd-2.2.26}/pyproject.toml +1 -1
  3. {domd-2.2.24 → domd-2.2.26}/src/domd/cli.py +31 -31
  4. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/command_executor.py +1 -0
  5. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/ansible_playbook.py +19 -9
  6. {domd-2.2.24 → domd-2.2.26}/src/domd/core/project_detection/command_handling.py +23 -11
  7. {domd-2.2.24 → domd-2.2.26}/src/domd/core/project_detection/detector.py +181 -41
  8. {domd-2.2.24 → domd-2.2.26}/LICENSE +0 -0
  9. {domd-2.2.24 → domd-2.2.26}/README.md +0 -0
  10. {domd-2.2.24 → domd-2.2.26}/src/domd/__init__.py +0 -0
  11. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/__init__.py +0 -0
  12. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/api/__init__.py +0 -0
  13. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/api/flask_api.py +0 -0
  14. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/cli/__init__.py +0 -0
  15. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/cli/command_presenter.py +0 -0
  16. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/formatters/__init__.py +0 -0
  17. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/formatters/markdown_formatter.py +0 -0
  18. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/persistence/__init__.py +0 -0
  19. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/persistence/in_memory_command_repository.py +0 -0
  20. {domd-2.2.24 → domd-2.2.26}/src/domd/adapters/persistence/shell_command_executor.py +0 -0
  21. {domd-2.2.24 → domd-2.2.26}/src/domd/api.py +0 -0
  22. {domd-2.2.24 → domd-2.2.26}/src/domd/application/__init__.py +0 -0
  23. {domd-2.2.24 → domd-2.2.26}/src/domd/application/factory.py +0 -0
  24. {domd-2.2.24 → domd-2.2.26}/src/domd/command_execution/__init__.py +0 -0
  25. {domd-2.2.24 → domd-2.2.26}/src/domd/core/__init__.py +0 -0
  26. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/__init__.py +0 -0
  27. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/base_detector.py +0 -0
  28. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/handlers/__init__.py +0 -0
  29. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/handlers/command_handler.py +0 -0
  30. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/handlers/config_handler.py +0 -0
  31. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/models.py +0 -0
  32. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/parsers/__init__.py +0 -0
  33. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/parsers/base_parser.py +0 -0
  34. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/parsers/parser_registry.py +0 -0
  35. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/project_detector.py +0 -0
  36. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/utils/__init__.py +0 -0
  37. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/utils/file_utils.py +0 -0
  38. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_detection/utils/virtualenv.py +0 -0
  39. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/__init__.py +0 -0
  40. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/command_recorder.py +0 -0
  41. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/command_runner.py +0 -0
  42. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/environment_detector.py +0 -0
  43. {domd-2.2.24 → domd-2.2.26}/src/domd/core/command_execution/executor.py +0 -0
  44. {domd-2.2.24 → domd-2.2.26}/src/domd/core/commands/__init__.py +0 -0
  45. {domd-2.2.24 → domd-2.2.26}/src/domd/core/commands/command.py +0 -0
  46. {domd-2.2.24 → domd-2.2.26}/src/domd/core/commands/executor.py +0 -0
  47. {domd-2.2.24 → domd-2.2.26}/src/domd/core/detector.py +0 -0
  48. {domd-2.2.24 → domd-2.2.26}/src/domd/core/domain/__init__.py +0 -0
  49. {domd-2.2.24 → domd-2.2.26}/src/domd/core/domain/command.py +0 -0
  50. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/__init__.py +0 -0
  51. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/ansible_galaxy.py +0 -0
  52. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/ansible_inventory.py +0 -0
  53. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/ansible_role.py +0 -0
  54. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/ansible_vault.py +0 -0
  55. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/base.py +0 -0
  56. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/cargo_toml.py +0 -0
  57. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/composer_json.py +0 -0
  58. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/go_mod.py +0 -0
  59. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/makefile.py +0 -0
  60. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/package_json.py +0 -0
  61. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/pyproject_toml.py +0 -0
  62. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsers/tox_ini.py +0 -0
  63. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsing/__init__.py +0 -0
  64. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsing/base.py +0 -0
  65. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsing/file_processor.py +0 -0
  66. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsing/parser_registry.py +0 -0
  67. {domd-2.2.24 → domd-2.2.26}/src/domd/core/parsing/pattern_matcher.py +0 -0
  68. {domd-2.2.24 → domd-2.2.26}/src/domd/core/ports/__init__.py +0 -0
  69. {domd-2.2.24 → domd-2.2.26}/src/domd/core/ports/command_executor.py +0 -0
  70. {domd-2.2.24 → domd-2.2.26}/src/domd/core/ports/command_repository.py +0 -0
  71. {domd-2.2.24 → domd-2.2.26}/src/domd/core/ports/report_formatter.py +0 -0
  72. {domd-2.2.24 → domd-2.2.26}/src/domd/core/project_detection/__init__.py +0 -0
  73. {domd-2.2.24 → domd-2.2.26}/src/domd/core/project_detection/config_files.py +0 -0
  74. {domd-2.2.24 → domd-2.2.26}/src/domd/core/project_detection/virtualenv.py +0 -0
  75. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporters/__init__.py +0 -0
  76. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporters/base.py +0 -0
  77. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporters/done_md.py +0 -0
  78. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporters/todo_md.py +0 -0
  79. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporting/__init__.py +0 -0
  80. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporting/formatters.py +0 -0
  81. {domd-2.2.24 → domd-2.2.26}/src/domd/core/reporting/reporter.py +0 -0
  82. {domd-2.2.24 → domd-2.2.26}/src/domd/core/services/__init__.py +0 -0
  83. {domd-2.2.24 → domd-2.2.26}/src/domd/core/services/command_service.py +0 -0
  84. {domd-2.2.24 → domd-2.2.26}/src/domd/core/services/report_service.py +0 -0
  85. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/__init__.py +0 -0
  86. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/command_utils.py +0 -0
  87. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/environment.py +0 -0
  88. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/file_utils.py +0 -0
  89. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/logging_utils.py +0 -0
  90. {domd-2.2.24 → domd-2.2.26}/src/domd/core/utils/virtualenv.py +0 -0
  91. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/__init__.py +0 -0
  92. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/ansible.py +0 -0
  93. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/base.py +0 -0
  94. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/build_systems.py +0 -0
  95. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/ci_cd.py +0 -0
  96. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/docker/__init__.py +0 -0
  97. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/docker/docker_compose.py +0 -0
  98. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/docker/dockerfile.py +0 -0
  99. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/docker.py +0 -0
  100. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/javascript/__init__.py +0 -0
  101. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/javascript.py +0 -0
  102. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/markdown_parser.py +0 -0
  103. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/package_json.py +0 -0
  104. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/python.py +0 -0
  105. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/test_build_systems.py +0 -0
  106. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/test_ci_cd.py +0 -0
  107. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/test_docker.py +0 -0
  108. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/test_javascript.py +0 -0
  109. {domd-2.2.24 → domd-2.2.26}/src/domd/parsers/test_python.py +0 -0
  110. {domd-2.2.24 → domd-2.2.26}/src/domd/parsing/__init__.py +0 -0
  111. {domd-2.2.24 → domd-2.2.26}/src/domd/parsing/base.py +0 -0
  112. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/__init__.py +0 -0
  113. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/console.py +0 -0
  114. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/json_reporter.py +0 -0
  115. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/test_console.py +0 -0
  116. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/test_json_reporter.py +0 -0
  117. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/test_todo_md.py +0 -0
  118. {domd-2.2.24 → domd-2.2.26}/src/domd/reporters/todo_md.py +0 -0
  119. {domd-2.2.24 → domd-2.2.26}/src/domd/reporting/__init__.py +0 -0
  120. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/__init__.py +0 -0
  121. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/command_runner.py +0 -0
  122. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/file_utils.py +0 -0
  123. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/path_utils.py +0 -0
  124. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/test_command_runner.py +0 -0
  125. {domd-2.2.24 → domd-2.2.26}/src/domd/utils/test_file_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: domd
3
- Version: 2.2.24
3
+ Version: 2.2.26
4
4
  Summary: ✓ DoMD - Do Markdown Docs - automatically detect, run and show working commands in DONE.md and errors in TODO.md markdown files
5
5
  License: Apache-2.0
6
6
  Author: Tom Sapletta
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "domd"
3
- version = "2.2.24"
3
+ version = "2.2.26"
4
4
  description = " ✓ DoMD - Do Markdown Docs - automatically detect, run and show working commands in DONE.md and errors in TODO.md markdown files"
5
5
  authors = ["Tom Sapletta <info@softreck.dev>"]
6
6
  maintainers = ["WRONAI Team"]
@@ -32,37 +32,34 @@ logger = logging.getLogger(__name__)
32
32
 
33
33
  def _check_node_installed() -> bool:
34
34
  """Check if Node.js and npm are installed.
35
-
35
+
36
36
  Returns:
37
37
  bool: True if both Node.js and npm are installed, False otherwise
38
38
  """
39
39
  try:
40
40
  # Check Node.js
41
41
  node_version = subprocess.check_output(
42
- ["node", "--version"],
43
- stderr=subprocess.PIPE,
44
- universal_newlines=True
42
+ ["node", "--version"], stderr=subprocess.PIPE, universal_newlines=True
45
43
  ).strip()
46
-
44
+
47
45
  # Check npm
48
46
  npm_version = subprocess.check_output(
49
- ["npm", "--version"],
50
- stderr=subprocess.PIPE,
51
- universal_newlines=True
47
+ ["npm", "--version"], stderr=subprocess.PIPE, universal_newlines=True
52
48
  ).strip()
53
-
49
+
54
50
  print(f"✓ Found {node_version} and npm {npm_version}")
55
51
  return True
56
-
52
+
57
53
  except (subprocess.CalledProcessError, FileNotFoundError):
58
54
  return False
59
55
 
56
+
60
57
  def _install_dependencies(frontend_dir: Path) -> bool:
61
58
  """Install frontend dependencies using npm.
62
-
59
+
63
60
  Args:
64
61
  frontend_dir: Path to the frontend directory
65
-
62
+
66
63
  Returns:
67
64
  bool: True if installation was successful, False otherwise
68
65
  """
@@ -80,59 +77,62 @@ def _install_dependencies(frontend_dir: Path) -> bool:
80
77
  print(f"❌ Failed to install dependencies: {e}")
81
78
  return False
82
79
 
80
+
83
81
  def start_web_interface(args: argparse.Namespace) -> int:
84
82
  """Start the web interface.
85
-
83
+
86
84
  Args:
87
85
  args: Command line arguments
88
-
86
+
89
87
  Returns:
90
88
  int: Exit code (0 for success, non-zero for error)
91
89
  """
92
90
  port = args.port
93
91
  open_browser = not args.no_browser
94
-
92
+
95
93
  # Check if Node.js and npm are installed
96
94
  print("🔍 Checking for Node.js and npm...")
97
95
  if not _check_node_installed():
98
- print("""
96
+ print(
97
+ """
99
98
  ❌ Node.js and npm are required to run the web interface.
100
99
  Please install them from https://nodejs.org/ and try again.
101
-
100
+
102
101
  On Ubuntu/Debian: sudo apt-get install nodejs npm
103
102
  On macOS: brew install node
104
103
  On Windows: Download from https://nodejs.org/
105
- """)
104
+ """
105
+ )
106
106
  return 1
107
-
107
+
108
108
  # Get the directory of this file
109
109
  current_dir = Path(__file__).parent
110
110
  frontend_dir = current_dir.parent.parent / "frontend"
111
-
111
+
112
112
  if not frontend_dir.exists():
113
113
  print(f"❌ Error: Frontend directory not found at {frontend_dir}")
114
114
  return 1
115
-
115
+
116
116
  # Check if node_modules exists, if not, install dependencies
117
117
  if not (frontend_dir / "node_modules").exists():
118
118
  if not _install_dependencies(frontend_dir):
119
119
  return 1
120
-
120
+
121
121
  # Change to the frontend directory
122
122
  os.chdir(frontend_dir)
123
-
123
+
124
124
  # Set the port environment variable
125
125
  os.environ["PORT"] = str(port)
126
-
126
+
127
127
  print(f"🚀 Starting DoMD web interface on port {port}...")
128
128
  print(" Press Ctrl+C to stop the server")
129
-
129
+
130
130
  # Start the React development server
131
131
  try:
132
132
  cmd = ["npm", "start"]
133
133
  if sys.platform == "win32":
134
134
  cmd = ["npm.cmd", "start"]
135
-
135
+
136
136
  # Start the process
137
137
  process = subprocess.Popen(
138
138
  cmd,
@@ -141,25 +141,25 @@ def start_web_interface(args: argparse.Namespace) -> int:
141
141
  stderr=subprocess.STDOUT,
142
142
  universal_newlines=True,
143
143
  )
144
-
144
+
145
145
  # Open browser after a short delay
146
146
  if open_browser:
147
147
  time.sleep(2) # Give the server a moment to start
148
148
  print(f"🌐 Opening browser to http://localhost:{port}...")
149
149
  webbrowser.open(f"http://localhost:{port}")
150
-
150
+
151
151
  # Stream the output
152
152
  for line in process.stdout:
153
153
  print(line, end="")
154
-
154
+
155
155
  # Wait for the process to complete
156
156
  process.wait()
157
157
  return process.returncode
158
-
158
+
159
159
  except KeyboardInterrupt:
160
160
  print("\n🛑 Web interface stopped by user")
161
161
  return 0
162
-
162
+
163
163
  except Exception as e:
164
164
  print(f"❌ Error starting web interface: {e}")
165
165
  print("\n💡 Try these troubleshooting steps:")
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import shlex
6
6
  import subprocess
7
+ import time
7
8
  from dataclasses import dataclass
8
9
  from pathlib import Path
9
10
  from typing import Any, Dict, List, Optional, Type, TypeVar, Union
@@ -54,31 +54,41 @@ class AnsiblePlaybookParser(BaseParser):
54
54
  """
55
55
  from domd.core.commands import Command
56
56
 
57
- if not self.file_path or not self.file_path.exists():
57
+ if not hasattr(self, "file_path") or self.file_path is None:
58
58
  return []
59
59
 
60
60
  self._commands: List[Command] = []
61
61
 
62
62
  # If content is not provided, read from file
63
- if content is None and not self.file_path.is_dir():
63
+ if (
64
+ content is None
65
+ and hasattr(self, "file_path")
66
+ and not self.file_path.is_dir()
67
+ ):
64
68
  try:
65
- with open(self.file_path, "r", encoding="utf-8") as f:
69
+ with open(str(self.file_path), "r", encoding="utf-8") as f:
66
70
  content = f.read()
67
- except (IOError, UnicodeDecodeError):
71
+ except (IOError, UnicodeDecodeError) as e:
68
72
  # If we can't read the file, return empty list
73
+ import logging
74
+
75
+ logger = logging.getLogger(__name__)
76
+ logger.warning(f"Error reading file {self.file_path}: {e}")
69
77
  return []
70
78
 
71
79
  try:
72
- playbook = yaml.safe_load(content)
80
+ playbook = yaml.safe_load(content) if content else []
73
81
 
74
82
  if not isinstance(playbook, list):
75
83
  return []
76
84
 
77
85
  # Get relative path if file is in project directory, otherwise use full path
78
- try:
79
- playbook_path = self.file_path.relative_to(self.project_root)
80
- except ValueError:
81
- playbook_path = self.file_path
86
+ playbook_path = str(self.file_path)
87
+ if hasattr(self, "project_root") and self.project_root:
88
+ try:
89
+ playbook_path = str(self.file_path.relative_to(self.project_root))
90
+ except ValueError:
91
+ pass
82
92
 
83
93
  for play in playbook:
84
94
  if not isinstance(play, dict):
@@ -250,18 +250,30 @@ class CommandHandler:
250
250
  bin_dir = "bin"
251
251
  python_exe = "python"
252
252
 
253
- python_path = os.path.join(venv_path, bin_dir, python_exe)
254
- if os.path.isfile(python_path):
253
+ # First try the direct path from venv_env if available
254
+ python_path = venv_env.get("python_path")
255
+ if not python_path or not os.path.isfile(python_path):
256
+ # Fall back to standard location
257
+ python_path = os.path.join(venv_path, bin_dir, python_exe)
258
+
259
+ # If that doesn't exist, try version-specific python
260
+ if not os.path.isfile(python_path):
261
+ python_path = os.path.join(
262
+ venv_path,
263
+ bin_dir,
264
+ f"python{sys.version_info.major}.{sys.version_info.minor}",
265
+ )
266
+ # If still not found, try without minor version
267
+ if not os.path.isfile(python_path):
268
+ python_path = os.path.join(
269
+ venv_path,
270
+ bin_dir,
271
+ f"python{sys.version_info.major}",
272
+ )
273
+
274
+ # If we found a valid Python executable, use it
275
+ if python_path and os.path.isfile(python_path):
255
276
  cmd_list[0] = python_path
256
- else:
257
- # Fallback to the Python executable in the virtualenv's bin directory
258
- python_path = os.path.join(
259
- venv_path,
260
- bin_dir,
261
- f"python{sys.version_info.major}.{sys.version_info.minor}",
262
- )
263
- if os.path.isfile(python_path):
264
- cmd_list[0] = python_path
265
277
 
266
278
  # Merge environments, with user-provided env taking precedence
267
279
  env = kwargs.pop("env", {})
@@ -1,6 +1,7 @@
1
1
  """Project command detector for finding and executing commands in project files."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  from pathlib import Path
5
6
  from typing import Any, Dict, List, Optional, Union
6
7
 
@@ -263,78 +264,137 @@ class ProjectCommandDetector:
263
264
  all_commands.extend(commands)
264
265
  logger.info(f"Found {len(commands)} commands in {display_path}")
265
266
 
266
- def _process_file_commands(self, file_path: Path) -> List[Dict]:
267
+ def _process_file_commands(self, file_path: Union[str, Path]) -> List[Dict]:
267
268
  """Process a single file and extract commands.
268
269
 
269
270
  Args:
270
- file_path: Path to the file to process
271
+ file_path: Path to the file to process (can be str or Path)
271
272
 
272
273
  Returns:
273
274
  List of command dictionaries with metadata
274
275
  """
275
- logger.debug(f"Processing file: {file_path}")
276
+ # Convert to Path object if needed
277
+ path = Path(file_path) if isinstance(file_path, str) else file_path
278
+ logger.debug(f"Processing file: {path}")
276
279
  commands = []
277
280
 
278
281
  try:
279
- if not file_path.exists():
280
- logger.warning(f"File not found: {file_path}")
282
+ # Check if file exists and is a file
283
+ if not path.exists():
284
+ logger.warning(f"File not found: {path}")
285
+ return commands
286
+
287
+ if not path.is_file():
288
+ logger.debug(f"Path is not a file: {path}")
281
289
  return commands
282
290
 
283
291
  # Get the appropriate parser for the file
284
- parser = self._get_parser_for_file(file_path)
292
+ parser = self._get_parser_for_file(path)
285
293
  if not parser:
286
- logger.debug(f"No parser found for file: {file_path}")
294
+ logger.debug(f"No suitable parser found for file: {path}")
287
295
  return commands
288
296
 
289
- try:
290
- # Set the file_path on the parser if it has that attribute
291
- if hasattr(parser, "file_path"):
292
- parser.file_path = str(file_path)
297
+ # Set the file_path on the parser if it has that attribute
298
+ if hasattr(parser, "file_path"):
299
+ parser.file_path = path
293
300
 
294
- # Parse commands from file
295
- file_commands = []
301
+ # Set project root if needed
302
+ if hasattr(parser, "project_root") and not hasattr(
303
+ parser, "_project_root_set"
304
+ ):
305
+ parser.project_root = self.project_path
306
+ parser._project_root_set = True
307
+
308
+ # Parse commands from file
309
+ file_commands = []
310
+ try:
311
+ # Try different parsing methods in order of preference
296
312
  if hasattr(parser, "parse_file") and callable(parser.parse_file):
297
- file_commands = parser.parse_file(str(file_path))
313
+ file_commands = parser.parse_file(path)
298
314
  elif hasattr(parser, "parse") and callable(parser.parse):
299
315
  # First try without arguments (parser will handle file reading)
300
316
  try:
301
317
  file_commands = parser.parse()
302
- except (TypeError, AttributeError):
318
+ except (TypeError, AttributeError) as e:
319
+ logger.debug(
320
+ f"Parser.parse() failed, trying with file path: {e}"
321
+ )
303
322
  # If that fails, try with file path
304
323
  try:
305
- file_commands = parser.parse(str(file_path))
306
- except (TypeError, AttributeError):
324
+ file_commands = parser.parse(path)
325
+ except (TypeError, AttributeError) as e:
326
+ logger.debug(
327
+ f"Parser.parse(path) failed, trying with file content: {e}"
328
+ )
307
329
  # As a last resort, try with file content
308
330
  try:
309
- content = file_path.read_text(encoding="utf-8")
331
+ content = path.read_text(encoding="utf-8")
310
332
  file_commands = parser.parse(content)
311
333
  except Exception as e:
312
334
  logger.error(
313
- f"Error parsing {file_path} with content: {e}"
335
+ f"Error parsing {path} with content: {e}",
336
+ exc_info=logger.isEnabledFor(logging.DEBUG),
314
337
  )
315
338
  return []
316
339
 
317
- # Add file path to commands if not already present
340
+ if not isinstance(file_commands, list):
341
+ logger.warning(
342
+ f"Parser returned non-list result: {type(file_commands)}"
343
+ )
344
+ file_commands = []
345
+
346
+ # Process the parsed commands
347
+ processed_commands = []
318
348
  for cmd in file_commands:
319
- if isinstance(cmd, dict):
320
- cmd["file"] = str(file_path)
321
- # Add relative path as source if not present
322
- if "source" not in cmd:
323
- cmd["source"] = str(
324
- file_path.relative_to(self.project_path)
325
- )
326
- elif hasattr(cmd, "file") and not cmd.file:
327
- cmd.file = str(file_path)
328
- if not hasattr(cmd, "source") or not cmd.source:
329
- cmd.source = str(file_path.relative_to(self.project_path))
349
+ try:
350
+ if cmd is None:
351
+ continue
352
+
353
+ # Handle both dict and object-style commands
354
+ if isinstance(cmd, dict):
355
+ # Ensure required fields exist
356
+ cmd.setdefault("file", str(path))
357
+ if "source" not in cmd:
358
+ try:
359
+ cmd["source"] = str(
360
+ path.relative_to(self.project_path)
361
+ )
362
+ except ValueError:
363
+ cmd["source"] = str(path)
364
+ processed_commands.append(cmd)
365
+
366
+ elif hasattr(cmd, "__dict__"): # Object with attributes
367
+ if not hasattr(cmd, "file") or not cmd.file:
368
+ cmd.file = str(path)
369
+ if not hasattr(cmd, "source") or not cmd.source:
370
+ try:
371
+ cmd.source = str(
372
+ path.relative_to(self.project_path)
373
+ )
374
+ except ValueError:
375
+ cmd.source = str(path)
376
+ processed_commands.append(cmd)
377
+
378
+ except Exception as e:
379
+ logger.warning(
380
+ f"Error processing command from {path}: {e}",
381
+ exc_info=logger.isEnabledFor(logging.DEBUG),
382
+ )
330
383
 
331
- commands.extend(file_commands)
332
- logger.debug(f"Found {len(file_commands)} commands in {file_path}")
384
+ commands.extend(processed_commands)
385
+ logger.debug(f"Found {len(processed_commands)} commands in {path}")
333
386
 
334
387
  except Exception as e:
335
- logger.error(f"Error parsing {file_path}: {e}", exc_info=True)
388
+ logger.error(
389
+ f"Error parsing {path}: {e}",
390
+ exc_info=logger.isEnabledFor(logging.DEBUG),
391
+ )
392
+
336
393
  except Exception as e:
337
- logger.error(f"Unexpected error processing {file_path}: {e}", exc_info=True)
394
+ logger.error(
395
+ f"Unexpected error processing {path}: {e}",
396
+ exc_info=logger.isEnabledFor(logging.DEBUG),
397
+ )
338
398
 
339
399
  return commands
340
400
 
@@ -421,24 +481,65 @@ class ProjectCommandDetector:
421
481
 
422
482
  return result_commands
423
483
 
424
- def _get_parser_for_file(self, file_path: Path) -> Optional[BaseParser]:
484
+ def _get_parser_for_file(self, file_path: Union[str, Path]) -> Optional[BaseParser]:
425
485
  """Get a parser for a specific file (legacy method).
426
486
 
427
487
  Args:
428
- file_path: Path to the file
488
+ file_path: Path to the file (can be str or Path)
429
489
 
430
490
  Returns:
431
491
  Parser instance or None if no parser found
432
492
  """
493
+ # Ensure we have a Path object
494
+ path = Path(file_path) if isinstance(file_path, str) else file_path
495
+
496
+ if not path.exists():
497
+ logger.debug(f"File does not exist: {path}")
498
+ return None
499
+
500
+ logger.debug(f"Looking for parser for file: {path}")
501
+
433
502
  for parser in self.parsers:
434
503
  try:
435
- if hasattr(parser, "can_parse") and parser.can_parse(file_path):
504
+ if not hasattr(parser, "can_parse"):
505
+ logger.debug(
506
+ f"Parser {parser.__class__.__name__} has no can_parse method"
507
+ )
508
+ continue
509
+
510
+ # Check if this parser can handle the file
511
+ can_parse = False
512
+ try:
513
+ can_parse = parser.can_parse(path)
514
+ except Exception as e:
515
+ logger.warning(
516
+ f"Error in can_parse for {parser.__class__.__name__} on {path}: {e}",
517
+ exc_info=logger.isEnabledFor(logging.DEBUG),
518
+ )
519
+ continue
520
+
521
+ if can_parse:
522
+ logger.debug(
523
+ f"Found matching parser: {parser.__class__.__name__} for {path}"
524
+ )
525
+ # Set the file_path on the parser if it has that attribute
526
+ if hasattr(parser, "file_path"):
527
+ parser.file_path = path
528
+ if hasattr(parser, "project_root") and not hasattr(
529
+ parser, "_project_root_set"
530
+ ):
531
+ parser.project_root = self.project_path
532
+ parser._project_root_set = True
436
533
  return parser
534
+
437
535
  except Exception as e:
438
536
  logger.warning(
439
- f"Error checking parser {parser.__class__.__name__}: {e}"
537
+ f"Error checking parser {parser.__class__.__name__} on {path}: {e}",
538
+ exc_info=logger.isEnabledFor(logging.DEBUG),
440
539
  )
540
+ continue
441
541
 
542
+ logger.debug(f"No parser found for file: {path}")
442
543
  return None
443
544
 
444
545
  def test_commands(self, commands: List) -> None:
@@ -489,11 +590,50 @@ class ProjectCommandDetector:
489
590
  Returns:
490
591
  Dictionary with command execution results
491
592
  """
593
+ logger.debug(f"Running command in virtualenv: {command}")
594
+
492
595
  # Get environment variables for virtualenv
493
596
  venv_env = self._get_environment()
597
+ logger.debug(f"Initial venv_env: {venv_env}")
598
+
599
+ # Ensure we have the virtual environment path in the environment
600
+ if self.venv_info.get("exists"):
601
+ if "VIRTUAL_ENV" not in venv_env:
602
+ venv_env["VIRTUAL_ENV"] = self.venv_info["path"]
603
+ logger.debug(f"Set VIRTUAL_ENV to: {venv_env['VIRTUAL_ENV']}")
604
+
605
+ # Ensure we have the Python path in the environment
606
+ if self.venv_info.get("python_path"):
607
+ if "python_path" not in venv_env:
608
+ venv_env["python_path"] = self.venv_info["python_path"]
609
+ logger.debug(f"Set python_path to: {venv_env['python_path']}")
610
+
611
+ # Ensure the virtualenv's bin/Scripts directory is in PATH
612
+ if self.venv_info.get("path"):
613
+ venv_path = Path(self.venv_info["path"])
614
+ bin_dir = "Scripts" if os.name == "nt" else "bin"
615
+ venv_bin = str(venv_path / bin_dir)
616
+
617
+ # Add to the beginning of PATH
618
+ path = venv_env.get("PATH", "")
619
+ if venv_bin not in path.split(os.pathsep):
620
+ venv_env["PATH"] = f"{venv_bin}{os.pathsep}{path}"
621
+ logger.debug(f"Updated PATH to: {venv_env['PATH']}")
622
+ else:
623
+ logger.warning("No virtual environment path found in venv_info")
494
624
 
495
- # Pass the environment to command_handler.run_in_venv
496
- return self.command_handler.run_in_venv(command, venv_env=venv_env, **kwargs)
625
+ logger.debug(f"Final environment for command execution: {venv_env}")
626
+
627
+ try:
628
+ # Pass the environment to command_handler.run_in_venv
629
+ result = self.command_handler.run_in_venv(
630
+ command, venv_env=venv_env, **kwargs
631
+ )
632
+ logger.debug(f"Command execution result: {result}")
633
+ return result
634
+ except Exception as e:
635
+ logger.error(f"Error executing command in virtualenv: {e}", exc_info=True)
636
+ raise
497
637
 
498
638
  def _execute_command(self, command_info: Dict[str, Any]) -> Dict[str, Any]:
499
639
  """Execute a command with the given information.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes