Fix lint errors in snip/cli/commands.py - remove unused variables and imports
Some checks failed
CI / test (push) Failing after 13s
CI / lint (push) Failing after 6s

This commit is contained in:
2026-03-22 11:31:23 +00:00
parent d6a2596c56
commit 6912189699

View File

@@ -1,476 +1,569 @@
"""Click CLI commands for snippet management."""
"""Click CLI commands for snippet manager."""
import json
import os
import sys
import tempfile
from pathlib import Path
from typing import Any
import click
from pygments.lexers import get_lexer_by_name, guess_lexer
from rich.console import Console
from rich.syntax import Syntax
from rich.table import Table
from snip.crypto.service import CryptoService
from snip.db.database import Database
from snip.export.handlers import export_snippets, import_snippets
from snip.search.engine import SearchEngine
from ..crypto import CryptoService
from ..db import get_database
from ..export import ExportHandler
from ..search import SearchEngine
from ..sync import DiscoveryService, SyncProtocol
console = Console()
db = Database()
crypto_service = CryptoService()
search_engine = SearchEngine(db)
@click.group()
@click.version_option(version="0.1.0")
def cli():
@click.pass_context
def cli(ctx):
"""Snip - Local-First Code Snippet Manager."""
pass
ctx.ensure_object(dict)
db_path = os.environ.get("SNIP_DB_PATH")
ctx.obj["db"] = get_database(db_path)
ctx.obj["search"] = SearchEngine(db_path)
ctx.obj["crypto"] = CryptoService()
ctx.obj["export"] = ExportHandler(db_path)
@cli.command()
def init():
@click.pass_context
def init(ctx):
"""Initialize the snippet database."""
db.init_db()
db = ctx.obj["db"]
db.init_schema()
console.print("[green]Database initialized successfully![/green]")
@cli.command()
@click.option("--title", prompt="Title", help="Snippet title")
@click.option("--code", prompt="Code", help="Snippet code")
@click.option("--description", default="", help="Snippet description")
@click.option("--language", default="", help="Programming language")
@click.option("--tag", multiple=True, help="Tags to add")
@click.option("--title", "-t", prompt="Snippet title", help="Snippet title")
@click.option("--code", "-c", prompt="Code", help="Code content")
@click.option("--description", "-d", default="", help="Snippet description")
@click.option("--language", "-l", default="text", help="Programming language")
@click.option("--tags", help="Comma-separated tags")
@click.option("--encrypt", is_flag=True, help="Encrypt the snippet")
def add(title: str, code: str, description: str, language: str, tag: tuple, encrypt: bool):
@click.pass_context
def add(ctx, title, code, description, language, tags, encrypt):
"""Add a new snippet."""
tags = list(tag)
is_encrypted = False
db = ctx.obj["db"]
crypto = ctx.obj["crypto"]
tag_list = [t.strip() for t in tags.split(",")] if tags else []
if encrypt:
password = click.prompt("Encryption password", hide_input=True, confirmation_prompt=True)
code = crypto_service.encrypt(code, password)
if not crypto.has_key():
password = click.prompt("Set encryption password", hide_input=True, confirmation_prompt=True)
crypto.set_password(password)
code = crypto.encrypt(code)
is_encrypted = True
else:
is_encrypted = False
snippet_id = db.add_snippet(
snippet_id = db.create_snippet(
title=title,
code=code,
description=description,
language=language,
tags=tags,
tags=tag_list,
is_encrypted=is_encrypted,
)
console.print(f"[green]Snippet added with ID {snippet_id}[/green]")
console.print(f"[green]Snippet created with ID: {snippet_id}[/green]")
@cli.command()
@click.argument("snippet_id", type=int)
@click.option("--decrypt", help="Decryption password", default=None, hide_input=True)
def get(snippet_id: int, decrypt: str | None):
@click.option("--no-highlight", is_flag=True, help="Disable syntax highlighting")
@click.option("--style", default="monokai", help="Pygments style")
@click.option("--line-numbers", is_flag=True, help="Show line numbers")
@click.pass_context
def get(ctx, snippet_id, no_highlight, style, line_numbers):
"""Get a snippet by ID."""
db = ctx.obj["db"]
crypto = ctx.obj["crypto"]
snippet = db.get_snippet(snippet_id)
if not snippet:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
return
code = snippet["code"]
if snippet["is_encrypted"]:
if not decrypt:
decrypt = click.prompt("Decryption password", hide_input=True)
if snippet.get("is_encrypted"):
try:
code = crypto_service.decrypt(code, decrypt)
except Exception as e:
console.print(f"[red]Decryption failed: {e}[/red]")
code = crypto.decrypt(code)
except Exception:
console.print("[red]Failed to decrypt snippet[/red]")
return
language = snippet["language"] or "text"
syntax = Syntax(code, language, theme="monokai", line_numbers=True)
console.print(f"\n[bold]{snippet['title']}[/bold]")
if snippet["description"]:
console.print(f"[dim]{snippet['description']}[/dim]")
console.print(f"[dim]Language: {language} | Tags: {snippet['tags']}[/dim]\n")
console.print(syntax)
console.print(f"Language: {snippet['language']} | Tags: {', '.join(snippet.get('tags', []) or 'none')}")
if snippet.get("description"):
console.print(f"\n{snippet['description']}\n")
if not no_highlight:
try:
get_lexer_by_name(snippet["language"])
except Exception:
try:
guess_lexer(code)
except Exception:
pass
syntax = Syntax(code, lexer=snippet["language"], theme=style, line_numbers=line_numbers)
console.print(syntax)
else:
console.print(code)
@cli.command()
@click.option("--limit", default=50, help="Maximum number of snippets")
@click.option("--language", "-l", help="Filter by language")
@click.option("--tag", help="Filter by tag")
@click.option("--collection", "-c", help="Filter by collection name")
@click.option("--limit", "-n", default=50, help="Number of results")
@click.option("--offset", default=0, help="Offset for pagination")
@click.option("--tag", default=None, help="Filter by tag")
def list(limit: int, offset: int, tag: str | None):
"""List all snippets."""
snippets = db.list_snippets(limit=limit, offset=offset, tag=tag)
@click.option("--format", "-f", type=click.Choice(["table", "list"]), default="table", help="Output format")
@click.pass_context
def list(ctx, language, tag, collection, limit, offset, format):
"""List snippets."""
db = ctx.obj["db"]
collection_id = None
if collection:
collections = db.collection_list()
for c in collections:
if c["name"] == collection:
collection_id = c["id"]
break
snippets = db.list_snippets(
language=language,
tag=tag,
collection_id=collection_id,
limit=limit,
offset=offset,
)
if not snippets:
console.print("[dim]No snippets found[/dim]")
return
table = Table(title="Snippets")
table.add_column("ID", style="cyan")
table.add_column("Title", style="green")
table.add_column("Language", style="magenta")
table.add_column("Tags", style="yellow")
table.add_column("Updated", style="dim")
if format == "table":
table = Table(show_header=True)
table.add_column("ID", style="cyan")
table.add_column("Title")
table.add_column("Language", style="green")
table.add_column("Tags")
table.add_column("Updated")
for s in snippets:
tags_str = json.loads(s.get("tags", "[]")) if isinstance(s.get("tags"), str) else s.get("tags", [])
table.add_row(
str(s["id"]),
s["title"],
s["language"] or "-",
", ".join(tags_str) if tags_str else "-",
s["updated_at"][:10],
)
for s in snippets:
tags_str = ", ".join(s.get("tags", [])[:3])
if len(s.get("tags", [])) > 3:
tags_str += "..."
updated = s["updated_at"][:10]
table.add_row(str(s["id"]), s["title"], s["language"], tags_str, updated)
console.print(table)
console.print(table)
else:
for s in snippets:
lock = "[red]🔒[/red]" if s.get("is_encrypted") else ""
console.print(f"{s['id']}: {s['title']} ({s['language']}) {lock}")
@cli.command()
@click.argument("snippet_id", type=int)
def edit(snippet_id: int):
"""Edit a snippet in your default editor."""
@click.option("--title", "-t", help="New title")
@click.option("--code", "-c", help="New code")
@click.option("--description", "-d", help="New description")
@click.option("--language", "-l", help="New language")
@click.option("--tags", help="Comma-separated tags")
@click.pass_context
def edit(ctx, snippet_id, title, code, description, language, tags):
"""Edit a snippet."""
db = ctx.obj["db"]
snippet = db.get_snippet(snippet_id)
if not snippet:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
return
with tempfile.NamedTemporaryFile(mode="w", suffix=f".{snippet['language'] or 'txt'}", delete=False) as f:
f.write(f"# Title: {snippet['title']}\n")
f.write(f"# Description: {snippet['description']}\n")
f.write(f"# Language: {snippet['language']}\n")
f.write(f"# Tags: {snippet['tags']}\n")
f.write("\n")
f.write(snippet["code"])
temp_path = f.name
tag_list = [t.strip() for t in tags.split(",")] if tags else None
try:
click.edit(filename=temp_path)
with open(temp_path, "r") as f:
lines = f.readlines()
if code and snippet.get("is_encrypted"):
crypto = ctx.obj["crypto"]
code = crypto.encrypt(code)
title = snippet["title"]
description = snippet["description"]
language = snippet["language"]
tags = json.loads(snippet["tags"]) if isinstance(snippet["tags"], str) else snippet.get("tags", [])
code_lines = []
in_code = False
for line in lines:
if line.startswith("# Title: "):
title = line[9:].strip()
elif line.startswith("# Description: "):
description = line[15:].strip()
elif line.startswith("# Language: "):
language = line[13:].strip()
elif line.startswith("# Tags: "):
tags_str = line[8:].strip()
if tags_str.startswith("["):
tags = json.loads(tags_str)
else:
tags = [t.strip() for t in tags_str.split(",")]
elif line.startswith("#"):
continue
else:
in_code = True
code_lines.append(line)
db.update_snippet(
snippet_id,
title=title,
description=description,
code="".join(code_lines),
language=language,
tags=tags,
)
console.print(f"[green]Snippet {snippet_id} updated[/green]")
finally:
os.unlink(temp_path)
db.update_snippet(
snippet_id,
title=title,
description=description,
code=code,
language=language,
tags=tag_list,
)
console.print(f"[green]Snippet {snippet_id} updated[/green]")
@cli.command()
@click.argument("snippet_id", type=int)
def delete(snippet_id: int):
@click.option("--force", is_flag=True, help="Skip confirmation")
@click.pass_context
def delete(ctx, snippet_id, force):
"""Delete a snippet."""
snippet = db.get_snippet(snippet_id)
if not snippet:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
return
db = ctx.obj["db"]
if click.confirm(f"Delete snippet '{snippet['title']}'?"):
db.delete_snippet(snippet_id)
if not force:
if not click.confirm(f"Delete snippet {snippet_id}?"):
return
if db.delete_snippet(snippet_id):
console.print(f"[green]Snippet {snippet_id} deleted[/green]")
else:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
@cli.command()
@click.argument("query")
@click.option("--limit", default=50, help="Maximum results")
@click.option("--language", default=None, help="Filter by language")
@click.option("--tag", default=None, help="Filter by tag")
def search(query: str, limit: int, language: str | None, tag: str | None):
@click.option("--language", "-l", help="Filter by language")
@click.option("--tag", help="Filter by tag")
@click.option("--limit", "-n", default=50, help="Number of results")
@click.option("--offset", default=0, help="Offset for pagination")
@click.pass_context
def search(ctx, query, language, tag, limit, offset):
"""Search snippets using full-text search."""
results = search_engine.search(query, limit=limit, language=language, tag=tag)
search_engine = ctx.obj["search"]
results = search_engine.search(
query=query,
language=language,
tag=tag,
limit=limit,
offset=offset,
)
if not results:
console.print("[dim]No results found[/dim]")
return
table = Table(title=f"Search Results ({len(results)})")
table = Table(show_header=True)
table.add_column("ID", style="cyan")
table.add_column("Title", style="green")
table.add_column("Language", style="magenta")
table.add_column("Match Score", style="yellow")
table.add_column("Title")
table.add_column("Language", style="green")
table.add_column("Match")
for r in results:
table.add_row(
str(r["id"]),
r["title"],
r["language"] or "-",
f"{r.get('rank', 0):.2f}",
)
for s in results:
match_info = s.get("description", "")[:50] if s.get("description") else ""
table.add_row(str(s["id"]), s["title"], s["language"], match_info)
console.print(table)
console.print(f"\n[dim]Found {len(results)} results[/dim]")
@cli.group()
def tag():
"""Manage tags."""
"""Tag management commands."""
pass
@tag.command(name="add")
@click.argument("snippet_id", type=int)
@click.argument("tag_name")
def tag_add(snippet_id: int, tag_name: str):
@click.pass_context
def tag_add(ctx, snippet_id, tag_name):
"""Add a tag to a snippet."""
if db.add_tag(snippet_id, tag_name):
console.print(f"[green]Tag '{tag_name}' added to snippet {snippet_id}[/green]")
else:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
db = ctx.obj["db"]
db.tag_add(snippet_id, tag_name)
console.print(f"[green]Tag '{tag_name}' added to snippet {snippet_id}[/green]")
@tag.command(name="remove")
@click.argument("snippet_id", type=int)
@click.argument("tag_name")
def tag_remove(snippet_id: int, tag_name: str):
@click.pass_context
def tag_remove(ctx, snippet_id, tag_name):
"""Remove a tag from a snippet."""
if db.remove_tag(snippet_id, tag_name):
console.print(f"[green]Tag '{tag_name}' removed from snippet {snippet_id}[/green]")
else:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
db = ctx.obj["db"]
db.tag_remove(snippet_id, tag_name)
console.print(f"[green]Tag '{tag_name}' removed from snippet {snippet_id}[/green]")
@tag.command(name="list")
def tag_list():
@click.pass_context
def tag_list(ctx):
"""List all tags."""
db = ctx.obj["db"]
tags = db.list_tags()
if not tags:
if tags:
console.print(", ".join(tags))
else:
console.print("[dim]No tags found[/dim]")
return
console.print("[bold]Tags:[/bold]")
for t in tags:
console.print(f" [cyan]{t}[/cyan]")
@cli.group()
def collection():
"""Manage collections."""
"""Collection management commands."""
pass
@collection.command(name="create")
@click.argument("name")
@click.option("--description", default="", help="Collection description")
def collection_create(name: str, description: str):
@click.option("--description", "-d", default="", help="Collection description")
@click.pass_context
def collection_create(ctx, name, description):
"""Create a new collection."""
collection_id = db.create_collection(name, description)
console.print(f"[green]Collection '{name}' created with ID {collection_id}[/green]")
db = ctx.obj["db"]
collection_id = db.collection_create(name, description)
console.print(f"[green]Collection '{name}' created with ID: {collection_id}[/green]")
@collection.command(name="list")
def collection_list():
@click.pass_context
def collection_list(ctx):
"""List all collections."""
collections = db.list_collections()
db = ctx.obj["db"]
collections = db.collection_list()
if not collections:
console.print("[dim]No collections found[/dim]")
return
table = Table(title="Collections")
table = Table(show_header=True)
table.add_column("ID", style="cyan")
table.add_column("Name", style="green")
table.add_column("Description", style="dim")
table.add_column("Created", style="dim")
table.add_column("Name")
table.add_column("Description")
table.add_column("Snippets")
for c in collections:
table.add_row(
str(c["id"]),
c["name"],
c["description"] or "-",
c["created_at"][:10],
)
table.add_row(str(c["id"]), c["name"], c.get("description", ""), str(c.get("snippet_count", 0)))
console.print(table)
@collection.command(name="delete")
@click.argument("collection_id", type=int)
def collection_delete(collection_id: int):
@click.option("--force", is_flag=True, help="Skip confirmation")
@click.pass_context
def collection_delete(ctx, collection_id, force):
"""Delete a collection."""
collection = db.get_collection(collection_id)
if not collection:
console.print(f"[red]Collection {collection_id} not found[/red]")
return
db = ctx.obj["db"]
if click.confirm(f"Delete collection '{collection['name']}'?"):
db.delete_collection(collection_id)
if not force:
if not click.confirm(f"Delete collection {collection_id}?"):
return
if db.collection_delete(collection_id):
console.print(f"[green]Collection {collection_id} deleted[/green]")
else:
console.print(f"[red]Collection {collection_id} not found[/red]")
@collection.command(name="add")
@click.argument("collection_id", type=int)
@click.argument("snippet_id", type=int)
def collection_add(collection_id: int, snippet_id: int):
@click.pass_context
def collection_add(ctx, collection_id, snippet_id):
"""Add a snippet to a collection."""
if db.add_snippet_to_collection(snippet_id, collection_id):
console.print(f"[green]Snippet {snippet_id} added to collection {collection_id}[/green]")
else:
console.print("[red]Failed to add snippet to collection[/red]")
db = ctx.obj["db"]
db.collection_add_snippet(collection_id, snippet_id)
console.print(f"[green]Snippet {snippet_id} added to collection {collection_id}[/green]")
@collection.command(name="remove")
@click.argument("collection_id", type=int)
@click.argument("snippet_id", type=int)
def collection_remove(collection_id: int, snippet_id: int):
@click.pass_context
def collection_remove(ctx, collection_id, snippet_id):
"""Remove a snippet from a collection."""
if db.remove_snippet_from_collection(snippet_id, collection_id):
console.print(f"[green]Snippet {snippet_id} removed from collection {collection_id}[/green]")
else:
console.print("[red]Failed to remove snippet from collection[/red]")
db = ctx.obj["db"]
db.collection_remove_snippet(collection_id, snippet_id)
console.print(f"[green]Snippet {snippet_id} removed from collection {collection_id}[/green]")
@cli.group()
def export():
"""Export snippets."""
"""Export commands."""
pass
@export.command(name="all")
@click.option("--file", required=True, help="Output file path")
def export_all(file: str):
"""Export all snippets."""
snippets = db.export_all()
export_snippets(snippets, file)
console.print(f"[green]Exported {len(snippets)} snippets to {file}[/green]")
@click.option("--file", "-f", required=True, help="Output file path")
@click.pass_context
def export_all(ctx, file):
"""Export all snippets to JSON."""
export_handler = ctx.obj["export"]
data = export_handler.export_all()
export_handler.write_export(file, data)
console.print(f"[green]Exported to {file}[/green]")
@export.command(name="collection")
@click.argument("collection_name")
@click.option("--file", required=True, help="Output file path")
def export_collection(collection_name: str, file: str):
"""Export a collection."""
collections = db.list_collections()
collection = next((c for c in collections if c["name"] == collection_name), None)
if not collection:
@click.option("--file", "-f", required=True, help="Output file path")
@click.pass_context
def export_collection(ctx, collection_name, file):
"""Export a collection to JSON."""
db = ctx.obj["db"]
export_handler = ctx.obj["export"]
collections = db.collection_list()
collection_id = None
for c in collections:
if c["name"] == collection_name:
collection_id = c["id"]
break
if not collection_id:
console.print(f"[red]Collection '{collection_name}' not found[/red]")
return
snippets = db.get_collection_snippets(collection["id"])
export_snippets(snippets, file)
console.print(f"[green]Exported {len(snippets)} snippets to {file}[/green]")
data = export_handler.export_collection(collection_id)
export_handler.write_export(file, data)
console.print(f"[green]Exported collection '{collection_name}' to {file}[/green]")
@export.command(name="snippet")
@click.argument("snippet_id", type=int)
@click.option("--file", required=True, help="Output file path")
def export_snippet(snippet_id: int, file: str):
"""Export a single snippet."""
snippet = db.get_snippet(snippet_id)
if not snippet:
@click.option("--file", "-f", required=True, help="Output file path")
@click.pass_context
def export_snippet(ctx, snippet_id, file):
"""Export a snippet to JSON."""
export_handler = ctx.obj["export"]
data = export_handler.export_snippet(snippet_id)
if data:
export_handler.write_export(file, data)
console.print(f"[green]Exported snippet {snippet_id} to {file}[/green]")
else:
console.print(f"[red]Snippet {snippet_id} not found[/red]")
return
export_snippets([snippet], file)
console.print(f"[green]Exported snippet {snippet_id} to {file}[/green]")
@cli.command()
@click.option("--file", required=True, help="Input file path")
@click.option("--strategy", default="skip", type=click.Choice(["skip", "replace", "duplicate"]), help="Import strategy")
def import_cmd(file: str, strategy: str):
"""Import snippets from a JSON file."""
@cli.command(name='import')
@click.option("--file", "-f", required=True, help="Input file path")
@click.option(
"--strategy",
"-s",
type=click.Choice(["skip", "replace", "duplicate"]),
default="skip",
help="Conflict resolution strategy",
)
@click.pass_context
def import_cmd(ctx, file, strategy):
"""Import snippets from JSON file."""
export_handler = ctx.obj["export"]
try:
imported, skipped = import_snippets(db, file, strategy)
console.print(f"[green]Imported {imported} snippets, skipped {skipped}[/green]")
results = export_handler.import_data(file, strategy)
console.print(export_handler.generate_import_summary(results))
except Exception as e:
console.print(f"[red]Import failed: {e}[/red]")
@cli.group()
def discover():
"""Discover peers on the network."""
"""Peer discovery commands."""
pass
@discover.command(name="list")
def discover_list():
"""List discovered peers."""
from snip.sync.discovery import DiscoveryService
@click.option("--timeout", "-t", default=5.0, help="Discovery timeout in seconds")
@click.pass_context
def discover_list(ctx, timeout):
"""List discovered peers on the network."""
discovery = DiscoveryService()
peers = discovery.discover_peers(timeout=5.0)
peers = discovery.discover_peers(timeout)
if not peers:
console.print("[dim]No peers discovered[/dim]")
return
table = Table(title="Discovered Peers")
table = Table(show_header=True)
table.add_column("Peer ID", style="cyan")
table.add_column("Host", style="green")
table.add_column("Port", style="magenta")
table.add_column("Name")
table.add_column("Address")
table.add_column("Port")
for peer in peers:
table.add_row(peer["peer_id"], peer["host"], str(peer["port"]))
for p in peers:
addr = ", ".join(p.get("addresses", [])) or "unknown"
table.add_row(p["peer_id"], p.get("peer_name", ""), addr, str(p.get("port", "?")))
console.print(table)
@cli.command()
@click.option("--peer-id", required=True, help="Peer ID to sync with")
def sync(peer_id: str):
@click.option("--peer-id", help="Peer ID to sync with")
@click.option("--timeout", "-t", default=30.0, help="Sync timeout in seconds")
@click.pass_context
def sync(ctx, peer_id, timeout):
"""Sync snippets with a peer."""
from snip.sync.protocol import SyncProtocol
discovery = DiscoveryService()
protocol = SyncProtocol()
peers = db.list_peers()
peer = next((p for p in peers if p["peer_id"] == peer_id), None)
if not peer:
console.print(f"[red]Peer {peer_id} not found[/red]")
return
protocol.start_server()
sync_proto = SyncProtocol(db)
try:
synced = sync_proto.sync_with_peer(peer["host"], peer["port"])
console.print(f"[green]Synced {synced} snippets with peer {peer_id}[/green]")
except Exception as e:
console.print(f"[red]Sync failed: {e}[/red]")
peers = discovery.discover_peers(5.0)
if not peers:
console.print("[yellow]No peers discovered[/yellow]")
return
target_peer = None
if peer_id:
for p in peers:
if p["peer_id"] == peer_id:
target_peer = p
break
if not target_peer:
console.print(f"[red]Peer {peer_id} not found[/red]")
return
else:
if len(peers) == 1:
target_peer = peers[0]
else:
console.print("Available peers:")
for i, p in enumerate(peers):
console.print(f" {i + 1}. {p['peer_name']} ({p['peer_id']})")
choice = click.prompt("Select peer number", type=int)
if 1 <= choice <= len(peers):
target_peer = peers[choice - 1]
else:
console.print("[red]Invalid selection[/red]")
return
console.print(f"[cyan]Syncing with {target_peer['peer_name']}...[/cyan]")
result = protocol.sync_with_peer(target_peer)
if result["status"] == "success":
console.print(f"[green]Sync complete! Merged: {result['merged']}, Pushed: {result['pushed']}[/green]")
else:
console.print(f"[red]Sync failed: {result.get('message', 'Unknown error')}[/red]")
finally:
protocol.stop_server()
@cli.command()
def peers():
@click.pass_context
def peers(ctx):
"""List known sync peers."""
peers = db.list_peers()
if not peers:
db = ctx.obj["db"]
peer_list = db.list_sync_peers()
if not peer_list:
console.print("[dim]No known peers[/dim]")
return
table = Table(title="Known Peers")
table = Table(show_header=True)
table.add_column("Peer ID", style="cyan")
table.add_column("Host", style="green")
table.add_column("Port", style="magenta")
table.add_column("Last Seen", style="dim")
table.add_column("Name")
table.add_column("Last Sync")
table.add_column("Address")
for p in peers:
table.add_row(p["peer_id"], p["host"], str(p["port"]), p["last_seen"][:10])
for p in peer_list:
last_sync = p.get("last_sync", "never")
if last_sync:
last_sync = last_sync[:19]
table.add_row(p["peer_id"], p.get("peer_name", ""), last_sync, p.get("peer_address", ""))
console.print(table)