Add local collections support with GUI, CLI, and publishing workflow
Features: - Local collection definitions stored in ~/.cmdforge/collections/ - CLI commands: create, show, add, remove, delete, publish, status - GUI Collections page with local and registry tabs - Collection publishing with tool resolution and visibility checks - New API endpoints: GET /api/v1/me, GET /api/v1/tools/.../approved, POST /api/v1/collections - RegistryClient methods: get_me(), has_approved_public_tool(), publish_collection() Implementation: - collection.py: Collection dataclass, resolve_tool_references(), classify_tool_reference(), ToolResolutionResult - collections_page.py: GUI with background workers for install/publish - collections_commands.py: Full CLI command implementations - registry/app.py: New authenticated endpoints with validation Tests: - test_collection.py: 27 unit tests for collection module - test_collection_api.py: 20 tests (8 client, 12 API with Flask skip) Documentation updated: README, CHANGELOG, CLAUDE.md, tests/README Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Collections commands for CmdForge CLI."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from ..resolver import install_from_registry
|
||||
@@ -8,23 +9,88 @@ from ..resolver import install_from_registry
|
||||
|
||||
def cmd_collections(args):
|
||||
"""Handle collections subcommands."""
|
||||
if args.collections_cmd == "list":
|
||||
cmd = args.collections_cmd
|
||||
|
||||
# Existing commands (with --local flag support added)
|
||||
if cmd == "list":
|
||||
return _cmd_collections_list(args)
|
||||
elif args.collections_cmd == "info":
|
||||
elif cmd == "info":
|
||||
return _cmd_collections_info(args)
|
||||
elif args.collections_cmd == "install":
|
||||
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")
|
||||
print(" info <name> Show collection details")
|
||||
print(" install <name> Install all tools in a collection")
|
||||
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."""
|
||||
"""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:
|
||||
@@ -45,8 +111,9 @@ def _cmd_collections_list(args):
|
||||
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 = len(tools) if isinstance(tools, list) else 0
|
||||
tool_count = coll.get("tool_count", len(tools) if isinstance(tools, list) else 0)
|
||||
|
||||
print(f" {name}")
|
||||
print(f" {display_name}")
|
||||
@@ -206,3 +273,558 @@ def _cmd_collections_install(args):
|
||||
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 _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.
|
||||
"""
|
||||
from ..tool import load_tool, get_tools_dir
|
||||
import yaml
|
||||
|
||||
tool = load_tool(tool_name)
|
||||
if not tool:
|
||||
return {"success": False, "error": f"Tool '{tool_name}' not found"}
|
||||
|
||||
# Load README if exists
|
||||
readme_path = get_tools_dir() / tool_name / "README.md"
|
||||
readme = readme_path.read_text() if readme_path.exists() else ""
|
||||
|
||||
try:
|
||||
config_yaml = yaml.safe_dump(tool.to_dict(), sort_keys=False)
|
||||
result = client.publish_tool(config_yaml, readme=readme, dry_run=False)
|
||||
|
||||
# Update local tool config with registry metadata
|
||||
config_path = get_tools_dir() / tool_name / "config.yaml"
|
||||
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
|
||||
# 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")
|
||||
}
|
||||
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
|
||||
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
|
||||
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")
|
||||
else:
|
||||
print(f"\nWaiting for tool approvals...")
|
||||
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user