home-hosted 0.2.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 +128 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/UI_CREATION.md +148 -0
- package/bin/home-hosted.mjs +34 -0
- package/dist/cli.js +7238 -0
- package/dist/cli.js.map +1 -0
- package/package.json +112 -0
- package/uis/stock/dist/assets/index-CmkrtNU0.css +2 -0
- package/uis/stock/dist/assets/index-DqcdQi-W.js +47 -0
- package/uis/stock/dist/assets/jetbrains-mono-latin-B9CIFXIH.woff2 +0 -0
- package/uis/stock/dist/assets/jetbrains-mono-latin-ext-DBQx-q_a.woff2 +0 -0
- package/uis/stock/dist/assets/plex-sans-latin-IvpUvPa2.woff2 +0 -0
- package/uis/stock/dist/assets/plex-sans-latin-ext-CIII54If.woff2 +0 -0
- package/uis/stock/dist/index.html +25 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
`home-hosted` is a Node 24 / TypeScript harness for self-hosted servers: `up` starts a Hono/srvx
|
|
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`; UI authors: `UI_CREATION.md`.
|
|
6
|
+
|
|
7
|
+
State lives only in `$HHOSTED_HOME` (default `~/.home-hosted`): `servers.config.json`,
|
|
8
|
+
`.control-secrets.json` (0600), `.logs/`, `.tls/`, `.backups/`, `.ui/`, and `run.json` — the live
|
|
9
|
+
daemon's pid/url/token, 0600. The package ships **no servers**: never commit a config, a seed
|
|
10
|
+
entry, or a path that names one.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm run up|down|restart|status # detached; `down` asks /_hh/shutdown, signals are the fallback
|
|
16
|
+
pnpm run start # up --foreground (systemd, docker, a foreground shell)
|
|
17
|
+
pnpm dev # tsx-watch panel :3999 + the stock UI's Vite :3998 (proxies /api)
|
|
18
|
+
pnpm run build # dist/cli.js + the stock UI (uis/stock/dist)
|
|
19
|
+
pnpm run build:uis # every UI under uis/, zipped into uis/dist/ (release assets)
|
|
20
|
+
pnpm run quickcheck # eslint + tsc + vue-tsc for every UI under uis/
|
|
21
|
+
pnpm exec vitest run # `pnpm test` is vitest in watch mode
|
|
22
|
+
pnpm run check # quickcheck + vitest run --coverage
|
|
23
|
+
pnpm run set-password # non-interactive through HHOSTED_PASSWORD
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Releases are dispatched from `.github/workflows/release.yml` with a version: changelogen writes the
|
|
27
|
+
changelog, bumps, commits and tags, the GitHub release carries the UI zips, and npm publishes through
|
|
28
|
+
trusted publishing.
|
|
29
|
+
|
|
30
|
+
## Architecture (and why)
|
|
31
|
+
|
|
32
|
+
- `src/cli.ts` — the CLI. Its only static imports are node builtins: `--home`/`--project` must set
|
|
33
|
+
`HHOSTED_HOME`/`HHOSTED_PROJECT` before any `#src` module resolves paths, so every `#src` import is
|
|
34
|
+
dynamic. `up` re-spawns itself detached as `up --foreground`.
|
|
35
|
+
- `src/index.ts` — `runControlPlane()`: wiring, startup guards (exposure, free port, live run.json),
|
|
36
|
+
`run.json`, signals. Wiring belongs here and nowhere else.
|
|
37
|
+
- `src/app.ts` — the Hono root, chained routes only. `/_hh` is mounted *before* the `/api/*` auth
|
|
38
|
+
guard on purpose (a local `down` uses run.json's token, not a session). `AppType` is the route type
|
|
39
|
+
that `hc<AppType>` clients and the OpenAPI document derive from.
|
|
40
|
+
- `src/api/**` — one file per URL group (`$.routes.ts` = several routes), mirroring the path.
|
|
41
|
+
- `src/shared/contracts.ts` — every ArkType schema (config, API and SSE DTOs), shared with the UIs;
|
|
42
|
+
the OpenAPI spec is generated from it, never hand-written.
|
|
43
|
+
- `src/config/` — `schema.ts` (on-disk shape), `store.ts` (validate/merge/atomic commit, reports
|
|
44
|
+
`configError` instead of throwing on a bad file), `secrets.ts`, `seed.ts`.
|
|
45
|
+
- `src/providers/` — stateless leaves: process, port, proc, health-check, host, telegram, archive.
|
|
46
|
+
- `src/services/` — stateful orchestration: supervisor, control-server, state, auth + exposure,
|
|
47
|
+
dependencies, history, log-buffer/log-files, notifications, host-monitor, backups, tls, ui.
|
|
48
|
+
- `src/helpers/` — paths (`dataRoot` vs `projectDir`), daemon (run.json + a loopback probe that
|
|
49
|
+
bypasses `fetch`, so TLS with a self-signed pair still answers), error, validator, atomic,
|
|
50
|
+
template, env-file, openapi, factory.
|
|
51
|
+
- `uis/<name>/` — each UI is a Vite app (Vue 3 + Tailwind v4) built through `uis/vite.shared.ts`;
|
|
52
|
+
`stock` is the one shipped inside the package. Aliases: `@` → that UI's `src`, `@shared` →
|
|
53
|
+
`src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
|
|
54
|
+
- `bin/home-hosted.mjs` — the published bin: `dist/cli.js`, or `src/cli.ts` through tsx when the
|
|
55
|
+
build is missing (a linked checkout).
|
|
56
|
+
|
|
57
|
+
## Conventions
|
|
58
|
+
|
|
59
|
+
- `#src/*` imports inside `src/`; UIs use `@shared/*`.
|
|
60
|
+
- ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
|
|
61
|
+
`c.req.valid(...)`; ad-hoc payloads use `parseOrThrow`. Schemas reject undeclared keys.
|
|
62
|
+
- Every failure is a `DetailedError` (`@namesmt/utils`), mapped by `src/helpers/error.ts` into one
|
|
63
|
+
envelope `{ message, code, detail }`. Never hand-roll `c.json({ error })`.
|
|
64
|
+
- Document routes with `describeRoute` + `jsonBody(schema)`; `jsonBody` needs a real schema.
|
|
65
|
+
- Patch schemas carry no defaults; nested groups (`restart`/`health`/`stop`/`auth`/`tls`/`telegram`/
|
|
66
|
+
`http`) merge key-by-key, and an explicit `null` clears a key.
|
|
67
|
+
- Two-sided bounds read inclusively (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
|
|
68
|
+
pins every boundary and the patch/schema parity — update it with any schema change.
|
|
69
|
+
- Conventional commits; ESLint via `@antfu/eslint-config`; sparse comments.
|
|
70
|
+
|
|
71
|
+
## Rules that matter
|
|
72
|
+
|
|
73
|
+
- **Server-agnostic.** No blessed ids, no `dataDir`-style globals: a server gets only its own
|
|
74
|
+
`command`/`args`/`env`/`dataEnvs`/`envFile`/`bootstrap`. A test fails if a core file learns one.
|
|
75
|
+
- **Paths.** `dataRoot` is state; `projectDir` is the base for relative entry paths. `{id}{port}`
|
|
76
|
+
`{host}{bind}{cwd}{projectDir}{dataRoot}{home}` and `${ENV}` expand in config; there is no
|
|
77
|
+
package-relative state.
|
|
78
|
+
- **Secrets never enter the config.** Password hash, bot token and TLS key live in the 0600 secrets
|
|
79
|
+
file; the config holds policy.
|
|
80
|
+
- **Never expose beyond loopback without auth and a non-default password.** `checkExposure()` is the
|
|
81
|
+
single rule, enforced at startup, on every settings write, and in the UI.
|
|
82
|
+
- **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
|
|
83
|
+
`$HHOSTED_HOME/.ui` overrides the packaged UI at runtime.
|
|
84
|
+
- **A restore writes only where a config says** — archive allowlist, `origin` matching first,
|
|
85
|
+
symlinks never recreated.
|
|
86
|
+
- **Cross-platform.** Linux/macOS/Windows: `/proc` vs `ps` vs Win32_Process, process groups vs
|
|
87
|
+
`taskkill /T`, graceful fallbacks, no shell utilities assumed.
|
|
88
|
+
- **Writes are atomic** (`writeFileAtomic`) and validated before commit.
|
|
89
|
+
|
|
90
|
+
## Gotchas
|
|
91
|
+
|
|
92
|
+
- `run.json` is the daemon's identity and `down`'s credential; keep `runtimeSchema` in sync with the
|
|
93
|
+
`Runtime` written in `src/index.ts`.
|
|
94
|
+
- Moving the listener (host/port/TLS) kills the connection answering that request, so
|
|
95
|
+
`PATCH /api/settings`, the TLS routes and `/_hh/shutdown` defer with `afterResponse()`.
|
|
96
|
+
- `Supervisor.start()` sets `starting` synchronously before its first await, and `stop()` sets
|
|
97
|
+
`stopping` first: overlapping calls would double-spawn or resurrect a stopped process. Tests must
|
|
98
|
+
call `supervisor.dispose()`.
|
|
99
|
+
- Port preflight re-probes after 300 ms — a just-closed listener can still complete a handshake.
|
|
100
|
+
- `stop.killPortHolders` frees a port only from a *listener* that is not our own process tree. Broad
|
|
101
|
+
`lsof -ti:<port>` sweeps and pid-as-text parses have killed supervisors in the field; don't add one.
|
|
102
|
+
- `ServerView.config.port` is normalized to `number | null`; the hand-narrowed types in
|
|
103
|
+
`contracts.ts` are deliberate.
|
|
104
|
+
- ArkType: an optional property (`'x?'`) rejects an explicit `undefined` (omit the key). Fields a UI
|
|
105
|
+
must clear are `'type | null?'`.
|
|
106
|
+
- Server args are logged *before* `${VAR}` expansion, so an expanded secret never reaches the log
|
|
107
|
+
buffer, disk, SSE or Telegram.
|
|
108
|
+
- Backups are zips; a password makes them WinZip AES-256/AE-2, and zero-byte entries stay
|
|
109
|
+
unencrypted on purpose (p7zip 16.02 reports a CRC failure otherwise). `list()` is sync, so
|
|
110
|
+
encryption flags are cached and refreshed in the background.
|
|
111
|
+
- `vite.server.config.ts` targets `node22` while `engines` requires >= 24 — deliberate margin, leave it.
|
|
112
|
+
- `pnpm test` watches; CI runs `vitest run`.
|
|
113
|
+
|
|
114
|
+
## Where to extend
|
|
115
|
+
|
|
116
|
+
- **Route**: `src/api/<x>.ts` (chained factory) → mount in `src/app.ts` → schemas in
|
|
117
|
+
`src/shared/contracts.ts` → `describeRoute` + `jsonBody`.
|
|
118
|
+
- **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/store.ts` when
|
|
119
|
+
nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
|
|
120
|
+
- **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
|
|
121
|
+
`node scripts/build-uis.mjs <name> --zip`. The contract is `UI_CREATION.md`.
|
|
122
|
+
- **Capability**: a stateless `src/providers/*` returning plain data.
|
|
123
|
+
|
|
124
|
+
## Publishing
|
|
125
|
+
|
|
126
|
+
`pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`,
|
|
127
|
+
`UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
|
|
128
|
+
build; `vue`/`vue-router` are devDependencies because the UIs are prebuilt.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NamesMT <https://github.com/namesmt>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🏠 home-hosted
|
|
4
|
+
|
|
5
|
+
**The control panel for everything you self-host at home.**
|
|
6
|
+
|
|
7
|
+
<sub>The harness for your servers.</sub>
|
|
8
|
+
|
|
9
|
+
Point it at the things you run — a gateway, a media server, a bot, a database — and it
|
|
10
|
+
starts them, watches them, restarts them when they die, and gives you one page to see
|
|
11
|
+
what is actually going on. Or BYOU — [bring your own UI](#-bring-your-own-ui-byou), for a
|
|
12
|
+
specialized panel that fits you exactly.
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/home-hosted)
|
|
15
|
+
[](https://www.npmjs.com/package/home-hosted)
|
|
16
|
+
[](./LICENSE)
|
|
17
|
+
[](https://nodejs.org)
|
|
18
|
+
|
|
19
|
+
[🚀 Quick start](#-quick-start) · [✨ Features](#-features) · [🧩 Adding a server](#-adding-a-server) · [🛠 CLI](#-cli) · [🔐 Security](#-security) · [💾 Backups](#-backups)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🤔 Why?
|
|
26
|
+
|
|
27
|
+
Running services on a home machine usually means one of two extremes: `tmux` sessions you
|
|
28
|
+
forget about, or a hand-written systemd unit per service (times six).
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| ❌ **"Is it still running?"** | You check with `ps`, then `curl`, then hope. |
|
|
33
|
+
| ❌ **Silent deaths** | Something crashes at 3am and you notice days later. |
|
|
34
|
+
| ❌ **One terminal per service** | Logs scroll away in tabs you closed. |
|
|
35
|
+
| ❌ **Fragile restarts** | The box reboots and half the stack is gone. |
|
|
36
|
+
| ✅ **home-hosted** | Declare it once, watch it forever, one command to stop it all. |
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
┌──────────────────────────────────────┐
|
|
40
|
+
your browser ──▶│ home-hosted · 127.0.0.1:3999 │
|
|
41
|
+
│ Vue panel + JSON API + SSE logs │
|
|
42
|
+
└───────────────┬──────────────────────┘
|
|
43
|
+
│ supervises
|
|
44
|
+
┌──────────────────────────┼──────────────────────────┐
|
|
45
|
+
▼ ▼ ▼
|
|
46
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
47
|
+
│ gateway │ │ files │ │ bot │
|
|
48
|
+
│ :4000 │ │ :4010 │ │ ... │
|
|
49
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
50
|
+
health ✓ health ✓ restarts ↻
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
It ships with **nothing**. No blessed paths, no "data directory" setting, no opinion about
|
|
54
|
+
what you run — a server is a command, some arguments, and the environment you give it.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ⚡ Quick start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx home-hosted # start it — detached, it stays running
|
|
62
|
+
npx home-hosted status # where is it, is it healthy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That is it. The panel is on **<http://127.0.0.1:3999>**, and it keeps running after the
|
|
66
|
+
terminal closes. Stop it whenever you like:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx home-hosted down # stops the panel *and* everything it started
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> [!NOTE]
|
|
73
|
+
> The first boot writes a default password (`hh`) so the panel is never unprotected. Change
|
|
74
|
+
> it under **Settings → Password** — binding beyond `127.0.0.1` stays refused until you do.
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary><b>📦 Install it instead of npx-ing it</b></summary>
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install -g home-hosted
|
|
81
|
+
home-hosted up
|
|
82
|
+
|
|
83
|
+
# or keep a project with its own servers *and* its own state directory
|
|
84
|
+
cd ~/my-servers
|
|
85
|
+
HHOSTED_HOME=./state npx home-hosted up
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Before the first npm release, the same commands work from a checkout: `pnpm install && pnpm run up`
|
|
89
|
+
in this repo, or `pnpm link` into a project that should depend on it.
|
|
90
|
+
|
|
91
|
+
Everything home-hosted owns — config, secrets, logs, TLS, backups — lives in one directory:
|
|
92
|
+
`$HHOSTED_HOME`, defaulting to `~/.home-hosted`. Delete it and nothing of yours is left behind.
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary><b>📁 Keep it in a project you can take anywhere (and share)</b></summary>
|
|
98
|
+
|
|
99
|
+
Pin the version, install the servers next to it, and commit the whole project — it *is* the
|
|
100
|
+
setup:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
mkdir my-servers && cd my-servers
|
|
104
|
+
npm init -y
|
|
105
|
+
npm install home-hosted@0.2.0 9router serve # pinned, so `git clone && npm ci` is enough
|
|
106
|
+
|
|
107
|
+
npx home-hosted up --home ./state # state lives inside the project
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```jsonc
|
|
111
|
+
// package.json
|
|
112
|
+
{
|
|
113
|
+
"scripts": {
|
|
114
|
+
"up": "home-hosted up --home ./state",
|
|
115
|
+
"down": "home-hosted down --home ./state",
|
|
116
|
+
"status": "home-hosted status --home ./state"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```gitignore
|
|
122
|
+
state/* # secrets, logs, TLS keys and archives stay local…
|
|
123
|
+
!state/servers.config.json # …but the server definitions are committed
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
<sub>Call them as `npm run up` — `npm up` is npm's own update, not your script.</sub>
|
|
127
|
+
|
|
128
|
+
One clone, `npm ci`, `npm run up`, and the whole setup is up on any machine with Node.
|
|
129
|
+
**Settings → Backups** exports the same thing as one archive for someone else to restore.
|
|
130
|
+
|
|
131
|
+
</details>
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary><b>🧰 Run it under systemd or Docker (no daemon needed)</b></summary>
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
home-hosted up --foreground # stays in the foreground, logs to stderr
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ini
|
|
141
|
+
[Unit]
|
|
142
|
+
Description=home-hosted
|
|
143
|
+
[Service]
|
|
144
|
+
ExecStart=/usr/local/bin/home-hosted up --foreground
|
|
145
|
+
Environment=HHOSTED_HOME=/srv/home-hosted
|
|
146
|
+
Restart=always
|
|
147
|
+
[Install]
|
|
148
|
+
WantedBy=multi-user.target
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## ✨ Features
|
|
156
|
+
|
|
157
|
+
| | |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| 🚦 **Lifecycle** | Start, stop, restart from the panel or the API; `autostart` entries come up with it. |
|
|
160
|
+
| ♻️ **Auto-restart** | Exponential backoff on crash, with the counter reset once a process stays up. |
|
|
161
|
+
| 🩺 **Health that acts** | TCP or HTTP probes per server: warn on the card, force a restart after a timeout, and check ports before starting. |
|
|
162
|
+
| 🔗 **Ordered startup** | `dependsOn` waits for a dependency to be *healthy* — not merely spawned — and stops in reverse. |
|
|
163
|
+
| 📜 **Logs** | Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear. |
|
|
164
|
+
| 📈 **Resources** | CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart. |
|
|
165
|
+
| 🌡️ **Host vitals** | Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery. |
|
|
166
|
+
| 💾 **Backups** | One click for config, secrets, TLS and your declared data directories — plain `.zip`, or AES-256 with a password, restored per path. |
|
|
167
|
+
| 🚚 **Portable setup** | Restore a shared backup onto a blank instance and the whole server setup is back: definitions, data, secrets and all. |
|
|
168
|
+
| 🎨 **BYOU — Bring Your Own UI** | The panel is a static site you can replace: upload your own build (or ship a whole alternative), and `home-hosted ui-revert` puts the stock one back. See [UI_CREATION.md](./UI_CREATION.md). |
|
|
169
|
+
| 🔔 **Notifications** | Telegram (grammY) on crash, unhealthy, forced restart, recovery and host thresholds. |
|
|
170
|
+
| 🔐 **Security** | httpOnly cookie sessions, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password. |
|
|
171
|
+
| 🧩 **Server-agnostic** | `command` + `args` + `env` + `cwd`. Nothing in the code knows what you run. |
|
|
172
|
+
| 🖥 **Cross-platform** | Linux, macOS and Windows: `/proc`, `ps` or Win32_Process, process groups or `taskkill /T`, no shell dependencies. |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 🧩 Adding a server
|
|
177
|
+
|
|
178
|
+
Use **➕ Add server** in the panel, or write it into `servers.config.json`:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"id": "myapp",
|
|
183
|
+
"command": "node",
|
|
184
|
+
"args": ["server.js"],
|
|
185
|
+
"cwd": "./apps/myapp",
|
|
186
|
+
"port": 8080,
|
|
187
|
+
"autostart": true,
|
|
188
|
+
"env": { "NODE_ENV": "production" },
|
|
189
|
+
"health": { "mode": "http", "http": { "path": "/healthz" } }
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Relative paths resolve against the directory you ran `home-hosted` from, so a project can keep
|
|
194
|
+
its servers and its state together:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
cd ~/my-servers
|
|
198
|
+
HHOSTED_HOME=./state npx home-hosted up
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
<details>
|
|
202
|
+
<summary><b>🧾 Field reference</b></summary>
|
|
203
|
+
|
|
204
|
+
| field | what it does |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
|
|
207
|
+
| `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
|
|
208
|
+
| `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
|
|
209
|
+
| `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
|
|
210
|
+
| `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
|
|
211
|
+
| `stop.*` | signal, `killGroup`, grace period, and whether to sweep leftover port holders |
|
|
212
|
+
| `dependsOn` | ids that must be healthy first; stopped in reverse order |
|
|
213
|
+
| `resources.maxRssBytes` | restart when the process tree grows past a limit |
|
|
214
|
+
| `bootstrap` | one command to run once before the first start (migrations, warmups) |
|
|
215
|
+
| `backupPaths` | extra paths this entry owns, included in backups |
|
|
216
|
+
|
|
217
|
+
Placeholders: `{id}` `{label}` `{port}` `{host}` `{displayHost}` `{bind}` `{lanIp}` `{cwd}`
|
|
218
|
+
`{projectDir}` `{dataRoot}` `{home}`, plus `${VAR}` from the environment. Unknown placeholders
|
|
219
|
+
stay visible instead of silently becoming empty.
|
|
220
|
+
|
|
221
|
+
Every supervised process also gets `HHOSTED_SERVER_ID` and `HHOSTED_CONTROL_PORT`, so a
|
|
222
|
+
service can tell which entry it is and how to reach the panel.
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 🛠 CLI
|
|
229
|
+
|
|
230
|
+
| command | |
|
|
231
|
+
| --- | --- |
|
|
232
|
+
| `home-hosted up` | start the panel detached, and keep it alive in the background |
|
|
233
|
+
| `home-hosted down` | stop it cleanly — supervised processes included |
|
|
234
|
+
| `home-hosted restart` | `down`, then `up` |
|
|
235
|
+
| `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
|
|
236
|
+
| `home-hosted set-password` | set the panel password without opening a browser |
|
|
237
|
+
| `home-hosted ui-revert` | go back to the stock panel UI after uploading your own |
|
|
238
|
+
|
|
239
|
+
<details>
|
|
240
|
+
<summary><b>⚙️ Flags</b></summary>
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
-c, --config <file> servers config (default: <state>/servers.config.json)
|
|
244
|
+
-p, --port <port> control panel port (default: 3999)
|
|
245
|
+
--host <bind> local | lan | an ipv4 address
|
|
246
|
+
--open open the panel in a browser once it is up
|
|
247
|
+
--no-autostart do not start the entries marked autostart
|
|
248
|
+
--foreground run in this process instead of detaching
|
|
249
|
+
--print-config print the effective config and exit
|
|
250
|
+
--home <dir> state directory (or $HHOSTED_HOME)
|
|
251
|
+
--project <dir> base for relative paths (or $HHOSTED_PROJECT)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
</details>
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 🔐 Security
|
|
259
|
+
|
|
260
|
+
Everything binds `127.0.0.1` until you say otherwise.
|
|
261
|
+
|
|
262
|
+
- **A password is required to expose the panel.** Replace the default, then bind to `lan` —
|
|
263
|
+
in the UI, in the config, or with `--host lan`. The same guard applies in all three places.
|
|
264
|
+
- **Sessions** live in memory only; the cookie is `HttpOnly` and `SameSite=Strict`, and the
|
|
265
|
+
login route locks out repeated failures per IP.
|
|
266
|
+
- **Secrets never enter the config**: the password hash, the Telegram token and the TLS key
|
|
267
|
+
live in `$HHOSTED_HOME/.control-secrets.json` with mode `0600`.
|
|
268
|
+
- **Behind a proxy** turn on `trustProxy` and let `cookieSecure: auto` add `Secure` on https,
|
|
269
|
+
or upload a PEM pair and let home-hosted terminate TLS itself.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 💾 Backups
|
|
274
|
+
|
|
275
|
+
**Settings → Backups** archives the config, the secrets file, the TLS pair and the data
|
|
276
|
+
directories your entries declare — as an ordinary `.zip`:
|
|
277
|
+
|
|
278
|
+
- no password → **plain zip**, openable by every operating system out of the box
|
|
279
|
+
- with a password → **WinZip AES-256**, openable in 7-Zip, WinRAR, Keka, PeaZip, Ark…
|
|
280
|
+
- restore is selective: check config, secrets, TLS or individual data paths, and nothing
|
|
281
|
+
else is touched
|
|
282
|
+
|
|
283
|
+
### 🚚 One archive is a whole setup
|
|
284
|
+
|
|
285
|
+
Start a **blank** home-hosted anywhere — another machine, another user, a fresh container —
|
|
286
|
+
open **Settings → Backups**, upload the archive and restore: the server definitions come
|
|
287
|
+
back, their data lands where *this* machine's config says it should, and the entries marked
|
|
288
|
+
`autostart` are brought up right away. Nothing to re-declare by hand.
|
|
289
|
+
|
|
290
|
+
That works because a backup carries its own `servers.config.json`, and paths are matched by
|
|
291
|
+
the **declaration** (`9router:DATA_DIR`), not by the absolute path they had on the source
|
|
292
|
+
machine — so `{home}` stays this user's home. A restore never writes anywhere a config does
|
|
293
|
+
not declare: yours, or the one inside the archive you chose to restore.
|
|
294
|
+
|
|
295
|
+
<details>
|
|
296
|
+
<summary><b>🗂 Declaring data directories</b></summary>
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"dataEnvs": { "DATA_DIR": "{home}/.myapp" },
|
|
301
|
+
"backupPaths": ["{home}/.myapp/uploads"]
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`dataEnvs` is exported to the process *and* backed up, so a data directory is declared once.
|
|
306
|
+
A path already covered by a declared parent is skipped.
|
|
307
|
+
|
|
308
|
+
</details>
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
Two UIs live in this repo: **stock** (the panel you get by default) and **noc-console**, an
|
|
313
|
+
operations console over the same API. `pnpm run build:uis` builds and zips every one of them, and
|
|
314
|
+
each release attaches them as `home-hosted-ui-<name>.zip` — install one from Settings → Interface.
|
|
315
|
+
|
|
316
|
+
## 🎨 Bring your own UI (BYOU)
|
|
317
|
+
|
|
318
|
+
The panel is a static site served from `$HHOSTED_HOME/.ui` when one is installed, and from
|
|
319
|
+
the package otherwise. **Settings → Interface** takes a zip; that is the whole install step,
|
|
320
|
+
no restart and no fork.
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
home-hosted ui-revert # if it breaks, the stock panel is one command away
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The API is documented at `/openapi/ui` (and `/openapi/spec.json`), generated from the same
|
|
327
|
+
schemas the server validates with, so a UI can be built in any framework — the stock one is
|
|
328
|
+
just the first client. [UI_CREATION.md](./UI_CREATION.md) has the contract, the rules and a
|
|
329
|
+
worked example.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 🌐 HTTP API
|
|
334
|
+
|
|
335
|
+
The panel is a client of its own API, so everything is scriptable:
|
|
336
|
+
|
|
337
|
+
| | |
|
|
338
|
+
| --- | --- |
|
|
339
|
+
| `GET /api/state` | the full snapshot: config, live status, host vitals |
|
|
340
|
+
| `GET /api/servers/:id/stream` | SSE stream of state and log lines |
|
|
341
|
+
| `POST /api/servers/:id/{start,stop,restart}` | lifecycle |
|
|
342
|
+
| `PATCH /api/servers/:id`, `PATCH /api/settings` | edit configuration |
|
|
343
|
+
| `GET /api/logs`, `/api/backups`, `/api/notifications` | logs, archives, Telegram |
|
|
344
|
+
| `GET /healthz` | no session needed — the one an external monitor wants |
|
|
345
|
+
| `GET /api/metrics` | Prometheus text |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## ❓ FAQ
|
|
350
|
+
|
|
351
|
+
<details>
|
|
352
|
+
<summary><b>Is it a systemd replacement?</b></summary>
|
|
353
|
+
|
|
354
|
+
No — it is a friendlier layer for the handful of things you run yourself. Keep systemd for the
|
|
355
|
+
panel itself (`--foreground`) and for system services; use home-hosted for the rest.
|
|
356
|
+
|
|
357
|
+
</details>
|
|
358
|
+
|
|
359
|
+
<details>
|
|
360
|
+
<summary><b>What happens to my servers when the panel stops?</b></summary>
|
|
361
|
+
|
|
362
|
+
`home-hosted down` stops them — that is the point of the command. `SIGTERM`/`SIGINT` are handled
|
|
363
|
+
the same way: every supervised process tree is stopped before the panel exits.
|
|
364
|
+
|
|
365
|
+
</details>
|
|
366
|
+
|
|
367
|
+
<details>
|
|
368
|
+
<summary><b>Which ports does it use?</b></summary>
|
|
369
|
+
|
|
370
|
+
Just the control panel, `3999` by default. Supervised servers use the ports you give them.
|
|
371
|
+
|
|
372
|
+
</details>
|
|
373
|
+
|
|
374
|
+
<details>
|
|
375
|
+
<summary><b>Where is my state?</b></summary>
|
|
376
|
+
|
|
377
|
+
`$HHOSTED_HOME`, default `~/.home-hosted`:
|
|
378
|
+
|
|
379
|
+
```text
|
|
380
|
+
servers.config.json your servers (the UI writes it back atomically)
|
|
381
|
+
servers.config.schema.json regenerated on every start, for editor autocomplete
|
|
382
|
+
.control-secrets.json password hash + Telegram token (mode 0600)
|
|
383
|
+
.logs/ rotated per-server logs + history
|
|
384
|
+
.tls/ an uploaded PEM pair
|
|
385
|
+
.backups/ zip archives
|
|
386
|
+
run.json the running panel (pid, url, token, mode 0600)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`home-hosted status` prints the paths.
|
|
390
|
+
|
|
391
|
+
</details>
|
|
392
|
+
|
|
393
|
+
<details>
|
|
394
|
+
<summary><b>Windows support, really?</b></summary>
|
|
395
|
+
|
|
396
|
+
Yes. Process trees are sampled from Win32_Process, termination uses `taskkill /T`, and the
|
|
397
|
+
shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the
|
|
398
|
+
OS does not expose them to an unprivileged process.
|
|
399
|
+
|
|
400
|
+
</details>
|
|
401
|
+
|
|
402
|
+
<details>
|
|
403
|
+
<summary><b>Nothing starts and the port is busy</b></summary>
|
|
404
|
+
|
|
405
|
+
A supervised server whose port is taken is reported rather than started over — the panel names
|
|
406
|
+
the holder. The control port itself is checked before the listener is opened.
|
|
407
|
+
|
|
408
|
+
</details>
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 🗂 Layout
|
|
413
|
+
|
|
414
|
+
```text
|
|
415
|
+
src/ control plane: config, supervisor, API, providers, services
|
|
416
|
+
src/cli.ts the command line (up/down/status/restart/set-password)
|
|
417
|
+
src/index.ts the control plane itself, used by `up --foreground`
|
|
418
|
+
uis/ UIs: `stock` (shipped) and alternatives, each a Vite app
|
|
419
|
+
bin/ the published entry point
|
|
420
|
+
vite.server.config.ts bundles src/cli.ts into dist/cli.js
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Development: `pnpm dev` runs the panel with `tsx watch` plus the Vite dev server (state goes to
|
|
424
|
+
`.dev-state/`), `pnpm build` produces `dist/` and `uis/stock/dist/`, `pnpm quickcheck` is lint plus
|
|
425
|
+
types, and `pnpm test` is vitest.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
<div align="center">
|
|
430
|
+
|
|
431
|
+
**MIT**
|
|
432
|
+
|
|
433
|
+
</div>
|