feat: Unified create command + consolidated enhancement flags
This commit includes two major improvements:
## 1. Unified Create Command (v3.0.0 feature)
- Auto-detects source type (web, GitHub, local, PDF, config)
- Three-tier argument organization (universal, source-specific, advanced)
- Routes to existing scrapers (100% backward compatible)
- Progressive disclosure: 15 universal flags in default help
**New files:**
- src/skill_seekers/cli/source_detector.py - Auto-detection logic
- src/skill_seekers/cli/arguments/create.py - Argument definitions
- src/skill_seekers/cli/create_command.py - Main orchestrator
- src/skill_seekers/cli/parsers/create_parser.py - Parser integration
**Tests:**
- tests/test_source_detector.py (35 tests)
- tests/test_create_arguments.py (30 tests)
- tests/test_create_integration_basic.py (10 tests)
## 2. Enhanced Flag Consolidation (Phase 1)
- Consolidated 3 flags (--enhance, --enhance-local, --enhance-level) → 1 flag
- --enhance-level 0-3 with auto-detection of API vs LOCAL mode
- Default: --enhance-level 2 (balanced enhancement)
**Modified files:**
- arguments/{common,create,scrape,github,analyze}.py - Added enhance_level
- {doc_scraper,github_scraper,config_extractor,main}.py - Updated logic
- create_command.py - Uses consolidated flag
**Auto-detection:**
- If ANTHROPIC_API_KEY set → API mode
- Else → LOCAL mode (Claude Code)
## 3. PresetManager Bug Fix
- Fixed module naming conflict (presets.py vs presets/ directory)
- Moved presets.py → presets/manager.py
- Updated __init__.py exports
**Test Results:**
- All 160+ tests passing
- Zero regressions
- 100% backward compatible
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
617
UI_INTEGRATION_GUIDE.md
Normal file
617
UI_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# UI Integration Guide
|
||||
## How the CLI Refactor Enables Future UI Development
|
||||
|
||||
**Date:** 2026-02-14
|
||||
**Status:** Planning Document
|
||||
**Related:** CLI_REFACTOR_PROPOSAL.md
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The "Pure Explicit" architecture proposed for fixing #285 is **ideal** for UI development because:
|
||||
|
||||
1. ✅ **Single source of truth** for all command options
|
||||
2. ✅ **Self-documenting** argument definitions
|
||||
3. ✅ **Easy to introspect** for dynamic form generation
|
||||
4. ✅ **Consistent validation** between CLI and UI
|
||||
|
||||
**Recommendation:** Proceed with the refactor. It actively enables future UI work.
|
||||
|
||||
---
|
||||
|
||||
## Why This Architecture is UI-Friendly
|
||||
|
||||
### Current Problem (Without Refactor)
|
||||
|
||||
```python
|
||||
# BEFORE: Arguments scattered in multiple files
|
||||
# doc_scraper.py
|
||||
def create_argument_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--name", help="Skill name") # ← Here
|
||||
parser.add_argument("--max-pages", type=int) # ← Here
|
||||
return parser
|
||||
|
||||
# parsers/scrape_parser.py
|
||||
class ScrapeParser:
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--name", help="Skill name") # ← Duplicate!
|
||||
# max-pages forgotten!
|
||||
```
|
||||
|
||||
**UI Problem:** Which arguments exist? What's the full schema? Hard to discover.
|
||||
|
||||
### After Refactor (UI-Friendly)
|
||||
|
||||
```python
|
||||
# AFTER: Centralized, structured definitions
|
||||
# arguments/scrape.py
|
||||
|
||||
SCRAPER_ARGUMENTS = {
|
||||
"name": {
|
||||
"type": str,
|
||||
"help": "Skill name",
|
||||
"ui_label": "Skill Name",
|
||||
"ui_section": "Basic",
|
||||
"placeholder": "e.g., React"
|
||||
},
|
||||
"max_pages": {
|
||||
"type": int,
|
||||
"help": "Maximum pages to scrape",
|
||||
"ui_label": "Max Pages",
|
||||
"ui_section": "Limits",
|
||||
"min": 1,
|
||||
"max": 1000,
|
||||
"default": 100
|
||||
},
|
||||
"async_mode": {
|
||||
"type": bool,
|
||||
"help": "Use async scraping",
|
||||
"ui_label": "Async Mode",
|
||||
"ui_section": "Performance",
|
||||
"ui_widget": "checkbox"
|
||||
}
|
||||
}
|
||||
|
||||
def add_scrape_arguments(parser):
|
||||
for name, config in SCRAPER_ARGUMENTS.items():
|
||||
parser.add_argument(f"--{name}", **config)
|
||||
```
|
||||
|
||||
**UI Benefit:** Arguments are data! Easy to iterate and build forms.
|
||||
|
||||
---
|
||||
|
||||
## UI Architecture Options
|
||||
|
||||
### Option 1: Console UI (TUI) - Recommended First Step
|
||||
|
||||
**Libraries:** `rich`, `textual`, `inquirer`, `questionary`
|
||||
|
||||
```python
|
||||
# Example: TUI using the shared argument definitions
|
||||
# src/skill_seekers/ui/console/scrape_wizard.py
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Prompt, IntPrompt, Confirm
|
||||
|
||||
from skill_seekers.cli.arguments.scrape import SCRAPER_ARGUMENTS
|
||||
from skill_seekers.cli.presets.scrape_presets import PRESETS
|
||||
|
||||
|
||||
class ScrapeWizard:
|
||||
"""Interactive TUI for scrape command."""
|
||||
|
||||
def __init__(self):
|
||||
self.console = Console()
|
||||
self.results = {}
|
||||
|
||||
def run(self):
|
||||
"""Run the wizard."""
|
||||
self.console.print(Panel.fit(
|
||||
"[bold blue]Skill Seekers - Scrape Wizard[/bold blue]",
|
||||
border_style="blue"
|
||||
))
|
||||
|
||||
# Step 1: Choose preset (simplified) or custom
|
||||
use_preset = Confirm.ask("Use a preset configuration?")
|
||||
|
||||
if use_preset:
|
||||
self._select_preset()
|
||||
else:
|
||||
self._custom_configuration()
|
||||
|
||||
# Execute
|
||||
self._execute()
|
||||
|
||||
def _select_preset(self):
|
||||
"""Let user pick a preset."""
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="Available Presets")
|
||||
table.add_column("Preset", style="cyan")
|
||||
table.add_column("Description")
|
||||
table.add_column("Time")
|
||||
|
||||
for name, preset in PRESETS.items():
|
||||
table.add_row(name, preset.description, preset.estimated_time)
|
||||
|
||||
self.console.print(table)
|
||||
|
||||
choice = Prompt.ask(
|
||||
"Select preset",
|
||||
choices=list(PRESETS.keys()),
|
||||
default="standard"
|
||||
)
|
||||
|
||||
self.results["preset"] = choice
|
||||
|
||||
def _custom_configuration(self):
|
||||
"""Interactive form based on argument definitions."""
|
||||
|
||||
# Group by UI section
|
||||
sections = {}
|
||||
for name, config in SCRAPER_ARGUMENTS.items():
|
||||
section = config.get("ui_section", "General")
|
||||
if section not in sections:
|
||||
sections[section] = []
|
||||
sections[section].append((name, config))
|
||||
|
||||
# Render each section
|
||||
for section_name, fields in sections.items():
|
||||
self.console.print(f"\n[bold]{section_name}[/bold]")
|
||||
|
||||
for name, config in fields:
|
||||
value = self._prompt_for_field(name, config)
|
||||
self.results[name] = value
|
||||
|
||||
def _prompt_for_field(self, name: str, config: dict):
|
||||
"""Generate appropriate prompt based on argument type."""
|
||||
|
||||
label = config.get("ui_label", name)
|
||||
help_text = config.get("help", "")
|
||||
|
||||
if config.get("type") == bool:
|
||||
return Confirm.ask(f"{label}?", default=config.get("default", False))
|
||||
|
||||
elif config.get("type") == int:
|
||||
return IntPrompt.ask(
|
||||
f"{label}",
|
||||
default=config.get("default")
|
||||
)
|
||||
|
||||
else:
|
||||
return Prompt.ask(
|
||||
f"{label}",
|
||||
default=config.get("default", ""),
|
||||
show_default=True
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Reuses all validation and help text
|
||||
- ✅ Consistent with CLI behavior
|
||||
- ✅ Can run in any terminal
|
||||
- ✅ No web server needed
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Web UI (Gradio/Streamlit)
|
||||
|
||||
**Libraries:** `gradio`, `streamlit`, `fastapi + htmx`
|
||||
|
||||
```python
|
||||
# Example: Web UI using Gradio
|
||||
# src/skill_seekers/ui/web/app.py
|
||||
|
||||
import gradio as gr
|
||||
from skill_seekers.cli.arguments.scrape import SCRAPER_ARGUMENTS
|
||||
|
||||
|
||||
def create_scrape_interface():
|
||||
"""Create Gradio interface for scrape command."""
|
||||
|
||||
# Generate inputs from argument definitions
|
||||
inputs = []
|
||||
|
||||
for name, config in SCRAPER_ARGUMENTS.items():
|
||||
arg_type = config.get("type")
|
||||
label = config.get("ui_label", name)
|
||||
help_text = config.get("help", "")
|
||||
|
||||
if arg_type == bool:
|
||||
inputs.append(gr.Checkbox(
|
||||
label=label,
|
||||
info=help_text,
|
||||
value=config.get("default", False)
|
||||
))
|
||||
|
||||
elif arg_type == int:
|
||||
inputs.append(gr.Number(
|
||||
label=label,
|
||||
info=help_text,
|
||||
value=config.get("default"),
|
||||
minimum=config.get("min"),
|
||||
maximum=config.get("max")
|
||||
))
|
||||
|
||||
else:
|
||||
inputs.append(gr.Textbox(
|
||||
label=label,
|
||||
info=help_text,
|
||||
placeholder=config.get("placeholder", ""),
|
||||
value=config.get("default", "")
|
||||
))
|
||||
|
||||
return gr.Interface(
|
||||
fn=run_scrape,
|
||||
inputs=inputs,
|
||||
outputs="text",
|
||||
title="Skill Seekers - Scrape Documentation",
|
||||
description="Convert documentation to AI-ready skills"
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Automatic form generation from argument definitions
|
||||
- ✅ Runs in browser
|
||||
- ✅ Can be deployed as web service
|
||||
- ✅ Great for non-technical users
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Desktop GUI (Tkinter/PyQt)
|
||||
|
||||
```python
|
||||
# Example: Tkinter GUI
|
||||
# src/skill_seekers/ui/desktop/app.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from skill_seekers.cli.arguments.scrape import SCRAPER_ARGUMENTS
|
||||
|
||||
|
||||
class SkillSeekersGUI:
|
||||
"""Desktop GUI for Skill Seekers."""
|
||||
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Skill Seekers")
|
||||
|
||||
# Create notebook (tabs)
|
||||
self.notebook = ttk.Notebook(root)
|
||||
self.notebook.pack(fill='both', expand=True)
|
||||
|
||||
# Create tabs from command arguments
|
||||
self._create_scrape_tab()
|
||||
self._create_github_tab()
|
||||
|
||||
def _create_scrape_tab(self):
|
||||
"""Create scrape tab from argument definitions."""
|
||||
tab = ttk.Frame(self.notebook)
|
||||
self.notebook.add(tab, text="Scrape")
|
||||
|
||||
# Group by section
|
||||
sections = {}
|
||||
for name, config in SCRAPER_ARGUMENTS.items():
|
||||
section = config.get("ui_section", "General")
|
||||
sections.setdefault(section, []).append((name, config))
|
||||
|
||||
# Create form fields
|
||||
row = 0
|
||||
for section_name, fields in sections.items():
|
||||
# Section label
|
||||
ttk.Label(tab, text=section_name, font=('Arial', 10, 'bold')).grid(
|
||||
row=row, column=0, columnspan=2, pady=(10, 5), sticky='w'
|
||||
)
|
||||
row += 1
|
||||
|
||||
for name, config in fields:
|
||||
# Label
|
||||
label = ttk.Label(tab, text=config.get("ui_label", name))
|
||||
label.grid(row=row, column=0, sticky='w', padx=5)
|
||||
|
||||
# Input widget
|
||||
if config.get("type") == bool:
|
||||
var = tk.BooleanVar(value=config.get("default", False))
|
||||
widget = ttk.Checkbutton(tab, variable=var)
|
||||
else:
|
||||
var = tk.StringVar(value=str(config.get("default", "")))
|
||||
widget = ttk.Entry(tab, textvariable=var, width=40)
|
||||
|
||||
widget.grid(row=row, column=1, sticky='ew', padx=5)
|
||||
|
||||
# Help tooltip (simplified)
|
||||
if "help" in config:
|
||||
label.bind("<Enter>", lambda e, h=config["help"]: self._show_tooltip(h))
|
||||
|
||||
row += 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enhancing Arguments for UI
|
||||
|
||||
To make arguments even more UI-friendly, we can add optional UI metadata:
|
||||
|
||||
```python
|
||||
# arguments/scrape.py - Enhanced with UI metadata
|
||||
|
||||
SCRAPER_ARGUMENTS = {
|
||||
"url": {
|
||||
"type": str,
|
||||
"help": "Documentation URL to scrape",
|
||||
|
||||
# UI-specific metadata (optional)
|
||||
"ui_label": "Documentation URL",
|
||||
"ui_section": "Source", # Groups fields in UI
|
||||
"ui_order": 1, # Display order
|
||||
"placeholder": "https://docs.example.com",
|
||||
"required": True,
|
||||
"validate": "url", # Auto-validate as URL
|
||||
},
|
||||
|
||||
"name": {
|
||||
"type": str,
|
||||
"help": "Name for the generated skill",
|
||||
|
||||
"ui_label": "Skill Name",
|
||||
"ui_section": "Output",
|
||||
"ui_order": 2,
|
||||
"placeholder": "e.g., React, Python, Docker",
|
||||
"validate": r"^[a-zA-Z0-9_-]+$", # Regex validation
|
||||
},
|
||||
|
||||
"max_pages": {
|
||||
"type": int,
|
||||
"help": "Maximum pages to scrape",
|
||||
"default": 100,
|
||||
|
||||
"ui_label": "Max Pages",
|
||||
"ui_section": "Limits",
|
||||
"ui_widget": "slider", # Use slider in GUI
|
||||
"min": 1,
|
||||
"max": 1000,
|
||||
"step": 10,
|
||||
},
|
||||
|
||||
"async_mode": {
|
||||
"type": bool,
|
||||
"help": "Enable async mode for faster scraping",
|
||||
"default": False,
|
||||
|
||||
"ui_label": "Async Mode",
|
||||
"ui_section": "Performance",
|
||||
"ui_widget": "toggle", # Use toggle switch in GUI
|
||||
"advanced": True, # Hide in simple mode
|
||||
},
|
||||
|
||||
"api_key": {
|
||||
"type": str,
|
||||
"help": "API key for enhancement",
|
||||
|
||||
"ui_label": "API Key",
|
||||
"ui_section": "Authentication",
|
||||
"ui_widget": "password", # Mask input
|
||||
"env_var": "ANTHROPIC_API_KEY", # Can read from env
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Modes
|
||||
|
||||
With this architecture, we can support multiple UI modes:
|
||||
|
||||
```bash
|
||||
# CLI mode (default)
|
||||
skill-seekers scrape --url https://react.dev --name react
|
||||
|
||||
# TUI mode (interactive)
|
||||
skill-seekers ui scrape
|
||||
|
||||
# Web mode
|
||||
skill-seekers ui --web
|
||||
|
||||
# Desktop mode
|
||||
skill-seekers ui --desktop
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
```python
|
||||
# src/skill_seekers/cli/ui_command.py
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", nargs="?", help="Command to run in UI")
|
||||
parser.add_argument("--web", action="store_true", help="Launch web UI")
|
||||
parser.add_argument("--desktop", action="store_true", help="Launch desktop UI")
|
||||
parser.add_argument("--port", type=int, default=7860, help="Port for web UI")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.web:
|
||||
from skill_seekers.ui.web.app import launch_web_ui
|
||||
launch_web_ui(port=args.port)
|
||||
|
||||
elif args.desktop:
|
||||
from skill_seekers.ui.desktop.app import launch_desktop_ui
|
||||
launch_desktop_ui()
|
||||
|
||||
else:
|
||||
# Default to TUI
|
||||
from skill_seekers.ui.console.app import launch_tui
|
||||
launch_tui(command=args.command)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Path to UI
|
||||
|
||||
### Phase 1: Refactor (Current Proposal)
|
||||
- Create `arguments/` module with structured definitions
|
||||
- Keep CLI working exactly as before
|
||||
- **Enables:** UI can introspect arguments
|
||||
|
||||
### Phase 2: Add TUI (Optional, ~1 week)
|
||||
- Build console UI using `rich` or `textual`
|
||||
- Reuses argument definitions
|
||||
- **Benefit:** Better UX for terminal users
|
||||
|
||||
### Phase 3: Add Web UI (Optional, ~2 weeks)
|
||||
- Build web UI using `gradio` or `streamlit`
|
||||
- Same argument definitions
|
||||
- **Benefit:** Accessible to non-technical users
|
||||
|
||||
### Phase 4: Add Desktop GUI (Optional, ~3 weeks)
|
||||
- Build native desktop app using `tkinter` or `PyQt`
|
||||
- **Benefit:** Standalone application experience
|
||||
|
||||
---
|
||||
|
||||
## Code Example: Complete UI Integration
|
||||
|
||||
Here's how a complete integration would look:
|
||||
|
||||
```python
|
||||
# src/skill_seekers/arguments/base.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, Callable
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArgumentDef:
|
||||
"""Definition of a CLI argument with UI metadata."""
|
||||
|
||||
# Core argparse fields
|
||||
name: str
|
||||
type: type
|
||||
help: str
|
||||
default: Any = None
|
||||
choices: Optional[list] = None
|
||||
action: Optional[str] = None
|
||||
|
||||
# UI metadata (all optional)
|
||||
ui_label: Optional[str] = None
|
||||
ui_section: str = "General"
|
||||
ui_order: int = 0
|
||||
ui_widget: str = "auto" # auto, text, checkbox, slider, select, etc.
|
||||
placeholder: Optional[str] = None
|
||||
required: bool = False
|
||||
advanced: bool = False # Hide in simple mode
|
||||
|
||||
# Validation
|
||||
validate: Optional[str] = None # "url", "email", regex pattern
|
||||
min: Optional[float] = None
|
||||
max: Optional[float] = None
|
||||
|
||||
# Environment
|
||||
env_var: Optional[str] = None # Read default from env
|
||||
|
||||
|
||||
class ArgumentRegistry:
|
||||
"""Registry of all command arguments."""
|
||||
|
||||
_commands = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, command: str, arguments: list[ArgumentDef]):
|
||||
"""Register arguments for a command."""
|
||||
cls._commands[command] = arguments
|
||||
|
||||
@classmethod
|
||||
def get_arguments(cls, command: str) -> list[ArgumentDef]:
|
||||
"""Get all arguments for a command."""
|
||||
return cls._commands.get(command, [])
|
||||
|
||||
@classmethod
|
||||
def to_argparse(cls, command: str, parser):
|
||||
"""Add registered arguments to argparse parser."""
|
||||
for arg in cls._commands.get(command, []):
|
||||
kwargs = {
|
||||
"help": arg.help,
|
||||
"default": arg.default,
|
||||
}
|
||||
if arg.type != bool:
|
||||
kwargs["type"] = arg.type
|
||||
if arg.action:
|
||||
kwargs["action"] = arg.action
|
||||
if arg.choices:
|
||||
kwargs["choices"] = arg.choices
|
||||
|
||||
parser.add_argument(f"--{arg.name}", **kwargs)
|
||||
|
||||
@classmethod
|
||||
def to_ui_form(cls, command: str) -> list[dict]:
|
||||
"""Convert arguments to UI form schema."""
|
||||
return [
|
||||
{
|
||||
"name": arg.name,
|
||||
"label": arg.ui_label or arg.name,
|
||||
"type": arg.ui_widget if arg.ui_widget != "auto" else cls._infer_widget(arg),
|
||||
"section": arg.ui_section,
|
||||
"order": arg.ui_order,
|
||||
"required": arg.required,
|
||||
"placeholder": arg.placeholder,
|
||||
"validation": arg.validate,
|
||||
"min": arg.min,
|
||||
"max": arg.max,
|
||||
}
|
||||
for arg in cls._commands.get(command, [])
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _infer_widget(arg: ArgumentDef) -> str:
|
||||
"""Infer UI widget type from argument type."""
|
||||
if arg.type == bool:
|
||||
return "checkbox"
|
||||
elif arg.choices:
|
||||
return "select"
|
||||
elif arg.type == int and arg.min is not None and arg.max is not None:
|
||||
return "slider"
|
||||
else:
|
||||
return "text"
|
||||
|
||||
|
||||
# Register all commands
|
||||
from .scrape import SCRAPE_ARGUMENTS
|
||||
from .github import GITHUB_ARGUMENTS
|
||||
|
||||
ArgumentRegistry.register("scrape", SCRAPE_ARGUMENTS)
|
||||
ArgumentRegistry.register("github", GITHUB_ARGUMENTS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| **Is this refactor UI-friendly?** | ✅ Yes, actively enables UI development |
|
||||
| **What UI types are supported?** | Console (TUI), Web, Desktop GUI |
|
||||
| **How much extra work for UI?** | Minimal - reuse argument definitions |
|
||||
| **Can we start with CLI only?** | ✅ Yes, UI is optional future work |
|
||||
| **Should we add UI metadata now?** | Optional - can be added incrementally |
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
1. **Proceed with the refactor** - It's the right foundation
|
||||
2. **Start with CLI** - Get it working first
|
||||
3. **Add basic UI metadata** - Just `ui_label` and `ui_section`
|
||||
4. **Build TUI later** - When you want better terminal UX
|
||||
5. **Consider Web UI** - If you need non-technical users
|
||||
|
||||
The refactor **doesn't commit you to a UI**, but makes it **easy to add one later**.
|
||||
|
||||
---
|
||||
|
||||
*End of Document*
|
||||
Reference in New Issue
Block a user