devsync 0.5.5__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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- devsync-0.5.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Package manifest parsing and validation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from aiconfigkit.core.models import (
|
|
8
|
+
CommandComponent,
|
|
9
|
+
CredentialDescriptor,
|
|
10
|
+
HookComponent,
|
|
11
|
+
InstructionComponent,
|
|
12
|
+
MCPServerComponent,
|
|
13
|
+
Package,
|
|
14
|
+
PackageComponents,
|
|
15
|
+
ResourceComponent,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ValidationError(Exception):
|
|
20
|
+
"""Raised when manifest validation fails."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PackageManifestParser:
|
|
26
|
+
"""
|
|
27
|
+
Parser and validator for package manifest files.
|
|
28
|
+
|
|
29
|
+
Parses ai-config-kit-package.yaml files and validates their structure
|
|
30
|
+
and component references.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, package_root: Path):
|
|
34
|
+
"""
|
|
35
|
+
Initialize parser.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
package_root: Root directory of the package
|
|
39
|
+
"""
|
|
40
|
+
self.package_root = package_root
|
|
41
|
+
self.manifest_path = package_root / "ai-config-kit-package.yaml"
|
|
42
|
+
|
|
43
|
+
def parse(self) -> Package:
|
|
44
|
+
"""
|
|
45
|
+
Parse package manifest YAML file.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Package object with all components
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
FileNotFoundError: If manifest file doesn't exist
|
|
52
|
+
yaml.YAMLError: If YAML is invalid
|
|
53
|
+
ValidationError: If manifest structure is invalid
|
|
54
|
+
"""
|
|
55
|
+
if not self.manifest_path.exists():
|
|
56
|
+
raise FileNotFoundError(f"Manifest not found: {self.manifest_path}")
|
|
57
|
+
|
|
58
|
+
with open(self.manifest_path, "r") as f:
|
|
59
|
+
data = yaml.safe_load(f)
|
|
60
|
+
|
|
61
|
+
if not data:
|
|
62
|
+
raise ValidationError("Manifest is empty")
|
|
63
|
+
|
|
64
|
+
# Extract required fields
|
|
65
|
+
try:
|
|
66
|
+
name = data["name"]
|
|
67
|
+
version = data["version"]
|
|
68
|
+
description = data["description"]
|
|
69
|
+
author = data["author"]
|
|
70
|
+
license_type = data["license"]
|
|
71
|
+
namespace = data.get("namespace", "local/local")
|
|
72
|
+
except KeyError as e:
|
|
73
|
+
raise ValidationError(f"Missing required field: {e}")
|
|
74
|
+
|
|
75
|
+
# Parse components
|
|
76
|
+
components_data = data.get("components", {})
|
|
77
|
+
# Handle case where components: is specified but empty (parses to None)
|
|
78
|
+
if components_data is None:
|
|
79
|
+
components_data = {}
|
|
80
|
+
components = self._parse_components(components_data)
|
|
81
|
+
|
|
82
|
+
# Create package
|
|
83
|
+
package = Package(
|
|
84
|
+
name=name,
|
|
85
|
+
version=version,
|
|
86
|
+
description=description,
|
|
87
|
+
author=author,
|
|
88
|
+
license=license_type,
|
|
89
|
+
namespace=namespace,
|
|
90
|
+
components=components,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return package
|
|
94
|
+
|
|
95
|
+
def _parse_components(self, components_data: dict) -> PackageComponents:
|
|
96
|
+
"""
|
|
97
|
+
Parse components section of manifest.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
components_data: Components dictionary from manifest
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
PackageComponents object
|
|
104
|
+
"""
|
|
105
|
+
# Parse instructions
|
|
106
|
+
instructions = []
|
|
107
|
+
for inst_data in components_data.get("instructions", []):
|
|
108
|
+
instructions.append(
|
|
109
|
+
InstructionComponent(
|
|
110
|
+
name=inst_data["name"],
|
|
111
|
+
file=inst_data["file"],
|
|
112
|
+
description=inst_data["description"],
|
|
113
|
+
tags=inst_data.get("tags", []),
|
|
114
|
+
ide_support=inst_data.get("ide_support"),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Parse MCP servers
|
|
119
|
+
mcp_servers = []
|
|
120
|
+
for mcp_data in components_data.get("mcp_servers", []):
|
|
121
|
+
credentials = []
|
|
122
|
+
for cred_data in mcp_data.get("credentials", []):
|
|
123
|
+
credentials.append(
|
|
124
|
+
CredentialDescriptor(
|
|
125
|
+
name=cred_data["name"],
|
|
126
|
+
description=cred_data["description"],
|
|
127
|
+
required=cred_data.get("required", True),
|
|
128
|
+
default=cred_data.get("default"),
|
|
129
|
+
example=cred_data.get("example"),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
mcp_servers.append(
|
|
134
|
+
MCPServerComponent(
|
|
135
|
+
name=mcp_data["name"],
|
|
136
|
+
file=mcp_data["file"],
|
|
137
|
+
description=mcp_data["description"],
|
|
138
|
+
credentials=credentials,
|
|
139
|
+
ide_support=mcp_data.get("ide_support", ["claude_code", "windsurf"]),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Parse hooks
|
|
144
|
+
hooks = []
|
|
145
|
+
for hook_data in components_data.get("hooks", []):
|
|
146
|
+
hooks.append(
|
|
147
|
+
HookComponent(
|
|
148
|
+
name=hook_data["name"],
|
|
149
|
+
file=hook_data["file"],
|
|
150
|
+
description=hook_data["description"],
|
|
151
|
+
hook_type=hook_data["hook_type"],
|
|
152
|
+
ide_support=hook_data.get("ide_support", ["claude_code"]),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Parse commands
|
|
157
|
+
commands = []
|
|
158
|
+
for cmd_data in components_data.get("commands", []):
|
|
159
|
+
commands.append(
|
|
160
|
+
CommandComponent(
|
|
161
|
+
name=cmd_data["name"],
|
|
162
|
+
file=cmd_data["file"],
|
|
163
|
+
description=cmd_data["description"],
|
|
164
|
+
command_type=cmd_data["command_type"],
|
|
165
|
+
ide_support=cmd_data.get("ide_support", []),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Parse resources
|
|
170
|
+
resources = []
|
|
171
|
+
for res_data in components_data.get("resources", []):
|
|
172
|
+
resources.append(
|
|
173
|
+
ResourceComponent(
|
|
174
|
+
name=res_data["name"],
|
|
175
|
+
file=res_data["file"],
|
|
176
|
+
description=res_data["description"],
|
|
177
|
+
install_path=res_data.get("install_path", res_data["file"]), # Default to file path
|
|
178
|
+
checksum=res_data["checksum"],
|
|
179
|
+
size=res_data["size"],
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return PackageComponents(
|
|
184
|
+
instructions=instructions,
|
|
185
|
+
mcp_servers=mcp_servers,
|
|
186
|
+
hooks=hooks,
|
|
187
|
+
commands=commands,
|
|
188
|
+
resources=resources,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def validate(self, package: Package) -> list[str]:
|
|
192
|
+
"""
|
|
193
|
+
Validate package manifest completeness and file references.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
package: Package object to validate
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of validation error messages (empty if valid)
|
|
200
|
+
"""
|
|
201
|
+
import re
|
|
202
|
+
|
|
203
|
+
errors = []
|
|
204
|
+
|
|
205
|
+
# Validate version format (semantic versioning: X.Y.Z)
|
|
206
|
+
version_pattern = r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
|
|
207
|
+
if not re.match(version_pattern, package.version):
|
|
208
|
+
errors.append(f"Invalid version format: {package.version}. Must follow semantic versioning (X.Y.Z)")
|
|
209
|
+
|
|
210
|
+
# Validate all component files exist
|
|
211
|
+
for instruction in package.components.instructions:
|
|
212
|
+
file_path = self.package_root / instruction.file
|
|
213
|
+
if not file_path.exists():
|
|
214
|
+
errors.append(f"Instruction file not found: {instruction.file}")
|
|
215
|
+
|
|
216
|
+
for mcp in package.components.mcp_servers:
|
|
217
|
+
file_path = self.package_root / mcp.file
|
|
218
|
+
if not file_path.exists():
|
|
219
|
+
errors.append(f"MCP config file not found: {mcp.file}")
|
|
220
|
+
|
|
221
|
+
for hook in package.components.hooks:
|
|
222
|
+
file_path = self.package_root / hook.file
|
|
223
|
+
if not file_path.exists():
|
|
224
|
+
errors.append(f"Hook file not found: {hook.file}")
|
|
225
|
+
|
|
226
|
+
for command in package.components.commands:
|
|
227
|
+
file_path = self.package_root / command.file
|
|
228
|
+
if not file_path.exists():
|
|
229
|
+
errors.append(f"Command file not found: {command.file}")
|
|
230
|
+
|
|
231
|
+
for resource in package.components.resources:
|
|
232
|
+
file_path = self.package_root / resource.file
|
|
233
|
+
if not file_path.exists():
|
|
234
|
+
errors.append(f"Resource file not found: {resource.file}")
|
|
235
|
+
|
|
236
|
+
# Validate component name uniqueness within each type
|
|
237
|
+
inst_names = [i.name for i in package.components.instructions]
|
|
238
|
+
if len(inst_names) != len(set(inst_names)):
|
|
239
|
+
errors.append("Duplicate instruction names found")
|
|
240
|
+
|
|
241
|
+
mcp_names = [m.name for m in package.components.mcp_servers]
|
|
242
|
+
if len(mcp_names) != len(set(mcp_names)):
|
|
243
|
+
errors.append("Duplicate MCP server names found")
|
|
244
|
+
|
|
245
|
+
# Note: We allow empty packages (for edge case testing)
|
|
246
|
+
# In practice, most packages should have at least one component
|
|
247
|
+
|
|
248
|
+
return errors
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Repository parsing and management."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from aiconfigkit.core.models import (
|
|
11
|
+
AIToolType,
|
|
12
|
+
Instruction,
|
|
13
|
+
InstructionBundle,
|
|
14
|
+
MCPServer,
|
|
15
|
+
MCPSet,
|
|
16
|
+
Repository,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RepositoryParser:
|
|
23
|
+
"""Parse instruction repository metadata and files."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, repo_path: Path):
|
|
26
|
+
"""
|
|
27
|
+
Initialize repository parser.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
repo_path: Path to cloned repository
|
|
31
|
+
"""
|
|
32
|
+
self.repo_path = repo_path
|
|
33
|
+
self.metadata_file = repo_path / "templatekit.yaml"
|
|
34
|
+
|
|
35
|
+
def parse(self) -> Repository:
|
|
36
|
+
"""
|
|
37
|
+
Parse repository and return Repository object.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Repository with instructions and bundles loaded
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
FileNotFoundError: If templatekit.yaml not found
|
|
44
|
+
ValueError: If metadata is invalid
|
|
45
|
+
"""
|
|
46
|
+
if not self.metadata_file.exists():
|
|
47
|
+
raise FileNotFoundError(f"Repository metadata file not found: {self.metadata_file}")
|
|
48
|
+
|
|
49
|
+
with open(self.metadata_file, "r", encoding="utf-8") as f:
|
|
50
|
+
metadata = yaml.safe_load(f)
|
|
51
|
+
|
|
52
|
+
if not metadata:
|
|
53
|
+
raise ValueError("Repository metadata file is empty")
|
|
54
|
+
|
|
55
|
+
# Parse instructions
|
|
56
|
+
instructions = []
|
|
57
|
+
for inst_data in metadata.get("instructions", []):
|
|
58
|
+
instruction = self._parse_instruction(inst_data)
|
|
59
|
+
instructions.append(instruction)
|
|
60
|
+
|
|
61
|
+
# Parse bundles
|
|
62
|
+
bundles = []
|
|
63
|
+
for bundle_data in metadata.get("bundles", []):
|
|
64
|
+
bundle = self._parse_bundle(bundle_data)
|
|
65
|
+
bundles.append(bundle)
|
|
66
|
+
|
|
67
|
+
# Extract repository-level metadata
|
|
68
|
+
repo_metadata = {
|
|
69
|
+
"name": metadata.get("name", ""),
|
|
70
|
+
"description": metadata.get("description", ""),
|
|
71
|
+
"version": metadata.get("version", ""),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Repository(
|
|
75
|
+
url="", # Will be set by caller
|
|
76
|
+
instructions=instructions,
|
|
77
|
+
bundles=bundles,
|
|
78
|
+
metadata=repo_metadata,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _parse_instruction(self, data: dict) -> Instruction:
|
|
82
|
+
"""Parse instruction from metadata dictionary."""
|
|
83
|
+
name = data.get("name")
|
|
84
|
+
if not name:
|
|
85
|
+
raise ValueError("Instruction missing required 'name' field")
|
|
86
|
+
|
|
87
|
+
description = data.get("description", "")
|
|
88
|
+
file_path = data.get("file", "")
|
|
89
|
+
|
|
90
|
+
if not file_path:
|
|
91
|
+
raise ValueError(f"Instruction '{name}' missing 'file' field")
|
|
92
|
+
|
|
93
|
+
# Load instruction content
|
|
94
|
+
full_path = self.repo_path / file_path
|
|
95
|
+
if not full_path.exists():
|
|
96
|
+
raise FileNotFoundError(f"Instruction file not found: {full_path}")
|
|
97
|
+
|
|
98
|
+
content = full_path.read_text(encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
# Calculate checksum
|
|
101
|
+
checksum = self._calculate_checksum(content)
|
|
102
|
+
|
|
103
|
+
# Parse AI tools
|
|
104
|
+
ai_tools = []
|
|
105
|
+
for tool_str in data.get("ai_tools", []):
|
|
106
|
+
try:
|
|
107
|
+
ai_tools.append(AIToolType(tool_str.lower()))
|
|
108
|
+
except ValueError:
|
|
109
|
+
# Skip unknown AI tool types
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
return Instruction(
|
|
113
|
+
name=name,
|
|
114
|
+
description=description,
|
|
115
|
+
content=content,
|
|
116
|
+
file_path=file_path,
|
|
117
|
+
tags=data.get("tags", []),
|
|
118
|
+
checksum=checksum,
|
|
119
|
+
ai_tools=ai_tools,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _parse_bundle(self, data: dict) -> InstructionBundle:
|
|
123
|
+
"""Parse bundle from metadata dictionary."""
|
|
124
|
+
name = data.get("name")
|
|
125
|
+
if not name:
|
|
126
|
+
raise ValueError("Bundle missing required 'name' field")
|
|
127
|
+
|
|
128
|
+
description = data.get("description", "")
|
|
129
|
+
instructions = data.get("instructions", [])
|
|
130
|
+
|
|
131
|
+
if not instructions:
|
|
132
|
+
raise ValueError(f"Bundle '{name}' has no instructions")
|
|
133
|
+
|
|
134
|
+
return InstructionBundle(
|
|
135
|
+
name=name,
|
|
136
|
+
description=description,
|
|
137
|
+
instructions=instructions,
|
|
138
|
+
tags=data.get("tags", []),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _calculate_checksum(self, content: str) -> str:
|
|
142
|
+
"""Calculate SHA-256 checksum of content."""
|
|
143
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
144
|
+
|
|
145
|
+
def get_instruction_by_name(self, name: str) -> Optional[Instruction]:
|
|
146
|
+
"""
|
|
147
|
+
Find instruction by name in repository.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Instruction name
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Instruction if found, None otherwise
|
|
154
|
+
"""
|
|
155
|
+
repo = self.parse()
|
|
156
|
+
for instruction in repo.instructions:
|
|
157
|
+
if instruction.name == name:
|
|
158
|
+
return instruction
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def get_bundle_by_name(self, name: str) -> Optional[InstructionBundle]:
|
|
162
|
+
"""
|
|
163
|
+
Find bundle by name in repository.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: Bundle name
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Bundle if found, None otherwise
|
|
170
|
+
"""
|
|
171
|
+
repo = self.parse()
|
|
172
|
+
for bundle in repo.bundles:
|
|
173
|
+
if bundle.name == name:
|
|
174
|
+
return bundle
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def get_instructions_for_bundle(self, bundle_name: str) -> list[Instruction]:
|
|
178
|
+
"""
|
|
179
|
+
Get all instructions for a bundle.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
bundle_name: Name of bundle
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of Instruction objects
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If bundle not found or instruction in bundle not found
|
|
189
|
+
"""
|
|
190
|
+
bundle = self.get_bundle_by_name(bundle_name)
|
|
191
|
+
if not bundle:
|
|
192
|
+
raise ValueError(f"Bundle not found: {bundle_name}")
|
|
193
|
+
|
|
194
|
+
repo = self.parse()
|
|
195
|
+
instructions = []
|
|
196
|
+
|
|
197
|
+
for inst_name in bundle.instructions:
|
|
198
|
+
# Find instruction
|
|
199
|
+
instruction = None
|
|
200
|
+
for inst in repo.instructions:
|
|
201
|
+
if inst.name == inst_name:
|
|
202
|
+
instruction = inst
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if not instruction:
|
|
206
|
+
raise ValueError(f"Bundle '{bundle_name}' references unknown instruction: {inst_name}")
|
|
207
|
+
|
|
208
|
+
instructions.append(instruction)
|
|
209
|
+
|
|
210
|
+
return instructions
|
|
211
|
+
|
|
212
|
+
def parse_mcp_servers(self, namespace: str) -> list[MCPServer]:
|
|
213
|
+
"""
|
|
214
|
+
Parse MCP servers from templatekit.yaml (templatekit.yaml).
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
namespace: Namespace for these servers
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of MCPServer objects
|
|
221
|
+
"""
|
|
222
|
+
if not self.metadata_file.exists():
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
with open(self.metadata_file, "r", encoding="utf-8") as f:
|
|
226
|
+
metadata = yaml.safe_load(f)
|
|
227
|
+
|
|
228
|
+
if not metadata:
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
servers = []
|
|
232
|
+
for server_data in metadata.get("mcp_servers", []):
|
|
233
|
+
try:
|
|
234
|
+
server = MCPServer.from_dict(server_data, namespace)
|
|
235
|
+
servers.append(server)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.warning(f"Failed to parse MCP server {server_data.get('name', 'unknown')}: {e}")
|
|
238
|
+
|
|
239
|
+
return servers
|
|
240
|
+
|
|
241
|
+
def parse_mcp_sets(self, namespace: str) -> list[MCPSet]:
|
|
242
|
+
"""
|
|
243
|
+
Parse MCP sets from templatekit.yaml (templatekit.yaml).
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
namespace: Namespace for these sets
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of MCPSet objects
|
|
250
|
+
"""
|
|
251
|
+
if not self.metadata_file.exists():
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
with open(self.metadata_file, "r", encoding="utf-8") as f:
|
|
255
|
+
metadata = yaml.safe_load(f)
|
|
256
|
+
|
|
257
|
+
if not metadata:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
sets = []
|
|
261
|
+
for set_data in metadata.get("mcp_sets", []):
|
|
262
|
+
try:
|
|
263
|
+
mcp_set = MCPSet.from_dict(set_data, namespace)
|
|
264
|
+
sets.append(mcp_set)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.warning(f"Failed to parse MCP set {set_data.get('name', 'unknown')}: {e}")
|
|
267
|
+
|
|
268
|
+
return sets
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def validate_repository_structure(repo_path: Path) -> Optional[str]:
|
|
272
|
+
"""
|
|
273
|
+
Validate repository has correct structure.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
repo_path: Path to repository
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
None if valid, error message if invalid
|
|
280
|
+
"""
|
|
281
|
+
# Check for metadata file
|
|
282
|
+
metadata_file = repo_path / "templatekit.yaml"
|
|
283
|
+
if not metadata_file.exists():
|
|
284
|
+
return "Missing templatekit.yaml metadata file"
|
|
285
|
+
|
|
286
|
+
# Try to parse metadata
|
|
287
|
+
try:
|
|
288
|
+
parser = RepositoryParser(repo_path)
|
|
289
|
+
repo = parser.parse()
|
|
290
|
+
|
|
291
|
+
# Validate at least one instruction or bundle
|
|
292
|
+
if not repo.instructions and not repo.bundles:
|
|
293
|
+
return "Repository has no instructions or bundles"
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
return f"Invalid repository metadata: {str(e)}"
|
|
297
|
+
|
|
298
|
+
return None
|