1466 lines
53 KiB
Python
1466 lines
53 KiB
Python
"""CLI entry point for SmartTools."""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from . import __version__
|
|
from .tool import list_tools, load_tool, save_tool, delete_tool, Tool, ToolArgument, PromptStep, CodeStep
|
|
from .ui import run_ui
|
|
from .providers import load_providers, add_provider, delete_provider, Provider, call_provider
|
|
from .config import load_config, save_config, set_registry_token
|
|
from .manifest import (
|
|
load_manifest, save_manifest, create_manifest, find_manifest,
|
|
Manifest, Dependency, MANIFEST_FILENAME
|
|
)
|
|
from .resolver import (
|
|
resolve_tool, find_tool, install_from_registry, uninstall_tool,
|
|
list_installed_tools, ToolNotFoundError, ToolSpec
|
|
)
|
|
|
|
|
|
def cmd_list(args):
|
|
"""List all tools."""
|
|
tools = list_tools()
|
|
|
|
if not tools:
|
|
print("No tools found.")
|
|
print("Create your first tool with: smarttools ui")
|
|
return 0
|
|
|
|
print(f"Available tools ({len(tools)}):\n")
|
|
for name in tools:
|
|
tool = load_tool(name)
|
|
if tool:
|
|
print(f" {name}")
|
|
print(f" {tool.description or 'No description'}")
|
|
|
|
# Show arguments
|
|
if tool.arguments:
|
|
args_str = ", ".join(arg.flag for arg in tool.arguments)
|
|
print(f" Arguments: {args_str}")
|
|
|
|
# Show steps
|
|
if tool.steps:
|
|
step_info = []
|
|
for step in tool.steps:
|
|
if isinstance(step, PromptStep):
|
|
step_info.append(f"PROMPT[{step.provider}]")
|
|
elif isinstance(step, CodeStep):
|
|
step_info.append("CODE")
|
|
print(f" Steps: {' -> '.join(step_info)}")
|
|
|
|
print()
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_create(args):
|
|
"""Create a new tool (basic CLI creation - use 'ui' for full builder)."""
|
|
name = args.name
|
|
|
|
# Check if already exists
|
|
existing = load_tool(name)
|
|
if existing and not args.force:
|
|
print(f"Error: Tool '{name}' already exists. Use --force to overwrite.")
|
|
return 1
|
|
|
|
# Create a tool with a single prompt step
|
|
steps = []
|
|
if args.prompt:
|
|
steps.append(PromptStep(
|
|
prompt=args.prompt,
|
|
provider=args.provider or "mock",
|
|
output_var="response"
|
|
))
|
|
|
|
tool = Tool(
|
|
name=name,
|
|
description=args.description or "",
|
|
arguments=[],
|
|
steps=steps,
|
|
output="{response}" if steps else "{input}"
|
|
)
|
|
|
|
path = save_tool(tool)
|
|
print(f"Created tool '{name}'")
|
|
print(f"Config: {path}")
|
|
print(f"\nUse 'smarttools ui' to add arguments, steps, and customize.")
|
|
print(f"Or run: {name} < input.txt")
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_edit(args):
|
|
"""Edit a tool (opens in $EDITOR or nano)."""
|
|
import os
|
|
import subprocess
|
|
|
|
tool = load_tool(args.name)
|
|
if not tool:
|
|
print(f"Error: Tool '{args.name}' not found.")
|
|
return 1
|
|
|
|
from .tool import get_tools_dir
|
|
config_path = get_tools_dir() / args.name / "config.yaml"
|
|
|
|
editor = os.environ.get("EDITOR", "nano")
|
|
|
|
try:
|
|
subprocess.run([editor, str(config_path)], check=True)
|
|
print(f"Tool '{args.name}' updated.")
|
|
return 0
|
|
except subprocess.CalledProcessError:
|
|
print(f"Error: Editor failed.")
|
|
return 1
|
|
except FileNotFoundError:
|
|
print(f"Error: Editor '{editor}' not found. Set $EDITOR environment variable.")
|
|
return 1
|
|
|
|
|
|
def cmd_delete(args):
|
|
"""Delete a tool."""
|
|
if not load_tool(args.name):
|
|
print(f"Error: Tool '{args.name}' not found.")
|
|
return 1
|
|
|
|
if not args.force:
|
|
confirm = input(f"Delete tool '{args.name}'? [y/N] ")
|
|
if confirm.lower() != 'y':
|
|
print("Cancelled.")
|
|
return 0
|
|
|
|
if delete_tool(args.name):
|
|
print(f"Deleted tool '{args.name}'.")
|
|
return 0
|
|
else:
|
|
print(f"Error: Failed to delete '{args.name}'.")
|
|
return 1
|
|
|
|
|
|
def cmd_test(args):
|
|
"""Test a tool with mock provider."""
|
|
tool = load_tool(args.name)
|
|
if not tool:
|
|
print(f"Error: Tool '{args.name}' not found.")
|
|
return 1
|
|
|
|
from .runner import run_tool
|
|
|
|
# Read test input
|
|
if args.input:
|
|
from pathlib import Path
|
|
input_text = Path(args.input).read_text()
|
|
else:
|
|
print("Enter test input (Ctrl+D to end):")
|
|
input_text = sys.stdin.read()
|
|
|
|
print("\n--- Running with mock provider ---\n")
|
|
|
|
output, code = run_tool(
|
|
tool=tool,
|
|
input_text=input_text,
|
|
custom_args={},
|
|
provider_override="mock",
|
|
dry_run=args.dry_run,
|
|
show_prompt=True,
|
|
verbose=True
|
|
)
|
|
|
|
if output:
|
|
print("\n--- Output ---\n")
|
|
print(output)
|
|
|
|
return code
|
|
|
|
|
|
def cmd_run(args):
|
|
"""Run a tool."""
|
|
from pathlib import Path
|
|
from .runner import run_tool
|
|
|
|
tool = load_tool(args.name)
|
|
if not tool:
|
|
print(f"Error: Tool '{args.name}' not found.", file=sys.stderr)
|
|
return 1
|
|
|
|
# Read input
|
|
if args.input:
|
|
# Read from file
|
|
input_path = Path(args.input)
|
|
if not input_path.exists():
|
|
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
|
return 1
|
|
input_text = input_path.read_text()
|
|
elif args.stdin:
|
|
# Explicit interactive input requested
|
|
print("Reading from stdin (Ctrl+D to end):", file=sys.stderr)
|
|
input_text = sys.stdin.read()
|
|
elif not sys.stdin.isatty():
|
|
# Stdin is piped - read it
|
|
input_text = sys.stdin.read()
|
|
else:
|
|
# No input provided - use empty string
|
|
input_text = ""
|
|
|
|
# Collect custom args from remaining arguments
|
|
custom_args = {}
|
|
if args.tool_args:
|
|
# Parse tool-specific arguments
|
|
i = 0
|
|
while i < len(args.tool_args):
|
|
arg = args.tool_args[i]
|
|
if arg.startswith('--'):
|
|
key = arg[2:].replace('-', '_')
|
|
if i + 1 < len(args.tool_args) and not args.tool_args[i + 1].startswith('--'):
|
|
custom_args[key] = args.tool_args[i + 1]
|
|
i += 2
|
|
else:
|
|
custom_args[key] = True
|
|
i += 1
|
|
elif arg.startswith('-'):
|
|
key = arg[1:].replace('-', '_')
|
|
if i + 1 < len(args.tool_args) and not args.tool_args[i + 1].startswith('-'):
|
|
custom_args[key] = args.tool_args[i + 1]
|
|
i += 2
|
|
else:
|
|
custom_args[key] = True
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
# Run tool
|
|
output, code = run_tool(
|
|
tool=tool,
|
|
input_text=input_text,
|
|
custom_args=custom_args,
|
|
provider_override=args.provider,
|
|
dry_run=args.dry_run,
|
|
show_prompt=args.show_prompt,
|
|
verbose=args.verbose
|
|
)
|
|
|
|
# Write output
|
|
if code == 0 and output:
|
|
if args.output:
|
|
Path(args.output).write_text(output)
|
|
else:
|
|
print(output)
|
|
|
|
return code
|
|
|
|
|
|
def cmd_ui(args):
|
|
"""Launch the interactive UI."""
|
|
run_ui()
|
|
return 0
|
|
|
|
|
|
def cmd_refresh(args):
|
|
"""Refresh all wrapper scripts with the current Python path."""
|
|
from .tool import list_tools, create_wrapper_script
|
|
|
|
tools = list_tools()
|
|
if not tools:
|
|
print("No tools found.")
|
|
return 0
|
|
|
|
print(f"Refreshing wrapper scripts for {len(tools)} tools...")
|
|
for name in tools:
|
|
path = create_wrapper_script(name)
|
|
print(f" {name} -> {path}")
|
|
|
|
print("\nDone. Wrapper scripts updated to use current Python interpreter.")
|
|
return 0
|
|
|
|
|
|
def cmd_docs(args):
|
|
"""View or edit tool documentation."""
|
|
import os
|
|
import subprocess
|
|
|
|
from .tool import get_tools_dir
|
|
|
|
tool = load_tool(args.name)
|
|
if not tool:
|
|
print(f"Error: Tool '{args.name}' not found.")
|
|
return 1
|
|
|
|
readme_path = get_tools_dir() / args.name / "README.md"
|
|
|
|
if args.edit:
|
|
# Edit/create README
|
|
editor = os.environ.get("EDITOR", "nano")
|
|
|
|
# Create a template if README doesn't exist
|
|
if not readme_path.exists():
|
|
template = f"""# {args.name}
|
|
|
|
{tool.description or 'No description provided.'}
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
echo "input" | {args.name}
|
|
```
|
|
|
|
## Arguments
|
|
|
|
| Flag | Default | Description |
|
|
|------|---------|-------------|
|
|
"""
|
|
for arg in tool.arguments:
|
|
template += f"| `{arg.flag}` | {arg.default or ''} | {arg.description or ''} |\n"
|
|
|
|
template += """
|
|
## Examples
|
|
|
|
```bash
|
|
# Example 1
|
|
```
|
|
|
|
## Requirements
|
|
|
|
- List any dependencies here
|
|
"""
|
|
readme_path.write_text(template)
|
|
print(f"Created template: {readme_path}")
|
|
|
|
try:
|
|
subprocess.run([editor, str(readme_path)], check=True)
|
|
print(f"Documentation updated: {readme_path}")
|
|
return 0
|
|
except subprocess.CalledProcessError:
|
|
print("Error: Editor failed.")
|
|
return 1
|
|
except FileNotFoundError:
|
|
print(f"Error: Editor '{editor}' not found. Set $EDITOR environment variable.")
|
|
return 1
|
|
else:
|
|
# View README
|
|
if not readme_path.exists():
|
|
print(f"No documentation found for '{args.name}'.")
|
|
print(f"Create it with: smarttools docs {args.name} --edit")
|
|
return 1
|
|
|
|
print(readme_path.read_text())
|
|
return 0
|
|
|
|
|
|
PROVIDER_INSTALL_INFO = {
|
|
"claude": {
|
|
"group": "Anthropic Claude",
|
|
"install_cmd": "npm install -g @anthropic-ai/claude-code",
|
|
"requires": "Node.js 18+ and npm",
|
|
"setup": "Run 'claude' - opens browser for sign-in (auto-saves auth tokens)",
|
|
"cost": "Pay-per-use (billed to your Anthropic account)",
|
|
"variants": ["claude", "claude-haiku", "claude-opus", "claude-sonnet"],
|
|
},
|
|
"codex": {
|
|
"group": "OpenAI Codex",
|
|
"install_cmd": "npm install -g @openai/codex",
|
|
"requires": "Node.js 18+ and npm",
|
|
"setup": "Run 'codex' - opens browser for sign-in (auto-saves auth tokens)",
|
|
"cost": "Pay-per-use (billed to your OpenAI account)",
|
|
"variants": ["codex"],
|
|
},
|
|
"gemini": {
|
|
"group": "Google Gemini",
|
|
"install_cmd": "npm install -g @google/gemini-cli",
|
|
"requires": "Node.js 18+ and npm",
|
|
"setup": "Run 'gemini' - opens browser for Google sign-in",
|
|
"cost": "Free tier available, pay-per-use for more",
|
|
"variants": ["gemini", "gemini-flash"],
|
|
},
|
|
"opencode": {
|
|
"group": "OpenCode (75+ providers)",
|
|
"install_cmd": "curl -fsSL https://opencode.ai/install | bash",
|
|
"requires": "curl, bash",
|
|
"setup": "Run 'opencode' - opens browser to connect more providers",
|
|
"cost": "4 FREE models included (Big Pickle, GLM-4.7, Grok Code Fast 1, MiniMax M2.1), 75+ more available",
|
|
"variants": ["opencode-pickle", "opencode-deepseek", "opencode-nano", "opencode-reasoner", "opencode-grok"],
|
|
},
|
|
"ollama": {
|
|
"group": "Ollama (Local LLMs)",
|
|
"install_cmd": "curl -fsSL https://ollama.ai/install.sh | bash",
|
|
"requires": "curl, bash, 8GB+ RAM (GPU recommended)",
|
|
"setup": "Run 'ollama pull llama3' to download a model, then add provider",
|
|
"cost": "FREE (runs entirely on your machine)",
|
|
"variants": [],
|
|
"custom": True,
|
|
"post_install_note": "After installing, add the provider:\n smarttools providers add ollama 'ollama run llama3' -d 'Local Llama 3'",
|
|
},
|
|
}
|
|
|
|
|
|
def cmd_providers(args):
|
|
"""Manage AI providers."""
|
|
import shutil
|
|
import subprocess
|
|
|
|
if args.providers_cmd == "install":
|
|
print("=" * 60)
|
|
print("SmartTools Provider Installation Guide")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
# Check what's already installed
|
|
providers = load_providers()
|
|
installed_groups = set()
|
|
for p in providers:
|
|
if p.name.lower() == "mock":
|
|
continue
|
|
cmd_parts = p.command.split()[0]
|
|
cmd_expanded = cmd_parts.replace("$HOME", str(Path.home())).replace("~", str(Path.home()))
|
|
if shutil.which(cmd_expanded) or Path(cmd_expanded).exists():
|
|
# Find which group this belongs to
|
|
for group, info in PROVIDER_INSTALL_INFO.items():
|
|
if p.name in info.get("variants", []):
|
|
installed_groups.add(group)
|
|
|
|
# Show available provider groups
|
|
print("Available AI Provider Groups:\n")
|
|
options = []
|
|
for i, (key, info) in enumerate(PROVIDER_INSTALL_INFO.items(), 1):
|
|
status = "[INSTALLED]" if key in installed_groups else ""
|
|
print(f" {i}. {info['group']} {status}")
|
|
print(f" Cost: {info['cost']}")
|
|
print(f" Models: {', '.join(info['variants']) if info['variants'] else 'Custom'}")
|
|
print()
|
|
options.append(key)
|
|
|
|
print(" 0. Cancel")
|
|
print()
|
|
|
|
try:
|
|
choice = input("Select a provider to install (1-{}, or 0 to cancel): ".format(len(options)))
|
|
choice = int(choice)
|
|
except (ValueError, EOFError):
|
|
print("Cancelled.")
|
|
return 0
|
|
|
|
if choice == 0 or choice > len(options):
|
|
print("Cancelled.")
|
|
return 0
|
|
|
|
selected = options[choice - 1]
|
|
info = PROVIDER_INSTALL_INFO[selected]
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print(f"Installing: {info['group']}")
|
|
print("=" * 60)
|
|
print()
|
|
print(f"Requirements: {info['requires']}")
|
|
print(f"Install command: {info['install_cmd']}")
|
|
print(f"Post-install: {info['setup']}")
|
|
print()
|
|
|
|
try:
|
|
confirm = input("Run installation command? (y/N): ").strip().lower()
|
|
except EOFError:
|
|
confirm = "n"
|
|
|
|
if confirm == "y":
|
|
print()
|
|
print(f"Running: {info['install_cmd']}")
|
|
print("-" * 40)
|
|
result = subprocess.run(info['install_cmd'], shell=True)
|
|
print("-" * 40)
|
|
|
|
if result.returncode == 0:
|
|
# Refresh PATH to pick up newly installed tools
|
|
import os
|
|
new_paths = []
|
|
|
|
# Common install locations that might have been added
|
|
potential_paths = [
|
|
Path.home() / ".opencode" / "bin", # OpenCode
|
|
Path.home() / ".local" / "bin", # pip/pipx installs
|
|
Path("/usr/local/bin"), # Ollama, system installs
|
|
]
|
|
|
|
# Also try to get npm global bin path
|
|
try:
|
|
npm_result = subprocess.run(
|
|
["npm", "bin", "-g"],
|
|
capture_output=True, text=True, timeout=5
|
|
)
|
|
if npm_result.returncode == 0:
|
|
npm_bin = npm_result.stdout.strip()
|
|
if npm_bin:
|
|
potential_paths.append(Path(npm_bin))
|
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
pass
|
|
|
|
current_path = os.environ.get("PATH", "")
|
|
for p in potential_paths:
|
|
if p.exists() and str(p) not in current_path:
|
|
new_paths.append(str(p))
|
|
|
|
if new_paths:
|
|
os.environ["PATH"] = ":".join(new_paths) + ":" + current_path
|
|
|
|
print()
|
|
print("Installation completed!")
|
|
print()
|
|
print("IMPORTANT: Refresh your shell PATH before continuing:")
|
|
print(" source ~/.bashrc")
|
|
print()
|
|
print(f"Next steps:")
|
|
print(f" 1. source ~/.bashrc (required!)")
|
|
print(f" 2. {info['setup']}")
|
|
if info.get('post_install_note'):
|
|
print(f" 3. {info['post_install_note']}")
|
|
print(f" 4. Test with: smarttools providers test {selected}")
|
|
else:
|
|
print(f" 3. Test with: smarttools providers test {info['variants'][0] if info['variants'] else selected}")
|
|
else:
|
|
print()
|
|
print(f"Installation failed (exit code {result.returncode})")
|
|
print("Try running the command manually:")
|
|
print(f" {info['install_cmd']}")
|
|
else:
|
|
print()
|
|
print("To install manually, run:")
|
|
print(f" {info['install_cmd']}")
|
|
print()
|
|
print(f"Then: {info['setup']}")
|
|
|
|
return 0
|
|
|
|
elif args.providers_cmd == "list":
|
|
providers = load_providers()
|
|
print(f"Configured providers ({len(providers)}):\n")
|
|
for p in providers:
|
|
# Mock provider is always available
|
|
if p.name.lower() == "mock":
|
|
print(f" [+] {p.name}")
|
|
print(f" Command: (built-in)")
|
|
print(f" Status: OK (always available)")
|
|
if p.description:
|
|
print(f" Info: {p.description}")
|
|
print()
|
|
continue
|
|
|
|
# Check if command exists
|
|
cmd_parts = p.command.split()[0]
|
|
cmd_expanded = cmd_parts.replace("$HOME", str(Path.home())).replace("~", str(Path.home()))
|
|
exists = shutil.which(cmd_expanded) is not None or Path(cmd_expanded).exists()
|
|
status = "OK" if exists else "NOT FOUND"
|
|
status_icon = "+" if exists else "-"
|
|
|
|
print(f" [{status_icon}] {p.name}")
|
|
print(f" Command: {p.command}")
|
|
print(f" Status: {status}")
|
|
if p.description:
|
|
print(f" Info: {p.description}")
|
|
print()
|
|
return 0
|
|
|
|
elif args.providers_cmd == "add":
|
|
name = args.name
|
|
command = args.command
|
|
description = args.description or ""
|
|
|
|
provider = Provider(name, command, description)
|
|
add_provider(provider)
|
|
print(f"Provider '{name}' added/updated.")
|
|
return 0
|
|
|
|
elif args.providers_cmd == "remove":
|
|
name = args.name
|
|
if delete_provider(name):
|
|
print(f"Provider '{name}' removed.")
|
|
return 0
|
|
else:
|
|
print(f"Provider '{name}' not found.")
|
|
return 1
|
|
|
|
elif args.providers_cmd == "test":
|
|
name = args.name
|
|
print(f"Testing provider '{name}'...")
|
|
result = call_provider(name, "Say 'hello' and nothing else.", timeout=30)
|
|
if result.success:
|
|
print(f"SUCCESS: {result.text[:200]}...")
|
|
else:
|
|
print(f"FAILED: {result.error}")
|
|
return 0 if result.success else 1
|
|
|
|
elif args.providers_cmd == "check":
|
|
providers = load_providers()
|
|
print("Checking all providers...\n")
|
|
available = []
|
|
missing = []
|
|
|
|
for p in providers:
|
|
# Mock provider is always available (handled specially)
|
|
if p.name.lower() == "mock":
|
|
available.append(p.name)
|
|
print(f" [+] {p.name}: OK (built-in)")
|
|
continue
|
|
|
|
cmd_parts = p.command.split()[0]
|
|
cmd_expanded = cmd_parts.replace("$HOME", str(Path.home())).replace("~", str(Path.home()))
|
|
exists = shutil.which(cmd_expanded) is not None or Path(cmd_expanded).exists()
|
|
|
|
if exists:
|
|
available.append(p.name)
|
|
print(f" [+] {p.name}: OK")
|
|
else:
|
|
missing.append(p.name)
|
|
print(f" [-] {p.name}: NOT FOUND ({cmd_parts})")
|
|
|
|
print(f"\nSummary: {len(available)} available, {len(missing)} missing")
|
|
if len(available) == 1 and available[0] == "mock":
|
|
print(f"\nNo real AI providers found. Install one of these:")
|
|
print(f" - claude: npm install -g @anthropic-ai/claude-cli")
|
|
print(f" - codex: pip install openai-codex")
|
|
print(f" - gemini: pip install google-generative-ai")
|
|
print(f"\nMeanwhile, use mock provider for testing:")
|
|
print(f" echo 'test' | summarize --provider mock")
|
|
elif missing:
|
|
print(f"\nAvailable providers: {', '.join(available)}")
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Registry Commands
|
|
# -------------------------------------------------------------------------
|
|
|
|
def cmd_registry(args):
|
|
"""Handle registry subcommands."""
|
|
from .registry_client import (
|
|
RegistryClient, RegistryError, RateLimitError,
|
|
get_client, search, install_tool as registry_install
|
|
)
|
|
|
|
if args.registry_cmd == "search":
|
|
try:
|
|
client = get_client()
|
|
results = client.search_tools(
|
|
query=args.query,
|
|
category=args.category,
|
|
per_page=args.limit or 20
|
|
)
|
|
|
|
if not results.data:
|
|
print(f"No tools found matching '{args.query}'")
|
|
return 0
|
|
|
|
print(f"Found {results.total} tools:\n")
|
|
for tool in results.data:
|
|
owner = tool.get("owner", "")
|
|
name = tool.get("name", "")
|
|
version = tool.get("version", "")
|
|
desc = tool.get("description", "")
|
|
downloads = tool.get("downloads", 0)
|
|
|
|
print(f" {owner}/{name} v{version}")
|
|
print(f" {desc[:60]}{'...' if len(desc) > 60 else ''}")
|
|
print(f" Downloads: {downloads}")
|
|
print()
|
|
|
|
if results.total_pages > 1:
|
|
print(f"Showing page {results.page}/{results.total_pages}")
|
|
|
|
except RegistryError as e:
|
|
if e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
elif e.code == "RATE_LIMITED":
|
|
print(f"Rate limited. Please wait and try again.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error searching registry: {e}", file=sys.stderr)
|
|
print("If the problem persists, check: smarttools config show", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "install":
|
|
tool_spec = args.tool
|
|
version = args.version
|
|
|
|
print(f"Installing {tool_spec}...")
|
|
|
|
try:
|
|
resolved = install_from_registry(tool_spec, version)
|
|
print(f"Installed: {resolved.full_name}@{resolved.version}")
|
|
print(f"Location: {resolved.path}")
|
|
|
|
# Show wrapper info
|
|
from .tool import BIN_DIR
|
|
wrapper_name = resolved.tool.name
|
|
if resolved.owner:
|
|
# Check for collision
|
|
short_wrapper = BIN_DIR / resolved.tool.name
|
|
if short_wrapper.exists():
|
|
wrapper_name = f"{resolved.owner}-{resolved.tool.name}"
|
|
|
|
print(f"\nRun with: {wrapper_name}")
|
|
|
|
except RegistryError as e:
|
|
if e.code == "TOOL_NOT_FOUND":
|
|
print(f"Tool '{tool_spec}' not found in the registry.", file=sys.stderr)
|
|
print(f"Try: smarttools registry search {tool_spec.split('/')[-1]}", file=sys.stderr)
|
|
elif e.code == "VERSION_NOT_FOUND" or e.code == "CONSTRAINT_UNSATISFIABLE":
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
if e.details and "available_versions" in e.details:
|
|
versions = e.details["available_versions"]
|
|
print(f"Available versions: {', '.join(versions[:5])}", file=sys.stderr)
|
|
if e.details.get("latest_stable"):
|
|
print(f"Latest stable: {e.details['latest_stable']}", file=sys.stderr)
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error installing tool: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "uninstall":
|
|
tool_spec = args.tool
|
|
|
|
print(f"Uninstalling {tool_spec}...")
|
|
|
|
if uninstall_tool(tool_spec):
|
|
print(f"Uninstalled: {tool_spec}")
|
|
else:
|
|
print(f"Tool '{tool_spec}' not found", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "info":
|
|
tool_spec = args.tool
|
|
|
|
try:
|
|
# Parse the tool spec
|
|
parsed = ToolSpec.parse(tool_spec)
|
|
owner = parsed.owner or "official"
|
|
|
|
client = get_client()
|
|
tool_info = client.get_tool(owner, parsed.name)
|
|
|
|
print(f"{tool_info.owner}/{tool_info.name} v{tool_info.version}")
|
|
print("=" * 50)
|
|
print(f"Description: {tool_info.description}")
|
|
print(f"Category: {tool_info.category}")
|
|
print(f"Tags: {', '.join(tool_info.tags)}")
|
|
print(f"Downloads: {tool_info.downloads}")
|
|
print(f"Published: {tool_info.published_at}")
|
|
|
|
if tool_info.deprecated:
|
|
print()
|
|
print(f"DEPRECATED: {tool_info.deprecated_message}")
|
|
if tool_info.replacement:
|
|
print(f"Use instead: {tool_info.replacement}")
|
|
|
|
# Show versions
|
|
versions = client.get_tool_versions(owner, parsed.name)
|
|
if versions:
|
|
print(f"\nVersions: {', '.join(versions[:5])}")
|
|
if len(versions) > 5:
|
|
print(f" ...and {len(versions) - 5} more")
|
|
|
|
print(f"\nInstall: smarttools registry install {tool_info.owner}/{tool_info.name}")
|
|
|
|
except RegistryError as e:
|
|
if e.code == "TOOL_NOT_FOUND":
|
|
print(f"Tool '{tool_spec}' not found in the registry.", file=sys.stderr)
|
|
print(f"Try: smarttools registry search {parsed.name}", file=sys.stderr)
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error fetching tool info: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "update":
|
|
print("Updating registry index...")
|
|
|
|
try:
|
|
client = get_client()
|
|
index = client.get_index(force_refresh=True)
|
|
|
|
tool_count = index.get("tool_count", len(index.get("tools", [])))
|
|
generated = index.get("generated_at", "unknown")
|
|
|
|
print(f"Index updated: {tool_count} tools")
|
|
print(f"Generated: {generated}")
|
|
|
|
except RegistryError as e:
|
|
if e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
elif e.code == "RATE_LIMITED":
|
|
print("Rate limited. Please wait a moment and try again.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error updating index: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "publish":
|
|
# Read tool from current directory or specified path
|
|
tool_path = Path(args.path) if args.path else Path.cwd()
|
|
|
|
if tool_path.is_dir():
|
|
config_path = tool_path / "config.yaml"
|
|
else:
|
|
config_path = tool_path
|
|
tool_path = config_path.parent
|
|
|
|
if not config_path.exists():
|
|
print(f"Error: config.yaml not found in {tool_path}", file=sys.stderr)
|
|
return 1
|
|
|
|
# Read config
|
|
import yaml
|
|
config_yaml = config_path.read_text()
|
|
|
|
# Read README if exists
|
|
readme_path = tool_path / "README.md"
|
|
readme = readme_path.read_text() if readme_path.exists() else ""
|
|
|
|
# Validate
|
|
try:
|
|
data = yaml.safe_load(config_yaml)
|
|
name = data.get("name", "")
|
|
version = data.get("version", "")
|
|
if not name or not version:
|
|
print("Error: config.yaml must have 'name' and 'version' fields", file=sys.stderr)
|
|
return 1
|
|
except yaml.YAMLError as e:
|
|
print(f"Error: Invalid YAML in config.yaml: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
if args.dry_run:
|
|
print("Dry run - validating only")
|
|
print()
|
|
print(f"Would publish:")
|
|
print(f" Name: {name}")
|
|
print(f" Version: {version}")
|
|
print(f" Config: {len(config_yaml)} bytes")
|
|
print(f" README: {len(readme)} bytes")
|
|
return 0
|
|
|
|
# Check for token
|
|
config = load_config()
|
|
if not config.registry.token:
|
|
print("No registry token configured.")
|
|
print()
|
|
print("1. Register at: https://gitea.brrd.tech/registry/register")
|
|
print("2. Generate a token from your dashboard")
|
|
print("3. Enter your token below")
|
|
print()
|
|
|
|
try:
|
|
token = input("Registry token: ").strip()
|
|
if not token:
|
|
print("Cancelled.")
|
|
return 1
|
|
set_registry_token(token)
|
|
print("Token saved.")
|
|
except (EOFError, KeyboardInterrupt):
|
|
print("\nCancelled.")
|
|
return 1
|
|
|
|
print(f"Publishing {name}@{version}...")
|
|
|
|
try:
|
|
client = get_client()
|
|
result = client.publish_tool(config_yaml, readme)
|
|
|
|
pr_url = result.get("pr_url", "")
|
|
status = result.get("status", "")
|
|
|
|
if status == "published" or result.get("version"):
|
|
print(f"Published: {result.get('owner', '')}/{result.get('name', '')}@{result.get('version', version)}")
|
|
elif pr_url:
|
|
print(f"PR created: {pr_url}")
|
|
print("Your tool is pending review.")
|
|
else:
|
|
print("Published successfully!")
|
|
|
|
# Show suggestions if provided (from Phase 6 smart features)
|
|
suggestions = result.get("suggestions", {})
|
|
if suggestions:
|
|
print()
|
|
|
|
# Category suggestion
|
|
cat_suggestion = suggestions.get("category")
|
|
if cat_suggestion and cat_suggestion.get("suggested"):
|
|
confidence = cat_suggestion.get("confidence", 0)
|
|
print(f"Suggested category: {cat_suggestion['suggested']} ({confidence:.0%} confidence)")
|
|
|
|
# Similar tools warning
|
|
similar = suggestions.get("similar_tools", [])
|
|
if similar:
|
|
print("Similar existing tools:")
|
|
for tool in similar[:3]:
|
|
similarity = tool.get("similarity", 0)
|
|
print(f" - {tool.get('name', 'unknown')} ({similarity:.0%} similar)")
|
|
|
|
except RegistryError as e:
|
|
if e.code == "UNAUTHORIZED":
|
|
print("Authentication failed.", file=sys.stderr)
|
|
print("Your token may have expired. Generate a new one from the registry.", file=sys.stderr)
|
|
elif e.code == "INVALID_CONFIG":
|
|
print(f"Invalid tool config: {e.message}", file=sys.stderr)
|
|
print("Check your config.yaml for errors.", file=sys.stderr)
|
|
elif e.code == "VERSION_EXISTS":
|
|
print(f"Version already exists: {e.message}", file=sys.stderr)
|
|
print("Bump the version in config.yaml and try again.", file=sys.stderr)
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
elif e.code == "RATE_LIMITED":
|
|
print("Rate limited. Please wait a moment and try again.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error publishing: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "my-tools":
|
|
try:
|
|
client = get_client()
|
|
tools = client.get_my_tools()
|
|
|
|
if not tools:
|
|
print("You haven't published any tools yet.")
|
|
print("Publish your first tool with: smarttools registry publish")
|
|
return 0
|
|
|
|
print(f"Your published tools ({len(tools)}):\n")
|
|
for tool in tools:
|
|
status = "[DEPRECATED]" if tool.deprecated else ""
|
|
print(f" {tool.owner}/{tool.name} v{tool.version} {status}")
|
|
print(f" Downloads: {tool.downloads}")
|
|
print()
|
|
|
|
except RegistryError as e:
|
|
if e.code == "UNAUTHORIZED":
|
|
print("Not logged in. Set your registry token first:", file=sys.stderr)
|
|
print(" smarttools config set-token <token>", file=sys.stderr)
|
|
print()
|
|
print("Don't have a token? Register at the registry website.", file=sys.stderr)
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print("Could not connect to the registry.", file=sys.stderr)
|
|
print("Check your internet connection or try again later.", file=sys.stderr)
|
|
elif e.code == "RATE_LIMITED":
|
|
print("Rate limited. Please wait a moment and try again.", file=sys.stderr)
|
|
else:
|
|
print(f"Error: {e.message}", file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
elif args.registry_cmd == "browse":
|
|
# Launch TUI browser
|
|
try:
|
|
from .ui_registry import run_registry_browser
|
|
return run_registry_browser()
|
|
except ImportError:
|
|
print("TUI browser requires urwid. Install with:", file=sys.stderr)
|
|
print(" pip install 'smarttools[tui]'", file=sys.stderr)
|
|
print()
|
|
print("Or search from command line:", file=sys.stderr)
|
|
print(" smarttools registry search <query>", file=sys.stderr)
|
|
return 1
|
|
|
|
else:
|
|
# Default: show registry help
|
|
print("Registry commands:")
|
|
print(" search <query> Search for tools")
|
|
print(" install <tool> Install a tool")
|
|
print(" uninstall <tool> Uninstall a tool")
|
|
print(" info <tool> Show tool information")
|
|
print(" update Update local index cache")
|
|
print(" publish [path] Publish a tool")
|
|
print(" my-tools List your published tools")
|
|
print(" browse Browse tools (TUI)")
|
|
return 0
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Project Commands
|
|
# -------------------------------------------------------------------------
|
|
|
|
def cmd_deps(args):
|
|
"""Show project dependencies from smarttools.yaml."""
|
|
manifest = load_manifest()
|
|
|
|
if manifest is None:
|
|
print("No smarttools.yaml found in current project.")
|
|
print("Create one with: smarttools init")
|
|
return 1
|
|
|
|
print(f"Project: {manifest.name} v{manifest.version}")
|
|
print()
|
|
|
|
if not manifest.dependencies:
|
|
print("No dependencies defined.")
|
|
print("Add one with: smarttools add <owner/name>")
|
|
return 0
|
|
|
|
print(f"Dependencies ({len(manifest.dependencies)}):")
|
|
print()
|
|
|
|
for dep in manifest.dependencies:
|
|
# Check if installed
|
|
installed = find_tool(dep.name)
|
|
status = "[installed]" if installed else "[not installed]"
|
|
|
|
print(f" {dep.name}")
|
|
print(f" Version: {dep.version}")
|
|
print(f" Status: {status}")
|
|
print()
|
|
|
|
if manifest.overrides:
|
|
print("Overrides:")
|
|
for name, override in manifest.overrides.items():
|
|
if override.provider:
|
|
print(f" {name}: provider={override.provider}")
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_install_deps(args):
|
|
"""Install dependencies from smarttools.yaml."""
|
|
from .registry_client import get_client, RegistryError
|
|
|
|
manifest = load_manifest()
|
|
|
|
if manifest is None:
|
|
print("No smarttools.yaml found in current project.")
|
|
print("Create one with: smarttools init")
|
|
return 1
|
|
|
|
if not manifest.dependencies:
|
|
print("No dependencies to install.")
|
|
return 0
|
|
|
|
print(f"Installing dependencies for {manifest.name}...")
|
|
print()
|
|
|
|
failed = []
|
|
installed = []
|
|
|
|
for i, dep in enumerate(manifest.dependencies, 1):
|
|
print(f"[{i}/{len(manifest.dependencies)}] {dep.name}@{dep.version}")
|
|
|
|
# Check if already installed
|
|
existing = find_tool(dep.name)
|
|
if existing:
|
|
print(f" Already installed: {existing.full_name}")
|
|
installed.append(dep.name)
|
|
continue
|
|
|
|
try:
|
|
print(f" Downloading...")
|
|
resolved = install_from_registry(dep.name, dep.version)
|
|
print(f" Installed: {resolved.full_name}@{resolved.version}")
|
|
installed.append(dep.name)
|
|
except RegistryError as e:
|
|
if e.code == "TOOL_NOT_FOUND":
|
|
print(f" Not found in registry")
|
|
elif e.code == "VERSION_NOT_FOUND" or e.code == "CONSTRAINT_UNSATISFIABLE":
|
|
print(f" Version {dep.version} not available")
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print(f" Connection failed (check network)")
|
|
else:
|
|
print(f" Failed: {e.message}")
|
|
failed.append(dep.name)
|
|
except Exception as e:
|
|
print(f" Failed: {e}")
|
|
failed.append(dep.name)
|
|
|
|
print()
|
|
print(f"Installed {len(installed)} tools")
|
|
|
|
if failed:
|
|
print(f"Failed: {', '.join(failed)}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_add(args):
|
|
"""Add a tool to project dependencies."""
|
|
tool_spec = args.tool
|
|
version = args.version or "*"
|
|
|
|
# Find or create manifest
|
|
manifest_path = find_manifest()
|
|
if manifest_path:
|
|
manifest = load_manifest(manifest_path)
|
|
else:
|
|
# Create in current directory
|
|
manifest = create_manifest(name=Path.cwd().name)
|
|
manifest_path = Path.cwd() / MANIFEST_FILENAME
|
|
|
|
# Parse tool spec
|
|
parsed = ToolSpec.parse(tool_spec)
|
|
full_name = parsed.full_name
|
|
|
|
# Add dependency
|
|
manifest.add_dependency(full_name, version)
|
|
|
|
# Save
|
|
save_manifest(manifest, manifest_path)
|
|
print(f"Added {full_name}@{version} to {manifest_path.name}")
|
|
|
|
# Install if requested
|
|
if not args.no_install:
|
|
from .registry_client import RegistryError
|
|
print(f"Installing {full_name}...")
|
|
try:
|
|
resolved = install_from_registry(tool_spec, version if version != "*" else None)
|
|
print(f"Installed: {resolved.full_name}@{resolved.version}")
|
|
except RegistryError as e:
|
|
if e.code == "TOOL_NOT_FOUND":
|
|
print(f"Tool not found in registry.", file=sys.stderr)
|
|
print(f"It's been added to your dependencies - you can install it manually later.", file=sys.stderr)
|
|
elif e.code == "CONNECTION_ERROR":
|
|
print(f"Could not connect to registry.", file=sys.stderr)
|
|
print("Run 'smarttools install' to try again later.", file=sys.stderr)
|
|
else:
|
|
print(f"Install failed: {e.message}", file=sys.stderr)
|
|
print("Run 'smarttools install' to try again.", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f"Install failed: {e}", file=sys.stderr)
|
|
print("Run 'smarttools install' to try again.", file=sys.stderr)
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_init(args):
|
|
"""Initialize a new smarttools.yaml."""
|
|
manifest_path = Path.cwd() / MANIFEST_FILENAME
|
|
|
|
if manifest_path.exists() and not args.force:
|
|
print(f"{MANIFEST_FILENAME} already exists. Use --force to overwrite.")
|
|
return 1
|
|
|
|
# Get project name
|
|
default_name = Path.cwd().name
|
|
if args.name:
|
|
name = args.name
|
|
else:
|
|
try:
|
|
name = input(f"Project name [{default_name}]: ").strip() or default_name
|
|
except (EOFError, KeyboardInterrupt):
|
|
print()
|
|
name = default_name
|
|
|
|
# Get version
|
|
if args.version:
|
|
version = args.version
|
|
else:
|
|
try:
|
|
version = input("Version [1.0.0]: ").strip() or "1.0.0"
|
|
except (EOFError, KeyboardInterrupt):
|
|
print()
|
|
version = "1.0.0"
|
|
|
|
# Create manifest
|
|
manifest = create_manifest(name=name, version=version)
|
|
save_manifest(manifest, manifest_path)
|
|
|
|
print(f"Created {MANIFEST_FILENAME}")
|
|
print()
|
|
print("Add dependencies with: smarttools add <owner/name>")
|
|
print("Install them with: smarttools install")
|
|
|
|
return 0
|
|
|
|
|
|
def cmd_config(args):
|
|
"""Manage SmartTools configuration."""
|
|
if args.config_cmd == "show":
|
|
config = load_config()
|
|
print("SmartTools Configuration:")
|
|
print(f" Registry URL: {config.registry.url}")
|
|
print(f" Token: {'***' if config.registry.token else '(not set)'}")
|
|
print(f" Client ID: {config.client_id}")
|
|
print(f" Auto-fetch: {config.auto_fetch_from_registry}")
|
|
if config.default_provider:
|
|
print(f" Default provider: {config.default_provider}")
|
|
return 0
|
|
|
|
elif args.config_cmd == "set-token":
|
|
token = args.token
|
|
set_registry_token(token)
|
|
print("Registry token saved.")
|
|
return 0
|
|
|
|
elif args.config_cmd == "set":
|
|
config = load_config()
|
|
key = args.key
|
|
value = args.value
|
|
|
|
if key == "auto_fetch":
|
|
config.auto_fetch_from_registry = value.lower() in ("true", "1", "yes")
|
|
elif key == "default_provider":
|
|
config.default_provider = value if value else None
|
|
elif key == "registry_url":
|
|
config.registry.url = value
|
|
else:
|
|
print(f"Unknown config key: {key}", file=sys.stderr)
|
|
print("Available keys: auto_fetch, default_provider, registry_url")
|
|
return 1
|
|
|
|
save_config(config)
|
|
print(f"Set {key} = {value}")
|
|
return 0
|
|
|
|
else:
|
|
print("Config commands:")
|
|
print(" show Show current configuration")
|
|
print(" set-token <token> Set registry authentication token")
|
|
print(" set <key> <value> Set a configuration value")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
"""Main CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
prog="smarttools",
|
|
description="A lightweight personal tool builder for AI-powered CLI commands"
|
|
)
|
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
|
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
|
|
# No command = launch UI
|
|
# 'list' command
|
|
p_list = subparsers.add_parser("list", help="List all tools")
|
|
p_list.set_defaults(func=cmd_list)
|
|
|
|
# 'create' command
|
|
p_create = subparsers.add_parser("create", help="Create a new tool")
|
|
p_create.add_argument("name", help="Tool name")
|
|
p_create.add_argument("-d", "--description", help="Tool description")
|
|
p_create.add_argument("-p", "--prompt", help="Prompt template")
|
|
p_create.add_argument("--provider", help="AI provider (default: mock)")
|
|
p_create.add_argument("-f", "--force", action="store_true", help="Overwrite existing")
|
|
p_create.set_defaults(func=cmd_create)
|
|
|
|
# 'edit' command
|
|
p_edit = subparsers.add_parser("edit", help="Edit a tool config")
|
|
p_edit.add_argument("name", help="Tool name")
|
|
p_edit.set_defaults(func=cmd_edit)
|
|
|
|
# 'delete' command
|
|
p_delete = subparsers.add_parser("delete", help="Delete a tool")
|
|
p_delete.add_argument("name", help="Tool name")
|
|
p_delete.add_argument("-f", "--force", action="store_true", help="Skip confirmation")
|
|
p_delete.set_defaults(func=cmd_delete)
|
|
|
|
# 'test' command
|
|
p_test = subparsers.add_parser("test", help="Test a tool with mock provider")
|
|
p_test.add_argument("name", help="Tool name")
|
|
p_test.add_argument("-i", "--input", help="Input file for testing")
|
|
p_test.add_argument("--dry-run", action="store_true", help="Show prompt only")
|
|
p_test.set_defaults(func=cmd_test)
|
|
|
|
# 'run' command
|
|
p_run = subparsers.add_parser("run", help="Run a tool")
|
|
p_run.add_argument("name", help="Tool name")
|
|
p_run.add_argument("-i", "--input", help="Input file (reads from stdin if piped)")
|
|
p_run.add_argument("-o", "--output", help="Output file (writes to stdout if omitted)")
|
|
p_run.add_argument("--stdin", action="store_true", help="Read input interactively (type then Ctrl+D)")
|
|
p_run.add_argument("-p", "--provider", help="Override provider")
|
|
p_run.add_argument("--dry-run", action="store_true", help="Show what would happen without executing")
|
|
p_run.add_argument("--show-prompt", action="store_true", help="Show prompts in addition to output")
|
|
p_run.add_argument("-v", "--verbose", action="store_true", help="Show debug information")
|
|
p_run.add_argument("tool_args", nargs="*", help="Additional tool-specific arguments")
|
|
p_run.set_defaults(func=cmd_run)
|
|
|
|
# 'ui' command (explicit)
|
|
p_ui = subparsers.add_parser("ui", help="Launch interactive UI")
|
|
p_ui.set_defaults(func=cmd_ui)
|
|
|
|
# 'refresh' command
|
|
p_refresh = subparsers.add_parser("refresh", help="Refresh all wrapper scripts")
|
|
p_refresh.set_defaults(func=cmd_refresh)
|
|
|
|
# 'docs' command
|
|
p_docs = subparsers.add_parser("docs", help="View or edit tool documentation")
|
|
p_docs.add_argument("name", help="Tool name")
|
|
p_docs.add_argument("-e", "--edit", action="store_true", help="Edit/create README in $EDITOR")
|
|
p_docs.set_defaults(func=cmd_docs)
|
|
|
|
# 'providers' command
|
|
p_providers = subparsers.add_parser("providers", help="Manage AI providers")
|
|
providers_sub = p_providers.add_subparsers(dest="providers_cmd", help="Provider commands")
|
|
|
|
# providers list
|
|
p_prov_list = providers_sub.add_parser("list", help="List all providers and their status")
|
|
p_prov_list.set_defaults(func=cmd_providers)
|
|
|
|
# providers check
|
|
p_prov_check = providers_sub.add_parser("check", help="Check which providers are available")
|
|
p_prov_check.set_defaults(func=cmd_providers)
|
|
|
|
# providers install
|
|
p_prov_install = providers_sub.add_parser("install", help="Interactive guide to install AI providers")
|
|
p_prov_install.set_defaults(func=cmd_providers)
|
|
|
|
# providers add
|
|
p_prov_add = providers_sub.add_parser("add", help="Add or update a provider")
|
|
p_prov_add.add_argument("name", help="Provider name")
|
|
p_prov_add.add_argument("command", help="Command to run (e.g., 'claude -p')")
|
|
p_prov_add.add_argument("-d", "--description", help="Provider description")
|
|
p_prov_add.set_defaults(func=cmd_providers)
|
|
|
|
# providers remove
|
|
p_prov_remove = providers_sub.add_parser("remove", help="Remove a provider")
|
|
p_prov_remove.add_argument("name", help="Provider name")
|
|
p_prov_remove.set_defaults(func=cmd_providers)
|
|
|
|
# providers test
|
|
p_prov_test = providers_sub.add_parser("test", help="Test a provider")
|
|
p_prov_test.add_argument("name", help="Provider name")
|
|
p_prov_test.set_defaults(func=cmd_providers)
|
|
|
|
# Default for providers with no subcommand
|
|
p_providers.set_defaults(func=lambda args: cmd_providers(args) if args.providers_cmd else (setattr(args, 'providers_cmd', 'list') or cmd_providers(args)))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Registry Commands
|
|
# -------------------------------------------------------------------------
|
|
p_registry = subparsers.add_parser("registry", help="Registry commands (search, install, publish)")
|
|
registry_sub = p_registry.add_subparsers(dest="registry_cmd", help="Registry commands")
|
|
|
|
# registry search
|
|
p_reg_search = registry_sub.add_parser("search", help="Search for tools")
|
|
p_reg_search.add_argument("query", help="Search query")
|
|
p_reg_search.add_argument("-c", "--category", help="Filter by category")
|
|
p_reg_search.add_argument("-l", "--limit", type=int, help="Max results (default: 20)")
|
|
p_reg_search.set_defaults(func=cmd_registry)
|
|
|
|
# registry install
|
|
p_reg_install = registry_sub.add_parser("install", help="Install a tool from registry")
|
|
p_reg_install.add_argument("tool", help="Tool to install (owner/name or name)")
|
|
p_reg_install.add_argument("-v", "--version", help="Version constraint")
|
|
p_reg_install.set_defaults(func=cmd_registry)
|
|
|
|
# registry uninstall
|
|
p_reg_uninstall = registry_sub.add_parser("uninstall", help="Uninstall a tool")
|
|
p_reg_uninstall.add_argument("tool", help="Tool to uninstall (owner/name)")
|
|
p_reg_uninstall.set_defaults(func=cmd_registry)
|
|
|
|
# registry info
|
|
p_reg_info = registry_sub.add_parser("info", help="Show tool information")
|
|
p_reg_info.add_argument("tool", help="Tool name (owner/name)")
|
|
p_reg_info.set_defaults(func=cmd_registry)
|
|
|
|
# registry update
|
|
p_reg_update = registry_sub.add_parser("update", help="Update local index cache")
|
|
p_reg_update.set_defaults(func=cmd_registry)
|
|
|
|
# registry publish
|
|
p_reg_publish = registry_sub.add_parser("publish", help="Publish a tool to registry")
|
|
p_reg_publish.add_argument("path", nargs="?", help="Path to tool directory (default: current dir)")
|
|
p_reg_publish.add_argument("--dry-run", action="store_true", help="Validate without publishing")
|
|
p_reg_publish.set_defaults(func=cmd_registry)
|
|
|
|
# registry my-tools
|
|
p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools")
|
|
p_reg_mytools.set_defaults(func=cmd_registry)
|
|
|
|
# registry browse
|
|
p_reg_browse = registry_sub.add_parser("browse", help="Browse tools (TUI)")
|
|
p_reg_browse.set_defaults(func=cmd_registry)
|
|
|
|
# Default for registry with no subcommand
|
|
p_registry.set_defaults(func=lambda args: cmd_registry(args) if args.registry_cmd else (setattr(args, 'registry_cmd', None) or cmd_registry(args)))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Project Commands
|
|
# -------------------------------------------------------------------------
|
|
|
|
# 'deps' command
|
|
p_deps = subparsers.add_parser("deps", help="Show project dependencies")
|
|
p_deps.set_defaults(func=cmd_deps)
|
|
|
|
# 'install' command (for dependencies)
|
|
p_install = subparsers.add_parser("install", help="Install dependencies from smarttools.yaml")
|
|
p_install.set_defaults(func=cmd_install_deps)
|
|
|
|
# 'add' command
|
|
p_add = subparsers.add_parser("add", help="Add a tool to project dependencies")
|
|
p_add.add_argument("tool", help="Tool to add (owner/name)")
|
|
p_add.add_argument("-v", "--version", help="Version constraint (default: *)")
|
|
p_add.add_argument("--no-install", action="store_true", help="Don't install after adding")
|
|
p_add.set_defaults(func=cmd_add)
|
|
|
|
# 'init' command
|
|
p_init = subparsers.add_parser("init", help="Initialize smarttools.yaml")
|
|
p_init.add_argument("-n", "--name", help="Project name")
|
|
p_init.add_argument("-v", "--version", help="Project version")
|
|
p_init.add_argument("-f", "--force", action="store_true", help="Overwrite existing")
|
|
p_init.set_defaults(func=cmd_init)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Config Commands
|
|
# -------------------------------------------------------------------------
|
|
p_config = subparsers.add_parser("config", help="Manage configuration")
|
|
config_sub = p_config.add_subparsers(dest="config_cmd", help="Config commands")
|
|
|
|
# config show
|
|
p_cfg_show = config_sub.add_parser("show", help="Show current configuration")
|
|
p_cfg_show.set_defaults(func=cmd_config)
|
|
|
|
# config set-token
|
|
p_cfg_token = config_sub.add_parser("set-token", help="Set registry authentication token")
|
|
p_cfg_token.add_argument("token", help="Registry token")
|
|
p_cfg_token.set_defaults(func=cmd_config)
|
|
|
|
# config set
|
|
p_cfg_set = config_sub.add_parser("set", help="Set a configuration value")
|
|
p_cfg_set.add_argument("key", help="Config key")
|
|
p_cfg_set.add_argument("value", help="Config value")
|
|
p_cfg_set.set_defaults(func=cmd_config)
|
|
|
|
# Default for config with no subcommand
|
|
p_config.set_defaults(func=lambda args: cmd_config(args) if args.config_cmd else (setattr(args, 'config_cmd', 'show') or cmd_config(args)))
|
|
|
|
args = parser.parse_args()
|
|
|
|
# If no command, launch UI
|
|
if args.command is None:
|
|
return cmd_ui(args)
|
|
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|