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_utils.py ADDED
@@ -0,0 +1,1289 @@
1
+ """Shared utilities for Hatch CLI.
2
+
3
+ This module provides common utilities used across CLI handlers, extracted
4
+ from the monolithic cli_hatch.py to enable cleaner handler-based architecture
5
+ and easier testing.
6
+
7
+ Constants:
8
+ EXIT_SUCCESS (int): Exit code for successful operations (0)
9
+ EXIT_ERROR (int): Exit code for failed operations (1)
10
+
11
+ Classes:
12
+ Color: ANSI color codes with brightness variants for tense distinction
13
+
14
+ Functions:
15
+ get_hatch_version(): Retrieve version from package metadata
16
+ request_confirmation(): Interactive user confirmation with auto-approve support
17
+ parse_env_vars(): Parse KEY=VALUE environment variable arguments
18
+ parse_header(): Parse KEY=VALUE HTTP header arguments
19
+ parse_input(): Parse VSCode input configurations
20
+ parse_host_list(): Parse comma-separated host list or 'all'
21
+ get_package_mcp_server_config(): Extract MCP server config from package metadata
22
+ _colors_enabled(): Check if color output should be enabled
23
+
24
+ Example:
25
+ >>> from hatch.cli.cli_utils import EXIT_SUCCESS, EXIT_ERROR, request_confirmation
26
+ >>> if request_confirmation("Proceed?", auto_approve=False):
27
+ ... return EXIT_SUCCESS
28
+ ... else:
29
+ ... return EXIT_ERROR
30
+
31
+ >>> from hatch.cli.cli_utils import parse_env_vars
32
+ >>> env_dict = parse_env_vars(["API_KEY=secret", "DEBUG=true"])
33
+ >>> # Returns: {"API_KEY": "secret", "DEBUG": "true"}
34
+ """
35
+
36
+ from enum import Enum
37
+ from importlib.metadata import PackageNotFoundError, version
38
+
39
+
40
+ # =============================================================================
41
+ # Color Infrastructure for CLI Output
42
+ # =============================================================================
43
+
44
+ import os as _os
45
+
46
+
47
+ def _supports_truecolor() -> bool:
48
+ """Detect if terminal supports 24-bit true color.
49
+
50
+ Checks environment variables and terminal identifiers to determine
51
+ if the terminal supports true color (24-bit RGB) output.
52
+
53
+ Reference: R12 §3.1 (12-enhancing_colors_v0.md)
54
+
55
+ Detection Logic:
56
+ 1. COLORTERM='truecolor' or '24bit' → True
57
+ 2. TERM contains 'truecolor' or '24bit' → True
58
+ 3. TERM_PROGRAM in known true color terminals → True
59
+ 4. WT_SESSION set (Windows Terminal) → True
60
+ 5. Otherwise → False (fallback to 16-color)
61
+
62
+ Returns:
63
+ bool: True if terminal supports true color, False otherwise.
64
+
65
+ Example:
66
+ >>> if _supports_truecolor():
67
+ ... # Use 24-bit RGB color codes
68
+ ... color = "\\033[38;2;128;201;144m"
69
+ ... else:
70
+ ... # Use 16-color ANSI codes
71
+ ... color = "\\033[92m"
72
+ """
73
+ # Check COLORTERM for 'truecolor' or '24bit'
74
+ colorterm = _os.environ.get('COLORTERM', '')
75
+ if colorterm in ('truecolor', '24bit'):
76
+ return True
77
+
78
+ # Check TERM for truecolor indicators
79
+ term = _os.environ.get('TERM', '')
80
+ if 'truecolor' in term or '24bit' in term:
81
+ return True
82
+
83
+ # Check TERM_PROGRAM for known true color terminals
84
+ term_program = _os.environ.get('TERM_PROGRAM', '')
85
+ if term_program in ('iTerm.app', 'Apple_Terminal', 'vscode', 'Hyper'):
86
+ return True
87
+
88
+ # Check WT_SESSION for Windows Terminal
89
+ if _os.environ.get('WT_SESSION'):
90
+ return True
91
+
92
+ return False
93
+
94
+
95
+ # Module-level constant for true color support detection
96
+ # Evaluated once at module load time
97
+ TRUECOLOR = _supports_truecolor()
98
+
99
+
100
+ class Color(Enum):
101
+ """HCL color palette with true color support and 16-color fallback.
102
+
103
+ Uses a qualitative HCL palette with equal perceived brightness
104
+ for accessibility and visual harmony. True color (24-bit) is used
105
+ when supported, falling back to standard 16-color ANSI codes.
106
+
107
+ Reference: R12 §3.2 (12-enhancing_colors_v0.md)
108
+ Reference: R06 §3.1 (06-dependency_analysis_v0.md)
109
+ Reference: R03 §4 (03-mutation_output_specification_v0.md)
110
+
111
+ HCL Palette Values:
112
+ GREEN #80C990 → rgb(128, 201, 144)
113
+ RED #EFA6A2 → rgb(239, 166, 162)
114
+ YELLOW #C8C874 → rgb(200, 200, 116)
115
+ BLUE #A3B8EF → rgb(163, 184, 239)
116
+ MAGENTA #E6A3DC → rgb(230, 163, 220)
117
+ CYAN #50CACD → rgb(80, 202, 205)
118
+ GRAY #808080 → rgb(128, 128, 128)
119
+ AMBER #A69460 → rgb(166, 148, 96)
120
+
121
+ Color Semantics:
122
+ Green → Constructive (CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE)
123
+ Blue → Recovery (RESTORE)
124
+ Red → Destructive (REMOVE, DELETE, CLEAN)
125
+ Yellow → Modification (SET, UPDATE)
126
+ Magenta → Transfer (SYNC)
127
+ Cyan → Informational (VALIDATE)
128
+ Gray → No-op (SKIP, EXISTS, UNCHANGED)
129
+ Amber → Entity highlighting (show commands)
130
+
131
+ Example:
132
+ >>> from hatch.cli.cli_utils import Color, _colors_enabled
133
+ >>> if _colors_enabled():
134
+ ... print(f"{Color.GREEN.value}Success{Color.RESET.value}")
135
+ ... else:
136
+ ... print("Success")
137
+ """
138
+
139
+ # === Bright colors (execution results - past tense) ===
140
+
141
+ # Green #80C990 - CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE
142
+ GREEN = "\033[38;2;128;201;144m" if TRUECOLOR else "\033[92m"
143
+
144
+ # Red #EFA6A2 - REMOVE, DELETE, CLEAN
145
+ RED = "\033[38;2;239;166;162m" if TRUECOLOR else "\033[91m"
146
+
147
+ # Yellow #C8C874 - SET, UPDATE
148
+ YELLOW = "\033[38;2;200;200;116m" if TRUECOLOR else "\033[93m"
149
+
150
+ # Blue #A3B8EF - RESTORE
151
+ BLUE = "\033[38;2;163;184;239m" if TRUECOLOR else "\033[94m"
152
+
153
+ # Magenta #E6A3DC - SYNC
154
+ MAGENTA = "\033[38;2;230;163;220m" if TRUECOLOR else "\033[95m"
155
+
156
+ # Cyan #50CACD - VALIDATE
157
+ CYAN = "\033[38;2;80;202;205m" if TRUECOLOR else "\033[96m"
158
+
159
+ # === Dim colors (confirmation prompts - present tense) ===
160
+
161
+ # Aquamarine #5ACCAF (green shifted)
162
+ GREEN_DIM = "\033[38;2;90;204;175m" if TRUECOLOR else "\033[2;32m"
163
+
164
+ # Orange #E0AF85 (red shifted)
165
+ RED_DIM = "\033[38;2;224;175;133m" if TRUECOLOR else "\033[2;31m"
166
+
167
+ # Amber #A69460 (yellow shifted)
168
+ YELLOW_DIM = "\033[38;2;166;148;96m" if TRUECOLOR else "\033[2;33m"
169
+
170
+ # Violet #CCACED (blue shifted)
171
+ BLUE_DIM = "\033[38;2;204;172;237m" if TRUECOLOR else "\033[2;34m"
172
+
173
+ # Rose #F2A1C2 (magenta shifted)
174
+ MAGENTA_DIM = "\033[38;2;242;161;194m" if TRUECOLOR else "\033[2;35m"
175
+
176
+ # Azure #74C3E4 (cyan shifted)
177
+ CYAN_DIM = "\033[38;2;116;195;228m" if TRUECOLOR else "\033[2;36m"
178
+
179
+ # === Utility colors ===
180
+
181
+ # Gray #808080 - SKIP, EXISTS, UNCHANGED
182
+ GRAY = "\033[38;2;128;128;128m" if TRUECOLOR else "\033[90m"
183
+
184
+ # Amber #A69460 - Entity name highlighting (NEW)
185
+ AMBER = "\033[38;2;166;148;96m" if TRUECOLOR else "\033[33m"
186
+
187
+ # Reset
188
+ RESET = "\033[0m"
189
+
190
+
191
+ def _supports_unicode() -> bool:
192
+ """Check if terminal supports UTF-8 for unicode symbols.
193
+
194
+ Used to determine whether to use ✓/✗ symbols or ASCII fallback (+/x)
195
+ in partial success reporting.
196
+
197
+ Reference: R13 §12.3 (13-error_message_formatting_v0.md)
198
+
199
+ Returns:
200
+ bool: True if terminal supports UTF-8, False otherwise.
201
+
202
+ Example:
203
+ >>> if _supports_unicode():
204
+ ... success_symbol = "✓"
205
+ ... else:
206
+ ... success_symbol = "+"
207
+ """
208
+ import locale
209
+ encoding = locale.getpreferredencoding(False)
210
+ return encoding.lower() in ('utf-8', 'utf8')
211
+
212
+
213
+ def _colors_enabled() -> bool:
214
+ """Check if color output should be enabled.
215
+
216
+ Colors are disabled when:
217
+ - NO_COLOR environment variable is set to a non-empty value
218
+ - stdout is not a TTY (e.g., piped output, CI environment)
219
+
220
+ Reference: R05 §3.4 (05-test_definition_v0.md)
221
+
222
+ Returns:
223
+ bool: True if colors should be enabled, False otherwise.
224
+
225
+ Example:
226
+ >>> if _colors_enabled():
227
+ ... print(f"{Color.GREEN.value}colored{Color.RESET.value}")
228
+ ... else:
229
+ ... print("plain")
230
+ """
231
+ import os
232
+ import sys
233
+
234
+ # Check NO_COLOR environment variable (https://no-color.org/)
235
+ no_color = os.environ.get('NO_COLOR', '')
236
+ if no_color: # Any non-empty value disables colors
237
+ return False
238
+
239
+ # Check if stdout is a TTY
240
+ if not sys.stdout.isatty():
241
+ return False
242
+
243
+ return True
244
+
245
+
246
+ def highlight(text: str) -> str:
247
+ """Apply highlight formatting (bold + amber) to entity names.
248
+
249
+ Used in show commands to emphasize host and server names for
250
+ quick visual scanning of detailed output.
251
+
252
+ Reference: R12 §3.3 (12-enhancing_colors_v0.md)
253
+ Reference: R11 §3.2 (11-enhancing_show_command_v0.md)
254
+
255
+ Args:
256
+ text: The entity name to highlight
257
+
258
+ Returns:
259
+ str: Text with bold + amber formatting if colors enabled,
260
+ otherwise plain text.
261
+
262
+ Example:
263
+ >>> print(f"MCP Host: {highlight('claude-desktop')}")
264
+ MCP Host: claude-desktop # (bold + amber in TTY)
265
+ """
266
+ if _colors_enabled():
267
+ # Bold (\033[1m) + Amber color
268
+ return f"\033[1m{Color.AMBER.value}{text}{Color.RESET.value}"
269
+ return text
270
+
271
+
272
+ class ConsequenceType(Enum):
273
+ """Action types with dual-tense labels and semantic colors.
274
+
275
+ Each consequence type has:
276
+ - prompt_label: Present tense for confirmation prompts (e.g., "CREATE")
277
+ - result_label: Past tense for execution results (e.g., "CREATED")
278
+ - prompt_color: Dim color for prompts
279
+ - result_color: Bright color for results
280
+
281
+ Reference: R06 §3.2 (06-dependency_analysis_v0.md)
282
+ Reference: R03 §2 (03-mutation_output_specification_v0.md)
283
+
284
+ Categories:
285
+ Constructive (Green): CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE
286
+ Recovery (Blue): RESTORE
287
+ Destructive (Red): REMOVE, DELETE, CLEAN
288
+ Modification (Yellow): SET, UPDATE
289
+ Transfer (Magenta): SYNC
290
+ Informational (Cyan): VALIDATE
291
+ No-op (Gray): SKIP, EXISTS, UNCHANGED
292
+
293
+ Example:
294
+ >>> ct = ConsequenceType.CREATE
295
+ >>> print(f"[{ct.prompt_label}]") # [CREATE]
296
+ >>> print(f"[{ct.result_label}]") # [CREATED]
297
+ """
298
+
299
+ # Value format: (prompt_label, result_label, prompt_color, result_color)
300
+
301
+ # Constructive actions (Green)
302
+ CREATE = ("CREATE", "CREATED", Color.GREEN_DIM, Color.GREEN)
303
+ ADD = ("ADD", "ADDED", Color.GREEN_DIM, Color.GREEN)
304
+ CONFIGURE = ("CONFIGURE", "CONFIGURED", Color.GREEN_DIM, Color.GREEN)
305
+ INSTALL = ("INSTALL", "INSTALLED", Color.GREEN_DIM, Color.GREEN)
306
+ INITIALIZE = ("INITIALIZE", "INITIALIZED", Color.GREEN_DIM, Color.GREEN)
307
+
308
+ # Recovery actions (Blue)
309
+ RESTORE = ("RESTORE", "RESTORED", Color.BLUE_DIM, Color.BLUE)
310
+
311
+ # Destructive actions (Red)
312
+ REMOVE = ("REMOVE", "REMOVED", Color.RED_DIM, Color.RED)
313
+ DELETE = ("DELETE", "DELETED", Color.RED_DIM, Color.RED)
314
+ CLEAN = ("CLEAN", "CLEANED", Color.RED_DIM, Color.RED)
315
+
316
+ # Modification actions (Yellow)
317
+ SET = ("SET", "SET", Color.YELLOW_DIM, Color.YELLOW) # Irregular: no change
318
+ UPDATE = ("UPDATE", "UPDATED", Color.YELLOW_DIM, Color.YELLOW)
319
+
320
+ # Transfer actions (Magenta)
321
+ SYNC = ("SYNC", "SYNCED", Color.MAGENTA_DIM, Color.MAGENTA)
322
+
323
+ # Informational actions (Cyan)
324
+ VALIDATE = ("VALIDATE", "VALIDATED", Color.CYAN_DIM, Color.CYAN)
325
+
326
+ # No-op actions (Gray) - same color for prompt and result
327
+ SKIP = ("SKIP", "SKIPPED", Color.GRAY, Color.GRAY)
328
+ EXISTS = ("EXISTS", "EXISTS", Color.GRAY, Color.GRAY) # Irregular: no change
329
+ UNCHANGED = ("UNCHANGED", "UNCHANGED", Color.GRAY, Color.GRAY) # Irregular: no change
330
+
331
+ @property
332
+ def prompt_label(self) -> str:
333
+ """Present tense label for confirmation prompts."""
334
+ return self.value[0]
335
+
336
+ @property
337
+ def result_label(self) -> str:
338
+ """Past tense label for execution results."""
339
+ return self.value[1]
340
+
341
+ @property
342
+ def prompt_color(self) -> Color:
343
+ """Dim color for confirmation prompts."""
344
+ return self.value[2]
345
+
346
+ @property
347
+ def result_color(self) -> Color:
348
+ """Bright color for execution results."""
349
+ return self.value[3]
350
+
351
+
352
+ # =============================================================================
353
+ # ValidationError Exception for Structured Error Reporting
354
+ # =============================================================================
355
+
356
+
357
+ class ValidationError(Exception):
358
+ """Validation error with structured context.
359
+
360
+ Provides structured error information for input validation failures,
361
+ including optional field name and suggestion for resolution.
362
+
363
+ Reference: R13 §4.2.2 (13-error_message_formatting_v0.md)
364
+
365
+ Attributes:
366
+ message: Human-readable error description
367
+ field: Optional field/argument name that caused the error
368
+ suggestion: Optional suggestion for resolving the error
369
+
370
+ Example:
371
+ >>> raise ValidationError(
372
+ ... "Invalid host 'vsc'",
373
+ ... field="--host",
374
+ ... suggestion="Supported hosts: claude-desktop, vscode, cursor"
375
+ ... )
376
+ """
377
+
378
+ def __init__(
379
+ self,
380
+ message: str,
381
+ field: str = None,
382
+ suggestion: str = None
383
+ ):
384
+ """Initialize ValidationError.
385
+
386
+ Args:
387
+ message: Human-readable error description
388
+ field: Optional field/argument name that caused the error
389
+ suggestion: Optional suggestion for resolving the error
390
+ """
391
+ self.message = message
392
+ self.field = field
393
+ self.suggestion = suggestion
394
+ super().__init__(message)
395
+
396
+
397
+ from dataclasses import dataclass, field
398
+ from typing import List
399
+
400
+
401
+ @dataclass
402
+ class Consequence:
403
+ """Data model for a single consequence (resource or field level).
404
+
405
+ Consequences represent actions that will be or have been performed.
406
+ They can be nested to show resource-level actions with field-level details.
407
+
408
+ Reference: R06 §3.3 (06-dependency_analysis_v0.md)
409
+ Reference: R04 §5.1 (04-reporting_infrastructure_coexistence_v0.md)
410
+
411
+ Attributes:
412
+ type: The ConsequenceType indicating the action category
413
+ message: Human-readable description of the consequence
414
+ children: Nested consequences (e.g., field-level details under resource)
415
+
416
+ Invariants:
417
+ - children only populated for resource-level consequences
418
+ - field-level consequences have empty children list
419
+ - nesting limited to 2 levels (resource → field)
420
+
421
+ Example:
422
+ >>> parent = Consequence(
423
+ ... type=ConsequenceType.CONFIGURE,
424
+ ... message="Server 'weather' on 'claude-desktop'",
425
+ ... children=[
426
+ ... Consequence(ConsequenceType.UPDATE, "command: None → 'python'"),
427
+ ... Consequence(ConsequenceType.SKIP, "timeout: unsupported"),
428
+ ... ]
429
+ ... )
430
+ """
431
+
432
+ type: ConsequenceType
433
+ message: str
434
+ children: List["Consequence"] = field(default_factory=list)
435
+
436
+
437
+ from typing import Optional, Tuple
438
+
439
+
440
+ class ResultReporter:
441
+ """Unified rendering system for all CLI output.
442
+
443
+ Tracks consequences and renders them with tense-aware, color-coded output.
444
+ Present tense (dim colors) for confirmation prompts, past tense (bright colors)
445
+ for execution results.
446
+
447
+ Reference: R06 §3.4 (06-dependency_analysis_v0.md)
448
+ Reference: R04 §5.2 (04-reporting_infrastructure_coexistence_v0.md)
449
+ Reference: R01 §8.2 (01-cli_output_analysis_v2.md)
450
+
451
+ Attributes:
452
+ command_name: Display name for the command (e.g., "hatch mcp configure")
453
+ dry_run: If True, append "- DRY RUN" suffix to result labels
454
+ consequences: List of tracked consequences in order of addition
455
+
456
+ Invariants:
457
+ - consequences list is append-only
458
+ - report_prompt() and report_result() are idempotent
459
+ - Order of add() calls determines output order
460
+
461
+ Example:
462
+ >>> reporter = ResultReporter("hatch env create", dry_run=False)
463
+ >>> reporter.add(ConsequenceType.CREATE, "Environment 'dev'")
464
+ >>> reporter.add(ConsequenceType.CREATE, "Python environment (3.11)")
465
+ >>> prompt = reporter.report_prompt() # Present tense, dim colors
466
+ >>> # ... user confirms ...
467
+ >>> reporter.report_result() # Past tense, bright colors
468
+ """
469
+
470
+ def __init__(self, command_name: str, dry_run: bool = False):
471
+ """Initialize ResultReporter.
472
+
473
+ Args:
474
+ command_name: Display name for the command
475
+ dry_run: If True, results show "- DRY RUN" suffix
476
+ """
477
+ self._command_name = command_name
478
+ self._dry_run = dry_run
479
+ self._consequences: List[Consequence] = []
480
+
481
+ @property
482
+ def command_name(self) -> str:
483
+ """Display name for the command."""
484
+ return self._command_name
485
+
486
+ @property
487
+ def dry_run(self) -> bool:
488
+ """Whether this is a dry-run preview."""
489
+ return self._dry_run
490
+
491
+ @property
492
+ def consequences(self) -> List[Consequence]:
493
+ """List of tracked consequences in order of addition."""
494
+ return self._consequences
495
+
496
+ def add(
497
+ self,
498
+ consequence_type: ConsequenceType,
499
+ message: str,
500
+ children: Optional[List[Consequence]] = None
501
+ ) -> None:
502
+ """Add a consequence with optional nested children.
503
+
504
+ Args:
505
+ consequence_type: The type of action
506
+ message: Human-readable description
507
+ children: Optional nested consequences (e.g., field-level details)
508
+
509
+ Invariants:
510
+ - Order of add() calls determines output order
511
+ - Children inherit parent's tense during rendering
512
+ """
513
+ consequence = Consequence(
514
+ type=consequence_type,
515
+ message=message,
516
+ children=children or []
517
+ )
518
+ self._consequences.append(consequence)
519
+
520
+ def add_from_conversion_report(self, report: "ConversionReport") -> None:
521
+ """Convert ConversionReport field operations to nested consequences.
522
+
523
+ Maps ConversionReport data to the unified consequence model:
524
+ - report.operation → resource ConsequenceType
525
+ - field_op "UPDATED" → ConsequenceType.UPDATE
526
+ - field_op "UNSUPPORTED" → ConsequenceType.SKIP
527
+ - field_op "UNCHANGED" → ConsequenceType.UNCHANGED
528
+
529
+ Reference: R06 §3.5 (06-dependency_analysis_v0.md)
530
+ Reference: R04 §1.2 (04-reporting_infrastructure_coexistence_v0.md)
531
+
532
+ Args:
533
+ report: ConversionReport with field operations to convert
534
+
535
+ Invariants:
536
+ - All field operations become children of resource consequence
537
+ - UNSUPPORTED fields include "(unsupported by host)" suffix
538
+ """
539
+ # Import here to avoid circular dependency
540
+ from hatch.mcp_host_config.reporting import ConversionReport
541
+
542
+ # Map report.operation to resource ConsequenceType
543
+ operation_map = {
544
+ "create": ConsequenceType.CONFIGURE,
545
+ "update": ConsequenceType.CONFIGURE,
546
+ "delete": ConsequenceType.REMOVE,
547
+ "migrate": ConsequenceType.CONFIGURE,
548
+ }
549
+ resource_type = operation_map.get(report.operation, ConsequenceType.CONFIGURE)
550
+
551
+ # Build resource message
552
+ resource_message = f"Server '{report.server_name}' on '{report.target_host.value}'"
553
+
554
+ # Map field operations to child consequences
555
+ field_op_map = {
556
+ "UPDATED": ConsequenceType.UPDATE,
557
+ "UNSUPPORTED": ConsequenceType.SKIP,
558
+ "UNCHANGED": ConsequenceType.UNCHANGED,
559
+ }
560
+
561
+ children = []
562
+ for field_op in report.field_operations:
563
+ child_type = field_op_map.get(field_op.operation, ConsequenceType.UPDATE)
564
+
565
+ # Format field message based on operation type
566
+ if field_op.operation == "UPDATED":
567
+ child_message = f"{field_op.field_name}: {repr(field_op.old_value)} → {repr(field_op.new_value)}"
568
+ elif field_op.operation == "UNSUPPORTED":
569
+ child_message = f"{field_op.field_name}: (unsupported by host)"
570
+ else: # UNCHANGED
571
+ child_message = f"{field_op.field_name}: {repr(field_op.new_value)}"
572
+
573
+ children.append(Consequence(type=child_type, message=child_message))
574
+
575
+ # Add the resource consequence with children
576
+ self.add(resource_type, resource_message, children=children)
577
+
578
+ def _format_consequence(
579
+ self,
580
+ consequence: Consequence,
581
+ use_result_tense: bool,
582
+ indent: int = 2
583
+ ) -> str:
584
+ """Format a single consequence with color and tense.
585
+
586
+ Args:
587
+ consequence: The consequence to format
588
+ use_result_tense: True for past tense (result), False for present (prompt)
589
+ indent: Number of spaces for indentation
590
+
591
+ Returns:
592
+ Formatted string with optional ANSI colors
593
+ """
594
+ ct = consequence.type
595
+ label = ct.result_label if use_result_tense else ct.prompt_label
596
+ color = ct.result_color if use_result_tense else ct.prompt_color
597
+
598
+ # Add dry-run suffix for results
599
+ if use_result_tense and self._dry_run:
600
+ label = f"{label} - DRY RUN"
601
+
602
+ # Format with or without colors
603
+ indent_str = " " * indent
604
+ if _colors_enabled():
605
+ line = f"{indent_str}{color.value}[{label}]{Color.RESET.value} {consequence.message}"
606
+ else:
607
+ line = f"{indent_str}[{label}] {consequence.message}"
608
+
609
+ return line
610
+
611
+ def report_prompt(self) -> str:
612
+ """Generate confirmation prompt (present tense, dim colors).
613
+
614
+ Output format:
615
+ {command_name}:
616
+ [VERB] resource message
617
+ [VERB] field message
618
+ [VERB] field message
619
+
620
+ Returns:
621
+ Formatted prompt string, empty string if no consequences.
622
+
623
+ Invariants:
624
+ - All consequences shown (including UNCHANGED, SKIP)
625
+ - Empty string if no consequences
626
+ """
627
+ if not self._consequences:
628
+ return ""
629
+
630
+ lines = [f"{self._command_name}:"]
631
+
632
+ for consequence in self._consequences:
633
+ lines.append(self._format_consequence(consequence, use_result_tense=False))
634
+ for child in consequence.children:
635
+ lines.append(self._format_consequence(child, use_result_tense=False, indent=4))
636
+
637
+ return "\n".join(lines)
638
+
639
+ def report_result(self) -> None:
640
+ """Print execution results (past tense, bright colors).
641
+
642
+ Output format:
643
+ [SUCCESS] summary (or [DRY RUN] for dry-run mode)
644
+ [VERB-ED] resource message
645
+ [VERB-ED] field message (only changed fields)
646
+
647
+ Invariants:
648
+ - UNCHANGED and SKIP fields may be omitted from result (noise reduction)
649
+ - Dry-run appends "- DRY RUN" suffix
650
+ - No output if consequences list is empty
651
+ """
652
+ if not self._consequences:
653
+ return
654
+
655
+ # Print header
656
+ if self._dry_run:
657
+ if _colors_enabled():
658
+ print(f"{Color.CYAN.value}[DRY RUN]{Color.RESET.value} Preview of changes:")
659
+ else:
660
+ print("[DRY RUN] Preview of changes:")
661
+ else:
662
+ if _colors_enabled():
663
+ print(f"{Color.GREEN.value}[SUCCESS]{Color.RESET.value} Operation completed:")
664
+ else:
665
+ print("[SUCCESS] Operation completed:")
666
+
667
+ # Print consequences
668
+ for consequence in self._consequences:
669
+ print(self._format_consequence(consequence, use_result_tense=True))
670
+ for child in consequence.children:
671
+ # Optionally filter out UNCHANGED/SKIP in results for noise reduction
672
+ # For now, show all for transparency
673
+ print(self._format_consequence(child, use_result_tense=True, indent=4))
674
+
675
+ def report_error(self, summary: str, details: Optional[List[str]] = None) -> None:
676
+ """Report execution failure with structured details.
677
+
678
+ Prints error message with [ERROR] prefix in bright red color (when colors enabled).
679
+ Details are indented with 2 spaces for visual hierarchy.
680
+
681
+ Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)
682
+
683
+ Args:
684
+ summary: High-level error description
685
+ details: Optional list of detail lines to print below summary
686
+
687
+ Output format:
688
+ [ERROR] <summary>
689
+ <detail_line_1>
690
+ <detail_line_2>
691
+
692
+ Example:
693
+ >>> reporter = ResultReporter("hatch env create")
694
+ >>> reporter.report_error(
695
+ ... "Failed to create environment 'dev'",
696
+ ... details=["Python environment creation failed: conda not available"]
697
+ ... )
698
+ [ERROR] Failed to create environment 'dev'
699
+ Python environment creation failed: conda not available
700
+ """
701
+ if not summary:
702
+ return
703
+
704
+ # Print error header with color
705
+ if _colors_enabled():
706
+ print(f"{Color.RED.value}[ERROR]{Color.RESET.value} {summary}")
707
+ else:
708
+ print(f"[ERROR] {summary}")
709
+
710
+ # Print details with indentation
711
+ if details:
712
+ for detail in details:
713
+ print(f" {detail}")
714
+
715
+ def report_partial_success(
716
+ self,
717
+ summary: str,
718
+ successes: List[str],
719
+ failures: List[Tuple[str, str]]
720
+ ) -> None:
721
+ """Report mixed success/failure results with ✓/✗ symbols.
722
+
723
+ Prints warning message with [WARNING] prefix in bright yellow color.
724
+ Uses ✓/✗ symbols for success/failure items (with ASCII fallback).
725
+ Includes summary line showing success ratio.
726
+
727
+ Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)
728
+
729
+ Args:
730
+ summary: High-level summary description
731
+ successes: List of successful item descriptions
732
+ failures: List of (item, reason) tuples for failed items
733
+
734
+ Output format:
735
+ [WARNING] <summary>
736
+ ✓ <success_item>
737
+ ✗ <failure_item>: <reason>
738
+ Summary: X/Y succeeded
739
+
740
+ Example:
741
+ >>> reporter = ResultReporter("hatch mcp sync")
742
+ >>> reporter.report_partial_success(
743
+ ... "Partial synchronization",
744
+ ... successes=["claude-desktop (backup: ~/.hatch/backups/...)"],
745
+ ... failures=[("cursor", "Config file not found")]
746
+ ... )
747
+ [WARNING] Partial synchronization
748
+ ✓ claude-desktop (backup: ~/.hatch/backups/...)
749
+ ✗ cursor: Config file not found
750
+ Summary: 1/2 succeeded
751
+ """
752
+ # Determine symbols based on unicode support
753
+ success_symbol = "✓" if _supports_unicode() else "+"
754
+ failure_symbol = "✗" if _supports_unicode() else "x"
755
+
756
+ # Print warning header with color
757
+ if _colors_enabled():
758
+ print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} {summary}")
759
+ else:
760
+ print(f"[WARNING] {summary}")
761
+
762
+ # Print success items
763
+ for item in successes:
764
+ if _colors_enabled():
765
+ print(f" {Color.GREEN.value}{success_symbol}{Color.RESET.value} {item}")
766
+ else:
767
+ print(f" {success_symbol} {item}")
768
+
769
+ # Print failure items
770
+ for item, reason in failures:
771
+ if _colors_enabled():
772
+ print(f" {Color.RED.value}{failure_symbol}{Color.RESET.value} {item}: {reason}")
773
+ else:
774
+ print(f" {failure_symbol} {item}: {reason}")
775
+
776
+ # Print summary line
777
+ total = len(successes) + len(failures)
778
+ succeeded = len(successes)
779
+ print(f" Summary: {succeeded}/{total} succeeded")
780
+
781
+
782
+ # =============================================================================
783
+ # Error Formatting Utilities
784
+ # =============================================================================
785
+
786
+
787
+ def format_validation_error(error: "ValidationError") -> None:
788
+ """Print formatted validation error with color.
789
+
790
+ Prints error message with [ERROR] prefix in bright red color.
791
+ Optionally includes field name and suggestion if provided.
792
+
793
+ Reference: R13 §4.3 (13-error_message_formatting_v0.md)
794
+
795
+ Args:
796
+ error: ValidationError instance with message, field, and suggestion
797
+
798
+ Output format:
799
+ [ERROR] <message>
800
+ Field: <field> (if provided)
801
+ Suggestion: <suggestion> (if provided)
802
+
803
+ Example:
804
+ >>> from hatch.cli.cli_utils import ValidationError, format_validation_error
805
+ >>> format_validation_error(ValidationError(
806
+ ... "Invalid host 'vsc'",
807
+ ... field="--host",
808
+ ... suggestion="Supported hosts: claude-desktop, vscode, cursor"
809
+ ... ))
810
+ [ERROR] Invalid host 'vsc'
811
+ Field: --host
812
+ Suggestion: Supported hosts: claude-desktop, vscode, cursor
813
+ """
814
+ # Print error header with color
815
+ if _colors_enabled():
816
+ print(f"{Color.RED.value}[ERROR]{Color.RESET.value} {error.message}")
817
+ else:
818
+ print(f"[ERROR] {error.message}")
819
+
820
+ # Print field if provided
821
+ if error.field:
822
+ print(f" Field: {error.field}")
823
+
824
+ # Print suggestion if provided
825
+ if error.suggestion:
826
+ print(f" Suggestion: {error.suggestion}")
827
+
828
+
829
+ def format_info(message: str) -> None:
830
+ """Print formatted info message with color.
831
+
832
+ Prints message with [INFO] prefix in bright blue color.
833
+ Used for informational messages like "Operation cancelled".
834
+
835
+ Reference: R13-B §B.6.2 (13-error_message_formatting_appendix_b_v0.md)
836
+
837
+ Args:
838
+ message: Info message to display
839
+
840
+ Output format:
841
+ [INFO] <message>
842
+
843
+ Example:
844
+ >>> from hatch.cli.cli_utils import format_info
845
+ >>> format_info("Operation cancelled")
846
+ [INFO] Operation cancelled
847
+ """
848
+ if _colors_enabled():
849
+ print(f"{Color.BLUE.value}[INFO]{Color.RESET.value} {message}")
850
+ else:
851
+ print(f"[INFO] {message}")
852
+
853
+
854
+ def format_warning(message: str, suggestion: str = None) -> None:
855
+ """Print formatted warning message with color.
856
+
857
+ Prints message with [WARNING] prefix in bright yellow color.
858
+ Used for non-fatal warnings that don't prevent operation completion.
859
+
860
+ Reference: R13-A §A.5 P3 (13-error_message_formatting_appendix_a_v0.md)
861
+
862
+ Args:
863
+ message: Warning message to display
864
+ suggestion: Optional suggestion for resolution
865
+
866
+ Output format:
867
+ [WARNING] <message>
868
+ Suggestion: <suggestion> (if provided)
869
+
870
+ Example:
871
+ >>> from hatch.cli.cli_utils import format_warning
872
+ >>> format_warning("Invalid header format 'foo'", suggestion="Expected KEY=VALUE")
873
+ [WARNING] Invalid header format 'foo'
874
+ Suggestion: Expected KEY=VALUE
875
+ """
876
+ if _colors_enabled():
877
+ print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} {message}")
878
+ else:
879
+ print(f"[WARNING] {message}")
880
+
881
+ if suggestion:
882
+ print(f" Suggestion: {suggestion}")
883
+
884
+
885
+ # =============================================================================
886
+ # TableFormatter Infrastructure for List Commands
887
+ # =============================================================================
888
+
889
+ from typing import Union, Literal
890
+
891
+
892
+ @dataclass
893
+ class ColumnDef:
894
+ """Column definition for TableFormatter.
895
+
896
+ Reference: R06 §3.6 (06-dependency_analysis_v0.md)
897
+ Reference: R02 §5 (02-list_output_format_specification_v2.md)
898
+
899
+ Attributes:
900
+ name: Column header text
901
+ width: Fixed width (int) or "auto" for auto-calculation
902
+ align: Text alignment ("left", "right", "center")
903
+
904
+ Example:
905
+ >>> col = ColumnDef(name="Name", width=20, align="left")
906
+ >>> col_auto = ColumnDef(name="Count", width="auto", align="right")
907
+ """
908
+
909
+ name: str
910
+ width: Union[int, Literal["auto"]]
911
+ align: Literal["left", "right", "center"] = "left"
912
+
913
+
914
+ class TableFormatter:
915
+ """Aligned table output for list commands.
916
+
917
+ Renders data as aligned columns with headers and separator line.
918
+ Supports fixed and auto-calculated column widths.
919
+
920
+ Reference: R06 §3.6 (06-dependency_analysis_v0.md)
921
+ Reference: R02 §5 (02-list_output_format_specification_v2.md)
922
+
923
+ Attributes:
924
+ columns: List of column definitions
925
+
926
+ Example:
927
+ >>> columns = [
928
+ ... ColumnDef(name="Name", width=20),
929
+ ... ColumnDef(name="Status", width=10),
930
+ ... ]
931
+ >>> formatter = TableFormatter(columns)
932
+ >>> formatter.add_row(["my-server", "active"])
933
+ >>> print(formatter.render())
934
+ Name Status
935
+ ─────────────────────────────────
936
+ my-server active
937
+ """
938
+
939
+ def __init__(self, columns: List[ColumnDef]):
940
+ """Initialize TableFormatter with column definitions.
941
+
942
+ Args:
943
+ columns: List of ColumnDef specifying table structure
944
+ """
945
+ self._columns = columns
946
+ self._rows: List[List[str]] = []
947
+
948
+ def add_row(self, values: List[str]) -> None:
949
+ """Add a data row to the table.
950
+
951
+ Args:
952
+ values: List of string values, one per column
953
+ """
954
+ self._rows.append(values)
955
+
956
+ def _calculate_widths(self) -> List[int]:
957
+ """Calculate actual column widths, resolving 'auto' widths.
958
+
959
+ Returns:
960
+ List of integer widths for each column
961
+ """
962
+ widths = []
963
+ for i, col in enumerate(self._columns):
964
+ if col.width == "auto":
965
+ # Calculate from header and all row values
966
+ max_width = len(col.name)
967
+ for row in self._rows:
968
+ if i < len(row):
969
+ max_width = max(max_width, len(row[i]))
970
+ widths.append(max_width)
971
+ else:
972
+ widths.append(col.width)
973
+ return widths
974
+
975
+ def _align_value(self, value: str, width: int, align: str) -> str:
976
+ """Align a value within the specified width.
977
+
978
+ Args:
979
+ value: The string value to align
980
+ width: Target width
981
+ align: Alignment type ("left", "right", "center")
982
+
983
+ Returns:
984
+ Aligned string, truncated with ellipsis if too long
985
+ """
986
+ # Truncate if too long
987
+ if len(value) > width:
988
+ if width > 1:
989
+ return value[:width - 1] + "…"
990
+ return value[:width]
991
+
992
+ # Apply alignment
993
+ if align == "right":
994
+ return value.rjust(width)
995
+ elif align == "center":
996
+ return value.center(width)
997
+ else: # left (default)
998
+ return value.ljust(width)
999
+
1000
+ def render(self) -> str:
1001
+ """Render the table as a formatted string.
1002
+
1003
+ Returns:
1004
+ Multi-line string with headers, separator, and data rows
1005
+ """
1006
+ widths = self._calculate_widths()
1007
+ lines = []
1008
+
1009
+ # Header row
1010
+ header_parts = []
1011
+ for i, col in enumerate(self._columns):
1012
+ header_parts.append(self._align_value(col.name, widths[i], col.align))
1013
+ lines.append(" " + " ".join(header_parts))
1014
+
1015
+ # Separator line
1016
+ total_width = sum(widths) + (len(widths) - 1) * 2 + 2 # columns + separators + indent
1017
+ lines.append(" " + "─" * (total_width - 2))
1018
+
1019
+ # Data rows
1020
+ for row in self._rows:
1021
+ row_parts = []
1022
+ for i, col in enumerate(self._columns):
1023
+ value = row[i] if i < len(row) else ""
1024
+ row_parts.append(self._align_value(value, widths[i], col.align))
1025
+ lines.append(" " + " ".join(row_parts))
1026
+
1027
+ return "\n".join(lines)
1028
+
1029
+
1030
+ # Exit code constants for consistent CLI return values
1031
+ EXIT_SUCCESS = 0
1032
+ EXIT_ERROR = 1
1033
+
1034
+
1035
+ def get_hatch_version() -> str:
1036
+ """Get Hatch version from package metadata.
1037
+
1038
+ Returns:
1039
+ str: Version string from package metadata, or 'unknown (development mode)'
1040
+ if package is not installed.
1041
+ """
1042
+ try:
1043
+ return version("hatch")
1044
+ except PackageNotFoundError:
1045
+ return "unknown (development mode)"
1046
+
1047
+
1048
+ import os
1049
+ import sys
1050
+ from typing import Optional
1051
+
1052
+
1053
+ def request_confirmation(message: str, auto_approve: bool = False) -> bool:
1054
+ """Request user confirmation with non-TTY support following Hatch patterns.
1055
+
1056
+ Args:
1057
+ message: The confirmation message to display
1058
+ auto_approve: If True, automatically approve without prompting
1059
+
1060
+ Returns:
1061
+ bool: True if confirmed, False otherwise
1062
+ """
1063
+ # Check for auto-approve first
1064
+ if auto_approve or os.getenv("HATCH_AUTO_APPROVE", "").lower() in (
1065
+ "1",
1066
+ "true",
1067
+ "yes",
1068
+ ):
1069
+ return True
1070
+
1071
+ # Interactive mode - request user input (works in both TTY and test environments)
1072
+ try:
1073
+ while True:
1074
+ response = input(f"{message} [y/N]: ").strip().lower()
1075
+ if response in ["y", "yes"]:
1076
+ return True
1077
+ elif response in ["n", "no", ""]:
1078
+ return False
1079
+ else:
1080
+ print("Please enter 'y' for yes or 'n' for no.")
1081
+ except (EOFError, KeyboardInterrupt):
1082
+ # Only auto-approve on EOF/interrupt if not in TTY (non-interactive environment)
1083
+ if not sys.stdin.isatty():
1084
+ return True
1085
+ return False
1086
+
1087
+
1088
+ def parse_env_vars(env_list: Optional[list]) -> dict:
1089
+ """Parse environment variables from command line format.
1090
+
1091
+ Args:
1092
+ env_list: List of strings in KEY=VALUE format
1093
+
1094
+ Returns:
1095
+ dict: Dictionary of environment variable key-value pairs
1096
+ """
1097
+ if not env_list:
1098
+ return {}
1099
+
1100
+ env_dict = {}
1101
+ for env_var in env_list:
1102
+ if "=" not in env_var:
1103
+ format_warning(
1104
+ f"Invalid environment variable format '{env_var}'",
1105
+ suggestion="Expected KEY=VALUE"
1106
+ )
1107
+ continue
1108
+ key, value = env_var.split("=", 1)
1109
+ env_dict[key.strip()] = value.strip()
1110
+
1111
+ return env_dict
1112
+
1113
+
1114
+ def parse_header(header_list: Optional[list]) -> dict:
1115
+ """Parse HTTP headers from command line format.
1116
+
1117
+ Args:
1118
+ header_list: List of strings in KEY=VALUE format
1119
+
1120
+ Returns:
1121
+ dict: Dictionary of header key-value pairs
1122
+ """
1123
+ if not header_list:
1124
+ return {}
1125
+
1126
+ headers_dict = {}
1127
+ for header in header_list:
1128
+ if "=" not in header:
1129
+ format_warning(
1130
+ f"Invalid header format '{header}'",
1131
+ suggestion="Expected KEY=VALUE"
1132
+ )
1133
+ continue
1134
+ key, value = header.split("=", 1)
1135
+ headers_dict[key.strip()] = value.strip()
1136
+
1137
+ return headers_dict
1138
+
1139
+
1140
+ def parse_input(input_list: Optional[list]) -> Optional[list]:
1141
+ """Parse VS Code input variable definitions from command line format.
1142
+
1143
+ Format: type,id,description[,password=true]
1144
+ Example: promptString,api-key,GitHub Personal Access Token,password=true
1145
+
1146
+ Args:
1147
+ input_list: List of input definition strings
1148
+
1149
+ Returns:
1150
+ List of input variable definition dictionaries, or None if no inputs provided.
1151
+ """
1152
+ if not input_list:
1153
+ return None
1154
+
1155
+ parsed_inputs = []
1156
+ for input_str in input_list:
1157
+ parts = [p.strip() for p in input_str.split(",")]
1158
+ if len(parts) < 3:
1159
+ format_warning(
1160
+ f"Invalid input format '{input_str}'",
1161
+ suggestion="Expected: type,id,description[,password=true]"
1162
+ )
1163
+ continue
1164
+
1165
+ input_def = {"type": parts[0], "id": parts[1], "description": parts[2]}
1166
+
1167
+ # Check for optional password flag
1168
+ if len(parts) > 3 and parts[3].lower() == "password=true":
1169
+ input_def["password"] = True
1170
+
1171
+ parsed_inputs.append(input_def)
1172
+
1173
+ return parsed_inputs if parsed_inputs else None
1174
+
1175
+
1176
+ from typing import List
1177
+
1178
+ from hatch.mcp_host_config import MCPHostRegistry, MCPHostType
1179
+
1180
+
1181
+ def parse_host_list(host_arg: str) -> List[str]:
1182
+ """Parse comma-separated host list or 'all'.
1183
+
1184
+ Args:
1185
+ host_arg: Comma-separated host names or 'all' for all available hosts
1186
+
1187
+ Returns:
1188
+ List[str]: List of host name strings
1189
+
1190
+ Raises:
1191
+ ValueError: If an unknown host name is provided
1192
+ """
1193
+ if not host_arg:
1194
+ return []
1195
+
1196
+ if host_arg.lower() == "all":
1197
+ available_hosts = MCPHostRegistry.detect_available_hosts()
1198
+ return [host.value for host in available_hosts]
1199
+
1200
+ hosts = []
1201
+ for host_str in host_arg.split(","):
1202
+ host_str = host_str.strip()
1203
+ try:
1204
+ host_type = MCPHostType(host_str)
1205
+ hosts.append(host_type.value)
1206
+ except ValueError:
1207
+ available = [h.value for h in MCPHostType]
1208
+ raise ValueError(f"Unknown host '{host_str}'. Available: {available}")
1209
+
1210
+ return hosts
1211
+
1212
+
1213
+ import json
1214
+ from pathlib import Path
1215
+
1216
+ from hatch.environment_manager import HatchEnvironmentManager
1217
+ from hatch.mcp_host_config import MCPServerConfig
1218
+
1219
+
1220
+ def get_package_mcp_server_config(
1221
+ env_manager: HatchEnvironmentManager, env_name: str, package_name: str
1222
+ ) -> MCPServerConfig:
1223
+ """Get MCP server configuration for a package using existing APIs.
1224
+
1225
+ Args:
1226
+ env_manager: The environment manager instance
1227
+ env_name: Name of the environment containing the package
1228
+ package_name: Name of the package to get config for
1229
+
1230
+ Returns:
1231
+ MCPServerConfig: Server configuration for the package
1232
+
1233
+ Raises:
1234
+ ValueError: If package not found, not a Hatch package, or has no MCP entry point
1235
+ """
1236
+ try:
1237
+ # Get package info from environment
1238
+ packages = env_manager.list_packages(env_name)
1239
+ package_info = next(
1240
+ (pkg for pkg in packages if pkg["name"] == package_name), None
1241
+ )
1242
+
1243
+ if not package_info:
1244
+ raise ValueError(
1245
+ f"Package '{package_name}' not found in environment '{env_name}'"
1246
+ )
1247
+
1248
+ # Load package metadata using existing pattern from environment_manager.py:716-727
1249
+ package_path = Path(package_info["source"]["path"])
1250
+ metadata_path = package_path / "hatch_metadata.json"
1251
+
1252
+ if not metadata_path.exists():
1253
+ raise ValueError(
1254
+ f"Package '{package_name}' is not a Hatch package (no hatch_metadata.json)"
1255
+ )
1256
+
1257
+ with open(metadata_path, "r") as f:
1258
+ metadata = json.load(f)
1259
+
1260
+ # Use PackageService for schema-aware access
1261
+ from hatch_validator.package.package_service import PackageService
1262
+
1263
+ package_service = PackageService(metadata)
1264
+
1265
+ # Get the HatchMCP entry point (this handles both v1.2.0 and v1.2.1 schemas)
1266
+ mcp_entry_point = package_service.get_mcp_entry_point()
1267
+ if not mcp_entry_point:
1268
+ raise ValueError(
1269
+ f"Package '{package_name}' does not have a HatchMCP entry point"
1270
+ )
1271
+
1272
+ # Get environment-specific Python executable
1273
+ python_executable = env_manager.get_current_python_executable()
1274
+ if not python_executable:
1275
+ # Fallback to system Python if no environment-specific Python available
1276
+ python_executable = "python"
1277
+
1278
+ # Create server configuration
1279
+ server_path = str(package_path / mcp_entry_point)
1280
+ server_config = MCPServerConfig(
1281
+ name=package_name, command=python_executable, args=[server_path], env={}
1282
+ )
1283
+
1284
+ return server_config
1285
+
1286
+ except Exception as e:
1287
+ raise ValueError(
1288
+ f"Failed to get MCP server config for package '{package_name}': {e}"
1289
+ )