287 lines
7.8 KiB
Python
Executable File
287 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate or verify all reviewed local extension adapter packages."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OFFICIAL_SRC = (
|
|
ROOT
|
|
/ "packages"
|
|
/ "jwebot-official-extensions"
|
|
/ "src"
|
|
)
|
|
SDK_SRC = ROOT / "packages" / "jwebot-sdk" / "src"
|
|
sys.path[:0] = [str(SDK_SRC), str(OFFICIAL_SRC)]
|
|
|
|
from jwebot_official_extensions import LOCAL_EXTENSIONS # noqa: E402
|
|
|
|
|
|
LEGACY_EXTENSION_DIRS = (
|
|
ROOT / "extensions" / "official" / "core-help",
|
|
ROOT / "extensions" / "official" / "group-insights",
|
|
ROOT / "extensions" / "official" / "local-operations",
|
|
ROOT / "extensions" / "official" / "random-games",
|
|
ROOT / "extensions" / "official" / "session-games",
|
|
ROOT / "extensions" / "official" / "steam-presence",
|
|
)
|
|
|
|
|
|
def module_name(extension_id: str) -> str:
|
|
return extension_id.replace("-", "_")
|
|
|
|
|
|
def manifest_text(descriptor) -> str:
|
|
command_values = ", ".join(
|
|
json.dumps(command, ensure_ascii=False)
|
|
for command in descriptor.commands
|
|
)
|
|
help_values = ", ".join(
|
|
json.dumps(entry, ensure_ascii=False)
|
|
for entry in descriptor.help_entries
|
|
)
|
|
extension_id = descriptor.extension_id
|
|
return f"""id = "{extension_id}"
|
|
author = "{descriptor.author}"
|
|
description = {json.dumps(descriptor.description, ensure_ascii=False)}
|
|
help = [{help_values}]
|
|
version = "1.0.0"
|
|
api_version = "1"
|
|
entrypoint = "{module_name(extension_id)}:plugin"
|
|
type = "command"
|
|
|
|
[capabilities]
|
|
commands = [{command_values}]
|
|
storage = false
|
|
network_profiles = []
|
|
service_profiles = ["{extension_id}"]
|
|
"""
|
|
|
|
|
|
def module_text(descriptor) -> str:
|
|
extension_id = descriptor.extension_id
|
|
return f'''"""Generated reviewed adapter for {extension_id}."""
|
|
|
|
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|
|
|
|
|
PROFILE = {extension_id!r}
|
|
AUTHOR = {descriptor.author!r}
|
|
DESCRIPTION = {descriptor.description!r}
|
|
HELP_ENTRIES = {descriptor.help_entries!r}
|
|
COMMANDS = {descriptor.commands!r}
|
|
|
|
|
|
def _handler(context: SingleGroupContext, command: str):
|
|
def handle(event: Event, arguments: str) -> ExtensionResult:
|
|
response = context.services.call(
|
|
PROFILE,
|
|
operation="execute",
|
|
payload={{
|
|
"command": command,
|
|
"arguments": arguments,
|
|
"event": event.to_mapping(),
|
|
}},
|
|
)
|
|
text = response.get("text")
|
|
if not isinstance(text, str) or not text.strip():
|
|
raise ValueError("local service returned invalid text")
|
|
return ExtensionResult(
|
|
reply=ReplyPlan.text(text),
|
|
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
|
|
)
|
|
|
|
return handle
|
|
|
|
|
|
def plugin(context: SingleGroupContext) -> None:
|
|
for command in COMMANDS:
|
|
context.commands.register(command, _handler(context, command))
|
|
'''
|
|
|
|
|
|
def readme_text(descriptor) -> str:
|
|
command_list = "\n".join(
|
|
f"- `{command}`"
|
|
for command in descriptor.commands
|
|
)
|
|
return f"""# {descriptor.extension_id}
|
|
|
|
Author: `{descriptor.author}`
|
|
|
|
{descriptor.description}
|
|
|
|
Help:
|
|
|
|
{chr(10).join(f"- `{entry}`" for entry in descriptor.help_entries)}
|
|
|
|
Registered commands:
|
|
|
|
{command_list}
|
|
|
|
The adapter has no direct Ledger, GUI, environment, shell, or network access.
|
|
It delegates to the reviewed `{descriptor.extension_id}` Kernel service profile
|
|
and returns a structured `ReplyPlan`.
|
|
"""
|
|
|
|
|
|
def test_text(descriptor) -> str:
|
|
module = module_name(descriptor.extension_id)
|
|
first_command = descriptor.commands[0]
|
|
return f'''import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1] / "src"
|
|
sys.path.insert(0, str(SOURCE))
|
|
|
|
from {module} import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin
|
|
|
|
|
|
class PluginTests(unittest.TestCase):
|
|
def test_registers_declared_commands_and_author(self):
|
|
context = mock.Mock()
|
|
plugin(context)
|
|
registered = [
|
|
call.args[0]
|
|
for call in context.commands.register.call_args_list
|
|
]
|
|
self.assertEqual(AUTHOR, {descriptor.author!r})
|
|
self.assertEqual(DESCRIPTION, {descriptor.description!r})
|
|
self.assertEqual(HELP_ENTRIES, {descriptor.help_entries!r})
|
|
self.assertIn({first_command!r}, registered)
|
|
self.assertEqual(len(registered), {len(descriptor.commands)})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
'''
|
|
|
|
|
|
def collection_readme(author: str) -> str:
|
|
if author == "official":
|
|
description = (
|
|
"Official-owned reviewed extensions. Person memory is the only "
|
|
"official command plugin in the current inventory."
|
|
)
|
|
else:
|
|
description = (
|
|
"Reviewed local extensions maintained by the JWeBot project. Each "
|
|
"independently "
|
|
"understandable function is packaged separately; dice and dice "
|
|
"challenge intentionally remain one complete feature collection."
|
|
)
|
|
return f"""# {author}
|
|
|
|
{description}
|
|
|
|
Every child directory is generated from the authoritative local extension
|
|
ownership inventory.
|
|
"""
|
|
|
|
|
|
def expected_files() -> dict[Path, str]:
|
|
values = {
|
|
ROOT / "extensions" / "official" / "README.md": collection_readme(
|
|
"official"
|
|
),
|
|
ROOT / "extensions" / "jwebot" / "README.md": collection_readme(
|
|
"jwebot"
|
|
),
|
|
}
|
|
for descriptor in LOCAL_EXTENSIONS:
|
|
root = (
|
|
ROOT
|
|
/ "extensions"
|
|
/ descriptor.directory
|
|
/ descriptor.extension_id
|
|
)
|
|
module = module_name(descriptor.extension_id)
|
|
values.update(
|
|
{
|
|
root / "plugin.toml": manifest_text(descriptor),
|
|
root / "src" / f"{module}.py": module_text(descriptor),
|
|
(
|
|
OFFICIAL_SRC
|
|
/ "jwebot_official_extensions"
|
|
/ "adapters"
|
|
/ f"{module}.py"
|
|
): module_text(descriptor),
|
|
root / "tests" / "test_plugin.py": test_text(descriptor),
|
|
root / "README.md": readme_text(descriptor),
|
|
}
|
|
)
|
|
return values
|
|
|
|
|
|
def stale_paths(expected: set[Path]) -> list[Path]:
|
|
stale = [
|
|
path
|
|
for path in LEGACY_EXTENSION_DIRS
|
|
if path.exists() and path not in expected
|
|
]
|
|
adapters = (
|
|
OFFICIAL_SRC
|
|
/ "jwebot_official_extensions"
|
|
/ "adapters"
|
|
)
|
|
stale.extend(
|
|
path
|
|
for path in adapters.glob("*.py")
|
|
if path.name != "__init__.py" and path not in expected
|
|
)
|
|
return sorted(stale)
|
|
|
|
|
|
def synchronize(*, check: bool) -> list[Path]:
|
|
expected = expected_files()
|
|
changed: list[Path] = []
|
|
for path, content in expected.items():
|
|
current = (
|
|
path.read_text(encoding="utf-8")
|
|
if path.is_file()
|
|
else None
|
|
)
|
|
if current == content:
|
|
continue
|
|
changed.append(path)
|
|
if not check:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
stale = stale_paths(set(expected))
|
|
changed.extend(stale)
|
|
if not check:
|
|
for path in stale:
|
|
if path.is_dir():
|
|
shutil.rmtree(path)
|
|
else:
|
|
path.unlink()
|
|
return changed
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--check", action="store_true")
|
|
args = parser.parse_args()
|
|
changed = synchronize(check=args.check)
|
|
if args.check and changed:
|
|
for path in changed:
|
|
print(path.relative_to(ROOT), file=sys.stderr)
|
|
return 1
|
|
for path in changed:
|
|
print(path.relative_to(ROOT))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|