home-hosted 0.7.17 → 0.7.18
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 +193 -385
- package/README.md +25 -6
- package/dist/cli.js +1055 -533
- package/dist/cli.js.map +1 -1
- package/docs/DDNS.md +6 -2
- package/docs/REVERSE_PROXY.md +11 -4
- package/docs/SERVERS.md +8 -3
- package/docs/UI_CREATION.md +13 -4
- package/package.json +3 -2
- package/uis/stock/dist/assets/{index-CAp6K13b.js → index-DaynUz2r.js} +7 -7
- package/uis/stock/dist/assets/{index-D8S55m4E.css → index-Dp4rjHQQ.css} +1 -1
- package/uis/stock/dist/index.html +2 -2
- package/uis/stock/dist/ui.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -2,37 +2,59 @@
|
|
|
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 each workspace's
|
|
5
|
-
`servers.config.json` and serves a UI. User docs: `README.md
|
|
6
|
-
|
|
7
|
-
`docs/UI_CREATION.md`.
|
|
5
|
+
`servers.config.json` and serves a UI. User docs: `README.md` and `docs/` (SERVERS, NOTIFICATIONS,
|
|
6
|
+
DDNS, REVERSE_PROXY, UI_CREATION).
|
|
8
7
|
|
|
9
8
|
State lives only in `$HHOSTED_HOME/.hh` (default `~/.home-hosted/.hh`). Global files sit at its top
|
|
10
9
|
level: `settings.json` (listener, auth, TLS policy, host vitals, backups, reverse proxy),
|
|
11
10
|
`workspaces.json`, `.control-secrets.json` (0600: password hash, API token hash), `.tls/`,
|
|
12
|
-
`.backups/`, `.ui/`, `.logs/` (the panel console), `.proxy/` (the
|
|
13
|
-
|
|
14
|
-
`.hh/<id>/`: `settings.json` (server defaults, log retention, notifications, DDNS),
|
|
15
|
-
`.secrets.json` (0600: Telegram
|
|
16
|
-
`HHOSTED_DDNS_SECRET` — only DDNS is sealed, because it is the one thing replayed
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`ensureLayout()
|
|
20
|
-
seed entry, or a path that names one.
|
|
11
|
+
`.backups/`, `.ui/`, `.logs/` (the panel console), `.proxy/` (the engine, its generated config and
|
|
12
|
+
nanny state), and `run.json` (the live daemon's pid/url/token, 0600). Every workspace owns
|
|
13
|
+
`.hh/<id>/`: `settings.json` (server defaults, log retention, notifications, DDNS),
|
|
14
|
+
`servers.config.json`, `.secrets.json` (0600: Telegram token, plus DDNS credentials sealed with
|
|
15
|
+
AES-256-GCM under `HHOSTED_DDNS_SECRET` — only DDNS is sealed, because it is the one thing replayed
|
|
16
|
+
to a third party), `.logs/` (per server, `<id>.log` plus `<id>.log.1` when it rotates), and
|
|
17
|
+
`.state/` (nanny state, its 0600 spawn spec until read, `ddns.json`). A pre-`.hh` instance is
|
|
18
|
+
relocated automatically by `ensureLayout()`. **The package ships no servers**: never commit a config,
|
|
19
|
+
a seed entry, or a path that names one.
|
|
20
|
+
|
|
21
|
+
## Docs
|
|
22
|
+
|
|
23
|
+
Three tiers, so a reader loads only what the task needs: **this file** (orientation and the rules —
|
|
24
|
+
read every session), **`.agentDocs/`** (depth — read on demand), and **`docs/`** (for a person using the
|
|
25
|
+
package, not for an agent). The section map for every repo in this workspace is
|
|
26
|
+
[`.agentDocs/AGENTS-SKELETON.md`](.agentDocs/AGENTS-SKELETON.md).
|
|
27
|
+
|
|
28
|
+
| file | what it covers |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| [`.agentDocs/AGENTS-SKELETON.md`](.agentDocs/AGENTS-SKELETON.md) | the canonical section names and order, and the required content |
|
|
31
|
+
| [`.agentDocs/ARCHITECTURE.md`](.agentDocs/ARCHITECTURE.md) | what each module owns and why; UI versioning |
|
|
32
|
+
| [`.agentDocs/GOTCHAS.md`](.agentDocs/GOTCHAS.md) | the traps this codebase already paid for, with causes |
|
|
33
|
+
| [`.agentDocs/COMPATIBILITY.md`](.agentDocs/COMPATIBILITY.md) | config and UI compatibility, migrations |
|
|
34
|
+
| [`docs/SERVERS.md`](docs/SERVERS.md) | entries, port conflicts, persistent entries (user-facing) |
|
|
35
|
+
| [`docs/UI_CREATION.md`](docs/UI_CREATION.md) | the contract for a new UI |
|
|
21
36
|
|
|
22
37
|
## Commands
|
|
23
38
|
|
|
39
|
+
`pnpm-workspace.yaml` is load-bearing: dropping an `allowBuilds` entry makes `pnpm install` **exit 1**
|
|
40
|
+
(`Ignored build scripts: esbuild@0.24.2` — measured, not inferred). Keep the file's own comments
|
|
41
|
+
explaining why each entry is there.
|
|
42
|
+
|
|
24
43
|
```sh
|
|
25
44
|
pnpm run up|down|restart|status # detached; `down` asks /_hh/shutdown, signals are the fallback
|
|
26
45
|
# `restart <id>` restarts one server via /_hh, panel stays up
|
|
27
46
|
pnpm run start # up --foreground (systemd, docker, a foreground shell)
|
|
28
47
|
pnpm exec tsx src/cli.ts logs # the panel's own console output (--lines, --follow, --json)
|
|
29
|
-
pnpm dev # tsx-watch panel :6000 + the stock UI's Vite :6001
|
|
48
|
+
pnpm dev # tsx-watch panel :6000 + the stock UI's Vite :6001, state in .dev-state/
|
|
30
49
|
pnpm dev --ui noc-console # the same, for another UI; --port/--ui-port override
|
|
31
50
|
pnpm run build # dist/cli.js + the stock UI (uis/stock/dist)
|
|
32
51
|
pnpm run build:uis # every UI under uis/, zipped into uis/dist/ (release assets)
|
|
33
|
-
pnpm run quickcheck # eslint + tsc + vue-tsc
|
|
52
|
+
pnpm run quickcheck # eslint + tsc + vue-tsc + the doc links — NOT the tests
|
|
34
53
|
pnpm exec vitest run # `pnpm test` is vitest in watch mode
|
|
35
|
-
pnpm run check # quickcheck + vitest run --coverage
|
|
54
|
+
pnpm run check # ← THE GATE: quickcheck + vitest run --coverage. Run this before
|
|
55
|
+
# every push — CI's `test.yml` is `pnpm test --coverage`, so an
|
|
56
|
+
# area floor failing there fails the push while `quickcheck` stays
|
|
57
|
+
# green. That divergence cost a day of red CI (2026-10-06).
|
|
36
58
|
pnpm run set-password # non-interactive through HHOSTED_PASSWORD
|
|
37
59
|
pnpm run set-token # --generate prints a new API token once
|
|
38
60
|
pnpm run migrate # bring the config up to this release's schema
|
|
@@ -43,383 +65,176 @@ pnpm run media # regenerate docs/media (mockups, both served
|
|
|
43
65
|
The published bin is `home-hosted`, with an `hh` alias: both names run the same CLI.
|
|
44
66
|
|
|
45
67
|
**Dev servers and test instances stay in the 6xxx range** — panel 6000, UI 6001, test-spawned panels
|
|
46
|
-
6100+ (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
tests red for the wrong reason.
|
|
50
|
-
|
|
51
|
-
Releases are dispatched from `.github/workflows/release.yml` with a version (and a `dry-run` switch
|
|
52
|
-
that stops before pushing). It verifies the version, lints/types/tests, builds the CLI plus the stock
|
|
53
|
-
UI and every UI zip, lets changelogen write the changelog, bump `package.json`, commit and tag
|
|
54
|
-
`v<version>`, creates the GitHub release with the UI bundles attached, and publishes to npm through
|
|
55
|
-
trusted publishing (OIDC, no token). npm only offers a trusted publisher for a package that already
|
|
56
|
-
exists, so the first release has to be published by hand.
|
|
57
|
-
|
|
58
|
-
Four workflows, and the split is deliberate:
|
|
59
|
-
|
|
60
|
-
| workflow | runs on | what it proves |
|
|
61
|
-
| --- | --- | --- |
|
|
62
|
-
| `quickcheck.yml` / `test.yml` | every push and PR | lint, types, the whole suite with coverage — Linux only |
|
|
63
|
-
| `cross-platform.yml` | the release gate, or by hand | the same suite on macOS and Windows |
|
|
64
|
-
| `release.yml` | dispatched with a version | the platform gate, then tag, publish, release assets |
|
|
65
|
-
|
|
66
|
-
The platform gate is not a per-push job on purpose: it is the expensive half, and it is what makes a
|
|
67
|
-
platform-specific break fail before shipping rather than on someone's machine. It also runs on a
|
|
68
|
-
`dry-run`, because that is exactly what a rehearsal needs to exercise. Every job carries a
|
|
69
|
-
`timeout-minutes`, so a wedged runner is reported instead of sitting out GitHub's 6-hour default.
|
|
70
|
-
|
|
71
|
-
### Which version to dispatch
|
|
72
|
-
|
|
73
|
-
Below 1.0 the **minor is the breaking channel**: a fix or a non-breaking feature is a **patch**
|
|
74
|
-
(`0.6.2` → `0.6.3`), while a minor (`0.6.3` → `0.7.0`) means someone has to read the release notes and
|
|
75
|
-
act. A `feat:` commit does not decide this — ask what the user has to do about it.
|
|
76
|
-
|
|
77
|
-
Never edit `package.json` by hand; changelogen bumps, commits and tags inside the workflow:
|
|
78
|
-
|
|
79
|
-
```sh
|
|
80
|
-
gh workflow run release.yml -f version=0.6.3 # -f dry-run=true to rehearse
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
`scripts/check-release-version.mjs` refuses a patch while a breaking commit is pending, and warns on
|
|
84
|
-
a minor without one.
|
|
85
|
-
|
|
86
|
-
## Architecture (and why)
|
|
87
|
-
|
|
88
|
-
- `src/cli.ts` — the CLI's thin root. Two things happen before [citty](https://github.com/unjs/citty)
|
|
89
|
-
is asked anything: `--home`/`--project` are peeled off and applied (`src/cli/args.ts`), and the
|
|
90
|
-
curated dispatch — `help`/`version`, `unknown command`, and the `-p 4000` shorthand for `up` — is
|
|
91
|
-
decided, because `#src/helpers/paths.ts` resolves at import time. Its static imports stay node
|
|
92
|
-
builtins, citty and the two path-free local modules (`src/cli/args.ts`, `src/helpers/runtime.ts`);
|
|
93
|
-
every command is a lazy
|
|
94
|
-
`() => import('#src/cli/<command>')` in citty's `subCommands`, so a command module *may* use static
|
|
95
|
-
`#src` imports (that is the whole point of the pre-pass). The hidden `__nanny` is dispatched by an
|
|
96
|
-
early branch, not `subCommands`, so it stays out of the curated help and the unknown-command
|
|
97
|
-
message — the panel spawns it, nobody types it. citty's `runMain` is deliberately not
|
|
98
|
-
used: it prints its own usage and `console.error`s before `process.exit(1)`, replacing `fail()`'s
|
|
99
|
-
one error shape; the root calls `runCommand` and catches. `src/cli/<command>.ts` is one command per
|
|
100
|
-
file — citty owns dispatch and argument parsing, with `--no-autostart`/`--no-install` declared as
|
|
101
|
-
boolean negations rather than literal arg names. The curated `--help` prose stays in `src/cli.ts`
|
|
102
|
-
because citty cannot generate it. `up` re-spawns itself detached as `up --foreground …`, with the
|
|
103
|
-
argv built from the *parsed* flags (`buildDaemonArgv`) plus this file's own URL as the entry, so it
|
|
104
|
-
works under tsx and from `dist/cli.js` alike. `src/cli/io.ts` is the one place the readline prompts
|
|
105
|
-
and colours live; commands take `prompt`/`style` through that seam (`src/cli/ui-switch.ts` keeps
|
|
106
|
-
`UiSwitchIo`).
|
|
107
|
-
- `src/index.ts` — `runControlPlane()`: wiring, startup guards (exposure, free port, live run.json),
|
|
108
|
-
`run.json`, signals. Wiring belongs here and nowhere else.
|
|
109
|
-
- `src/app.ts` — the Hono root, chained routes only. `/_hh` is mounted *before* the `/api/*` auth
|
|
110
|
-
guard on purpose (a local `down` uses run.json's token, not a session). `AppType` is the route type
|
|
111
|
-
that `hc<AppType>` clients and the OpenAPI document derive from.
|
|
112
|
-
- `src/api/**` — one file per URL group (`$.routes.ts` = several routes), mirroring the path.
|
|
113
|
-
- `src/shared/contracts.ts` — every ArkType schema (config, API and SSE DTOs), shared with the UIs;
|
|
114
|
-
the OpenAPI spec is generated from it, never hand-written.
|
|
115
|
-
- `src/config/` — split by scope. `settings.ts` (`GlobalSettingsStore`: `.hh/settings.json` — listener,
|
|
116
|
-
auth, TLS policy, host vitals, backups) and `store.ts` (`WorkspaceStore`: a workspace's
|
|
117
|
-
`settings.json` + `servers.config.json`, validate/merge/atomic commit, reporting
|
|
118
|
-
`configError`/`configWarnings` instead of throwing on a bad file). `workspaces.ts` is the registry
|
|
119
|
-
(`workspaces.json`), `layout.ts` the one-time relocation of a pre-workspace `$HHOSTED_HOME`.
|
|
120
|
-
`schema.ts` holds the three on-disk shapes plus the `meta` stamp, `parse.ts` the tolerant readers
|
|
121
|
-
(`parseGlobalSettings`/`parseWorkspaceSettings`/`parseServersFile`: unknown keys are reported and
|
|
122
|
-
kept, everything else blocking), `patch.ts` the merge helpers, `migrations.ts` the schema constant
|
|
123
|
-
and ordered step registry, and `secrets.ts`/`seed.ts` the two secret scopes and the seeds.
|
|
124
|
-
- `src/providers/` — stateless leaves: `process` (spawn, `terminate`, `terminatePid` for a process we
|
|
125
|
-
adopted), `port` (probe, holder lookup, `terminatePids`), `proc` (the sampler, plus
|
|
126
|
-
`processCarriesServerId` for the environment marker and `processTreePids` for tree ownership),
|
|
127
|
-
`identity` (which port holder is this entry's own successor: marker first, then resolved image +
|
|
128
|
-
argv), `nanny` (a persistent entry's spec/state files and the liveness rules for one),
|
|
129
|
-
`log-tail` (an offset reader that survives rotation and truncation), `ddns/` (one `DdnsProvider` per
|
|
130
|
-
registrar over one HTTP call each, plus `ip` and `zone` helpers), `proxy/` (the reverse-proxy
|
|
131
|
-
engines this build can drive: where the binary comes from and how to start it — the route model
|
|
132
|
-
stays the panel's), health-check, host, telegram, archive.
|
|
133
|
-
- `src/services/` — stateful orchestration: `panel` (one `WorkspaceRuntime` per workspace, workspace
|
|
134
|
-
CRUD, and the single aggregate state frame — every per-workspace service hangs off it), supervisor,
|
|
135
|
-
control-server, config-watch, state, auth + exposure, dependencies, history, log-buffer/log-files,
|
|
136
|
-
notifications, host-monitor, ddns, proxy, backups, tls, ui, plus `init` (the scaffold behind
|
|
137
|
-
`home-hosted init`: a manifest, a `.gitignore`, and the prompts stay in the CLI), `nanny` (the
|
|
138
|
-
process a persistent entry runs under) and `log-relay` (the tailer that feeds its lines to the
|
|
139
|
-
supervisor). It names no server — the scaffold must stay as neutral as the supervisor.
|
|
140
|
-
- `src/middleware/auth.ts` — the `/api/*` guard, and `requestIdentity()`, the one place a request's
|
|
141
|
-
credentials are read: the `hh_session` cookie or `Authorization: Bearer <api token>`. A token is
|
|
142
|
-
a first-class credential (same authority as a signed-in browser) and is verified from the secrets
|
|
143
|
-
file on every request, so `set-token` needs no restart.
|
|
144
|
-
- `src/helpers/` — paths (`dataRoot` vs `projectDir`), daemon (run.json + a loopback probe that
|
|
145
|
-
bypasses `fetch`, so TLS with a self-signed pair still answers), error, validator, atomic,
|
|
146
|
-
template, env-file, openapi, factory.
|
|
147
|
-
- `uis/<name>/` — each UI is a Vite app (Vue 3 + Tailwind v4) built through `uis/vite.shared.ts`;
|
|
148
|
-
`stock` is the one shipped inside the package. Aliases: `@` → that UI's `src`, `@shared` →
|
|
149
|
-
`src/shared`, `@server` → `src` (**types only** — never import runtime server code into a UI).
|
|
150
|
-
Its `public/ui.json` is the UI's identity: `name` and `version` (what Global Settings → Interface shows),
|
|
151
|
-
`repo`/`tag`/`asset` (which release carries it, for `ui-update`) and `unix` (when it was built).
|
|
152
|
-
`UiService` carries those fields into the installed `$HHOSTED_HOME/.ui/ui.json` unchanged and adds
|
|
153
|
-
`uploadedAt`/`files` of its own.
|
|
154
|
-
- **Bumping a UI ships a new asset, so its `ui.json` is part of the change — and it is bumped once,
|
|
155
|
-
when the commit is made, not on every edit.** A commit that alters what a UI *ships*
|
|
156
|
-
(`uis/<name>/src`, `public`, its build inputs — not `test/`, which is not in the asset) raises that
|
|
157
|
-
`ui.json`'s `version`: patch for a fix, minor for a feature, and **major only for a rewrite or a
|
|
158
|
-
restyle**. The number is what tells a person how big the change is — `ui-update` prints it and holds
|
|
159
|
-
it against the release it is offering. `unix` is that commit's own epoch, never a leftover from
|
|
160
|
-
whenever the file was edited: `scripts/stamp-uis.mjs` runs from the pre-commit hook, stamps it, and
|
|
161
|
-
refuses a commit that changed a shipped UI without raising its version. `tag` is what pairs an
|
|
162
|
-
official UI with its panel, and it is **stamped by `build-uis.mjs`**, not trusted from the source
|
|
163
|
-
file: a UI zip is built before its release is cut, so the committed value is always a release behind
|
|
164
|
-
the asset it ends up inside. The release workflow passes `HHOSTED_UI_TAG`; anywhere else the build
|
|
165
|
-
stamps `v<package.json version>`. Do not hand-edit `tag` to chase a release — a stale one makes
|
|
166
|
-
`ui-update` re-install the same UI on every boot.
|
|
167
|
-
- `bin/home-hosted.mjs` — the published bin: `dist/cli.js`, or `src/cli.ts` through tsx when the
|
|
168
|
-
build is missing (a linked checkout).
|
|
169
|
-
- `scripts/` — `build-uis.mjs` (build one UI, optionally zip it), `typecheck-uis.mjs`,
|
|
170
|
-
`capture-media.mjs` (the README's media), `check-release-version.mjs` and `release-notes.mjs`
|
|
171
|
-
(used by the release workflow), `dev.mjs` (`pnpm dev`).
|
|
172
|
-
- `docs/` — the topic docs (`SERVERS.md`, `NOTIFICATIONS.md`, `DDNS.md`, `REVERSE_PROXY.md`,
|
|
173
|
-
`UI_CREATION.md`),
|
|
174
|
-
`mockups/*.html` (hand-written UI examples in four directions) and `media/*`: their screenshots plus
|
|
175
|
-
the README's `tour.gif`, all regenerated by `pnpm run media`. Only the five `.md` files are
|
|
176
|
-
published; the media is a build artifact.
|
|
68
|
+
6100+ (`--port`/`--ui-port`, or `HHOSTED_DEV_PANEL_PORT`/`HHOSTED_DEV_UI_PORT`). Never assume 3999 is
|
|
69
|
+
free: an installed panel or another dev instance may hold it, and the CLI's port preflight runs
|
|
70
|
+
*before* its config guard, so a busy default port turns config tests red for the wrong reason.
|
|
177
71
|
|
|
178
72
|
## Conventions
|
|
179
73
|
|
|
180
|
-
- `#src/*`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
- ArkType at every runtime boundary: routes use `validate('json'|'query'|'param', schema)` then
|
|
189
|
-
`c.req.valid(...)`; ad-hoc payloads use `parseOrThrow`. Schemas reject undeclared keys.
|
|
190
|
-
- Every failure is a `DetailedError` (`@namesmt/utils`), mapped by `src/helpers/error.ts` into one
|
|
74
|
+
- **Server-agnostic core; UIs are clients.** `#src/*` inside `src/`; UIs use `@shared/*` (and
|
|
75
|
+
`@server` for types only).
|
|
76
|
+
- **ArkType at every runtime boundary.** Routes use `validate('json'|'query'|'param', schema)` then
|
|
77
|
+
`c.req.valid(...)`; ad-hoc payloads use `parseOrThrow`. **A request schema rejects undeclared keys**
|
|
78
|
+
(`.onUndeclaredKey('reject')`); **a response/view schema does not** — a newer panel may add a field
|
|
79
|
+
while an older client still reads it, so rejecting there would blank that client. 46 of the 94
|
|
80
|
+
schemas in `contracts.ts` are responses and omit it on purpose.
|
|
81
|
+
- **Every failure is a `DetailedError`** (`@namesmt/utils`), mapped by `src/helpers/error.ts` into one
|
|
191
82
|
envelope `{ message, code, detail }`. Never hand-roll `c.json({ error })`.
|
|
192
|
-
- Document routes with `describeRoute` + `jsonBody(schema)
|
|
193
|
-
- Patch schemas carry no defaults
|
|
194
|
-
`
|
|
195
|
-
|
|
196
|
-
`ipv4`/`ipv6` merge.
|
|
197
|
-
- Two-sided bounds read inclusively (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
|
|
83
|
+
- **Document routes with `describeRoute` + `jsonBody(schema)`**; `jsonBody` needs a real schema.
|
|
84
|
+
- **Patch schemas carry no defaults**; nested groups merge key-by-key and an explicit `null` clears a
|
|
85
|
+
key. `ddns`'s `accounts`/`domains` are **replaced** — a merge cannot express removing a hostname.
|
|
86
|
+
- **Two-sided bounds read inclusively** (`'1 <= number.integer <= 512'`). `test/shared/contracts.test.ts`
|
|
198
87
|
pins every boundary and the patch/schema parity — update it with any schema change.
|
|
199
|
-
- A new
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
-
|
|
205
|
-
guarding mounts under `// @vitest-environment happy-dom` (see `number-field.test.ts`). Reach for
|
|
206
|
-
that rather than trusting a component to be thin: `NumberField`'s setter assumed the string a text
|
|
207
|
-
input reports, but Vue casts `<input type="number">` to a *number* first, so `raw.trim()` threw and
|
|
208
|
-
every value typed into a numeric field was silently discarded — no pure-module test could see it.
|
|
209
|
-
The suite resolves `@/…` from the UI that asks, so each UI's own tests can import its own
|
|
210
|
-
components; a single pinned target made every UI but that one untestable, and resolved a
|
|
211
|
-
`noc-console` test's `@/lib/proxy` to *stock's* differing copy without a murmur.
|
|
212
|
-
- A destructive action that is one click away confirms in a **popover** (`KillPortButton.vue`),
|
|
213
|
-
never by arming the same button for a second press: an impatient double click on an arming
|
|
214
|
-
button fires it. Keep the safe choice first in the popover's tab order.
|
|
215
|
-
- **Docs ship with the change.** A user-visible change updates the docs that describe it
|
|
216
|
-
(`README.md`, `docs/*.md`) in the same commit — do not forget them; a stale doc is a bug like
|
|
217
|
-
stale code.
|
|
218
|
-
- **UIs move together.** `uis/stock` is not the only client: a change to it — or to a shared
|
|
219
|
-
contract it reads — lands in every other UI under `uis/`, and each altered UI bumps its
|
|
220
|
-
`ui.json` per the rule above.
|
|
221
|
-
|
|
222
|
-
## Compatibility
|
|
223
|
-
|
|
224
|
-
Two surfaces outlive the release that wrote them: **configs absolutely, UIs within reason.** Breaking
|
|
225
|
-
either is a last resort, and never an accidental one.
|
|
226
|
-
|
|
227
|
-
- **A config written by an older release has to load in a newer one.** That direction is the priority:
|
|
228
|
-
add fields with defaults, never repurpose or remove one, and treat every existing key as permanent.
|
|
229
|
-
- **Both directions matter, and both are now handled.** An unrecognized key is read, reported and
|
|
230
|
-
left on disk instead of failing anything: it is the normal way a config from a newer release looks
|
|
231
|
-
here. Anything that is not merely unrecognized — a wrong value, a duplicate id, an unreadable file —
|
|
232
|
-
stops the panel instead of being papered over with defaults.
|
|
233
|
-
- **The UI moves in minor steps.** Routes, response fields and SSE frames are additive: keep the old
|
|
234
|
-
one and add the new one. A new response field is optional (`'x?'`) and read defensively, because an
|
|
235
|
-
upgrade writes a new `uis/stock/dist` while the old panel process keeps serving — and a
|
|
236
|
-
user-uploaded UI may be older than the panel it talks to.
|
|
237
|
-
- **Breaking is allowed; silent is not.** When nothing compatible can be done, say so in the final
|
|
238
|
-
answer *and* in the commit message with a `BREAKING CHANGE:` footer, naming the exact migration the
|
|
239
|
-
user must run.
|
|
240
|
-
- **A config records what wrote it.** Every write stamps a top-level `meta`
|
|
241
|
-
(`{ writtenBy, schema }`): the release that wrote the file and the config shape it wrote. An
|
|
242
|
-
unstamped file reads as the current schema, so nothing that existed before needed changing.
|
|
243
|
-
- **Nobody runs a config this release cannot read.** `up` refuses to start — exit 1, the exact
|
|
244
|
-
problem printed — when the file is unparseable, has an invalid value or a duplicate id, carries a
|
|
245
|
-
newer `meta.schema`, or has a registered migration pending. A panel that is *already* running keeps
|
|
246
|
-
the config it has and only reports the error, so a bad edit never disturbs supervision.
|
|
247
|
-
- **Unknown keys are dropped from the resolved config, kept on disk, and listed in a startup
|
|
248
|
-
warning.** Dropping one is the normal way a newer config looks here, so it must never fail the
|
|
249
|
-
group it sits in (that used to reset `control` — port, bind, auth policy — to schema defaults).
|
|
250
|
-
A *write* to a group validates the same tolerant way (`parseTolerant` via the store), so a key this
|
|
251
|
-
release does not know never blocks a save — removing a key from a schema must not brick the page
|
|
252
|
-
that writes it.
|
|
253
|
-
- **Migrations ship inside the package** (`src/config/migrations.ts`), are ordered, idempotent and
|
|
254
|
-
described in one line each. `home-hosted migrate` prints the plan, keeps a `.bak` beside each file it
|
|
255
|
-
rewrites, refuses to write a config it cannot read, and needs consent: `--yes`, `HHOSTED_MIGRATE=allow`,
|
|
256
|
-
or a person at a terminal. The layout relocation is not a schema change: it runs automatically from
|
|
257
|
-
the CLI pre-pass (`ensureLayout()`), so an upgrade never starts against a half-moved directory. Starting a fetch of migration
|
|
258
|
-
code from GitHub was considered and rejected: the panel supervises processes, so remote code is an
|
|
259
|
-
RCE surface, and a migration would age against a newer store API anyway.
|
|
88
|
+
- **A new response *field* is optional (`'x?'`)** and read defensively: an upgrade writes a new UI
|
|
89
|
+
while an old panel keeps serving, so a required field blanks the app.
|
|
90
|
+
- **A single on/off setting is a `ToggleSwitch`**; `CheckField` is only for picking items out of a set.
|
|
91
|
+
- **A destructive action one click away confirms in a popover**, never by arming the same button for a
|
|
92
|
+
second press — an impatient double click fires an armed button. Safe choice first in tab order.
|
|
93
|
+
- **Conventional commits**; ESLint via `@antfu/eslint-config` owns formatting; sparse comments.
|
|
260
94
|
|
|
261
95
|
## Rules that matter
|
|
262
96
|
|
|
97
|
+
These prevent defects if broken. They stay here rather than in `.agentDocs/` for that reason — a rule
|
|
98
|
+
nobody reads is worse than a long file.
|
|
99
|
+
|
|
263
100
|
- **Server-agnostic.** No blessed ids, no `dataDir`-style globals: a server gets only its own
|
|
264
101
|
`command`/`args`/`env`/`dataEnvs`/`envFile`/`bootstrap`. A test fails if a core file learns one.
|
|
265
|
-
- **Paths.** `dataRoot` is state,
|
|
266
|
-
|
|
267
|
-
`
|
|
268
|
-
|
|
269
|
-
- **Secrets never enter the config.**
|
|
270
|
-
API token hash, each workspace's `.secrets.json` holds its Telegram token and DDNS credentials, and
|
|
271
|
-
the TLS pair sits in `.hh/.tls/`; every one is 0600 and the config holds only policy.
|
|
272
|
-
- **A port holder that is this entry's own successor is not a stranger.** A program that restarts
|
|
273
|
-
itself leaves a detached process behind; with `follow` the panel adopts it as-is (pid, liveness,
|
|
274
|
-
health, resources, stop — but not its output); with `reclaim` it stops that successor and starts a
|
|
275
|
-
fully supervised child instead; with `kill` it stops whatever holds the port without asking whose it
|
|
276
|
-
is. All three beat blocking forever on a port that is already serving, and only `kill` ever touches a
|
|
277
|
-
process the panel could not identify — which is why it logs the pids it stopped.
|
|
278
|
-
Ownership is read from `HHOSTED_SERVER_ID` in the environment (`/proc` on Linux, `ps -E` on macOS),
|
|
279
|
-
falling back to the entry's own resolved image **plus its expanded argv** where that is unavailable
|
|
280
|
-
or unmatched — the fallback is what gives `follow`/`reclaim` any reach on Windows, which exposes no
|
|
281
|
-
per-process environment. That fallback is strict on purpose, because `reclaim` kills what it
|
|
282
|
-
identifies: arguments are compared **literally** (never by basename — `/srv/a/server.js` must not
|
|
283
|
-
match `/tmp/b/server.js`), an argument can never be satisfied by a word that is not in the argv, and
|
|
284
|
-
two matching holders mean the panel refuses to guess and blocks. It still cannot see through a
|
|
285
|
-
Windows `.cmd` shim (the holder is `node.exe`, whose argv never mentions the shim) or a macOS
|
|
286
|
-
argument containing a space (`ps` joins argv without quoting); those cases block, and `kill` is the
|
|
287
|
-
answer for them. `src/providers/identity.ts` owns the matching, and the argv it is given comes from
|
|
288
|
-
the supervisor's single `resolveSpawn()`, the same one the spawn itself used — keep it to one
|
|
289
|
-
resolver, because a second one that expands or filters args differently re-opens these cases.
|
|
290
|
-
Ownership covers the entry's whole **tree**, not the pid it recorded (`processTreePids`): a nanny
|
|
291
|
-
puts the real server one generation down, and so does any wrapper entry. The panel's own tree is
|
|
292
|
-
deliberately excluded — "ours" has to mean a process a server owns.
|
|
293
|
-
- **A persistent entry is run by its own nanny, not by the panel.** `persistent: true` makes the panel
|
|
294
|
-
spawn the hidden `__nanny` (`src/services/nanny.ts`), which owns the child's pipes, writes its JSONL
|
|
295
|
-
and mirrors its exit — the only arrangement that outlives the panel *and* keeps logging. The panel
|
|
296
|
-
skips such an entry in `stopAll()`/`dispose()` (`down` reports what it left running), reattaches on
|
|
297
|
-
boot through `.state/<id>.json` before any port preflight (`nannyIsAlive` = live pid **and** a fresh
|
|
298
|
-
heartbeat, the `HHOSTED_SERVER_ID` marker or the argv), tails that file for live logs, and reads
|
|
299
|
-
`lastExit` once so a crash nobody watched is reported. The spawn spec is consumed by the read and
|
|
300
|
-
swept at boot — it carries expanded env. Three lifetimes are pinned: the nanny **exits with its child**
|
|
301
|
-
(an inherited pipe would keep it alive, and the panel would report a healthy entry whose server is a
|
|
302
|
-
detached stranger), a stop reaches the child by pid from the state file, because `SIGKILL` and
|
|
303
|
-
`killGroup: false` cannot be forwarded — which is why a self-restarting program belongs on `reclaim` —
|
|
304
|
-
and its `SIGTERM`/`SIGINT` traps are armed *before* the child exists and before that state file
|
|
305
|
-
appears, so a stop that beats the spawn can never orphan a child nobody has a record of.
|
|
306
|
-
The nanny never restarts anything: retries and health stay the supervisor's. `logs.persist: false`
|
|
307
|
-
keeps that file out of the Logs page but never stops it being written — it is the transport.
|
|
308
|
-
- **A port is only ever freed by re-listing its listeners.** `POST /api/servers/:id/free-port` never
|
|
309
|
-
trusts a pid quoted in a message, and refuses any listener in `supervisedPids()` (the panel plus
|
|
310
|
-
every entry's child) instead of killing it — a port held by a sibling is a config mistake.
|
|
102
|
+
- **Paths.** `dataRoot` is state, under `dataRoot/.hh`; `projectDir` is the base for relative entry
|
|
103
|
+
paths. `{…}` placeholders and `${ENV}` expand in config — the full list is the table in
|
|
104
|
+
[`docs/SERVERS.md`](docs/SERVERS.md), which is what to keep in step. There is no package-relative
|
|
105
|
+
state.
|
|
106
|
+
- **Secrets never enter the config.** Only policy goes in a config file; every secret is 0600.
|
|
311
107
|
- **Never expose beyond loopback without auth and a non-default password.** `checkExposure()` is the
|
|
312
|
-
single rule, enforced at startup, on every settings write, and in the UI.
|
|
108
|
+
single rule, enforced at startup, on every settings write, and in the UI. `checkProxyExposure()` is
|
|
109
|
+
the same bar for a proxy route pointing at the panel — **`tls` does not change that bar.**
|
|
313
110
|
- **The CLI drives a running panel through `/_hh`, never `/api`.** `down` and `start`/`stop` read
|
|
314
|
-
run.json's token
|
|
315
|
-
|
|
316
|
-
- **A server id is only unique inside its workspace.** Anything keyed by id alone — SSE
|
|
317
|
-
|
|
318
|
-
on the pair, or two workspaces' same-named servers mix frames.
|
|
111
|
+
run.json's token over loopback through one guard (`assertLocalCall`), so they need no session. A new
|
|
112
|
+
local command must not open an unauthenticated `/api` route.
|
|
113
|
+
- **A server id is only unique inside its workspace.** Anything keyed by id alone — SSE, log buffers,
|
|
114
|
+
series, route params — must key on `(workspaceId, serverId)`, or same-named servers mix frames.
|
|
319
115
|
- **A workspace is the ownership boundary.** Servers, secrets, logs and nanny state belong to exactly
|
|
320
|
-
one
|
|
321
|
-
|
|
116
|
+
one; only listener, auth, TLS, host vitals, backups, UI and `run.json` are panel-wide. Reach a
|
|
117
|
+
per-workspace service through `PanelService.requireWorkspace(id)`, never a module global.
|
|
322
118
|
- **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
|
|
323
119
|
`$HHOSTED_HOME/.hh/.ui` overrides the packaged UI at runtime.
|
|
324
|
-
- **A restore writes only where a config says** — archive allowlist, `origin` matching first,
|
|
325
|
-
|
|
326
|
-
- **
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
- **
|
|
333
|
-
|
|
120
|
+
- **A restore writes only where a config says** — archive allowlist, `origin` matching first, symlinks
|
|
121
|
+
never recreated.
|
|
122
|
+
- **A port is only ever freed by re-listing its listeners.** `free-port` never trusts a pid quoted in
|
|
123
|
+
a message, and refuses any listener in `supervisedPids()` instead of killing it.
|
|
124
|
+
- **Only `kill` ever touches a process the panel could not identify** — which is why it logs the pids
|
|
125
|
+
it stopped. `follow` adopts this entry's own successor, `reclaim` replaces it. Ownership is the
|
|
126
|
+
entry's whole **tree**, and the panel's own tree is excluded. Full matching rules:
|
|
127
|
+
[`.agentDocs/GOTCHAS.md`](.agentDocs/GOTCHAS.md).
|
|
128
|
+
- **A persistent entry is run by its own nanny** (`src/services/nanny.ts`), which owns the pipes and
|
|
129
|
+
mirrors the exit — the only arrangement that outlives the panel *and* keeps logging. The nanny
|
|
130
|
+
never restarts anything: retries and health stay the supervisor's. Three lifetimes are pinned (it
|
|
131
|
+
exits with its child; a stop reaches the child by pid; its signal traps arm before the child
|
|
132
|
+
exists) — see [`.agentDocs/GOTCHAS.md`](.agentDocs/GOTCHAS.md).
|
|
133
|
+
- **The reverse proxy's engine is a process the panel owns, not a server entry.** Its config is
|
|
134
|
+
applied through the engine's admin API (atomic, never hand-edited), it runs under the same nanny a
|
|
135
|
+
`persistent: true` entry uses, and its admin endpoint stays panel-side.
|
|
136
|
+
- **Cross-platform is a hard requirement.** Linux/macOS/Windows: `/proc` vs `ps` vs Win32_Process,
|
|
137
|
+
process groups vs `taskkill /T`, graceful fallbacks, no shell utilities assumed.
|
|
334
138
|
- **Writes are atomic** (`writeFileAtomic`) and validated before commit.
|
|
335
139
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
- **
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
140
|
+
## How to work here
|
|
141
|
+
|
|
142
|
+
- **Check who calls it before you change it.** Grep the callers and the tests that name it, and say
|
|
143
|
+
when the impact is unclear rather than guessing. One guard here looked like the one that runs and
|
|
144
|
+
was not: `declaredData` marks a path `included: false` so the copy never starts, while the check
|
|
145
|
+
inside `copyInto` is a second line that route does not reach.
|
|
146
|
+
- **Never overwrite or delete a large section you have not fully understood.** Read it, or say what
|
|
147
|
+
you could not determine and ask.
|
|
148
|
+
- **Report the risk, not only the change** — what could break, what you could not verify, and every
|
|
149
|
+
assumption you made. Worth checking by kind: correctness, security, operational (deploy, host,
|
|
150
|
+
scaling) and integration (dependencies, breaking changes). A confident answer that omits this is
|
|
151
|
+
worth less than a hedged one that has it.
|
|
152
|
+
- **Fix the root cause, not the instance.** When the same bug keeps reappearing under different
|
|
153
|
+
names — a copied helper, a rule stated twice, a guard bypassed by a second code path — fix the
|
|
154
|
+
class: one shared implementation, one formatter, one guard. That is the work, not a follow-up to
|
|
155
|
+
ask for. Say what you changed and what it now prevents, and keep it inside the task's scope rather
|
|
156
|
+
than refactoring the world.
|
|
157
|
+
- **Verify before claiming, and say what you checked.** A green test proves only what it asserts —
|
|
158
|
+
**break the thing it guards and watch it fail.** If it still passes, either the test is decoration
|
|
159
|
+
or a different guard is running; find out which. Where a stub cannot answer the question, drive the
|
|
160
|
+
real thing. Mark anything unverified as unverified.
|
|
161
|
+
- **If recall of this project is missing** (a compacted or fresh session, a different machine), read
|
|
162
|
+
this file, `.agentDocs/`, and `git log` before acting, and ask 1–3 targeted questions rather than
|
|
163
|
+
reconstructing intent from guesswork.
|
|
164
|
+
- **Leave the docs better than you found them.** Delete what is stale, compact what has grown, and
|
|
165
|
+
never leave a pointer to content that does not exist.
|
|
166
|
+
|
|
167
|
+
## Conciseness
|
|
168
|
+
|
|
169
|
+
**Prune verbose; keep correctness.** This covers code, comments, user docs and agent docs alike.
|
|
170
|
+
|
|
171
|
+
- Code: say it once, name it well; a comment only for non-obvious *intent*, never to restate the line.
|
|
172
|
+
- Docs: one idea per sentence, prefer a table or a line to a paragraph. Cut a sentence that would not
|
|
173
|
+
change what a reader does.
|
|
174
|
+
- Delete history that `git log` already holds. Keep the *rule* that came out of it, not the story —
|
|
175
|
+
a path list of where something used to live is archaeology, not guidance.
|
|
176
|
+
- Do not drop a caveat to save a line. Concise means no filler, not fewer facts.
|
|
177
|
+
|
|
178
|
+
## User-facing docs
|
|
179
|
+
|
|
180
|
+
`README.md` and the topical `docs/*.md` are for a person, not an agent:
|
|
181
|
+
|
|
182
|
+
- **Concise first read**, depth behind collapsible `<details>` spoilers, and **visuals for skimmers**
|
|
183
|
+
— the media in `docs/media/` is regenerated by `pnpm run media`.
|
|
184
|
+
- **Docs ship with the change.** A user-visible change updates the docs that describe it in the *same
|
|
185
|
+
commit*. A stale doc is a bug like stale code.
|
|
186
|
+
- **UIs move together.** `uis/stock` is not the only client: a change to it — or to a shared contract
|
|
187
|
+
it reads — lands in every other UI under `uis/`, and each altered UI bumps its `ui.json` (see
|
|
188
|
+
[`.agentDocs/ARCHITECTURE.md`](.agentDocs/ARCHITECTURE.md)).
|
|
189
|
+
|
|
190
|
+
## Releasing
|
|
191
|
+
|
|
192
|
+
Dispatched from `.github/workflows/release.yml` with a version (`-f dry-run=true` to rehearse). It
|
|
193
|
+
verifies the version, lints/types/tests, builds the CLI and every UI zip, lets changelogen write the
|
|
194
|
+
changelog and bump `package.json`, commits and tags `v<version>`, creates the GitHub release with the
|
|
195
|
+
UI bundles attached, and publishes to npm through trusted publishing (OIDC, no token). npm only
|
|
196
|
+
offers a trusted publisher for a package that **already exists**, so a first-ever release has to be
|
|
197
|
+
published by hand.
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
gh workflow run release.yml -f version=0.6.3
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Four workflows, and the split is deliberate:
|
|
204
|
+
|
|
205
|
+
| workflow | runs on | what it proves |
|
|
206
|
+
| --- | --- | --- |
|
|
207
|
+
| `quickcheck.yml` / `test.yml` | every push and PR | lint, types, the whole suite with coverage — Linux only |
|
|
208
|
+
| `cross-platform.yml` | the release gate, or by hand | the same suite on macOS and Windows |
|
|
209
|
+
| `release.yml` | dispatched with a version | the platform gate, then tag, publish, release assets |
|
|
210
|
+
|
|
211
|
+
The platform gate is not a per-push job on purpose: it is the expensive half, and it is what makes a
|
|
212
|
+
platform-specific break fail before shipping rather than on someone's machine. Every job carries a
|
|
213
|
+
`timeout-minutes`, so a wedged runner is reported instead of sitting out GitHub's 6-hour default.
|
|
214
|
+
|
|
215
|
+
**Read the skip count, not just the tick.** Linux has no skips; macOS and Windows report a handful
|
|
216
|
+
(Windows more), all from platform guards. What to watch is the *shape*: a new skip appearing, or a
|
|
217
|
+
clean pass where a guard should have fired. **A test that returns early instead of skipping reports a
|
|
218
|
+
pass**, hiding a platform that never ran the code — use `it.runIf`/`it.skipIf` with the condition
|
|
219
|
+
computed at **module load**.
|
|
220
|
+
|
|
221
|
+
**`npm publish` exiting 0 is the whole answer.** The registry can take up to ten minutes to show a
|
|
222
|
+
package it has accepted — npm's queue, not this run's. Do not poll it, and do not hold work waiting.
|
|
223
|
+
|
|
224
|
+
### Which version to dispatch
|
|
225
|
+
|
|
226
|
+
Below 1.0 the **minor is the breaking channel**: a fix or non-breaking feature is a **patch**, while a
|
|
227
|
+
minor means someone has to read the release notes and act. A `feat:` commit does not decide this —
|
|
228
|
+
ask what the user has to do about it. Never edit `package.json` by hand; changelogen bumps inside the
|
|
229
|
+
workflow. `scripts/check-release-version.mjs` refuses a patch while a breaking commit is pending.
|
|
230
|
+
|
|
231
|
+
### What the package ships
|
|
232
|
+
|
|
233
|
+
`pnpm pack` runs `prepack` (a full build) and ships the `files` list in `package.json`: `bin/`,
|
|
234
|
+
`dist/`, `uis/stock/dist`, `README.md`, the five user-facing `docs/*.md`, `AGENTS.md` and `LICENSE`.
|
|
235
|
+
`.agentDocs/` is deliberately not published — it is for agents working in this repo, not for people
|
|
236
|
+
installing the package. The bin falls back to tsx so `pnpm link` works before a build;
|
|
237
|
+
`vue`/`vue-router` are devDependencies because the UIs are prebuilt.
|
|
423
238
|
|
|
424
239
|
## Where to extend
|
|
425
240
|
|
|
@@ -430,14 +245,7 @@ either is a last resort, and never an accidental one.
|
|
|
430
245
|
- **Server field**: `serverSchema` + its patch in contracts, merge keys in `config/patch.ts` when
|
|
431
246
|
nested, the form in `uis/stock/src/components/settings/`, and the contract tests.
|
|
432
247
|
- **UI**: a new `uis/<name>/` with a `vite.config.ts` from the shared factory;
|
|
433
|
-
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`.
|
|
434
|
-
UI (`stock`, `noc-console`) is carried to the others in the same commit.
|
|
248
|
+
`node scripts/build-uis.mjs <name> --zip`. The contract is `docs/UI_CREATION.md`.
|
|
435
249
|
- **Capability**: a stateless `src/providers/*` returning plain data; a DDNS provider is one file plus
|
|
436
|
-
a registry line (`src/providers/ddns/index.ts`)
|
|
437
|
-
follow from its metadata
|
|
438
|
-
|
|
439
|
-
## Publishing
|
|
440
|
-
|
|
441
|
-
`pnpm pack` runs `prepack` (a full build) and ships `bin/`, `dist/`, `uis/stock/dist`, `README.md`, `docs/DDNS.md`,
|
|
442
|
-
`docs/SERVERS.md`, `docs/NOTIFICATIONS.md`, `docs/REVERSE_PROXY.md`, `docs/UI_CREATION.md`, `AGENTS.md` and `LICENSE`. The bin falls back to tsx so `pnpm link` works before a
|
|
443
|
-
build; `vue`/`vue-router` are devDependencies because the UIs are prebuilt.
|
|
250
|
+
a registry line (`src/providers/ddns/index.ts`) — the settings form, credentials and validation
|
|
251
|
+
follow from its metadata. See `docs/DDNS.md`.
|