diff --git a/src/cmdforge/cli/__init__.py b/src/cmdforge/cli/__init__.py index 8a50847..5b5d393 100644 --- a/src/cmdforge/cli/__init__.py +++ b/src/cmdforge/cli/__init__.py @@ -190,6 +190,12 @@ def main(): p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')") 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 p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools") p_reg_mytools.set_defaults(func=cmd_registry) diff --git a/src/cmdforge/cli/registry_commands.py b/src/cmdforge/cli/registry_commands.py index 63459a1..0feb048 100644 --- a/src/cmdforge/cli/registry_commands.py +++ b/src/cmdforge/cli/registry_commands.py @@ -29,6 +29,8 @@ def cmd_registry(args): return _cmd_registry_update(args) elif args.registry_cmd == "publish": return _cmd_registry_publish(args) + elif args.registry_cmd == "update-readme": + return _cmd_registry_update_readme(args) elif args.registry_cmd == "my-tools": return _cmd_registry_my_tools(args) elif args.registry_cmd == "status": @@ -47,6 +49,7 @@ def cmd_registry(args): print(" info Show tool information") print(" update Update local index cache") print(" publish [path] Publish a tool") + print(" update-readme Update README for published tool(s)") print(" my-tools List your published tools") print(" status Check moderation status of a tool") print(" browse Browse tools (GUI)") @@ -635,6 +638,86 @@ def _cmd_registry_publish(args): 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): """List your published tools.""" from ..registry_client import RegistryError, get_client diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index 64cce2c..ce1402d 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -2815,6 +2815,36 @@ def create_app() -> Flask: return jsonify({"data": {"status": "active", "owner": owner, "name": name}}) + @app.route("/api/v1/tools///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"]) @require_token def update_settings() -> Response: diff --git a/src/cmdforge/registry_client.py b/src/cmdforge/registry_client.py index 87e183d..78106eb 100644 --- a/src/cmdforge/registry_client.py +++ b/src/cmdforge/registry_client.py @@ -666,6 +666,35 @@ class RegistryClient: 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]: """ Validate that the current token is valid.