Add admin publish-as-official, hash-based status sync, and GUI fixes
Registry: - Add admin owner override for publishing tools as "official" - Add POST /api/v1/tools/status-by-hash batch endpoint for status lookup scoped to publisher_id (works for tools published under any owner) GUI: - Add "Publish as" dropdown in publish dialog for admin users - Add "installed" tool state (teal with arrow indicator) for registry-installed tools - Fix tool editing for official/* qualified tool names (_get_qualified_name helper) - Fix cancel navigation returning to wrong page (Welcome instead of Tools) - Fix collections tab not refreshing after publish - Refactor StatusSyncWorker to use batch hash lookup (1 request instead of N) with chunking (100 max) and hash collision handling CLI: - Switch registry status sync to hash-based lookup - Add collection dependency checking and unpublished dep detection Publish dialog cleanup: - Move yaml import to module level, remove duplicate _bump_patch_version - Fix owner combo using currentText() for reliable selection Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -640,22 +640,79 @@ def cmd_collections_publish(args):
|
||||
return _publish_collection_to_registry(collection, registry_refs, transformed_pinned, client, user_slug)
|
||||
|
||||
|
||||
def _bump_patch_version(version: str) -> str:
|
||||
"""Bump the patch version of a semver string."""
|
||||
import re
|
||||
match = re.match(r'^(\d+)\.(\d+)\.(\d+)(.*)?$', version)
|
||||
if match:
|
||||
major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||
suffix = match.group(4) or ""
|
||||
return f"{major}.{minor}.{patch + 1}{suffix}"
|
||||
# Fallback: append .1 if not valid semver
|
||||
return f"{version}.1"
|
||||
|
||||
|
||||
def _publish_single_tool(tool_name: str, client) -> dict:
|
||||
"""
|
||||
Publish a single tool. Returns status dict.
|
||||
|
||||
Updates local tool config with registry_hash and registry_status
|
||||
to maintain consistency with direct `cmdforge registry publish` flow.
|
||||
|
||||
If the tool was previously rejected, automatically bumps the version
|
||||
to allow resubmission.
|
||||
"""
|
||||
from ..tool import load_tool, get_tools_dir
|
||||
from ..registry_client import RegistryError
|
||||
import yaml
|
||||
|
||||
tool = load_tool(tool_name)
|
||||
# For publishing, prefer qualified (owned) path first, then fall back to unqualified.
|
||||
# This ensures we publish the explicitly-owned version when both exist.
|
||||
tool = None
|
||||
try:
|
||||
me = client.get_me()
|
||||
owner = me.get("slug", "")
|
||||
except Exception:
|
||||
owner = ""
|
||||
|
||||
if owner:
|
||||
tool = load_tool(f"{owner}/{tool_name}")
|
||||
if not tool:
|
||||
tool = load_tool(tool_name)
|
||||
if not tool:
|
||||
return {"success": False, "error": f"Tool '{tool_name}' not found"}
|
||||
|
||||
# Load README/defaults if exists
|
||||
tool_dir = tool.path.parent if tool.path else (get_tools_dir() / tool_name)
|
||||
config_path = tool_dir / "config.yaml"
|
||||
|
||||
# Check if tool was previously rejected - if so, bump version
|
||||
version_bumped = False
|
||||
try:
|
||||
status_info = client.get_my_tool_status(tool_name)
|
||||
current_status = status_info.get("status", "")
|
||||
if current_status == "rejected":
|
||||
# Bump version to allow resubmission
|
||||
old_version = tool.version
|
||||
new_version = _bump_patch_version(old_version)
|
||||
|
||||
# Update local config
|
||||
if config_path.exists():
|
||||
config_data = yaml.safe_load(config_path.read_text()) or {}
|
||||
config_data["version"] = new_version
|
||||
config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False))
|
||||
|
||||
# Reload tool from same file (not by name, to preserve path)
|
||||
from ..tool import Tool
|
||||
config_data = yaml.safe_load(config_path.read_text()) or {}
|
||||
tool = Tool.from_dict(config_data)
|
||||
tool.path = config_path
|
||||
version_bumped = True
|
||||
print(f" (Version bumped {old_version} -> {new_version} due to previous rejection)")
|
||||
except RegistryError as e:
|
||||
if e.code != "TOOL_NOT_FOUND":
|
||||
pass # Other errors - continue with publish attempt
|
||||
|
||||
# Load README/defaults if exists
|
||||
readme_path = tool_dir / "README.md"
|
||||
readme = readme_path.read_text() if readme_path.exists() else ""
|
||||
defaults_path = tool_dir / "defaults.yaml"
|
||||
@@ -665,8 +722,7 @@ def _publish_single_tool(tool_name: str, client) -> dict:
|
||||
config_yaml = yaml.safe_dump(tool.to_dict(), sort_keys=False)
|
||||
result = client.publish_tool(config_yaml, readme=readme, defaults=defaults, dry_run=False)
|
||||
|
||||
# Update local tool config with registry metadata
|
||||
config_path = get_tools_dir() / tool_name / "config.yaml"
|
||||
# Update local tool config with registry metadata (use same config_path from above)
|
||||
config_hash = result.get("config_hash")
|
||||
moderation_status = result.get("status", "pending")
|
||||
|
||||
@@ -689,7 +745,9 @@ def _publish_single_tool(tool_name: str, client) -> dict:
|
||||
"pending": is_pending,
|
||||
"status": moderation_status,
|
||||
"config_hash": config_hash,
|
||||
"pr_url": result.get("pr_url")
|
||||
"pr_url": result.get("pr_url"),
|
||||
"version_bumped": version_bumped,
|
||||
"version": tool.version if tool else None
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -810,6 +868,11 @@ def cmd_collections_status(args):
|
||||
print(f"Status: Pending tool approvals\n")
|
||||
|
||||
all_approved = True
|
||||
has_rejected = False
|
||||
has_changes_requested = False
|
||||
rejected_tools = []
|
||||
changes_tools = []
|
||||
|
||||
for tool_name in collection.pending_tools:
|
||||
try:
|
||||
# Use authenticated endpoint to see pending/private tools
|
||||
@@ -820,6 +883,12 @@ def cmd_collections_status(args):
|
||||
else:
|
||||
print(f" {user_slug}/{tool_name}: {status}")
|
||||
all_approved = False
|
||||
if status == "rejected":
|
||||
has_rejected = True
|
||||
rejected_tools.append(tool_name)
|
||||
elif status == "changes_requested":
|
||||
has_changes_requested = True
|
||||
changes_tools.append(tool_name)
|
||||
except RegistryError as e:
|
||||
print(f" {user_slug}/{tool_name}: {e.message}")
|
||||
all_approved = False
|
||||
@@ -827,6 +896,25 @@ def cmd_collections_status(args):
|
||||
if all_approved:
|
||||
print(f"\nAll tools approved!")
|
||||
print(f"Run: cmdforge collections publish {collection.name} --continue")
|
||||
elif has_rejected:
|
||||
print(f"\nSome tools were rejected:")
|
||||
for t in rejected_tools:
|
||||
print(f" - {t}")
|
||||
print(f"\nTo proceed, you need to:")
|
||||
print(f" 1. Fix the issues with rejected tools")
|
||||
print(f" 2. Republish them: cmdforge registry publish <toolname>")
|
||||
print(f" 3. Then republish collection: cmdforge collections publish {collection.name}")
|
||||
|
||||
# Clear pending state since tools were rejected
|
||||
collection.pending_approval = False
|
||||
collection.pending_tools = []
|
||||
collection.save()
|
||||
print(f"\n(Collection pending state cleared)")
|
||||
elif has_changes_requested:
|
||||
print(f"\nSome tools need changes:")
|
||||
for t in changes_tools:
|
||||
print(f" - {t}")
|
||||
print(f"\nAddress the feedback and republish the tools.")
|
||||
else:
|
||||
print(f"\nWaiting for tool approvals...")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user