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,274 @@
|
|
|
1
|
+
"""Download command for fetching instructions into the library."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
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
|
+
|
|
12
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
13
|
+
from aiconfigkit.core.git_operations import GitOperations, RepositoryOperationError
|
|
14
|
+
from aiconfigkit.core.models import LibraryInstruction
|
|
15
|
+
from aiconfigkit.core.repository import RepositoryParser
|
|
16
|
+
from aiconfigkit.storage.library import LibraryManager
|
|
17
|
+
from aiconfigkit.utils.ui import print_error, print_success
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
app = typer.Typer()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def download_instructions(
|
|
25
|
+
repo: str,
|
|
26
|
+
force: bool = False,
|
|
27
|
+
alias: Optional[str] = None,
|
|
28
|
+
ref: Optional[str] = None,
|
|
29
|
+
) -> int:
|
|
30
|
+
"""
|
|
31
|
+
Download instructions from a repository into the local library.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
repo: Repository URL or local path
|
|
35
|
+
force: If True, re-download even if already in library
|
|
36
|
+
alias: User-friendly alias for this source (auto-generated if not provided)
|
|
37
|
+
ref: Git reference (tag, branch, or commit) to download
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Exit code (0 = success)
|
|
41
|
+
"""
|
|
42
|
+
library = LibraryManager()
|
|
43
|
+
|
|
44
|
+
# Display what we're downloading
|
|
45
|
+
if ref:
|
|
46
|
+
console.print(f"\n[bold]Downloading from:[/bold] {repo} [bold cyan]@{ref}[/bold cyan]\n")
|
|
47
|
+
else:
|
|
48
|
+
console.print(f"\n[bold]Downloading from:[/bold] {repo}\n")
|
|
49
|
+
|
|
50
|
+
# Determine if local or remote
|
|
51
|
+
is_local = GitOperations.is_local_path(repo)
|
|
52
|
+
temp_repo_path = None # Track temp directory for cleanup
|
|
53
|
+
ref_type = None # Track the reference type
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Validate and detect ref type for remote repositories
|
|
57
|
+
if ref and not is_local:
|
|
58
|
+
try:
|
|
59
|
+
validated_ref, ref_type = GitOperations.detect_ref_type(repo, ref)
|
|
60
|
+
ref = validated_ref # Use the validated reference
|
|
61
|
+
except RepositoryOperationError as e:
|
|
62
|
+
if e.error_type == "invalid_reference":
|
|
63
|
+
print_error(f"Invalid reference '{ref}': not found in repository")
|
|
64
|
+
return 1
|
|
65
|
+
elif e.error_type == "network_error":
|
|
66
|
+
print_error("Network error: unable to access repository")
|
|
67
|
+
return 1
|
|
68
|
+
else:
|
|
69
|
+
print_error(f"Failed to validate reference: {e}")
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
with Progress(
|
|
73
|
+
SpinnerColumn(),
|
|
74
|
+
TextColumn("[progress.description]{task.description}"),
|
|
75
|
+
console=console,
|
|
76
|
+
) as progress:
|
|
77
|
+
if is_local:
|
|
78
|
+
if ref:
|
|
79
|
+
print_error("Version references (--ref) are not supported for local repositories")
|
|
80
|
+
return 1
|
|
81
|
+
progress.add_task("Loading local repository...", total=None)
|
|
82
|
+
repo_path = Path(repo).resolve()
|
|
83
|
+
else:
|
|
84
|
+
if ref:
|
|
85
|
+
task = progress.add_task(f"Cloning repository at {ref}...", total=None)
|
|
86
|
+
else:
|
|
87
|
+
task = progress.add_task("Cloning repository...", total=None)
|
|
88
|
+
|
|
89
|
+
# Use new clone_at_ref for versioned cloning
|
|
90
|
+
import tempfile
|
|
91
|
+
from pathlib import Path as PathlibPath
|
|
92
|
+
|
|
93
|
+
temp_dir = PathlibPath(tempfile.mkdtemp(prefix="instructionkit-"))
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
GitOperations.clone_at_ref(repo, temp_dir, ref, ref_type)
|
|
97
|
+
repo_path = temp_dir
|
|
98
|
+
temp_repo_path = repo_path # Save for cleanup
|
|
99
|
+
except RepositoryOperationError as e:
|
|
100
|
+
if temp_dir.exists():
|
|
101
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
102
|
+
print_error(f"Failed to clone repository: {e}")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
progress.update(task, completed=True)
|
|
106
|
+
|
|
107
|
+
# Parse repository
|
|
108
|
+
task = progress.add_task("Parsing repository metadata...", total=None)
|
|
109
|
+
parser = RepositoryParser(repo_path)
|
|
110
|
+
repository = parser.parse()
|
|
111
|
+
repository.url = repo
|
|
112
|
+
progress.update(task, completed=True)
|
|
113
|
+
|
|
114
|
+
# Generate versioned namespace if ref is specified
|
|
115
|
+
repo_name = repository.metadata.get("name", "Unknown")
|
|
116
|
+
if ref:
|
|
117
|
+
repo_namespace = library.get_versioned_namespace(repo, ref)
|
|
118
|
+
else:
|
|
119
|
+
repo_namespace = library.get_repo_namespace(repo, repo_name)
|
|
120
|
+
|
|
121
|
+
# Check if this specific version already exists
|
|
122
|
+
existing_repo = library.get_repository(repo_namespace)
|
|
123
|
+
|
|
124
|
+
if existing_repo and not force:
|
|
125
|
+
if ref:
|
|
126
|
+
print_error(
|
|
127
|
+
f"Version '{ref}' of '{repo_name}' already exists in library.\n" f"Use --force to re-download."
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
print_error(
|
|
131
|
+
f"Source '{existing_repo.alias or repo_name}' already exists in library.\n"
|
|
132
|
+
f"Use --force to re-download."
|
|
133
|
+
)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
# Prepare library instructions
|
|
137
|
+
library_instructions = []
|
|
138
|
+
library_repo_dir = library.library_dir / repo_namespace
|
|
139
|
+
repo_dir = library_repo_dir / "instructions"
|
|
140
|
+
repo_dir.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
console.print("\n[bold]Copying instructions to library...[/bold]\n")
|
|
143
|
+
|
|
144
|
+
for instruction in repository.instructions:
|
|
145
|
+
# Copy instruction file to library
|
|
146
|
+
source_file = repo_path / instruction.file_path
|
|
147
|
+
if not source_file.exists():
|
|
148
|
+
print_error(f"Warning: File not found: {instruction.file_path}")
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
dest_file = repo_dir / f"{instruction.name}.md"
|
|
152
|
+
shutil.copy2(source_file, dest_file)
|
|
153
|
+
|
|
154
|
+
# Calculate checksum
|
|
155
|
+
checksum = calculate_file_checksum(str(dest_file))
|
|
156
|
+
|
|
157
|
+
# Create library instruction
|
|
158
|
+
lib_inst = LibraryInstruction(
|
|
159
|
+
id=f"{repo_namespace}/{instruction.name}",
|
|
160
|
+
name=instruction.name,
|
|
161
|
+
description=instruction.description,
|
|
162
|
+
repo_namespace=repo_namespace,
|
|
163
|
+
repo_url=repo,
|
|
164
|
+
repo_name=repo_name,
|
|
165
|
+
author=repository.metadata.get("author", "Unknown"),
|
|
166
|
+
version=repository.metadata.get("version", "1.0.0"),
|
|
167
|
+
file_path=str(dest_file),
|
|
168
|
+
tags=instruction.tags,
|
|
169
|
+
downloaded_at=datetime.now(),
|
|
170
|
+
checksum=checksum,
|
|
171
|
+
)
|
|
172
|
+
library_instructions.append(lib_inst)
|
|
173
|
+
|
|
174
|
+
console.print(f" ✓ {instruction.name}")
|
|
175
|
+
|
|
176
|
+
# Preserve .git directory for Git sources to enable updates
|
|
177
|
+
if not is_local:
|
|
178
|
+
git_dir = repo_path / ".git"
|
|
179
|
+
if git_dir.exists():
|
|
180
|
+
dest_git_dir = library_repo_dir / ".git"
|
|
181
|
+
if dest_git_dir.exists():
|
|
182
|
+
shutil.rmtree(dest_git_dir)
|
|
183
|
+
shutil.copytree(git_dir, dest_git_dir)
|
|
184
|
+
|
|
185
|
+
# Add to library
|
|
186
|
+
library_repo = library.add_repository(
|
|
187
|
+
repo_name=repo_name,
|
|
188
|
+
repo_description=repository.metadata.get("description", ""),
|
|
189
|
+
repo_url=repo,
|
|
190
|
+
repo_author=repository.metadata.get("author", "Unknown"),
|
|
191
|
+
repo_version=repository.metadata.get("version", "1.0.0"),
|
|
192
|
+
instructions=library_instructions,
|
|
193
|
+
alias=alias,
|
|
194
|
+
namespace=repo_namespace,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Build success message
|
|
198
|
+
success_msg = f"\n✓ Downloaded {len(library_instructions)} instruction(s) from '{repo_name}'"
|
|
199
|
+
if ref and ref_type:
|
|
200
|
+
ref_type_badge = {"tag": "📌", "branch": "🌿", "commit": "📍"}.get(ref_type.value, "")
|
|
201
|
+
success_msg += f" {ref_type_badge} {ref}"
|
|
202
|
+
success_msg += f"\n Alias: {library_repo.alias}\n" f" Namespace: {repo_namespace}\n"
|
|
203
|
+
if ref:
|
|
204
|
+
ref_labels = {"tag": "Tag", "branch": "Branch", "commit": "Commit"}
|
|
205
|
+
ref_label = ref_labels.get(ref_type.value if ref_type else "", "Ref")
|
|
206
|
+
success_msg += f" {ref_label}: {ref}\n"
|
|
207
|
+
success_msg += (
|
|
208
|
+
" Use 'inskit list library' to see all downloaded instructions\n"
|
|
209
|
+
" Use 'inskit install' to install into your AI tools"
|
|
210
|
+
)
|
|
211
|
+
print_success(success_msg)
|
|
212
|
+
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
except FileNotFoundError as e:
|
|
216
|
+
print_error(f"Repository metadata file not found: {e}")
|
|
217
|
+
return 1
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print_error(f"Failed to download: {e}")
|
|
220
|
+
return 1
|
|
221
|
+
finally:
|
|
222
|
+
# Clean up temp directory if not local
|
|
223
|
+
if temp_repo_path and not is_local:
|
|
224
|
+
GitOperations.cleanup_repository(temp_repo_path, is_temp=True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.command(name="download")
|
|
228
|
+
def download_command(
|
|
229
|
+
repo: str = typer.Option(
|
|
230
|
+
...,
|
|
231
|
+
"--repo",
|
|
232
|
+
"-r",
|
|
233
|
+
help="Repository URL or local path to download from",
|
|
234
|
+
),
|
|
235
|
+
ref: Optional[str] = typer.Option(
|
|
236
|
+
None,
|
|
237
|
+
"--ref",
|
|
238
|
+
help="Git reference (tag, branch, or commit) to download",
|
|
239
|
+
),
|
|
240
|
+
force: bool = typer.Option(
|
|
241
|
+
False,
|
|
242
|
+
"--force",
|
|
243
|
+
"-f",
|
|
244
|
+
help="Re-download even if already in library",
|
|
245
|
+
),
|
|
246
|
+
) -> None:
|
|
247
|
+
"""
|
|
248
|
+
Download instructions from a repository into your local library.
|
|
249
|
+
|
|
250
|
+
This downloads and caches instructions locally without installing them.
|
|
251
|
+
After downloading, use 'instructionkit install' to select and install
|
|
252
|
+
instructions into your AI coding tools.
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
# Download from GitHub (default branch)
|
|
256
|
+
instructionkit download --repo https://github.com/company/instructions
|
|
257
|
+
|
|
258
|
+
# Download specific tag version
|
|
259
|
+
instructionkit download --repo https://github.com/company/instructions --ref v1.0.0
|
|
260
|
+
|
|
261
|
+
# Download from specific branch
|
|
262
|
+
instructionkit download --repo https://github.com/company/instructions --ref main
|
|
263
|
+
|
|
264
|
+
# Download from specific commit
|
|
265
|
+
instructionkit download --repo https://github.com/company/instructions --ref abc123def
|
|
266
|
+
|
|
267
|
+
# Download from local folder (no --ref support)
|
|
268
|
+
instructionkit download --repo ./my-instructions
|
|
269
|
+
|
|
270
|
+
# Force re-download
|
|
271
|
+
instructionkit download --repo https://github.com/company/instructions --force
|
|
272
|
+
"""
|
|
273
|
+
exit_code = download_instructions(repo=repo, ref=ref, force=force)
|
|
274
|
+
raise typer.Exit(code=exit_code)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Install command implementation."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
|
|
10
|
+
from aiconfigkit.ai_tools.base import AITool
|
|
11
|
+
from aiconfigkit.ai_tools.detector import get_detector
|
|
12
|
+
from aiconfigkit.core.checksum import ChecksumValidator
|
|
13
|
+
from aiconfigkit.core.conflict_resolution import (
|
|
14
|
+
ConflictResolver,
|
|
15
|
+
)
|
|
16
|
+
from aiconfigkit.core.git_operations import GitOperations
|
|
17
|
+
from aiconfigkit.core.models import (
|
|
18
|
+
ConflictResolution,
|
|
19
|
+
InstallationRecord,
|
|
20
|
+
InstallationScope,
|
|
21
|
+
)
|
|
22
|
+
from aiconfigkit.core.repository import RepositoryParser
|
|
23
|
+
from aiconfigkit.storage.tracker import InstallationTracker
|
|
24
|
+
from aiconfigkit.utils.project import find_project_root
|
|
25
|
+
from aiconfigkit.utils.validation import is_valid_git_url, normalize_repo_url
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def install_instruction(
|
|
31
|
+
name: str,
|
|
32
|
+
repo: str,
|
|
33
|
+
tool: Optional[str] = None,
|
|
34
|
+
conflict_strategy: str = "skip",
|
|
35
|
+
bundle: bool = False,
|
|
36
|
+
) -> int:
|
|
37
|
+
"""
|
|
38
|
+
Install an instruction from a Git repository.
|
|
39
|
+
|
|
40
|
+
All installations are at project level.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: Instruction or bundle name to install
|
|
44
|
+
repo: Git repository URL
|
|
45
|
+
tool: AI tool to install to (cursor, copilot, etc.)
|
|
46
|
+
conflict_strategy: How to handle conflicts (skip, rename, overwrite)
|
|
47
|
+
bundle: Whether this is a bundle installation
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Exit code (0 for success, 1 for error)
|
|
51
|
+
"""
|
|
52
|
+
# Validate repository URL
|
|
53
|
+
if not is_valid_git_url(repo):
|
|
54
|
+
console.print(f"[red]Error:[/red] Invalid Git repository URL: {repo}")
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
# Parse conflict strategy
|
|
58
|
+
try:
|
|
59
|
+
strategy = ConflictResolution(conflict_strategy.lower())
|
|
60
|
+
except ValueError:
|
|
61
|
+
console.print(
|
|
62
|
+
f"[red]Error:[/red] Invalid conflict strategy: {conflict_strategy}. "
|
|
63
|
+
f"Must be 'skip', 'rename', or 'overwrite'."
|
|
64
|
+
)
|
|
65
|
+
return 1
|
|
66
|
+
|
|
67
|
+
# Always use project scope
|
|
68
|
+
install_scope = InstallationScope.PROJECT
|
|
69
|
+
|
|
70
|
+
# Detect project root
|
|
71
|
+
project_root = find_project_root()
|
|
72
|
+
if project_root is None:
|
|
73
|
+
console.print(
|
|
74
|
+
"[red]Error:[/red] Could not detect project root. "
|
|
75
|
+
"Make sure you're running this command from within a project directory."
|
|
76
|
+
)
|
|
77
|
+
return 1
|
|
78
|
+
console.print(f"Detected project root: [cyan]{project_root}[/cyan]")
|
|
79
|
+
|
|
80
|
+
# Check Git is installed
|
|
81
|
+
if not GitOperations.is_git_installed():
|
|
82
|
+
console.print("[red]Error:[/red] Git is not installed. " "Please install Git and try again.")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
# Determine AI tool
|
|
86
|
+
ai_tool = _get_ai_tool(tool)
|
|
87
|
+
if ai_tool is None:
|
|
88
|
+
console.print("[red]Error:[/red] Could not determine AI coding tool. " "Please specify with --tool flag.")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
# Validate AI tool
|
|
92
|
+
validation_error = ai_tool.validate_installation()
|
|
93
|
+
if validation_error:
|
|
94
|
+
console.print(f"[red]Error:[/red] {validation_error}")
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
console.print(f"Installing to [cyan]{ai_tool.tool_name}[/cyan]...")
|
|
98
|
+
|
|
99
|
+
# Clone repository or use local path
|
|
100
|
+
git_ops = GitOperations()
|
|
101
|
+
is_local = git_ops.is_local_path(repo)
|
|
102
|
+
|
|
103
|
+
if is_local:
|
|
104
|
+
# Use local directory directly
|
|
105
|
+
try:
|
|
106
|
+
repo_path = git_ops.clone_repository(repo)
|
|
107
|
+
console.print(f"Using local directory: [cyan]{repo_path}[/cyan]")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
console.print(f"[red]Error:[/red] Failed to access local directory: {e}")
|
|
110
|
+
return 1
|
|
111
|
+
else:
|
|
112
|
+
# Clone remote repository
|
|
113
|
+
with Progress(
|
|
114
|
+
SpinnerColumn(),
|
|
115
|
+
TextColumn("[progress.description]{task.description}"),
|
|
116
|
+
console=console,
|
|
117
|
+
) as progress:
|
|
118
|
+
progress.add_task(description="Cloning repository...", total=None)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
repo_path = git_ops.clone_repository(repo)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"[red]Error:[/red] Failed to clone repository: {e}")
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Parse repository
|
|
128
|
+
parser = RepositoryParser(repo_path)
|
|
129
|
+
repository = parser.parse()
|
|
130
|
+
repository.url = normalize_repo_url(repo)
|
|
131
|
+
|
|
132
|
+
# Get instructions to install
|
|
133
|
+
if bundle:
|
|
134
|
+
instructions = parser.get_instructions_for_bundle(name)
|
|
135
|
+
console.print(f"Installing bundle '[cyan]{name}[/cyan]' " f"with {len(instructions)} instruction(s)...")
|
|
136
|
+
else:
|
|
137
|
+
instruction = parser.get_instruction_by_name(name)
|
|
138
|
+
if not instruction:
|
|
139
|
+
console.print(f"[red]Error:[/red] Instruction '{name}' not found in repository")
|
|
140
|
+
return 1
|
|
141
|
+
instructions = [instruction]
|
|
142
|
+
|
|
143
|
+
# Install instructions
|
|
144
|
+
tracker = InstallationTracker()
|
|
145
|
+
resolver = ConflictResolver(default_strategy=strategy)
|
|
146
|
+
checksum_validator = ChecksumValidator()
|
|
147
|
+
|
|
148
|
+
installed_count = 0
|
|
149
|
+
skipped_count = 0
|
|
150
|
+
|
|
151
|
+
for instruction in instructions:
|
|
152
|
+
# Check if already exists
|
|
153
|
+
target_path = ai_tool.get_instruction_path(instruction.name, install_scope, project_root)
|
|
154
|
+
|
|
155
|
+
if target_path.exists():
|
|
156
|
+
# Handle conflict
|
|
157
|
+
if strategy == ConflictResolution.SKIP:
|
|
158
|
+
console.print(f" [yellow]Skipped:[/yellow] {instruction.name} (already exists)")
|
|
159
|
+
skipped_count += 1
|
|
160
|
+
continue
|
|
161
|
+
elif strategy == ConflictResolution.RENAME:
|
|
162
|
+
conflict_info = resolver.resolve(instruction.name, target_path, strategy)
|
|
163
|
+
if conflict_info.new_path is None:
|
|
164
|
+
console.print(f" [red]Error:[/red] Failed to rename {instruction.name}")
|
|
165
|
+
continue
|
|
166
|
+
target_path = Path(conflict_info.new_path)
|
|
167
|
+
console.print(f" [yellow]Renamed:[/yellow] {instruction.name} -> " f"{target_path.name}")
|
|
168
|
+
elif strategy == ConflictResolution.OVERWRITE:
|
|
169
|
+
console.print(f" [yellow]Overwriting:[/yellow] {instruction.name}")
|
|
170
|
+
|
|
171
|
+
# Validate checksum
|
|
172
|
+
try:
|
|
173
|
+
checksum_validator.validate(instruction.content, instruction.checksum)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
console.print(f" [red]Error:[/red] {instruction.name}: {e}")
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Install instruction
|
|
179
|
+
try:
|
|
180
|
+
# Write file
|
|
181
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
target_path.write_text(instruction.content, encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
# Track installation
|
|
185
|
+
record = InstallationRecord(
|
|
186
|
+
instruction_name=instruction.name,
|
|
187
|
+
ai_tool=ai_tool.tool_type,
|
|
188
|
+
source_repo=repository.url,
|
|
189
|
+
installed_path=str(target_path),
|
|
190
|
+
installed_at=datetime.now(),
|
|
191
|
+
checksum=instruction.checksum,
|
|
192
|
+
bundle_name=name if bundle else None,
|
|
193
|
+
scope=install_scope,
|
|
194
|
+
)
|
|
195
|
+
tracker.add_installation(record, project_root)
|
|
196
|
+
|
|
197
|
+
console.print(f" [green]✓[/green] Installed: {instruction.name}")
|
|
198
|
+
installed_count += 1
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
console.print(f" [red]Error:[/red] {instruction.name}: {e}")
|
|
202
|
+
|
|
203
|
+
# Summary
|
|
204
|
+
console.print()
|
|
205
|
+
console.print(f"[green]Successfully installed {installed_count} instruction(s)[/green]")
|
|
206
|
+
if skipped_count > 0:
|
|
207
|
+
console.print(f"[yellow]Skipped {skipped_count} existing instruction(s)[/yellow]")
|
|
208
|
+
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
finally:
|
|
212
|
+
# Clean up cloned repository (but not local directories)
|
|
213
|
+
GitOperations.cleanup_repository(repo_path, is_temp=not is_local)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _get_ai_tool(tool_name: Optional[str]) -> Optional[AITool]:
|
|
217
|
+
"""
|
|
218
|
+
Get AI tool instance from name.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
tool_name: Name of AI tool (or None to auto-detect)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
AITool instance or None if not found
|
|
225
|
+
"""
|
|
226
|
+
detector = get_detector()
|
|
227
|
+
|
|
228
|
+
if tool_name:
|
|
229
|
+
# Use specified tool
|
|
230
|
+
tool = detector.get_tool_by_name(tool_name)
|
|
231
|
+
if tool and not tool.is_installed():
|
|
232
|
+
console.print(f"[yellow]Warning:[/yellow] {tool.tool_name} is not installed")
|
|
233
|
+
return None
|
|
234
|
+
return tool
|
|
235
|
+
|
|
236
|
+
# Auto-detect: find first installed tool
|
|
237
|
+
return detector.get_primary_tool()
|