style: Auto-format 48 files with ruff format

- Fixed formatting to comply with ruff standards
- No functional changes, only formatting/style
- Completes CI/CD pipeline formatting requirements
This commit is contained in:
yusyus
2026-02-15 20:24:32 +03:00
parent 83b03d9f9f
commit 57061b7daf
48 changed files with 626 additions and 548 deletions

View File

@@ -171,6 +171,7 @@ ANALYZE_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_analyze_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all analyze command arguments to a parser."""
for arg_name, arg_def in ANALYZE_ARGUMENTS.items():
@@ -178,6 +179,7 @@ def add_analyze_arguments(parser: argparse.ArgumentParser) -> None:
kwargs = arg_def["kwargs"]
parser.add_argument(*flags, **kwargs)
def get_analyze_argument_names() -> set:
"""Get the set of analyze argument destination names."""
return set(ANALYZE_ARGUMENTS.keys())

View File

@@ -96,6 +96,7 @@ RAG_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
"""Add common arguments to a parser.
@@ -114,6 +115,7 @@ def add_common_arguments(parser: argparse.ArgumentParser) -> None:
kwargs = arg_def["kwargs"]
parser.add_argument(*flags, **kwargs)
def get_common_argument_names() -> set:
"""Get the set of common argument destination names.
@@ -122,6 +124,7 @@ def get_common_argument_names() -> set:
"""
return set(COMMON_ARGUMENTS.keys())
def add_rag_arguments(parser: argparse.ArgumentParser) -> None:
"""Add RAG (Retrieval-Augmented Generation) arguments to a parser.
@@ -140,6 +143,7 @@ def add_rag_arguments(parser: argparse.ArgumentParser) -> None:
kwargs = arg_def["kwargs"]
parser.add_argument(*flags, **kwargs)
def get_rag_argument_names() -> set:
"""Get the set of RAG argument destination names.
@@ -148,6 +152,7 @@ def get_rag_argument_names() -> set:
"""
return set(RAG_ARGUMENTS.keys())
def get_argument_help(arg_name: str) -> str:
"""Get the help text for a common argument.

View File

@@ -388,10 +388,12 @@ ADVANCED_ARGUMENTS: dict[str, dict[str, Any]] = {
# HELPER FUNCTIONS
# =============================================================================
def get_universal_argument_names() -> set[str]:
"""Get set of universal argument names."""
return set(UNIVERSAL_ARGUMENTS.keys())
def get_source_specific_arguments(source_type: str) -> dict[str, dict[str, Any]]:
"""Get source-specific arguments for a given source type.
@@ -402,14 +404,15 @@ def get_source_specific_arguments(source_type: str) -> dict[str, dict[str, Any]]
Dict of argument definitions
"""
source_args = {
'web': WEB_ARGUMENTS,
'github': GITHUB_ARGUMENTS,
'local': LOCAL_ARGUMENTS,
'pdf': PDF_ARGUMENTS,
'config': {}, # Config files don't have extra args
"web": WEB_ARGUMENTS,
"github": GITHUB_ARGUMENTS,
"local": LOCAL_ARGUMENTS,
"pdf": PDF_ARGUMENTS,
"config": {}, # Config files don't have extra args
}
return source_args.get(source_type, {})
def get_compatible_arguments(source_type: str) -> list[str]:
"""Get list of compatible argument names for a source type.
@@ -431,7 +434,8 @@ def get_compatible_arguments(source_type: str) -> list[str]:
return compatible
def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default') -> None:
def add_create_arguments(parser: argparse.ArgumentParser, mode: str = "default") -> None:
"""Add create command arguments to parser.
Supports multiple help modes for progressive disclosure:
@@ -449,10 +453,10 @@ def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default')
"""
# Positional argument for source
parser.add_argument(
'source',
nargs='?',
"source",
nargs="?",
type=str,
help='Source to create skill from (URL, GitHub repo, directory, PDF, or config file)'
help="Source to create skill from (URL, GitHub repo, directory, PDF, or config file)",
)
# Always add universal arguments
@@ -460,23 +464,23 @@ def add_create_arguments(parser: argparse.ArgumentParser, mode: str = 'default')
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])
# Add source-specific arguments based on mode
if mode in ['web', 'all']:
if mode in ["web", "all"]:
for arg_name, arg_def in WEB_ARGUMENTS.items():
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])
if mode in ['github', 'all']:
if mode in ["github", "all"]:
for arg_name, arg_def in GITHUB_ARGUMENTS.items():
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])
if mode in ['local', 'all']:
if mode in ["local", "all"]:
for arg_name, arg_def in LOCAL_ARGUMENTS.items():
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])
if mode in ['pdf', 'all']:
if mode in ["pdf", "all"]:
for arg_name, arg_def in PDF_ARGUMENTS.items():
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])
# Add advanced arguments if requested
if mode in ['advanced', 'all']:
if mode in ["advanced", "all"]:
for arg_name, arg_def in ADVANCED_ARGUMENTS.items():
parser.add_argument(*arg_def["flags"], **arg_def["kwargs"])

View File

@@ -68,6 +68,7 @@ ENHANCE_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_enhance_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all enhance command arguments to a parser."""
for arg_name, arg_def in ENHANCE_ARGUMENTS.items():

View File

@@ -133,6 +133,7 @@ GITHUB_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_github_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all github command arguments to a parser.
@@ -153,6 +154,7 @@ def add_github_arguments(parser: argparse.ArgumentParser) -> None:
kwargs = arg_def["kwargs"]
parser.add_argument(*flags, **kwargs)
def get_github_argument_names() -> set:
"""Get the set of github argument destination names.
@@ -161,6 +163,7 @@ def get_github_argument_names() -> set:
"""
return set(GITHUB_ARGUMENTS.keys())
def get_github_argument_count() -> int:
"""Get the total number of github arguments.

View File

@@ -123,6 +123,7 @@ PACKAGE_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_package_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all package command arguments to a parser."""
for arg_name, arg_def in PACKAGE_ARGUMENTS.items():

View File

@@ -51,6 +51,7 @@ PDF_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_pdf_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all pdf command arguments to a parser."""
for arg_name, arg_def in PDF_ARGUMENTS.items():

View File

@@ -198,6 +198,7 @@ SCRAPE_ARGUMENTS: dict[str, dict[str, Any]] = {
# Merge RAG arguments from common.py
SCRAPE_ARGUMENTS.update(RAG_ARGUMENTS)
def add_scrape_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all scrape command arguments to a parser.
@@ -218,6 +219,7 @@ def add_scrape_arguments(parser: argparse.ArgumentParser) -> None:
kwargs = arg_def["kwargs"]
parser.add_argument(*flags, **kwargs)
def get_scrape_argument_names() -> set:
"""Get the set of scrape argument destination names.
@@ -226,6 +228,7 @@ def get_scrape_argument_names() -> set:
"""
return set(SCRAPE_ARGUMENTS.keys())
def get_scrape_argument_count() -> int:
"""Get the total number of scrape arguments.

View File

@@ -42,6 +42,7 @@ UNIFIED_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_unified_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all unified command arguments to a parser."""
for arg_name, arg_def in UNIFIED_ARGUMENTS.items():

View File

@@ -98,6 +98,7 @@ UPLOAD_ARGUMENTS: dict[str, dict[str, Any]] = {
},
}
def add_upload_arguments(parser: argparse.ArgumentParser) -> None:
"""Add all upload command arguments to a parser."""
for arg_name, arg_def in UPLOAD_ARGUMENTS.items():

View File

@@ -870,7 +870,7 @@ def main():
# AI Enhancement (if requested)
enhance_mode = args.ai_mode
if getattr(args, 'enhance_level', 0) > 0:
if getattr(args, "enhance_level", 0) > 0:
# Auto-detect mode if enhance_level is set
enhance_mode = "auto" # ConfigEnhancer will auto-detect API vs LOCAL

View File

@@ -16,6 +16,7 @@ from skill_seekers.cli.arguments.create import (
logger = logging.getLogger(__name__)
class CreateCommand:
"""Unified create command implementation."""
@@ -74,7 +75,7 @@ class CreateCommand:
continue
# Skip internal arguments
if arg_name in ['source', 'func', 'subcommand']:
if arg_name in ["source", "func", "subcommand"]:
continue
# Warn about incompatible argument
@@ -104,10 +105,10 @@ class CreateCommand:
# Check against common defaults
defaults = {
'max_issues': 100,
'chunk_size': 512,
'chunk_overlap': 50,
'output': None,
"max_issues": 100,
"chunk_size": 512,
"chunk_overlap": 50,
"output": None,
}
if arg_name in defaults:
@@ -122,15 +123,15 @@ class CreateCommand:
Returns:
Exit code from scraper
"""
if self.source_info.type == 'web':
if self.source_info.type == "web":
return self._route_web()
elif self.source_info.type == 'github':
elif self.source_info.type == "github":
return self._route_github()
elif self.source_info.type == 'local':
elif self.source_info.type == "local":
return self._route_local()
elif self.source_info.type == 'pdf':
elif self.source_info.type == "pdf":
return self._route_pdf()
elif self.source_info.type == 'config':
elif self.source_info.type == "config":
return self._route_config()
else:
logger.error(f"Unknown source type: {self.source_info.type}")
@@ -141,10 +142,10 @@ class CreateCommand:
from skill_seekers.cli import doc_scraper
# Reconstruct argv for doc_scraper
argv = ['doc_scraper']
argv = ["doc_scraper"]
# Add URL
url = self.source_info.parsed['url']
url = self.source_info.parsed["url"]
argv.append(url)
# Add universal arguments
@@ -152,21 +153,21 @@ class CreateCommand:
# Add web-specific arguments
if self.args.max_pages:
argv.extend(['--max-pages', str(self.args.max_pages)])
if getattr(self.args, 'skip_scrape', False):
argv.append('--skip-scrape')
if getattr(self.args, 'resume', False):
argv.append('--resume')
if getattr(self.args, 'fresh', False):
argv.append('--fresh')
if getattr(self.args, 'rate_limit', None):
argv.extend(['--rate-limit', str(self.args.rate_limit)])
if getattr(self.args, 'workers', None):
argv.extend(['--workers', str(self.args.workers)])
if getattr(self.args, 'async_mode', False):
argv.append('--async')
if getattr(self.args, 'no_rate_limit', False):
argv.append('--no-rate-limit')
argv.extend(["--max-pages", str(self.args.max_pages)])
if getattr(self.args, "skip_scrape", False):
argv.append("--skip-scrape")
if getattr(self.args, "resume", False):
argv.append("--resume")
if getattr(self.args, "fresh", False):
argv.append("--fresh")
if getattr(self.args, "rate_limit", None):
argv.extend(["--rate-limit", str(self.args.rate_limit)])
if getattr(self.args, "workers", None):
argv.extend(["--workers", str(self.args.workers)])
if getattr(self.args, "async_mode", False):
argv.append("--async")
if getattr(self.args, "no_rate_limit", False):
argv.append("--no-rate-limit")
# Call doc_scraper with modified argv
logger.debug(f"Calling doc_scraper with argv: {argv}")
@@ -182,32 +183,32 @@ class CreateCommand:
from skill_seekers.cli import github_scraper
# Reconstruct argv for github_scraper
argv = ['github_scraper']
argv = ["github_scraper"]
# Add repo
repo = self.source_info.parsed['repo']
argv.extend(['--repo', repo])
repo = self.source_info.parsed["repo"]
argv.extend(["--repo", repo])
# Add universal arguments
self._add_common_args(argv)
# Add GitHub-specific arguments
if getattr(self.args, 'token', None):
argv.extend(['--token', self.args.token])
if getattr(self.args, 'profile', None):
argv.extend(['--profile', self.args.profile])
if getattr(self.args, 'non_interactive', False):
argv.append('--non-interactive')
if getattr(self.args, 'no_issues', False):
argv.append('--no-issues')
if getattr(self.args, 'no_changelog', False):
argv.append('--no-changelog')
if getattr(self.args, 'no_releases', False):
argv.append('--no-releases')
if getattr(self.args, 'max_issues', None) and self.args.max_issues != 100:
argv.extend(['--max-issues', str(self.args.max_issues)])
if getattr(self.args, 'scrape_only', False):
argv.append('--scrape-only')
if getattr(self.args, "token", None):
argv.extend(["--token", self.args.token])
if getattr(self.args, "profile", None):
argv.extend(["--profile", self.args.profile])
if getattr(self.args, "non_interactive", False):
argv.append("--non-interactive")
if getattr(self.args, "no_issues", False):
argv.append("--no-issues")
if getattr(self.args, "no_changelog", False):
argv.append("--no-changelog")
if getattr(self.args, "no_releases", False):
argv.append("--no-releases")
if getattr(self.args, "max_issues", None) and self.args.max_issues != 100:
argv.extend(["--max-issues", str(self.args.max_issues)])
if getattr(self.args, "scrape_only", False):
argv.append("--scrape-only")
# Call github_scraper with modified argv
logger.debug(f"Calling github_scraper with argv: {argv}")
@@ -223,30 +224,30 @@ class CreateCommand:
from skill_seekers.cli import codebase_scraper
# Reconstruct argv for codebase_scraper
argv = ['codebase_scraper']
argv = ["codebase_scraper"]
# Add directory
directory = self.source_info.parsed['directory']
argv.extend(['--directory', directory])
directory = self.source_info.parsed["directory"]
argv.extend(["--directory", directory])
# Add universal arguments
self._add_common_args(argv)
# Add local-specific arguments
if getattr(self.args, 'languages', None):
argv.extend(['--languages', self.args.languages])
if getattr(self.args, 'file_patterns', None):
argv.extend(['--file-patterns', self.args.file_patterns])
if getattr(self.args, 'skip_patterns', False):
argv.append('--skip-patterns')
if getattr(self.args, 'skip_test_examples', False):
argv.append('--skip-test-examples')
if getattr(self.args, 'skip_how_to_guides', False):
argv.append('--skip-how-to-guides')
if getattr(self.args, 'skip_config', False):
argv.append('--skip-config')
if getattr(self.args, 'skip_docs', False):
argv.append('--skip-docs')
if getattr(self.args, "languages", None):
argv.extend(["--languages", self.args.languages])
if getattr(self.args, "file_patterns", None):
argv.extend(["--file-patterns", self.args.file_patterns])
if getattr(self.args, "skip_patterns", False):
argv.append("--skip-patterns")
if getattr(self.args, "skip_test_examples", False):
argv.append("--skip-test-examples")
if getattr(self.args, "skip_how_to_guides", False):
argv.append("--skip-how-to-guides")
if getattr(self.args, "skip_config", False):
argv.append("--skip-config")
if getattr(self.args, "skip_docs", False):
argv.append("--skip-docs")
# Call codebase_scraper with modified argv
logger.debug(f"Calling codebase_scraper with argv: {argv}")
@@ -262,20 +263,20 @@ class CreateCommand:
from skill_seekers.cli import pdf_scraper
# Reconstruct argv for pdf_scraper
argv = ['pdf_scraper']
argv = ["pdf_scraper"]
# Add PDF file
file_path = self.source_info.parsed['file_path']
argv.extend(['--pdf', file_path])
file_path = self.source_info.parsed["file_path"]
argv.extend(["--pdf", file_path])
# Add universal arguments
self._add_common_args(argv)
# Add PDF-specific arguments
if getattr(self.args, 'ocr', False):
argv.append('--ocr')
if getattr(self.args, 'pages', None):
argv.extend(['--pages', self.args.pages])
if getattr(self.args, "ocr", False):
argv.append("--ocr")
if getattr(self.args, "pages", None):
argv.extend(["--pages", self.args.pages])
# Call pdf_scraper with modified argv
logger.debug(f"Calling pdf_scraper with argv: {argv}")
@@ -291,11 +292,11 @@ class CreateCommand:
from skill_seekers.cli import unified_scraper
# Reconstruct argv for unified_scraper
argv = ['unified_scraper']
argv = ["unified_scraper"]
# Add config file
config_path = self.source_info.parsed['config_path']
argv.extend(['--config', config_path])
config_path = self.source_info.parsed["config_path"]
argv.extend(["--config", config_path])
# Add universal arguments (unified scraper supports most)
self._add_common_args(argv)
@@ -317,53 +318,54 @@ class CreateCommand:
"""
# Identity arguments
if self.args.name:
argv.extend(['--name', self.args.name])
elif hasattr(self, 'source_info') and self.source_info:
argv.extend(["--name", self.args.name])
elif hasattr(self, "source_info") and self.source_info:
# Use suggested name from source detection
argv.extend(['--name', self.source_info.suggested_name])
argv.extend(["--name", self.source_info.suggested_name])
if self.args.description:
argv.extend(['--description', self.args.description])
argv.extend(["--description", self.args.description])
if self.args.output:
argv.extend(['--output', self.args.output])
argv.extend(["--output", self.args.output])
# Enhancement arguments (consolidated to --enhance-level only)
if self.args.enhance_level > 0:
argv.extend(['--enhance-level', str(self.args.enhance_level)])
argv.extend(["--enhance-level", str(self.args.enhance_level)])
if self.args.api_key:
argv.extend(['--api-key', self.args.api_key])
argv.extend(["--api-key", self.args.api_key])
# Behavior arguments
if self.args.dry_run:
argv.append('--dry-run')
argv.append("--dry-run")
if self.args.verbose:
argv.append('--verbose')
argv.append("--verbose")
if self.args.quiet:
argv.append('--quiet')
argv.append("--quiet")
# RAG arguments (NEW - universal!)
if getattr(self.args, 'chunk_for_rag', False):
argv.append('--chunk-for-rag')
if getattr(self.args, 'chunk_size', None) and self.args.chunk_size != 512:
argv.extend(['--chunk-size', str(self.args.chunk_size)])
if getattr(self.args, 'chunk_overlap', None) and self.args.chunk_overlap != 50:
argv.extend(['--chunk-overlap', str(self.args.chunk_overlap)])
if getattr(self.args, "chunk_for_rag", False):
argv.append("--chunk-for-rag")
if getattr(self.args, "chunk_size", None) and self.args.chunk_size != 512:
argv.extend(["--chunk-size", str(self.args.chunk_size)])
if getattr(self.args, "chunk_overlap", None) and self.args.chunk_overlap != 50:
argv.extend(["--chunk-overlap", str(self.args.chunk_overlap)])
# Preset argument
if getattr(self.args, 'preset', None):
argv.extend(['--preset', self.args.preset])
if getattr(self.args, "preset", None):
argv.extend(["--preset", self.args.preset])
# Config file
if self.args.config:
argv.extend(['--config', self.args.config])
argv.extend(["--config", self.args.config])
# Advanced arguments
if getattr(self.args, 'no_preserve_code_blocks', False):
argv.append('--no-preserve-code-blocks')
if getattr(self.args, 'no_preserve_paragraphs', False):
argv.append('--no-preserve-paragraphs')
if getattr(self.args, 'interactive_enhancement', False):
argv.append('--interactive-enhancement')
if getattr(self.args, "no_preserve_code_blocks", False):
argv.append("--no-preserve-code-blocks")
if getattr(self.args, "no_preserve_paragraphs", False):
argv.append("--no-preserve-paragraphs")
if getattr(self.args, "interactive_enhancement", False):
argv.append("--interactive-enhancement")
def main() -> int:
"""Entry point for create command.
@@ -381,8 +383,8 @@ def main() -> int:
return text.splitlines()
parser = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill from any source (auto-detects type)',
prog="skill-seekers create",
description="Create skill from any source (auto-detects type)",
formatter_class=NoWrapFormatter,
epilog=textwrap.dedent("""\
Examples:
@@ -416,19 +418,25 @@ Common Workflows:
skill-seekers create <source> -p quick
skill-seekers create <source> -p standard --enhance-level 2
skill-seekers create <source> --chunk-for-rag
""")
"""),
)
# Add arguments in default mode (universal only)
add_create_arguments(parser, mode='default')
add_create_arguments(parser, mode="default")
# Add hidden help mode flags (use underscore prefix to match CreateParser)
parser.add_argument('--help-web', action='store_true', help=argparse.SUPPRESS, dest='_help_web')
parser.add_argument('--help-github', action='store_true', help=argparse.SUPPRESS, dest='_help_github')
parser.add_argument('--help-local', action='store_true', help=argparse.SUPPRESS, dest='_help_local')
parser.add_argument('--help-pdf', action='store_true', help=argparse.SUPPRESS, dest='_help_pdf')
parser.add_argument('--help-advanced', action='store_true', help=argparse.SUPPRESS, dest='_help_advanced')
parser.add_argument('--help-all', action='store_true', help=argparse.SUPPRESS, dest='_help_all')
parser.add_argument("--help-web", action="store_true", help=argparse.SUPPRESS, dest="_help_web")
parser.add_argument(
"--help-github", action="store_true", help=argparse.SUPPRESS, dest="_help_github"
)
parser.add_argument(
"--help-local", action="store_true", help=argparse.SUPPRESS, dest="_help_local"
)
parser.add_argument("--help-pdf", action="store_true", help=argparse.SUPPRESS, dest="_help_pdf")
parser.add_argument(
"--help-advanced", action="store_true", help=argparse.SUPPRESS, dest="_help_advanced"
)
parser.add_argument("--help-all", action="store_true", help=argparse.SUPPRESS, dest="_help_all")
# Parse arguments
args = parser.parse_args()
@@ -437,67 +445,62 @@ Common Workflows:
if args._help_web:
# Recreate parser with web-specific arguments
parser_web = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill from web documentation',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill from web documentation",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_web, mode='web')
add_create_arguments(parser_web, mode="web")
parser_web.print_help()
return 0
elif args._help_github:
parser_github = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill from GitHub repository',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill from GitHub repository",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_github, mode='github')
add_create_arguments(parser_github, mode="github")
parser_github.print_help()
return 0
elif args._help_local:
parser_local = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill from local codebase',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill from local codebase",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_local, mode='local')
add_create_arguments(parser_local, mode="local")
parser_local.print_help()
return 0
elif args._help_pdf:
parser_pdf = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill from PDF file',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill from PDF file",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_pdf, mode='pdf')
add_create_arguments(parser_pdf, mode="pdf")
parser_pdf.print_help()
return 0
elif args._help_advanced:
parser_advanced = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill - advanced options',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill - advanced options",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_advanced, mode='advanced')
add_create_arguments(parser_advanced, mode="advanced")
parser_advanced.print_help()
return 0
elif args._help_all:
parser_all = argparse.ArgumentParser(
prog='skill-seekers create',
description='Create skill - all options',
formatter_class=argparse.RawDescriptionHelpFormatter
prog="skill-seekers create",
description="Create skill - all options",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_create_arguments(parser_all, mode='all')
add_create_arguments(parser_all, mode="all")
parser_all.print_help()
return 0
# Setup logging
log_level = logging.DEBUG if args.verbose else (
logging.WARNING if args.quiet else logging.INFO
)
logging.basicConfig(
level=log_level,
format='%(levelname)s: %(message)s'
)
log_level = logging.DEBUG if args.verbose else (logging.WARNING if args.quiet else logging.INFO)
logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
# Validate source provided
if not args.source:
@@ -507,5 +510,6 @@ Common Workflows:
command = CreateCommand(args)
return command.execute()
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main())

View File

@@ -2231,8 +2231,9 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non
import subprocess
# Optional enhancement with auto-detected mode (API or LOCAL)
if getattr(args, 'enhance_level', 0) > 0:
if getattr(args, "enhance_level", 0) > 0:
import os
has_api_key = bool(os.environ.get("ANTHROPIC_API_KEY") or args.api_key)
mode = "API" if has_api_key else "LOCAL"
@@ -2246,7 +2247,7 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non
if args.api_key:
enhance_cmd.extend(["--api-key", args.api_key])
if getattr(args, 'interactive_enhancement', False):
if getattr(args, "interactive_enhancement", False):
enhance_cmd.append("--interactive-enhancement")
result = subprocess.run(enhance_cmd, check=True)
@@ -2256,14 +2257,18 @@ def execute_enhancement(config: dict[str, Any], args: argparse.Namespace) -> Non
logger.warning("\n⚠ Enhancement failed, but skill was still built")
except FileNotFoundError:
logger.warning("\n⚠ skill-seekers-enhance command not found. Run manually:")
logger.info(" skill-seekers-enhance output/%s/ --enhance-level %d", config["name"], args.enhance_level)
logger.info(
" skill-seekers-enhance output/%s/ --enhance-level %d",
config["name"],
args.enhance_level,
)
# Print packaging instructions
logger.info("\n📦 Package your skill:")
logger.info(" skill-seekers-package output/%s/", config["name"])
# Suggest enhancement if not done
if getattr(args, 'enhance_level', 0) == 0:
if getattr(args, "enhance_level", 0) == 0:
logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
logger.info(" skill-seekers-enhance output/%s/ --enhance-level 2", config["name"])
logger.info(" or re-run with: --enhance-level 2 (auto-detects API vs LOCAL mode)")

View File

@@ -100,6 +100,7 @@ EXCLUDED_DIRS = {
".tmp",
}
def extract_description_from_readme(readme_content: str, repo_name: str) -> str:
"""
Extract a meaningful description from README content for skill description.
@@ -180,6 +181,7 @@ def extract_description_from_readme(readme_content: str, repo_name: str) -> str:
project_name = repo_name.split("/")[-1]
return f"Use when working with {project_name}"
class GitHubScraper:
"""
GitHub Repository Scraper (C1.1-C1.9)
@@ -892,6 +894,7 @@ class GitHubScraper:
logger.info(f"Data saved to: {self.data_file}")
class GitHubToSkillConverter:
"""
Convert extracted GitHub data to Claude skill format (C1.10).
@@ -1347,6 +1350,7 @@ Use this skill when you need to:
f.write(content)
logger.info(f"Generated: {structure_path}")
def setup_argument_parser() -> argparse.ArgumentParser:
"""Setup and configure command-line argument parser.
@@ -1374,6 +1378,7 @@ Examples:
return parser
def main():
"""C1.10: CLI tool entry point."""
parser = setup_argument_parser()
@@ -1421,14 +1426,16 @@ def main():
skill_dir = f"output/{skill_name}"
# Phase 3: Optional enhancement with auto-detected mode
if getattr(args, 'enhance_level', 0) > 0:
if getattr(args, "enhance_level", 0) > 0:
import os
# Auto-detect mode based on API key availability
api_key = args.api_key or os.environ.get("ANTHROPIC_API_KEY")
mode = "API" if api_key else "LOCAL"
logger.info(f"\n📝 Enhancing SKILL.md with Claude ({mode} mode, level {args.enhance_level})...")
logger.info(
f"\n📝 Enhancing SKILL.md with Claude ({mode} mode, level {args.enhance_level})..."
)
if api_key:
# API-based enhancement
@@ -1438,9 +1445,7 @@ def main():
enhance_skill_md(skill_dir, api_key)
logger.info("✅ API enhancement complete!")
except ImportError:
logger.error(
"❌ API enhancement not available. Install: pip install anthropic"
)
logger.error("❌ API enhancement not available. Install: pip install anthropic")
logger.info("💡 Falling back to LOCAL mode...")
# Fall back to LOCAL mode
from pathlib import Path
@@ -1460,7 +1465,7 @@ def main():
logger.info(f"\n✅ Success! Skill created at: {skill_dir}/")
if getattr(args, 'enhance_level', 0) == 0:
if getattr(args, "enhance_level", 0) == 0:
logger.info("\n💡 Optional: Enhance SKILL.md with Claude:")
logger.info(f" skill-seekers enhance {skill_dir}/ --enhance-level 2")
logger.info(" (auto-detects API vs LOCAL mode based on ANTHROPIC_API_KEY)")
@@ -1471,5 +1476,6 @@ def main():
logger.error(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -133,7 +133,7 @@ def _reconstruct_argv(command: str, args: argparse.Namespace) -> list[str]:
if key.startswith("_help_"):
if value:
# Convert _help_web -> --help-web
help_flag = key.replace('_help_', 'help-')
help_flag = key.replace("_help_", "help-")
argv.append(f"--{help_flag}")
continue
@@ -181,6 +181,7 @@ def main(argv: list[str] | None = None) -> int:
argv = sys.argv[1:]
if len(argv) >= 2 and argv[0] == "analyze" and "--preset-list" in argv:
from skill_seekers.cli.codebase_scraper import main as analyze_main
original_argv = sys.argv.copy()
sys.argv = ["codebase_scraper.py", "--preset-list"]
try:
@@ -274,8 +275,8 @@ def _handle_analyze_command(args: argparse.Namespace) -> int:
sys.argv.extend(["--depth", args.depth])
# Determine enhance_level (simplified - use default or override)
enhance_level = getattr(args, 'enhance_level', 2) # Default is 2
if getattr(args, 'quick', False):
enhance_level = getattr(args, "enhance_level", 2) # Default is 2
if getattr(args, "quick", False):
enhance_level = 0 # Quick mode disables enhancement
sys.argv.extend(["--enhance-level", str(enhance_level)])

View File

@@ -52,6 +52,7 @@ PARSERS = [
QualityParser(),
]
def register_parsers(subparsers):
"""Register all subcommand parsers.
@@ -64,6 +65,7 @@ def register_parsers(subparsers):
for parser_instance in PARSERS:
parser_instance.create_parser(subparsers)
def get_parser_names():
"""Get list of all subcommand names.
@@ -72,6 +74,7 @@ def get_parser_names():
"""
return [p.name for p in PARSERS]
__all__ = [
"SubcommandParser",
"PARSERS",

View File

@@ -9,6 +9,7 @@ Includes preset system support (Issue #268).
from .base import SubcommandParser
from skill_seekers.cli.arguments.analyze import add_analyze_arguments
class AnalyzeParser(SubcommandParser):
"""Parser for analyze subcommand."""

View File

@@ -3,6 +3,7 @@
from abc import ABC, abstractmethod
import argparse
class SubcommandParser(ABC):
"""Base class for subcommand parsers.

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class ConfigParser(SubcommandParser):
"""Parser for config subcommand."""

View File

@@ -13,6 +13,7 @@ import argparse
from .base import SubcommandParser
from skill_seekers.cli.arguments.create import add_create_arguments
class CreateParser(SubcommandParser):
"""Parser for create subcommand with multi-mode help."""
@@ -54,45 +55,45 @@ Presets: -p quick (1-2min) | -p standard (5-10min) | -p comprehensive (20-60min)
"""
# Add all arguments in 'default' mode (universal only)
# This keeps help text clean and focused
add_create_arguments(parser, mode='default')
add_create_arguments(parser, mode="default")
# Add hidden help mode flags
# These won't show in default help but can be used to get source-specific help
parser.add_argument(
'--help-web',
action='store_true',
help='Show web scraping specific options',
dest='_help_web'
"--help-web",
action="store_true",
help="Show web scraping specific options",
dest="_help_web",
)
parser.add_argument(
'--help-github',
action='store_true',
help='Show GitHub repository specific options',
dest='_help_github'
"--help-github",
action="store_true",
help="Show GitHub repository specific options",
dest="_help_github",
)
parser.add_argument(
'--help-local',
action='store_true',
help='Show local codebase specific options',
dest='_help_local'
"--help-local",
action="store_true",
help="Show local codebase specific options",
dest="_help_local",
)
parser.add_argument(
'--help-pdf',
action='store_true',
help='Show PDF extraction specific options',
dest='_help_pdf'
"--help-pdf",
action="store_true",
help="Show PDF extraction specific options",
dest="_help_pdf",
)
parser.add_argument(
'--help-advanced',
action='store_true',
help='Show advanced/rare options',
dest='_help_advanced'
"--help-advanced",
action="store_true",
help="Show advanced/rare options",
dest="_help_advanced",
)
parser.add_argument(
'--help-all',
action='store_true',
help='Show all available options (120+ flags)',
dest='_help_all'
"--help-all",
action="store_true",
help="Show all available options (120+ flags)",
dest="_help_all",
)
def register(self, subparsers):
@@ -104,16 +105,14 @@ Presets: -p quick (1-2min) | -p standard (5-10min) | -p comprehensive (20-60min)
Returns:
Configured ArgumentParser for this subcommand
"""
# Custom formatter that preserves line breaks
class NoWrapFormatter(argparse.RawDescriptionHelpFormatter):
def _split_lines(self, text, width):
return text.splitlines()
parser = subparsers.add_parser(
self.name,
help=self.help,
description=self.description,
formatter_class=NoWrapFormatter
self.name, help=self.help, description=self.description, formatter_class=NoWrapFormatter
)
self.add_arguments(parser)
return parser

View File

@@ -7,6 +7,7 @@ consistency with the standalone enhance_skill_local module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.enhance import add_enhance_arguments
class EnhanceParser(SubcommandParser):
"""Parser for enhance subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class EnhanceStatusParser(SubcommandParser):
"""Parser for enhance-status subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class EstimateParser(SubcommandParser):
"""Parser for estimate subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone github_scraper module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.github import add_github_arguments
class GitHubParser(SubcommandParser):
"""Parser for github subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class InstallAgentParser(SubcommandParser):
"""Parser for install-agent subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class InstallParser(SubcommandParser):
"""Parser for install subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class MultilangParser(SubcommandParser):
"""Parser for multilang subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone package_skill module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.package import add_package_arguments
class PackageParser(SubcommandParser):
"""Parser for package subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone pdf_scraper module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.pdf import add_pdf_arguments
class PDFParser(SubcommandParser):
"""Parser for pdf subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class QualityParser(SubcommandParser):
"""Parser for quality subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class ResumeParser(SubcommandParser):
"""Parser for resume subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone doc_scraper module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.scrape import add_scrape_arguments
class ScrapeParser(SubcommandParser):
"""Parser for scrape subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class StreamParser(SubcommandParser):
"""Parser for stream subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class TestExamplesParser(SubcommandParser):
"""Parser for extract-test-examples subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone unified_scraper module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.unified import add_unified_arguments
class UnifiedParser(SubcommandParser):
"""Parser for unified subcommand."""

View File

@@ -2,6 +2,7 @@
from .base import SubcommandParser
class UpdateParser(SubcommandParser):
"""Parser for update subcommand."""

View File

@@ -7,6 +7,7 @@ consistency with the standalone upload_skill module.
from .base import SubcommandParser
from skill_seekers.cli.arguments.upload import add_upload_arguments
class UploadParser(SubcommandParser):
"""Parser for upload subcommand."""

View File

@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
import argparse
@dataclass(frozen=True)
class AnalysisPreset:
"""Definition of an analysis preset.
@@ -29,12 +30,14 @@ class AnalysisPreset:
features: Dict of feature flags (feature_name -> enabled)
estimated_time: Human-readable time estimate
"""
name: str
description: str
depth: str
features: dict[str, bool] = field(default_factory=dict)
estimated_time: str = ""
# Preset definitions
ANALYZE_PRESETS = {
"quick": AnalysisPreset(
@@ -49,9 +52,8 @@ ANALYZE_PRESETS = {
"how_to_guides": False,
"config_patterns": False,
},
estimated_time="1-2 minutes"
estimated_time="1-2 minutes",
),
"standard": AnalysisPreset(
name="Standard",
description="Balanced analysis with core features (recommended)",
@@ -64,9 +66,8 @@ ANALYZE_PRESETS = {
"how_to_guides": False,
"config_patterns": True,
},
estimated_time="5-10 minutes"
estimated_time="5-10 minutes",
),
"comprehensive": AnalysisPreset(
name="Comprehensive",
description="Full analysis with all features",
@@ -79,10 +80,11 @@ ANALYZE_PRESETS = {
"how_to_guides": True,
"config_patterns": True,
},
estimated_time="20-60 minutes"
estimated_time="20-60 minutes",
),
}
def apply_analyze_preset(args: argparse.Namespace, preset_name: str) -> None:
"""Apply an analysis preset to the args namespace.
@@ -113,6 +115,7 @@ def apply_analyze_preset(args: argparse.Namespace, preset_name: str) -> None:
skip_attr = f"skip_{feature}"
setattr(args, skip_attr, not enabled)
def get_preset_help_text(preset_name: str) -> str:
"""Get formatted help text for a preset.
@@ -129,6 +132,7 @@ def get_preset_help_text(preset_name: str) -> str:
f" Depth: {preset.depth}"
)
def show_preset_list() -> None:
"""Print the list of available presets to stdout.
@@ -161,6 +165,7 @@ def show_preset_list() -> None:
print(" skill-seekers analyze --directory <dir> --preset comprehensive --enhance-level 2")
print()
def resolve_enhance_level(args: argparse.Namespace) -> int:
"""Determine the enhance level based on user arguments.
@@ -186,6 +191,7 @@ def resolve_enhance_level(args: argparse.Namespace) -> int:
# Default is no enhancement
return 0
def apply_preset_with_warnings(args: argparse.Namespace) -> str:
"""Apply preset with deprecation warnings for legacy flags.
@@ -240,6 +246,7 @@ def apply_preset_with_warnings(args: argparse.Namespace) -> str:
return preset_name
def print_deprecation_warning(old_flag: str, new_flag: str) -> None:
"""Print a deprecation warning for legacy flags.

View File

@@ -12,6 +12,7 @@ from dataclasses import dataclass, field
import argparse
@dataclass(frozen=True)
class GitHubPreset:
"""Definition of a GitHub preset.
@@ -23,12 +24,14 @@ class GitHubPreset:
features: Dict of feature flags (feature_name -> enabled)
estimated_time: Human-readable time estimate
"""
name: str
description: str
max_issues: int
features: dict[str, bool] = field(default_factory=dict)
estimated_time: str = ""
# Preset definitions
GITHUB_PRESETS = {
"quick": GitHubPreset(
@@ -40,9 +43,8 @@ GITHUB_PRESETS = {
"include_changelog": True,
"include_releases": False,
},
estimated_time="1-3 minutes"
estimated_time="1-3 minutes",
),
"standard": GitHubPreset(
name="Standard",
description="Balanced scraping with issues and releases (recommended)",
@@ -52,9 +54,8 @@ GITHUB_PRESETS = {
"include_changelog": True,
"include_releases": True,
},
estimated_time="5-15 minutes"
estimated_time="5-15 minutes",
),
"comprehensive": GitHubPreset(
name="Comprehensive",
description="Comprehensive scraping with all available data",
@@ -64,10 +65,11 @@ GITHUB_PRESETS = {
"include_changelog": True,
"include_releases": True,
},
estimated_time="20-60 minutes"
estimated_time="20-60 minutes",
),
}
def apply_github_preset(args: argparse.Namespace, preset_name: str) -> None:
"""Apply a GitHub preset to the args namespace.
@@ -90,6 +92,7 @@ def apply_github_preset(args: argparse.Namespace, preset_name: str) -> None:
if not hasattr(args, skip_attr) or not getattr(args, skip_attr):
setattr(args, skip_attr, not enabled)
def show_github_preset_list() -> None:
"""Print the list of available GitHub presets to stdout."""
print("\nAvailable GitHub Presets")

View File

@@ -6,6 +6,7 @@ between speed and comprehensiveness.
from dataclasses import dataclass
@dataclass
class AnalysisPreset:
"""Analysis preset configuration.
@@ -22,6 +23,7 @@ class AnalysisPreset:
estimated_time: str
icon: str
# Preset definitions
PRESETS = {
"quick": AnalysisPreset(
@@ -77,6 +79,7 @@ PRESETS = {
),
}
class PresetManager:
"""Manages analysis presets and applies them to CLI arguments."""
@@ -164,6 +167,7 @@ class PresetManager:
"""
return "standard"
# Public API
__all__ = [
"AnalysisPreset",

View File

@@ -12,6 +12,7 @@ from dataclasses import dataclass, field
import argparse
@dataclass(frozen=True)
class ScrapePreset:
"""Definition of a scrape preset.
@@ -25,6 +26,7 @@ class ScrapePreset:
workers: Number of parallel workers
estimated_time: Human-readable time estimate
"""
name: str
description: str
rate_limit: float
@@ -33,6 +35,7 @@ class ScrapePreset:
workers: int = 1
estimated_time: str = ""
# Preset definitions
SCRAPE_PRESETS = {
"quick": ScrapePreset(
@@ -45,9 +48,8 @@ SCRAPE_PRESETS = {
},
async_mode=True,
workers=5,
estimated_time="2-5 minutes"
estimated_time="2-5 minutes",
),
"standard": ScrapePreset(
name="Standard",
description="Balanced scraping with good coverage (recommended)",
@@ -58,9 +60,8 @@ SCRAPE_PRESETS = {
},
async_mode=True,
workers=3,
estimated_time="10-30 minutes"
estimated_time="10-30 minutes",
),
"comprehensive": ScrapePreset(
name="Comprehensive",
description="Comprehensive scraping with all features",
@@ -71,10 +72,11 @@ SCRAPE_PRESETS = {
},
async_mode=True,
workers=2,
estimated_time="1-3 hours"
estimated_time="1-3 hours",
),
}
def apply_scrape_preset(args: argparse.Namespace, preset_name: str) -> None:
"""Apply a scrape preset to the args namespace.
@@ -100,9 +102,12 @@ def apply_scrape_preset(args: argparse.Namespace, preset_name: str) -> None:
# Apply feature flags
for feature, enabled in preset.features.items():
if feature == "rag_chunking" and (not hasattr(args, 'chunk_for_rag') or not args.chunk_for_rag):
if feature == "rag_chunking" and (
not hasattr(args, "chunk_for_rag") or not args.chunk_for_rag
):
args.chunk_for_rag = enabled
def show_scrape_preset_list() -> None:
"""Print the list of available scrape presets to stdout."""
print("\nAvailable Scrape Presets")

View File

@@ -13,6 +13,7 @@ import logging
logger = logging.getLogger(__name__)
@dataclass
class SourceInfo:
"""Information about a detected source.
@@ -23,18 +24,20 @@ class SourceInfo:
suggested_name: Auto-suggested name for the skill
raw_input: Original user input
"""
type: str
parsed: dict[str, Any]
suggested_name: str
raw_input: str
class SourceDetector:
"""Detects source type from user input and extracts relevant information."""
# GitHub repo patterns
GITHUB_REPO_PATTERN = re.compile(r'^([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)$')
GITHUB_REPO_PATTERN = re.compile(r"^([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)$")
GITHUB_URL_PATTERN = re.compile(
r'(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)(?:\.git)?'
r"(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)(?:\.git)?"
)
@classmethod
@@ -51,10 +54,10 @@ class SourceDetector:
ValueError: If source type cannot be determined
"""
# 1. File extension detection
if source.endswith('.json'):
if source.endswith(".json"):
return cls._detect_config(source)
if source.endswith('.pdf'):
if source.endswith(".pdf"):
return cls._detect_pdf(source)
# 2. Directory detection
@@ -67,12 +70,12 @@ class SourceDetector:
return github_info
# 4. URL detection
if source.startswith('http://') or source.startswith('https://'):
if source.startswith("http://") or source.startswith("https://"):
return cls._detect_web(source)
# 5. Domain inference (add https://)
if '.' in source and not source.startswith('/'):
return cls._detect_web(f'https://{source}')
if "." in source and not source.startswith("/"):
return cls._detect_web(f"https://{source}")
# 6. Error - cannot determine
raise ValueError(
@@ -90,10 +93,7 @@ class SourceDetector:
"""Detect config file source."""
name = os.path.splitext(os.path.basename(source))[0]
return SourceInfo(
type='config',
parsed={'config_path': source},
suggested_name=name,
raw_input=source
type="config", parsed={"config_path": source}, suggested_name=name, raw_input=source
)
@classmethod
@@ -101,10 +101,7 @@ class SourceDetector:
"""Detect PDF file source."""
name = os.path.splitext(os.path.basename(source))[0]
return SourceInfo(
type='pdf',
parsed={'file_path': source},
suggested_name=name,
raw_input=source
type="pdf", parsed={"file_path": source}, suggested_name=name, raw_input=source
)
@classmethod
@@ -115,10 +112,7 @@ class SourceDetector:
name = os.path.basename(directory)
return SourceInfo(
type='local',
parsed={'directory': directory},
suggested_name=name,
raw_input=source
type="local", parsed={"directory": directory}, suggested_name=name, raw_input=source
)
@classmethod
@@ -135,10 +129,10 @@ class SourceDetector:
if match:
owner, repo = match.groups()
return SourceInfo(
type='github',
parsed={'repo': f'{owner}/{repo}'},
type="github",
parsed={"repo": f"{owner}/{repo}"},
suggested_name=repo,
raw_input=source
raw_input=source,
)
# Try GitHub URL pattern
@@ -146,13 +140,13 @@ class SourceDetector:
if match:
owner, repo = match.groups()
# Clean up repo name (remove .git suffix if present)
if repo.endswith('.git'):
if repo.endswith(".git"):
repo = repo[:-4]
return SourceInfo(
type='github',
parsed={'repo': f'{owner}/{repo}'},
type="github",
parsed={"repo": f"{owner}/{repo}"},
suggested_name=repo,
raw_input=source
raw_input=source,
)
return None
@@ -167,15 +161,10 @@ class SourceDetector:
# Clean up domain for name suggestion
# docs.react.dev -> react
# reactjs.org -> react
name = domain.replace('www.', '').replace('docs.', '')
name = name.split('.')[0] # Take first part before TLD
name = domain.replace("www.", "").replace("docs.", "")
name = name.split(".")[0] # Take first part before TLD
return SourceInfo(
type='web',
parsed={'url': source},
suggested_name=name,
raw_input=source
)
return SourceInfo(type="web", parsed={"url": source}, suggested_name=name, raw_input=source)
@classmethod
def validate_source(cls, source_info: SourceInfo) -> None:
@@ -187,22 +176,22 @@ class SourceDetector:
Raises:
ValueError: If source is not accessible
"""
if source_info.type == 'local':
directory = source_info.parsed['directory']
if source_info.type == "local":
directory = source_info.parsed["directory"]
if not os.path.exists(directory):
raise ValueError(f"Directory does not exist: {directory}")
if not os.path.isdir(directory):
raise ValueError(f"Path is not a directory: {directory}")
elif source_info.type == 'pdf':
file_path = source_info.parsed['file_path']
elif source_info.type == "pdf":
file_path = source_info.parsed["file_path"]
if not os.path.exists(file_path):
raise ValueError(f"PDF file does not exist: {file_path}")
if not os.path.isfile(file_path):
raise ValueError(f"Path is not a file: {file_path}")
elif source_info.type == 'config':
config_path = source_info.parsed['config_path']
elif source_info.type == "config":
config_path = source_info.parsed["config_path"]
if not os.path.exists(config_path):
raise ValueError(f"Config file does not exist: {config_path}")
if not os.path.isfile(config_path):

View File

@@ -13,15 +13,14 @@ import pytest
import subprocess
import argparse
class TestParserSync:
"""E2E tests for parser synchronization (Issue #285)."""
def test_scrape_interactive_flag_works(self):
"""Test that --interactive flag (previously missing) now works."""
result = subprocess.run(
["skill-seekers", "scrape", "--interactive", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--interactive", "--help"], capture_output=True, text=True
)
assert result.returncode == 0, "Command should execute successfully"
assert "--interactive" in result.stdout, "Help should show --interactive flag"
@@ -30,9 +29,7 @@ class TestParserSync:
def test_scrape_chunk_for_rag_flag_works(self):
"""Test that --chunk-for-rag flag (previously missing) now works."""
result = subprocess.run(
["skill-seekers", "scrape", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
)
assert "--chunk-for-rag" in result.stdout, "Help should show --chunk-for-rag flag"
assert "--chunk-size" in result.stdout, "Help should show --chunk-size flag"
@@ -41,9 +38,7 @@ class TestParserSync:
def test_scrape_verbose_flag_works(self):
"""Test that --verbose flag (previously missing) now works."""
result = subprocess.run(
["skill-seekers", "scrape", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
)
assert "--verbose" in result.stdout, "Help should show --verbose flag"
assert "-v" in result.stdout, "Help should show short form -v"
@@ -51,18 +46,14 @@ class TestParserSync:
def test_scrape_url_flag_works(self):
"""Test that --url flag (previously missing) now works."""
result = subprocess.run(
["skill-seekers", "scrape", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
)
assert "--url URL" in result.stdout, "Help should show --url flag"
def test_github_all_flags_present(self):
"""Test that github command has all expected flags."""
result = subprocess.run(
["skill-seekers", "github", "--help"],
capture_output=True,
text=True
["skill-seekers", "github", "--help"], capture_output=True, text=True
)
# Key github flags that should be present
expected_flags = [
@@ -74,15 +65,14 @@ class TestParserSync:
for flag in expected_flags:
assert flag in result.stdout, f"Help should show {flag} flag"
class TestPresetSystem:
"""E2E tests for preset system (Issue #268)."""
def test_analyze_preset_flag_exists(self):
"""Test that analyze command has --preset flag."""
result = subprocess.run(
["skill-seekers", "analyze", "--help"],
capture_output=True,
text=True
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
)
assert "--preset" in result.stdout, "Help should show --preset flag"
assert "quick" in result.stdout, "Help should mention 'quick' preset"
@@ -92,18 +82,14 @@ class TestPresetSystem:
def test_analyze_preset_list_flag_exists(self):
"""Test that analyze command has --preset-list flag."""
result = subprocess.run(
["skill-seekers", "analyze", "--help"],
capture_output=True,
text=True
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
)
assert "--preset-list" in result.stdout, "Help should show --preset-list flag"
def test_preset_list_shows_presets(self):
"""Test that --preset-list shows all available presets."""
result = subprocess.run(
["skill-seekers", "analyze", "--preset-list"],
capture_output=True,
text=True
["skill-seekers", "analyze", "--preset-list"], capture_output=True, text=True
)
assert result.returncode == 0, "Command should execute successfully"
assert "Available presets" in result.stdout, "Should show preset list header"
@@ -118,7 +104,7 @@ class TestPresetSystem:
result = subprocess.run(
["skill-seekers", "analyze", "--directory", ".", "--quick"],
capture_output=True,
text=True
text=True,
)
# Note: Deprecation warnings go to stderr
output = result.stdout + result.stderr
@@ -131,22 +117,19 @@ class TestPresetSystem:
result = subprocess.run(
["skill-seekers", "analyze", "--directory", ".", "--comprehensive"],
capture_output=True,
text=True
text=True,
)
output = result.stdout + result.stderr
assert "DEPRECATED" in output, "Should show deprecation warning"
assert "--preset comprehensive" in output, "Should suggest alternative"
class TestBackwardCompatibility:
"""E2E tests for backward compatibility."""
def test_old_scrape_command_still_works(self):
"""Test that old scrape command invocations still work."""
result = subprocess.run(
["skill-seekers-scrape", "--help"],
capture_output=True,
text=True
)
result = subprocess.run(["skill-seekers-scrape", "--help"], capture_output=True, text=True)
assert result.returncode == 0, "Old command should still work"
assert "documentation" in result.stdout.lower(), "Help should mention documentation"
@@ -154,16 +137,12 @@ class TestBackwardCompatibility:
"""Test that unified CLI and standalone have identical arguments."""
# Get help from unified CLI
unified_result = subprocess.run(
["skill-seekers", "scrape", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
)
# Get help from standalone
standalone_result = subprocess.run(
["skill-seekers-scrape", "--help"],
capture_output=True,
text=True
["skill-seekers-scrape", "--help"], capture_output=True, text=True
)
# Both should have the same key flags
@@ -180,6 +159,7 @@ class TestBackwardCompatibility:
assert flag in unified_result.stdout, f"Unified should have {flag}"
assert flag in standalone_result.stdout, f"Standalone should have {flag}"
class TestProgrammaticAPI:
"""Test that the shared argument functions work programmatically."""
@@ -221,16 +201,13 @@ class TestProgrammaticAPI:
# Note: enhance_level is not part of AnalysisPreset anymore.
# It's controlled separately via --enhance-level flag (default 2)
class TestIntegration:
"""Integration tests for the complete flow."""
def test_unified_cli_subcommands_registered(self):
"""Test that all subcommands are properly registered."""
result = subprocess.run(
["skill-seekers", "--help"],
capture_output=True,
text=True
)
result = subprocess.run(["skill-seekers", "--help"], capture_output=True, text=True)
# All major commands should be listed
expected_commands = [
@@ -250,9 +227,7 @@ class TestIntegration:
def test_scrape_help_detailed(self):
"""Test that scrape help shows all argument details."""
result = subprocess.run(
["skill-seekers", "scrape", "--help"],
capture_output=True,
text=True
["skill-seekers", "scrape", "--help"], capture_output=True, text=True
)
# Check for argument categories
@@ -263,13 +238,14 @@ class TestIntegration:
def test_analyze_help_shows_presets(self):
"""Test that analyze help prominently shows preset information."""
result = subprocess.run(
["skill-seekers", "analyze", "--help"],
capture_output=True,
text=True
["skill-seekers", "analyze", "--help"], capture_output=True, text=True
)
assert "--preset" in result.stdout, "Should show --preset flag"
assert "DEFAULT" in result.stdout or "default" in result.stdout, "Should indicate default preset"
assert "DEFAULT" in result.stdout or "default" in result.stdout, (
"Should indicate default preset"
)
class TestE2EWorkflow:
"""End-to-end workflow tests."""
@@ -279,15 +255,18 @@ class TestE2EWorkflow:
"""Test scraping with previously missing arguments (dry run)."""
result = subprocess.run(
[
"skill-seekers", "scrape",
"--url", "https://example.com",
"--interactive", "false", # Would fail if arg didn't exist
"skill-seekers",
"scrape",
"--url",
"https://example.com",
"--interactive",
"false", # Would fail if arg didn't exist
"--verbose", # Would fail if arg didn't exist
"--dry-run",
],
capture_output=True,
text=True,
timeout=10
timeout=10,
)
# Dry run should complete without errors
@@ -314,5 +293,6 @@ class TestE2EWorkflow:
assert "--preset" in result.stdout, "Should have --preset flag"
assert "unrecognized arguments" not in result.stderr.lower()
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])

View File

@@ -19,6 +19,7 @@ from skill_seekers.cli.arguments.create import (
add_create_arguments,
)
class TestUniversalArguments:
"""Test universal argument definitions."""
@@ -29,25 +30,34 @@ class TestUniversalArguments:
def test_universal_argument_names(self):
"""Universal arguments should have expected names."""
expected_names = {
'name', 'description', 'output',
'enhance_level', 'api_key', # Phase 1: consolidated from enhance + enhance_local
'dry_run', 'verbose', 'quiet',
'chunk_for_rag', 'chunk_size', 'chunk_overlap', # Phase 2: RAG args from common.py
'preset', 'config'
"name",
"description",
"output",
"enhance_level",
"api_key", # Phase 1: consolidated from enhance + enhance_local
"dry_run",
"verbose",
"quiet",
"chunk_for_rag",
"chunk_size",
"chunk_overlap", # Phase 2: RAG args from common.py
"preset",
"config",
}
assert set(UNIVERSAL_ARGUMENTS.keys()) == expected_names
def test_all_universal_have_flags(self):
"""All universal arguments should have flags."""
for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items():
assert 'flags' in arg_def
assert len(arg_def['flags']) > 0
assert "flags" in arg_def
assert len(arg_def["flags"]) > 0
def test_all_universal_have_kwargs(self):
"""All universal arguments should have kwargs."""
for arg_name, arg_def in UNIVERSAL_ARGUMENTS.items():
assert 'kwargs' in arg_def
assert 'help' in arg_def['kwargs']
assert "kwargs" in arg_def
assert "help" in arg_def["kwargs"]
class TestSourceSpecificArguments:
"""Test source-specific argument definitions."""
@@ -55,29 +65,29 @@ class TestSourceSpecificArguments:
def test_web_arguments_exist(self):
"""Web-specific arguments should be defined."""
assert len(WEB_ARGUMENTS) > 0
assert 'max_pages' in WEB_ARGUMENTS
assert 'rate_limit' in WEB_ARGUMENTS
assert 'workers' in WEB_ARGUMENTS
assert "max_pages" in WEB_ARGUMENTS
assert "rate_limit" in WEB_ARGUMENTS
assert "workers" in WEB_ARGUMENTS
def test_github_arguments_exist(self):
"""GitHub-specific arguments should be defined."""
assert len(GITHUB_ARGUMENTS) > 0
assert 'repo' in GITHUB_ARGUMENTS
assert 'token' in GITHUB_ARGUMENTS
assert 'max_issues' in GITHUB_ARGUMENTS
assert "repo" in GITHUB_ARGUMENTS
assert "token" in GITHUB_ARGUMENTS
assert "max_issues" in GITHUB_ARGUMENTS
def test_local_arguments_exist(self):
"""Local-specific arguments should be defined."""
assert len(LOCAL_ARGUMENTS) > 0
assert 'directory' in LOCAL_ARGUMENTS
assert 'languages' in LOCAL_ARGUMENTS
assert 'skip_patterns' in LOCAL_ARGUMENTS
assert "directory" in LOCAL_ARGUMENTS
assert "languages" in LOCAL_ARGUMENTS
assert "skip_patterns" in LOCAL_ARGUMENTS
def test_pdf_arguments_exist(self):
"""PDF-specific arguments should be defined."""
assert len(PDF_ARGUMENTS) > 0
assert 'pdf' in PDF_ARGUMENTS
assert 'ocr' in PDF_ARGUMENTS
assert "pdf" in PDF_ARGUMENTS
assert "ocr" in PDF_ARGUMENTS
def test_no_duplicate_flags_across_sources(self):
"""Source-specific arguments should not have duplicate flags."""
@@ -86,21 +96,25 @@ class TestSourceSpecificArguments:
for source_args in [WEB_ARGUMENTS, GITHUB_ARGUMENTS, LOCAL_ARGUMENTS, PDF_ARGUMENTS]:
for arg_name, arg_def in source_args.items():
flags = arg_def['flags']
flags = arg_def["flags"]
for flag in flags:
# Check if this flag already exists in source-specific args
if flag not in [f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg['flags']]:
if flag not in [
f for arg in UNIVERSAL_ARGUMENTS.values() for f in arg["flags"]
]:
assert flag not in all_flags, f"Duplicate flag: {flag}"
all_flags.add(flag)
class TestAdvancedArguments:
"""Test advanced/rare argument definitions."""
def test_advanced_arguments_exist(self):
"""Advanced arguments should be defined."""
assert len(ADVANCED_ARGUMENTS) > 0
assert 'no_rate_limit' in ADVANCED_ARGUMENTS
assert 'interactive_enhancement' in ADVANCED_ARGUMENTS
assert "no_rate_limit" in ADVANCED_ARGUMENTS
assert "interactive_enhancement" in ADVANCED_ARGUMENTS
class TestArgumentHelpers:
"""Test helper functions."""
@@ -110,106 +124,108 @@ class TestArgumentHelpers:
names = get_universal_argument_names()
assert isinstance(names, set)
assert len(names) == 13
assert 'name' in names
assert 'enhance_level' in names # Phase 1: consolidated flag
assert "name" in names
assert "enhance_level" in names # Phase 1: consolidated flag
def test_get_source_specific_web(self):
"""Should return web-specific arguments."""
args = get_source_specific_arguments('web')
args = get_source_specific_arguments("web")
assert args == WEB_ARGUMENTS
def test_get_source_specific_github(self):
"""Should return github-specific arguments."""
args = get_source_specific_arguments('github')
args = get_source_specific_arguments("github")
assert args == GITHUB_ARGUMENTS
def test_get_source_specific_local(self):
"""Should return local-specific arguments."""
args = get_source_specific_arguments('local')
args = get_source_specific_arguments("local")
assert args == LOCAL_ARGUMENTS
def test_get_source_specific_pdf(self):
"""Should return pdf-specific arguments."""
args = get_source_specific_arguments('pdf')
args = get_source_specific_arguments("pdf")
assert args == PDF_ARGUMENTS
def test_get_source_specific_config(self):
"""Config should return empty dict (no extra args)."""
args = get_source_specific_arguments('config')
args = get_source_specific_arguments("config")
assert args == {}
def test_get_source_specific_unknown(self):
"""Unknown source should return empty dict."""
args = get_source_specific_arguments('unknown')
args = get_source_specific_arguments("unknown")
assert args == {}
class TestCompatibleArguments:
"""Test compatible argument detection."""
def test_web_compatible_arguments(self):
"""Web source should include universal + web + advanced."""
compatible = get_compatible_arguments('web')
compatible = get_compatible_arguments("web")
# Should include universal arguments
assert 'name' in compatible
assert 'enhance_level' in compatible # Phase 1: consolidated flag
assert "name" in compatible
assert "enhance_level" in compatible # Phase 1: consolidated flag
# Should include web-specific arguments
assert 'max_pages' in compatible
assert 'rate_limit' in compatible
assert "max_pages" in compatible
assert "rate_limit" in compatible
# Should include advanced arguments
assert 'no_rate_limit' in compatible
assert "no_rate_limit" in compatible
def test_github_compatible_arguments(self):
"""GitHub source should include universal + github + advanced."""
compatible = get_compatible_arguments('github')
compatible = get_compatible_arguments("github")
# Should include universal arguments
assert 'name' in compatible
assert "name" in compatible
# Should include github-specific arguments
assert 'repo' in compatible
assert 'token' in compatible
assert "repo" in compatible
assert "token" in compatible
# Should include advanced arguments
assert 'interactive_enhancement' in compatible
assert "interactive_enhancement" in compatible
def test_local_compatible_arguments(self):
"""Local source should include universal + local + advanced."""
compatible = get_compatible_arguments('local')
compatible = get_compatible_arguments("local")
# Should include universal arguments
assert 'description' in compatible
assert "description" in compatible
# Should include local-specific arguments
assert 'directory' in compatible
assert 'languages' in compatible
assert "directory" in compatible
assert "languages" in compatible
def test_pdf_compatible_arguments(self):
"""PDF source should include universal + pdf + advanced."""
compatible = get_compatible_arguments('pdf')
compatible = get_compatible_arguments("pdf")
# Should include universal arguments
assert 'output' in compatible
assert "output" in compatible
# Should include pdf-specific arguments
assert 'pdf' in compatible
assert 'ocr' in compatible
assert "pdf" in compatible
assert "ocr" in compatible
def test_config_compatible_arguments(self):
"""Config source should include universal + advanced only."""
compatible = get_compatible_arguments('config')
compatible = get_compatible_arguments("config")
# Should include universal arguments
assert 'config' in compatible
assert "config" in compatible
# Should include advanced arguments
assert 'no_preserve_code_blocks' in compatible
assert "no_preserve_code_blocks" in compatible
# Should not include source-specific arguments
assert 'repo' not in compatible
assert 'directory' not in compatible
assert "repo" not in compatible
assert "directory" not in compatible
class TestAddCreateArguments:
"""Test add_create_arguments function."""
@@ -217,16 +233,17 @@ class TestAddCreateArguments:
def test_default_mode_adds_universal_only(self):
"""Default mode should add only universal arguments + source positional."""
import argparse
parser = argparse.ArgumentParser()
add_create_arguments(parser, mode='default')
add_create_arguments(parser, mode="default")
# Parse to get all arguments
args = vars(parser.parse_args([]))
# Should have universal arguments
assert 'name' in args
assert 'enhance_level' in args
assert 'chunk_for_rag' in args
assert "name" in args
assert "enhance_level" in args
assert "chunk_for_rag" in args
# Should not have source-specific arguments (they're not added in default mode)
# Note: argparse won't error on unknown args, but they won't be in namespace
@@ -234,62 +251,60 @@ class TestAddCreateArguments:
def test_web_mode_adds_web_arguments(self):
"""Web mode should add universal + web arguments."""
import argparse
parser = argparse.ArgumentParser()
add_create_arguments(parser, mode='web')
add_create_arguments(parser, mode="web")
args = vars(parser.parse_args([]))
# Should have universal arguments
assert 'name' in args
assert "name" in args
# Should have web-specific arguments
assert 'max_pages' in args
assert 'rate_limit' in args
assert "max_pages" in args
assert "rate_limit" in args
def test_all_mode_adds_all_arguments(self):
"""All mode should add every argument."""
import argparse
parser = argparse.ArgumentParser()
add_create_arguments(parser, mode='all')
add_create_arguments(parser, mode="all")
args = vars(parser.parse_args([]))
# Should have universal arguments
assert 'name' in args
assert "name" in args
# Should have all source-specific arguments
assert 'max_pages' in args # web
assert 'repo' in args # github
assert 'directory' in args # local
assert 'pdf' in args # pdf
assert "max_pages" in args # web
assert "repo" in args # github
assert "directory" in args # local
assert "pdf" in args # pdf
# Should have advanced arguments
assert 'no_rate_limit' in args
assert "no_rate_limit" in args
def test_positional_source_argument_always_added(self):
"""Source positional argument should always be added."""
import argparse
for mode in ['default', 'web', 'github', 'local', 'pdf', 'all']:
for mode in ["default", "web", "github", "local", "pdf", "all"]:
parser = argparse.ArgumentParser()
add_create_arguments(parser, mode=mode)
# Should accept source as positional
args = parser.parse_args(['some_source'])
assert args.source == 'some_source'
args = parser.parse_args(["some_source"])
assert args.source == "some_source"
class TestNoDuplicates:
"""Test that there are no duplicate arguments across tiers."""
def test_no_duplicates_between_universal_and_web(self):
"""Universal and web args should not overlap."""
universal_flags = {
flag for arg in UNIVERSAL_ARGUMENTS.values()
for flag in arg['flags']
}
web_flags = {
flag for arg in WEB_ARGUMENTS.values()
for flag in arg['flags']
}
universal_flags = {flag for arg in UNIVERSAL_ARGUMENTS.values() for flag in arg["flags"]}
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]}
# Allow some overlap since we intentionally include common args
# in multiple places, but check that they're properly defined
@@ -299,10 +314,10 @@ class TestNoDuplicates:
def test_no_duplicates_between_source_specific_args(self):
"""Different source-specific arg groups should not overlap."""
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg['flags']}
github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg['flags']}
local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg['flags']}
pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg['flags']}
web_flags = {flag for arg in WEB_ARGUMENTS.values() for flag in arg["flags"]}
github_flags = {flag for arg in GITHUB_ARGUMENTS.values() for flag in arg["flags"]}
local_flags = {flag for arg in LOCAL_ARGUMENTS.values() for flag in arg["flags"]}
pdf_flags = {flag for arg in PDF_ARGUMENTS.values() for flag in arg["flags"]}
# No overlap between different source types
assert len(web_flags & github_flags) == 0
@@ -312,6 +327,7 @@ class TestNoDuplicates:
assert len(github_flags & pdf_flags) == 0
assert len(local_flags & pdf_flags) == 0
class TestArgumentQuality:
"""Test argument definition quality."""
@@ -327,8 +343,8 @@ class TestArgumentQuality:
}
for arg_name, arg_def in all_args.items():
assert 'help' in arg_def['kwargs'], f"{arg_name} missing help text"
assert len(arg_def['kwargs']['help']) > 0, f"{arg_name} has empty help text"
assert "help" in arg_def["kwargs"], f"{arg_name} missing help text"
assert len(arg_def["kwargs"]["help"]) > 0, f"{arg_name} has empty help text"
def test_boolean_arguments_use_store_true(self):
"""Boolean flags should use store_true action."""
@@ -342,13 +358,25 @@ class TestArgumentQuality:
}
boolean_args = [
'dry_run', 'verbose', 'quiet',
'chunk_for_rag', 'skip_scrape', 'resume', 'fresh', 'async_mode',
'no_issues', 'no_changelog', 'no_releases', 'scrape_only',
'skip_patterns', 'skip_test_examples', 'ocr', 'no_rate_limit'
"dry_run",
"verbose",
"quiet",
"chunk_for_rag",
"skip_scrape",
"resume",
"fresh",
"async_mode",
"no_issues",
"no_changelog",
"no_releases",
"scrape_only",
"skip_patterns",
"skip_test_examples",
"ocr",
"no_rate_limit",
]
for arg_name in boolean_args:
if arg_name in all_args:
action = all_args[arg_name]['kwargs'].get('action')
assert action == 'store_true', f"{arg_name} should use store_true"
action = all_args[arg_name]["kwargs"].get("action")
assert action == "store_true", f"{arg_name} should use store_true"

View File

@@ -6,21 +6,21 @@ and routes to the correct scrapers without actually scraping.
import pytest
class TestCreateCommandBasic:
"""Basic integration tests for create command (dry-run mode)."""
def test_create_command_help(self):
"""Test that create command help works."""
import subprocess
result = subprocess.run(
['skill-seekers', 'create', '--help'],
capture_output=True,
text=True
["skill-seekers", "create", "--help"], capture_output=True, text=True
)
assert result.returncode == 0
assert 'Auto-detects source type' in result.stdout
assert 'auto-detected' in result.stdout
assert '--help-web' in result.stdout
assert "Auto-detects source type" in result.stdout
assert "auto-detected" in result.stdout
assert "--help-web" in result.stdout
def test_create_detects_web_url(self):
"""Test that web URLs are detected and routed correctly."""
@@ -31,11 +31,12 @@ class TestCreateCommandBasic:
def test_create_detects_github_repo(self):
"""Test that GitHub repos are detected."""
import subprocess
result = subprocess.run(
['skill-seekers', 'create', 'facebook/react', '--help'],
["skill-seekers", "create", "facebook/react", "--help"],
capture_output=True,
text=True,
timeout=10
timeout=10,
)
# Just verify help works - actual scraping would need API token
assert result.returncode in [0, 2] # 0 for success, 2 for argparse help
@@ -49,10 +50,10 @@ class TestCreateCommandBasic:
test_dir.mkdir()
result = subprocess.run(
['skill-seekers', 'create', str(test_dir), '--help'],
["skill-seekers", "create", str(test_dir), "--help"],
capture_output=True,
text=True,
timeout=10
timeout=10,
)
# Verify help works
assert result.returncode in [0, 2]
@@ -66,10 +67,10 @@ class TestCreateCommandBasic:
pdf_file.touch()
result = subprocess.run(
['skill-seekers', 'create', str(pdf_file), '--help'],
["skill-seekers", "create", str(pdf_file), "--help"],
capture_output=True,
text=True,
timeout=10
timeout=10,
)
# Verify help works
assert result.returncode in [0, 2]
@@ -81,17 +82,14 @@ class TestCreateCommandBasic:
# Create a minimal config file
config_file = tmp_path / "test.json"
config_data = {
"name": "test",
"base_url": "https://example.com/"
}
config_data = {"name": "test", "base_url": "https://example.com/"}
config_file.write_text(json.dumps(config_data))
result = subprocess.run(
['skill-seekers', 'create', str(config_file), '--help'],
["skill-seekers", "create", str(config_file), "--help"],
capture_output=True,
text=True,
timeout=10
timeout=10,
)
# Verify help works
assert result.returncode in [0, 2]
@@ -105,20 +103,19 @@ class TestCreateCommandBasic:
def test_create_supports_universal_flags(self):
"""Test that universal flags are accepted."""
import subprocess
result = subprocess.run(
['skill-seekers', 'create', '--help'],
capture_output=True,
text=True,
timeout=10
["skill-seekers", "create", "--help"], capture_output=True, text=True, timeout=10
)
assert result.returncode == 0
# Check that universal flags are present
assert '--name' in result.stdout
assert '--enhance' in result.stdout
assert '--chunk-for-rag' in result.stdout
assert '--preset' in result.stdout
assert '--dry-run' in result.stdout
assert "--name" in result.stdout
assert "--enhance" in result.stdout
assert "--chunk-for-rag" in result.stdout
assert "--preset" in result.stdout
assert "--dry-run" in result.stdout
class TestBackwardCompatibility:
"""Test that old commands still work."""
@@ -126,53 +123,45 @@ class TestBackwardCompatibility:
def test_scrape_command_still_works(self):
"""Old scrape command should still function."""
import subprocess
result = subprocess.run(
['skill-seekers', 'scrape', '--help'],
capture_output=True,
text=True,
timeout=10
["skill-seekers", "scrape", "--help"], capture_output=True, text=True, timeout=10
)
assert result.returncode == 0
assert 'scrape' in result.stdout.lower()
assert "scrape" in result.stdout.lower()
def test_github_command_still_works(self):
"""Old github command should still function."""
import subprocess
result = subprocess.run(
['skill-seekers', 'github', '--help'],
capture_output=True,
text=True,
timeout=10
["skill-seekers", "github", "--help"], capture_output=True, text=True, timeout=10
)
assert result.returncode == 0
assert 'github' in result.stdout.lower()
assert "github" in result.stdout.lower()
def test_analyze_command_still_works(self):
"""Old analyze command should still function."""
import subprocess
result = subprocess.run(
['skill-seekers', 'analyze', '--help'],
capture_output=True,
text=True,
timeout=10
["skill-seekers", "analyze", "--help"], capture_output=True, text=True, timeout=10
)
assert result.returncode == 0
assert 'analyze' in result.stdout.lower()
assert "analyze" in result.stdout.lower()
def test_main_help_shows_all_commands(self):
"""Main help should show both old and new commands."""
import subprocess
result = subprocess.run(
['skill-seekers', '--help'],
capture_output=True,
text=True,
timeout=10
["skill-seekers", "--help"], capture_output=True, text=True, timeout=10
)
assert result.returncode == 0
# Should show create command
assert 'create' in result.stdout
assert "create" in result.stdout
# Should still show old commands
assert 'scrape' in result.stdout
assert 'github' in result.stdout
assert 'analyze' in result.stdout
assert "scrape" in result.stdout
assert "github" in result.stdout
assert "analyze" in result.stdout

View File

@@ -167,7 +167,8 @@ class TestIssue219Problem2CLIFlags(unittest.TestCase):
"test/test",
"--name",
"test",
"--enhance-level", "2",
"--enhance-level",
"2",
]
with (

View File

@@ -7,6 +7,7 @@ the same arguments as the standalone scraper modules. This prevents the
import argparse
class TestScrapeParserSync:
"""Ensure scrape_parser has all arguments from doc_scraper."""
@@ -17,12 +18,12 @@ class TestScrapeParserSync:
# Get source arguments from doc_scraper
source_parser = setup_argument_parser()
source_count = len([a for a in source_parser._actions if a.dest != 'help'])
source_count = len([a for a in source_parser._actions if a.dest != "help"])
# Get target arguments from unified CLI parser
target_parser = argparse.ArgumentParser()
ScrapeParser().add_arguments(target_parser)
target_count = len([a for a in target_parser._actions if a.dest != 'help'])
target_count = len([a for a in target_parser._actions if a.dest != "help"])
assert source_count == target_count, (
f"Argument count mismatch: doc_scraper has {source_count}, "
@@ -36,12 +37,12 @@ class TestScrapeParserSync:
# Get source arguments from doc_scraper
source_parser = setup_argument_parser()
source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'}
source_dests = {a.dest for a in source_parser._actions if a.dest != "help"}
# Get target arguments from unified CLI parser
target_parser = argparse.ArgumentParser()
ScrapeParser().add_arguments(target_parser)
target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'}
target_dests = {a.dest for a in target_parser._actions if a.dest != "help"}
# Check for missing arguments
missing = source_dests - target_dests
@@ -64,27 +65,28 @@ class TestScrapeParserSync:
break
assert subparsers_action is not None, "No subparsers found"
assert 'scrape' in subparsers_action.choices, "scrape subparser not found"
assert "scrape" in subparsers_action.choices, "scrape subparser not found"
scrape_parser = subparsers_action.choices['scrape']
arg_dests = {a.dest for a in scrape_parser._actions if a.dest != 'help'}
scrape_parser = subparsers_action.choices["scrape"]
arg_dests = {a.dest for a in scrape_parser._actions if a.dest != "help"}
# Check key arguments that were missing in Issue #285
required_args = [
'interactive',
'url',
'verbose',
'quiet',
'resume',
'fresh',
'rate_limit',
'no_rate_limit',
'chunk_for_rag',
"interactive",
"url",
"verbose",
"quiet",
"resume",
"fresh",
"rate_limit",
"no_rate_limit",
"chunk_for_rag",
]
for arg in required_args:
assert arg in arg_dests, f"Required argument '{arg}' missing from scrape parser"
class TestGitHubParserSync:
"""Ensure github_parser has all arguments from github_scraper."""
@@ -95,12 +97,12 @@ class TestGitHubParserSync:
# Get source arguments from github_scraper
source_parser = setup_argument_parser()
source_count = len([a for a in source_parser._actions if a.dest != 'help'])
source_count = len([a for a in source_parser._actions if a.dest != "help"])
# Get target arguments from unified CLI parser
target_parser = argparse.ArgumentParser()
GitHubParser().add_arguments(target_parser)
target_count = len([a for a in target_parser._actions if a.dest != 'help'])
target_count = len([a for a in target_parser._actions if a.dest != "help"])
assert source_count == target_count, (
f"Argument count mismatch: github_scraper has {source_count}, "
@@ -114,12 +116,12 @@ class TestGitHubParserSync:
# Get source arguments from github_scraper
source_parser = setup_argument_parser()
source_dests = {a.dest for a in source_parser._actions if a.dest != 'help'}
source_dests = {a.dest for a in source_parser._actions if a.dest != "help"}
# Get target arguments from unified CLI parser
target_parser = argparse.ArgumentParser()
GitHubParser().add_arguments(target_parser)
target_dests = {a.dest for a in target_parser._actions if a.dest != 'help'}
target_dests = {a.dest for a in target_parser._actions if a.dest != "help"}
# Check for missing arguments
missing = source_dests - target_dests
@@ -128,6 +130,7 @@ class TestGitHubParserSync:
assert not missing, f"github_parser missing arguments: {missing}"
assert not extra, f"github_parser has extra arguments not in github_scraper: {extra}"
class TestUnifiedCLI:
"""Test the unified CLI main parser."""
@@ -154,7 +157,7 @@ class TestUnifiedCLI:
assert subparsers_action is not None, "No subparsers found"
# Check expected subcommands
expected_commands = ['scrape', 'github']
expected_commands = ["scrape", "github"]
for cmd in expected_commands:
assert cmd in subparsers_action.choices, f"Subcommand '{cmd}' not found"
@@ -166,7 +169,7 @@ class TestUnifiedCLI:
# This should not raise an exception
try:
parser.parse_args(['scrape', '--help'])
parser.parse_args(["scrape", "--help"])
except SystemExit as e:
# --help causes SystemExit(0) which is expected
assert e.code == 0
@@ -179,7 +182,7 @@ class TestUnifiedCLI:
# This should not raise an exception
try:
parser.parse_args(['github', '--help'])
parser.parse_args(["github", "--help"])
except SystemExit as e:
# --help causes SystemExit(0) which is expected
assert e.code == 0

View File

@@ -13,47 +13,49 @@ import pytest
from skill_seekers.cli.source_detector import SourceDetector, SourceInfo
class TestWebDetection:
"""Test web URL detection."""
def test_detect_full_https_url(self):
"""Full HTTPS URL should be detected as web."""
info = SourceDetector.detect("https://docs.react.dev/")
assert info.type == 'web'
assert info.parsed['url'] == "https://docs.react.dev/"
assert info.suggested_name == 'react'
assert info.type == "web"
assert info.parsed["url"] == "https://docs.react.dev/"
assert info.suggested_name == "react"
def test_detect_full_http_url(self):
"""Full HTTP URL should be detected as web."""
info = SourceDetector.detect("http://example.com/docs")
assert info.type == 'web'
assert info.parsed['url'] == "http://example.com/docs"
assert info.type == "web"
assert info.parsed["url"] == "http://example.com/docs"
def test_detect_domain_only(self):
"""Domain without protocol should add https:// and detect as web."""
info = SourceDetector.detect("docs.react.dev")
assert info.type == 'web'
assert info.parsed['url'] == "https://docs.react.dev"
assert info.suggested_name == 'react'
assert info.type == "web"
assert info.parsed["url"] == "https://docs.react.dev"
assert info.suggested_name == "react"
def test_detect_complex_url(self):
"""Complex URL with path should be detected as web."""
info = SourceDetector.detect("https://docs.python.org/3/library/")
assert info.type == 'web'
assert info.parsed['url'] == "https://docs.python.org/3/library/"
assert info.suggested_name == 'python'
assert info.type == "web"
assert info.parsed["url"] == "https://docs.python.org/3/library/"
assert info.suggested_name == "python"
def test_suggested_name_removes_www(self):
"""Should remove www. prefix from suggested name."""
info = SourceDetector.detect("https://www.example.com/")
assert info.type == 'web'
assert info.suggested_name == 'example'
assert info.type == "web"
assert info.suggested_name == "example"
def test_suggested_name_removes_docs(self):
"""Should remove docs. prefix from suggested name."""
info = SourceDetector.detect("https://docs.vue.org/")
assert info.type == 'web'
assert info.suggested_name == 'vue'
assert info.type == "web"
assert info.suggested_name == "vue"
class TestGitHubDetection:
"""Test GitHub repository detection."""
@@ -61,37 +63,38 @@ class TestGitHubDetection:
def test_detect_owner_repo_format(self):
"""owner/repo format should be detected as GitHub."""
info = SourceDetector.detect("facebook/react")
assert info.type == 'github'
assert info.parsed['repo'] == "facebook/react"
assert info.suggested_name == 'react'
assert info.type == "github"
assert info.parsed["repo"] == "facebook/react"
assert info.suggested_name == "react"
def test_detect_github_https_url(self):
"""Full GitHub HTTPS URL should be detected."""
info = SourceDetector.detect("https://github.com/facebook/react")
assert info.type == 'github'
assert info.parsed['repo'] == "facebook/react"
assert info.suggested_name == 'react'
assert info.type == "github"
assert info.parsed["repo"] == "facebook/react"
assert info.suggested_name == "react"
def test_detect_github_url_with_git_suffix(self):
"""GitHub URL with .git should strip suffix."""
info = SourceDetector.detect("https://github.com/facebook/react.git")
assert info.type == 'github'
assert info.parsed['repo'] == "facebook/react"
assert info.suggested_name == 'react'
assert info.type == "github"
assert info.parsed["repo"] == "facebook/react"
assert info.suggested_name == "react"
def test_detect_github_url_without_protocol(self):
"""GitHub URL without protocol should be detected."""
info = SourceDetector.detect("github.com/vuejs/vue")
assert info.type == 'github'
assert info.parsed['repo'] == "vuejs/vue"
assert info.suggested_name == 'vue'
assert info.type == "github"
assert info.parsed["repo"] == "vuejs/vue"
assert info.suggested_name == "vue"
def test_owner_repo_with_dots_and_dashes(self):
"""Repo names with dots and dashes should work."""
info = SourceDetector.detect("microsoft/vscode-python")
assert info.type == 'github'
assert info.parsed['repo'] == "microsoft/vscode-python"
assert info.suggested_name == 'vscode-python'
assert info.type == "github"
assert info.parsed["repo"] == "microsoft/vscode-python"
assert info.suggested_name == "vscode-python"
class TestLocalDetection:
"""Test local directory detection."""
@@ -107,9 +110,9 @@ class TestLocalDetection:
try:
os.chdir(tmp_path)
info = SourceDetector.detect("./my_project")
assert info.type == 'local'
assert 'my_project' in info.parsed['directory']
assert info.suggested_name == 'my_project'
assert info.type == "local"
assert "my_project" in info.parsed["directory"]
assert info.suggested_name == "my_project"
finally:
os.chdir(original_cwd)
@@ -120,16 +123,17 @@ class TestLocalDetection:
test_dir.mkdir()
info = SourceDetector.detect(str(test_dir))
assert info.type == 'local'
assert info.parsed['directory'] == str(test_dir.resolve())
assert info.suggested_name == 'test_repo'
assert info.type == "local"
assert info.parsed["directory"] == str(test_dir.resolve())
assert info.suggested_name == "test_repo"
def test_detect_current_directory(self):
"""Current directory (.) should be detected."""
cwd = os.getcwd()
info = SourceDetector.detect(".")
assert info.type == 'local'
assert info.parsed['directory'] == cwd
assert info.type == "local"
assert info.parsed["directory"] == cwd
class TestPDFDetection:
"""Test PDF file detection."""
@@ -137,22 +141,23 @@ class TestPDFDetection:
def test_detect_pdf_extension(self):
"""File with .pdf extension should be detected."""
info = SourceDetector.detect("tutorial.pdf")
assert info.type == 'pdf'
assert info.parsed['file_path'] == "tutorial.pdf"
assert info.suggested_name == 'tutorial'
assert info.type == "pdf"
assert info.parsed["file_path"] == "tutorial.pdf"
assert info.suggested_name == "tutorial"
def test_detect_pdf_with_path(self):
"""PDF file with path should be detected."""
info = SourceDetector.detect("/path/to/guide.pdf")
assert info.type == 'pdf'
assert info.parsed['file_path'] == "/path/to/guide.pdf"
assert info.suggested_name == 'guide'
assert info.type == "pdf"
assert info.parsed["file_path"] == "/path/to/guide.pdf"
assert info.suggested_name == "guide"
def test_suggested_name_removes_pdf_extension(self):
"""Suggested name should not include .pdf extension."""
info = SourceDetector.detect("my-awesome-guide.pdf")
assert info.type == 'pdf'
assert info.suggested_name == 'my-awesome-guide'
assert info.type == "pdf"
assert info.suggested_name == "my-awesome-guide"
class TestConfigDetection:
"""Test config file detection."""
@@ -160,16 +165,17 @@ class TestConfigDetection:
def test_detect_json_extension(self):
"""File with .json extension should be detected as config."""
info = SourceDetector.detect("react.json")
assert info.type == 'config'
assert info.parsed['config_path'] == "react.json"
assert info.suggested_name == 'react'
assert info.type == "config"
assert info.parsed["config_path"] == "react.json"
assert info.suggested_name == "react"
def test_detect_config_with_path(self):
"""Config file with path should be detected."""
info = SourceDetector.detect("configs/django.json")
assert info.type == 'config'
assert info.parsed['config_path'] == "configs/django.json"
assert info.suggested_name == 'django'
assert info.type == "config"
assert info.parsed["config_path"] == "configs/django.json"
assert info.suggested_name == "django"
class TestValidation:
"""Test source validation."""
@@ -191,10 +197,10 @@ class TestValidation:
# First try to detect it (will succeed since it looks like a path)
with pytest.raises(ValueError, match="Directory does not exist"):
info = SourceInfo(
type='local',
parsed={'directory': nonexistent},
suggested_name='test',
raw_input=nonexistent
type="local",
parsed={"directory": nonexistent},
suggested_name="test",
raw_input=nonexistent,
)
SourceDetector.validate_source(info)
@@ -211,10 +217,10 @@ class TestValidation:
"""Validation should fail for nonexistent PDF."""
with pytest.raises(ValueError, match="PDF file does not exist"):
info = SourceInfo(
type='pdf',
parsed={'file_path': '/tmp/nonexistent.pdf'},
suggested_name='test',
raw_input='/tmp/nonexistent.pdf'
type="pdf",
parsed={"file_path": "/tmp/nonexistent.pdf"},
suggested_name="test",
raw_input="/tmp/nonexistent.pdf",
)
SourceDetector.validate_source(info)
@@ -231,13 +237,14 @@ class TestValidation:
"""Validation should fail for nonexistent config."""
with pytest.raises(ValueError, match="Config file does not exist"):
info = SourceInfo(
type='config',
parsed={'config_path': '/tmp/nonexistent.json'},
suggested_name='test',
raw_input='/tmp/nonexistent.json'
type="config",
parsed={"config_path": "/tmp/nonexistent.json"},
suggested_name="test",
raw_input="/tmp/nonexistent.json",
)
SourceDetector.validate_source(info)
class TestAmbiguousCases:
"""Test handling of ambiguous inputs."""
@@ -255,8 +262,8 @@ class TestAmbiguousCases:
"""GitHub URL should be detected as github, not web."""
# Even though this is a URL, it should be detected as GitHub
info = SourceDetector.detect("https://github.com/owner/repo")
assert info.type == 'github'
assert info.parsed['repo'] == "owner/repo"
assert info.type == "github"
assert info.parsed["repo"] == "owner/repo"
def test_directory_takes_precedence_over_domain(self, tmp_path):
"""Existing directory should be detected even if it looks like domain."""
@@ -266,7 +273,8 @@ class TestAmbiguousCases:
info = SourceDetector.detect(str(dir_like_domain))
# Should detect as local directory, not web
assert info.type == 'local'
assert info.type == "local"
class TestRawInputPreservation:
"""Test that raw_input is preserved correctly."""
@@ -292,6 +300,7 @@ class TestRawInputPreservation:
info = SourceDetector.detect(original)
assert info.raw_input == original
class TestEdgeCases:
"""Test edge cases and corner cases."""
@@ -300,19 +309,19 @@ class TestEdgeCases:
info1 = SourceDetector.detect("https://docs.react.dev/")
info2 = SourceDetector.detect("https://docs.react.dev")
assert info1.type == 'web'
assert info2.type == 'web'
assert info1.type == "web"
assert info2.type == "web"
def test_uppercase_in_github_repo(self):
"""GitHub repos with uppercase should be detected."""
info = SourceDetector.detect("Microsoft/TypeScript")
assert info.type == 'github'
assert info.parsed['repo'] == "Microsoft/TypeScript"
assert info.type == "github"
assert info.parsed["repo"] == "Microsoft/TypeScript"
def test_numbers_in_repo_name(self):
"""GitHub repos with numbers should be detected."""
info = SourceDetector.detect("python/cpython3.11")
assert info.type == 'github'
assert info.type == "github"
def test_nested_directory_path(self, tmp_path):
"""Nested directory paths should work."""
@@ -320,5 +329,5 @@ class TestEdgeCases:
nested.mkdir(parents=True)
info = SourceDetector.detect(str(nested))
assert info.type == 'local'
assert info.suggested_name == 'c'
assert info.type == "local"
assert info.suggested_name == "c"