llming-stage 0.1.1__tar.gz
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.
- llming_stage-0.1.1/LICENSE +21 -0
- llming_stage-0.1.1/PKG-INFO +147 -0
- llming_stage-0.1.1/README.md +123 -0
- llming_stage-0.1.1/THIRD_PARTY.md +50 -0
- llming_stage-0.1.1/llming_stage/__init__.py +34 -0
- llming_stage-0.1.1/llming_stage/asset_server.py +131 -0
- llming_stage-0.1.1/llming_stage/assets/noto-emoji.zip +0 -0
- llming_stage-0.1.1/llming_stage/assets/phosphor-icons.zip +0 -0
- llming_stage-0.1.1/llming_stage/assets/tabler-icons.zip +0 -0
- llming_stage-0.1.1/llming_stage/cli.py +76 -0
- llming_stage-0.1.1/llming_stage/dev_reload.py +457 -0
- llming_stage-0.1.1/llming_stage/fonts/01d40c9fd448face.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/042d031f00f025d3.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/0953a0ea5ec2fbe6.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/096bf4e001f30f3d.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/0c19a63c7528cc1a.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/0ca83f77c744e2a2.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/18cea8b33059950e.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/1e891d4a2a7f2f96.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/28b81f220ca048e2.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/2af55f54d9fac778.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/2c8ec1765f13887d.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/2e2ca037aef7b51d.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/334116c5190f1716.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/34d83f10ce3fa7b4.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/425b348f7eab066c.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/47c853f95b759bef.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/4ad9ee1c03eed305.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/4c401738b2e90c99.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/55a208f32a6be540.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/5f02a1396c9fc305.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/616907143b870cb1.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/62873223be6b39e0.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/66c1c422e62faf1d.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/6d6ebdc60d2cd01e.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/707a7c6749d7c089.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/7104372e0a34311e.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/7420c823209e4609.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/7c7f3bb965e2deae.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/7f5c1101832b02f5.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/81471017108054b9.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/851ed7636af29416.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/8d21fc3a9d063f00.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/8e146ffc255e03c3.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/935e23d0cd87ac6f.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/9b5609bd55cc78e3.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/a35a05f781345176.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/ab8cf1fb84e33cd6.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/afd447df78322d0d.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/bb828b03a1f11130.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/c3f2f68bb77a7944.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/c854362e488d0bec.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/cf8e1fe457b4cc05.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/d4e71f49ac06b20e.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/d5ec90d9ff9fdc02.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/dcbef151cf127173.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/e62204447c1ed92a.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/ea7496e801e7e453.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/f3d429b4c95dd3ce.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/fdbd937e991f101f.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/fonts.css +506 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
- llming_stage-0.1.1/llming_stage/lang/ar-TN.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ar.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/az-Latn.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/bg.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/bn.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/bs-BA.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ca.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/cs.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/da.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/de-CH.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/de-DE.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/de.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/el.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/en-GB.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/en-US.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/eo.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/es.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/et.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/eu.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/fa-IR.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/fa.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/fi.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/fr.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/gn.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/he.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/hi.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/hr.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/hu.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/id.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/is.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/it.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ja.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/kk.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/km.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ko-KR.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/kur-CKB.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/lt.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/lu.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/lv.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/mk.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ml.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/mm.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ms-MY.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ms.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/my.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/nb-NO.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/nl.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/pl.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/pt-BR.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/pt.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ro.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ru.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sk.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sl.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sm.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sq.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sr-CYR.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sr.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/sv.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ta.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/th.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/tl.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/tr.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ug.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/uk.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/ur-PK.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/uz-Cyrl.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/uz-Latn.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/vi.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/zh-CN.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/lang/zh-TW.umd.prod.js +7 -0
- llming_stage-0.1.1/llming_stage/shell.py +317 -0
- llming_stage-0.1.1/llming_stage/stage.py +875 -0
- llming_stage-0.1.1/llming_stage/static/loader.js +236 -0
- llming_stage-0.1.1/llming_stage/static/router.js +95 -0
- llming_stage-0.1.1/llming_stage/vendor/codemirror-addon-closebrackets.js +201 -0
- llming_stage-0.1.1/llming_stage/vendor/codemirror-addon-matchbrackets.js +160 -0
- llming_stage-0.1.1/llming_stage/vendor/codemirror-mode-javascript.js +960 -0
- llming_stage-0.1.1/llming_stage/vendor/codemirror.css +344 -0
- llming_stage-0.1.1/llming_stage/vendor/codemirror.js +9884 -0
- llming_stage-0.1.1/llming_stage/vendor/dompurify.min.js +3 -0
- llming_stage-0.1.1/llming_stage/vendor/drawflow.min.css +1 -0
- llming_stage-0.1.1/llming_stage/vendor/drawflow.min.js +1 -0
- llming_stage-0.1.1/llming_stage/vendor/echarts.min.js +45 -0
- llming_stage-0.1.1/llming_stage/vendor/katex.min.css +1 -0
- llming_stage-0.1.1/llming_stage/vendor/katex.min.js +1 -0
- llming_stage-0.1.1/llming_stage/vendor/marked.umd.js +79 -0
- llming_stage-0.1.1/llming_stage/vendor/mermaid.min.js +3298 -0
- llming_stage-0.1.1/llming_stage/vendor/plotly-basic.min.js +8 -0
- llming_stage-0.1.1/llming_stage/vendor/plotly-full.min.js +3882 -0
- llming_stage-0.1.1/llming_stage/vendor/quasar.prod.css +1 -0
- llming_stage-0.1.1/llming_stage/vendor/quasar.umd.prod.js +127 -0
- llming_stage-0.1.1/llming_stage/vendor/tailwindcss.browser.global.js +947 -0
- llming_stage-0.1.1/llming_stage/vendor/three-orbit-controls.module.js +1963 -0
- llming_stage-0.1.1/llming_stage/vendor/three.core.min.js +6 -0
- llming_stage-0.1.1/llming_stage/vendor/three.module.min.js +6 -0
- llming_stage-0.1.1/llming_stage/vendor/vue.global.prod.js +9 -0
- llming_stage-0.1.1/llming_stage/vendor/xterm-addon-fit.js +2 -0
- llming_stage-0.1.1/llming_stage/vendor/xterm-addon-web-links.js +2 -0
- llming_stage-0.1.1/llming_stage/vendor/xterm-addon-webgl.js +2 -0
- llming_stage-0.1.1/llming_stage/vendor/xterm.css +285 -0
- llming_stage-0.1.1/llming_stage/vendor/xterm.js +2 -0
- llming_stage-0.1.1/llming_stage/zip_server.py +95 -0
- llming_stage-0.1.1/pyproject.toml +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Ikemann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: llming-stage
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Shared frontend foundation and SPA app shell for the llming ecosystem — vendor JS, fonts, icons, and a lazy-loading client-side router
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: spa,frontend,assets,vue,quasar,starlette,lazy-loading
|
|
7
|
+
Author: Michael Ikemann
|
|
8
|
+
Requires-Python: >=3.14,<4
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Dist: llming-com (>=0.1.5,<0.2)
|
|
17
|
+
Requires-Dist: starlette (>=0.27)
|
|
18
|
+
Requires-Dist: uvicorn[standard] (>=0.27)
|
|
19
|
+
Project-URL: Author, https://github.com/Alyxion
|
|
20
|
+
Project-URL: Homepage, https://github.com/Alyxion/llming-stage
|
|
21
|
+
Project-URL: Repository, https://github.com/Alyxion/llming-stage
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<p align="center"><img src="https://raw.githubusercontent.com/Alyxion/llming-stage/main/media/logo-small.png" alt="LLMing Stage" width="400"></p>
|
|
25
|
+
|
|
26
|
+
# llming-stage
|
|
27
|
+
|
|
28
|
+
[](https://www.python.org/downloads/)
|
|
29
|
+
[](https://github.com/Alyxion/llming-stage/blob/main/LICENSE)
|
|
30
|
+
[](https://pypi.org/project/llming-stage/)
|
|
31
|
+
[](https://github.com/astral-sh/ruff)
|
|
32
|
+
|
|
33
|
+
### Build Vue + Quasar UIs that AI can build, drive, and debug.
|
|
34
|
+
|
|
35
|
+
`llming-stage` is the SPA foundation for AI-assisted frontend work. You write a real Vue + Quasar app, not a Python shim. Reactive traffic and sessions flow through [`llming-com`](https://github.com/Alyxion/llming-com), and that's the same channel an AI assistant uses to inspect state, invoke commands, and push events into the running page.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
flowchart LR
|
|
39
|
+
User(["👤 User"]) --> Browser
|
|
40
|
+
AI(["🤖 AI assistant"]) <--> Com
|
|
41
|
+
Browser["<b>Browser</b><br/>Vue + Quasar SPA<br/>(llming-stage shell)"]
|
|
42
|
+
Com["<b>Python server</b><br/>llming-com<br/>sessions · auth · commands<br/>HTTP / MCP debug API"]
|
|
43
|
+
Browser <-->|"per-user WebSocket<br/>reactive commands · server pushes"| Com
|
|
44
|
+
|
|
45
|
+
classDef live fill:#1976d2,stroke:#1565c0,color:#fff,stroke-width:1.5px
|
|
46
|
+
classDef ai fill:#a855f7,stroke:#7c3aed,color:#fff,stroke-width:1.5px
|
|
47
|
+
class Browser,Com live
|
|
48
|
+
class AI ai
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`llming-com` ships the sessions, auth, command dispatcher, and the debug surface — without it, the AI side of the picture goes away. When you eventually deploy and the app no longer needs server reactivity, the same shell + view modules can ship as a static bundle to any CDN.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### What you get
|
|
56
|
+
|
|
57
|
+
- **An AI-debuggable runtime** — every reactive command, session, and event is browseable, invokable, and observable through one HTTP / MCP surface.
|
|
58
|
+
- **FastAPI-native app mounting** — create your own `FastAPI()` app and attach `Stage(app)`, or let `Stage()` create the default FastAPI app for compact demos. The internal `/_stage` routes and development reload are ensured once; server startup stays normal FastAPI/ASGI (`uvicorn main:app --reload`, deployment servers, etc.).
|
|
59
|
+
- **Modern frontend, zero boilerplate** — Vue 3 + Quasar 2 + bundled Tailwind utilities with a lazy-load orchestrator and an SPA router that keeps the WebSocket and view state alive across navigations.
|
|
60
|
+
- **Per-user sessions out of the box** — `llming-com` runs the wire and the auth; you write JS views and Python handlers.
|
|
61
|
+
- **Static-deployable** — when there's no server-side reactivity at runtime, the same code ships to GitHub Pages, S3, or any CDN.
|
|
62
|
+
- **No third-party network** — every asset is vendored. A page loaded from an `llming-stage` app makes zero requests to Google Fonts, jsDelivr, or any other external host. Privacy/GDPR-friendly by default; enforced by static and runtime tests.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### See it in action
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
poetry install
|
|
70
|
+
./samples/run.sh # opens a web gallery at http://localhost:8000
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Fourteen sample apps — from a tiny static app to generated decorator views, llming-com reactive loops, a Three.js particle tornado, an 8-chart ECharts dashboard, Plotly full-bundle charts, a core component workbench, and an optional-extension workbench — with dark/light theme, hot reload, and AI-debug control.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Minimal app
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from llming_stage import Stage
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
Stage(title="Hello world").add_view("/", "hello.vue").run()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```vue
|
|
87
|
+
<template>
|
|
88
|
+
<main class="min-h-screen grid place-items-center p-8">
|
|
89
|
+
<h1 class="text-5xl font-bold">Hello llming-stage</h1>
|
|
90
|
+
</main>
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For a purely static app, no Python file is needed:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
llming-stage serve hello.vue
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Stage()` mounts the bundled assets, the Vue + Quasar shell, the SPA
|
|
101
|
+
router, bundled Tailwind utilities, and content-hash development reload
|
|
102
|
+
by default.
|
|
103
|
+
|
|
104
|
+
`stage.run()` is a thin local-development wrapper around `uvicorn.run`.
|
|
105
|
+
If you need workers, custom logging, TLS, or deployment process
|
|
106
|
+
management, run the same app directly with normal ASGI tooling.
|
|
107
|
+
|
|
108
|
+
Reactive apps add a typed session router and let Stage mount the
|
|
109
|
+
conventional session routes:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from fastapi import FastAPI
|
|
113
|
+
from llming_stage import Stage
|
|
114
|
+
|
|
115
|
+
app = FastAPI()
|
|
116
|
+
stage = Stage(app)
|
|
117
|
+
sessions = stage.session()
|
|
118
|
+
counter = sessions.add_router("counter")
|
|
119
|
+
|
|
120
|
+
@counter.handler("inc")
|
|
121
|
+
async def inc(session, by: int = 1):
|
|
122
|
+
value = int(session.state.get("count", 0)) + by
|
|
123
|
+
session.state["count"] = value
|
|
124
|
+
await session.call("home.setCounter", value)
|
|
125
|
+
return {"ok": True}
|
|
126
|
+
|
|
127
|
+
stage.add_view("/", "home.vue")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The browser gets a real Vue + Quasar SPA from `.vue` view files.
|
|
131
|
+
`llming-com` carries the wire, the sessions, and the debug surface the
|
|
132
|
+
AI uses.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### Learn more
|
|
137
|
+
|
|
138
|
+
Full docs in [`docs/content/`](docs/content):
|
|
139
|
+
|
|
140
|
+
- [Quick start](docs/content/installation.md) · [Stage apps](docs/content/stage.md) · [App shell](docs/content/shell.md) · [Communication model](docs/content/communication-model.md)
|
|
141
|
+
- [llming-com integration](docs/content/llming-com.md) · [Assets & lazy loading](docs/content/lazy-loading.md)
|
|
142
|
+
- [Security](docs/content/security.md) · [API reference](docs/content/api.md)
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
MIT licensed. © 2026 [Michael Ikemann](https://github.com/Alyxion). Bundled third-party files are listed in [`THIRD_PARTY.md`](THIRD_PARTY.md); no AGPL/GPL/LGPL is ever permitted.
|
|
147
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<p align="center"><img src="https://raw.githubusercontent.com/Alyxion/llming-stage/main/media/logo-small.png" alt="LLMing Stage" width="400"></p>
|
|
2
|
+
|
|
3
|
+
# llming-stage
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://github.com/Alyxion/llming-stage/blob/main/LICENSE)
|
|
7
|
+
[](https://pypi.org/project/llming-stage/)
|
|
8
|
+
[](https://github.com/astral-sh/ruff)
|
|
9
|
+
|
|
10
|
+
### Build Vue + Quasar UIs that AI can build, drive, and debug.
|
|
11
|
+
|
|
12
|
+
`llming-stage` is the SPA foundation for AI-assisted frontend work. You write a real Vue + Quasar app, not a Python shim. Reactive traffic and sessions flow through [`llming-com`](https://github.com/Alyxion/llming-com), and that's the same channel an AI assistant uses to inspect state, invoke commands, and push events into the running page.
|
|
13
|
+
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart LR
|
|
16
|
+
User(["👤 User"]) --> Browser
|
|
17
|
+
AI(["🤖 AI assistant"]) <--> Com
|
|
18
|
+
Browser["<b>Browser</b><br/>Vue + Quasar SPA<br/>(llming-stage shell)"]
|
|
19
|
+
Com["<b>Python server</b><br/>llming-com<br/>sessions · auth · commands<br/>HTTP / MCP debug API"]
|
|
20
|
+
Browser <-->|"per-user WebSocket<br/>reactive commands · server pushes"| Com
|
|
21
|
+
|
|
22
|
+
classDef live fill:#1976d2,stroke:#1565c0,color:#fff,stroke-width:1.5px
|
|
23
|
+
classDef ai fill:#a855f7,stroke:#7c3aed,color:#fff,stroke-width:1.5px
|
|
24
|
+
class Browser,Com live
|
|
25
|
+
class AI ai
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`llming-com` ships the sessions, auth, command dispatcher, and the debug surface — without it, the AI side of the picture goes away. When you eventually deploy and the app no longer needs server reactivity, the same shell + view modules can ship as a static bundle to any CDN.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### What you get
|
|
33
|
+
|
|
34
|
+
- **An AI-debuggable runtime** — every reactive command, session, and event is browseable, invokable, and observable through one HTTP / MCP surface.
|
|
35
|
+
- **FastAPI-native app mounting** — create your own `FastAPI()` app and attach `Stage(app)`, or let `Stage()` create the default FastAPI app for compact demos. The internal `/_stage` routes and development reload are ensured once; server startup stays normal FastAPI/ASGI (`uvicorn main:app --reload`, deployment servers, etc.).
|
|
36
|
+
- **Modern frontend, zero boilerplate** — Vue 3 + Quasar 2 + bundled Tailwind utilities with a lazy-load orchestrator and an SPA router that keeps the WebSocket and view state alive across navigations.
|
|
37
|
+
- **Per-user sessions out of the box** — `llming-com` runs the wire and the auth; you write JS views and Python handlers.
|
|
38
|
+
- **Static-deployable** — when there's no server-side reactivity at runtime, the same code ships to GitHub Pages, S3, or any CDN.
|
|
39
|
+
- **No third-party network** — every asset is vendored. A page loaded from an `llming-stage` app makes zero requests to Google Fonts, jsDelivr, or any other external host. Privacy/GDPR-friendly by default; enforced by static and runtime tests.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### See it in action
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
poetry install
|
|
47
|
+
./samples/run.sh # opens a web gallery at http://localhost:8000
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Fourteen sample apps — from a tiny static app to generated decorator views, llming-com reactive loops, a Three.js particle tornado, an 8-chart ECharts dashboard, Plotly full-bundle charts, a core component workbench, and an optional-extension workbench — with dark/light theme, hot reload, and AI-debug control.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Minimal app
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from llming_stage import Stage
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
Stage(title="Hello world").add_view("/", "hello.vue").run()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<template>
|
|
65
|
+
<main class="min-h-screen grid place-items-center p-8">
|
|
66
|
+
<h1 class="text-5xl font-bold">Hello llming-stage</h1>
|
|
67
|
+
</main>
|
|
68
|
+
</template>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For a purely static app, no Python file is needed:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
llming-stage serve hello.vue
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`Stage()` mounts the bundled assets, the Vue + Quasar shell, the SPA
|
|
78
|
+
router, bundled Tailwind utilities, and content-hash development reload
|
|
79
|
+
by default.
|
|
80
|
+
|
|
81
|
+
`stage.run()` is a thin local-development wrapper around `uvicorn.run`.
|
|
82
|
+
If you need workers, custom logging, TLS, or deployment process
|
|
83
|
+
management, run the same app directly with normal ASGI tooling.
|
|
84
|
+
|
|
85
|
+
Reactive apps add a typed session router and let Stage mount the
|
|
86
|
+
conventional session routes:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from fastapi import FastAPI
|
|
90
|
+
from llming_stage import Stage
|
|
91
|
+
|
|
92
|
+
app = FastAPI()
|
|
93
|
+
stage = Stage(app)
|
|
94
|
+
sessions = stage.session()
|
|
95
|
+
counter = sessions.add_router("counter")
|
|
96
|
+
|
|
97
|
+
@counter.handler("inc")
|
|
98
|
+
async def inc(session, by: int = 1):
|
|
99
|
+
value = int(session.state.get("count", 0)) + by
|
|
100
|
+
session.state["count"] = value
|
|
101
|
+
await session.call("home.setCounter", value)
|
|
102
|
+
return {"ok": True}
|
|
103
|
+
|
|
104
|
+
stage.add_view("/", "home.vue")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The browser gets a real Vue + Quasar SPA from `.vue` view files.
|
|
108
|
+
`llming-com` carries the wire, the sessions, and the debug surface the
|
|
109
|
+
AI uses.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Learn more
|
|
114
|
+
|
|
115
|
+
Full docs in [`docs/content/`](docs/content):
|
|
116
|
+
|
|
117
|
+
- [Quick start](docs/content/installation.md) · [Stage apps](docs/content/stage.md) · [App shell](docs/content/shell.md) · [Communication model](docs/content/communication-model.md)
|
|
118
|
+
- [llming-com integration](docs/content/llming-com.md) · [Assets & lazy loading](docs/content/lazy-loading.md)
|
|
119
|
+
- [Security](docs/content/security.md) · [API reference](docs/content/api.md)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
MIT licensed. © 2026 [Michael Ikemann](https://github.com/Alyxion). Bundled third-party files are listed in [`THIRD_PARTY.md`](THIRD_PARTY.md); no AGPL/GPL/LGPL is ever permitted.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Third-Party Resources
|
|
2
|
+
|
|
3
|
+
All bundled third-party resources and their licenses. This file MUST be updated
|
|
4
|
+
whenever a vendored file or asset is added, removed, or upgraded.
|
|
5
|
+
|
|
6
|
+
No AGPL, GPL, or LGPL resource is ever permitted in this repository.
|
|
7
|
+
|
|
8
|
+
## Vendor Libraries (`llming_stage/vendor/`)
|
|
9
|
+
|
|
10
|
+
| Resource | Version | License | Source | Purpose |
|
|
11
|
+
|----------|---------|---------|--------|---------|
|
|
12
|
+
| Vue.js | 3.x | MIT | https://github.com/vuejs/core | Reactive UI framework |
|
|
13
|
+
| Quasar Framework | 2.x | MIT | https://github.com/quasarframework/quasar | UI component library |
|
|
14
|
+
| Tailwind CSS browser bundle | 4.2.4 | MIT | https://registry.npmjs.org/@tailwindcss/browser/-/browser-4.2.4.tgz (sha1 `e43e14e347d7b68a6c3afc7d77d8c18a335e98c3`) | Utility CSS runtime. The shell imports theme + utilities only, not preflight, to avoid global resets. |
|
|
15
|
+
| Three.js | 0.184.0 | MIT | https://registry.npmjs.org/three/-/three-0.184.0.tgz (sha1 `5bca0a3851eea5345e4c205567b40dfa49b791b5`) | 3D rendering. Ships as two files — `three.module.min.js` re-exports from the co-located `three.core.min.js`; both are required. |
|
|
16
|
+
| Three.js OrbitControls | 0.184.0 | MIT | `examples/jsm/controls/OrbitControls.js` from the same Three.js tarball | Mouse / touch / wheel camera controls (vendored as `three-orbit-controls.module.js`). |
|
|
17
|
+
| Apache ECharts | 6.0.0 | Apache 2.0 | https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz (sha1 `2935aa7751c282d1abbbf7d719d397199a15b9e7`) | Chart library — line, bar, pie, gauge, radar, heatmap, scatter, candlestick. |
|
|
18
|
+
| Plotly.js (basic) | 2.x | MIT | https://github.com/plotly/plotly.js | Charts and data visualization (lighter bundle — scatter, bar, pie). |
|
|
19
|
+
| Plotly.js (full) | 3.5.0 | MIT | https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-3.5.0.tgz (sha1 `d81d1c77dd93c927de2f84cdcd3f8697fb47583b`) | Full Plotly bundle — every chart type incl. heatmap, candlestick, choropleth. Loaded only when an app calls `__stage.load('plotly/full')`. |
|
|
20
|
+
| KaTeX | 0.16.45 | MIT | https://registry.npmjs.org/katex/-/katex-0.16.45.tgz (sha1 `ba60d39c54746b6b8d39ce0e7f6eace07143149c`) | LaTeX math rendering. Ships `katex.min.{js,css}` plus 20 woff2 font files under `fonts/katex/` (CSS rewritten to `url(../fonts/katex/…)` so every font ends up under the unified `/_stage/fonts/` URL tree). |
|
|
21
|
+
| Mermaid | 11.14.0 | MIT | https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz (sha1 `ce81b22bc10f3117ef7737406ef2d10ee1741769`) | Diagram rendering — UMD bundle (`mermaid.min.js`) so a single fetch covers every diagram type. |
|
|
22
|
+
| marked | 18.0.2 | MIT | https://registry.npmjs.org/marked/-/marked-18.0.2.tgz (sha1 `180cb158a2d2dc377821cfb088a10ca1b5630ef0`) | Markdown parser (UMD). |
|
|
23
|
+
| DOMPurify | 3.x | Apache 2.0 / MPL 2.0 | https://github.com/cure53/DOMPurify | HTML sanitization |
|
|
24
|
+
| xterm.js | 6.0.0 | MIT | https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz (sha1 `93637b0f2ee3a70718b5746a27c9c506af16745b`) | Terminal emulator. |
|
|
25
|
+
| xterm.js fit addon | 0.11.0 | MIT | https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz (sha1 `ba4778b69fcc9044a060c2176bbe077657d7b37e`) | Terminal autosize. |
|
|
26
|
+
| xterm.js web-links addon | 0.12.0 | MIT | https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz (sha1 `bc34ae46f4c12012256d451ac2b9be791c0b6bfa`) | Click-through URL detection. |
|
|
27
|
+
| xterm.js webgl addon | 0.19.0 | MIT | https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz (sha1 `02533c3f7c0af3ac9a44bbb095cbd47d48e90c25`) | GPU-accelerated terminal renderer. |
|
|
28
|
+
| Drawflow | 0.0.60 | MIT | https://registry.npmjs.org/drawflow/-/drawflow-0.0.60.tgz (sha1 `313eadb43190a04ebba16d2cb49d1c9516030a15`) | Visual node-graph editor. |
|
|
29
|
+
| CodeMirror 5 | 5.65.21 | MIT | https://registry.npmjs.org/codemirror/-/codemirror-5.65.21.tgz (sha1 `cacf320606c5450ad3b3da34bb9c666afec21068`) | In-browser code editor. Vendored files: `codemirror.{js,css}`, `codemirror-mode-javascript.js`, `codemirror-addon-matchbrackets.js`, `codemirror-addon-closebrackets.js`. |
|
|
30
|
+
|
|
31
|
+
## Fonts (`llming_stage/fonts/`)
|
|
32
|
+
|
|
33
|
+
| Resource | Version | License | Source | Purpose |
|
|
34
|
+
|----------|---------|---------|--------|---------|
|
|
35
|
+
| Roboto | 3.x | Apache 2.0 | https://github.com/googlefonts/roboto | Default UI font |
|
|
36
|
+
| Material Icons | 4.x | Apache 2.0 | https://github.com/google/material-design-icons | UI icons (filled, outlined, round, sharp) |
|
|
37
|
+
| Material Symbols | 1.x | Apache 2.0 | https://github.com/google/material-design-icons | UI icons (outlined, rounded, sharp) |
|
|
38
|
+
|
|
39
|
+
## Icon & Emoji Sets (`llming_stage/assets/*.zip`)
|
|
40
|
+
|
|
41
|
+
| Resource | Version | License | Source | Purpose |
|
|
42
|
+
|----------|---------|---------|--------|---------|
|
|
43
|
+
| Phosphor Icons | 2.x | MIT | https://github.com/phosphor-icons/core | UI icons (6 weights: bold, duotone, fill, light, regular, thin) |
|
|
44
|
+
| Noto Emoji | 15.x | SIL OFL 1.1 / Apache 2.0 | https://github.com/googlefonts/noto-emoji | Emoji rendering |
|
|
45
|
+
| Tabler Icons | 3.41.1 | MIT | https://registry.npmjs.org/@tabler/icons/-/icons-3.41.1.tgz (sha1 `dff9c77af287c73c2fcab4074cfa9f2069f9b7ee`) | UI icons (~6 000 SVGs, outline + filled). Served via `tabler-icons.zip` at `/_stage/tabler/<style>/<name>.svg`. |
|
|
46
|
+
|
|
47
|
+
## Quasar Locale Packs (`llming_stage/lang/`)
|
|
48
|
+
|
|
49
|
+
Bundled with Quasar — MIT licensed, sourced from the Quasar distribution
|
|
50
|
+
(https://github.com/quasarframework/quasar).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""llming-stage — shared frontend foundation and SPA app shell.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- :func:`mount_assets` — attach path-hardened asset routes to any
|
|
5
|
+
Starlette/FastAPI app.
|
|
6
|
+
- :func:`mount_shell` — serve the SPA shell HTML document.
|
|
7
|
+
- :func:`mount_dev_reload` — attach development-only reload routes.
|
|
8
|
+
- :class:`Stage` — OOP helper for FastAPI-native stage apps.
|
|
9
|
+
- :class:`StageSession` — Stage-owned llming-com session wiring.
|
|
10
|
+
- :class:`VueResponse` — response type for generated Vue views.
|
|
11
|
+
- :class:`ShellConfig` — configuration dataclass for the shell.
|
|
12
|
+
- :func:`render_shell` — render the shell HTML (for custom wiring).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .dev_reload import DevReloadConfig, dev_reload_head, mount_dev_reload
|
|
18
|
+
from .shell import ShellConfig, mount_assets, mount_shell, render_shell
|
|
19
|
+
from .stage import Stage, StageSession, VueResponse
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DevReloadConfig",
|
|
23
|
+
"Stage",
|
|
24
|
+
"StageSession",
|
|
25
|
+
"ShellConfig",
|
|
26
|
+
"VueResponse",
|
|
27
|
+
"dev_reload_head",
|
|
28
|
+
"mount_assets",
|
|
29
|
+
"mount_dev_reload",
|
|
30
|
+
"mount_shell",
|
|
31
|
+
"render_shell",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Path-hardened static asset serving.
|
|
2
|
+
|
|
3
|
+
Each asset group (vendor, fonts, lang) is a separate whitelist-scoped
|
|
4
|
+
handler. No generic file serving, no catch-all static mount.
|
|
5
|
+
|
|
6
|
+
Security guarantees
|
|
7
|
+
-------------------
|
|
8
|
+
1. **Whitelist-only routing** — each route maps to one fixed root directory.
|
|
9
|
+
2. **Path canonicalization** — requested paths are resolved against the root;
|
|
10
|
+
requests that escape the root (via ``..``, absolute paths, or symlinks)
|
|
11
|
+
return 403/404.
|
|
12
|
+
3. **Dangerous pattern rejection** — ``..``, backslashes, null bytes, and
|
|
13
|
+
non-ASCII control characters are rejected before the filesystem is touched.
|
|
14
|
+
4. **Extension whitelist** — only ``.js``, ``.css``, ``.woff2``, ``.svg``,
|
|
15
|
+
``.json``, ``.mjs`` are served. Anything else is 404.
|
|
16
|
+
5. **No directory listing** — directory requests return 404.
|
|
17
|
+
6. **Symlink rejection** — any path component that is a symlink pointing
|
|
18
|
+
outside the root is rejected.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import mimetypes
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from starlette.requests import Request
|
|
27
|
+
from starlette.responses import FileResponse, Response
|
|
28
|
+
|
|
29
|
+
ALLOWED_EXTENSIONS: frozenset[str] = frozenset(
|
|
30
|
+
{".js", ".mjs", ".css", ".woff2", ".svg", ".json"}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_MIME_OVERRIDES: dict[str, str] = {
|
|
34
|
+
".js": "application/javascript; charset=utf-8",
|
|
35
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
36
|
+
".css": "text/css; charset=utf-8",
|
|
37
|
+
".woff2": "font/woff2",
|
|
38
|
+
".svg": "image/svg+xml; charset=utf-8",
|
|
39
|
+
".json": "application/json; charset=utf-8",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_FORBIDDEN_SUBSTRINGS: tuple[str, ...] = ("..", "\\", "\x00")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_relative_path(path: str) -> str | None:
|
|
46
|
+
"""Return the cleaned path, or ``None`` if the input is unsafe.
|
|
47
|
+
|
|
48
|
+
Applies only textual checks — the caller must still resolve against a
|
|
49
|
+
root and verify containment (see :func:`resolve_under_root`).
|
|
50
|
+
"""
|
|
51
|
+
if not path:
|
|
52
|
+
return None
|
|
53
|
+
if path.startswith("/"):
|
|
54
|
+
return None
|
|
55
|
+
for bad in _FORBIDDEN_SUBSTRINGS:
|
|
56
|
+
if bad in path:
|
|
57
|
+
return None
|
|
58
|
+
for ch in path:
|
|
59
|
+
code = ord(ch)
|
|
60
|
+
if code < 0x20 or code == 0x7F:
|
|
61
|
+
return None
|
|
62
|
+
for segment in path.split("/"):
|
|
63
|
+
if not segment or segment in (".", ".."):
|
|
64
|
+
return None
|
|
65
|
+
return path
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_under_root(root: Path, rel: str) -> Path | None:
|
|
69
|
+
"""Resolve *rel* against *root*, ensuring the result stays inside.
|
|
70
|
+
|
|
71
|
+
Returns ``None`` if containment cannot be verified, if any component
|
|
72
|
+
is a symlink pointing outside *root*, or if the resolved path does not
|
|
73
|
+
exist as a regular file.
|
|
74
|
+
"""
|
|
75
|
+
root_resolved = root.resolve(strict=True)
|
|
76
|
+
candidate = (root_resolved / rel).resolve(strict=False)
|
|
77
|
+
try:
|
|
78
|
+
candidate.relative_to(root_resolved)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return None
|
|
81
|
+
if not candidate.exists():
|
|
82
|
+
return None
|
|
83
|
+
if not candidate.is_file():
|
|
84
|
+
return None
|
|
85
|
+
current = root_resolved
|
|
86
|
+
for part in Path(rel).parts:
|
|
87
|
+
current = current / part
|
|
88
|
+
try:
|
|
89
|
+
if current.is_symlink():
|
|
90
|
+
real = current.resolve(strict=True)
|
|
91
|
+
real.relative_to(root_resolved)
|
|
92
|
+
except (OSError, ValueError):
|
|
93
|
+
return None
|
|
94
|
+
return candidate
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _media_type_for(path: Path) -> str:
|
|
98
|
+
suffix = path.suffix.lower()
|
|
99
|
+
if suffix in _MIME_OVERRIDES:
|
|
100
|
+
return _MIME_OVERRIDES[suffix]
|
|
101
|
+
guessed, _ = mimetypes.guess_type(path.name)
|
|
102
|
+
return guessed or "application/octet-stream"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def make_dir_handler(root: Path, cache_max_age: int = 31536000):
|
|
106
|
+
"""Build an async Starlette handler that serves files under *root*.
|
|
107
|
+
|
|
108
|
+
The handler reads the relative path from the ``path`` route parameter,
|
|
109
|
+
validates it, resolves it under *root*, and returns the file — or 404
|
|
110
|
+
on any failure. Never returns 403: "not found" is the single observable
|
|
111
|
+
outcome for every rejection path, to avoid leaking structure.
|
|
112
|
+
"""
|
|
113
|
+
root = root.resolve(strict=True)
|
|
114
|
+
cache_control = f"public, max-age={cache_max_age}, immutable"
|
|
115
|
+
|
|
116
|
+
async def handler(request: Request) -> Response:
|
|
117
|
+
rel = validate_relative_path(request.path_params.get("path", ""))
|
|
118
|
+
if rel is None:
|
|
119
|
+
return Response(status_code=404)
|
|
120
|
+
if Path(rel).suffix.lower() not in ALLOWED_EXTENSIONS:
|
|
121
|
+
return Response(status_code=404)
|
|
122
|
+
resolved = resolve_under_root(root, rel)
|
|
123
|
+
if resolved is None:
|
|
124
|
+
return Response(status_code=404)
|
|
125
|
+
return FileResponse(
|
|
126
|
+
resolved,
|
|
127
|
+
media_type=_media_type_for(resolved),
|
|
128
|
+
headers={"Cache-Control": cache_control},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return handler
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Command line tools for no-Python llming-stage apps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .stage import Stage
|
|
9
|
+
|
|
10
|
+
_VIEW_EXTENSIONS = {".vue", ".html", ".htm", ".js"}
|
|
11
|
+
|
|
12
|
+
def build_app(root: Path, *, dev: bool = True):
|
|
13
|
+
title = _title_for_root(root)
|
|
14
|
+
if root.is_file():
|
|
15
|
+
stage = Stage(root=root.parent, title=title, dev=dev)
|
|
16
|
+
stage.add_view("/", root)
|
|
17
|
+
return stage.app
|
|
18
|
+
|
|
19
|
+
views = root / "views"
|
|
20
|
+
stage = Stage(root=root, title=title, dev=dev)
|
|
21
|
+
if views.is_dir():
|
|
22
|
+
stage.discover(views)
|
|
23
|
+
return stage.app
|
|
24
|
+
|
|
25
|
+
view_files = sorted(
|
|
26
|
+
path
|
|
27
|
+
for path in root.rglob("*")
|
|
28
|
+
if path.is_file() and path.suffix.lower() in _VIEW_EXTENSIONS
|
|
29
|
+
)
|
|
30
|
+
if len(view_files) == 1:
|
|
31
|
+
stage.add_view("/", view_files[0])
|
|
32
|
+
else:
|
|
33
|
+
stage.discover(root)
|
|
34
|
+
return stage.app
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _title_for_root(root: Path) -> str:
|
|
38
|
+
raw = root.stem if root.is_file() else root.name
|
|
39
|
+
text = raw.replace("_", " ").replace("-", " ").strip()
|
|
40
|
+
return text[:1].upper() + text[1:] if text else "llming"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
parser = argparse.ArgumentParser(prog="llming-stage")
|
|
45
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
46
|
+
|
|
47
|
+
serve = sub.add_parser("serve", help="serve a no-Python stage app")
|
|
48
|
+
serve.add_argument("root", nargs="?", default=".")
|
|
49
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
50
|
+
serve.add_argument("--port", type=int, default=8765)
|
|
51
|
+
|
|
52
|
+
build = sub.add_parser("build", help="build a static no-Python stage app")
|
|
53
|
+
build.add_argument("root", nargs="?", default=".")
|
|
54
|
+
build.add_argument("--out", default="dist")
|
|
55
|
+
|
|
56
|
+
args = parser.parse_args(argv)
|
|
57
|
+
root = Path(args.root).resolve()
|
|
58
|
+
|
|
59
|
+
if args.cmd == "serve":
|
|
60
|
+
try:
|
|
61
|
+
import uvicorn
|
|
62
|
+
except ImportError as exc:
|
|
63
|
+
raise SystemExit(
|
|
64
|
+
"llming-stage serve requires uvicorn. Install it with `pip install uvicorn`."
|
|
65
|
+
) from exc
|
|
66
|
+
uvicorn.run(build_app(root, dev=True), host=args.host, port=args.port)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
app = build_app(root, dev=False)
|
|
70
|
+
stage = getattr(app.state, "llming_stage_instance")
|
|
71
|
+
stage.build(Path(args.out))
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
raise SystemExit(main())
|