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 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/`, and `run.json` — the live daemon's pid/url/token, 0600. The package ships **no
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 against `package.json`, lints/types/tests,
35
- builds the CLI plus the stock UI and every UI zip, lets changelogen write the changelog and tag
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 those two path-free local modules; every command is a lazy
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). citty's `runMain` is deliberately not
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), `identity` (which port holder is this entry's
73
- own successor: marker first, then resolved image + argv), health-check, host, telegram, archive.
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). It names no server — the scaffold must stay as neutral as the supervisor.
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
- 🙂 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.
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
- health ✓ health ✓ restarts ↻
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-9router-dsh](https://github.com/NamesMT/hhosted-9router-dsh)**.
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/9router/restart
194
- curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=9router' # SSE
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** (`9router:DATA_DIR`), not by an absolute path from the source machine. A restore never
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**