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
ai_config/__init__.py
ADDED
ai_config/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters for different AI tool targets."""
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Claude Code adapter for ai-config.
|
|
2
|
+
|
|
3
|
+
This module shells out to the `claude` CLI to manage plugins and marketplaces.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from ai_config.types import PluginSource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class InstalledPlugin:
|
|
17
|
+
"""Information about an installed Claude Code plugin."""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
version: str
|
|
21
|
+
scope: Literal["user", "project", "local"]
|
|
22
|
+
enabled: bool
|
|
23
|
+
install_path: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class InstalledMarketplace:
|
|
28
|
+
"""Information about an installed Claude Code marketplace."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
source: PluginSource
|
|
32
|
+
repo: str
|
|
33
|
+
install_location: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CommandResult:
|
|
38
|
+
"""Result of a CLI command execution."""
|
|
39
|
+
|
|
40
|
+
success: bool
|
|
41
|
+
stdout: str
|
|
42
|
+
stderr: str
|
|
43
|
+
returncode: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_claude_command(args: list[str], timeout: int = 60) -> CommandResult:
|
|
47
|
+
"""Run a claude CLI command and return the result.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
args: Command arguments (without 'claude' prefix).
|
|
51
|
+
timeout: Timeout in seconds.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
CommandResult with output and status.
|
|
55
|
+
"""
|
|
56
|
+
cmd = ["claude"] + args
|
|
57
|
+
try:
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
cmd,
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
)
|
|
64
|
+
return CommandResult(
|
|
65
|
+
success=result.returncode == 0,
|
|
66
|
+
stdout=result.stdout,
|
|
67
|
+
stderr=result.stderr,
|
|
68
|
+
returncode=result.returncode,
|
|
69
|
+
)
|
|
70
|
+
except subprocess.TimeoutExpired:
|
|
71
|
+
return CommandResult(
|
|
72
|
+
success=False,
|
|
73
|
+
stdout="",
|
|
74
|
+
stderr=f"Command timed out after {timeout}s",
|
|
75
|
+
returncode=-1,
|
|
76
|
+
)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
return CommandResult(
|
|
79
|
+
success=False,
|
|
80
|
+
stdout="",
|
|
81
|
+
stderr="claude CLI not found. Is Claude Code installed?",
|
|
82
|
+
returncode=-1,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def list_installed_plugins() -> tuple[list[InstalledPlugin], list[str]]:
|
|
87
|
+
"""List all installed plugins.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (plugins, errors). Plugins list may be empty on error.
|
|
91
|
+
"""
|
|
92
|
+
result = _run_claude_command(["plugin", "list", "--json"])
|
|
93
|
+
|
|
94
|
+
if not result.success:
|
|
95
|
+
return [], [f"Failed to list plugins: {result.stderr}"]
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(result.stdout)
|
|
99
|
+
except json.JSONDecodeError as e:
|
|
100
|
+
return [], [f"Failed to parse plugin list JSON: {e}"]
|
|
101
|
+
|
|
102
|
+
plugins: list[InstalledPlugin] = []
|
|
103
|
+
for item in data:
|
|
104
|
+
plugins.append(
|
|
105
|
+
InstalledPlugin(
|
|
106
|
+
id=item.get("id", ""),
|
|
107
|
+
version=item.get("version", ""),
|
|
108
|
+
scope=item.get("scope", "user"),
|
|
109
|
+
enabled=item.get("enabled", True),
|
|
110
|
+
install_path=item.get("installPath", ""),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return plugins, []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def list_installed_marketplaces() -> tuple[list[InstalledMarketplace], list[str]]:
|
|
118
|
+
"""List all installed marketplaces.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (marketplaces, errors). Marketplaces list may be empty on error.
|
|
122
|
+
"""
|
|
123
|
+
result = _run_claude_command(["plugin", "marketplace", "list", "--json"])
|
|
124
|
+
|
|
125
|
+
if not result.success:
|
|
126
|
+
return [], [f"Failed to list marketplaces: {result.stderr}"]
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(result.stdout)
|
|
130
|
+
except json.JSONDecodeError as e:
|
|
131
|
+
return [], [f"Failed to parse marketplace list JSON: {e}"]
|
|
132
|
+
|
|
133
|
+
marketplaces: list[InstalledMarketplace] = []
|
|
134
|
+
for item in data:
|
|
135
|
+
source_str = item.get("source", "github")
|
|
136
|
+
# Claude CLI returns "directory" for local marketplaces, map to LOCAL
|
|
137
|
+
if source_str == "directory":
|
|
138
|
+
source = PluginSource.LOCAL
|
|
139
|
+
else:
|
|
140
|
+
try:
|
|
141
|
+
source = PluginSource(source_str)
|
|
142
|
+
except ValueError:
|
|
143
|
+
source = PluginSource.GITHUB # Default to github if unknown
|
|
144
|
+
marketplaces.append(
|
|
145
|
+
InstalledMarketplace(
|
|
146
|
+
name=item.get("name", ""),
|
|
147
|
+
source=source,
|
|
148
|
+
repo=item.get("repo", item.get("path", "")), # Use path for local
|
|
149
|
+
install_location=item.get("installLocation", ""),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return marketplaces, []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def install_plugin(
|
|
157
|
+
plugin_id: str, scope: Literal["user", "project", "local"] = "user"
|
|
158
|
+
) -> CommandResult:
|
|
159
|
+
"""Install a plugin from a marketplace.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
plugin_id: Plugin ID, optionally with @marketplace suffix.
|
|
163
|
+
scope: Installation scope (user, project, or local).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
CommandResult from the install command.
|
|
167
|
+
"""
|
|
168
|
+
return _run_claude_command(["plugin", "install", plugin_id, "--scope", scope])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def uninstall_plugin(plugin_id: str) -> CommandResult:
|
|
172
|
+
"""Uninstall a plugin.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
plugin_id: Plugin ID to uninstall.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
CommandResult from the uninstall command.
|
|
179
|
+
"""
|
|
180
|
+
return _run_claude_command(["plugin", "uninstall", plugin_id])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def enable_plugin(plugin_id: str) -> CommandResult:
|
|
184
|
+
"""Enable a disabled plugin.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
plugin_id: Plugin ID to enable.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
CommandResult from the enable command.
|
|
191
|
+
"""
|
|
192
|
+
return _run_claude_command(["plugin", "enable", plugin_id])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def disable_plugin(plugin_id: str) -> CommandResult:
|
|
196
|
+
"""Disable an enabled plugin.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
plugin_id: Plugin ID to disable.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
CommandResult from the disable command.
|
|
203
|
+
"""
|
|
204
|
+
return _run_claude_command(["plugin", "disable", plugin_id])
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def update_plugin(plugin_id: str) -> CommandResult:
|
|
208
|
+
"""Update a plugin to the latest version.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
plugin_id: Plugin ID to update.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
CommandResult from the update command.
|
|
215
|
+
"""
|
|
216
|
+
return _run_claude_command(["plugin", "update", plugin_id])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def add_marketplace(
|
|
220
|
+
repo: str | None = None,
|
|
221
|
+
name: str | None = None, # Note: --name flag not supported by Claude CLI, kept for API compat
|
|
222
|
+
path: str | None = None,
|
|
223
|
+
) -> CommandResult:
|
|
224
|
+
"""Add a marketplace from a GitHub repo or local path.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
repo: GitHub repo in owner/repo format (for github source).
|
|
228
|
+
name: Ignored. Marketplace name comes from marketplace.json.
|
|
229
|
+
path: Local filesystem path (for local source).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
CommandResult from the add command.
|
|
233
|
+
"""
|
|
234
|
+
# Note: The name parameter is accepted but not used - Claude CLI doesn't support
|
|
235
|
+
# custom naming. The marketplace name comes from the marketplace.json file.
|
|
236
|
+
_ = name # Unused, kept for backward compatibility
|
|
237
|
+
|
|
238
|
+
if path:
|
|
239
|
+
# Local marketplace: claude plugin marketplace add <path>
|
|
240
|
+
args = ["plugin", "marketplace", "add", path]
|
|
241
|
+
elif repo:
|
|
242
|
+
# GitHub marketplace: claude plugin marketplace add <repo>
|
|
243
|
+
args = ["plugin", "marketplace", "add", repo]
|
|
244
|
+
else:
|
|
245
|
+
return CommandResult(
|
|
246
|
+
success=False,
|
|
247
|
+
stdout="",
|
|
248
|
+
stderr="Either repo or path must be provided",
|
|
249
|
+
returncode=1,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return _run_claude_command(args)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def remove_marketplace(name: str) -> CommandResult:
|
|
256
|
+
"""Remove a marketplace.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
name: Marketplace name to remove.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
CommandResult from the remove command.
|
|
263
|
+
"""
|
|
264
|
+
return _run_claude_command(["plugin", "marketplace", "remove", name])
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def update_marketplace(name: str | None = None) -> CommandResult:
|
|
268
|
+
"""Update marketplace(s) from their source.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
name: Specific marketplace to update, or None to update all.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
CommandResult from the update command.
|
|
275
|
+
"""
|
|
276
|
+
args = ["plugin", "marketplace", "update"]
|
|
277
|
+
if name:
|
|
278
|
+
args.append(name)
|
|
279
|
+
return _run_claude_command(args)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def clear_cache() -> CommandResult:
|
|
283
|
+
"""Clear the plugin cache by removing the cache directory.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
CommandResult indicating success or failure.
|
|
287
|
+
"""
|
|
288
|
+
cache_dir = Path.home() / ".claude" / "plugins" / "cache"
|
|
289
|
+
if not cache_dir.exists():
|
|
290
|
+
return CommandResult(
|
|
291
|
+
success=True,
|
|
292
|
+
stdout="Cache directory does not exist",
|
|
293
|
+
stderr="",
|
|
294
|
+
returncode=0,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
import shutil
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
shutil.rmtree(cache_dir)
|
|
301
|
+
return CommandResult(
|
|
302
|
+
success=True,
|
|
303
|
+
stdout=f"Removed cache directory: {cache_dir}",
|
|
304
|
+
stderr="",
|
|
305
|
+
returncode=0,
|
|
306
|
+
)
|
|
307
|
+
except OSError as e:
|
|
308
|
+
return CommandResult(
|
|
309
|
+
success=False,
|
|
310
|
+
stdout="",
|
|
311
|
+
stderr=f"Failed to remove cache directory: {e}",
|
|
312
|
+
returncode=1,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_plugin_by_id(plugin_id: str) -> tuple[InstalledPlugin | None, list[str]]:
|
|
317
|
+
"""Get a specific installed plugin by ID.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
plugin_id: Plugin ID to find.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Tuple of (plugin or None, errors).
|
|
324
|
+
"""
|
|
325
|
+
plugins, errors = list_installed_plugins()
|
|
326
|
+
if errors:
|
|
327
|
+
return None, errors
|
|
328
|
+
|
|
329
|
+
for plugin in plugins:
|
|
330
|
+
if plugin.id == plugin_id:
|
|
331
|
+
return plugin, []
|
|
332
|
+
|
|
333
|
+
return None, []
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def get_marketplace_by_name(name: str) -> tuple[InstalledMarketplace | None, list[str]]:
|
|
337
|
+
"""Get a specific installed marketplace by name.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
name: Marketplace name to find.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Tuple of (marketplace or None, errors).
|
|
344
|
+
"""
|
|
345
|
+
marketplaces, errors = list_installed_marketplaces()
|
|
346
|
+
if errors:
|
|
347
|
+
return None, errors
|
|
348
|
+
|
|
349
|
+
for mp in marketplaces:
|
|
350
|
+
if mp.name == name:
|
|
351
|
+
return mp, []
|
|
352
|
+
|
|
353
|
+
return None, []
|