open-codex-ui 0.1.4__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.
Files changed (40) hide show
  1. open_codex_ui-0.1.4.dist-info/METADATA +249 -0
  2. open_codex_ui-0.1.4.dist-info/RECORD +40 -0
  3. open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
  4. open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
  5. open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
  6. yier_web/__init__.py +6 -0
  7. yier_web/app.py +223 -0
  8. yier_web/auth.py +269 -0
  9. yier_web/cli.py +172 -0
  10. yier_web/codex/__init__.py +3 -0
  11. yier_web/codex/ipc_manager.py +1761 -0
  12. yier_web/codex/session_events.py +110 -0
  13. yier_web/codex/ws_commands.py +299 -0
  14. yier_web/config.py +651 -0
  15. yier_web/directory_picker.py +93 -0
  16. yier_web/event_stream.py +29 -0
  17. yier_web/frontend.py +204 -0
  18. yier_web/routes/__init__.py +17 -0
  19. yier_web/routes/codex.py +573 -0
  20. yier_web/routes/core.py +281 -0
  21. yier_web/schemas.py +534 -0
  22. yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
  23. yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
  24. yier_web/static/assets/LoginView-CELCom2O.js +101 -0
  25. yier_web/static/assets/api-CeihACIV.js +1099 -0
  26. yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
  27. yier_web/static/assets/index-CjVNk6ja.js +183 -0
  28. yier_web/static/assets/index-mSBvq1p8.js +79 -0
  29. yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
  30. yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
  31. yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
  32. yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
  33. yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
  34. yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
  35. yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
  36. yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
  37. yier_web/static/brand/open-codex-ui-logo.svg +85 -0
  38. yier_web/static/favicon.ico +0 -0
  39. yier_web/static/favicon.svg +99 -0
  40. yier_web/static/index.html +24 -0
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: open-codex-ui
3
+ Version: 0.1.4
4
+ Summary: Remote-friendly web workspace for Codex sessions
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: open-codex-bridge>=0.1.9
8
+ Requires-Dist: granian[reload]>=2.7.2
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: litestar>=2.21.1
11
+ Requires-Dist: pydantic>=2.12.5
12
+ Requires-Dist: yier-agents>=0.1.3
13
+
14
+ <p align="center">
15
+ <img src="web/public/brand/open-codex-ui-logo.svg" alt="Open Codex UI" width="520">
16
+ </p>
17
+
18
+ # open-codex-ui
19
+
20
+ Remote-friendly Codex web workspace for continuing sessions from desktop and mobile browsers.
21
+
22
+ ## Resume
23
+ [一二Resume](https://baike.baidu.com/item/%E4%B8%80%E4%BA%8C/23434669)
24
+
25
+ ## Requirements
26
+
27
+ - [`uv`](https://docs.astral.sh/uv/) for running the published application
28
+ - Python 3.12+, Node.js 20+, and `pnpm` only for source development
29
+
30
+ ## Install
31
+
32
+ Run the published application without a permanent installation:
33
+
34
+ ```bash
35
+ uvx open-codex-ui
36
+ ```
37
+
38
+ The wheel includes the compiled frontend and starts in production mode. For a
39
+ persistent command, install it as a uv tool:
40
+
41
+ ```bash
42
+ uv tool install open-codex-ui
43
+ open-codex-ui
44
+ ```
45
+
46
+ For source development, install the backend dependencies:
47
+
48
+ ```bash
49
+ uv sync
50
+ ```
51
+
52
+ Codex workspace support uses the published `open-codex-bridge` package from PyPI.
53
+
54
+ Then install the frontend dependencies:
55
+
56
+ ```bash
57
+ cd web
58
+ pnpm install
59
+ ```
60
+
61
+ ## Authentication
62
+
63
+ This app now supports password protection for deployed environments.
64
+
65
+ Enable auth with either:
66
+
67
+ - `YIER_AUTH_PASSWORD`
68
+ - `YIER_AUTH_PASSWORD_HASH`
69
+
70
+ Plain password example:
71
+
72
+ ```bash
73
+ export YIER_AUTH_PASSWORD='change-this-password'
74
+ ```
75
+
76
+ Hashed password example:
77
+
78
+ ```bash
79
+ uv run python -c "from yier_web.auth import hash_password; print(hash_password('change-this-password'))"
80
+ export YIER_AUTH_PASSWORD_HASH='paste-generated-hash-here'
81
+ ```
82
+
83
+ Optional auth settings:
84
+
85
+ - `YIER_AUTH_SECRET`: optional extra signing secret for session cookies
86
+ - `YIER_AUTH_SESSION_TTL_HOURS`: cookie lifetime in hours, default is `168`
87
+ - `YIER_CODEX_EMBED_TOKEN`: token for unauthenticated Codex iframe access
88
+
89
+ If neither password variable is set, authentication is disabled.
90
+
91
+ ## Codex Iframe Embed
92
+
93
+ See [IFRAME.md](./IFRAME.md) for iframe setup, authentication, and the
94
+ `postMessage` API.
95
+
96
+ ## Development Startup
97
+
98
+ Development mode is different from production:
99
+
100
+ - The backend should run with `--debug`
101
+ - The frontend should run with Vite dev server
102
+ - In this mode, the backend proxies frontend requests to `http://127.0.0.1:5173`
103
+
104
+ Recommended one-command startup:
105
+
106
+ ```bash
107
+ uv run open-codex-ui-dev
108
+ ```
109
+
110
+ This starts:
111
+
112
+ - frontend: `pnpm dev`
113
+ - backend: debug mode with reload
114
+
115
+ If you prefer split terminals:
116
+
117
+ Frontend only:
118
+
119
+ ```bash
120
+ uv run open-codex-ui-dev-web
121
+ ```
122
+
123
+ Backend only:
124
+
125
+ ```bash
126
+ uv run open-codex-ui-dev-backend
127
+ ```
128
+
129
+ You can also override backend bind settings:
130
+
131
+ ```bash
132
+ uv run open-codex-ui-dev --host 127.0.0.1 --port 9999
133
+ uv run open-codex-ui-dev-backend --host 127.0.0.1 --port 9999
134
+ ```
135
+
136
+ Default address:
137
+
138
+ - App: `http://127.0.0.1:9999`
139
+ - Vite dev server: `http://127.0.0.1:5173`
140
+
141
+ Notes:
142
+
143
+ - Keep `pnpm dev` running, otherwise the backend cannot proxy the frontend in debug mode.
144
+ - API requests still go through the Python server at port `9999`.
145
+
146
+ ## Production Startup
147
+
148
+ The published wheel includes the compiled frontend and does not use the Vite
149
+ dev server:
150
+
151
+ ```bash
152
+ uvx open-codex-ui
153
+ ```
154
+
155
+ The server listens on `127.0.0.1:9999` by default. Bind to the local network
156
+ explicitly when needed:
157
+
158
+ ```bash
159
+ uvx open-codex-ui --host 0.0.0.0 --port 9999
160
+ ```
161
+
162
+ When running from a source checkout, build the frontend before starting:
163
+
164
+ ```bash
165
+ uv run open-codex-ui-build-web
166
+ uv run open-codex-ui-prod
167
+ ```
168
+
169
+ In production mode:
170
+
171
+ - The backend serves the compiled assets packaged under `yier_web/static`
172
+ - No Vite proxy is used
173
+ - Authentication should usually be enabled with `YIER_AUTH_PASSWORD` or `YIER_AUTH_PASSWORD_HASH`
174
+
175
+ Production example:
176
+
177
+ ```bash
178
+ export YIER_AUTH_PASSWORD='change-this-password'
179
+ uvx open-codex-ui --host 0.0.0.0 --port 9999
180
+ ```
181
+
182
+ ## Common Commands
183
+
184
+ Backend tests:
185
+
186
+ ```bash
187
+ uv run --all-packages pytest
188
+ ```
189
+
190
+ Targeted backend tests:
191
+
192
+ ```bash
193
+ uv run --all-packages pytest tests/test_codex_backend.py tests/test_codex_workspace.py tests/test_app.py
194
+ ```
195
+
196
+ Backend compile check:
197
+
198
+ ```bash
199
+ uv run python -m compileall yier_web
200
+ ```
201
+
202
+ Frontend unit tests:
203
+
204
+ ```bash
205
+ cd web
206
+ pnpm test:unit
207
+ ```
208
+
209
+ Frontend type check:
210
+
211
+ ```bash
212
+ cd web
213
+ pnpm type-check
214
+ ```
215
+
216
+ Frontend production build:
217
+
218
+ ```bash
219
+ uv run open-codex-ui-build-web
220
+ ```
221
+
222
+ ## Startup Summary
223
+
224
+ Development:
225
+
226
+ ```bash
227
+ uv run open-codex-ui-dev
228
+ ```
229
+
230
+ Production:
231
+
232
+ ```bash
233
+ export YIER_AUTH_PASSWORD='change-this-password'
234
+ uvx open-codex-ui --host 0.0.0.0 --port 9999
235
+ ```
236
+
237
+ ## Available uv Scripts
238
+
239
+ Development:
240
+
241
+ - `uv run open-codex-ui-dev`: start frontend and backend together
242
+ - `uv run open-codex-ui-dev-web`: start Vite only
243
+ - `uv run open-codex-ui-dev-backend`: start backend only in debug mode
244
+
245
+ Production:
246
+
247
+ - `uvx open-codex-ui`: run the published wheel in production mode
248
+ - `uv run open-codex-ui-build-web`: build frontend assets
249
+ - `uv run open-codex-ui-prod`: start backend in production mode
@@ -0,0 +1,40 @@
1
+ yier_web/__init__.py,sha256=-r2mRpa4wroHvmUsgEW-KHSGwYwKBWBVfacqLSTXBjU,205
2
+ yier_web/app.py,sha256=vL0k_CyXF34QTNgtb8APT4AMU3CMqlnld0wGfgiWDns,7183
3
+ yier_web/auth.py,sha256=69hRO2QbLVE-f77EldFUQz9JXD6EdC9ErUmUWgyjdDE,8533
4
+ yier_web/cli.py,sha256=t6W5E9qBEF4Plf0DhB-YB-RR5S-o_8rz3jyyhBNMZ8k,4487
5
+ yier_web/config.py,sha256=KpoOu5Oq0Roqxga7p8ioeZ3HNNqINNx-UnaI_ON-4OE,25515
6
+ yier_web/directory_picker.py,sha256=clnA-nDqXBWuXxCmylM0tUkUd4k6mtd3Orra8uZVW14,3149
7
+ yier_web/event_stream.py,sha256=G4ekSXUOHyymfBnPJHdR_cPm5dhO9Trty558F4Kpwyc,854
8
+ yier_web/frontend.py,sha256=JDfRqHj3rShl2ASD2Je-5ya7dC2a2jElbsfpZimqKIs,6607
9
+ yier_web/schemas.py,sha256=h-p2CwQqFebaGXBbBBxoUieTMaxhZ6szhubHrgdzrUs,15410
10
+ yier_web/codex/__init__.py,sha256=HOpAEcNgvHUEbRPUxdueiVzR9dHCZcZ8Kdrge-JSDpA,86
11
+ yier_web/codex/ipc_manager.py,sha256=10hd2xDCWms7BrGBU0pAb8rg-Zohquszp104yeGx-Z8,63588
12
+ yier_web/codex/session_events.py,sha256=USaxcbLZ8xm5ZW8JavVRxlPh-O7v2_JAjJdu_ZpnTl4,3423
13
+ yier_web/codex/ws_commands.py,sha256=SCkajF0FIKG7U4ZyIsjjajTmW4PlVZJC9BDbVIu5InE,12130
14
+ yier_web/routes/__init__.py,sha256=e78z7qWu_A8iBECHGSsfkuHgl53o14ck34cm94yRRAI,351
15
+ yier_web/routes/codex.py,sha256=Hg2bao3OP5Pyq2RrZake7ufeUC3Nlc8LWj8giqkAHwA,18994
16
+ yier_web/routes/core.py,sha256=OyLJzX_utJG7X2e1E_uYxlbF9hwkIXDeYw1qLRErq7w,9337
17
+ yier_web/static/favicon.ico,sha256=23SrC3gzjB93j4OYxF9BA8ma6g6EWjEYp3ULTur9NEU,4286
18
+ yier_web/static/favicon.svg,sha256=LD-C-zFycjHm88vsZ8CufvM5FtTgSXBCKrm1aF497-Y,5456
19
+ yier_web/static/index.html,sha256=tw1-6vi7ZkHiVVzELn5Kgxfj28flOEQcTLsjkzoYCGY,461
20
+ yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js,sha256=lw6gRNcZEqy8TbAvchvq43g_KzgxP097BYJ8LG4mNRY,13167
21
+ yier_web/static/assets/CodexView-wpI61iXa.js,sha256=H_2hX3G7Vaje0Kli4_20tei3R8qA0KGWnd-6wpQbwq0,140981
22
+ yier_web/static/assets/LoginView-CELCom2O.js,sha256=OIM2-1NDoICAUfMQWBq2JMdRQo10y2y0gzpEU6TsvAM,25258
23
+ yier_web/static/assets/api-CeihACIV.js,sha256=46tjWnbZqNHmu01V2FywnWjlz2jiqAxSlrp4cbuCVRM,89285
24
+ yier_web/static/assets/index-BdFqJ-Kl.css,sha256=p_DpD0lihpNIfCS7SFdqsWLz7HoRVcntpSYVQNgeeDE,225466
25
+ yier_web/static/assets/index-CjVNk6ja.js,sha256=TFF8oghOBwOo01FHwoMwcRY7QU4d0e3XmkFKYAh36as,262843
26
+ yier_web/static/assets/index-mSBvq1p8.js,sha256=ZI-MHBca6RRdu4FS0nbcFAcB4VlNNaya_GQTdnf4ZlI,7940
27
+ yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg,sha256=LD-C-zFycjHm88vsZ8CufvM5FtTgSXBCKrm1aF497-Y,5456
28
+ yier_web/static/assets/primeicons-C6QP2o4f.woff2,sha256=nupuC8uXTsVo_cse7BgtO1ODo6I4maaQFAssAWeIaII,35148
29
+ yier_web/static/assets/primeicons-DMOk5skT.eot,sha256=mH8j6t-nDGAydqccPJdcjQU02ccZ2Kbj2WUAXnuTdHs,85156
30
+ yier_web/static/assets/primeicons-Dr5RGzOO.svg,sha256=NsVM7PypR6aRPJbfiEMAzhQQjE2MscskGWkyE0L4kMk,342525
31
+ yier_web/static/assets/primeicons-MpK4pl85.ttf,sha256=3Ilf7p884bDq8WtLn6sgB3_CmB5sCzMuTJfBdoMvwFs,84980
32
+ yier_web/static/assets/primeicons-WjwUDZjB.woff,sha256=9dHSGV4FuAhZU6p9ULLncHDNESDgEAqGAHcapboRoB4,85056
33
+ yier_web/static/assets/useCodexWorkspace-6QenDzvb.css,sha256=5zqWEvm9Rk9W_0yFhBM38z9_Z-rQ8kKweqgVj1rNzjk,688
34
+ yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js,sha256=vARGj-G54vKSYdH2VdwcGJww7O4mfMHu2oOebFQ-ORU,535100
35
+ yier_web/static/brand/open-codex-ui-logo.svg,sha256=1b_HKKMW5KJJRFKbMfVMUtYgrvCm9sIIqq34hcigGsk,4659
36
+ open_codex_ui-0.1.4.dist-info/METADATA,sha256=JdXS10nB8qKFgUcdXv7JWCZGjjrX7exGUzql9EL77Bs,5129
37
+ open_codex_ui-0.1.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
38
+ open_codex_ui-0.1.4.dist-info/entry_points.txt,sha256=msxW84jsEug9cqyQb1KtvAH67L6YucmA9kREUoPCSbo,275
39
+ open_codex_ui-0.1.4.dist-info/top_level.txt,sha256=oz3Nm_WgVsyUMc_ygyvELnJvP6nllfVQu1cn_XRHZKU,9
40
+ open_codex_ui-0.1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ open-codex-ui = yier_web.cli:prod
3
+ open-codex-ui-build-web = yier_web.cli:build_web
4
+ open-codex-ui-dev = yier_web.cli:dev
5
+ open-codex-ui-dev-backend = yier_web.cli:dev_backend
6
+ open-codex-ui-dev-web = yier_web.cli:dev_web
7
+ open-codex-ui-prod = yier_web.cli:prod
@@ -0,0 +1 @@
1
+ yier_web
yier_web/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ import logging
2
+ from yier_web.app import AppServices, app, build_services, create_app
3
+
4
+ logging.getLogger("httpx").setLevel(logging.WARNING)
5
+
6
+ __all__ = ["AppServices", "app", "build_services", "create_app"]
yier_web/app.py ADDED
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import asynccontextmanager
5
+ from dataclasses import dataclass
6
+ import os
7
+ from pathlib import Path
8
+ import signal
9
+ from typing import Any, AsyncIterator
10
+
11
+ from litestar import Litestar, Request, Router, get, websocket
12
+ from litestar.connection import WebSocket
13
+ from litestar.datastructures import State
14
+ from litestar.exceptions import HTTPException
15
+ from litestar.logging import LoggingConfig
16
+ from litestar.response import Response
17
+ from litestar.status_codes import HTTP_404_NOT_FOUND
18
+
19
+ from yier_web.auth import AuthService
20
+ from yier_web.config import AppConfigService
21
+ from yier_web.codex.ipc_manager import CodexIpcManager
22
+ from yier_web.directory_picker import LocalDirectoryPickerService
23
+ from yier_web.event_stream import EventStreamBroker
24
+ from yier_web.frontend import FrontendService
25
+ from yier_web.routes import (
26
+ AuthController,
27
+ CodexController,
28
+ ConfigController,
29
+ EventsController,
30
+ HealthController,
31
+ SystemController,
32
+ )
33
+ from yier_web.routes.core import (
34
+ wait_for_event_stream_item as wait_for_event_stream_item,
35
+ )
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class AppServices:
40
+ config_service: AppConfigService
41
+ codex_ipc_manager: CodexIpcManager
42
+ event_broker: EventStreamBroker
43
+ frontend_service: FrontendService
44
+ directory_picker_service: LocalDirectoryPickerService
45
+ auth_service: AuthService
46
+
47
+
48
+ def _env_flag(name: str) -> bool:
49
+ return os.getenv(name, "").strip().lower() not in {
50
+ "",
51
+ "0",
52
+ "false",
53
+ "no",
54
+ "off",
55
+ }
56
+
57
+
58
+ def _build_logging_config(*, debug: bool) -> LoggingConfig:
59
+ return LoggingConfig(
60
+ root={"level": "DEBUG" if debug else "INFO", "handlers": ["queue_listener"]},
61
+ formatters={
62
+ "standard": {
63
+ "format": "%(asctime)s %(levelname)s %(name)s - %(message)s",
64
+ }
65
+ },
66
+ loggers={
67
+ "granian": {"level": "INFO", "handlers": ["queue_listener"]},
68
+ "httpx": {"level": "WARNING", "handlers": ["queue_listener"]},
69
+ "httpcore": {"level": "WARNING", "handlers": ["queue_listener"]},
70
+ "httpcore.connection": {"level": "WARNING", "handlers": ["queue_listener"]},
71
+ "httpcore.http11": {"level": "WARNING", "handlers": ["queue_listener"]},
72
+ },
73
+ log_exceptions="always",
74
+ disable_stack_trace={HTTP_404_NOT_FOUND},
75
+ )
76
+
77
+
78
+ def install_shutdown_signal_bridge(shutdown_event: asyncio.Event) -> None:
79
+ try:
80
+ loop = asyncio.get_running_loop()
81
+ except RuntimeError:
82
+ return
83
+
84
+ if getattr(loop, "_yier_shutdown_signal_bridge_installed", False):
85
+ return
86
+
87
+ signal_handlers = getattr(loop, "_signal_handlers", None)
88
+ if not isinstance(signal_handlers, dict):
89
+ return
90
+
91
+ installed = False
92
+ for signum in (signal.SIGINT, signal.SIGTERM):
93
+ existing_handle = signal_handlers.get(signum)
94
+ if existing_handle is None or not hasattr(existing_handle, "_run"):
95
+ continue
96
+
97
+ def chained_handler(
98
+ handle: Any = existing_handle,
99
+ current_loop: asyncio.AbstractEventLoop = loop,
100
+ ) -> None:
101
+ shutdown_event.set()
102
+ current_loop.call_soon(handle._run)
103
+
104
+ try:
105
+ loop.add_signal_handler(signum, chained_handler)
106
+ except (NotImplementedError, RuntimeError, ValueError):
107
+ continue
108
+ installed = True
109
+
110
+ if installed:
111
+ setattr(loop, "_yier_shutdown_signal_bridge_installed", True)
112
+
113
+
114
+ def build_services(
115
+ project_root: Path | None = None,
116
+ home_dir: Path | None = None,
117
+ ) -> AppServices:
118
+ resolved_root = (project_root or Path.cwd()).resolve()
119
+ config_service = AppConfigService(project_root=resolved_root, home_dir=home_dir)
120
+ event_broker = EventStreamBroker()
121
+ return AppServices(
122
+ config_service=config_service,
123
+ codex_ipc_manager=CodexIpcManager(
124
+ config_service=config_service,
125
+ event_broker=event_broker,
126
+ ),
127
+ event_broker=event_broker,
128
+ frontend_service=FrontendService(
129
+ debug=_env_flag("YIER_DEBUG"),
130
+ ),
131
+ directory_picker_service=LocalDirectoryPickerService(),
132
+ auth_service=AuthService(),
133
+ )
134
+
135
+
136
+ api_router = Router(
137
+ path="/api",
138
+ route_handlers=[
139
+ AuthController,
140
+ HealthController,
141
+ ConfigController,
142
+ SystemController,
143
+ CodexController,
144
+ EventsController,
145
+ ],
146
+ )
147
+
148
+
149
+ @get(path=["/", "/{path:path}"], include_in_schema=False)
150
+ async def frontend_entry(
151
+ request: Request[Any, Any, Any], state: State, path: str = ""
152
+ ) -> Response:
153
+ if path.startswith("api/"):
154
+ raise HTTPException(status_code=HTTP_404_NOT_FOUND)
155
+ return await state.frontend_service.handle_request(request, path)
156
+
157
+
158
+ @websocket(path=["/", "/{path:path}"], include_in_schema=False)
159
+ async def frontend_entry_websocket(
160
+ socket: WebSocket,
161
+ path: str = "",
162
+ ) -> None:
163
+ if path.startswith("api/"):
164
+ await socket.accept()
165
+ await socket.close(code=1008, reason="WebSocket route not found.")
166
+ return
167
+
168
+ await socket.app.state.frontend_service.handle_websocket(socket, path)
169
+
170
+
171
+ def create_app(
172
+ project_root: Path | None = None,
173
+ home_dir: Path | None = None,
174
+ services: AppServices | None = None,
175
+ ) -> Litestar:
176
+ resolved_root = (project_root or Path.cwd()).resolve()
177
+ debug = _env_flag("YIER_DEBUG")
178
+ app_services = services or build_services(
179
+ project_root=resolved_root, home_dir=home_dir
180
+ )
181
+
182
+ async def before_request(request: Request[Any, Any, Any]) -> Response | None:
183
+ auth_service = app_services.auth_service
184
+ if not auth_service.enabled:
185
+ return None
186
+
187
+ request_path = request.url.path
188
+ if auth_service.is_public_path(request_path):
189
+ return None
190
+ if auth_service.is_authenticated(request):
191
+ return None
192
+ if request_path.startswith("/api/"):
193
+ return auth_service.build_unauthorized_api_response()
194
+ return auth_service.build_login_redirect(request)
195
+
196
+ @asynccontextmanager
197
+ async def lifespan(app: Litestar) -> AsyncIterator[None]:
198
+ shutdown_event = asyncio.Event()
199
+ install_shutdown_signal_bridge(shutdown_event)
200
+ app.state.shutdown_event = shutdown_event
201
+ app.state.config_service = app_services.config_service
202
+ app.state.codex_ipc_manager = app_services.codex_ipc_manager
203
+ app.state.event_broker = app_services.event_broker
204
+ app.state.frontend_service = app_services.frontend_service
205
+ app.state.directory_picker_service = app_services.directory_picker_service
206
+ app.state.auth_service = app_services.auth_service
207
+ await app_services.codex_ipc_manager.start()
208
+ try:
209
+ yield
210
+ finally:
211
+ shutdown_event.set()
212
+ await app_services.codex_ipc_manager.stop()
213
+
214
+ return Litestar(
215
+ route_handlers=[api_router, frontend_entry, frontend_entry_websocket],
216
+ before_request=before_request,
217
+ lifespan=[lifespan],
218
+ debug=debug,
219
+ logging_config=_build_logging_config(debug=debug),
220
+ )
221
+
222
+
223
+ app = create_app()