crackerjack 0.28.0__py3-none-any.whl → 0.29.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.
- crackerjack/__main__.py +8 -0
- crackerjack/crackerjack.py +647 -48
- crackerjack/interactive.py +7 -16
- crackerjack/pyproject.toml +3 -1
- {crackerjack-0.28.0.dist-info → crackerjack-0.29.0.dist-info}/METADATA +636 -52
- {crackerjack-0.28.0.dist-info → crackerjack-0.29.0.dist-info}/RECORD +8 -8
- {crackerjack-0.28.0.dist-info → crackerjack-0.29.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.28.0.dist-info → crackerjack-0.29.0.dist-info}/licenses/LICENSE +0 -0
crackerjack/crackerjack.py
CHANGED
|
@@ -274,7 +274,7 @@ python -m crackerjack --resume-from {self.progress_file.name}
|
|
|
274
274
|
|
|
275
275
|
"""
|
|
276
276
|
|
|
277
|
-
all_files = set()
|
|
277
|
+
all_files: set[str] = set()
|
|
278
278
|
for task in self.tasks.values():
|
|
279
279
|
if task.files_changed:
|
|
280
280
|
all_files.update(task.files_changed)
|
|
@@ -351,7 +351,7 @@ python -m crackerjack --resume-from {self.progress_file.name}
|
|
|
351
351
|
|
|
352
352
|
@classmethod
|
|
353
353
|
def find_recent_progress_files(cls, directory: Path = Path.cwd()) -> list[Path]:
|
|
354
|
-
progress_files = []
|
|
354
|
+
progress_files: list[Path] = []
|
|
355
355
|
for file_path in directory.glob("SESSION-PROGRESS-*.md"):
|
|
356
356
|
try:
|
|
357
357
|
if file_path.is_file():
|
|
@@ -525,6 +525,7 @@ class OptionsProtocol(t.Protocol):
|
|
|
525
525
|
update_precommit: bool
|
|
526
526
|
update_docs: bool
|
|
527
527
|
force_update_docs: bool
|
|
528
|
+
compress_docs: bool
|
|
528
529
|
clean: bool
|
|
529
530
|
test: bool
|
|
530
531
|
benchmark: bool
|
|
@@ -1683,38 +1684,321 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1683
1684
|
if configs_to_add:
|
|
1684
1685
|
self.execute_command(["git", "add"] + configs_to_add)
|
|
1685
1686
|
|
|
1686
|
-
def copy_documentation_templates(
|
|
1687
|
+
def copy_documentation_templates(
|
|
1688
|
+
self, force_update: bool = False, compress_docs: bool = False
|
|
1689
|
+
) -> None:
|
|
1687
1690
|
docs_to_add: list[str] = []
|
|
1688
1691
|
for doc_file in documentation_files:
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
continue
|
|
1693
|
-
if self.pkg_path.stem == "crackerjack":
|
|
1694
|
-
continue
|
|
1695
|
-
should_update = force_update or not pkg_doc_path.exists()
|
|
1696
|
-
if should_update:
|
|
1697
|
-
pkg_doc_path.touch()
|
|
1698
|
-
content = doc_path.read_text(encoding="utf-8")
|
|
1699
|
-
updated_content = self._customize_documentation_content(
|
|
1700
|
-
content, doc_file
|
|
1701
|
-
)
|
|
1702
|
-
pkg_doc_path.write_text(updated_content, encoding="utf-8")
|
|
1703
|
-
docs_to_add.append(doc_file)
|
|
1704
|
-
self.console.print(
|
|
1705
|
-
f"[green]📋[/green] Updated {doc_file} with latest Crackerjack quality standards"
|
|
1692
|
+
if self._should_process_doc_file(doc_file):
|
|
1693
|
+
self._process_single_doc_file(
|
|
1694
|
+
doc_file, force_update, compress_docs, docs_to_add
|
|
1706
1695
|
)
|
|
1696
|
+
|
|
1707
1697
|
if docs_to_add:
|
|
1708
1698
|
self.execute_command(["git", "add"] + docs_to_add)
|
|
1709
1699
|
|
|
1710
|
-
def
|
|
1700
|
+
def _should_process_doc_file(self, doc_file: str) -> bool:
|
|
1701
|
+
doc_path = self.our_path / doc_file
|
|
1702
|
+
if not doc_path.exists():
|
|
1703
|
+
return False
|
|
1704
|
+
if self.pkg_path.stem == "crackerjack":
|
|
1705
|
+
return False
|
|
1706
|
+
return True
|
|
1707
|
+
|
|
1708
|
+
def _process_single_doc_file(
|
|
1709
|
+
self,
|
|
1710
|
+
doc_file: str,
|
|
1711
|
+
force_update: bool,
|
|
1712
|
+
compress_docs: bool,
|
|
1713
|
+
docs_to_add: list[str],
|
|
1714
|
+
) -> None:
|
|
1715
|
+
doc_path = self.our_path / doc_file
|
|
1716
|
+
pkg_doc_path = self.pkg_path / doc_file
|
|
1717
|
+
should_update = force_update or not pkg_doc_path.exists()
|
|
1718
|
+
|
|
1719
|
+
if should_update:
|
|
1720
|
+
pkg_doc_path.touch()
|
|
1721
|
+
content = doc_path.read_text(encoding="utf-8")
|
|
1722
|
+
|
|
1723
|
+
auto_compress = self._should_compress_doc(doc_file, compress_docs)
|
|
1724
|
+
updated_content = self._customize_documentation_content(
|
|
1725
|
+
content, doc_file, auto_compress
|
|
1726
|
+
)
|
|
1727
|
+
pkg_doc_path.write_text(updated_content, encoding="utf-8")
|
|
1728
|
+
docs_to_add.append(doc_file)
|
|
1729
|
+
|
|
1730
|
+
self._print_doc_update_message(doc_file, auto_compress)
|
|
1731
|
+
|
|
1732
|
+
def _should_compress_doc(self, doc_file: str, compress_docs: bool) -> bool:
|
|
1733
|
+
return compress_docs or (
|
|
1734
|
+
self.pkg_path.stem != "crackerjack" and doc_file == "CLAUDE.md"
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
def _print_doc_update_message(self, doc_file: str, auto_compress: bool) -> None:
|
|
1738
|
+
compression_note = (
|
|
1739
|
+
" (compressed for Claude Code)"
|
|
1740
|
+
if auto_compress and doc_file == "CLAUDE.md"
|
|
1741
|
+
else ""
|
|
1742
|
+
)
|
|
1743
|
+
self.console.print(
|
|
1744
|
+
f"[green]📋[/green] Updated {doc_file} with latest Crackerjack quality standards{compression_note}"
|
|
1745
|
+
)
|
|
1746
|
+
|
|
1747
|
+
def _customize_documentation_content(
|
|
1748
|
+
self, content: str, filename: str, compress: bool = False
|
|
1749
|
+
) -> str:
|
|
1711
1750
|
if filename == "CLAUDE.md":
|
|
1712
|
-
return self._customize_claude_md(content)
|
|
1751
|
+
return self._customize_claude_md(content, compress)
|
|
1713
1752
|
elif filename == "RULES.md":
|
|
1714
1753
|
return self._customize_rules_md(content)
|
|
1715
1754
|
return content
|
|
1716
1755
|
|
|
1717
|
-
def
|
|
1756
|
+
def _compress_claude_md(self, content: str, target_size: int = 30000) -> str:
|
|
1757
|
+
content.split("\n")
|
|
1758
|
+
current_size = len(content)
|
|
1759
|
+
if current_size <= target_size:
|
|
1760
|
+
return content
|
|
1761
|
+
essential_sections = [
|
|
1762
|
+
"# ",
|
|
1763
|
+
"## Project Overview",
|
|
1764
|
+
"## Key Commands",
|
|
1765
|
+
"## Development Guidelines",
|
|
1766
|
+
"## Code Quality Compliance",
|
|
1767
|
+
"### Refurb Standards",
|
|
1768
|
+
"### Bandit Security Standards",
|
|
1769
|
+
"### Pyright Type Safety Standards",
|
|
1770
|
+
"## AI Code Generation Best Practices",
|
|
1771
|
+
"## Task Completion Requirements",
|
|
1772
|
+
]
|
|
1773
|
+
compression_strategies = [
|
|
1774
|
+
self._remove_redundant_examples,
|
|
1775
|
+
self._compress_command_examples,
|
|
1776
|
+
self._remove_verbose_sections,
|
|
1777
|
+
self._compress_repeated_patterns,
|
|
1778
|
+
self._summarize_long_sections,
|
|
1779
|
+
]
|
|
1780
|
+
compressed_content = content
|
|
1781
|
+
for strategy in compression_strategies:
|
|
1782
|
+
compressed_content = strategy(compressed_content)
|
|
1783
|
+
if len(compressed_content) <= target_size:
|
|
1784
|
+
break
|
|
1785
|
+
if len(compressed_content) > target_size:
|
|
1786
|
+
compressed_content = self._extract_essential_sections(
|
|
1787
|
+
compressed_content, essential_sections, target_size
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
return self._add_compression_notice(compressed_content)
|
|
1791
|
+
|
|
1792
|
+
def _remove_redundant_examples(self, content: str) -> str:
|
|
1793
|
+
lines = content.split("\n")
|
|
1794
|
+
result = []
|
|
1795
|
+
in_example_block = False
|
|
1796
|
+
example_count = 0
|
|
1797
|
+
max_examples_per_section = 2
|
|
1798
|
+
for line in lines:
|
|
1799
|
+
if line.strip().startswith("```"):
|
|
1800
|
+
if not in_example_block:
|
|
1801
|
+
example_count += 1
|
|
1802
|
+
if example_count <= max_examples_per_section:
|
|
1803
|
+
result.append(line)
|
|
1804
|
+
in_example_block = True
|
|
1805
|
+
else:
|
|
1806
|
+
in_example_block = "skip"
|
|
1807
|
+
else:
|
|
1808
|
+
if in_example_block != "skip":
|
|
1809
|
+
result.append(line)
|
|
1810
|
+
in_example_block = False
|
|
1811
|
+
elif in_example_block == "skip":
|
|
1812
|
+
continue
|
|
1813
|
+
elif line.startswith(("## ", "### ")):
|
|
1814
|
+
example_count = 0
|
|
1815
|
+
result.append(line)
|
|
1816
|
+
else:
|
|
1817
|
+
result.append(line)
|
|
1818
|
+
|
|
1819
|
+
return "\n".join(result)
|
|
1820
|
+
|
|
1821
|
+
def _compress_command_examples(self, content: str) -> str:
|
|
1822
|
+
import re
|
|
1823
|
+
|
|
1824
|
+
content = re.sub(
|
|
1825
|
+
r"```bash\n((?:[^`]+\n){3,})```",
|
|
1826
|
+
lambda m: "```bash\n"
|
|
1827
|
+
+ "\n".join(m.group(1).split("\n")[:3])
|
|
1828
|
+
+ "\n# ... (additional commands available)\n```",
|
|
1829
|
+
content,
|
|
1830
|
+
flags=re.MULTILINE,
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
return content
|
|
1834
|
+
|
|
1835
|
+
def _remove_verbose_sections(self, content: str) -> str:
|
|
1836
|
+
sections_to_compress = [
|
|
1837
|
+
"## Recent Bug Fixes and Improvements",
|
|
1838
|
+
"## Development Memories",
|
|
1839
|
+
"## Self-Maintenance Protocol for AI Assistants",
|
|
1840
|
+
"## Pre-commit Hook Maintenance",
|
|
1841
|
+
]
|
|
1842
|
+
lines = content.split("\n")
|
|
1843
|
+
result = []
|
|
1844
|
+
skip_section = False
|
|
1845
|
+
for line in lines:
|
|
1846
|
+
if any(line.startswith(section) for section in sections_to_compress):
|
|
1847
|
+
skip_section = True
|
|
1848
|
+
result.extend(
|
|
1849
|
+
(line, "*[Detailed information available in full CLAUDE.md]*")
|
|
1850
|
+
)
|
|
1851
|
+
result.append("")
|
|
1852
|
+
elif line.startswith("## ") and skip_section:
|
|
1853
|
+
skip_section = False
|
|
1854
|
+
result.append(line)
|
|
1855
|
+
elif not skip_section:
|
|
1856
|
+
result.append(line)
|
|
1857
|
+
|
|
1858
|
+
return "\n".join(result)
|
|
1859
|
+
|
|
1860
|
+
def _compress_repeated_patterns(self, content: str) -> str:
|
|
1861
|
+
import re
|
|
1862
|
+
|
|
1863
|
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
1864
|
+
content = re.sub(
|
|
1865
|
+
r"(\*\*[A-Z][^*]+:\*\*[^\n]+\n){3,}",
|
|
1866
|
+
lambda m: m.group(0)[:200]
|
|
1867
|
+
+ "...\n*[Additional patterns available in full documentation]*\n",
|
|
1868
|
+
content,
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
return content
|
|
1872
|
+
|
|
1873
|
+
def _summarize_long_sections(self, content: str) -> str:
|
|
1874
|
+
lines = content.split("\n")
|
|
1875
|
+
result = []
|
|
1876
|
+
current_section = []
|
|
1877
|
+
section_header = ""
|
|
1878
|
+
for line in lines:
|
|
1879
|
+
if line.startswith(("### ", "## ")):
|
|
1880
|
+
if current_section and len("\n".join(current_section)) > 1000:
|
|
1881
|
+
summary = self._create_section_summary(
|
|
1882
|
+
section_header, current_section
|
|
1883
|
+
)
|
|
1884
|
+
result.extend(summary)
|
|
1885
|
+
else:
|
|
1886
|
+
result.extend(current_section)
|
|
1887
|
+
current_section = [line]
|
|
1888
|
+
section_header = line
|
|
1889
|
+
else:
|
|
1890
|
+
current_section.append(line)
|
|
1891
|
+
if current_section:
|
|
1892
|
+
if len("\n".join(current_section)) > 1000:
|
|
1893
|
+
summary = self._create_section_summary(section_header, current_section)
|
|
1894
|
+
result.extend(summary)
|
|
1895
|
+
else:
|
|
1896
|
+
result.extend(current_section)
|
|
1897
|
+
|
|
1898
|
+
return "\n".join(result)
|
|
1899
|
+
|
|
1900
|
+
def _create_section_summary(
|
|
1901
|
+
self, header: str, section_lines: list[str]
|
|
1902
|
+
) -> list[str]:
|
|
1903
|
+
summary = [header, ""]
|
|
1904
|
+
|
|
1905
|
+
key_points = []
|
|
1906
|
+
for line in section_lines[2:]:
|
|
1907
|
+
if line.strip().startswith(("- ", "* ", "1. ", "2. ")):
|
|
1908
|
+
key_points.append(line)
|
|
1909
|
+
elif line.strip().startswith("**") and ":" in line:
|
|
1910
|
+
key_points.append(line)
|
|
1911
|
+
|
|
1912
|
+
if len(key_points) >= 5:
|
|
1913
|
+
break
|
|
1914
|
+
|
|
1915
|
+
if key_points:
|
|
1916
|
+
summary.extend(key_points[:5])
|
|
1917
|
+
summary.append("*[Complete details available in full CLAUDE.md]*")
|
|
1918
|
+
else:
|
|
1919
|
+
content_preview = " ".join(
|
|
1920
|
+
line.strip()
|
|
1921
|
+
for line in section_lines[2:10]
|
|
1922
|
+
if line.strip() and not line.startswith("#")
|
|
1923
|
+
)[:200]
|
|
1924
|
+
summary.extend(
|
|
1925
|
+
(
|
|
1926
|
+
f"{content_preview}...",
|
|
1927
|
+
"*[Full section available in complete documentation]*",
|
|
1928
|
+
)
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
summary.append("")
|
|
1932
|
+
return summary
|
|
1933
|
+
|
|
1934
|
+
def _extract_essential_sections(
|
|
1935
|
+
self, content: str, essential_sections: list[str], target_size: int
|
|
1936
|
+
) -> str:
|
|
1937
|
+
lines = content.split("\n")
|
|
1938
|
+
result = []
|
|
1939
|
+
current_section = []
|
|
1940
|
+
keep_section = False
|
|
1941
|
+
|
|
1942
|
+
for line in lines:
|
|
1943
|
+
new_section_started = self._process_line_for_section(
|
|
1944
|
+
line, essential_sections, current_section, keep_section, result
|
|
1945
|
+
)
|
|
1946
|
+
if new_section_started is not None:
|
|
1947
|
+
current_section, keep_section = new_section_started
|
|
1948
|
+
else:
|
|
1949
|
+
current_section.append(line)
|
|
1950
|
+
|
|
1951
|
+
if self._should_stop_extraction(result, target_size):
|
|
1952
|
+
break
|
|
1953
|
+
|
|
1954
|
+
self._finalize_extraction(current_section, keep_section, result, target_size)
|
|
1955
|
+
return "\n".join(result)
|
|
1956
|
+
|
|
1957
|
+
def _process_line_for_section(
|
|
1958
|
+
self,
|
|
1959
|
+
line: str,
|
|
1960
|
+
essential_sections: list[str],
|
|
1961
|
+
current_section: list[str],
|
|
1962
|
+
keep_section: bool,
|
|
1963
|
+
result: list[str],
|
|
1964
|
+
) -> tuple[list[str], bool] | None:
|
|
1965
|
+
if any(line.startswith(section) for section in essential_sections):
|
|
1966
|
+
if current_section and keep_section:
|
|
1967
|
+
result.extend(current_section)
|
|
1968
|
+
return ([line], True)
|
|
1969
|
+
elif line.startswith(("## ", "### ")):
|
|
1970
|
+
if current_section and keep_section:
|
|
1971
|
+
result.extend(current_section)
|
|
1972
|
+
return ([line], False)
|
|
1973
|
+
return None
|
|
1974
|
+
|
|
1975
|
+
def _should_stop_extraction(self, result: list[str], target_size: int) -> bool:
|
|
1976
|
+
return len("\n".join(result)) > target_size
|
|
1977
|
+
|
|
1978
|
+
def _finalize_extraction(
|
|
1979
|
+
self,
|
|
1980
|
+
current_section: list[str],
|
|
1981
|
+
keep_section: bool,
|
|
1982
|
+
result: list[str],
|
|
1983
|
+
target_size: int,
|
|
1984
|
+
) -> None:
|
|
1985
|
+
if current_section and keep_section and len("\n".join(result)) < target_size:
|
|
1986
|
+
result.extend(current_section)
|
|
1987
|
+
|
|
1988
|
+
def _add_compression_notice(self, content: str) -> str:
|
|
1989
|
+
notice = """
|
|
1990
|
+
*Note: This CLAUDE.md has been automatically compressed by Crackerjack to optimize for Claude Code usage.
|
|
1991
|
+
Complete documentation is available in the source repository.*
|
|
1992
|
+
|
|
1993
|
+
"""
|
|
1994
|
+
|
|
1995
|
+
lines = content.split("\n")
|
|
1996
|
+
if len(lines) > 5:
|
|
1997
|
+
lines.insert(5, notice)
|
|
1998
|
+
|
|
1999
|
+
return "\n".join(lines)
|
|
2000
|
+
|
|
2001
|
+
def _customize_claude_md(self, content: str, compress: bool = False) -> str:
|
|
1718
2002
|
project_name = self.pkg_name
|
|
1719
2003
|
content = content.replace("crackerjack", project_name).replace(
|
|
1720
2004
|
"Crackerjack", project_name.title()
|
|
@@ -1737,9 +2021,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
1737
2021
|
|
|
1738
2022
|
if start_idx > 0:
|
|
1739
2023
|
relevant_content = "\n".join(lines[start_idx:])
|
|
1740
|
-
|
|
2024
|
+
full_content = header + relevant_content
|
|
2025
|
+
else:
|
|
2026
|
+
full_content = header + content
|
|
1741
2027
|
|
|
1742
|
-
|
|
2028
|
+
if compress:
|
|
2029
|
+
return self._compress_claude_md(full_content)
|
|
2030
|
+
return full_content
|
|
1743
2031
|
|
|
1744
2032
|
def _customize_rules_md(self, content: str) -> str:
|
|
1745
2033
|
project_name = self.pkg_name
|
|
@@ -1844,13 +2132,22 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1844
2132
|
"[bold bright_blue]⚡ INIT[/bold bright_blue] [bold bright_white]First-time project setup[/bold bright_white]"
|
|
1845
2133
|
)
|
|
1846
2134
|
self.console.print("─" * 80 + "\n")
|
|
1847
|
-
self.
|
|
2135
|
+
if self.options and getattr(self.options, "ai_agent", False):
|
|
2136
|
+
import subprocess
|
|
2137
|
+
|
|
2138
|
+
self.execute_command(
|
|
2139
|
+
["uv", "tool", "install", "keyring"],
|
|
2140
|
+
capture_output=True,
|
|
2141
|
+
stderr=subprocess.DEVNULL,
|
|
2142
|
+
)
|
|
2143
|
+
else:
|
|
2144
|
+
self.execute_command(["uv", "tool", "install", "keyring"])
|
|
1848
2145
|
self.execute_command(["git", "init"])
|
|
1849
2146
|
self.execute_command(["git", "branch", "-m", "main"])
|
|
1850
2147
|
self.execute_command(["git", "add", "pyproject.toml", "uv.lock"])
|
|
1851
2148
|
self.execute_command(["git", "config", "advice.addIgnoredFile", "false"])
|
|
1852
2149
|
install_cmd = ["uv", "run", "pre-commit", "install"]
|
|
1853
|
-
if
|
|
2150
|
+
if self.options and getattr(self.options, "ai_agent", False):
|
|
1854
2151
|
install_cmd.extend(["-c", ".pre-commit-config-ai.yaml"])
|
|
1855
2152
|
else:
|
|
1856
2153
|
install_cmd.extend(["-c", ".pre-commit-config-fast.yaml"])
|
|
@@ -1927,7 +2224,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1927
2224
|
result = self.execute_command(cmd, capture_output=True, text=True)
|
|
1928
2225
|
total_duration = time.time() - start_time
|
|
1929
2226
|
hook_results = self._parse_hook_output(result.stdout, result.stderr)
|
|
1930
|
-
if
|
|
2227
|
+
if self.options and getattr(self.options, "ai_agent", False):
|
|
1931
2228
|
self._generate_hooks_analysis(hook_results, total_duration)
|
|
1932
2229
|
self._generate_quality_metrics()
|
|
1933
2230
|
self._generate_project_structure_analysis()
|
|
@@ -2049,7 +2346,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2049
2346
|
return suggestions
|
|
2050
2347
|
|
|
2051
2348
|
def _generate_quality_metrics(self) -> None:
|
|
2052
|
-
if not (
|
|
2349
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
2053
2350
|
return
|
|
2054
2351
|
metrics = {
|
|
2055
2352
|
"project_info": {
|
|
@@ -2224,7 +2521,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2224
2521
|
return recommendations
|
|
2225
2522
|
|
|
2226
2523
|
def _generate_project_structure_analysis(self) -> None:
|
|
2227
|
-
if not (
|
|
2524
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
2228
2525
|
return
|
|
2229
2526
|
structure = {
|
|
2230
2527
|
"project_overview": {
|
|
@@ -2248,7 +2545,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2248
2545
|
)
|
|
2249
2546
|
|
|
2250
2547
|
def _generate_error_context_analysis(self) -> None:
|
|
2251
|
-
if not (
|
|
2548
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
2252
2549
|
return
|
|
2253
2550
|
context = {
|
|
2254
2551
|
"analysis_info": {
|
|
@@ -2269,7 +2566,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2269
2566
|
)
|
|
2270
2567
|
|
|
2271
2568
|
def _generate_ai_agent_summary(self) -> None:
|
|
2272
|
-
if not (
|
|
2569
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
2273
2570
|
return
|
|
2274
2571
|
summary = {
|
|
2275
2572
|
"analysis_summary": {
|
|
@@ -2549,7 +2846,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2549
2846
|
raise SystemExit(1)
|
|
2550
2847
|
else:
|
|
2551
2848
|
self.console.print(
|
|
2552
|
-
"\n[bold bright_green]
|
|
2849
|
+
"\n[bold bright_green]🏆 Pre-commit passed all checks![/bold bright_green]"
|
|
2553
2850
|
)
|
|
2554
2851
|
|
|
2555
2852
|
async def run_pre_commit_with_analysis_async(self) -> list[HookResult]:
|
|
@@ -2595,7 +2892,7 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2595
2892
|
raise SystemExit(1)
|
|
2596
2893
|
else:
|
|
2597
2894
|
self.console.print(
|
|
2598
|
-
"\n[bold bright_green]
|
|
2895
|
+
"\n[bold bright_green]🏆 Pre-commit passed all checks![/bold bright_green]"
|
|
2599
2896
|
)
|
|
2600
2897
|
self._generate_analysis_files(hook_results)
|
|
2601
2898
|
|
|
@@ -2686,6 +2983,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2686
2983
|
config_manager: ConfigManager | None = None
|
|
2687
2984
|
project_manager: ProjectManager | None = None
|
|
2688
2985
|
session_tracker: SessionTracker | None = None
|
|
2986
|
+
options: t.Any = None
|
|
2689
2987
|
_file_cache: dict[str, list[Path]] = {}
|
|
2690
2988
|
_file_cache_with_mtime: dict[str, tuple[float, list[Path]]] = {}
|
|
2691
2989
|
_state_file: Path = Path(".crackerjack-state")
|
|
@@ -3080,7 +3378,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3080
3378
|
|
|
3081
3379
|
def _handle_test_success(self, options: t.Any) -> None:
|
|
3082
3380
|
self.console.print(
|
|
3083
|
-
"\n\n[bold bright_green]
|
|
3381
|
+
"\n\n[bold bright_green]🏆 Tests passed successfully![/bold bright_green]\n"
|
|
3084
3382
|
)
|
|
3085
3383
|
self._print_ai_agent_files(options)
|
|
3086
3384
|
|
|
@@ -3149,6 +3447,121 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3149
3447
|
self._mark_version_bumped(version_type)
|
|
3150
3448
|
break
|
|
3151
3449
|
|
|
3450
|
+
def _validate_authentication_setup(self) -> None:
|
|
3451
|
+
import os
|
|
3452
|
+
import shutil
|
|
3453
|
+
|
|
3454
|
+
keyring_provider = self._get_keyring_provider()
|
|
3455
|
+
has_publish_token = bool(os.environ.get("UV_PUBLISH_TOKEN"))
|
|
3456
|
+
has_keyring = shutil.which("keyring") is not None
|
|
3457
|
+
self.console.print("[dim]🔐 Validating authentication setup...[/dim]")
|
|
3458
|
+
if has_publish_token:
|
|
3459
|
+
self._handle_publish_token_found()
|
|
3460
|
+
return
|
|
3461
|
+
if keyring_provider == "subprocess" and has_keyring:
|
|
3462
|
+
self._handle_keyring_validation()
|
|
3463
|
+
return
|
|
3464
|
+
if keyring_provider == "subprocess" and not has_keyring:
|
|
3465
|
+
self._handle_missing_keyring()
|
|
3466
|
+
if not keyring_provider:
|
|
3467
|
+
self._handle_no_keyring_provider()
|
|
3468
|
+
|
|
3469
|
+
def _handle_publish_token_found(self) -> None:
|
|
3470
|
+
self.console.print(
|
|
3471
|
+
"[dim] ✅ UV_PUBLISH_TOKEN environment variable found[/dim]"
|
|
3472
|
+
)
|
|
3473
|
+
|
|
3474
|
+
def _handle_keyring_validation(self) -> None:
|
|
3475
|
+
self.console.print(
|
|
3476
|
+
"[dim] ✅ Keyring provider configured and keyring executable found[/dim]"
|
|
3477
|
+
)
|
|
3478
|
+
try:
|
|
3479
|
+
result = self.execute_command(
|
|
3480
|
+
["keyring", "get", "https://upload.pypi.org/legacy/", "__token__"],
|
|
3481
|
+
capture_output=True,
|
|
3482
|
+
text=True,
|
|
3483
|
+
)
|
|
3484
|
+
if result.returncode == 0:
|
|
3485
|
+
self.console.print("[dim] ✅ PyPI token found in keyring[/dim]")
|
|
3486
|
+
else:
|
|
3487
|
+
self.console.print(
|
|
3488
|
+
"[yellow] ⚠️ No PyPI token found in keyring - will prompt during publish[/yellow]"
|
|
3489
|
+
)
|
|
3490
|
+
except Exception:
|
|
3491
|
+
self.console.print(
|
|
3492
|
+
"[yellow] ⚠️ Could not check keyring - will attempt publish anyway[/yellow]"
|
|
3493
|
+
)
|
|
3494
|
+
|
|
3495
|
+
def _handle_missing_keyring(self) -> None:
|
|
3496
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
3497
|
+
self.console.print(
|
|
3498
|
+
"[yellow] ⚠️ Keyring provider set to 'subprocess' but keyring executable not found[/yellow]"
|
|
3499
|
+
)
|
|
3500
|
+
self.console.print(
|
|
3501
|
+
"[yellow] Install keyring: uv tool install keyring[/yellow]"
|
|
3502
|
+
)
|
|
3503
|
+
|
|
3504
|
+
def _handle_no_keyring_provider(self) -> None:
|
|
3505
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
3506
|
+
self.console.print(
|
|
3507
|
+
"[yellow] ⚠️ No keyring provider configured and no UV_PUBLISH_TOKEN set[/yellow]"
|
|
3508
|
+
)
|
|
3509
|
+
|
|
3510
|
+
def _get_keyring_provider(self) -> str | None:
|
|
3511
|
+
import os
|
|
3512
|
+
import tomllib
|
|
3513
|
+
from pathlib import Path
|
|
3514
|
+
|
|
3515
|
+
env_provider = os.environ.get("UV_KEYRING_PROVIDER")
|
|
3516
|
+
if env_provider:
|
|
3517
|
+
return env_provider
|
|
3518
|
+
for config_file in ("pyproject.toml", "uv.toml"):
|
|
3519
|
+
config_path = Path(config_file)
|
|
3520
|
+
if config_path.exists():
|
|
3521
|
+
try:
|
|
3522
|
+
with config_path.open("rb") as f:
|
|
3523
|
+
config = tomllib.load(f)
|
|
3524
|
+
return config.get("tool", {}).get("uv", {}).get("keyring-provider")
|
|
3525
|
+
except Exception:
|
|
3526
|
+
continue
|
|
3527
|
+
|
|
3528
|
+
return None
|
|
3529
|
+
|
|
3530
|
+
def _build_publish_command(self) -> list[str]:
|
|
3531
|
+
import os
|
|
3532
|
+
|
|
3533
|
+
cmd = ["uv", "publish"]
|
|
3534
|
+
publish_token = os.environ.get("UV_PUBLISH_TOKEN")
|
|
3535
|
+
if publish_token:
|
|
3536
|
+
cmd.extend(["--token", publish_token])
|
|
3537
|
+
keyring_provider = self._get_keyring_provider()
|
|
3538
|
+
if keyring_provider:
|
|
3539
|
+
cmd.extend(["--keyring-provider", keyring_provider])
|
|
3540
|
+
|
|
3541
|
+
return cmd
|
|
3542
|
+
|
|
3543
|
+
def _display_authentication_help(self) -> None:
|
|
3544
|
+
self.console.print(
|
|
3545
|
+
"\n[bold bright_red]❌ Publish failed. Run crackerjack again to retry publishing without re-bumping version.[/bold bright_red]"
|
|
3546
|
+
)
|
|
3547
|
+
if not (self.options and getattr(self.options, "ai_agent", False)):
|
|
3548
|
+
self.console.print("\n[bold yellow]🔐 Authentication Help:[/bold yellow]")
|
|
3549
|
+
self.console.print(" [dim]To fix authentication issues, you can:[/dim]")
|
|
3550
|
+
self.console.print(
|
|
3551
|
+
" [dim]1. Set PyPI token: export UV_PUBLISH_TOKEN=pypi-your-token-here[/dim]"
|
|
3552
|
+
)
|
|
3553
|
+
self.console.print(
|
|
3554
|
+
" [dim]2. Install keyring: uv tool install keyring[/dim]"
|
|
3555
|
+
)
|
|
3556
|
+
self.console.print(
|
|
3557
|
+
" [dim]3. Store token in keyring: keyring set https://upload.pypi.org/legacy/ __token__[/dim]"
|
|
3558
|
+
)
|
|
3559
|
+
self.console.print(
|
|
3560
|
+
" [dim]4. Ensure keyring-provider is set in pyproject.toml:[/dim]"
|
|
3561
|
+
)
|
|
3562
|
+
self.console.print(" [dim] [tool.uv][/dim]")
|
|
3563
|
+
self.console.print(' [dim] keyring-provider = "subprocess"[/dim]')
|
|
3564
|
+
|
|
3152
3565
|
def _publish_project(self, options: OptionsProtocol) -> None:
|
|
3153
3566
|
if options.publish:
|
|
3154
3567
|
self.console.print("\n" + "-" * 80)
|
|
@@ -3167,18 +3580,169 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3167
3580
|
)
|
|
3168
3581
|
raise SystemExit(1)
|
|
3169
3582
|
try:
|
|
3170
|
-
self.
|
|
3583
|
+
self._validate_authentication_setup()
|
|
3584
|
+
publish_cmd = self._build_publish_command()
|
|
3585
|
+
self.execute_command(publish_cmd)
|
|
3171
3586
|
self._mark_publish_completed()
|
|
3172
3587
|
self._clear_state()
|
|
3173
3588
|
self.console.print(
|
|
3174
|
-
"\n[bold bright_green]
|
|
3589
|
+
"\n[bold bright_green]🏆 Package published successfully![/bold bright_green]"
|
|
3175
3590
|
)
|
|
3176
3591
|
except SystemExit:
|
|
3177
|
-
self.
|
|
3178
|
-
"\n[bold bright_red]❌ Publish failed. Run crackerjack again to retry publishing without re-bumping version.[/bold bright_red]"
|
|
3179
|
-
)
|
|
3592
|
+
self._display_authentication_help()
|
|
3180
3593
|
raise
|
|
3181
3594
|
|
|
3595
|
+
def _analyze_git_changes(self) -> dict[str, t.Any]:
|
|
3596
|
+
diff_result = self._get_git_diff_output()
|
|
3597
|
+
changes = self._parse_git_diff_output(diff_result)
|
|
3598
|
+
changes["stats"] = self._get_git_stats()
|
|
3599
|
+
return changes
|
|
3600
|
+
|
|
3601
|
+
def _get_git_diff_output(self) -> t.Any:
|
|
3602
|
+
diff_cmd = ["git", "diff", "--cached", "--name-status"]
|
|
3603
|
+
diff_result = self.execute_command(diff_cmd, capture_output=True, text=True)
|
|
3604
|
+
if not diff_result.stdout and diff_result.returncode == 0:
|
|
3605
|
+
diff_cmd = ["git", "diff", "--name-status"]
|
|
3606
|
+
diff_result = self.execute_command(diff_cmd, capture_output=True, text=True)
|
|
3607
|
+
return diff_result
|
|
3608
|
+
|
|
3609
|
+
def _parse_git_diff_output(self, diff_result: t.Any) -> dict[str, t.Any]:
|
|
3610
|
+
changes = {
|
|
3611
|
+
"added": [],
|
|
3612
|
+
"modified": [],
|
|
3613
|
+
"deleted": [],
|
|
3614
|
+
"renamed": [],
|
|
3615
|
+
"total_changes": 0,
|
|
3616
|
+
}
|
|
3617
|
+
if diff_result.returncode == 0 and diff_result.stdout:
|
|
3618
|
+
self._process_diff_lines(diff_result.stdout, changes)
|
|
3619
|
+
return changes
|
|
3620
|
+
|
|
3621
|
+
def _process_diff_lines(self, stdout: str, changes: dict[str, t.Any]) -> None:
|
|
3622
|
+
for line in stdout.strip().split("\n"):
|
|
3623
|
+
if not line:
|
|
3624
|
+
continue
|
|
3625
|
+
self._process_single_diff_line(line, changes)
|
|
3626
|
+
|
|
3627
|
+
def _process_single_diff_line(self, line: str, changes: dict[str, t.Any]) -> None:
|
|
3628
|
+
parts = line.split("\t")
|
|
3629
|
+
if len(parts) >= 2:
|
|
3630
|
+
status, filename = parts[0], parts[1]
|
|
3631
|
+
self._categorize_file_change(status, filename, parts, changes)
|
|
3632
|
+
changes["total_changes"] += 1
|
|
3633
|
+
|
|
3634
|
+
def _categorize_file_change(
|
|
3635
|
+
self, status: str, filename: str, parts: list[str], changes: dict[str, t.Any]
|
|
3636
|
+
) -> None:
|
|
3637
|
+
if status == "A":
|
|
3638
|
+
changes["added"].append(filename)
|
|
3639
|
+
elif status == "M":
|
|
3640
|
+
changes["modified"].append(filename)
|
|
3641
|
+
elif status == "D":
|
|
3642
|
+
changes["deleted"].append(filename)
|
|
3643
|
+
elif status.startswith("R"):
|
|
3644
|
+
if len(parts) >= 3:
|
|
3645
|
+
changes["renamed"].append((parts[1], parts[2]))
|
|
3646
|
+
else:
|
|
3647
|
+
changes["renamed"].append((filename, "unknown"))
|
|
3648
|
+
|
|
3649
|
+
def _get_git_stats(self) -> str:
|
|
3650
|
+
stat_cmd = ["git", "diff", "--cached", "--stat"]
|
|
3651
|
+
stat_result = self.execute_command(stat_cmd, capture_output=True, text=True)
|
|
3652
|
+
if not stat_result.stdout and stat_result.returncode == 0:
|
|
3653
|
+
stat_cmd = ["git", "diff", "--stat"]
|
|
3654
|
+
stat_result = self.execute_command(stat_cmd, capture_output=True, text=True)
|
|
3655
|
+
return stat_result.stdout if stat_result.returncode == 0 else ""
|
|
3656
|
+
|
|
3657
|
+
def _categorize_changes(self, changes: dict[str, t.Any]) -> dict[str, list[str]]:
|
|
3658
|
+
categories = {
|
|
3659
|
+
"docs": [],
|
|
3660
|
+
"tests": [],
|
|
3661
|
+
"config": [],
|
|
3662
|
+
"core": [],
|
|
3663
|
+
"ci": [],
|
|
3664
|
+
"deps": [],
|
|
3665
|
+
}
|
|
3666
|
+
file_patterns = {
|
|
3667
|
+
"docs": ["README.md", "CLAUDE.md", "RULES.md", "docs/", ".md"],
|
|
3668
|
+
"tests": ["test_", "_test.py", "tests/", "conftest.py"],
|
|
3669
|
+
"config": ["pyproject.toml", ".yaml", ".yml", ".json", ".gitignore"],
|
|
3670
|
+
"ci": [".github/", "ci/", ".pre-commit"],
|
|
3671
|
+
"deps": ["requirements", "pyproject.toml", "uv.lock"],
|
|
3672
|
+
}
|
|
3673
|
+
for file_list in ("added", "modified", "deleted"):
|
|
3674
|
+
for filename in changes.get(file_list, []):
|
|
3675
|
+
categorized = False
|
|
3676
|
+
for category, patterns in file_patterns.items():
|
|
3677
|
+
if any(pattern in filename for pattern in patterns):
|
|
3678
|
+
categories[category].append(filename)
|
|
3679
|
+
categorized = True
|
|
3680
|
+
break
|
|
3681
|
+
if not categorized:
|
|
3682
|
+
categories["core"].append(filename)
|
|
3683
|
+
|
|
3684
|
+
return categories
|
|
3685
|
+
|
|
3686
|
+
def _get_primary_changes(self, categories: dict[str, list[str]]) -> list[str]:
|
|
3687
|
+
primary_changes = []
|
|
3688
|
+
category_mapping = [
|
|
3689
|
+
("core", "core functionality"),
|
|
3690
|
+
("tests", "tests"),
|
|
3691
|
+
("docs", "documentation"),
|
|
3692
|
+
("config", "configuration"),
|
|
3693
|
+
("deps", "dependencies"),
|
|
3694
|
+
]
|
|
3695
|
+
for key, label in category_mapping:
|
|
3696
|
+
if categories[key]:
|
|
3697
|
+
primary_changes.append(label)
|
|
3698
|
+
|
|
3699
|
+
return primary_changes or ["project files"]
|
|
3700
|
+
|
|
3701
|
+
def _determine_primary_action(self, changes: dict[str, t.Any]) -> str:
|
|
3702
|
+
added_count = len(changes["added"])
|
|
3703
|
+
modified_count = len(changes["modified"])
|
|
3704
|
+
deleted_count = len(changes["deleted"])
|
|
3705
|
+
if added_count > modified_count + deleted_count:
|
|
3706
|
+
return "Add"
|
|
3707
|
+
elif deleted_count > modified_count + added_count:
|
|
3708
|
+
return "Remove"
|
|
3709
|
+
elif changes["renamed"]:
|
|
3710
|
+
return "Refactor"
|
|
3711
|
+
return "Update"
|
|
3712
|
+
|
|
3713
|
+
def _generate_body_lines(self, changes: dict[str, t.Any]) -> list[str]:
|
|
3714
|
+
body_lines = []
|
|
3715
|
+
change_types = [
|
|
3716
|
+
("added", "Added"),
|
|
3717
|
+
("modified", "Modified"),
|
|
3718
|
+
("deleted", "Deleted"),
|
|
3719
|
+
("renamed", "Renamed"),
|
|
3720
|
+
]
|
|
3721
|
+
for change_type, label in change_types:
|
|
3722
|
+
items = changes.get(change_type, [])
|
|
3723
|
+
if items:
|
|
3724
|
+
count = len(items)
|
|
3725
|
+
body_lines.append(f"- {label} {count} file(s)")
|
|
3726
|
+
if change_type not in ("deleted", "renamed"):
|
|
3727
|
+
for file in items[:3]:
|
|
3728
|
+
body_lines.append(f" * {file}")
|
|
3729
|
+
if count > 3:
|
|
3730
|
+
body_lines.append(f" * ... and {count - 3} more")
|
|
3731
|
+
|
|
3732
|
+
return body_lines
|
|
3733
|
+
|
|
3734
|
+
def _generate_commit_message(self, changes: dict[str, t.Any]) -> str:
|
|
3735
|
+
if changes["total_changes"] == 0:
|
|
3736
|
+
return "Update project files"
|
|
3737
|
+
categories = self._categorize_changes(changes)
|
|
3738
|
+
primary_changes = self._get_primary_changes(categories)
|
|
3739
|
+
primary_action = self._determine_primary_action(changes)
|
|
3740
|
+
commit_subject = f"{primary_action} {' and '.join(primary_changes[:2])}"
|
|
3741
|
+
body_lines = self._generate_body_lines(changes)
|
|
3742
|
+
if body_lines:
|
|
3743
|
+
return f"{commit_subject}\n\n" + "\n".join(body_lines)
|
|
3744
|
+
return commit_subject
|
|
3745
|
+
|
|
3182
3746
|
def _commit_and_push(self, options: OptionsProtocol) -> None:
|
|
3183
3747
|
if options.commit:
|
|
3184
3748
|
self.console.print("\n" + "-" * 80)
|
|
@@ -3186,7 +3750,39 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3186
3750
|
"[bold bright_white]📝 COMMIT[/bold bright_white] [bold bright_white]Saving changes to git[/bold bright_white]"
|
|
3187
3751
|
)
|
|
3188
3752
|
self.console.print("-" * 80 + "\n")
|
|
3189
|
-
|
|
3753
|
+
changes = self._analyze_git_changes()
|
|
3754
|
+
if changes["total_changes"] > 0:
|
|
3755
|
+
self.console.print("[dim]🔍 Analyzing changes...[/dim]\n")
|
|
3756
|
+
if changes["stats"]:
|
|
3757
|
+
self.console.print(changes["stats"])
|
|
3758
|
+
suggested_msg = self._generate_commit_message(changes)
|
|
3759
|
+
self.console.print(
|
|
3760
|
+
"\n[bold cyan]📋 Suggested commit message:[/bold cyan]"
|
|
3761
|
+
)
|
|
3762
|
+
self.console.print(f"[cyan]{suggested_msg}[/cyan]\n")
|
|
3763
|
+
user_choice = (
|
|
3764
|
+
input("Use suggested message? [Y/n/e to edit]: ").strip().lower()
|
|
3765
|
+
)
|
|
3766
|
+
if user_choice in ("", "y"):
|
|
3767
|
+
commit_msg = suggested_msg
|
|
3768
|
+
elif user_choice == "e":
|
|
3769
|
+
import os
|
|
3770
|
+
import tempfile
|
|
3771
|
+
|
|
3772
|
+
with tempfile.NamedTemporaryFile(
|
|
3773
|
+
mode="w", suffix=".txt", delete=False
|
|
3774
|
+
) as f:
|
|
3775
|
+
f.write(suggested_msg)
|
|
3776
|
+
temp_path = f.name
|
|
3777
|
+
editor = os.environ.get("EDITOR", "vi")
|
|
3778
|
+
self.execute_command([editor, temp_path])
|
|
3779
|
+
with open(temp_path) as f:
|
|
3780
|
+
commit_msg = f.read().strip()
|
|
3781
|
+
Path(temp_path).unlink()
|
|
3782
|
+
else:
|
|
3783
|
+
commit_msg = input("\nEnter custom commit message: ")
|
|
3784
|
+
else:
|
|
3785
|
+
commit_msg = input("\nCommit message: ")
|
|
3190
3786
|
self.execute_command(
|
|
3191
3787
|
["git", "commit", "-m", commit_msg, "--no-verify", "--", "."]
|
|
3192
3788
|
)
|
|
@@ -3215,7 +3811,8 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3215
3811
|
)
|
|
3216
3812
|
self.console.print("-" * 80 + "\n")
|
|
3217
3813
|
self.config_manager.copy_documentation_templates(
|
|
3218
|
-
force_update=options.force_update_docs
|
|
3814
|
+
force_update=options.force_update_docs,
|
|
3815
|
+
compress_docs=options.compress_docs,
|
|
3219
3816
|
)
|
|
3220
3817
|
|
|
3221
3818
|
def execute_command(
|
|
@@ -3289,7 +3886,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3289
3886
|
raise SystemExit(1)
|
|
3290
3887
|
else:
|
|
3291
3888
|
self.console.print(
|
|
3292
|
-
"\n[bold bright_green]
|
|
3889
|
+
"\n[bold bright_green]🏆 All comprehensive quality checks passed![/bold bright_green]"
|
|
3293
3890
|
)
|
|
3294
3891
|
|
|
3295
3892
|
async def _run_comprehensive_quality_checks_async(
|
|
@@ -3339,7 +3936,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3339
3936
|
raise SystemExit(1)
|
|
3340
3937
|
else:
|
|
3341
3938
|
self.console.print(
|
|
3342
|
-
"[bold bright_green]
|
|
3939
|
+
"[bold bright_green]🏆 All comprehensive quality checks passed![/bold bright_green]"
|
|
3343
3940
|
)
|
|
3344
3941
|
|
|
3345
3942
|
def _run_tracked_task(
|
|
@@ -3450,7 +4047,8 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3450
4047
|
self._run_tracked_task(
|
|
3451
4048
|
"clean_project", "Clean project code", lambda: self._clean_project(options)
|
|
3452
4049
|
)
|
|
3453
|
-
self.project_manager
|
|
4050
|
+
if self.project_manager is not None:
|
|
4051
|
+
self.project_manager.options = options
|
|
3454
4052
|
if not options.skip_hooks:
|
|
3455
4053
|
self._run_tracked_task(
|
|
3456
4054
|
"pre_commit",
|
|
@@ -3480,7 +4078,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3480
4078
|
)
|
|
3481
4079
|
self.console.print("\n" + "-" * 80)
|
|
3482
4080
|
self.console.print(
|
|
3483
|
-
"[bold bright_green]
|
|
4081
|
+
"[bold bright_green]🏆 CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
|
|
3484
4082
|
)
|
|
3485
4083
|
self.console.print("-" * 80 + "\n")
|
|
3486
4084
|
|
|
@@ -3500,7 +4098,8 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3500
4098
|
self._update_project(options)
|
|
3501
4099
|
self._update_precommit(options)
|
|
3502
4100
|
await self._clean_project_async(options)
|
|
3503
|
-
self.project_manager
|
|
4101
|
+
if self.project_manager is not None:
|
|
4102
|
+
self.project_manager.options = options
|
|
3504
4103
|
if not options.skip_hooks:
|
|
3505
4104
|
if getattr(options, "ai_agent", False):
|
|
3506
4105
|
await self.project_manager.run_pre_commit_with_analysis_async()
|
|
@@ -3517,7 +4116,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
3517
4116
|
self._publish_project(options)
|
|
3518
4117
|
self.console.print("\n" + "-" * 80)
|
|
3519
4118
|
self.console.print(
|
|
3520
|
-
"[bold bright_green]
|
|
4119
|
+
"[bold bright_green]🏆 CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
|
|
3521
4120
|
)
|
|
3522
4121
|
self.console.print("-" * 80 + "\n")
|
|
3523
4122
|
|