159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
|
|
"""Tool: create/read/update/validate project manifests (DB-backed, replaces .a0/manifests/*.json)."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
from typing import Any, Dict
|
|||
|
|
|
|||
|
|
from helpers.tool import Tool, Response
|
|||
|
|
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import get_project_id, get_kv, set_kv, list_kv_keys
|
|||
|
|
from usr.plugins.a0_software_orchestrator.helpers.manifest_schema import PROJECT_MANIFEST_SCHEMA, DEPLOYMENT_MANIFEST_SCHEMA
|
|||
|
|
from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _resolve_name(project_name: str = "", project_root: str = "") -> str:
|
|||
|
|
if project_name:
|
|||
|
|
return project_name.strip()
|
|||
|
|
if project_root:
|
|||
|
|
return os.path.basename(project_root.rstrip("/"))
|
|||
|
|
return os.path.basename(os.getcwd())
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _schema_for(manifest_type: str) -> Dict[str, Any]:
|
|||
|
|
if manifest_type == "project":
|
|||
|
|
return PROJECT_MANIFEST_SCHEMA
|
|||
|
|
if manifest_type == "deployment":
|
|||
|
|
return DEPLOYMENT_MANIFEST_SCHEMA
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _validate(data: Dict[str, Any], schema: Dict[str, Any]) -> list[str]:
|
|||
|
|
"""Lightweight required-keys validator (subset of JSON-Schema)."""
|
|||
|
|
errors = []
|
|||
|
|
if not isinstance(data, dict):
|
|||
|
|
return [f"manifest must be a JSON object, got {type(data).__name__}"]
|
|||
|
|
required = schema.get("required", [])
|
|||
|
|
for key in required:
|
|||
|
|
if key not in data:
|
|||
|
|
errors.append(f"missing required field: {key!r}")
|
|||
|
|
return errors
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RepoManifest(Tool):
|
|||
|
|
"""Create, read, update, list or validate project/deployment/quality manifests.
|
|||
|
|
|
|||
|
|
Args (kwargs):
|
|||
|
|
project_name (str): Projektname (bevorzugt).
|
|||
|
|
project_root (str): Legacy – wird zu basename() aufgelöst.
|
|||
|
|
manifest_type (str): "project" (default), "deployment", "quality".
|
|||
|
|
action (str): "create" (default), "read", "update", "list", "validate".
|
|||
|
|
data (str|dict): JSON-string oder dict (für create/update).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
async def execute(
|
|||
|
|
self,
|
|||
|
|
project_name: str = "",
|
|||
|
|
project_root: str = "",
|
|||
|
|
manifest_type: str = "project",
|
|||
|
|
action: str = "create",
|
|||
|
|
data: str = "",
|
|||
|
|
**kwargs,
|
|||
|
|
):
|
|||
|
|
name = _resolve_name(project_name, project_root)
|
|||
|
|
try:
|
|||
|
|
pid = get_project_id(name)
|
|||
|
|
if pid is None:
|
|||
|
|
raise LookupError(f"project '{name}' is not registered")
|
|||
|
|
except (ValueError, LookupError) as e:
|
|||
|
|
return Response(message=f"ERROR: {e}", break_loop=False)
|
|||
|
|
|
|||
|
|
except PluginDBError as e:
|
|||
|
|
return Response(message=f"Database error: {e}", break_loop=False)
|
|||
|
|
key = f"manifest_{manifest_type}"
|
|||
|
|
schema = _schema_for(manifest_type)
|
|||
|
|
|
|||
|
|
if action == "list":
|
|||
|
|
keys = [k for k in list_kv_keys(pid) if k.startswith("manifest_")]
|
|||
|
|
if not keys:
|
|||
|
|
return Response(
|
|||
|
|
message=f"No manifests for project {name!r}.",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
return Response(
|
|||
|
|
message=f"Manifests for {name!r}: {', '.join(keys)}",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if action == "read":
|
|||
|
|
manifest = get_kv(pid, key, default=None)
|
|||
|
|
if manifest is None:
|
|||
|
|
return Response(
|
|||
|
|
message=f"No {manifest_type!r} manifest for project {name!r}.",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
return Response(
|
|||
|
|
message=json.dumps(manifest, indent=2, ensure_ascii=False),
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if action in ("create", "update"):
|
|||
|
|
# Parse data
|
|||
|
|
if not data:
|
|||
|
|
return Response(
|
|||
|
|
message="ERROR: 'data' required for create/update (JSON string or dict)",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
if isinstance(data, str):
|
|||
|
|
try:
|
|||
|
|
parsed = json.loads(data)
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
return Response(
|
|||
|
|
message=f"ERROR: 'data' is not valid JSON: {e}",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
parsed = data
|
|||
|
|
|
|||
|
|
# Validate against schema (if known)
|
|||
|
|
if schema:
|
|||
|
|
errors = _validate(parsed, schema)
|
|||
|
|
if errors:
|
|||
|
|
return Response(
|
|||
|
|
message=f"ERROR: manifest validation failed: {errors}",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
set_kv(pid, key, parsed)
|
|||
|
|
return Response(
|
|||
|
|
message=f"{manifest_type!r} manifest saved for project {name!r} (key={key!r}).",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if action == "validate":
|
|||
|
|
manifest = get_kv(pid, key, default=None)
|
|||
|
|
if manifest is None:
|
|||
|
|
return Response(
|
|||
|
|
message=f"No {manifest_type!r} manifest to validate for project {name!r}.",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
if not schema:
|
|||
|
|
return Response(
|
|||
|
|
message=f"No schema defined for {manifest_type!r}; cannot validate.",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
errors = _validate(manifest, schema)
|
|||
|
|
if errors:
|
|||
|
|
return Response(
|
|||
|
|
message=f"Validation FAILED: {errors}",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
return Response(
|
|||
|
|
message=f"Validation PASSED for {manifest_type!r} manifest of project {name!r}.",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return Response(
|
|||
|
|
message=f"Unknown action {action!r}. Supported: create, read, update, list, validate",
|
|||
|
|
break_loop=False,
|
|||
|
|
)
|