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,472 @@
1
+ """Package installation command and logic."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from aiconfigkit.ai_tools.capability_registry import get_capability
10
+ from aiconfigkit.ai_tools.translator import get_translator
11
+ from aiconfigkit.core.models import (
12
+ AIToolType,
13
+ ComponentStatus,
14
+ ComponentType,
15
+ ConflictResolution,
16
+ InstallationScope,
17
+ InstallationStatus,
18
+ InstalledComponent,
19
+ Package,
20
+ PackageInstallationRecord,
21
+ )
22
+ from aiconfigkit.core.package_manifest import PackageManifestParser
23
+ from aiconfigkit.storage.package_tracker import PackageTracker
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class InstallationResult:
30
+ """Result of package installation operation."""
31
+
32
+ success: bool
33
+ status: InstallationStatus
34
+ package_name: str
35
+ version: str
36
+ installed_count: int = 0
37
+ skipped_count: int = 0
38
+ failed_count: int = 0
39
+ components_installed: dict[ComponentType, int] = field(default_factory=dict)
40
+ error_message: Optional[str] = None
41
+ is_reinstall: bool = False
42
+
43
+ @property
44
+ def total_components(self) -> int:
45
+ """Total number of components processed."""
46
+ return self.installed_count + self.skipped_count + self.failed_count
47
+
48
+
49
+ def install_package(
50
+ package_path: Path,
51
+ project_root: Path,
52
+ target_ide: AIToolType,
53
+ scope: InstallationScope = InstallationScope.PROJECT,
54
+ conflict_resolution: ConflictResolution = ConflictResolution.SKIP,
55
+ force: bool = False,
56
+ ) -> InstallationResult:
57
+ """
58
+ Install a package to a project for a specific IDE.
59
+
60
+ Args:
61
+ package_path: Path to package directory containing manifest
62
+ project_root: Root directory of target project
63
+ target_ide: Target IDE for installation
64
+ scope: Installation scope (project or global)
65
+ conflict_resolution: How to handle file conflicts
66
+ force: Force reinstallation even if already installed
67
+
68
+ Returns:
69
+ InstallationResult with details of installation
70
+
71
+ Raises:
72
+ FileNotFoundError: If manifest not found
73
+ ValidationError: If manifest is invalid
74
+ """
75
+ logger.info(f"Installing package from {package_path} to {project_root} for {target_ide.value}")
76
+
77
+ try:
78
+ # Step 1: Parse and validate manifest
79
+ parser = PackageManifestParser(package_path)
80
+ package = parser.parse()
81
+
82
+ # Validate manifest
83
+ validation_errors = parser.validate(package)
84
+ if validation_errors:
85
+ error_msg = "; ".join(validation_errors)
86
+ return InstallationResult(
87
+ success=False,
88
+ status=InstallationStatus.FAILED,
89
+ package_name=package.name,
90
+ version=package.version,
91
+ error_message=f"Manifest validation failed: {error_msg}",
92
+ )
93
+
94
+ logger.info(f"Parsed package: {package.name} v{package.version}")
95
+
96
+ # Step 2: Check if already installed
97
+ tracker_file = project_root / ".ai-config-kit" / "packages.json"
98
+ tracker = PackageTracker(tracker_file)
99
+ is_reinstall = tracker.is_package_installed(package.name, scope)
100
+
101
+ if is_reinstall and not force:
102
+ logger.info(f"Package {package.name} already installed")
103
+
104
+ # Step 3: Get IDE capabilities
105
+ capability = get_capability(target_ide)
106
+ logger.debug(f"Target IDE capabilities: {capability.supported_components}")
107
+
108
+ # Step 4: Get component translator
109
+ translator = get_translator(target_ide)
110
+
111
+ # Step 5: Filter components by IDE capabilities and track skipped
112
+ installable_components = _filter_components_by_capability(package, capability)
113
+
114
+ # Calculate skipped components (filtered out by IDE capabilities)
115
+ total_in_package = package.components.total_count
116
+ installable_count = sum(len(comps) for comps in installable_components.values())
117
+ capability_skipped = total_in_package - installable_count
118
+
119
+ logger.info(
120
+ f"Found {installable_count} installable components, {capability_skipped} filtered by IDE capabilities"
121
+ )
122
+
123
+ # Step 6: Install each component type
124
+ installed_components: list[InstalledComponent] = []
125
+ installed_count = 0
126
+ skipped_count = capability_skipped # Start with capability-filtered components
127
+ failed_count = 0
128
+ components_by_type: dict[ComponentType, int] = {}
129
+
130
+ # Install instructions
131
+ for instruction in installable_components.get("instructions", []):
132
+ result = _install_instruction_component(
133
+ instruction,
134
+ package_path,
135
+ project_root,
136
+ translator,
137
+ conflict_resolution,
138
+ )
139
+ if result:
140
+ installed_components.append(result)
141
+ installed_count += 1
142
+ components_by_type[ComponentType.INSTRUCTION] = components_by_type.get(ComponentType.INSTRUCTION, 0) + 1
143
+ else:
144
+ skipped_count += 1
145
+
146
+ # Install MCP servers
147
+ for mcp in installable_components.get("mcp_servers", []):
148
+ result = _install_mcp_component(mcp, package_path, project_root, translator, conflict_resolution)
149
+ if result:
150
+ installed_components.append(result)
151
+ installed_count += 1
152
+ components_by_type[ComponentType.MCP_SERVER] = components_by_type.get(ComponentType.MCP_SERVER, 0) + 1
153
+ else:
154
+ skipped_count += 1
155
+
156
+ # Install hooks
157
+ for hook in installable_components.get("hooks", []):
158
+ result = _install_hook_component(hook, package_path, project_root, translator, conflict_resolution)
159
+ if result:
160
+ installed_components.append(result)
161
+ installed_count += 1
162
+ components_by_type[ComponentType.HOOK] = components_by_type.get(ComponentType.HOOK, 0) + 1
163
+ else:
164
+ skipped_count += 1
165
+
166
+ # Install commands
167
+ for command in installable_components.get("commands", []):
168
+ result = _install_command_component(command, package_path, project_root, translator, conflict_resolution)
169
+ if result:
170
+ installed_components.append(result)
171
+ installed_count += 1
172
+ components_by_type[ComponentType.COMMAND] = components_by_type.get(ComponentType.COMMAND, 0) + 1
173
+ else:
174
+ skipped_count += 1
175
+
176
+ # Install resources
177
+ for resource in installable_components.get("resources", []):
178
+ result = _install_resource_component(resource, package_path, project_root, translator, conflict_resolution)
179
+ if result:
180
+ installed_components.append(result)
181
+ installed_count += 1
182
+ components_by_type[ComponentType.RESOURCE] = components_by_type.get(ComponentType.RESOURCE, 0) + 1
183
+ else:
184
+ skipped_count += 1
185
+
186
+ # Step 7: Determine installation status
187
+ total_in_package = package.components.total_count
188
+ if installed_count == 0:
189
+ status = InstallationStatus.FAILED
190
+ elif installed_count == total_in_package:
191
+ status = InstallationStatus.COMPLETE
192
+ else:
193
+ status = InstallationStatus.PARTIAL
194
+
195
+ # Step 8: Record installation
196
+ now = datetime.now()
197
+ # Get original install time for reinstalls
198
+ existing_record = tracker.get_package(package.name, scope) if is_reinstall else None
199
+ original_install_time = existing_record.installed_at if existing_record else now
200
+
201
+ installation_record = PackageInstallationRecord(
202
+ package_name=package.name,
203
+ namespace=package.namespace,
204
+ version=package.version,
205
+ installed_at=original_install_time,
206
+ updated_at=now,
207
+ scope=scope,
208
+ components=installed_components,
209
+ status=status,
210
+ )
211
+ tracker.record_installation(installation_record)
212
+
213
+ logger.info(
214
+ f"Installation complete: {installed_count} installed, {skipped_count} skipped, {failed_count} failed"
215
+ )
216
+
217
+ return InstallationResult(
218
+ success=True,
219
+ status=status,
220
+ package_name=package.name,
221
+ version=package.version,
222
+ installed_count=installed_count,
223
+ skipped_count=skipped_count,
224
+ failed_count=failed_count,
225
+ components_installed=components_by_type,
226
+ is_reinstall=is_reinstall,
227
+ )
228
+
229
+ except Exception as e:
230
+ logger.error(f"Installation failed: {e}", exc_info=True)
231
+ return InstallationResult(
232
+ success=False,
233
+ status=InstallationStatus.FAILED,
234
+ package_name=package_path.name if package_path else "unknown",
235
+ version="unknown",
236
+ error_message=str(e),
237
+ )
238
+
239
+
240
+ def _filter_components_by_capability(package: Package, capability: Any) -> dict[str, list[Any]]:
241
+ """Filter package components by IDE capability."""
242
+ filtered: dict[str, list[Any]] = {}
243
+
244
+ if capability.supports_component(ComponentType.INSTRUCTION):
245
+ filtered["instructions"] = package.components.instructions
246
+
247
+ if capability.supports_component(ComponentType.MCP_SERVER):
248
+ filtered["mcp_servers"] = package.components.mcp_servers
249
+
250
+ if capability.supports_component(ComponentType.HOOK):
251
+ filtered["hooks"] = package.components.hooks
252
+
253
+ if capability.supports_component(ComponentType.COMMAND):
254
+ filtered["commands"] = package.components.commands
255
+
256
+ if capability.supports_component(ComponentType.RESOURCE):
257
+ filtered["resources"] = package.components.resources
258
+
259
+ return filtered
260
+
261
+
262
+ def _install_instruction_component(
263
+ component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
264
+ ) -> Optional[InstalledComponent]:
265
+ """Install instruction component."""
266
+ try:
267
+ # Translate component
268
+ translated = translator.translate_instruction(component, package_path)
269
+
270
+ # Determine target path
271
+ target_file = project_root / translated.target_path
272
+
273
+ # Check for conflicts
274
+ if target_file.exists():
275
+ if conflict_resolution == ConflictResolution.SKIP:
276
+ logger.info(f"Skipping existing file: {target_file}")
277
+ return None
278
+ elif conflict_resolution == ConflictResolution.RENAME:
279
+ # Find available numbered suffix
280
+ counter = 1
281
+ stem = target_file.stem
282
+ suffix = target_file.suffix
283
+ while target_file.exists():
284
+ target_file = target_file.parent / f"{stem}-{counter}{suffix}"
285
+ counter += 1
286
+ logger.info(f"Renaming to avoid conflict: {target_file}")
287
+
288
+ # Install file
289
+ target_file.parent.mkdir(parents=True, exist_ok=True)
290
+ target_file.write_text(translated.content)
291
+
292
+ # Calculate checksum
293
+ from aiconfigkit.core.checksum import calculate_file_checksum
294
+
295
+ checksum = calculate_file_checksum(str(target_file), "sha256")
296
+
297
+ return InstalledComponent(
298
+ type=ComponentType.INSTRUCTION,
299
+ name=component.name,
300
+ installed_path=str(target_file.relative_to(project_root)),
301
+ checksum=checksum,
302
+ status=ComponentStatus.INSTALLED,
303
+ )
304
+
305
+ except Exception as e:
306
+ logger.error(f"Failed to install instruction {component.name}: {e}")
307
+ return None
308
+
309
+
310
+ def _install_mcp_component(
311
+ component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
312
+ ) -> Optional[InstalledComponent]:
313
+ """Install MCP server component."""
314
+ try:
315
+ # Translate component
316
+ translated = translator.translate_mcp_server(component, package_path)
317
+ target_file = project_root / translated.target_path
318
+
319
+ # Check for conflicts
320
+ if target_file.exists() and conflict_resolution == ConflictResolution.SKIP:
321
+ return None
322
+
323
+ # Create target directory
324
+ target_file.parent.mkdir(parents=True, exist_ok=True)
325
+
326
+ # Write MCP config file
327
+ target_file.write_text(translated.content)
328
+
329
+ # Calculate checksum
330
+ from aiconfigkit.core.checksum import calculate_file_checksum
331
+
332
+ checksum = calculate_file_checksum(str(target_file), "sha256")
333
+
334
+ return InstalledComponent(
335
+ type=ComponentType.MCP_SERVER,
336
+ name=component.name,
337
+ installed_path=str(target_file.relative_to(project_root)),
338
+ checksum=checksum,
339
+ status=ComponentStatus.INSTALLED,
340
+ )
341
+
342
+ except Exception as e:
343
+ logger.error(f"Failed to install MCP server {component.name}: {e}")
344
+ return None
345
+
346
+
347
+ def _install_hook_component(
348
+ component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
349
+ ) -> Optional[InstalledComponent]:
350
+ """Install hook component."""
351
+ try:
352
+ translated = translator.translate_hook(component, package_path)
353
+ target_file = project_root / translated.target_path
354
+
355
+ # Check for conflicts
356
+ if target_file.exists():
357
+ if conflict_resolution == ConflictResolution.SKIP:
358
+ logger.info(f"Skipping existing file: {target_file}")
359
+ return None
360
+ elif conflict_resolution == ConflictResolution.RENAME:
361
+ # Find available numbered suffix
362
+ counter = 1
363
+ stem = target_file.stem
364
+ suffix = target_file.suffix
365
+ while target_file.exists():
366
+ target_file = target_file.parent / f"{stem}-{counter}{suffix}"
367
+ counter += 1
368
+ logger.info(f"Renaming to avoid conflict: {target_file}")
369
+
370
+ target_file.parent.mkdir(parents=True, exist_ok=True)
371
+ target_file.write_text(translated.content)
372
+ target_file.chmod(0o755) # Make executable
373
+
374
+ from aiconfigkit.core.checksum import calculate_file_checksum
375
+
376
+ checksum = calculate_file_checksum(str(target_file), "sha256")
377
+
378
+ return InstalledComponent(
379
+ type=ComponentType.HOOK,
380
+ name=component.name,
381
+ installed_path=str(target_file.relative_to(project_root)),
382
+ checksum=checksum,
383
+ status=ComponentStatus.INSTALLED,
384
+ )
385
+
386
+ except Exception as e:
387
+ logger.error(f"Failed to install hook {component.name}: {e}")
388
+ return None
389
+
390
+
391
+ def _install_command_component(
392
+ component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
393
+ ) -> Optional[InstalledComponent]:
394
+ """Install command component."""
395
+ try:
396
+ translated = translator.translate_command(component, package_path)
397
+ target_file = project_root / translated.target_path
398
+
399
+ # Check for conflicts
400
+ if target_file.exists():
401
+ if conflict_resolution == ConflictResolution.SKIP:
402
+ logger.info(f"Skipping existing file: {target_file}")
403
+ return None
404
+ elif conflict_resolution == ConflictResolution.RENAME:
405
+ # Find available numbered suffix
406
+ counter = 1
407
+ stem = target_file.stem
408
+ suffix = target_file.suffix
409
+ while target_file.exists():
410
+ target_file = target_file.parent / f"{stem}-{counter}{suffix}"
411
+ counter += 1
412
+ logger.info(f"Renaming to avoid conflict: {target_file}")
413
+
414
+ target_file.parent.mkdir(parents=True, exist_ok=True)
415
+ target_file.write_text(translated.content)
416
+ target_file.chmod(0o755) # Make executable
417
+
418
+ from aiconfigkit.core.checksum import calculate_file_checksum
419
+
420
+ checksum = calculate_file_checksum(str(target_file), "sha256")
421
+
422
+ return InstalledComponent(
423
+ type=ComponentType.COMMAND,
424
+ name=component.name,
425
+ installed_path=str(target_file.relative_to(project_root)),
426
+ checksum=checksum,
427
+ status=ComponentStatus.INSTALLED,
428
+ )
429
+
430
+ except Exception as e:
431
+ logger.error(f"Failed to install command {component.name}: {e}")
432
+ return None
433
+
434
+
435
+ def _install_resource_component(
436
+ component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
437
+ ) -> Optional[InstalledComponent]:
438
+ """Install resource component."""
439
+ try:
440
+ import shutil
441
+
442
+ translated = translator.translate_resource(component, package_path)
443
+ target_file = project_root / translated.target_path
444
+
445
+ if target_file.exists() and conflict_resolution == ConflictResolution.SKIP:
446
+ return None
447
+
448
+ # Copy file directly (handles both text and binary)
449
+ source_path = translated.metadata.get("source_path")
450
+ if source_path:
451
+ target_file.parent.mkdir(parents=True, exist_ok=True)
452
+ shutil.copy2(source_path, target_file)
453
+ else:
454
+ # Fallback to writing content (for compatibility)
455
+ target_file.parent.mkdir(parents=True, exist_ok=True)
456
+ target_file.write_text(translated.content)
457
+
458
+ from aiconfigkit.core.checksum import calculate_file_checksum
459
+
460
+ checksum = calculate_file_checksum(str(target_file), "sha256")
461
+
462
+ return InstalledComponent(
463
+ type=ComponentType.RESOURCE,
464
+ name=component.name,
465
+ installed_path=str(target_file.relative_to(project_root)),
466
+ checksum=checksum,
467
+ status=ComponentStatus.INSTALLED,
468
+ )
469
+
470
+ except Exception as e:
471
+ logger.error(f"Failed to install resource {component.name}: {e}")
472
+ return None
@@ -0,0 +1,19 @@
1
+ """Template management commands."""
2
+
3
+ import typer
4
+
5
+ # Create template subcommand group
6
+ template_app = typer.Typer(
7
+ name="template",
8
+ help="Manage template repositories for consistent project standards",
9
+ add_completion=False,
10
+ )
11
+
12
+
13
+ @template_app.callback(invoke_without_command=True)
14
+ def template_callback(ctx: typer.Context) -> None:
15
+ """Manage template repositories."""
16
+ # If no subcommand was provided, show help
17
+ if ctx.invoked_subcommand is None:
18
+ typer.echo(ctx.get_help())
19
+ raise typer.Exit(0)