hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__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 (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,566 @@
1
+ """Package CLI handlers for Hatch.
2
+
3
+ This module contains handlers for package management commands. Packages are
4
+ MCP server implementations that can be installed into environments and
5
+ configured on MCP host platforms.
6
+
7
+ Commands:
8
+ - hatch package add <name>: Add a package to an environment
9
+ - hatch package remove <name>: Remove a package from an environment
10
+ - hatch package list: List packages in an environment
11
+ - hatch package sync <name>: Synchronize package MCP servers to hosts
12
+
13
+ Package Workflow:
14
+ 1. Add package to environment: hatch package add my-mcp-server
15
+ 2. Configure on hosts: hatch mcp configure claude-desktop my-mcp-server ...
16
+ 3. Or sync automatically: hatch package sync my-mcp-server --host all
17
+
18
+ Handler Signature:
19
+ All handlers follow: (args: Namespace) -> int
20
+ - args.env_manager: HatchEnvironmentManager instance
21
+ - Returns: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
22
+
23
+ Internal Helpers:
24
+ _configure_packages_on_hosts(): Shared logic for configuring packages on hosts
25
+
26
+ Example:
27
+ $ hatch package add mcp-server-fetch
28
+ $ hatch package list
29
+ $ hatch package sync mcp-server-fetch --host claude-desktop,cursor
30
+ """
31
+
32
+ import json
33
+ from argparse import Namespace
34
+ from pathlib import Path
35
+ from typing import TYPE_CHECKING, List, Tuple, Optional
36
+
37
+ from hatch_validator.package.package_service import PackageService
38
+
39
+ from hatch.cli.cli_utils import (
40
+ EXIT_SUCCESS,
41
+ EXIT_ERROR,
42
+ request_confirmation,
43
+ parse_host_list,
44
+ get_package_mcp_server_config,
45
+ ResultReporter,
46
+ ConsequenceType,
47
+ format_warning,
48
+ format_info,
49
+ )
50
+ from hatch.mcp_host_config import (
51
+ MCPHostConfigurationManager,
52
+ MCPHostType,
53
+ MCPServerConfig,
54
+ )
55
+ from hatch.mcp_host_config.reporting import generate_conversion_report
56
+
57
+ if TYPE_CHECKING:
58
+ from hatch.environment_manager import HatchEnvironmentManager
59
+
60
+
61
+ def handle_package_remove(args: Namespace) -> int:
62
+ """Handle 'hatch package remove' command.
63
+
64
+ Args:
65
+ args: Namespace with:
66
+ - env_manager: HatchEnvironmentManager instance
67
+ - package_name: Name of package to remove
68
+ - env: Optional environment name (default: current)
69
+ - dry_run: Preview changes without execution
70
+ - auto_approve: Skip confirmation prompt
71
+
72
+ Returns:
73
+ Exit code (0 for success, 1 for error)
74
+
75
+ Reference: R03 §3.1 (03-mutation_output_specification_v0.md)
76
+ """
77
+ env_manager: "HatchEnvironmentManager" = args.env_manager
78
+ package_name = args.package_name
79
+ env = getattr(args, "env", None)
80
+ dry_run = getattr(args, "dry_run", False)
81
+ auto_approve = getattr(args, "auto_approve", False)
82
+
83
+ # Create reporter for unified output
84
+ reporter = ResultReporter("hatch package remove", dry_run=dry_run)
85
+ reporter.add(ConsequenceType.REMOVE, f"Package '{package_name}'")
86
+
87
+ if dry_run:
88
+ reporter.report_result()
89
+ return EXIT_SUCCESS
90
+
91
+ # Show prompt and request confirmation unless auto-approved
92
+ if not auto_approve:
93
+ prompt = reporter.report_prompt()
94
+ if prompt:
95
+ print(prompt)
96
+
97
+ if not request_confirmation("Proceed?"):
98
+ format_info("Operation cancelled")
99
+ return EXIT_SUCCESS
100
+
101
+ if env_manager.remove_package(package_name, env):
102
+ reporter.report_result()
103
+ return EXIT_SUCCESS
104
+ else:
105
+ reporter.report_error(f"Failed to remove package '{package_name}'")
106
+ return EXIT_ERROR
107
+
108
+
109
+ def handle_package_list(args: Namespace) -> int:
110
+ """Handle 'hatch package list' command.
111
+
112
+ .. deprecated::
113
+ This command is deprecated. Use 'hatch env list' instead,
114
+ which shows packages inline with environment information.
115
+
116
+ Args:
117
+ args: Namespace with:
118
+ - env_manager: HatchEnvironmentManager instance
119
+ - env: Optional environment name (default: current)
120
+
121
+ Returns:
122
+ Exit code (0 for success)
123
+ """
124
+ import sys
125
+
126
+ # Emit deprecation warning to stderr
127
+ print(
128
+ "Warning: 'hatch package list' is deprecated. "
129
+ "Use 'hatch env list' instead, which shows packages inline.",
130
+ file=sys.stderr
131
+ )
132
+
133
+ env_manager: "HatchEnvironmentManager" = args.env_manager
134
+ env = getattr(args, "env", None)
135
+
136
+ packages = env_manager.list_packages(env)
137
+
138
+ if not packages:
139
+ print(f"No packages found in environment: {env}")
140
+ return EXIT_SUCCESS
141
+
142
+ print(f"Packages in environment '{env}':")
143
+ for pkg in packages:
144
+ print(
145
+ f"{pkg['name']} ({pkg['version']})\tHatch compliant: {pkg['hatch_compliant']}\tsource: {pkg['source']['uri']}\tlocation: {pkg['source']['path']}"
146
+ )
147
+ return EXIT_SUCCESS
148
+
149
+
150
+
151
+ def _get_package_names_with_dependencies(
152
+ env_manager: "HatchEnvironmentManager",
153
+ package_path_or_name: str,
154
+ env_name: str,
155
+ ) -> Tuple[str, List[str], Optional[PackageService]]:
156
+ """Get package name and its dependencies.
157
+
158
+ Args:
159
+ env_manager: HatchEnvironmentManager instance
160
+ package_path_or_name: Package path or name
161
+ env_name: Environment name
162
+
163
+ Returns:
164
+ Tuple of (package_name, list_of_all_package_names, package_service_or_none)
165
+ """
166
+ package_name = package_path_or_name
167
+ package_service = None
168
+ package_names = []
169
+
170
+ # Check if it's a local package path
171
+ pkg_path = Path(package_path_or_name)
172
+ if pkg_path.exists() and pkg_path.is_dir():
173
+ # Local package - load metadata from directory
174
+ with open(pkg_path / "hatch_metadata.json", "r") as f:
175
+ metadata = json.load(f)
176
+ package_service = PackageService(metadata)
177
+ package_name = package_service.get_field("name")
178
+ else:
179
+ # Registry package - get metadata from environment manager
180
+ try:
181
+ env_data = env_manager.get_environment_data(env_name)
182
+ if env_data:
183
+ # Find the package in the environment
184
+ for pkg in env_data.packages:
185
+ if pkg.name == package_name:
186
+ # Create a minimal metadata structure for PackageService
187
+ metadata = {
188
+ "name": pkg.name,
189
+ "version": pkg.version,
190
+ "dependencies": {},
191
+ }
192
+ package_service = PackageService(metadata)
193
+ break
194
+
195
+ if package_service is None:
196
+ format_warning(
197
+ f"Could not find package '{package_name}' in environment '{env_name}'",
198
+ suggestion="Skipping dependency analysis"
199
+ )
200
+ except Exception as e:
201
+ format_warning(
202
+ f"Could not load package metadata for '{package_name}': {e}",
203
+ suggestion="Skipping dependency analysis"
204
+ )
205
+
206
+ # Get dependency names if we have package service
207
+ if package_service:
208
+ # Get Hatch dependencies
209
+ dependencies = package_service.get_dependencies()
210
+ hatch_deps = dependencies.get("hatch", [])
211
+ package_names = [dep.get("name") for dep in hatch_deps if dep.get("name")]
212
+
213
+ # Resolve local dependency paths to actual names
214
+ for i in range(len(package_names)):
215
+ dep_path = Path(package_names[i])
216
+ if dep_path.exists() and dep_path.is_dir():
217
+ try:
218
+ with open(dep_path / "hatch_metadata.json", "r") as f:
219
+ dep_metadata = json.load(f)
220
+ dep_service = PackageService(dep_metadata)
221
+ package_names[i] = dep_service.get_field("name")
222
+ except Exception as e:
223
+ format_warning(
224
+ f"Could not resolve dependency path '{package_names[i]}': {e}"
225
+ )
226
+
227
+ # Add the main package to the list
228
+ package_names.append(package_name)
229
+
230
+ return package_name, package_names, package_service
231
+
232
+
233
+ def _configure_packages_on_hosts(
234
+ env_manager: "HatchEnvironmentManager",
235
+ mcp_manager: MCPHostConfigurationManager,
236
+ env_name: str,
237
+ package_names: List[str],
238
+ hosts: List[str],
239
+ no_backup: bool = False,
240
+ dry_run: bool = False,
241
+ reporter: Optional[ResultReporter] = None,
242
+ ) -> Tuple[int, int]:
243
+ """Configure MCP servers for packages on specified hosts.
244
+
245
+ This is shared logic used by both package add and package sync commands.
246
+
247
+ Args:
248
+ env_manager: HatchEnvironmentManager instance
249
+ mcp_manager: MCPHostConfigurationManager instance
250
+ env_name: Environment name
251
+ package_names: List of package names to configure
252
+ hosts: List of host names to configure on
253
+ no_backup: Skip backup creation
254
+ dry_run: Preview only, don't execute
255
+ reporter: Optional ResultReporter for unified output
256
+
257
+ Returns:
258
+ Tuple of (success_count, total_operations)
259
+ """
260
+ # Get MCP server configurations for all packages
261
+ server_configs: List[Tuple[str, MCPServerConfig]] = []
262
+ for pkg_name in package_names:
263
+ try:
264
+ config = get_package_mcp_server_config(env_manager, env_name, pkg_name)
265
+ server_configs.append((pkg_name, config))
266
+ except Exception as e:
267
+ format_warning(f"Could not get MCP configuration for package '{pkg_name}': {e}")
268
+
269
+ if not server_configs:
270
+ return 0, 0
271
+
272
+ total_operations = len(server_configs) * len(hosts)
273
+ success_count = 0
274
+
275
+ for host in hosts:
276
+ try:
277
+ # Convert string to MCPHostType enum
278
+ host_type = MCPHostType(host)
279
+
280
+ for pkg_name, server_config in server_configs:
281
+ try:
282
+ # Generate conversion report for field-level details
283
+ report = generate_conversion_report(
284
+ operation="create",
285
+ server_name=server_config.name,
286
+ target_host=host_type,
287
+ config=server_config,
288
+ dry_run=dry_run,
289
+ )
290
+
291
+ # Add to reporter if provided
292
+ if reporter:
293
+ reporter.add_from_conversion_report(report)
294
+
295
+ if dry_run:
296
+ success_count += 1
297
+ continue
298
+
299
+ # Pass MCPServerConfig directly - adapters handle serialization
300
+ result = mcp_manager.configure_server(
301
+ hostname=host,
302
+ server_config=server_config,
303
+ no_backup=no_backup,
304
+ )
305
+
306
+ if result.success:
307
+ success_count += 1
308
+
309
+ # Update package metadata with host configuration tracking
310
+ try:
311
+ server_config_dict = {
312
+ "name": server_config.name,
313
+ "command": server_config.command,
314
+ "args": server_config.args,
315
+ }
316
+
317
+ env_manager.update_package_host_configuration(
318
+ env_name=env_name,
319
+ package_name=pkg_name,
320
+ hostname=host,
321
+ server_config=server_config_dict,
322
+ )
323
+ except Exception as e:
324
+ format_warning(f"Failed to update package metadata for {pkg_name}: {e}")
325
+ else:
326
+ print(f"✗ Failed to configure {server_config.name} ({pkg_name}) on {host}: {result.error_message}")
327
+
328
+ except Exception as e:
329
+ print(f"✗ Error configuring {server_config.name} ({pkg_name}) on {host}: {e}")
330
+
331
+ except ValueError as e:
332
+ print(f"✗ Invalid host '{host}': {e}")
333
+ continue
334
+
335
+ return success_count, total_operations
336
+
337
+
338
+
339
+ def handle_package_add(args: Namespace) -> int:
340
+ """Handle 'hatch package add' command.
341
+
342
+ Args:
343
+ args: Namespace with:
344
+ - env_manager: HatchEnvironmentManager instance
345
+ - mcp_manager: MCPHostConfigurationManager instance
346
+ - package_path_or_name: Package path or name
347
+ - env: Optional environment name
348
+ - version: Optional version
349
+ - force_download: Force download even if cached
350
+ - refresh_registry: Force registry refresh
351
+ - auto_approve: Skip confirmation prompts
352
+ - host: Optional comma-separated host list for MCP configuration
353
+
354
+ Returns:
355
+ Exit code (0 for success, 1 for error)
356
+ """
357
+ env_manager: "HatchEnvironmentManager" = args.env_manager
358
+ mcp_manager: MCPHostConfigurationManager = args.mcp_manager
359
+
360
+ package_path_or_name = args.package_path_or_name
361
+ env = getattr(args, "env", None)
362
+ version = getattr(args, "version", None)
363
+ force_download = getattr(args, "force_download", False)
364
+ refresh_registry = getattr(args, "refresh_registry", False)
365
+ auto_approve = getattr(args, "auto_approve", False)
366
+ host_arg = getattr(args, "host", None)
367
+ dry_run = getattr(args, "dry_run", False)
368
+
369
+ # Create reporter for unified output
370
+ reporter = ResultReporter("hatch package add", dry_run=dry_run)
371
+
372
+ # Add package to environment
373
+ reporter.add(ConsequenceType.ADD, f"Package '{package_path_or_name}'")
374
+
375
+ if not env_manager.add_package_to_environment(
376
+ package_path_or_name,
377
+ env,
378
+ version,
379
+ force_download,
380
+ refresh_registry,
381
+ auto_approve,
382
+ ):
383
+ reporter.report_error(f"Failed to add package '{package_path_or_name}'")
384
+ return EXIT_ERROR
385
+
386
+ # Handle MCP host configuration if requested
387
+ if host_arg:
388
+ try:
389
+ hosts = parse_host_list(host_arg)
390
+ env_name = env or env_manager.get_current_environment()
391
+
392
+ package_name, package_names, _ = _get_package_names_with_dependencies(
393
+ env_manager, package_path_or_name, env_name
394
+ )
395
+
396
+ success_count, total = _configure_packages_on_hosts(
397
+ env_manager=env_manager,
398
+ mcp_manager=mcp_manager,
399
+ env_name=env_name,
400
+ package_names=package_names,
401
+ hosts=hosts,
402
+ no_backup=False, # Always backup when adding packages
403
+ dry_run=dry_run,
404
+ reporter=reporter,
405
+ )
406
+
407
+ except ValueError as e:
408
+ format_warning(f"MCP host configuration failed: {e}")
409
+ # Don't fail the entire operation for MCP configuration issues
410
+
411
+ # Report results
412
+ reporter.report_result()
413
+ return EXIT_SUCCESS
414
+
415
+
416
+ def handle_package_sync(args: Namespace) -> int:
417
+ """Handle 'hatch package sync' command.
418
+
419
+ Args:
420
+ args: Namespace with:
421
+ - env_manager: HatchEnvironmentManager instance
422
+ - mcp_manager: MCPHostConfigurationManager instance
423
+ - package_name: Package name to sync
424
+ - host: Comma-separated host list (required)
425
+ - env: Optional environment name
426
+ - dry_run: Preview only
427
+ - auto_approve: Skip confirmation
428
+ - no_backup: Skip backup creation
429
+
430
+ Returns:
431
+ Exit code (0 for success, 1 for error)
432
+ """
433
+ env_manager: "HatchEnvironmentManager" = args.env_manager
434
+ mcp_manager: MCPHostConfigurationManager = args.mcp_manager
435
+
436
+ package_name = args.package_name
437
+ host_arg = args.host
438
+ env = getattr(args, "env", None)
439
+ dry_run = getattr(args, "dry_run", False)
440
+ auto_approve = getattr(args, "auto_approve", False)
441
+ no_backup = getattr(args, "no_backup", False)
442
+
443
+ # Create reporter for unified output
444
+ reporter = ResultReporter("hatch package sync", dry_run=dry_run)
445
+
446
+ try:
447
+ # Parse host list
448
+ hosts = parse_host_list(host_arg)
449
+ env_name = env or env_manager.get_current_environment()
450
+
451
+ # Get all packages to sync (main package + dependencies)
452
+ package_names = [package_name]
453
+
454
+ # Try to get dependencies for the main package
455
+ try:
456
+ env_data = env_manager.get_environment_data(env_name)
457
+ if env_data:
458
+ # Find the main package in the environment
459
+ main_package = None
460
+ for pkg in env_data.packages:
461
+ if pkg.name == package_name:
462
+ main_package = pkg
463
+ break
464
+
465
+ if main_package:
466
+ # Create a minimal metadata structure for PackageService
467
+ metadata = {
468
+ "name": main_package.name,
469
+ "version": main_package.version,
470
+ "dependencies": {},
471
+ }
472
+ package_service = PackageService(metadata)
473
+
474
+ # Get Hatch dependencies
475
+ dependencies = package_service.get_dependencies()
476
+ hatch_deps = dependencies.get("hatch", [])
477
+ dep_names = [dep.get("name") for dep in hatch_deps if dep.get("name")]
478
+
479
+ # Add dependencies to the sync list (before main package)
480
+ package_names = dep_names + [package_name]
481
+ else:
482
+ format_warning(
483
+ f"Package '{package_name}' not found in environment '{env_name}'",
484
+ suggestion="Syncing only the specified package"
485
+ )
486
+ else:
487
+ format_warning(
488
+ f"Could not access environment '{env_name}'",
489
+ suggestion="Syncing only the specified package"
490
+ )
491
+ except Exception as e:
492
+ format_warning(
493
+ f"Could not analyze dependencies for '{package_name}': {e}",
494
+ suggestion="Syncing only the specified package"
495
+ )
496
+
497
+ # Get MCP server configurations for all packages
498
+ server_configs: List[Tuple[str, MCPServerConfig]] = []
499
+ for pkg_name in package_names:
500
+ try:
501
+ config = get_package_mcp_server_config(env_manager, env_name, pkg_name)
502
+ server_configs.append((pkg_name, config))
503
+ except Exception as e:
504
+ format_warning(f"Could not get MCP configuration for package '{pkg_name}': {e}")
505
+
506
+ if not server_configs:
507
+ reporter.report_error(
508
+ f"No MCP server configurations found for package '{package_name}' or its dependencies"
509
+ )
510
+ return EXIT_ERROR
511
+
512
+ # Build consequences for preview/confirmation
513
+ for pkg_name, config in server_configs:
514
+ for host in hosts:
515
+ try:
516
+ host_type = MCPHostType(host)
517
+ report = generate_conversion_report(
518
+ operation="create",
519
+ server_name=config.name,
520
+ target_host=host_type,
521
+ config=config,
522
+ dry_run=dry_run,
523
+ )
524
+ reporter.add_from_conversion_report(report)
525
+ except ValueError:
526
+ reporter.add(ConsequenceType.SKIP, f"Invalid host '{host}'")
527
+
528
+ # Show preview and get confirmation
529
+ prompt = reporter.report_prompt()
530
+ if prompt:
531
+ print(prompt)
532
+
533
+ if dry_run:
534
+ reporter.report_result()
535
+ return EXIT_SUCCESS
536
+
537
+ # Confirm operation unless auto-approved
538
+ if not request_confirmation("Proceed?", auto_approve):
539
+ format_info("Operation cancelled")
540
+ return EXIT_SUCCESS
541
+
542
+ # Perform synchronization (reporter already has consequences from preview)
543
+ success_count, total_operations = _configure_packages_on_hosts(
544
+ env_manager=env_manager,
545
+ mcp_manager=mcp_manager,
546
+ env_name=env_name,
547
+ package_names=[pkg_name for pkg_name, _ in server_configs],
548
+ hosts=hosts,
549
+ no_backup=no_backup,
550
+ dry_run=False,
551
+ reporter=None, # Don't add again, we already have consequences
552
+ )
553
+
554
+ # Report results
555
+ reporter.report_result()
556
+
557
+ if success_count == total_operations:
558
+ return EXIT_SUCCESS
559
+ elif success_count > 0:
560
+ return EXIT_ERROR
561
+ else:
562
+ return EXIT_ERROR
563
+
564
+ except ValueError as e:
565
+ reporter.report_error(str(e))
566
+ return EXIT_ERROR
@@ -0,0 +1,136 @@
1
+ """System CLI handlers for Hatch.
2
+
3
+ This module contains handlers for system-level commands that operate on
4
+ packages as a whole rather than within environments.
5
+
6
+ Commands:
7
+ - hatch create <name>: Create a new package template from scratch
8
+ - hatch validate <path>: Validate a package against the Hatch schema
9
+
10
+ Package Creation:
11
+ The create command generates a complete package template with:
12
+ - pyproject.toml with Hatch metadata
13
+ - Source directory structure
14
+ - README and LICENSE files
15
+ - Basic MCP server implementation
16
+
17
+ Package Validation:
18
+ The validate command checks:
19
+ - pyproject.toml structure and required fields
20
+ - Hatch-specific metadata (mcp_server entry points)
21
+ - Package dependencies and version constraints
22
+
23
+ Handler Signature:
24
+ All handlers follow: (args: Namespace) -> int
25
+ Returns: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
26
+
27
+ Example:
28
+ $ hatch create my-mcp-server --description "My custom MCP server"
29
+ $ hatch validate ./my-mcp-server
30
+ """
31
+
32
+ from argparse import Namespace
33
+ from pathlib import Path
34
+
35
+ from hatch_validator import HatchPackageValidator
36
+
37
+ from hatch.cli.cli_utils import (
38
+ EXIT_SUCCESS,
39
+ EXIT_ERROR,
40
+ ResultReporter,
41
+ ConsequenceType,
42
+ )
43
+ from hatch.template_generator import create_package_template
44
+
45
+
46
+ def handle_create(args: Namespace) -> int:
47
+ """Handle 'hatch create' command.
48
+
49
+ Args:
50
+ args: Namespace with:
51
+ - name: Package name
52
+ - dir: Target directory (default: current directory)
53
+ - description: Package description (optional)
54
+
55
+ Returns:
56
+ Exit code (0 for success, 1 for error)
57
+ """
58
+ target_dir = Path(args.dir).resolve()
59
+ description = getattr(args, "description", "")
60
+ dry_run = getattr(args, "dry_run", False)
61
+
62
+ # Create reporter for unified output
63
+ reporter = ResultReporter("hatch create", dry_run=dry_run)
64
+ reporter.add(ConsequenceType.CREATE, f"Package '{args.name}' at {target_dir}")
65
+
66
+ if dry_run:
67
+ reporter.report_result()
68
+ return EXIT_SUCCESS
69
+
70
+ try:
71
+ package_dir = create_package_template(
72
+ target_dir=target_dir,
73
+ package_name=args.name,
74
+ description=description,
75
+ )
76
+ reporter.report_result()
77
+ return EXIT_SUCCESS
78
+ except Exception as e:
79
+ reporter.report_error(
80
+ f"Failed to create package template",
81
+ details=[f"Reason: {e}"]
82
+ )
83
+ return EXIT_ERROR
84
+
85
+
86
+ def handle_validate(args: Namespace) -> int:
87
+ """Handle 'hatch validate' command.
88
+
89
+ Args:
90
+ args: Namespace with:
91
+ - env_manager: HatchEnvironmentManager instance
92
+ - package_dir: Path to package directory
93
+
94
+ Returns:
95
+ Exit code (0 for success, 1 for error)
96
+ """
97
+ from hatch.environment_manager import HatchEnvironmentManager
98
+
99
+ env_manager: HatchEnvironmentManager = args.env_manager
100
+ package_path = Path(args.package_dir).resolve()
101
+
102
+ # Create reporter for unified output
103
+ reporter = ResultReporter("hatch validate", dry_run=False)
104
+
105
+ # Create validator with registry data from environment manager
106
+ validator = HatchPackageValidator(
107
+ version="latest",
108
+ allow_local_dependencies=True,
109
+ registry_data=env_manager.registry_data,
110
+ )
111
+
112
+ # Validate the package
113
+ is_valid, validation_results = validator.validate_package(package_path)
114
+
115
+ if is_valid:
116
+ reporter.add(ConsequenceType.VALIDATE, f"Package '{package_path.name}'")
117
+ reporter.report_result()
118
+ return EXIT_SUCCESS
119
+ else:
120
+ # Collect detailed validation errors
121
+ error_details = [f"Package: {package_path}"]
122
+
123
+ if validation_results and isinstance(validation_results, dict):
124
+ for category, result in validation_results.items():
125
+ if (
126
+ category != "valid"
127
+ and category != "metadata"
128
+ and isinstance(result, dict)
129
+ ):
130
+ if not result.get("valid", True) and result.get("errors"):
131
+ error_details.append(f"{category.replace('_', ' ').title()} errors:")
132
+ for error in result["errors"]:
133
+ error_details.append(f" - {error}")
134
+
135
+ reporter.report_error("Package validation failed", details=error_details)
136
+ return EXIT_ERROR