197 lines
5.8 KiB
Python
197 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Plugin Manifest Generator
|
|
|
|
Generates manifest JSON files with computed SHA256 hashes for ComfyUI-Doctor plugins.
|
|
|
|
Usage:
|
|
python scripts/plugin_manifest.py pipeline/plugins/community/example.py
|
|
python scripts/plugin_manifest.py pipeline/plugins/community/example.py --write
|
|
python scripts/plugin_manifest.py pipeline/plugins/community/*.py --write
|
|
|
|
Exit codes:
|
|
0 = Success
|
|
1 = Error
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Valid plugin filename pattern
|
|
VALID_FILENAME_PATTERN = re.compile(r'^[A-Za-z0-9_]+\.py$')
|
|
|
|
# Max file size (256 KiB by default)
|
|
MAX_FILE_SIZE = 262144
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def compute_sha256(file_path: Path) -> str:
|
|
"""Compute SHA256 hash of file."""
|
|
return hashlib.sha256(file_path.read_bytes()).hexdigest()
|
|
|
|
|
|
def get_doctor_version() -> str:
|
|
"""Read the canonical Doctor version from pyproject.toml."""
|
|
pyproject = PROJECT_ROOT / "pyproject.toml"
|
|
try:
|
|
match = re.search(
|
|
r'(?m)^version\s*=\s*["\']([^"\']+)["\']',
|
|
pyproject.read_text(encoding="utf-8", errors="ignore"),
|
|
)
|
|
if match:
|
|
return match.group(1).strip()
|
|
except Exception:
|
|
pass
|
|
return "0.0.0"
|
|
|
|
|
|
def validate_plugin_file(file_path: Path) -> tuple[bool, str]:
|
|
"""Validate plugin file meets requirements."""
|
|
# Check exists
|
|
if not file_path.exists():
|
|
return False, "File does not exist"
|
|
|
|
# Check is file
|
|
if not file_path.is_file():
|
|
return False, "Not a regular file"
|
|
|
|
# Check filename pattern
|
|
if not VALID_FILENAME_PATTERN.match(file_path.name):
|
|
return False, f"Filename must match pattern: {VALID_FILENAME_PATTERN.pattern}"
|
|
|
|
# Check file size
|
|
size = file_path.stat().st_size
|
|
if size > MAX_FILE_SIZE:
|
|
return False, f"File too large: {size} bytes (max: {MAX_FILE_SIZE})"
|
|
|
|
return True, "OK"
|
|
|
|
|
|
def get_manifest_path(plugin_path: Path) -> Path:
|
|
"""Get manifest path for plugin file."""
|
|
return plugin_path.with_suffix('.json')
|
|
|
|
|
|
def prompt_manifest_fields(plugin_path: Path) -> dict:
|
|
"""Prompt user for manifest fields."""
|
|
plugin_name = plugin_path.stem
|
|
|
|
# Suggest defaults
|
|
default_id = f"community.{plugin_name}"
|
|
default_name = plugin_name.replace('_', ' ').title()
|
|
default_version = "1.0.0"
|
|
default_author = "Community"
|
|
default_min_version = get_doctor_version()
|
|
|
|
print("\nEnter plugin details (press Enter to use defaults):")
|
|
|
|
plugin_id = input(f" ID [{default_id}]: ").strip() or default_id
|
|
name = input(f" Name [{default_name}]: ").strip() or default_name
|
|
version = input(f" Version [{default_version}]: ").strip() or default_version
|
|
author = input(f" Author [{default_author}]: ").strip() or default_author
|
|
min_doctor_version = input(f" Min Doctor Version [{default_min_version}]: ").strip() or default_min_version
|
|
|
|
return {
|
|
"id": plugin_id,
|
|
"name": name,
|
|
"version": version,
|
|
"author": author,
|
|
"min_doctor_version": min_doctor_version
|
|
}
|
|
|
|
|
|
def generate_manifest(plugin_path: Path, sha256: str, fields: dict) -> dict:
|
|
"""Generate complete manifest."""
|
|
manifest = {
|
|
"id": fields["id"],
|
|
"name": fields["name"],
|
|
"version": fields["version"],
|
|
"author": fields["author"],
|
|
"min_doctor_version": fields["min_doctor_version"],
|
|
"sha256": sha256
|
|
}
|
|
return manifest
|
|
|
|
|
|
def write_manifest(manifest_path: Path, manifest: dict):
|
|
"""Write manifest to file."""
|
|
with manifest_path.open('w', encoding='utf-8') as f:
|
|
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
|
f.write('\n') # Trailing newline
|
|
|
|
|
|
def process_plugin(plugin_path: Path, write_mode: bool) -> bool:
|
|
"""Process a single plugin file."""
|
|
print("\nPlugin Manifest Generator\n")
|
|
print(f"Analyzing: {plugin_path}")
|
|
|
|
# Validate
|
|
valid, message = validate_plugin_file(plugin_path)
|
|
if not valid:
|
|
print(f" FAIL Validation failed: {message}")
|
|
return False
|
|
|
|
# Compute hash
|
|
sha256 = compute_sha256(plugin_path)
|
|
size_kb = plugin_path.stat().st_size / 1024
|
|
print(f" SHA256: {sha256}")
|
|
print(f" Size: {size_kb:.1f} KiB (within limit)")
|
|
|
|
# Get manifest fields
|
|
fields = prompt_manifest_fields(plugin_path)
|
|
|
|
# Generate manifest
|
|
manifest = generate_manifest(plugin_path, sha256, fields)
|
|
|
|
print("\nGenerated manifest:")
|
|
print(json.dumps(manifest, indent=2))
|
|
|
|
# Write or dry-run
|
|
manifest_path = get_manifest_path(plugin_path)
|
|
|
|
if write_mode:
|
|
write_manifest(manifest_path, manifest)
|
|
print(f"\nPASS Manifest written to: {manifest_path}")
|
|
else:
|
|
print(f"\n[--dry-run] Would write to: {manifest_path}")
|
|
print("To write, run with --write flag")
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Plugin Manifest Generator")
|
|
parser.add_argument("plugins", nargs='+', type=Path, help="Plugin file(s) to process")
|
|
parser.add_argument("--write", action="store_true", help="Write manifest files (default is dry-run)")
|
|
args = parser.parse_args()
|
|
|
|
if not args.write:
|
|
print("INFO: Running in DRY-RUN mode (no files will be written)")
|
|
print("Use --write to actually create manifest files\n")
|
|
|
|
success_count = 0
|
|
for plugin_path in args.plugins:
|
|
try:
|
|
if process_plugin(plugin_path, args.write):
|
|
success_count += 1
|
|
except Exception as e:
|
|
print(f"FAIL Error processing {plugin_path}: {e}")
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"Processed: {success_count}/{len(args.plugins)} plugins")
|
|
|
|
if success_count == len(args.plugins):
|
|
print("PASS All plugins processed successfully")
|
|
return 0
|
|
else:
|
|
print("WARN Some plugins failed to process")
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|