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.
- hatch/__init__.py +1 -1
- hatch/cli/__init__.py +71 -0
- hatch/cli/__main__.py +1035 -0
- hatch/cli/cli_env.py +865 -0
- hatch/cli/cli_mcp.py +1965 -0
- hatch/cli/cli_package.py +566 -0
- hatch/cli/cli_system.py +136 -0
- hatch/cli/cli_utils.py +1289 -0
- hatch/cli_hatch.py +160 -2838
- hatch/mcp_host_config/__init__.py +10 -10
- hatch/mcp_host_config/adapters/__init__.py +34 -0
- hatch/mcp_host_config/adapters/base.py +170 -0
- hatch/mcp_host_config/adapters/claude.py +105 -0
- hatch/mcp_host_config/adapters/codex.py +104 -0
- hatch/mcp_host_config/adapters/cursor.py +83 -0
- hatch/mcp_host_config/adapters/gemini.py +75 -0
- hatch/mcp_host_config/adapters/kiro.py +78 -0
- hatch/mcp_host_config/adapters/lmstudio.py +79 -0
- hatch/mcp_host_config/adapters/registry.py +149 -0
- hatch/mcp_host_config/adapters/vscode.py +83 -0
- hatch/mcp_host_config/backup.py +5 -3
- hatch/mcp_host_config/fields.py +126 -0
- hatch/mcp_host_config/models.py +161 -456
- hatch/mcp_host_config/reporting.py +57 -16
- hatch/mcp_host_config/strategies.py +155 -87
- hatch/template_generator.py +1 -1
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
- hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
- tests/cli_test_utils.py +280 -0
- tests/integration/cli/__init__.py +14 -0
- tests/integration/cli/test_cli_reporter_integration.py +2439 -0
- tests/integration/mcp/__init__.py +0 -0
- tests/integration/mcp/test_adapter_serialization.py +173 -0
- tests/regression/cli/__init__.py +16 -0
- tests/regression/cli/test_color_logic.py +268 -0
- tests/regression/cli/test_consequence_type.py +298 -0
- tests/regression/cli/test_error_formatting.py +328 -0
- tests/regression/cli/test_result_reporter.py +586 -0
- tests/regression/cli/test_table_formatter.py +211 -0
- tests/regression/mcp/__init__.py +0 -0
- tests/regression/mcp/test_field_filtering.py +162 -0
- tests/test_cli_version.py +7 -5
- tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
- tests/unit/__init__.py +0 -0
- tests/unit/mcp/__init__.py +0 -0
- tests/unit/mcp/test_adapter_protocol.py +138 -0
- tests/unit/mcp/test_adapter_registry.py +158 -0
- tests/unit/mcp/test_config_model.py +146 -0
- hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
- tests/integration/test_mcp_kiro_integration.py +0 -153
- tests/regression/test_mcp_codex_backup_integration.py +0 -162
- tests/regression/test_mcp_codex_host_strategy.py +0 -163
- tests/regression/test_mcp_codex_model_validation.py +0 -117
- tests/regression/test_mcp_kiro_backup_integration.py +0 -241
- tests/regression/test_mcp_kiro_cli_integration.py +0 -141
- tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
- tests/regression/test_mcp_kiro_host_strategy.py +0 -214
- tests/regression/test_mcp_kiro_model_validation.py +0 -116
- tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
- tests/test_mcp_atomic_operations.py +0 -276
- tests/test_mcp_backup_integration.py +0 -308
- tests/test_mcp_cli_all_host_specific_args.py +0 -496
- tests/test_mcp_cli_backup_management.py +0 -295
- tests/test_mcp_cli_direct_management.py +0 -456
- tests/test_mcp_cli_discovery_listing.py +0 -582
- tests/test_mcp_cli_host_config_integration.py +0 -823
- tests/test_mcp_cli_package_management.py +0 -360
- tests/test_mcp_cli_partial_updates.py +0 -859
- tests/test_mcp_environment_integration.py +0 -520
- tests/test_mcp_host_config_backup.py +0 -257
- tests/test_mcp_host_configuration_manager.py +0 -331
- tests/test_mcp_host_registry_decorator.py +0 -348
- tests/test_mcp_pydantic_architecture_v4.py +0 -603
- tests/test_mcp_server_config_models.py +0 -242
- tests/test_mcp_server_config_type_field.py +0 -221
- tests/test_mcp_sync_functionality.py +0 -316
- tests/test_mcp_user_feedback_reporting.py +0 -359
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
hatch/cli/cli_package.py
ADDED
|
@@ -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
|
hatch/cli/cli_system.py
ADDED
|
@@ -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
|