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 ADDED
@@ -0,0 +1,3 @@
1
+ """ai-config: Declarative plugin manager for AI coding tools."""
2
+
3
+ __version__ = "0.1.0"
ai_config/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m ai_config."""
2
+
3
+ from ai_config.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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, []