home-hosted 0.4.1 → 0.6.0
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.
- package/AGENTS.md +101 -26
- package/README.md +113 -43
- package/dist/cli.js +2153 -765
- package/dist/cli.js.map +1 -1
- package/{SERVERS.md → docs/SERVERS.md} +61 -8
- package/{UI_CREATION.md → docs/UI_CREATION.md} +34 -2
- package/package.json +9 -5
- package/uis/stock/dist/assets/index-4_Q69j7h.js +46 -0
- package/uis/stock/dist/assets/index-DMo6krK0.css +2 -0
- package/uis/stock/dist/index.html +2 -2
- package/uis/stock/dist/assets/index-DOD80X00.css +0 -2
- package/uis/stock/dist/assets/index-Dl2nDThm.js +0 -47
- /package/{NOTIFICATIONS.md → docs/NOTIFICATIONS.md} +0 -0
package/AGENTS.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`home-hosted` is a Node 24 / TypeScript harness for self-hosted servers: `up` starts a Hono/srvx
|
|
4
4
|
panel (default `127.0.0.1:3999`) that supervises the entries in `$HHOSTED_HOME/servers.config.json`
|
|
5
|
-
and serves a UI. User docs: `README.md`, `SERVERS.md` (entries and port conflicts),
|
|
6
|
-
`NOTIFICATIONS.md`; UI authors: `UI_CREATION.md`.
|
|
5
|
+
and serves a UI. User docs: `README.md`, `docs/SERVERS.md` (entries and port conflicts),
|
|
6
|
+
`docs/NOTIFICATIONS.md`; UI authors: `docs/UI_CREATION.md`.
|
|
7
7
|
|
|
8
8
|
State lives only in `$HHOSTED_HOME` (default `~/.home-hosted`): `servers.config.json`,
|
|
9
9
|
`.control-secrets.json` (0600: password hash, API token hash, Telegram bot token), `.logs/`, `.tls/`,
|
|
@@ -28,6 +28,8 @@ pnpm exec tsx src/cli.ts init # scaffold a project (interactive; --yes for
|
|
|
28
28
|
pnpm run media # regenerate docs/media (mockups, both served UIs, tour.gif)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
The published bin is `home-hosted`, with an `hh` alias: both names run the same CLI.
|
|
32
|
+
|
|
31
33
|
Releases are dispatched from `.github/workflows/release.yml` with a version (and a `dry-run` switch
|
|
32
34
|
that stops before pushing). It verifies the version against `package.json`, lints/types/tests,
|
|
33
35
|
builds the CLI plus the stock UI and every UI zip, lets changelogen write the changelog and tag
|
|
@@ -37,9 +39,22 @@ exists, so the first release has to be published by hand.
|
|
|
37
39
|
|
|
38
40
|
## Architecture (and why)
|
|
39
41
|
|
|
40
|
-
- `src/cli.ts` — the CLI
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
- `src/cli.ts` — the CLI's thin root. Two things happen before [citty](https://github.com/unjs/citty)
|
|
43
|
+
is asked anything: `--home`/`--project` are peeled off and applied (`src/cli/args.ts`), and the
|
|
44
|
+
curated dispatch — `help`/`version`, `unknown command`, and the `-p 4000` shorthand for `up` — is
|
|
45
|
+
decided, because `#src/helpers/paths.ts` resolves at import time. Its static imports stay node
|
|
46
|
+
builtins, citty and those two path-free local modules; every command is a lazy
|
|
47
|
+
`() => import('#src/cli/<command>')` in citty's `subCommands`, so a command module *may* use static
|
|
48
|
+
`#src` imports (that is the whole point of the pre-pass). citty's `runMain` is deliberately not
|
|
49
|
+
used: it prints its own usage and `console.error`s before `process.exit(1)`, replacing `fail()`'s
|
|
50
|
+
one error shape; the root calls `runCommand` and catches. `src/cli/<command>.ts` is one command per
|
|
51
|
+
file — citty owns dispatch and argument parsing, with `--no-autostart`/`--no-install` declared as
|
|
52
|
+
boolean negations rather than literal arg names. The curated `--help` prose stays in `src/cli.ts`
|
|
53
|
+
because citty cannot generate it. `up` re-spawns itself detached as `up --foreground …`, with the
|
|
54
|
+
argv built from the *parsed* flags (`buildDaemonArgv`) plus this file's own URL as the entry, so it
|
|
55
|
+
works under tsx and from `dist/cli.js` alike. `src/cli/io.ts` is the one place the readline prompts
|
|
56
|
+
and colours live; commands take `prompt`/`style` through that seam (`src/cli/ui-switch.ts` keeps
|
|
57
|
+
`UiSwitchIo`).
|
|
43
58
|
- `src/index.ts` — `runControlPlane()`: wiring, startup guards (exposure, free port, live run.json),
|
|
44
59
|
`run.json`, signals. Wiring belongs here and nowhere else.
|
|
45
60
|
- `src/app.ts` — the Hono root, chained routes only. `/_hh` is mounted *before* the `/api/*` auth
|
|
@@ -54,13 +69,14 @@ exists, so the first release has to be published by hand.
|
|
|
54
69
|
`migrations.ts` (the schema constant and the ordered step registry), `secrets.ts`, `seed.ts`.
|
|
55
70
|
- `src/providers/` — stateless leaves: `process` (spawn, `terminate`, `terminatePid` for a process we
|
|
56
71
|
adopted), `port` (probe, holder lookup, `terminatePids`), `proc` (the sampler, plus
|
|
57
|
-
`processCarriesServerId` for
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
`processCarriesServerId` for the environment marker), `identity` (which port holder is this entry's
|
|
73
|
+
own successor: marker first, then resolved image + argv), health-check, host, telegram, archive.
|
|
74
|
+
- `src/services/` — stateful orchestration: supervisor, control-server, config-watch, state,
|
|
75
|
+
auth + exposure, dependencies, history, log-buffer/log-files, notifications, host-monitor, backups,
|
|
76
|
+
tls, ui, plus `init` (the scaffold behind `home-hosted init`: a manifest, a `.gitignore`, and the
|
|
77
|
+
prompts stay in the CLI). It names no server — the scaffold must stay as neutral as the supervisor.
|
|
62
78
|
- `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
|
|
63
|
-
credentials are read: the `
|
|
79
|
+
credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
|
|
64
80
|
a first-class credential (same authority as a signed-in browser) and is verified from the secrets
|
|
65
81
|
file on every request, so `set-token` needs no restart.
|
|
66
82
|
- `src/helpers/` — paths (`dataRoot` vs `projectDir`), daemon (run.json + a loopback probe that
|
|
@@ -74,12 +90,20 @@ exists, so the first release has to be published by hand.
|
|
|
74
90
|
- `scripts/` — `build-uis.mjs` (build one UI, optionally zip it), `typecheck-uis.mjs`,
|
|
75
91
|
`capture-media.mjs` (the README's media), `check-release-version.mjs` and `release-notes.mjs`
|
|
76
92
|
(used by the release workflow), `dev.mjs` (`pnpm dev`).
|
|
77
|
-
- `docs/` —
|
|
78
|
-
|
|
93
|
+
- `docs/` — the topic docs (`SERVERS.md`, `NOTIFICATIONS.md`, `UI_CREATION.md`), `mockups/*.html`
|
|
94
|
+
(hand-written UI examples in four directions) and `media/*`: their screenshots plus the README's
|
|
95
|
+
`tour.gif`, all regenerated by `pnpm run media`. Only the three `.md` files are published; the
|
|
96
|
+
media is a build artifact.
|
|
79
97
|
|
|
80
98
|
## Conventions
|
|
81
99
|
|
|
82
100
|
- `#src/*` imports inside `src/`; UIs use `@shared/*`.
|
|
101
|
+
- A single on/off setting is a `ToggleSwitch`; `CheckField` is only for picking items out of a set
|
|
102
|
+
(the restore plan). A checkbox in a `FieldGroup` grid reads as misaligned next to the inputs.
|
|
103
|
+
- A create body carries only what differs from what the entry would inherit — the schema's defaults
|
|
104
|
+
with the panel's `Settings → Server defaults` on top (`inheritBaseline` + `diffFields` in both
|
|
105
|
+
UIs). A value written into `servers.config.json` stops following those defaults, so anything the
|
|
106
|
+
person did not decide stays out; the editor applies the same rule to an edit.
|
|
83
107
|
- ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
|
|
84
108
|
`c.req.valid(...)`; ad-hoc payloads use `parseOrThrow`. Schemas reject undeclared keys.
|
|
85
109
|
- Every failure is a `DetailedError` (`@namesmt/utils`), mapped by `src/helpers/error.ts` into one
|
|
@@ -145,15 +169,26 @@ either is a last resort, and never an accidental one.
|
|
|
145
169
|
- **Paths.** `dataRoot` is state; `projectDir` is the base for relative entry paths. `{id}{port}`
|
|
146
170
|
`{host}{bind}{cwd}{projectDir}{dataRoot}{home}` and `${ENV}` expand in config; there is no
|
|
147
171
|
package-relative state.
|
|
148
|
-
- **Secrets never enter the config.**
|
|
149
|
-
|
|
150
|
-
- **A port holder that
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
with `
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
- **Secrets never enter the config.** The password hash, API token hash and bot token live in the
|
|
173
|
+
0600 secrets file, and the TLS pair in `.tls/`; the config holds policy.
|
|
174
|
+
- **A port holder that is this entry's own successor is not a stranger.** A program that restarts
|
|
175
|
+
itself leaves a detached process behind; with `follow` the panel adopts it as-is (pid, liveness,
|
|
176
|
+
health, resources, stop — but not its output); with `reclaim` it stops that successor and starts a
|
|
177
|
+
fully supervised child instead; with `kill` it stops whatever holds the port without asking whose it
|
|
178
|
+
is. All three beat blocking forever on a port that is already serving, and only `kill` ever touches a
|
|
179
|
+
process the panel could not identify — which is why it logs the pids it stopped.
|
|
180
|
+
Ownership is read from `HHOSTED_SERVER_ID` in the environment (`/proc` on Linux, `ps -E` on macOS),
|
|
181
|
+
falling back to the entry's own resolved image **plus its expanded argv** where that is unavailable
|
|
182
|
+
or unmatched — the fallback is what gives `follow`/`reclaim` any reach on Windows, which exposes no
|
|
183
|
+
per-process environment. That fallback is strict on purpose, because `reclaim` kills what it
|
|
184
|
+
identifies: arguments are compared **literally** (never by basename — `/srv/a/server.js` must not
|
|
185
|
+
match `/tmp/b/server.js`), an argument can never be satisfied by a word that is not in the argv, and
|
|
186
|
+
two matching holders mean the panel refuses to guess and blocks. It still cannot see through a
|
|
187
|
+
Windows `.cmd` shim (the holder is `node.exe`, whose argv never mentions the shim) or a macOS
|
|
188
|
+
argument containing a space (`ps` joins argv without quoting); those cases block, and `kill` is the
|
|
189
|
+
answer for them. `src/providers/identity.ts` owns the matching, and the argv it is given comes from
|
|
190
|
+
the supervisor's single `resolveSpawn()`, the same one the spawn itself used — keep it to one
|
|
191
|
+
resolver, because a second one that expands or filters args differently re-opens these cases.
|
|
157
192
|
- **A port is only ever freed by re-listing its listeners.** `POST /api/servers/:id/free-port` never
|
|
158
193
|
trusts a pid quoted in a message, and refuses any listener in `supervisedPids()` (the panel plus
|
|
159
194
|
every entry's child) instead of killing it — a port held by a sibling is a config mistake.
|
|
@@ -175,8 +210,44 @@ either is a last resort, and never an accidental one.
|
|
|
175
210
|
`PATCH /api/settings`, the TLS routes and `/_hh/shutdown` defer with `afterResponse()`.
|
|
176
211
|
- `Supervisor.start()` sets `starting` synchronously before its first await, and `stop()` sets
|
|
177
212
|
`stopping` first: overlapping calls would double-spawn or resurrect a stopped process. Tests must
|
|
178
|
-
call `supervisor.dispose()`.
|
|
213
|
+
call `supervisor.dispose()`. `stopping` is cleared on **every** exit path of `stopEntry()`: the
|
|
214
|
+
early return for an entry with no child used to leave it set, and since `start()` refuses while it
|
|
215
|
+
is true, one Stop on an already-stopped server made that entry unstartable until a daemon restart.
|
|
216
|
+
- Timers and `void`-ed promises in the supervisor carry a `.catch`: a throw in the tick or in a retry
|
|
217
|
+
is an unhandled rejection, and Node 24 ends the process on one.
|
|
218
|
+
- **`Settings → Server defaults` merge into the nested groups key by key** (`mergeDefaults` in
|
|
219
|
+
`src/config/schema.ts`, used by both the config parser and `validateServer`). A shallow
|
|
220
|
+
`{ ...defaults, ...entry }` lets an entry that decides `restart.maxRetries` silently drop the
|
|
221
|
+
panel's `restart.baseDelayMs` — and the create/edit diff flow depends on inheriting exactly that.
|
|
222
|
+
- The curated CLI dispatch reads only the **flag** forms (`--help`/`-h`, `--version`/`-v`) after a
|
|
223
|
+
command name, never a bare `help`/`version`: that is an option's value (`init --name help`).
|
|
179
224
|
- Port preflight re-probes after 300 ms — a just-closed listener can still complete a handshake.
|
|
225
|
+
- **The config is re-read whenever the file changes on disk** (`src/services/config-watch.ts` →
|
|
226
|
+
`ConfigStore.reloadFromDisk()`, wired in `src/index.ts`). The watch is on the *directory*, because
|
|
227
|
+
an editor's save is a temporary file renamed over the target — the inode changes, the name does not
|
|
228
|
+
— and a two-second poll backs it up where `fs.watch` is undependable (network mounts). The store
|
|
229
|
+
compares the bytes it last read and the bytes it wrote, so the panel's own saves never reload
|
|
230
|
+
anything, and a revision it cannot read (unparseable JSON included — that path used to fall back to
|
|
231
|
+
an *empty* config until a watcher made it reachable, which would have stopped every server) is
|
|
232
|
+
reported in the state frame while the running config is kept. A changed definition takes effect on
|
|
233
|
+
that entry's next start; only an added `autostart` entry is started, and `--no-autostart` still
|
|
234
|
+
means the panel starts nothing on its own.
|
|
235
|
+
- **The shell must watch the *session*, not only the flags it derives.** With authentication off,
|
|
236
|
+
`authRequired` and `authenticated` are both `false` from the first paint to the last, so a watcher
|
|
237
|
+
on those two never ran after the session landed: the panel never opened its event stream, the
|
|
238
|
+
connection badge sat on "Connecting", and the dashboard showed one stale snapshot forever. Both
|
|
239
|
+
UIs turn it into a single `streamDecision(session)` value (`wait` | `connect` | `login`) and watch
|
|
240
|
+
that, which is also what the test pins.
|
|
241
|
+
- **A form that copies live state must guard per block, never globally.** The host thresholds and the
|
|
242
|
+
backups policy arrive from `/api/settings` *after* the SSE frame, so a single "has anything
|
|
243
|
+
changed?" gate leaves them showing schema defaults forever — the backups toggle reported itself as
|
|
244
|
+
changed and flipped back to `true` on every reload. `uis/stock` compares each block against the
|
|
245
|
+
snapshot it was last filled from (`blockSnapshot`/`isBlockEdited`, `syncFromLive`) and reads the
|
|
246
|
+
file-only blocks on their own.
|
|
247
|
+
- **A dialog's footer has to be a flex sibling of a scrolling body** (`uis/stock/src/components/ui/Modal.vue`):
|
|
248
|
+
the sheet is `flex flex-col` with `max-h-[88dvh]`, the body `min-h-0 flex-1 overflow-y-auto`. When
|
|
249
|
+
only the body carried a max-height, a tall form pushed its own save button below the clipped
|
|
250
|
+
edge — the add-server dialog looked like it had no save button at all.
|
|
180
251
|
- Vue does not notify a computed's subscribers when its recomputed value is `Object.is`-equal to the
|
|
181
252
|
old one, so anything mutated in place silently freezes every value derived from it. The log buffers
|
|
182
253
|
(`uis/stock/src/composables/useControlPlane.ts`) therefore hand out a **new array per batch**, and
|
|
@@ -197,6 +268,10 @@ either is a last resort, and never an accidental one.
|
|
|
197
268
|
- Backups are zips; a password makes them WinZip AES-256/AE-2, and zero-byte entries stay
|
|
198
269
|
unencrypted on purpose (p7zip 16.02 reports a CRC failure otherwise). `list()` is sync, so
|
|
199
270
|
encryption flags are cached and refreshed in the background.
|
|
271
|
+
- Generated directories are skipped by the `filter` handed to `fs.cpSync`, matched on an exact path
|
|
272
|
+
segment at any depth (`src/shared/generated.ts`): `dist/` and `app/node_modules/` go, while
|
|
273
|
+
`distributed/` and `my-node_modules/` stay. Only the paths an entry declares are filtered — a
|
|
274
|
+
global `backups.includePaths` entry is captured as it stands.
|
|
200
275
|
- `vite.server.config.ts` targets `node22` while `engines` requires >= 24 — deliberate margin, leave it.
|
|
201
276
|
- `pnpm test` watches; CI runs `vitest run`.
|
|
202
277
|
- `pnpm run media` drives Chromium through Playwright, which needs fonts *and* the X/NSS/Mesa
|
|
@@ -211,11 +286,11 @@ either is a last resort, and never an accidental one.
|
|
|
211
286
|
- **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/store.ts` when
|
|
212
287
|
nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
|
|
213
288
|
- **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
|
|
214
|
-
`node scripts/build-uis.mjs <name> --zip`. The contract is `UI_CREATION.md`.
|
|
289
|
+
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`.
|
|
215
290
|
- **Capability**: a stateless `src/providers/*` returning plain data.
|
|
216
291
|
|
|
217
292
|
## Publishing
|
|
218
293
|
|
|
219
|
-
`pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`, `SERVERS.md`,
|
|
220
|
-
`NOTIFICATIONS.md`, `UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
|
|
294
|
+
`pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`, `docs/SERVERS.md`,
|
|
295
|
+
`docs/NOTIFICATIONS.md`, `docs/UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
|
|
221
296
|
build; `vue`/`vue-router` are devDependencies because the UIs are prebuilt.
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ them, watches them, restarts what dies, and shows you one page of what is going
|
|
|
16
16
|
[](./LICENSE)
|
|
17
17
|
[](https://nodejs.org)
|
|
18
18
|
|
|
19
|
-
[🚀 Quick start](#-quick-start) · [🤖 Agents & API](#-agents-scripts-and-tools) · [✨ Features](#-features) · [🧩 Servers](./SERVERS.md) · [🛠 CLI](#-cli) · [🔔 Notifications](./NOTIFICATIONS.md) · [🎨 BYOU](#-bring-your-own-ui-byou)
|
|
19
|
+
[🚀 Quick start](#-quick-start) · [🤖 Agents & API](#-agents-scripts-and-tools) · [✨ Features](#-features) · [🧩 Servers](./docs/SERVERS.md) · [🛠 CLI](#-cli) · [🔔 Notifications](./docs/NOTIFICATIONS.md) · [🎨 BYOU](#-bring-your-own-ui-byou)
|
|
20
20
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
@@ -34,6 +34,50 @@ shortcuts wizards), and UI directions you could build yourself —
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## 🤔 Why?
|
|
38
|
+
|
|
39
|
+
You run a handful of services at home. The usual choices are extremes — 🧟 `tmux` sessions you
|
|
40
|
+
forget about, 📜 a hand-written systemd unit per service (times six), or 🐳 a whole docker/k8s
|
|
41
|
+
stack??? - too extreme! — plus 😩 monitoring, rebooting and changing the host machine, yuck!
|
|
42
|
+
|
|
43
|
+
🙂 home-hosted, well, joins in somewhere: a panel/supervisor that starts them, watches them, restarts what dies, and
|
|
44
|
+
puts the whole stack on one page, with deep backup support.
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
┌──────────────────────────────────────┐
|
|
48
|
+
your browser ──▶│ home-hosted · 127.0.0.1:3999 │
|
|
49
|
+
│ your UI + JSON API + SSE logs │
|
|
50
|
+
└───────────────┬──────────────────────┘
|
|
51
|
+
│ supervises
|
|
52
|
+
┌──────────────────────────┼──────────────────────────┐
|
|
53
|
+
▼ ▼ ▼
|
|
54
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
55
|
+
│ gateway │ │ files │ │ bot │
|
|
56
|
+
│ :4000 │ │ :4010 │ │ ... │
|
|
57
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
58
|
+
health ✓ health ✓ restarts ↻
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| | |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| ❌ **"Is it still running?"** | Health, CPU/mem, uptime and live logs per server — no `ps`, no `curl`, no hope |
|
|
64
|
+
| ❌ **Silent deaths** | Restarted automatically, and the panel or Telegram notifies |
|
|
65
|
+
| ❌ **Fragile reboots** | `autostart` brings the stack back; one `down` stops it all cleanly |
|
|
66
|
+
| ❌ **"Move it to the new box"** | One archive: config **and** data, restored on a blank host |
|
|
67
|
+
| ✅ **home-hosted** | Declare it once, watch it forever, one command to stop it all |
|
|
68
|
+
|
|
69
|
+
It ships with **nothing**: no blessed paths, no opinion about what you run — a server is a command,
|
|
70
|
+
some arguments, and the environment you give it:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "id": "gateway", "command": "node", "args": ["server.js"], "port": 4000, "autostart": true }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
<sub>Manage them from the panel, `home-hosted status --json`, or `GET /api/state`; `GET /healthz` is
|
|
77
|
+
the same status line for your own monitor, no session needed.</sub>
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
37
81
|
## ⚡ Quick start
|
|
38
82
|
|
|
39
83
|
```bash
|
|
@@ -58,12 +102,15 @@ npx home-hosted down # stops the panel *and* everything it started
|
|
|
58
102
|
|
|
59
103
|
```bash
|
|
60
104
|
npm install -g home-hosted # or: pnpm add -g home-hosted
|
|
61
|
-
home-hosted up
|
|
105
|
+
home-hosted up # `hh up` does the same
|
|
62
106
|
```
|
|
63
107
|
|
|
64
108
|
Everything it owns — config, secrets, logs, TLS, backups — lives in `$HHOSTED_HOME`, default
|
|
65
109
|
`~/.home-hosted`. Delete that and nothing of yours is left behind.
|
|
66
110
|
|
|
111
|
+
<sub>An installed home-hosted answers to **`hh`** too — `hh up`, `hh status`, `hh down`. Only the
|
|
112
|
+
installed form gets it; npx stays `npx home-hosted …`.</sub>
|
|
113
|
+
|
|
67
114
|
</details>
|
|
68
115
|
|
|
69
116
|
<details>
|
|
@@ -104,6 +151,10 @@ Worked example, with per-server data inside the project:
|
|
|
104
151
|
|
|
105
152
|
<sub>Call them as `pnpm run up` — `pnpm up` is pnpm's own update, not your script.</sub>
|
|
106
153
|
|
|
154
|
+
> [!TIP]
|
|
155
|
+
> Give a project its own panel port (`control.port`, e.g. `4399`) — the default `3999` is what the
|
|
156
|
+
> global instance and every other project also want.
|
|
157
|
+
|
|
107
158
|
</details>
|
|
108
159
|
|
|
109
160
|
<details>
|
|
@@ -140,7 +191,7 @@ home-hosted set-token --generate
|
|
|
140
191
|
|
|
141
192
|
curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
|
|
142
193
|
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/9router/restart
|
|
143
|
-
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?
|
|
194
|
+
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=9router' # SSE
|
|
144
195
|
home-hosted status --json # machine-readable: pid, url, health, paths
|
|
145
196
|
```
|
|
146
197
|
|
|
@@ -158,9 +209,10 @@ revokes it instantly.
|
|
|
158
209
|
| `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
|
|
159
210
|
| `POST /api/servers/:id/free-port` | ask whatever holds that server's port to stop |
|
|
160
211
|
| `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
|
|
161
|
-
| `GET /api/logs`, `/api/backups
|
|
212
|
+
| `GET /api/logs`, `/api/backups` | history and archives |
|
|
213
|
+
| `PUT`/`DELETE /api/notifications/token`, `POST /api/notifications/{test,detect-chats}` | the bot credential, a test send |
|
|
162
214
|
| `GET /healthz` | no session needed — the one an external monitor wants (its per-server detail needs a credential) |
|
|
163
|
-
| `GET /api/metrics` | Prometheus text |
|
|
215
|
+
| `GET /api/metrics` | Prometheus text (needs a token or session, like every `/api` route) |
|
|
164
216
|
|
|
165
217
|
`GET /openapi/spec.json` describes all of it, `/openapi/ui` is the browsable version, and every error
|
|
166
218
|
comes back as one envelope (`{ message, code, detail }`) with a stable `code` a tool can branch on.
|
|
@@ -175,9 +227,9 @@ Give the agent four things and it can run your home server without guessing:
|
|
|
175
227
|
1. the token (`home-hosted set-token --generate`),
|
|
176
228
|
2. `http://127.0.0.1:3999/openapi/spec.json` — the API it may call,
|
|
177
229
|
3. `home-hosted status --json` — where things are,
|
|
178
|
-
4. [SERVERS.md](./SERVERS.md) — how an entry is declared when it needs a new server.
|
|
230
|
+
4. [SERVERS.md](./docs/SERVERS.md) — how an entry is declared when it needs a new server.
|
|
179
231
|
|
|
180
|
-
For a UI rather than the API, [UI_CREATION.md](./UI_CREATION.md) is the whole contract, and the panel
|
|
232
|
+
For a UI rather than the API, [UI_CREATION.md](./docs/UI_CREATION.md) is the whole contract, and the panel
|
|
181
233
|
can be told what to be: *"Help me build a UI for home-hosted: nostalgic game theme, including …"*.
|
|
182
234
|
|
|
183
235
|
</details>
|
|
@@ -189,16 +241,17 @@ can be told what to be: *"Help me build a UI for home-hosted: nostalgic game the
|
|
|
189
241
|
| | |
|
|
190
242
|
| --- | --- |
|
|
191
243
|
| 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
|
|
244
|
+
| 📝 **Hand edits welcome** | Change `servers.config.json` in an editor, a `git checkout` or a config tool: the panel notices within seconds, no restart. A file it cannot read is reported in the panel, and the running servers are left alone. |
|
|
192
245
|
| ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
|
|
193
|
-
| 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, check ports before starting — and [follow or replace](./SERVERS.md#when-a-program-restarts-itself) a program that restarts itself. |
|
|
246
|
+
| 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, check ports before starting — and [follow or replace](./docs/SERVERS.md#when-a-program-restarts-itself) a program that restarts itself. |
|
|
194
247
|
| 🔗 **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* — not merely spawned — and stops in reverse. |
|
|
195
248
|
| 📜 **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
|
|
196
249
|
| 📈 **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
|
|
197
250
|
| 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
|
|
198
251
|
| 🤖 **Token API** | Scripts and agents drive it with `Authorization: Bearer` — no browser, no session. [↑](#-agents-scripts-and-tools) |
|
|
199
|
-
| 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery and host thresholds — [setup here](./NOTIFICATIONS.md). |
|
|
252
|
+
| 🔔 **Notifications** | Telegram on crash, unhealthy, forced restart, recovery and host thresholds — [setup here](./docs/NOTIFICATIONS.md). |
|
|
200
253
|
| 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
|
|
201
|
-
| 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./UI_CREATION.md) |
|
|
254
|
+
| 🎨 **BYOU — Bring Your Own UI** | Upload a static build, `home-hosted ui-revert` to go back. [UI_CREATION.md](./docs/UI_CREATION.md) |
|
|
202
255
|
| 🔐 **Security** | Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
|
|
203
256
|
| 🧩 **No special treatment** | A server is `command` + `args` + `env` + `cwd`; nothing is built in for any particular app. |
|
|
204
257
|
| 🖥 **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
|
|
@@ -222,7 +275,7 @@ An entry is a few lines. Add one with **➕ Add server**, or write it into `serv
|
|
|
222
275
|
|
|
223
276
|
`dataEnvs` declares a data directory once: it is exported to the process *and* picked up by Backups.
|
|
224
277
|
**Every field, every placeholder, the port-conflict policies (including adopting a server that
|
|
225
|
-
restarts itself), and how hand-edits are validated: [SERVERS.md](./SERVERS.md).**
|
|
278
|
+
restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.md).**
|
|
226
279
|
|
|
227
280
|
---
|
|
228
281
|
|
|
@@ -238,23 +291,32 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./SERVERS.md).*
|
|
|
238
291
|
| `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
|
|
239
292
|
| `home-hosted migrate` | bring `servers.config.json` up to this release's schema (`--dry-run`, `--yes`) |
|
|
240
293
|
| `home-hosted init` | scaffold a project that keeps `state/` and its data in the repo |
|
|
294
|
+
| `home-hosted ui-switch` | install a UI from a release asset, a zip file or a URL (interactive) |
|
|
241
295
|
| `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
|
|
242
296
|
|
|
243
297
|
<details>
|
|
244
298
|
<summary><b>⚙️ Flags</b></summary>
|
|
245
299
|
|
|
300
|
+
`home-hosted <command> --help` prints what that command takes.
|
|
301
|
+
|
|
246
302
|
```text
|
|
247
|
-
-c
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
303
|
+
up, restart -c/--config -p/--port --host --open --no-autostart --foreground --print-config
|
|
304
|
+
|
|
305
|
+
down (no flags)
|
|
306
|
+
status --json
|
|
307
|
+
init --dir --name --pm --no-install -y/--yes
|
|
308
|
+
set-password --clear
|
|
309
|
+
set-token --generate --clear
|
|
310
|
+
migrate --config --dry-run -y/--yes
|
|
311
|
+
ui-switch --repo --tag --asset --file --list --token -y/--yes
|
|
312
|
+
ui-revert (no flags)
|
|
313
|
+
|
|
314
|
+
every command --home <dir> --project <dir> (or $HHOSTED_HOME, $HHOSTED_PROJECT)
|
|
315
|
+
env vars HHOSTED_PASSWORD, HHOSTED_MIGRATE=allow, HHOSTED_TOKEN, GITHUB_TOKEN or GH_TOKEN
|
|
256
316
|
```
|
|
257
317
|
|
|
318
|
+
`up` and `restart` share the same flags: `restart` is `down`, then `up` with exactly what it was given.
|
|
319
|
+
|
|
258
320
|
</details>
|
|
259
321
|
|
|
260
322
|
<details>
|
|
@@ -299,10 +361,10 @@ Everything binds `127.0.0.1` until you say otherwise.
|
|
|
299
361
|
- **Port conflicts** are named — `port 4010 is already in use (pid 4242)` — and can be resolved from
|
|
300
362
|
a confirmation popover on that banner or card. The process is looked up again at that moment,
|
|
301
363
|
never taken from the message, and anything the panel supervises is refused, not killed. A server
|
|
302
|
-
that [restarts itself](./SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
|
|
364
|
+
that [restarts itself](./docs/SERVERS.md#when-a-program-restarts-itself) can be followed, or replaced
|
|
303
365
|
with a supervised copy.
|
|
304
|
-
- **Secrets never enter the config**: the password hash, the API token hash
|
|
305
|
-
|
|
366
|
+
- **Secrets never enter the config**: the password hash, the API token hash and the Telegram bot
|
|
367
|
+
token live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`; the TLS pair in `.tls/`.
|
|
306
368
|
- **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https, or
|
|
307
369
|
upload a PEM pair and let home-hosted terminate TLS itself.
|
|
308
370
|
|
|
@@ -313,14 +375,16 @@ Everything binds `127.0.0.1` until you say otherwise.
|
|
|
313
375
|
Telegram, when something happens while you are not looking: a server that gave up restarting, a
|
|
314
376
|
failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load,
|
|
315
377
|
temperature). Opt-in, rate-limited per server *and* reason, and the bot token stays in the secrets
|
|
316
|
-
file. **Two minutes of setup: [NOTIFICATIONS.md](./NOTIFICATIONS.md).**
|
|
378
|
+
file. **Two minutes of setup: [NOTIFICATIONS.md](./docs/NOTIFICATIONS.md).**
|
|
317
379
|
|
|
318
380
|
---
|
|
319
381
|
|
|
320
382
|
## 💾 Backups
|
|
321
383
|
|
|
322
384
|
**Settings → Backups** archives the config, secrets, TLS pair and every data directory your entries
|
|
323
|
-
declare — an ordinary `.zip`, or WinZip AES-256 with a password, restored per path.
|
|
385
|
+
declare — an ordinary `.zip`, or WinZip AES-256 with a password, restored per path. Known build output
|
|
386
|
+
and dependency directories (`node_modules`, `dist`, `.next`, framework caches) are skipped per entry;
|
|
387
|
+
`backupIgnoreGenerated: false` captures them anyway.
|
|
324
388
|
|
|
325
389
|
<details>
|
|
326
390
|
<summary><b>🚚 One archive is a whole setup</b></summary>
|
|
@@ -347,6 +411,9 @@ Two ship in this repo: `uis/stock`, and `uis/noc-console` for TUI and shortcuts
|
|
|
347
411
|
attaches both as `home-hosted-ui-<name>.zip`. Yours can be anything that compiles to static files —
|
|
348
412
|
the server never cares what built it.
|
|
349
413
|
|
|
414
|
+
<sub>Install a UI from the CLI: `home-hosted ui-switch` — with no flags it fetches the official asset
|
|
415
|
+
built for this release.</sub>
|
|
416
|
+
|
|
350
417
|
<details>
|
|
351
418
|
<summary><b>🤖 Or have an agent build the UI you actually want</b></summary>
|
|
352
419
|
|
|
@@ -355,7 +422,7 @@ specific:
|
|
|
355
422
|
|
|
356
423
|
> Help me build a UI for `home-hosted`: nostalgic game theme, including … features.
|
|
357
424
|
|
|
358
|
-
[UI_CREATION.md](./UI_CREATION.md) has the endpoints, the SSE frames, the auth rules and a checklist.
|
|
425
|
+
[UI_CREATION.md](./docs/UI_CREATION.md) has the endpoints, the SSE frames, the auth rules and a checklist.
|
|
359
426
|
|
|
360
427
|
</details>
|
|
361
428
|
|
|
@@ -384,7 +451,7 @@ the same way: every supervised process tree is stopped before the panel exits.
|
|
|
384
451
|
|
|
385
452
|
A supervised server whose port is taken is reported rather than started over — the panel names the
|
|
386
453
|
holder and offers to free it, and a program that restarts itself can be followed or reclaimed instead
|
|
387
|
-
([SERVERS.md](./SERVERS.md#a-busy-port)). The control port itself is checked before the listener is
|
|
454
|
+
([SERVERS.md](./docs/SERVERS.md#a-busy-port)). The control port itself is checked before the listener is
|
|
388
455
|
opened.
|
|
389
456
|
|
|
390
457
|
</details>
|
|
@@ -395,13 +462,13 @@ opened.
|
|
|
395
462
|
`$HHOSTED_HOME`, default `~/.home-hosted`:
|
|
396
463
|
|
|
397
464
|
```text
|
|
398
|
-
servers.config.json
|
|
399
|
-
servers.config.schema.json
|
|
400
|
-
.control-secrets.json
|
|
401
|
-
.logs/
|
|
402
|
-
.tls/
|
|
403
|
-
.backups/
|
|
404
|
-
run.json
|
|
465
|
+
servers.config.json your servers, plus meta: which release and schema wrote it
|
|
466
|
+
servers.config.schema.json regenerated on every start, for editor autocomplete
|
|
467
|
+
.control-secrets.json password hash + API token hash + Telegram token (mode 0600)
|
|
468
|
+
.logs/ rotated per-server logs + history
|
|
469
|
+
.tls/ an uploaded PEM pair
|
|
470
|
+
.backups/ zip archives
|
|
471
|
+
run.json the running panel (pid, url, token, mode 0600)
|
|
405
472
|
```
|
|
406
473
|
|
|
407
474
|
`home-hosted status` prints the paths.
|
|
@@ -430,10 +497,13 @@ expose them to an unprivileged process, and adopting a self-restarted process is
|
|
|
430
497
|
|
|
431
498
|
```text
|
|
432
499
|
src/ control plane: config, supervisor, API, providers, services
|
|
433
|
-
src/cli.ts the command line
|
|
500
|
+
src/cli.ts the command line; one file per command under src/cli/
|
|
434
501
|
src/index.ts the control plane itself, used by `up --foreground`
|
|
435
|
-
uis/ UIs: `stock` (shipped) and alternatives — any framework, static output
|
|
502
|
+
uis/ UIs: `stock` (shipped in the package) and alternatives — any framework, static output
|
|
436
503
|
bin/ the published entry point
|
|
504
|
+
docs/ topic docs, UI examples and the README's media
|
|
505
|
+
scripts/ builds, typechecks, media capture, release helpers
|
|
506
|
+
test/ the vitest suite
|
|
437
507
|
```
|
|
438
508
|
|
|
439
509
|
`pnpm dev` runs the panel with `tsx watch` plus the stock UI's dev server (state goes to
|
|
@@ -441,15 +511,15 @@ bin/ the published entry point
|
|
|
441
511
|
types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
|
|
442
512
|
|
|
443
513
|
<details>
|
|
444
|
-
<summary><b>📚
|
|
514
|
+
<summary><b>📚 Which doc do I need?</b></summary>
|
|
445
515
|
|
|
446
|
-
|
|
|
516
|
+
| if you want to… | read |
|
|
447
517
|
| --- | --- |
|
|
448
|
-
|
|
|
449
|
-
| [NOTIFICATIONS.md](./NOTIFICATIONS.md) |
|
|
450
|
-
| [UI_CREATION.md](./UI_CREATION.md) |
|
|
451
|
-
|
|
|
452
|
-
| [/openapi/ui](http://127.0.0.1:3999/openapi/ui) |
|
|
518
|
+
| declare a server: every field, placeholders, port conflicts | [SERVERS.md](./docs/SERVERS.md) |
|
|
519
|
+
| get Telegram alerts working end to end | [NOTIFICATIONS.md](./docs/NOTIFICATIONS.md) |
|
|
520
|
+
| build a UI against the API | [UI_CREATION.md](./docs/UI_CREATION.md) |
|
|
521
|
+
| change the internals: architecture and the rules | [AGENTS.md](./AGENTS.md) |
|
|
522
|
+
| poke the live API on your own panel | [/openapi/ui](http://127.0.0.1:3999/openapi/ui) |
|
|
453
523
|
|
|
454
524
|
</details>
|
|
455
525
|
|