#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""The web shell: the solver shell, extended for a PTY framed by the browser.The solver shell runs standalone and knows nothing of the web. This package composes it withan :class:`~solver.shell.extension.Extension` (docs/package-guide.md § 3.2): the overlay table`overlays.toml` makes the state-changing commands nudge the page over `OSC 5379`(:mod:`euler_webshell.shell.osc`) and replaces `show` / `edit` with bodies that swap the appshell's left pane (:mod:`euler_webshell.shell.viewer`); a subshell lent the terminal tells thepage so too, which disables its command-typing controls until the shell is back. The session'sidentity is a one-time ticket (:mod:`euler_webshell.shell.identity`), the vault is never promptedfor (the browser unlocks it), and the shell syncs the clone with origin before its banner, sothe banner reports the synced clone. The PTY runs `python -m euler_webshell.shell`.Both extensions here add the host's own commands — `msg`, `users`, key distribution(:mod:`euler_webshell.shell.keys`) and `logout` (:mod:`euler_webshell.shell.logout`), whichrefuses off the web channel but is loaded there so `update-docs` sees it — and carry the solvershell's notices on the message spool (:mod:`euler_webshell.shell.notices`).:func:`host_extension` is the same minus the browser: the OS login, a terminal vault prompt, noOSC; `solver-host` runs it, for the operator's terminal on the web host."""from__future__importannotations__all__=['HOST_COMMANDS','HOST_OVERLAYS_FILE','OVERLAYS_FILE','extension','host_extension','host_main','sync_at_start']frompathlibimportPathfromtypingimportCallablefromsolver.shell.extensionimportExtension,ExtensionError,read_overlaysfromsolver.shell.ttyimportconsolefromeuler_webshell.shellimportoscfromeuler_webshell.shell.identityimportticket_subjectfromeuler_webshell.shell.noticesimportHOST_KEY_HINT,WEB_KEY_HINT,Spool#: The overlay tables, shipped as package data: the web shell's, and the host terminal's.OVERLAYS_FILE:Path=Path(__file__).with_name('overlays.toml')HOST_OVERLAYS_FILE:Path=Path(__file__).with_name('host.toml')#: The command modules only the web host can run: they need its spool, roster and mail relay.HOST_COMMANDS:tuple[str,...]=('euler_webshell.users','euler_webshell.msg.commands','euler_webshell.shell.keys','euler_webshell.shell.logout')defextension()->Extension:"""The web shell's extension; an overlay emitting an event :mod:`osc` cannot send is refused."""overlays=read_overlays(OVERLAYS_FILE)unknown=sorted({eventforoverlayinoverlaysforeventinoverlay.emit}-set(osc.EVENTS))ifunknown:raiseExtensionError(f'{OVERLAYS_FILE.name}: unknown event(s) {unknown}; known: {sorted(osc.EVENTS)}')returnExtension(modules=HOST_COMMANDS,overlays=overlays,notify=osc.nudge,identity=ticket_subject,prompt_vault=False,on_start=sync_at_start,notices=Spool(WEB_KEY_HINT),handoff=osc.handoff,working=osc.working)defhost_extension()->Extension:"""The host terminal's extension: the host's commands and the spool, nothing of the browser."""returnExtension(modules=HOST_COMMANDS,overlays=read_overlays(HOST_OVERLAYS_FILE),notices=Spool(HOST_KEY_HINT))defhost_main()->int:# pragma: no cover — the `solver-host` console script"""`solver-host`: the solver shell with the web host's commands, in a terminal."""fromsolver.mainimportmaintry:ext=host_extension()exceptExtensionErrorasexc:raiseSystemExit(f'extension: {exc} — refusing to start')fromNonereturnmain(extension=ext)defsync_at_start(dispatch:Callable[[str],int])->str:"""Bring the clone up to date before the banner — quietly; return what to say if it did not. Before the banner, so its status line (`git describe`, the master-key state) describes the clone after the sync — which may have pulled a release or a key grant — not before. While it runs, one muted line says what the wait is; the banner's screen clear takes it away. Silence would open the shell on a stale clone with no hint — a diverged branch, a locked vault, no network — so a failed sync returns one line naming what to do, which the shell prints under the banner. After a key rotation `git-sync` is the very thing that cannot work and the fix is a message waiting in the spool, so that is asked first. """console.print('[muted]syncing with origin…[/muted]')prev_quiet,console.quiet=console.quiet,Truetry:rc=dispatch('git-sync')finally:console.quiet=prev_quietifrc==0:return''fromsolver.core.gitimportkey_waiting_hinthint=key_waiting_hint()or('could not sync with origin/master — this clone may be ''stale; run [accent]git-sync[/accent] to see why.')returnf'[warning]{hint}[/warning]'