devsync 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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()