hatch-xclam 0.7.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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. tests/test_system_installer.py +733 -0
@@ -0,0 +1,588 @@
1
+ """Installer for system package dependencies using apt.
2
+
3
+ This module implements installation logic for system packages using apt via subprocess,
4
+ with support for Ubuntu/Debian platforms, version constraints, and comprehensive error handling.
5
+ """
6
+
7
+ import platform
8
+ import subprocess
9
+ import logging
10
+ import re
11
+ import shutil
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Dict, Any, Optional, Callable, List
15
+ from packaging.specifiers import SpecifierSet
16
+
17
+ from .installer_base import DependencyInstaller, InstallationError
18
+ from .installation_context import InstallationContext, InstallationResult, InstallationStatus
19
+
20
+
21
+ class SystemInstaller(DependencyInstaller):
22
+ """Installer for system package dependencies using apt.
23
+
24
+ Handles installation of system packages using apt package manager via subprocess.
25
+ Supports Ubuntu/Debian platforms with platform detection and version constraint handling.
26
+ User consent is managed at the orchestrator level - this installer assumes permission
27
+ has been granted.
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize the SystemInstaller."""
32
+ self.logger = logging.getLogger("hatch.installers.system_installer")
33
+ self.logger.setLevel(logging.INFO)
34
+
35
+ @property
36
+ def installer_type(self) -> str:
37
+ """Get the type identifier for this installer.
38
+
39
+ Returns:
40
+ str: Unique identifier for the installer type ("system").
41
+ """
42
+ return "system"
43
+
44
+ @property
45
+ def supported_schemes(self) -> List[str]:
46
+ """Get the URI schemes this installer can handle.
47
+
48
+ Returns:
49
+ List[str]: List of URI schemes (["apt"] for apt package manager).
50
+ """
51
+ return ["apt"]
52
+
53
+ def can_install(self, dependency: Dict[str, Any]) -> bool:
54
+ """Check if this installer can handle the given dependency.
55
+
56
+ Args:
57
+ dependency (Dict[str, Any]): Dependency object.
58
+
59
+ Returns:
60
+ bool: True if this installer can handle the dependency, False otherwise.
61
+ """
62
+ if dependency.get("type") != self.installer_type:
63
+ return False
64
+
65
+ # Check platform compatibility
66
+ if not self._is_platform_supported():
67
+ return False
68
+
69
+ # Check if apt is available
70
+ return self._is_apt_available()
71
+
72
+ def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
73
+ """Validate that a dependency object has required fields for system packages.
74
+
75
+ Args:
76
+ dependency (Dict[str, Any]): Dependency object to validate.
77
+
78
+ Returns:
79
+ bool: True if dependency is valid, False otherwise.
80
+ """
81
+ # Required fields per schema
82
+ required_fields = ["name", "version_constraint"]
83
+ if not all(field in dependency for field in required_fields):
84
+ self.logger.error(f"Missing required fields. Expected: {required_fields}, got: {list(dependency.keys())}")
85
+ return False
86
+
87
+ # Validate package manager
88
+ package_manager = dependency.get("package_manager", "apt")
89
+ if package_manager != "apt":
90
+ self.logger.error(f"Unsupported package manager: {package_manager}. Only 'apt' is supported.")
91
+ return False
92
+
93
+ # Validate version constraint format
94
+ version_constraint = dependency.get("version_constraint", "")
95
+ if not self._validate_version_constraint(version_constraint):
96
+ self.logger.error(f"Invalid version constraint format: {version_constraint}")
97
+ return False
98
+
99
+ return True
100
+
101
+ def install(self, dependency: Dict[str, Any], context: InstallationContext,
102
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
103
+ """Install a system dependency using apt.
104
+
105
+ Args:
106
+ dependency (Dict[str, Any]): Dependency object containing:
107
+ - name (str): Name of the system package
108
+ - version_constraint (str): Version constraint
109
+ - package_manager (str): Must be "apt"
110
+ context (InstallationContext): Installation context with environment info
111
+ progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
112
+
113
+ Returns:
114
+ InstallationResult: Result of the installation operation.
115
+
116
+ Raises:
117
+ InstallationError: If installation fails for any reason.
118
+ """
119
+ if not self.validate_dependency(dependency):
120
+ raise InstallationError(
121
+ f"Invalid dependency: {dependency}",
122
+ dependency_name=dependency.get("name"),
123
+ error_code="INVALID_DEPENDENCY"
124
+ )
125
+
126
+ package_name = dependency["name"]
127
+ version_constraint = dependency["version_constraint"]
128
+
129
+ if progress_callback:
130
+ progress_callback(f"Installing {package_name}", 0.0, "Starting installation")
131
+
132
+ self.logger.info(f"Installing system package: {package_name} with constraint: {version_constraint}")
133
+
134
+ try:
135
+ # Handle dry-run/simulation mode
136
+ if context.simulation_mode:
137
+ return self._simulate_installation(dependency, context, progress_callback)
138
+
139
+ # Run apt-get update first
140
+ update_cmd = ["sudo", "apt-get", "update"]
141
+ update_returncode = self._run_apt_subprocess(update_cmd)
142
+ if update_returncode != 0:
143
+ raise InstallationError(
144
+ f"apt-get update failed (see logs for details).",
145
+ dependency_name=package_name,
146
+ error_code="APT_UPDATE_FAILED",
147
+ cause=None
148
+ )
149
+
150
+ # Build and execute apt install command
151
+ cmd = self._build_apt_command(dependency, context)
152
+
153
+ if progress_callback:
154
+ progress_callback(f"Installing {package_name}", 25.0, "Executing apt command")
155
+
156
+ returncode = self._run_apt_subprocess(cmd)
157
+ self.logger.debug(f"apt command: {cmd}\nreturn code: {returncode}")
158
+
159
+ if returncode != 0:
160
+ raise InstallationError(
161
+ f"Installation failed for {package_name} (see logs for details).",
162
+ dependency_name=package_name,
163
+ error_code="APT_INSTALL_FAILED",
164
+ cause=None
165
+ )
166
+
167
+ if progress_callback:
168
+ progress_callback(f"Installing {package_name}", 75.0, "Verifying installation")
169
+
170
+ # Verify installation
171
+ installed_version = self._verify_installation(package_name)
172
+
173
+ if progress_callback:
174
+ progress_callback(f"Installing {package_name}", 100.0, "Installation complete")
175
+
176
+ return InstallationResult(
177
+ dependency_name=package_name,
178
+ status=InstallationStatus.COMPLETED,
179
+ installed_version=installed_version,
180
+ metadata={
181
+ "package_manager": "apt",
182
+ "command_executed": " ".join(cmd),
183
+ "platform": platform.platform(),
184
+ "automated": context.get_config("automated", False),
185
+ }
186
+ )
187
+
188
+ except InstallationError as e:
189
+ self.logger.error(f"Installation error for {package_name}: {str(e)}")
190
+ raise e
191
+
192
+ except Exception as e:
193
+ self.logger.error(f"Unexpected error installing {package_name}: {str(e)}")
194
+ raise InstallationError(
195
+ f"Unexpected error installing {package_name}: {str(e)}",
196
+ dependency_name=package_name,
197
+ error_code="UNEXPECTED_ERROR",
198
+ cause=e
199
+ )
200
+
201
+ def uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
202
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
203
+ """Uninstall a system dependency using apt.
204
+
205
+ Args:
206
+ dependency (Dict[str, Any]): Dependency object to uninstall.
207
+ context (InstallationContext): Installation context.
208
+ progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
209
+
210
+ Returns:
211
+ InstallationResult: Result of the uninstall operation.
212
+
213
+ Raises:
214
+ InstallationError: If uninstall fails for any reason.
215
+ """
216
+ package_name = dependency["name"]
217
+
218
+ if progress_callback:
219
+ progress_callback(f"Uninstalling {package_name}", 0.0, "Starting uninstall")
220
+
221
+ self.logger.info(f"Uninstalling system package: {package_name}")
222
+
223
+ try:
224
+ # Handle dry-run/simulation mode
225
+ if context.simulation_mode:
226
+ return self._simulate_uninstall(dependency, context, progress_callback)
227
+
228
+ # Build apt remove command
229
+ cmd = ["sudo", "apt", "remove", package_name]
230
+
231
+ # Add automation flag if configured
232
+ if context.get_config("automated", False):
233
+ cmd.append("-y")
234
+
235
+ if progress_callback:
236
+ progress_callback(f"Uninstalling {package_name}", 50.0, "Executing apt remove")
237
+
238
+ # Execute command
239
+ returncode = self._run_apt_subprocess(cmd)
240
+
241
+ if returncode != 0:
242
+ raise InstallationError(
243
+ f"Uninstallation failed for {package_name} (see logs for details).",
244
+ dependency_name=package_name,
245
+ error_code="APT_UNINSTALL_FAILED",
246
+ cause=None
247
+ )
248
+
249
+ if progress_callback:
250
+ progress_callback(f"Uninstalling {package_name}", 100.0, "Uninstall complete")
251
+
252
+ return InstallationResult(
253
+ dependency_name=package_name,
254
+ status=InstallationStatus.COMPLETED,
255
+ metadata={
256
+ "operation": "uninstall",
257
+ "package_manager": "apt",
258
+ "command_executed": " ".join(cmd),
259
+ "automated": context.get_config("automated", False),
260
+ }
261
+ )
262
+ except InstallationError as e:
263
+ self.logger.error(f"Uninstallation error for {package_name}: {str(e)}")
264
+ raise e
265
+
266
+ except Exception as e:
267
+ self.logger.error(f"Unexpected error uninstalling {package_name}: {str(e)}")
268
+ raise InstallationError(
269
+ f"Unexpected error uninstalling {package_name}: {str(e)}",
270
+ dependency_name=package_name,
271
+ error_code="UNEXPECTED_ERROR",
272
+ cause=e
273
+ )
274
+
275
+ def _is_platform_supported(self) -> bool:
276
+ """Check if the current platform supports apt package manager.
277
+
278
+ Returns:
279
+ bool: True if platform is Ubuntu/Debian-based, False otherwise.
280
+ """
281
+ try:
282
+ # Check if we're on a Debian-based system
283
+ if Path("/etc/debian_version").exists():
284
+ return True
285
+
286
+ # Check platform string
287
+ system = platform.system().lower()
288
+ if system == "linux":
289
+ # Additional check for Ubuntu
290
+ try:
291
+ with open("/etc/os-release", "r") as f:
292
+ content = f.read().lower()
293
+ return "ubuntu" in content or "debian" in content
294
+
295
+ except FileNotFoundError:
296
+ pass
297
+
298
+ return False
299
+
300
+ except Exception:
301
+ return False
302
+
303
+ def _is_apt_available(self) -> bool:
304
+ """Check if apt command is available on the system.
305
+
306
+ Returns:
307
+ bool: True if apt is available, False otherwise.
308
+ """
309
+ return shutil.which("apt") is not None
310
+
311
+ def _validate_version_constraint(self, version_constraint: str) -> bool:
312
+ """Validate version constraint format.
313
+
314
+ Args:
315
+ version_constraint (str): Version constraint to validate.
316
+
317
+ Returns:
318
+ bool: True if format is valid, False otherwise.
319
+ """
320
+ try:
321
+ if not version_constraint.strip():
322
+ return True
323
+
324
+ SpecifierSet(version_constraint)
325
+
326
+ return True
327
+
328
+ except Exception:
329
+ self.logger.error(f"Invalid version constraint format: {version_constraint}")
330
+ return False
331
+
332
+ def _build_apt_command(self, dependency: Dict[str, Any], context: InstallationContext) -> List[str]:
333
+ """Build the apt install command for the dependency.
334
+
335
+ Args:
336
+ dependency (Dict[str, Any]): Dependency object.
337
+ context (InstallationContext): Installation context.
338
+
339
+ Returns:
340
+ List[str]: Apt command as list of arguments.
341
+ """
342
+ package_name = dependency["name"]
343
+ version_constraint = dependency["version_constraint"]
344
+
345
+ # Start with base command
346
+ command = ["sudo", "apt", "install"]
347
+
348
+ # Add automation flag if configured
349
+ if context.get_config("automated", False):
350
+ command.append("-y")
351
+
352
+ # Handle version constraints
353
+ # apt doesn't support complex version constraints directly,
354
+ # but we can specify exact versions for == constraints
355
+ if version_constraint.startswith("=="):
356
+ # Extract version from constraint like "== 1.2.3"
357
+ version = version_constraint.replace("==", "").strip()
358
+ package_spec = f"{package_name}={version}"
359
+ else:
360
+ # For other constraints (>=, <=, !=), install latest and let apt handle it
361
+ package_spec = package_name
362
+ self.logger.warning(f"Version constraint {version_constraint} simplified to latest version for {package_name}")
363
+
364
+ command.append(package_spec)
365
+ return command
366
+
367
+ def _run_apt_subprocess(self, cmd: List[str]) -> int:
368
+ """Run an apt subprocess and return the return code.
369
+
370
+ Args:
371
+ cmd (List[str]): The apt command to execute as a list.
372
+
373
+ Returns:
374
+ int: The return code of the process.
375
+
376
+ Raises:
377
+ subprocess.TimeoutExpired: If the process times out.
378
+ InstallationError: For unexpected errors.
379
+ """
380
+ env = os.environ.copy()
381
+ try:
382
+
383
+ process = subprocess.Popen(
384
+ cmd,
385
+ text=True,
386
+ universal_newlines=True
387
+ )
388
+
389
+ process.communicate() # Set a timeout for the command
390
+ process.wait() # Ensure cleanup
391
+ return process.returncode
392
+
393
+ except subprocess.TimeoutExpired:
394
+ process.kill()
395
+ process.wait() # Ensure cleanup
396
+ raise InstallationError("Apt subprocess timed out", error_code="TIMEOUT", cause=None)
397
+
398
+ except Exception as e:
399
+ raise InstallationError(
400
+ f"Unexpected error running apt command: {e}",
401
+ error_code="APT_SUBPROCESS_ERROR",
402
+ cause=e
403
+ )
404
+
405
+ def _verify_installation(self, package_name: str) -> Optional[str]:
406
+ """Verify that a package was installed and get its version.
407
+
408
+ Args:
409
+ package_name (str): Name of package to verify.
410
+
411
+ Returns:
412
+ Optional[str]: Installed version if found, None otherwise.
413
+ """
414
+ try:
415
+ result = subprocess.run(
416
+ ["apt-cache", "policy", package_name],
417
+ text=True,
418
+ capture_output=True,
419
+ check=False
420
+ )
421
+ if result.returncode == 0:
422
+ for line in result.stdout.splitlines():
423
+ if "***" in line:
424
+ parts = line.split()
425
+ if len(parts) > 1:
426
+ version = parts[1]
427
+ if version and version != "(none)":
428
+ return version
429
+ return None
430
+ except Exception:
431
+ return None
432
+
433
+ def _parse_apt_error(self, error: InstallationError) -> str:
434
+ """Parse apt error output to provide actionable error messages.
435
+
436
+ Args:
437
+ error (InstallationError): The installation error.
438
+
439
+ Returns:
440
+ str: Human-readable error message with suggestions.
441
+ """
442
+ error_output = error.message
443
+
444
+ # Common apt error patterns and suggestions
445
+ if "permission denied" in error_output.lower():
446
+ return "Permission denied. Try running with sudo or check user permissions."
447
+ elif "could not get lock" in error_output.lower():
448
+ return "Another package manager is running. Wait for it to finish and try again."
449
+ elif "unable to locate package" in error_output.lower():
450
+ return "Package not found. Check package name and update package lists with 'apt update'."
451
+ elif "network" in error_output.lower() or "connection" in error_output.lower():
452
+ return "Network connectivity issue. Check internet connection and repository availability."
453
+ elif "space" in error_output.lower():
454
+ return "Insufficient disk space. Free up space and try again."
455
+ else:
456
+ return f"Apt command failed: {error_output}"
457
+
458
+ def _simulate_installation(self, dependency: Dict[str, Any], context: InstallationContext,
459
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
460
+ """Simulate installation without making actual changes.
461
+
462
+ Args:
463
+ dependency (Dict[str, Any]): Dependency object.
464
+ context (InstallationContext): Installation context.
465
+ progress_callback (Callable[[str, float, str], None], optional): Progress callback.
466
+
467
+ Returns:
468
+ InstallationResult: Simulated result.
469
+ """
470
+ package_name = dependency["name"]
471
+
472
+ if progress_callback:
473
+ progress_callback(f"Simulating {package_name}", 0.5, "Running dry-run")
474
+
475
+ try:
476
+ # Use apt's dry-run functionality - need to use apt-get with --dry-run
477
+ cmd = ["apt-get", "install", "--dry-run", dependency["name"]]
478
+
479
+ # Add automation flag if configured
480
+ if context.get_config("automated", False):
481
+ cmd.append("-y")
482
+
483
+ returncode = self._run_apt_subprocess(cmd)
484
+
485
+ if returncode != 0:
486
+ raise InstallationError(
487
+ f"Simulation failed for {package_name} (see logs for details).",
488
+ dependency_name=package_name,
489
+ error_code="APT_SIMULATION_FAILED",
490
+ cause=None
491
+ )
492
+
493
+ if progress_callback:
494
+ progress_callback(f"Simulating {package_name}", 1.0, "Simulation complete")
495
+
496
+ return InstallationResult(
497
+ dependency_name=package_name,
498
+ status=InstallationStatus.COMPLETED,
499
+ metadata={
500
+ "simulation": True,
501
+ "command_simulated": " ".join(cmd),
502
+ "automated": context.get_config("automated", False),
503
+ "package_manager": "apt",
504
+ }
505
+ )
506
+
507
+ except InstallationError as e:
508
+ self.logger.error(f"Error during installation simulation for {package_name}: {e.message}")
509
+ raise e
510
+
511
+ except Exception as e:
512
+ return InstallationResult(
513
+ dependency_name=package_name,
514
+ status=InstallationStatus.FAILED,
515
+ error_message=f"Simulation failed: {e}",
516
+ metadata={
517
+ "simulation": True,
518
+ "simulation_error": e,
519
+ "command_simulated": " ".join(cmd),
520
+ "automated": context.get_config("automated", False)
521
+ }
522
+ )
523
+
524
+ def _simulate_uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
525
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
526
+ """Simulate uninstall without making actual changes.
527
+
528
+ Args:
529
+ dependency (Dict[str, Any]): Dependency object.
530
+ context (InstallationContext): Installation context.
531
+ progress_callback (Callable[[str, float, str], None], optional): Progress callback.
532
+
533
+ Returns:
534
+ InstallationResult: Simulated result.
535
+ """
536
+ package_name = dependency["name"]
537
+
538
+ if progress_callback:
539
+ progress_callback(f"Simulating uninstall {package_name}", 0.5, "Running dry-run")
540
+
541
+ try:
542
+ # Use apt's dry-run functionality for remove - use apt-get with --dry-run
543
+ cmd = ["apt-get", "remove", "--dry-run", dependency["name"]]
544
+ returncode = self._run_apt_subprocess(cmd)
545
+
546
+ if returncode != 0:
547
+ raise InstallationError(
548
+ f"Uninstall simulation failed for {package_name} (see logs for details).",
549
+ dependency_name=package_name,
550
+ error_code="APT_UNINSTALL_SIMULATION_FAILED",
551
+ cause=None
552
+ )
553
+
554
+ if progress_callback:
555
+ progress_callback(f"Simulating uninstall {package_name}", 1.0, "Simulation complete")
556
+
557
+ return InstallationResult(
558
+ dependency_name=package_name,
559
+ status=InstallationStatus.COMPLETED,
560
+ metadata={
561
+ "operation": "uninstall",
562
+ "simulation": True,
563
+ "command_simulated": " ".join(cmd),
564
+ "automated": context.get_config("automated", False)
565
+ }
566
+ )
567
+
568
+ except InstallationError as e:
569
+ self.logger.error(f"Uninstall simulation error for {package_name}: {str(e)}")
570
+ raise e
571
+
572
+ except Exception as e:
573
+ return InstallationResult(
574
+ dependency_name=package_name,
575
+ status=InstallationStatus.FAILED,
576
+ error_message=f"Uninstall simulation failed: {str(e)}",
577
+ metadata={
578
+ "operation": "uninstall",
579
+ "simulation": True,
580
+ "simulation_error": str(e),
581
+ "command_simulated": " ".join(cmd),
582
+ "automated": context.get_config("automated", False)
583
+ }
584
+ )
585
+
586
+ # Register this installer with the global registry
587
+ from .registry import installer_registry
588
+ installer_registry.register_installer("system", SystemInstaller)
@@ -0,0 +1,38 @@
1
+ """MCP (Model Context Protocol) support for Hatch.
2
+
3
+ This module provides MCP host configuration management functionality,
4
+ including backup and restore capabilities for MCP server configurations,
5
+ decorator-based strategy registration, and consolidated Pydantic models.
6
+ """
7
+
8
+ from .backup import MCPHostConfigBackupManager
9
+ from .models import (
10
+ MCPHostType, MCPServerConfig, HostConfiguration, EnvironmentData,
11
+ PackageHostConfiguration, EnvironmentPackageEntry, ConfigurationResult, SyncResult,
12
+ # Host-specific configuration models
13
+ MCPServerConfigBase, MCPServerConfigGemini, MCPServerConfigVSCode,
14
+ MCPServerConfigCursor, MCPServerConfigClaude, MCPServerConfigOmni,
15
+ HOST_MODEL_REGISTRY
16
+ )
17
+ from .host_management import (
18
+ MCPHostRegistry, MCPHostStrategy, MCPHostConfigurationManager, register_host_strategy
19
+ )
20
+ from .reporting import (
21
+ FieldOperation, ConversionReport, generate_conversion_report, display_report
22
+ )
23
+
24
+ # Import strategies to trigger decorator registration
25
+ from . import strategies
26
+
27
+ __all__ = [
28
+ 'MCPHostConfigBackupManager',
29
+ 'MCPHostType', 'MCPServerConfig', 'HostConfiguration', 'EnvironmentData',
30
+ 'PackageHostConfiguration', 'EnvironmentPackageEntry', 'ConfigurationResult', 'SyncResult',
31
+ # Host-specific configuration models
32
+ 'MCPServerConfigBase', 'MCPServerConfigGemini', 'MCPServerConfigVSCode',
33
+ 'MCPServerConfigCursor', 'MCPServerConfigClaude', 'MCPServerConfigOmni',
34
+ 'HOST_MODEL_REGISTRY',
35
+ # User feedback reporting
36
+ 'FieldOperation', 'ConversionReport', 'generate_conversion_report', 'display_report',
37
+ 'MCPHostRegistry', 'MCPHostStrategy', 'MCPHostConfigurationManager', 'register_host_strategy'
38
+ ]