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
hatch/cli/cli_env.py ADDED
@@ -0,0 +1,865 @@
1
+ """Environment CLI handlers for Hatch.
2
+
3
+ This module contains handlers for environment management commands. Environments
4
+ provide isolated contexts for managing packages and their MCP server configurations.
5
+
6
+ Commands:
7
+ Basic Environment Management:
8
+ - hatch env create <name>: Create a new environment
9
+ - hatch env remove <name>: Remove an environment
10
+ - hatch env list: List all environments
11
+ - hatch env use <name>: Set current environment
12
+ - hatch env current: Show current environment
13
+
14
+ Python Environment Management:
15
+ - hatch env python init: Initialize Python virtual environment
16
+ - hatch env python info: Show Python environment info
17
+ - hatch env python remove: Remove Python virtual environment
18
+ - hatch env python shell: Launch interactive Python shell
19
+ - hatch env python add-hatch-mcp: Add hatch_mcp_server wrapper script
20
+
21
+ Handler Signature:
22
+ All handlers follow: (args: Namespace) -> int
23
+ - args.env_manager: HatchEnvironmentManager instance
24
+ - Returns: EXIT_SUCCESS (0) on success, EXIT_ERROR (1) on failure
25
+
26
+ Example:
27
+ $ hatch env create my-project
28
+ $ hatch env use my-project
29
+ $ hatch env python init
30
+ $ hatch env python shell
31
+ """
32
+
33
+ from argparse import Namespace
34
+ from typing import TYPE_CHECKING
35
+
36
+ from hatch.cli.cli_utils import (
37
+ EXIT_SUCCESS,
38
+ EXIT_ERROR,
39
+ request_confirmation,
40
+ ResultReporter,
41
+ ConsequenceType,
42
+ TableFormatter,
43
+ ColumnDef,
44
+ ValidationError,
45
+ format_validation_error,
46
+ format_info,
47
+ )
48
+
49
+ if TYPE_CHECKING:
50
+ from hatch.environment_manager import HatchEnvironmentManager
51
+
52
+
53
+ def handle_env_create(args: Namespace) -> int:
54
+ """Handle 'hatch env create' command.
55
+
56
+ Args:
57
+ args: Namespace with:
58
+ - env_manager: HatchEnvironmentManager instance
59
+ - name: Environment name
60
+ - description: Environment description
61
+ - python_version: Optional Python version
62
+ - no_python: Skip Python environment creation
63
+ - no_hatch_mcp_server: Skip hatch_mcp_server installation
64
+ - hatch_mcp_server_tag: Git tag for hatch_mcp_server
65
+
66
+ Returns:
67
+ Exit code (0 for success, 1 for error)
68
+ """
69
+ env_manager: "HatchEnvironmentManager" = args.env_manager
70
+ name = args.name
71
+ description = getattr(args, "description", "")
72
+ python_version = getattr(args, "python_version", None)
73
+ create_python_env = not getattr(args, "no_python", False)
74
+ no_hatch_mcp_server = getattr(args, "no_hatch_mcp_server", False)
75
+ hatch_mcp_server_tag = getattr(args, "hatch_mcp_server_tag", None)
76
+ dry_run = getattr(args, "dry_run", False)
77
+
78
+ # Create reporter for unified output
79
+ reporter = ResultReporter("hatch env create", dry_run=dry_run)
80
+ reporter.add(ConsequenceType.CREATE, f"Environment '{name}'")
81
+
82
+ if create_python_env:
83
+ version_str = f" ({python_version})" if python_version else ""
84
+ reporter.add(ConsequenceType.CREATE, f"Python environment{version_str}")
85
+
86
+ if dry_run:
87
+ reporter.report_result()
88
+ return EXIT_SUCCESS
89
+
90
+ if env_manager.create_environment(
91
+ name,
92
+ description,
93
+ python_version=python_version,
94
+ create_python_env=create_python_env,
95
+ no_hatch_mcp_server=no_hatch_mcp_server,
96
+ hatch_mcp_server_tag=hatch_mcp_server_tag,
97
+ ):
98
+ # Update reporter with actual Python environment details
99
+ if create_python_env and env_manager.is_python_environment_available():
100
+ python_exec = env_manager.python_env_manager.get_python_executable(name)
101
+ if python_exec:
102
+ python_version_info = env_manager.python_env_manager.get_python_version(name)
103
+ # Add details as child consequences would be ideal, but for now just report success
104
+
105
+ reporter.report_result()
106
+ return EXIT_SUCCESS
107
+ else:
108
+ reporter.report_error(f"Failed to create environment '{name}'")
109
+ return EXIT_ERROR
110
+
111
+
112
+ def handle_env_remove(args: Namespace) -> int:
113
+ """Handle 'hatch env remove' command.
114
+
115
+ Args:
116
+ args: Namespace with:
117
+ - env_manager: HatchEnvironmentManager instance
118
+ - name: Environment name to remove
119
+ - dry_run: Preview changes without execution
120
+ - auto_approve: Skip confirmation prompt
121
+
122
+ Returns:
123
+ Exit code (0 for success, 1 for error)
124
+
125
+ Reference: R03 §3.1 (03-mutation_output_specification_v0.md)
126
+ """
127
+ env_manager: "HatchEnvironmentManager" = args.env_manager
128
+ name = args.name
129
+ dry_run = getattr(args, "dry_run", False)
130
+ auto_approve = getattr(args, "auto_approve", False)
131
+
132
+ # Create reporter for unified output
133
+ reporter = ResultReporter("hatch env remove", dry_run=dry_run)
134
+ reporter.add(ConsequenceType.REMOVE, f"Environment '{name}'")
135
+
136
+ if dry_run:
137
+ reporter.report_result()
138
+ return EXIT_SUCCESS
139
+
140
+ # Show prompt and request confirmation unless auto-approved
141
+ if not auto_approve:
142
+ prompt = reporter.report_prompt()
143
+ if prompt:
144
+ print(prompt)
145
+
146
+ if not request_confirmation("Proceed?"):
147
+ format_info("Operation cancelled")
148
+ return EXIT_SUCCESS
149
+
150
+ if env_manager.remove_environment(name):
151
+ reporter.report_result()
152
+ return EXIT_SUCCESS
153
+ else:
154
+ reporter.report_error(f"Failed to remove environment '{name}'")
155
+ return EXIT_ERROR
156
+
157
+
158
+ def handle_env_list(args: Namespace) -> int:
159
+ """Handle 'hatch env list' command.
160
+
161
+ Args:
162
+ args: Namespace with:
163
+ - env_manager: HatchEnvironmentManager instance
164
+ - pattern: Optional regex pattern to filter environments
165
+ - json: Optional flag for JSON output
166
+
167
+ Returns:
168
+ Exit code (0 for success)
169
+
170
+ Reference: R02 §2.1 (02-list_output_format_specification_v2.md)
171
+ """
172
+ import json as json_module
173
+ import re
174
+
175
+ env_manager: "HatchEnvironmentManager" = args.env_manager
176
+ json_output: bool = getattr(args, 'json', False)
177
+ pattern: str = getattr(args, 'pattern', None)
178
+ environments = env_manager.list_environments()
179
+
180
+ # Apply pattern filter if specified
181
+ if pattern:
182
+ try:
183
+ regex = re.compile(pattern)
184
+ environments = [env for env in environments if regex.search(env.get("name", ""))]
185
+ except re.error as e:
186
+ format_validation_error(ValidationError(
187
+ f"Invalid regex pattern: {e}",
188
+ field="--pattern",
189
+ suggestion="Use a valid Python regex pattern"
190
+ ))
191
+ return EXIT_ERROR
192
+
193
+ if json_output:
194
+ # JSON output per R02 §8.1
195
+ env_data = []
196
+ for env in environments:
197
+ env_name = env.get("name")
198
+ python_version = None
199
+ if env.get("python_environment", False):
200
+ python_info = env_manager.get_python_environment_info(env_name)
201
+ if python_info:
202
+ python_version = python_info.get("python_version")
203
+
204
+ packages_list = env_manager.list_packages(env_name)
205
+ pkg_names = [pkg["name"] for pkg in packages_list] if packages_list else []
206
+
207
+ env_data.append({
208
+ "name": env_name,
209
+ "is_current": env.get("is_current", False),
210
+ "python_version": python_version,
211
+ "packages": pkg_names
212
+ })
213
+
214
+ print(json_module.dumps({"environments": env_data}, indent=2))
215
+ return EXIT_SUCCESS
216
+
217
+ # Table output
218
+ print("Environments:")
219
+
220
+ # Define table columns per R10 §5.1 (simplified output - count only)
221
+ columns = [
222
+ ColumnDef(name="Name", width=15),
223
+ ColumnDef(name="Python", width=10),
224
+ ColumnDef(name="Packages", width=10, align="right"),
225
+ ]
226
+ formatter = TableFormatter(columns)
227
+
228
+ for env in environments:
229
+ # Name with current marker
230
+ current_marker = "* " if env.get("is_current") else " "
231
+ name = f"{current_marker}{env.get('name')}"
232
+
233
+ # Python version
234
+ python_version = "-"
235
+ if env.get("python_environment", False):
236
+ python_info = env_manager.get_python_environment_info(env.get("name"))
237
+ if python_info:
238
+ python_version = python_info.get("python_version", "Unknown")
239
+
240
+ # Packages - show count only per R10 §5.1
241
+ packages_list = env_manager.list_packages(env.get("name"))
242
+ packages_count = str(len(packages_list)) if packages_list else "0"
243
+
244
+ formatter.add_row([name, python_version, packages_count])
245
+
246
+ print(formatter.render())
247
+ return EXIT_SUCCESS
248
+
249
+
250
+ def handle_env_use(args: Namespace) -> int:
251
+ """Handle 'hatch env use' command.
252
+
253
+ Args:
254
+ args: Namespace with:
255
+ - env_manager: HatchEnvironmentManager instance
256
+ - name: Environment name to set as current
257
+
258
+ Returns:
259
+ Exit code (0 for success, 1 for error)
260
+ """
261
+ env_manager: "HatchEnvironmentManager" = args.env_manager
262
+ name = args.name
263
+ dry_run = getattr(args, "dry_run", False)
264
+
265
+ # Create reporter for unified output
266
+ reporter = ResultReporter("hatch env use", dry_run=dry_run)
267
+ reporter.add(ConsequenceType.SET, f"Current environment → '{name}'")
268
+
269
+ if dry_run:
270
+ reporter.report_result()
271
+ return EXIT_SUCCESS
272
+
273
+ if env_manager.set_current_environment(name):
274
+ reporter.report_result()
275
+ return EXIT_SUCCESS
276
+ else:
277
+ reporter.report_error(f"Failed to set environment '{name}'")
278
+ return EXIT_ERROR
279
+
280
+
281
+ def handle_env_current(args: Namespace) -> int:
282
+ """Handle 'hatch env current' command.
283
+
284
+ Args:
285
+ args: Namespace with:
286
+ - env_manager: HatchEnvironmentManager instance
287
+
288
+ Returns:
289
+ Exit code (0 for success)
290
+ """
291
+ env_manager: "HatchEnvironmentManager" = args.env_manager
292
+ current_env = env_manager.get_current_environment()
293
+ print(f"Current environment: {current_env}")
294
+ return EXIT_SUCCESS
295
+
296
+
297
+
298
+ def handle_env_python_init(args: Namespace) -> int:
299
+ """Handle 'hatch env python init' command.
300
+
301
+ Args:
302
+ args: Namespace with:
303
+ - env_manager: HatchEnvironmentManager instance
304
+ - hatch_env: Optional environment name (default: current)
305
+ - python_version: Optional Python version
306
+ - force: Force recreation if exists
307
+ - no_hatch_mcp_server: Skip hatch_mcp_server installation
308
+ - hatch_mcp_server_tag: Git tag for hatch_mcp_server
309
+
310
+ Returns:
311
+ Exit code (0 for success, 1 for error)
312
+ """
313
+ env_manager: "HatchEnvironmentManager" = args.env_manager
314
+ hatch_env = getattr(args, "hatch_env", None)
315
+ python_version = getattr(args, "python_version", None)
316
+ force = getattr(args, "force", False)
317
+ no_hatch_mcp_server = getattr(args, "no_hatch_mcp_server", False)
318
+ hatch_mcp_server_tag = getattr(args, "hatch_mcp_server_tag", None)
319
+ dry_run = getattr(args, "dry_run", False)
320
+
321
+ env_name = hatch_env or env_manager.get_current_environment()
322
+
323
+ # Create reporter for unified output
324
+ reporter = ResultReporter("hatch env python init", dry_run=dry_run)
325
+ version_str = f" ({python_version})" if python_version else ""
326
+ reporter.add(ConsequenceType.INITIALIZE, f"Python environment for '{env_name}'{version_str}")
327
+
328
+ if dry_run:
329
+ reporter.report_result()
330
+ return EXIT_SUCCESS
331
+
332
+ if env_manager.create_python_environment_only(
333
+ hatch_env,
334
+ python_version,
335
+ force,
336
+ no_hatch_mcp_server=no_hatch_mcp_server,
337
+ hatch_mcp_server_tag=hatch_mcp_server_tag,
338
+ ):
339
+ reporter.report_result()
340
+ return EXIT_SUCCESS
341
+ else:
342
+ reporter.report_error(f"Failed to initialize Python environment for '{env_name}'")
343
+ return EXIT_ERROR
344
+
345
+
346
+ def handle_env_python_info(args: Namespace) -> int:
347
+ """Handle 'hatch env python info' command.
348
+
349
+ Args:
350
+ args: Namespace with:
351
+ - env_manager: HatchEnvironmentManager instance
352
+ - hatch_env: Optional environment name (default: current)
353
+ - detailed: Show detailed diagnostics
354
+
355
+ Returns:
356
+ Exit code (0 for success, 1 for error)
357
+ """
358
+ env_manager: "HatchEnvironmentManager" = args.env_manager
359
+ hatch_env = getattr(args, "hatch_env", None)
360
+ detailed = getattr(args, "detailed", False)
361
+
362
+ python_info = env_manager.get_python_environment_info(hatch_env)
363
+
364
+ if python_info:
365
+ env_name = hatch_env or env_manager.get_current_environment()
366
+ print(f"Python environment info for '{env_name}':")
367
+ print(f" Status: {'Active' if python_info.get('enabled', False) else 'Inactive'}")
368
+ print(f" Python executable: {python_info['python_executable']}")
369
+ print(f" Python version: {python_info.get('python_version', 'Unknown')}")
370
+ print(f" Conda environment: {python_info.get('conda_env_name', 'N/A')}")
371
+ print(f" Environment path: {python_info['environment_path']}")
372
+ print(f" Created: {python_info.get('created_at', 'Unknown')}")
373
+ print(f" Package count: {python_info.get('package_count', 0)}")
374
+ print(f" Packages:")
375
+ for pkg in python_info.get("packages", []):
376
+ print(f" - {pkg['name']} ({pkg['version']})")
377
+
378
+ if detailed:
379
+ print(f"\nDiagnostics:")
380
+ diagnostics = env_manager.get_python_environment_diagnostics(hatch_env)
381
+ if diagnostics:
382
+ for key, value in diagnostics.items():
383
+ print(f" {key}: {value}")
384
+ else:
385
+ print(" No diagnostics available")
386
+
387
+ return EXIT_SUCCESS
388
+ else:
389
+ env_name = hatch_env or env_manager.get_current_environment()
390
+ print(f"No Python environment found for: {env_name}")
391
+
392
+ # Show diagnostics for missing environment
393
+ if detailed:
394
+ print("\nDiagnostics:")
395
+ general_diagnostics = env_manager.get_python_manager_diagnostics()
396
+ for key, value in general_diagnostics.items():
397
+ print(f" {key}: {value}")
398
+
399
+ return EXIT_ERROR
400
+
401
+
402
+ def handle_env_python_remove(args: Namespace) -> int:
403
+ """Handle 'hatch env python remove' command.
404
+
405
+ Args:
406
+ args: Namespace with:
407
+ - env_manager: HatchEnvironmentManager instance
408
+ - hatch_env: Optional environment name (default: current)
409
+ - force: Skip confirmation prompt
410
+
411
+ Returns:
412
+ Exit code (0 for success, 1 for error)
413
+ """
414
+ env_manager: "HatchEnvironmentManager" = args.env_manager
415
+ hatch_env = getattr(args, "hatch_env", None)
416
+ force = getattr(args, "force", False)
417
+ dry_run = getattr(args, "dry_run", False)
418
+
419
+ env_name = hatch_env or env_manager.get_current_environment()
420
+
421
+ # Create reporter for unified output
422
+ reporter = ResultReporter("hatch env python remove", dry_run=dry_run)
423
+ reporter.add(ConsequenceType.REMOVE, f"Python environment for '{env_name}'")
424
+
425
+ if dry_run:
426
+ reporter.report_result()
427
+ return EXIT_SUCCESS
428
+
429
+ if not force:
430
+ # Ask for confirmation using TTY-aware function
431
+ if not request_confirmation(f"Remove Python environment for '{env_name}'?"):
432
+ format_info("Operation cancelled")
433
+ return EXIT_SUCCESS
434
+
435
+ if env_manager.remove_python_environment_only(hatch_env):
436
+ reporter.report_result()
437
+ return EXIT_SUCCESS
438
+ else:
439
+ reporter.report_error(f"Failed to remove Python environment from '{env_name}'")
440
+ return EXIT_ERROR
441
+
442
+
443
+ def handle_env_python_shell(args: Namespace) -> int:
444
+ """Handle 'hatch env python shell' command.
445
+
446
+ Args:
447
+ args: Namespace with:
448
+ - env_manager: HatchEnvironmentManager instance
449
+ - hatch_env: Optional environment name (default: current)
450
+ - cmd: Optional command to run in shell
451
+
452
+ Returns:
453
+ Exit code (0 for success, 1 for error)
454
+ """
455
+ env_manager: "HatchEnvironmentManager" = args.env_manager
456
+ hatch_env = getattr(args, "hatch_env", None)
457
+ cmd = getattr(args, "cmd", None)
458
+
459
+ if env_manager.launch_python_shell(hatch_env, cmd):
460
+ return EXIT_SUCCESS
461
+ else:
462
+ env_name = hatch_env or env_manager.get_current_environment()
463
+ reporter = ResultReporter("hatch env python shell")
464
+ reporter.report_error(f"Failed to launch Python shell for '{env_name}'")
465
+ return EXIT_ERROR
466
+
467
+
468
+ def handle_env_python_add_hatch_mcp(args: Namespace) -> int:
469
+ """Handle 'hatch env python add-hatch-mcp' command.
470
+
471
+ Args:
472
+ args: Namespace with:
473
+ - env_manager: HatchEnvironmentManager instance
474
+ - hatch_env: Optional environment name (default: current)
475
+ - tag: Git tag/branch for wrapper installation
476
+
477
+ Returns:
478
+ Exit code (0 for success, 1 for error)
479
+ """
480
+ env_manager: "HatchEnvironmentManager" = args.env_manager
481
+ hatch_env = getattr(args, "hatch_env", None)
482
+ tag = getattr(args, "tag", None)
483
+ dry_run = getattr(args, "dry_run", False)
484
+
485
+ env_name = hatch_env or env_manager.get_current_environment()
486
+
487
+ # Create reporter for unified output
488
+ reporter = ResultReporter("hatch env python add-hatch-mcp", dry_run=dry_run)
489
+ reporter.add(ConsequenceType.INSTALL, f"hatch_mcp_server wrapper in '{env_name}'")
490
+
491
+ if dry_run:
492
+ reporter.report_result()
493
+ return EXIT_SUCCESS
494
+
495
+ if env_manager.install_mcp_server(env_name, tag):
496
+ reporter.report_result()
497
+ return EXIT_SUCCESS
498
+ else:
499
+ reporter.report_error(f"Failed to install hatch_mcp_server wrapper in environment '{env_name}'")
500
+ return EXIT_ERROR
501
+
502
+
503
+ def handle_env_show(args: Namespace) -> int:
504
+ """Handle 'hatch env show' command.
505
+
506
+ Displays detailed hierarchical view of a specific environment.
507
+
508
+ Args:
509
+ args: Namespace with:
510
+ - env_manager: HatchEnvironmentManager instance
511
+ - name: Environment name to show
512
+
513
+ Returns:
514
+ Exit code (0 for success, 1 for error)
515
+
516
+ Reference: R02 §2.2 (02-list_output_format_specification_v2.md)
517
+ """
518
+ env_manager: "HatchEnvironmentManager" = args.env_manager
519
+ name = args.name
520
+
521
+ # Validate environment exists
522
+ if not env_manager.environment_exists(name):
523
+ format_validation_error(ValidationError(
524
+ f"Environment '{name}' does not exist",
525
+ field="name",
526
+ suggestion="Use 'hatch env list' to see available environments"
527
+ ))
528
+ return EXIT_ERROR
529
+
530
+ # Get environment data
531
+ env_data = env_manager.get_environment_data(name)
532
+ current_env = env_manager.get_current_environment()
533
+ is_current = name == current_env
534
+
535
+ # Header
536
+ status = " (active)" if is_current else ""
537
+ print(f"Environment: {name}{status}")
538
+
539
+ # Description
540
+ description = env_data.get("description", "")
541
+ if description:
542
+ print(f" Description: {description}")
543
+
544
+ # Created timestamp
545
+ created_at = env_data.get("created_at", "Unknown")
546
+ print(f" Created: {created_at}")
547
+ print()
548
+
549
+ # Python Environment section
550
+ python_info = env_manager.get_python_environment_info(name)
551
+ print(" Python Environment:")
552
+ if python_info:
553
+ print(f" Version: {python_info.get('python_version', 'Unknown')}")
554
+ print(f" Executable: {python_info.get('python_executable', 'N/A')}")
555
+ conda_env = python_info.get('conda_env_name', 'N/A')
556
+ if conda_env and conda_env != 'N/A':
557
+ print(f" Conda env: {conda_env}")
558
+ status = "Active" if python_info.get('enabled', False) else "Inactive"
559
+ print(f" Status: {status}")
560
+ else:
561
+ print(" (not initialized)")
562
+ print()
563
+
564
+ # Packages section
565
+ packages = env_manager.list_packages(name)
566
+ pkg_count = len(packages) if packages else 0
567
+ print(f" Packages ({pkg_count}):")
568
+
569
+ if packages:
570
+ for pkg in packages:
571
+ pkg_name = pkg.get("name", "unknown")
572
+ print(f" {pkg_name}")
573
+
574
+ # Version
575
+ version = pkg.get("version", "unknown")
576
+ print(f" Version: {version}")
577
+
578
+ # Source
579
+ source = pkg.get("source", {})
580
+ source_type = source.get("type", "unknown")
581
+ source_path = source.get("path", source.get("url", "N/A"))
582
+ print(f" Source: {source_type} ({source_path})")
583
+
584
+ # Deployed hosts
585
+ configured_hosts = pkg.get("configured_hosts", {})
586
+ if configured_hosts:
587
+ hosts_list = ", ".join(configured_hosts.keys())
588
+ print(f" Deployed to: {hosts_list}")
589
+ else:
590
+ print(f" Deployed to: (none)")
591
+ print()
592
+ else:
593
+ print(" (empty)")
594
+
595
+ return EXIT_SUCCESS
596
+
597
+
598
+ def handle_env_list_hosts(args: Namespace) -> int:
599
+ """Handle 'hatch env list hosts' command.
600
+
601
+ Lists environment/host/server deployments from environment data.
602
+ Shows only Hatch-managed packages and their host deployments.
603
+
604
+ Args:
605
+ args: Namespace with:
606
+ - env_manager: HatchEnvironmentManager instance
607
+ - env: Optional regex pattern to filter by environment name
608
+ - server: Optional regex pattern to filter by server name
609
+ - json: Optional flag for JSON output
610
+
611
+ Returns:
612
+ Exit code (0 for success, 1 for error)
613
+
614
+ Reference: R10 §3.3 (10-namespace_consistency_specification_v2.md)
615
+ """
616
+ import json as json_module
617
+ import re
618
+
619
+ env_manager: "HatchEnvironmentManager" = args.env_manager
620
+ env_pattern: str = getattr(args, 'env', None)
621
+ server_pattern: str = getattr(args, 'server', None)
622
+ json_output: bool = getattr(args, 'json', False)
623
+
624
+ # Compile regex patterns if provided
625
+ env_re = None
626
+ if env_pattern:
627
+ try:
628
+ env_re = re.compile(env_pattern)
629
+ except re.error as e:
630
+ format_validation_error(ValidationError(
631
+ f"Invalid env regex pattern: {e}",
632
+ field="--env",
633
+ suggestion="Use a valid Python regex pattern"
634
+ ))
635
+ return EXIT_ERROR
636
+
637
+ server_re = None
638
+ if server_pattern:
639
+ try:
640
+ server_re = re.compile(server_pattern)
641
+ except re.error as e:
642
+ format_validation_error(ValidationError(
643
+ f"Invalid server regex pattern: {e}",
644
+ field="--server",
645
+ suggestion="Use a valid Python regex pattern"
646
+ ))
647
+ return EXIT_ERROR
648
+
649
+ # Get all environments
650
+ environments = env_manager.list_environments()
651
+
652
+ # Collect rows: (environment, host, server, version)
653
+ rows = []
654
+
655
+ for env_info in environments:
656
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
657
+
658
+ # Apply environment filter
659
+ if env_re and not env_re.search(env_name):
660
+ continue
661
+
662
+ try:
663
+ env_data = env_manager.get_environment_data(env_name)
664
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else []
665
+
666
+ for pkg in packages:
667
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else None
668
+ pkg_version = pkg.get("version", "-") if isinstance(pkg, dict) else "-"
669
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else {}
670
+
671
+ if not pkg_name or not configured_hosts:
672
+ continue
673
+
674
+ # Apply server filter
675
+ if server_re and not server_re.search(pkg_name):
676
+ continue
677
+
678
+ # Add a row for each host deployment
679
+ for host_name in configured_hosts.keys():
680
+ rows.append((env_name, host_name, pkg_name, pkg_version))
681
+ except Exception:
682
+ continue
683
+
684
+ # Sort rows by environment (alphabetically), then host, then server
685
+ rows.sort(key=lambda x: (x[0], x[1], x[2]))
686
+
687
+ # JSON output per R10 §8
688
+ if json_output:
689
+ rows_data = []
690
+ for env, host, server, version in rows:
691
+ rows_data.append({
692
+ "environment": env,
693
+ "host": host,
694
+ "server": server,
695
+ "version": version
696
+ })
697
+ print(json_module.dumps({"rows": rows_data}, indent=2))
698
+ return EXIT_SUCCESS
699
+
700
+ # Display results
701
+ if not rows:
702
+ if env_pattern or server_pattern:
703
+ print("No matching environment host deployments found")
704
+ else:
705
+ print("No environment host deployments found")
706
+ return EXIT_SUCCESS
707
+
708
+ print("Environment Host Deployments:")
709
+
710
+ # Define table columns per R10 §3.3: Environment → Host → Server → Version
711
+ columns = [
712
+ ColumnDef(name="Environment", width=15),
713
+ ColumnDef(name="Host", width=18),
714
+ ColumnDef(name="Server", width=18),
715
+ ColumnDef(name="Version", width=10),
716
+ ]
717
+ formatter = TableFormatter(columns)
718
+
719
+ for env, host, server, version in rows:
720
+ formatter.add_row([env, host, server, version])
721
+
722
+ print(formatter.render())
723
+ return EXIT_SUCCESS
724
+
725
+
726
+ def handle_env_list_servers(args: Namespace) -> int:
727
+ """Handle 'hatch env list servers' command.
728
+
729
+ Lists environment/server/host deployments from environment data.
730
+ Shows only Hatch-managed packages. Undeployed packages show '-' in Host column.
731
+
732
+ Args:
733
+ args: Namespace with:
734
+ - env_manager: HatchEnvironmentManager instance
735
+ - env: Optional regex pattern to filter by environment name
736
+ - host: Optional regex pattern to filter by host name (use '-' for undeployed)
737
+ - json: Optional flag for JSON output
738
+
739
+ Returns:
740
+ Exit code (0 for success, 1 for error)
741
+
742
+ Reference: R10 §3.4 (10-namespace_consistency_specification_v2.md)
743
+ """
744
+ import json as json_module
745
+ import re
746
+
747
+ env_manager: "HatchEnvironmentManager" = args.env_manager
748
+ env_pattern: str = getattr(args, 'env', None)
749
+ host_pattern: str = getattr(args, 'host', None)
750
+ json_output: bool = getattr(args, 'json', False)
751
+
752
+ # Compile regex patterns if provided
753
+ env_re = None
754
+ if env_pattern:
755
+ try:
756
+ env_re = re.compile(env_pattern)
757
+ except re.error as e:
758
+ format_validation_error(ValidationError(
759
+ f"Invalid env regex pattern: {e}",
760
+ field="--env",
761
+ suggestion="Use a valid Python regex pattern"
762
+ ))
763
+ return EXIT_ERROR
764
+
765
+ # Special handling for '-' (undeployed filter)
766
+ filter_undeployed = host_pattern == "-"
767
+ host_re = None
768
+ if host_pattern and not filter_undeployed:
769
+ try:
770
+ host_re = re.compile(host_pattern)
771
+ except re.error as e:
772
+ format_validation_error(ValidationError(
773
+ f"Invalid host regex pattern: {e}",
774
+ field="--host",
775
+ suggestion="Use a valid Python regex pattern"
776
+ ))
777
+ return EXIT_ERROR
778
+
779
+ # Get all environments
780
+ environments = env_manager.list_environments()
781
+
782
+ # Collect rows: (environment, server, host, version)
783
+ rows = []
784
+
785
+ for env_info in environments:
786
+ env_name = env_info.get("name", env_info) if isinstance(env_info, dict) else env_info
787
+
788
+ # Apply environment filter
789
+ if env_re and not env_re.search(env_name):
790
+ continue
791
+
792
+ try:
793
+ env_data = env_manager.get_environment_data(env_name)
794
+ packages = env_data.get("packages", []) if isinstance(env_data, dict) else []
795
+
796
+ for pkg in packages:
797
+ pkg_name = pkg.get("name") if isinstance(pkg, dict) else None
798
+ pkg_version = pkg.get("version", "-") if isinstance(pkg, dict) else "-"
799
+ configured_hosts = pkg.get("configured_hosts", {}) if isinstance(pkg, dict) else {}
800
+
801
+ if not pkg_name:
802
+ continue
803
+
804
+ if configured_hosts:
805
+ # Package is deployed to one or more hosts
806
+ for host_name in configured_hosts.keys():
807
+ # Apply host filter
808
+ if filter_undeployed:
809
+ # Skip deployed packages when filtering for undeployed
810
+ continue
811
+ if host_re and not host_re.search(host_name):
812
+ continue
813
+ rows.append((env_name, pkg_name, host_name, pkg_version))
814
+ else:
815
+ # Package is not deployed (undeployed)
816
+ if host_re:
817
+ # Skip undeployed when filtering by specific host pattern
818
+ continue
819
+ if not filter_undeployed and host_pattern:
820
+ # Skip undeployed when filtering by host (unless specifically filtering for undeployed)
821
+ continue
822
+ rows.append((env_name, pkg_name, "-", pkg_version))
823
+ except Exception:
824
+ continue
825
+
826
+ # Sort rows by environment (alphabetically), then server, then host
827
+ rows.sort(key=lambda x: (x[0], x[1], x[2]))
828
+
829
+ # JSON output per R10 §8
830
+ if json_output:
831
+ rows_data = []
832
+ for env, server, host, version in rows:
833
+ rows_data.append({
834
+ "environment": env,
835
+ "server": server,
836
+ "host": host if host != "-" else None,
837
+ "version": version
838
+ })
839
+ print(json_module.dumps({"rows": rows_data}, indent=2))
840
+ return EXIT_SUCCESS
841
+
842
+ # Display results
843
+ if not rows:
844
+ if env_pattern or host_pattern:
845
+ print("No matching environment server deployments found")
846
+ else:
847
+ print("No environment server deployments found")
848
+ return EXIT_SUCCESS
849
+
850
+ print("Environment Servers:")
851
+
852
+ # Define table columns per R10 §3.4: Environment → Server → Host → Version
853
+ columns = [
854
+ ColumnDef(name="Environment", width=15),
855
+ ColumnDef(name="Server", width=18),
856
+ ColumnDef(name="Host", width=18),
857
+ ColumnDef(name="Version", width=10),
858
+ ]
859
+ formatter = TableFormatter(columns)
860
+
861
+ for env, server, host, version in rows:
862
+ formatter.add_row([env, server, host, version])
863
+
864
+ print(formatter.render())
865
+ return EXIT_SUCCESS