skill-seekers 2.7.3__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.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Coding Agent Detection and Configuration Module
|
|
3
|
+
|
|
4
|
+
This module provides functionality to detect installed AI coding agents
|
|
5
|
+
and generate appropriate MCP server configurations for each agent.
|
|
6
|
+
|
|
7
|
+
Supported agents:
|
|
8
|
+
- Claude Code (stdio)
|
|
9
|
+
- Cursor (HTTP)
|
|
10
|
+
- Windsurf (HTTP)
|
|
11
|
+
- VS Code + Cline extension (stdio)
|
|
12
|
+
- IntelliJ IDEA (HTTP)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import platform
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentDetector:
|
|
22
|
+
"""Detects installed AI coding agents and generates their MCP configurations."""
|
|
23
|
+
|
|
24
|
+
# Agent configuration templates
|
|
25
|
+
AGENT_CONFIG = {
|
|
26
|
+
"claude-code": {
|
|
27
|
+
"name": "Claude Code",
|
|
28
|
+
"transport": "stdio",
|
|
29
|
+
"config_paths": {
|
|
30
|
+
"Linux": "~/.claude.json",
|
|
31
|
+
"Darwin": "~/.claude.json",
|
|
32
|
+
"Windows": "~/.claude.json",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"cursor": {
|
|
36
|
+
"name": "Cursor",
|
|
37
|
+
"transport": "http",
|
|
38
|
+
"config_paths": {
|
|
39
|
+
"Linux": "~/.cursor/mcp_settings.json",
|
|
40
|
+
"Darwin": "~/Library/Application Support/Cursor/mcp_settings.json",
|
|
41
|
+
"Windows": "~\\AppData\\Roaming\\Cursor\\mcp_settings.json",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"windsurf": {
|
|
45
|
+
"name": "Windsurf",
|
|
46
|
+
"transport": "http",
|
|
47
|
+
"config_paths": {
|
|
48
|
+
"Linux": "~/.windsurf/mcp_config.json",
|
|
49
|
+
"Darwin": "~/Library/Application Support/Windsurf/mcp_config.json",
|
|
50
|
+
"Windows": "~\\AppData\\Roaming\\Windsurf\\mcp_config.json",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
"vscode-cline": {
|
|
54
|
+
"name": "VS Code + Cline",
|
|
55
|
+
"transport": "stdio",
|
|
56
|
+
"config_paths": {
|
|
57
|
+
"Linux": "~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
|
|
58
|
+
"Darwin": "~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
|
|
59
|
+
"Windows": "~\\AppData\\Roaming\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"intellij": {
|
|
63
|
+
"name": "IntelliJ IDEA",
|
|
64
|
+
"transport": "http",
|
|
65
|
+
"config_paths": {
|
|
66
|
+
"Linux": "~/.config/JetBrains/IntelliJIdea2024.3/mcp.xml",
|
|
67
|
+
"Darwin": "~/Library/Application Support/JetBrains/IntelliJIdea2024.3/mcp.xml",
|
|
68
|
+
"Windows": "~\\AppData\\Roaming\\JetBrains\\IntelliJIdea2024.3\\mcp.xml",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
"""Initialize the agent detector."""
|
|
75
|
+
self.system = platform.system()
|
|
76
|
+
|
|
77
|
+
def detect_agents(self) -> list[dict[str, str]]:
|
|
78
|
+
"""
|
|
79
|
+
Detect installed AI coding agents on the system.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List of detected agents with their config paths.
|
|
83
|
+
Each dict contains: {'agent': str, 'name': str, 'config_path': str, 'transport': str}
|
|
84
|
+
"""
|
|
85
|
+
detected = []
|
|
86
|
+
|
|
87
|
+
for agent_id, config in self.AGENT_CONFIG.items():
|
|
88
|
+
config_path = self._get_config_path(agent_id)
|
|
89
|
+
if config_path:
|
|
90
|
+
detected.append(
|
|
91
|
+
{
|
|
92
|
+
"agent": agent_id,
|
|
93
|
+
"name": config["name"],
|
|
94
|
+
"config_path": config_path,
|
|
95
|
+
"transport": config["transport"],
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return detected
|
|
100
|
+
|
|
101
|
+
def _get_config_path(self, agent_id: str) -> str | None:
|
|
102
|
+
"""
|
|
103
|
+
Get the configuration path for a specific agent.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
agent_id: Agent identifier (e.g., 'claude-code', 'cursor')
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Expanded config path if the parent directory exists, None otherwise
|
|
110
|
+
"""
|
|
111
|
+
if agent_id not in self.AGENT_CONFIG:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
config_paths = self.AGENT_CONFIG[agent_id]["config_paths"]
|
|
115
|
+
if self.system not in config_paths:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
path = Path(config_paths[self.system]).expanduser()
|
|
119
|
+
|
|
120
|
+
# Check if parent directory exists (agent is likely installed)
|
|
121
|
+
parent = path.parent
|
|
122
|
+
if parent.exists():
|
|
123
|
+
return str(path)
|
|
124
|
+
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def get_transport_type(self, agent_id: str) -> str | None:
|
|
128
|
+
"""
|
|
129
|
+
Get the transport type for a specific agent.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
agent_id: Agent identifier
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
'stdio' or 'http', or None if agent not found
|
|
136
|
+
"""
|
|
137
|
+
if agent_id not in self.AGENT_CONFIG:
|
|
138
|
+
return None
|
|
139
|
+
return self.AGENT_CONFIG[agent_id]["transport"]
|
|
140
|
+
|
|
141
|
+
def generate_config(
|
|
142
|
+
self, agent_id: str, server_command: str, http_port: int | None = 3000
|
|
143
|
+
) -> str | None:
|
|
144
|
+
"""
|
|
145
|
+
Generate MCP configuration for a specific agent.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
agent_id: Agent identifier
|
|
149
|
+
server_command: Command to start the MCP server (e.g., 'skill-seekers mcp')
|
|
150
|
+
http_port: Port for HTTP transport (default: 3000)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Configuration string (JSON or XML) or None if agent not found
|
|
154
|
+
"""
|
|
155
|
+
if agent_id not in self.AGENT_CONFIG:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
transport = self.AGENT_CONFIG[agent_id]["transport"]
|
|
159
|
+
|
|
160
|
+
if agent_id == "intellij":
|
|
161
|
+
return self._generate_intellij_config(server_command, http_port)
|
|
162
|
+
elif transport == "stdio":
|
|
163
|
+
return self._generate_stdio_config(server_command)
|
|
164
|
+
else: # http
|
|
165
|
+
return self._generate_http_config(http_port)
|
|
166
|
+
|
|
167
|
+
def _generate_stdio_config(self, server_command: str) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Generate stdio-based MCP configuration (JSON format).
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
server_command: Command to start the MCP server
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
JSON configuration string
|
|
176
|
+
"""
|
|
177
|
+
# Split command into program and args
|
|
178
|
+
parts = server_command.split()
|
|
179
|
+
command = parts[0] if parts else "skill-seekers"
|
|
180
|
+
args = parts[1:] if len(parts) > 1 else ["mcp"]
|
|
181
|
+
|
|
182
|
+
config = {"mcpServers": {"skill-seeker": {"command": command, "args": args}}}
|
|
183
|
+
|
|
184
|
+
return json.dumps(config, indent=2)
|
|
185
|
+
|
|
186
|
+
def _generate_http_config(self, http_port: int) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Generate HTTP-based MCP configuration (JSON format).
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
http_port: Port number for HTTP server
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
JSON configuration string
|
|
195
|
+
"""
|
|
196
|
+
config = {"mcpServers": {"skill-seeker": {"url": f"http://localhost:{http_port}"}}}
|
|
197
|
+
|
|
198
|
+
return json.dumps(config, indent=2)
|
|
199
|
+
|
|
200
|
+
def _generate_intellij_config(self, _server_command: str, http_port: int) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Generate IntelliJ IDEA MCP configuration (XML format).
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
server_command: Command to start the MCP server
|
|
206
|
+
http_port: Port number for HTTP server
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
XML configuration string
|
|
210
|
+
"""
|
|
211
|
+
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
212
|
+
<application>
|
|
213
|
+
<component name="MCPSettings">
|
|
214
|
+
<servers>
|
|
215
|
+
<server>
|
|
216
|
+
<name>skill-seeker</name>
|
|
217
|
+
<url>http://localhost:{http_port}</url>
|
|
218
|
+
<enabled>true</enabled>
|
|
219
|
+
</server>
|
|
220
|
+
</servers>
|
|
221
|
+
</component>
|
|
222
|
+
</application>"""
|
|
223
|
+
return xml
|
|
224
|
+
|
|
225
|
+
def get_all_config_paths(self) -> dict[str, str]:
|
|
226
|
+
"""
|
|
227
|
+
Get all possible configuration paths for the current system.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Dict mapping agent_id to config_path
|
|
231
|
+
"""
|
|
232
|
+
paths = {}
|
|
233
|
+
for agent_id in self.AGENT_CONFIG:
|
|
234
|
+
path = self._get_config_path(agent_id)
|
|
235
|
+
if path:
|
|
236
|
+
paths[agent_id] = path
|
|
237
|
+
return paths
|
|
238
|
+
|
|
239
|
+
def is_agent_installed(self, agent_id: str) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
Check if a specific agent is installed.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
agent_id: Agent identifier
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if agent appears to be installed, False otherwise
|
|
248
|
+
"""
|
|
249
|
+
return self._get_config_path(agent_id) is not None
|
|
250
|
+
|
|
251
|
+
def get_agent_info(self, agent_id: str) -> dict[str, Any] | None:
|
|
252
|
+
"""
|
|
253
|
+
Get detailed information about a specific agent.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
agent_id: Agent identifier
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict with agent details or None if not found
|
|
260
|
+
"""
|
|
261
|
+
if agent_id not in self.AGENT_CONFIG:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
config = self.AGENT_CONFIG[agent_id]
|
|
265
|
+
config_path = self._get_config_path(agent_id)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"agent": agent_id,
|
|
269
|
+
"name": config["name"],
|
|
270
|
+
"transport": config["transport"],
|
|
271
|
+
"config_path": config_path,
|
|
272
|
+
"installed": config_path is not None,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def detect_agents() -> list[dict[str, str]]:
|
|
277
|
+
"""
|
|
278
|
+
Convenience function to detect installed agents.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of detected agents
|
|
282
|
+
"""
|
|
283
|
+
detector = AgentDetector()
|
|
284
|
+
return detector.detect_agents()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def generate_config(
|
|
288
|
+
agent_name: str, server_command: str = "skill-seekers mcp", http_port: int = 3000
|
|
289
|
+
) -> str | None:
|
|
290
|
+
"""
|
|
291
|
+
Convenience function to generate config for a specific agent.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
agent_name: Agent identifier
|
|
295
|
+
server_command: Command to start the MCP server
|
|
296
|
+
http_port: Port for HTTP transport
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Configuration string or None
|
|
300
|
+
"""
|
|
301
|
+
detector = AgentDetector()
|
|
302
|
+
return detector.generate_config(agent_name, server_command, http_port)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_transport_type(agent_name: str) -> str | None:
|
|
306
|
+
"""
|
|
307
|
+
Convenience function to get transport type for an agent.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
agent_name: Agent identifier
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
'stdio' or 'http', or None
|
|
314
|
+
"""
|
|
315
|
+
detector = AgentDetector()
|
|
316
|
+
return detector.get_transport_type(agent_name)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git Config Repository Manager
|
|
4
|
+
Handles git clone/pull operations for custom config sources
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import git
|
|
14
|
+
from git.exc import GitCommandError, InvalidGitRepositoryError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitConfigRepo:
|
|
18
|
+
"""Manages git operations for config repositories."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, cache_dir: str | None = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize git repository manager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
cache_dir: Base cache directory. Defaults to $SKILL_SEEKERS_CACHE_DIR
|
|
26
|
+
or ~/.skill-seekers/cache/
|
|
27
|
+
"""
|
|
28
|
+
if cache_dir:
|
|
29
|
+
self.cache_dir = Path(cache_dir)
|
|
30
|
+
else:
|
|
31
|
+
# Use environment variable or default
|
|
32
|
+
env_cache = os.environ.get("SKILL_SEEKERS_CACHE_DIR")
|
|
33
|
+
if env_cache:
|
|
34
|
+
self.cache_dir = Path(env_cache).expanduser()
|
|
35
|
+
else:
|
|
36
|
+
self.cache_dir = Path.home() / ".skill-seekers" / "cache"
|
|
37
|
+
|
|
38
|
+
# Ensure cache directory exists
|
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
def clone_or_pull(
|
|
42
|
+
self,
|
|
43
|
+
source_name: str,
|
|
44
|
+
git_url: str,
|
|
45
|
+
branch: str = "main",
|
|
46
|
+
token: str | None = None,
|
|
47
|
+
force_refresh: bool = False,
|
|
48
|
+
) -> Path:
|
|
49
|
+
"""
|
|
50
|
+
Clone repository if not cached, else pull latest changes.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
source_name: Source identifier (used for cache path)
|
|
54
|
+
git_url: Git repository URL
|
|
55
|
+
branch: Branch to clone/pull (default: main)
|
|
56
|
+
token: Optional authentication token
|
|
57
|
+
force_refresh: If True, delete cache and re-clone
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Path to cloned repository
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
GitCommandError: If clone/pull fails
|
|
64
|
+
ValueError: If git_url is invalid
|
|
65
|
+
"""
|
|
66
|
+
# Validate URL
|
|
67
|
+
if not self.validate_git_url(git_url):
|
|
68
|
+
raise ValueError(f"Invalid git URL: {git_url}")
|
|
69
|
+
|
|
70
|
+
# Determine cache path
|
|
71
|
+
repo_path = self.cache_dir / source_name
|
|
72
|
+
|
|
73
|
+
# Force refresh: delete existing cache
|
|
74
|
+
if force_refresh and repo_path.exists():
|
|
75
|
+
shutil.rmtree(repo_path)
|
|
76
|
+
|
|
77
|
+
# Inject token if provided
|
|
78
|
+
clone_url = git_url
|
|
79
|
+
if token:
|
|
80
|
+
clone_url = self.inject_token(git_url, token)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if repo_path.exists() and (repo_path / ".git").exists():
|
|
84
|
+
# Repository exists - pull latest
|
|
85
|
+
try:
|
|
86
|
+
repo = git.Repo(repo_path)
|
|
87
|
+
origin = repo.remotes.origin
|
|
88
|
+
|
|
89
|
+
# Update remote URL if token provided
|
|
90
|
+
if token:
|
|
91
|
+
origin.set_url(clone_url)
|
|
92
|
+
|
|
93
|
+
# Pull latest changes
|
|
94
|
+
origin.pull(branch)
|
|
95
|
+
return repo_path
|
|
96
|
+
except (InvalidGitRepositoryError, GitCommandError):
|
|
97
|
+
# Corrupted repo - delete and re-clone
|
|
98
|
+
shutil.rmtree(repo_path)
|
|
99
|
+
raise # Re-raise to trigger clone below
|
|
100
|
+
|
|
101
|
+
# Repository doesn't exist - clone
|
|
102
|
+
git.Repo.clone_from(
|
|
103
|
+
clone_url,
|
|
104
|
+
repo_path,
|
|
105
|
+
branch=branch,
|
|
106
|
+
depth=1, # Shallow clone
|
|
107
|
+
single_branch=True, # Only clone one branch
|
|
108
|
+
)
|
|
109
|
+
return repo_path
|
|
110
|
+
|
|
111
|
+
except GitCommandError as e:
|
|
112
|
+
error_msg = str(e)
|
|
113
|
+
|
|
114
|
+
# Provide helpful error messages
|
|
115
|
+
if "authentication failed" in error_msg.lower() or "403" in error_msg:
|
|
116
|
+
raise GitCommandError(
|
|
117
|
+
f"Authentication failed for {git_url}. Check your token or permissions.", 128
|
|
118
|
+
) from e
|
|
119
|
+
elif "not found" in error_msg.lower() or "404" in error_msg:
|
|
120
|
+
raise GitCommandError(
|
|
121
|
+
f"Repository not found: {git_url}. Verify the URL is correct and you have access.",
|
|
122
|
+
128,
|
|
123
|
+
) from e
|
|
124
|
+
else:
|
|
125
|
+
raise GitCommandError(f"Failed to clone repository: {error_msg}", 128) from e
|
|
126
|
+
|
|
127
|
+
def find_configs(self, repo_path: Path) -> list[Path]:
|
|
128
|
+
"""
|
|
129
|
+
Find all config files (*.json) in repository.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
repo_path: Path to cloned repo
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of paths to *.json files (sorted by name)
|
|
136
|
+
"""
|
|
137
|
+
if not repo_path.exists():
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
# Find all .json files, excluding .git directory
|
|
141
|
+
configs = []
|
|
142
|
+
for json_file in repo_path.rglob("*.json"):
|
|
143
|
+
# Skip files in .git directory
|
|
144
|
+
if ".git" in json_file.parts:
|
|
145
|
+
continue
|
|
146
|
+
configs.append(json_file)
|
|
147
|
+
|
|
148
|
+
# Sort by filename
|
|
149
|
+
return sorted(configs, key=lambda p: p.name)
|
|
150
|
+
|
|
151
|
+
def get_config(self, repo_path: Path, config_name: str) -> dict:
|
|
152
|
+
"""
|
|
153
|
+
Load specific config by name from repository.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
repo_path: Path to cloned repo
|
|
157
|
+
config_name: Config name (without .json extension)
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Config dictionary
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
FileNotFoundError: If config not found
|
|
164
|
+
ValueError: If config is invalid JSON
|
|
165
|
+
"""
|
|
166
|
+
# Ensure .json extension
|
|
167
|
+
if not config_name.endswith(".json"):
|
|
168
|
+
config_name = f"{config_name}.json"
|
|
169
|
+
|
|
170
|
+
# Search for config file
|
|
171
|
+
all_configs = self.find_configs(repo_path)
|
|
172
|
+
|
|
173
|
+
# Try exact filename match first
|
|
174
|
+
for config_path in all_configs:
|
|
175
|
+
if config_path.name == config_name:
|
|
176
|
+
return self._load_config_file(config_path)
|
|
177
|
+
|
|
178
|
+
# Try case-insensitive match
|
|
179
|
+
config_name_lower = config_name.lower()
|
|
180
|
+
for config_path in all_configs:
|
|
181
|
+
if config_path.name.lower() == config_name_lower:
|
|
182
|
+
return self._load_config_file(config_path)
|
|
183
|
+
|
|
184
|
+
# Config not found - provide helpful error
|
|
185
|
+
available = [p.stem for p in all_configs] # Just filenames without .json
|
|
186
|
+
raise FileNotFoundError(
|
|
187
|
+
f"Config '{config_name}' not found in repository. "
|
|
188
|
+
f"Available configs: {', '.join(available) if available else 'none'}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _load_config_file(self, config_path: Path) -> dict:
|
|
192
|
+
"""
|
|
193
|
+
Load and validate config JSON file.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
config_path: Path to config file
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Config dictionary
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If JSON is invalid
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
with open(config_path, encoding="utf-8") as f:
|
|
206
|
+
return json.load(f)
|
|
207
|
+
except json.JSONDecodeError as e:
|
|
208
|
+
raise ValueError(f"Invalid JSON in config file {config_path.name}: {e}") from e
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def inject_token(git_url: str, token: str) -> str:
|
|
212
|
+
"""
|
|
213
|
+
Inject authentication token into git URL.
|
|
214
|
+
|
|
215
|
+
Converts SSH URLs to HTTPS and adds token for authentication.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
git_url: Original git URL
|
|
219
|
+
token: Authentication token
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
URL with token injected
|
|
223
|
+
|
|
224
|
+
Examples:
|
|
225
|
+
https://github.com/org/repo.git → https://TOKEN@github.com/org/repo.git
|
|
226
|
+
git@github.com:org/repo.git → https://TOKEN@github.com/org/repo.git
|
|
227
|
+
"""
|
|
228
|
+
# Convert SSH to HTTPS
|
|
229
|
+
if git_url.startswith("git@"):
|
|
230
|
+
# git@github.com:org/repo.git → github.com/org/repo.git
|
|
231
|
+
parts = git_url.replace("git@", "").replace(":", "/", 1)
|
|
232
|
+
git_url = f"https://{parts}"
|
|
233
|
+
|
|
234
|
+
# Parse URL
|
|
235
|
+
parsed = urlparse(git_url)
|
|
236
|
+
|
|
237
|
+
# Inject token
|
|
238
|
+
if parsed.hostname:
|
|
239
|
+
# https://github.com/org/repo.git → https://TOKEN@github.com/org/repo.git
|
|
240
|
+
netloc = f"{token}@{parsed.hostname}"
|
|
241
|
+
if parsed.port:
|
|
242
|
+
netloc = f"{netloc}:{parsed.port}"
|
|
243
|
+
|
|
244
|
+
return f"{parsed.scheme}://{netloc}{parsed.path}"
|
|
245
|
+
|
|
246
|
+
return git_url
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def validate_git_url(git_url: str) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Validate git URL format.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
git_url: Git repository URL
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if valid, False otherwise
|
|
258
|
+
"""
|
|
259
|
+
if not git_url:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
# Accept HTTPS URLs
|
|
263
|
+
if git_url.startswith("https://") or git_url.startswith("http://"):
|
|
264
|
+
parsed = urlparse(git_url)
|
|
265
|
+
return bool(parsed.hostname and parsed.path)
|
|
266
|
+
|
|
267
|
+
# Accept SSH URLs
|
|
268
|
+
if git_url.startswith("git@"):
|
|
269
|
+
# git@github.com:org/repo.git
|
|
270
|
+
return ":" in git_url and len(git_url.split(":")) == 2
|
|
271
|
+
|
|
272
|
+
# Accept file:// URLs (for local testing)
|
|
273
|
+
return bool(git_url.startswith("file://"))
|