Files
JWeBot/tools/create-plugin.py

234 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Create a reviewed command-extension skeleton without runtime privileges."""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [
str(ROOT / "packages" / "jwebot-sdk" / "src"),
str(ROOT / "packages" / "jwebot-kernel" / "src"),
]
from jwebot_kernel import ManifestError, parse_manifest # noqa: E402
def module_name(plugin_id: str) -> str:
return plugin_id.replace("-", "_")
def manifest_text(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> str:
module = module_name(plugin_id)
return f"""id = "{plugin_id}"
author = "{author}"
description = {json.dumps(description, ensure_ascii=False)}
help = [{json.dumps(help_entry, ensure_ascii=False)}]
version = "1.0.0"
api_version = "1"
entrypoint = "{module}:plugin"
type = "command"
[capabilities]
commands = ["{command}"]
storage = false
network_profiles = []
service_profiles = []
"""
def module_text(plugin_id: str, command: str) -> str:
return f'''"""Command extension: {plugin_id}."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
def handle(event: Event, arguments: str) -> ExtensionResult:
del event, arguments
return ExtensionResult(reply=ReplyPlan.text("TODO"))
def plugin(context: SingleGroupContext) -> None:
context.commands.register("{command}", handle)
'''
def test_text(plugin_id: str) -> str:
module = module_name(plugin_id)
return f'''import sys
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1] / "src"
sys.path.insert(0, str(SOURCE))
from jwebot_sdk import Event, PlanKind
from {module} import handle
class PluginTests(unittest.TestCase):
def test_command_returns_a_structured_reply(self):
event = Event(
event_id="event-1",
observed_at="2026-01-01T00:00:00+08:00",
sender="tester",
text="@JWeBot test",
group_fingerprint="group-test",
mention_bot=True,
)
result = handle(event, "")
self.assertEqual(result.reply.kind, PlanKind.TEXT)
if __name__ == "__main__":
unittest.main()
'''
def readme_text(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> str:
return f"""# {plugin_id}
Author: `{author}`
{description}
Command: `{command}`
Help: `{help_entry}`
This extension may return structured JWeBot SDK results only. It must not use
GUI automation, raw storage, environment variables, subprocesses, SSH, Docker,
or unrestricted network access.
"""
def validate_inputs(
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str,
) -> None:
parse_manifest(
{
"id": plugin_id,
"author": author,
"description": description,
"help": [help_entry],
"version": "1.0.0",
"api_version": "1",
"entrypoint": f"{module_name(plugin_id)}:plugin",
"type": "command",
"capabilities": {
"commands": [command],
"storage": False,
"network_profiles": [],
},
}
)
def create_plugin(
output: Path,
plugin_id: str,
command: str,
author: str,
description: str,
help_entry: str | None = None,
) -> Path:
help_entry = help_entry or command
validate_inputs(
plugin_id,
command,
author,
description,
help_entry,
)
destination = output / plugin_id
if destination.exists():
raise FileExistsError(f"destination already exists: {destination}")
source = destination / "src"
tests = destination / "tests"
source.mkdir(parents=True)
tests.mkdir()
module = module_name(plugin_id)
(destination / "plugin.toml").write_text(
manifest_text(
plugin_id,
command,
author,
description,
help_entry,
),
encoding="utf-8",
)
(source / f"{module}.py").write_text(
module_text(plugin_id, command),
encoding="utf-8",
)
(tests / "test_plugin.py").write_text(
test_text(plugin_id),
encoding="utf-8",
)
(destination / "README.md").write_text(
readme_text(
plugin_id,
command,
author,
description,
help_entry,
),
encoding="utf-8",
)
return destination
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("plugin_id")
parser.add_argument("command")
parser.add_argument("--author", required=True)
parser.add_argument("--description", required=True)
parser.add_argument("--help-entry")
parser.add_argument(
"--output",
type=Path,
default=ROOT / "extensions" / "friends",
)
args = parser.parse_args()
try:
destination = create_plugin(
args.output,
args.plugin_id,
args.command,
args.author,
args.description,
args.help_entry,
)
except (ManifestError, FileExistsError) as error:
parser.error(str(error))
print(destination)
return 0
if __name__ == "__main__":
raise SystemExit(main())