crackerjack 0.27.8__py3-none-any.whl → 0.28.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/.pre-commit-config-ai.yaml +1 -5
- crackerjack/__main__.py +40 -0
- crackerjack/crackerjack.py +705 -23
- crackerjack/pyproject.toml +1 -1
- {crackerjack-0.27.8.dist-info → crackerjack-0.28.0.dist-info}/METADATA +1 -1
- {crackerjack-0.27.8.dist-info → crackerjack-0.28.0.dist-info}/RECORD +8 -8
- {crackerjack-0.27.8.dist-info → crackerjack-0.28.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.27.8.dist-info → crackerjack-0.28.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -96,7 +96,7 @@ repos:
|
|
|
96
96
|
rev: v3.0.0
|
|
97
97
|
hooks:
|
|
98
98
|
- id: complexipy
|
|
99
|
-
args: ["-d", "low", "--output", "complexipy.json"]
|
|
99
|
+
args: ["-d", "low", "--output-json", "complexipy.json"]
|
|
100
100
|
stages: [pre-commit]
|
|
101
101
|
|
|
102
102
|
- repo: https://github.com/dosisod/refurb
|
|
@@ -116,10 +116,6 @@ repos:
|
|
|
116
116
|
- --aggressive
|
|
117
117
|
- --only-without-imports
|
|
118
118
|
- --guess-common-names
|
|
119
|
-
- --cache-dir=.autotyping-cache
|
|
120
|
-
- --workers=4
|
|
121
|
-
- --max-line-length=88
|
|
122
|
-
- --exclude-name=test_*,conftest
|
|
123
119
|
- crackerjack
|
|
124
120
|
types_or: [ python, pyi ]
|
|
125
121
|
language: python
|
crackerjack/__main__.py
CHANGED
|
@@ -30,6 +30,8 @@ class Options(BaseModel):
|
|
|
30
30
|
bump: BumpOption | None = None
|
|
31
31
|
verbose: bool = False
|
|
32
32
|
update_precommit: bool = False
|
|
33
|
+
update_docs: bool = False
|
|
34
|
+
force_update_docs: bool = False
|
|
33
35
|
clean: bool = False
|
|
34
36
|
test: bool = False
|
|
35
37
|
benchmark: bool = False
|
|
@@ -43,6 +45,9 @@ class Options(BaseModel):
|
|
|
43
45
|
skip_hooks: bool = False
|
|
44
46
|
comprehensive: bool = False
|
|
45
47
|
async_mode: bool = False
|
|
48
|
+
track_progress: bool = False
|
|
49
|
+
resume_from: str | None = None
|
|
50
|
+
progress_file: str | None = None
|
|
46
51
|
|
|
47
52
|
@classmethod
|
|
48
53
|
@field_validator("publish", "bump", mode="before")
|
|
@@ -72,6 +77,16 @@ cli_options = {
|
|
|
72
77
|
"update_precommit": typer.Option(
|
|
73
78
|
False, "-u", "--update-precommit", help="Update pre-commit hooks."
|
|
74
79
|
),
|
|
80
|
+
"update_docs": typer.Option(
|
|
81
|
+
False,
|
|
82
|
+
"--update-docs",
|
|
83
|
+
help="Update CLAUDE.md and RULES.md with latest quality standards.",
|
|
84
|
+
),
|
|
85
|
+
"force_update_docs": typer.Option(
|
|
86
|
+
False,
|
|
87
|
+
"--force-update-docs",
|
|
88
|
+
help="Force update CLAUDE.md and RULES.md even if they exist.",
|
|
89
|
+
),
|
|
75
90
|
"verbose": typer.Option(False, "-v", "--verbose", help="Enable verbose output."),
|
|
76
91
|
"publish": typer.Option(
|
|
77
92
|
None,
|
|
@@ -152,6 +167,21 @@ cli_options = {
|
|
|
152
167
|
help="Enable async mode for faster file operations (experimental).",
|
|
153
168
|
hidden=True,
|
|
154
169
|
),
|
|
170
|
+
"track_progress": typer.Option(
|
|
171
|
+
False,
|
|
172
|
+
"--track-progress",
|
|
173
|
+
help="Enable session progress tracking with detailed markdown output.",
|
|
174
|
+
),
|
|
175
|
+
"resume_from": typer.Option(
|
|
176
|
+
None,
|
|
177
|
+
"--resume-from",
|
|
178
|
+
help="Resume session from existing progress file.",
|
|
179
|
+
),
|
|
180
|
+
"progress_file": typer.Option(
|
|
181
|
+
None,
|
|
182
|
+
"--progress-file",
|
|
183
|
+
help="Custom path for progress file (default: SESSION-PROGRESS-{timestamp}.md).",
|
|
184
|
+
),
|
|
155
185
|
}
|
|
156
186
|
|
|
157
187
|
|
|
@@ -161,6 +191,8 @@ def main(
|
|
|
161
191
|
interactive: bool = cli_options["interactive"],
|
|
162
192
|
no_config_updates: bool = cli_options["no_config_updates"],
|
|
163
193
|
update_precommit: bool = cli_options["update_precommit"],
|
|
194
|
+
update_docs: bool = cli_options["update_docs"],
|
|
195
|
+
force_update_docs: bool = cli_options["force_update_docs"],
|
|
164
196
|
verbose: bool = cli_options["verbose"],
|
|
165
197
|
publish: BumpOption | None = cli_options["publish"],
|
|
166
198
|
all: BumpOption | None = cli_options["all"],
|
|
@@ -179,12 +211,17 @@ def main(
|
|
|
179
211
|
ai_agent: bool = cli_options["ai_agent"],
|
|
180
212
|
comprehensive: bool = cli_options["comprehensive"],
|
|
181
213
|
async_mode: bool = cli_options["async_mode"],
|
|
214
|
+
track_progress: bool = cli_options["track_progress"],
|
|
215
|
+
resume_from: str | None = cli_options["resume_from"],
|
|
216
|
+
progress_file: str | None = cli_options["progress_file"],
|
|
182
217
|
) -> None:
|
|
183
218
|
options = Options(
|
|
184
219
|
commit=commit,
|
|
185
220
|
interactive=interactive,
|
|
186
221
|
no_config_updates=no_config_updates,
|
|
187
222
|
update_precommit=update_precommit,
|
|
223
|
+
update_docs=update_docs,
|
|
224
|
+
force_update_docs=force_update_docs,
|
|
188
225
|
verbose=verbose,
|
|
189
226
|
publish=publish,
|
|
190
227
|
bump=bump,
|
|
@@ -201,6 +238,9 @@ def main(
|
|
|
201
238
|
comprehensive=comprehensive,
|
|
202
239
|
create_pr=create_pr,
|
|
203
240
|
async_mode=async_mode,
|
|
241
|
+
track_progress=track_progress,
|
|
242
|
+
resume_from=resume_from,
|
|
243
|
+
progress_file=progress_file,
|
|
204
244
|
)
|
|
205
245
|
if ai_agent:
|
|
206
246
|
import os
|
crackerjack/crackerjack.py
CHANGED
|
@@ -37,6 +37,463 @@ class HookResult:
|
|
|
37
37
|
self.issues_found = []
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
@dataclass
|
|
41
|
+
class TaskStatus:
|
|
42
|
+
id: str
|
|
43
|
+
name: str
|
|
44
|
+
status: str
|
|
45
|
+
start_time: float | None = None
|
|
46
|
+
end_time: float | None = None
|
|
47
|
+
duration: float | None = None
|
|
48
|
+
details: str | None = None
|
|
49
|
+
error_message: str | None = None
|
|
50
|
+
files_changed: list[str] | None = None
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
if self.files_changed is None:
|
|
54
|
+
self.files_changed = []
|
|
55
|
+
if self.start_time is not None and self.end_time is not None:
|
|
56
|
+
self.duration = self.end_time - self.start_time
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SessionTracker(BaseModel, arbitrary_types_allowed=True):
|
|
60
|
+
console: Console
|
|
61
|
+
session_id: str
|
|
62
|
+
start_time: float
|
|
63
|
+
progress_file: Path
|
|
64
|
+
tasks: dict[str, TaskStatus] = {}
|
|
65
|
+
current_task: str | None = None
|
|
66
|
+
metadata: dict[str, t.Any] = {}
|
|
67
|
+
|
|
68
|
+
def __init__(self, **data: t.Any) -> None:
|
|
69
|
+
super().__init__(**data)
|
|
70
|
+
if not self.tasks:
|
|
71
|
+
self.tasks = {}
|
|
72
|
+
if not self.metadata:
|
|
73
|
+
self.metadata = {}
|
|
74
|
+
|
|
75
|
+
def start_task(
|
|
76
|
+
self, task_id: str, task_name: str, details: str | None = None
|
|
77
|
+
) -> None:
|
|
78
|
+
task = TaskStatus(
|
|
79
|
+
id=task_id,
|
|
80
|
+
name=task_name,
|
|
81
|
+
status="in_progress",
|
|
82
|
+
start_time=time.time(),
|
|
83
|
+
details=details,
|
|
84
|
+
)
|
|
85
|
+
self.tasks[task_id] = task
|
|
86
|
+
self.current_task = task_id
|
|
87
|
+
self._update_progress_file()
|
|
88
|
+
self.console.print(f"[yellow]⏳[/yellow] Started: {task_name}")
|
|
89
|
+
|
|
90
|
+
def complete_task(
|
|
91
|
+
self,
|
|
92
|
+
task_id: str,
|
|
93
|
+
details: str | None = None,
|
|
94
|
+
files_changed: list[str] | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
if task_id in self.tasks:
|
|
97
|
+
task = self.tasks[task_id]
|
|
98
|
+
task.status = "completed"
|
|
99
|
+
task.end_time = time.time()
|
|
100
|
+
task.duration = task.end_time - (task.start_time or task.end_time)
|
|
101
|
+
if details:
|
|
102
|
+
task.details = details
|
|
103
|
+
if files_changed:
|
|
104
|
+
task.files_changed = files_changed
|
|
105
|
+
self._update_progress_file()
|
|
106
|
+
self.console.print(f"[green]✅[/green] Completed: {task.name}")
|
|
107
|
+
if self.current_task == task_id:
|
|
108
|
+
self.current_task = None
|
|
109
|
+
|
|
110
|
+
def fail_task(
|
|
111
|
+
self,
|
|
112
|
+
task_id: str,
|
|
113
|
+
error_message: str,
|
|
114
|
+
details: str | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
if task_id in self.tasks:
|
|
117
|
+
task = self.tasks[task_id]
|
|
118
|
+
task.status = "failed"
|
|
119
|
+
task.end_time = time.time()
|
|
120
|
+
task.duration = task.end_time - (task.start_time or task.end_time)
|
|
121
|
+
task.error_message = error_message
|
|
122
|
+
if details:
|
|
123
|
+
task.details = details
|
|
124
|
+
self._update_progress_file()
|
|
125
|
+
self.console.print(f"[red]❌[/red] Failed: {task.name} - {error_message}")
|
|
126
|
+
if self.current_task == task_id:
|
|
127
|
+
self.current_task = None
|
|
128
|
+
|
|
129
|
+
def skip_task(self, task_id: str, reason: str) -> None:
|
|
130
|
+
if task_id in self.tasks:
|
|
131
|
+
task = self.tasks[task_id]
|
|
132
|
+
task.status = "skipped"
|
|
133
|
+
task.end_time = time.time()
|
|
134
|
+
task.details = f"Skipped: {reason}"
|
|
135
|
+
self._update_progress_file()
|
|
136
|
+
self.console.print(f"[blue]⏩[/blue] Skipped: {task.name} - {reason}")
|
|
137
|
+
if self.current_task == task_id:
|
|
138
|
+
self.current_task = None
|
|
139
|
+
|
|
140
|
+
def _update_progress_file(self) -> None:
|
|
141
|
+
try:
|
|
142
|
+
content = self._generate_markdown_content()
|
|
143
|
+
self.progress_file.write_text(content, encoding="utf-8")
|
|
144
|
+
except OSError as e:
|
|
145
|
+
self.console.print(
|
|
146
|
+
f"[yellow]Warning: Failed to update progress file: {e}[/yellow]"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _generate_header_section(self) -> str:
|
|
150
|
+
from datetime import datetime
|
|
151
|
+
|
|
152
|
+
completed_tasks = sum(
|
|
153
|
+
1 for task in self.tasks.values() if task.status == "completed"
|
|
154
|
+
)
|
|
155
|
+
total_tasks = len(self.tasks)
|
|
156
|
+
overall_status = "In Progress"
|
|
157
|
+
if completed_tasks == total_tasks and total_tasks > 0:
|
|
158
|
+
overall_status = "Completed"
|
|
159
|
+
elif any(task.status == "failed" for task in self.tasks.values()):
|
|
160
|
+
overall_status = "Failed"
|
|
161
|
+
start_datetime = datetime.fromtimestamp(self.start_time)
|
|
162
|
+
|
|
163
|
+
return f"""# Crackerjack Session Progress: {self.session_id}
|
|
164
|
+
**Session ID**: {self.session_id}
|
|
165
|
+
**Started**: {start_datetime.strftime("%Y-%m-%d %H:%M:%S")}
|
|
166
|
+
**Status**: {overall_status}
|
|
167
|
+
**Progress**: {completed_tasks}/{total_tasks} tasks completed
|
|
168
|
+
|
|
169
|
+
- **Working Directory**: {self.metadata.get("working_dir", Path.cwd())}
|
|
170
|
+
- **Python Version**: {self.metadata.get("python_version", "Unknown")}
|
|
171
|
+
- **Crackerjack Version**: {self.metadata.get("crackerjack_version", "Unknown")}
|
|
172
|
+
- **CLI Options**: {self.metadata.get("cli_options", "Unknown")}
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def _generate_task_overview_section(self) -> str:
|
|
177
|
+
content = """## Task Progress Overview
|
|
178
|
+
| Task | Status | Duration | Details |
|
|
179
|
+
|------|--------|----------|---------|
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
for task in self.tasks.values():
|
|
183
|
+
status_emoji = {
|
|
184
|
+
"pending": "⏸️",
|
|
185
|
+
"in_progress": "⏳",
|
|
186
|
+
"completed": "✅",
|
|
187
|
+
"failed": "❌",
|
|
188
|
+
"skipped": "⏩",
|
|
189
|
+
}.get(task.status, "❓")
|
|
190
|
+
|
|
191
|
+
duration_str = f"{task.duration:.2f}s" if task.duration else "N/A"
|
|
192
|
+
details_str = (
|
|
193
|
+
task.details[:50] + "..."
|
|
194
|
+
if task.details and len(task.details) > 50
|
|
195
|
+
else (task.details or "")
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
content += f"| {task.name} | {status_emoji} {task.status} | {duration_str} | {details_str} |\n"
|
|
199
|
+
|
|
200
|
+
return content + "\n"
|
|
201
|
+
|
|
202
|
+
def _generate_task_details_section(self) -> str:
|
|
203
|
+
content = "## Detailed Task Log\n\n"
|
|
204
|
+
for task in self.tasks.values():
|
|
205
|
+
content += self._format_task_detail(task)
|
|
206
|
+
return content
|
|
207
|
+
|
|
208
|
+
def _format_task_detail(self, task: TaskStatus) -> str:
|
|
209
|
+
from datetime import datetime
|
|
210
|
+
|
|
211
|
+
if task.status == "completed":
|
|
212
|
+
return self._format_completed_task(task, datetime)
|
|
213
|
+
elif task.status == "in_progress":
|
|
214
|
+
return self._format_in_progress_task(task, datetime)
|
|
215
|
+
elif task.status == "failed":
|
|
216
|
+
return self._format_failed_task(task, datetime)
|
|
217
|
+
elif task.status == "skipped":
|
|
218
|
+
return self._format_skipped_task(task)
|
|
219
|
+
return ""
|
|
220
|
+
|
|
221
|
+
def _format_completed_task(self, task: TaskStatus, datetime: t.Any) -> str:
|
|
222
|
+
start_time = (
|
|
223
|
+
datetime.fromtimestamp(task.start_time) if task.start_time else "Unknown"
|
|
224
|
+
)
|
|
225
|
+
end_time = datetime.fromtimestamp(task.end_time) if task.end_time else "Unknown"
|
|
226
|
+
files_list = ", ".join(task.files_changed) if task.files_changed else "None"
|
|
227
|
+
return f"""### ✅ {task.name} - COMPLETED
|
|
228
|
+
- **Started**: {start_time}
|
|
229
|
+
- **Completed**: {end_time}
|
|
230
|
+
- **Duration**: {task.duration:.2f}s
|
|
231
|
+
- **Files Changed**: {files_list}
|
|
232
|
+
- **Details**: {task.details or "N/A"}
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def _format_in_progress_task(self, task: TaskStatus, datetime: t.Any) -> str:
|
|
237
|
+
start_time = (
|
|
238
|
+
datetime.fromtimestamp(task.start_time) if task.start_time else "Unknown"
|
|
239
|
+
)
|
|
240
|
+
return f"""### ⏳ {task.name} - IN PROGRESS
|
|
241
|
+
- **Started**: {start_time}
|
|
242
|
+
- **Current Status**: {task.details or "Processing..."}
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def _format_failed_task(self, task: TaskStatus, datetime: t.Any) -> str:
|
|
247
|
+
start_time = (
|
|
248
|
+
datetime.fromtimestamp(task.start_time) if task.start_time else "Unknown"
|
|
249
|
+
)
|
|
250
|
+
fail_time = (
|
|
251
|
+
datetime.fromtimestamp(task.end_time) if task.end_time else "Unknown"
|
|
252
|
+
)
|
|
253
|
+
return f"""### ❌ {task.name} - FAILED
|
|
254
|
+
- **Started**: {start_time}
|
|
255
|
+
- **Failed**: {fail_time}
|
|
256
|
+
- **Error**: {task.error_message or "Unknown error"}
|
|
257
|
+
- **Recovery Suggestions**: Check error details and retry the failed operation
|
|
258
|
+
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def _format_skipped_task(self, task: TaskStatus) -> str:
|
|
262
|
+
return f"""### ⏩ {task.name} - SKIPPED
|
|
263
|
+
- **Reason**: {task.details or "No reason provided"}
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def _generate_footer_section(self) -> str:
|
|
268
|
+
content = f"""## Session Recovery Information
|
|
269
|
+
If this session was interrupted, you can resume from where you left off:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
python -m crackerjack --resume-from {self.progress_file.name}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
all_files = set()
|
|
278
|
+
for task in self.tasks.values():
|
|
279
|
+
if task.files_changed:
|
|
280
|
+
all_files.update(task.files_changed)
|
|
281
|
+
|
|
282
|
+
if all_files:
|
|
283
|
+
for file_path in sorted(all_files):
|
|
284
|
+
content += f"- {file_path}\n"
|
|
285
|
+
else:
|
|
286
|
+
content += "- No files modified yet\n"
|
|
287
|
+
|
|
288
|
+
content += "\n## Next Steps\n\n"
|
|
289
|
+
|
|
290
|
+
pending_tasks = [
|
|
291
|
+
task for task in self.tasks.values() if task.status == "pending"
|
|
292
|
+
]
|
|
293
|
+
in_progress_tasks = [
|
|
294
|
+
task for task in self.tasks.values() if task.status == "in_progress"
|
|
295
|
+
]
|
|
296
|
+
failed_tasks = [task for task in self.tasks.values() if task.status == "failed"]
|
|
297
|
+
|
|
298
|
+
if failed_tasks:
|
|
299
|
+
content += "⚠️ Address failed tasks:\n"
|
|
300
|
+
for task in failed_tasks:
|
|
301
|
+
content += f"- Fix {task.name}: {task.error_message}\n"
|
|
302
|
+
elif in_progress_tasks:
|
|
303
|
+
content += "🔄 Currently working on:\n"
|
|
304
|
+
for task in in_progress_tasks:
|
|
305
|
+
content += f"- {task.name}\n"
|
|
306
|
+
elif pending_tasks:
|
|
307
|
+
content += "📋 Next tasks to complete:\n"
|
|
308
|
+
for task in pending_tasks:
|
|
309
|
+
content += f"- {task.name}\n"
|
|
310
|
+
else:
|
|
311
|
+
content += "🎉 All tasks completed successfully!\n"
|
|
312
|
+
|
|
313
|
+
return content
|
|
314
|
+
|
|
315
|
+
def _generate_markdown_content(self) -> str:
|
|
316
|
+
return (
|
|
317
|
+
self._generate_header_section()
|
|
318
|
+
+ self._generate_task_overview_section()
|
|
319
|
+
+ self._generate_task_details_section()
|
|
320
|
+
+ self._generate_footer_section()
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
def create_session(
|
|
325
|
+
cls,
|
|
326
|
+
console: Console,
|
|
327
|
+
session_id: str | None = None,
|
|
328
|
+
progress_file: Path | None = None,
|
|
329
|
+
metadata: dict[str, t.Any] | None = None,
|
|
330
|
+
) -> "SessionTracker":
|
|
331
|
+
import uuid
|
|
332
|
+
|
|
333
|
+
if session_id is None:
|
|
334
|
+
session_id = str(uuid.uuid4())[:8]
|
|
335
|
+
|
|
336
|
+
if progress_file is None:
|
|
337
|
+
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
338
|
+
progress_file = Path(f"SESSION-PROGRESS-{timestamp}.md")
|
|
339
|
+
|
|
340
|
+
tracker = cls(
|
|
341
|
+
console=console,
|
|
342
|
+
session_id=session_id,
|
|
343
|
+
start_time=time.time(),
|
|
344
|
+
progress_file=progress_file,
|
|
345
|
+
metadata=metadata or {},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
tracker._update_progress_file()
|
|
349
|
+
console.print(f"[green]📋[/green] Session tracking started: {progress_file}")
|
|
350
|
+
return tracker
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def find_recent_progress_files(cls, directory: Path = Path.cwd()) -> list[Path]:
|
|
354
|
+
progress_files = []
|
|
355
|
+
for file_path in directory.glob("SESSION-PROGRESS-*.md"):
|
|
356
|
+
try:
|
|
357
|
+
if file_path.is_file():
|
|
358
|
+
progress_files.append(file_path)
|
|
359
|
+
except (OSError, PermissionError):
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
return sorted(progress_files, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def is_session_incomplete(cls, progress_file: Path) -> bool:
|
|
366
|
+
if not progress_file.exists():
|
|
367
|
+
return False
|
|
368
|
+
try:
|
|
369
|
+
content = progress_file.read_text(encoding="utf-8")
|
|
370
|
+
has_in_progress = "⏳" in content or "in_progress" in content
|
|
371
|
+
has_failed = "❌" in content or "failed" in content
|
|
372
|
+
has_pending = "⏸️" in content or "pending" in content
|
|
373
|
+
stat = progress_file.stat()
|
|
374
|
+
age_hours = (time.time() - stat.st_mtime) / 3600
|
|
375
|
+
is_recent = age_hours < 24
|
|
376
|
+
|
|
377
|
+
return (has_in_progress or has_failed or has_pending) and is_recent
|
|
378
|
+
except (OSError, UnicodeDecodeError):
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
@classmethod
|
|
382
|
+
def find_incomplete_session(cls, directory: Path = Path.cwd()) -> Path | None:
|
|
383
|
+
recent_files = cls.find_recent_progress_files(directory)
|
|
384
|
+
for progress_file in recent_files:
|
|
385
|
+
if cls.is_session_incomplete(progress_file):
|
|
386
|
+
return progress_file
|
|
387
|
+
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def auto_detect_session(
|
|
392
|
+
cls, console: Console, directory: Path = Path.cwd()
|
|
393
|
+
) -> "SessionTracker | None":
|
|
394
|
+
incomplete_session = cls.find_incomplete_session(directory)
|
|
395
|
+
if incomplete_session:
|
|
396
|
+
return cls._handle_incomplete_session(console, incomplete_session)
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def _handle_incomplete_session(
|
|
401
|
+
cls, console: Console, incomplete_session: Path
|
|
402
|
+
) -> "SessionTracker | None":
|
|
403
|
+
console.print(
|
|
404
|
+
f"[yellow]📋[/yellow] Found incomplete session: {incomplete_session.name}"
|
|
405
|
+
)
|
|
406
|
+
try:
|
|
407
|
+
content = incomplete_session.read_text(encoding="utf-8")
|
|
408
|
+
session_info = cls._parse_session_info(content)
|
|
409
|
+
cls._display_session_info(console, session_info)
|
|
410
|
+
return cls._prompt_resume_session(console, incomplete_session)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
console.print(f"[yellow]⚠️[/yellow] Could not parse session file: {e}")
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def _parse_session_info(cls, content: str) -> dict[str, str | list[str] | None]:
|
|
417
|
+
import re
|
|
418
|
+
|
|
419
|
+
session_match = re.search(r"Session ID\*\*:\s*(.+)", content)
|
|
420
|
+
session_id: str = session_match.group(1).strip() if session_match else "unknown"
|
|
421
|
+
progress_match = re.search(r"Progress\*\*:\s*(\d+)/(\d+)", content)
|
|
422
|
+
progress_info: str | None = None
|
|
423
|
+
if progress_match:
|
|
424
|
+
completed = progress_match.group(1)
|
|
425
|
+
total = progress_match.group(2)
|
|
426
|
+
progress_info = f"{completed}/{total} tasks completed"
|
|
427
|
+
failed_tasks: list[str] = []
|
|
428
|
+
for line in content.split("\n"):
|
|
429
|
+
if "❌" in line and "- FAILED" in line:
|
|
430
|
+
task_match = re.search(r"### ❌ (.+?) - FAILED", line)
|
|
431
|
+
if task_match:
|
|
432
|
+
task_name: str = task_match.group(1)
|
|
433
|
+
failed_tasks.append(task_name)
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
"session_id": session_id,
|
|
437
|
+
"progress_info": progress_info,
|
|
438
|
+
"failed_tasks": failed_tasks,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@classmethod
|
|
442
|
+
def _display_session_info(
|
|
443
|
+
cls, console: Console, session_info: dict[str, str | list[str] | None]
|
|
444
|
+
) -> None:
|
|
445
|
+
console.print(f"[cyan] Session ID:[/cyan] {session_info['session_id']}")
|
|
446
|
+
if session_info["progress_info"]:
|
|
447
|
+
console.print(f"[cyan] Progress:[/cyan] {session_info['progress_info']}")
|
|
448
|
+
if session_info["failed_tasks"]:
|
|
449
|
+
console.print(
|
|
450
|
+
f"[red] Failed tasks:[/red] {', '.join(session_info['failed_tasks'])}"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def _prompt_resume_session(
|
|
455
|
+
cls, console: Console, incomplete_session: Path
|
|
456
|
+
) -> "SessionTracker | None":
|
|
457
|
+
try:
|
|
458
|
+
import sys
|
|
459
|
+
|
|
460
|
+
console.print("[yellow]❓[/yellow] Resume this session? [y/N]: ", end="")
|
|
461
|
+
sys.stdout.flush()
|
|
462
|
+
response = input().strip().lower()
|
|
463
|
+
if response in ("y", "yes"):
|
|
464
|
+
return cls.resume_session(console, incomplete_session)
|
|
465
|
+
else:
|
|
466
|
+
console.print("[blue]ℹ️[/blue] Starting new session instead")
|
|
467
|
+
return None
|
|
468
|
+
except (KeyboardInterrupt, EOFError):
|
|
469
|
+
console.print("\n[blue]ℹ️[/blue] Starting new session instead")
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def resume_session(cls, console: Console, progress_file: Path) -> "SessionTracker":
|
|
474
|
+
if not progress_file.exists():
|
|
475
|
+
raise FileNotFoundError(f"Progress file not found: {progress_file}")
|
|
476
|
+
try:
|
|
477
|
+
content = progress_file.read_text(encoding="utf-8")
|
|
478
|
+
session_id = "resumed"
|
|
479
|
+
import re
|
|
480
|
+
|
|
481
|
+
session_match = re.search(r"Session ID\*\*:\s*(.+)", content)
|
|
482
|
+
if session_match:
|
|
483
|
+
session_id = session_match.group(1).strip()
|
|
484
|
+
tracker = cls(
|
|
485
|
+
console=console,
|
|
486
|
+
session_id=session_id,
|
|
487
|
+
start_time=time.time(),
|
|
488
|
+
progress_file=progress_file,
|
|
489
|
+
metadata={},
|
|
490
|
+
)
|
|
491
|
+
console.print(f"[green]🔄[/green] Resumed session from: {progress_file}")
|
|
492
|
+
return tracker
|
|
493
|
+
except Exception as e:
|
|
494
|
+
raise RuntimeError(f"Failed to resume session: {e}") from e
|
|
495
|
+
|
|
496
|
+
|
|
40
497
|
config_files = (
|
|
41
498
|
".gitignore",
|
|
42
499
|
".pre-commit-config.yaml",
|
|
@@ -44,6 +501,11 @@ config_files = (
|
|
|
44
501
|
".pre-commit-config-fast.yaml",
|
|
45
502
|
".libcst.codemod.yaml",
|
|
46
503
|
)
|
|
504
|
+
|
|
505
|
+
documentation_files = (
|
|
506
|
+
"CLAUDE.md",
|
|
507
|
+
"RULES.md",
|
|
508
|
+
)
|
|
47
509
|
default_python_version = "3.13"
|
|
48
510
|
|
|
49
511
|
|
|
@@ -61,6 +523,8 @@ class OptionsProtocol(t.Protocol):
|
|
|
61
523
|
no_config_updates: bool
|
|
62
524
|
verbose: bool
|
|
63
525
|
update_precommit: bool
|
|
526
|
+
update_docs: bool
|
|
527
|
+
force_update_docs: bool
|
|
64
528
|
clean: bool
|
|
65
529
|
test: bool
|
|
66
530
|
benchmark: bool
|
|
@@ -76,6 +540,9 @@ class OptionsProtocol(t.Protocol):
|
|
|
76
540
|
skip_hooks: bool = False
|
|
77
541
|
comprehensive: bool = False
|
|
78
542
|
async_mode: bool = False
|
|
543
|
+
track_progress: bool = False
|
|
544
|
+
resume_from: str | None = None
|
|
545
|
+
progress_file: str | None = None
|
|
79
546
|
|
|
80
547
|
|
|
81
548
|
class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
|
|
@@ -1216,6 +1683,76 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
1216
1683
|
if configs_to_add:
|
|
1217
1684
|
self.execute_command(["git", "add"] + configs_to_add)
|
|
1218
1685
|
|
|
1686
|
+
def copy_documentation_templates(self, force_update: bool = False) -> None:
|
|
1687
|
+
docs_to_add: list[str] = []
|
|
1688
|
+
for doc_file in documentation_files:
|
|
1689
|
+
doc_path = self.our_path / doc_file
|
|
1690
|
+
pkg_doc_path = self.pkg_path / doc_file
|
|
1691
|
+
if not doc_path.exists():
|
|
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"
|
|
1706
|
+
)
|
|
1707
|
+
if docs_to_add:
|
|
1708
|
+
self.execute_command(["git", "add"] + docs_to_add)
|
|
1709
|
+
|
|
1710
|
+
def _customize_documentation_content(self, content: str, filename: str) -> str:
|
|
1711
|
+
if filename == "CLAUDE.md":
|
|
1712
|
+
return self._customize_claude_md(content)
|
|
1713
|
+
elif filename == "RULES.md":
|
|
1714
|
+
return self._customize_rules_md(content)
|
|
1715
|
+
return content
|
|
1716
|
+
|
|
1717
|
+
def _customize_claude_md(self, content: str) -> str:
|
|
1718
|
+
project_name = self.pkg_name
|
|
1719
|
+
content = content.replace("crackerjack", project_name).replace(
|
|
1720
|
+
"Crackerjack", project_name.title()
|
|
1721
|
+
)
|
|
1722
|
+
header = f"""# {project_name.upper()}.md
|
|
1723
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
1724
|
+
|
|
1725
|
+
*This file was automatically generated by Crackerjack and contains the latest Python quality standards.*
|
|
1726
|
+
|
|
1727
|
+
{project_name.title()} is a Python project that follows modern development practices and maintains high code quality standards using automated tools and best practices.
|
|
1728
|
+
|
|
1729
|
+
"""
|
|
1730
|
+
|
|
1731
|
+
lines = content.split("\n")
|
|
1732
|
+
start_idx = 0
|
|
1733
|
+
for i, line in enumerate(lines):
|
|
1734
|
+
if line.startswith(("## Development Guidelines", "## Code Quality")):
|
|
1735
|
+
start_idx = i
|
|
1736
|
+
break
|
|
1737
|
+
|
|
1738
|
+
if start_idx > 0:
|
|
1739
|
+
relevant_content = "\n".join(lines[start_idx:])
|
|
1740
|
+
return header + relevant_content
|
|
1741
|
+
|
|
1742
|
+
return header + content
|
|
1743
|
+
|
|
1744
|
+
def _customize_rules_md(self, content: str) -> str:
|
|
1745
|
+
project_name = self.pkg_name
|
|
1746
|
+
content = content.replace("crackerjack", project_name).replace(
|
|
1747
|
+
"Crackerjack", project_name.title()
|
|
1748
|
+
)
|
|
1749
|
+
header = f"""# {project_name.title()} Style Rules
|
|
1750
|
+
*This file was automatically generated by Crackerjack and contains the latest Python quality standards.*
|
|
1751
|
+
|
|
1752
|
+
"""
|
|
1753
|
+
|
|
1754
|
+
return header + content
|
|
1755
|
+
|
|
1219
1756
|
def execute_command(
|
|
1220
1757
|
self, cmd: list[str], **kwargs: t.Any
|
|
1221
1758
|
) -> subprocess.CompletedProcess[str]:
|
|
@@ -2116,6 +2653,26 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
|
|
|
2116
2653
|
f"[yellow]Warning: Failed to generate AI summary: {e}[/yellow]"
|
|
2117
2654
|
)
|
|
2118
2655
|
|
|
2656
|
+
def update_precommit_hooks(self) -> None:
|
|
2657
|
+
try:
|
|
2658
|
+
result = self.execute_command(
|
|
2659
|
+
["uv", "run", "pre-commit", "autoupdate"],
|
|
2660
|
+
capture_output=True,
|
|
2661
|
+
text=True,
|
|
2662
|
+
)
|
|
2663
|
+
if result.returncode == 0:
|
|
2664
|
+
self.console.print(
|
|
2665
|
+
"[green]✅ Pre-commit hooks updated successfully[/green]"
|
|
2666
|
+
)
|
|
2667
|
+
if result.stdout.strip():
|
|
2668
|
+
self.console.print(f"[dim]{result.stdout}[/dim]")
|
|
2669
|
+
else:
|
|
2670
|
+
self.console.print(
|
|
2671
|
+
f"[red]❌ Failed to update pre-commit hooks: {result.stderr}[/red]"
|
|
2672
|
+
)
|
|
2673
|
+
except Exception as e:
|
|
2674
|
+
self.console.print(f"[red]❌ Error updating pre-commit hooks: {e}[/red]")
|
|
2675
|
+
|
|
2119
2676
|
|
|
2120
2677
|
class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
2121
2678
|
our_path: Path = Path(__file__).parent
|
|
@@ -2128,6 +2685,7 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2128
2685
|
code_cleaner: CodeCleaner | None = None
|
|
2129
2686
|
config_manager: ConfigManager | None = None
|
|
2130
2687
|
project_manager: ProjectManager | None = None
|
|
2688
|
+
session_tracker: SessionTracker | None = None
|
|
2131
2689
|
_file_cache: dict[str, list[Path]] = {}
|
|
2132
2690
|
_file_cache_with_mtime: dict[str, tuple[float, list[Path]]] = {}
|
|
2133
2691
|
_state_file: Path = Path(".crackerjack-state")
|
|
@@ -2255,13 +2813,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2255
2813
|
"\n\n[bold red]❌ UV sync failed. Is UV installed? Run `pipx install uv` and try again.[/bold red]\n\n"
|
|
2256
2814
|
)
|
|
2257
2815
|
|
|
2258
|
-
def _update_precommit(self, options: t.Any) -> None:
|
|
2259
|
-
if self.pkg_path.stem == "crackerjack" and options.update_precommit:
|
|
2260
|
-
update_cmd = ["uv", "run", "pre-commit", "autoupdate"]
|
|
2261
|
-
if options.ai_agent:
|
|
2262
|
-
update_cmd.extend(["-c", ".pre-commit-config-ai.yaml"])
|
|
2263
|
-
self.execute_command(update_cmd)
|
|
2264
|
-
|
|
2265
2816
|
def _clean_project(self, options: t.Any) -> None:
|
|
2266
2817
|
assert self.code_cleaner is not None
|
|
2267
2818
|
if options.clean:
|
|
@@ -2641,6 +3192,32 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2641
3192
|
)
|
|
2642
3193
|
self.execute_command(["git", "push", "origin", "main", "--no-verify"])
|
|
2643
3194
|
|
|
3195
|
+
def _update_precommit(self, options: OptionsProtocol) -> None:
|
|
3196
|
+
if options.update_precommit:
|
|
3197
|
+
self.console.print("\n" + "-" * 80)
|
|
3198
|
+
self.console.print(
|
|
3199
|
+
"[bold bright_blue]🔄 UPDATE[/bold bright_blue] [bold bright_white]Updating pre-commit hooks[/bold bright_white]"
|
|
3200
|
+
)
|
|
3201
|
+
self.console.print("-" * 80 + "\n")
|
|
3202
|
+
if self.pkg_path.stem == "crackerjack":
|
|
3203
|
+
update_cmd = ["uv", "run", "pre-commit", "autoupdate"]
|
|
3204
|
+
if getattr(options, "ai_agent", False):
|
|
3205
|
+
update_cmd.extend(["-c", ".pre-commit-config-ai.yaml"])
|
|
3206
|
+
self.execute_command(update_cmd)
|
|
3207
|
+
else:
|
|
3208
|
+
self.project_manager.update_precommit_hooks()
|
|
3209
|
+
|
|
3210
|
+
def _update_docs(self, options: OptionsProtocol) -> None:
|
|
3211
|
+
if options.update_docs or options.force_update_docs:
|
|
3212
|
+
self.console.print("\n" + "-" * 80)
|
|
3213
|
+
self.console.print(
|
|
3214
|
+
"[bold bright_blue]📋 DOCS UPDATE[/bold bright_blue] [bold bright_white]Updating documentation with quality standards[/bold bright_white]"
|
|
3215
|
+
)
|
|
3216
|
+
self.console.print("-" * 80 + "\n")
|
|
3217
|
+
self.config_manager.copy_documentation_templates(
|
|
3218
|
+
force_update=options.force_update_docs
|
|
3219
|
+
)
|
|
3220
|
+
|
|
2644
3221
|
def execute_command(
|
|
2645
3222
|
self, cmd: list[str], **kwargs: t.Any
|
|
2646
3223
|
) -> subprocess.CompletedProcess[str]:
|
|
@@ -2765,8 +3342,83 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2765
3342
|
"[bold bright_green]✅ All comprehensive quality checks passed![/bold bright_green]"
|
|
2766
3343
|
)
|
|
2767
3344
|
|
|
3345
|
+
def _run_tracked_task(
|
|
3346
|
+
self, task_id: str, task_name: str, task_func: t.Callable[[], None]
|
|
3347
|
+
) -> None:
|
|
3348
|
+
if self.session_tracker:
|
|
3349
|
+
self.session_tracker.start_task(task_id, task_name)
|
|
3350
|
+
try:
|
|
3351
|
+
task_func()
|
|
3352
|
+
if self.session_tracker:
|
|
3353
|
+
self.session_tracker.complete_task(task_id, f"{task_name} completed")
|
|
3354
|
+
except Exception as e:
|
|
3355
|
+
if self.session_tracker:
|
|
3356
|
+
self.session_tracker.fail_task(task_id, str(e))
|
|
3357
|
+
raise
|
|
3358
|
+
|
|
3359
|
+
def _run_pre_commit_task(self, options: OptionsProtocol) -> None:
|
|
3360
|
+
if not options.skip_hooks:
|
|
3361
|
+
if getattr(options, "ai_agent", False):
|
|
3362
|
+
self.project_manager.run_pre_commit_with_analysis()
|
|
3363
|
+
else:
|
|
3364
|
+
self.project_manager.run_pre_commit()
|
|
3365
|
+
else:
|
|
3366
|
+
self.console.print(
|
|
3367
|
+
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
3368
|
+
)
|
|
3369
|
+
if self.session_tracker:
|
|
3370
|
+
self.session_tracker.skip_task("pre_commit", "Skipped by user request")
|
|
3371
|
+
|
|
3372
|
+
def _initialize_session_tracking(self, options: OptionsProtocol) -> None:
|
|
3373
|
+
if options.resume_from:
|
|
3374
|
+
try:
|
|
3375
|
+
progress_file = Path(options.resume_from)
|
|
3376
|
+
self.session_tracker = SessionTracker.resume_session(
|
|
3377
|
+
console=self.console,
|
|
3378
|
+
progress_file=progress_file,
|
|
3379
|
+
)
|
|
3380
|
+
return
|
|
3381
|
+
except Exception as e:
|
|
3382
|
+
self.console.print(
|
|
3383
|
+
f"[yellow]Warning: Failed to resume from {options.resume_from}: {e}[/yellow]"
|
|
3384
|
+
)
|
|
3385
|
+
self.session_tracker = None
|
|
3386
|
+
return
|
|
3387
|
+
if options.track_progress:
|
|
3388
|
+
try:
|
|
3389
|
+
auto_tracker = SessionTracker.auto_detect_session(self.console)
|
|
3390
|
+
if auto_tracker:
|
|
3391
|
+
self.session_tracker = auto_tracker
|
|
3392
|
+
return
|
|
3393
|
+
progress_file = (
|
|
3394
|
+
Path(options.progress_file) if options.progress_file else None
|
|
3395
|
+
)
|
|
3396
|
+
try:
|
|
3397
|
+
from importlib.metadata import version
|
|
3398
|
+
|
|
3399
|
+
crackerjack_version = version("crackerjack")
|
|
3400
|
+
except (ImportError, ModuleNotFoundError):
|
|
3401
|
+
crackerjack_version = "unknown"
|
|
3402
|
+
metadata = {
|
|
3403
|
+
"working_dir": str(self.pkg_path),
|
|
3404
|
+
"python_version": self.python_version,
|
|
3405
|
+
"crackerjack_version": crackerjack_version,
|
|
3406
|
+
"cli_options": str(options),
|
|
3407
|
+
}
|
|
3408
|
+
self.session_tracker = SessionTracker.create_session(
|
|
3409
|
+
console=self.console,
|
|
3410
|
+
progress_file=progress_file,
|
|
3411
|
+
metadata=metadata,
|
|
3412
|
+
)
|
|
3413
|
+
except Exception as e:
|
|
3414
|
+
self.console.print(
|
|
3415
|
+
f"[yellow]Warning: Failed to initialize session tracking: {e}[/yellow]"
|
|
3416
|
+
)
|
|
3417
|
+
self.session_tracker = None
|
|
3418
|
+
|
|
2768
3419
|
def process(self, options: OptionsProtocol) -> None:
|
|
2769
3420
|
assert self.project_manager is not None
|
|
3421
|
+
self._initialize_session_tracking(options)
|
|
2770
3422
|
self.console.print("\n" + "-" * 80)
|
|
2771
3423
|
self.console.print(
|
|
2772
3424
|
"[bold bright_cyan]⚒️ CRACKERJACKING[/bold bright_cyan] [bold bright_white]Starting workflow execution[/bold bright_white]"
|
|
@@ -2777,25 +3429,55 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
|
|
|
2777
3429
|
options.test = True
|
|
2778
3430
|
options.publish = options.all
|
|
2779
3431
|
options.commit = True
|
|
2780
|
-
self.
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
self.
|
|
3432
|
+
self._run_tracked_task(
|
|
3433
|
+
"setup", "Initialize project structure", self._setup_package
|
|
3434
|
+
)
|
|
3435
|
+
self._run_tracked_task(
|
|
3436
|
+
"update_project",
|
|
3437
|
+
"Update project configuration",
|
|
3438
|
+
lambda: self._update_project(options),
|
|
3439
|
+
)
|
|
3440
|
+
self._run_tracked_task(
|
|
3441
|
+
"update_precommit",
|
|
3442
|
+
"Update pre-commit hooks",
|
|
3443
|
+
lambda: self._update_precommit(options),
|
|
3444
|
+
)
|
|
3445
|
+
self._run_tracked_task(
|
|
3446
|
+
"update_docs",
|
|
3447
|
+
"Update documentation templates",
|
|
3448
|
+
lambda: self._update_docs(options),
|
|
3449
|
+
)
|
|
3450
|
+
self._run_tracked_task(
|
|
3451
|
+
"clean_project", "Clean project code", lambda: self._clean_project(options)
|
|
3452
|
+
)
|
|
2784
3453
|
self.project_manager.options = options
|
|
2785
3454
|
if not options.skip_hooks:
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
self.
|
|
2790
|
-
else:
|
|
2791
|
-
self.console.print(
|
|
2792
|
-
"\n[bold bright_yellow]⏭️ Skipping pre-commit hooks...[/bold bright_yellow]\n"
|
|
3455
|
+
self._run_tracked_task(
|
|
3456
|
+
"pre_commit",
|
|
3457
|
+
"Run pre-commit hooks",
|
|
3458
|
+
lambda: self._run_pre_commit_task(options),
|
|
2793
3459
|
)
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
self.
|
|
2797
|
-
|
|
2798
|
-
|
|
3460
|
+
else:
|
|
3461
|
+
self._run_pre_commit_task(options)
|
|
3462
|
+
self._run_tracked_task(
|
|
3463
|
+
"run_tests", "Execute test suite", lambda: self._run_tests(options)
|
|
3464
|
+
)
|
|
3465
|
+
self._run_tracked_task(
|
|
3466
|
+
"quality_checks",
|
|
3467
|
+
"Run comprehensive quality checks",
|
|
3468
|
+
lambda: self._run_comprehensive_quality_checks(options),
|
|
3469
|
+
)
|
|
3470
|
+
self._run_tracked_task(
|
|
3471
|
+
"bump_version", "Bump version numbers", lambda: self._bump_version(options)
|
|
3472
|
+
)
|
|
3473
|
+
self._run_tracked_task(
|
|
3474
|
+
"commit_push",
|
|
3475
|
+
"Commit and push changes",
|
|
3476
|
+
lambda: self._commit_and_push(options),
|
|
3477
|
+
)
|
|
3478
|
+
self._run_tracked_task(
|
|
3479
|
+
"publish", "Publish project", lambda: self._publish_project(options)
|
|
3480
|
+
)
|
|
2799
3481
|
self.console.print("\n" + "-" * 80)
|
|
2800
3482
|
self.console.print(
|
|
2801
3483
|
"[bold bright_green]✨ CRACKERJACK COMPLETE[/bold bright_green] [bold bright_white]Workflow completed successfully![/bold bright_white]"
|
crackerjack/pyproject.toml
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
crackerjack/.gitignore,sha256=ELNEeIblDM1mKM-LJyj_iLcBzZJPbeJ89YXupvXuLug,498
|
|
2
2
|
crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
|
|
3
3
|
crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
|
|
4
|
-
crackerjack/.pre-commit-config-ai.yaml,sha256
|
|
4
|
+
crackerjack/.pre-commit-config-ai.yaml,sha256=IvD0WkglFacIxt-OwzMtwnjOr-i6NzdiazxpdnVH-Gg,4130
|
|
5
5
|
crackerjack/.pre-commit-config-fast.yaml,sha256=IpGHKznEfy-fhdOu-6nCWNa0-CpZ3CJf9YB-MKV8E4Y,1855
|
|
6
6
|
crackerjack/.pre-commit-config.yaml,sha256=UgPPAC7O_npBLGJkC_v_2YQUSEIOgsBOXzZjyW2hNvs,2987
|
|
7
7
|
crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
|
|
8
|
-
crackerjack/__main__.py,sha256=
|
|
9
|
-
crackerjack/crackerjack.py,sha256=
|
|
8
|
+
crackerjack/__main__.py,sha256=W6Wb7VGYaNvWOEghYY67xQoG9piPLoBQyeqDfXdN2F0,8524
|
|
9
|
+
crackerjack/crackerjack.py,sha256=6TSnhNim39DHla0QLYXPXqyZF1Qk9906tWYqSWAGet0,138512
|
|
10
10
|
crackerjack/errors.py,sha256=Wcv0rXfzV9pHOoXYrhQEjyJd4kUUBbdiY-5M9nI8pDw,4050
|
|
11
11
|
crackerjack/interactive.py,sha256=jnf3klyYFvuQ3u_iVVPshPW1LISfU1VXTOiczTWLxys,16138
|
|
12
12
|
crackerjack/py313.py,sha256=1imwWZUQwcZt09yIrnTSWr73ITTKH8yXlgIe2ESTeLA,5977
|
|
13
|
-
crackerjack/pyproject.toml,sha256=
|
|
14
|
-
crackerjack-0.
|
|
15
|
-
crackerjack-0.
|
|
16
|
-
crackerjack-0.
|
|
17
|
-
crackerjack-0.
|
|
13
|
+
crackerjack/pyproject.toml,sha256=cFx31cecC1Zx-xQKMEHiITRcvN8B__3rKYhTAasSOsI,6870
|
|
14
|
+
crackerjack-0.28.0.dist-info/METADATA,sha256=kQCT0vsAX1e7LRqnf2Gu8Xalk6vWIzBxd91JwRkLU8s,28788
|
|
15
|
+
crackerjack-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
crackerjack-0.28.0.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
|
|
17
|
+
crackerjack-0.28.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|