Initial commit
This commit is contained in:
410
skills/artifact.scaffold/artifact_scaffold.py
Executable file
410
skills/artifact.scaffold/artifact_scaffold.py
Executable file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
artifact_scaffold.py - Generate new artifact templates automatically from metadata inputs
|
||||
|
||||
Creates compliant artifact descriptors, registers them in the registry, and optionally validates them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
import argparse
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
from betty.config import BASE_DIR
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import format_error_response
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# Default artifact directories
|
||||
ARTIFACTS_DIR = os.path.join(BASE_DIR, "artifacts")
|
||||
REGISTRY_DIR = os.path.join(BASE_DIR, "registry")
|
||||
ARTIFACTS_REGISTRY_FILE = os.path.join(REGISTRY_DIR, "artifacts.json")
|
||||
|
||||
|
||||
def ensure_directories():
|
||||
"""Ensure required directories exist"""
|
||||
os.makedirs(ARTIFACTS_DIR, exist_ok=True)
|
||||
os.makedirs(REGISTRY_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def load_artifacts_registry() -> Dict[str, Any]:
|
||||
"""Load the artifacts registry, or create a new one if it doesn't exist"""
|
||||
if os.path.exists(ARTIFACTS_REGISTRY_FILE):
|
||||
try:
|
||||
with open(ARTIFACTS_REGISTRY_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load artifacts registry: {e}")
|
||||
return create_empty_registry()
|
||||
else:
|
||||
return create_empty_registry()
|
||||
|
||||
|
||||
def create_empty_registry() -> Dict[str, Any]:
|
||||
"""Create a new empty artifacts registry"""
|
||||
return {
|
||||
"registry_version": "1.0.0",
|
||||
"generated_at": datetime.utcnow().isoformat() + "Z",
|
||||
"artifacts": []
|
||||
}
|
||||
|
||||
|
||||
def save_artifacts_registry(registry: Dict[str, Any]):
|
||||
"""Save the artifacts registry"""
|
||||
registry["generated_at"] = datetime.utcnow().isoformat() + "Z"
|
||||
with open(ARTIFACTS_REGISTRY_FILE, 'w') as f:
|
||||
json.dump(registry, f, indent=2)
|
||||
logger.info(f"Saved artifacts registry to {ARTIFACTS_REGISTRY_FILE}")
|
||||
|
||||
|
||||
def generate_artifact_yaml(
|
||||
artifact_id: str,
|
||||
category: str,
|
||||
extends: Optional[str] = None,
|
||||
fields: Optional[List[Dict[str, str]]] = None,
|
||||
version: str = "0.1.0"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate an artifact YAML structure
|
||||
|
||||
Args:
|
||||
artifact_id: Unique identifier for the artifact (e.g., "new.artifact")
|
||||
category: Category/type of artifact (e.g., "report", "specification")
|
||||
extends: Optional base artifact to extend from
|
||||
fields: List of field definitions with name and type
|
||||
version: Semantic version (default: 0.1.0)
|
||||
|
||||
Returns:
|
||||
Dictionary representing the artifact structure
|
||||
"""
|
||||
artifact = {
|
||||
"id": artifact_id,
|
||||
"version": version,
|
||||
"category": category,
|
||||
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||
"metadata": {
|
||||
"description": f"{artifact_id} artifact",
|
||||
"tags": [category]
|
||||
}
|
||||
}
|
||||
|
||||
if extends:
|
||||
artifact["extends"] = extends
|
||||
|
||||
if fields:
|
||||
artifact["schema"] = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
for field in fields:
|
||||
field_name = field.get("name", "")
|
||||
field_type = field.get("type", "string")
|
||||
field_description = field.get("description", f"{field_name} field")
|
||||
field_required = field.get("required", False)
|
||||
|
||||
artifact["schema"]["properties"][field_name] = {
|
||||
"type": field_type,
|
||||
"description": field_description
|
||||
}
|
||||
|
||||
if field_required:
|
||||
artifact["schema"]["required"].append(field_name)
|
||||
|
||||
return artifact
|
||||
|
||||
|
||||
def get_artifact_filename(artifact_id: str) -> str:
|
||||
"""
|
||||
Generate filename for artifact YAML file
|
||||
|
||||
Args:
|
||||
artifact_id: The artifact ID (e.g., "new.artifact")
|
||||
|
||||
Returns:
|
||||
Filename in format: {artifact_id}.artifact.yaml
|
||||
"""
|
||||
# Replace dots with hyphens for filename
|
||||
safe_id = artifact_id.replace(".", "-")
|
||||
return f"{safe_id}.artifact.yaml"
|
||||
|
||||
|
||||
def save_artifact_yaml(artifact: Dict[str, Any], output_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Save artifact to YAML file
|
||||
|
||||
Args:
|
||||
artifact: The artifact dictionary
|
||||
output_path: Optional custom output path
|
||||
|
||||
Returns:
|
||||
Path to the saved file
|
||||
"""
|
||||
artifact_id = artifact["id"]
|
||||
|
||||
if output_path:
|
||||
file_path = output_path
|
||||
else:
|
||||
filename = get_artifact_filename(artifact_id)
|
||||
file_path = os.path.join(ARTIFACTS_DIR, filename)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
yaml.dump(artifact, f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
logger.info(f"Saved artifact to {file_path}")
|
||||
return file_path
|
||||
|
||||
|
||||
def register_artifact(artifact: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Register artifact in the artifacts registry
|
||||
|
||||
Args:
|
||||
artifact: The artifact dictionary
|
||||
|
||||
Returns:
|
||||
The updated registry
|
||||
"""
|
||||
registry = load_artifacts_registry()
|
||||
|
||||
# Check if artifact already exists
|
||||
artifact_id = artifact["id"]
|
||||
existing_idx = None
|
||||
for idx, reg_artifact in enumerate(registry["artifacts"]):
|
||||
if reg_artifact["id"] == artifact_id:
|
||||
existing_idx = idx
|
||||
break
|
||||
|
||||
# Create registry entry
|
||||
registry_entry = {
|
||||
"id": artifact["id"],
|
||||
"version": artifact["version"],
|
||||
"category": artifact["category"],
|
||||
"created_at": artifact["created_at"],
|
||||
"description": artifact.get("metadata", {}).get("description", ""),
|
||||
"tags": artifact.get("metadata", {}).get("tags", [])
|
||||
}
|
||||
|
||||
if "extends" in artifact:
|
||||
registry_entry["extends"] = artifact["extends"]
|
||||
|
||||
if "schema" in artifact:
|
||||
registry_entry["schema"] = artifact["schema"]
|
||||
|
||||
# Update or add entry
|
||||
if existing_idx is not None:
|
||||
registry["artifacts"][existing_idx] = registry_entry
|
||||
logger.info(f"Updated artifact {artifact_id} in registry")
|
||||
else:
|
||||
registry["artifacts"].append(registry_entry)
|
||||
logger.info(f"Added artifact {artifact_id} to registry")
|
||||
|
||||
save_artifacts_registry(registry)
|
||||
return registry
|
||||
|
||||
|
||||
def scaffold_artifact(
|
||||
artifact_id: str,
|
||||
category: str,
|
||||
extends: Optional[str] = None,
|
||||
fields: Optional[List[Dict[str, str]]] = None,
|
||||
output_path: Optional[str] = None,
|
||||
validate: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Main scaffolding function
|
||||
|
||||
Args:
|
||||
artifact_id: Unique identifier for the artifact
|
||||
category: Category/type of artifact
|
||||
extends: Optional base artifact to extend from
|
||||
fields: List of field definitions
|
||||
output_path: Optional custom output path
|
||||
validate: Whether to run validation after scaffolding
|
||||
|
||||
Returns:
|
||||
Result dictionary with status and details
|
||||
"""
|
||||
try:
|
||||
ensure_directories()
|
||||
|
||||
# Generate artifact structure
|
||||
artifact = generate_artifact_yaml(
|
||||
artifact_id=artifact_id,
|
||||
category=category,
|
||||
extends=extends,
|
||||
fields=fields
|
||||
)
|
||||
|
||||
# Save to file
|
||||
file_path = save_artifact_yaml(artifact, output_path)
|
||||
|
||||
# Register in artifacts registry
|
||||
registry = register_artifact(artifact)
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"status": "success",
|
||||
"artifact_id": artifact_id,
|
||||
"file_path": file_path,
|
||||
"version": artifact["version"],
|
||||
"category": category,
|
||||
"registry_path": ARTIFACTS_REGISTRY_FILE,
|
||||
"artifacts_registered": len(registry["artifacts"])
|
||||
}
|
||||
|
||||
# Optional validation
|
||||
if validate:
|
||||
validation_result = validate_artifact(file_path)
|
||||
result["validation"] = validation_result
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to scaffold artifact: {e}", exc_info=True)
|
||||
return {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": str(e),
|
||||
"details": format_error_response(e)
|
||||
}
|
||||
|
||||
|
||||
def validate_artifact(file_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate an artifact YAML file
|
||||
|
||||
Args:
|
||||
file_path: Path to the artifact YAML file
|
||||
|
||||
Returns:
|
||||
Validation result dictionary
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
artifact = yaml.safe_load(f)
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Required fields
|
||||
required_fields = ["id", "version", "category", "created_at"]
|
||||
for field in required_fields:
|
||||
if field not in artifact:
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
# Version format check
|
||||
if "version" in artifact:
|
||||
version = artifact["version"]
|
||||
parts = version.split(".")
|
||||
if len(parts) != 3 or not all(p.isdigit() for p in parts):
|
||||
warnings.append(f"Version {version} may not follow semantic versioning (X.Y.Z)")
|
||||
|
||||
# Category check
|
||||
if "category" in artifact and not artifact["category"]:
|
||||
warnings.append("Category is empty")
|
||||
|
||||
# Schema validation
|
||||
if "schema" in artifact:
|
||||
schema = artifact["schema"]
|
||||
if "properties" not in schema:
|
||||
warnings.append("Schema missing 'properties' field")
|
||||
|
||||
is_valid = len(errors) == 0
|
||||
|
||||
return {
|
||||
"valid": is_valid,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"file_path": file_path
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [f"Failed to validate: {str(e)}"],
|
||||
"warnings": [],
|
||||
"file_path": file_path
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate new artifact templates from metadata"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--id",
|
||||
required=True,
|
||||
help="Artifact ID (e.g., 'new.artifact')"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--category",
|
||||
required=True,
|
||||
help="Artifact category (e.g., 'report', 'specification')"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--extends",
|
||||
help="Base artifact to extend from (optional)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--fields",
|
||||
help="JSON string of field definitions (e.g., '[{\"name\":\"summary\",\"type\":\"string\"}]')"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="Custom output path for the artifact file"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--validate",
|
||||
action="store_true",
|
||||
help="Validate the artifact after generation"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse fields if provided
|
||||
fields = None
|
||||
if args.fields:
|
||||
try:
|
||||
fields = json.loads(args.fields)
|
||||
except json.JSONDecodeError as e:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": f"Invalid JSON for fields: {e}"
|
||||
}, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
# Scaffold the artifact
|
||||
result = scaffold_artifact(
|
||||
artifact_id=args.id,
|
||||
category=args.category,
|
||||
extends=args.extends,
|
||||
fields=fields,
|
||||
output_path=args.output,
|
||||
validate=args.validate
|
||||
)
|
||||
|
||||
# Output result as JSON
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if result.get("ok", False) else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user