home-hosted 0.6.1 → 0.6.3
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 +51 -9
- package/README.md +30 -9
- package/dist/cli.js +1058 -90
- package/dist/cli.js.map +1 -1
- package/docs/SERVERS.md +47 -0
- package/package.json +1 -1
- package/uis/stock/dist/assets/{index-sj5eYUeD.js → index-CCPLyw-2.js} +6 -6
- package/uis/stock/dist/index.html +1 -1
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,8 @@ and serves a UI. User docs: `README.md`, `docs/SERVERS.md` (entries and port con
|
|
|
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/`,
|
|
10
|
-
`.backups/`, `.ui/`,
|
|
10
|
+
`.backups/`, `.ui/`, `.state/` (a persistent entry's nanny state, plus its 0600 spawn spec until the
|
|
11
|
+
nanny reads it), and `run.json` — the live daemon's pid/url/token, 0600. The package ships **no
|
|
11
12
|
servers**: never commit a config, a seed entry, or a path that names one.
|
|
12
13
|
|
|
13
14
|
## Commands
|
|
@@ -31,21 +32,39 @@ pnpm run media # regenerate docs/media (mockups, both served
|
|
|
31
32
|
The published bin is `home-hosted`, with an `hh` alias: both names run the same CLI.
|
|
32
33
|
|
|
33
34
|
Releases are dispatched from `.github/workflows/release.yml` with a version (and a `dry-run` switch
|
|
34
|
-
that stops before pushing). It verifies the version
|
|
35
|
-
|
|
35
|
+
that stops before pushing). It verifies the version, lints/types/tests, builds the CLI plus the stock
|
|
36
|
+
UI and every UI zip, lets changelogen write the changelog, bump `package.json`, commit and tag
|
|
36
37
|
`v<version>`, creates the GitHub release with the UI bundles attached, and publishes to npm through
|
|
37
38
|
trusted publishing (OIDC, no token). npm only offers a trusted publisher for a package that already
|
|
38
39
|
exists, so the first release has to be published by hand.
|
|
39
40
|
|
|
41
|
+
### Which version to dispatch
|
|
42
|
+
|
|
43
|
+
Below 1.0 the **minor is the breaking channel**: a fix or a non-breaking feature is a **patch**
|
|
44
|
+
(`0.6.2` → `0.6.3`), while a minor (`0.6.3` → `0.7.0`) means someone has to read the release notes and
|
|
45
|
+
act. A `feat:` commit does not decide this — ask what the user has to do about it.
|
|
46
|
+
|
|
47
|
+
Never edit `package.json` by hand; changelogen bumps, commits and tags inside the workflow:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
gh workflow run release.yml -f version=0.6.3 # -f dry-run=true to rehearse
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`scripts/check-release-version.mjs` refuses a patch while a breaking commit is pending, and warns on
|
|
54
|
+
a minor without one.
|
|
55
|
+
|
|
40
56
|
## Architecture (and why)
|
|
41
57
|
|
|
42
58
|
- `src/cli.ts` — the CLI's thin root. Two things happen before [citty](https://github.com/unjs/citty)
|
|
43
59
|
is asked anything: `--home`/`--project` are peeled off and applied (`src/cli/args.ts`), and the
|
|
44
60
|
curated dispatch — `help`/`version`, `unknown command`, and the `-p 4000` shorthand for `up` — is
|
|
45
61
|
decided, because `#src/helpers/paths.ts` resolves at import time. Its static imports stay node
|
|
46
|
-
builtins, citty and
|
|
62
|
+
builtins, citty and the two path-free local modules (`src/cli/args.ts`, `src/helpers/runtime.ts`);
|
|
63
|
+
every command is a lazy
|
|
47
64
|
`() => 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).
|
|
65
|
+
`#src` imports (that is the whole point of the pre-pass). The hidden `__nanny` is dispatched by an
|
|
66
|
+
early branch, not `subCommands`, so it stays out of the curated help and the unknown-command
|
|
67
|
+
message — the panel spawns it, nobody types it. citty's `runMain` is deliberately not
|
|
49
68
|
used: it prints its own usage and `console.error`s before `process.exit(1)`, replacing `fail()`'s
|
|
50
69
|
one error shape; the root calls `runCommand` and catches. `src/cli/<command>.ts` is one command per
|
|
51
70
|
file — citty owns dispatch and argument parsing, with `--no-autostart`/`--no-install` declared as
|
|
@@ -69,12 +88,17 @@ exists, so the first release has to be published by hand.
|
|
|
69
88
|
`migrations.ts` (the schema constant and the ordered step registry), `secrets.ts`, `seed.ts`.
|
|
70
89
|
- `src/providers/` — stateless leaves: `process` (spawn, `terminate`, `terminatePid` for a process we
|
|
71
90
|
adopted), `port` (probe, holder lookup, `terminatePids`), `proc` (the sampler, plus
|
|
72
|
-
`processCarriesServerId` for the environment marker
|
|
73
|
-
own successor: marker first, then resolved image +
|
|
91
|
+
`processCarriesServerId` for the environment marker and `processTreePids` for tree ownership),
|
|
92
|
+
`identity` (which port holder is this entry's own successor: marker first, then resolved image +
|
|
93
|
+
argv), `nanny` (a persistent entry's spec/state files and the liveness rules for one),
|
|
94
|
+
`log-tail` (an offset reader that survives rotation and truncation), health-check, host, telegram,
|
|
95
|
+
archive.
|
|
74
96
|
- `src/services/` — stateful orchestration: supervisor, control-server, config-watch, state,
|
|
75
97
|
auth + exposure, dependencies, history, log-buffer/log-files, notifications, host-monitor, backups,
|
|
76
98
|
tls, ui, plus `init` (the scaffold behind `home-hosted init`: a manifest, a `.gitignore`, and the
|
|
77
|
-
prompts stay in the CLI)
|
|
99
|
+
prompts stay in the CLI), `nanny` (the process a persistent entry runs under) and `log-relay` (the
|
|
100
|
+
tailer that feeds its lines to the supervisor). It names no server — the scaffold must stay as
|
|
101
|
+
neutral as the supervisor.
|
|
78
102
|
- `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
|
|
79
103
|
credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
|
|
80
104
|
a first-class credential (same authority as a signed-in browser) and is verified from the secrets
|
|
@@ -202,6 +226,22 @@ either is a last resort, and never an accidental one.
|
|
|
202
226
|
answer for them. `src/providers/identity.ts` owns the matching, and the argv it is given comes from
|
|
203
227
|
the supervisor's single `resolveSpawn()`, the same one the spawn itself used — keep it to one
|
|
204
228
|
resolver, because a second one that expands or filters args differently re-opens these cases.
|
|
229
|
+
Ownership covers the entry's whole **tree**, not the pid it recorded (`processTreePids`): a nanny
|
|
230
|
+
puts the real server one generation down, and so does any wrapper entry. The panel's own tree is
|
|
231
|
+
deliberately excluded — "ours" has to mean a process a server owns.
|
|
232
|
+
- **A persistent entry is run by its own nanny, not by the panel.** `persistent: true` makes the panel
|
|
233
|
+
spawn the hidden `__nanny` (`src/services/nanny.ts`), which owns the child's pipes, writes its JSONL
|
|
234
|
+
and mirrors its exit — the only arrangement that outlives the panel *and* keeps logging. The panel
|
|
235
|
+
skips such an entry in `stopAll()`/`dispose()` (`down` reports what it left running), reattaches on
|
|
236
|
+
boot through `.state/<id>.json` before any port preflight (`nannyIsAlive` = live pid **and** a fresh
|
|
237
|
+
heartbeat, the `HHOSTED_SERVER_ID` marker or the argv), tails that file for live logs, and reads
|
|
238
|
+
`lastExit` once so a crash nobody watched is reported. The spawn spec is consumed by the read and
|
|
239
|
+
swept at boot — it carries expanded env. Two lifetimes are pinned: the nanny **exits with its child**
|
|
240
|
+
(an inherited pipe would keep it alive, and the panel would report a healthy entry whose server is a
|
|
241
|
+
detached stranger), and a stop reaches the child by pid from the state file, because `SIGKILL` and
|
|
242
|
+
`killGroup: false` cannot be forwarded — which is why a self-restarting program belongs on `reclaim`.
|
|
243
|
+
The nanny never restarts anything: retries and health stay the supervisor's. `logs.persist: false`
|
|
244
|
+
keeps that file out of the Logs page but never stops it being written — it is the transport.
|
|
205
245
|
- **A port is only ever freed by re-listing its listeners.** `POST /api/servers/:id/free-port` never
|
|
206
246
|
trusts a pid quoted in a message, and refuses any listener in `supervisedPids()` (the panel plus
|
|
207
247
|
every entry's child) instead of killing it — a port held by a sibling is a config mistake.
|
|
@@ -268,7 +308,9 @@ either is a last resort, and never an accidental one.
|
|
|
268
308
|
the live output view empty until a remount — a test that reads the array itself will not catch it.
|
|
269
309
|
- An adopted process is not a `ChildProcess`, so nothing reports its exit: the tick polls liveness and
|
|
270
310
|
hands the entry back to the normal `afterExit` path. Its output is not captured either — it was
|
|
271
|
-
redirected by whoever spawned it.
|
|
311
|
+
redirected by whoever spawned it. The exception is a persistent entry, whose output the panel
|
|
312
|
+
*does* capture: its nanny writes the JSONL, and `log-relay` tails it into the same buffer and SSE
|
|
313
|
+
frames a pipe would feed.
|
|
272
314
|
- `stop.killPortHolders` frees a port only from a *listener* that is not our own process tree. Broad
|
|
273
315
|
`lsof -ti:<port>` sweeps and pid-as-text parses have killed supervisors in the field; don't add one.
|
|
274
316
|
`free-port` reuses the same lookup and adds the supervisor's own pid set on top.
|
package/README.md
CHANGED
|
@@ -40,8 +40,9 @@ You run a handful of services at home. The usual choices are extremes — 🧟 `
|
|
|
40
40
|
forget about, 📜 a hand-written systemd unit per service (times six), or 🐳 a whole docker/k8s
|
|
41
41
|
stack??? - too extreme! — plus 😩 monitoring, rebooting and changing the host machine, yuck!
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
puts the whole stack on one page, with deep backup support
|
|
43
|
+
🙂✨ home-hosted enhances on top: a panel/supervisor that starts them, watches them, restarts what
|
|
44
|
+
dies, and puts the whole stack on one page, with deep backup support — whether a server is a plain
|
|
45
|
+
command or a `docker compose` stack.
|
|
45
46
|
|
|
46
47
|
```text
|
|
47
48
|
┌──────────────────────────────────────┐
|
|
@@ -54,8 +55,13 @@ puts the whole stack on one page, with deep backup support.
|
|
|
54
55
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
55
56
|
│ gateway │ │ files │ │ bot │
|
|
56
57
|
│ :4000 │ │ :4010 │ │ ... │
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
└────┬────┘ └─────────┘ └─────────┘
|
|
59
|
+
│ compose up -d
|
|
60
|
+
▼
|
|
61
|
+
┌────────────┬────────────┬────────────┐
|
|
62
|
+
│ gateway │ postgres │ redis │ restarts ↻
|
|
63
|
+
└────────────┴────────────┴────────────┘
|
|
64
|
+
health ✓ (the published port is the probe)
|
|
59
65
|
```
|
|
60
66
|
|
|
61
67
|
| | |
|
|
@@ -147,7 +153,7 @@ data/ # per-server data directories, declared through da
|
|
|
147
153
|
|
|
148
154
|
One clone, `pnpm install --frozen-lockfile`, `pnpm run up` — the setup is up on any machine with Node.
|
|
149
155
|
Worked example, with per-server data inside the project:
|
|
150
|
-
**[hhosted-
|
|
156
|
+
**[hhosted-ai-pack](https://github.com/NamesMT/hhosted-ai-pack)**.
|
|
151
157
|
|
|
152
158
|
<sub>Call them as `pnpm run up` — `pnpm up` is pnpm's own update, not your script.</sub>
|
|
153
159
|
|
|
@@ -190,8 +196,8 @@ home-hosted set-token --generate
|
|
|
190
196
|
# hh_9uA2… (printed once; only its hash is kept, mode 0600)
|
|
191
197
|
|
|
192
198
|
curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
|
|
193
|
-
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/
|
|
194
|
-
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=
|
|
199
|
+
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/omniroute/restart
|
|
200
|
+
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=omniroute' # SSE
|
|
195
201
|
home-hosted status --json # machine-readable: pid, url, health, paths
|
|
196
202
|
```
|
|
197
203
|
|
|
@@ -284,7 +290,7 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
|
|
|
284
290
|
| command | |
|
|
285
291
|
| --- | --- |
|
|
286
292
|
| `home-hosted up` | start the panel detached, and keep it alive in the background |
|
|
287
|
-
| `home-hosted down` | stop it cleanly — supervised processes included |
|
|
293
|
+
| `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
|
|
288
294
|
| `home-hosted restart` | `down`, then `up` |
|
|
289
295
|
| `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
|
|
290
296
|
| `home-hosted set-password` | set the panel password without opening a browser |
|
|
@@ -396,7 +402,7 @@ archive and restore. Definitions come back, data lands where *this* machine's co
|
|
|
396
402
|
`autostart` entries come up immediately.
|
|
397
403
|
|
|
398
404
|
It works because an archive carries its own `servers.config.json` and paths are matched by the
|
|
399
|
-
**declaration** (`
|
|
405
|
+
**declaration** (`omniroute:DATA_DIR`), not by an absolute path from the source machine. A restore never
|
|
400
406
|
writes where no config declares.
|
|
401
407
|
|
|
402
408
|
</details>
|
|
@@ -448,6 +454,10 @@ panel itself (`--foreground`) and for system services; use home-hosted for the r
|
|
|
448
454
|
`home-hosted down` stops them — that is the point of the command. `SIGTERM`/`SIGINT` are handled
|
|
449
455
|
the same way: every supervised process tree is stopped before the panel exits.
|
|
450
456
|
|
|
457
|
+
An entry marked `persistent` is the exception, and `down` names it instead of stopping it: it runs
|
|
458
|
+
under its own nanny process, keeps logging, and is reattached by the next panel
|
|
459
|
+
([SERVERS.md](./docs/SERVERS.md#persistent-entries)).
|
|
460
|
+
|
|
451
461
|
</details>
|
|
452
462
|
|
|
453
463
|
<details>
|
|
@@ -529,6 +539,17 @@ types; `pnpm test` is vitest; `pnpm run media` regenerates the GIF above.
|
|
|
529
539
|
|
|
530
540
|
---
|
|
531
541
|
|
|
542
|
+
<details>
|
|
543
|
+
<summary><b>🔗 Interesting resources</b></summary>
|
|
544
|
+
|
|
545
|
+
- [dsh-home-hosted](https://github.com/NamesMT/dsh-home-hosted) — home-hosted servers management with boot autostart from [DeepSeek Harness](https://github.com/deepseek-ai/dsh)
|
|
546
|
+
|
|
547
|
+
<sub><i>+ PR to add yours</i></sub>
|
|
548
|
+
|
|
549
|
+
</details>
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
532
553
|
<div align="center">
|
|
533
554
|
|
|
534
555
|
**MIT**
|