Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand All @@ -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",
Expand Down
187 changes: 187 additions & 0 deletions scripts/cli.py
Original file line number Diff line number Diff line change
@@ -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()
93 changes: 93 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -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
Loading