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,937 @@
|
|
|
1
|
+
"""Refactored install command with library support."""
|
|
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.prompt import Confirm
|
|
10
|
+
|
|
11
|
+
from aiconfigkit.ai_tools.base import AITool
|
|
12
|
+
from aiconfigkit.ai_tools.detector import AIToolDetector, get_detector
|
|
13
|
+
from aiconfigkit.core.conflict_resolution import ConflictResolver, prompt_conflict_resolution
|
|
14
|
+
from aiconfigkit.core.models import (
|
|
15
|
+
ConflictResolution,
|
|
16
|
+
InstallationRecord,
|
|
17
|
+
InstallationScope,
|
|
18
|
+
LibraryInstruction,
|
|
19
|
+
RefType,
|
|
20
|
+
)
|
|
21
|
+
from aiconfigkit.storage.library import LibraryManager
|
|
22
|
+
from aiconfigkit.storage.tracker import InstallationTracker
|
|
23
|
+
from aiconfigkit.tui.installer import show_installer_tui
|
|
24
|
+
from aiconfigkit.utils.project import find_project_root
|
|
25
|
+
from aiconfigkit.utils.ui import print_error, print_info, print_success
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Helper Functions - Shared Installation Logic
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_ref_from_namespace(namespace: str) -> tuple[Optional[str], Optional[RefType]]:
|
|
36
|
+
"""Extract Git reference from versioned namespace.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
namespace: Repository namespace (e.g., 'github.com_owner_repo@v1.0.0')
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (ref, ref_type) or (None, None) if no version info
|
|
43
|
+
"""
|
|
44
|
+
if "@" not in namespace:
|
|
45
|
+
return (None, None)
|
|
46
|
+
|
|
47
|
+
# Split at @ to get the ref part
|
|
48
|
+
ref = namespace.split("@", 1)[1]
|
|
49
|
+
|
|
50
|
+
# Try to determine ref type from the ref format
|
|
51
|
+
# This is a best-effort detection since we don't have the original ref_type stored
|
|
52
|
+
import re
|
|
53
|
+
|
|
54
|
+
# Tags typically start with 'v' followed by numbers
|
|
55
|
+
if re.match(r"^v?\d+\.\d+", ref):
|
|
56
|
+
return (ref, RefType.TAG)
|
|
57
|
+
# Commit hashes are hex strings
|
|
58
|
+
elif re.match(r"^[0-9a-f]{7,40}$", ref):
|
|
59
|
+
return (ref, RefType.COMMIT)
|
|
60
|
+
# Everything else is likely a branch
|
|
61
|
+
else:
|
|
62
|
+
# Restore slashes that were converted to underscores
|
|
63
|
+
# This is approximate - feature_new might have been feature/new
|
|
64
|
+
return (ref, RefType.BRANCH)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_conflict_strategy(conflict_strategy: str) -> Optional[ConflictResolution]:
|
|
68
|
+
"""Parse and validate conflict resolution strategy.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
conflict_strategy: Strategy string (prompt, skip, rename, overwrite)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
ConflictResolution enum or None if invalid
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return ConflictResolution(conflict_strategy.lower())
|
|
78
|
+
except ValueError:
|
|
79
|
+
print_error(
|
|
80
|
+
f"Invalid conflict strategy: {conflict_strategy}. " "Must be 'prompt', 'skip', 'rename', or 'overwrite'."
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_project_root_for_installation() -> Optional[Path]:
|
|
86
|
+
"""Detect and validate project root for installation.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Project root path or None if not found
|
|
90
|
+
"""
|
|
91
|
+
project_root = find_project_root()
|
|
92
|
+
if not project_root:
|
|
93
|
+
print_error("Could not detect project root. " "Make sure you're running from within a project directory.")
|
|
94
|
+
return None
|
|
95
|
+
console.print(f"Detected project root: [cyan]{project_root}[/cyan]")
|
|
96
|
+
return project_root
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load_instructions_from_library(instruction_ids: list[str], library: LibraryManager) -> Optional[list]:
|
|
100
|
+
"""Load instructions from library by IDs.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
instruction_ids: List of instruction IDs to load
|
|
104
|
+
library: Library manager instance
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of LibraryInstruction objects or None if any not found
|
|
108
|
+
"""
|
|
109
|
+
instructions = []
|
|
110
|
+
for inst_id in instruction_ids:
|
|
111
|
+
inst = library.get_instruction(inst_id)
|
|
112
|
+
if not inst:
|
|
113
|
+
print_error(f"Instruction not found in library: {inst_id}")
|
|
114
|
+
return None
|
|
115
|
+
instructions.append(inst)
|
|
116
|
+
return instructions
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _detect_installed_collisions(
|
|
120
|
+
instructions: list[LibraryInstruction],
|
|
121
|
+
ai_tools: list[AITool],
|
|
122
|
+
install_names: dict[str, str],
|
|
123
|
+
project_root: Optional[Path],
|
|
124
|
+
) -> dict[str, list[InstallationRecord]]:
|
|
125
|
+
"""Detect collisions with already-installed instructions.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
instructions: List of instructions to install
|
|
129
|
+
ai_tools: List of AI tools to install to
|
|
130
|
+
install_names: Mapping of instruction IDs to install names
|
|
131
|
+
project_root: Project root path
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary mapping instruction_id to list of existing installations with same name
|
|
135
|
+
"""
|
|
136
|
+
tracker = InstallationTracker()
|
|
137
|
+
collisions = {}
|
|
138
|
+
|
|
139
|
+
for inst in instructions:
|
|
140
|
+
install_name = install_names[inst.id]
|
|
141
|
+
|
|
142
|
+
# Check if this name is already used in any tool
|
|
143
|
+
existing = tracker.find_instructions_by_name(install_name, project_root)
|
|
144
|
+
|
|
145
|
+
# Filter to only collisions from different repositories
|
|
146
|
+
different_repo_collisions = [e for e in existing if e.source_repo != inst.repo_url]
|
|
147
|
+
|
|
148
|
+
if different_repo_collisions:
|
|
149
|
+
collisions[inst.id] = different_repo_collisions
|
|
150
|
+
|
|
151
|
+
return collisions
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _prompt_for_custom_filename(
|
|
155
|
+
instruction: LibraryInstruction,
|
|
156
|
+
existing_installations: list[InstallationRecord],
|
|
157
|
+
current_name: str,
|
|
158
|
+
) -> Optional[str]:
|
|
159
|
+
"""Prompt user to provide custom filename for collision resolution.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
instruction: Instruction being installed
|
|
163
|
+
existing_installations: List of existing installations with same name
|
|
164
|
+
current_name: Current proposed install name
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Custom filename or None to skip installation
|
|
168
|
+
"""
|
|
169
|
+
console.print(f"\n[yellow]⚠️ Name Collision:[/yellow] '{current_name}' is already installed")
|
|
170
|
+
console.print("\n[bold]Existing installations:[/bold]")
|
|
171
|
+
for existing in existing_installations:
|
|
172
|
+
repo_display = existing.source_repo or "unknown"
|
|
173
|
+
ref_display = existing.source_ref or "unknown"
|
|
174
|
+
console.print(f" • {existing.ai_tool.value}: {repo_display} @ {ref_display}")
|
|
175
|
+
|
|
176
|
+
console.print("\n[bold]New installation:[/bold]")
|
|
177
|
+
console.print(f" • Repository: {instruction.repo_url}")
|
|
178
|
+
console.print(f" • Namespace: {instruction.repo_namespace}")
|
|
179
|
+
|
|
180
|
+
console.print("\n[bold]Options:[/bold]")
|
|
181
|
+
console.print(" [1] Provide custom filename")
|
|
182
|
+
console.print(" [2] Skip this instruction")
|
|
183
|
+
|
|
184
|
+
choice = typer.prompt("Select (1-2)", default="2")
|
|
185
|
+
|
|
186
|
+
if choice == "1":
|
|
187
|
+
custom_name = typer.prompt(
|
|
188
|
+
"Enter custom filename (without extension)",
|
|
189
|
+
default=f"{instruction.repo_namespace.replace('@', '-').replace('/', '-')}_{instruction.name}",
|
|
190
|
+
)
|
|
191
|
+
return str(custom_name)
|
|
192
|
+
else:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _resolve_name_conflicts(instructions: list[LibraryInstruction]) -> Optional[dict[str, str]]:
|
|
197
|
+
"""Resolve naming conflicts for instructions with duplicate names.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
instructions: List of LibraryInstruction objects
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dict mapping instruction ID to install name, or None if cancelled
|
|
204
|
+
"""
|
|
205
|
+
# Check for name conflicts
|
|
206
|
+
name_conflicts: dict[str, list[LibraryInstruction]] = {}
|
|
207
|
+
for inst in instructions:
|
|
208
|
+
if inst.name not in name_conflicts:
|
|
209
|
+
name_conflicts[inst.name] = []
|
|
210
|
+
name_conflicts[inst.name].append(inst)
|
|
211
|
+
|
|
212
|
+
# Handle conflicts
|
|
213
|
+
install_names = {} # Map instruction ID to final install name
|
|
214
|
+
for name, insts in name_conflicts.items():
|
|
215
|
+
if len(insts) > 1:
|
|
216
|
+
console.print(f"\n[yellow]⚠️ Name Conflict:[/yellow] " f"{len(insts)} instructions named '{name}'")
|
|
217
|
+
console.print("\nHow should they be installed?")
|
|
218
|
+
console.print(" [1] Namespace by repository (recommended)")
|
|
219
|
+
for inst in insts:
|
|
220
|
+
console.print(f" → {inst.repo_namespace}/{name}")
|
|
221
|
+
console.print(" [2] Skip installation (cancel)")
|
|
222
|
+
|
|
223
|
+
choice = typer.prompt("Select (1-2)", default="1")
|
|
224
|
+
|
|
225
|
+
if choice == "1":
|
|
226
|
+
# Use namespaced names
|
|
227
|
+
for inst in insts:
|
|
228
|
+
install_names[inst.id] = f"{inst.repo_namespace}/{name}"
|
|
229
|
+
else:
|
|
230
|
+
print_info("Installation cancelled")
|
|
231
|
+
return None
|
|
232
|
+
else:
|
|
233
|
+
# No conflict, use simple name
|
|
234
|
+
install_names[insts[0].id] = name
|
|
235
|
+
|
|
236
|
+
return install_names
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _get_ai_tools_from_names(tool_names: list[str], detector: AIToolDetector) -> Optional[list[AITool]]:
|
|
240
|
+
"""Get AI tool instances from tool names.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
tool_names: List of tool name strings
|
|
244
|
+
detector: AI tool detector instance
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of AITool instances or None if any invalid/not installed
|
|
248
|
+
"""
|
|
249
|
+
ai_tools = []
|
|
250
|
+
for tool_name in tool_names:
|
|
251
|
+
tool = detector.get_tool_by_name(tool_name)
|
|
252
|
+
if not tool:
|
|
253
|
+
print_error(f"AI tool not found: {tool_name}")
|
|
254
|
+
return None
|
|
255
|
+
if not tool.is_installed():
|
|
256
|
+
print_error(f"{tool.tool_name} is not installed")
|
|
257
|
+
return None
|
|
258
|
+
ai_tools.append(tool)
|
|
259
|
+
return ai_tools
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _show_installation_preview(
|
|
263
|
+
project_root: Path, instructions: list, ai_tools: list, install_names: dict[str, str]
|
|
264
|
+
) -> bool:
|
|
265
|
+
"""Show installation preview and ask for confirmation.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
project_root: Project root path
|
|
269
|
+
instructions: List of instructions to install
|
|
270
|
+
ai_tools: List of AI tools to install to
|
|
271
|
+
install_names: Mapping of instruction IDs to install names
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
True if user confirms, False otherwise
|
|
275
|
+
"""
|
|
276
|
+
console.print("\n[bold cyan]📦 Installation Preview[/bold cyan]")
|
|
277
|
+
console.print(f"\n[bold]Project:[/bold] {project_root}")
|
|
278
|
+
console.print(f"[bold]Instructions:[/bold] {len(instructions)} selected")
|
|
279
|
+
console.print(f"[bold]Target tools:[/bold] {', '.join([t.tool_name for t in ai_tools])}\n")
|
|
280
|
+
|
|
281
|
+
# Show where files will be created
|
|
282
|
+
console.print("[bold yellow]The following files will be created:[/bold yellow]\n")
|
|
283
|
+
|
|
284
|
+
for ai_tool in ai_tools:
|
|
285
|
+
tool_dir = ai_tool.get_project_instructions_directory(project_root)
|
|
286
|
+
console.print(f"[cyan]{ai_tool.tool_name}[/cyan] → {tool_dir}")
|
|
287
|
+
for inst in instructions:
|
|
288
|
+
install_name = install_names[inst.id]
|
|
289
|
+
filename = f"{install_name}{ai_tool.get_instruction_file_extension()}"
|
|
290
|
+
console.print(f" • {filename}")
|
|
291
|
+
console.print()
|
|
292
|
+
|
|
293
|
+
# Ask for confirmation
|
|
294
|
+
return Confirm.ask("\n[bold]Proceed with installation?[/bold]", default=True)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _check_for_upgrades(
|
|
298
|
+
instructions: list,
|
|
299
|
+
ai_tools: list,
|
|
300
|
+
install_names: dict[str, str],
|
|
301
|
+
project_root: Optional[Path],
|
|
302
|
+
) -> dict[str, tuple[InstallationRecord, LibraryInstruction]]:
|
|
303
|
+
"""Check if any instructions being installed are upgrades of existing installations.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
instructions: List of instructions to install
|
|
307
|
+
ai_tools: List of AI tools to install to
|
|
308
|
+
install_names: Mapping of instruction IDs to install names
|
|
309
|
+
project_root: Project root path
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dictionary mapping instruction_id to (existing_record, new_instruction) for upgrades
|
|
313
|
+
"""
|
|
314
|
+
tracker = InstallationTracker()
|
|
315
|
+
upgrades = {}
|
|
316
|
+
|
|
317
|
+
for ai_tool in ai_tools:
|
|
318
|
+
for inst in instructions:
|
|
319
|
+
install_name = install_names[inst.id]
|
|
320
|
+
|
|
321
|
+
# Check if this instruction is already installed for this tool
|
|
322
|
+
existing = tracker.get_installation(install_name, ai_tool.tool_type, project_root)
|
|
323
|
+
|
|
324
|
+
if existing:
|
|
325
|
+
# Extract ref from both old and new
|
|
326
|
+
old_ref = existing.source_ref
|
|
327
|
+
new_ref, new_ref_type = _extract_ref_from_namespace(inst.repo_namespace)
|
|
328
|
+
|
|
329
|
+
# Check if versions differ (potential upgrade)
|
|
330
|
+
if old_ref and new_ref and old_ref != new_ref:
|
|
331
|
+
key = f"{inst.id}_{ai_tool.tool_type.value}"
|
|
332
|
+
upgrades[key] = (existing, inst)
|
|
333
|
+
|
|
334
|
+
return upgrades
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _prompt_for_upgrade(
|
|
338
|
+
existing: InstallationRecord,
|
|
339
|
+
new_instruction: LibraryInstruction,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Prompt user to confirm upgrade from one version to another.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
existing: Existing installation record
|
|
345
|
+
new_instruction: New instruction being installed
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
True if user confirms upgrade, False otherwise
|
|
349
|
+
"""
|
|
350
|
+
old_ref = existing.source_ref or "unknown"
|
|
351
|
+
new_ref, _ = _extract_ref_from_namespace(new_instruction.repo_namespace)
|
|
352
|
+
new_ref = new_ref or "unknown"
|
|
353
|
+
|
|
354
|
+
console.print(f"\n[yellow]⚠️ Upgrade Detected:[/yellow] {existing.instruction_name}")
|
|
355
|
+
console.print(f" Current version: [cyan]{old_ref}[/cyan]")
|
|
356
|
+
console.print(f" New version: [green]{new_ref}[/green]")
|
|
357
|
+
console.print(f" Tool: {existing.ai_tool.value}")
|
|
358
|
+
|
|
359
|
+
return Confirm.ask("\n[bold]Upgrade to new version?[/bold]", default=True)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _perform_installation(
|
|
363
|
+
instructions: list,
|
|
364
|
+
ai_tools: list,
|
|
365
|
+
install_names: dict[str, str],
|
|
366
|
+
install_scope: InstallationScope,
|
|
367
|
+
project_root: Optional[Path],
|
|
368
|
+
strategy: ConflictResolution,
|
|
369
|
+
) -> tuple[int, int]:
|
|
370
|
+
"""Perform the actual installation of instructions to tools.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
instructions: List of instructions to install
|
|
374
|
+
ai_tools: List of AI tools to install to
|
|
375
|
+
install_names: Mapping of instruction IDs to install names
|
|
376
|
+
install_scope: Installation scope (project/global)
|
|
377
|
+
project_root: Project root path
|
|
378
|
+
strategy: Conflict resolution strategy
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Tuple of (installed_count, skipped_count)
|
|
382
|
+
"""
|
|
383
|
+
tracker = InstallationTracker()
|
|
384
|
+
resolver = ConflictResolver(default_strategy=strategy)
|
|
385
|
+
|
|
386
|
+
installed_count = 0
|
|
387
|
+
skipped_count = 0
|
|
388
|
+
|
|
389
|
+
for ai_tool in ai_tools:
|
|
390
|
+
console.print(f"\nInstalling to [cyan]{ai_tool.tool_name}[/cyan]...")
|
|
391
|
+
|
|
392
|
+
for inst in instructions:
|
|
393
|
+
install_name = install_names[inst.id]
|
|
394
|
+
|
|
395
|
+
# Get target path
|
|
396
|
+
target_path = ai_tool.get_instruction_path(install_name, install_scope, project_root)
|
|
397
|
+
|
|
398
|
+
# Handle existing files
|
|
399
|
+
if target_path.exists():
|
|
400
|
+
# Determine the actual strategy to use
|
|
401
|
+
actual_strategy = strategy
|
|
402
|
+
|
|
403
|
+
# If strategy is PROMPT, ask user interactively
|
|
404
|
+
if strategy == ConflictResolution.PROMPT:
|
|
405
|
+
actual_strategy = prompt_conflict_resolution(install_name)
|
|
406
|
+
|
|
407
|
+
if actual_strategy == ConflictResolution.SKIP:
|
|
408
|
+
console.print(f" [yellow]Skipped:[/yellow] {install_name} (already exists)")
|
|
409
|
+
skipped_count += 1
|
|
410
|
+
continue
|
|
411
|
+
elif actual_strategy == ConflictResolution.RENAME:
|
|
412
|
+
conflict_info = resolver.resolve(install_name, target_path, actual_strategy)
|
|
413
|
+
if conflict_info.new_path is None:
|
|
414
|
+
console.print(f" [red]Error:[/red] Failed to rename {install_name}")
|
|
415
|
+
continue
|
|
416
|
+
target_path = Path(conflict_info.new_path)
|
|
417
|
+
console.print(f" [yellow]Renamed:[/yellow] {install_name} -> {target_path.name}")
|
|
418
|
+
elif actual_strategy == ConflictResolution.OVERWRITE:
|
|
419
|
+
console.print(f" [yellow]Overwriting:[/yellow] {install_name}")
|
|
420
|
+
|
|
421
|
+
# Copy file from library
|
|
422
|
+
try:
|
|
423
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
424
|
+
|
|
425
|
+
# Read from library
|
|
426
|
+
source_path = Path(inst.file_path)
|
|
427
|
+
content = source_path.read_text(encoding="utf-8")
|
|
428
|
+
|
|
429
|
+
# Write to target
|
|
430
|
+
target_path.write_text(content, encoding="utf-8")
|
|
431
|
+
|
|
432
|
+
# Extract ref information from namespace
|
|
433
|
+
source_ref, source_ref_type = _extract_ref_from_namespace(inst.repo_namespace)
|
|
434
|
+
|
|
435
|
+
# Track installation
|
|
436
|
+
record = InstallationRecord(
|
|
437
|
+
instruction_name=install_name,
|
|
438
|
+
ai_tool=ai_tool.tool_type,
|
|
439
|
+
source_repo=inst.repo_url,
|
|
440
|
+
installed_path=str(target_path),
|
|
441
|
+
installed_at=datetime.now(),
|
|
442
|
+
checksum=inst.checksum,
|
|
443
|
+
bundle_name=None,
|
|
444
|
+
scope=install_scope,
|
|
445
|
+
source_ref=source_ref,
|
|
446
|
+
source_ref_type=source_ref_type,
|
|
447
|
+
)
|
|
448
|
+
tracker.add_installation(record, project_root)
|
|
449
|
+
|
|
450
|
+
console.print(f" [green]✓[/green] Installed: {install_name}")
|
|
451
|
+
installed_count += 1
|
|
452
|
+
|
|
453
|
+
except Exception as e:
|
|
454
|
+
print_error(f"Failed to install {install_name}: {e}")
|
|
455
|
+
|
|
456
|
+
return installed_count, skipped_count
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ============================================================================
|
|
460
|
+
# Public Installation Functions
|
|
461
|
+
# ============================================================================
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def install_from_library_tui(
|
|
465
|
+
tool: Optional[str] = None,
|
|
466
|
+
) -> int:
|
|
467
|
+
"""
|
|
468
|
+
Show TUI to select and install instructions from library.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
tool: AI tool to install to (None = auto-detect)
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Exit code
|
|
475
|
+
"""
|
|
476
|
+
library = LibraryManager()
|
|
477
|
+
|
|
478
|
+
# Check if library is empty
|
|
479
|
+
if not library.list_instructions():
|
|
480
|
+
print_info("Library is empty. Use 'instructionkit download --repo <url>' to add instructions.")
|
|
481
|
+
return 1
|
|
482
|
+
|
|
483
|
+
# Show TUI (always installs to project level)
|
|
484
|
+
result = show_installer_tui(library=library, tool=tool)
|
|
485
|
+
|
|
486
|
+
if not result:
|
|
487
|
+
console.print("[dim]Cancelled[/dim]")
|
|
488
|
+
return 0
|
|
489
|
+
|
|
490
|
+
# Install selected instructions
|
|
491
|
+
selected_instructions = result["instructions"]
|
|
492
|
+
selected_tools = result["tools"] # Now a list of tool names
|
|
493
|
+
|
|
494
|
+
return install_from_library_direct_multi_tool(
|
|
495
|
+
instruction_ids=[inst.id for inst in selected_instructions],
|
|
496
|
+
tools=selected_tools,
|
|
497
|
+
conflict_strategy="skip",
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def install_from_library_direct_multi_tool(
|
|
502
|
+
instruction_ids: list[str],
|
|
503
|
+
tools: list[str],
|
|
504
|
+
conflict_strategy: str = "skip",
|
|
505
|
+
) -> int:
|
|
506
|
+
"""
|
|
507
|
+
Install specific instructions from library by ID to multiple tools.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
instruction_ids: List of instruction IDs to install
|
|
511
|
+
tools: List of AI tool names to install to
|
|
512
|
+
conflict_strategy: Conflict resolution strategy
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Exit code
|
|
516
|
+
"""
|
|
517
|
+
library = LibraryManager()
|
|
518
|
+
install_scope = InstallationScope.PROJECT
|
|
519
|
+
|
|
520
|
+
# Parse conflict strategy
|
|
521
|
+
strategy = _parse_conflict_strategy(conflict_strategy)
|
|
522
|
+
if strategy is None:
|
|
523
|
+
return 1
|
|
524
|
+
|
|
525
|
+
# Get project root
|
|
526
|
+
project_root = _get_project_root_for_installation()
|
|
527
|
+
if project_root is None:
|
|
528
|
+
return 1
|
|
529
|
+
|
|
530
|
+
# Load instructions from library
|
|
531
|
+
instructions = _load_instructions_from_library(instruction_ids, library)
|
|
532
|
+
if instructions is None:
|
|
533
|
+
return 1
|
|
534
|
+
|
|
535
|
+
# Resolve name conflicts
|
|
536
|
+
install_names = _resolve_name_conflicts(instructions)
|
|
537
|
+
if install_names is None:
|
|
538
|
+
return 0
|
|
539
|
+
|
|
540
|
+
# Get AI tools
|
|
541
|
+
detector = get_detector()
|
|
542
|
+
ai_tools = _get_ai_tools_from_names(tools, detector)
|
|
543
|
+
if ai_tools is None:
|
|
544
|
+
return 1
|
|
545
|
+
|
|
546
|
+
# Detect collisions with installed instructions from different repositories
|
|
547
|
+
collisions = _detect_installed_collisions(instructions, ai_tools, install_names, project_root)
|
|
548
|
+
if collisions:
|
|
549
|
+
console.print("\n[bold cyan]Handling Name Collisions[/bold cyan]")
|
|
550
|
+
for inst in instructions:
|
|
551
|
+
if inst.id in collisions:
|
|
552
|
+
current_name = install_names[inst.id]
|
|
553
|
+
custom_name = _prompt_for_custom_filename(inst, collisions[inst.id], current_name)
|
|
554
|
+
if custom_name is None:
|
|
555
|
+
# User chose to skip
|
|
556
|
+
console.print(f"[dim]Skipping {current_name}[/dim]")
|
|
557
|
+
# Remove from installation list
|
|
558
|
+
instructions = [i for i in instructions if i.id != inst.id]
|
|
559
|
+
else:
|
|
560
|
+
# Use custom name
|
|
561
|
+
install_names[inst.id] = custom_name
|
|
562
|
+
|
|
563
|
+
if not instructions:
|
|
564
|
+
console.print("[yellow]No instructions remaining to install[/yellow]")
|
|
565
|
+
return 0
|
|
566
|
+
|
|
567
|
+
# Show preview and confirm
|
|
568
|
+
if not _show_installation_preview(project_root, instructions, ai_tools, install_names):
|
|
569
|
+
console.print("[dim]Installation cancelled[/dim]")
|
|
570
|
+
return 0
|
|
571
|
+
|
|
572
|
+
# Check for upgrades and prompt if needed
|
|
573
|
+
upgrades = _check_for_upgrades(instructions, ai_tools, install_names, project_root)
|
|
574
|
+
if upgrades:
|
|
575
|
+
console.print("\n[bold cyan]Upgrade Confirmation[/bold cyan]")
|
|
576
|
+
for key, (existing, new_inst) in upgrades.items():
|
|
577
|
+
if not _prompt_for_upgrade(existing, new_inst):
|
|
578
|
+
console.print("[dim]Installation cancelled[/dim]")
|
|
579
|
+
return 0
|
|
580
|
+
|
|
581
|
+
# Perform installation
|
|
582
|
+
installed_count, skipped_count = _perform_installation(
|
|
583
|
+
instructions, ai_tools, install_names, install_scope, project_root, strategy
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Summary
|
|
587
|
+
console.print()
|
|
588
|
+
if installed_count > 0:
|
|
589
|
+
print_success(f"✓ Successfully installed {installed_count} instruction(s)")
|
|
590
|
+
if skipped_count > 0:
|
|
591
|
+
print_info(f"Skipped {skipped_count} existing instruction(s)")
|
|
592
|
+
|
|
593
|
+
return 0
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def install_from_library_direct(
|
|
597
|
+
instruction_ids: list[str],
|
|
598
|
+
tool: Optional[str] = None,
|
|
599
|
+
conflict_strategy: str = "skip",
|
|
600
|
+
) -> int:
|
|
601
|
+
"""
|
|
602
|
+
Install specific instructions from library by ID.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
instruction_ids: List of instruction IDs to install
|
|
606
|
+
tool: AI tool to install to
|
|
607
|
+
conflict_strategy: Conflict resolution strategy
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Exit code
|
|
611
|
+
"""
|
|
612
|
+
library = LibraryManager()
|
|
613
|
+
install_scope = InstallationScope.PROJECT
|
|
614
|
+
|
|
615
|
+
# Parse conflict strategy
|
|
616
|
+
strategy = _parse_conflict_strategy(conflict_strategy)
|
|
617
|
+
if strategy is None:
|
|
618
|
+
return 1
|
|
619
|
+
|
|
620
|
+
# Get project root
|
|
621
|
+
project_root = _get_project_root_for_installation()
|
|
622
|
+
if project_root is None:
|
|
623
|
+
return 1
|
|
624
|
+
|
|
625
|
+
# Load instructions from library
|
|
626
|
+
instructions = _load_instructions_from_library(instruction_ids, library)
|
|
627
|
+
if instructions is None:
|
|
628
|
+
return 1
|
|
629
|
+
|
|
630
|
+
# Resolve name conflicts
|
|
631
|
+
install_names = _resolve_name_conflicts(instructions)
|
|
632
|
+
if install_names is None:
|
|
633
|
+
return 0
|
|
634
|
+
|
|
635
|
+
# Determine AI tool(s)
|
|
636
|
+
detector = get_detector()
|
|
637
|
+
if tool:
|
|
638
|
+
ai_tools = _get_ai_tools_from_names([tool], detector)
|
|
639
|
+
if ai_tools is None:
|
|
640
|
+
return 1
|
|
641
|
+
else:
|
|
642
|
+
ai_tools = detector.detect_installed_tools()
|
|
643
|
+
if not ai_tools:
|
|
644
|
+
print_error("No AI coding tools detected")
|
|
645
|
+
return 1
|
|
646
|
+
|
|
647
|
+
# Detect collisions with installed instructions from different repositories
|
|
648
|
+
collisions = _detect_installed_collisions(instructions, ai_tools, install_names, project_root)
|
|
649
|
+
if collisions:
|
|
650
|
+
console.print("\n[bold cyan]Handling Name Collisions[/bold cyan]")
|
|
651
|
+
for inst in instructions:
|
|
652
|
+
if inst.id in collisions:
|
|
653
|
+
current_name = install_names[inst.id]
|
|
654
|
+
custom_name = _prompt_for_custom_filename(inst, collisions[inst.id], current_name)
|
|
655
|
+
if custom_name is None:
|
|
656
|
+
# User chose to skip
|
|
657
|
+
console.print(f"[dim]Skipping {current_name}[/dim]")
|
|
658
|
+
# Remove from installation list
|
|
659
|
+
instructions = [i for i in instructions if i.id != inst.id]
|
|
660
|
+
else:
|
|
661
|
+
# Use custom name
|
|
662
|
+
install_names[inst.id] = custom_name
|
|
663
|
+
|
|
664
|
+
if not instructions:
|
|
665
|
+
console.print("[yellow]No instructions remaining to install[/yellow]")
|
|
666
|
+
return 0
|
|
667
|
+
|
|
668
|
+
# Show preview and confirm
|
|
669
|
+
if not _show_installation_preview(project_root, instructions, ai_tools, install_names):
|
|
670
|
+
console.print("[dim]Installation cancelled[/dim]")
|
|
671
|
+
return 0
|
|
672
|
+
|
|
673
|
+
# Check for upgrades and prompt if needed
|
|
674
|
+
upgrades = _check_for_upgrades(instructions, ai_tools, install_names, project_root)
|
|
675
|
+
if upgrades:
|
|
676
|
+
console.print("\n[bold cyan]Upgrade Confirmation[/bold cyan]")
|
|
677
|
+
for key, (existing, new_inst) in upgrades.items():
|
|
678
|
+
if not _prompt_for_upgrade(existing, new_inst):
|
|
679
|
+
console.print("[dim]Installation cancelled[/dim]")
|
|
680
|
+
return 0
|
|
681
|
+
|
|
682
|
+
# Perform installation
|
|
683
|
+
installed_count, skipped_count = _perform_installation(
|
|
684
|
+
instructions, ai_tools, install_names, install_scope, project_root, strategy
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
# Summary
|
|
688
|
+
console.print()
|
|
689
|
+
if installed_count > 0:
|
|
690
|
+
print_success(f"✓ Successfully installed {installed_count} instruction(s)")
|
|
691
|
+
if skipped_count > 0:
|
|
692
|
+
print_info(f"Skipped {skipped_count} existing instruction(s)")
|
|
693
|
+
|
|
694
|
+
return 0
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def install_from_library_by_name(
|
|
698
|
+
name: str,
|
|
699
|
+
tool: Optional[str] = None,
|
|
700
|
+
conflict_strategy: str = "skip",
|
|
701
|
+
) -> int:
|
|
702
|
+
"""
|
|
703
|
+
Install instruction(s) from library by name.
|
|
704
|
+
|
|
705
|
+
Supports source/name format for disambiguation (e.g., 'company/python-best-practices').
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
name: Instruction name (or source/name format)
|
|
709
|
+
tool: AI tool to install to
|
|
710
|
+
conflict_strategy: Conflict resolution strategy
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
Exit code
|
|
714
|
+
"""
|
|
715
|
+
library = LibraryManager()
|
|
716
|
+
|
|
717
|
+
# Parse source/name format
|
|
718
|
+
source_alias = None
|
|
719
|
+
instruction_name = name
|
|
720
|
+
if "/" in name:
|
|
721
|
+
parts = name.split("/", 1)
|
|
722
|
+
source_alias = parts[0]
|
|
723
|
+
instruction_name = parts[1]
|
|
724
|
+
|
|
725
|
+
# Find instructions with this name
|
|
726
|
+
if source_alias:
|
|
727
|
+
# Filter by source alias
|
|
728
|
+
instructions = library.get_instructions_by_source_and_name(source_alias, instruction_name)
|
|
729
|
+
else:
|
|
730
|
+
instructions = library.get_instructions_by_name(instruction_name)
|
|
731
|
+
|
|
732
|
+
if not instructions:
|
|
733
|
+
print_error(f"No instruction named '{name}' found in library.")
|
|
734
|
+
print_info("Use 'instructionkit list library --instructions' to see available instructions.")
|
|
735
|
+
return 1
|
|
736
|
+
|
|
737
|
+
if len(instructions) == 1:
|
|
738
|
+
# Single match, install it
|
|
739
|
+
return install_from_library_direct(
|
|
740
|
+
instruction_ids=[instructions[0].id],
|
|
741
|
+
tool=tool,
|
|
742
|
+
conflict_strategy=conflict_strategy,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Multiple matches - show options
|
|
746
|
+
console.print(f"\n[yellow]Multiple instructions named '{name}' found:[/yellow]\n")
|
|
747
|
+
for i, inst in enumerate(instructions, 1):
|
|
748
|
+
# Extract ref from namespace for display
|
|
749
|
+
ref, ref_type = _extract_ref_from_namespace(inst.repo_namespace)
|
|
750
|
+
ref_display = f"@{ref}" if ref else f"v{inst.version}"
|
|
751
|
+
console.print(f" [{i}] {inst.repo_name} ({ref_display}) - {inst.author}")
|
|
752
|
+
console.print(f" {inst.description}")
|
|
753
|
+
console.print()
|
|
754
|
+
|
|
755
|
+
console.print(" [A] Install all")
|
|
756
|
+
console.print(" [C] Cancel")
|
|
757
|
+
console.print()
|
|
758
|
+
|
|
759
|
+
choice = typer.prompt("Select", default="C")
|
|
760
|
+
|
|
761
|
+
if choice.upper() == "C":
|
|
762
|
+
print_info("Installation cancelled")
|
|
763
|
+
return 0
|
|
764
|
+
elif choice.upper() == "A":
|
|
765
|
+
# Install all
|
|
766
|
+
return install_from_library_direct(
|
|
767
|
+
instruction_ids=[inst.id for inst in instructions],
|
|
768
|
+
tool=tool,
|
|
769
|
+
conflict_strategy=conflict_strategy,
|
|
770
|
+
)
|
|
771
|
+
elif choice.isdigit():
|
|
772
|
+
idx = int(choice) - 1
|
|
773
|
+
if 0 <= idx < len(instructions):
|
|
774
|
+
return install_from_library_direct(
|
|
775
|
+
instruction_ids=[instructions[idx].id],
|
|
776
|
+
tool=tool,
|
|
777
|
+
conflict_strategy=conflict_strategy,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
print_error("Invalid selection")
|
|
781
|
+
return 1
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# Keep the original direct install function for backward compatibility
|
|
785
|
+
def install_from_repo_direct(
|
|
786
|
+
name: str,
|
|
787
|
+
repo: str,
|
|
788
|
+
tool: Optional[str] = None,
|
|
789
|
+
conflict_strategy: str = "skip",
|
|
790
|
+
bundle: bool = False,
|
|
791
|
+
) -> int:
|
|
792
|
+
"""
|
|
793
|
+
Install directly from a repository (backward compatibility).
|
|
794
|
+
|
|
795
|
+
This is the original install function, preserved for --repo usage.
|
|
796
|
+
"""
|
|
797
|
+
# Import the original function
|
|
798
|
+
from aiconfigkit.cli.install import install_instruction as original_install
|
|
799
|
+
|
|
800
|
+
return original_install(
|
|
801
|
+
name=name,
|
|
802
|
+
repo=repo,
|
|
803
|
+
tool=tool,
|
|
804
|
+
conflict_strategy=conflict_strategy,
|
|
805
|
+
bundle=bundle,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def install_multiple_from_library(
|
|
810
|
+
names: list[str],
|
|
811
|
+
tools: Optional[list[str]],
|
|
812
|
+
conflict_strategy: str,
|
|
813
|
+
) -> int:
|
|
814
|
+
"""
|
|
815
|
+
Install multiple instructions from library.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
names: List of instruction names
|
|
819
|
+
tools: List of AI tool names (None = all detected tools)
|
|
820
|
+
conflict_strategy: Conflict resolution strategy
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Exit code
|
|
824
|
+
"""
|
|
825
|
+
library = LibraryManager()
|
|
826
|
+
|
|
827
|
+
# Get all instructions by name
|
|
828
|
+
all_instructions = []
|
|
829
|
+
for name in names:
|
|
830
|
+
insts = library.get_instructions_by_name(name)
|
|
831
|
+
if not insts:
|
|
832
|
+
print_error(f"No instruction named '{name}' found in library.")
|
|
833
|
+
return 1
|
|
834
|
+
|
|
835
|
+
if len(insts) > 1:
|
|
836
|
+
# Multiple matches - show options
|
|
837
|
+
console.print(f"\n[yellow]Multiple instructions named '{name}' found:[/yellow]\n")
|
|
838
|
+
for i, inst in enumerate(insts, 1):
|
|
839
|
+
console.print(f" [{i}] {inst.repo_name} (v{inst.version}) - {inst.author}")
|
|
840
|
+
console.print()
|
|
841
|
+
|
|
842
|
+
choice = typer.prompt(f"Select which '{name}' to install (1-{len(insts)})", default="1")
|
|
843
|
+
|
|
844
|
+
if choice.isdigit():
|
|
845
|
+
idx = int(choice) - 1
|
|
846
|
+
if 0 <= idx < len(insts):
|
|
847
|
+
all_instructions.append(insts[idx])
|
|
848
|
+
else:
|
|
849
|
+
print_error("Invalid selection")
|
|
850
|
+
return 1
|
|
851
|
+
else:
|
|
852
|
+
print_error("Invalid selection")
|
|
853
|
+
return 1
|
|
854
|
+
else:
|
|
855
|
+
all_instructions.append(insts[0])
|
|
856
|
+
|
|
857
|
+
# Get instruction IDs
|
|
858
|
+
instruction_ids = [inst.id for inst in all_instructions]
|
|
859
|
+
|
|
860
|
+
# Install using the multi-tool function
|
|
861
|
+
if tools:
|
|
862
|
+
return install_from_library_direct_multi_tool(
|
|
863
|
+
instruction_ids=instruction_ids,
|
|
864
|
+
tools=tools,
|
|
865
|
+
conflict_strategy=conflict_strategy,
|
|
866
|
+
)
|
|
867
|
+
else:
|
|
868
|
+
# Use existing single-tool logic (all tools)
|
|
869
|
+
return install_from_library_direct(
|
|
870
|
+
instruction_ids=instruction_ids,
|
|
871
|
+
tool=None, # Will install to all detected tools
|
|
872
|
+
conflict_strategy=conflict_strategy,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def install_instruction_unified(
|
|
877
|
+
names: Optional[list[str]] = None,
|
|
878
|
+
repo: Optional[str] = None,
|
|
879
|
+
tools: Optional[list[str]] = None,
|
|
880
|
+
conflict_strategy: str = "prompt",
|
|
881
|
+
bundle: bool = False,
|
|
882
|
+
) -> int:
|
|
883
|
+
"""
|
|
884
|
+
Unified install function that routes to appropriate implementation.
|
|
885
|
+
|
|
886
|
+
All installations are at project level.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
names: Instruction name(s) (optional, can be multiple)
|
|
890
|
+
repo: Repository URL (optional)
|
|
891
|
+
tools: AI tool(s) to install to (optional, can be multiple)
|
|
892
|
+
conflict_strategy: Conflict resolution strategy
|
|
893
|
+
bundle: Whether installing a bundle
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
Exit code
|
|
897
|
+
"""
|
|
898
|
+
# Convert single tool format (backward compat)
|
|
899
|
+
tool = tools[0] if tools and len(tools) == 1 else None
|
|
900
|
+
|
|
901
|
+
# Case 1: Direct install from repo (backward compat)
|
|
902
|
+
if repo:
|
|
903
|
+
if not names or len(names) == 0:
|
|
904
|
+
print_error("When using --repo, you must specify an instruction name")
|
|
905
|
+
return 1
|
|
906
|
+
|
|
907
|
+
# Only support single name with --repo for now
|
|
908
|
+
if len(names) > 1:
|
|
909
|
+
print_error("Cannot install multiple instructions with --repo. Install one at a time or use the library.")
|
|
910
|
+
return 1
|
|
911
|
+
|
|
912
|
+
return install_from_repo_direct(
|
|
913
|
+
name=names[0],
|
|
914
|
+
repo=repo,
|
|
915
|
+
tool=tool,
|
|
916
|
+
conflict_strategy=conflict_strategy,
|
|
917
|
+
bundle=bundle,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
# Case 2: Install from library with TUI
|
|
921
|
+
if not names or len(names) == 0:
|
|
922
|
+
return install_from_library_tui(tool=tool)
|
|
923
|
+
|
|
924
|
+
# Case 3: Install multiple instructions from library
|
|
925
|
+
if len(names) > 1 or (tools and len(tools) > 1):
|
|
926
|
+
return install_multiple_from_library(
|
|
927
|
+
names=names,
|
|
928
|
+
tools=tools,
|
|
929
|
+
conflict_strategy=conflict_strategy,
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# Case 4: Install single instruction from library by name
|
|
933
|
+
return install_from_library_by_name(
|
|
934
|
+
name=names[0],
|
|
935
|
+
tool=tool,
|
|
936
|
+
conflict_strategy=conflict_strategy,
|
|
937
|
+
)
|