crackerjack 0.19.7__py3-none-any.whl → 0.20.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.
crackerjack/.gitignore CHANGED
@@ -12,3 +12,5 @@
12
12
  /*.pyc
13
13
  /scratch/
14
14
  /.zencoder/
15
+
16
+ **/.claude/settings.local.json
@@ -75,7 +75,6 @@ repos:
75
75
  rev: v2.1.0
76
76
  hooks:
77
77
  - id: refurb
78
- exclude: ^tests/
79
78
 
80
79
  # Code quality tier 3 - thorough checks
81
80
  - repo: local
crackerjack/__init__.py CHANGED
@@ -1,5 +1,42 @@
1
1
  import typing as t
2
2
 
3
3
  from .crackerjack import Crackerjack, create_crackerjack_runner
4
+ from .errors import (
5
+ CleaningError,
6
+ ConfigError,
7
+ CrackerjackError,
8
+ ErrorCode,
9
+ ExecutionError,
10
+ FileError,
11
+ GitError,
12
+ PublishError,
13
+ TestError,
14
+ check_command_result,
15
+ check_file_exists,
16
+ handle_error,
17
+ )
4
18
 
5
- __all__: t.Sequence[str] = ["create_crackerjack_runner", "Crackerjack"]
19
+ try:
20
+ from importlib.metadata import version
21
+
22
+ __version__ = version("crackerjack")
23
+ except (ImportError, ModuleNotFoundError):
24
+ __version__ = "0.19.8"
25
+
26
+ __all__: t.Sequence[str] = [
27
+ "create_crackerjack_runner",
28
+ "Crackerjack",
29
+ "__version__",
30
+ "CrackerjackError",
31
+ "ConfigError",
32
+ "ExecutionError",
33
+ "TestError",
34
+ "PublishError",
35
+ "GitError",
36
+ "FileError",
37
+ "CleaningError",
38
+ "ErrorCode",
39
+ "handle_error",
40
+ "check_file_exists",
41
+ "check_command_result",
42
+ ]
crackerjack/__main__.py CHANGED
@@ -38,6 +38,7 @@ class Options(BaseModel):
38
38
  ai_agent: bool = False
39
39
  create_pr: bool = False
40
40
  skip_hooks: bool = False
41
+ rich_ui: bool = False
41
42
 
42
43
  @classmethod
43
44
  @field_validator("publish", "bump", mode="before")
@@ -84,7 +85,7 @@ cli_options = {
84
85
  False,
85
86
  "-x",
86
87
  "--clean",
87
- help="Remove docstrings, line comments, and unnecessary whitespace.",
88
+ help="Remove docstrings, line comments, and unnecessary whitespace from source code (doesn't affect test files).",
88
89
  ),
89
90
  "test": typer.Option(False, "-t", "--test", help="Run tests."),
90
91
  "benchmark": typer.Option(
@@ -121,6 +122,11 @@ cli_options = {
121
122
  "--pr",
122
123
  help="Create a pull request to the upstream repository.",
123
124
  ),
125
+ "rich_ui": typer.Option(
126
+ False,
127
+ "--rich-ui",
128
+ help="Use the interactive Rich UI for a better experience.",
129
+ ),
124
130
  "ai_agent": typer.Option(
125
131
  False,
126
132
  "--ai-agent",
@@ -150,6 +156,7 @@ def main(
150
156
  ],
151
157
  skip_hooks: bool = cli_options["skip_hooks"],
152
158
  create_pr: bool = cli_options["create_pr"],
159
+ rich_ui: bool = cli_options["rich_ui"],
153
160
  ai_agent: bool = cli_options["ai_agent"],
154
161
  ) -> None:
155
162
  options = Options(
@@ -170,6 +177,7 @@ def main(
170
177
  all=all,
171
178
  ai_agent=ai_agent,
172
179
  create_pr=create_pr,
180
+ rich_ui=rich_ui,
173
181
  )
174
182
 
175
183
  if ai_agent:
@@ -177,8 +185,20 @@ def main(
177
185
 
178
186
  os.environ["AI_AGENT"] = "1"
179
187
 
180
- runner = create_crackerjack_runner(console=console)
181
- runner.process(options)
188
+ if rich_ui:
189
+ from crackerjack.interactive import launch_interactive_cli
190
+
191
+ try:
192
+ from importlib.metadata import version
193
+
194
+ pkg_version = version("crackerjack")
195
+ except (ImportError, ModuleNotFoundError):
196
+ pkg_version = "0.19.8"
197
+
198
+ launch_interactive_cli(pkg_version)
199
+ else:
200
+ runner = create_crackerjack_runner(console=console)
201
+ runner.process(options)
182
202
 
183
203
 
184
204
  if __name__ == "__main__":
@@ -62,22 +62,77 @@ class CodeCleaner:
62
62
  self.clean_file(file_path)
63
63
 
64
64
  def clean_file(self, file_path: Path) -> None:
65
+ from .errors import CleaningError, ErrorCode, FileError, handle_error
66
+
65
67
  try:
66
68
  if file_path.resolve() == Path(__file__).resolve():
67
69
  self.console.print(f"Skipping cleaning of {file_path} (self file).")
68
70
  return
69
71
  except Exception as e:
70
- self.console.print(f"Error comparing file paths: {e}")
72
+ error = FileError(
73
+ message="Error comparing file paths",
74
+ error_code=ErrorCode.FILE_READ_ERROR,
75
+ details=f"Failed to compare {file_path} with the current file: {e}",
76
+ recovery="This is likely a file system permission issue. Check file permissions.",
77
+ exit_code=0, # Non-fatal error
78
+ )
79
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
80
+ return
81
+
71
82
  try:
72
- code = file_path.read_text()
83
+ # Check if file exists and is readable
84
+ if not file_path.exists():
85
+ error = FileError(
86
+ message="File not found",
87
+ error_code=ErrorCode.FILE_NOT_FOUND,
88
+ details=f"The file {file_path} does not exist.",
89
+ recovery="Check the file path and ensure the file exists.",
90
+ exit_code=0, # Non-fatal error
91
+ )
92
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
93
+ return
94
+
95
+ try:
96
+ code = file_path.read_text()
97
+ except Exception as e:
98
+ error = FileError(
99
+ message="Error reading file",
100
+ error_code=ErrorCode.FILE_READ_ERROR,
101
+ details=f"Failed to read {file_path}: {e}",
102
+ recovery="Check file permissions and ensure the file is not locked by another process.",
103
+ exit_code=0, # Non-fatal error
104
+ )
105
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
106
+ return
107
+
108
+ # Process the file content
73
109
  code = self.remove_docstrings(code)
74
110
  code = self.remove_line_comments(code)
75
111
  code = self.remove_extra_whitespace(code)
76
112
  code = self.reformat_code(code)
77
- file_path.write_text(code) # type: ignore
78
- self.console.print(f"Cleaned: {file_path}")
113
+
114
+ try:
115
+ file_path.write_text(code) # type: ignore
116
+ self.console.print(f"Cleaned: {file_path}")
117
+ except Exception as e:
118
+ error = FileError(
119
+ message="Error writing file",
120
+ error_code=ErrorCode.FILE_WRITE_ERROR,
121
+ details=f"Failed to write to {file_path}: {e}",
122
+ recovery="Check file permissions and ensure the file is not locked by another process.",
123
+ exit_code=0, # Non-fatal error
124
+ )
125
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
126
+
79
127
  except Exception as e:
80
- self.console.print(f"Error cleaning {file_path}: {e}")
128
+ error = CleaningError(
129
+ message="Error cleaning file",
130
+ error_code=ErrorCode.CODE_CLEANING_ERROR,
131
+ details=f"Failed to clean {file_path}: {e}",
132
+ recovery="This could be due to syntax errors in the file. Try manually checking the file for syntax errors.",
133
+ exit_code=0, # Non-fatal error
134
+ )
135
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
81
136
 
82
137
  def remove_line_comments(self, code: str) -> str:
83
138
  new_lines = []
@@ -199,14 +254,30 @@ class CodeCleaner:
199
254
  return "\n".join(cleaned_lines)
200
255
 
201
256
  def reformat_code(self, code: str) -> str | None:
257
+ from .errors import CleaningError, ErrorCode, handle_error
258
+
202
259
  try:
203
260
  import tempfile
204
261
 
205
- with tempfile.NamedTemporaryFile(
206
- suffix=".py", mode="w+", delete=False
207
- ) as temp:
208
- temp_path = Path(temp.name)
209
- temp_path.write_text(code)
262
+ # Create a temporary file for formatting
263
+ try:
264
+ with tempfile.NamedTemporaryFile(
265
+ suffix=".py", mode="w+", delete=False
266
+ ) as temp:
267
+ temp_path = Path(temp.name)
268
+ temp_path.write_text(code)
269
+ except Exception as e:
270
+ error = CleaningError(
271
+ message="Failed to create temporary file for formatting",
272
+ error_code=ErrorCode.FORMATTING_ERROR,
273
+ details=f"Error: {e}",
274
+ recovery="Check disk space and permissions for the temp directory.",
275
+ exit_code=0, # Non-fatal
276
+ )
277
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
278
+ return code
279
+
280
+ # Run Ruff to format the code
210
281
  try:
211
282
  result = subprocess.run(
212
283
  ["ruff", "format", str(temp_path)],
@@ -214,20 +285,58 @@ class CodeCleaner:
214
285
  capture_output=True,
215
286
  text=True,
216
287
  )
288
+
217
289
  if result.returncode == 0:
218
- formatted_code = temp_path.read_text()
290
+ try:
291
+ formatted_code = temp_path.read_text()
292
+ except Exception as e:
293
+ error = CleaningError(
294
+ message="Failed to read formatted code",
295
+ error_code=ErrorCode.FORMATTING_ERROR,
296
+ details=f"Error reading temporary file after formatting: {e}",
297
+ recovery="This might be a permissions issue. Check if Ruff is installed properly.",
298
+ exit_code=0, # Non-fatal
299
+ )
300
+ handle_error(
301
+ error, self.console, verbose=True, exit_on_error=False
302
+ )
303
+ formatted_code = code
219
304
  else:
220
- self.console.print(f"Ruff formatting failed: {result.stderr}")
305
+ error = CleaningError(
306
+ message="Ruff formatting failed",
307
+ error_code=ErrorCode.FORMATTING_ERROR,
308
+ details=f"Ruff output: {result.stderr}",
309
+ recovery="The file might contain syntax errors. Check the file manually.",
310
+ exit_code=0, # Non-fatal
311
+ )
312
+ handle_error(error, self.console, exit_on_error=False)
221
313
  formatted_code = code
222
314
  except Exception as e:
223
- self.console.print(f"Error running Ruff: {e}")
315
+ error = CleaningError(
316
+ message="Error running Ruff formatter",
317
+ error_code=ErrorCode.FORMATTING_ERROR,
318
+ details=f"Error: {e}",
319
+ recovery="Ensure Ruff is installed correctly. Run 'pip install ruff' to install it.",
320
+ exit_code=0, # Non-fatal
321
+ )
322
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
224
323
  formatted_code = code
225
324
  finally:
325
+ # Clean up temporary file
226
326
  with suppress(FileNotFoundError):
227
327
  temp_path.unlink()
328
+
228
329
  return formatted_code
330
+
229
331
  except Exception as e:
230
- self.console.print(f"Error during reformatting: {e}")
332
+ error = CleaningError(
333
+ message="Unexpected error during code formatting",
334
+ error_code=ErrorCode.FORMATTING_ERROR,
335
+ details=f"Error: {e}",
336
+ recovery="This is an unexpected error. Please report this issue.",
337
+ exit_code=0, # Non-fatal
338
+ )
339
+ handle_error(error, self.console, verbose=True, exit_on_error=False)
231
340
  return code
232
341
 
233
342
 
@@ -368,18 +477,52 @@ class ProjectManager:
368
477
  dry_run: bool = False
369
478
 
370
479
  def run_interactive(self, hook: str) -> None:
480
+ from .errors import ErrorCode, ExecutionError, handle_error
481
+
371
482
  success: bool = False
372
- while not success:
373
- fail = self.execute_command(
483
+ attempts = 0
484
+ max_attempts = 3
485
+
486
+ while not success and attempts < max_attempts:
487
+ attempts += 1
488
+ result = self.execute_command(
374
489
  ["pre-commit", "run", hook.lower(), "--all-files"]
375
490
  )
376
- if fail.returncode > 0:
377
- retry = input(f"\n\n{hook.title()} failed. Retry? (y/N): ")
491
+
492
+ if result.returncode > 0:
493
+ self.console.print(
494
+ f"\n\n[yellow]Hook '{hook}' failed (attempt {attempts}/{max_attempts})[/yellow]"
495
+ )
496
+
497
+ # Give more detailed information about the failure
498
+ if result.stderr:
499
+ self.console.print(f"[red]Error details:[/red]\n{result.stderr}")
500
+
501
+ retry = input(f"Retry running {hook.title()}? (y/N): ")
378
502
  self.console.print()
379
- if retry.strip().lower() == "y":
380
- continue
381
- raise SystemExit(1)
382
- success = True
503
+
504
+ if retry.strip().lower() != "y":
505
+ error = ExecutionError(
506
+ message=f"Interactive hook '{hook}' failed",
507
+ error_code=ErrorCode.PRE_COMMIT_ERROR,
508
+ details=f"Hook execution output:\n{result.stderr or result.stdout}",
509
+ recovery=f"Try running the hook manually: pre-commit run {hook.lower()} --all-files",
510
+ exit_code=1,
511
+ )
512
+ handle_error(error=error, console=self.console)
513
+ else:
514
+ self.console.print(f"[green]✅ Hook '{hook}' succeeded![/green]")
515
+ success = True
516
+
517
+ if not success:
518
+ error = ExecutionError(
519
+ message=f"Interactive hook '{hook}' failed after {max_attempts} attempts",
520
+ error_code=ErrorCode.PRE_COMMIT_ERROR,
521
+ details="The hook continued to fail after multiple attempts.",
522
+ recovery=f"Fix the issues manually and run: pre-commit run {hook.lower()} --all-files",
523
+ exit_code=1,
524
+ )
525
+ handle_error(error=error, console=self.console)
383
526
 
384
527
  def update_pkg_configs(self) -> None:
385
528
  self.config_manager.copy_configs()
@@ -399,13 +542,25 @@ class ProjectManager:
399
542
  self.config_manager.update_pyproject_configs()
400
543
 
401
544
  def run_pre_commit(self) -> None:
545
+ from .errors import ErrorCode, ExecutionError, handle_error
546
+
402
547
  self.console.print("\nRunning pre-commit hooks...\n")
403
548
  check_all = self.execute_command(["pre-commit", "run", "--all-files"])
549
+
404
550
  if check_all.returncode > 0:
551
+ # First retry
552
+ self.console.print("\nSome pre-commit hooks failed. Retrying once...\n")
405
553
  check_all = self.execute_command(["pre-commit", "run", "--all-files"])
554
+
406
555
  if check_all.returncode > 0:
407
- self.console.print("\n\nPre-commit failed. Please fix errors.\n")
408
- raise SystemExit(1)
556
+ error = ExecutionError(
557
+ message="Pre-commit hooks failed",
558
+ error_code=ErrorCode.PRE_COMMIT_ERROR,
559
+ details="Pre-commit hooks failed even after a retry. Check the output above for specific hook failures.",
560
+ recovery="Review the error messages above. Manually fix the issues or run specific hooks interactively with 'pre-commit run <hook-id>'.",
561
+ exit_code=1,
562
+ )
563
+ handle_error(error=error, console=self.console, verbose=True)
409
564
 
410
565
  def execute_command(
411
566
  self, cmd: list[str], **kwargs: t.Any
@@ -463,6 +618,8 @@ class Crackerjack:
463
618
  self.project_manager.pkg_dir = self.pkg_dir
464
619
 
465
620
  def _update_project(self, options: OptionsProtocol) -> None:
621
+ from .errors import ErrorCode, ExecutionError, handle_error
622
+
466
623
  if not options.no_config_updates:
467
624
  self.project_manager.update_pkg_configs()
468
625
  result: CompletedProcess[str] = self.execute_command(
@@ -471,8 +628,21 @@ class Crackerjack:
471
628
  if result.returncode == 0:
472
629
  self.console.print("PDM installed: ✅\n")
473
630
  else:
474
- self.console.print(
475
- "\n\n❌ PDM installation failed. Is PDM is installed? Run `pipx install pdm` and try again.\n\n"
631
+ error = ExecutionError(
632
+ message="PDM installation failed",
633
+ error_code=ErrorCode.PDM_INSTALL_ERROR,
634
+ details=f"Command output:\n{result.stderr}",
635
+ recovery="Ensure PDM is installed. Run `pipx install pdm` and try again. Check for network issues or package conflicts.",
636
+ exit_code=1,
637
+ )
638
+
639
+ # Don't exit immediately - this isn't always fatal
640
+ handle_error(
641
+ error=error,
642
+ console=self.console,
643
+ verbose=options.verbose,
644
+ ai_agent=options.ai_agent,
645
+ exit_on_error=False,
476
646
  )
477
647
 
478
648
  def _update_precommit(self, options: OptionsProtocol) -> None:
@@ -488,10 +658,8 @@ class Crackerjack:
488
658
  if options.clean:
489
659
  if self.pkg_dir:
490
660
  self.code_cleaner.clean_files(self.pkg_dir)
491
- tests_dir = self.pkg_path / "tests"
492
- if tests_dir.exists() and tests_dir.is_dir():
493
- self.console.print("\nCleaning tests directory...\n")
494
- self.code_cleaner.clean_files(tests_dir)
661
+ # Skip cleaning test files as they may contain test data in docstrings and comments
662
+ # that are necessary for the tests to function properly
495
663
 
496
664
  def _prepare_pytest_command(self, options: OptionsProtocol) -> list[str]:
497
665
  """Prepare pytest command with appropriate options.
@@ -557,6 +725,8 @@ class Crackerjack:
557
725
  def _run_pytest_process(
558
726
  self, test_command: list[str]
559
727
  ) -> subprocess.CompletedProcess[str]:
728
+ from .errors import ErrorCode, ExecutionError, handle_error
729
+
560
730
  try:
561
731
  process = subprocess.Popen(
562
732
  test_command,
@@ -572,6 +742,13 @@ class Crackerjack:
572
742
  stderr_data = []
573
743
  while process.poll() is None:
574
744
  if time.time() - start_time > timeout:
745
+ error = ExecutionError(
746
+ message="Test execution timed out after 5 minutes.",
747
+ error_code=ErrorCode.COMMAND_TIMEOUT,
748
+ details=f"Command: {' '.join(test_command)}\nTimeout: {timeout} seconds",
749
+ recovery="Check for infinite loops or deadlocks in your tests. Consider increasing the timeout or optimizing your tests.",
750
+ )
751
+
575
752
  self.console.print(
576
753
  "[red]Test execution timed out after 5 minutes. Terminating...[/red]"
577
754
  )
@@ -580,7 +757,11 @@ class Crackerjack:
580
757
  process.wait(timeout=5)
581
758
  except subprocess.TimeoutExpired:
582
759
  process.kill()
760
+ stderr_data.append(
761
+ "Process had to be forcefully terminated after timeout."
762
+ )
583
763
  break
764
+
584
765
  if process.stdout:
585
766
  line = process.stdout.readline()
586
767
  if line:
@@ -592,6 +773,7 @@ class Crackerjack:
592
773
  stderr_data.append(line)
593
774
  self.console.print(f"[red]{line}[/red]", end="")
594
775
  time.sleep(0.1)
776
+
595
777
  if process.stdout:
596
778
  for line in process.stdout:
597
779
  stdout_data.append(line)
@@ -600,23 +782,42 @@ class Crackerjack:
600
782
  for line in process.stderr:
601
783
  stderr_data.append(line)
602
784
  self.console.print(f"[red]{line}[/red]", end="")
785
+
603
786
  returncode = process.returncode or 0
604
787
  stdout = "".join(stdout_data)
605
788
  stderr = "".join(stderr_data)
789
+
606
790
  return subprocess.CompletedProcess(
607
791
  args=test_command, returncode=returncode, stdout=stdout, stderr=stderr
608
792
  )
609
793
 
610
794
  except Exception as e:
611
- self.console.print(f"[red]Error running tests: {e}[/red]")
795
+ error = ExecutionError(
796
+ message=f"Error running tests: {e}",
797
+ error_code=ErrorCode.TEST_EXECUTION_ERROR,
798
+ details=f"Command: {' '.join(test_command)}\nError: {e}",
799
+ recovery="Check if pytest is installed and that your test files are properly formatted.",
800
+ exit_code=1,
801
+ )
802
+
803
+ # Don't exit here, let the caller handle it
804
+ handle_error(
805
+ error=error, console=self.console, verbose=True, exit_on_error=False
806
+ )
807
+
612
808
  return subprocess.CompletedProcess(test_command, 1, "", str(e))
613
809
 
614
810
  def _report_test_results(
615
811
  self, result: subprocess.CompletedProcess[str], ai_agent: str
616
812
  ) -> None:
813
+ from .errors import ErrorCode, TestError, handle_error
814
+
617
815
  if result.returncode > 0:
816
+ error_details = None
618
817
  if result.stderr:
619
818
  self.console.print(result.stderr)
819
+ error_details = result.stderr
820
+
620
821
  if ai_agent:
621
822
  self.console.print(
622
823
  '[json]{"status": "failed", "action": "tests", "returncode": '
@@ -624,8 +825,19 @@ class Crackerjack:
624
825
  + "}[/json]"
625
826
  )
626
827
  else:
627
- self.console.print("\n\n❌ Tests failed. Please fix errors.\n")
628
- raise SystemExit(1)
828
+ # Use the structured error handler
829
+ error = TestError(
830
+ message="Tests failed. Please fix the errors.",
831
+ error_code=ErrorCode.TEST_FAILURE,
832
+ details=error_details,
833
+ recovery="Review the test output above for specific failures. Fix the issues in your code and run tests again.",
834
+ exit_code=1,
835
+ )
836
+ handle_error(
837
+ error=error,
838
+ console=self.console,
839
+ ai_agent=(ai_agent != ""),
840
+ )
629
841
 
630
842
  if ai_agent:
631
843
  self.console.print('[json]{"status": "success", "action": "tests"}[/json]')
@@ -653,25 +865,68 @@ class Crackerjack:
653
865
  break
654
866
 
655
867
  def _publish_project(self, options: OptionsProtocol) -> None:
868
+ from .errors import ErrorCode, PublishError, handle_error
869
+
656
870
  if options.publish:
657
871
  if platform.system() == "Darwin":
658
872
  authorize = self.execute_command(
659
873
  ["pdm", "self", "add", "keyring"], capture_output=True, text=True
660
874
  )
661
875
  if authorize.returncode > 0:
662
- self.console.print(
663
- "\n\nAuthorization failed. Please add your keyring credentials to PDM. Run `pdm self add keyring` and try again.\n\n"
876
+ error = PublishError(
877
+ message="Authentication setup failed",
878
+ error_code=ErrorCode.AUTHENTICATION_ERROR,
879
+ details=f"Failed to add keyring support to PDM.\nCommand output:\n{authorize.stderr}",
880
+ recovery="Please manually add your keyring credentials to PDM. Run `pdm self add keyring` and try again.",
881
+ exit_code=1,
882
+ )
883
+ handle_error(
884
+ error=error,
885
+ console=self.console,
886
+ verbose=options.verbose,
887
+ ai_agent=options.ai_agent,
664
888
  )
665
- raise SystemExit(1)
889
+
666
890
  build = self.execute_command(
667
891
  ["pdm", "build"], capture_output=True, text=True
668
892
  )
669
893
  self.console.print(build.stdout)
894
+
670
895
  if build.returncode > 0:
671
- self.console.print(build.stderr)
672
- self.console.print("\n\nBuild failed. Please fix errors.\n")
673
- raise SystemExit(1)
674
- self.execute_command(["pdm", "publish", "--no-build"])
896
+ error = PublishError(
897
+ message="Package build failed",
898
+ error_code=ErrorCode.BUILD_ERROR,
899
+ details=f"Command output:\n{build.stderr}",
900
+ recovery="Review the error message above for details. Common issues include missing dependencies, invalid project structure, or incorrect metadata in pyproject.toml.",
901
+ exit_code=1,
902
+ )
903
+ handle_error(
904
+ error=error,
905
+ console=self.console,
906
+ verbose=options.verbose,
907
+ ai_agent=options.ai_agent,
908
+ )
909
+
910
+ publish_result = self.execute_command(
911
+ ["pdm", "publish", "--no-build"], capture_output=True, text=True
912
+ )
913
+
914
+ if publish_result.returncode > 0:
915
+ error = PublishError(
916
+ message="Package publication failed",
917
+ error_code=ErrorCode.PUBLISH_ERROR,
918
+ details=f"Command output:\n{publish_result.stderr}",
919
+ recovery="Ensure you have the correct PyPI credentials configured. Check your internet connection and that the package name is available on PyPI.",
920
+ exit_code=1,
921
+ )
922
+ handle_error(
923
+ error=error,
924
+ console=self.console,
925
+ verbose=options.verbose,
926
+ ai_agent=options.ai_agent,
927
+ )
928
+ else:
929
+ self.console.print("[green]✅ Package published successfully![/green]")
675
930
 
676
931
  def _commit_and_push(self, options: OptionsProtocol) -> None:
677
932
  if options.commit: