Files
CmdForge/src/cmdforge/cli/collections_commands.py
T
robandClaude Opus 4.5 fe5b2047a9 Preserve registry fields on tool save and store owner in all publish paths
- save_tool() now preserves registry_hash, registry_status, registry_owner,
  and registry_feedback from existing config when writing Tool.to_dict(),
  preventing edits from erasing registry metadata.
- CLI publish (registry_commands.py) and collection publish
  (collections_commands.py) now save registry_owner from the publish
  response, matching the GUI publish dialog behavior.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-01-30 03:07:00 -04:00

925 lines
33 KiB
Python

"""Collections commands for CmdForge CLI."""
import json
import re
import sys
from ..resolver import install_from_registry
def cmd_collections(args):
"""Handle collections subcommands."""
cmd = args.collections_cmd
# Existing commands (with --local flag support added)
if cmd == "list":
return _cmd_collections_list(args)
elif cmd == "info":
return _cmd_collections_info(args)
elif cmd == "install":
return _cmd_collections_install(args)
# Local management commands
elif cmd == "create":
return cmd_collections_create(args)
elif cmd == "show":
return cmd_collections_show(args)
elif cmd == "add":
return cmd_collections_add(args)
elif cmd == "remove":
return cmd_collections_remove(args)
elif cmd == "delete":
return cmd_collections_delete(args)
# Publishing commands
elif cmd == "publish":
return cmd_collections_publish(args)
elif cmd == "status":
return cmd_collections_status(args)
else:
# Default: show help
print("Collections commands:")
print(" list List available collections (registry)")
print(" info <name> Show registry collection details")
print(" install <name> Install all tools in a registry collection")
print()
print("Local collection management:")
print(" create <name> Create a new local collection")
print(" show <name> Show local collection details")
print(" add <coll> <tool> Add a tool to a collection")
print(" remove <coll> <tool> Remove a tool from a collection")
print(" delete <name> Delete a local collection")
print()
print("Publishing:")
print(" publish <name> Publish a collection to the registry")
print(" status <name> Check status of a pending collection")
return 0
def _cmd_collections_list(args):
"""List available collections (registry or local)."""
import json as json_mod
from ..collection import list_collections, get_collection
# Handle --local flag
if getattr(args, 'local', False):
local_colls = list_collections()
if getattr(args, 'json', False):
data = []
for name in local_colls:
coll = get_collection(name)
if coll:
data.append(coll.to_dict())
print(json_mod.dumps({"collections": data}, indent=2))
return 0
if not local_colls:
print("No local collections.")
print("Create one with: cmdforge collections create <name>")
return 0
print(f"Local collections ({len(local_colls)}):\n")
for name in local_colls:
coll = get_collection(name)
if coll:
status = "pending" if coll.pending_approval else ("published" if coll.published else "local")
print(f" {coll.display_name}")
print(f" {coll.name} ({len(coll.tools)} tools) [{status}]")
print()
return 0
# Registry listing
from ..registry_client import RegistryError, get_client
try:
client = get_client()
collections = client.get_collections()
# JSON output
if getattr(args, 'json', False):
print(json.dumps({"collections": collections}, indent=2))
return 0
if not collections:
print("No collections available.")
return 0
print(f"Available collections ({len(collections)}):\n")
for coll in collections:
name = coll.get("name", "")
display_name = coll.get("display_name", name)
description = coll.get("description", "")
# Use tool_count if available, fall back to counting tools list
tools = coll.get("tools", [])
tool_count = coll.get("tool_count", len(tools) if isinstance(tools, list) else 0)
print(f" {name}")
print(f" {display_name}")
if description:
desc_short = description[:60] + ('...' if len(description) > 60 else '')
print(f" {desc_short}")
print(f" Tools: {tool_count}")
print()
print("Install a collection with: cmdforge collections install <name>")
except RegistryError as e:
if e.code == "CONNECTION_ERROR":
print("Could not connect to the registry.", file=sys.stderr)
else:
print(f"Error: {e.message}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
return 0
def _cmd_collections_info(args):
"""Show collection details."""
from ..registry_client import RegistryError, get_client
name = args.name
try:
client = get_client()
coll = client.get_collection(name)
# JSON output
if getattr(args, 'json', False):
print(json.dumps(coll, indent=2))
return 0
display_name = coll.get("display_name", name)
description = coll.get("description", "")
maintainer = coll.get("maintainer", "")
tools = coll.get("tools", [])
pinned = coll.get("pinned", {})
tags = coll.get("tags", [])
print(f"{display_name}")
print("=" * 50)
if description:
print(f"\n{description}\n")
if maintainer:
print(f"Maintainer: {maintainer}")
if tags:
print(f"Tags: {', '.join(tags)}")
print(f"\nTools ({len(tools)}):")
for tool in tools:
# Handle both object format (from API) and string format
if isinstance(tool, dict):
tool_ref = f"{tool['owner']}/{tool['name']}"
version_constraint = tool.get('pinned_version', '')
else:
tool_ref = tool
version_constraint = pinned.get(tool_ref, '')
if version_constraint:
print(f" - {tool_ref} @ {version_constraint}")
else:
print(f" - {tool_ref}")
print(f"\nInstall all: cmdforge collections install {name}")
except RegistryError as e:
if e.code == "COLLECTION_NOT_FOUND":
print(f"Collection '{name}' not found.", file=sys.stderr)
print("List available collections: cmdforge collections list", file=sys.stderr)
elif e.code == "CONNECTION_ERROR":
print("Could not connect to the registry.", file=sys.stderr)
else:
print(f"Error: {e.message}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
return 0
def _cmd_collections_install(args):
"""Install all tools in a collection."""
from ..registry_client import RegistryError, get_client
name = args.name
use_pinned = getattr(args, 'pinned', False)
try:
client = get_client()
coll = client.get_collection(name)
tools = coll.get("tools", [])
pinned = coll.get("pinned", {}) if use_pinned else {}
display_name = coll.get("display_name", name)
if not tools:
print(f"Collection '{name}' has no tools.")
return 0
print(f"Installing collection: {display_name}")
print(f"Tools to install: {len(tools)}")
if use_pinned and pinned:
print(f"Using pinned versions: {len(pinned)} tools")
print()
installed = 0
failed = 0
for tool in tools:
# Handle both object format (from API) and string format
if isinstance(tool, dict):
tool_ref = f"{tool['owner']}/{tool['name']}"
version = tool.get('pinned_version') if use_pinned else None
else:
tool_ref = tool
version = pinned.get(tool_ref) if use_pinned else None
try:
print(f" Installing {tool_ref}...", end=" ", flush=True)
resolved = install_from_registry(tool_ref, version)
print(f"v{resolved.version}")
installed += 1
except RegistryError as e:
print(f"FAILED: {e.message}")
failed += 1
except Exception as e:
print(f"FAILED: {e}")
failed += 1
print()
print(f"Installed: {installed}/{len(tools)}")
if failed:
print(f"Failed: {failed}")
return 1
print(f"\nCollection '{name}' installed successfully!")
except RegistryError as e:
if e.code == "COLLECTION_NOT_FOUND":
print(f"Collection '{name}' not found.", file=sys.stderr)
print("List available collections: cmdforge collections list", file=sys.stderr)
elif e.code == "CONNECTION_ERROR":
print("Could not connect to the registry.", file=sys.stderr)
else:
print(f"Error: {e.message}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
return 0
# ---------------------------------------------------------------------------
# Local Collection Management Commands
# ---------------------------------------------------------------------------
def cmd_collections_create(args):
"""Create a new local collection."""
from ..collection import Collection, COLLECTIONS_DIR, list_collections
name = args.name.strip().lower()
# Validate name
if not name or not re.match(r'^[a-z0-9-]+$', name):
print("Error: Collection name must be lowercase alphanumeric with hyphens only")
return 1
# Check if exists
if name in list_collections():
print(f"Error: Collection '{name}' already exists")
print(f"Use: cmdforge collections show {name}")
print(f"Or delete it first: cmdforge collections delete {name}")
return 1
# Interactive prompts
display_name = input(f"Display name [{name.replace('-', ' ').title()}]: ").strip()
if not display_name:
display_name = name.replace("-", " ").title()
description = input("Description: ").strip()
tags_input = input("Tags (comma-separated): ").strip()
tags = [t.strip() for t in tags_input.split(",") if t.strip()]
collection = Collection(
name=name,
display_name=display_name,
description=description,
tags=tags,
)
collection.save()
print(f"\nCreated collection: {name}")
print(f" Location: {COLLECTIONS_DIR / f'{name}.yaml'}")
print(f"\nAdd tools with: cmdforge collections add {name} <tool>")
return 0
def cmd_collections_add(args):
"""Add a tool to a collection."""
from ..collection import get_collection
from ..tool import list_tools
collection = get_collection(args.collection)
if not collection:
print(f"Error: Collection '{args.collection}' not found")
return 1
tool_ref = args.tool
# Validate tool exists (local or suggest registry format)
if "/" not in tool_ref:
# Local tool reference
if tool_ref not in list_tools():
print(f"Warning: Local tool '{tool_ref}' not found")
confirm = input("Add anyway? [y/N]: ").strip().lower()
if confirm != "y":
return 1
if tool_ref in collection.tools:
print(f"Tool '{tool_ref}' already in collection")
return 0
collection.tools.append(tool_ref)
# Optional: set pinned version
if hasattr(args, 'version') and args.version:
collection.pinned[tool_ref] = args.version
collection.save()
print(f"Added '{tool_ref}' to collection '{collection.name}'")
return 0
def cmd_collections_remove(args):
"""Remove a tool from a collection."""
from ..collection import get_collection
collection = get_collection(args.collection)
if not collection:
print(f"Error: Collection '{args.collection}' not found")
return 1
tool_ref = args.tool
if tool_ref not in collection.tools:
print(f"Tool '{tool_ref}' not in collection")
return 1
collection.tools.remove(tool_ref)
collection.pinned.pop(tool_ref, None)
collection.save()
print(f"Removed '{tool_ref}' from collection '{collection.name}'")
return 0
def cmd_collections_show(args):
"""Show local collection details."""
from ..collection import get_collection, classify_tool_reference
from ..tool import list_tools
collection = get_collection(args.name)
if not collection:
print(f"Error: Collection '{args.name}' not found")
return 1
local_tools = set(list_tools())
print(f"\n{collection.display_name}")
print("=" * 50)
if collection.description:
print(f"\n{collection.description}\n")
if collection.tags:
print(f"Tags: {', '.join(collection.tags)}")
# Publishing status
if collection.pending_approval:
print(f"\nStatus: Pending (waiting for tool approvals)")
elif collection.published:
print(f"\nStatus: Published as '{collection.registry_name}'")
else:
print(f"\nStatus: Local only (not published)")
print(f"\nTools ({len(collection.tools)}):")
for tool_ref in collection.tools:
owner, name, is_local = classify_tool_reference(tool_ref)
pinned = collection.pinned.get(tool_ref, "")
version_str = f" @ {pinned}" if pinned else ""
if is_local:
exists = name in local_tools
status = "" if exists else " (not found)"
print(f" - {name}{version_str} [local]{status}")
else:
print(f" - {tool_ref}{version_str} [registry]")
if collection.pending_tools:
print(f"\nPending tool approvals:")
for tool in collection.pending_tools:
print(f" - {tool}")
return 0
def cmd_collections_delete(args):
"""Delete a local collection."""
from ..collection import get_collection
collection = get_collection(args.name)
if not collection:
print(f"Error: Collection '{args.name}' not found")
return 1
if not getattr(args, 'force', False):
confirm = input(f"Delete collection '{args.name}'? [y/N]: ").strip().lower()
if confirm != "y":
print("Cancelled")
return 0
collection.delete()
print(f"Deleted collection '{args.name}'")
return 0
# ---------------------------------------------------------------------------
# Publishing Commands
# ---------------------------------------------------------------------------
def cmd_collections_publish(args):
"""Publish a collection to the registry."""
from ..collection import get_collection, resolve_tool_references
from ..registry_client import get_client, RegistryError
collection = get_collection(args.name)
if not collection:
print(f"Error: Collection '{args.name}' not found")
return 1
if not collection.tools:
print("Error: Collection has no tools")
return 1
# Get user info
client = get_client()
is_valid, error = client.validate_token()
if not is_valid:
print(f"Error: Not authenticated. Run: cmdforge config connect")
return 1
# Get user's registry slug
try:
user_info = client.get_me()
user_slug = user_info.get("slug", "")
except RegistryError as e:
print(f"Error getting user info: {e.message}")
return 1
if not user_slug:
print("Error: Could not determine your registry username")
return 1
# Check if resuming a pending publish
if getattr(args, 'resume', False) and collection.pending_approval:
return _resume_collection_publish(collection, client, user_slug)
# Resolve tool references (uses authenticated endpoint for user's own tools)
print(f"Analyzing collection '{collection.display_name}'...")
result = resolve_tool_references(
collection.tools, collection.pinned, user_slug, client
)
registry_refs = result.registry_refs
transformed_pinned = result.transformed_pinned
unpublished = result.local_unpublished
published = result.local_published # List of (name, status, has_approved) tuples
visibility_issues = result.visibility_issues
registry_tool_issues = result.registry_tool_issues
# Check for tools where latest version is pending
pending_latest = [(name, status) for name, status, has_approved in published if status != "approved"]
# Dry run mode
if getattr(args, 'dry_run', False):
print(f"\n[DRY RUN] Would publish collection: {collection.name}")
print(f" Display name: {collection.display_name}")
print(f" Tools: {len(collection.tools)}")
if visibility_issues:
print(f"\n BLOCKING: Non-public local tools ({len(visibility_issues)}):")
for name, visibility in visibility_issues:
print(f" - {name} (visibility: {visibility})")
if registry_tool_issues:
print(f"\n BLOCKING: Registry tool issues ({len(registry_tool_issues)}):")
for ref, reason in registry_tool_issues:
print(f" - {ref}: {reason}")
if unpublished:
print(f"\n Unpublished local tools ({len(unpublished)}):")
for tool in unpublished:
print(f" - {tool} -> {user_slug}/{tool}")
if pending_latest:
print(f"\n Pending approval ({len(pending_latest)}):")
for name, status in pending_latest:
print(f" - {user_slug}/{name} ({status})")
if transformed_pinned:
print(f"\n Version pins (transformed):")
for ref, version in transformed_pinned.items():
print(f" - {ref}: {version}")
print(f"\n Final tool references:")
for ref in registry_refs:
print(f" - {ref}")
return 0
# Handle visibility issues FIRST (local tools that aren't public)
if visibility_issues:
print(f"\n{len(visibility_issues)} local tool(s) are not public:")
for name, visibility in visibility_issues:
print(f" - {name} (visibility: {visibility})")
print(f"\nCollections can only include PUBLIC tools.")
print(f"Update these tools to be public before including in a collection:")
print(f" Edit the tool's config.yaml and set: visibility: public")
print(f" Then republish with: cmdforge registry publish <toolname>")
return 1
# Handle registry tool issues (external tools that aren't public/approved)
if registry_tool_issues:
print(f"\n{len(registry_tool_issues)} registry tool(s) cannot be included:")
for ref, reason in registry_tool_issues:
print(f" - {ref}: {reason}")
print(f"\nCollections can only include PUBLIC, APPROVED tools.")
print(f"Remove these tools from the collection or wait for them to be approved.")
return 1
# Handle tools where latest version is pending (warn but don't block)
if pending_latest:
print(f"\n{len(pending_latest)} tool(s) have pending latest versions:")
for name, status in pending_latest:
print(f" - {user_slug}/{name} ({status})")
print(f"\n (An older approved version may still be usable - will attempt publish)")
# Handle unpublished tools
if unpublished:
print(f"\nThis collection contains {len(unpublished)} unpublished local tool(s):")
for tool in unpublished:
print(f" - {tool}")
print(f"\nThese must be published as '{user_slug}/<name>' before the collection.")
print("\nNOTE: Only PUBLIC, APPROVED tools can be included in collections.")
print("\nOptions:")
print(" [1] Publish all tools now, then collection (recommended)")
print(" [2] Remove unpublished tools and publish remaining")
print(" [3] Cancel")
choice = input("\nChoice [1/2/3]: ").strip()
if choice == "1":
# Publish tools first
pending_tools = []
for tool_name in unpublished:
print(f"\nPublishing tool: {tool_name}...")
result = _publish_single_tool(tool_name, client)
if result.get("pending"):
pending_tools.append(tool_name)
print(f" -> Submitted for review (pending approval)")
elif result.get("success"):
print(f" -> Published as {user_slug}/{tool_name}")
else:
print(f" -> Failed: {result.get('error')}")
return 1
if pending_tools:
# Save pending state
collection.pending_approval = True
collection.pending_tools = pending_tools
collection.maintainer = user_slug
collection.save()
print(f"\nSome tools are pending approval:")
for tool in pending_tools:
print(f" - {user_slug}/{tool}")
print(f"\nCollection will be published once tools are approved.")
print(f"Check status with: cmdforge collections status {collection.name}")
print(f"Resume with: cmdforge collections publish {collection.name} --continue")
return 0
# All tools approved immediately (or auto-approved)
# Fall through to publish collection
elif choice == "2":
# Remove unpublished tools from refs and pinned
unpublished_full_refs = {f"{user_slug}/{u}" for u in unpublished}
registry_refs = [ref for ref in registry_refs if ref not in unpublished_full_refs]
# Also remove from transformed_pinned to avoid orphaned keys
for u in unpublished:
full_ref = f"{user_slug}/{u}"
transformed_pinned.pop(full_ref, None)
if not registry_refs:
print("Error: No tools remaining after removal")
return 1
# Update local collection to match what we're publishing
collection.tools = [t for t in collection.tools if t not in unpublished]
for u in unpublished:
collection.pinned.pop(u, None)
collection.save()
print(f"\nProceeding with {len(registry_refs)} tool(s)")
else:
print("Cancelled")
return 0
# Publish collection (using transformed pinned keys)
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
# 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"}
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"
defaults = defaults_path.read_text() if defaults_path.exists() else ""
try:
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 (use same config_path from above)
config_hash = result.get("config_hash")
moderation_status = result.get("status", "pending")
if config_hash and config_path.exists():
try:
config_data = yaml.safe_load(config_path.read_text()) or {}
config_data["registry_hash"] = config_hash
config_data["registry_status"] = moderation_status
registry_owner = result.get("owner")
if registry_owner:
config_data["registry_owner"] = registry_owner
# Clear any old feedback when republishing
if "registry_feedback" in config_data:
del config_data["registry_feedback"]
config_path.write_text(yaml.dump(config_data, default_flow_style=False, sort_keys=False))
except Exception:
pass # Non-critical
# Check if pending moderation
is_pending = moderation_status == "pending" or result.get("pr_url")
return {
"success": True,
"pending": is_pending,
"status": moderation_status,
"config_hash": config_hash,
"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)}
def _resume_collection_publish(collection, client, user_slug) -> int:
"""Resume publishing a collection after tools are approved."""
from ..registry_client import RegistryError
print(f"Checking status of pending tools...")
still_pending = []
for tool_name in collection.pending_tools:
try:
# Use authenticated endpoint to check user's own tools
status_info = client.get_my_tool_status(tool_name)
status = status_info.get("status", "pending")
if status != "approved":
still_pending.append(tool_name)
print(f" {user_slug}/{tool_name}: {status}")
else:
print(f" {user_slug}/{tool_name}: approved")
except RegistryError:
still_pending.append(tool_name)
print(f" {user_slug}/{tool_name}: not found")
if still_pending:
collection.pending_tools = still_pending
collection.save()
print(f"\nStill waiting for {len(still_pending)} tool(s) to be approved")
return 0
# All approved - publish collection
print(f"\nAll tools approved! Publishing collection...")
# Rebuild registry refs AND transform pinned keys
registry_refs = []
transformed_pinned = {}
for tool_ref in collection.tools:
if "/" in tool_ref:
registry_refs.append(tool_ref)
# Copy pinned if exists
if tool_ref in collection.pinned:
transformed_pinned[tool_ref] = collection.pinned[tool_ref]
else:
full_ref = f"{user_slug}/{tool_ref}"
registry_refs.append(full_ref)
# Transform pinned key from local name to owner/name
if tool_ref in collection.pinned:
transformed_pinned[full_ref] = collection.pinned[tool_ref]
return _publish_collection_to_registry(collection, registry_refs, transformed_pinned, client, user_slug)
def _publish_collection_to_registry(collection, registry_refs, transformed_pinned, client, user_slug) -> int:
"""Actually publish the collection to registry."""
print(f"\nPublishing collection '{collection.display_name}'...")
payload = {
"name": collection.name,
"display_name": collection.display_name,
"description": collection.description,
"maintainer": user_slug,
"tools": registry_refs,
"pinned": transformed_pinned,
"tags": collection.tags,
}
try:
result = client.publish_collection(payload)
# Update local state
collection.published = True
collection.registry_name = collection.name
collection.pending_approval = False
collection.pending_tools = []
collection.maintainer = user_slug
collection.save()
print(f"\nCollection published: {collection.name}")
print(f" View at: {collection.get_registry_url()}")
return 0
except Exception as e:
print(f"\nError publishing collection: {e}")
return 1
def cmd_collections_status(args):
"""Check status of a pending collection publish."""
from ..collection import get_collection
from ..registry_client import get_client, RegistryError
collection = get_collection(args.name)
if not collection:
print(f"Error: Collection '{args.name}' not found")
return 1
if not collection.pending_approval:
if collection.published:
print(f"Collection '{collection.name}' is published")
else:
print(f"Collection '{collection.name}' is local (not published)")
return 0
# Check pending tools using authenticated endpoint
client = get_client()
# Validate auth first
is_valid, error = client.validate_token()
if not is_valid:
print(f"Error: Not authenticated. Run: cmdforge config connect")
return 1
user_slug = collection.maintainer
print(f"\nCollection: {collection.display_name}")
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
status_info = client.get_my_tool_status(tool_name)
status = status_info.get("status", "pending")
if status == "approved":
print(f" {user_slug}/{tool_name}: approved")
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
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...")
return 0