gabriel / muse public
registry.py python
147 lines 5.2 KB
Raw
sha256:832d1ca80cb25129c91d8c8c190ec30af2e638a3e8160431f4704fd881bceaee feat: add the todo domain plugin -- Build With Muse Episode 06 Sonnet 5 patch 3 hours ago
1 """Plugin registry — maps domain names to :class:`~muse.domain.MuseDomainPlugin` instances.
2
3 Every CLI command that operates on domain state calls :func:`resolve_plugin`
4 once to obtain the active plugin for the current repository. Adding support
5 for a new domain requires only two changes:
6
7 1. Implement :class:`~muse.domain.MuseDomainPlugin` in a new module under
8 ``muse/plugins/<domain>/plugin.py``.
9 2. Register the plugin instance in ``_REGISTRY`` below.
10
11 The domain for a repository is stored in ``.muse/repo.json`` under the key
12 ``"domain"``. Repositories created before this key was introduced default to
13 ``'midi'``.
14 """
15
16 import os
17 import pathlib
18
19 from muse.core.types import load_json_file
20 from muse.core.paths import repo_json_path as _repo_json_path
21 from muse.core.errors import MuseCLIError
22 from muse.core.schema import DomainSchema
23 from muse.domain import MuseDomainPlugin
24 from muse.plugins.code.plugin import CodePlugin
25 from muse.plugins.identity.plugin import IdentityPlugin
26 from muse.plugins.mist.plugin import MistPlugin
27 from muse.plugins.scaffold.plugin import ScaffoldPlugin
28 from muse.plugins.social.plugin import SocialPlugin
29 from muse.plugins.timeline.plugin import TimelinePlugin
30 from muse.plugins.todo.plugin import TodoPlugin
31
32 type _PluginRegistry = dict[str, "MuseDomainPlugin"]
33
34 _REGISTRY: _PluginRegistry = {
35 "code": CodePlugin(),
36 "identity": IdentityPlugin(),
37 "mist": MistPlugin(),
38 "social": SocialPlugin(),
39 "scaffold": ScaffoldPlugin(),
40 "timeline": TimelinePlugin(),
41 "todo": TodoPlugin(),
42 }
43
44 # MIDI domain is suspended by default pending its own security and
45 # performance audit — not sunset, just deferred while focus stays on the
46 # code domain. The plugin itself is fully maintained; only its registration
47 # is gated. Set MUSE_ENABLE_MIDI=1 to opt in locally (e.g. for a demo) —
48 # never set in code shipped to end users.
49 if os.environ.get("MUSE_ENABLE_MIDI"):
50 from muse.plugins.midi.plugin import MidiPlugin
51 _REGISTRY["midi"] = MidiPlugin()
52
53 _DEFAULT_DOMAIN = "code"
54
55 def _read_domain(root: pathlib.Path) -> str:
56 """Return the domain name stored in ``.muse/repo.json``.
57
58 Falls back to ``'midi'`` for repos that pre-date the ``domain`` field.
59 """
60 data = load_json_file(_repo_json_path(root))
61 if data is None:
62 return _DEFAULT_DOMAIN
63 domain = data.get("domain")
64 return str(domain) if domain else _DEFAULT_DOMAIN
65
66 def resolve_plugin(root: pathlib.Path) -> MuseDomainPlugin:
67 """Return the active domain plugin for the repository at *root*.
68
69 Reads the ``"domain"`` key from ``.muse/repo.json`` and looks it up in
70 the plugin registry. Raises :class:`~muse.core.errors.MuseCLIError` if
71 the domain is not registered.
72
73 Args:
74 root: Repository root directory (contains ``.muse/``).
75
76 Returns:
77 The :class:`~muse.domain.MuseDomainPlugin` instance for this repo.
78
79 Raises:
80 MuseCLIError: When the domain stored in ``repo.json`` is not in the
81 registry. This is a configuration error — either the plugin was
82 not installed or ``repo.json`` was edited manually.
83 """
84 domain = _read_domain(root)
85 plugin = _REGISTRY.get(domain)
86 if plugin is None:
87 registered = ", ".join(sorted(_REGISTRY))
88 raise MuseCLIError(
89 f"Unknown domain {domain!r}. Registered domains: {registered}"
90 )
91 return plugin
92
93 def read_domain(root: pathlib.Path) -> str:
94 """Return the domain name for the repository at *root*.
95
96 This is the same lookup used internally by :func:`resolve_plugin`.
97 Use it when you need the domain string to construct a
98 :class:`~muse.domain.SnapshotManifest` for a stored manifest.
99 """
100 return _read_domain(root)
101
102 def resolve_plugin_by_domain(domain: str) -> MuseDomainPlugin:
103 """Return the plugin for *domain* without reading the filesystem.
104
105 Use this when the caller has already read ``repo.json`` and only needs
106 the plugin instance — avoids a redundant ``repo.json`` read compared to
107 :func:`resolve_plugin`.
108
109 Args:
110 domain: Domain name string (e.g. ``'code'``).
111
112 Returns:
113 The :class:`~muse.domain.MuseDomainPlugin` instance for *domain*.
114
115 Raises:
116 MuseCLIError: When *domain* is not in the registry.
117 """
118 plugin = _REGISTRY.get(domain)
119 if plugin is None:
120 registered = ", ".join(sorted(_REGISTRY))
121 raise MuseCLIError(
122 f"Unknown domain {domain!r}. Registered domains: {registered}"
123 )
124 return plugin
125
126 def registered_domains() -> list[str]:
127 """Return the sorted list of registered domain names."""
128 return sorted(_REGISTRY)
129
130 def schema_for(domain: str) -> DomainSchema | None:
131 """Return the ``DomainSchema`` for *domain*, or ``None`` if not registered.
132
133 Allows the CLI and merge engine to look up a domain's schema without
134 holding a plugin instance. Returns ``None`` rather than raising so callers
135 can decide whether an unknown domain is an error or a soft miss.
136
137 Args:
138 domain: Domain name string (e.g. ``'midi'``).
139
140 Returns:
141 The :class:`~muse.core.schema.DomainSchema` declared by the plugin,
142 or ``None`` if *domain* is not in the registry.
143 """
144 plugin = _REGISTRY.get(domain)
145 if plugin is None:
146 return None
147 return plugin.schema()
File History 1 commit
sha256:832d1ca80cb25129c91d8c8c190ec30af2e638a3e8160431f4704fd881bceaee feat: add the todo domain plugin -- Build With Muse Episode 06 Sonnet 5 patch 3 hours ago