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,329 @@
|
|
|
1
|
+
"""Conflict resolution for instruction and template installations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
|
|
9
|
+
from aiconfigkit.core.checksum import sha256_file, sha256_string
|
|
10
|
+
from aiconfigkit.core.models import ConflictInfo, ConflictResolution, ConflictType, TemplateInstallationRecord
|
|
11
|
+
from aiconfigkit.utils.paths import resolve_conflict_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConflictResolver:
|
|
15
|
+
"""Handle file conflicts during instruction installation."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, default_strategy: ConflictResolution = ConflictResolution.SKIP):
|
|
18
|
+
"""
|
|
19
|
+
Initialize conflict resolver.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
default_strategy: Default resolution strategy
|
|
23
|
+
"""
|
|
24
|
+
self.default_strategy = default_strategy
|
|
25
|
+
|
|
26
|
+
def resolve(
|
|
27
|
+
self, instruction_name: str, target_path: Path, strategy: Optional[ConflictResolution] = None
|
|
28
|
+
) -> ConflictInfo:
|
|
29
|
+
"""
|
|
30
|
+
Resolve a file conflict.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
instruction_name: Name of instruction causing conflict
|
|
34
|
+
target_path: Path where file would be installed
|
|
35
|
+
strategy: Resolution strategy (uses default if None)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ConflictInfo with resolution details
|
|
39
|
+
"""
|
|
40
|
+
if strategy is None:
|
|
41
|
+
strategy = self.default_strategy
|
|
42
|
+
|
|
43
|
+
# Handle each strategy
|
|
44
|
+
if strategy == ConflictResolution.SKIP:
|
|
45
|
+
return ConflictInfo(
|
|
46
|
+
instruction_name=instruction_name,
|
|
47
|
+
existing_path=str(target_path),
|
|
48
|
+
resolution=ConflictResolution.SKIP,
|
|
49
|
+
new_path=None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
elif strategy == ConflictResolution.OVERWRITE:
|
|
53
|
+
return ConflictInfo(
|
|
54
|
+
instruction_name=instruction_name,
|
|
55
|
+
existing_path=str(target_path),
|
|
56
|
+
resolution=ConflictResolution.OVERWRITE,
|
|
57
|
+
new_path=str(target_path), # Same path, will be overwritten
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
elif strategy == ConflictResolution.RENAME:
|
|
61
|
+
# Generate new path with auto-increment
|
|
62
|
+
new_path = resolve_conflict_name(target_path)
|
|
63
|
+
return ConflictInfo(
|
|
64
|
+
instruction_name=instruction_name,
|
|
65
|
+
existing_path=str(target_path),
|
|
66
|
+
resolution=ConflictResolution.RENAME,
|
|
67
|
+
new_path=str(new_path),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Unknown conflict resolution strategy: {strategy}")
|
|
72
|
+
|
|
73
|
+
def should_install(self, conflict_info: ConflictInfo) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Determine if instruction should be installed based on conflict resolution.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
conflict_info: Conflict resolution info
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if installation should proceed
|
|
82
|
+
"""
|
|
83
|
+
return conflict_info.resolution != ConflictResolution.SKIP
|
|
84
|
+
|
|
85
|
+
def get_install_path(self, original_path: Path, conflict_info: Optional[ConflictInfo] = None) -> Path:
|
|
86
|
+
"""
|
|
87
|
+
Get the actual path where file should be installed.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
original_path: Original intended path
|
|
91
|
+
conflict_info: Conflict resolution info (if conflict occurred)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Path where file should be installed
|
|
95
|
+
"""
|
|
96
|
+
if conflict_info is None:
|
|
97
|
+
return original_path
|
|
98
|
+
|
|
99
|
+
if conflict_info.resolution == ConflictResolution.SKIP:
|
|
100
|
+
# Should not install, but return original for reference
|
|
101
|
+
return original_path
|
|
102
|
+
|
|
103
|
+
elif conflict_info.resolution == ConflictResolution.OVERWRITE:
|
|
104
|
+
return original_path
|
|
105
|
+
|
|
106
|
+
elif conflict_info.resolution == ConflictResolution.RENAME:
|
|
107
|
+
if conflict_info.new_path:
|
|
108
|
+
return Path(conflict_info.new_path)
|
|
109
|
+
else:
|
|
110
|
+
# Fallback: should not happen
|
|
111
|
+
return original_path
|
|
112
|
+
|
|
113
|
+
return original_path
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def prompt_conflict_resolution(instruction_name: str) -> ConflictResolution:
|
|
117
|
+
"""
|
|
118
|
+
Prompt user for conflict resolution choice.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
instruction_name: Name of conflicting instruction
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Selected resolution strategy
|
|
125
|
+
"""
|
|
126
|
+
print(f"\nConflict: Instruction '{instruction_name}' already exists.")
|
|
127
|
+
print("How would you like to resolve this?")
|
|
128
|
+
print(" 1. Skip (keep existing file)")
|
|
129
|
+
print(" 2. Rename (install with auto-incremented name)")
|
|
130
|
+
print(" 3. Overwrite (replace existing file)")
|
|
131
|
+
|
|
132
|
+
while True:
|
|
133
|
+
choice = input("Enter choice (1/2/3): ").strip()
|
|
134
|
+
|
|
135
|
+
if choice == "1":
|
|
136
|
+
return ConflictResolution.SKIP
|
|
137
|
+
elif choice == "2":
|
|
138
|
+
return ConflictResolution.RENAME
|
|
139
|
+
elif choice == "3":
|
|
140
|
+
return ConflictResolution.OVERWRITE
|
|
141
|
+
else:
|
|
142
|
+
print("Invalid choice. Please enter 1, 2, or 3.")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def check_conflicts(target_paths: list[Path]) -> dict[str, Path]:
|
|
146
|
+
"""
|
|
147
|
+
Check which target paths already exist.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
target_paths: List of paths to check
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dictionary mapping instruction names to conflicting paths
|
|
154
|
+
"""
|
|
155
|
+
conflicts = {}
|
|
156
|
+
|
|
157
|
+
for path in target_paths:
|
|
158
|
+
if path.exists():
|
|
159
|
+
# Extract instruction name from filename
|
|
160
|
+
instruction_name = path.stem
|
|
161
|
+
conflicts[instruction_name] = path
|
|
162
|
+
|
|
163
|
+
return conflicts
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def batch_resolve_conflicts(conflicts: dict[str, Path], strategy: ConflictResolution) -> dict[str, ConflictInfo]:
|
|
167
|
+
"""
|
|
168
|
+
Resolve multiple conflicts with the same strategy.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
conflicts: Dictionary of instruction names to conflicting paths
|
|
172
|
+
strategy: Resolution strategy to apply to all
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dictionary mapping instruction names to conflict resolutions
|
|
176
|
+
"""
|
|
177
|
+
resolver = ConflictResolver(default_strategy=strategy)
|
|
178
|
+
resolutions = {}
|
|
179
|
+
|
|
180
|
+
for instruction_name, path in conflicts.items():
|
|
181
|
+
resolution = resolver.resolve(instruction_name, path)
|
|
182
|
+
resolutions[instruction_name] = resolution
|
|
183
|
+
|
|
184
|
+
return resolutions
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Template Sync System - Checksum-based conflict detection
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def detect_conflict(
|
|
191
|
+
installed_file: Path, new_template_content: str, installation_record: TemplateInstallationRecord
|
|
192
|
+
) -> ConflictType:
|
|
193
|
+
"""
|
|
194
|
+
Detect if conflict exists between installed and new template using checksums.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
installed_file: Path to currently installed file
|
|
198
|
+
new_template_content: Content of new template version
|
|
199
|
+
installation_record: Original installation record with checksum
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ConflictType indicating conflict status
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> from pathlib import Path
|
|
206
|
+
>>> conflict = detect_conflict(
|
|
207
|
+
... Path(".cursor/rules/acme.test.md"),
|
|
208
|
+
... "new template content",
|
|
209
|
+
... installation_record
|
|
210
|
+
... )
|
|
211
|
+
>>> conflict == ConflictType.NONE
|
|
212
|
+
True
|
|
213
|
+
"""
|
|
214
|
+
if not installed_file.exists():
|
|
215
|
+
# File doesn't exist, no conflict
|
|
216
|
+
return ConflictType.NONE
|
|
217
|
+
|
|
218
|
+
# Calculate current file checksum
|
|
219
|
+
current_checksum = sha256_file(installed_file)
|
|
220
|
+
|
|
221
|
+
# Get original checksum at installation
|
|
222
|
+
original_checksum = installation_record.checksum
|
|
223
|
+
|
|
224
|
+
# Calculate new template checksum
|
|
225
|
+
new_checksum = sha256_string(new_template_content)
|
|
226
|
+
|
|
227
|
+
# Decision matrix:
|
|
228
|
+
if current_checksum == original_checksum:
|
|
229
|
+
# File unchanged since installation
|
|
230
|
+
if new_checksum == original_checksum:
|
|
231
|
+
return ConflictType.NONE # No changes anywhere
|
|
232
|
+
else:
|
|
233
|
+
return ConflictType.NONE # Only remote changed, safe to update
|
|
234
|
+
else:
|
|
235
|
+
# File modified locally
|
|
236
|
+
if new_checksum == original_checksum:
|
|
237
|
+
return ConflictType.LOCAL_MODIFIED # Only local changed
|
|
238
|
+
else:
|
|
239
|
+
return ConflictType.BOTH_MODIFIED # Both changed
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def prompt_conflict_resolution_template(template_name: str, conflict_type: ConflictType) -> ConflictResolution:
|
|
243
|
+
"""
|
|
244
|
+
Interactive prompt for template conflict resolution using Rich.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
template_name: Name of conflicting template
|
|
248
|
+
conflict_type: Type of conflict detected
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
User's resolution choice
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> resolution = prompt_conflict_resolution_template(
|
|
255
|
+
... "test-command",
|
|
256
|
+
... ConflictType.BOTH_MODIFIED
|
|
257
|
+
... )
|
|
258
|
+
"""
|
|
259
|
+
console = Console()
|
|
260
|
+
|
|
261
|
+
console.print(f"\n[yellow]⚠️ Conflict detected for '{template_name}'[/yellow]")
|
|
262
|
+
|
|
263
|
+
if conflict_type == ConflictType.LOCAL_MODIFIED:
|
|
264
|
+
console.print("Local file was modified since installation")
|
|
265
|
+
elif conflict_type == ConflictType.BOTH_MODIFIED:
|
|
266
|
+
console.print("Both local and remote versions have changes")
|
|
267
|
+
|
|
268
|
+
console.print("\nChoose action:")
|
|
269
|
+
console.print(" [K]eep local version (ignore remote update)")
|
|
270
|
+
console.print(" [O]verwrite with remote version (discard local changes)")
|
|
271
|
+
console.print(" [R]ename local and install remote")
|
|
272
|
+
|
|
273
|
+
choice = Prompt.ask("Your choice", choices=["k", "o", "r", "K", "O", "R"], default="k").lower()
|
|
274
|
+
|
|
275
|
+
return {"k": ConflictResolution.SKIP, "o": ConflictResolution.OVERWRITE, "r": ConflictResolution.RENAME}[choice]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def apply_resolution(template_path: Path, new_content: str, resolution: ConflictResolution) -> Path:
|
|
279
|
+
"""
|
|
280
|
+
Apply conflict resolution and install template.
|
|
281
|
+
|
|
282
|
+
SAFETY: Automatically creates backups before overwriting files.
|
|
283
|
+
Backups stored in .instructionkit/backups/<timestamp>/
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
template_path: Original template path
|
|
287
|
+
new_content: New template content
|
|
288
|
+
resolution: How to resolve the conflict
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Path where template was actually installed
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
ValueError: If resolution strategy is unknown
|
|
295
|
+
"""
|
|
296
|
+
if resolution == ConflictResolution.SKIP:
|
|
297
|
+
# Keep existing file, don't install
|
|
298
|
+
return template_path
|
|
299
|
+
|
|
300
|
+
elif resolution == ConflictResolution.OVERWRITE:
|
|
301
|
+
# SAFETY: Create backup before overwriting
|
|
302
|
+
if template_path.exists():
|
|
303
|
+
from aiconfigkit.utils.backup import create_backup
|
|
304
|
+
|
|
305
|
+
backup_path = create_backup(template_path)
|
|
306
|
+
from rich.console import Console
|
|
307
|
+
|
|
308
|
+
console = Console()
|
|
309
|
+
console.print(f"[dim] Backup created: {backup_path.relative_to(backup_path.parent.parent)}[/dim]")
|
|
310
|
+
|
|
311
|
+
# Overwrite existing file
|
|
312
|
+
template_path.write_text(new_content, encoding="utf-8")
|
|
313
|
+
return template_path
|
|
314
|
+
|
|
315
|
+
elif resolution == ConflictResolution.RENAME:
|
|
316
|
+
# Rename local file and install new one
|
|
317
|
+
# Generate new path with suffix
|
|
318
|
+
renamed_path = resolve_conflict_name(template_path)
|
|
319
|
+
|
|
320
|
+
# Rename existing file (this preserves the original)
|
|
321
|
+
if template_path.exists():
|
|
322
|
+
template_path.rename(renamed_path)
|
|
323
|
+
|
|
324
|
+
# Install new content at original path
|
|
325
|
+
template_path.write_text(new_content, encoding="utf-8")
|
|
326
|
+
return template_path
|
|
327
|
+
|
|
328
|
+
else:
|
|
329
|
+
raise ValueError(f"Unknown conflict resolution strategy: {resolution}")
|