fix: make editable plugin install symlink

This commit is contained in:
LIghtJUNction
2026-05-30 23:25:59 +08:00
parent f65172f278
commit 748901decd
3 changed files with 61 additions and 11 deletions

View File

@@ -150,7 +150,7 @@ def list(all: bool) -> None:
"-e",
"local_path",
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Install a plugin from a local directory",
help="Install a plugin from a local directory as a symlink",
)
@click.option("--proxy", help="Proxy server address")
def install(name: str | None, local_path: Path | None, proxy: str | None) -> None:
@@ -159,7 +159,7 @@ def install(name: str | None, local_path: Path | None, proxy: str | None) -> Non
plug_path = base_path / "plugins"
if local_path is not None:
install_local_plugin(local_path, plug_path)
install_local_plugin(local_path, plug_path, editable=True)
return
if name is None:
@@ -167,7 +167,7 @@ def install(name: str | None, local_path: Path | None, proxy: str | None) -> Non
local_name_path = Path(name).expanduser()
if local_name_path.exists() and local_name_path.is_dir():
install_local_plugin(local_name_path, plug_path)
install_local_plugin(local_name_path, plug_path, editable=False)
return
plugins = build_plug_list(base_path / "plugins")

View File

@@ -19,6 +19,18 @@ class PluginStatus(str, Enum):
NOT_PUBLISHED = "unpublished"
LOCAL_PLUGIN_COPY_IGNORE = shutil.ignore_patterns(
".git",
"__pycache__",
"*.pyc",
".venv",
"venv",
".idea",
".vscode",
".zed",
)
def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
"""Download code from a Git repository and extract to the specified path"""
temp_dir = Path(tempfile.mkdtemp())
@@ -190,7 +202,18 @@ def build_plug_list(plugins_dir: Path) -> list:
return result
def install_local_plugin(source_path: Path, plugins_dir: Path) -> None:
def _cleanup_local_plugin_target(target_path: Path) -> None:
if target_path.is_symlink() or target_path.is_file():
target_path.unlink(missing_ok=True)
elif target_path.exists():
shutil.rmtree(target_path, ignore_errors=True)
def install_local_plugin(
source_path: Path,
plugins_dir: Path,
editable: bool = False,
) -> None:
"""Install a plugin from a local directory."""
source_path = source_path.expanduser().resolve()
plugins_dir = plugins_dir.resolve()
@@ -211,11 +234,22 @@ def install_local_plugin(source_path: Path, plugins_dir: Path) -> None:
try:
plugins_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(source_path, target_path)
if editable:
try:
target_path.symlink_to(source_path, target_is_directory=True)
except OSError as e:
raise click.ClickException(
f"Failed to create symlink for editable install: {e}. "
"On Windows, you may need to run as Administrator or enable Developer Mode."
) from e
else:
shutil.copytree(source_path, target_path, ignore=LOCAL_PLUGIN_COPY_IGNORE)
click.echo(f"Plugin {plugin_name} installed successfully from {source_path}")
except click.ClickException:
_cleanup_local_plugin_target(target_path)
raise
except Exception as e:
if target_path.exists():
shutil.rmtree(target_path, ignore_errors=True)
_cleanup_local_plugin_target(target_path)
raise click.ClickException(
f"Error installing local plugin {plugin_name}: {e}"
) from e

View File

@@ -23,12 +23,20 @@ def _write_plugin(path: Path, name: str = "astrbot_plugin_local_demo") -> None:
(path / "main.py").write_text("PLUGIN_LOADED = True\n", encoding="utf-8")
def _write_ignored_plugin_files(path: Path) -> None:
for ignored_dir in [".git", ".venv", "__pycache__", ".idea", ".vscode", ".zed"]:
ignored_path = path / ignored_dir
ignored_path.mkdir()
(ignored_path / "ignored.txt").write_text("ignored\n", encoding="utf-8")
(path / "__pycache__" / "main.pyc").write_bytes(b"ignored")
def _write_astrbot_root(path: Path) -> None:
(path / ".astrbot").touch()
(path / "data" / "plugins").mkdir(parents=True)
def test_plugin_install_editable_copies_local_plugin(
def test_plugin_install_editable_symlinks_local_plugin(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
@@ -47,6 +55,7 @@ def test_plugin_install_editable_copies_local_plugin(
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
assert result.exit_code == 0
assert target.is_symlink()
assert (target / "metadata.yaml").exists()
assert (target / "main.py").read_text(encoding="utf-8") == "PLUGIN_LOADED = True\n"
@@ -60,14 +69,21 @@ def test_plugin_install_accepts_local_path_without_editable_flag(
root.mkdir()
_write_astrbot_root(root)
_write_plugin(source)
_write_ignored_plugin_files(source)
monkeypatch.chdir(root)
result = CliRunner().invoke(plug, ["install", str(source)])
target = root / "data" / "plugins" / "astrbot_plugin_local_demo"
assert result.exit_code == 0
assert (
root / "data" / "plugins" / "astrbot_plugin_local_demo" / "metadata.yaml"
).exists()
assert not target.is_symlink()
assert (target / "metadata.yaml").exists()
assert not (target / ".git").exists()
assert not (target / ".venv").exists()
assert not (target / "__pycache__").exists()
assert not (target / ".idea").exists()
assert not (target / ".vscode").exists()
assert not (target / ".zed").exists()
def test_plugin_install_editable_rejects_existing_plugin(