aiaddons 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.
Files changed (130) hide show
  1. aiaddons/__init__.py +5 -0
  2. aiaddons/__main__.py +6 -0
  3. aiaddons/agents/__init__.py +16 -0
  4. aiaddons/agents/antigravity.py +106 -0
  5. aiaddons/agents/base.py +31 -0
  6. aiaddons/agents/claude_code.py +85 -0
  7. aiaddons/agents/codex.py +81 -0
  8. aiaddons/agents/manager.py +58 -0
  9. aiaddons/agents/utils.py +48 -0
  10. aiaddons/cli/__init__.py +5 -0
  11. aiaddons/cli/commands/__init__.py +1 -0
  12. aiaddons/cli/commands/agents.py +74 -0
  13. aiaddons/cli/commands/compatibility.py +110 -0
  14. aiaddons/cli/commands/doctor.py +122 -0
  15. aiaddons/cli/commands/install.py +963 -0
  16. aiaddons/cli/commands/registry.py +345 -0
  17. aiaddons/cli/commands/remove.py +564 -0
  18. aiaddons/cli/commands/sync.py +599 -0
  19. aiaddons/cli/commands/tui.py +20 -0
  20. aiaddons/cli/commands/update.py +667 -0
  21. aiaddons/cli/exit_codes.py +17 -0
  22. aiaddons/cli/main.py +87 -0
  23. aiaddons/core/__init__.py +1 -0
  24. aiaddons/core/acquisition/__init__.py +29 -0
  25. aiaddons/core/acquisition/base.py +38 -0
  26. aiaddons/core/acquisition/engine.py +97 -0
  27. aiaddons/core/acquisition/extractor.py +268 -0
  28. aiaddons/core/acquisition/git.py +163 -0
  29. aiaddons/core/acquisition/local.py +136 -0
  30. aiaddons/core/acquisition/models.py +42 -0
  31. aiaddons/core/acquisition/package.py +94 -0
  32. aiaddons/core/acquisition/staging.py +97 -0
  33. aiaddons/core/acquisition/url.py +162 -0
  34. aiaddons/core/compatibility/__init__.py +15 -0
  35. aiaddons/core/compatibility/engine.py +365 -0
  36. aiaddons/core/compatibility/models.py +49 -0
  37. aiaddons/core/drift.py +73 -0
  38. aiaddons/core/exceptions.py +119 -0
  39. aiaddons/core/execution/__init__.py +37 -0
  40. aiaddons/core/execution/engine.py +1730 -0
  41. aiaddons/core/execution/external/__init__.py +27 -0
  42. aiaddons/core/execution/external/models.py +47 -0
  43. aiaddons/core/execution/external/runner.py +165 -0
  44. aiaddons/core/execution/external/runtimes.py +136 -0
  45. aiaddons/core/execution/external/security.py +115 -0
  46. aiaddons/core/execution/models.py +44 -0
  47. aiaddons/core/execution/primitives.py +213 -0
  48. aiaddons/core/execution/security.py +82 -0
  49. aiaddons/core/health/__init__.py +17 -0
  50. aiaddons/core/health/engine.py +1144 -0
  51. aiaddons/core/health/models.py +75 -0
  52. aiaddons/core/installer/__init__.py +47 -0
  53. aiaddons/core/installer/engine.py +515 -0
  54. aiaddons/core/installer/models.py +596 -0
  55. aiaddons/core/models/__init__.py +47 -0
  56. aiaddons/core/models/agent.py +63 -0
  57. aiaddons/core/models/manifest.py +448 -0
  58. aiaddons/core/models/stack.py +117 -0
  59. aiaddons/core/secrets/__init__.py +29 -0
  60. aiaddons/core/secrets/models.py +52 -0
  61. aiaddons/core/secrets/resolver.py +472 -0
  62. aiaddons/core/sync/__init__.py +21 -0
  63. aiaddons/core/sync/engine.py +601 -0
  64. aiaddons/core/sync/models.py +106 -0
  65. aiaddons/core/update/__init__.py +6 -0
  66. aiaddons/core/update/engine.py +501 -0
  67. aiaddons/core/update/models.py +78 -0
  68. aiaddons/core/verification/__init__.py +16 -0
  69. aiaddons/core/verification/engine.py +916 -0
  70. aiaddons/core/verification/models.py +51 -0
  71. aiaddons/integrations/__init__.py +15 -0
  72. aiaddons/integrations/base.py +38 -0
  73. aiaddons/integrations/cli_tool.py +97 -0
  74. aiaddons/integrations/mcp.py +190 -0
  75. aiaddons/integrations/plugin.py +241 -0
  76. aiaddons/integrations/skill.py +128 -0
  77. aiaddons/registry/__init__.py +22 -0
  78. aiaddons/registry/addons/brave-search-mcp.yaml +48 -0
  79. aiaddons/registry/addons/caveman.yaml +35 -0
  80. aiaddons/registry/addons/context7-mcp.yaml +48 -0
  81. aiaddons/registry/addons/figma-mcp.yaml +50 -0
  82. aiaddons/registry/addons/filesystem-mcp.yaml +40 -0
  83. aiaddons/registry/addons/firecrawl-mcp.yaml +50 -0
  84. aiaddons/registry/addons/github-mcp.yaml +48 -0
  85. aiaddons/registry/addons/karpathy-behavioral-skill.yaml +36 -0
  86. aiaddons/registry/addons/notion-mcp.yaml +49 -0
  87. aiaddons/registry/addons/playwright-mcp.yaml +41 -0
  88. aiaddons/registry/addons/ponytail-audit-skill.yaml +32 -0
  89. aiaddons/registry/addons/ponytail-debt-skill.yaml +32 -0
  90. aiaddons/registry/addons/ponytail-gain-skill.yaml +32 -0
  91. aiaddons/registry/addons/ponytail-help-skill.yaml +32 -0
  92. aiaddons/registry/addons/ponytail-review-skill.yaml +32 -0
  93. aiaddons/registry/addons/ponytail-skill.yaml +32 -0
  94. aiaddons/registry/addons/ponytail.yaml +40 -0
  95. aiaddons/registry/addons/postgres-mcp.yaml +47 -0
  96. aiaddons/registry/addons/python-lint-plugin.yaml +38 -0
  97. aiaddons/registry/addons/sentry-mcp.yaml +48 -0
  98. aiaddons/registry/addons/sequential-thinking-mcp.yaml +40 -0
  99. aiaddons/registry/addons/skill-creator.yaml +37 -0
  100. aiaddons/registry/addons/stripe-mcp.yaml +48 -0
  101. aiaddons/registry/addons/supabase-mcp.yaml +48 -0
  102. aiaddons/registry/addons/superpowers-brainstorming.yaml +35 -0
  103. aiaddons/registry/addons/superpowers-planning.yaml +35 -0
  104. aiaddons/registry/addons/superpowers-review.yaml +35 -0
  105. aiaddons/registry/addons/superpowers-tdd.yaml +35 -0
  106. aiaddons/registry/addons/superpowers-worktree.yaml +40 -0
  107. aiaddons/registry/addons/superpowers.yaml +40 -0
  108. aiaddons/registry/addons/vibesec-skill.yaml +35 -0
  109. aiaddons/registry/cache.py +126 -0
  110. aiaddons/registry/client.py +85 -0
  111. aiaddons/registry/loader.py +103 -0
  112. aiaddons/registry/models.py +45 -0
  113. aiaddons/registry/registry.py +179 -0
  114. aiaddons/registry/validator.py +123 -0
  115. aiaddons/state/__init__.py +21 -0
  116. aiaddons/state/lockfile.py +136 -0
  117. aiaddons/state/locking.py +55 -0
  118. aiaddons/state/store.py +158 -0
  119. aiaddons/state/transaction.py +136 -0
  120. aiaddons/tui/__init__.py +5 -0
  121. aiaddons/tui/app.py +1010 -0
  122. aiaddons/tui/screens/__init__.py +17 -0
  123. aiaddons/tui/screens/health.py +266 -0
  124. aiaddons/tui/screens/modals.py +314 -0
  125. aiaddons/tui/screens/sync.py +387 -0
  126. aiaddons-0.1.0.dist-info/METADATA +458 -0
  127. aiaddons-0.1.0.dist-info/RECORD +130 -0
  128. aiaddons-0.1.0.dist-info/WHEEL +4 -0
  129. aiaddons-0.1.0.dist-info/entry_points.txt +2 -0
  130. aiaddons-0.1.0.dist-info/licenses/LICENSE +21 -0
aiaddons/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """AI Add-ons Manager (`aiaddons`)."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
aiaddons/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Executable module entry point for `python -m aiaddons`."""
2
+
3
+ from aiaddons.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,16 @@
1
+ """Agent detection and adapters subpackage."""
2
+
3
+ from aiaddons.agents.antigravity import AntigravityAdapter
4
+ from aiaddons.agents.base import BaseAgentAdapter
5
+ from aiaddons.agents.claude_code import ClaudeCodeAdapter
6
+ from aiaddons.agents.codex import CodexAdapter
7
+ from aiaddons.agents.manager import AgentDetectionManager, detect_agents
8
+
9
+ __all__ = [
10
+ "AntigravityAdapter",
11
+ "BaseAgentAdapter",
12
+ "ClaudeCodeAdapter",
13
+ "CodexAdapter",
14
+ "AgentDetectionManager",
15
+ "detect_agents",
16
+ ]
@@ -0,0 +1,106 @@
1
+ """Antigravity CLI agent detection adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from aiaddons.agents.utils import find_executable, run_version_command
6
+ from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
7
+
8
+
9
+ class AntigravityAdapter:
10
+ """Adapter for detecting and configuring Antigravity CLI."""
11
+
12
+ agent_id: str = "antigravity"
13
+ name: str = "Antigravity CLI"
14
+ capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
15
+
16
+ def supports_capability(self, capability: AgentCapability) -> bool:
17
+ """Check if Antigravity CLI supports the given capability."""
18
+ return capability in self.capabilities
19
+
20
+ def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
21
+ """Get the configuration path for Antigravity CLI based on scope."""
22
+ if scope == Scope.GLOBAL:
23
+ global_mcp = Path.home() / ".gemini" / "config" / "mcp_config.json"
24
+ if global_mcp.exists():
25
+ return global_mcp
26
+ global_config_dir = Path.home() / ".gemini" / "config"
27
+ if global_config_dir.exists():
28
+ return global_mcp
29
+ global_gemini_dir = Path.home() / ".gemini"
30
+ if global_gemini_dir.exists():
31
+ return global_mcp
32
+ return global_mcp
33
+
34
+ base_dir = project_path if project_path is not None else Path.cwd()
35
+ ws_mcp = base_dir / ".agents" / "mcp_config.json"
36
+ if ws_mcp.exists():
37
+ return ws_mcp
38
+ ws_agents = base_dir / ".agents"
39
+ if ws_agents.exists():
40
+ return ws_mcp
41
+ return ws_mcp
42
+
43
+ def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
44
+ """Get the skill installation directory for Antigravity CLI based on scope."""
45
+ if scope == Scope.GLOBAL:
46
+ return Path.home() / ".gemini" / "config" / "skills"
47
+ base_dir = project_path if project_path is not None else Path.cwd()
48
+ return base_dir / ".agents" / "skills"
49
+
50
+ def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
51
+ """Detect Antigravity CLI installation status, binary path, version, and config location."""
52
+ exec_path = find_executable("agy")
53
+ if exec_path is None:
54
+ exec_path = find_executable("antigravity")
55
+
56
+ global_config = self.get_config_path(Scope.GLOBAL)
57
+ workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
58
+
59
+ base_dir = project_path if project_path is not None else Path.cwd()
60
+ has_global = (
61
+ (global_config is not None and global_config.exists())
62
+ or (Path.home() / ".gemini").exists()
63
+ )
64
+ has_workspace = (
65
+ (workspace_config is not None and workspace_config.exists())
66
+ or (base_dir / ".agents").exists()
67
+ or (base_dir / "AGENTS.md").exists()
68
+ or (base_dir / "GEMINI.md").exists()
69
+ )
70
+ installed = (exec_path is not None) or has_global or has_workspace
71
+
72
+ version: str | None = None
73
+ detection_error: str | None = None
74
+
75
+ if exec_path is not None:
76
+ try:
77
+ cmd_name = exec_path.name
78
+ if cmd_name.lower().endswith(".exe"):
79
+ cmd_name = cmd_name[:-4]
80
+ version = run_version_command(cmd_name)
81
+ except Exception as exc:
82
+ detection_error = f"Error running version command: {exc}"
83
+
84
+ active_config = (
85
+ workspace_config
86
+ if (has_workspace and workspace_config and workspace_config.exists())
87
+ else (
88
+ global_config
89
+ if (has_global and global_config and global_config.exists())
90
+ else (workspace_config or global_config)
91
+ )
92
+ )
93
+
94
+ return AgentDetectionResult(
95
+ agent_id=self.agent_id,
96
+ name=self.name,
97
+ installed=installed,
98
+ version=version,
99
+ executable_path=str(exec_path) if exec_path else None,
100
+ global_config_path=str(global_config) if global_config else None,
101
+ workspace_config_path=str(workspace_config) if workspace_config else None,
102
+ config_path=str(active_config) if active_config else None,
103
+ supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
104
+ capabilities=list(self.capabilities),
105
+ detection_error=detection_error,
106
+ )
@@ -0,0 +1,31 @@
1
+ """Base agent adapter protocol defining standard interface for agent detection and configuration."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
7
+
8
+
9
+ @runtime_checkable
10
+ class BaseAgentAdapter(Protocol):
11
+ """Protocol for AI coding agent detection and configuration adapters."""
12
+
13
+ agent_id: str
14
+ name: str
15
+ capabilities: list[AgentCapability]
16
+
17
+ def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
18
+ """Detect agent installation status, binary path, version, and config location."""
19
+ ...
20
+
21
+ def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
22
+ """Get path to the agent's configuration file for the specified scope."""
23
+ ...
24
+
25
+ def supports_capability(self, capability: AgentCapability) -> bool:
26
+ """Check if this agent supports a specific integration capability."""
27
+ ...
28
+
29
+ def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
30
+ """Get canonical skill installation directory for the specified scope."""
31
+ ...
@@ -0,0 +1,85 @@
1
+ """Claude Code agent detection adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from aiaddons.agents.utils import find_executable, run_version_command
6
+ from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
7
+
8
+
9
+ class ClaudeCodeAdapter:
10
+ """Adapter for detecting and configuring Claude Code."""
11
+
12
+ agent_id: str = "claude-code"
13
+ name: str = "Claude Code"
14
+ capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
15
+
16
+ def supports_capability(self, capability: AgentCapability) -> bool:
17
+ """Check if Claude Code supports the given capability."""
18
+ return capability in self.capabilities
19
+
20
+ def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
21
+ """Get the configuration path for Claude Code based on scope."""
22
+ if scope == Scope.GLOBAL:
23
+ global_json = Path.home() / ".claude.json"
24
+ if global_json.exists():
25
+ return global_json
26
+ global_dir = Path.home() / ".claude"
27
+ if global_dir.exists():
28
+ return global_dir
29
+ return global_json
30
+
31
+ base_dir = project_path if project_path is not None else Path.cwd()
32
+ ws_json = base_dir / ".claude.json"
33
+ if ws_json.exists():
34
+ return ws_json
35
+ ws_dir = base_dir / ".claude"
36
+ if ws_dir.exists():
37
+ return ws_dir
38
+ return ws_json
39
+
40
+ def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
41
+ """Get the skill installation directory for Claude Code based on scope."""
42
+ if scope == Scope.GLOBAL:
43
+ return Path.home() / ".claude" / "skills"
44
+ base_dir = project_path if project_path is not None else Path.cwd()
45
+ return base_dir / ".claude" / "skills"
46
+
47
+ def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
48
+ """Detect Claude Code installation status, binary path, version, and config location."""
49
+ exec_path = find_executable("claude")
50
+ global_config = self.get_config_path(Scope.GLOBAL)
51
+ workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
52
+
53
+ # Check if config exists on disk
54
+ has_global = global_config is not None and global_config.exists()
55
+ has_workspace = workspace_config is not None and workspace_config.exists()
56
+ installed = (exec_path is not None) or has_global or has_workspace
57
+
58
+ version: str | None = None
59
+ detection_error: str | None = None
60
+
61
+ if exec_path is not None:
62
+ try:
63
+ version = run_version_command("claude")
64
+ except Exception as exc:
65
+ detection_error = f"Error running version command: {exc}"
66
+
67
+ active_config = (
68
+ workspace_config
69
+ if (has_workspace and workspace_config)
70
+ else (global_config if (has_global and global_config) else None)
71
+ )
72
+
73
+ return AgentDetectionResult(
74
+ agent_id=self.agent_id,
75
+ name=self.name,
76
+ installed=installed,
77
+ version=version,
78
+ executable_path=str(exec_path) if exec_path else None,
79
+ global_config_path=str(global_config) if global_config else None,
80
+ workspace_config_path=str(workspace_config) if workspace_config else None,
81
+ config_path=str(active_config) if active_config else None,
82
+ supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
83
+ capabilities=list(self.capabilities),
84
+ detection_error=detection_error,
85
+ )
@@ -0,0 +1,81 @@
1
+ """OpenAI Codex agent detection adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from aiaddons.agents.utils import find_executable, run_version_command
6
+ from aiaddons.core.models.agent import AgentCapability, AgentDetectionResult, Scope
7
+
8
+
9
+ class CodexAdapter:
10
+ """Adapter for detecting and configuring OpenAI Codex CLI."""
11
+
12
+ agent_id: str = "codex"
13
+ name: str = "OpenAI Codex"
14
+ capabilities: list[AgentCapability] = [AgentCapability.MCP, AgentCapability.SKILL]
15
+
16
+ def supports_capability(self, capability: AgentCapability) -> bool:
17
+ """Check if OpenAI Codex supports the given capability."""
18
+ return capability in self.capabilities
19
+
20
+ def get_config_path(self, scope: Scope, project_path: Path | None = None) -> Path | None:
21
+ """Get the configuration path for OpenAI Codex based on scope."""
22
+ if scope == Scope.GLOBAL:
23
+ global_dir = Path.home() / ".codex"
24
+ if global_dir.exists():
25
+ return global_dir
26
+ return global_dir
27
+
28
+ base_dir = project_path if project_path is not None else Path.cwd()
29
+ ws_dir = base_dir / ".codex"
30
+ if ws_dir.exists():
31
+ return ws_dir
32
+ ws_agents = base_dir / ".agents"
33
+ if ws_agents.exists():
34
+ return ws_agents
35
+ return ws_dir
36
+
37
+ def get_skill_directory(self, scope: Scope, project_path: Path | None = None) -> Path:
38
+ """Get the skill installation directory for OpenAI Codex based on scope."""
39
+ if scope == Scope.GLOBAL:
40
+ return Path.home() / ".codex" / "skills"
41
+ base_dir = project_path if project_path is not None else Path.cwd()
42
+ return base_dir / ".agents" / "skills"
43
+
44
+ def detect(self, project_path: Path | None = None) -> AgentDetectionResult:
45
+ """Detect OpenAI Codex installation status, binary path, version, and config location."""
46
+ exec_path = find_executable("codex")
47
+ global_config = self.get_config_path(Scope.GLOBAL)
48
+ workspace_config = self.get_config_path(Scope.WORKSPACE, project_path=project_path)
49
+
50
+ has_global = global_config is not None and global_config.exists()
51
+ has_workspace = workspace_config is not None and workspace_config.exists()
52
+ installed = (exec_path is not None) or has_global or has_workspace
53
+
54
+ version: str | None = None
55
+ detection_error: str | None = None
56
+
57
+ if exec_path is not None:
58
+ try:
59
+ version = run_version_command("codex")
60
+ except Exception as exc:
61
+ detection_error = f"Error running version command: {exc}"
62
+
63
+ active_config = (
64
+ workspace_config
65
+ if (has_workspace and workspace_config)
66
+ else (global_config if (has_global and global_config) else None)
67
+ )
68
+
69
+ return AgentDetectionResult(
70
+ agent_id=self.agent_id,
71
+ name=self.name,
72
+ installed=installed,
73
+ version=version,
74
+ executable_path=str(exec_path) if exec_path else None,
75
+ global_config_path=str(global_config) if global_config else None,
76
+ workspace_config_path=str(workspace_config) if workspace_config else None,
77
+ config_path=str(active_config) if active_config else None,
78
+ supported_scopes=[Scope.GLOBAL, Scope.WORKSPACE],
79
+ capabilities=list(self.capabilities),
80
+ detection_error=detection_error,
81
+ )
@@ -0,0 +1,58 @@
1
+ """Agent detection manager aggregating all registered AI agent adapters."""
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from aiaddons.agents.antigravity import AntigravityAdapter
7
+ from aiaddons.agents.base import BaseAgentAdapter
8
+ from aiaddons.agents.claude_code import ClaudeCodeAdapter
9
+ from aiaddons.agents.codex import CodexAdapter
10
+ from aiaddons.core.models.agent import AgentDetectionResult
11
+
12
+
13
+ class AgentDetectionManager:
14
+ """Manager component for detecting installed AI coding agents."""
15
+
16
+ def __init__(self, adapters: Sequence[BaseAgentAdapter] | None = None) -> None:
17
+ if adapters is None:
18
+ self._adapters: list[BaseAgentAdapter] = [
19
+ ClaudeCodeAdapter(),
20
+ CodexAdapter(),
21
+ AntigravityAdapter(),
22
+ ]
23
+ else:
24
+ self._adapters = list(adapters)
25
+
26
+ def register_adapter(self, adapter: BaseAgentAdapter) -> None:
27
+ """Register a new agent adapter."""
28
+ self._adapters.append(adapter)
29
+
30
+ def get_adapter(self, agent_id: str) -> BaseAgentAdapter | None:
31
+ """Retrieve registered adapter for a given agent ID."""
32
+ aid = agent_id.lower()
33
+ for adapter in self._adapters:
34
+ if adapter.agent_id.lower() == aid:
35
+ return adapter
36
+ return None
37
+
38
+ def detect_agents(self, project_path: Path | None = None) -> dict[str, AgentDetectionResult]:
39
+ """Detect all registered agents and return results mapped by agent ID."""
40
+ results: dict[str, AgentDetectionResult] = {}
41
+ for adapter in self._adapters:
42
+ try:
43
+ results[adapter.agent_id] = adapter.detect(project_path=project_path)
44
+ except Exception as exc:
45
+ results[adapter.agent_id] = AgentDetectionResult(
46
+ agent_id=adapter.agent_id,
47
+ name=adapter.name,
48
+ installed=False,
49
+ capabilities=list(adapter.capabilities),
50
+ detection_error=f"Unexpected detection error: {exc}",
51
+ )
52
+ return results
53
+
54
+
55
+ def detect_agents(project_path: Path | None = None) -> dict[str, AgentDetectionResult]:
56
+ """Convenience function to run agent detection with default adapters."""
57
+ manager = AgentDetectionManager()
58
+ return manager.detect_agents(project_path=project_path)
@@ -0,0 +1,48 @@
1
+ """Helper utilities for agent detection and CLI binary version parsing."""
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+
9
+ def find_executable(name: str) -> Path | None:
10
+ """Find executable in system PATH using shutil.which, returning absolute Path or None."""
11
+ found = shutil.which(name)
12
+ if found:
13
+ return Path(found)
14
+ return None
15
+
16
+
17
+ def parse_version_output(output: str) -> str | None:
18
+ """Extract a SemVer-like version string from raw command output."""
19
+ if not output:
20
+ return None
21
+ match = re.search(r"v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)", output)
22
+ if match:
23
+ return match.group(1)
24
+ return None
25
+
26
+
27
+ def run_version_command(
28
+ cmd: str, args: list[str] | None = None, timeout_seconds: float = 5.0
29
+ ) -> str | None:
30
+ """Safely run executable version command without shell=True and return parsed version."""
31
+ executable_path = find_executable(cmd)
32
+ if executable_path is None:
33
+ return None
34
+
35
+ full_cmd = [str(executable_path)] + (args if args is not None else ["--version"])
36
+
37
+ try:
38
+ res = subprocess.run(
39
+ full_cmd,
40
+ capture_output=True,
41
+ text=True,
42
+ timeout=timeout_seconds,
43
+ check=False,
44
+ )
45
+ output_text = res.stdout if res.stdout else res.stderr
46
+ return parse_version_output(output_text)
47
+ except (subprocess.SubprocessError, OSError, ValueError):
48
+ return None
@@ -0,0 +1,5 @@
1
+ """CLI subpackage for aiaddons."""
2
+
3
+ from aiaddons.cli.main import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1 @@
1
+ """CLI commands subpackage."""
@@ -0,0 +1,74 @@
1
+ """CLI command for listing and detecting AI coding agents."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from aiaddons.agents.manager import detect_agents
11
+
12
+ console = Console()
13
+
14
+
15
+ def _get_symbols() -> tuple[str, str]:
16
+ """Return platform and encoding safe status symbols."""
17
+ try:
18
+ "✓".encode(sys.stdout.encoding or "utf-8")
19
+ return "✓", "✗"
20
+ except (UnicodeEncodeError, AttributeError, TypeError):
21
+ return "+", "-"
22
+
23
+
24
+ def agents_command(
25
+ json_output: bool = typer.Option(
26
+ False,
27
+ "--json",
28
+ "-j",
29
+ help="Output raw detection results in JSON format.",
30
+ ),
31
+ project_path: Path | None = typer.Option(
32
+ None,
33
+ "--project-path",
34
+ "-p",
35
+ help="Specify custom project directory for detection.",
36
+ ),
37
+ ) -> None:
38
+ """List detected AI coding agents and their installation status."""
39
+ results = detect_agents(project_path=project_path)
40
+
41
+ if json_output:
42
+ serialized = {k: v.model_dump() for k, v in results.items()}
43
+ sys.stdout.write(json.dumps(serialized, indent=2) + "\n")
44
+ sys.stdout.flush()
45
+ return
46
+
47
+ console.print("\n[bold cyan]AI Coding Agents[/bold cyan]\n")
48
+ check_sym, cross_sym = _get_symbols()
49
+
50
+ for _agent_id, res in results.items():
51
+ if res.installed:
52
+ console.print(f"[bold green]{check_sym}[/bold green] [bold]{res.name}[/bold]")
53
+ if res.version:
54
+ console.print(f" [dim]Version:[/dim] {res.version}")
55
+ else:
56
+ console.print(" [dim]Version:[/dim] [yellow]Unknown[/yellow]")
57
+
58
+ if res.executable_path:
59
+ console.print(f" [dim]Path:[/dim] {res.executable_path}")
60
+
61
+ if res.config_path:
62
+ console.print(f" [dim]Config:[/dim] {res.config_path}")
63
+
64
+ if res.capabilities:
65
+ caps_str = ", ".join(c.value for c in res.capabilities)
66
+ console.print(f" [dim]Capabilities:[/dim] {caps_str}")
67
+ else:
68
+ console.print(f"[bold red]{cross_sym}[/bold red] [bold]{res.name}[/bold]")
69
+ console.print(" [dim]Not installed[/dim]")
70
+
71
+ if res.detection_error:
72
+ console.print(f" [red]Detection Note:[/red] {res.detection_error}")
73
+
74
+ console.print()
@@ -0,0 +1,110 @@
1
+ """CLI command for checking add-on compatibility against detected AI coding agents."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from aiaddons.agents.manager import AgentDetectionManager
11
+ from aiaddons.core.compatibility.engine import CompatibilityEngine
12
+ from aiaddons.core.models.agent import Scope
13
+ from aiaddons.registry.registry import Registry
14
+
15
+ console = Console()
16
+
17
+
18
+ def _get_symbols() -> tuple[str, str]:
19
+ """Return platform-safe symbols for checkmark and cross mark."""
20
+ try:
21
+ "✓".encode(sys.stdout.encoding or "utf-8")
22
+ return "✓", "✗"
23
+ except Exception:
24
+ return "+", "-"
25
+
26
+
27
+ def check_command(
28
+ addon_id: str = typer.Argument(..., help="Add-on ID to evaluate compatibility for."),
29
+ scope: str = typer.Option(
30
+ "workspace",
31
+ "--scope",
32
+ "-s",
33
+ help="Requested installation scope (global or workspace).",
34
+ ),
35
+ json_output: bool = typer.Option(
36
+ False,
37
+ "--json",
38
+ "-j",
39
+ help="Output evaluation results in JSON format.",
40
+ ),
41
+ registry_dir: Path | None = typer.Option(
42
+ None,
43
+ "--registry-dir",
44
+ "-r",
45
+ help="Path to custom registry directory.",
46
+ ),
47
+ ) -> None:
48
+ """Evaluate compatibility of an add-on against all detected AI coding agents (read-only)."""
49
+ try:
50
+ req_scope = Scope.from_str(scope)
51
+ except ValueError as e:
52
+ console.print(f"[bold red]Error:[/bold red] {e}")
53
+ raise typer.Exit(code=1) from e
54
+
55
+ registry, _ = Registry.load_auto(custom_dir=registry_dir)
56
+
57
+ manifest = registry.get(addon_id)
58
+ if not manifest:
59
+ console.print(f"[bold red]Error:[/bold red] Add-on '{addon_id}' not found in registry.")
60
+ raise typer.Exit(code=1)
61
+
62
+ manager = AgentDetectionManager()
63
+ detected_agents = list(manager.detect_agents().values())
64
+
65
+ engine = CompatibilityEngine(registry=registry)
66
+ results = engine.evaluate_all(
67
+ manifest, detected_agents, requested_scope=req_scope, registry=registry
68
+ )
69
+
70
+ if json_output:
71
+ serialized = [r.model_dump() for r in results]
72
+ sys.stdout.write(json.dumps(serialized, indent=2) + "\n")
73
+ sys.stdout.flush()
74
+ return
75
+
76
+ sym_ok, sym_fail = _get_symbols()
77
+
78
+ console.print(
79
+ f"\n[bold cyan]Compatibility Evaluation for '{manifest.name}' ({manifest.id})[/bold cyan]"
80
+ )
81
+ console.print(f"[bold]Requested Scope:[/bold] {req_scope.value}\n")
82
+
83
+ for r in results:
84
+ status_str = (
85
+ f"[bold green]{sym_ok} Compatible[/bold green]"
86
+ if r.compatible
87
+ else f"[bold red]{sym_fail} Incompatible[/bold red]"
88
+ )
89
+ console.print(f"[bold]{r.agent_name}[/bold] ({r.agent_id}): {status_str}")
90
+
91
+ for reason in r.reasons:
92
+ color = "green" if r.compatible else "yellow"
93
+ console.print(f" [{color}]• {reason}[/{color}]")
94
+
95
+ if r.missing_requirements:
96
+ console.print(" [bold red]Missing Requirements:[/bold red]")
97
+ for req in r.missing_requirements:
98
+ console.print(f" - {req}")
99
+
100
+ if r.unsupported_requirements:
101
+ console.print(" [bold red]Unsupported Requirements:[/bold red]")
102
+ for unsupp in r.unsupported_requirements:
103
+ console.print(f" - {unsupp}")
104
+
105
+ if r.warnings:
106
+ console.print(" [bold yellow]Warnings:[/bold yellow]")
107
+ for warn in r.warnings:
108
+ console.print(f" - {warn}")
109
+
110
+ console.print()