sidepage 0.1.0__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.
Files changed (41) hide show
  1. sidepage-0.1.0/LICENSE +21 -0
  2. sidepage-0.1.0/PKG-INFO +306 -0
  3. sidepage-0.1.0/README.md +288 -0
  4. sidepage-0.1.0/pyproject.toml +46 -0
  5. sidepage-0.1.0/src/sidepage/__init__.py +9 -0
  6. sidepage-0.1.0/src/sidepage/cli.py +143 -0
  7. sidepage-0.1.0/src/sidepage/commands/__init__.py +7 -0
  8. sidepage-0.1.0/src/sidepage/commands/account.py +98 -0
  9. sidepage-0.1.0/src/sidepage/commands/app_registry.py +344 -0
  10. sidepage-0.1.0/src/sidepage/commands/directory.py +73 -0
  11. sidepage-0.1.0/src/sidepage/commands/inspect.py +39 -0
  12. sidepage-0.1.0/src/sidepage/commands/new.py +29 -0
  13. sidepage-0.1.0/src/sidepage/commands/scope.py +26 -0
  14. sidepage-0.1.0/src/sidepage/commands/secrets.py +68 -0
  15. sidepage-0.1.0/src/sidepage/commands/serve.py +260 -0
  16. sidepage-0.1.0/src/sidepage/commands/setup.py +59 -0
  17. sidepage-0.1.0/src/sidepage/commands/usage.py +37 -0
  18. sidepage-0.1.0/src/sidepage/config/__init__.py +4 -0
  19. sidepage-0.1.0/src/sidepage/config/settings.py +162 -0
  20. sidepage-0.1.0/src/sidepage/core/__init__.py +8 -0
  21. sidepage-0.1.0/src/sidepage/core/account.py +163 -0
  22. sidepage-0.1.0/src/sidepage/core/app_registry.py +200 -0
  23. sidepage-0.1.0/src/sidepage/core/auth.py +40 -0
  24. sidepage-0.1.0/src/sidepage/core/cloudflared_installer.py +281 -0
  25. sidepage-0.1.0/src/sidepage/core/directory_client.py +128 -0
  26. sidepage-0.1.0/src/sidepage/core/ecosystem.py +90 -0
  27. sidepage-0.1.0/src/sidepage/core/exceptions.py +140 -0
  28. sidepage-0.1.0/src/sidepage/core/guardrail.py +30 -0
  29. sidepage-0.1.0/src/sidepage/core/inspector.py +239 -0
  30. sidepage-0.1.0/src/sidepage/core/notebook.py +77 -0
  31. sidepage-0.1.0/src/sidepage/core/process.py +464 -0
  32. sidepage-0.1.0/src/sidepage/core/registry.py +171 -0
  33. sidepage-0.1.0/src/sidepage/core/reverse_proxy.py +495 -0
  34. sidepage-0.1.0/src/sidepage/core/scaffold.py +40 -0
  35. sidepage-0.1.0/src/sidepage/core/secrets_vault.py +117 -0
  36. sidepage-0.1.0/src/sidepage/core/static.py +32 -0
  37. sidepage-0.1.0/src/sidepage/core/target.py +235 -0
  38. sidepage-0.1.0/src/sidepage/core/token_runtime.py +89 -0
  39. sidepage-0.1.0/src/sidepage/core/tunnel_manager.py +686 -0
  40. sidepage-0.1.0/src/sidepage/core/usage_reporter.py +70 -0
  41. sidepage-0.1.0/src/sidepage/output.py +49 -0
sidepage-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kalpit B
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,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: sidepage
3
+ Version: 0.1.0
4
+ Summary: Sidepage CLI — local-first hosting, tunneling, and directory for small apps and MCP servers
5
+ Author: Kalpit B
6
+ Author-email: Kalpit B <kalpit@dolphins.dev>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: cryptography>=50.0.0,<51
10
+ Requires-Dist: httpx>=0.28.1,<0.29
11
+ Requires-Dist: rich>=15.0.0,<16
12
+ Requires-Dist: starlette>=1.3.1,<2
13
+ Requires-Dist: typer>=0.27.1,<0.28
14
+ Requires-Dist: uvicorn>=0.52.1,<0.53
15
+ Requires-Dist: websockets>=17.0.1,<18
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+
19
+ # sidepage
20
+
21
+ Local-first hosting and tunneling for code, static sites, and notebooks.
22
+ `sidepage serve` wraps almost anything — a script, a static site, a
23
+ Streamlit or FastAPI app, a Python MCP server, a Jupyter notebook —
24
+ behind a local reverse proxy and hands you a URL. `sidepage new`
25
+ scaffolds a static site to get started.
26
+
27
+ **Status:** `serve`, `secrets`, `inspect`, and bring-your-own-domain
28
+ tunneling are real and tested end to end. Features that need a Sidepage
29
+ cloud backend that doesn't exist yet (brokered tunneling, account login,
30
+ the directory beyond this machine) print a clear "not implemented"
31
+ message instead of failing silently or being left out of the CLI. See
32
+ [Project status](#project-status) for the full breakdown.
33
+
34
+ ## Install
35
+
36
+ Requires [uv](https://docs.astral.sh/uv/) (it fetches Python 3.12+ for you
37
+ if needed).
38
+
39
+ ```bash
40
+ uv sync
41
+ uv run sidepage --help
42
+ ```
43
+
44
+ Or activate the venv and call it directly: `source .venv/bin/activate &&
45
+ sidepage --help`.
46
+
47
+ ## Quickstart
48
+
49
+ ```bash
50
+ # Serve a static site
51
+ uv run sidepage serve tests/fixtures/static-site --name demo
52
+
53
+ # Serve a Streamlit app, gated behind a token
54
+ uv run sidepage serve tests/fixtures/streamlit-app/app.py --name demo --auth token
55
+
56
+ # Serve a FastAPI app — /docs (Swagger UI) works automatically
57
+ uv run sidepage serve tests/fixtures/fastapi-app/app.py --name demo
58
+
59
+ # Serve a Python MCP server over real Streamable HTTP — even if its own
60
+ # __main__ only ever calls mcp.run() (stdio), sidepage never runs that
61
+ # entrypoint, so it's reachable at /mcp regardless
62
+ uv run sidepage serve tests/fixtures/mcp-app/app.py --name demo
63
+
64
+ # Serve a Jupyter notebook — a full, editable Lab instance with a live
65
+ # kernel, reachable through the proxy like anything else
66
+ uv run sidepage serve tests/fixtures/notebook-app/notebook.ipynb --name demo
67
+
68
+ # Inject a secret and expose it over a real public tunnel
69
+ uv run sidepage secrets set MY_KEY
70
+ uv run sidepage serve some_app.py --env MY_KEY --anon
71
+
72
+ # Auto-stop after 30 minutes of no traffic, and inject another running
73
+ # app's URL as SIDEPAGE_PEER_API_URL
74
+ uv run sidepage serve frontend.py --idle-timeout 1800 --peer api=backend
75
+ ```
76
+
77
+ Every `serve` call blocks the terminal until Ctrl+C (or `sidepage stop
78
+ <app-name>` from another terminal), tearing everything down immediately —
79
+ no background/daemon mode.
80
+
81
+ ## How it works
82
+
83
+ Two things sit between "just run a script" and what `serve` does:
84
+
85
+ - **A local reverse proxy** runs on your machine in front of the app's
86
+ real port. It enforces `--auth`, counts usage, shows a holding page
87
+ while the app boots, and proxies HTTP + WebSockets — the wrapped app
88
+ itself needs zero Sidepage-specific code.
89
+ - **A tunnel**, chosen per call: `--anon` for a free, no-account
90
+ `*.trycloudflare.com` URL, or `--domain <domain>` for your own
91
+ Cloudflare domain (see [Bring your own domain](#bring-your-own-domain)
92
+ below). Without either flag, `serve` just listens on `127.0.0.1`.
93
+
94
+ ## Commands
95
+
96
+ | Command | What it does |
97
+ |---|---|
98
+ | `sidepage serve <target>` | Wrap and host a static dir, script, or app — see flags below. |
99
+ | `sidepage stop <app-name>` | Tear down a running app. |
100
+ | `sidepage ls` / `sidepage status <app-name>` | List / check apps running on this machine. |
101
+ | `sidepage usage <app-name>` | Request and connection counts for an app. |
102
+ | `sidepage inspect [<app-name>]` | Interactive HTTP console against a running app. |
103
+ | `sidepage secrets set\|list\|remove` | Encrypted local vault for standing credentials. |
104
+ | `sidepage account domain set` | Provision a BYO Cloudflare domain — see below. |
105
+ | `sidepage new <name>` | Scaffold a static site. |
106
+ | `sidepage app register "<invocation>" <name>` | Save a `serve` invocation under a short name. |
107
+ | `sidepage app list` / `show <name>` / `unregister <name>` | Manage saved apps — see below. |
108
+ | `sidepage promote <app-name>` | Widen an app's discovery scope. Not yet meaningful — only `local` scope exists today. |
109
+ | `sidepage login` / `sidepage account status` | Not implemented — no Sidepage account backend to talk to yet. |
110
+
111
+ `serve`'s main flags:
112
+
113
+ ```bash
114
+ sidepage serve <target> [--type auto|code|static|notebook] [--name <app-name>]
115
+ [--auth open|token] [--anon | --domain <domain>]
116
+ [--token <value>] [--env <SECRET_NAME>]...
117
+ [--timeout <seconds>] [--idle-timeout <seconds>]
118
+ [--peer <role>=<app-name>]...
119
+ ```
120
+
121
+ - `--type` is usually inferred: `code` targets are auto-detected as
122
+ Streamlit, FastAPI, or a Python MCP server (official `mcp` SDK or the
123
+ third-party `fastmcp` package) and launched with their real launcher
124
+ (`streamlit run`, `uvicorn <module>:<app>`, or `uvicorn --factory
125
+ <module>:<mcp-var>.<app-method>`); anything else falls back to a
126
+ generic `$PORT`-reading launch. `notebook` (`.ipynb`) targets get a
127
+ full, editable Jupyter Lab instance with a live kernel.
128
+ MCP servers are launched by bypassing their own entrypoint entirely
129
+ (same trick as FastAPI) — a script whose `__main__` only calls
130
+ `mcp.run()` (stdio, the default) still ends up served over real
131
+ Streamable HTTP at `/mcp`, since that entrypoint is never executed.
132
+ - `--auth open|token` — `token` gates the app behind a header, query
133
+ param, or browser cookie set by a gate page. (`network`/`oauth` parse
134
+ but aren't built.)
135
+ - `--env <SECRET_NAME>` — repeatable; injects a named vault secret into
136
+ the wrapped process's environment. Fails loud if the name isn't stored.
137
+ - `--anon` / `--domain` are mutually exclusive — see [How it
138
+ works](#how-it-works).
139
+ - `--timeout <seconds>` / `--idle-timeout <seconds>` — auto-teardown; see
140
+ [Timeouts, lazy start, and peers](#timeouts-lazy-start-and-peers) below.
141
+ - `--peer <role>=<app-name>` — repeatable; wire one served app to
142
+ another's URL. Same section below.
143
+
144
+ Run `sidepage <command> --help` for the full flag list, including ones
145
+ that parse but aren't implemented yet (they report that clearly rather
146
+ than silently doing nothing).
147
+
148
+ ## Timeouts, lazy start, and peers
149
+
150
+ **Auto-teardown.** `--timeout <seconds>` stops the app once its total
151
+ lifetime (from `serve` start) reaches the limit; `--idle-timeout
152
+ <seconds>` stops it once that many seconds pass with no proxied HTTP
153
+ request or WebSocket message — the timer resets on every one. Both are
154
+ composable with each other and checked in the same blocking loop Ctrl+C
155
+ already interrupts, so an auto-stop tears down exactly like `sidepage
156
+ stop` would: immediately, no drain window.
157
+
158
+ ```bash
159
+ sidepage serve demo.py --idle-timeout 1800 # stop after 30 idle minutes
160
+ sidepage serve demo.py --timeout 3600 # stop after 1 hour no matter what
161
+ ```
162
+
163
+ **Lazy start.** For `code`/`notebook` targets, the wrapped process isn't
164
+ launched at `serve` time — it launches on the *first* inbound request,
165
+ behind the same "starting…" holding page a slow boot already shows. A
166
+ `serve` call that nobody ever hits never spends the CPU/memory to boot
167
+ the wrapped app at all. (`static` targets are already in-process and
168
+ instant, so there's nothing to defer there.) This is automatic — no flag.
169
+
170
+ **Peers.** `--peer <role>=<app-name>` (repeatable) resolves another
171
+ *currently running* served app's URL and injects it as
172
+ `SIDEPAGE_PEER_<ROLE>_URL` in the wrapped process's environment — useful
173
+ for a frontend that needs to reach a backend whose tunnel URL doesn't
174
+ exist until it's actually served, and changes across `--anon` runs.
175
+ Resolution fails loud (nonzero exit, clear message) if the named peer
176
+ isn't running yet. The app can also re-resolve peers live, at any point,
177
+ via `GET /.sidepage/peers.json` — gated by the app's own `--auth` tier
178
+ like any other route — so a peer that restarts mid-session with a fresh
179
+ URL is never stale the way the boot-time env var would be. `code`/
180
+ `notebook` targets only; there's no subprocess to inject into for a
181
+ `static` target, so `--peer` on one is rejected up front.
182
+
183
+ ```bash
184
+ sidepage serve backend.py --name backend
185
+ sidepage serve frontend.py --peer api=backend # $SIDEPAGE_PEER_API_URL in frontend's env
186
+ ```
187
+
188
+ ## Saved apps (the local registry)
189
+
190
+ Save a `serve` invocation under a short name and re-run it without
191
+ retyping flags:
192
+
193
+ ```bash
194
+ sidepage app register "abc.py --auth token" abc-app
195
+ sidepage serve abc-app
196
+ ```
197
+
198
+ Any flag passed at `serve` time overrides the registered one **for that
199
+ one run only** — the saved registration itself is never changed:
200
+
201
+ ```bash
202
+ sidepage serve abc-app --scope web # runs with --auth token (registered)
203
+ # but --scope web for just this run
204
+ ```
205
+
206
+ `sidepage app show abc-app` prints the saved config; add `--with "<flags>"`
207
+ to preview the effective merged config before actually running it, e.g.
208
+ `sidepage app show abc-app --with "--scope web"`.
209
+
210
+ A registered app's target is resolved once, at registration time — so
211
+ `--type` is stored as a concrete value (`code`, `static`, `notebook`),
212
+ never "auto." `sidepage app register` **refuses** a literal `--token
213
+ <value>`: auth tokens are per-process and regenerate on every `serve`
214
+ call, so storing one would defeat the point of them being ephemeral.
215
+ `--env <SECRET_NAME>` is fine to save — it's a reference to a vault entry,
216
+ never the secret value itself.
217
+
218
+ ```bash
219
+ sidepage app list
220
+ sidepage app unregister abc-app
221
+ ```
222
+
223
+ ## Bring your own domain
224
+
225
+ Route apps through your own Cloudflare domain instead of
226
+ `*.trycloudflare.com`. One-time setup:
227
+
228
+ 1. Create a Cloudflare API token (dashboard → My Profile → API Tokens)
229
+ scoped to:
230
+ - Account → Cloudflare Tunnel → Edit
231
+ - Zone → DNS → Edit
232
+ - Zone → Zone → Read
233
+ 2. Store it in the vault, then provision the domain:
234
+ ```bash
235
+ sidepage secrets set cf-api-token
236
+ sidepage account domain set example.com --api-token-name cf-api-token
237
+ ```
238
+ This creates one Cloudflare Tunnel for the whole domain and stores its
239
+ run-token in the vault automatically — the CLI prints the vault name it
240
+ landed under (`cf-tunnel-token::example.com`), since it was never typed
241
+ by you.
242
+ 3. Serve apps through it:
243
+ ```bash
244
+ sidepage serve app.py --domain example.com
245
+ ```
246
+
247
+ Every app served under the same domain shares that one tunnel — no new
248
+ Cloudflare resources or tokens per app. The shared `cloudflared` process
249
+ starts with the first app on a domain and stops with the last.
250
+
251
+ ## Project layout
252
+
253
+ ```
254
+ src/sidepage/
255
+ ├── cli.py Root Typer app
256
+ ├── commands/ Argument parsing & help text — one module per command group
257
+ ├── core/ The SDK: serve/tunnel/proxy orchestration, secrets vault, running-app registry, saved-app registry
258
+ └── config/ Local config paths (XDG-style, overridable via SIDEPAGE_HOME)
259
+
260
+ tests/
261
+ ├── fixtures/ Real apps used as test targets (static site, Streamlit, FastAPI, MCP, notebook)
262
+ └── test_*.py Unit and integration tests
263
+
264
+ docs/
265
+ ├── CHECKLIST.md Build status for every command and core module
266
+ ├── OPEN_QUESTIONS.md Design decisions — resolved and still-open
267
+ └── SPEC_V5_DRAFT.md v5 proposals — timeout/lazy-start/--peer (built, this doc) plus still-parked ideas
268
+ ```
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ uv sync # install runtime + dev deps
274
+ uv run ruff check . # lint
275
+ uv run pytest # full suite (~4 min; mostly first-run dependency resolves)
276
+ ```
277
+
278
+ Runtime dependencies are real, not stubs: Starlette, uvicorn, httpx, and
279
+ `websockets` back the reverse proxy; `cryptography` backs the secrets
280
+ vault. `cloudflared` and network access (for `uv run` to resolve wrapped
281
+ apps' dependencies) are expected to be available wherever tests run.
282
+
283
+ ## Project status
284
+
285
+ **Real and tested end to end:** `serve` for static, code, and notebook
286
+ targets (Streamlit/FastAPI/Python-MCP auto-detected, generic `$PORT`
287
+ fallback, full Jupyter Lab for `.ipynb`), `open`/`token` auth, `--env`
288
+ secret injection, `--anon` tunneling, BYO-domain tunneling (`account
289
+ domain set` + `serve --domain`), `secrets`, `stop`/`ls`/`status`/`usage`,
290
+ `inspect` for HTTP/static targets, the local app registry (`app
291
+ register|list|show|unregister` + `serve <app-name>`, with real one-off
292
+ override merging), `--timeout`/`--idle-timeout` auto-teardown, lazy start
293
+ for code/notebook targets (subprocess deferred to the first request), and
294
+ `--peer <role>=<app-name>` (boot-time env injection plus a live
295
+ `GET /.sidepage/peers.json`).
296
+
297
+ **Not implemented, and reports that clearly rather than silently
298
+ no-op'ing:** brokered (default) tunneling, `login`/`account status`, the
299
+ discovery directory beyond this machine, `--guardrail`, `--auth
300
+ network`/`oauth`, MCP tool browsing in `inspect`, and the OS-keychain
301
+ backend for the secrets vault (encrypted-file only for
302
+ now).
303
+
304
+ See [`docs/CHECKLIST.md`](docs/CHECKLIST.md) for the full per-feature
305
+ breakdown, and [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md) for
306
+ design rationale behind what's resolved and what's still open.
@@ -0,0 +1,288 @@
1
+ # sidepage
2
+
3
+ Local-first hosting and tunneling for code, static sites, and notebooks.
4
+ `sidepage serve` wraps almost anything — a script, a static site, a
5
+ Streamlit or FastAPI app, a Python MCP server, a Jupyter notebook —
6
+ behind a local reverse proxy and hands you a URL. `sidepage new`
7
+ scaffolds a static site to get started.
8
+
9
+ **Status:** `serve`, `secrets`, `inspect`, and bring-your-own-domain
10
+ tunneling are real and tested end to end. Features that need a Sidepage
11
+ cloud backend that doesn't exist yet (brokered tunneling, account login,
12
+ the directory beyond this machine) print a clear "not implemented"
13
+ message instead of failing silently or being left out of the CLI. See
14
+ [Project status](#project-status) for the full breakdown.
15
+
16
+ ## Install
17
+
18
+ Requires [uv](https://docs.astral.sh/uv/) (it fetches Python 3.12+ for you
19
+ if needed).
20
+
21
+ ```bash
22
+ uv sync
23
+ uv run sidepage --help
24
+ ```
25
+
26
+ Or activate the venv and call it directly: `source .venv/bin/activate &&
27
+ sidepage --help`.
28
+
29
+ ## Quickstart
30
+
31
+ ```bash
32
+ # Serve a static site
33
+ uv run sidepage serve tests/fixtures/static-site --name demo
34
+
35
+ # Serve a Streamlit app, gated behind a token
36
+ uv run sidepage serve tests/fixtures/streamlit-app/app.py --name demo --auth token
37
+
38
+ # Serve a FastAPI app — /docs (Swagger UI) works automatically
39
+ uv run sidepage serve tests/fixtures/fastapi-app/app.py --name demo
40
+
41
+ # Serve a Python MCP server over real Streamable HTTP — even if its own
42
+ # __main__ only ever calls mcp.run() (stdio), sidepage never runs that
43
+ # entrypoint, so it's reachable at /mcp regardless
44
+ uv run sidepage serve tests/fixtures/mcp-app/app.py --name demo
45
+
46
+ # Serve a Jupyter notebook — a full, editable Lab instance with a live
47
+ # kernel, reachable through the proxy like anything else
48
+ uv run sidepage serve tests/fixtures/notebook-app/notebook.ipynb --name demo
49
+
50
+ # Inject a secret and expose it over a real public tunnel
51
+ uv run sidepage secrets set MY_KEY
52
+ uv run sidepage serve some_app.py --env MY_KEY --anon
53
+
54
+ # Auto-stop after 30 minutes of no traffic, and inject another running
55
+ # app's URL as SIDEPAGE_PEER_API_URL
56
+ uv run sidepage serve frontend.py --idle-timeout 1800 --peer api=backend
57
+ ```
58
+
59
+ Every `serve` call blocks the terminal until Ctrl+C (or `sidepage stop
60
+ <app-name>` from another terminal), tearing everything down immediately —
61
+ no background/daemon mode.
62
+
63
+ ## How it works
64
+
65
+ Two things sit between "just run a script" and what `serve` does:
66
+
67
+ - **A local reverse proxy** runs on your machine in front of the app's
68
+ real port. It enforces `--auth`, counts usage, shows a holding page
69
+ while the app boots, and proxies HTTP + WebSockets — the wrapped app
70
+ itself needs zero Sidepage-specific code.
71
+ - **A tunnel**, chosen per call: `--anon` for a free, no-account
72
+ `*.trycloudflare.com` URL, or `--domain <domain>` for your own
73
+ Cloudflare domain (see [Bring your own domain](#bring-your-own-domain)
74
+ below). Without either flag, `serve` just listens on `127.0.0.1`.
75
+
76
+ ## Commands
77
+
78
+ | Command | What it does |
79
+ |---|---|
80
+ | `sidepage serve <target>` | Wrap and host a static dir, script, or app — see flags below. |
81
+ | `sidepage stop <app-name>` | Tear down a running app. |
82
+ | `sidepage ls` / `sidepage status <app-name>` | List / check apps running on this machine. |
83
+ | `sidepage usage <app-name>` | Request and connection counts for an app. |
84
+ | `sidepage inspect [<app-name>]` | Interactive HTTP console against a running app. |
85
+ | `sidepage secrets set\|list\|remove` | Encrypted local vault for standing credentials. |
86
+ | `sidepage account domain set` | Provision a BYO Cloudflare domain — see below. |
87
+ | `sidepage new <name>` | Scaffold a static site. |
88
+ | `sidepage app register "<invocation>" <name>` | Save a `serve` invocation under a short name. |
89
+ | `sidepage app list` / `show <name>` / `unregister <name>` | Manage saved apps — see below. |
90
+ | `sidepage promote <app-name>` | Widen an app's discovery scope. Not yet meaningful — only `local` scope exists today. |
91
+ | `sidepage login` / `sidepage account status` | Not implemented — no Sidepage account backend to talk to yet. |
92
+
93
+ `serve`'s main flags:
94
+
95
+ ```bash
96
+ sidepage serve <target> [--type auto|code|static|notebook] [--name <app-name>]
97
+ [--auth open|token] [--anon | --domain <domain>]
98
+ [--token <value>] [--env <SECRET_NAME>]...
99
+ [--timeout <seconds>] [--idle-timeout <seconds>]
100
+ [--peer <role>=<app-name>]...
101
+ ```
102
+
103
+ - `--type` is usually inferred: `code` targets are auto-detected as
104
+ Streamlit, FastAPI, or a Python MCP server (official `mcp` SDK or the
105
+ third-party `fastmcp` package) and launched with their real launcher
106
+ (`streamlit run`, `uvicorn <module>:<app>`, or `uvicorn --factory
107
+ <module>:<mcp-var>.<app-method>`); anything else falls back to a
108
+ generic `$PORT`-reading launch. `notebook` (`.ipynb`) targets get a
109
+ full, editable Jupyter Lab instance with a live kernel.
110
+ MCP servers are launched by bypassing their own entrypoint entirely
111
+ (same trick as FastAPI) — a script whose `__main__` only calls
112
+ `mcp.run()` (stdio, the default) still ends up served over real
113
+ Streamable HTTP at `/mcp`, since that entrypoint is never executed.
114
+ - `--auth open|token` — `token` gates the app behind a header, query
115
+ param, or browser cookie set by a gate page. (`network`/`oauth` parse
116
+ but aren't built.)
117
+ - `--env <SECRET_NAME>` — repeatable; injects a named vault secret into
118
+ the wrapped process's environment. Fails loud if the name isn't stored.
119
+ - `--anon` / `--domain` are mutually exclusive — see [How it
120
+ works](#how-it-works).
121
+ - `--timeout <seconds>` / `--idle-timeout <seconds>` — auto-teardown; see
122
+ [Timeouts, lazy start, and peers](#timeouts-lazy-start-and-peers) below.
123
+ - `--peer <role>=<app-name>` — repeatable; wire one served app to
124
+ another's URL. Same section below.
125
+
126
+ Run `sidepage <command> --help` for the full flag list, including ones
127
+ that parse but aren't implemented yet (they report that clearly rather
128
+ than silently doing nothing).
129
+
130
+ ## Timeouts, lazy start, and peers
131
+
132
+ **Auto-teardown.** `--timeout <seconds>` stops the app once its total
133
+ lifetime (from `serve` start) reaches the limit; `--idle-timeout
134
+ <seconds>` stops it once that many seconds pass with no proxied HTTP
135
+ request or WebSocket message — the timer resets on every one. Both are
136
+ composable with each other and checked in the same blocking loop Ctrl+C
137
+ already interrupts, so an auto-stop tears down exactly like `sidepage
138
+ stop` would: immediately, no drain window.
139
+
140
+ ```bash
141
+ sidepage serve demo.py --idle-timeout 1800 # stop after 30 idle minutes
142
+ sidepage serve demo.py --timeout 3600 # stop after 1 hour no matter what
143
+ ```
144
+
145
+ **Lazy start.** For `code`/`notebook` targets, the wrapped process isn't
146
+ launched at `serve` time — it launches on the *first* inbound request,
147
+ behind the same "starting…" holding page a slow boot already shows. A
148
+ `serve` call that nobody ever hits never spends the CPU/memory to boot
149
+ the wrapped app at all. (`static` targets are already in-process and
150
+ instant, so there's nothing to defer there.) This is automatic — no flag.
151
+
152
+ **Peers.** `--peer <role>=<app-name>` (repeatable) resolves another
153
+ *currently running* served app's URL and injects it as
154
+ `SIDEPAGE_PEER_<ROLE>_URL` in the wrapped process's environment — useful
155
+ for a frontend that needs to reach a backend whose tunnel URL doesn't
156
+ exist until it's actually served, and changes across `--anon` runs.
157
+ Resolution fails loud (nonzero exit, clear message) if the named peer
158
+ isn't running yet. The app can also re-resolve peers live, at any point,
159
+ via `GET /.sidepage/peers.json` — gated by the app's own `--auth` tier
160
+ like any other route — so a peer that restarts mid-session with a fresh
161
+ URL is never stale the way the boot-time env var would be. `code`/
162
+ `notebook` targets only; there's no subprocess to inject into for a
163
+ `static` target, so `--peer` on one is rejected up front.
164
+
165
+ ```bash
166
+ sidepage serve backend.py --name backend
167
+ sidepage serve frontend.py --peer api=backend # $SIDEPAGE_PEER_API_URL in frontend's env
168
+ ```
169
+
170
+ ## Saved apps (the local registry)
171
+
172
+ Save a `serve` invocation under a short name and re-run it without
173
+ retyping flags:
174
+
175
+ ```bash
176
+ sidepage app register "abc.py --auth token" abc-app
177
+ sidepage serve abc-app
178
+ ```
179
+
180
+ Any flag passed at `serve` time overrides the registered one **for that
181
+ one run only** — the saved registration itself is never changed:
182
+
183
+ ```bash
184
+ sidepage serve abc-app --scope web # runs with --auth token (registered)
185
+ # but --scope web for just this run
186
+ ```
187
+
188
+ `sidepage app show abc-app` prints the saved config; add `--with "<flags>"`
189
+ to preview the effective merged config before actually running it, e.g.
190
+ `sidepage app show abc-app --with "--scope web"`.
191
+
192
+ A registered app's target is resolved once, at registration time — so
193
+ `--type` is stored as a concrete value (`code`, `static`, `notebook`),
194
+ never "auto." `sidepage app register` **refuses** a literal `--token
195
+ <value>`: auth tokens are per-process and regenerate on every `serve`
196
+ call, so storing one would defeat the point of them being ephemeral.
197
+ `--env <SECRET_NAME>` is fine to save — it's a reference to a vault entry,
198
+ never the secret value itself.
199
+
200
+ ```bash
201
+ sidepage app list
202
+ sidepage app unregister abc-app
203
+ ```
204
+
205
+ ## Bring your own domain
206
+
207
+ Route apps through your own Cloudflare domain instead of
208
+ `*.trycloudflare.com`. One-time setup:
209
+
210
+ 1. Create a Cloudflare API token (dashboard → My Profile → API Tokens)
211
+ scoped to:
212
+ - Account → Cloudflare Tunnel → Edit
213
+ - Zone → DNS → Edit
214
+ - Zone → Zone → Read
215
+ 2. Store it in the vault, then provision the domain:
216
+ ```bash
217
+ sidepage secrets set cf-api-token
218
+ sidepage account domain set example.com --api-token-name cf-api-token
219
+ ```
220
+ This creates one Cloudflare Tunnel for the whole domain and stores its
221
+ run-token in the vault automatically — the CLI prints the vault name it
222
+ landed under (`cf-tunnel-token::example.com`), since it was never typed
223
+ by you.
224
+ 3. Serve apps through it:
225
+ ```bash
226
+ sidepage serve app.py --domain example.com
227
+ ```
228
+
229
+ Every app served under the same domain shares that one tunnel — no new
230
+ Cloudflare resources or tokens per app. The shared `cloudflared` process
231
+ starts with the first app on a domain and stops with the last.
232
+
233
+ ## Project layout
234
+
235
+ ```
236
+ src/sidepage/
237
+ ├── cli.py Root Typer app
238
+ ├── commands/ Argument parsing & help text — one module per command group
239
+ ├── core/ The SDK: serve/tunnel/proxy orchestration, secrets vault, running-app registry, saved-app registry
240
+ └── config/ Local config paths (XDG-style, overridable via SIDEPAGE_HOME)
241
+
242
+ tests/
243
+ ├── fixtures/ Real apps used as test targets (static site, Streamlit, FastAPI, MCP, notebook)
244
+ └── test_*.py Unit and integration tests
245
+
246
+ docs/
247
+ ├── CHECKLIST.md Build status for every command and core module
248
+ ├── OPEN_QUESTIONS.md Design decisions — resolved and still-open
249
+ └── SPEC_V5_DRAFT.md v5 proposals — timeout/lazy-start/--peer (built, this doc) plus still-parked ideas
250
+ ```
251
+
252
+ ## Development
253
+
254
+ ```bash
255
+ uv sync # install runtime + dev deps
256
+ uv run ruff check . # lint
257
+ uv run pytest # full suite (~4 min; mostly first-run dependency resolves)
258
+ ```
259
+
260
+ Runtime dependencies are real, not stubs: Starlette, uvicorn, httpx, and
261
+ `websockets` back the reverse proxy; `cryptography` backs the secrets
262
+ vault. `cloudflared` and network access (for `uv run` to resolve wrapped
263
+ apps' dependencies) are expected to be available wherever tests run.
264
+
265
+ ## Project status
266
+
267
+ **Real and tested end to end:** `serve` for static, code, and notebook
268
+ targets (Streamlit/FastAPI/Python-MCP auto-detected, generic `$PORT`
269
+ fallback, full Jupyter Lab for `.ipynb`), `open`/`token` auth, `--env`
270
+ secret injection, `--anon` tunneling, BYO-domain tunneling (`account
271
+ domain set` + `serve --domain`), `secrets`, `stop`/`ls`/`status`/`usage`,
272
+ `inspect` for HTTP/static targets, the local app registry (`app
273
+ register|list|show|unregister` + `serve <app-name>`, with real one-off
274
+ override merging), `--timeout`/`--idle-timeout` auto-teardown, lazy start
275
+ for code/notebook targets (subprocess deferred to the first request), and
276
+ `--peer <role>=<app-name>` (boot-time env injection plus a live
277
+ `GET /.sidepage/peers.json`).
278
+
279
+ **Not implemented, and reports that clearly rather than silently
280
+ no-op'ing:** brokered (default) tunneling, `login`/`account status`, the
281
+ discovery directory beyond this machine, `--guardrail`, `--auth
282
+ network`/`oauth`, MCP tool browsing in `inspect`, and the OS-keychain
283
+ backend for the secrets vault (encrypted-file only for
284
+ now).
285
+
286
+ See [`docs/CHECKLIST.md`](docs/CHECKLIST.md) for the full per-feature
287
+ breakdown, and [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md) for
288
+ design rationale behind what's resolved and what's still open.
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "sidepage"
3
+ version = "0.1.0"
4
+ description = "Sidepage CLI — local-first hosting, tunneling, and directory for small apps and MCP servers"
5
+ readme = "README.md"
6
+ authors = [{ name = "Kalpit B", email = "kalpit@dolphins.dev" }]
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ requires-python = ">=3.12"
10
+ # Floors are versions actually tested against; ceilings block the next
11
+ # major (or, for pre-1.0 packages, the next minor — semver treats those as
12
+ # potentially breaking) so an untested future release can't silently pull
13
+ # in behavior changes on `uv sync`/`pip install`.
14
+ dependencies = [
15
+ "cryptography>=50.0.0,<51",
16
+ "httpx>=0.28.1,<0.29",
17
+ "rich>=15.0.0,<16",
18
+ "starlette>=1.3.1,<2",
19
+ "typer>=0.27.1,<0.28",
20
+ "uvicorn>=0.52.1,<0.53",
21
+ "websockets>=17.0.1,<18",
22
+ ]
23
+
24
+ [project.scripts]
25
+ sidepage = "sidepage.cli:app"
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.9.21,<0.10.0"]
29
+ build-backend = "uv_build"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "pytest>=8,<10",
34
+ "ruff>=0.6,<1",
35
+ ]
36
+
37
+ [tool.ruff]
38
+ line-length = 100
39
+ src = ["src", "tests"]
40
+ extend-exclude = ["tests/fixtures"]
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I", "UP", "B"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,9 @@
1
+ """sidepage — local-first hosting, tunneling, and directory CLI for small apps and MCP servers.
2
+
3
+ See README.md for the full command reference and docs/OPEN_QUESTIONS.md for
4
+ decisions the spec left open. This package is currently CLI scaffolding only:
5
+ command wiring, help text, and options are real; the underlying behavior
6
+ lives in `sidepage.core` as commented placeholders until the SDK is built.
7
+ """
8
+
9
+ __version__ = "0.1.0"