crackerjack 0.27.9__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.

@@ -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
@@ -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._setup_package()
2781
- self._update_project(options)
2782
- self._update_precommit(options)
2783
- self._clean_project(options)
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
- if getattr(options, "ai_agent", False):
2787
- self.project_manager.run_pre_commit_with_analysis()
2788
- else:
2789
- self.project_manager.run_pre_commit()
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
- self._run_tests(options)
2795
- self._run_comprehensive_quality_checks(options)
2796
- self._bump_version(options)
2797
- self._commit_and_push(options)
2798
- self._publish_project(options)
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]"
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.27.8"
7
+ version = "0.27.9"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crackerjack
3
- Version: 0.27.9
3
+ Version: 0.28.0
4
4
  Summary: Crackerjack: code quality toolkit
5
5
  Project-URL: documentation, https://github.com/lesleslie/crackerjack
6
6
  Project-URL: homepage, https://github.com/lesleslie/crackerjack
@@ -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=-8WIT-6l6crGnQBlX-z3G6-3mKUsBWsURRyeti1ySmI,4267
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=p7S0GftrU9BHtyqT8q931UtEBoz3n8vPcT1R0OJnS1A,7073
9
- crackerjack/crackerjack.py,sha256=Qer5_ZDTaU2h0RpFPq9VPLCqxOQJgJj7dmlSX3f6xhg,112787
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=mgcLhtKAX9i5UdnxRxHU2GBjbacTgmeaIZlcPLewAcI,6870
14
- crackerjack-0.27.9.dist-info/METADATA,sha256=8sjSlZKj6OHNLTSGIKHBTzWwWwLkAKSZpSOifsuCbJA,28788
15
- crackerjack-0.27.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- crackerjack-0.27.9.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
17
- crackerjack-0.27.9.dist-info/RECORD,,
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,,