diff --git a/pyproject.toml b/pyproject.toml index 2313b95..f8dcb0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mini-wiki" -version = "3.1.0" +version = "3.2.0" description = "AI Agent skill package for automatic project documentation generation" authors = [ { name = "trsoliu" } @@ -9,9 +9,14 @@ license = { text = "Apache-2.0" } requires-python = ">=3.10" dependencies = [ "PyYAML>=6.0", + "click>=8.1", + "rich>=13.0", "tomli>=2.0; python_version<'3.11'", ] +[project.scripts] +mini-wiki = "cli:main" + [project.optional-dependencies] dev = [ "pytest>=8.0", diff --git a/scripts/cli.py b/scripts/cli.py new file mode 100644 index 0000000..a0f3726 --- /dev/null +++ b/scripts/cli.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +Mini-Wiki CLI — Generate professional project documentation with AI. + +Usage: + mini-wiki init [--force] + mini-wiki analyze [PATH] + mini-wiki check [PATH] + mini-wiki changes [PATH] + mini-wiki plugins list [PATH] + mini-wiki plugins enable NAME [PATH] + mini-wiki plugins disable NAME [PATH] + mini-wiki plugins install SOURCE [PATH] + mini-wiki plugins uninstall NAME [PATH] +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +import click + +from analyze_project import analyze_project, print_analysis +from check_quality import check_wiki_quality +from detect_changes import detect_changes, print_changes +from init_wiki import init_mini_wiki, print_result +from plugin_manager import ( + enable_plugin, + install_plugin, + list_plugins, + print_plugins, + uninstall_plugin, + update_plugin, +) + + +def _resolve_project(path: str | None) -> str: + if path: + return str(Path(path).resolve()) + return os.getcwd() + + +@click.group() +@click.version_option(version="3.2.0", prog_name="mini-wiki") +def main(): + """Mini-Wiki: AI-powered project documentation generator.""" + + +# --- init --- + + +@main.command() +@click.option("--force", is_flag=True, help="Force re-initialization (backs up existing config).") +@click.argument("path", required=False) +def init(force: bool, path: str | None): + """Initialize .mini-wiki directory structure.""" + project = _resolve_project(path) + result = init_mini_wiki(project, force=force) + print_result(result) + sys.exit(0 if result["success"] else 1) + + +# --- analyze --- + + +@main.command() +@click.option("--no-cache", is_flag=True, help="Don't save results to cache.") +@click.argument("path", required=False) +def analyze(no_cache: bool, path: str | None): + """Analyze project structure and tech stack.""" + project = _resolve_project(path) + result = analyze_project(project, save_to_cache=not no_cache) + print_analysis(result) + + +# --- check --- + + +@main.command() +@click.argument("path", required=False) +def check(path: str | None): + """Check documentation quality against standards.""" + project = _resolve_project(path) + wiki_dir = str(Path(project) / ".mini-wiki" / "wiki") + + if not Path(wiki_dir).exists(): + click.echo("No wiki found. Run 'mini-wiki init' first, then generate docs.") + sys.exit(1) + + report = check_wiki_quality(wiki_dir) + click.echo(f"Checked {report.total_docs} documents") + click.echo(f" Professional: {report.professional_count}") + click.echo(f" Standard: {report.standard_count}") + click.echo(f" Basic: {report.basic_count}") + + if report.summary_issues: + click.echo("\nIssues:") + for issue in report.summary_issues: + click.echo(f" - {issue}") + + +# --- changes --- + + +@main.command() +@click.argument("path", required=False) +def changes(path: str | None): + """Detect file changes since last documentation generation.""" + project = _resolve_project(path) + result = detect_changes(project) + print_changes(result) + + +# --- plugins --- + + +@main.group() +def plugins(): + """Manage Mini-Wiki plugins.""" + + +@plugins.command("list") +@click.argument("path", required=False) +def plugins_list(path: str | None): + """List installed plugins.""" + project = _resolve_project(path) + result = list_plugins(project) + print_plugins(result) + + +@plugins.command("install") +@click.argument("source") +@click.argument("path", required=False) +def plugins_install(source: str, path: str | None): + """Install a plugin from path, URL, or GitHub (owner/repo).""" + project = _resolve_project(path) + result = install_plugin(project, source) + click.echo(result["message"]) + sys.exit(0 if result["success"] else 1) + + +@plugins.command("uninstall") +@click.argument("name") +@click.argument("path", required=False) +def plugins_uninstall(name: str, path: str | None): + """Uninstall a plugin.""" + project = _resolve_project(path) + result = uninstall_plugin(project, name) + click.echo(result["message"]) + sys.exit(0 if result["success"] else 1) + + +@plugins.command("enable") +@click.argument("name") +@click.argument("path", required=False) +def plugins_enable(name: str, path: str | None): + """Enable a plugin.""" + project = _resolve_project(path) + result = enable_plugin(project, name, enabled=True) + click.echo(result["message"]) + + +@plugins.command("disable") +@click.argument("name") +@click.argument("path", required=False) +def plugins_disable(name: str, path: str | None): + """Disable a plugin.""" + project = _resolve_project(path) + result = enable_plugin(project, name, enabled=False) + click.echo(result["message"]) + + +@plugins.command("update") +@click.argument("name") +@click.argument("path", required=False) +def plugins_update(name: str, path: str | None): + """Update a plugin to the latest version.""" + project = _resolve_project(path) + result = update_plugin(project, name) + click.echo(result["message"]) + sys.exit(0 if result["success"] else 1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..5bcafa6 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,93 @@ +"""Tests for scripts/cli.py.""" + +from __future__ import annotations + +from click.testing import CliRunner + +from cli import main + + +runner = CliRunner() + + +def test_version(): + result = runner.invoke(main, ["--version"]) + assert result.exit_code == 0 + assert "3.2.0" in result.output + + +def test_help(): + result = runner.invoke(main, ["--help"]) + assert result.exit_code == 0 + assert "Mini-Wiki" in result.output + assert "init" in result.output + assert "analyze" in result.output + assert "check" in result.output + assert "changes" in result.output + assert "plugins" in result.output + + +def test_init_creates_wiki_dir(tmp_path): + result = runner.invoke(main, ["init", str(tmp_path)]) + assert result.exit_code == 0 + assert (tmp_path / ".mini-wiki").exists() + assert (tmp_path / ".mini-wiki" / "config.yaml").exists() + assert (tmp_path / ".mini-wiki" / "meta.json").exists() + + +def test_init_already_exists(tmp_path): + runner.invoke(main, ["init", str(tmp_path)]) + result = runner.invoke(main, ["init", str(tmp_path)]) + assert result.exit_code == 1 + assert "已存在" in result.output + + +def test_init_force(tmp_path): + runner.invoke(main, ["init", str(tmp_path)]) + result = runner.invoke(main, ["init", "--force", str(tmp_path)]) + assert result.exit_code == 0 + + +def test_analyze(tmp_path): + (tmp_path / "main.py").write_text("print('hello')") + result = runner.invoke(main, ["analyze", "--no-cache", str(tmp_path)]) + assert result.exit_code == 0 + assert "项目" in result.output or "技术栈" in result.output + + +def test_changes(tmp_path): + (tmp_path / "app.py").write_text("pass") + result = runner.invoke(main, ["changes", str(tmp_path)]) + assert result.exit_code == 0 + + +def test_check_no_wiki(tmp_path): + result = runner.invoke(main, ["check", str(tmp_path)]) + assert result.exit_code == 1 + assert "No wiki found" in result.output + + +def test_plugins_list(tmp_path): + result = runner.invoke(main, ["plugins", "list", str(tmp_path)]) + assert result.exit_code == 0 + + +def test_plugins_help(): + result = runner.invoke(main, ["plugins", "--help"]) + assert result.exit_code == 0 + assert "install" in result.output + assert "uninstall" in result.output + assert "enable" in result.output + assert "disable" in result.output + + +def test_plugins_enable_not_found(tmp_path): + (tmp_path / "plugins").mkdir() + result = runner.invoke(main, ["plugins", "enable", "nonexistent", str(tmp_path)]) + assert "not found" in result.output + + +def test_plugins_uninstall_not_found(tmp_path): + result = runner.invoke(main, ["plugins", "uninstall", "nonexistent", str(tmp_path)]) + assert result.exit_code == 1 + assert "not found" in result.output