Fixes ruff format --check CI failure. 22 files reformatted to satisfy the ruff formatter's style requirements. No logic changes, only whitespace/formatting adjustments. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
312 lines
10 KiB
Python
312 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Workflows CLI Command
|
||
|
||
Manage enhancement workflow presets:
|
||
list List all workflows (bundled + user)
|
||
show Print YAML content of a workflow
|
||
copy Copy a bundled workflow to user dir for editing
|
||
add Install a custom YAML into user dir
|
||
remove Delete a user workflow (bundled ones cannot be removed)
|
||
validate Parse and validate a workflow YAML
|
||
|
||
Usage:
|
||
skill-seekers workflows list
|
||
skill-seekers workflows show security-focus
|
||
skill-seekers workflows copy security-focus
|
||
skill-seekers workflows add ./my-workflow.yaml
|
||
skill-seekers workflows remove my-workflow
|
||
skill-seekers workflows validate security-focus
|
||
"""
|
||
|
||
import shutil
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import yaml
|
||
|
||
from skill_seekers.cli.enhancement_workflow import (
|
||
WorkflowEngine,
|
||
list_bundled_workflows,
|
||
)
|
||
|
||
USER_WORKFLOWS_DIR = Path.home() / ".config" / "skill-seekers" / "workflows"
|
||
|
||
|
||
def _ensure_user_dir() -> Path:
|
||
USER_WORKFLOWS_DIR.mkdir(parents=True, exist_ok=True)
|
||
return USER_WORKFLOWS_DIR
|
||
|
||
|
||
def _bundled_yaml_text(name: str) -> str | None:
|
||
"""Return raw YAML text of a bundled workflow, or None if not found."""
|
||
from importlib.resources import files as importlib_files
|
||
|
||
for suffix in (".yaml", ".yml"):
|
||
try:
|
||
pkg_ref = importlib_files("skill_seekers.workflows").joinpath(name + suffix)
|
||
return pkg_ref.read_text(encoding="utf-8")
|
||
except (FileNotFoundError, TypeError, ModuleNotFoundError):
|
||
continue
|
||
return None
|
||
|
||
|
||
def _workflow_yaml_text(name_or_path: str) -> str | None:
|
||
"""Resolve a workflow by name or path and return its raw YAML text."""
|
||
# Try as a file path first
|
||
p = Path(name_or_path)
|
||
if p.suffix in (".yaml", ".yml") and p.exists():
|
||
return p.read_text(encoding="utf-8")
|
||
|
||
# Try as a name with .yaml extension
|
||
for suffix in (".yaml", ".yml"):
|
||
candidate = Path(name_or_path + suffix)
|
||
if candidate.exists():
|
||
return candidate.read_text(encoding="utf-8")
|
||
|
||
# User dir
|
||
user_file = USER_WORKFLOWS_DIR / (name_or_path + ".yaml")
|
||
if user_file.exists():
|
||
return user_file.read_text(encoding="utf-8")
|
||
user_file_yml = USER_WORKFLOWS_DIR / (name_or_path + ".yml")
|
||
if user_file_yml.exists():
|
||
return user_file_yml.read_text(encoding="utf-8")
|
||
|
||
# Bundled
|
||
return _bundled_yaml_text(name_or_path)
|
||
|
||
|
||
def _list_user_workflow_names() -> list[str]:
|
||
"""Return names of user workflows (without extension) from USER_WORKFLOWS_DIR."""
|
||
if not USER_WORKFLOWS_DIR.exists():
|
||
return []
|
||
return sorted(p.stem for p in USER_WORKFLOWS_DIR.iterdir() if p.suffix in (".yaml", ".yml"))
|
||
|
||
|
||
def cmd_list() -> int:
|
||
"""List all available workflows."""
|
||
bundled = list_bundled_workflows()
|
||
user = _list_user_workflow_names()
|
||
|
||
if not bundled and not user:
|
||
print("No workflows found.")
|
||
return 0
|
||
|
||
if bundled:
|
||
print("Bundled workflows (read-only):")
|
||
for name in bundled:
|
||
# Load description from YAML
|
||
text = _bundled_yaml_text(name)
|
||
desc = ""
|
||
if text:
|
||
try:
|
||
data = yaml.safe_load(text)
|
||
desc = data.get("description", "")
|
||
except Exception:
|
||
pass
|
||
print(f" {name:<32} {desc}")
|
||
|
||
if user:
|
||
print("\nUser workflows (~/.config/skill-seekers/workflows/):")
|
||
for name in user:
|
||
user_file = USER_WORKFLOWS_DIR / (name + ".yaml")
|
||
if not user_file.exists():
|
||
user_file = USER_WORKFLOWS_DIR / (name + ".yml")
|
||
desc = ""
|
||
try:
|
||
data = yaml.safe_load(user_file.read_text(encoding="utf-8"))
|
||
desc = data.get("description", "")
|
||
except Exception:
|
||
pass
|
||
print(f" {name:<32} {desc}")
|
||
|
||
return 0
|
||
|
||
|
||
def cmd_show(name: str) -> int:
|
||
"""Print YAML content of a workflow."""
|
||
text = _workflow_yaml_text(name)
|
||
if text is None:
|
||
print(f"Error: Workflow '{name}' not found.", file=sys.stderr)
|
||
print("Use 'skill-seekers workflows list' to see available workflows.", file=sys.stderr)
|
||
return 1
|
||
print(text, end="")
|
||
return 0
|
||
|
||
|
||
def cmd_copy(names: list[str]) -> int:
|
||
"""Copy one or more bundled workflows to user dir."""
|
||
rc = 0
|
||
for name in names:
|
||
text = _bundled_yaml_text(name)
|
||
if text is None:
|
||
print(f"Error: Bundled workflow '{name}' not found.", file=sys.stderr)
|
||
bundled = list_bundled_workflows()
|
||
if bundled:
|
||
print(f"Available bundled workflows: {', '.join(bundled)}", file=sys.stderr)
|
||
rc = 1
|
||
continue
|
||
|
||
dest = _ensure_user_dir() / (name + ".yaml")
|
||
if dest.exists():
|
||
print(f"Warning: '{dest}' already exists. Overwriting.")
|
||
|
||
dest.write_text(text, encoding="utf-8")
|
||
print(f"Copied '{name}' to: {dest}")
|
||
print(
|
||
f"Edit it with your favourite editor, then reference it as '--enhance-workflow {name}'"
|
||
)
|
||
|
||
return rc
|
||
|
||
|
||
def cmd_add(file_paths: list[str], override_name: str | None = None) -> int:
|
||
"""Install one or more custom YAML workflows into user dir."""
|
||
if override_name and len(file_paths) > 1:
|
||
print("Error: --name cannot be used when adding multiple files.", file=sys.stderr)
|
||
return 1
|
||
|
||
rc = 0
|
||
for file_path in file_paths:
|
||
src = Path(file_path)
|
||
if not src.exists():
|
||
print(f"Error: File '{file_path}' does not exist.", file=sys.stderr)
|
||
rc = 1
|
||
continue
|
||
if src.suffix not in (".yaml", ".yml"):
|
||
print(f"Error: '{file_path}' must have a .yaml or .yml extension.", file=sys.stderr)
|
||
rc = 1
|
||
continue
|
||
|
||
# Validate before installing
|
||
try:
|
||
text = src.read_text(encoding="utf-8")
|
||
data = yaml.safe_load(text)
|
||
if not isinstance(data, dict):
|
||
raise ValueError("YAML root must be a mapping")
|
||
if "stages" not in data:
|
||
raise ValueError("Workflow must contain a 'stages' key")
|
||
except Exception as exc:
|
||
print(f"Error: Invalid workflow YAML '{file_path}' – {exc}", file=sys.stderr)
|
||
rc = 1
|
||
continue
|
||
|
||
dest_name = override_name if override_name else src.stem
|
||
dest = _ensure_user_dir() / (dest_name + ".yaml")
|
||
|
||
if dest.exists():
|
||
print(f"Warning: '{dest}' already exists. Overwriting.")
|
||
|
||
shutil.copy2(src, dest)
|
||
print(f"Installed workflow '{dest_name}' to: {dest}")
|
||
|
||
return rc
|
||
|
||
|
||
def cmd_remove(names: list[str]) -> int:
|
||
"""Delete one or more user workflows."""
|
||
rc = 0
|
||
bundled = list_bundled_workflows()
|
||
for name in names:
|
||
if name in bundled:
|
||
print(
|
||
f"Error: '{name}' is a bundled workflow and cannot be removed.",
|
||
file=sys.stderr,
|
||
)
|
||
print("Use 'skill-seekers workflows copy' to create an editable copy.", file=sys.stderr)
|
||
rc = 1
|
||
continue
|
||
|
||
removed = False
|
||
for suffix in (".yaml", ".yml"):
|
||
candidate = USER_WORKFLOWS_DIR / (name + suffix)
|
||
if candidate.exists():
|
||
candidate.unlink()
|
||
print(f"Removed workflow: {candidate}")
|
||
removed = True
|
||
break
|
||
|
||
if not removed:
|
||
print(f"Error: User workflow '{name}' not found.", file=sys.stderr)
|
||
rc = 1
|
||
|
||
return rc
|
||
|
||
|
||
def cmd_validate(name_or_path: str) -> int:
|
||
"""Parse and validate a workflow."""
|
||
try:
|
||
engine = WorkflowEngine(name_or_path)
|
||
wf = engine.workflow
|
||
print(f"✅ Workflow '{wf.name}' is valid.")
|
||
print(f" Description : {wf.description}")
|
||
print(f" Version : {wf.version}")
|
||
print(f" Stages : {len(wf.stages)}")
|
||
for stage in wf.stages:
|
||
status = "enabled" if stage.enabled else "disabled"
|
||
print(f" - {stage.name} ({stage.type}, {status})")
|
||
return 0
|
||
except FileNotFoundError as exc:
|
||
print(f"Error: {exc}", file=sys.stderr)
|
||
return 1
|
||
except Exception as exc:
|
||
print(f"Error: Invalid workflow – {exc}", file=sys.stderr)
|
||
return 1
|
||
|
||
|
||
def main(argv=None) -> None:
|
||
"""Entry point for skill-seekers-workflows."""
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(
|
||
prog="skill-seekers-workflows",
|
||
description="Manage enhancement workflow presets",
|
||
)
|
||
subparsers = parser.add_subparsers(dest="action", metavar="ACTION")
|
||
|
||
subparsers.add_parser("list", help="List all workflows (bundled + user)")
|
||
|
||
show_p = subparsers.add_parser("show", help="Print YAML content of a workflow")
|
||
show_p.add_argument("workflow_name")
|
||
|
||
copy_p = subparsers.add_parser("copy", help="Copy bundled workflow(s) to user dir")
|
||
copy_p.add_argument("workflow_names", nargs="+")
|
||
|
||
add_p = subparsers.add_parser("add", help="Install custom YAML file(s) into user dir")
|
||
add_p.add_argument("files", nargs="+")
|
||
add_p.add_argument("--name")
|
||
|
||
remove_p = subparsers.add_parser("remove", help="Delete user workflow(s)")
|
||
remove_p.add_argument("workflow_names", nargs="+")
|
||
|
||
validate_p = subparsers.add_parser("validate", help="Validate a workflow by name or file")
|
||
validate_p.add_argument("workflow_name")
|
||
|
||
args = parser.parse_args(argv)
|
||
|
||
if args.action is None:
|
||
parser.print_help()
|
||
sys.exit(0)
|
||
|
||
rc = 0
|
||
if args.action == "list":
|
||
rc = cmd_list()
|
||
elif args.action == "show":
|
||
rc = cmd_show(args.workflow_name)
|
||
elif args.action == "copy":
|
||
rc = cmd_copy(args.workflow_names)
|
||
elif args.action == "add":
|
||
rc = cmd_add(args.files, getattr(args, "name", None))
|
||
elif args.action == "remove":
|
||
rc = cmd_remove(args.workflow_names)
|
||
elif args.action == "validate":
|
||
rc = cmd_validate(args.workflow_name)
|
||
else:
|
||
parser.print_help()
|
||
|
||
sys.exit(rc)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|