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,225 @@
1
+ """Template update command."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+
11
+ from aiconfigkit.core.checksum import sha256_string
12
+ from aiconfigkit.core.conflict_resolution import (
13
+ apply_resolution,
14
+ detect_conflict,
15
+ prompt_conflict_resolution_template,
16
+ )
17
+ from aiconfigkit.core.models import ConflictResolution, ConflictType
18
+ from aiconfigkit.storage.template_library import TemplateLibraryManager
19
+ from aiconfigkit.storage.template_tracker import TemplateInstallationTracker
20
+ from aiconfigkit.utils.git_helpers import TemplateNetworkError, update_template_repo
21
+ from aiconfigkit.utils.project import find_project_root
22
+
23
+ console = Console()
24
+
25
+
26
+ def update_command(
27
+ repo_name: Optional[str] = typer.Argument(None, help="Repository name to update (omit for --all)"),
28
+ all_repos: bool = typer.Option(
29
+ False,
30
+ "--all",
31
+ "-a",
32
+ help="Update all installed template repositories",
33
+ ),
34
+ scope: str = typer.Option(
35
+ "project",
36
+ "--scope",
37
+ "-s",
38
+ help="Which installations to update (project, global, both)",
39
+ ),
40
+ force: bool = typer.Option(
41
+ False,
42
+ "--force",
43
+ "-f",
44
+ help="Overwrite local changes without prompting",
45
+ ),
46
+ dry_run: bool = typer.Option(
47
+ False,
48
+ "--dry-run",
49
+ "-n",
50
+ help="Show what would be updated without making changes",
51
+ ),
52
+ ) -> None:
53
+ """
54
+ Update installed templates to latest version.
55
+
56
+ Example:
57
+ inskit template update acme-templates
58
+ inskit template update --all
59
+ inskit template update acme-templates --dry-run
60
+ inskit template update --all --force
61
+ """
62
+ try:
63
+ # Validate arguments
64
+ if not repo_name and not all_repos:
65
+ console.print("[red]Error: Must specify repo-name or --all[/red]")
66
+ console.print("Usage: inskit template update <repo-name> or inskit template update --all")
67
+ raise typer.Exit(2)
68
+
69
+ if scope not in ["project", "global", "both"]:
70
+ console.print(f"[red]Error: Invalid scope '{scope}'. Must be 'project', 'global', or 'both'[/red]")
71
+ raise typer.Exit(1)
72
+
73
+ # Determine which trackers to use
74
+ trackers: list[tuple[str, TemplateInstallationTracker]] = []
75
+ if scope in ["project", "both"]:
76
+ try:
77
+ project_root = find_project_root()
78
+ if project_root:
79
+ trackers.append(("project", TemplateInstallationTracker.for_project(project_root)))
80
+ except Exception:
81
+ if scope == "project":
82
+ console.print("[red]Error: Not in a project directory[/red]")
83
+ raise typer.Exit(1)
84
+
85
+ if scope in ["global", "both"]:
86
+ trackers.append(("global", TemplateInstallationTracker.for_global()))
87
+
88
+ # Collect repositories to update
89
+ repos_to_update: set[str] = set()
90
+ for scope_name, tracker in trackers:
91
+ records = tracker.load_installation_records()
92
+ if all_repos:
93
+ repos_to_update.update(r.namespace for r in records)
94
+ elif repo_name:
95
+ matching = [r for r in records if r.source_repo == repo_name or r.namespace == repo_name]
96
+ if matching:
97
+ repos_to_update.add(matching[0].namespace)
98
+
99
+ if not repos_to_update:
100
+ if repo_name:
101
+ console.print(f"[yellow]Repository '{repo_name}' not found in {scope} installations[/yellow]")
102
+ else:
103
+ console.print(f"[yellow]No repositories found in {scope} installations[/yellow]")
104
+ raise typer.Exit(0)
105
+
106
+ # Update each repository
107
+ library_manager = TemplateLibraryManager()
108
+ total_updated = 0
109
+
110
+ for namespace in sorted(repos_to_update):
111
+ console.print(f"\n[bold]Checking {namespace} for updates...[/bold]")
112
+
113
+ # Get repository path
114
+ try:
115
+ repo_path, old_manifest = library_manager.get_template_repository(namespace)
116
+ except FileNotFoundError:
117
+ console.print("[yellow]⚠️ Repository not found in library, skipping[/yellow]")
118
+ continue
119
+
120
+ # Check for updates
121
+ try:
122
+ with Progress(
123
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console
124
+ ) as progress:
125
+ task = progress.add_task("Fetching updates...", total=None)
126
+ has_updates = update_template_repo(repo_path)
127
+ progress.update(task, completed=True)
128
+
129
+ if not has_updates:
130
+ console.print("[green]✓ Already up-to-date[/green]")
131
+ continue
132
+
133
+ except TemplateNetworkError as e:
134
+ console.print(f"[red]❌ Failed to check for updates: {e}[/red]")
135
+ continue
136
+
137
+ # Load new manifest
138
+ from aiconfigkit.core.template_manifest import load_manifest
139
+
140
+ new_manifest = load_manifest(repo_path / "templatekit.yaml")
141
+
142
+ console.print(f"[green]Found updates[/green] (v{old_manifest.version} → v{new_manifest.version})\n")
143
+
144
+ if dry_run:
145
+ console.print("[dim]Dry run - no changes will be made[/dim]")
146
+ # Show what would be updated
147
+ for template in new_manifest.templates:
148
+ console.print(f" Would update: {namespace}.{template.name}")
149
+ continue
150
+
151
+ # Update templates
152
+ updated_count = 0
153
+ skipped_count = 0
154
+
155
+ for scope_name, tracker in trackers:
156
+ records = tracker.get_installations_by_namespace(namespace)
157
+
158
+ for record in records:
159
+ # Find matching template in new manifest
160
+ matching_template = next(
161
+ (t for t in new_manifest.templates if t.name == record.template_name), None
162
+ )
163
+ if not matching_template:
164
+ console.print(
165
+ f"[yellow]⚠️ Template {record.template_name} no longer in repository, skipping[/yellow]"
166
+ )
167
+ continue
168
+
169
+ # Read new template content
170
+ template_file = matching_template.files[0]
171
+ source_file = repo_path / template_file.path
172
+ new_content = source_file.read_text(encoding="utf-8")
173
+
174
+ # Check for conflicts
175
+ installed_path = Path(record.installed_path)
176
+ conflict_type = detect_conflict(installed_path, new_content, record)
177
+
178
+ if conflict_type != ConflictType.NONE and not force:
179
+ # Prompt for resolution
180
+ resolution = prompt_conflict_resolution_template(
181
+ f"{namespace}.{record.template_name}", conflict_type
182
+ )
183
+
184
+ if resolution == ConflictResolution.SKIP:
185
+ console.print(f"Skipping [cyan]{namespace}.{record.template_name}[/cyan]")
186
+ skipped_count += 1
187
+ continue
188
+
189
+ # Apply resolution
190
+ apply_resolution(installed_path, new_content, resolution)
191
+ else:
192
+ # Safe to update or force mode
193
+ console.print(f"Updating [cyan]{namespace}.{record.template_name}[/cyan]...", end=" ")
194
+ installed_path.write_text(new_content, encoding="utf-8")
195
+
196
+ # Update installation record
197
+ record.source_version = new_manifest.version
198
+ record.checksum = sha256_string(new_content)
199
+ record.installed_at = datetime.now()
200
+ tracker.update_installation(record.id, record)
201
+
202
+ console.print("[green]✓[/green]")
203
+ updated_count += 1
204
+
205
+ if updated_count > 0:
206
+ console.print(f"\n[green]✓ Updated {updated_count} template(s)[/green]")
207
+ total_updated += updated_count
208
+ if skipped_count > 0:
209
+ console.print(f"[yellow]Skipped {skipped_count} template(s) due to conflicts[/yellow]")
210
+
211
+ if dry_run:
212
+ console.print("\n[dim]Dry run complete - no changes were made[/dim]")
213
+ elif total_updated > 0:
214
+ console.print(f"\n[green]✓ Total updated: {total_updated} template(s)[/green]")
215
+ else:
216
+ console.print("\n[yellow]No templates were updated[/yellow]")
217
+
218
+ except typer.Exit:
219
+ raise
220
+ except KeyboardInterrupt:
221
+ console.print("\n[yellow]Cancelled by user[/yellow]")
222
+ raise typer.Exit(130)
223
+ except Exception as e:
224
+ console.print(f"\n[red]Error: {e}[/red]")
225
+ raise typer.Exit(1)
@@ -0,0 +1,234 @@
1
+ """Template validation command."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from aiconfigkit.core.checksum import calculate_file_checksum
10
+ from aiconfigkit.storage.template_library import TemplateLibraryManager
11
+ from aiconfigkit.storage.template_tracker import TemplateInstallationTracker
12
+ from aiconfigkit.utils.project import find_project_root
13
+
14
+ console = Console()
15
+
16
+
17
+ class ValidationIssue:
18
+ """Represents a validation issue found during template validation."""
19
+
20
+ def __init__(self, severity: str, template: str, issue_type: str, description: str, remediation: str = ""):
21
+ """
22
+ Initialize validation issue.
23
+
24
+ Args:
25
+ severity: Issue severity (error, warning, info)
26
+ template: Template identifier
27
+ issue_type: Type of issue
28
+ description: Issue description
29
+ remediation: Suggested remediation
30
+ """
31
+ self.severity = severity
32
+ self.template = template
33
+ self.issue_type = issue_type
34
+ self.description = description
35
+ self.remediation = remediation
36
+
37
+
38
+ def validate_command(
39
+ scope: str = typer.Option(
40
+ "all",
41
+ "--scope",
42
+ "-s",
43
+ help="Which installations to validate (project, global, all)",
44
+ ),
45
+ fix: bool = typer.Option(
46
+ False,
47
+ "--fix",
48
+ help="Attempt to fix issues automatically",
49
+ ),
50
+ verbose: bool = typer.Option(
51
+ False,
52
+ "--verbose",
53
+ "-v",
54
+ help="Show detailed information",
55
+ ),
56
+ ) -> None:
57
+ """
58
+ Validate installed templates for issues.
59
+
60
+ Checks for:
61
+ - Tracking inconsistencies (installed but files missing)
62
+ - Missing files referenced in manifest
63
+ - Outdated versions (local vs remote)
64
+ - Broken template dependencies
65
+ - Local modifications (checksum mismatch)
66
+
67
+ Example:
68
+ inskit template validate
69
+ inskit template validate --scope project
70
+ inskit template validate --fix
71
+ """
72
+ try:
73
+ # Validate scope
74
+ if scope not in ["project", "global", "all"]:
75
+ console.print(f"[red]Error: Invalid scope '{scope}'. Must be 'project', 'global', or 'all'[/red]")
76
+ raise typer.Exit(1)
77
+
78
+ issues: list[ValidationIssue] = []
79
+
80
+ # Validate project templates
81
+ if scope in ["project", "all"]:
82
+ try:
83
+ project_root = find_project_root()
84
+ if project_root:
85
+ console.print(f"\n[bold]Validating project templates[/bold] ({project_root})...")
86
+ project_issues = _validate_installations(
87
+ TemplateInstallationTracker.for_project(project_root), "project", verbose
88
+ )
89
+ issues.extend(project_issues)
90
+ elif scope == "project":
91
+ console.print("[yellow]⚠️ Not in a project directory[/yellow]")
92
+ raise typer.Exit(1)
93
+ except Exception as e:
94
+ if scope == "project":
95
+ console.print(f"[red]Error: {e}[/red]")
96
+ raise typer.Exit(1)
97
+
98
+ # Validate global templates
99
+ if scope in ["global", "all"]:
100
+ console.print("\n[bold]Validating global templates...[/bold]")
101
+ global_issues = _validate_installations(TemplateInstallationTracker.for_global(), "global", verbose)
102
+ issues.extend(global_issues)
103
+
104
+ # Display results
105
+ _display_validation_results(issues, fix, verbose)
106
+
107
+ except typer.Exit:
108
+ raise
109
+ except KeyboardInterrupt:
110
+ console.print("\n[yellow]Validation cancelled by user[/yellow]")
111
+ raise typer.Exit(130)
112
+ except Exception as e:
113
+ console.print(f"\n[red]Error: {e}[/red]")
114
+ raise typer.Exit(1)
115
+
116
+
117
+ def _validate_installations(tracker: TemplateInstallationTracker, scope: str, verbose: bool) -> list[ValidationIssue]:
118
+ """Validate all installations tracked by a tracker."""
119
+ issues: list[ValidationIssue] = []
120
+ records = tracker.load_installation_records()
121
+
122
+ if not records:
123
+ console.print(f" [dim]No {scope} templates installed[/dim]")
124
+ return issues
125
+
126
+ console.print(f" Found {len(records)} template(s)")
127
+
128
+ for record in records:
129
+ template_id = f"{record.namespace}.{record.template_name}"
130
+
131
+ # Check 1: File exists
132
+ installed_path = Path(record.installed_path)
133
+ if not installed_path.exists():
134
+ issues.append(
135
+ ValidationIssue(
136
+ severity="error",
137
+ template=template_id,
138
+ issue_type="missing_file",
139
+ description=f"Installed file not found: {installed_path}",
140
+ remediation=(
141
+ f"Reinstall template with: "
142
+ f"inskit template install {record.source_repo} --template {record.template_name}"
143
+ ),
144
+ )
145
+ )
146
+ continue
147
+
148
+ # Check 2: Local modifications (checksum mismatch)
149
+ try:
150
+ current_checksum = calculate_file_checksum(str(installed_path))
151
+ if current_checksum != record.checksum:
152
+ issues.append(
153
+ ValidationIssue(
154
+ severity="warning",
155
+ template=template_id,
156
+ issue_type="modified",
157
+ description="Template has been modified locally",
158
+ remediation=f"Update to restore original: inskit template update {record.namespace}",
159
+ )
160
+ )
161
+ except Exception as e:
162
+ if verbose:
163
+ console.print(f" [dim]Could not verify checksum for {template_id}: {e}[/dim]")
164
+
165
+ # Check 3: Outdated version
166
+ try:
167
+ library_manager = TemplateLibraryManager()
168
+ local_version = library_manager.get_repository_version(record.namespace)
169
+ if local_version and record.source_version and local_version != record.source_version:
170
+ issues.append(
171
+ ValidationIssue(
172
+ severity="info",
173
+ template=template_id,
174
+ issue_type="outdated",
175
+ description=f"Newer version available ({record.source_version} -> {local_version})",
176
+ remediation=f"Update with: inskit template update {record.namespace}",
177
+ )
178
+ )
179
+ except Exception as e:
180
+ if verbose:
181
+ console.print(f" [dim]Could not check version for {template_id}: {e}[/dim]")
182
+
183
+ return issues
184
+
185
+
186
+ def _display_validation_results(issues: list[ValidationIssue], fix: bool, verbose: bool) -> None:
187
+ """Display validation results in a formatted table."""
188
+ if not issues:
189
+ console.print("\n[green]✓ All templates are valid![/green]")
190
+ return
191
+
192
+ # Group by severity
193
+ errors = [i for i in issues if i.severity == "error"]
194
+ warnings = [i for i in issues if i.severity == "warning"]
195
+ info = [i for i in issues if i.severity == "info"]
196
+
197
+ console.print("\n[bold]Validation Summary:[/bold]")
198
+ if errors:
199
+ console.print(f" [red]✗ {len(errors)} error(s)[/red]")
200
+ if warnings:
201
+ console.print(f" [yellow]⚠ {len(warnings)} warning(s)[/yellow]")
202
+ if info:
203
+ console.print(f" [blue]ℹ {len(info)} info[/blue]")
204
+
205
+ # Display issues table
206
+ if verbose or errors:
207
+ table = Table(title="\nValidation Issues")
208
+ table.add_column("Severity", style="bold")
209
+ table.add_column("Template")
210
+ table.add_column("Issue Type")
211
+ table.add_column("Description")
212
+ table.add_column("Remediation")
213
+
214
+ for issue in errors + warnings + info:
215
+ severity_color = {"error": "red", "warning": "yellow", "info": "blue"}[issue.severity]
216
+ severity_symbol = {"error": "✗", "warning": "⚠", "info": "ℹ"}[issue.severity]
217
+
218
+ table.add_row(
219
+ f"[{severity_color}]{severity_symbol} {issue.severity.upper()}[/{severity_color}]",
220
+ issue.template,
221
+ issue.issue_type,
222
+ issue.description,
223
+ issue.remediation,
224
+ )
225
+
226
+ console.print(table)
227
+
228
+ if fix:
229
+ console.print("\n[yellow]⚠️ Auto-fix is not yet implemented[/yellow]")
230
+ console.print("Please use the suggested remediation commands above")
231
+
232
+ # Exit with error code if there are errors
233
+ if errors:
234
+ raise typer.Exit(1)
@@ -0,0 +1,47 @@
1
+ """Tools command to show detected AI coding tools."""
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from aiconfigkit.ai_tools.detector import get_detector
7
+
8
+ console = Console()
9
+
10
+
11
+ def show_tools() -> int:
12
+ """
13
+ Show detected AI coding tools.
14
+
15
+ Returns:
16
+ Exit code (0 for success)
17
+ """
18
+ detector = get_detector()
19
+
20
+ # Create table
21
+ table = Table(title="AI Coding Tools", show_header=True, header_style="bold cyan")
22
+ table.add_column("Tool", style="cyan", no_wrap=True)
23
+ table.add_column("Status", style="green")
24
+
25
+ # Get all tools and their status
26
+ for tool_type, tool in detector.tools.items():
27
+ is_installed = tool.is_installed()
28
+ status = "[green]✓ Installed[/green]" if is_installed else "[red]✗ Not found[/red]"
29
+
30
+ table.add_row(tool.tool_name, status)
31
+
32
+ # Display table
33
+ console.print()
34
+ console.print(table)
35
+ console.print()
36
+
37
+ # Summary
38
+ installed = detector.detect_installed_tools()
39
+ if installed:
40
+ tool_names = ", ".join([t.tool_name for t in installed])
41
+ console.print(f"[green]Found {len(installed)} installed tool(s):[/green] {tool_names}")
42
+ else:
43
+ console.print("[yellow]No AI coding tools detected[/yellow]")
44
+ console.print("\nSupported tools: Cursor, GitHub Copilot, Winsurf, Claude Code")
45
+
46
+ console.print()
47
+ return 0
@@ -0,0 +1,125 @@
1
+ """Uninstall command implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from aiconfigkit.ai_tools.detector import get_detector
10
+ from aiconfigkit.core.models import AIToolType, InstallationScope
11
+ from aiconfigkit.storage.tracker import InstallationTracker
12
+ from aiconfigkit.utils.project import find_project_root
13
+ from aiconfigkit.utils.ui import print_error, print_success, print_warning
14
+
15
+ console = Console()
16
+
17
+
18
+ def uninstall_instruction(
19
+ name: str,
20
+ tool: Optional[str] = None,
21
+ force: bool = False,
22
+ ) -> int:
23
+ """
24
+ Uninstall an instruction.
25
+
26
+ Uninstalls from project level only.
27
+
28
+ Args:
29
+ name: Instruction name to uninstall
30
+ tool: Specific AI tool to uninstall from (or None for all)
31
+ force: Skip confirmation prompt
32
+
33
+ Returns:
34
+ Exit code (0 for success, 1 for error)
35
+ """
36
+ tracker = InstallationTracker()
37
+
38
+ # Detect project root
39
+ project_root = find_project_root()
40
+
41
+ # Get installed instructions matching the name (project scope only)
42
+ all_records = tracker.get_installed_instructions(project_root=project_root)
43
+ matching_records = [r for r in all_records if r.instruction_name == name and r.scope == InstallationScope.PROJECT]
44
+
45
+ # Filter by tool if specified
46
+ if tool:
47
+ try:
48
+ ai_tool_type = AIToolType(tool.lower())
49
+ matching_records = [r for r in matching_records if r.ai_tool == ai_tool_type]
50
+ except ValueError:
51
+ print_error(f"Invalid AI tool: {tool}. Valid options: cursor, copilot, windsurf, claude", console)
52
+ return 1
53
+
54
+ # Check if instruction is installed
55
+ if not matching_records:
56
+ if tool:
57
+ print_error(f"Instruction '{name}' is not installed for {tool}", console)
58
+ else:
59
+ print_error(f"Instruction '{name}' is not installed", console)
60
+ return 1
61
+
62
+ # Show what will be uninstalled
63
+ console.print("\n[yellow]The following will be uninstalled:[/yellow]")
64
+ for record in matching_records:
65
+ console.print(f" • {record.instruction_name} ({record.ai_tool.value}, {record.scope.value})")
66
+
67
+ # Confirm unless --force
68
+ if not force:
69
+ console.print()
70
+ confirm = typer.confirm("Continue with uninstall?", default=False)
71
+ if not confirm:
72
+ console.print("[yellow]Uninstall cancelled[/yellow]")
73
+ return 0
74
+
75
+ # Get AI tool detector
76
+ detector = get_detector()
77
+
78
+ # Uninstall each record
79
+ removed_count = 0
80
+ error_count = 0
81
+
82
+ for record in matching_records:
83
+ ai_tool = detector.get_tool_by_type(record.ai_tool)
84
+ if not ai_tool:
85
+ print_warning(f"Unknown AI tool: {record.ai_tool}", console)
86
+ error_count += 1
87
+ continue
88
+
89
+ try:
90
+ # Remove file
91
+ file_path = Path(record.installed_path)
92
+ if file_path.exists():
93
+ file_path.unlink()
94
+ else:
95
+ print_warning(f"File not found: {file_path}", console)
96
+
97
+ # Determine scope filter for removal
98
+ scope_filter = record.scope.value
99
+
100
+ # Determine project root for removal (only for project-scoped installations)
101
+ removal_project_root = project_root if record.scope == InstallationScope.PROJECT else None
102
+
103
+ # Remove from tracker
104
+ tracker.remove_installation(
105
+ record.instruction_name, record.ai_tool, project_root=removal_project_root, scope_filter=scope_filter
106
+ )
107
+
108
+ print_success(
109
+ f"Uninstalled {record.instruction_name} from {ai_tool.tool_name} ({record.scope.value})", console
110
+ )
111
+ removed_count += 1
112
+
113
+ except Exception as e:
114
+ print_error(f"Failed to uninstall {record.instruction_name}: {e}", console)
115
+ error_count += 1
116
+
117
+ # Summary
118
+ console.print()
119
+ if removed_count > 0:
120
+ console.print(f"[green]Successfully uninstalled {removed_count} instruction(s)[/green]")
121
+ if error_count > 0:
122
+ console.print(f"[red]Failed to uninstall {error_count} instruction(s)[/red]")
123
+ return 1
124
+
125
+ return 0