crackerjack 0.32.0__py3-none-any.whl → 0.33.0__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 crackerjack might be problematic. Click here for more details.

Files changed (34) hide show
  1. crackerjack/core/enhanced_container.py +67 -0
  2. crackerjack/core/phase_coordinator.py +183 -44
  3. crackerjack/core/workflow_orchestrator.py +459 -138
  4. crackerjack/managers/publish_manager.py +22 -5
  5. crackerjack/managers/test_command_builder.py +4 -2
  6. crackerjack/managers/test_manager.py +15 -4
  7. crackerjack/mcp/server_core.py +162 -34
  8. crackerjack/mcp/tools/core_tools.py +1 -1
  9. crackerjack/mcp/tools/execution_tools.py +8 -3
  10. crackerjack/mixins/__init__.py +5 -0
  11. crackerjack/mixins/error_handling.py +214 -0
  12. crackerjack/models/config.py +9 -0
  13. crackerjack/models/protocols.py +69 -0
  14. crackerjack/models/task.py +3 -0
  15. crackerjack/security/__init__.py +1 -1
  16. crackerjack/security/audit.py +92 -78
  17. crackerjack/services/config.py +3 -2
  18. crackerjack/services/config_merge.py +11 -5
  19. crackerjack/services/coverage_ratchet.py +22 -0
  20. crackerjack/services/git.py +37 -24
  21. crackerjack/services/initialization.py +25 -9
  22. crackerjack/services/memory_optimizer.py +477 -0
  23. crackerjack/services/parallel_executor.py +474 -0
  24. crackerjack/services/performance_benchmarks.py +292 -577
  25. crackerjack/services/performance_cache.py +443 -0
  26. crackerjack/services/performance_monitor.py +633 -0
  27. crackerjack/services/security.py +63 -0
  28. crackerjack/services/security_logger.py +9 -1
  29. crackerjack/services/terminal_utils.py +0 -0
  30. {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/METADATA +2 -2
  31. {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/RECORD +34 -27
  32. {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/WHEEL +0 -0
  33. {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/entry_points.txt +0 -0
  34. {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/licenses/LICENSE +0 -0
@@ -144,6 +144,75 @@ class SecurityAwareHookManager(HookManager, t.Protocol):
144
144
  ...
145
145
 
146
146
 
147
+ @t.runtime_checkable
148
+ class CoverageRatchetProtocol(t.Protocol):
149
+ """Protocol for coverage ratchet service."""
150
+
151
+ def get_baseline_coverage(self) -> float: ...
152
+
153
+ def update_baseline_coverage(self, new_coverage: float) -> bool: ...
154
+
155
+ def is_coverage_regression(self, current_coverage: float) -> bool: ...
156
+
157
+ def get_coverage_improvement_needed(self) -> float: ...
158
+
159
+ def get_status_report(self) -> dict[str, t.Any]: ...
160
+
161
+ def get_coverage_report(self) -> str | None: ...
162
+
163
+ def check_and_update_coverage(self) -> dict[str, t.Any]: ...
164
+
165
+
166
+ @t.runtime_checkable
167
+ class ConfigurationServiceProtocol(t.Protocol):
168
+ """Protocol for configuration service."""
169
+
170
+ def update_precommit_config(self, options: OptionsProtocol) -> bool: ...
171
+
172
+ def update_pyproject_config(self, options: OptionsProtocol) -> bool: ...
173
+
174
+ def get_temp_config_path(self) -> str | None: ...
175
+
176
+
177
+ @t.runtime_checkable
178
+ class SecurityServiceProtocol(t.Protocol):
179
+ """Protocol for security service."""
180
+
181
+ def validate_file_safety(self, path: str | Path) -> bool: ...
182
+
183
+ def check_hardcoded_secrets(self, content: str) -> list[dict[str, t.Any]]: ...
184
+
185
+ def is_safe_subprocess_call(self, cmd: list[str]) -> bool: ...
186
+
187
+ def create_secure_command_env(self) -> dict[str, str]: ...
188
+
189
+ def mask_tokens(self, text: str) -> str: ...
190
+
191
+ def validate_token_format(self, token: str, token_type: str) -> bool: ...
192
+
193
+
194
+ @t.runtime_checkable
195
+ class InitializationServiceProtocol(t.Protocol):
196
+ """Protocol for initialization service."""
197
+
198
+ def initialize_project(self, project_path: str | Path) -> bool: ...
199
+
200
+ def validate_project_structure(self) -> bool: ...
201
+
202
+ def setup_git_hooks(self) -> bool: ...
203
+
204
+
205
+ @t.runtime_checkable
206
+ class UnifiedConfigurationServiceProtocol(t.Protocol):
207
+ """Protocol for unified configuration service."""
208
+
209
+ def merge_configurations(self) -> dict[str, t.Any]: ...
210
+
211
+ def validate_configuration(self, config: dict[str, t.Any]) -> bool: ...
212
+
213
+ def get_merged_config(self) -> dict[str, t.Any]: ...
214
+
215
+
147
216
  @t.runtime_checkable
148
217
  class TestManagerProtocol(t.Protocol):
149
218
  def run_tests(self, options: OptionsProtocol) -> bool: ...
@@ -56,10 +56,13 @@ class TaskStatusData:
56
56
  details: str | None = None
57
57
  error_message: str | None = None
58
58
  files_changed: list[str] | None = None
59
+ hook_results: list[t.Any] | None = None
59
60
 
60
61
  def __post_init__(self) -> None:
61
62
  if self.files_changed is None:
62
63
  self.files_changed = []
64
+ if self.hook_results is None:
65
+ self.hook_results = []
63
66
  if self.start_time is not None and self.end_time is not None:
64
67
  self.duration = self.end_time - self.start_time
65
68
 
@@ -1 +1 @@
1
- """Security utilities for Crackerjack."""
1
+ """Security utilities for Crackerjack."""
@@ -2,7 +2,6 @@
2
2
 
3
3
  import typing as t
4
4
  from dataclasses import dataclass
5
- from enum import Enum
6
5
 
7
6
  from crackerjack.config.hooks import SecurityLevel
8
7
 
@@ -10,7 +9,7 @@ from crackerjack.config.hooks import SecurityLevel
10
9
  @dataclass
11
10
  class SecurityCheckResult:
12
11
  """Result of a security check."""
13
-
12
+
14
13
  hook_name: str
15
14
  security_level: SecurityLevel
16
15
  passed: bool
@@ -18,74 +17,72 @@ class SecurityCheckResult:
18
17
  details: dict[str, t.Any] | None = None
19
18
 
20
19
 
21
- @dataclass
20
+ @dataclass
22
21
  class SecurityAuditReport:
23
22
  """Comprehensive security audit report for publishing decisions."""
24
-
23
+
25
24
  critical_failures: list[SecurityCheckResult]
26
- high_failures: list[SecurityCheckResult]
25
+ high_failures: list[SecurityCheckResult]
27
26
  medium_failures: list[SecurityCheckResult]
28
27
  low_failures: list[SecurityCheckResult]
29
-
28
+
30
29
  allows_publishing: bool
31
30
  security_warnings: list[str]
32
31
  recommendations: list[str]
33
-
32
+
34
33
  @property
35
34
  def has_critical_failures(self) -> bool:
36
35
  """Check if there are any critical security failures."""
37
36
  return len(self.critical_failures) > 0
38
-
37
+
39
38
  @property
40
39
  def total_failures(self) -> int:
41
40
  """Get total number of failed checks."""
42
41
  return (
43
- len(self.critical_failures) +
44
- len(self.high_failures) +
45
- len(self.medium_failures) +
46
- len(self.low_failures)
42
+ len(self.critical_failures)
43
+ + len(self.high_failures)
44
+ + len(self.medium_failures)
45
+ + len(self.low_failures)
47
46
  )
48
47
 
49
48
 
50
49
  class SecurityAuditor:
51
50
  """Security auditor for hook results following OWASP secure SDLC practices."""
52
-
51
+
53
52
  # Security-critical hooks that CANNOT be bypassed for publishing
54
53
  CRITICAL_HOOKS = {
55
- 'bandit': 'Security vulnerability detection (OWASP A09)',
56
- 'pyright': 'Type safety prevents runtime security holes (OWASP A04)',
57
- 'gitleaks': 'Secret/credential detection (OWASP A07)',
54
+ "bandit": "Security vulnerability detection (OWASP A09)",
55
+ "pyright": "Type safety prevents runtime security holes (OWASP A04)",
56
+ "gitleaks": "Secret/credential detection (OWASP A07)",
58
57
  }
59
-
58
+
60
59
  # High-importance security hooks that can be bypassed with warnings
61
60
  HIGH_SECURITY_HOOKS = {
62
- 'validate-regex-patterns': 'Regex vulnerability detection',
63
- 'creosote': 'Dependency vulnerability analysis',
64
- 'check-added-large-files': 'Large file security analysis',
65
- 'uv-lock': 'Dependency lock security',
61
+ "validate-regex-patterns": "Regex vulnerability detection",
62
+ "creosote": "Dependency vulnerability analysis",
63
+ "check-added-large-files": "Large file security analysis",
64
+ "uv-lock": "Dependency lock security",
66
65
  }
67
-
66
+
68
67
  def audit_hook_results(
69
- self,
70
- fast_results: list[t.Any],
71
- comprehensive_results: list[t.Any]
68
+ self, fast_results: list[t.Any], comprehensive_results: list[t.Any]
72
69
  ) -> SecurityAuditReport:
73
70
  """Audit hook results and generate security report.
74
-
71
+
75
72
  Args:
76
73
  fast_results: Results from fast hooks
77
74
  comprehensive_results: Results from comprehensive hooks
78
-
75
+
79
76
  Returns:
80
77
  SecurityAuditReport with security analysis
81
78
  """
82
79
  all_results = fast_results + comprehensive_results
83
-
80
+
84
81
  critical_failures = []
85
- high_failures = []
82
+ high_failures = []
86
83
  medium_failures = []
87
84
  low_failures = []
88
-
85
+
89
86
  for result in all_results:
90
87
  check_result = self._analyze_hook_result(result)
91
88
  if not check_result.passed:
@@ -97,18 +94,18 @@ class SecurityAuditor:
97
94
  medium_failures.append(check_result)
98
95
  else:
99
96
  low_failures.append(check_result)
100
-
97
+
101
98
  # Publishing is allowed only if no critical failures exist
102
99
  allows_publishing = len(critical_failures) == 0
103
-
100
+
104
101
  security_warnings = self._generate_security_warnings(
105
102
  critical_failures, high_failures, medium_failures
106
103
  )
107
-
104
+
108
105
  recommendations = self._generate_security_recommendations(
109
106
  critical_failures, high_failures, medium_failures
110
107
  )
111
-
108
+
112
109
  return SecurityAuditReport(
113
110
  critical_failures=critical_failures,
114
111
  high_failures=high_failures,
@@ -118,95 +115,112 @@ class SecurityAuditor:
118
115
  security_warnings=security_warnings,
119
116
  recommendations=recommendations,
120
117
  )
121
-
118
+
122
119
  def _analyze_hook_result(self, result: t.Any) -> SecurityCheckResult:
123
120
  """Analyze a single hook result for security implications."""
124
- hook_name = getattr(result, 'name', 'unknown')
125
- is_failed = getattr(result, 'status', 'unknown') in ('failed', 'error', 'timeout')
126
- error_message = getattr(result, 'output', None) or getattr(result, 'error', None)
127
-
121
+ hook_name = getattr(result, "name", "unknown")
122
+ is_failed = getattr(result, "status", "unknown") in (
123
+ "failed",
124
+ "error",
125
+ "timeout",
126
+ )
127
+ error_message = getattr(result, "output", None) or getattr(
128
+ result, "error", None
129
+ )
130
+
128
131
  # Determine security level
129
132
  security_level = self._get_hook_security_level(hook_name)
130
-
133
+
131
134
  return SecurityCheckResult(
132
135
  hook_name=hook_name,
133
136
  security_level=security_level,
134
137
  passed=not is_failed,
135
138
  error_message=error_message,
136
- details={'status': getattr(result, 'status', 'unknown')},
139
+ details={"status": getattr(result, "status", "unknown")},
137
140
  )
138
-
141
+
139
142
  def _get_hook_security_level(self, hook_name: str) -> SecurityLevel:
140
143
  """Get security level for a hook name."""
141
144
  hook_name_lower = hook_name.lower()
142
-
145
+
143
146
  if hook_name_lower in [name.lower() for name in self.CRITICAL_HOOKS]:
144
147
  return SecurityLevel.CRITICAL
145
148
  elif hook_name_lower in [name.lower() for name in self.HIGH_SECURITY_HOOKS]:
146
149
  return SecurityLevel.HIGH
147
- elif hook_name_lower in ['ruff-check', 'vulture', 'refurb', 'complexipy']:
150
+ elif hook_name_lower in ("ruff-check", "vulture", "refurb", "complexipy"):
148
151
  return SecurityLevel.MEDIUM
149
- else:
150
- return SecurityLevel.LOW
151
-
152
+ return SecurityLevel.LOW
153
+
152
154
  def _generate_security_warnings(
153
- self,
155
+ self,
154
156
  critical: list[SecurityCheckResult],
155
- high: list[SecurityCheckResult],
156
- medium: list[SecurityCheckResult]
157
+ high: list[SecurityCheckResult],
158
+ medium: list[SecurityCheckResult],
157
159
  ) -> list[str]:
158
160
  """Generate security warnings based on failed checks."""
159
161
  warnings = []
160
-
162
+
161
163
  if critical:
162
164
  warnings.append(
163
165
  f"🔒 CRITICAL: {len(critical)} security-critical checks failed - publishing BLOCKED"
164
166
  )
165
167
  for failure in critical:
166
- reason = self.CRITICAL_HOOKS.get(failure.hook_name.lower(), "Security-critical check")
168
+ reason = self.CRITICAL_HOOKS.get(
169
+ failure.hook_name.lower(), "Security-critical check"
170
+ )
167
171
  warnings.append(f" • {failure.hook_name}: {reason}")
168
-
172
+
169
173
  if high:
170
174
  warnings.append(
171
175
  f"⚠️ HIGH: {len(high)} high-security checks failed - review recommended"
172
176
  )
173
-
177
+
174
178
  if medium:
175
- warnings.append(
176
- f"ℹ️ MEDIUM: {len(medium)} standard quality checks failed"
177
- )
178
-
179
+ warnings.append(f"ℹ️ MEDIUM: {len(medium)} standard quality checks failed")
180
+
179
181
  return warnings
180
-
182
+
181
183
  def _generate_security_recommendations(
182
184
  self,
183
185
  critical: list[SecurityCheckResult],
184
186
  high: list[SecurityCheckResult],
185
- medium: list[SecurityCheckResult]
187
+ medium: list[SecurityCheckResult],
186
188
  ) -> list[str]:
187
189
  """Generate security recommendations based on OWASP best practices."""
188
190
  recommendations = []
189
-
191
+
190
192
  if critical:
191
- recommendations.append("🔧 Fix all CRITICAL security issues before publishing")
192
-
193
+ recommendations.append(
194
+ "🔧 Fix all CRITICAL security issues before publishing"
195
+ )
196
+
193
197
  # Specific recommendations based on failed checks
194
198
  critical_names = [f.hook_name.lower() for f in critical]
195
-
196
- if 'bandit' in critical_names:
197
- recommendations.append(" • Review bandit security findings - may indicate vulnerabilities")
198
- if 'pyright' in critical_names:
199
- recommendations.append(" • Fix type errors - type safety prevents runtime security holes")
200
- if 'gitleaks' in critical_names:
201
- recommendations.append(" • Remove secrets/credentials from code - use environment variables")
202
-
199
+
200
+ if "bandit" in critical_names:
201
+ recommendations.append(
202
+ " • Review bandit security findings - may indicate vulnerabilities"
203
+ )
204
+ if "pyright" in critical_names:
205
+ recommendations.append(
206
+ " • Fix type errors - type safety prevents runtime security holes"
207
+ )
208
+ if "gitleaks" in critical_names:
209
+ recommendations.append(
210
+ " • Remove secrets/credentials from code - use environment variables"
211
+ )
212
+
203
213
  if high:
204
- recommendations.append("🔍 Review HIGH-security findings before production deployment")
205
-
206
- if len(critical) == 0 and len(high) == 0:
214
+ recommendations.append(
215
+ "🔍 Review HIGH-security findings before production deployment"
216
+ )
217
+
218
+ if not critical and not high:
207
219
  recommendations.append("✅ Security posture is acceptable for publishing")
208
-
220
+
209
221
  # Add OWASP best practices reference
210
- recommendations.append("📖 Follow OWASP Secure Coding Practices for comprehensive security")
211
-
212
- return recommendations
222
+ recommendations.append(
223
+ "📖 Follow OWASP Secure Coding Practices for comprehensive security"
224
+ )
225
+
226
+ return recommendations
@@ -53,8 +53,9 @@ class ConfigurationService:
53
53
  )
54
54
  return False
55
55
 
56
- def get_temp_config_path(self) -> Path | None:
57
- return getattr(self, "_temp_config_path", None)
56
+ def get_temp_config_path(self) -> str | None:
57
+ path = getattr(self, "_temp_config_path", None)
58
+ return str(path) if path else None
58
59
 
59
60
  def _determine_config_mode(self, options: OptionsProtocol) -> str:
60
61
  if options.experimental_hooks:
@@ -8,9 +8,11 @@ import tomli_w
8
8
  import yaml
9
9
  from rich.console import Console
10
10
 
11
- from crackerjack.models.protocols import ConfigMergeServiceProtocol
12
- from crackerjack.services.filesystem import FileSystemService
13
- from crackerjack.services.git import GitService
11
+ from crackerjack.models.protocols import (
12
+ ConfigMergeServiceProtocol,
13
+ FileSystemInterface,
14
+ GitInterface,
15
+ )
14
16
  from crackerjack.services.logging import get_logger
15
17
 
16
18
 
@@ -29,8 +31,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
29
31
  def __init__(
30
32
  self,
31
33
  console: Console,
32
- filesystem: FileSystemService,
33
- git_service: GitService,
34
+ filesystem: FileSystemInterface,
35
+ git_service: GitInterface,
34
36
  ) -> None:
35
37
  self.console = console
36
38
  self.filesystem = filesystem
@@ -323,6 +325,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
323
325
  content = buffer.getvalue().decode("utf-8")
324
326
 
325
327
  # Clean trailing whitespace
328
+ from crackerjack.services.filesystem import FileSystemService
329
+
326
330
  content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
327
331
 
328
332
  with target_path.open("w", encoding="utf-8") as f:
@@ -350,6 +354,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
350
354
  content = content or ""
351
355
 
352
356
  # Clean trailing whitespace
357
+ from crackerjack.services.filesystem import FileSystemService
358
+
353
359
  content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
354
360
 
355
361
  with target_path.open("w") as f:
@@ -52,6 +52,28 @@ class CoverageRatchetService:
52
52
  def get_baseline(self) -> float:
53
53
  return self.get_ratchet_data().get("baseline", 0.0)
54
54
 
55
+ def get_baseline_coverage(self) -> float:
56
+ """Protocol method: Get baseline coverage."""
57
+ return self.get_baseline()
58
+
59
+ def update_baseline_coverage(self, new_coverage: float) -> bool:
60
+ """Protocol method: Update baseline coverage."""
61
+ return self.update_coverage(new_coverage).get("success", False)
62
+
63
+ def is_coverage_regression(self, current_coverage: float) -> bool:
64
+ """Protocol method: Check if coverage is a regression."""
65
+ baseline = self.get_baseline()
66
+ return current_coverage < (baseline - self.TOLERANCE_MARGIN)
67
+
68
+ def get_coverage_improvement_needed(self) -> float:
69
+ """Protocol method: Get coverage improvement needed."""
70
+ data = self.get_ratchet_data()
71
+ baseline = data.get("baseline", 0.0)
72
+ next_milestone = data.get("next_milestone")
73
+ if next_milestone:
74
+ return next_milestone - baseline
75
+ return 100.0 - baseline
76
+
55
77
  def update_coverage(self, new_coverage: float) -> dict[str, t.Any]:
56
78
  """Update coverage with 2% tolerance margin to prevent test flakiness.
57
79
 
@@ -6,6 +6,22 @@ from rich.console import Console
6
6
  from .secure_subprocess import execute_secure_subprocess
7
7
  from .security_logger import get_security_logger
8
8
 
9
+ # Centralized Git command registry for security validation
10
+ GIT_COMMANDS = {
11
+ "git_dir": ["rev-parse", "--git-dir"],
12
+ "staged_files": ["diff", "--cached", "--name-only", "--diff-filter=ACMRT"],
13
+ "unstaged_files": ["diff", "--name-only", "--diff-filter=ACMRT"],
14
+ "untracked_files": ["ls-files", "--others", "--exclude-standard"],
15
+ "staged_files_simple": ["diff", "--cached", "--name-only"],
16
+ "add_file": ["add"], # File path will be appended
17
+ "add_all": ["add", "-A", "."],
18
+ "commit": ["commit", "-m"], # Message will be appended
19
+ "add_updated": ["add", "-u"],
20
+ "push_porcelain": ["push", "--porcelain"],
21
+ "current_branch": ["branch", "--show-current"],
22
+ "commits_ahead": ["rev-list", "--count", "@{u}..HEAD"],
23
+ }
24
+
9
25
 
10
26
  class FailedGitResult:
11
27
  """A Git result object compatible with subprocess.CompletedProcess."""
@@ -51,34 +67,28 @@ class GitService:
51
67
 
52
68
  def is_git_repo(self) -> bool:
53
69
  try:
54
- result = self._run_git_command(["rev-parse", "- - git-dir"])
70
+ result = self._run_git_command(GIT_COMMANDS["git_dir"])
55
71
  return result.returncode == 0
56
72
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
57
73
  return False
58
74
 
59
75
  def get_changed_files(self) -> list[str]:
60
76
  try:
61
- staged_result = self._run_git_command(
62
- ["diff", "--cached", "- - name-only", "- - diff-filter=ACMRT"]
63
- )
77
+ staged_result = self._run_git_command(GIT_COMMANDS["staged_files"])
64
78
  staged_files = (
65
79
  staged_result.stdout.strip().split("\n")
66
80
  if staged_result.stdout.strip()
67
81
  else []
68
82
  )
69
83
 
70
- unstaged_result = self._run_git_command(
71
- ["diff", "- - name-only", "- - diff-filter=ACMRT"]
72
- )
84
+ unstaged_result = self._run_git_command(GIT_COMMANDS["unstaged_files"])
73
85
  unstaged_files = (
74
86
  unstaged_result.stdout.strip().split("\n")
75
87
  if unstaged_result.stdout.strip()
76
88
  else []
77
89
  )
78
90
 
79
- untracked_result = self._run_git_command(
80
- ["ls-files", "--others", "- - exclude-standard"],
81
- )
91
+ untracked_result = self._run_git_command(GIT_COMMANDS["untracked_files"])
82
92
  untracked_files = (
83
93
  untracked_result.stdout.strip().split("\n")
84
94
  if untracked_result.stdout.strip()
@@ -93,7 +103,7 @@ class GitService:
93
103
 
94
104
  def get_staged_files(self) -> list[str]:
95
105
  try:
96
- result = self._run_git_command(["diff", "--cached", "- - name-only"])
106
+ result = self._run_git_command(GIT_COMMANDS["staged_files_simple"])
97
107
  return result.stdout.strip().split("\n") if result.stdout.strip() else []
98
108
  except Exception as e:
99
109
  self.console.print(f"[yellow]⚠️[/ yellow] Error getting staged files: {e}")
@@ -102,7 +112,8 @@ class GitService:
102
112
  def add_files(self, files: list[str]) -> bool:
103
113
  try:
104
114
  for file in files:
105
- result = self._run_git_command(["add", file])
115
+ cmd = GIT_COMMANDS["add_file"] + [file]
116
+ result = self._run_git_command(cmd)
106
117
  if result.returncode != 0:
107
118
  self.console.print(
108
119
  f"[red]❌[/ red] Failed to add {file}: {result.stderr}",
@@ -116,7 +127,7 @@ class GitService:
116
127
  def add_all_files(self) -> bool:
117
128
  """Stage all changes including new, modified, and deleted files."""
118
129
  try:
119
- result = self._run_git_command(["add", "-A", "."])
130
+ result = self._run_git_command(GIT_COMMANDS["add_all"])
120
131
  if result.returncode == 0:
121
132
  self.console.print("[green]✅[/ green] Staged all changes")
122
133
  return True
@@ -130,7 +141,8 @@ class GitService:
130
141
 
131
142
  def commit(self, message: str) -> bool:
132
143
  try:
133
- result = self._run_git_command(["commit", "- m", message])
144
+ cmd = GIT_COMMANDS["commit"] + [message]
145
+ result = self._run_git_command(cmd)
134
146
  if result.returncode == 0:
135
147
  self.console.print(f"[green]✅[/ green] Committed: {message}")
136
148
  return True
@@ -153,14 +165,15 @@ class GitService:
153
165
  "[yellow]🔄[/ yellow] Pre - commit hooks modified files - attempting to re-stage and retry commit"
154
166
  )
155
167
 
156
- add_result = self._run_git_command(["add", "- u"])
168
+ add_result = self._run_git_command(GIT_COMMANDS["add_updated"])
157
169
  if add_result.returncode != 0:
158
170
  self.console.print(
159
171
  f"[red]❌[/ red] Failed to re-stage files: {add_result.stderr}"
160
172
  )
161
173
  return False
162
174
 
163
- retry_result = self._run_git_command(["commit", "- m", message])
175
+ cmd = GIT_COMMANDS["commit"] + [message]
176
+ retry_result = self._run_git_command(cmd)
164
177
  if retry_result.returncode == 0:
165
178
  self.console.print(
166
179
  f"[green]✅[/ green] Committed after re-staging: {message}"
@@ -188,7 +201,7 @@ class GitService:
188
201
  def push(self) -> bool:
189
202
  try:
190
203
  # Get detailed push information
191
- result = self._run_git_command(["push", "--porcelain"])
204
+ result = self._run_git_command(GIT_COMMANDS["push_porcelain"])
192
205
  if result.returncode == 0:
193
206
  self._display_push_success(result.stdout)
194
207
  return True
@@ -241,7 +254,7 @@ class GitService:
241
254
  """Fallback method to show commit count information."""
242
255
  try:
243
256
  # Get commits ahead of remote
244
- result = self._run_git_command(["rev-list", "--count", "@{u}..HEAD"])
257
+ result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
245
258
  if result.returncode == 0 and result.stdout.strip().isdigit():
246
259
  commit_count = int(result.stdout.strip())
247
260
  if commit_count > 0:
@@ -260,17 +273,17 @@ class GitService:
260
273
 
261
274
  def get_current_branch(self) -> str | None:
262
275
  try:
263
- result = self._run_git_command(["branch", "- - show-current"])
276
+ result = self._run_git_command(GIT_COMMANDS["current_branch"])
264
277
  return result.stdout.strip() if result.returncode == 0 else None
265
278
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
266
279
  return None
267
280
 
268
- def get_commit_message_suggestions(self, files: list[str]) -> list[str]:
269
- if not files:
281
+ def get_commit_message_suggestions(self, changed_files: list[str]) -> list[str]:
282
+ if not changed_files:
270
283
  return ["Update project files"]
271
- file_categories = self._categorize_files(files)
284
+ file_categories = self._categorize_files(changed_files)
272
285
  messages = self._generate_category_messages(file_categories)
273
- messages.extend(self._generate_specific_messages(files))
286
+ messages.extend(self._generate_specific_messages(changed_files))
274
287
 
275
288
  return messages[:5]
276
289
 
@@ -326,7 +339,7 @@ class GitService:
326
339
  from contextlib import suppress
327
340
 
328
341
  with suppress(ValueError, Exception):
329
- result = self._run_git_command(["rev-list", "--count", "@{u}..HEAD"])
342
+ result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
330
343
  if result.returncode == 0 and result.stdout.strip().isdigit():
331
344
  return int(result.stdout.strip())
332
345
  return 0