crackerjack 0.37.7__py3-none-any.whl → 0.37.9__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.
@@ -67,10 +67,8 @@ class WorkflowPipeline:
67
67
 
68
68
  # Initialize quality intelligence for advanced decision making
69
69
  try:
70
- quality_baseline = EnhancedQualityBaselineService(console, pkg_path)
71
- self._quality_intelligence = QualityIntelligenceService(
72
- quality_baseline, console
73
- )
70
+ quality_baseline = EnhancedQualityBaselineService()
71
+ self._quality_intelligence = QualityIntelligenceService(quality_baseline)
74
72
  except Exception:
75
73
  # Fallback gracefully if quality intelligence is not available
76
74
  self._quality_intelligence = None
@@ -418,11 +416,11 @@ class WorkflowPipeline:
418
416
  success = success and config_success
419
417
 
420
418
  quality_success = await self._execute_quality_phase(options, workflow_id)
421
- if not quality_success:
422
- success = False
419
+ success = success and quality_success
423
420
 
424
- if self._is_publishing_workflow(options):
425
- return False
421
+ # If quality phase failed and we're in publishing mode, stop here
422
+ if not quality_success and self._is_publishing_workflow(options):
423
+ return False
426
424
 
427
425
  # Execute publishing workflow if requested
428
426
  publishing_success = await self._execute_publishing_workflow(
@@ -447,6 +445,30 @@ class WorkflowPipeline:
447
445
 
448
446
  return success
449
447
 
448
+ def _handle_quality_phase_result(
449
+ self, success: bool, quality_success: bool, options: OptionsProtocol
450
+ ) -> bool:
451
+ """Handle the result of the quality phase execution."""
452
+ if not quality_success:
453
+ if self._is_publishing_workflow(options):
454
+ # For publishing workflows, quality failures should stop execution
455
+ return False
456
+ # For non-publishing workflows, we continue but mark as failed
457
+ return False
458
+ return success
459
+
460
+ def _handle_workflow_completion(
461
+ self, success: bool, publishing_success: bool, options: OptionsProtocol
462
+ ) -> bool:
463
+ """Handle workflow completion and determine final success status."""
464
+ # Only fail the overall workflow if publishing was explicitly requested and failed
465
+ if not publishing_success and (options.publish or options.all):
466
+ self.console.print(
467
+ "[red]❌ Publishing failed - overall workflow marked as failed[/red]"
468
+ )
469
+ return False
470
+ return success
471
+
450
472
  def _is_publishing_workflow(self, options: OptionsProtocol) -> bool:
451
473
  return bool(options.publish or options.all)
452
474
 
@@ -504,14 +526,14 @@ class WorkflowPipeline:
504
526
  return "Quality intelligence not available"
505
527
 
506
528
  # Analyze recent quality trends and anomalies
507
- anomalies = await self._quality_intelligence.detect_anomalies()
508
- patterns = await self._quality_intelligence.identify_patterns()
529
+ anomalies = self._quality_intelligence.detect_anomalies()
530
+ patterns = self._quality_intelligence.identify_patterns()
509
531
 
510
532
  # Make intelligent recommendations based on current state
511
533
  recommendations = []
512
534
  if anomalies:
513
535
  high_severity_anomalies = [
514
- a for a in anomalies if a.severity.name in ["CRITICAL", "HIGH"]
536
+ a for a in anomalies if a.severity.name in ("CRITICAL", "HIGH")
515
537
  ]
516
538
  if high_severity_anomalies:
517
539
  recommendations.append(
@@ -397,13 +397,29 @@ class ReferenceGenerator:
397
397
 
398
398
  arg_index = func_node.args.args.index(arg)
399
399
  if arg_index >= defaults_start:
400
- default_index = arg_index - defaults_start
401
- default_node = func_node.args.defaults[default_index]
402
- if isinstance(default_node, ast.Constant):
403
- return default_node.value, False
400
+ return self._extract_argument_default(arg_index, defaults_start, func_node)
404
401
 
405
402
  return None, True
406
403
 
404
+ def _extract_argument_default(
405
+ self, arg_index: int, defaults_start: int, func_node: ast.FunctionDef
406
+ ) -> tuple[t.Any, bool]:
407
+ """Extract default value for a specific argument.
408
+
409
+ Args:
410
+ arg_index: Index of the argument
411
+ defaults_start: Index where defaults start
412
+ func_node: Parent function node
413
+
414
+ Returns:
415
+ Tuple of (default_value, required)
416
+ """
417
+ default_index = arg_index - defaults_start
418
+ default_node = func_node.args.defaults[default_index]
419
+ if isinstance(default_node, ast.Constant):
420
+ return default_node.value, False
421
+ return None, True
422
+
407
423
  async def _enhance_with_examples(
408
424
  self, commands: dict[str, CommandInfo]
409
425
  ) -> dict[str, CommandInfo]:
@@ -416,35 +432,47 @@ class ReferenceGenerator:
416
432
  Enhanced commands with examples
417
433
  """
418
434
  for command in commands.values():
419
- # Generate basic examples
420
- basic_example = f"python -m crackerjack --{command.name}"
421
-
422
- # Add parameter examples
423
- param_examples = []
424
- for param in command.parameters:
425
- if not param.required and param.default_value is not None:
426
- if isinstance(param.default_value, bool):
427
- param_examples.append(f"--{param.name}")
428
- else:
429
- param_examples.append(f"--{param.name} {param.default_value}")
430
-
431
- if param_examples:
432
- enhanced_example = f"{basic_example} {' '.join(param_examples)}"
433
- command.examples.append(
434
- {
435
- "description": f"Using {command.name} with parameters",
436
- "command": enhanced_example,
437
- }
438
- )
435
+ self._add_basic_example(command)
436
+ self._add_parameter_examples(command)
437
+ return commands
438
+
439
+ def _add_basic_example(self, command: CommandInfo) -> None:
440
+ """Add a basic example for a command."""
441
+ basic_example = f"python -m crackerjack --{command.name}"
442
+ command.examples.append(
443
+ {
444
+ "description": f"Basic {command.name} usage",
445
+ "command": basic_example,
446
+ }
447
+ )
439
448
 
449
+ def _add_parameter_examples(self, command: CommandInfo) -> None:
450
+ """Add parameter examples for a command."""
451
+ # Generate basic examples
452
+ basic_example = f"python -m crackerjack --{command.name}"
453
+
454
+ # Add parameter examples
455
+ param_examples = []
456
+ for param in command.parameters:
457
+ if not param.required and param.default_value is not None:
458
+ param_example = self._format_parameter_example(param)
459
+ if param_example:
460
+ param_examples.append(param_example)
461
+
462
+ if param_examples:
463
+ enhanced_example = f"{basic_example} {' '.join(param_examples)}"
440
464
  command.examples.append(
441
465
  {
442
- "description": f"Basic {command.name} usage",
443
- "command": basic_example,
466
+ "description": f"Using {command.name} with parameters",
467
+ "command": enhanced_example,
444
468
  }
445
469
  )
446
470
 
447
- return commands
471
+ def _format_parameter_example(self, param: ParameterInfo) -> str | None:
472
+ """Format a parameter example."""
473
+ if isinstance(param.default_value, bool):
474
+ return f"--{param.name}"
475
+ return f"--{param.name} {param.default_value}"
448
476
 
449
477
  async def _enhance_with_workflows(
450
478
  self, commands: dict[str, CommandInfo]
@@ -632,26 +660,26 @@ class ReferenceGenerator:
632
660
  ]
633
661
 
634
662
  def _render_markdown_categories(self, reference: CommandReference) -> list[str]:
635
- """Render command categories for markdown.
636
-
637
- Args:
638
- reference: Command reference
639
-
640
- Returns:
641
- List of category section lines
642
- """
663
+ """Render command categories for markdown."""
643
664
  category_lines = []
644
665
  for category, command_names in reference.categories.items():
645
- category_lines.extend(
646
- [
647
- f"## {category.title()}",
648
- "",
649
- ]
650
- )
666
+ category_section = self._render_markdown_category(category, reference.commands, command_names)
667
+ category_lines.extend(category_section)
668
+ return category_lines
651
669
 
652
- for command_name in command_names:
653
- command = reference.commands[command_name]
654
- category_lines.extend(self._render_command_markdown(command))
670
+ def _render_markdown_category(
671
+ self, category: str, commands: dict[str, CommandInfo], command_names: list[str]
672
+ ) -> list[str]:
673
+ """Render markdown for a single category."""
674
+ category_lines = [
675
+ f"## {category.title()}",
676
+ "",
677
+ ]
678
+
679
+ for command_name in command_names:
680
+ command = commands[command_name]
681
+ command_lines = self._render_command_markdown(command)
682
+ category_lines.extend(command_lines)
655
683
 
656
684
  return category_lines
657
685
 
@@ -685,14 +713,7 @@ class ReferenceGenerator:
685
713
  return workflow_lines
686
714
 
687
715
  def _render_command_markdown(self, command: CommandInfo) -> list[str]:
688
- """Render single command as Markdown.
689
-
690
- Args:
691
- command: Command to render
692
-
693
- Returns:
694
- List of markdown lines
695
- """
716
+ """Render single command as Markdown."""
696
717
  lines = [
697
718
  f"### `{command.name}`",
698
719
  "",
@@ -702,50 +723,47 @@ class ReferenceGenerator:
702
723
 
703
724
  # Add parameters section
704
725
  if command.parameters:
705
- lines.extend(self._render_command_parameters_markdown(command.parameters))
726
+ param_lines = self._render_command_parameters_markdown(command.parameters)
727
+ lines.extend(param_lines)
706
728
 
707
729
  # Add examples section
708
730
  if command.examples:
709
- lines.extend(self._render_command_examples_markdown(command.examples))
731
+ example_lines = self._render_command_examples_markdown(command.examples)
732
+ lines.extend(example_lines)
710
733
 
711
734
  # Add related commands section
712
735
  if command.related_commands:
713
- lines.extend(
714
- self._render_command_related_markdown(command.related_commands)
715
- )
736
+ related_lines = self._render_command_related_markdown(command.related_commands)
737
+ lines.extend(related_lines)
716
738
 
717
739
  return lines
718
740
 
719
741
  def _render_command_parameters_markdown(
720
742
  self, parameters: list[ParameterInfo]
721
743
  ) -> list[str]:
722
- """Render command parameters for markdown.
723
-
724
- Args:
725
- parameters: List of parameters to render
726
-
727
- Returns:
728
- List of parameter section lines
729
- """
744
+ """Render command parameters for markdown."""
730
745
  param_lines = [
731
746
  "**Parameters:**",
732
747
  "",
733
748
  ]
734
749
 
735
750
  for param in parameters:
736
- required_str = " (required)" if param.required else ""
737
- default_str = (
738
- f" (default: {param.default_value})"
739
- if param.default_value is not None
740
- else ""
741
- )
742
- param_lines.append(
743
- f"- `--{param.name}` ({param.type_hint}){required_str}{default_str}: {param.description}"
744
- )
751
+ param_line = self._format_parameter_line(param)
752
+ param_lines.append(param_line)
745
753
 
746
754
  param_lines.append("")
747
755
  return param_lines
748
756
 
757
+ def _format_parameter_line(self, param: ParameterInfo) -> str:
758
+ """Format a single parameter line."""
759
+ required_str = " (required)" if param.required else ""
760
+ default_str = (
761
+ f" (default: {param.default_value})"
762
+ if param.default_value is not None
763
+ else ""
764
+ )
765
+ return f"- `--{param.name}` ({param.type_hint}){required_str}{default_str}: {param.description}"
766
+
749
767
  def _render_command_examples_markdown(
750
768
  self, examples: list[dict[str, str]]
751
769
  ) -> list[str]:
@@ -794,12 +812,12 @@ class ReferenceGenerator:
794
812
 
795
813
  def _render_html(self, reference: CommandReference) -> str:
796
814
  """Render reference as HTML."""
797
- html = self._render_html_header(
798
- reference.generated_at.strftime("%Y-%m-%d %H:%M:%S")
799
- )
800
- html += self._render_html_commands(reference)
801
- html += "</body></html>"
802
- return html
815
+ html_parts = [
816
+ self._render_html_header(reference.generated_at.strftime("%Y-%m-%d %H:%M:%S")),
817
+ self._render_html_commands(reference),
818
+ "</body></html>"
819
+ ]
820
+ return "".join(html_parts)
803
821
 
804
822
  def _render_html_header(self, generated_at: str) -> str:
805
823
  """Render HTML header with styles and metadata."""
@@ -822,26 +840,38 @@ class ReferenceGenerator:
822
840
 
823
841
  def _render_html_commands(self, reference: CommandReference) -> str:
824
842
  """Render HTML commands by category."""
825
- html = ""
843
+ html_parts = []
826
844
  for category, command_names in reference.categories.items():
827
- html += f"<h2>{category.title()}</h2>"
828
- html += self._render_html_category_commands(
829
- reference.commands, command_names
830
- )
845
+ category_html = self._render_html_category(category, reference.commands, command_names)
846
+ html_parts.append(category_html)
847
+ return "".join(html_parts)
848
+
849
+ def _render_html_category(
850
+ self, category: str, commands: dict[str, CommandInfo], command_names: list[str]
851
+ ) -> str:
852
+ """Render HTML for a single category."""
853
+ html = f"<h2>{category.title()}</h2>"
854
+ html += self._render_html_category_commands(commands, command_names)
831
855
  return html
832
856
 
833
857
  def _render_html_category_commands(
834
858
  self, commands: dict[str, CommandInfo], command_names: list[str]
835
859
  ) -> str:
836
860
  """Render HTML for commands in a category."""
837
- html = ""
861
+ html_parts = []
838
862
  for command_name in command_names:
839
863
  command = commands[command_name]
840
- html += '<div class="command">'
841
- html += f"<h3><code>{command.name}</code></h3>"
842
- html += f"<p>{command.description}</p>"
843
- html += self._render_html_command_parameters(command.parameters)
844
- html += "</div>"
864
+ command_html = self._render_single_html_command(command)
865
+ html_parts.append(command_html)
866
+ return "".join(html_parts)
867
+
868
+ def _render_single_html_command(self, command: CommandInfo) -> str:
869
+ """Render HTML for a single command."""
870
+ html = '<div class="command">'
871
+ html += f"<h3><code>{command.name}</code></h3>"
872
+ html += f"<p>{command.description}</p>"
873
+ html += self._render_html_command_parameters(command.parameters)
874
+ html += "</div>"
845
875
  return html
846
876
 
847
877
  def _render_html_command_parameters(self, parameters: list[ParameterInfo]) -> str:
@@ -864,31 +894,44 @@ class ReferenceGenerator:
864
894
  "version": reference.version,
865
895
  "categories": reference.categories,
866
896
  "workflows": reference.workflows,
867
- "commands": {},
897
+ "commands": self._serialize_commands(reference.commands),
868
898
  }
869
899
 
870
- for name, command in reference.commands.items():
871
- data["commands"][name] = {
872
- "name": command.name,
873
- "description": command.description,
874
- "category": command.category,
875
- "parameters": [
876
- {
877
- "name": param.name,
878
- "type": param.type_hint,
879
- "default": param.default_value,
880
- "description": param.description,
881
- "required": param.required,
882
- }
883
- for param in command.parameters
884
- ],
885
- "examples": command.examples,
886
- "related_commands": command.related_commands,
887
- "aliases": command.aliases,
888
- }
889
-
890
900
  return json.dumps(data, indent=2, default=str)
891
901
 
902
+ def _serialize_commands(self, commands: dict[str, CommandInfo]) -> dict[str, t.Any]:
903
+ """Serialize commands for JSON output."""
904
+ serialized_commands = {}
905
+ for name, command in commands.items():
906
+ serialized_commands[name] = self._serialize_command(command)
907
+ return serialized_commands
908
+
909
+ def _serialize_command(self, command: CommandInfo) -> dict[str, t.Any]:
910
+ """Serialize a single command for JSON output."""
911
+ return {
912
+ "name": command.name,
913
+ "description": command.description,
914
+ "category": command.category,
915
+ "parameters": self._serialize_parameters(command.parameters),
916
+ "examples": command.examples,
917
+ "related_commands": command.related_commands,
918
+ "aliases": command.aliases,
919
+ }
920
+
921
+ def _serialize_parameters(self, parameters: list[ParameterInfo]) -> list[dict[str, t.Any]]:
922
+ """Serialize parameters for JSON output."""
923
+ return [self._serialize_parameter(param) for param in parameters]
924
+
925
+ def _serialize_parameter(self, param: ParameterInfo) -> dict[str, t.Any]:
926
+ """Serialize a single parameter for JSON output."""
927
+ return {
928
+ "name": param.name,
929
+ "type": param.type_hint,
930
+ "default": param.default_value,
931
+ "description": param.description,
932
+ "required": param.required,
933
+ }
934
+
892
935
  def _render_yaml(self, reference: CommandReference) -> str:
893
936
  """Render reference as YAML."""
894
937
  import yaml
@@ -1,5 +1,6 @@
1
1
  import tempfile
2
2
  import typing as t
3
+ from contextlib import suppress
3
4
  from pathlib import Path
4
5
 
5
6
  import jinja2
@@ -459,33 +460,36 @@ PRE_COMMIT_TEMPLATE = """repos:
459
460
  class DynamicConfigGenerator:
460
461
  def __init__(self, package_directory: str | None = None) -> None:
461
462
  self.template = jinja2.Template(PRE_COMMIT_TEMPLATE)
462
- self.package_directory = package_directory or self._detect_package_directory()
463
+ self.package_directory = self._sanitize_package_directory(
464
+ package_directory or self._detect_package_directory()
465
+ )
466
+
467
+ def _sanitize_package_directory(self, package_directory: str) -> str:
468
+ """Convert hyphens to underscores in package directory names."""
469
+ return package_directory.replace("-", "_")
463
470
 
464
471
  def _detect_package_directory(self) -> str:
465
472
  """Detect the package directory name for the current project."""
466
- import os
467
473
  from pathlib import Path
468
474
 
469
475
  # Check if we're in the crackerjack project itself
470
- current_dir = Path(os.getcwd())
476
+ current_dir = Path.cwd()
471
477
  if (current_dir / "crackerjack").exists() and (
472
478
  current_dir / "pyproject.toml"
473
479
  ).exists():
474
480
  # Check if this is actually the crackerjack project
475
- try:
481
+ with suppress(Exception):
476
482
  import tomllib
477
483
 
478
484
  with (current_dir / "pyproject.toml").open("rb") as f:
479
485
  data = tomllib.load(f)
480
486
  if data.get("project", {}).get("name") == "crackerjack":
481
487
  return "crackerjack"
482
- except Exception:
483
- pass
484
488
 
485
489
  # Try to read package name from pyproject.toml
486
490
  pyproject_path = current_dir / "pyproject.toml"
487
491
  if pyproject_path.exists():
488
- try:
492
+ with suppress(Exception):
489
493
  import tomllib
490
494
 
491
495
  with pyproject_path.open("rb") as f:
@@ -496,8 +500,6 @@ class DynamicConfigGenerator:
496
500
  # Check if package directory exists
497
501
  if (current_dir / package_name).exists():
498
502
  return package_name
499
- except Exception:
500
- pass
501
503
 
502
504
  # Fallback to project directory name
503
505
  if (current_dir / current_dir.name).exists():
@@ -23,9 +23,7 @@ def main() -> int:
23
23
 
24
24
  # If no files specified, check project files
25
25
  if not files_to_check:
26
- project_path = Path.cwd()
27
- lsp_client = LSPClient(console)
28
- files_to_check = lsp_client.get_project_files(project_path)
26
+ files_to_check = _get_project_files(console)
29
27
 
30
28
  if not files_to_check:
31
29
  console.print("🔍 No Python files to check")
@@ -36,29 +34,46 @@ def main() -> int:
36
34
 
37
35
  # Check if LSP server is running
38
36
  if not lsp_client.is_server_running():
39
- console.print(
40
- "⚠️ Zuban LSP server not running, falling back to direct zuban check"
41
- )
42
- # Fall back to regular zuban execution
43
- import subprocess
44
-
45
- try:
46
- result = subprocess.run(
47
- ["zuban", "check"] + files_to_check,
48
- capture_output=True,
49
- text=True,
50
- timeout=120,
51
- )
52
- if result.stdout:
53
- console.print(result.stdout)
54
- if result.stderr:
55
- console.print(result.stderr, style="red")
56
- return result.returncode
57
- except (subprocess.TimeoutExpired, FileNotFoundError) as e:
58
- console.print(f"❌ Error running zuban: {e}", style="red")
59
- return 1
37
+ return _fallback_to_zuban_check(console, files_to_check)
60
38
 
61
39
  # Use LSP server for type checking
40
+ return _check_files_with_lsp(console, lsp_client, files_to_check)
41
+
42
+
43
+ def _get_project_files(console: Console) -> list[str]:
44
+ """Get project files to check."""
45
+ project_path = Path.cwd()
46
+ lsp_client = LSPClient(console)
47
+ return lsp_client.get_project_files(project_path)
48
+
49
+
50
+ def _fallback_to_zuban_check(console: Console, files_to_check: list[str]) -> int:
51
+ """Fall back to regular zuban execution when LSP server is not running."""
52
+ console.print("⚠️ Zuban LSP server not running, falling back to direct zuban check")
53
+ # Fall back to regular zuban execution
54
+ import subprocess
55
+
56
+ try:
57
+ result = subprocess.run(
58
+ ["zuban", "check"] + files_to_check,
59
+ capture_output=True,
60
+ text=True,
61
+ timeout=120,
62
+ )
63
+ if result.stdout:
64
+ console.print(result.stdout)
65
+ if result.stderr:
66
+ console.print(result.stderr, style="red")
67
+ return result.returncode
68
+ except (subprocess.TimeoutExpired, FileNotFoundError) as e:
69
+ console.print(f"❌ Error running zuban: {e}", style="red")
70
+ return 1
71
+
72
+
73
+ def _check_files_with_lsp(
74
+ console: Console, lsp_client: LSPClient, files_to_check: list[str]
75
+ ) -> int:
76
+ """Check files using LSP server."""
62
77
  server_info = lsp_client.get_server_info()
63
78
  if server_info:
64
79
  console.print(f"🔍 Using Zuban LSP server (PID: {server_info['pid']})")
@@ -37,23 +37,12 @@ class WorkflowOptions:
37
37
  @classmethod
38
38
  def from_args(cls, args: t.Any) -> "WorkflowOptions":
39
39
  return cls(
40
- cleaning=CleaningConfig(
41
- clean=getattr(args, "clean", False),
42
- ),
43
- testing=TestConfig(
44
- test=getattr(args, "test", False),
45
- ),
46
- publishing=PublishConfig(
47
- publish=getattr(args, "publish", None),
48
- bump=getattr(args, "bump", None),
49
- ),
50
- git=GitConfig(
51
- commit=getattr(args, "commit", False),
52
- create_pr=getattr(args, "create_pr", False),
53
- ),
54
- ai=AIConfig(
55
- ai_agent=getattr(args, "ai_agent", False),
56
- ),
40
+ clean=getattr(args, "clean", False),
41
+ test=getattr(args, "test", False),
42
+ publish=getattr(args, "publish", None),
43
+ bump=getattr(args, "bump", None),
44
+ commit=getattr(args, "commit", False),
45
+ create_pr=getattr(args, "create_pr", False),
57
46
  interactive=getattr(args, "interactive", True),
58
47
  dry_run=getattr(args, "dry_run", False),
59
48
  )
@@ -124,7 +124,7 @@ class PublishManagerImpl:
124
124
 
125
125
  # Get intelligent version analysis and recommendation
126
126
  recommendation = self._get_version_recommendation()
127
- if recommendation and version_type not in ["interactive"]:
127
+ if recommendation and version_type != "interactive":
128
128
  self._display_version_analysis(recommendation)
129
129
  if version_type == "auto":
130
130
  version_type = recommendation.bump_type.value
@@ -812,24 +812,23 @@ def _register_agent_suggestions_tool(mcp_app: t.Any) -> None:
812
812
  t.cast(list[str], suggestions["usage_patterns"])
813
813
 
814
814
  if project_type.lower() == "python" or "python" in task_description.lower():
815
- primary_agents.append(
816
- {
817
- "name": "crackerjack-architect",
818
- "emoji": "🏗️",
819
- "description": "Expert in crackerjack's modular architecture and Python project management patterns",
820
- "usage": "Use PROACTIVELY for all feature development, architectural decisions, and ensuring code follows crackerjack standards",
821
- "priority": "HIGH",
822
- }
823
- )
824
-
825
- primary_agents.append(
826
- {
827
- "name": "python-pro",
828
- "emoji": "🐍",
829
- "description": "Modern Python development with type hints, async/await patterns, and clean architecture",
830
- "usage": "Use for implementing Python code with best practices",
831
- "priority": "HIGH",
832
- }
815
+ primary_agents.extend(
816
+ (
817
+ {
818
+ "name": "crackerjack-architect",
819
+ "emoji": "🏗️",
820
+ "description": "Expert in crackerjack's modular architecture and Python project management patterns",
821
+ "usage": "Use PROACTIVELY for all feature development, architectural decisions, and ensuring code follows crackerjack standards",
822
+ "priority": "HIGH",
823
+ },
824
+ {
825
+ "name": "python-pro",
826
+ "emoji": "🐍",
827
+ "description": "Modern Python development with type hints, async/await patterns, and clean architecture",
828
+ "usage": "Use for implementing Python code with best practices",
829
+ "priority": "HIGH",
830
+ },
831
+ )
833
832
  )
834
833
 
835
834
  task_lower = task_description.lower()
@@ -839,24 +838,23 @@ def _register_agent_suggestions_tool(mcp_app: t.Any) -> None:
839
838
  word in task_lower + context_lower
840
839
  for word in ("test", "testing", "coverage", "pytest")
841
840
  ):
842
- task_specific_agents.append(
843
- {
844
- "name": "crackerjack-test-specialist",
845
- "emoji": "🧪",
846
- "description": "Advanced testing specialist for complex scenarios and coverage optimization",
847
- "usage": "Use for test creation, debugging test failures, and coverage improvements",
848
- "priority": "HIGH",
849
- }
850
- )
851
-
852
- task_specific_agents.append(
853
- {
854
- "name": "pytest-hypothesis-specialist",
855
- "emoji": "🧪",
856
- "description": "Advanced testing patterns and property-based testing",
857
- "usage": "Use for comprehensive test development and optimization",
858
- "priority": "MEDIUM",
859
- }
841
+ task_specific_agents.extend(
842
+ (
843
+ {
844
+ "name": "crackerjack-test-specialist",
845
+ "emoji": "🧪",
846
+ "description": "Advanced testing specialist for complex scenarios and coverage optimization",
847
+ "usage": "Use for test creation, debugging test failures, and coverage improvements",
848
+ "priority": "HIGH",
849
+ },
850
+ {
851
+ "name": "pytest-hypothesis-specialist",
852
+ "emoji": "🧪",
853
+ "description": "Advanced testing patterns and property-based testing",
854
+ "usage": "Use for comprehensive test development and optimization",
855
+ "priority": "MEDIUM",
856
+ },
857
+ )
860
858
  )
861
859
 
862
860
  if any(
@@ -12,8 +12,10 @@ class ConfigurationService:
12
12
  def __init__(self, console: Console, pkg_path: Path) -> None:
13
13
  self.console = console
14
14
  self.pkg_path = pkg_path
15
- # Extract package directory name from the pkg_path
15
+ # Extract package directory name from the pkg_path and sanitize it
16
16
  package_directory = pkg_path.name if pkg_path != Path.cwd() else None
17
+ if package_directory:
18
+ package_directory = package_directory.replace("-", "_")
17
19
  self.config_generator = DynamicConfigGenerator(package_directory)
18
20
 
19
21
  def update_precommit_config(self, options: OptionsProtocol) -> bool:
@@ -310,7 +310,7 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
310
310
  sort_keys=False,
311
311
  width=float("inf"),
312
312
  )
313
- content = yaml_content or ""
313
+ content = yaml_content
314
314
 
315
315
  from crackerjack.services.filesystem import FileSystemService
316
316
 
@@ -506,35 +506,38 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
506
506
  processed_repos = []
507
507
  for repo in repos:
508
508
  processed_repo = copy.deepcopy(repo)
509
-
510
- # Process hooks within each repo
511
- hooks = processed_repo.get("hooks", [])
512
- for hook in hooks:
513
- if isinstance(hook, dict):
514
- # Replace crackerjack directory references in args
515
- if "args" in hook:
516
- hook["args"] = [
517
- arg.replace("crackerjack", project_name)
518
- if isinstance(arg, str)
519
- else arg
520
- for arg in hook["args"]
521
- ]
522
-
523
- # Replace crackerjack directory references in files pattern
524
- if "files" in hook:
525
- files_pattern = hook["files"]
526
- if isinstance(files_pattern, str):
527
- hook["files"] = files_pattern.replace(
528
- "^crackerjack/", f"^{project_name}/"
529
- )
530
-
531
- # Special handling for validate-regex-patterns hook - keep it pointing to crackerjack package
532
- if hook.get("id") == "validate-regex-patterns":
533
- # This should reference the installed crackerjack package, not the current project
534
- # The entry already uses "uv run python -m crackerjack.tools.validate_regex_patterns"
535
- # which is correct - it runs from the installed crackerjack package
536
- pass
537
-
509
+ self._process_repo_hooks(processed_repo, project_name)
538
510
  processed_repos.append(processed_repo)
539
511
 
540
512
  return processed_repos
513
+
514
+ def _process_repo_hooks(self, repo: dict[str, t.Any], project_name: str) -> None:
515
+ """Process hooks within a repo to replace project-specific references."""
516
+ hooks = repo.get("hooks", [])
517
+ for hook in hooks:
518
+ if isinstance(hook, dict):
519
+ self._process_hook_args(hook, project_name)
520
+ self._process_hook_files(hook, project_name)
521
+ # Special handling for validate-regex-patterns hook - keep it pointing to crackerjack package
522
+ # This should reference the installed crackerjack package, not the current project
523
+ # The entry already uses "uv run python -m crackerjack.tools.validate_regex_patterns"
524
+ # which is correct - it runs from the installed crackerjack package
525
+
526
+ def _process_hook_args(self, hook: dict[str, t.Any], project_name: str) -> None:
527
+ """Process hook args to replace project-specific references."""
528
+ if "args" in hook:
529
+ hook["args"] = [
530
+ arg.replace("crackerjack", project_name)
531
+ if isinstance(arg, str)
532
+ else arg
533
+ for arg in hook["args"]
534
+ ]
535
+
536
+ def _process_hook_files(self, hook: dict[str, t.Any], project_name: str) -> None:
537
+ """Process hook files pattern to replace project-specific references."""
538
+ if "files" in hook:
539
+ files_pattern = hook["files"]
540
+ if isinstance(files_pattern, str):
541
+ hook["files"] = files_pattern.replace(
542
+ "^crackerjack/", f"^{project_name}/"
543
+ )
@@ -329,36 +329,54 @@ class SecureSubprocessExecutor:
329
329
  self, arg: str, index: int, issues: list[str], command: list[str]
330
330
  ) -> bool:
331
331
  # First check if this is an allowed git pattern
332
- for git_pattern in self.allowed_git_patterns:
333
- if re.match(git_pattern, arg):
334
- return False # It's an allowed git pattern, don't flag as dangerous
332
+ if self._is_allowed_git_pattern(arg):
333
+ return False
335
334
 
336
335
  # Special handling for git commit messages
337
336
  if self._is_git_commit_message(index, command):
338
- # For git commit messages, only check for truly dangerous patterns
339
- # Parentheses are common in commit messages and should be allowed
340
- safe_commit_patterns = [
341
- r"[;&|`$]", # Still dangerous in commit messages
342
- r"\.\./", # Path traversal
343
- r"\$\{.*\}", # Variable expansion
344
- r"`.*`", # Command substitution
345
- r"\$\(.*\)", # Command substitution (but allow simple parentheses)
346
- r">\s*/", # Redirection to paths
347
- r"<\s*/", # Redirection from paths
348
- ]
349
-
350
- for pattern in safe_commit_patterns:
351
- if re.search(pattern, arg):
352
- # Allow simple parentheses that don't look like command substitution
353
- if pattern == r"\$\(.*\)" and not re.search(r"\$\(", arg):
354
- continue
355
- issues.append(
356
- f"Dangerous pattern '{pattern}' in argument {index}: {arg[:50]}"
357
- )
358
- return True
359
- return False
337
+ return self._check_dangerous_patterns_in_commit_message(arg, index, issues)
360
338
 
361
339
  # Check for dangerous patterns in other contexts
340
+ return self._check_dangerous_patterns_in_other_contexts(arg, index, issues)
341
+
342
+ def _is_allowed_git_pattern(self, arg: str) -> bool:
343
+ """Check if the argument matches an allowed git pattern."""
344
+ for git_pattern in self.allowed_git_patterns:
345
+ if re.match(git_pattern, arg):
346
+ return True
347
+ return False
348
+
349
+ def _check_dangerous_patterns_in_commit_message(
350
+ self, arg: str, index: int, issues: list[str]
351
+ ) -> bool:
352
+ """Check for dangerous patterns specifically in git commit messages."""
353
+ # For git commit messages, only check for truly dangerous patterns
354
+ # Parentheses are common in commit messages and should be allowed
355
+ safe_commit_patterns = [
356
+ r"[;&|`$]", # Still dangerous in commit messages
357
+ r"\.\./", # Path traversal
358
+ r"\$\{.*\}", # Variable expansion
359
+ r"`.*`", # Command substitution
360
+ r"\$\(.*\)", # Command substitution (but allow simple parentheses)
361
+ r">\s*/", # Redirection to paths
362
+ r"<\s*/", # Redirection from paths
363
+ ]
364
+
365
+ for pattern in safe_commit_patterns:
366
+ if re.search(pattern, arg):
367
+ # Allow simple parentheses that don't look like command substitution
368
+ if pattern == r"\$\(.*\)" and not re.search(r"\$\(", arg):
369
+ continue
370
+ issues.append(
371
+ f"Dangerous pattern '{pattern}' in argument {index}: {arg[:50]}"
372
+ )
373
+ return True
374
+ return False
375
+
376
+ def _check_dangerous_patterns_in_other_contexts(
377
+ self, arg: str, index: int, issues: list[str]
378
+ ) -> bool:
379
+ """Check for dangerous patterns in non-commit message contexts."""
362
380
  for pattern in self.dangerous_patterns:
363
381
  if re.search(pattern, arg):
364
382
  issues.append(
@@ -37,6 +37,7 @@ ALLOWED_PATTERNS = {
37
37
  r"crackerjack/intelligence/agent_selector\.py$",
38
38
  r"crackerjack/managers/test_.*\.py$",
39
39
  r"crackerjack/core/async_workflow_orchestrator\.py$",
40
+ r"crackerjack/core/workflow_orchestrator\.py$",
40
41
  r"crackerjack/agents/.*\.py$",
41
42
  }
42
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crackerjack
3
- Version: 0.37.7
3
+ Version: 0.37.9
4
4
  Summary: Crackerjack Python project management tool
5
5
  Project-URL: documentation, https://github.com/lesleslie/crackerjack
6
6
  Project-URL: homepage, https://github.com/lesleslie/crackerjack
@@ -2,9 +2,9 @@ crackerjack/__init__.py,sha256=k8_Ev_3fWdjFtGNSJdSOvyaSLW54y3j484d3a8k_Ob4,1396
2
2
  crackerjack/__main__.py,sha256=lE5ZDbAzI9TLCzMTxFGw23Dk1hP-MEhd1i_nNbi_Mag,52515
3
3
  crackerjack/api.py,sha256=sfKzCphsTMFCvdNizviPdYemwg0H8Oei_5C08LAuDvA,21538
4
4
  crackerjack/code_cleaner.py,sha256=M1zVaq31uW0nOkPneKR8kfR3892gyyVx0VhFgRaxsj4,44338
5
- crackerjack/dynamic_config.py,sha256=YGlUyTEp_K9rGWtLsboXMtVeKyI7bUoNoftO3ZnCiwc,21815
5
+ crackerjack/dynamic_config.py,sha256=nwHVFCf84mokfzXhMo91-DD1ecEB6tXIgGBg0836auU,22017
6
6
  crackerjack/errors.py,sha256=yYbZ92kn_y6acEWgQvEPvozAYs2HT65uLwAXrtXxGsE,10049
7
- crackerjack/interactive.py,sha256=QXFZrnbY0nc8rFJcivFAVFNgUTHXqLCu3FFv5bmq_eI,21634
7
+ crackerjack/interactive.py,sha256=yjTnk0xWvDnZeCaB11TcRGxVLTXWPRnGBsekiMcyRvU,21317
8
8
  crackerjack/adapters/__init__.py,sha256=k-8ajMDL9DS9hV2FYOu694nmNQg3HkudJRuNcXmx8N4,451
9
9
  crackerjack/adapters/lsp_client.py,sha256=4kQ3T5JiWC7uc6kOjZuPdtUboseKSDjZpuKQpV74onc,10963
10
10
  crackerjack/adapters/rust_tool_adapter.py,sha256=ui_qMt_WIwInRvRCeT7MnIdp8eln7Fvp4hakXQiVnjg,5999
@@ -54,7 +54,7 @@ crackerjack/core/service_watchdog.py,sha256=Ttj1imOxvUea4Tkf5JO1e2dQtGIK7D-bX1xO
54
54
  crackerjack/core/session_coordinator.py,sha256=TgoGE9DfXe2x-OkH93Ld9dX9ROjx2_mZFkGXen-z5YI,15680
55
55
  crackerjack/core/timeout_manager.py,sha256=_sbEsfYDwWx7y0Pn89QCoAZ5DpWIbCdtR9qkG_Kqj5E,15013
56
56
  crackerjack/core/websocket_lifecycle.py,sha256=74kn6ugu6FLlDQhCNSPgqguCFwRoT1WFOvtl8G2OyFc,12860
57
- crackerjack/core/workflow_orchestrator.py,sha256=jh8G1xKqxXwD0yGaFHgdESRjm23-kUZt4qriMJltowM,70789
57
+ crackerjack/core/workflow_orchestrator.py,sha256=y_bYIZ2cn-DD8J1A9xkS9TawUJ8qdeirmB5HHrzTwg8,71867
58
58
  crackerjack/docs/INDEX.md,sha256=a6CGFEeL5DX_FRft_JFWd0nOxoBmCSSp-QHIC3B7ato,342
59
59
  crackerjack/docs/generated/api/API_REFERENCE.md,sha256=mWoqImZA7AhDvRqqF1MhUo70g_pnZr3NoBeZQRotqN8,155816
60
60
  crackerjack/docs/generated/api/CLI_REFERENCE.md,sha256=ikuG0hO5EjIiQlJtAUnvEuAhXDa-JHPULPXNNmUwvk4,2805
@@ -65,7 +65,7 @@ crackerjack/documentation/__init__.py,sha256=gGR--er5oTHhbwLKOHVlU2QgGmQtA0qUXf3
65
65
  crackerjack/documentation/ai_templates.py,sha256=GRBKB5bqWudh9MDLjo1b3vNiFAgpL62ezzRp_WxTews,21629
66
66
  crackerjack/documentation/dual_output_generator.py,sha256=w7rDthOnyFeRPQDWvYiR4aiScPxsHzkwjJ3blMwT9-w,28552
67
67
  crackerjack/documentation/mkdocs_integration.py,sha256=KqU2_9mA-rjP_VDrrfr6KTuPWtTlcvkInPxoH03LTC0,15657
68
- crackerjack/documentation/reference_generator.py,sha256=qthAvJU32xUjpl6GZ5s0sWILdCS3OGQLzIuw6UBsUU4,30618
68
+ crackerjack/documentation/reference_generator.py,sha256=RezcoengHOnPJp988KB--EYcMfjJqPpWWFIvzvOMNCc,33094
69
69
  crackerjack/executors/__init__.py,sha256=HF-DmXvKN45uKKDdiMxOT9bYxuy1B-Z91BihOhkK5lg,322
70
70
  crackerjack/executors/async_hook_executor.py,sha256=FmKpiAxpZXKwvOWXnRQ73N-reDfX8NusESQ9a4HeacM,17620
71
71
  crackerjack/executors/cached_hook_executor.py,sha256=izwdW0B22EZcl_2_llmTIyq5oTcZDZTRL2G97ZwYiXg,11173
@@ -74,7 +74,7 @@ crackerjack/executors/hook_lock_manager.py,sha256=ft24q6VFEkW_QrifNnSlrQeHGx9GuJ
74
74
  crackerjack/executors/individual_hook_executor.py,sha256=t0W9vuqLGPx0HmbTo01N9MRFyqLfY0ZLG06ZIA5qfBY,24362
75
75
  crackerjack/executors/lsp_aware_hook_executor.py,sha256=aZxtTwpaDxfmN71II2MEAtfzEst-jPJp-GXmMLwnibA,10388
76
76
  crackerjack/executors/tool_proxy.py,sha256=dJnrt72aFXEMuZLsgrLZrf51vfELDaXs5nBxzGHu7SQ,14881
77
- crackerjack/hooks/lsp_hook.py,sha256=hJCkJ-jUJasE35XPWdnGRDXgynEh2-gUiEd1wxLFp48,2397
77
+ crackerjack/hooks/lsp_hook.py,sha256=MPP9EYmKdZMNBvAQSAwzI5L8TkwKTd0uVx3heaTHmM0,2877
78
78
  crackerjack/intelligence/__init__.py,sha256=3Sty6xFyxBEXTyy8W34dpAQk7S7m8SnxUz4wrb3xrZc,878
79
79
  crackerjack/intelligence/adaptive_learning.py,sha256=Q0pUyxzBBCtySTfp96zHObQVAUaN0c5kEFOFJKIG0j8,25610
80
80
  crackerjack/intelligence/agent_orchestrator.py,sha256=YOG2sQc0l5ddz7TzRZ_1YAugTNwuyY1brWrlj5tnMhk,16728
@@ -84,7 +84,7 @@ crackerjack/intelligence/integration.py,sha256=vVaC2Fp5RbbABpaohCePzGw1XANuRztGl
84
84
  crackerjack/managers/__init__.py,sha256=PFWccXx4hDQA76T02idAViOLVD-aPeVpgjdfSkh_Dmk,298
85
85
  crackerjack/managers/async_hook_manager.py,sha256=c0HFR98sFwfk0uZ3NmAe_6OVZpBrq9I570V8A2DoIxw,5129
86
86
  crackerjack/managers/hook_manager.py,sha256=_FT0ngwPwujqg0KZGpLz-pP07mwDmptJ5pVkiy5yS8k,7820
87
- crackerjack/managers/publish_manager.py,sha256=5jpYM2IeTxavP2KYypZqz83ASQrBQDOhcKpws8DLFbM,22040
87
+ crackerjack/managers/publish_manager.py,sha256=Pe0zWtLaho5BCTO8-EC9sY0Gl4L1B2guDM2D8B8TryA,22034
88
88
  crackerjack/managers/test_command_builder.py,sha256=1TlPzddNcDDxRORH6UvAudcbRc6hKwFyknSEVLkiWAo,3459
89
89
  crackerjack/managers/test_executor.py,sha256=2837Ti4OaNsmLxnmELjbQ18hmfL0-Z2EW-W2UeFSDcE,13871
90
90
  crackerjack/managers/test_manager.py,sha256=BRPBWXx4flPDK0w96xyHhg-9dmUca1vpKQRM2VofSlI,13158
@@ -110,7 +110,7 @@ crackerjack/mcp/tools/__init__.py,sha256=T_RMPXHQQWJLvSJjrPkJYtWi5tmyoRrzm0WS2G-
110
110
  crackerjack/mcp/tools/core_tools.py,sha256=8gytjhqxKglR-ZvLIy0DwJzzaqStex_s9WHiAjkXlD0,14612
111
111
  crackerjack/mcp/tools/error_analyzer.py,sha256=8ap3_TIqEfrN0n49w_wJFRE3zu7eHB9E8UyXqqz7unM,8515
112
112
  crackerjack/mcp/tools/execution_tools.py,sha256=iMtcTm45v04wNgfiIdzOjMXyB-lZ9x-yUtN9SuLGxNI,10410
113
- crackerjack/mcp/tools/execution_tools_backup.py,sha256=aq3njWeBlHT8EbBlmWsdtKwYujssV1hyI5waGku68Ec,33269
113
+ crackerjack/mcp/tools/execution_tools_backup.py,sha256=sWOtid00SkuonFLT2XCtljOGZiibbaIXzEdLWsPrXyI,33351
114
114
  crackerjack/mcp/tools/intelligence_tool_registry.py,sha256=eAOBzW3EZTKSz_MeYVzgjD6a1xVZFSXUdgOsgcQLSBk,1262
115
115
  crackerjack/mcp/tools/intelligence_tools.py,sha256=R0TK-sLuCezJQu0FfTg1xXs-xI_oI2aJeueZgOrM58g,9106
116
116
  crackerjack/mcp/tools/monitoring_tools.py,sha256=5RYbytBmLjJdz1sE33xYchdrO9LgzGwSvsqRsZIV7oA,21804
@@ -157,9 +157,9 @@ crackerjack/services/backup_service.py,sha256=0e8AAo9svSBtbsbI9vwnAVSilB2fjph61l
157
157
  crackerjack/services/bounded_status_operations.py,sha256=mrBkUQwgtYjkbp-Y-5MdrU2A3rp3ZLVgOJv9vLdnJ8k,18763
158
158
  crackerjack/services/cache.py,sha256=FutQKxItKSdQFzOlWQaQzVBEfn6lbPXCDdvNnz3NCDQ,14536
159
159
  crackerjack/services/changelog_automation.py,sha256=KUeXCYLihRfLR0mUIRiI2aRQdCfe-45GnbB2gYNJJQE,14095
160
- crackerjack/services/config.py,sha256=fF3EpPfIxFBmZ9oTr7V64zCMBmxHWCmD_649qr0dgsI,13590
160
+ crackerjack/services/config.py,sha256=1gUQkcHPCGHVYSlx6mcrJlJLVIWhdaL7RjEmuy8_ev4,13704
161
161
  crackerjack/services/config_integrity.py,sha256=Ac6-c7WuupsyrP2dxx_ijgjzpNnx9G0NWsXB-SZjelg,2904
162
- crackerjack/services/config_merge.py,sha256=FPh4u4J68JkNVf0AT1paNeEy2MjusSbYu9kN72LzR9w,18825
162
+ crackerjack/services/config_merge.py,sha256=ubApDKbYMREaHARP3oaW9vY8iVeYuknsr6tJIiIz-_E,18999
163
163
  crackerjack/services/config_template.py,sha256=RgSYFVNBxdBfMitlRqz7bzkEHaQhEWMm3pUMS7maRFU,18035
164
164
  crackerjack/services/contextual_ai_assistant.py,sha256=6Pnb2r824c4JYkP5mtCH8sJ2OPPvI-KtzbXcosspCfE,19962
165
165
  crackerjack/services/coverage_badge_service.py,sha256=gzC3LEyVLt4rpf378APsFVRGgNwkLc50w9S_9xEPPoM,6645
@@ -199,7 +199,7 @@ crackerjack/services/regex_patterns.py,sha256=iis9gSzXZtnX14lODGfSUsf7bcCRTw7rdS
199
199
  crackerjack/services/regex_utils.py,sha256=e7AD59_L-T5-oOxzqsGgrLd94uxRE9aKnwasZkohwI8,14966
200
200
  crackerjack/services/secure_path_utils.py,sha256=aHsLwxDch42DidPYtTL_ko40g2rhbXDLnRhcx2LlGk8,16688
201
201
  crackerjack/services/secure_status_formatter.py,sha256=yhwNtzvvQVcuHsNOTNZMzlqIMQT9zx-lzAtq9LuSDuk,14121
202
- crackerjack/services/secure_subprocess.py,sha256=upuiBL0sYIElC8DWQ622-A6572zBaSBoMPVnNM8AeaQ,19838
202
+ crackerjack/services/secure_subprocess.py,sha256=tDHyuxavTES1uXzJOdxZlza8d92hvza2xzx5JMamsoA,20488
203
203
  crackerjack/services/security.py,sha256=plgIz-B8oYN_mpF4NYrqHnT7TRcsp4jr0-YlV9WgD5o,7298
204
204
  crackerjack/services/security_logger.py,sha256=AAjd9VKVmCo158UifyEKd79VgtWKeuaIVyXYL8qvqT8,17001
205
205
  crackerjack/services/server_manager.py,sha256=MwvMMxhaCiZD4pcKM2ODXB-gu2s22n_v_uIqrwoJsHs,11934
@@ -221,11 +221,11 @@ crackerjack/slash_commands/run.md,sha256=VEWWset52jPV0m00LnQpjRkHL8g8XRPYAzgOq6B
221
221
  crackerjack/slash_commands/status.md,sha256=U3qqppVLtIIm2lEiMYaKagaHYLI9UplL7OH1j6SRJGw,3921
222
222
  crackerjack/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
223
223
  crackerjack/tools/validate_input_validator_patterns.py,sha256=NN7smYlXWrHLQXTb-81gRam2vjW-cJav92f1klPA0qA,8234
224
- crackerjack/tools/validate_regex_patterns.py,sha256=9ejFb7Tw1js_oydzuEeeeXvrU5ipHUEX9ATBfkLCCE8,5811
224
+ crackerjack/tools/validate_regex_patterns.py,sha256=J7GG9EP1fASpRIsG8qRPeiCSkdCwmk0sdo29GgoJ6w8,5863
225
225
  crackerjack/ui/__init__.py,sha256=eMb1OeTU-dSLICAACn0YdYB4Amdr8wHckjKfn0wOIZE,37
226
226
  crackerjack/ui/server_panels.py,sha256=F5IH6SNN06BaZQMsFx_D-OA286aojmaFPJ5kvvSRv_c,4232
227
- crackerjack-0.37.7.dist-info/METADATA,sha256=BmdPeVCTs9I1CJz43F4edjP6sV3PQxKk8ffgBE2IYwQ,37949
228
- crackerjack-0.37.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
229
- crackerjack-0.37.7.dist-info/entry_points.txt,sha256=AJKNft0WXm9xoGUJ3Trl-iXHOWxRAYbagQiza3AILr4,57
230
- crackerjack-0.37.7.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
231
- crackerjack-0.37.7.dist-info/RECORD,,
227
+ crackerjack-0.37.9.dist-info/METADATA,sha256=CCZxJnuN5szGZEggJFKNED1WCXd8B1EY7xHAaqL7E6M,37949
228
+ crackerjack-0.37.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
229
+ crackerjack-0.37.9.dist-info/entry_points.txt,sha256=AJKNft0WXm9xoGUJ3Trl-iXHOWxRAYbagQiza3AILr4,57
230
+ crackerjack-0.37.9.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
231
+ crackerjack-0.37.9.dist-info/RECORD,,