studio-console 1.3.4__tar.gz → 1.3.5__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 (29) hide show
  1. {studio_console-1.3.4 → studio_console-1.3.5}/PKG-INFO +64 -44
  2. {studio_console-1.3.4 → studio_console-1.3.5}/README.md +63 -43
  3. studio_console-1.3.5/VERSION +1 -0
  4. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands.py +61 -65
  5. studio_console-1.3.4/VERSION +0 -1
  6. {studio_console-1.3.4 → studio_console-1.3.5}/.gitignore +0 -0
  7. {studio_console-1.3.4 → studio_console-1.3.5}/LEGAL.md +0 -0
  8. {studio_console-1.3.4 → studio_console-1.3.5}/LICENSE +0 -0
  9. {studio_console-1.3.4 → studio_console-1.3.5}/docker-compose.yml +0 -0
  10. {studio_console-1.3.4 → studio_console-1.3.5}/install.sh +0 -0
  11. {studio_console-1.3.4 → studio_console-1.3.5}/nginx/studio.conf.template +0 -0
  12. {studio_console-1.3.4 → studio_console-1.3.5}/pyproject.toml +0 -0
  13. {studio_console-1.3.4 → studio_console-1.3.5}/studio-console.py +0 -0
  14. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/__init__.py +0 -0
  15. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/__main__.py +0 -0
  16. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cli.py +0 -0
  17. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/__init__.py +0 -0
  18. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/cf_api.py +0 -0
  19. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/cloudflare/cf_wizard.py +0 -0
  20. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands_container.py +0 -0
  21. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/commands_launch.py +0 -0
  22. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/constants.py +0 -0
  23. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/data/README.md +0 -0
  24. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/data/known_baselines.json +0 -0
  25. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/env.py +0 -0
  26. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/major_version.py +0 -0
  27. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/tui.py +0 -0
  28. {studio_console-1.3.4 → studio_console-1.3.5}/studio_console/wizard.py +0 -0
  29. {studio_console-1.3.4 → studio_console-1.3.5}/templates/.env.example +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: studio-console
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Studio operator management console
5
5
  Project-URL: Homepage, https://github.com/selfhosthub/studio-console
6
6
  Project-URL: Issues, https://github.com/selfhosthub/studio-console/issues
@@ -24,9 +24,9 @@ Operator CLI for managing a self-hosted [Studio](https://github.com/selfhosthub/
24
24
 
25
25
  Studio ships three images. Most operators want **Split**.
26
26
 
27
- - **Split** multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
28
- - **Core** (`studio-core`) single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
29
- - **Full** (`studio-full` / `studio-standalone`) single container with bundled Postgres.
27
+ - **Split**: multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
28
+ - **Core** (`studio-core`): single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
29
+ - **Full** (`studio-full`): single container with bundled Postgres.
30
30
 
31
31
  In Core and Full the container entrypoint provisions Studio on first boot; the console is a diagnostic + ops tool.
32
32
 
@@ -36,6 +36,7 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
36
36
 
37
37
  - [Quick Start](#quick-start)
38
38
  - [Run the Full image](#run-the-full-image)
39
+ - [Run the Core image](#run-the-core-image)
39
40
  - [Daily Operations](#daily-operations)
40
41
  - [Upgrading](#upgrading)
41
42
  - [CLI Reference](#cli-reference)
@@ -52,15 +53,17 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
52
53
  ```sh
53
54
  # 1. Install uv (one-time)
54
55
  curl -LsSf https://astral.sh/uv/install.sh | sh
55
- source ~/.bashrc # macOS default shell is zsh use ~/.zshrc instead
56
+ source ~/.bashrc # macOS default shell is zsh, use ~/.zshrc instead
56
57
 
57
- # 2. Install studio-console
58
- uv tool install https://github.com/selfhosthub/studio-console/releases/download/v1.3.4/studio_console-1.3.4-py3-none-any.whl
58
+ # 2. Install studio-console (isolated CLI, recommended)
59
+ uv tool install studio-console
59
60
 
60
61
  # 3. Run
61
62
  studio-console
62
63
  ```
63
64
 
65
+ No uv and don't want it? `pip install studio-console` works anywhere Python 3.8+ is available. It installs into the active environment instead of an isolated one, so a virtualenv is a good idea.
66
+
64
67
  On first run with no `~/.studio/.env`, the wizard launches. Walk through the sections, save, then go to **Services → Start all**.
65
68
 
66
69
  The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`, `nginx/studio.conf`, and four data subdirs:
@@ -69,15 +72,15 @@ The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`,
69
72
  ~/.studio/
70
73
  ├── .env, .bootstrapped, docker-compose.yml, nginx/
71
74
  ├── db/ postgres data (SHS_DB_DATA)
72
- ├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT mounted at /workspace in containers)
75
+ ├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT, mounted at /workspace in containers)
73
76
  ├── models/ model files (SHS_MODELS_ROOT)
74
77
  └── backups/ local DB dumps (SHS_BACKUP_ROOT)
75
78
  ```
76
79
 
77
- Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`** two of its values cannot be regenerated:
80
+ Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`.** Two of its values cannot be regenerated:
78
81
 
79
- - `SHS_CREDENTIAL_ENCRYPTION_KEY` losing it makes all stored provider API keys unrecoverable.
80
- - `POSTGRES_PASSWORD` set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
82
+ - `SHS_CREDENTIAL_ENCRYPTION_KEY`: losing it makes all stored provider API keys unrecoverable.
83
+ - `POSTGRES_PASSWORD`: set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
81
84
 
82
85
  Back both up separately (password manager, secrets vault) so you can recover if the host or `.env` is ever lost.
83
86
 
@@ -96,23 +99,23 @@ Full list of supported environment variables in [docs/env-vars.md](docs/env-vars
96
99
 
97
100
  ## Run the Full image
98
101
 
99
- The **Full** image (`studio-full`) is a single self-contained container bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
102
+ The **Full** image (`studio-full`) is a single self-contained container with bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
100
103
 
101
104
  ```sh
102
- # Launch (workspace defaults to ~/.studio, shared with Split)
103
- studio-console launch-full --tag 1.0.0
105
+ # Launch (defaults to the latest tag; workspace defaults to ~/.studio, shared with Split)
106
+ studio-console launch-full
104
107
 
105
- # Use a separate workspace to run Full alongside a Split install
106
- studio-console launch-full --tag 1.0.0 --workspace ~/.studio-full
108
+ # Pin a tag and/or use a separate workspace to run Full alongside a Split install
109
+ studio-console launch-full --tag 1.2.4 --workspace ~/.studio-full
107
110
  ```
108
111
 
109
- The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
112
+ The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files; it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
110
113
 
111
114
  > **Do not point Full at an existing Split `~/.studio` workspace** unless you mean to share it. Split and Full share the same `.env`/encryption key but **must never run concurrently** against the same data. Use `--workspace` to keep them separate.
112
115
 
113
116
  ### Re-entering the console after you exit
114
117
 
115
- Exiting the console **does not stop the container** it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
118
+ Exiting the console **does not stop the container**; it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
116
119
 
117
120
  ```sh
118
121
  docker exec -it studio-full studio-console # re-open the in-container menu
@@ -127,6 +130,38 @@ Inside the container the console manages services via `supervisorctl` (health, r
127
130
 
128
131
  ---
129
132
 
133
+ ## Run the Core image
134
+
135
+ The **Core** image (`studio-core`) is the same single-container API + UI as Full, but without a bundled database. You bring your own Postgres (a managed cloud instance, a RunPod sidecar, any reachable server). `launch-core` runs it on your machine and drops you into its in-container console.
136
+
137
+ Point Core at your database first, then launch:
138
+
139
+ ```sh
140
+ # Set the external database URL (prompts if you omit it)
141
+ studio-console core-db-url postgresql+asyncpg://user:pass@host:5432/studio
142
+
143
+ # Launch (defaults to the latest tag; workspace defaults to ~/.studio-core)
144
+ studio-console launch-core
145
+
146
+ # Pin a tag and/or use a separate workspace
147
+ studio-console launch-core --tag 1.2.4 --workspace ~/.studio-core
148
+ ```
149
+
150
+ Precedence for the database URL: a saved value from `core-db-url`, then the `SHS_DATABASE_URL` environment variable, then an interactive prompt. Export the env var to boot Core unattended against a remote Postgres with no prompt:
151
+
152
+ ```sh
153
+ SHS_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/studio studio-console launch-core
154
+ ```
155
+
156
+ Re-enter and run one-off commands the same way as Full, swapping the container name:
157
+
158
+ ```sh
159
+ docker exec -it studio-core studio-console # re-open the in-container menu
160
+ docker exec studio-core studio-console health
161
+ ```
162
+
163
+ ---
164
+
130
165
  ## Daily Operations
131
166
 
132
167
  ```
@@ -153,33 +188,15 @@ Backups land in `~/.studio/backups/studio-YYYYMMDD_HHMMSS/`.
153
188
  Images → Upgrade
154
189
  ```
155
190
 
156
- Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message see [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
191
+ Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message. See [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
157
192
 
158
193
  **studio-console itself:**
159
194
 
160
195
  ```bash
161
- uv tool install --force <new-release-url>
162
- ```
163
-
164
- Latest release is always at [github.com/selfhosthub/studio-console/releases/latest](https://github.com/selfhosthub/studio-console/releases/latest).
165
-
166
- ---
167
-
168
- ## Cutting a release
169
-
170
- **One command. Never tag, bump `VERSION`, or run `gh release` by hand** — the script does all of it (bumps `VERSION`, updates the README install URL, commits, pushes, tags, builds the wheel, publishes the release with the correct title).
171
-
172
- ```bash
173
- # 1. Land your fix on main first (commit + push as normal).
174
- # 2. Then cut the release — pick the bump size:
175
- scripts/release-console.sh patch # bug fix 1.0.0 → 1.0.1
176
- scripts/release-console.sh minor # new feature 1.0.0 → 1.1.0
177
- scripts/release-console.sh major # breaking 1.0.0 → 2.0.0
196
+ studio-console self-update
178
197
  ```
179
198
 
180
- Preview without touching anything: add `--dry-run`. Override the notes with `--message "..."`. Re-cut a release you already published (e.g. it shipped before a fix landed): `scripts/release-console.sh <X.Y.Z> --force`.
181
-
182
- After release, bump the console pin in the Studio versions file to the new version so the next core/full image build bakes it in.
199
+ `self-update` detects how Console was installed (uv, pip, or Homebrew) and upgrades in place.
183
200
 
184
201
  ---
185
202
 
@@ -200,6 +217,7 @@ studio-console restore [path] # restore from backup directory
200
217
  studio-console links # print service URLs
201
218
  studio-console config # show current .env values
202
219
  studio-console config set KEY VALUE # set a single .env value
220
+ studio-console config unset KEY # remove a single .env value
203
221
  studio-console workers # list/scale workers
204
222
  studio-console reset-password # reset super admin password
205
223
  studio-console wizard # re-run setup wizard
@@ -208,18 +226,20 @@ studio-console self-update # upgrade studio-console itself
208
226
  studio-console version # print version
209
227
 
210
228
  studio-console launch-full [--tag T] [--workspace DIR] # run the Full single-container image on the host
229
+ studio-console launch-core [--tag T] [--workspace DIR] # run the Core single-container image (external Postgres)
230
+ studio-console core-db-url [URL] # set Core's external database URL for the next launch-core
211
231
  ```
212
232
 
213
- For the Full image, run operational subcommands through `docker exec studio-full studio-console <cmd>` see [Run the Full image](#run-the-full-image).
233
+ For the Full or Core image, run operational subcommands through `docker exec studio-full studio-console <cmd>` (or `studio-core`). See [Run the Full image](#run-the-full-image) and [Run the Core image](#run-the-core-image).
214
234
 
215
235
  ---
216
236
 
217
237
  ## Further reading
218
238
 
219
- - **[Architecture](docs/architecture.md)** file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
220
- - **[Environment variables](docs/env-vars.md)** every `SHS_*` and supporting var.
221
- - **[Public hostname topology](docs/topology.md)** single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
222
- - **[VPS + RunPod deployments](docs/vps-runpod.md)** hybrid setups with GPU worker pods on RunPod.
239
+ - **[Architecture](docs/architecture.md)**: file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
240
+ - **[Environment variables](docs/env-vars.md)**: every `SHS_*` and supporting var.
241
+ - **[Public hostname topology](docs/topology.md)**: single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
242
+ - **[VPS + RunPod deployments](docs/vps-runpod.md)**: hybrid setups with GPU worker pods on RunPod.
223
243
 
224
244
  ---
225
245
 
@@ -6,9 +6,9 @@ Operator CLI for managing a self-hosted [Studio](https://github.com/selfhosthub/
6
6
 
7
7
  Studio ships three images. Most operators want **Split**.
8
8
 
9
- - **Split** multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
10
- - **Core** (`studio-core`) single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
11
- - **Full** (`studio-full` / `studio-standalone`) single container with bundled Postgres.
9
+ - **Split**: multi-container compose stack. Console runs on the host and manages `docker compose`. **This README documents Split.**
10
+ - **Core** (`studio-core`): single container bundling API + UI; you bring an external Postgres. Console runs inside the container.
11
+ - **Full** (`studio-full`): single container with bundled Postgres.
12
12
 
13
13
  In Core and Full the container entrypoint provisions Studio on first boot; the console is a diagnostic + ops tool.
14
14
 
@@ -18,6 +18,7 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
18
18
 
19
19
  - [Quick Start](#quick-start)
20
20
  - [Run the Full image](#run-the-full-image)
21
+ - [Run the Core image](#run-the-core-image)
21
22
  - [Daily Operations](#daily-operations)
22
23
  - [Upgrading](#upgrading)
23
24
  - [CLI Reference](#cli-reference)
@@ -34,15 +35,17 @@ In Core and Full the container entrypoint provisions Studio on first boot; the c
34
35
  ```sh
35
36
  # 1. Install uv (one-time)
36
37
  curl -LsSf https://astral.sh/uv/install.sh | sh
37
- source ~/.bashrc # macOS default shell is zsh use ~/.zshrc instead
38
+ source ~/.bashrc # macOS default shell is zsh, use ~/.zshrc instead
38
39
 
39
- # 2. Install studio-console
40
- uv tool install https://github.com/selfhosthub/studio-console/releases/download/v1.3.4/studio_console-1.3.4-py3-none-any.whl
40
+ # 2. Install studio-console (isolated CLI, recommended)
41
+ uv tool install studio-console
41
42
 
42
43
  # 3. Run
43
44
  studio-console
44
45
  ```
45
46
 
47
+ No uv and don't want it? `pip install studio-console` works anywhere Python 3.8+ is available. It installs into the active environment instead of an isolated one, so a virtualenv is a good idea.
48
+
46
49
  On first run with no `~/.studio/.env`, the wizard launches. Walk through the sections, save, then go to **Services → Start all**.
47
50
 
48
51
  The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`, `nginx/studio.conf`, and four data subdirs:
@@ -51,15 +54,15 @@ The wizard creates `~/.studio/` containing `.env` (0600), `docker-compose.yml`,
51
54
  ~/.studio/
52
55
  ├── .env, .bootstrapped, docker-compose.yml, nginx/
53
56
  ├── db/ postgres data (SHS_DB_DATA)
54
- ├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT mounted at /workspace in containers)
57
+ ├── storage/ orgs, uploads, outputs (SHS_STORAGE_ROOT, mounted at /workspace in containers)
55
58
  ├── models/ model files (SHS_MODELS_ROOT)
56
59
  └── backups/ local DB dumps (SHS_BACKUP_ROOT)
57
60
  ```
58
61
 
59
- Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`** two of its values cannot be regenerated:
62
+ Each data subdir is its own env var, so a cloud deploy can repoint individual roots at CloudSQL, GCS, or a network volume without changing code. **Protect this directory and never delete `.env`.** Two of its values cannot be regenerated:
60
63
 
61
- - `SHS_CREDENTIAL_ENCRYPTION_KEY` losing it makes all stored provider API keys unrecoverable.
62
- - `POSTGRES_PASSWORD` set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
64
+ - `SHS_CREDENTIAL_ENCRYPTION_KEY`: losing it makes all stored provider API keys unrecoverable.
65
+ - `POSTGRES_PASSWORD`: set on first DB init; the live database keeps using this value, so a regenerated one will lock the API out.
63
66
 
64
67
  Back both up separately (password manager, secrets vault) so you can recover if the host or `.env` is ever lost.
65
68
 
@@ -78,23 +81,23 @@ Full list of supported environment variables in [docs/env-vars.md](docs/env-vars
78
81
 
79
82
  ## Run the Full image
80
83
 
81
- The **Full** image (`studio-full`) is a single self-contained container bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
84
+ The **Full** image (`studio-full`) is a single self-contained container with bundled Postgres, API, UI, and workers under supervisord. No Compose stack, no external database. `launch-full` runs it on your machine and drops you into its in-container console.
82
85
 
83
86
  ```sh
84
- # Launch (workspace defaults to ~/.studio, shared with Split)
85
- studio-console launch-full --tag 1.0.0
87
+ # Launch (defaults to the latest tag; workspace defaults to ~/.studio, shared with Split)
88
+ studio-console launch-full
86
89
 
87
- # Use a separate workspace to run Full alongside a Split install
88
- studio-console launch-full --tag 1.0.0 --workspace ~/.studio-full
90
+ # Pin a tag and/or use a separate workspace to run Full alongside a Split install
91
+ studio-console launch-full --tag 1.2.4 --workspace ~/.studio-full
89
92
  ```
90
93
 
91
- The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
94
+ The workspace (mounted at `/workspace`) holds the generated `.env`, the Postgres data dir, and org files; it persists across restarts. You're prompted once for a supervisor username/password; the console remembers them and re-injects on every launch.
92
95
 
93
96
  > **Do not point Full at an existing Split `~/.studio` workspace** unless you mean to share it. Split and Full share the same `.env`/encryption key but **must never run concurrently** against the same data. Use `--workspace` to keep them separate.
94
97
 
95
98
  ### Re-entering the console after you exit
96
99
 
97
- Exiting the console **does not stop the container** it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
100
+ Exiting the console **does not stop the container**; it keeps running. `launch-full` won't re-attach to an already-running container; instead, exec back in from the host:
98
101
 
99
102
  ```sh
100
103
  docker exec -it studio-full studio-console # re-open the in-container menu
@@ -109,6 +112,38 @@ Inside the container the console manages services via `supervisorctl` (health, r
109
112
 
110
113
  ---
111
114
 
115
+ ## Run the Core image
116
+
117
+ The **Core** image (`studio-core`) is the same single-container API + UI as Full, but without a bundled database. You bring your own Postgres (a managed cloud instance, a RunPod sidecar, any reachable server). `launch-core` runs it on your machine and drops you into its in-container console.
118
+
119
+ Point Core at your database first, then launch:
120
+
121
+ ```sh
122
+ # Set the external database URL (prompts if you omit it)
123
+ studio-console core-db-url postgresql+asyncpg://user:pass@host:5432/studio
124
+
125
+ # Launch (defaults to the latest tag; workspace defaults to ~/.studio-core)
126
+ studio-console launch-core
127
+
128
+ # Pin a tag and/or use a separate workspace
129
+ studio-console launch-core --tag 1.2.4 --workspace ~/.studio-core
130
+ ```
131
+
132
+ Precedence for the database URL: a saved value from `core-db-url`, then the `SHS_DATABASE_URL` environment variable, then an interactive prompt. Export the env var to boot Core unattended against a remote Postgres with no prompt:
133
+
134
+ ```sh
135
+ SHS_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/studio studio-console launch-core
136
+ ```
137
+
138
+ Re-enter and run one-off commands the same way as Full, swapping the container name:
139
+
140
+ ```sh
141
+ docker exec -it studio-core studio-console # re-open the in-container menu
142
+ docker exec studio-core studio-console health
143
+ ```
144
+
145
+ ---
146
+
112
147
  ## Daily Operations
113
148
 
114
149
  ```
@@ -135,33 +170,15 @@ Backups land in `~/.studio/backups/studio-YYYYMMDD_HHMMSS/`.
135
170
  Images → Upgrade
136
171
  ```
137
172
 
138
- Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message see [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
173
+ Pulls the latest tag from GHCR, updates `SHS_STUDIO_VERSION` in `.env`, and restarts all services. If the new version's major doesn't match your database's major, the upgrade is blocked with a clear message. See [docs/architecture.md](docs/architecture.md#major-version-boundary-detection).
139
174
 
140
175
  **studio-console itself:**
141
176
 
142
177
  ```bash
143
- uv tool install --force <new-release-url>
144
- ```
145
-
146
- Latest release is always at [github.com/selfhosthub/studio-console/releases/latest](https://github.com/selfhosthub/studio-console/releases/latest).
147
-
148
- ---
149
-
150
- ## Cutting a release
151
-
152
- **One command. Never tag, bump `VERSION`, or run `gh release` by hand** — the script does all of it (bumps `VERSION`, updates the README install URL, commits, pushes, tags, builds the wheel, publishes the release with the correct title).
153
-
154
- ```bash
155
- # 1. Land your fix on main first (commit + push as normal).
156
- # 2. Then cut the release — pick the bump size:
157
- scripts/release-console.sh patch # bug fix 1.0.0 → 1.0.1
158
- scripts/release-console.sh minor # new feature 1.0.0 → 1.1.0
159
- scripts/release-console.sh major # breaking 1.0.0 → 2.0.0
178
+ studio-console self-update
160
179
  ```
161
180
 
162
- Preview without touching anything: add `--dry-run`. Override the notes with `--message "..."`. Re-cut a release you already published (e.g. it shipped before a fix landed): `scripts/release-console.sh <X.Y.Z> --force`.
163
-
164
- After release, bump the console pin in the Studio versions file to the new version so the next core/full image build bakes it in.
181
+ `self-update` detects how Console was installed (uv, pip, or Homebrew) and upgrades in place.
165
182
 
166
183
  ---
167
184
 
@@ -182,6 +199,7 @@ studio-console restore [path] # restore from backup directory
182
199
  studio-console links # print service URLs
183
200
  studio-console config # show current .env values
184
201
  studio-console config set KEY VALUE # set a single .env value
202
+ studio-console config unset KEY # remove a single .env value
185
203
  studio-console workers # list/scale workers
186
204
  studio-console reset-password # reset super admin password
187
205
  studio-console wizard # re-run setup wizard
@@ -190,18 +208,20 @@ studio-console self-update # upgrade studio-console itself
190
208
  studio-console version # print version
191
209
 
192
210
  studio-console launch-full [--tag T] [--workspace DIR] # run the Full single-container image on the host
211
+ studio-console launch-core [--tag T] [--workspace DIR] # run the Core single-container image (external Postgres)
212
+ studio-console core-db-url [URL] # set Core's external database URL for the next launch-core
193
213
  ```
194
214
 
195
- For the Full image, run operational subcommands through `docker exec studio-full studio-console <cmd>` see [Run the Full image](#run-the-full-image).
215
+ For the Full or Core image, run operational subcommands through `docker exec studio-full studio-console <cmd>` (or `studio-core`). See [Run the Full image](#run-the-full-image) and [Run the Core image](#run-the-core-image).
196
216
 
197
217
  ---
198
218
 
199
219
  ## Further reading
200
220
 
201
- - **[Architecture](docs/architecture.md)** file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
202
- - **[Environment variables](docs/env-vars.md)** every `SHS_*` and supporting var.
203
- - **[Public hostname topology](docs/topology.md)** single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
204
- - **[VPS + RunPod deployments](docs/vps-runpod.md)** hybrid setups with GPU worker pods on RunPod.
221
+ - **[Architecture](docs/architecture.md)**: file layout, state flow, Compose wiring, first-boot, internals (wizard vs init, orphan workers, backup format, major-version detection, deployment contexts).
222
+ - **[Environment variables](docs/env-vars.md)**: every `SHS_*` and supporting var.
223
+ - **[Public hostname topology](docs/topology.md)**: single vs split hostnames, Cloudflare tunnel + Access, IP restrictions.
224
+ - **[VPS + RunPod deployments](docs/vps-runpod.md)**: hybrid setups with GPU worker pods on RunPod.
205
225
 
206
226
  ---
207
227
 
@@ -0,0 +1 @@
1
+ 1.3.5
@@ -76,7 +76,7 @@ from .wizard import (
76
76
  def _apply_scale_flags(up_cmd: list[str], env_data: dict) -> list[str]:
77
77
  """Append --scale flags for any *active* worker with a count != 1.
78
78
 
79
- Only scales services whose profile is in COMPOSE_PROFILES compose errors
79
+ Only scales services whose profile is in COMPOSE_PROFILES, compose errors
80
80
  'no such service: X: disabled' if you --scale a profile that isn't active.
81
81
  """
82
82
  active = {p for p in env_data.get("COMPOSE_PROFILES", "").split(",") if p}
@@ -92,7 +92,7 @@ def _apply_scale_flags(up_cmd: list[str], env_data: dict) -> list[str]:
92
92
  def _stop_api_ui_containers() -> None:
93
93
  """Remove all studio-api-*, studio-ui-*, studio-nginx-* containers directly.
94
94
 
95
- Uses 'ps -a' so Created/Exited containers (not just running ones) are caught
95
+ Uses 'ps -a' so Created/Exited containers (not just running ones) are caught
96
96
  otherwise a leftover 'studio-api-2' in Created state collides with compose on
97
97
  recreate ('container name already in use'). 'docker rm -f' force-removes
98
98
  running ones too; '-s' is NOT a valid rm flag.
@@ -167,7 +167,7 @@ def config_menu(context: str, env_file: Path) -> None:
167
167
 
168
168
  menu_options = [
169
169
  f"Services {_dim('start · stop · restart · health · logs · links')}",
170
- f"Setup {_dim('wizard components · secrets · domain · cloudflare')}",
170
+ f"Setup {_dim('wizard: components · secrets · domain · cloudflare')}",
171
171
  f"Images {_dim('build · upgrade · rollback')}",
172
172
  f"Advanced {_dim('scale API/UI · per-service ops · cloudflare')}",
173
173
  f"Backup {_dim('backup · restore')}",
@@ -212,7 +212,7 @@ def config_menu(context: str, env_file: Path) -> None:
212
212
  print(f" Status: {_cyan('configured')} | Services → Start all")
213
213
  elif mv.scrape_guardrail_failure(env_file, context):
214
214
  print(
215
- f" Status: {_red('blocked')} | major-version boundary see Services → Health"
215
+ f" Status: {_red('blocked')} | major-version boundary, see Services → Health"
216
216
  )
217
217
  else:
218
218
  print(f" Status: {_yellow('starting...')}")
@@ -265,7 +265,7 @@ def _restart_for_setup(
265
265
  base = compose_cmd(env_file)
266
266
  new_profiles = {p for p in env_data.get("COMPOSE_PROFILES", "").split(",") if p}
267
267
 
268
- # Resolve missing images BEFORE stopping anything a missing image must not
268
+ # Resolve missing images BEFORE stopping anything, a missing image must not
269
269
  # leave a running stack torn down. In source mode we build; in registry mode
270
270
  # we pull. Only if a pull fails do we bail, with everything still up.
271
271
  missing = _missing_images(env_file)
@@ -279,10 +279,10 @@ def _restart_for_setup(
279
279
  if not _pull_images(missing, tag):
280
280
  error(f"Failed to pull images: {', '.join(missing)}")
281
281
  warn("Check the tag (SHS_STUDIO_VERSION) and registry access.")
282
- warn("No changes applied services left as they were.")
282
+ warn("No changes applied, services left as they were.")
283
283
  return
284
284
 
285
- # Stop workers directly catches orphans whose profiles were removed from .env.
285
+ # Stop workers directly, catches orphans whose profiles were removed from .env.
286
286
  # 'ps -a' so Created/Exited workers are removed too (avoids name collisions on
287
287
  # recreate); 'rm -f' force-removes running ones ('-s' is not a valid rm flag).
288
288
  _, ps_out = run_quiet(["docker", "ps", "-aq", "--filter", "name=studio-worker-"])
@@ -302,7 +302,7 @@ def _restart_for_setup(
302
302
  timeout=120,
303
303
  )
304
304
  else:
305
- # Workers only explicit service names so API/UI/nginx are never touched
305
+ # Workers only, explicit service names so API/UI/nginx are never touched
306
306
  worker_services = [
307
307
  svc for svc in SCALE_PROFILES.values() if svc in new_profiles
308
308
  ]
@@ -315,7 +315,7 @@ def _restart_for_setup(
315
315
  timeout=120,
316
316
  )
317
317
  else:
318
- info("No active worker profiles nothing to start.")
318
+ info("No active worker profiles, nothing to start.")
319
319
  ok("Done")
320
320
  except Exception as e:
321
321
  error(f"Docker Compose failed: {e}")
@@ -327,13 +327,13 @@ def _restart_for_setup(
327
327
 
328
328
 
329
329
  def _submenu_setup(context: str, env_file: Path) -> None:
330
- """Setup submenu runs the wizard then offers Apply now / Skip."""
330
+ """Setup submenu, runs the wizard then offers Apply now / Skip."""
331
331
  old_profiles = (
332
332
  read_env(env_file).get("COMPOSE_PROFILES", "") if env_file.exists() else ""
333
333
  )
334
334
  if wizard(context, env_file):
335
335
  apply_options = [
336
- f"Apply now {_dim('restarts changed services brief downtime possible')}",
336
+ f"Apply now {_dim('restarts changed services, brief downtime possible')}",
337
337
  f"Skip {_dim('apply on next manual restart')}",
338
338
  ]
339
339
  pick = _interactive_single(
@@ -364,7 +364,7 @@ def _cmd_scale_api_ui(env_file: Path) -> None:
364
364
  ok("Scaling updated")
365
365
  if not _interactive_yn("Restart API + UI to apply?", default=True):
366
366
  return
367
- warn_header("This will restart API, UI, and nginx workers will keep running")
367
+ warn_header("This will restart API, UI, and nginx. Workers will keep running")
368
368
  _stop_api_ui_containers()
369
369
  env_data = read_env(env_file)
370
370
  run(
@@ -377,7 +377,7 @@ def _cmd_scale_api_ui(env_file: Path) -> None:
377
377
 
378
378
 
379
379
  def _submenu_services(context: str, env_file: Path) -> None:
380
- """Services submenu daily ops: start/stop/restart all, health, logs, links."""
380
+ """Services submenu, daily ops: start/stop/restart all, health, logs, links."""
381
381
  options = [
382
382
  f"Start all {_dim('pull/build if needed, then start')}",
383
383
  f"Stop all {_dim('stop all containers')}",
@@ -459,7 +459,7 @@ def _owns_postgres(env_data: dict) -> bool:
459
459
  """True if SHS_DATABASE_URL points at a postgres instance we manage.
460
460
 
461
461
  External DBs (CloudSQL, RDS, etc., or a CloudSQL Auth Proxy sidecar) own
462
- their own backup tooling console refuses backup/restore in those cases
462
+ their own backup tooling, console refuses backup/restore in those cases
463
463
  rather than producing dumps that can't be restored back to the source.
464
464
  """
465
465
  url = env_data.get("SHS_DATABASE_URL", "")
@@ -511,7 +511,7 @@ def _submenu_backup(context: str, env_file: Path) -> None:
511
511
  def cmd_db_role(context: str, env_file: Path) -> None:
512
512
  """Show/enable the restricted runtime DB role (shs_app cutover).
513
513
 
514
- Console only writes SHS_DATABASE_APP_URL the API's bootstrap provisions
514
+ Console only writes SHS_DATABASE_APP_URL, the API's bootstrap provisions
515
515
  the role, grants, and RLS posture from it on every boot. SHS_DATABASE_URL
516
516
  stays privileged; console's own psql/dump/restore tooling keeps using it.
517
517
  """
@@ -527,10 +527,10 @@ def cmd_db_role(context: str, env_file: Path) -> None:
527
527
  "SHS_DATABASE_URL", ""
528
528
  )
529
529
  if not db_url:
530
- error("No SHS_DATABASE_URL found configure the database first.")
530
+ error("No SHS_DATABASE_URL found. Configure the database first.")
531
531
  return
532
532
 
533
- warn_header("The API currently connects as the privileged DB role RLS is inert")
533
+ warn_header("The API currently connects as the privileged DB role; RLS is inert")
534
534
  info("Enabling writes SHS_DATABASE_APP_URL (role shs_app) next to SHS_DATABASE_URL.")
535
535
  info("The API provisions the role itself on next boot; console runs no SQL.")
536
536
  info(_dim("Requires a studio image with restricted-role support; older images ignore it."))
@@ -542,7 +542,7 @@ def cmd_db_role(context: str, env_file: Path) -> None:
542
542
  print()
543
543
 
544
544
  if not _interactive_yn("Enable the restricted DB role?", default=False):
545
- info("Skipped no changes made.")
545
+ info("Skipped, no changes made.")
546
546
  return
547
547
 
548
548
  set_env_value(env_file, "SHS_DATABASE_APP_URL", derive_app_db_url(db_url))
@@ -560,26 +560,26 @@ def cmd_db_role(context: str, env_file: Path) -> None:
560
560
  ),
561
561
  timeout=120,
562
562
  )
563
- ok("Services restarted check Services → Health for the RLS posture.")
563
+ ok("Services restarted. Check Services → Health for the RLS posture.")
564
564
  else:
565
565
  info("Takes effect on the next restart of the API.")
566
566
  else:
567
567
  warn(
568
- "Restart this container from the host to apply provisioning runs in "
568
+ "Restart this container from the host to apply, provisioning runs in "
569
569
  "the container entrypoint, not under supervisord."
570
570
  )
571
571
  print(f" {_bold('docker restart <container>')} {_dim('(or stop/start the pod on RunPod)')}")
572
572
 
573
573
 
574
574
  def _submenu_advanced(context: str, env_file: Path) -> None:
575
- """Advanced submenu scale API/UI, per-service ops, Cloudflare ops."""
575
+ """Advanced submenu, scale API/UI, per-service ops, Cloudflare ops."""
576
576
  options = [
577
577
  f"Scale API/UI {_dim('set replica count, enable/disable nginx LB')}",
578
578
  f"Start one {_dim('start a stopped service')}",
579
579
  f"Stop one {_dim('stop a running service')}",
580
580
  f"Restart one {_dim('restart a single service')}",
581
581
  f"Show .env {_dim('current configuration values')}",
582
- f"DB role {_dim('restricted runtime role (RLS) status · enable')}",
582
+ f"DB role {_dim('restricted runtime role (RLS): status · enable')}",
583
583
  f"Cloudflare {_dim('tunnel · routes · IP rules · Access')}",
584
584
  ]
585
585
  idx = _interactive_single("Advanced", options, default=0)
@@ -759,7 +759,7 @@ def _submenu_cloudflare(context: str, env_file: Path) -> None:
759
759
 
760
760
  _sync_derived_urls(env_file)
761
761
  ok(f"Domain updated to {new_domain.rstrip('/')}")
762
- warn("API token not set DNS records and Access app not updated")
762
+ warn("API token not set, DNS records and Access app not updated")
763
763
  else:
764
764
  warn("No domain entered")
765
765
 
@@ -902,7 +902,7 @@ def cmd_build(env_file: Path, images: list[str] | None, confirm: bool = True) ->
902
902
  running = [line.strip() for line in out.strip().splitlines() if line.strip()]
903
903
  if running:
904
904
  warn_header(
905
- "Restart required to apply new images Studio will be briefly unavailable"
905
+ "Restart required to apply new images. Studio will be briefly unavailable"
906
906
  )
907
907
  info(f"Running services: {', '.join(running)}")
908
908
  if _interactive_yn("Restart now?", default=True, nav=False):
@@ -963,7 +963,7 @@ def cmd_start(context: str, env_file: Path) -> None:
963
963
  run(compose_cmd(env_file) + ["pull"], timeout=600)
964
964
  except Exception:
965
965
  error("The download was interrupted before it finished.")
966
- warn("Run Start again it should pick up where it left off.")
966
+ warn("Run Start again, it should pick up where it left off.")
967
967
  return False
968
968
  ok("Images pulled")
969
969
 
@@ -1077,7 +1077,7 @@ def _core_plan(
1077
1077
  container: str, pg_container: str, nginx_port: int | str = 80
1078
1078
  ) -> "_BootstrapPlan":
1079
1079
  # Core's API runs in `container`, but Postgres is an external sidecar
1080
- # (`pg_container`) so password hashing execs into the API container while
1080
+ # (`pg_container`), so password hashing execs into the API container while
1081
1081
  # every psql runs against the sidecar. base omits "exec"; helpers append it.
1082
1082
  # Core launches with publish_internal=False, so port 8000 is not published:
1083
1083
  # reach the API through the front door.
@@ -1134,7 +1134,7 @@ def _unset_env_for_bootstrap(
1134
1134
 
1135
1135
 
1136
1136
  def _super_admin_exists(plan: "_BootstrapPlan", env_data: dict) -> bool:
1137
- """True if a super_admin user is already in the DB the bootstrap gate."""
1137
+ """True if a super_admin user is already in the DB, the bootstrap gate."""
1138
1138
  pg_user = env_data.get("POSTGRES_USER", "postgres")
1139
1139
  rc, out = run_quiet(
1140
1140
  plan.base
@@ -1251,7 +1251,7 @@ def _bootstrap_first_admin(
1251
1251
  plan,
1252
1252
  )
1253
1253
  return True
1254
- warn("Bootstrap incomplete will retry account creation on next start.")
1254
+ warn("Bootstrap incomplete, will retry account creation on next start.")
1255
1255
  return False
1256
1256
 
1257
1257
 
@@ -1574,7 +1574,7 @@ def _app_db_url(env_data: dict) -> str:
1574
1574
 
1575
1575
  def _print_db_role_posture(env_data: dict, api_up: bool) -> None:
1576
1576
  """One-line RLS posture. A healthy API with the app URL set proves
1577
- restricted mode boot is fail-closed on an RLS-inert role."""
1577
+ restricted mode, boot is fail-closed on an RLS-inert role."""
1578
1578
  if _app_db_url(env_data):
1579
1579
  if api_up:
1580
1580
  print(
@@ -1584,12 +1584,12 @@ def _print_db_role_posture(env_data: dict, api_up: bool) -> None:
1584
1584
  else:
1585
1585
  print(
1586
1586
  f" {'DB role':24s} {_yellow('restricted (configured)')} "
1587
- f"{_dim('API down an RlsInertError in its logs means SHS_DATABASE_APP_URL is misconfigured')}"
1587
+ f"{_dim('API down, an RlsInertError in its logs means SHS_DATABASE_APP_URL is misconfigured')}"
1588
1588
  )
1589
1589
  else:
1590
1590
  print(
1591
1591
  f" {'DB role':24s} {_yellow('privileged')} "
1592
- f"{_dim('RLS inert, app-layer checks only enable via the DB role menu')}"
1592
+ f"{_dim('RLS inert, app-layer checks only, enable via the DB role menu')}"
1593
1593
  )
1594
1594
 
1595
1595
 
@@ -1864,7 +1864,7 @@ def cmd_reset_password(context: str, env_file: Path) -> None:
1864
1864
  info("Resetting admin password...")
1865
1865
  try:
1866
1866
  # SHS_ names are the app script's env contract (scripts/reset_admin_password.py
1867
- # reads SHS_ADMIN_PASSWORD / SHS_FORCE_PRODUCTION) keep them, do NOT rename
1867
+ # reads SHS_ADMIN_PASSWORD / SHS_FORCE_PRODUCTION), keep them, do NOT rename
1868
1868
  # to CONSOLE_. Transient injection only; never written to .env.
1869
1869
  run(
1870
1870
  compose_cmd(env_file)
@@ -1884,7 +1884,7 @@ def cmd_reset_password(context: str, env_file: Path) -> None:
1884
1884
  error("Failed to reset password. Check that the API container is healthy.")
1885
1885
  else:
1886
1886
  info("Resetting admin password...")
1887
- # SHS_ names are the app script's env contract keep, do NOT rename to CONSOLE_.
1887
+ # SHS_ names are the app script's env contract, keep, do NOT rename to CONSOLE_.
1888
1888
  os.environ["SHS_ADMIN_PASSWORD"] = new_password
1889
1889
  os.environ["SHS_FORCE_PRODUCTION"] = "true"
1890
1890
  try:
@@ -2039,7 +2039,7 @@ def _format_local_time(iso_string: str) -> tuple[str, str]:
2039
2039
  def _verify_post_restore(context: str, env_file: Path, db_file: str) -> None:
2040
2040
  """After restore, confirm the live DB's alembic_version matches the dump's.
2041
2041
 
2042
- Mismatch means the apply succeeded but landed on the wrong schema surface
2042
+ Mismatch means the apply succeeded but landed on the wrong schema, surface
2043
2043
  it loudly so the operator doesn't trust a silent success.
2044
2044
  """
2045
2045
  expected = _read_revision_from_dump(db_file)
@@ -2111,7 +2111,7 @@ def _restore_preflight(context: str, env_file: Path, db_file: str) -> bool:
2111
2111
 
2112
2112
  # Encryption-key check: the dump's encrypted columns are only readable with
2113
2113
  # the key they were written under. We never store that key in the backup, so
2114
- # we compare fingerprints live key vs. the fp recorded in the header.
2114
+ # we compare fingerprints, live key vs. the fp recorded in the header.
2115
2115
  backup_key_fp = header.get("encryption_key_fp", "")
2116
2116
  live_key_fp = _encryption_key_fingerprint(env_data)
2117
2117
  key_mismatch = bool(backup_key_fp and live_key_fp and backup_key_fp != live_key_fp)
@@ -2172,7 +2172,7 @@ def _restore_preflight(context: str, env_file: Path, db_file: str) -> bool:
2172
2172
  )
2173
2173
  if _offer_key_recovery(env_file, backup_key_fp):
2174
2174
  return True # key now matches; fall through to restore
2175
- info(_cyan("Restore needs the matching key the data is unreadable without it."))
2175
+ info(_cyan("Restore needs the matching key. The data is unreadable without it."))
2176
2176
  info("Aborted (no valid encryption key).")
2177
2177
  return False
2178
2178
 
@@ -2259,7 +2259,7 @@ def cmd_backup(
2259
2259
 
2260
2260
  ok(f"Backup complete: {backup_dir}")
2261
2261
 
2262
- # The encryption key is deliberately NOT stored in the backup co-locating
2262
+ # The encryption key is deliberately NOT stored in the backup, co-locating
2263
2263
  # it with the ciphertext would let anyone holding the backup decrypt it.
2264
2264
  # Skip the notice for internal pre-restore snapshots (not operator-facing).
2265
2265
  if what in ("all", "db") and name_prefix != "pre-restore":
@@ -2275,7 +2275,7 @@ def _print_encryption_key_notice(env_data: dict) -> None:
2275
2275
  print(_bold("⚠ Record your encryption key separately"))
2276
2276
  print("─" * 40)
2277
2277
  if key:
2278
- print("This backup does NOT contain your encryption key by design, so a")
2278
+ print("This backup does NOT contain your encryption key, by design, so a")
2279
2279
  print("stolen backup can't be decrypted. The dump is unrecoverable without it.")
2280
2280
  print()
2281
2281
  print("Copy SHS_CREDENTIAL_ENCRYPTION_KEY from your .env into a password manager or")
@@ -2283,7 +2283,7 @@ def _print_encryption_key_notice(env_data: dict) -> None:
2283
2283
  print()
2284
2284
  print(f" SHS_CREDENTIAL_ENCRYPTION_KEY={key}")
2285
2285
  else:
2286
- print(_yellow("No SHS_CREDENTIAL_ENCRYPTION_KEY is set in .env nothing to record."))
2286
+ print(_yellow("No SHS_CREDENTIAL_ENCRYPTION_KEY is set in .env. Nothing to record."))
2287
2287
  print()
2288
2288
 
2289
2289
 
@@ -2416,7 +2416,7 @@ def cmd_restore_db(
2416
2416
 
2417
2417
  try:
2418
2418
  if context == "host":
2419
- # Stop API/UI before dropping the schema a live API reconnects
2419
+ # Stop API/UI before dropping the schema, a live API reconnects
2420
2420
  # to postgres mid-restore (pg_terminate_backend only kills current
2421
2421
  # connections) and reads/writes a half-restored schema. The
2422
2422
  # pre-restore snapshot above is the recovery net if this fails.
@@ -2512,10 +2512,10 @@ def cmd_restore_db(
2512
2512
 
2513
2513
  ok("Database restored")
2514
2514
  _verify_post_restore(context, env_file, db_file)
2515
- # .env (and its encryption key) is intentionally left untouched the key
2515
+ # .env (and its encryption key) is intentionally left untouched, the key
2516
2516
  # is never stored in the backup. The preflight already gated key mismatch.
2517
2517
 
2518
- # API/UI were stopped for exclusive DB access bring them back up.
2518
+ # API/UI were stopped for exclusive DB access, bring them back up.
2519
2519
  if context == "host":
2520
2520
  info("Restarting API/UI...")
2521
2521
  env_data2 = read_env(env_file)
@@ -2533,14 +2533,14 @@ def cmd_restore_db(
2533
2533
  )
2534
2534
  else:
2535
2535
  # Provisioning/grants/RLS re-apply in the container ENTRYPOINT, so the
2536
- # restore flow ends with a container restart supervisorctl restart
2536
+ # restore flow ends with a container restart, supervisorctl restart
2537
2537
  # is not enough. Until then the restricted role authenticates but every
2538
2538
  # table access is permission-denied → sanitized 500s (fail-closed).
2539
2539
  warn("Restart this container from the host now to finish the restore.")
2540
2540
  print(f" {_bold('docker restart <container>')} {_dim('(or stop/start the pod on RunPod)')}")
2541
2541
  if _app_db_url(env_data):
2542
2542
  warn(
2543
- "Until the restart, API requests fail with sanitized 500s "
2543
+ "Until the restart, API requests fail with sanitized 500s, "
2544
2544
  "the restore dropped the restricted role's grants; boot re-applies them."
2545
2545
  )
2546
2546
 
@@ -2579,7 +2579,7 @@ def cmd_restore(
2579
2579
  db_name = "selfhost_studio"
2580
2580
 
2581
2581
  if context == "host":
2582
- # Stop API/UI before dropping the schema see cmd_restore_db.
2582
+ # Stop API/UI before dropping the schema, see cmd_restore_db.
2583
2583
  # A live API reconnects mid-restore and reads a half-restored
2584
2584
  # schema; the pre-restore snapshot above is the recovery net.
2585
2585
  info("Stopping API/UI for exclusive DB access...")
@@ -2668,7 +2668,7 @@ def cmd_restore(
2668
2668
  )
2669
2669
  ok("Database restored")
2670
2670
  _verify_post_restore(context, env_file, db_file)
2671
- # .env (and its encryption key) is intentionally left untouched the
2671
+ # .env (and its encryption key) is intentionally left untouched, the
2672
2672
  # key is never stored in the backup. Preflight gated key mismatch.
2673
2673
  else:
2674
2674
  warn(f"No database.sql in {restore_dir}")
@@ -2684,7 +2684,7 @@ def cmd_restore(
2684
2684
  else:
2685
2685
  warn(f"No orgs.tar.gz in {restore_dir}")
2686
2686
 
2687
- # API/UI were stopped for exclusive DB access bring them back up.
2687
+ # API/UI were stopped for exclusive DB access, bring them back up.
2688
2688
  if context == "host":
2689
2689
  print()
2690
2690
  info("Restarting API/UI...")
@@ -2812,7 +2812,7 @@ def cmd_upgrade(context: str, env_file: Path) -> None:
2812
2812
  run(compose_cmd(env_file) + ["pull"], timeout=300)
2813
2813
  ok("Images pulled")
2814
2814
 
2815
- # Restart reapply scale flags + --remove-orphans so replica counts survive
2815
+ # Restart, reapply scale flags + --remove-orphans so replica counts survive
2816
2816
  # the upgrade and stale containers from the old version are cleaned up.
2817
2817
  info("Restarting services...")
2818
2818
  env_data = read_env(env_file)
@@ -2870,7 +2870,7 @@ def _detect_install_method() -> str:
2870
2870
  if marker.exists():
2871
2871
  return marker.read_text().strip()
2872
2872
 
2873
- # uv tool install path contains uv/tools, no pip module available
2873
+ # uv tool install, path contains uv/tools, no pip module available
2874
2874
  if "uv/tools" in str(pkg_dir).replace("\\", "/"):
2875
2875
  return "uv"
2876
2876
 
@@ -2882,7 +2882,7 @@ def _detect_install_method() -> str:
2882
2882
  if "Cellar" in str(pkg_dir) or "homebrew" in str(pkg_dir).lower():
2883
2883
  return "brew"
2884
2884
 
2885
- # Inside a git repo dev mode
2885
+ # Inside a git repo, dev mode
2886
2886
  if (pkg_dir / ".git").exists() or (pkg_dir.parent / ".git").exists():
2887
2887
  return "dev"
2888
2888
 
@@ -2890,7 +2890,7 @@ def _detect_install_method() -> str:
2890
2890
 
2891
2891
 
2892
2892
  def cmd_self_update(context: str) -> None:
2893
- """Update studio-console delegates to the correct mechanism for the install method."""
2893
+ """Update studio-console, delegates to the correct mechanism for the install method."""
2894
2894
  import json
2895
2895
  import os
2896
2896
  import shutil
@@ -2905,14 +2905,14 @@ def cmd_self_update(context: str) -> None:
2905
2905
 
2906
2906
  if context in ("container", "runpod"):
2907
2907
  warn("studio-console is baked into the container image.")
2908
- warn("Update by pulling a new image tag not via self-update.")
2908
+ warn("Update by pulling a new image tag, not via self-update.")
2909
2909
  return
2910
2910
 
2911
2911
  method = _detect_install_method()
2912
2912
  info(f"Current version: {__version__} (installed via {method})")
2913
2913
 
2914
2914
  if method == "dev":
2915
- warn("Running from source use git pull to update.")
2915
+ warn("Running from source. Use git pull to update.")
2916
2916
  return
2917
2917
 
2918
2918
  if method == "brew":
@@ -2927,10 +2927,16 @@ def cmd_self_update(context: str) -> None:
2927
2927
  [os.sys.executable, "-m", "pip", "install", "--upgrade", "studio-console"],
2928
2928
  timeout=120,
2929
2929
  )
2930
- ok("Updated restart studio-console to use the new version")
2930
+ ok("Updated. Restart studio-console to use the new version.")
2931
2931
  return
2932
2932
 
2933
- # uv and curl installs both resolve the latest GitHub release first
2933
+ if method == "uv":
2934
+ info("Updating via uv tool...")
2935
+ run(["uv", "tool", "install", "--force", "studio-console"], timeout=120)
2936
+ ok("Updated. Restart studio-console to use the new version.")
2937
+ return
2938
+
2939
+ # curl installs resolve the latest GitHub release and extract its tarball
2934
2940
  info("Checking for updates...")
2935
2941
  api_url = "https://api.github.com/repos/selfhosthub/studio-console/releases/latest"
2936
2942
  try:
@@ -2953,16 +2959,6 @@ def cmd_self_update(context: str) -> None:
2953
2959
 
2954
2960
  info(f"Updating {__version__} → {latest_version}")
2955
2961
 
2956
- if method == "uv":
2957
- wheel_url = (
2958
- f"https://github.com/selfhosthub/studio-console/releases/download/"
2959
- f"{latest_tag}/studio_console-{latest_version}-py3-none-any.whl"
2960
- )
2961
- info("Updating via uv tool...")
2962
- run(["uv", "tool", "install", "--force", wheel_url], timeout=120)
2963
- ok("Updated — restart studio-console to use the new version")
2964
- return
2965
-
2966
2962
  tarball_url = f"https://github.com/selfhosthub/studio-console/archive/refs/tags/{latest_tag}.tar.gz"
2967
2963
  install_dir = Path(__file__).resolve().parent.parent
2968
2964
 
@@ -1 +0,0 @@
1
- 1.3.4
File without changes
File without changes