From 9d89a5f48caa38f37f484c8da190125d013cf011 Mon Sep 17 00:00:00 2001 From: Felix Rudat Date: Tue, 27 Jan 2026 14:38:39 +0100 Subject: [PATCH] Add 'show' command to display card details including attachments and comments --- Makefile | 2 +- README.md | 1 + SKILL.md | 8 ++ scripts/planka_cli.py | 177 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bc73273..0ba17fd 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ run: uv run python scripts/planka_cli.py status build: - uv run pyinstaller --onefile --name planka-cli --collect-all plankapy scripts/planka_cli.py + uv run pyinstaller --onefile --name planka-cli --collect-all plankapy --collect-submodules rich._unicode_data scripts/planka_cli.py smoke: build @set -e; \ diff --git a/README.md b/README.md index fd8a0e3..d0a9fa5 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ planka-cli projects list planka-cli boards list [PROJECT_ID] planka-cli lists list planka-cli cards list +planka-cli cards show planka-cli cards create "Card title" --description "Details" planka-cli cards update --name "New title" diff --git a/SKILL.md b/SKILL.md index 29b47f8..3d50cf1 100644 --- a/SKILL.md +++ b/SKILL.md @@ -54,6 +54,9 @@ planka-cli lists list # List Cards in a List planka-cli cards list +# Show a Card (includes attachments with URLs and comment text) +planka-cli cards show + # Create a Card planka-cli cards create "Card title" @@ -82,6 +85,11 @@ planka-cli boards list planka-cli cards list 1619901252164912136 ``` +**Show card details for card ID 1619901252164912137:** +```bash +planka-cli cards show 1619901252164912137 +``` + **Create a card in list ID 1619901252164912136:** ```bash planka-cli cards create 1619901252164912136 "Ship CLI" diff --git a/scripts/planka_cli.py b/scripts/planka_cli.py index 9ba0159..000bd38 100644 --- a/scripts/planka_cli.py +++ b/scripts/planka_cli.py @@ -325,6 +325,183 @@ def list_cards(list_id: str): except Exception as e: console.print(f"[bold red]Error:[/bold red] {e}") +@cards_app.command("show") +def show_card(card_id: str): + """Show details for a card.""" + planka = get_planka() + try: + card = get_card_by_id(planka, card_id) + if not card: + console.print(f"[red]Card {card_id} not found.[/red]") + return + + table = make_table(f"Card: {card.name}") + table.add_column("Field", style="cyan", no_wrap=True) + table.add_column("Value", style="magenta") + + def safe_attr(obj: object, name: str) -> object: + try: + return getattr(obj, name) + except Exception: + return None + + def normalize_url(base_url: Optional[str], value: Optional[str]) -> Optional[str]: + if not value: + return None + candidate = str(value).strip() + if not candidate: + return None + if candidate.startswith("http://") or candidate.startswith("https://"): + return candidate + if base_url: + return f"{base_url.rstrip('/')}/{candidate.lstrip('/')}" + return candidate + + def extract_attachment_url(attachment: object, base_url: Optional[str]) -> Optional[str]: + data = safe_attr(attachment, "data") + if isinstance(data, dict): + for key in ("url", "downloadUrl", "download_url", "link", "href", "path"): + value = data.get(key) + if isinstance(value, str) and value.strip(): + return normalize_url(base_url, value) + file_info = data.get("file") + if isinstance(file_info, dict): + for key in ( + "url", + "downloadUrl", + "download_url", + "path", + "thumbnailUrl", + "thumbUrl", + "thumbnail_url", + ): + value = file_info.get(key) + if isinstance(value, str) and value.strip(): + return normalize_url(base_url, value) + direct_url = safe_attr(attachment, "url") + if isinstance(direct_url, str) and direct_url.strip(): + return normalize_url(base_url, direct_url) + return None + + def add_row(label: str, value: object) -> None: + if value is None or value == "": + table.add_row(label, "-") + else: + table.add_row(label, str(value)) + + try: + schema = card.schema or {} + except Exception: + schema = {} + list_id_value = schema.get("listId") + list_name_value = None + list_obj = safe_attr(card, "list") + if list_obj is not None: + list_id_value = safe_attr(list_obj, "id") or list_id_value + list_name_value = safe_attr(list_obj, "name") + + list_display = "-" + if list_name_value and list_id_value: + list_display = f"{list_name_value} ({list_id_value})" + elif list_name_value: + list_display = list_name_value + elif list_id_value: + list_display = list_id_value + + due_date = safe_attr(card, "due_date") or schema.get("dueDate") + due_completed = safe_attr(card, "due_date_completed") + if due_completed is None: + due_completed = schema.get("isDueCompleted") + if isinstance(due_completed, bool): + due_completed = "yes" if due_completed else "no" + + attachments_error = None + comments_error = None + try: + attachments = list(safe_attr(card, "attachments") or []) + except Exception as exc: + attachments = [] + attachments_error = str(exc) + try: + comments = list(safe_attr(card, "comments") or []) + except Exception as exc: + comments = [] + comments_error = str(exc) + + comments_count = safe_attr(card, "comments_count") + if comments_count is None and comments_error is None: + comments_count = len(comments) + + add_row("ID", card.id) + add_row("Name", card.name) + add_row("Description", safe_attr(card, "description") or schema.get("description")) + add_row("List", list_display) + add_row("Position", safe_attr(card, "position") or schema.get("position")) + add_row("Type", safe_attr(card, "type") or schema.get("type")) + add_row("Due Date", due_date) + add_row("Due Completed", due_completed) + if attachments_error: + add_row("Attachments", f"Error: {attachments_error}") + else: + add_row("Attachments", len(attachments)) + if comments_error: + add_row("Comments", f"Error: {comments_error}") + else: + add_row("Comments", comments_count) + add_row("Created At", safe_attr(card, "created_at") or schema.get("createdAt")) + add_row("Updated At", safe_attr(card, "updated_at") or schema.get("updatedAt")) + + console.print(table) + + planka_url, _, _ = get_env_config() + if attachments and attachments_error is None: + attachments_table = make_table("Attachments") + attachments_table.add_column("ID", justify="right", style="cyan", no_wrap=True) + attachments_table.add_column("Name", style="magenta") + attachments_table.add_column("Type", style="magenta") + attachments_table.add_column("URL", style="magenta") + attachments_table.add_column("Created At", justify="right") + + for attachment in attachments: + attachment_url = extract_attachment_url(attachment, planka_url) + attachments_table.add_row( + str(safe_attr(attachment, "id") or "-"), + str(safe_attr(attachment, "name") or "-"), + str(safe_attr(attachment, "type") or "-"), + str(attachment_url or "-"), + str(safe_attr(attachment, "created_at") or "-"), + ) + + console.print(attachments_table) + + if comments and comments_error is None: + comments_table = make_table("Comments") + comments_table.add_column("ID", justify="right", style="cyan", no_wrap=True) + comments_table.add_column("User", style="magenta") + comments_table.add_column("Text", style="magenta") + comments_table.add_column("Created At", justify="right") + + for comment in comments: + user = safe_attr(comment, "user") + user_label = ( + safe_attr(user, "name") + or safe_attr(user, "username") + or safe_attr(user, "id") + or "-" + ) + text = safe_attr(comment, "text") + text_label = " ".join(str(text).split()) if text else "-" + comments_table.add_row( + str(safe_attr(comment, "id") or "-"), + str(user_label), + text_label, + str(safe_attr(comment, "created_at") or "-"), + ) + + console.print(comments_table) + except Exception as e: + console.print(f"[bold red]Error:[/bold red] {e}") + @cards_app.command("create") def create_card( list_id: str = typer.Argument(..., help="List ID to create the card in"),