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,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)
|
aiconfigkit/cli/tools.py
ADDED
|
@@ -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
|