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,283 @@
1
+ """Template manifest parsing and validation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from aiconfigkit.core.models import TemplateBundle, TemplateDefinition, TemplateFile, TemplateManifest
9
+
10
+
11
+ class TemplateManifestError(Exception):
12
+ """Raised when template manifest is invalid."""
13
+
14
+ pass
15
+
16
+
17
+ def load_manifest(manifest_path: Path) -> TemplateManifest:
18
+ """
19
+ Load and validate template manifest from YAML file.
20
+
21
+ Args:
22
+ manifest_path: Path to templatekit.yaml
23
+
24
+ Returns:
25
+ Parsed and validated TemplateManifest
26
+
27
+ Raises:
28
+ TemplateManifestError: If manifest is invalid or missing
29
+ FileNotFoundError: If manifest file doesn't exist
30
+
31
+ Example:
32
+ >>> from pathlib import Path
33
+ >>> manifest = load_manifest(Path("repo/templatekit.yaml"))
34
+ >>> manifest.name
35
+ 'My Templates'
36
+ """
37
+ if not manifest_path.exists():
38
+ raise FileNotFoundError(f"Manifest not found: {manifest_path}")
39
+
40
+ try:
41
+ with open(manifest_path, "r", encoding="utf-8") as f:
42
+ data = yaml.safe_load(f)
43
+ except yaml.YAMLError as e:
44
+ raise TemplateManifestError(f"Invalid YAML in manifest: {e}") from e
45
+
46
+ if not data:
47
+ raise TemplateManifestError("Manifest file is empty")
48
+
49
+ try:
50
+ return parse_manifest(data, manifest_path)
51
+ except (ValueError, KeyError) as e:
52
+ raise TemplateManifestError(f"Invalid manifest structure: {e}") from e
53
+
54
+
55
+ def parse_manifest(data: dict[str, Any], manifest_path: Path) -> TemplateManifest:
56
+ """
57
+ Parse manifest data into TemplateManifest object.
58
+
59
+ Args:
60
+ data: Parsed YAML data
61
+ manifest_path: Path to manifest (for validation context)
62
+
63
+ Returns:
64
+ TemplateManifest object
65
+
66
+ Raises:
67
+ ValueError: If required fields are missing or invalid
68
+ """
69
+ # Required fields
70
+ if "name" not in data:
71
+ raise ValueError("Manifest missing required field: name")
72
+ if "description" not in data:
73
+ raise ValueError("Manifest missing required field: description")
74
+ if "version" not in data:
75
+ raise ValueError("Manifest missing required field: version")
76
+ if "templates" not in data:
77
+ raise ValueError("Manifest missing required field: templates")
78
+
79
+ # Parse templates
80
+ templates = []
81
+ for template_data in data["templates"]:
82
+ template = parse_template(template_data, manifest_path)
83
+ templates.append(template)
84
+
85
+ # Parse bundles (optional)
86
+ bundles = []
87
+ if "bundles" in data:
88
+ for bundle_data in data["bundles"]:
89
+ bundle = parse_bundle(bundle_data)
90
+ bundles.append(bundle)
91
+
92
+ # Validate bundle references
93
+ template_names = {t.name for t in templates}
94
+ for bundle in bundles:
95
+ for template_ref in bundle.template_refs:
96
+ if template_ref not in template_names:
97
+ raise ValueError(f"Bundle '{bundle.name}' references non-existent template '{template_ref}'")
98
+
99
+ return TemplateManifest(
100
+ name=data["name"],
101
+ description=data["description"],
102
+ version=data["version"],
103
+ author=data.get("author"),
104
+ templates=templates,
105
+ bundles=bundles,
106
+ )
107
+
108
+
109
+ def parse_template(data: dict[str, Any], manifest_path: Path) -> TemplateDefinition:
110
+ """
111
+ Parse template definition from manifest data.
112
+
113
+ Args:
114
+ data: Template data from manifest
115
+ manifest_path: Path to manifest (for file validation)
116
+
117
+ Returns:
118
+ TemplateDefinition object
119
+
120
+ Raises:
121
+ ValueError: If template data is invalid
122
+ """
123
+ if "name" not in data:
124
+ raise ValueError("Template missing required field: name")
125
+ if "description" not in data:
126
+ raise ValueError(f"Template '{data.get('name', 'unknown')}' missing required field: description")
127
+ if "files" not in data or not data["files"]:
128
+ raise ValueError(f"Template '{data['name']}' must have at least one file")
129
+
130
+ # Parse files
131
+ files = []
132
+ for file_data in data["files"]:
133
+ if isinstance(file_data, str):
134
+ # Simple format: just path string
135
+ file_obj = TemplateFile(path=file_data, ide="all")
136
+ elif isinstance(file_data, dict):
137
+ # Detailed format with path and ide
138
+ if "path" not in file_data:
139
+ raise ValueError(f"Template '{data['name']}' file entry missing 'path'")
140
+ file_obj = TemplateFile(path=file_data["path"], ide=file_data.get("ide", "all"))
141
+ else:
142
+ raise ValueError(f"Invalid file entry in template '{data['name']}': {file_data}")
143
+
144
+ # Validate file exists in repository
145
+ repo_path = manifest_path.parent
146
+ file_path = repo_path / file_obj.path
147
+ if not file_path.exists():
148
+ raise ValueError(f"Template '{data['name']}' references non-existent file: {file_obj.path}")
149
+
150
+ files.append(file_obj)
151
+
152
+ return TemplateDefinition(
153
+ name=data["name"],
154
+ description=data["description"],
155
+ files=files,
156
+ tags=data.get("tags", []),
157
+ dependencies=data.get("dependencies", []),
158
+ )
159
+
160
+
161
+ def parse_bundle(data: dict[str, Any]) -> TemplateBundle:
162
+ """
163
+ Parse bundle definition from manifest data.
164
+
165
+ Args:
166
+ data: Bundle data from manifest
167
+
168
+ Returns:
169
+ TemplateBundle object
170
+
171
+ Raises:
172
+ ValueError: If bundle data is invalid
173
+ """
174
+ if "name" not in data:
175
+ raise ValueError("Bundle missing required field: name")
176
+ if "description" not in data:
177
+ raise ValueError(f"Bundle '{data.get('name', 'unknown')}' missing required field: description")
178
+ if "templates" not in data or not data["templates"]:
179
+ raise ValueError(f"Bundle '{data['name']}' must reference at least one template")
180
+
181
+ return TemplateBundle(
182
+ name=data["name"],
183
+ description=data["description"],
184
+ template_refs=data["templates"],
185
+ tags=data.get("tags", []),
186
+ )
187
+
188
+
189
+ def validate_manifest_size(manifest_path: Path, template_count: int, soft_limit_templates: int = 100) -> list[str]:
190
+ """
191
+ Check manifest against soft limits and return warnings.
192
+
193
+ Args:
194
+ manifest_path: Path to manifest
195
+ template_count: Number of templates in manifest
196
+ soft_limit_templates: Soft limit for template count
197
+
198
+ Returns:
199
+ List of warning messages (empty if no warnings)
200
+
201
+ Example:
202
+ >>> warnings = validate_manifest_size(Path("repo/templatekit.yaml"), 150)
203
+ >>> len(warnings) > 0
204
+ True
205
+ """
206
+ warnings = []
207
+
208
+ # Check template count
209
+ if template_count > soft_limit_templates:
210
+ warnings.append(
211
+ f"⚠️ Repository contains {template_count} templates "
212
+ f"(soft limit: {soft_limit_templates}). "
213
+ f"Large repositories may take longer to install."
214
+ )
215
+
216
+ # Check repository size (approximate based on manifest directory)
217
+ repo_path = manifest_path.parent
218
+ total_size = 0
219
+ for file_path in repo_path.rglob("*"):
220
+ if file_path.is_file():
221
+ total_size += file_path.stat().st_size
222
+
223
+ size_mb = total_size / (1024 * 1024)
224
+ soft_limit_mb = 50
225
+
226
+ if size_mb > soft_limit_mb:
227
+ warnings.append(
228
+ f"⚠️ Repository size is {size_mb:.1f}MB "
229
+ f"(soft limit: {soft_limit_mb}MB). "
230
+ f"Installation may take longer."
231
+ )
232
+
233
+ return warnings
234
+
235
+
236
+ def validate_dependencies(templates: list[TemplateDefinition]) -> list[str]:
237
+ """
238
+ Validate template dependencies for circular references.
239
+
240
+ Args:
241
+ templates: List of template definitions
242
+
243
+ Returns:
244
+ List of error messages (empty if valid)
245
+
246
+ Example:
247
+ >>> errors = validate_dependencies(templates)
248
+ >>> len(errors) == 0
249
+ True
250
+ """
251
+ errors = []
252
+ template_names = {t.name for t in templates}
253
+
254
+ # Build dependency graph
255
+ dependencies = {t.name: set(t.dependencies) for t in templates}
256
+
257
+ # Check for circular dependencies using DFS
258
+ def has_cycle(node: str, visited: set[str], rec_stack: set[str]) -> bool:
259
+ visited.add(node)
260
+ rec_stack.add(node)
261
+
262
+ for neighbor in dependencies.get(node, []):
263
+ if neighbor not in visited:
264
+ if has_cycle(neighbor, visited, rec_stack):
265
+ return True
266
+ elif neighbor in rec_stack:
267
+ return True
268
+
269
+ rec_stack.remove(node)
270
+ return False
271
+
272
+ visited: set[str] = set()
273
+ for template in templates:
274
+ if template.name not in visited:
275
+ if has_cycle(template.name, visited, set()):
276
+ errors.append(f"Circular dependency detected involving template '{template.name}'")
277
+
278
+ # Check for non-existent dependencies
279
+ for dep in template.dependencies:
280
+ if dep not in template_names:
281
+ errors.append(f"Template '{template.name}' depends on non-existent template '{dep}'")
282
+
283
+ return errors
@@ -0,0 +1,201 @@
1
+ """Semantic version management for packages."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from packaging.version import InvalidVersion, Version
8
+ from packaging.version import parse as parse_version
9
+
10
+
11
+ class VersionError(Exception):
12
+ """Raised when version operations fail."""
13
+
14
+ pass
15
+
16
+
17
+ class VersionManager:
18
+ """
19
+ Manager for semantic versioning operations.
20
+
21
+ Handles parsing, comparison, and validation of package versions
22
+ using semantic versioning (major.minor.patch).
23
+ """
24
+
25
+ def __init__(self, repository_path: Optional[Path] = None):
26
+ """
27
+ Initialize version manager.
28
+
29
+ Args:
30
+ repository_path: Path to Git repository for querying versions from tags
31
+ """
32
+ self.repository_path = repository_path
33
+
34
+ def parse(self, version_string: str) -> Version:
35
+ """
36
+ Parse version string into Version object.
37
+
38
+ Args:
39
+ version_string: Version string (e.g., "1.0.0", "v2.1.3")
40
+
41
+ Returns:
42
+ Version object
43
+
44
+ Raises:
45
+ VersionError: If version string is invalid
46
+ """
47
+ # Strip leading 'v' if present
48
+ if version_string.startswith("v"):
49
+ version_string = version_string[1:]
50
+
51
+ try:
52
+ return parse_version(version_string)
53
+ except InvalidVersion as e:
54
+ raise VersionError(f"Invalid version string '{version_string}': {e}")
55
+
56
+ def compare(self, version1: str, version2: str) -> int:
57
+ """
58
+ Compare two version strings.
59
+
60
+ Args:
61
+ version1: First version string
62
+ version2: Second version string
63
+
64
+ Returns:
65
+ -1 if version1 < version2
66
+ 0 if version1 == version2
67
+ 1 if version1 > version2
68
+
69
+ Raises:
70
+ VersionError: If either version string is invalid
71
+ """
72
+ v1 = self.parse(version1)
73
+ v2 = self.parse(version2)
74
+
75
+ if v1 < v2:
76
+ return -1
77
+ elif v1 > v2:
78
+ return 1
79
+ else:
80
+ return 0
81
+
82
+ def is_compatible(self, required_version: str, available_version: str) -> bool:
83
+ """
84
+ Check if available version satisfies required version.
85
+
86
+ Uses semantic versioning compatibility rules:
87
+ - Major version must match exactly (breaking changes)
88
+ - Minor version must be >= required (backwards compatible features)
89
+ - Patch version must be >= required (backwards compatible fixes)
90
+
91
+ Args:
92
+ required_version: Minimum required version
93
+ available_version: Available version to check
94
+
95
+ Returns:
96
+ True if compatible, False otherwise
97
+ """
98
+ required = self.parse(required_version)
99
+ available = self.parse(available_version)
100
+
101
+ # Major version must match (breaking changes)
102
+ if required.major != available.major:
103
+ return False
104
+
105
+ # Available must be >= required
106
+ return available >= required
107
+
108
+ def get_available_versions(self) -> list[str]:
109
+ """
110
+ Query available versions from Git tags.
111
+
112
+ Looks for tags matching semantic version format (with or without 'v' prefix).
113
+
114
+ Returns:
115
+ List of version strings sorted in descending order (newest first)
116
+
117
+ Raises:
118
+ VersionError: If repository path not set or Git operation fails
119
+ """
120
+ if not self.repository_path:
121
+ raise VersionError("Repository path not set")
122
+
123
+ if not self.repository_path.exists():
124
+ raise VersionError(f"Repository path does not exist: {self.repository_path}")
125
+
126
+ try:
127
+ # Run git tag command
128
+ result = subprocess.run(
129
+ ["git", "tag", "--list"],
130
+ cwd=self.repository_path,
131
+ capture_output=True,
132
+ text=True,
133
+ check=True,
134
+ )
135
+
136
+ # Parse tags into versions
137
+ versions = []
138
+ for tag in result.stdout.strip().split("\n"):
139
+ if not tag:
140
+ continue
141
+
142
+ try:
143
+ # Try to parse as version (strip 'v' prefix if present)
144
+ version = self.parse(tag)
145
+ versions.append(str(version))
146
+ except VersionError:
147
+ # Skip non-version tags
148
+ continue
149
+
150
+ # Sort in descending order (newest first)
151
+ versions.sort(key=lambda v: self.parse(v), reverse=True)
152
+
153
+ return versions
154
+
155
+ except subprocess.CalledProcessError as e:
156
+ raise VersionError(f"Failed to query Git tags: {e}")
157
+
158
+ def get_latest_version(self) -> Optional[str]:
159
+ """
160
+ Get the latest version from Git tags.
161
+
162
+ Returns:
163
+ Latest version string, or None if no versions found
164
+
165
+ Raises:
166
+ VersionError: If repository path not set or Git operation fails
167
+ """
168
+ versions = self.get_available_versions()
169
+ return versions[0] if versions else None
170
+
171
+ def validate_version_string(self, version_string: str) -> bool:
172
+ """
173
+ Validate that version string follows semantic versioning format.
174
+
175
+ Enforces strict semantic versioning: major.minor.patch (three components).
176
+
177
+ Args:
178
+ version_string: Version string to validate
179
+
180
+ Returns:
181
+ True if valid, False otherwise
182
+ """
183
+ try:
184
+ version = self.parse(version_string)
185
+ # Enforce strict semantic versioning: major.minor.patch
186
+ # Check that the version has exactly 3 numeric components
187
+ version_str = str(version)
188
+ parts = version_str.split(".")
189
+ # Must have at least major.minor.patch
190
+ if len(parts) < 3:
191
+ return False
192
+ # First three parts must be numeric
193
+ try:
194
+ int(parts[0])
195
+ int(parts[1])
196
+ int(parts[2])
197
+ except (ValueError, IndexError):
198
+ return False
199
+ return True
200
+ except VersionError:
201
+ return False
File without changes