froid-loop 0.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- froid_loop/__init__.py +11 -0
- froid_loop/__main__.py +12 -0
- froid_loop/adapters/__init__.py +3 -0
- froid_loop/adapters/base.py +254 -0
- froid_loop/adapters/entrypoints.py +63 -0
- froid_loop/adapters/env_fault.py +290 -0
- froid_loop/adapters/generic.py +2013 -0
- froid_loop/adapters/mock.py +49 -0
- froid_loop/adapters/multiplexer.py +914 -0
- froid_loop/adapters/opencode_http.py +1687 -0
- froid_loop/adapters/profile.py +650 -0
- froid_loop/adapters/psmux_backend.py +1428 -0
- froid_loop/adapters/registry.py +322 -0
- froid_loop/adapters/tmux_backend.py +35 -0
- froid_loop/adapters/tmux_base.py +630 -0
- froid_loop/checks.py +187 -0
- froid_loop/cli.py +5041 -0
- froid_loop/data/__init__.py +0 -0
- froid_loop/data/froid_loop_hook.py +228 -0
- froid_loop/data/froid_loop_probe_hook.py +88 -0
- froid_loop/data/plugins/example/plugin.toml +21 -0
- froid_loop/data/plugins/tea/plugin.toml +184 -0
- froid_loop/data/plugins/tea/tea_plugin.py +258 -0
- froid_loop/data/plugins/unity/plugin.toml +140 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
- froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
- froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
- froid_loop/data/plugins/unity/unity_facts.md +17 -0
- froid_loop/data/plugins/unity/unity_plugin.py +415 -0
- froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
- froid_loop/data/plugins/unity/unity_ready.py +230 -0
- froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
- froid_loop/data/plugins/unity/unity_setup.py +551 -0
- froid_loop/data/plugins/unity/unity_teardown.py +362 -0
- froid_loop/data/profiles/antigravity.toml +52 -0
- froid_loop/data/profiles/claude.toml +85 -0
- froid_loop/data/profiles/codex.toml +22 -0
- froid_loop/data/profiles/copilot.toml +52 -0
- froid_loop/data/profiles/gemini.toml +26 -0
- froid_loop/data/profiles/opencode.toml +54 -0
- froid_loop/data/settings/core.toml +458 -0
- froid_loop/data/skills/README.md +93 -0
- froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
- froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
- froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
- froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
- froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
- froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
- froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
- froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
- froid_loop/decisions.py +202 -0
- froid_loop/deferredwork.py +2282 -0
- froid_loop/devcontract.py +892 -0
- froid_loop/diagnostics.py +1104 -0
- froid_loop/documents.py +532 -0
- froid_loop/engine.py +7732 -0
- froid_loop/envvars.py +111 -0
- froid_loop/escalation.py +225 -0
- froid_loop/events.py +266 -0
- froid_loop/fences.py +103 -0
- froid_loop/froidconfig.py +226 -0
- froid_loop/frontmatter.py +526 -0
- froid_loop/gates.py +133 -0
- froid_loop/install.py +2936 -0
- froid_loop/journal.py +178 -0
- froid_loop/machine.py +148 -0
- froid_loop/model.py +898 -0
- froid_loop/operatoractions.py +474 -0
- froid_loop/platform_util.py +1490 -0
- froid_loop/plugins/__init__.py +64 -0
- froid_loop/plugins/bus.py +259 -0
- froid_loop/plugins/context.py +319 -0
- froid_loop/plugins/loader.py +145 -0
- froid_loop/plugins/manifest.py +279 -0
- froid_loop/plugins/model.py +296 -0
- froid_loop/plugins/registry.py +245 -0
- froid_loop/plugins/trust.py +75 -0
- froid_loop/policy.py +1569 -0
- froid_loop/probe.py +1044 -0
- froid_loop/process_host.py +408 -0
- froid_loop/recovery_flow.py +1561 -0
- froid_loop/resolve.py +283 -0
- froid_loop/runs.py +4715 -0
- froid_loop/runsetup.py +1293 -0
- froid_loop/sanitize.py +593 -0
- froid_loop/settings_schema.py +276 -0
- froid_loop/signals.py +160 -0
- froid_loop/sprintstatus.py +609 -0
- froid_loop/statemachine.py +57 -0
- froid_loop/stories.py +615 -0
- froid_loop/stories_engine.py +796 -0
- froid_loop/sweep.py +1892 -0
- froid_loop/tokens.py +196 -0
- froid_loop/tui/__init__.py +11 -0
- froid_loop/tui/app.py +1584 -0
- froid_loop/tui/data.py +840 -0
- froid_loop/tui/launch.py +1003 -0
- froid_loop/tui/screens/__init__.py +1 -0
- froid_loop/tui/screens/dashboard.py +1071 -0
- froid_loop/tui/screens/modals.py +943 -0
- froid_loop/tui/screens/settings_screen.py +477 -0
- froid_loop/tui/settings.py +135 -0
- froid_loop/tui/widgets.py +981 -0
- froid_loop/verify.py +4545 -0
- froid_loop/workspace.py +320 -0
- froid_loop/worktree_flow.py +2301 -0
- froid_loop-0.11.1.dist-info/METADATA +728 -0
- froid_loop-0.11.1.dist-info/RECORD +116 -0
- froid_loop-0.11.1.dist-info/WHEEL +4 -0
- froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
- froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Coding-CLI adapter registry — the out-of-tree extension seam for the CLI axis.
|
|
2
|
+
|
|
3
|
+
The transport axis (:mod:`~.multiplexer`) has long been extensible out-of-tree:
|
|
4
|
+
a backend registers through ``register_multiplexer`` and a co-installed package
|
|
5
|
+
is discovered via the ``froid_loop.mux_backends`` entry-point group. This module
|
|
6
|
+
is the same seam for the *other* axis — which adapter **class** drives a coding
|
|
7
|
+
CLI. A CLI that fits the tmux-injection + hook-signal transport still needs no
|
|
8
|
+
Python at all (drop a TOML :class:`~.profile.CLIProfile` and run ``froid-loop
|
|
9
|
+
probe-adapter``); this registry is for a CLI that needs a whole new adapter
|
|
10
|
+
subclass (the shipped example is the HTTP/SSE ``opencode-http`` adapter, which no
|
|
11
|
+
tmux profile can host).
|
|
12
|
+
|
|
13
|
+
An adapter *kind* is selected by **data**: ``profile.adapter`` names the kind, and
|
|
14
|
+
:func:`get_adapter_kind` resolves it. Two registration-time dataclasses carry the
|
|
15
|
+
family:
|
|
16
|
+
|
|
17
|
+
- :class:`AdapterKind` — ``name`` + ``needs_mux`` (does the family drive a
|
|
18
|
+
terminal multiplexer?) + ``load``, a lazy thunk returning the builder. The thunk
|
|
19
|
+
is why registration, validation (``known_adapter_kinds``) and listing
|
|
20
|
+
(``detect_adapters``) never import a heavy adapter module — nor an optional
|
|
21
|
+
dependency like ``httpx``, which only the opencode family pulls in at
|
|
22
|
+
construction.
|
|
23
|
+
- :class:`AdapterBuilder` — the ``plain`` class, the ``_DevSynthesisMixin``-composed
|
|
24
|
+
``dev`` class (both share the ``(*args, paths, **kwargs)`` dev ``__init__``), and
|
|
25
|
+
the family's construction-failure exception type(s) (``()`` = none;
|
|
26
|
+
``(OpencodeServerError,)`` for the HTTP family, which fails loud when its server
|
|
27
|
+
can't spawn). ``runsetup.make_adapters`` converts a raised ``construct_error``
|
|
28
|
+
into a ``SystemExit``.
|
|
29
|
+
|
|
30
|
+
Bundled kinds register from :func:`_load_builtin_adapters` (:data:`GENERIC`,
|
|
31
|
+
:data:`OPENCODE_HTTP`); out-of-tree kinds arrive at import time, triggered by the
|
|
32
|
+
``froid_loop.adapters`` entry-point scan in :func:`_load_external_adapters` — so a
|
|
33
|
+
pip/uv co-installed adapter package is selectable with no config step. Builtins
|
|
34
|
+
are seeded by :func:`register_adapter` itself, so an external can never shadow a
|
|
35
|
+
bundled name however early its import lands. A broken third-party distribution
|
|
36
|
+
degrades to a recorded, surfaced reason (:func:`external_adapter_errors`) and can
|
|
37
|
+
never break selection — one reason per failing distribution, kept even when two of
|
|
38
|
+
them advertise the same entry-point name (:mod:`~.entrypoints`).
|
|
39
|
+
|
|
40
|
+
**Two deliberate asymmetries versus the multiplexer seam** (this is not a
|
|
41
|
+
copy-paste omission):
|
|
42
|
+
|
|
43
|
+
- *No process-wide cache / no ``cache_clear``.* The multiplexer is a single
|
|
44
|
+
process-wide singleton behind an ``lru_cache``; adapters are built **per run**
|
|
45
|
+
in ``runsetup.make_adapters``, which keeps its own ``by_cfg`` cache keyed on
|
|
46
|
+
``(resolved-config, synthesizes)``. Selection here is a pure registry lookup, so
|
|
47
|
+
there is nothing to cache and :func:`register_adapter` invalidates nothing.
|
|
48
|
+
- *No ``configure_*`` / ``matches(platform)`` / platform defaults.* The multiplexer
|
|
49
|
+
is chosen by a policy knob and a ``sys.platform`` predicate; an adapter kind is
|
|
50
|
+
chosen by the ``profile.adapter`` field alone. There is no host-dependent
|
|
51
|
+
auto-selection and no persisted choice to install, so none of that machinery
|
|
52
|
+
exists — ``get_adapter_kind(name)`` fails loud on an unknown name rather than
|
|
53
|
+
falling back.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import importlib.metadata
|
|
59
|
+
from collections.abc import Callable
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
|
|
62
|
+
from .entrypoints import record_load_error
|
|
63
|
+
|
|
64
|
+
# The two bundled kind names, as constants rather than literals scattered across
|
|
65
|
+
# modules. `validate`'s httpx check keys on OPENCODE_HTTP because httpx is *that
|
|
66
|
+
# family's* optional extra — a fact about one bundled family, which is a different
|
|
67
|
+
# thing from the set of VALID kinds (that set is only ever `known_adapter_kinds()`,
|
|
68
|
+
# never a literal). GENERIC is the `profile.adapter` default.
|
|
69
|
+
GENERIC = "generic"
|
|
70
|
+
OPENCODE_HTTP = "opencode-http"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AdapterError(Exception):
|
|
74
|
+
"""An adapter kind could not be resolved (an unknown ``profile.adapter``).
|
|
75
|
+
|
|
76
|
+
Construction failures a *known* family raises during ``__init__`` are its own
|
|
77
|
+
types (e.g. ``OpencodeServerError``), carried by
|
|
78
|
+
:attr:`AdapterBuilder.construct_error`, not this seam-level type."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class AdapterBuilder:
|
|
83
|
+
"""The classes and failure modes of one adapter family.
|
|
84
|
+
|
|
85
|
+
``plain`` and ``dev`` are the two variants ``runsetup.make_adapters`` picks
|
|
86
|
+
between on the ``synthesizes`` axis (a ``froid-build-auto`` dev/review session
|
|
87
|
+
gets ``dev``, which takes an extra ``paths=`` kwarg; every other role gets
|
|
88
|
+
``plain``). ``construct_error`` is the tuple of exception types the family's
|
|
89
|
+
constructor raises when the session cannot be built — empty for a family that
|
|
90
|
+
cannot fail construction (``generic``); the caller wraps a match into a
|
|
91
|
+
``SystemExit`` so a run aborts with a clean message instead of a traceback."""
|
|
92
|
+
|
|
93
|
+
plain: type
|
|
94
|
+
dev: type
|
|
95
|
+
construct_error: tuple[type[BaseException], ...] = ()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class AdapterKind:
|
|
100
|
+
"""One registered adapter family, keyed by ``name`` (the ``profile.adapter``
|
|
101
|
+
value). ``needs_mux`` gates whether ``runsetup.make_adapters`` resolves and
|
|
102
|
+
usability-checks the shared terminal multiplexer for this family (a hookless
|
|
103
|
+
HTTP/SSE family needs no transport). ``load`` is a lazy thunk returning the
|
|
104
|
+
:class:`AdapterBuilder`; it is the *only* place the family's classes (and any
|
|
105
|
+
optional dependency they pull in) are imported."""
|
|
106
|
+
|
|
107
|
+
name: str
|
|
108
|
+
needs_mux: bool
|
|
109
|
+
load: Callable[[], AdapterBuilder]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------- builtins
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _generic_builder() -> AdapterBuilder:
|
|
116
|
+
from .generic import GenericAdapter, GenericDevAdapter
|
|
117
|
+
|
|
118
|
+
return AdapterBuilder(plain=GenericAdapter, dev=GenericDevAdapter, construct_error=())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _opencode_http_builder() -> AdapterBuilder:
|
|
122
|
+
from .opencode_http import (
|
|
123
|
+
OpencodeDevAdapter,
|
|
124
|
+
OpencodeHttpAdapter,
|
|
125
|
+
OpencodeServerError,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return AdapterBuilder(
|
|
129
|
+
plain=OpencodeHttpAdapter,
|
|
130
|
+
dev=OpencodeDevAdapter,
|
|
131
|
+
construct_error=(OpencodeServerError,),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# The bundled kinds, as (name, needs_mux, load-thunk). A module constant, not
|
|
136
|
+
# mutable registry state, so detect_adapters can label a row builtin-vs-external
|
|
137
|
+
# without the fixtures having to snapshot it. `generic` drives tmux + hooks and
|
|
138
|
+
# needs the multiplexer; `opencode-http` is hookless HTTP/SSE and does not.
|
|
139
|
+
_BUILTIN_ADAPTERS: tuple[tuple[str, bool, Callable[[], AdapterBuilder]], ...] = (
|
|
140
|
+
(GENERIC, True, _generic_builder),
|
|
141
|
+
(OPENCODE_HTTP, False, _opencode_http_builder),
|
|
142
|
+
)
|
|
143
|
+
_BUILTIN_NAMES = frozenset(name for name, _, _ in _BUILTIN_ADAPTERS)
|
|
144
|
+
|
|
145
|
+
# The live registry: name -> AdapterKind. Unlike the multiplexer's ordered list
|
|
146
|
+
# (registration order breaks selection ties), adapter selection is a pure by-name
|
|
147
|
+
# lookup, so a dict with first-registration-wins semantics is the whole story.
|
|
148
|
+
_ADAPTERS: dict[str, AdapterKind] = {}
|
|
149
|
+
_BUILTINS_LOADED = False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def register_adapter(name: str, needs_mux: bool, load: Callable[[], AdapterBuilder]) -> None:
|
|
153
|
+
"""Register an adapter kind. ``name`` is the ``profile.adapter`` key that
|
|
154
|
+
selects it; ``needs_mux`` declares whether the family drives a terminal
|
|
155
|
+
multiplexer; ``load`` is the lazy builder thunk. First registration of a name
|
|
156
|
+
wins, and the builtins are seeded here rather than only by the resolution
|
|
157
|
+
entry points, so an out-of-tree package can never shadow a bundled name. An
|
|
158
|
+
out-of-tree kind calls this at import time — no core edit required. There is
|
|
159
|
+
no selection cache to invalidate (see the module docstring).
|
|
160
|
+
|
|
161
|
+
Seeding on *this* side is what makes first-wins an invariant instead of an
|
|
162
|
+
ordering coincidence. An external module runs its ``register_adapter`` calls
|
|
163
|
+
as an import side effect, and the import is not always triggered by an
|
|
164
|
+
adapter resolution: the documented packaging layout puts both entry points in
|
|
165
|
+
one module, so the ``froid_loop.profiles`` scan in :mod:`~.profile` — which
|
|
166
|
+
runs long before any kind is resolved — imports it too, as does any plugin
|
|
167
|
+
that imports the package directly. Any of those arriving first would have
|
|
168
|
+
``setdefault`` keep the external under a bundled name and silently redirect
|
|
169
|
+
every default profile to it."""
|
|
170
|
+
_load_builtin_adapters()
|
|
171
|
+
_ADAPTERS.setdefault(name, AdapterKind(name=name, needs_mux=needs_mux, load=load))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_builtin_adapters() -> None:
|
|
175
|
+
"""Register the bundled adapter kinds. Idempotent and lazy (called from the
|
|
176
|
+
resolution entry points and from :func:`register_adapter`, not at module
|
|
177
|
+
import) to stay cycle-safe: the load thunks import ``generic`` /
|
|
178
|
+
``opencode_http``, which import back through the package. Builtins register
|
|
179
|
+
before externals so a bundled name keeps first-wins on any collision.
|
|
180
|
+
|
|
181
|
+
The flag is set BEFORE the loop because the loop re-enters through
|
|
182
|
+
``register_adapter``; setting it afterwards would recurse without end."""
|
|
183
|
+
global _BUILTINS_LOADED
|
|
184
|
+
if _BUILTINS_LOADED:
|
|
185
|
+
return
|
|
186
|
+
_BUILTINS_LOADED = True
|
|
187
|
+
for name, needs_mux, load in _BUILTIN_ADAPTERS:
|
|
188
|
+
register_adapter(name, needs_mux, load)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# The entry-point group an out-of-tree adapter package advertises its module
|
|
192
|
+
# under; importing the module runs its register_adapter call. Loader state:
|
|
193
|
+
# scanned-once flag + per-entry-point failure reasons for adapters/validate.
|
|
194
|
+
ADAPTERS_GROUP = "froid_loop.adapters"
|
|
195
|
+
_EXTERNALS_LOADED = False
|
|
196
|
+
_EXTERNAL_ERRORS: dict[str, str] = {}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _load_external_adapters() -> None:
|
|
200
|
+
"""Import every ``froid_loop.adapters`` entry point; each module self-registers
|
|
201
|
+
via :func:`register_adapter` at import time. Called after
|
|
202
|
+
:func:`_load_builtin_adapters`, so builtins keep first registration.
|
|
203
|
+
|
|
204
|
+
A broken third-party distribution must never break adapter selection:
|
|
205
|
+
failures are recorded in ``_EXTERNAL_ERRORS`` (surfaced by ``froid-loop
|
|
206
|
+
adapters`` and the ``validate`` preflight via :func:`external_adapter_errors`),
|
|
207
|
+
not raised. The loaded-flag is set up front: a third-party import failure is
|
|
208
|
+
not transient, and retrying on every resolution would re-import (and re-fail)
|
|
209
|
+
each time — mirroring the multiplexer's external scan.
|
|
210
|
+
|
|
211
|
+
A recorded failure does NOT mean the entry point registered nothing: a module
|
|
212
|
+
that registers kind A and then raises while registering kind B leaves A
|
|
213
|
+
registered and selectable. Deliberate — unwinding would mean tracking which
|
|
214
|
+
names a half-run import claimed, and a kind that registered cleanly is usable
|
|
215
|
+
whatever else its package got wrong. The recorded reason is a fact about the
|
|
216
|
+
import, not a promise about the registry.
|
|
217
|
+
|
|
218
|
+
Entry points are visited in (name, distribution) order. ``importlib.metadata``
|
|
219
|
+
yields them in distribution-discovery order, which varies with ``sys.path``, so
|
|
220
|
+
without an explicit sort two hosts carrying the same packages could resolve a
|
|
221
|
+
collision differently — first-wins would be a fact about the install rather
|
|
222
|
+
than about the packages.
|
|
223
|
+
|
|
224
|
+
The distribution belongs in the key because the name alone is NOT a total
|
|
225
|
+
order. ``entry_points(group=...)`` does not dedup across distributions, so two
|
|
226
|
+
packages advertising the same entry-point name come back as two entries, and
|
|
227
|
+
``sorted`` is stable — a name-only key resolves that tie straight back into
|
|
228
|
+
``sys.path`` order. That tie is the whole case the sort exists for: a package
|
|
229
|
+
conventionally names its entry point after the kind it registers, so packages
|
|
230
|
+
that collide on a kind normally collide on the entry-point name too."""
|
|
231
|
+
global _EXTERNALS_LOADED
|
|
232
|
+
if _EXTERNALS_LOADED:
|
|
233
|
+
return
|
|
234
|
+
_EXTERNALS_LOADED = True
|
|
235
|
+
try:
|
|
236
|
+
eps = sorted(
|
|
237
|
+
importlib.metadata.entry_points(group=ADAPTERS_GROUP),
|
|
238
|
+
key=lambda e: (e.name, getattr(e.dist, "name", "") or ""),
|
|
239
|
+
)
|
|
240
|
+
except Exception as exc: # noqa: BLE001 — diagnostics path, never crash selection
|
|
241
|
+
_EXTERNAL_ERRORS["<entry-point scan>"] = f"{type(exc).__name__}: {exc}"
|
|
242
|
+
return
|
|
243
|
+
for ep in eps:
|
|
244
|
+
try:
|
|
245
|
+
ep.load() # module import runs register_adapter(...)
|
|
246
|
+
except Exception as exc: # noqa: BLE001 — one bad package must not hide the rest
|
|
247
|
+
record_load_error(_EXTERNAL_ERRORS, ep, exc)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def external_adapter_errors() -> dict[str, str]:
|
|
251
|
+
"""Entry-point name -> failure reason(s) for every external adapter that failed
|
|
252
|
+
to load this process (empty when all loaded). For diagnostics surfaces.
|
|
253
|
+
|
|
254
|
+
One value may carry MORE than one reason, ``"; "``-joined: two distributions
|
|
255
|
+
may advertise the same entry-point name, and each of their failures is kept
|
|
256
|
+
(see :func:`~.entrypoints.record_load_error`). Each reason is labelled with
|
|
257
|
+
its distribution whenever one is resolvable.
|
|
258
|
+
|
|
259
|
+
Performs the scan itself rather than relying on a neighbouring
|
|
260
|
+
``known_adapter_kinds`` / ``detect_adapters`` call having run first — an
|
|
261
|
+
accessor whose emptiness depends on call order reads as "nothing failed"."""
|
|
262
|
+
_load_builtin_adapters()
|
|
263
|
+
_load_external_adapters()
|
|
264
|
+
return dict(_EXTERNAL_ERRORS)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _known() -> str:
|
|
268
|
+
return ", ".join(sorted(_ADAPTERS)) or "(none registered)"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_adapter_kind(name: str) -> AdapterKind:
|
|
272
|
+
"""Resolve the adapter kind named ``name`` (a ``profile.adapter`` value),
|
|
273
|
+
loading the builtins and scanning the entry-point group first.
|
|
274
|
+
|
|
275
|
+
Fails loud on an unknown name, listing the registered kinds — an explicit but
|
|
276
|
+
unregistered adapter is a misconfiguration (a typo, or a plugin package that
|
|
277
|
+
isn't installed), never something to silently fall back from. The caller
|
|
278
|
+
(``runsetup.make_adapters``) adds the offending profile's name to the
|
|
279
|
+
message."""
|
|
280
|
+
_load_builtin_adapters()
|
|
281
|
+
_load_external_adapters()
|
|
282
|
+
kind = _ADAPTERS.get(name)
|
|
283
|
+
if kind is None:
|
|
284
|
+
raise AdapterError(f"unknown adapter kind {name!r}; known: {_known()}")
|
|
285
|
+
return kind
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def known_adapter_kinds() -> list[str]:
|
|
289
|
+
"""Sorted names of every registered adapter kind (builtins + successfully
|
|
290
|
+
loaded externals). The oracle for ``validate``'s ``adapter.kind`` finding —
|
|
291
|
+
validity of ``profile.adapter`` is enforced against this registry, never a
|
|
292
|
+
hardcoded set."""
|
|
293
|
+
_load_builtin_adapters()
|
|
294
|
+
_load_external_adapters()
|
|
295
|
+
return sorted(_ADAPTERS)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(frozen=True)
|
|
299
|
+
class AdapterKindInfo:
|
|
300
|
+
"""One registered adapter kind's detection row, for ``froid-loop adapters`` and
|
|
301
|
+
the ``validate`` preflight."""
|
|
302
|
+
|
|
303
|
+
name: str
|
|
304
|
+
needs_mux: bool
|
|
305
|
+
builtin: bool
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def detect_adapters() -> list[AdapterKindInfo]:
|
|
309
|
+
"""Enumerate every registered adapter kind (builtins + loaded externals),
|
|
310
|
+
sorted by name, each labelled builtin-vs-external. Never raises — this feeds
|
|
311
|
+
diagnostics, which must work on a misconfigured host. The ``load`` thunk is
|
|
312
|
+
never invoked, so listing stays free of any heavy adapter import."""
|
|
313
|
+
_load_builtin_adapters()
|
|
314
|
+
_load_external_adapters()
|
|
315
|
+
return [
|
|
316
|
+
AdapterKindInfo(
|
|
317
|
+
name=kind.name,
|
|
318
|
+
needs_mux=kind.needs_mux,
|
|
319
|
+
builtin=kind.name in _BUILTIN_NAMES,
|
|
320
|
+
)
|
|
321
|
+
for kind in sorted(_ADAPTERS.values(), key=lambda k: k.name)
|
|
322
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""POSIX tmux backend for the terminal-multiplexer seam.
|
|
2
|
+
|
|
3
|
+
The tmux/POSIX-shell quarantine spans this file and its base
|
|
4
|
+
(:mod:`.tmux_base`) — together they are the **only** place in the codebase
|
|
5
|
+
allowed to shell out to ``tmux``, so a future non-POSIX backend (an eventual
|
|
6
|
+
native-Windows "psmux") can replace them wholesale. All argv construction and
|
|
7
|
+
the single spawn primitive live in :class:`~.tmux_base.BaseTmuxBackend`; this
|
|
8
|
+
leaf is the POSIX implementation and inherits the full contract unchanged. See
|
|
9
|
+
:mod:`.multiplexer` for the contract.
|
|
10
|
+
|
|
11
|
+
``subprocess`` and ``shutil`` are imported (and re-exported) here so existing
|
|
12
|
+
callers and tests can still reach the spawn seam via ``tmux_backend.subprocess``
|
|
13
|
+
/ ``tmux_backend.shutil``; the live calls run through ``tmux_base``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import shutil # noqa: F401 — re-exported for callers/tests reaching the spawn seam
|
|
19
|
+
import subprocess # noqa: F401 — re-exported for callers/tests reaching the spawn seam
|
|
20
|
+
|
|
21
|
+
from .tmux_base import PARKED_RETURN_DETACH # noqa: F401 — re-exported for back-compat
|
|
22
|
+
from .tmux_base import TMUX_TIMEOUT_S # noqa: F401 — re-exported for back-compat
|
|
23
|
+
from .tmux_base import TmuxError # noqa: F401 — re-exported for back-compat
|
|
24
|
+
from .tmux_base import (
|
|
25
|
+
BaseTmuxBackend,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TmuxMultiplexer(BaseTmuxBackend):
|
|
30
|
+
"""POSIX tmux backend — inherits the full contract from BaseTmuxBackend.
|
|
31
|
+
|
|
32
|
+
Registered by :func:`~.multiplexer._load_builtin_backends` (the bundled loader),
|
|
33
|
+
not at import time, so the registry can be cleared and re-loaded deterministically
|
|
34
|
+
in tests — mirroring how ``process_host._load_builtin_hosts`` registers its hosts.
|
|
35
|
+
"""
|