Rename project from SmartTools to CmdForge
Major rename operation: - Rename Python package: smarttools -> cmdforge - Update CLI entry point: smarttools -> cmdforge - Update all imports and module references - Update pyproject.toml with new name, URLs, entry point - Update all HTML templates with new branding - Update documentation (CLAUDE.md, README.md, docs/*, wiki/*) - Update environment variables: - SMARTTOOLS_ENV -> CMDFORGE_ENV - SMARTTOOLS_REGISTRY_DB -> CMDFORGE_REGISTRY_DB - SMARTTOOLS_TOKEN -> CMDFORGE_TOKEN - SMARTTOOLS_REGISTRY_* -> CMDFORGE_REGISTRY_* - Update Dockerfile and docker-compose.yml - Update tests to use new package name - Update scripts and examples - Update package.json and tailwind.config.js All 158 unit tests pass. The CLI is working correctly with the new cmdforge command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
"""Registry commands."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from ..config import load_config, set_registry_token
|
||||
from ..resolver import install_from_registry, uninstall_tool, ToolSpec
|
||||
|
||||
|
||||
def cmd_registry(args):
|
||||
"""Handle registry subcommands."""
|
||||
from ..registry_client import RegistryClient, RegistryError, RateLimitError, get_client
|
||||
|
||||
if args.registry_cmd == "search":
|
||||
return _cmd_registry_search(args)
|
||||
elif args.registry_cmd == "install":
|
||||
return _cmd_registry_install(args)
|
||||
elif args.registry_cmd == "uninstall":
|
||||
return _cmd_registry_uninstall(args)
|
||||
elif args.registry_cmd == "info":
|
||||
return _cmd_registry_info(args)
|
||||
elif args.registry_cmd == "update":
|
||||
return _cmd_registry_update(args)
|
||||
elif args.registry_cmd == "publish":
|
||||
return _cmd_registry_publish(args)
|
||||
elif args.registry_cmd == "my-tools":
|
||||
return _cmd_registry_my_tools(args)
|
||||
elif args.registry_cmd == "browse":
|
||||
return _cmd_registry_browse(args)
|
||||
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
|
||||
|
||||
|
||||
def _cmd_registry_search(args):
|
||||
"""Search for tools in the registry."""
|
||||
from ..registry_client import RegistryError, get_client
|
||||
|
||||
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: cmdforge config show", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_registry_install(args):
|
||||
"""Install a tool from the registry."""
|
||||
from ..registry_client import RegistryError
|
||||
from ..tool import BIN_DIR
|
||||
|
||||
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
|
||||
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: cmdforge 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
|
||||
|
||||
|
||||
def _cmd_registry_uninstall(args):
|
||||
"""Uninstall a tool."""
|
||||
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
|
||||
|
||||
|
||||
def _cmd_registry_info(args):
|
||||
"""Show tool information."""
|
||||
from ..registry_client import RegistryError, get_client
|
||||
|
||||
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: cmdforge 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: cmdforge 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
|
||||
|
||||
|
||||
def _cmd_registry_update(args):
|
||||
"""Update local index cache."""
|
||||
from ..registry_client import RegistryError, get_client
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _cmd_registry_publish(args):
|
||||
"""Publish a tool to the registry."""
|
||||
from ..registry_client import RegistryError, get_client
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
def _cmd_registry_my_tools(args):
|
||||
"""List your published tools."""
|
||||
from ..registry_client import RegistryError, get_client
|
||||
|
||||
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: cmdforge 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(" cmdforge 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
|
||||
|
||||
|
||||
def _cmd_registry_browse(args):
|
||||
"""Browse tools (TUI)."""
|
||||
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 'cmdforge[tui]'", file=sys.stderr)
|
||||
print()
|
||||
print("Or search from command line:", file=sys.stderr)
|
||||
print(" cmdforge registry search <query>", file=sys.stderr)
|
||||
return 1
|
||||
Reference in New Issue
Block a user