diff --git a/README.md b/README.md index 5c7f07d..6fc9b19 100644 --- a/README.md +++ b/README.md @@ -409,7 +409,7 @@ Example output: ``` ╔══════════════════════════════════════════════════════════╗ - ║ opencode-db — справка ║ + ║ opencode-db — справка ║ ╚══════════════════════════════════════════════════════════╝ Использование: opencode-db <команда> [флаги] diff --git a/cmd_costs.py b/cmd_costs.py index c7af77e..75fe996 100644 --- a/cmd_costs.py +++ b/cmd_costs.py @@ -3,13 +3,28 @@ Стоимость берётся напрямую из поля session.cost БД OpenCode. """ +import argparse + from db import SessionError, get_session_info, get_session_title, parse_model from i18n import _ -from utils import format_cost, format_tokens +from utils import build_help_epilog, format_cost, format_tokens + +_COSTS_EXAMPLES = [ + ("", "help.costs.e0"), + ("", "help.costs.e1"), + ("--total", "help.costs.e2"), + ("--project proj_xxx", "help.costs.e3"), + ("--json", "help.costs.e4"), +] def register(subparsers) -> None: - p = subparsers.add_parser("costs", help=_("help.cmd.costs")) + p = subparsers.add_parser( + "costs", + help=_("help.cmd.costs"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("costs", _COSTS_EXAMPLES), + ) p.add_argument("session_id", nargs="?", help="Session ID") p.add_argument("--project", type=str, help="Filter by project") p.add_argument("--total", action="store_true", help="Total across all sessions") diff --git a/cmd_delete.py b/cmd_delete.py index fbbeb2b..58da6fd 100644 --- a/cmd_delete.py +++ b/cmd_delete.py @@ -1,5 +1,7 @@ """Команда delete: удаление сессии.""" +import argparse + from db import ( SessionError, get_message_count, @@ -9,11 +11,22 @@ parse_model, ) from i18n import _ -from utils import confirm, format_cost, format_ts +from utils import build_help_epilog, confirm, format_cost, format_ts + +_DELETE_EXAMPLES = [ + ("", "help.delete.e0"), + (" --dry-run", "help.delete.e1"), + (" --force", "help.delete.e2"), +] def register(subparsers) -> None: - p = subparsers.add_parser("delete", help=_("help.cmd.delete")) + p = subparsers.add_parser( + "delete", + help=_("help.cmd.delete"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("delete", _DELETE_EXAMPLES), + ) p.add_argument("session_id", help="Session ID to delete") p.add_argument("--dry-run", action="store_true", help="Preview without deleting") p.add_argument("--force", "-f", action="store_true", help="Skip confirmation") diff --git a/cmd_export.py b/cmd_export.py index be2ac64..acf9c4c 100644 --- a/cmd_export.py +++ b/cmd_export.py @@ -6,6 +6,7 @@ - export (без аргументов) — интерактивный выбор из списка """ +import argparse import os from db import ( @@ -19,11 +20,25 @@ ) from formatters import collect_snapshot, format_part_to_md, update_log from i18n import _ -from utils import format_cost, format_ts, format_ts_short +from utils import build_help_epilog, format_cost, format_ts, format_ts_short + +_EXPORT_EXAMPLES = [ + ("", "help.export.e0"), + ("--latest", "help.export.e1"), + ("", "help.export.e2"), + ("--full", "help.export.e3"), + ("--force", "help.export.e4"), + ('--note "text"', "help.export.e5"), +] def register(subparsers) -> None: - p = subparsers.add_parser("export", help=_("help.cmd.export")) + p = subparsers.add_parser( + "export", + help=_("help.cmd.export"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("export", _EXPORT_EXAMPLES), + ) p.add_argument( "session_id", nargs="?", diff --git a/cmd_help.py b/cmd_help.py index e78fc17..0b294ed 100644 --- a/cmd_help.py +++ b/cmd_help.py @@ -1,17 +1,37 @@ """Команда help: подробная справка по opencode-db.""" +import argparse from typing import Literal from i18n import _ +from utils import build_help_epilog + +_HELP_EXAMPLES = [ + ("", "help.help.e0"), + ("", "help.help.e1"), +] def register(subparsers) -> None: - p = subparsers.add_parser("help", help=_("help.cmd.help")) + p = subparsers.add_parser( + "help", + help=_("help.cmd.help"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("help", _HELP_EXAMPLES), + ) p.add_argument("topic", nargs="?", help="Command name") p.add_argument("--json", action="store_true", help="JSON output") _COMMANDS = [ + ( + "help", + "help.cmd.help", + [ + ("", "help.help.e0"), + ("", "help.help.e1"), + ], + ), ( "list", "help.cmd.list", @@ -173,8 +193,14 @@ def _print_command_help(cmd_name) -> Literal[1] | Literal[0]: desc_key, examples = entry print() - print(_("help.cmd_header", cmd=cmd_name, desc=_(desc_key))) + print(f" {'─' * 55}") + print(f" {_('help.cmd_header', cmd=cmd_name, desc=_(desc_key))}") + print(f" {'─' * 55}") + print() + print(f" {_('help.flags_header', cmd=cmd_name)}") + print(f" opencode-db {cmd_name} --help") print() + print(f" {_('help.usage_header', cmd=cmd_name)}") for flag, example_key in examples: example_desc = _(example_key) if flag: diff --git a/cmd_info.py b/cmd_info.py index 41f492b..ae30ab1 100644 --- a/cmd_info.py +++ b/cmd_info.py @@ -1,5 +1,7 @@ """Команда info: детальная информация о сессии.""" +import argparse + from db import ( SessionError, get_children_sessions, @@ -13,11 +15,21 @@ parse_model, ) from i18n import _ -from utils import format_cost, format_tokens, format_ts +from utils import build_help_epilog, format_cost, format_tokens, format_ts + +_INFO_EXAMPLES = [ + ("", "help.info.e0"), + (" --json", "help.info.e1"), +] def register(subparsers) -> None: - p = subparsers.add_parser("info", help=_("help.cmd.info")) + p = subparsers.add_parser( + "info", + help=_("help.cmd.info"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("info", _INFO_EXAMPLES), + ) p.add_argument("session_id", help="Session ID") p.add_argument("--json", action="store_true", help="JSON output") diff --git a/cmd_list.py b/cmd_list.py index 9381eb2..292cab1 100644 --- a/cmd_list.py +++ b/cmd_list.py @@ -1,16 +1,30 @@ """Команда list: список сессий с фильтрацией и сортировкой.""" +import argparse from typing import Literal import db as db_module from db import get_session_title, parse_model from formatters import print_json, print_table_simple from i18n import _ -from utils import format_ts +from utils import build_help_epilog, format_ts + +_LIST_EXAMPLES = [ + ("", "help.list.e0"), + ("--limit 10", "help.list.e1"), + ("--sort cost", "help.list.e2"), + ("--project proj_xxx", "help.list.e3"), + ("--json", "help.list.e4"), +] def register(subparsers) -> None: - p = subparsers.add_parser("list", help=_("help.cmd.list")) + p = subparsers.add_parser( + "list", + help=_("help.cmd.list"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("list", _LIST_EXAMPLES), + ) p.add_argument("--limit", type=int, default=25, help="Max results (default 25)") p.add_argument("--offset", type=int, default=0, help="Offset") p.add_argument("--project", type=str, help="Filter by project ID") diff --git a/cmd_projects.py b/cmd_projects.py index e725f26..1e6d39a 100644 --- a/cmd_projects.py +++ b/cmd_projects.py @@ -1,13 +1,24 @@ """Команда projects: список проектов со статистикой.""" +import argparse from typing import Literal from i18n import _ -from utils import format_cost, format_tokens, format_ts +from utils import build_help_epilog, format_cost, format_tokens, format_ts + +_PROJECTS_EXAMPLES = [ + ("", "help.projects.e0"), + ("--json", "help.projects.e1"), +] def register(subparsers) -> None: - p = subparsers.add_parser("projects", help=_("help.cmd.projects")) + p = subparsers.add_parser( + "projects", + help=_("help.cmd.projects"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("projects", _PROJECTS_EXAMPLES), + ) p.add_argument("--json", action="store_true", help="JSON output") diff --git a/cmd_prune.py b/cmd_prune.py index 0329eab..ca0a57d 100644 --- a/cmd_prune.py +++ b/cmd_prune.py @@ -1,15 +1,26 @@ """Команда prune: массовая очистка старых сессий.""" -# from db import get_session_info, get_session_title, parse_model, \ -# get_message_count, get_part_count +import argparse from typing import Literal from i18n import _ -from utils import confirm, format_ts, parse_time_spec +from utils import build_help_epilog, confirm, format_ts, parse_time_spec + +_PRUNE_EXAMPLES = [ + ("--older-than 90d", "help.prune.e0"), + ("--keep-last 20", "help.prune.e1"), + ("--dry-run", "help.prune.e2"), + ("--force", "help.prune.e3"), +] def register(subparsers) -> None: - p = subparsers.add_parser("prune", help=_("help.cmd.prune")) + p = subparsers.add_parser( + "prune", + help=_("help.cmd.prune"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("prune", _PRUNE_EXAMPLES), + ) p.add_argument("--older-than", type=str, help="Delete sessions older than (30d, 6m, 1y)") p.add_argument("--keep-last", type=int, help="Keep N most recent sessions") p.add_argument("--project", type=str, help="Limit to project") diff --git a/cmd_search.py b/cmd_search.py index 7437a88..46096ee 100644 --- a/cmd_search.py +++ b/cmd_search.py @@ -1,14 +1,26 @@ """Команда search: поиск по тексту сообщений и частей.""" +import argparse import json from db import SessionError, resolve_session_id from i18n import _ -from utils import format_ts +from utils import build_help_epilog, format_ts + +_SEARCH_EXAMPLES = [ + ('"text"', "help.search.e0"), + ('"text" --session ses_xxx', "help.search.e1"), + ('"text" --json', "help.search.e2"), +] def register(subparsers) -> None: - p = subparsers.add_parser("search", help=_("help.cmd.search")) + p = subparsers.add_parser( + "search", + help=_("help.cmd.search"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("search", _SEARCH_EXAMPLES), + ) p.add_argument("query", help="Search text") p.add_argument("--session", type=str, help="Limit to session") p.add_argument("--limit", type=int, default=30, help="Max results") diff --git a/cmd_stats.py b/cmd_stats.py index 875827f..f09f94c 100644 --- a/cmd_stats.py +++ b/cmd_stats.py @@ -1,14 +1,25 @@ """Команда stats: общая статистика по базе данных.""" +import argparse from typing import Literal from db import parse_model from i18n import _ -from utils import format_cost, format_tokens, format_ts +from utils import build_help_epilog, format_cost, format_tokens, format_ts + +_STATS_EXAMPLES = [ + ("", "help.stats.e0"), + ("--json", "help.stats.e1"), +] def register(subparsers) -> None: - p = subparsers.add_parser("stats", help=_("help.cmd.stats")) + p = subparsers.add_parser( + "stats", + help=_("help.cmd.stats"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("stats", _STATS_EXAMPLES), + ) p.add_argument("--json", action="store_true", help="JSON output") diff --git a/cmd_todos.py b/cmd_todos.py index fd270d3..3278278 100644 --- a/cmd_todos.py +++ b/cmd_todos.py @@ -1,12 +1,26 @@ """Команда todos: просмотр задач (todo) из сессий.""" +import argparse + from db import SessionError, resolve_session_id from i18n import _ -from utils import format_ts +from utils import build_help_epilog, format_ts + +_TODOS_EXAMPLES = [ + ("", "help.todos.e0"), + ("", "help.todos.e1"), + ("--status pending", "help.todos.e2"), + ("--json", "help.todos.e3"), +] def register(subparsers) -> None: - p = subparsers.add_parser("todos", help=_("help.cmd.todos")) + p = subparsers.add_parser( + "todos", + help=_("help.cmd.todos"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("todos", _TODOS_EXAMPLES), + ) p.add_argument("session_id", nargs="?", help="Session ID") p.add_argument( "--status", diff --git a/cmd_tree.py b/cmd_tree.py index e9bb8e1..a87c905 100644 --- a/cmd_tree.py +++ b/cmd_tree.py @@ -1,12 +1,25 @@ """Команда tree: дерево ветвления сессий.""" +import argparse + from db import SessionError, get_session_title, resolve_session_id from i18n import _ -from utils import format_ts +from utils import build_help_epilog, format_ts + +_TREE_EXAMPLES = [ + ("", "help.tree.e0"), + ("", "help.tree.e1"), + ("--depth 3", "help.tree.e2"), +] def register(subparsers) -> None: - p = subparsers.add_parser("tree", help=_("help.cmd.tree")) + p = subparsers.add_parser( + "tree", + help=_("help.cmd.tree"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("tree", _TREE_EXAMPLES), + ) p.add_argument("session_id", nargs="?", help="Start from this session") p.add_argument("--project", type=str, help="Filter by project") p.add_argument("--depth", type=int, default=5, help="Max depth") diff --git a/cmd_vacuum.py b/cmd_vacuum.py index e2f9289..c68ff4f 100644 --- a/cmd_vacuum.py +++ b/cmd_vacuum.py @@ -1,15 +1,26 @@ """Команда vacuum: оптимизация базы данных.""" +import argparse import os from typing import Literal from config import OPENCODE_DB from i18n import _ -from utils import confirm +from utils import build_help_epilog, confirm + +_VACUUM_EXAMPLES = [ + ("", "help.vacuum.e0"), + ("--force", "help.vacuum.e1"), +] def register(subparsers) -> None: - p = subparsers.add_parser("vacuum", help=_("help.cmd.vacuum")) + p = subparsers.add_parser( + "vacuum", + help=_("help.cmd.vacuum"), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=build_help_epilog("vacuum", _VACUUM_EXAMPLES), + ) p.add_argument("--force", "-f", action="store_true", help="Skip confirmation") diff --git a/i18n.py b/i18n.py index 848da07..069a7f9 100644 --- a/i18n.py +++ b/i18n.py @@ -322,8 +322,8 @@ "en": "╔══════════════════════════════════════════════════════════╗", }, "help.header_title": { - "ru": "║ opencode-db — справка ║", - "en": "║ opencode-db — help ║", + "ru": "║ opencode-db — справка ║", + "en": "║ opencode-db — help ║", }, "help.header_box_bottom": { "ru": "╚══════════════════════════════════════════════════════════╝", @@ -355,6 +355,14 @@ }, "help.unknown": {"ru": "❌ Неизвестная команда: {cmd}", "en": "❌ Unknown command: {cmd}"}, "help.available": {"ru": " Доступные: {cmds}", "en": " Available: {cmds}"}, + "help.usage_header": { + "ru": "Примеры использования команды {cmd}:", + "en": "Usage examples for {cmd}:", + }, + "help.flags_header": { + "ru": "Все доступные флаги для {cmd}:", + "en": "Available flags for {cmd}:", + }, # ================================================================== # HELP — команды (краткие описания в справке) # ================================================================== @@ -372,8 +380,12 @@ "help.cmd.vacuum": {"ru": "Оптимизация БД", "en": "Database optimization"}, "help.cmd.help": {"ru": "Подробная справка", "en": "Detailed help"}, # ================================================================== - # HELP — примеры + # HELP — примеры (epilog для argparse --help) # ================================================================== + "help.cmd.list.examples": { + "ru": "\nПримеры:\n opencode-db list # Сессии за последнее время\n opencode-db list --limit 10 # Показать 10 последних\n opencode-db list --sort cost # Сортировка по стоимости\n opencode-db list --project proj_xxx # Фильтр по проекту\n opencode-db list --json # Вывод в JSON\n", + "en": "\nExamples:\n opencode-db list # Recent sessions\n opencode-db list --limit 10 # Show last 10\n opencode-db list --sort cost # Sort by cost\n opencode-db list --project proj_xxx # Filter by project\n opencode-db list --json # JSON output\n", + }, "help.list.e0": {"ru": "Сессии за последнее время", "en": "Recent sessions"}, "help.list.e1": {"ru": "Показать 10 последних", "en": "Show last 10"}, "help.list.e2": {"ru": "Сортировка по стоимости", "en": "Sort by cost"}, @@ -427,6 +439,14 @@ "en": "VACUUM + REINDEX + ANALYZE with confirmation", }, "help.vacuum.e1": {"ru": "Без подтверждения", "en": "Skip confirmation"}, + "help.help.e0": { + "ru": "Общая справка по всем командам", + "en": "General help for all commands", + }, + "help.help.e1": { + "ru": "Справка по конкретной команде", + "en": "Help for a specific command", + }, # ================================================================== # MARKDOWN EXPORT (formatters.py, config.py) # ================================================================== diff --git a/pyproject.toml b/pyproject.toml index 6493e5d..375423a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "opencode-db" -version = "0.5.0" +version = "0.5.1" description = "OpenCode database CLI manager — browse, export, analyze, clean up sessions" readme = "README_PYPI.md" requires-python = ">=3.12" diff --git a/utils.py b/utils.py index 2901956..220aace 100644 --- a/utils.py +++ b/utils.py @@ -91,6 +91,27 @@ def confirm(prompt=None, default=False) -> bool: return False +def build_help_epilog(cmd_name: str, examples: list[tuple[str, str]]) -> str: + """Формирует epilog для argparse из списка примеров. + + Args: + cmd_name: имя команды (для заголовка) + examples: список (флаги, ключ в i18n с описанием) + + Returns: + str — готовый текст для epilog + """ + header = _("help.usage_header", cmd=cmd_name) + lines = [f"\n{header}"] + for flags, desc_key in examples: + desc = _(desc_key) + if flags: + lines.append(f" opencode-db {cmd_name} {flags:33s} # {desc}") + else: + lines.append(f" opencode-db {cmd_name:36s} # {desc}") + return "\n".join(lines) + + def format_cost(cost_value) -> str: """Форматирует стоимость в долларах.""" if cost_value is None: