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,261 @@
1
+ """Template installation command."""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+ from rich.table import Table
12
+
13
+ from aiconfigkit.ai_tools.detector import get_detector
14
+ from aiconfigkit.core.checksum import sha256_string
15
+ from aiconfigkit.core.models import AIToolType, InstallationScope, TemplateInstallationRecord
16
+ from aiconfigkit.core.template_manifest import validate_dependencies, validate_manifest_size
17
+ from aiconfigkit.storage.template_library import TemplateLibraryManager
18
+ from aiconfigkit.storage.template_tracker import TemplateInstallationTracker
19
+ from aiconfigkit.utils.git_helpers import TemplateAuthError, TemplateNetworkError
20
+ from aiconfigkit.utils.namespace import derive_namespace, get_install_path
21
+ from aiconfigkit.utils.project import find_project_root
22
+
23
+ console = Console()
24
+
25
+
26
+ def install_command(
27
+ repo_url: str = typer.Argument(..., help="Git repository URL (https:// or git@)"),
28
+ scope: str = typer.Option(
29
+ "project",
30
+ "--scope",
31
+ "-s",
32
+ help="Installation scope (project or global)",
33
+ ),
34
+ namespace_override: Optional[str] = typer.Option(
35
+ None,
36
+ "--as",
37
+ help="Override namespace (default: derived from repository name)",
38
+ ),
39
+ force: bool = typer.Option(
40
+ False,
41
+ "--force",
42
+ "-f",
43
+ help="Overwrite existing templates without prompting",
44
+ ),
45
+ ) -> None:
46
+ """
47
+ Install templates from a repository.
48
+
49
+ Example:
50
+ inskit template install https://github.com/acme/templates
51
+ inskit template install https://github.com/acme/templates --scope global
52
+ inskit template install https://github.com/acme/templates --as acme
53
+ """
54
+ try:
55
+ # Validate scope
56
+ if scope not in ["project", "global"]:
57
+ console.print(f"[red]Error: Invalid scope '{scope}'. Must be 'project' or 'global'[/red]")
58
+ raise typer.Exit(1)
59
+
60
+ installation_scope = InstallationScope.PROJECT if scope == "project" else InstallationScope.GLOBAL
61
+
62
+ # Derive namespace
63
+ try:
64
+ namespace = derive_namespace(repo_url, namespace_override)
65
+ if namespace_override:
66
+ console.print(f"Using custom namespace: [cyan]{namespace}[/cyan]")
67
+ else:
68
+ console.print(f"Deriving namespace from repository: [cyan]{namespace}[/cyan]")
69
+ except ValueError as e:
70
+ console.print(f"[red]Error: {e}[/red]")
71
+ raise typer.Exit(1)
72
+
73
+ # Clone repository
74
+ library_manager = TemplateLibraryManager()
75
+
76
+ console.print(f"\n[bold]Cloning repository from {repo_url}...[/bold]")
77
+
78
+ with Progress(
79
+ SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console
80
+ ) as progress:
81
+ task = progress.add_task("Cloning repository...", total=None)
82
+
83
+ try:
84
+ repo_path, manifest = library_manager.clone_repository(repo_url, namespace_override)
85
+ progress.update(task, completed=True)
86
+
87
+ except TemplateAuthError as e:
88
+ progress.stop()
89
+ console.print(f"\n[red]❌ {e}[/red]")
90
+ raise typer.Exit(3)
91
+
92
+ except TemplateNetworkError as e:
93
+ progress.stop()
94
+ console.print(f"\n[red]❌ {e}[/red]")
95
+ raise typer.Exit(4)
96
+
97
+ except Exception as e:
98
+ progress.stop()
99
+ console.print(f"\n[red]❌ Failed to clone repository: {e}[/red]")
100
+ raise typer.Exit(1)
101
+
102
+ console.print("[green]✓ Repository cloned[/green]\n")
103
+
104
+ # Validate manifest
105
+ warnings = validate_manifest_size(repo_path / "templatekit.yaml", len(manifest.templates))
106
+ for warning in warnings:
107
+ console.print(warning)
108
+
109
+ dep_errors = validate_dependencies(manifest.templates)
110
+ if dep_errors:
111
+ console.print("[red]❌ Manifest validation errors:[/red]")
112
+ for error in dep_errors:
113
+ console.print(f" - {error}")
114
+ raise typer.Exit(5)
115
+
116
+ # Detect IDEs
117
+ if installation_scope == InstallationScope.PROJECT:
118
+ try:
119
+ project_root = find_project_root()
120
+ except Exception:
121
+ console.print("[red]Error: Could not detect project root. Ensure you're in a project directory.[/red]")
122
+ raise typer.Exit(1)
123
+ else:
124
+ project_root = None
125
+
126
+ detector = get_detector()
127
+ detected_tool_instances = detector.detect_installed_tools() if project_root else []
128
+ detected_tools = [tool.tool_type for tool in detected_tool_instances]
129
+
130
+ if not detected_tools and installation_scope == InstallationScope.PROJECT:
131
+ console.print("[yellow]⚠️ No AI coding tools detected in project.[/yellow]")
132
+ console.print("Templates will be installed but may not be accessible until IDE is configured.")
133
+
134
+ if not detected_tools and installation_scope == InstallationScope.GLOBAL:
135
+ # For global, use a default set
136
+ detected_tools = [AIToolType.CURSOR, AIToolType.CLAUDE, AIToolType.WINSURF, AIToolType.COPILOT]
137
+
138
+ # Install templates
139
+ console.print(f"[bold]Installing {len(manifest.templates)} templates...[/bold]\n")
140
+
141
+ # Initialize tracker
142
+ if installation_scope == InstallationScope.PROJECT:
143
+ if project_root is None:
144
+ console.print("[red]Error: Project root not found[/red]")
145
+ raise typer.Exit(1)
146
+ tracker = TemplateInstallationTracker.for_project(project_root)
147
+ else:
148
+ tracker = TemplateInstallationTracker.for_global()
149
+
150
+ installed_count = 0
151
+ skipped_count = 0
152
+ failed_count = 0
153
+
154
+ for template in manifest.templates:
155
+ template_display_name = f"{namespace}.{template.name}"
156
+
157
+ try:
158
+ console.print(f"Installing [cyan]{template_display_name}[/cyan]...", end=" ")
159
+
160
+ # Get template file
161
+ template_file = template.files[0] # Use first file for now
162
+ source_file = repo_path / template_file.path
163
+ content = source_file.read_text(encoding="utf-8")
164
+
165
+ # Calculate checksum
166
+ checksum = sha256_string(content)
167
+
168
+ # Install for each detected IDE
169
+ for ide_type in detected_tools:
170
+ # Get IDE-specific paths
171
+ if installation_scope == InstallationScope.PROJECT:
172
+ tool = detector.get_tool_by_type(ide_type)
173
+ if tool is None or project_root is None:
174
+ continue
175
+
176
+ ide_base_path = tool.get_project_instructions_directory(project_root)
177
+ extension = tool.get_instruction_file_extension().lstrip(".")
178
+ else:
179
+ # Global installation
180
+ global_base = Path.home() / ".instructionkit" / "global-templates" / ide_type.value
181
+ global_base.mkdir(parents=True, exist_ok=True)
182
+ ide_base_path = global_base
183
+ extension = "md" # Default
184
+
185
+ # Get install path with namespace
186
+ install_path = get_install_path(namespace, template.name, ide_base_path, extension)
187
+
188
+ # Check for conflicts
189
+ if install_path.exists() and not force:
190
+ console.print("[yellow]⚠️ (already exists, skipping)[/yellow]")
191
+ skipped_count += 1
192
+ continue
193
+
194
+ # Create parent directory
195
+ install_path.parent.mkdir(parents=True, exist_ok=True)
196
+
197
+ # Write template file
198
+ install_path.write_text(content, encoding="utf-8")
199
+
200
+ # Create installation record
201
+ record = TemplateInstallationRecord(
202
+ id=str(uuid.uuid4()),
203
+ template_name=template.name,
204
+ source_repo=manifest.name,
205
+ source_version=manifest.version,
206
+ namespace=namespace,
207
+ installed_path=str(install_path),
208
+ scope=installation_scope,
209
+ installed_at=datetime.now(),
210
+ checksum=checksum,
211
+ ide_type=ide_type,
212
+ )
213
+
214
+ tracker.add_installation(record)
215
+
216
+ console.print("[green]✓[/green]")
217
+ installed_count += 1
218
+
219
+ except Exception as e:
220
+ console.print(f"[red]✗ {e}[/red]")
221
+ failed_count += 1
222
+
223
+ # Display summary
224
+ console.print()
225
+ table = Table(title="Installation Summary")
226
+ table.add_column("Status", style="cyan")
227
+ table.add_column("Count", style="magenta")
228
+ table.add_column("Templates", style="green")
229
+
230
+ if installed_count > 0:
231
+ template_names = ", ".join([t.name for t in manifest.templates[:3]])
232
+ if len(manifest.templates) > 3:
233
+ template_names += f", ... ({len(manifest.templates)} total)"
234
+ table.add_row("✓ Installed", str(installed_count), template_names)
235
+
236
+ if skipped_count > 0:
237
+ table.add_row("⊘ Skipped", str(skipped_count), "(already exists)")
238
+
239
+ if failed_count > 0:
240
+ table.add_row("✗ Failed", str(failed_count), "(see errors above)")
241
+
242
+ console.print(table)
243
+
244
+ # Show available commands
245
+ if installed_count > 0:
246
+ console.print("\n[bold]Commands available:[/bold]")
247
+ for template in manifest.templates[:5]:
248
+ console.print(f" /{namespace}.{template.name}")
249
+ if len(manifest.templates) > 5:
250
+ console.print(f" ... and {len(manifest.templates) - 5} more")
251
+
252
+ console.print("\n[green]✓ Installation complete[/green]")
253
+
254
+ except typer.Exit:
255
+ raise
256
+ except KeyboardInterrupt:
257
+ console.print("\n[yellow]Installation cancelled by user[/yellow]")
258
+ raise typer.Exit(130)
259
+ except Exception as e:
260
+ console.print(f"\n[red]Unexpected error: {e}[/red]")
261
+ raise typer.Exit(1)
@@ -0,0 +1,172 @@
1
+ """Template list command."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from aiconfigkit.storage.template_tracker import TemplateInstallationTracker
10
+ from aiconfigkit.utils.project import find_project_root
11
+
12
+ console = Console()
13
+
14
+
15
+ def list_command(
16
+ scope: str = typer.Option(
17
+ "all",
18
+ "--scope",
19
+ "-s",
20
+ help="Which installations to list (project, global, all)",
21
+ ),
22
+ repo: Optional[str] = typer.Option(
23
+ None,
24
+ "--repo",
25
+ "-r",
26
+ help="Filter by repository name",
27
+ ),
28
+ format_type: str = typer.Option(
29
+ "table",
30
+ "--format",
31
+ "-f",
32
+ help="Output format (table, json, simple)",
33
+ ),
34
+ verbose: bool = typer.Option(
35
+ False,
36
+ "--verbose",
37
+ "-v",
38
+ help="Show detailed information",
39
+ ),
40
+ ) -> None:
41
+ """
42
+ List installed templates.
43
+
44
+ Example:
45
+ inskit template list
46
+ inskit template list --scope project
47
+ inskit template list --repo acme-templates
48
+ inskit template list --format json
49
+ """
50
+ try:
51
+ # Validate scope
52
+ if scope not in ["project", "global", "all"]:
53
+ console.print(f"[red]Error: Invalid scope '{scope}'. Must be 'project', 'global', or 'all'[/red]")
54
+ raise typer.Exit(1)
55
+
56
+ # Validate format
57
+ if format_type not in ["table", "json", "simple"]:
58
+ console.print(f"[red]Error: Invalid format '{format_type}'. Must be 'table', 'json', or 'simple'[/red]")
59
+ raise typer.Exit(1)
60
+
61
+ # Load installation records
62
+ project_records: list = []
63
+ global_records: list = []
64
+
65
+ if scope in ["project", "all"]:
66
+ try:
67
+ project_root = find_project_root()
68
+ if project_root:
69
+ tracker = TemplateInstallationTracker.for_project(project_root)
70
+ project_records = tracker.load_installation_records()
71
+ except Exception:
72
+ if scope == "project":
73
+ console.print("[yellow]⚠️ Not in a project directory[/yellow]")
74
+ raise typer.Exit(1)
75
+
76
+ if scope in ["global", "all"]:
77
+ tracker = TemplateInstallationTracker.for_global()
78
+ global_records = tracker.load_installation_records()
79
+
80
+ # Combine records
81
+ all_records = []
82
+ if project_records:
83
+ all_records.extend(project_records)
84
+ if global_records:
85
+ all_records.extend(global_records)
86
+
87
+ # Filter by repository if specified
88
+ if repo:
89
+ all_records = [r for r in all_records if r.source_repo == repo or r.namespace == repo]
90
+
91
+ # Check if empty
92
+ if not all_records:
93
+ if repo:
94
+ console.print(f"[yellow]No templates installed from repository '{repo}'[/yellow]")
95
+ else:
96
+ console.print("[yellow]No templates installed.[/yellow]")
97
+ console.print("\nTo install templates:")
98
+ console.print(" inskit template install <repo-url>")
99
+ raise typer.Exit(0)
100
+
101
+ # Output based on format
102
+ if format_type == "json":
103
+
104
+ output = {
105
+ "installations": [r.to_dict() for r in all_records],
106
+ "count": len(all_records),
107
+ "repositories": len(set(r.source_repo for r in all_records)),
108
+ }
109
+ console.print_json(data=output)
110
+
111
+ elif format_type == "simple":
112
+ for record in all_records:
113
+ console.print(f"{record.namespace}.{record.template_name}")
114
+
115
+ else: # table format
116
+ # Group by repository
117
+ repos: dict = {}
118
+ for record in all_records:
119
+ repo_key = record.source_repo
120
+ if repo_key not in repos:
121
+ repos[repo_key] = {
122
+ "version": record.source_version,
123
+ "namespace": record.namespace,
124
+ "records": [],
125
+ }
126
+ repos[repo_key]["records"].append(record)
127
+
128
+ # Display each repository
129
+ for repo_name, repo_data in repos.items():
130
+ console.print(f"\n[bold]Repository: {repo_name}[/bold] (v{repo_data['version']})")
131
+ console.print(f"[dim]Namespace: {repo_data['namespace']}[/dim]\n")
132
+
133
+ table = Table()
134
+ table.add_column("Template", style="cyan")
135
+ table.add_column("IDE", style="green")
136
+ table.add_column("Scope", style="yellow")
137
+ table.add_column("Installed", style="magenta")
138
+
139
+ if verbose:
140
+ table.add_column("Path", style="dim")
141
+ table.add_column("Checksum", style="dim")
142
+
143
+ for record in repo_data["records"]:
144
+ installed_date = record.installed_at.strftime("%Y-%m-%d")
145
+ row = [
146
+ record.template_name,
147
+ record.ide_type.value,
148
+ record.scope.value,
149
+ installed_date,
150
+ ]
151
+
152
+ if verbose:
153
+ row.append(str(record.installed_path))
154
+ row.append(record.checksum[:8] + "...")
155
+
156
+ table.add_row(*row)
157
+
158
+ console.print(table)
159
+
160
+ # Summary
161
+ total_repos = len(repos)
162
+ total_templates = len(all_records)
163
+ console.print(f"\n[bold]Total:[/bold] {total_templates} templates from {total_repos} repository(ies)")
164
+
165
+ except typer.Exit:
166
+ raise
167
+ except KeyboardInterrupt:
168
+ console.print("\n[yellow]Cancelled by user[/yellow]")
169
+ raise typer.Exit(130)
170
+ except Exception as e:
171
+ console.print(f"\n[red]Error: {e}[/red]")
172
+ raise typer.Exit(1)
@@ -0,0 +1,146 @@
1
+ """Template uninstall command."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.prompt import Confirm
9
+
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
+ def uninstall_command(
18
+ repo_name: str = typer.Argument(..., help="Repository name or namespace to uninstall"),
19
+ scope: str = typer.Option(
20
+ "project",
21
+ "--scope",
22
+ "-s",
23
+ help="Which installation to remove (project or global)",
24
+ ),
25
+ template: Optional[str] = typer.Option(
26
+ None,
27
+ "--template",
28
+ "-t",
29
+ help="Uninstall specific template (not entire repository)",
30
+ ),
31
+ force: bool = typer.Option(
32
+ False,
33
+ "--force",
34
+ "-f",
35
+ help="Skip confirmation prompt",
36
+ ),
37
+ keep_files: bool = typer.Option(
38
+ False,
39
+ "--keep-files",
40
+ "-k",
41
+ help="Remove from tracking but keep files on disk",
42
+ ),
43
+ ) -> None:
44
+ """
45
+ Remove installed templates.
46
+
47
+ Example:
48
+ inskit template uninstall acme-templates
49
+ inskit template uninstall acme-templates --force
50
+ inskit template uninstall acme-templates --template test-command
51
+ inskit template uninstall acme-templates --keep-files
52
+ """
53
+ try:
54
+ # Validate scope
55
+ if scope not in ["project", "global"]:
56
+ console.print(f"[red]Error: Invalid scope '{scope}'. Must be 'project' or 'global'[/red]")
57
+ raise typer.Exit(1)
58
+
59
+ # Get tracker
60
+ if scope == "project":
61
+ try:
62
+ project_root = find_project_root()
63
+ if not project_root:
64
+ console.print("[red]Error: Not in a project directory[/red]")
65
+ raise typer.Exit(1)
66
+ except Exception:
67
+ console.print("[red]Error: Not in a project directory[/red]")
68
+ raise typer.Exit(1)
69
+ tracker = TemplateInstallationTracker.for_project(project_root)
70
+ else:
71
+ tracker = TemplateInstallationTracker.for_global()
72
+
73
+ # Load records
74
+ all_records = tracker.load_installation_records()
75
+
76
+ # Filter by repository name or namespace
77
+ repo_records = [r for r in all_records if r.source_repo == repo_name or r.namespace == repo_name]
78
+
79
+ if not repo_records:
80
+ console.print(f"[red]Error: Repository '{repo_name}' not found in {scope} installations[/red]")
81
+ console.print("\nInstalled repositories:")
82
+ repos = set(f"{r.source_repo} ({r.namespace})" for r in all_records)
83
+ for repo in sorted(repos):
84
+ console.print(f" - {repo}")
85
+ raise typer.Exit(1)
86
+
87
+ # Filter by specific template if requested
88
+ if template:
89
+ repo_records = [r for r in repo_records if r.template_name == template]
90
+ if not repo_records:
91
+ console.print(f"[red]Error: Template '{template}' not found in repository '{repo_name}'[/red]")
92
+ raise typer.Exit(1)
93
+
94
+ # Show what will be removed
95
+ console.print("\n[bold]The following templates will be removed:[/bold]")
96
+ for record in repo_records:
97
+ console.print(f" - {record.namespace}.{record.template_name} ({record.ide_type.value})")
98
+
99
+ # Confirm unless --force
100
+ if not force:
101
+ confirm_msg = f"Remove {len(repo_records)} template(s) from {repo_name}?"
102
+ if not Confirm.ask(confirm_msg, default=False):
103
+ console.print("[yellow]Uninstall cancelled[/yellow]")
104
+ raise typer.Exit(0)
105
+
106
+ # Remove templates
107
+ removed_count = 0
108
+ for record in repo_records:
109
+ console.print(f"Removing [cyan]{record.namespace}.{record.template_name}[/cyan]...", end=" ")
110
+
111
+ # Delete file if not keeping
112
+ if not keep_files:
113
+ try:
114
+ file_path = Path(record.installed_path)
115
+ if file_path.exists():
116
+ file_path.unlink()
117
+ except Exception as e:
118
+ console.print(f"[yellow]⚠️ (failed to delete file: {e})[/yellow]")
119
+ continue
120
+
121
+ # Remove from tracking
122
+ tracker.remove_installation(record.id)
123
+ removed_count += 1
124
+ console.print("[green]✓[/green]")
125
+
126
+ # Clean up library if removing entire repository and no templates remain
127
+ if not template:
128
+ remaining = tracker.get_installations_by_namespace(repo_records[0].namespace)
129
+ if not remaining:
130
+ try:
131
+ library_manager = TemplateLibraryManager()
132
+ library_manager.remove_repository(repo_records[0].namespace)
133
+ console.print(f"\n[dim]Removed repository from library: {repo_records[0].namespace}[/dim]")
134
+ except Exception:
135
+ pass # Library removal is optional
136
+
137
+ console.print(f"\n[green]✓ Uninstalled {removed_count} template(s)[/green]")
138
+
139
+ except typer.Exit:
140
+ raise
141
+ except KeyboardInterrupt:
142
+ console.print("\n[yellow]Cancelled by user[/yellow]")
143
+ raise typer.Exit(130)
144
+ except Exception as e:
145
+ console.print(f"\n[red]Error: {e}[/red]")
146
+ raise typer.Exit(1)