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
|
@@ -17,17 +17,26 @@ import logging
|
|
|
17
17
|
from .host_management import MCPHostStrategy, register_host_strategy
|
|
18
18
|
from .models import MCPHostType, MCPServerConfig, HostConfiguration
|
|
19
19
|
from .backup import MCPHostConfigBackupManager, AtomicFileOperations
|
|
20
|
+
from .adapters import get_adapter
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class ClaudeHostStrategy(MCPHostStrategy):
|
|
25
26
|
"""Base strategy for Claude family hosts with shared patterns."""
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
def __init__(self):
|
|
28
29
|
self.company_origin = "Anthropic"
|
|
29
30
|
self.config_format = "claude_format"
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
def get_adapter_host_name(self) -> str:
|
|
33
|
+
"""Return the adapter host name for this strategy.
|
|
34
|
+
|
|
35
|
+
Subclasses should override to return their specific adapter host name.
|
|
36
|
+
Default is 'claude-desktop' for backward compatibility.
|
|
37
|
+
"""
|
|
38
|
+
return "claude-desktop"
|
|
39
|
+
|
|
31
40
|
def get_config_key(self) -> str:
|
|
32
41
|
"""Claude family uses 'mcpServers' key."""
|
|
33
42
|
return "mcpServers"
|
|
@@ -85,15 +94,15 @@ class ClaudeHostStrategy(MCPHostStrategy):
|
|
|
85
94
|
return HostConfiguration()
|
|
86
95
|
|
|
87
96
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
88
|
-
"""Write Claude configuration file."""
|
|
97
|
+
"""Write Claude configuration file using adapter-based serialization."""
|
|
89
98
|
config_path = self.get_config_path()
|
|
90
99
|
if not config_path:
|
|
91
100
|
return False
|
|
92
|
-
|
|
101
|
+
|
|
93
102
|
try:
|
|
94
103
|
# Ensure parent directory exists
|
|
95
104
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
-
|
|
105
|
+
|
|
97
106
|
# Read existing configuration to preserve non-MCP settings
|
|
98
107
|
existing_config = {}
|
|
99
108
|
if config_path.exists():
|
|
@@ -102,23 +111,24 @@ class ClaudeHostStrategy(MCPHostStrategy):
|
|
|
102
111
|
existing_config = json.load(f)
|
|
103
112
|
except Exception:
|
|
104
113
|
pass # Start with empty config if read fails
|
|
105
|
-
|
|
106
|
-
#
|
|
114
|
+
|
|
115
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
116
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
107
117
|
servers_dict = {}
|
|
108
118
|
for name, server_config in config.servers.items():
|
|
109
|
-
servers_dict[name] =
|
|
110
|
-
|
|
119
|
+
servers_dict[name] = adapter.serialize(server_config)
|
|
120
|
+
|
|
111
121
|
# Preserve Claude-specific settings
|
|
112
122
|
updated_config = self._preserve_claude_settings(existing_config, servers_dict)
|
|
113
|
-
|
|
123
|
+
|
|
114
124
|
# Write atomically
|
|
115
125
|
temp_path = config_path.with_suffix('.tmp')
|
|
116
126
|
with open(temp_path, 'w') as f:
|
|
117
127
|
json.dump(updated_config, f, indent=2)
|
|
118
|
-
|
|
128
|
+
|
|
119
129
|
temp_path.replace(config_path)
|
|
120
130
|
return True
|
|
121
|
-
|
|
131
|
+
|
|
122
132
|
except Exception as e:
|
|
123
133
|
logger.error(f"Failed to write Claude configuration: {e}")
|
|
124
134
|
return False
|
|
@@ -149,13 +159,17 @@ class ClaudeDesktopStrategy(ClaudeHostStrategy):
|
|
|
149
159
|
@register_host_strategy(MCPHostType.CLAUDE_CODE)
|
|
150
160
|
class ClaudeCodeStrategy(ClaudeHostStrategy):
|
|
151
161
|
"""Configuration strategy for Claude for VS Code."""
|
|
152
|
-
|
|
162
|
+
|
|
163
|
+
def get_adapter_host_name(self) -> str:
|
|
164
|
+
"""Return the adapter host name for Claude Code."""
|
|
165
|
+
return "claude-code"
|
|
166
|
+
|
|
153
167
|
def get_config_path(self) -> Optional[Path]:
|
|
154
168
|
"""Get Claude Code configuration path (workspace-specific)."""
|
|
155
169
|
# Claude Code uses workspace-specific configuration
|
|
156
170
|
# This would be determined at runtime based on current workspace
|
|
157
171
|
return Path.home() / ".claude.json"
|
|
158
|
-
|
|
172
|
+
|
|
159
173
|
def is_host_available(self) -> bool:
|
|
160
174
|
"""Check if Claude Code is available."""
|
|
161
175
|
# Check for Claude Code user configuration file
|
|
@@ -165,15 +179,22 @@ class ClaudeCodeStrategy(ClaudeHostStrategy):
|
|
|
165
179
|
|
|
166
180
|
class CursorBasedHostStrategy(MCPHostStrategy):
|
|
167
181
|
"""Base strategy for Cursor-based hosts (Cursor and LM Studio)."""
|
|
168
|
-
|
|
182
|
+
|
|
169
183
|
def __init__(self):
|
|
170
184
|
self.config_format = "cursor_format"
|
|
171
185
|
self.supports_remote_servers = True
|
|
172
|
-
|
|
186
|
+
|
|
187
|
+
def get_adapter_host_name(self) -> str:
|
|
188
|
+
"""Return the adapter host name for this strategy.
|
|
189
|
+
|
|
190
|
+
Subclasses should override. Default is 'cursor'.
|
|
191
|
+
"""
|
|
192
|
+
return "cursor"
|
|
193
|
+
|
|
173
194
|
def get_config_key(self) -> str:
|
|
174
195
|
"""Cursor family uses 'mcpServers' key."""
|
|
175
196
|
return "mcpServers"
|
|
176
|
-
|
|
197
|
+
|
|
177
198
|
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
178
199
|
"""Cursor family validation - supports both local and remote servers."""
|
|
179
200
|
# Cursor family is more flexible with paths and supports remote servers
|
|
@@ -182,11 +203,14 @@ class CursorBasedHostStrategy(MCPHostStrategy):
|
|
|
182
203
|
elif server_config.url:
|
|
183
204
|
return True # Remote server
|
|
184
205
|
return False
|
|
185
|
-
|
|
206
|
+
|
|
186
207
|
def _format_cursor_server_config(self, server_config: MCPServerConfig) -> Dict:
|
|
187
|
-
"""Format server configuration for Cursor family.
|
|
208
|
+
"""Format server configuration for Cursor family.
|
|
209
|
+
|
|
210
|
+
DEPRECATED: Use adapter.serialize() instead.
|
|
211
|
+
"""
|
|
188
212
|
config = {}
|
|
189
|
-
|
|
213
|
+
|
|
190
214
|
if server_config.command:
|
|
191
215
|
# Local server configuration
|
|
192
216
|
config["command"] = server_config.command
|
|
@@ -199,22 +223,22 @@ class CursorBasedHostStrategy(MCPHostStrategy):
|
|
|
199
223
|
config["url"] = server_config.url
|
|
200
224
|
if server_config.headers:
|
|
201
225
|
config["headers"] = server_config.headers
|
|
202
|
-
|
|
226
|
+
|
|
203
227
|
return config
|
|
204
|
-
|
|
228
|
+
|
|
205
229
|
def read_configuration(self) -> HostConfiguration:
|
|
206
230
|
"""Read Cursor-based configuration file."""
|
|
207
231
|
config_path = self.get_config_path()
|
|
208
232
|
if not config_path or not config_path.exists():
|
|
209
233
|
return HostConfiguration()
|
|
210
|
-
|
|
234
|
+
|
|
211
235
|
try:
|
|
212
236
|
with open(config_path, 'r') as f:
|
|
213
237
|
config_data = json.load(f)
|
|
214
|
-
|
|
238
|
+
|
|
215
239
|
# Extract MCP servers
|
|
216
240
|
mcp_servers = config_data.get(self.get_config_key(), {})
|
|
217
|
-
|
|
241
|
+
|
|
218
242
|
# Convert to MCPServerConfig objects
|
|
219
243
|
servers = {}
|
|
220
244
|
for name, server_data in mcp_servers.items():
|
|
@@ -223,23 +247,23 @@ class CursorBasedHostStrategy(MCPHostStrategy):
|
|
|
223
247
|
except Exception as e:
|
|
224
248
|
logger.warning(f"Invalid server config for {name}: {e}")
|
|
225
249
|
continue
|
|
226
|
-
|
|
250
|
+
|
|
227
251
|
return HostConfiguration(servers=servers)
|
|
228
|
-
|
|
252
|
+
|
|
229
253
|
except Exception as e:
|
|
230
254
|
logger.error(f"Failed to read Cursor configuration: {e}")
|
|
231
255
|
return HostConfiguration()
|
|
232
|
-
|
|
256
|
+
|
|
233
257
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
234
|
-
"""Write Cursor-based configuration file."""
|
|
258
|
+
"""Write Cursor-based configuration file using adapter-based serialization."""
|
|
235
259
|
config_path = self.get_config_path()
|
|
236
260
|
if not config_path:
|
|
237
261
|
return False
|
|
238
|
-
|
|
262
|
+
|
|
239
263
|
try:
|
|
240
264
|
# Ensure parent directory exists
|
|
241
265
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
242
|
-
|
|
266
|
+
|
|
243
267
|
# Read existing configuration
|
|
244
268
|
existing_config = {}
|
|
245
269
|
if config_path.exists():
|
|
@@ -248,23 +272,24 @@ class CursorBasedHostStrategy(MCPHostStrategy):
|
|
|
248
272
|
existing_config = json.load(f)
|
|
249
273
|
except Exception:
|
|
250
274
|
pass
|
|
251
|
-
|
|
252
|
-
#
|
|
275
|
+
|
|
276
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
277
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
253
278
|
servers_dict = {}
|
|
254
279
|
for name, server_config in config.servers.items():
|
|
255
|
-
servers_dict[name] =
|
|
256
|
-
|
|
280
|
+
servers_dict[name] = adapter.serialize(server_config)
|
|
281
|
+
|
|
257
282
|
# Update configuration
|
|
258
283
|
existing_config[self.get_config_key()] = servers_dict
|
|
259
|
-
|
|
284
|
+
|
|
260
285
|
# Write atomically
|
|
261
286
|
temp_path = config_path.with_suffix('.tmp')
|
|
262
287
|
with open(temp_path, 'w') as f:
|
|
263
288
|
json.dump(existing_config, f, indent=2)
|
|
264
|
-
|
|
289
|
+
|
|
265
290
|
temp_path.replace(config_path)
|
|
266
291
|
return True
|
|
267
|
-
|
|
292
|
+
|
|
268
293
|
except Exception as e:
|
|
269
294
|
logger.error(f"Failed to write Cursor configuration: {e}")
|
|
270
295
|
return False
|
|
@@ -287,11 +312,15 @@ class CursorHostStrategy(CursorBasedHostStrategy):
|
|
|
287
312
|
@register_host_strategy(MCPHostType.LMSTUDIO)
|
|
288
313
|
class LMStudioHostStrategy(CursorBasedHostStrategy):
|
|
289
314
|
"""Configuration strategy for LM Studio (follows Cursor format)."""
|
|
290
|
-
|
|
315
|
+
|
|
316
|
+
def get_adapter_host_name(self) -> str:
|
|
317
|
+
"""Return the adapter host name for LM Studio."""
|
|
318
|
+
return "lmstudio"
|
|
319
|
+
|
|
291
320
|
def get_config_path(self) -> Optional[Path]:
|
|
292
321
|
"""Get LM Studio configuration path."""
|
|
293
322
|
return Path.home() / ".lmstudio" / "mcp.json"
|
|
294
|
-
|
|
323
|
+
|
|
295
324
|
def is_host_available(self) -> bool:
|
|
296
325
|
"""Check if LM Studio is installed."""
|
|
297
326
|
config_path = self.get_config_path()
|
|
@@ -302,6 +331,10 @@ class LMStudioHostStrategy(CursorBasedHostStrategy):
|
|
|
302
331
|
class VSCodeHostStrategy(MCPHostStrategy):
|
|
303
332
|
"""Configuration strategy for VS Code MCP extension with user-wide mcp support."""
|
|
304
333
|
|
|
334
|
+
def get_adapter_host_name(self) -> str:
|
|
335
|
+
"""Return the adapter host name for VS Code."""
|
|
336
|
+
return "vscode"
|
|
337
|
+
|
|
305
338
|
def get_config_path(self) -> Optional[Path]:
|
|
306
339
|
"""Get VS Code user mcp configuration path (cross-platform)."""
|
|
307
340
|
try:
|
|
@@ -343,7 +376,7 @@ class VSCodeHostStrategy(MCPHostStrategy):
|
|
|
343
376
|
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
344
377
|
"""VS Code validation - flexible path handling."""
|
|
345
378
|
return server_config.command is not None or server_config.url is not None
|
|
346
|
-
|
|
379
|
+
|
|
347
380
|
def read_configuration(self) -> HostConfiguration:
|
|
348
381
|
"""Read VS Code mcp.json configuration."""
|
|
349
382
|
config_path = self.get_config_path()
|
|
@@ -373,7 +406,7 @@ class VSCodeHostStrategy(MCPHostStrategy):
|
|
|
373
406
|
return HostConfiguration()
|
|
374
407
|
|
|
375
408
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
376
|
-
"""Write VS Code mcp.json configuration."""
|
|
409
|
+
"""Write VS Code mcp.json configuration using adapter-based serialization."""
|
|
377
410
|
config_path = self.get_config_path()
|
|
378
411
|
if not config_path:
|
|
379
412
|
return False
|
|
@@ -391,10 +424,11 @@ class VSCodeHostStrategy(MCPHostStrategy):
|
|
|
391
424
|
except Exception:
|
|
392
425
|
pass
|
|
393
426
|
|
|
394
|
-
#
|
|
427
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
428
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
395
429
|
servers_dict = {}
|
|
396
430
|
for name, server_config in config.servers.items():
|
|
397
|
-
servers_dict[name] =
|
|
431
|
+
servers_dict[name] = adapter.serialize(server_config)
|
|
398
432
|
|
|
399
433
|
# Update configuration with new servers (preserves non-MCP settings)
|
|
400
434
|
existing_config[self.get_config_key()] = servers_dict
|
|
@@ -415,83 +449,88 @@ class VSCodeHostStrategy(MCPHostStrategy):
|
|
|
415
449
|
@register_host_strategy(MCPHostType.KIRO)
|
|
416
450
|
class KiroHostStrategy(MCPHostStrategy):
|
|
417
451
|
"""Configuration strategy for Kiro IDE."""
|
|
418
|
-
|
|
452
|
+
|
|
453
|
+
def get_adapter_host_name(self) -> str:
|
|
454
|
+
"""Return the adapter host name for Kiro."""
|
|
455
|
+
return "kiro"
|
|
456
|
+
|
|
419
457
|
def get_config_path(self) -> Optional[Path]:
|
|
420
458
|
"""Get Kiro configuration path (user-level only per constraint)."""
|
|
421
459
|
return Path.home() / ".kiro" / "settings" / "mcp.json"
|
|
422
|
-
|
|
460
|
+
|
|
423
461
|
def get_config_key(self) -> str:
|
|
424
462
|
"""Kiro uses 'mcpServers' key."""
|
|
425
463
|
return "mcpServers"
|
|
426
|
-
|
|
464
|
+
|
|
427
465
|
def is_host_available(self) -> bool:
|
|
428
466
|
"""Check if Kiro is available by checking for settings directory."""
|
|
429
467
|
kiro_dir = Path.home() / ".kiro" / "settings"
|
|
430
468
|
return kiro_dir.exists()
|
|
431
|
-
|
|
469
|
+
|
|
432
470
|
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
433
471
|
"""Kiro validation - supports both local and remote servers."""
|
|
434
472
|
return server_config.command is not None or server_config.url is not None
|
|
435
|
-
|
|
473
|
+
|
|
436
474
|
def read_configuration(self) -> HostConfiguration:
|
|
437
475
|
"""Read Kiro configuration file."""
|
|
438
476
|
config_path_str = self.get_config_path()
|
|
439
477
|
if not config_path_str:
|
|
440
478
|
return HostConfiguration(servers={})
|
|
441
|
-
|
|
479
|
+
|
|
442
480
|
config_path = Path(config_path_str)
|
|
443
481
|
if not config_path.exists():
|
|
444
482
|
return HostConfiguration(servers={})
|
|
445
|
-
|
|
483
|
+
|
|
446
484
|
try:
|
|
447
485
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
448
486
|
data = json.load(f)
|
|
449
|
-
|
|
487
|
+
|
|
450
488
|
servers = {}
|
|
451
489
|
mcp_servers = data.get(self.get_config_key(), {})
|
|
452
|
-
|
|
490
|
+
|
|
453
491
|
for name, config in mcp_servers.items():
|
|
454
492
|
try:
|
|
455
493
|
servers[name] = MCPServerConfig(**config)
|
|
456
494
|
except Exception as e:
|
|
457
495
|
logger.warning(f"Invalid server config for {name}: {e}")
|
|
458
496
|
continue
|
|
459
|
-
|
|
497
|
+
|
|
460
498
|
return HostConfiguration(servers=servers)
|
|
461
|
-
|
|
499
|
+
|
|
462
500
|
except Exception as e:
|
|
463
501
|
logger.error(f"Failed to read Kiro configuration: {e}")
|
|
464
502
|
return HostConfiguration(servers={})
|
|
465
|
-
|
|
503
|
+
|
|
466
504
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
467
|
-
"""Write configuration to Kiro with backup support."""
|
|
505
|
+
"""Write configuration to Kiro with backup support using adapter-based serialization."""
|
|
468
506
|
config_path_str = self.get_config_path()
|
|
469
507
|
if not config_path_str:
|
|
470
508
|
return False
|
|
471
|
-
|
|
509
|
+
|
|
472
510
|
config_path = Path(config_path_str)
|
|
473
|
-
|
|
511
|
+
|
|
474
512
|
try:
|
|
475
513
|
# Ensure directory exists
|
|
476
514
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
477
|
-
|
|
515
|
+
|
|
478
516
|
# Read existing configuration to preserve other settings
|
|
479
517
|
existing_data = {}
|
|
480
518
|
if config_path.exists():
|
|
481
519
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
482
520
|
existing_data = json.load(f)
|
|
483
|
-
|
|
484
|
-
#
|
|
521
|
+
|
|
522
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
523
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
485
524
|
servers_data = {}
|
|
486
525
|
for name, server_config in config.servers.items():
|
|
487
|
-
servers_data[name] =
|
|
488
|
-
|
|
526
|
+
servers_data[name] = adapter.serialize(server_config)
|
|
527
|
+
|
|
489
528
|
existing_data[self.get_config_key()] = servers_data
|
|
490
|
-
|
|
529
|
+
|
|
491
530
|
# Use atomic write with backup support
|
|
492
531
|
backup_manager = MCPHostConfigBackupManager()
|
|
493
532
|
atomic_ops = AtomicFileOperations()
|
|
494
|
-
|
|
533
|
+
|
|
495
534
|
atomic_ops.atomic_write_with_backup(
|
|
496
535
|
file_path=config_path,
|
|
497
536
|
data=existing_data,
|
|
@@ -499,9 +538,9 @@ class KiroHostStrategy(MCPHostStrategy):
|
|
|
499
538
|
hostname="kiro",
|
|
500
539
|
skip_backup=no_backup
|
|
501
540
|
)
|
|
502
|
-
|
|
541
|
+
|
|
503
542
|
return True
|
|
504
|
-
|
|
543
|
+
|
|
505
544
|
except Exception as e:
|
|
506
545
|
logger.error(f"Failed to write Kiro configuration: {e}")
|
|
507
546
|
return False
|
|
@@ -510,40 +549,44 @@ class KiroHostStrategy(MCPHostStrategy):
|
|
|
510
549
|
@register_host_strategy(MCPHostType.GEMINI)
|
|
511
550
|
class GeminiHostStrategy(MCPHostStrategy):
|
|
512
551
|
"""Configuration strategy for Google Gemini CLI MCP integration."""
|
|
513
|
-
|
|
552
|
+
|
|
553
|
+
def get_adapter_host_name(self) -> str:
|
|
554
|
+
"""Return the adapter host name for Gemini."""
|
|
555
|
+
return "gemini"
|
|
556
|
+
|
|
514
557
|
def get_config_path(self) -> Optional[Path]:
|
|
515
558
|
"""Get Gemini configuration path based on official documentation."""
|
|
516
559
|
# Based on official Gemini CLI documentation: ~/.gemini/settings.json
|
|
517
560
|
return Path.home() / ".gemini" / "settings.json"
|
|
518
|
-
|
|
561
|
+
|
|
519
562
|
def get_config_key(self) -> str:
|
|
520
563
|
"""Gemini uses 'mcpServers' key in settings.json."""
|
|
521
564
|
return "mcpServers"
|
|
522
|
-
|
|
565
|
+
|
|
523
566
|
def is_host_available(self) -> bool:
|
|
524
567
|
"""Check if Gemini CLI is available."""
|
|
525
568
|
# Check if Gemini CLI directory exists
|
|
526
569
|
gemini_dir = Path.home() / ".gemini"
|
|
527
570
|
return gemini_dir.exists()
|
|
528
|
-
|
|
571
|
+
|
|
529
572
|
def validate_server_config(self, server_config: MCPServerConfig) -> bool:
|
|
530
573
|
"""Gemini validation - supports both local and remote servers."""
|
|
531
574
|
# Gemini CLI supports both command-based and URL-based servers
|
|
532
575
|
return server_config.command is not None or server_config.url is not None
|
|
533
|
-
|
|
576
|
+
|
|
534
577
|
def read_configuration(self) -> HostConfiguration:
|
|
535
578
|
"""Read Gemini settings.json configuration."""
|
|
536
579
|
config_path = self.get_config_path()
|
|
537
580
|
if not config_path or not config_path.exists():
|
|
538
581
|
return HostConfiguration()
|
|
539
|
-
|
|
582
|
+
|
|
540
583
|
try:
|
|
541
584
|
with open(config_path, 'r') as f:
|
|
542
585
|
config_data = json.load(f)
|
|
543
|
-
|
|
586
|
+
|
|
544
587
|
# Extract MCP servers from Gemini configuration
|
|
545
588
|
mcp_servers = config_data.get(self.get_config_key(), {})
|
|
546
|
-
|
|
589
|
+
|
|
547
590
|
# Convert to MCPServerConfig objects
|
|
548
591
|
servers = {}
|
|
549
592
|
for name, server_data in mcp_servers.items():
|
|
@@ -552,15 +595,15 @@ class GeminiHostStrategy(MCPHostStrategy):
|
|
|
552
595
|
except Exception as e:
|
|
553
596
|
logger.warning(f"Invalid server config for {name}: {e}")
|
|
554
597
|
continue
|
|
555
|
-
|
|
598
|
+
|
|
556
599
|
return HostConfiguration(servers=servers)
|
|
557
|
-
|
|
600
|
+
|
|
558
601
|
except Exception as e:
|
|
559
602
|
logger.error(f"Failed to read Gemini configuration: {e}")
|
|
560
603
|
return HostConfiguration()
|
|
561
|
-
|
|
604
|
+
|
|
562
605
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
563
|
-
"""Write Gemini settings.json configuration."""
|
|
606
|
+
"""Write Gemini settings.json configuration using adapter-based serialization."""
|
|
564
607
|
config_path = self.get_config_path()
|
|
565
608
|
if not config_path:
|
|
566
609
|
return False
|
|
@@ -578,14 +621,15 @@ class GeminiHostStrategy(MCPHostStrategy):
|
|
|
578
621
|
except Exception:
|
|
579
622
|
pass
|
|
580
623
|
|
|
581
|
-
#
|
|
624
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
625
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
582
626
|
servers_dict = {}
|
|
583
627
|
for name, server_config in config.servers.items():
|
|
584
|
-
servers_dict[name] =
|
|
628
|
+
servers_dict[name] = adapter.serialize(server_config)
|
|
585
629
|
|
|
586
630
|
# Update configuration with new servers (preserves non-MCP settings)
|
|
587
631
|
existing_config[self.get_config_key()] = servers_dict
|
|
588
|
-
|
|
632
|
+
|
|
589
633
|
# Write atomically with enhanced error handling
|
|
590
634
|
temp_path = config_path.with_suffix('.tmp')
|
|
591
635
|
try:
|
|
@@ -605,7 +649,7 @@ class GeminiHostStrategy(MCPHostStrategy):
|
|
|
605
649
|
temp_path.unlink()
|
|
606
650
|
logger.error(f"JSON serialization/verification failed: {json_error}")
|
|
607
651
|
raise
|
|
608
|
-
|
|
652
|
+
|
|
609
653
|
except Exception as e:
|
|
610
654
|
logger.error(f"Failed to write Gemini configuration: {e}")
|
|
611
655
|
return False
|
|
@@ -623,6 +667,10 @@ class CodexHostStrategy(MCPHostStrategy):
|
|
|
623
667
|
self.config_format = "toml"
|
|
624
668
|
self._preserved_features = {} # Preserve [features] section
|
|
625
669
|
|
|
670
|
+
def get_adapter_host_name(self) -> str:
|
|
671
|
+
"""Return the adapter host name for Codex."""
|
|
672
|
+
return "codex"
|
|
673
|
+
|
|
626
674
|
def get_config_path(self) -> Optional[Path]:
|
|
627
675
|
"""Get Codex configuration path."""
|
|
628
676
|
return Path.home() / ".codex" / "config.toml"
|
|
@@ -673,7 +721,7 @@ class CodexHostStrategy(MCPHostStrategy):
|
|
|
673
721
|
return HostConfiguration(servers={})
|
|
674
722
|
|
|
675
723
|
def write_configuration(self, config: HostConfiguration, no_backup: bool = False) -> bool:
|
|
676
|
-
"""Write Codex TOML configuration file with backup support."""
|
|
724
|
+
"""Write Codex TOML configuration file with backup support using adapter-based serialization."""
|
|
677
725
|
config_path = self.get_config_path()
|
|
678
726
|
if not config_path:
|
|
679
727
|
return False
|
|
@@ -694,10 +742,13 @@ class CodexHostStrategy(MCPHostStrategy):
|
|
|
694
742
|
if 'features' in existing_data:
|
|
695
743
|
self._preserved_features = existing_data['features']
|
|
696
744
|
|
|
697
|
-
#
|
|
745
|
+
# Use adapter for serialization (includes validation and field filtering)
|
|
746
|
+
adapter = get_adapter(self.get_adapter_host_name())
|
|
698
747
|
servers_data = {}
|
|
699
748
|
for name, server_config in config.servers.items():
|
|
700
|
-
|
|
749
|
+
# Adapter serializes and filters fields, then apply TOML-specific transforms
|
|
750
|
+
serialized = adapter.serialize(server_config)
|
|
751
|
+
servers_data[name] = self._to_toml_server_from_dict(serialized)
|
|
701
752
|
|
|
702
753
|
# Build final TOML structure
|
|
703
754
|
final_data = {}
|
|
@@ -778,3 +829,20 @@ class CodexHostStrategy(MCPHostStrategy):
|
|
|
778
829
|
data['http_headers'] = data.pop('headers')
|
|
779
830
|
|
|
780
831
|
return data
|
|
832
|
+
|
|
833
|
+
def _to_toml_server_from_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
834
|
+
"""Apply TOML-specific transformations to an already-serialized dict.
|
|
835
|
+
|
|
836
|
+
This is used after adapter serialization to apply Codex-specific field mappings.
|
|
837
|
+
Maps universal 'headers' field back to Codex-specific 'http_headers'.
|
|
838
|
+
"""
|
|
839
|
+
result = dict(data)
|
|
840
|
+
|
|
841
|
+
# Remove 'name' field as it's the table key in TOML
|
|
842
|
+
result.pop('name', None)
|
|
843
|
+
|
|
844
|
+
# Map universal 'headers' to Codex 'http_headers' for TOML
|
|
845
|
+
if 'headers' in result:
|
|
846
|
+
result['http_headers'] = result.pop('headers')
|
|
847
|
+
|
|
848
|
+
return result
|
hatch/template_generator.py
CHANGED
|
@@ -73,7 +73,7 @@ if __name__ == \"__main__\":
|
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def generate_metadata_json(package_name: str, description: str = ""):
|
|
76
|
+
def generate_metadata_json(package_name: str, description: str = "") -> dict:
|
|
77
77
|
"""Generate the metadata JSON content for a template package.
|
|
78
78
|
|
|
79
79
|
Args:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hatch-xclam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0.dev1
|
|
4
4
|
Summary: Package manager for the Cracking Shells ecosystem
|
|
5
5
|
Author: Cracking Shells Team
|
|
6
6
|
Project-URL: Homepage, https://github.com/CrackingShells/Hatch
|
|
@@ -22,7 +22,8 @@ Provides-Extra: docs
|
|
|
22
22
|
Requires-Dist: mkdocs>=1.4.0; extra == "docs"
|
|
23
23
|
Requires-Dist: mkdocstrings[python]>=0.20.0; extra == "docs"
|
|
24
24
|
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: wobble>=0.2.0; extra == "dev"
|
|
25
|
+
Requires-Dist: cs-wobble>=0.2.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
26
27
|
Dynamic: license-file
|
|
27
28
|
|
|
28
29
|
# Hatch
|