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,309 @@
1
+ """Update command for refreshing library instructions."""
2
+
3
+ import shutil
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from git import Repo
10
+ from rich.console import Console
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn
12
+
13
+ from aiconfigkit.core.checksum import calculate_file_checksum
14
+ from aiconfigkit.core.git_operations import GitOperations, RepositoryOperationError
15
+ from aiconfigkit.core.models import LibraryInstruction, RefType
16
+ from aiconfigkit.core.repository import RepositoryParser
17
+ from aiconfigkit.storage.library import LibraryManager
18
+ from aiconfigkit.storage.tracker import InstallationTracker
19
+ from aiconfigkit.utils.project import find_project_root
20
+ from aiconfigkit.utils.ui import print_error, print_info, print_success
21
+
22
+ console = Console()
23
+
24
+ app = typer.Typer()
25
+
26
+
27
+ def update_repository(
28
+ namespace: Optional[str] = None,
29
+ all_repos: bool = False,
30
+ ) -> int:
31
+ """
32
+ Update instructions from their source repositories.
33
+
34
+ Only updates mutable references (branches). Tags and commits are immutable and will be skipped.
35
+
36
+ Args:
37
+ namespace: Specific repository namespace to update
38
+ all_repos: Update all repositories
39
+
40
+ Returns:
41
+ Exit code (0 = success)
42
+ """
43
+ library = LibraryManager()
44
+ tracker = InstallationTracker()
45
+
46
+ if not all_repos and not namespace:
47
+ print_error("Must specify either --namespace or --all")
48
+ return 1
49
+
50
+ # Get repositories to update
51
+ if all_repos:
52
+ repositories = library.list_repositories()
53
+ if not repositories:
54
+ print_info("No repositories in library to update")
55
+ return 0
56
+ else:
57
+ assert namespace is not None, "namespace should not be None here"
58
+ repo = library.get_repository(namespace)
59
+ if not repo:
60
+ print_error(f"Repository not found: {namespace}")
61
+ print_info("Use 'inskit list library' to see available repositories")
62
+ return 1
63
+ repositories = [repo]
64
+
65
+ console.print(f"\n[bold]Updating {len(repositories)} repository(ies)...[/bold]\n")
66
+
67
+ updated_count = 0
68
+ skipped_count = 0
69
+ error_count = 0
70
+
71
+ for repo in repositories:
72
+ # Extract ref info from namespace
73
+ ref, ref_type = _extract_ref_from_namespace(repo.namespace)
74
+
75
+ # Skip immutable refs (tags and commits)
76
+ if ref and ref_type in (RefType.TAG, RefType.COMMIT):
77
+ ref_type_name = "tag" if ref_type == RefType.TAG else "commit"
78
+ console.print(
79
+ f"[yellow]⊘ Skipped:[/yellow] {repo.name} " f"({ref_type_name} [cyan]{ref}[/cyan] is immutable)"
80
+ )
81
+ skipped_count += 1
82
+ continue
83
+
84
+ # Update branch-based or non-versioned repositories
85
+ console.print(f"[cyan]Updating:[/cyan] {repo.name} ", end="")
86
+ if ref:
87
+ console.print(f"(branch [green]{ref}[/green])")
88
+ else:
89
+ console.print("(default branch)")
90
+
91
+ try:
92
+ # Get the repository directory
93
+ repo_dir = library.library_dir / repo.namespace
94
+
95
+ if not repo_dir.exists():
96
+ print_error(f" Repository directory not found: {repo_dir}")
97
+ error_count += 1
98
+ continue
99
+
100
+ # Check if it's a git repository (skip local non-git repos)
101
+ git_dir = repo_dir / ".git"
102
+ if not git_dir.exists():
103
+ console.print(" [yellow]⊘ Skipped:[/yellow] Not a Git repository (local source)")
104
+ skipped_count += 1
105
+ continue
106
+
107
+ with Progress(
108
+ SpinnerColumn(),
109
+ TextColumn("[progress.description]{task.description}"),
110
+ console=console,
111
+ transient=True,
112
+ ) as progress:
113
+ task = progress.add_task(" Checking for updates...", total=None)
114
+
115
+ # Open repository and check for updates
116
+ git_repo = Repo(repo_dir)
117
+ branch_name = ref if ref else git_repo.active_branch.name
118
+
119
+ # Check if updates are available
120
+ has_updates = GitOperations.check_for_updates(git_repo, branch_name)
121
+
122
+ if not has_updates:
123
+ console.print(" [green]✓[/green] Already up to date")
124
+ continue
125
+
126
+ # Pull updates
127
+ progress.update(task, description=" Pulling updates...")
128
+ result = GitOperations.pull_repository_updates(git_repo, branch_name)
129
+
130
+ if not result.get("success"):
131
+ error_type = result.get("error", "unknown")
132
+ if error_type == "local_modifications":
133
+ console.print(f" [red]✗[/red] {result.get('message')}")
134
+ elif error_type == "conflict":
135
+ console.print(" [red]✗[/red] Merge conflict detected")
136
+ else:
137
+ console.print(f" [red]✗[/red] Update failed: {result.get('message')}")
138
+ error_count += 1
139
+ continue
140
+
141
+ # Re-parse repository to update library index
142
+ parser = RepositoryParser(repo_dir)
143
+ repository = parser.parse()
144
+
145
+ # Update library instructions
146
+ library_instructions = []
147
+ instructions_dir = repo_dir / "instructions"
148
+
149
+ for instruction in repository.instructions:
150
+ source_file = repo_dir / instruction.file_path
151
+ if not source_file.exists():
152
+ continue
153
+
154
+ dest_file = instructions_dir / f"{instruction.name}.md"
155
+ if source_file != dest_file:
156
+ shutil.copy2(source_file, dest_file)
157
+
158
+ checksum = calculate_file_checksum(str(dest_file))
159
+
160
+ lib_inst = LibraryInstruction(
161
+ id=f"{repo.namespace}/{instruction.name}",
162
+ name=instruction.name,
163
+ description=instruction.description,
164
+ repo_namespace=repo.namespace,
165
+ repo_url=repo.url,
166
+ repo_name=repo.name,
167
+ author=repository.metadata.get("author", "Unknown"),
168
+ version=repository.metadata.get("version", "1.0.0"),
169
+ file_path=str(dest_file),
170
+ tags=instruction.tags,
171
+ downloaded_at=datetime.now(),
172
+ checksum=checksum,
173
+ )
174
+ library_instructions.append(lib_inst)
175
+
176
+ # Update library index
177
+ library.add_repository(
178
+ repo_name=repo.name,
179
+ repo_description=repository.metadata.get("description", ""),
180
+ repo_url=repo.url,
181
+ repo_author=repository.metadata.get("author", "Unknown"),
182
+ repo_version=repository.metadata.get("version", "1.0.0"),
183
+ instructions=library_instructions,
184
+ alias=repo.alias,
185
+ )
186
+
187
+ # Update installed instructions
188
+ _update_installed_instructions(repo.namespace, library_instructions, tracker)
189
+
190
+ console.print(f" [green]✓[/green] Updated successfully ({len(library_instructions)} instructions)")
191
+ updated_count += 1
192
+
193
+ except RepositoryOperationError as e:
194
+ console.print(f" [red]✗[/red] Failed: {e}")
195
+ error_count += 1
196
+ except Exception as e:
197
+ console.print(f" [red]✗[/red] Unexpected error: {e}")
198
+ error_count += 1
199
+
200
+ # Summary
201
+ console.print()
202
+ if updated_count > 0:
203
+ print_success(f"✓ Updated: {updated_count} repository(ies)")
204
+ if skipped_count > 0:
205
+ print_info(f"⊘ Skipped: {skipped_count} immutable reference(s)")
206
+ if error_count > 0:
207
+ print_error(f"✗ Failed: {error_count} repository(ies)")
208
+
209
+ return 0 if error_count == 0 else 1
210
+
211
+
212
+ def _extract_ref_from_namespace(namespace: str) -> tuple[Optional[str], Optional[RefType]]:
213
+ """Extract Git reference from versioned namespace."""
214
+ if "@" not in namespace:
215
+ return (None, None)
216
+
217
+ ref = namespace.split("@", 1)[1]
218
+
219
+ import re
220
+
221
+ # Tags typically start with 'v' followed by numbers
222
+ if re.match(r"^v?\d+\.\d+", ref):
223
+ return (ref, RefType.TAG)
224
+ # Commit hashes are hex strings
225
+ elif re.match(r"^[0-9a-f]{7,40}$", ref):
226
+ return (ref, RefType.COMMIT)
227
+ # Everything else is likely a branch
228
+ else:
229
+ return (ref, RefType.BRANCH)
230
+
231
+
232
+ def _update_installed_instructions(
233
+ repo_namespace: str, library_instructions: list[LibraryInstruction], tracker: InstallationTracker
234
+ ) -> None:
235
+ """Update files for installed instructions from this repository."""
236
+ project_root = find_project_root()
237
+
238
+ # Get all installed instructions from this repository
239
+ all_records = tracker.get_installed_instructions(project_root=project_root)
240
+ repo_records = [r for r in all_records if repo_namespace in r.source_repo or repo_namespace in r.instruction_name]
241
+
242
+ if not repo_records:
243
+ return
244
+
245
+ # Update each installed instruction file
246
+ for record in repo_records:
247
+ # Find matching library instruction
248
+ matching_inst = None
249
+ for lib_inst in library_instructions:
250
+ if lib_inst.name in record.instruction_name or record.instruction_name in lib_inst.id:
251
+ matching_inst = lib_inst
252
+ break
253
+
254
+ if not matching_inst:
255
+ continue
256
+
257
+ # Update the installed file
258
+ try:
259
+ source_path = Path(matching_inst.file_path)
260
+ if not source_path.exists():
261
+ continue
262
+
263
+ installed_path = Path(record.installed_path)
264
+ # Handle relative paths for project-scoped installations
265
+ if not installed_path.is_absolute() and project_root:
266
+ installed_path = project_root / installed_path
267
+
268
+ if installed_path.exists():
269
+ # Read new content and write to installed location
270
+ content = source_path.read_text(encoding="utf-8")
271
+ installed_path.write_text(content, encoding="utf-8")
272
+ except Exception:
273
+ # Silently skip if update fails (file might have been manually removed)
274
+ continue
275
+
276
+
277
+ @app.command(name="update")
278
+ def update_command(
279
+ namespace: Optional[str] = typer.Option(
280
+ None,
281
+ "--namespace",
282
+ "-n",
283
+ help="Repository namespace to update",
284
+ ),
285
+ all_repos: bool = typer.Option(
286
+ False,
287
+ "--all",
288
+ "-a",
289
+ help="Update all repositories in library",
290
+ ),
291
+ ) -> None:
292
+ """
293
+ Update downloaded instructions to their latest versions.
294
+
295
+ This re-downloads instructions from their source repositories,
296
+ ensuring you have the latest versions in your library.
297
+
298
+ Examples:
299
+ # Update a specific repository
300
+ instructionkit update --namespace github.com_company_instructions
301
+
302
+ # Update all repositories
303
+ instructionkit update --all
304
+
305
+ # List repositories to find namespace
306
+ instructionkit list library
307
+ """
308
+ exit_code = update_repository(namespace=namespace, all_repos=all_repos)
309
+ raise typer.Exit(code=exit_code)
File without changes
@@ -0,0 +1,211 @@
1
+ """Checksum verification for instruction files and templates."""
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ class ChecksumError(Exception):
9
+ """Raised when checksum verification fails."""
10
+
11
+ pass
12
+
13
+
14
+ def calculate_checksum(content: str, algorithm: str = "sha256") -> str:
15
+ """
16
+ Calculate checksum of content.
17
+
18
+ Args:
19
+ content: Content to hash
20
+ algorithm: Hash algorithm (sha256, sha1, md5)
21
+
22
+ Returns:
23
+ Hex digest of checksum
24
+
25
+ Raises:
26
+ ValueError: If algorithm is not supported
27
+ """
28
+ algorithms = {
29
+ "sha256": hashlib.sha256,
30
+ "sha1": hashlib.sha1,
31
+ "md5": hashlib.md5,
32
+ }
33
+
34
+ if algorithm.lower() not in algorithms:
35
+ raise ValueError(f"Unsupported hash algorithm: {algorithm}")
36
+
37
+ hash_func = algorithms[algorithm.lower()]
38
+ return hash_func(content.encode("utf-8")).hexdigest()
39
+
40
+
41
+ def verify_checksum(content: str, expected_checksum: str, algorithm: str = "sha256") -> bool:
42
+ """
43
+ Verify content matches expected checksum.
44
+
45
+ Args:
46
+ content: Content to verify
47
+ expected_checksum: Expected checksum value
48
+ algorithm: Hash algorithm used
49
+
50
+ Returns:
51
+ True if checksum matches
52
+ """
53
+ actual_checksum = calculate_checksum(content, algorithm)
54
+ return actual_checksum.lower() == expected_checksum.lower()
55
+
56
+
57
+ def verify_checksum_strict(content: str, expected_checksum: str, algorithm: str = "sha256") -> None:
58
+ """
59
+ Verify content matches expected checksum, raise on mismatch.
60
+
61
+ Args:
62
+ content: Content to verify
63
+ expected_checksum: Expected checksum value
64
+ algorithm: Hash algorithm used
65
+
66
+ Raises:
67
+ ChecksumError: If checksum does not match
68
+ """
69
+ if not verify_checksum(content, expected_checksum, algorithm):
70
+ actual = calculate_checksum(content, algorithm)
71
+ raise ChecksumError(f"Checksum mismatch! Expected: {expected_checksum}, " f"Actual: {actual}")
72
+
73
+
74
+ def calculate_file_checksum(file_path: str, algorithm: str = "sha256") -> str:
75
+ """
76
+ Calculate checksum of a file.
77
+
78
+ Args:
79
+ file_path: Path to file
80
+ algorithm: Hash algorithm (sha256, sha1, md5)
81
+
82
+ Returns:
83
+ Hex digest of checksum
84
+
85
+ Raises:
86
+ ValueError: If algorithm is not supported
87
+ FileNotFoundError: If file does not exist
88
+ """
89
+ algorithms = {
90
+ "sha256": hashlib.sha256,
91
+ "sha1": hashlib.sha1,
92
+ "md5": hashlib.md5,
93
+ }
94
+
95
+ if algorithm.lower() not in algorithms:
96
+ raise ValueError(f"Unsupported hash algorithm: {algorithm}")
97
+
98
+ hash_func = algorithms[algorithm.lower()]()
99
+
100
+ with open(file_path, "rb") as f:
101
+ # Read file in chunks for memory efficiency
102
+ for chunk in iter(lambda: f.read(8192), b""):
103
+ hash_func.update(chunk)
104
+
105
+ return hash_func.hexdigest()
106
+
107
+
108
+ def verify_file_checksum(file_path: str, expected_checksum: str, algorithm: str = "sha256") -> bool:
109
+ """
110
+ Verify file matches expected checksum.
111
+
112
+ Args:
113
+ file_path: Path to file
114
+ expected_checksum: Expected checksum value
115
+ algorithm: Hash algorithm used
116
+
117
+ Returns:
118
+ True if checksum matches
119
+ """
120
+ actual_checksum = calculate_file_checksum(file_path, algorithm)
121
+ return actual_checksum.lower() == expected_checksum.lower()
122
+
123
+
124
+ class ChecksumValidator:
125
+ """Helper class for checksum validation with configuration."""
126
+
127
+ def __init__(self, algorithm: str = "sha256", strict: bool = True):
128
+ """
129
+ Initialize validator.
130
+
131
+ Args:
132
+ algorithm: Hash algorithm to use
133
+ strict: Whether to raise exceptions on mismatch
134
+ """
135
+ self.algorithm = algorithm
136
+ self.strict = strict
137
+
138
+ def validate(self, content: str, expected_checksum: Optional[str]) -> bool:
139
+ """
140
+ Validate content against expected checksum.
141
+
142
+ Args:
143
+ content: Content to validate
144
+ expected_checksum: Expected checksum (None to skip validation)
145
+
146
+ Returns:
147
+ True if valid or checksum not provided
148
+
149
+ Raises:
150
+ ChecksumError: If strict mode and checksum mismatch
151
+ """
152
+ # Skip validation if no checksum provided
153
+ if expected_checksum is None:
154
+ return True
155
+
156
+ matches = verify_checksum(content, expected_checksum, self.algorithm)
157
+
158
+ if not matches and self.strict:
159
+ actual = calculate_checksum(content, self.algorithm)
160
+ raise ChecksumError(
161
+ f"Checksum validation failed!\n"
162
+ f"Expected: {expected_checksum}\n"
163
+ f"Actual: {actual}\n"
164
+ f"Algorithm: {self.algorithm}"
165
+ )
166
+
167
+ return matches
168
+
169
+
170
+ # Template-specific helper functions (for Template Sync System)
171
+
172
+
173
+ def sha256_file(file_path: Path) -> str:
174
+ """
175
+ Calculate SHA-256 checksum of a file.
176
+
177
+ Template-specific wrapper around calculate_file_checksum for Path objects.
178
+
179
+ Args:
180
+ file_path: Path to file
181
+
182
+ Returns:
183
+ 64-character hexadecimal string (SHA-256 hash)
184
+
185
+ Example:
186
+ >>> from pathlib import Path
187
+ >>> checksum = sha256_file(Path("template.md"))
188
+ >>> len(checksum)
189
+ 64
190
+ """
191
+ return calculate_file_checksum(str(file_path), "sha256")
192
+
193
+
194
+ def sha256_string(content: str) -> str:
195
+ """
196
+ Calculate SHA-256 checksum of a string.
197
+
198
+ Template-specific wrapper around calculate_checksum.
199
+
200
+ Args:
201
+ content: String content to hash
202
+
203
+ Returns:
204
+ 64-character hexadecimal string (SHA-256 hash)
205
+
206
+ Example:
207
+ >>> checksum = sha256_string("Hello, world!")
208
+ >>> len(checksum)
209
+ 64
210
+ """
211
+ return calculate_checksum(content, "sha256")