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,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)
|