Source code for scitex_dev._core.dispatch

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Reusable CLI mixins for ``docs`` and ``skills`` subcommands.

Each package adds subcommands with minimal boilerplate::

    # In scitex_writer/_cli/__init__.py (argparse)
    from scitex_dev.cli import register_docs_subcommand, register_skills_subcommand
    register_docs_subcommand(subparsers, package="scitex-writer")
    register_skills_subcommand(subparsers, package="scitex-writer")

    # Or with Click
    from scitex_dev.cli import docs_click_group
    cli.add_command(docs_click_group(package="scitex-writer"))
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path


[docs] def register_docs_subcommand( subparsers: argparse._SubParsersAction, package: str, ) -> argparse.ArgumentParser: """Register ``docs`` with ``list`` and ``get`` verb subcommands. Usage:: scitex-stats docs list # List doc pages scitex-stats docs get # Show available pages scitex-stats docs get api # Show specific page """ prog = package.replace("_", "-") parser = subparsers.add_parser( "docs", help=f"View documentation for {package}", description=f"Browse and query {package} documentation.", epilog=( f"Examples:\n" f" {prog} docs list # List doc pages\n" f" {prog} docs list --json # JSON output\n" f" {prog} docs get # Show available pages\n" f" {prog} docs get api # Show specific page\n" f" {prog} docs get api --json # Page as JSON\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--help-recursive", action="store_true", help="Show help for all subcommands", ) docs_sub = parser.add_subparsers(dest="docs_command", title="Commands") # docs list list_p = docs_sub.add_parser( "list", help="List available documentation pages", epilog=(f"Examples:\n {prog} docs list\n {prog} docs list --json\n"), formatter_class=argparse.RawDescriptionHelpFormatter, ) list_p.add_argument("--json", action="store_true", dest="as_json") list_p.set_defaults( func=lambda args: _run_docs_command( argparse.Namespace( list_pages=True, page=None, as_json=args.as_json, tldr=False, format=None, ), package=package, ) ) # docs get [page] get_p = docs_sub.add_parser( "get", help="Show a documentation page", epilog=( f"Examples:\n" f" {prog} docs get # show overview\n" f" {prog} docs get api # specific page\n" f" {prog} docs get api --json\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) get_p.add_argument( "name", nargs="?", default=None, help="Page name (see 'docs list')" ) get_p.add_argument("--json", action="store_true", dest="as_json") get_p.add_argument("--format", type=str, default=None, choices=["html", "json"]) get_p.set_defaults(func=lambda args: _run_docs_get(args, package=package)) # bare `docs` → show help def _default_handler(args): if getattr(args, "help_recursive", False): parser.print_help() print() for sub_name, sub_p in [("list", list_p), ("get", get_p)]: print(f"--- {sub_name} ---") sub_p.print_help() print() return if args.docs_command is None: parser.print_help() parser.set_defaults(func=_default_handler) return parser
def _run_docs_get(args: argparse.Namespace, package: str) -> None: """Handle 'docs get [name]' — show page or list available pages.""" if args.name is None: ns = argparse.Namespace( list_pages=True, page=None, as_json=getattr(args, "as_json", False), tldr=False, format=None, ) else: ns = argparse.Namespace( list_pages=False, page=args.name, as_json=getattr(args, "as_json", False), tldr=False, format=getattr(args, "format", None), ) _run_docs_command(ns, package=package) def _run_docs_command(args: argparse.Namespace, package: str) -> None: """Execute the docs subcommand.""" import logging logging.getLogger("scitex_dev._core.discovery").setLevel(logging.ERROR) from .._docs.docs import get_docs # --tldr: concise quick-start if args.tldr: tldr = _get_tldr(package) if args.as_json: print(json.dumps({"package": package, "tldr": tldr})) else: print(tldr) return # Determine format. Note: page listings always use the rich manifest # (format=None) so the text and --json paths show the same data — # downgrading to format="json" here would drop page titles. fmt = args.format if args.list_pages: fetch_fmt = None elif args.as_json and fmt is None: fetch_fmt = "json" else: fetch_fmt = fmt try: result = get_docs(package=package, format=fetch_fmt, page=args.page) except LookupError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(2) # --list: show page index if args.list_pages: _print_page_list(result, as_json=args.as_json) return # Default output if args.as_json: print(json.dumps(result, indent=2, default=str)) elif isinstance(result, dict): print(json.dumps(result, indent=2, default=str)) elif isinstance(result, Path) and result.is_file(): # --page returns a file path — print its content print(result.read_text(encoding="utf-8")) else: print(result) def _print_page_list(result, as_json: bool = False) -> None: """Print a page listing from a manifest or page dict.""" if isinstance(result, dict): pages = result.get("pages", []) if as_json: print(json.dumps({"pages": pages})) else: if isinstance(pages, list) and pages and isinstance(pages[0], dict): for p in pages: print(f" {p.get('name', '?'):20s} {p.get('title', '')}") elif isinstance(pages, list): for name in pages: print(f" {name}") else: print(" (no pages found)") else: print(result) def _get_tldr(package: str) -> str: """Get a concise quick-start summary for a package. Tries to extract from built docs, falls back to a generic template. """ from .._docs.docs import get_docs try: result = get_docs(package=package) except LookupError: return f"{package}: package not found in scitex ecosystem." # Try to find a quick-start section in the docs if isinstance(result, dict): description = result.get("description", "") version = result.get("version", "?") pages = result.get("pages", []) page_names = [p["name"] if isinstance(p, dict) else p for p in pages[:5]] lines = [ f"{package} v{version}", description[:100] if description else "(no description)", "", f" pip install {package}", f' python -c "import {package.replace("-", "_")}"', ] if page_names: lines.append("") lines.append(f"Docs pages: {', '.join(page_names)}") return "\n".join(lines) return f"{package}: documentation available via get_docs(package='{package}')"
[docs] def docs_click_group(package: str, name: str = "docs"): # noqa: D401 """Create a Click command group for docs (requires Click installed). Usage:: from scitex_dev.cli import docs_click_group cli.add_command(docs_click_group(package="scitex-writer")) """ try: import click except ImportError: raise ImportError("Click is required for docs_click_group. pip install click") prog = package.replace("_", "-") @click.group(name=name, invoke_without_command=True) @click.pass_context def docs_grp(ctx): """View package documentation (list / get / search).""" if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) # Click reads the function docstring for short help; the f-string above # used to silently evaluate to None (f-strings are not docstrings). docs_grp.help = ( "View package documentation.\n\n" "\b\n" "Examples:\n" f" {prog} docs list # List doc pages\n" f" {prog} docs get # Show available pages\n" f" {prog} docs get api # Show specific page\n" f" {prog} docs search QUERY # Search across docs/APIs/CLI/MCP\n" ) @docs_grp.command( "list", epilog=f"Examples:\n $ {prog} docs list\n $ {prog} docs list --json", ) @click.option("--json", "as_json", is_flag=True, help="JSON output") def docs_list(as_json): """List available documentation pages.""" ns = argparse.Namespace( list_pages=True, page=None, as_json=as_json, tldr=False, format=None, ) _run_docs_command(ns, package=package) @docs_grp.command( "get", epilog=( f"Examples:\n" f" $ {prog} docs get # show overview\n" f" $ {prog} docs get api # specific page\n" f" $ {prog} docs get api --format json" ), ) @click.argument("page_name", required=False, default=None) @click.option("--json", "as_json", is_flag=True, help="JSON output") @click.option("--format", "fmt", type=click.Choice(["html", "json"]), default=None) def docs_get(page_name, as_json, fmt): """Show documentation. Without PAGE_NAME, shows package overview.""" if page_name is None: # No page given — show available pages ns = argparse.Namespace( list_pages=True, page=None, as_json=as_json, tldr=False, format=None, ) else: ns = argparse.Namespace( list_pages=False, page=page_name, as_json=as_json, tldr=False, format=fmt, ) _run_docs_command(ns, package=package) return docs_grp
[docs] def skills_click_group(package: str, name: str = "skills"): """Create a Click command group for skills (requires Click installed). Usage:: from scitex_dev.cli import skills_click_group cli.add_command(skills_click_group(package="scitex-app")) """ try: import click except ImportError: raise ImportError("Click is required. pip install click") prog = package.replace("_", "-") @click.group(name=name, invoke_without_command=True) @click.pass_context def skills_grp(ctx): """View package skills (workflow-oriented guides).""" if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) @skills_grp.command( "list", epilog=f"Examples:\n $ {prog} skills list\n $ {prog} skills list --json", ) @click.option("--json", "as_json", is_flag=True, help="JSON output") def skills_list(as_json): """List available skill pages.""" ns = argparse.Namespace(as_json=as_json) _skills_list(ns, package=package) @skills_grp.command( "get", epilog=( f"Examples:\n" f" $ {prog} skills get # list available\n" f" $ {prog} skills get python-scitex # show specific\n" f" $ {prog} skills get python-scitex --json" ), ) @click.argument("skill_name", required=False, default=None) @click.option("--json", "as_json", is_flag=True, help="JSON output") def skills_get(skill_name, as_json): """Show a specific skill page.""" if skill_name is None: # No name given — show available skills ns = argparse.Namespace(as_json=as_json) _skills_list(ns, package=package) return ns = argparse.Namespace(name=skill_name, as_json=as_json) _skills_get(ns, package=package) # hook-bypass: line-limit — adding export subcommand; cli.py split # refactor tracked in GITIGNORED/REFACTORING.md, follow-up work item. @skills_grp.command( "export", epilog=( f"Examples:\n" f" $ {prog} skills export # default dest\n" f" $ {prog} skills export --dest /tmp/skills\n" f" $ {prog} skills export --dry-run --json # preview" ), ) @click.option( "--dest", default=None, help="Destination dir (default: ~/.claude/skills/scitex/).", ) @click.option( "--source", type=click.Choice(["package", "dev", "auto"]), default="auto", help="Which source to export from.", ) @click.option("--clean", is_flag=True, help="Delete pkg subdir first.") @click.option("--dry-run", is_flag=True, help="Preview without writing.") @click.option( "--yes", "-y", is_flag=True, help="Skip confirmation when overwriting." ) @click.option("--json", "as_json", is_flag=True, help="JSON output") def skills_export(dest, source, clean, dry_run, yes, as_json): """Export this package's skills to <dest>.""" from pathlib import Path as _P from .._ecosystem._skills.skills import ( _get_default_export_dest, export_skills, list_skills, ) target = _P(dest) if dest else _get_default_export_dest() # hook-bypass: line-limit (tracked in GITIGNORED/REFACTORING.md) if dry_run: sl_by_pkg = list_skills(package=package) flat = [s for lst in sl_by_pkg.values() for s in lst] if as_json: import json as _json click.echo( _json.dumps({str(target): [s["name"] for s in flat]}, indent=2) ) else: click.echo( f"Would export {len(flat)} files for {package} " f"to {target}/ (source={source})" ) for s in flat: click.echo(f" - {s['name']}") return exported = export_skills(target, package=package, clean=clean, source=source) if not exported: click.echo(f"No skills found to export for {package}.") return if as_json: import json as _json click.echo( _json.dumps( {k: [str(f) for f in v] for k, v in exported.items()}, indent=2, ) ) else: total = sum(len(v) for v in exported.values()) click.echo(f"Exported {total} files for {package} to {target}") # `install` is the audit-cli §3 canonical verb name for materialising # cached resources to the user's filesystem; we keep `export` as a # back-compat alias and bind both to the same callback so existing # docs / muscle memory keep working. skills_grp.add_command(skills_export, name="install") return skills_grp
# ============================================================================= # Skills subcommand (argparse) # =============================================================================
[docs] def register_skills_subcommand( subparsers: argparse._SubParsersAction, package: str, ) -> argparse.ArgumentParser: """Register ``skills`` with ``list`` and ``get`` verb subcommands. Usage:: scitex-stats skills list # List skill pages scitex-stats skills get # Show main SKILL.md scitex-stats skills get test-selection # Show a reference page """ prog = package.replace("_", "-") parser = subparsers.add_parser( "skills", help=f"View skills for {package}", description=f"Browse {package} skills (workflow-oriented guides).", epilog=( f"Examples:\n" f" {prog} skills list\n" f" {prog} skills get test-selection\n" f" {prog} skills export --dry-run\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--help-recursive", action="store_true", help="Show help for all subcommands", ) skills_sub = parser.add_subparsers(dest="skills_command", title="Commands") # skills list list_p = skills_sub.add_parser( "list", help="List available skill pages", epilog=f"Examples:\n {prog} skills list\n {prog} skills list --json\n", formatter_class=argparse.RawDescriptionHelpFormatter, ) list_p.add_argument("--json", action="store_true", dest="as_json") list_p.set_defaults(func=lambda args: _skills_list(args, package)) # skills get [name] get_p = skills_sub.add_parser( "get", help="Show a skill page", epilog=( f"Examples:\n" f" {prog} skills get # list available\n" f" {prog} skills get test-selection # show specific\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) get_p.add_argument( "name", nargs="?", default=None, help="Skill name (see 'skills list')", ) get_p.add_argument("--json", action="store_true", dest="as_json") get_p.set_defaults(func=lambda args: _skills_get(args, package)) # skills export export_p = skills_sub.add_parser( "export", help="Export skills to ~/.claude/skills/scitex/", epilog=( f"Examples:\n" f" {prog} skills export # default dest\n" f" {prog} skills export --dest /tmp/skills\n" f" {prog} skills export --dry-run --json\n" ), formatter_class=argparse.RawDescriptionHelpFormatter, ) export_p.add_argument( "--dest", default=None, help="Exact target directory (default: ~/.claude/skills/scitex/)", ) export_p.add_argument( "--source", choices=["installed", "pypi"], default="installed", help="local (installed packages) or pypi (download wheels)", ) export_p.add_argument( "--clean", action="store_true", help="Delete package subdirs before exporting", ) export_p.add_argument( "--dry-run", action="store_true", help="Preview what would be exported without writing", ) export_p.add_argument( "--yes", "-y", action="store_true", help="Skip confirmation when overwriting destination", ) export_p.add_argument( "--json", action="store_true", dest="as_json", help="Output as JSON", ) export_p.set_defaults(func=lambda args: _skills_export(args, package)) parser.set_defaults( func=lambda args: parser.print_help() if args.skills_command is None else None ) return parser
def _skills_list(args: argparse.Namespace, package: str) -> None: import logging logging.getLogger("scitex_dev._core.discovery").setLevel(logging.ERROR) from .._ecosystem._skills.skills import list_skills result = list_skills(package=package) if args.as_json: print(json.dumps(result, indent=2)) else: items = result.get(package, []) if not items: print(f"No skills found for {package}.") return print(f"Available skills for {package}:\n") for s in items: desc = f" {s['description']}" if s["description"] else "" print(f" {s['name']}") if desc: print(f" {s['description']}") prog = package.replace("_", "-") print(f"\nUsage: {prog} skills get <name>") def _skills_export(args: argparse.Namespace, package: str) -> None: import logging logging.getLogger("scitex_dev._core.discovery").setLevel(logging.ERROR) from .._ecosystem._skills.skills import export_skills from .._ecosystem._skills.skills import _get_default_export_dest dest = ( Path(args.dest) if getattr(args, "dest", None) else _get_default_export_dest() ) source = getattr(args, "source", "installed") clean = getattr(args, "clean", False) if getattr(args, "dry_run", False): from .._ecosystem._skills.skills import list_skills result = { k: [e["name"] + ".md" for e in v] for k, v in list_skills(package=package).items() } total = sum(len(v) for v in result.values()) if getattr(args, "as_json", False): print( json.dumps( {"dest": str(dest), "source": source, "packages": result}, indent=2 ) ) else: print(f"Would export {total} files to {dest}/ (source={source})") for k, v in sorted(result.items()): print(f" {k}/: {len(v)} files") return exported = export_skills(dest, package=package, clean=clean, source=source) if not exported: print(f"No skills found for {package}.", file=sys.stderr) sys.exit(2) if getattr(args, "as_json", False): print( json.dumps({k: [str(f) for f in v] for k, v in exported.items()}, indent=2) ) else: total = sum(len(v) for v in exported.values()) print(f"Exported {total} files across {len(exported)} packages") for k, v in exported.items(): print(f" {k}: {len(v)} files") def _skills_get(args: argparse.Namespace, package: str) -> None: import logging logging.getLogger("scitex_dev._core.discovery").setLevel(logging.ERROR) # No name given → show available names if args.name is None: _skills_list(argparse.Namespace(as_json=False), package) return from .._ecosystem._skills.skills import get_skill content = get_skill(package=package, name=args.name) if content: if args.as_json: print( json.dumps({"package": package, "name": args.name, "content": content}) ) else: print(content) else: print(f"Skill '{args.name}' not found in {package}.", file=sys.stderr) print(f"Run: {package} skills list", file=sys.stderr) sys.exit(2)