ai-config-cli 0.1.0__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.
- ai_config/__init__.py +3 -0
- ai_config/__main__.py +6 -0
- ai_config/adapters/__init__.py +1 -0
- ai_config/adapters/claude.py +353 -0
- ai_config/cli.py +729 -0
- ai_config/cli_render.py +525 -0
- ai_config/cli_theme.py +44 -0
- ai_config/config.py +260 -0
- ai_config/init.py +763 -0
- ai_config/operations.py +357 -0
- ai_config/scaffold.py +87 -0
- ai_config/settings.py +63 -0
- ai_config/types.py +143 -0
- ai_config/validators/__init__.py +149 -0
- ai_config/validators/base.py +48 -0
- ai_config/validators/component/__init__.py +1 -0
- ai_config/validators/component/hook.py +366 -0
- ai_config/validators/component/mcp.py +230 -0
- ai_config/validators/component/skill.py +411 -0
- ai_config/validators/context.py +69 -0
- ai_config/validators/marketplace/__init__.py +1 -0
- ai_config/validators/marketplace/validators.py +433 -0
- ai_config/validators/plugin/__init__.py +1 -0
- ai_config/validators/plugin/validators.py +336 -0
- ai_config/validators/target/__init__.py +1 -0
- ai_config/validators/target/claude.py +154 -0
- ai_config/watch.py +279 -0
- ai_config_cli-0.1.0.dist-info/METADATA +235 -0
- ai_config_cli-0.1.0.dist-info/RECORD +32 -0
- ai_config_cli-0.1.0.dist-info/WHEEL +4 -0
- ai_config_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ai_config_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Validation context for sharing state across validators."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ai_config.adapters.claude import InstalledMarketplace, InstalledPlugin
|
|
9
|
+
from ai_config.types import AIConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ValidationContext:
|
|
14
|
+
"""Shared context passed to all validators."""
|
|
15
|
+
|
|
16
|
+
config: "AIConfig"
|
|
17
|
+
config_path: Path
|
|
18
|
+
target_type: str = "claude"
|
|
19
|
+
|
|
20
|
+
# Cached data from Claude CLI (lazy-loaded)
|
|
21
|
+
_installed_plugins: list["InstalledPlugin"] | None = field(default=None, repr=False)
|
|
22
|
+
_installed_marketplaces: list["InstalledMarketplace"] | None = field(default=None, repr=False)
|
|
23
|
+
_known_marketplaces_json: dict | None = field(default=None, repr=False)
|
|
24
|
+
_errors: list[str] = field(default_factory=list, repr=False)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def installed_plugins(self) -> list["InstalledPlugin"]:
|
|
28
|
+
"""Lazy-load installed plugins from Claude CLI."""
|
|
29
|
+
if self._installed_plugins is None:
|
|
30
|
+
from ai_config.adapters import claude
|
|
31
|
+
|
|
32
|
+
plugins, errors = claude.list_installed_plugins()
|
|
33
|
+
self._installed_plugins = plugins
|
|
34
|
+
self._errors.extend(errors)
|
|
35
|
+
return self._installed_plugins
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def installed_marketplaces(self) -> list["InstalledMarketplace"]:
|
|
39
|
+
"""Lazy-load installed marketplaces from Claude CLI."""
|
|
40
|
+
if self._installed_marketplaces is None:
|
|
41
|
+
from ai_config.adapters import claude
|
|
42
|
+
|
|
43
|
+
marketplaces, errors = claude.list_installed_marketplaces()
|
|
44
|
+
self._installed_marketplaces = marketplaces
|
|
45
|
+
self._errors.extend(errors)
|
|
46
|
+
return self._installed_marketplaces
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def known_marketplaces_json(self) -> dict:
|
|
50
|
+
"""Lazy-load Claude's known_marketplaces.json file."""
|
|
51
|
+
if self._known_marketplaces_json is None:
|
|
52
|
+
import json
|
|
53
|
+
|
|
54
|
+
known_path = Path.home() / ".claude" / "plugins" / "known_marketplaces.json"
|
|
55
|
+
if known_path.exists():
|
|
56
|
+
try:
|
|
57
|
+
with open(known_path) as f:
|
|
58
|
+
self._known_marketplaces_json = json.load(f)
|
|
59
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
60
|
+
self._errors.append(f"Failed to read known_marketplaces.json: {e}")
|
|
61
|
+
self._known_marketplaces_json = {}
|
|
62
|
+
else:
|
|
63
|
+
self._known_marketplaces_json = {}
|
|
64
|
+
return self._known_marketplaces_json
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def errors(self) -> list[str]:
|
|
68
|
+
"""Return accumulated errors from lazy loading."""
|
|
69
|
+
return self._errors
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Marketplace validators for ai-config."""
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Marketplace validators for ai-config.
|
|
2
|
+
|
|
3
|
+
Validates marketplace.json manifests per the official Claude Code schema:
|
|
4
|
+
https://code.claude.com/docs/en/plugin-marketplaces
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from ai_config.types import PluginSource
|
|
13
|
+
from ai_config.validators.base import ValidationResult
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ai_config.validators.context import ValidationContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Reserved marketplace names that cannot be used
|
|
20
|
+
RESERVED_MARKETPLACE_NAMES = frozenset(
|
|
21
|
+
[
|
|
22
|
+
"claude-code-marketplace",
|
|
23
|
+
"claude-code-plugins",
|
|
24
|
+
"claude-plugins-official",
|
|
25
|
+
"anthropic-marketplace",
|
|
26
|
+
"anthropic-plugins",
|
|
27
|
+
"agent-skills",
|
|
28
|
+
"life-sciences",
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Pattern for valid kebab-case names
|
|
33
|
+
KEBAB_CASE_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_kebab_case(name: str) -> bool:
|
|
37
|
+
"""Check if a name is valid kebab-case."""
|
|
38
|
+
if not name:
|
|
39
|
+
return False
|
|
40
|
+
return bool(KEBAB_CASE_PATTERN.match(name))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MarketplacePathValidator:
|
|
44
|
+
"""Validates that local marketplace paths exist."""
|
|
45
|
+
|
|
46
|
+
name = "marketplace_path"
|
|
47
|
+
description = "Validates that local marketplace directories exist"
|
|
48
|
+
|
|
49
|
+
async def validate(self, context: "ValidationContext") -> list[ValidationResult]:
|
|
50
|
+
"""Validate marketplace path existence.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
context: The validation context.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of validation results.
|
|
57
|
+
"""
|
|
58
|
+
results: list[ValidationResult] = []
|
|
59
|
+
|
|
60
|
+
for target in context.config.targets:
|
|
61
|
+
if target.type != "claude":
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
for mp_name, mp_config in target.config.marketplaces.items():
|
|
65
|
+
# Only check local marketplaces
|
|
66
|
+
if mp_config.source != PluginSource.LOCAL:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
mp_path = Path(mp_config.path)
|
|
70
|
+
if mp_path.exists():
|
|
71
|
+
results.append(
|
|
72
|
+
ValidationResult(
|
|
73
|
+
check_name="marketplace_path_exists",
|
|
74
|
+
status="pass",
|
|
75
|
+
message=f"Marketplace '{mp_name}' path exists: {mp_path}",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
results.append(
|
|
80
|
+
ValidationResult(
|
|
81
|
+
check_name="marketplace_path_exists",
|
|
82
|
+
status="fail",
|
|
83
|
+
message=f"Marketplace '{mp_name}' path does not exist: {mp_path}",
|
|
84
|
+
details=f"Expected directory at: {mp_path}",
|
|
85
|
+
fix_hint=f"Create the directory: mkdir -p {mp_path}",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MarketplaceManifestValidator:
|
|
93
|
+
"""Validates that local marketplaces have valid marketplace.json manifests.
|
|
94
|
+
|
|
95
|
+
Per the official Claude Code marketplace schema:
|
|
96
|
+
- name: Required, kebab-case, not a reserved name
|
|
97
|
+
- owner: Required, must have 'name' field
|
|
98
|
+
- plugins: Required, must be an array of plugin entries
|
|
99
|
+
- Each plugin entry must have 'name' and 'source'
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
name = "marketplace_manifest"
|
|
103
|
+
description = "Validates marketplace.json manifest files"
|
|
104
|
+
|
|
105
|
+
async def validate(self, context: "ValidationContext") -> list[ValidationResult]:
|
|
106
|
+
"""Validate marketplace manifest files.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
context: The validation context.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of validation results.
|
|
113
|
+
"""
|
|
114
|
+
results: list[ValidationResult] = []
|
|
115
|
+
|
|
116
|
+
for target in context.config.targets:
|
|
117
|
+
if target.type != "claude":
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
for mp_name, mp_config in target.config.marketplaces.items():
|
|
121
|
+
# Only check local marketplaces
|
|
122
|
+
if mp_config.source != PluginSource.LOCAL:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
mp_path = Path(mp_config.path)
|
|
126
|
+
if not mp_path.exists():
|
|
127
|
+
# Path validation will catch this
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
manifest_path = mp_path / ".claude-plugin" / "marketplace.json"
|
|
131
|
+
if not manifest_path.exists():
|
|
132
|
+
results.append(
|
|
133
|
+
ValidationResult(
|
|
134
|
+
check_name="marketplace_manifest_exists",
|
|
135
|
+
status="fail",
|
|
136
|
+
message=f"Marketplace '{mp_name}' is missing marketplace.json",
|
|
137
|
+
details=f"Expected file at: {manifest_path}",
|
|
138
|
+
fix_hint=(
|
|
139
|
+
f'Create manifest with: {{"name": "{mp_name}", '
|
|
140
|
+
'"owner": {"name": "..."}, "plugins": []}'
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Validate manifest JSON
|
|
147
|
+
try:
|
|
148
|
+
with open(manifest_path) as f:
|
|
149
|
+
manifest = json.load(f)
|
|
150
|
+
|
|
151
|
+
manifest_results = self._validate_manifest(mp_name, manifest)
|
|
152
|
+
results.extend(manifest_results)
|
|
153
|
+
|
|
154
|
+
if not any(r.status == "fail" for r in manifest_results):
|
|
155
|
+
results.append(
|
|
156
|
+
ValidationResult(
|
|
157
|
+
check_name="marketplace_manifest_valid",
|
|
158
|
+
status="pass",
|
|
159
|
+
message=f"Marketplace '{mp_name}' manifest is valid",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except json.JSONDecodeError as e:
|
|
164
|
+
results.append(
|
|
165
|
+
ValidationResult(
|
|
166
|
+
check_name="marketplace_manifest_valid",
|
|
167
|
+
status="fail",
|
|
168
|
+
message=f"Marketplace '{mp_name}' has invalid JSON in marketplace.json",
|
|
169
|
+
details=str(e),
|
|
170
|
+
fix_hint="Fix the JSON syntax in marketplace.json",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
except OSError as e:
|
|
174
|
+
results.append(
|
|
175
|
+
ValidationResult(
|
|
176
|
+
check_name="marketplace_manifest_readable",
|
|
177
|
+
status="fail",
|
|
178
|
+
message=f"Failed to read marketplace.json for '{mp_name}'",
|
|
179
|
+
details=str(e),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
def _validate_manifest(self, mp_name: str, manifest: dict) -> list[ValidationResult]:
|
|
186
|
+
"""Validate marketplace manifest content.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
mp_name: The marketplace name from config.
|
|
190
|
+
manifest: The parsed marketplace.json content.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of validation results.
|
|
194
|
+
"""
|
|
195
|
+
results: list[ValidationResult] = []
|
|
196
|
+
|
|
197
|
+
if not isinstance(manifest, dict):
|
|
198
|
+
results.append(
|
|
199
|
+
ValidationResult(
|
|
200
|
+
check_name="marketplace_manifest_valid",
|
|
201
|
+
status="fail",
|
|
202
|
+
message=f"Marketplace '{mp_name}' manifest is not a JSON object",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return results
|
|
206
|
+
|
|
207
|
+
# Check required 'name' field
|
|
208
|
+
name = manifest.get("name")
|
|
209
|
+
if not name:
|
|
210
|
+
results.append(
|
|
211
|
+
ValidationResult(
|
|
212
|
+
check_name="marketplace_manifest_name_required",
|
|
213
|
+
status="fail",
|
|
214
|
+
message=f"Marketplace '{mp_name}' manifest is missing required 'name' field",
|
|
215
|
+
fix_hint="Add 'name' field to marketplace.json",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
elif not isinstance(name, str):
|
|
219
|
+
results.append(
|
|
220
|
+
ValidationResult(
|
|
221
|
+
check_name="marketplace_manifest_name_type",
|
|
222
|
+
status="fail",
|
|
223
|
+
message=f"Marketplace '{mp_name}' manifest 'name' must be a string",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
# Check kebab-case
|
|
228
|
+
if not is_kebab_case(name):
|
|
229
|
+
results.append(
|
|
230
|
+
ValidationResult(
|
|
231
|
+
check_name="marketplace_manifest_name_format",
|
|
232
|
+
status="fail",
|
|
233
|
+
message=f"Marketplace '{mp_name}' manifest 'name' must be kebab-case",
|
|
234
|
+
details=f"Got: '{name}'. Use lowercase letters, numbers, and hyphens only.",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Check reserved names
|
|
239
|
+
if name.lower() in RESERVED_MARKETPLACE_NAMES:
|
|
240
|
+
results.append(
|
|
241
|
+
ValidationResult(
|
|
242
|
+
check_name="marketplace_manifest_name_reserved",
|
|
243
|
+
status="fail",
|
|
244
|
+
message=f"Marketplace '{mp_name}' manifest uses reserved name: '{name}'",
|
|
245
|
+
details=f"Reserved names: {', '.join(sorted(RESERVED_MARKETPLACE_NAMES))}",
|
|
246
|
+
fix_hint="Choose a different marketplace name",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Check required 'owner' field
|
|
251
|
+
owner = manifest.get("owner")
|
|
252
|
+
if owner is None:
|
|
253
|
+
results.append(
|
|
254
|
+
ValidationResult(
|
|
255
|
+
check_name="marketplace_manifest_owner_required",
|
|
256
|
+
status="fail",
|
|
257
|
+
message=f"Marketplace '{mp_name}' manifest is missing required 'owner' field",
|
|
258
|
+
fix_hint='Add \'owner\': {"name": "Your Name"} to marketplace.json',
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
elif not isinstance(owner, dict):
|
|
262
|
+
results.append(
|
|
263
|
+
ValidationResult(
|
|
264
|
+
check_name="marketplace_manifest_owner_type",
|
|
265
|
+
status="fail",
|
|
266
|
+
message=f"Marketplace '{mp_name}' manifest 'owner' must be an object",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
elif not owner.get("name"):
|
|
270
|
+
results.append(
|
|
271
|
+
ValidationResult(
|
|
272
|
+
check_name="marketplace_manifest_owner_name_required",
|
|
273
|
+
status="fail",
|
|
274
|
+
message=(
|
|
275
|
+
f"Marketplace '{mp_name}' manifest 'owner' is missing required 'name' field"
|
|
276
|
+
),
|
|
277
|
+
fix_hint='Add \'name\' to owner: {"owner": {"name": "Your Name"}}',
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Check required 'plugins' field
|
|
282
|
+
plugins = manifest.get("plugins")
|
|
283
|
+
if plugins is None:
|
|
284
|
+
results.append(
|
|
285
|
+
ValidationResult(
|
|
286
|
+
check_name="marketplace_manifest_plugins_required",
|
|
287
|
+
status="fail",
|
|
288
|
+
message=f"Marketplace '{mp_name}' manifest is missing required 'plugins' field",
|
|
289
|
+
fix_hint="Add 'plugins': [] to marketplace.json",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
elif not isinstance(plugins, list):
|
|
293
|
+
results.append(
|
|
294
|
+
ValidationResult(
|
|
295
|
+
check_name="marketplace_manifest_plugins_type",
|
|
296
|
+
status="fail",
|
|
297
|
+
message=f"Marketplace '{mp_name}' manifest 'plugins' must be an array (list)",
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
# Validate each plugin entry
|
|
302
|
+
for i, plugin_entry in enumerate(plugins):
|
|
303
|
+
plugin_results = self._validate_plugin_entry(mp_name, i, plugin_entry)
|
|
304
|
+
results.extend(plugin_results)
|
|
305
|
+
|
|
306
|
+
return results
|
|
307
|
+
|
|
308
|
+
def _validate_plugin_entry(
|
|
309
|
+
self, mp_name: str, index: int, entry: dict
|
|
310
|
+
) -> list[ValidationResult]:
|
|
311
|
+
"""Validate a plugin entry in the plugins array.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
mp_name: The marketplace name.
|
|
315
|
+
index: Index of the plugin entry.
|
|
316
|
+
entry: The plugin entry dict.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of validation results.
|
|
320
|
+
"""
|
|
321
|
+
results: list[ValidationResult] = []
|
|
322
|
+
|
|
323
|
+
if not isinstance(entry, dict):
|
|
324
|
+
results.append(
|
|
325
|
+
ValidationResult(
|
|
326
|
+
check_name="marketplace_plugin_entry_type",
|
|
327
|
+
status="fail",
|
|
328
|
+
message=f"Marketplace '{mp_name}' plugins[{index}] must be an object",
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
return results
|
|
332
|
+
|
|
333
|
+
# Check required 'name' field
|
|
334
|
+
if not entry.get("name"):
|
|
335
|
+
results.append(
|
|
336
|
+
ValidationResult(
|
|
337
|
+
check_name="marketplace_plugin_name_required",
|
|
338
|
+
status="fail",
|
|
339
|
+
message=(
|
|
340
|
+
f"Marketplace '{mp_name}' plugins[{index}] is missing required 'name' field"
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Check required 'source' field
|
|
346
|
+
if not entry.get("source"):
|
|
347
|
+
results.append(
|
|
348
|
+
ValidationResult(
|
|
349
|
+
check_name="marketplace_plugin_source_required",
|
|
350
|
+
status="fail",
|
|
351
|
+
message=(
|
|
352
|
+
f"Marketplace '{mp_name}' plugins[{index}] "
|
|
353
|
+
"is missing required 'source' field"
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return results
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class PathDriftValidator:
|
|
362
|
+
"""Validates that config marketplace paths match Claude's known_marketplaces.json."""
|
|
363
|
+
|
|
364
|
+
name = "path_drift"
|
|
365
|
+
description = "Detects when marketplace paths in config differ from Claude's registered paths"
|
|
366
|
+
|
|
367
|
+
async def validate(self, context: "ValidationContext") -> list[ValidationResult]:
|
|
368
|
+
"""Check for path drift between config and Claude's state.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
context: The validation context.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of validation results.
|
|
375
|
+
"""
|
|
376
|
+
results: list[ValidationResult] = []
|
|
377
|
+
|
|
378
|
+
# Get Claude's known marketplaces
|
|
379
|
+
known_marketplaces = context.known_marketplaces_json
|
|
380
|
+
|
|
381
|
+
for target in context.config.targets:
|
|
382
|
+
if target.type != "claude":
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
for mp_name, mp_config in target.config.marketplaces.items():
|
|
386
|
+
# Only check local marketplaces for path drift
|
|
387
|
+
if mp_config.source != PluginSource.LOCAL:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
config_path = Path(mp_config.path).resolve()
|
|
391
|
+
|
|
392
|
+
# Check if marketplace is registered in Claude
|
|
393
|
+
if mp_name not in known_marketplaces:
|
|
394
|
+
results.append(
|
|
395
|
+
ValidationResult(
|
|
396
|
+
check_name="marketplace_registered",
|
|
397
|
+
status="warn",
|
|
398
|
+
message=f"Marketplace '{mp_name}' is not registered in Claude",
|
|
399
|
+
details="Marketplace is in config but not in known_marketplaces.json",
|
|
400
|
+
fix_hint="Run: ai-config sync",
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# Check for path drift
|
|
406
|
+
claude_mp = known_marketplaces[mp_name]
|
|
407
|
+
claude_path_str = claude_mp.get("path", "")
|
|
408
|
+
if claude_path_str:
|
|
409
|
+
claude_path = Path(claude_path_str).resolve()
|
|
410
|
+
|
|
411
|
+
if config_path != claude_path:
|
|
412
|
+
results.append(
|
|
413
|
+
ValidationResult(
|
|
414
|
+
check_name="marketplace_path_drift",
|
|
415
|
+
status="fail",
|
|
416
|
+
message=f"Path drift detected for marketplace '{mp_name}'",
|
|
417
|
+
details=(f"Config path: {config_path}\nClaude path: {claude_path}"),
|
|
418
|
+
fix_hint=(
|
|
419
|
+
f"Run: claude plugin marketplace remove {mp_name} && "
|
|
420
|
+
f"claude plugin marketplace add {config_path}"
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
results.append(
|
|
426
|
+
ValidationResult(
|
|
427
|
+
check_name="marketplace_path_drift",
|
|
428
|
+
status="pass",
|
|
429
|
+
message=f"Marketplace '{mp_name}' path matches registration",
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Plugin validators for ai-config."""
|