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.
Files changed (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. 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