Files
JWeBot/tools/sync-local-extensions.py
JWeBot Contributors e4365841ed
check / local-gate (push) Failing after 1m38s
Release beta 2.0.0.1021
2026-10-21 17:05:09 +08:00

268 lines
7.2 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 ServiceCommandAdapter, SingleGroupContext
PROFILE = {extension_id!r}
AUTHOR = {descriptor.author!r}
DESCRIPTION = {descriptor.description!r}
HELP_ENTRIES = {descriptor.help_entries!r}
COMMANDS = {descriptor.commands!r}
ADAPTER = ServiceCommandAdapter(
PROFILE,
metadata={{"local_extension": PROFILE, "author": AUTHOR}},
)
def plugin(context: SingleGroupContext) -> None:
ADAPTER.register(context, COMMANDS)
'''
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())