656 lines
31 KiB
Python
656 lines
31 KiB
Python
"""CLI entry point for CmdForge."""
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from .. import __version__
|
|
|
|
from .tool_commands import (
|
|
cmd_list, cmd_create, cmd_edit, cmd_delete, cmd_test, cmd_run,
|
|
cmd_ui, cmd_refresh, cmd_docs, cmd_check
|
|
)
|
|
from .provider_commands import cmd_providers
|
|
from .registry_commands import cmd_registry
|
|
from .collections_commands import cmd_collections
|
|
from .project_commands import cmd_deps, cmd_deps_tree, cmd_install_deps, cmd_add, cmd_remove, cmd_init, cmd_lock, cmd_verify
|
|
from .config_commands import cmd_config
|
|
from .settings_commands import cmd_settings
|
|
from .system_deps_commands import cmd_system_deps
|
|
from .mcp_commands import cmd_mcp
|
|
|
|
|
|
def main():
|
|
"""Main CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
prog="cmdforge",
|
|
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("-c", "--category", choices=["Text", "Developer", "Data", "Other"], default="Other", help="Tool category")
|
|
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("--auto-install", action="store_true", help="Automatically install missing tool dependencies")
|
|
p_run.add_argument("tool_args", nargs=argparse.REMAINDER, help="Additional tool-specific arguments (use -- to separate)")
|
|
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)
|
|
|
|
# 'inspect' command
|
|
p_inspect = subparsers.add_parser("inspect", help="Run preflight analysis on a tool")
|
|
p_inspect.add_argument("name", help="Tool name")
|
|
p_inspect.add_argument(
|
|
"--registry", action="store_true",
|
|
help="Include similar tools from the configured registry",
|
|
)
|
|
p_inspect.add_argument(
|
|
"--save-baseline", action="store_true",
|
|
help="Save the current passing conformance evidence as the baseline",
|
|
)
|
|
p_inspect.set_defaults(func=cmd_inspect)
|
|
|
|
# 'check' command
|
|
p_check = subparsers.add_parser("check", help="Check dependencies for a tool (meta-tools)")
|
|
p_check.add_argument("name", help="Tool name")
|
|
p_check.set_defaults(func=cmd_check)
|
|
|
|
# '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 discover
|
|
p_prov_discover = providers_sub.add_parser("discover", help="Scan system for installed CLIs and API keys")
|
|
p_prov_discover.add_argument("--add", action="store_true", help="Add newly discovered providers to the config")
|
|
p_prov_discover.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, or base URL for API providers")
|
|
p_prov_add.add_argument("-d", "--description", help="Provider description")
|
|
p_prov_add.add_argument("--type", choices=["subprocess", "api", "pty"], help="Provider type")
|
|
p_prov_add.add_argument("--model", help="Model ID for API providers")
|
|
p_prov_add.add_argument("--api-key-env", help="Environment variable containing the API key")
|
|
p_prov_add.add_argument("--tag", action="append", dest="tags", help="Provider tag (repeatable)")
|
|
p_prov_add.add_argument("--fallback", help="Fallback provider name; pass an empty value to clear")
|
|
p_prov_add.add_argument(
|
|
"--fallback-chain",
|
|
help="Preset name or comma-separated provider names; pass an empty value to clear",
|
|
)
|
|
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)
|
|
|
|
# providers for-tools
|
|
p_prov_for_tools = providers_sub.add_parser("for-tools", help="List providers used by specified tools")
|
|
p_prov_for_tools.add_argument("tools", nargs="+", help="Tool names to check")
|
|
p_prov_for_tools.add_argument("--warm", action="store_true", help="Warm up the providers (send minimal prompt)")
|
|
p_prov_for_tools.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("-t", "--tag", action="append", dest="tags", help="Filter by tag (repeatable, AND logic)")
|
|
p_reg_search.add_argument("-o", "--owner", help="Filter by publisher/owner")
|
|
p_reg_search.add_argument("--min-downloads", type=int, help="Minimum downloads")
|
|
p_reg_search.add_argument("--popular", action="store_true", help="Shortcut for --min-downloads 100")
|
|
p_reg_search.add_argument("--new", action="store_true", help="Shortcut for --max-downloads 10")
|
|
p_reg_search.add_argument("--since", help="Published after date (YYYY-MM-DD)")
|
|
p_reg_search.add_argument("--before", help="Published before date (YYYY-MM-DD)")
|
|
p_reg_search.add_argument("-s", "--sort", choices=["relevance", "downloads", "published_at", "name"], default="relevance", help="Sort by field")
|
|
p_reg_search.add_argument("-l", "--limit", type=int, help="Max results (default: 20)")
|
|
p_reg_search.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_reg_search.add_argument("--show-facets", action="store_true", help="Show category/tag counts")
|
|
p_reg_search.add_argument("--deprecated", action="store_true", help="Include deprecated tools")
|
|
p_reg_search.set_defaults(func=cmd_registry)
|
|
|
|
# registry tags
|
|
p_reg_tags = registry_sub.add_parser("tags", help="List available tags")
|
|
p_reg_tags.add_argument("-c", "--category", help="Filter tags by category")
|
|
p_reg_tags.add_argument("-l", "--limit", type=int, default=50, help="Max tags to show (default: 50)")
|
|
p_reg_tags.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_reg_tags.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.add_argument("-y", "--yes", action="store_true", help="Auto-install system packages without prompting")
|
|
p_reg_install.add_argument("--no-system-deps", action="store_true", help="Skip system dependency check")
|
|
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.add_argument("-f", "--force", action="store_true", help="Skip confirmation prompts")
|
|
p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')")
|
|
p_reg_publish.set_defaults(func=cmd_registry)
|
|
|
|
# registry update-readme
|
|
p_reg_update_readme = registry_sub.add_parser("update-readme", help="Update README for a published tool")
|
|
p_reg_update_readme.add_argument("tool", nargs="?", default="", help="Tool name (local name, will resolve owner)")
|
|
p_reg_update_readme.add_argument("--all", action="store_true", dest="update_all", help="Update README for all published tools that have a local README.md")
|
|
p_reg_update_readme.set_defaults(func=cmd_registry)
|
|
|
|
# registry describe (semantic search)
|
|
p_reg_describe = registry_sub.add_parser("describe", help="Find tools by describing what you need")
|
|
p_reg_describe.add_argument("query", help="Natural language description (e.g., 'something that analyzes CSV files')")
|
|
p_reg_describe.add_argument("-n", "--limit", type=int, default=20, help="Max results (default: 20)")
|
|
p_reg_describe.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_reg_describe.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 status
|
|
p_reg_status = registry_sub.add_parser("status", help="Check moderation status of a tool")
|
|
p_reg_status.add_argument("tool", help="Tool name to check status for")
|
|
p_reg_status.add_argument("--sync", action="store_true", help="Sync status from server and update local config")
|
|
p_reg_status.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)
|
|
|
|
# registry config (admin settings management)
|
|
p_reg_config = registry_sub.add_parser("config", help="Manage registry settings (admin)")
|
|
p_reg_config.add_argument("action", nargs="?", choices=["list", "get", "set"], default="list",
|
|
help="Action to perform (default: list)")
|
|
p_reg_config.add_argument("key", nargs="?", help="Setting key (for get/set)")
|
|
p_reg_config.add_argument("value", nargs="?", help="Setting value (for set)")
|
|
p_reg_config.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_reg_config.add_argument("--category", "-c", help="Filter by category (for list)")
|
|
p_reg_config.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)))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Collections Commands
|
|
# -------------------------------------------------------------------------
|
|
p_collections = subparsers.add_parser("collections", help="Manage tool collections")
|
|
collections_sub = p_collections.add_subparsers(dest="collections_cmd", help="Collections commands")
|
|
|
|
# collections list
|
|
p_coll_list = collections_sub.add_parser("list", help="List available collections")
|
|
p_coll_list.add_argument("--local", action="store_true", help="Show local collections only")
|
|
p_coll_list.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_coll_list.set_defaults(func=cmd_collections)
|
|
|
|
# collections info
|
|
p_coll_info = collections_sub.add_parser("info", help="Show registry collection details")
|
|
p_coll_info.add_argument("name", help="Collection name")
|
|
p_coll_info.add_argument("--json", action="store_true", help="Output as JSON")
|
|
p_coll_info.set_defaults(func=cmd_collections)
|
|
|
|
# collections install
|
|
p_coll_install = collections_sub.add_parser("install", help="Install all tools in a registry collection")
|
|
p_coll_install.add_argument("name", help="Collection name")
|
|
p_coll_install.add_argument("--pinned", action="store_true", help="Use pinned versions from collection")
|
|
p_coll_install.set_defaults(func=cmd_collections)
|
|
|
|
# collections create (local)
|
|
p_coll_create = collections_sub.add_parser("create", help="Create a new local collection")
|
|
p_coll_create.add_argument("name", help="Collection name (slug)")
|
|
p_coll_create.set_defaults(func=cmd_collections)
|
|
|
|
# collections show (local)
|
|
p_coll_show = collections_sub.add_parser("show", help="Show local collection details")
|
|
p_coll_show.add_argument("name", help="Collection name")
|
|
p_coll_show.set_defaults(func=cmd_collections)
|
|
|
|
# collections add (local)
|
|
p_coll_add = collections_sub.add_parser("add", help="Add tool to collection")
|
|
p_coll_add.add_argument("collection", help="Collection name")
|
|
p_coll_add.add_argument("tool", help="Tool reference (name or owner/name)")
|
|
p_coll_add.add_argument("--version", "-v", help="Pin to specific version")
|
|
p_coll_add.set_defaults(func=cmd_collections)
|
|
|
|
# collections remove (local)
|
|
p_coll_remove = collections_sub.add_parser("remove", help="Remove tool from collection")
|
|
p_coll_remove.add_argument("collection", help="Collection name")
|
|
p_coll_remove.add_argument("tool", help="Tool reference")
|
|
p_coll_remove.set_defaults(func=cmd_collections)
|
|
|
|
# collections delete (local)
|
|
p_coll_delete = collections_sub.add_parser("delete", help="Delete local collection")
|
|
p_coll_delete.add_argument("name", help="Collection name")
|
|
p_coll_delete.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
p_coll_delete.set_defaults(func=cmd_collections)
|
|
|
|
# collections publish
|
|
p_coll_publish = collections_sub.add_parser("publish", help="Publish collection to registry")
|
|
p_coll_publish.add_argument("name", help="Collection name")
|
|
p_coll_publish.add_argument("--dry-run", action="store_true", help="Validate without publishing")
|
|
p_coll_publish.add_argument("--continue", dest="resume", action="store_true",
|
|
help="Resume pending publish after tools approved")
|
|
p_coll_publish.set_defaults(func=cmd_collections)
|
|
|
|
# collections status
|
|
p_coll_status = collections_sub.add_parser("status", help="Check pending collection status")
|
|
p_coll_status.add_argument("name", help="Collection name")
|
|
p_coll_status.set_defaults(func=cmd_collections)
|
|
|
|
# Default for collections with no subcommand
|
|
p_collections.set_defaults(func=lambda args: cmd_collections(args) if args.collections_cmd else (setattr(args, 'collections_cmd', None) or cmd_collections(args)))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Project Commands
|
|
# -------------------------------------------------------------------------
|
|
|
|
# 'deps' command with subcommands
|
|
p_deps = subparsers.add_parser("deps", help="Show project dependencies")
|
|
deps_sub = p_deps.add_subparsers(dest="deps_cmd", help="Deps commands")
|
|
|
|
# deps tree
|
|
p_deps_tree = deps_sub.add_parser("tree", help="Show dependency tree")
|
|
p_deps_tree.add_argument("--verbose", "-v", action="store_true", help="Show detailed info")
|
|
p_deps_tree.set_defaults(func=cmd_deps_tree)
|
|
|
|
# Default for deps with no subcommand shows basic deps list
|
|
p_deps.set_defaults(func=cmd_deps)
|
|
|
|
# 'install' command (for dependencies)
|
|
p_install = subparsers.add_parser("install", help="Install dependencies from cmdforge.yaml")
|
|
p_install.add_argument("--dry-run", action="store_true", help="Show what would be installed without installing")
|
|
p_install.add_argument("--force", action="store_true", help="Install despite version conflicts or stale lock")
|
|
p_install.add_argument("--verbose", "-v", action="store_true", help="Show detailed resolution info")
|
|
p_install.add_argument("--ignore-lock", action="store_true", help="Ignore lock file and resolve fresh from manifest")
|
|
p_install.add_argument("--frozen", action="store_true", help="Fail if lock file is missing (for CI)")
|
|
p_install.add_argument("--strict-frozen", action="store_true", help="Fail if lock is missing or stale (stricter CI mode)")
|
|
p_install.set_defaults(func=cmd_install_deps)
|
|
|
|
# 'lock' command
|
|
p_lock = subparsers.add_parser("lock", help="Generate or update cmdforge.lock")
|
|
p_lock.add_argument("--force", "-f", action="store_true", help="Force regenerate even if up to date")
|
|
p_lock.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
|
|
p_lock.set_defaults(func=cmd_lock)
|
|
|
|
# 'verify' command
|
|
p_verify = subparsers.add_parser("verify", help="Verify installed tools match lock file")
|
|
p_verify.set_defaults(func=cmd_verify)
|
|
|
|
# '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)
|
|
|
|
# 'remove' command
|
|
p_remove = subparsers.add_parser("remove", help="Remove a tool from project dependencies")
|
|
p_remove.add_argument("tool", help="Tool to remove (owner/name or just name)")
|
|
p_remove.set_defaults(func=cmd_remove)
|
|
|
|
# 'init' command
|
|
p_init = subparsers.add_parser("init", help="Initialize cmdforge.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)
|
|
|
|
# config connect
|
|
p_cfg_connect = config_sub.add_parser("connect", help="Connect this app to your CmdForge account")
|
|
p_cfg_connect.add_argument("username", help="Your CmdForge username")
|
|
p_cfg_connect.set_defaults(func=cmd_config)
|
|
|
|
# config disconnect
|
|
p_cfg_disconnect = config_sub.add_parser("disconnect", help="Disconnect from registry (clear token)")
|
|
p_cfg_disconnect.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)))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Settings Commands
|
|
# -------------------------------------------------------------------------
|
|
p_settings = subparsers.add_parser("settings", help="Manage tool settings")
|
|
p_settings.add_argument("tool", help="Tool name")
|
|
settings_sub = p_settings.add_subparsers(dest="settings_cmd")
|
|
|
|
# settings show (default)
|
|
p_settings_show = settings_sub.add_parser("show", help="Show current settings")
|
|
p_settings_show.set_defaults(func=cmd_settings)
|
|
|
|
# settings edit
|
|
p_settings_edit = settings_sub.add_parser("edit", help="Edit settings in $EDITOR")
|
|
p_settings_edit.set_defaults(func=cmd_settings)
|
|
|
|
# settings reset
|
|
p_settings_reset = settings_sub.add_parser("reset", help="Reset settings to defaults")
|
|
p_settings_reset.add_argument("-f", "--force", action="store_true", help="Skip confirmation")
|
|
p_settings_reset.set_defaults(func=cmd_settings)
|
|
|
|
# settings diff
|
|
p_settings_diff = settings_sub.add_parser("diff", help="Show differences from defaults")
|
|
p_settings_diff.set_defaults(func=cmd_settings)
|
|
|
|
# Default for settings with no subcommand
|
|
p_settings.set_defaults(func=cmd_settings)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# System Dependencies Commands
|
|
# -------------------------------------------------------------------------
|
|
p_sysdeps = subparsers.add_parser("system-deps", help="Manage system package dependencies")
|
|
p_sysdeps.add_argument("tool", help="Tool name")
|
|
sysdeps_sub = p_sysdeps.add_subparsers(dest="system_deps_cmd")
|
|
|
|
# system-deps install
|
|
p_sysdeps_install = sysdeps_sub.add_parser("install", help="Install missing system packages")
|
|
p_sysdeps_install.add_argument("-y", "--yes", action="store_true", help="Install without prompting")
|
|
p_sysdeps_install.set_defaults(func=cmd_system_deps)
|
|
|
|
# Default for system-deps with no subcommand (show status)
|
|
p_sysdeps.set_defaults(func=cmd_system_deps)
|
|
|
|
# mcp command
|
|
p_mcp = subparsers.add_parser("mcp", help="Manage MCP servers")
|
|
mcp_sub = p_mcp.add_subparsers(dest="mcp_cmd", help="MCP commands")
|
|
|
|
# mcp serve
|
|
p_mcp_serve = mcp_sub.add_parser("serve", help="Start CmdForge as an MCP server")
|
|
p_mcp_serve.add_argument(
|
|
"--transport", choices=["stdio"], default="stdio",
|
|
help="Transport (M7.3 supports stdio)",
|
|
)
|
|
p_mcp_serve.set_defaults(func=cmd_mcp)
|
|
|
|
# mcp list
|
|
p_mcp_list = mcp_sub.add_parser("list", help="List configured MCP servers")
|
|
p_mcp_list.set_defaults(func=cmd_mcp)
|
|
|
|
# mcp connect
|
|
p_mcp_connect = mcp_sub.add_parser("connect", help="Connect to an MCP server and discover tools")
|
|
p_mcp_connect.add_argument("name", help="Server name from mcp.yaml")
|
|
p_mcp_connect.set_defaults(func=cmd_mcp)
|
|
|
|
# mcp add
|
|
p_mcp_add = mcp_sub.add_parser("add", help="Add an MCP server")
|
|
p_mcp_add.add_argument("name", help="Server name")
|
|
p_mcp_add.add_argument(
|
|
"--transport", choices=["stdio"], default="stdio",
|
|
help="Transport type (M7.2 supports stdio)",
|
|
)
|
|
p_mcp_add.add_argument("--command", help="Executable for stdio servers")
|
|
p_mcp_add.add_argument(
|
|
"--arg", dest="server_args", action="append", default=[], metavar="VALUE",
|
|
help="Command argument; repeat for each argument (use --arg=-y for leading dashes)",
|
|
)
|
|
p_mcp_add.add_argument("--cwd", help="Working directory")
|
|
p_mcp_add.add_argument(
|
|
"--env", action="append", default=[], metavar="NAME=VALUE",
|
|
help="Server environment entry; repeat as needed (supports ${NAME} references)",
|
|
)
|
|
p_mcp_add.add_argument(
|
|
"--inherit-env", action="append", default=[], metavar="NAME",
|
|
help="Additionally inherit this environment variable; repeat as needed",
|
|
)
|
|
p_mcp_add.add_argument("--timeout", type=float, default=30, help="Per-operation timeout in seconds")
|
|
p_mcp_add.add_argument("-d", "--description", help="Server description")
|
|
p_mcp_add.add_argument("--force", action="store_true", help="Overwrite existing server")
|
|
p_mcp_add.set_defaults(func=cmd_mcp)
|
|
|
|
# mcp remove
|
|
p_mcp_remove = mcp_sub.add_parser("remove", help="Remove an MCP server")
|
|
p_mcp_remove.add_argument("name", help="Server name")
|
|
p_mcp_remove.set_defaults(func=cmd_mcp)
|
|
|
|
# Default for mcp with no subcommand (list)
|
|
p_mcp.set_defaults(func=cmd_mcp, mcp_cmd="list")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# If no command, launch UI
|
|
if args.command is None:
|
|
return cmd_ui(args)
|
|
|
|
return args.func(args)
|
|
|
|
|
|
def cmd_inspect(args):
|
|
"""Run preflight analysis on a tool."""
|
|
from ..preflight import analyze_tool
|
|
from ..tool import load_tool
|
|
|
|
tool = load_tool(args.name)
|
|
if not tool:
|
|
print(f"Error: Tool '{args.name}' not found.", file=sys.stderr)
|
|
return 1
|
|
|
|
registry_client = None
|
|
if getattr(args, "registry", False):
|
|
from ..registry_client import get_client
|
|
registry_client = get_client()
|
|
report = analyze_tool(
|
|
tool,
|
|
registry_client=registry_client,
|
|
include_contract_tests=True,
|
|
)
|
|
|
|
if report.errors:
|
|
print(f"Errors ({len(report.errors)}):")
|
|
for e in report.errors:
|
|
print(f" ERROR: {e}")
|
|
print()
|
|
|
|
if report.warnings:
|
|
print(f"Warnings ({len(report.warnings)}):")
|
|
for w in report.warnings:
|
|
print(f" WARN: {w}")
|
|
print()
|
|
|
|
if report.suggestions:
|
|
print(f"Suggestions ({len(report.suggestions)}):")
|
|
for s in report.suggestions:
|
|
print(f" HINT: {s}")
|
|
print()
|
|
|
|
if report.similar_tools:
|
|
print(f"Similar registry tools ({len(report.similar_tools)}):")
|
|
for t in report.similar_tools:
|
|
print(f" - {t['name']}: {t.get('description', '')[:80]}")
|
|
print()
|
|
|
|
proposal = report.contract_proposal or {}
|
|
proposed_schemas = {
|
|
key: proposal.get(key)
|
|
for key in ("input_schema", "output_schema")
|
|
if proposal.get(key) is not None
|
|
}
|
|
if proposed_schemas:
|
|
import yaml
|
|
|
|
print(f"Contract proposal ({proposal.get('confidence', 'low')} confidence):")
|
|
print(yaml.safe_dump(proposed_schemas, sort_keys=False).rstrip())
|
|
for evidence in proposal.get("evidence", []):
|
|
print(f" EVIDENCE: {evidence}")
|
|
print(" Contracts are suggestions only and were not saved.")
|
|
print()
|
|
|
|
if report.generated_tests:
|
|
print(f"Contract conformance ({len(report.generated_tests)} case(s)):")
|
|
for result in report.generated_tests:
|
|
print(
|
|
f" {result['state'].upper()}: {result['step']} — "
|
|
f"{result.get('detail', '')}"
|
|
)
|
|
print()
|
|
|
|
if report.regression:
|
|
regression = report.regression
|
|
status = (
|
|
"REGRESSION" if regression.get("has_regressions")
|
|
else "REVIEW" if regression.get("contract_changed")
|
|
else "STABLE"
|
|
)
|
|
print(
|
|
f"Regression comparison: {status} — "
|
|
f"{regression.get('summary', 'no changes')}"
|
|
)
|
|
print(
|
|
f" Baseline {regression.get('baseline_version') or '(unversioned)'} "
|
|
f"-> current {regression.get('current_version') or '(unversioned)'}"
|
|
)
|
|
print()
|
|
|
|
if report.compatibility:
|
|
print(f"ToolStep compatibility ({len(report.compatibility)}):")
|
|
for finding in report.compatibility:
|
|
print(
|
|
f" {str(finding.get('state', 'unknown')).upper()}: "
|
|
f"step {finding.get('step', '?')} -> "
|
|
f"{finding.get('tool', 'unknown')} — {finding.get('detail', '')}"
|
|
)
|
|
print()
|
|
|
|
if getattr(args, "save_baseline", False):
|
|
from ..contract_testing import (
|
|
ConformanceReport,
|
|
ConformanceResult,
|
|
save_conformance_baseline,
|
|
)
|
|
|
|
conformance = ConformanceReport([
|
|
ConformanceResult(**result) for result in report.generated_tests
|
|
])
|
|
if not tool.path:
|
|
print("Error: Tool has no local directory for a baseline.", file=sys.stderr)
|
|
return 1
|
|
try:
|
|
path = save_conformance_baseline(tool, conformance, tool.path.parent)
|
|
except ValueError as exc:
|
|
print(f"Error: Baseline not saved: {exc}", file=sys.stderr)
|
|
return 1
|
|
print(f"Saved conformance baseline: {path}")
|
|
print()
|
|
|
|
if not report.errors and not report.warnings and not report.suggestions:
|
|
print("No issues found.")
|
|
|
|
return 0 if not report.errors else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|