Files
gh-vanman2024-cli-builder-p…/skills/fire-patterns/scripts/extract-commands.py
2025-11-30 09:04:14 +08:00

209 lines
6.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Extract command structure from Fire CLI for documentation"""
import ast
import sys
import json
from pathlib import Path
from typing import List, Dict, Optional
class CommandExtractor:
"""Extract command structure from Fire CLI Python files"""
def __init__(self, filepath: Path):
self.filepath = filepath
self.tree = None
self.commands = []
def extract(self) -> List[Dict]:
"""Extract all commands from Fire CLI"""
try:
content = self.filepath.read_text()
self.tree = ast.parse(content)
except Exception as e:
print(f"Error parsing file: {e}", file=sys.stderr)
return []
# Find all classes
for node in self.tree.body:
if isinstance(node, ast.ClassDef):
self._extract_from_class(node)
return self.commands
def _extract_from_class(self, class_node: ast.ClassDef, parent_path: str = ""):
"""Extract commands from a class"""
class_name = class_node.name
class_doc = ast.get_docstring(class_node) or ""
current_path = f"{parent_path}.{class_name}" if parent_path else class_name
for item in class_node.body:
if isinstance(item, ast.FunctionDef):
# Skip private methods
if item.name.startswith('_'):
continue
command = self._extract_command(item, current_path)
if command:
self.commands.append(command)
elif isinstance(item, ast.ClassDef):
# Nested class - recurse
self._extract_from_class(item, current_path)
def _extract_command(self, func_node: ast.FunctionDef, class_path: str) -> Optional[Dict]:
"""Extract command information from function"""
func_name = func_node.name
docstring = ast.get_docstring(func_node) or ""
# Parse docstring for description and args
description = ""
args_help = {}
if docstring:
lines = docstring.split('\n')
desc_lines = []
in_args = False
for line in lines:
line = line.strip()
if line.startswith('Args:'):
in_args = True
continue
elif line.startswith('Returns:') or line.startswith('Raises:'):
in_args = False
continue
if not in_args and line:
desc_lines.append(line)
elif in_args and line:
# Parse arg line: "arg_name: description"
if ':' in line:
arg_name, arg_desc = line.split(':', 1)
args_help[arg_name.strip()] = arg_desc.strip()
description = ' '.join(desc_lines)
# Extract arguments
args = []
for arg in func_node.args.args:
if arg.arg == 'self':
continue
arg_info = {
'name': arg.arg,
'type': self._get_type_annotation(arg),
'help': args_help.get(arg.arg, ''),
}
# Check for default value
defaults_offset = len(func_node.args.args) - len(func_node.args.defaults)
arg_index = func_node.args.args.index(arg)
if arg_index >= defaults_offset:
default_index = arg_index - defaults_offset
default_value = self._get_default_value(func_node.args.defaults[default_index])
arg_info['default'] = default_value
arg_info['required'] = False
else:
arg_info['required'] = True
args.append(arg_info)
return {
'name': func_name,
'path': f"{class_path}.{func_name}",
'description': description,
'arguments': args
}
def _get_type_annotation(self, arg: ast.arg) -> Optional[str]:
"""Extract type annotation from argument"""
if arg.annotation:
return ast.unparse(arg.annotation)
return None
def _get_default_value(self, node) -> str:
"""Extract default value from AST node"""
try:
return ast.unparse(node)
except:
return repr(node)
def print_tree(self):
"""Print command tree in human-readable format"""
print(f"\n{'='*60}")
print(f"Fire CLI Commands: {self.filepath.name}")
print(f"{'='*60}\n")
for cmd in self.commands:
print(f"📌 {cmd['path']}")
if cmd['description']:
print(f" {cmd['description']}")
if cmd['arguments']:
print(f" Arguments:")
for arg in cmd['arguments']:
required = "required" if arg['required'] else "optional"
type_str = f": {arg['type']}" if arg['type'] else ""
default_str = f" = {arg['default']}" if 'default' in arg else ""
help_str = f" - {arg['help']}" if arg['help'] else ""
print(f"{arg['name']}{type_str}{default_str} ({required}){help_str}")
print()
def export_json(self) -> str:
"""Export commands as JSON"""
return json.dumps(self.commands, indent=2)
def export_markdown(self) -> str:
"""Export commands as Markdown"""
lines = [f"# {self.filepath.name} Commands\n"]
for cmd in self.commands:
lines.append(f"## `{cmd['path']}`\n")
if cmd['description']:
lines.append(f"{cmd['description']}\n")
if cmd['arguments']:
lines.append("### Arguments\n")
for arg in cmd['arguments']:
required = "**required**" if arg['required'] else "*optional*"
type_str = f" (`{arg['type']}`)" if arg['type'] else ""
default_str = f" Default: `{arg['default']}`" if 'default' in arg else ""
lines.append(f"- `{arg['name']}`{type_str} - {required}{default_str}")
if arg['help']:
lines.append(f" - {arg['help']}")
lines.append("")
return '\n'.join(lines)
def main():
if len(sys.argv) < 2:
print("Usage: extract-commands.py <fire-cli-file.py> [--json|--markdown]")
sys.exit(1)
filepath = Path(sys.argv[1])
output_format = sys.argv[2] if len(sys.argv) > 2 else '--tree'
if not filepath.exists():
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
extractor = CommandExtractor(filepath)
extractor.extract()
if output_format == '--json':
print(extractor.export_json())
elif output_format == '--markdown':
print(extractor.export_markdown())
else:
extractor.print_tree()
if __name__ == '__main__':
main()