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