Add registry endpoint and CLI command for updating tool READMEs
- PATCH /api/v1/tools/<owner>/<name>/readme updates README without affecting config hash or requiring version bump - Add RegistryClient.update_readme() method - Add `cmdforge registry update-readme` CLI command with --all flag for batch updating all published tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2cb9c0057
commit
e83c9c15f3
|
|
@ -190,6 +190,12 @@ def main():
|
||||||
p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')")
|
p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')")
|
||||||
p_reg_publish.set_defaults(func=cmd_registry)
|
p_reg_publish.set_defaults(func=cmd_registry)
|
||||||
|
|
||||||
|
# registry update-readme
|
||||||
|
p_reg_update_readme = registry_sub.add_parser("update-readme", help="Update README for a published tool")
|
||||||
|
p_reg_update_readme.add_argument("tool", help="Tool name (local name, will resolve owner)")
|
||||||
|
p_reg_update_readme.add_argument("--all", action="store_true", dest="update_all", help="Update README for all published tools that have a local README.md")
|
||||||
|
p_reg_update_readme.set_defaults(func=cmd_registry)
|
||||||
|
|
||||||
# registry my-tools
|
# registry my-tools
|
||||||
p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools")
|
p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools")
|
||||||
p_reg_mytools.set_defaults(func=cmd_registry)
|
p_reg_mytools.set_defaults(func=cmd_registry)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ def cmd_registry(args):
|
||||||
return _cmd_registry_update(args)
|
return _cmd_registry_update(args)
|
||||||
elif args.registry_cmd == "publish":
|
elif args.registry_cmd == "publish":
|
||||||
return _cmd_registry_publish(args)
|
return _cmd_registry_publish(args)
|
||||||
|
elif args.registry_cmd == "update-readme":
|
||||||
|
return _cmd_registry_update_readme(args)
|
||||||
elif args.registry_cmd == "my-tools":
|
elif args.registry_cmd == "my-tools":
|
||||||
return _cmd_registry_my_tools(args)
|
return _cmd_registry_my_tools(args)
|
||||||
elif args.registry_cmd == "status":
|
elif args.registry_cmd == "status":
|
||||||
|
|
@ -47,6 +49,7 @@ def cmd_registry(args):
|
||||||
print(" info <tool> Show tool information")
|
print(" info <tool> Show tool information")
|
||||||
print(" update Update local index cache")
|
print(" update Update local index cache")
|
||||||
print(" publish [path] Publish a tool")
|
print(" publish [path] Publish a tool")
|
||||||
|
print(" update-readme Update README for published tool(s)")
|
||||||
print(" my-tools List your published tools")
|
print(" my-tools List your published tools")
|
||||||
print(" status <tool> Check moderation status of a tool")
|
print(" status <tool> Check moderation status of a tool")
|
||||||
print(" browse Browse tools (GUI)")
|
print(" browse Browse tools (GUI)")
|
||||||
|
|
@ -635,6 +638,86 @@ def _cmd_registry_publish(args):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_registry_update_readme(args):
|
||||||
|
"""Update README for published tool(s) on the registry."""
|
||||||
|
from ..registry_client import RegistryError, get_client
|
||||||
|
from ..tool import TOOLS_DIR, list_tools
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
except RegistryError as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if getattr(args, "update_all", False):
|
||||||
|
# Update all published tools that have a local README
|
||||||
|
tools = list_tools()
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
failed = 0
|
||||||
|
for tool_name in tools:
|
||||||
|
tool_dir = TOOLS_DIR / tool_name
|
||||||
|
config_path = tool_dir / "config.yaml"
|
||||||
|
readme_path = tool_dir / "README.md"
|
||||||
|
|
||||||
|
if not readme_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if published
|
||||||
|
try:
|
||||||
|
config = yaml.safe_load(config_path.read_text()) or {}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
owner = config.get("registry_owner", "")
|
||||||
|
if not owner:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
readme_content = readme_path.read_text()
|
||||||
|
try:
|
||||||
|
result = client.update_readme(owner, tool_name, readme_content)
|
||||||
|
count = result.get("versions_updated", 0)
|
||||||
|
print(f" {owner}/{tool_name}: updated ({count} version(s))")
|
||||||
|
updated += 1
|
||||||
|
except RegistryError as e:
|
||||||
|
print(f" {owner}/{tool_name}: FAILED - {e}", file=sys.stderr)
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\nDone: {updated} updated, {failed} failed, {skipped} skipped (not published)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Single tool mode
|
||||||
|
tool_name = args.tool
|
||||||
|
tool_dir = TOOLS_DIR / tool_name
|
||||||
|
config_path = tool_dir / "config.yaml"
|
||||||
|
readme_path = tool_dir / "README.md"
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
print(f"Error: Tool '{tool_name}' not found", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not readme_path.exists():
|
||||||
|
print(f"Error: No README.md found for '{tool_name}'", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
config = yaml.safe_load(config_path.read_text()) or {}
|
||||||
|
owner = config.get("registry_owner", "")
|
||||||
|
if not owner:
|
||||||
|
print(f"Error: Tool '{tool_name}' has no registry_owner — not published?", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
readme_content = readme_path.read_text()
|
||||||
|
try:
|
||||||
|
result = client.update_readme(owner, tool_name, readme_content)
|
||||||
|
count = result.get("versions_updated", 0)
|
||||||
|
print(f"Updated README for {owner}/{tool_name} ({count} version(s))")
|
||||||
|
except RegistryError as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _cmd_registry_my_tools(args):
|
def _cmd_registry_my_tools(args):
|
||||||
"""List your published tools."""
|
"""List your published tools."""
|
||||||
from ..registry_client import RegistryError, get_client
|
from ..registry_client import RegistryError, get_client
|
||||||
|
|
|
||||||
|
|
@ -2815,6 +2815,36 @@ def create_app() -> Flask:
|
||||||
|
|
||||||
return jsonify({"data": {"status": "active", "owner": owner, "name": name}})
|
return jsonify({"data": {"status": "active", "owner": owner, "name": name}})
|
||||||
|
|
||||||
|
@app.route("/api/v1/tools/<owner>/<name>/readme", methods=["PATCH"])
|
||||||
|
@require_token
|
||||||
|
def update_tool_readme(owner: str, name: str) -> Response:
|
||||||
|
"""Update the README for an existing tool without affecting config or hash."""
|
||||||
|
if g.current_publisher["slug"] != owner:
|
||||||
|
return error_response("FORBIDDEN", "You can only update your own tools", 403)
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
readme = data.get("readme")
|
||||||
|
if readme is None:
|
||||||
|
return error_response("VALIDATION_ERROR", "Missing 'readme' field", 400)
|
||||||
|
|
||||||
|
version = data.get("version")
|
||||||
|
if version:
|
||||||
|
result = g.db.execute(
|
||||||
|
"UPDATE tools SET readme = ? WHERE owner = ? AND name = ? AND version = ?",
|
||||||
|
[readme, owner, name, version],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Update all versions
|
||||||
|
result = g.db.execute(
|
||||||
|
"UPDATE tools SET readme = ? WHERE owner = ? AND name = ?",
|
||||||
|
[readme, owner, name],
|
||||||
|
)
|
||||||
|
if result.rowcount == 0:
|
||||||
|
return error_response("TOOL_NOT_FOUND", f"Tool {owner}/{name} not found", 404)
|
||||||
|
g.db.commit()
|
||||||
|
|
||||||
|
return jsonify({"data": {"status": "updated", "owner": owner, "name": name, "versions_updated": result.rowcount}})
|
||||||
|
|
||||||
@app.route("/api/v1/me/settings", methods=["PUT"])
|
@app.route("/api/v1/me/settings", methods=["PUT"])
|
||||||
@require_token
|
@require_token
|
||||||
def update_settings() -> Response:
|
def update_settings() -> Response:
|
||||||
|
|
|
||||||
|
|
@ -666,6 +666,35 @@ class RegistryClient:
|
||||||
|
|
||||||
return response.json().get("data", {})
|
return response.json().get("data", {})
|
||||||
|
|
||||||
|
def update_readme(self, owner: str, name: str, readme: str, version: str = "") -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Update the README for an existing published tool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Tool owner slug
|
||||||
|
name: Tool name
|
||||||
|
readme: New README content
|
||||||
|
version: Optional specific version to update (default: all versions)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with update status
|
||||||
|
"""
|
||||||
|
payload: Dict[str, Any] = {"readme": readme}
|
||||||
|
if version:
|
||||||
|
payload["version"] = version
|
||||||
|
|
||||||
|
response = self._request(
|
||||||
|
"PATCH",
|
||||||
|
f"/tools/{owner}/{name}/readme",
|
||||||
|
json_data=payload,
|
||||||
|
require_auth=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code not in (200, 201):
|
||||||
|
self._handle_error_response(response)
|
||||||
|
|
||||||
|
return response.json().get("data", {})
|
||||||
|
|
||||||
def validate_token(self) -> tuple[bool, str]:
|
def validate_token(self) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Validate that the current token is valid.
|
Validate that the current token is valid.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue