Fix lint errors in snip/cli/commands.py - remove unused variables and imports
This commit is contained in:
@@ -1,476 +1,569 @@
|
|||||||
"""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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user