dsh-home-hosted 0.8.1 โ 0.9.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/README.md +1 -1
- package/docs/BOOT.md +47 -4
- package/docs/DESIGN.md +3 -2
- package/docs/PANEL.md +71 -8
- package/lib/client.js +6 -6
- package/lib/index.js +684 -309
- package/lib/index.js.map +4 -4
- package/lib/types/boot/index.d.ts +2 -2
- package/lib/types/boot/types.d.ts +33 -1
- package/lib/types/boot/windows.d.ts +32 -0
- package/lib/types/home-hosted/config-file.d.ts +11 -5
- package/lib/types/home-hosted/panel-schema.d.ts +12 -0
- package/lib/types/home-hosted/resolve.d.ts +8 -4
- package/lib/types/service.d.ts +34 -7
- package/lib/types/shared/contracts.d.ts +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Ask for a Gitea, a Jellyfin, an FTP server; `dsh-home-hosted` sets up the entrie
|
|
|
16
16
|
[](https://github.com/NamesMT/dsh-home-hosted/actions/workflows/ci.yml)
|
|
17
17
|
[](./LICENSE)
|
|
18
18
|
[](https://nodejs.org)
|
|
19
|
-
[](https://github.com/deepseek-ai/dsh)
|
|
20
20
|
|
|
21
21
|
[๐ Quick start](#-quick-start) ยท [๐๏ธ What the plugin does](#-what-the-plugin-does) ยท [๐ค Agent tools](#-agent-tools) ยท [๐งฉ Depth](#-depth)
|
|
22
22
|
|
package/docs/BOOT.md
CHANGED
|
@@ -92,6 +92,33 @@ stays `available` and its install reports "loads at the next login" instead of r
|
|
|
92
92
|
`bootstrap`/`enable`/`print` steps need the domain, and they are skipped when it is unreachable. The
|
|
93
93
|
daemon scope still needs root for `/Library/LaunchDaemons`.
|
|
94
94
|
|
|
95
|
+
## Windows: logon, or boot as SYSTEM (opt-in)
|
|
96
|
+
|
|
97
|
+
Windows gets three mechanisms, because "starts at login" and "starts before login" are genuinely
|
|
98
|
+
different trades on that platform:
|
|
99
|
+
|
|
100
|
+
| mechanism | starts | who it runs as | needs |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| `windows-run` | at login | you, in your session | nothing |
|
|
103
|
+
| `windows-task` | at login | you, in your session | admin |
|
|
104
|
+
| `windows-boot` | at boot, before any login | `SYSTEM` | admin |
|
|
105
|
+
|
|
106
|
+
`windows-boot` is **never chosen by Automatic**, even though it is the only one that comes up without a
|
|
107
|
+
login. It runs in **session 0**, which has no interactive desktop and no loaded user profile โ so a
|
|
108
|
+
supervised server that wants a display, a GUI, or the user's own environment breaks there. The panel
|
|
109
|
+
itself is fine (server rows are launched with `--no-open`, so nothing tries to open a browser), which is
|
|
110
|
+
exactly why this is a per-machine decision rather than a default. Measured on Windows 11: a
|
|
111
|
+
`BootTrigger` task with the SYSTEM SID really does run in session 0, and the Task Scheduler accepts the
|
|
112
|
+
generated XML.
|
|
113
|
+
|
|
114
|
+
The two task mechanisms register under **different names** โ `home-hosted` and `home-hosted-boot` โ so
|
|
115
|
+
installing one does not replace the other and you can switch back. Only the boot task carries a boot
|
|
116
|
+
trigger, and only it uses the SYSTEM SID; neither stores a password, because a task that needs a named
|
|
117
|
+
account's credentials would be a credential store, not a boot entry.
|
|
118
|
+
|
|
119
|
+
Neither mechanism offers a stop: `/End` was measured to deliver no shutdown request and to leave
|
|
120
|
+
orphans behind, so the CLI's `down` remains the only stop that runs the supervised stop policy.
|
|
121
|
+
|
|
95
122
|
## Escaping is per setting, not per file
|
|
96
123
|
|
|
97
124
|
A unit file is read by systemd, not by a shell, and its settings do not share one
|
|
@@ -137,6 +164,21 @@ keys (`Path=`, `Name=`, `Comment=`), and `Exec=` applies it on top of its shell
|
|
|
137
164
|
Every ownership check reads the escaped marker too, or a marker carrying a backslash would
|
|
138
165
|
be written escaped and searched for raw โ and the plugin would call its own entry foreign.
|
|
139
166
|
|
|
167
|
+
## Once an entry is installed, it owns the panel's start and stop
|
|
168
|
+
|
|
169
|
+
The page's Start and Stop buttons drive that entry, not the CLI. On systemd that is
|
|
170
|
+
`systemctl restart|stop` โ a restart rather than a start, because the button appears when `/healthz`
|
|
171
|
+
does not answer and a wedged panel is still active to systemd, where a plain `start` is a no-op; a `Restart=always` unit restarts a process that dies *outside* systemd, so
|
|
172
|
+
the CLI's `down` brought the panel back seconds later and the page reported a stop the machine had
|
|
173
|
+
already undone. systemd's own manual is explicit: "When the death of the process is a result of
|
|
174
|
+
systemd operation (e.g. service stop or restart), the service will not be restarted." On launchd it
|
|
175
|
+
is `kickstart -k`/`kill` for the same reason (`KeepAlive`). A Windows scheduled task offers the start
|
|
176
|
+
only โ `schtasks /End` kills the process tree rather than shutting the panel down.
|
|
177
|
+
|
|
178
|
+
The wait after each action is for the same reason: a `Type=exec` unit reports started once `execve`
|
|
179
|
+
returned, before the panel bound its port, so the call polls `/healthz` and returns as soon as it
|
|
180
|
+
answers (~1.2 s measured here). The 20 s ceiling only bounds a panel that never answers.
|
|
181
|
+
|
|
140
182
|
## Enabling autostart hands the panel over
|
|
141
183
|
|
|
142
184
|
An install alone proves nothing. The panel the plugin started keeps running, so nothing shows whether
|
|
@@ -161,10 +203,11 @@ whole design:
|
|
|
161
203
|
4. start the entry;
|
|
162
204
|
5. only then remove the other mechanisms' entries.
|
|
163
205
|
|
|
164
|
-
|
|
165
|
-
starting a *second* panel, which home-hosted refuses while `run.json` names a live pid, so a
|
|
206
|
+
In a handover the stop is the CLI's `down`, never the new mechanism's own `restart`: the entry would
|
|
207
|
+
then be starting a *second* panel, which home-hosted refuses while `run.json` names a live pid, so a
|
|
166
208
|
restart-in-place over an orphan only produces a failed unit. Waiting on the pid matters for the same
|
|
167
|
-
reason.
|
|
209
|
+
reason. This is the one place the CLI is the right stop for a supervised panel โ it is the path that
|
|
210
|
+
retires the old entry, so nothing is left to restart it.
|
|
168
211
|
|
|
169
212
|
Retirement is last, and deliberately not part of `install()`. Switching mechanism has to uninstall the
|
|
170
213
|
old one, but the old entry is often what is currently running the panel: removing it during the install
|
|
@@ -202,7 +245,7 @@ Each mechanism sends it somewhere different, and the status line now names where
|
|
|
202
245
|
| `systemd-user` / `systemd-system` | the journal โ the unit sets no `StandardOutput=`, so the detail names `journalctl --user -u <unit>` or `journalctl -u <unit>` |
|
|
203
246
|
| `launchd-agent` / `launchd-daemon` | `<logDir>/<label>.out.log` and `<label>.err.log`, the two paths the generated plist carries |
|
|
204
247
|
| `xdg-autostart` | nowhere persistent: a `.desktop` entry with `Terminal=false` has its output discarded by the session |
|
|
205
|
-
| `windows-run` / `windows-task` | nowhere persistent either |
|
|
248
|
+
| `windows-run` / `windows-task` / `windows-boot` | nowhere persistent either |
|
|
206
249
|
|
|
207
250
|
So the hint appears only where a file or a query actually exists โ and only once the entry is
|
|
208
251
|
installed, because naming a log for an entry that was never written sends someone to a
|
package/docs/DESIGN.md
CHANGED
|
@@ -38,11 +38,12 @@ its page.
|
|
|
38
38
|
`onPortConflict: kill` (0.6.0), `persistent` (0.6.3), `restart <id>` (0.7.13) and the nullable-`label`
|
|
39
39
|
patch (0.7.21) all predate the release this plugin pins. The plugin used to consult the answering
|
|
40
40
|
panel's version and refuse or drop those keys (`KILL_UNSUPPORTED`), and later to retry a `restart` as a
|
|
41
|
-
stop-then-start pair. Both are gone: it pins `home-hosted@^0.
|
|
41
|
+
stop-then-start pair. Both are gone: it pins `home-hosted@^0.9.0`, which refuses an older panel
|
|
42
42
|
outright, so neither check could ever fire again.
|
|
43
43
|
|
|
44
44
|
An old state root is upgraded by running the latest 0.7.x **once, before installing 0.8** โ its first
|
|
45
|
-
run moves the state into `.hh/`.
|
|
45
|
+
run moves the state into `.hh/`. A root a 0.8-era panel left on config schema 1 needs
|
|
46
|
+
`home-hosted migrate` before this plugin will write it; see [PANEL](PANEL.md#upgrading-an-existing-state-root).
|
|
46
47
|
|
|
47
48
|
## Tests
|
|
48
49
|
|
package/docs/PANEL.md
CHANGED
|
@@ -56,10 +56,40 @@ plus its `.1`, and the 5 MB rotation โ is unchanged too, and this plugin's rea
|
|
|
56
56
|
**0.8.0 stopped relocating an old state root, and this plugin follows it.** A 0.6.x root used to be
|
|
57
57
|
moved into `.hh/` on 0.7's first run; 0.8 refuses it instead, before any command resolves a path into
|
|
58
58
|
it, so a config at the old paths is never read as an empty panel. **Upgrading from 0.7 or earlier? Run
|
|
59
|
-
the latest 0.7.x once first โ that release moves the state โ then install 0.8.**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
the latest 0.7.x once first โ that release moves the state โ then install 0.8.** That removed the last
|
|
60
|
+
version-conditional code here: a stop-then-start fallback for a panel without `restart <id>`, and
|
|
61
|
+
`panel.migrate`, whose CLI no longer performs the move.
|
|
62
|
+
|
|
63
|
+
**0.9.0 raised the config schema to 2, and it changed what an unstamped file means.** Schema 2 folds
|
|
64
|
+
the Telegram singleton and the webhook list into one `notifications.destinations` list. The part that
|
|
65
|
+
bites here is the stamp: a file with no `meta.schema` is read as the **oldest** shape, so 0.9's `up`
|
|
66
|
+
refuses to start it โ `config schema 1 needs 1 migration`. This plugin writes workspace configs
|
|
67
|
+
directly whenever the panel does not answer, and it used to stamp them with `meta.writtenBy` alone, so
|
|
68
|
+
a config it created before any panel had run was unstartable on 0.9. It now stamps `meta.schema` on
|
|
69
|
+
every file it **creates**, reading the number from the pinned release rather than hard-coding it โ a
|
|
70
|
+
fixed `2` would break again at the next bump, from the other side.
|
|
71
|
+
|
|
72
|
+
A file that already exists **without** a stamp is refused instead of stamped: its content may still
|
|
73
|
+
need lifting, and stamping it would claim a migration that never ran and silently strand what the step
|
|
74
|
+
rewrites. Measured: a schema-2 stamp over an old `notifications.telegram` block makes 0.9 report the
|
|
75
|
+
key as unrecognized and ignore it, so the person's notifications end up silently off. Lifting a config
|
|
76
|
+
is `home-hosted migrate`'s job, and the refusal names it.
|
|
77
|
+
|
|
78
|
+
The floor is `0.9.0` for the same reason the pin is: a 0.8.x panel reads a schema-2 stamp as a *newer*
|
|
79
|
+
release's file and refuses that in turn, so no stamp satisfies both and there is no version-conditional
|
|
80
|
+
path. A 0.8.x panel is not a lesser-supported copy โ it is one this plugin cannot drive.
|
|
81
|
+
|
|
82
|
+
### Upgrading an existing state root
|
|
83
|
+
|
|
84
|
+
A root a 0.8-era panel wrote is on config schema 1. Run the panel's own migration once, before letting
|
|
85
|
+
0.9 โ or this plugin โ write it:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
home-hosted migrate --dry-run # print the steps, write nothing
|
|
89
|
+
home-hosted migrate --yes # keep a .bak beside each rewritten file
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Both sides are then on schema 2, and every write stays readable by the release that pinned it.
|
|
63
93
|
|
|
64
94
|
**0.7.21 widens that nullable set by one field, and it closed a bug here.** Clearing a server's label
|
|
65
95
|
sent an empty string, which the panel accepts and saves โ so the entry kept a blank label instead of
|
|
@@ -104,10 +134,43 @@ exact stock body, guards every assignment, and replaces the response's `end` for
|
|
|
104
134
|
authentication plugin's own page (or an exotic response) is passed through untouched rather than fought
|
|
105
135
|
over. `authNotice` turns it off; `{url}` is read per 401.
|
|
106
136
|
|
|
107
|
-
## Starting and
|
|
108
|
-
|
|
109
|
-
`panel.start`
|
|
110
|
-
|
|
137
|
+
## Starting and stopping the panel
|
|
138
|
+
|
|
139
|
+
`panel.start` and `panel.stop` ask the *installed* autostart entry first, and fall back to the CLI's
|
|
140
|
+
`up`/`down` only when nothing supervises the panel. That fallback is the out-of-the-box path: with no
|
|
141
|
+
entry, the CLI is the only thing that can start one.
|
|
142
|
+
|
|
143
|
+
It cannot be the other way round. An entry that carries a restart policy โ `Restart=always` for
|
|
144
|
+
systemd, `KeepAlive` for launchd โ reads the CLI's `down` as an unexpected death and starts the panel
|
|
145
|
+
again seconds later, so "Stop the panel" came back on its own: a stop the page reported and the machine
|
|
146
|
+
did not honour. The mirror is just as wrong: a CLI `up` starts a panel *outside* the entry, which then
|
|
147
|
+
cannot be stopped or observed by it either. Whoever supervises the panel is who may change its state.
|
|
148
|
+
|
|
149
|
+
So an installed `systemd-user`/`systemd-system` entry answers with `systemctl start|restart` and
|
|
150
|
+
`stop`, and a `launchd-agent`/`launchd-daemon` with `kickstart`/`kickstart -k` and `kill`. The verb
|
|
151
|
+
follows the entry's own state, read before acting: `start` when the unit is not running, `restart`
|
|
152
|
+
when it is โ the Start button appears when `/healthz` is silent, and a wedged panel is still active,
|
|
153
|
+
where a plain start would be a no-op. Probing matters, because that trigger is a 2 s health probe: a
|
|
154
|
+
merely slow panel must not be restarted, and restarting it would stop the servers it supervises.
|
|
155
|
+
|
|
156
|
+
Mechanisms that start the panel from outside their own control โ a `.desktop` file, a `Run` value, a
|
|
157
|
+
container restart policy โ have no stop to offer, so those fall back to the CLI.
|
|
158
|
+
|
|
159
|
+
A Windows scheduled task is a partial case, and the one place Start may not recover a wedged panel: it
|
|
160
|
+
offers `schtasks /Run` and no stop. Measured on Windows 11: `/End` asks the task to shut down in no
|
|
161
|
+
form a program can observe (a PowerShell `trap` never ran) and, while reporting success, leaves child
|
|
162
|
+
processes alive โ so the CLI's `down` is the only stop that runs the panel's own stop policy. The task is registered `MultipleInstancesPolicy=IgnoreNew`, so `/Run` on a running
|
|
163
|
+
instance does nothing, and `schtasks` has no graceful restart โ pressing Start on a wedged Windows
|
|
164
|
+
panel therefore reports that the panel did not answer, rather than recovering it. Stopping still goes
|
|
165
|
+
through the CLI's `down`, which is graceful.
|
|
166
|
+
|
|
167
|
+
The entry's command returns as soon as the service manager accepted it, which is not the same as the
|
|
168
|
+
panel answering: a `Type=exec` unit counts as started once the binary was executed, before it binds
|
|
169
|
+
its port. So the call then polls `/healthz`, and stops as soon as the panel answers โ about a second
|
|
170
|
+
on the development machine this was measured on. The 20 s is only the ceiling for a panel that never
|
|
171
|
+
answers, matching the CLI's own startup budget. A stop follows the same rule with a 5 s ceiling: if
|
|
172
|
+
the entry's stop ran but the panel still answers, it was not the panel that entry supervises, and the
|
|
173
|
+
CLI's `down` finishes the job.
|
|
111
174
|
|
|
112
175
|
`panel.takeover` replaces an answering panel with the preferred copy. Stopping the old panel also stops
|
|
113
176
|
the non-persistent servers it supervises โ this dsh only if it is one, which an entry this plugin
|