dsh-home-hosted 0.9.0 → 0.9.2
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/docs/DESIGN.md +2 -2
- package/docs/PANEL.md +27 -5
- package/lib/index.js +86 -68
- package/lib/index.js.map +4 -4
- package/lib/types/home-hosted/config-file.d.ts +40 -0
- package/lib/types/home-hosted/resolve.d.ts +2 -2
- package/package.json +2 -2
package/docs/DESIGN.md
CHANGED
|
@@ -38,8 +38,8 @@ 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.9.0
|
|
42
|
-
outright, so neither check could ever fire again.
|
|
41
|
+
stop-then-start pair. Both are gone: it pins `home-hosted@^0.9.x` — 0.9.0 at the time of writing —
|
|
42
|
+
which refuses an older panel 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
45
|
run moves the state into `.hh/`. A root a 0.8-era panel left on config schema 1 needs
|
package/docs/PANEL.md
CHANGED
|
@@ -69,11 +69,17 @@ a config it created before any panel had run was unstartable on 0.9. It now stam
|
|
|
69
69
|
every file it **creates**, reading the number from the pinned release rather than hard-coding it — a
|
|
70
70
|
fixed `2` would break again at the next bump, from the other side.
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
An existing file that does not already declare **the** schema the pinned panel reads is refused, in
|
|
73
|
+
both directions: below it needs `home-hosted migrate`, above it was written by a newer home-hosted
|
|
74
|
+
than this plugin pins. Stamping over either would claim a migration that never ran — measured, a
|
|
75
|
+
schema-2 stamp over an old `notifications.telegram` block makes 0.9 report the key as unrecognized and
|
|
76
|
+
ignore it, so the person's notifications end up silently off — and writing on regardless would leave
|
|
77
|
+
this plugin editing a panel that cannot boot while every call reports success. Lifting or downgrading
|
|
78
|
+
a config is `home-hosted migrate`'s job, and the refusal names the remedy for the direction it found.
|
|
79
|
+
|
|
80
|
+
The page says so **before** a write is attempted: a config the panel cannot boot is reported as the
|
|
81
|
+
panel's error, so the root is not shown as healthy until the first call fails. The entries are still
|
|
82
|
+
listed — the file is readable, it is only un-bootable — so nothing looks lost while you migrate it.
|
|
77
83
|
|
|
78
84
|
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
85
|
release's file and refuses that in turn, so no stamp satisfies both and there is no version-conditional
|
|
@@ -102,6 +108,22 @@ recreating its own log, an unflushed log batch, a Windows test skip — touch no
|
|
|
102
108
|
no `.proxy/proxy.json`, `ProxyService`, `terminatePid`, or `DELETE /api/logs`, and `serverSchema`,
|
|
103
109
|
`CONFIG_SCHEMA`, `/_hh` and the CLI are unchanged.
|
|
104
110
|
|
|
111
|
+
**0.9.4 also needs no adaptation, and that was checked by use.** It adds no command and no required
|
|
112
|
+
field: `PUT /api/servers/:id`, `POST /api/servers/orphans/:id/stop`, presets and restore reporting are
|
|
113
|
+
new routes and UI, none of which this plugin calls; `stoppedByPanel`, `orphans`, `healthRestarts` and
|
|
114
|
+
the action-result `code` are new *optional* fields. `serverSchema`, `editableFields`, the merge keys,
|
|
115
|
+
`CONFIG_SCHEMA` (still 2), the four `/_hh` routes and every CLI subcommand and flag are unchanged, so
|
|
116
|
+
the pin moved and the floor did not. Verified by diffing the published sourcemaps of 0.9.2 and 0.9.4
|
|
117
|
+
(34 changed files) and by driving a real 0.9.4 panel: this plugin's client lists, creates, patches,
|
|
118
|
+
starts, stops and deletes an entry, and reads workspaces and settings, exactly as on 0.9.2.
|
|
119
|
+
|
|
120
|
+
Two 0.9.4 behaviour changes reach the page without breaking it. `/healthz` now answers 503 for **any**
|
|
121
|
+
crashed server rather than only an `autostart` one — measured, and harmless here because `probePanel`
|
|
122
|
+
asks whether the panel answered at all, not whether it said 200. And the supervisor now bounds health
|
|
123
|
+
force-restarts and then **stops** the entry, reporting it as `stopped` with `stoppedByPanel: true`; the
|
|
124
|
+
page renders that as a plain `stopped`, so it reads like a deliberate stop. That is a fidelity gap in
|
|
125
|
+
the status chip, not a break: the reason is still on the panel's own card and in `lastError`.
|
|
126
|
+
|
|
105
127
|
That copy resolves to a pnpm path carrying a version and a peer hash, which moves on the next install
|
|
106
128
|
and disappears when the profile is rebuilt. A boot entry that baked it in would fail exactly when it
|
|
107
129
|
matters. So the entry runs a generated launcher in the plugin state dir (`bin/home-hosted.mjs`,
|
package/lib/index.js
CHANGED
|
@@ -109,6 +109,63 @@ var DEFAULT_SETTINGS = {
|
|
|
109
109
|
cli: { prefer: "pinned" }
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// src/rpc.ts
|
|
113
|
+
function answer(endpoint, result) {
|
|
114
|
+
return Response.json({ v: RPC_VERSION, endpoint, result });
|
|
115
|
+
}
|
|
116
|
+
function registerRpc(ctx, service) {
|
|
117
|
+
ctx.inject(["connection"], (scoped) => {
|
|
118
|
+
const connection = scoped.connection;
|
|
119
|
+
if (connection?.fetch?.register === void 0)
|
|
120
|
+
return;
|
|
121
|
+
scoped.effect(() => {
|
|
122
|
+
let dispose;
|
|
123
|
+
try {
|
|
124
|
+
dispose = connection.fetch.register({
|
|
125
|
+
// The exact-route registry is keyed by the full request path, so the
|
|
126
|
+
// `/api` prefix belongs here; the page composes the same URL from
|
|
127
|
+
// API_BASE + RPC_PATH.
|
|
128
|
+
path: `/api${RPC_PATH}`,
|
|
129
|
+
methods: ["POST"],
|
|
130
|
+
requestBody: "buffered",
|
|
131
|
+
fetch: async (request) => {
|
|
132
|
+
let body;
|
|
133
|
+
try {
|
|
134
|
+
body = await request.json();
|
|
135
|
+
} catch {
|
|
136
|
+
return Response.json({ error: "the request body is not JSON" }, { status: 400 });
|
|
137
|
+
}
|
|
138
|
+
if (body === null || typeof body !== "object" || body.v !== RPC_VERSION) {
|
|
139
|
+
return Response.json(
|
|
140
|
+
{ error: `this page and the host disagree on the protocol version (expected ${String(RPC_VERSION)})` },
|
|
141
|
+
{ status: 409 }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const value = await service.call(body.endpoint, body.payload);
|
|
146
|
+
return answer(body.endpoint, { ok: true, value });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const code = error?.code;
|
|
149
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
150
|
+
const detail = error?.detail;
|
|
151
|
+
return answer(body.endpoint, { ok: false, error: { code: typeof code === "string" ? code : "INTERNAL", message: message2, ...detail === void 0 ? {} : { detail } } });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const logger = scoped.logger;
|
|
157
|
+
logger?.warn?.(`[dsh-home-hosted] could not register ${RPC_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
return () => {
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return () => {
|
|
162
|
+
void Promise.resolve(dispose()).catch(() => {
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
}, "dsh-home-hosted: rpc route");
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
112
169
|
// src/service.ts
|
|
113
170
|
import { Service } from "@deepseek-ai/cordis";
|
|
114
171
|
import fs18 from "node:fs";
|
|
@@ -2960,7 +3017,7 @@ function buildHomeHostedBootSpec(facts, launch, options) {
|
|
|
2960
3017
|
}
|
|
2961
3018
|
|
|
2962
3019
|
// src/home-hosted/resolve.ts
|
|
2963
|
-
var EXPECTED_RANGE = "^0.9.
|
|
3020
|
+
var EXPECTED_RANGE = "^0.9.4";
|
|
2964
3021
|
var MIN_SUPPORTED_VERSION = "0.9.0";
|
|
2965
3022
|
function pinnedManifestPath() {
|
|
2966
3023
|
try {
|
|
@@ -3176,9 +3233,12 @@ function readConfigSchemaFrom(manifestPath) {
|
|
|
3176
3233
|
// src/home-hosted/config-file.ts
|
|
3177
3234
|
var ConfigLayoutError = class extends Error {
|
|
3178
3235
|
name = "ConfigLayoutError";
|
|
3236
|
+
/** The wire code a caller reports; the root's layout is a readable config problem, not a crash. */
|
|
3237
|
+
code = "CONFIG_UNREADABLE";
|
|
3179
3238
|
};
|
|
3180
3239
|
var ConfigPatchError = class extends Error {
|
|
3181
3240
|
name = "ConfigPatchError";
|
|
3241
|
+
code = "INVALID_PATCH";
|
|
3182
3242
|
};
|
|
3183
3243
|
function readConfig(home, workspace = DEFAULT_WORKSPACE) {
|
|
3184
3244
|
const refusal = migrationRefusal(home);
|
|
@@ -3208,17 +3268,30 @@ function writeConfig(home, raw, writtenBy = "dsh-home-hosted", workspace = DEFAU
|
|
|
3208
3268
|
writeFileAtomic(file, `${JSON.stringify(next, null, 2)}
|
|
3209
3269
|
`);
|
|
3210
3270
|
}
|
|
3271
|
+
function schemaRefusal(meta, where, creating) {
|
|
3272
|
+
const pinned = panelConfigSchema();
|
|
3273
|
+
if (pinned === null)
|
|
3274
|
+
return null;
|
|
3275
|
+
const declared = isRecord(meta) && typeof meta.schema === "number" ? meta.schema : null;
|
|
3276
|
+
if (declared === null) {
|
|
3277
|
+
return creating ? null : `${where} carries no meta.schema, so its config shape is unknown; run \`home-hosted migrate\` before this plugin writes it`;
|
|
3278
|
+
}
|
|
3279
|
+
if (declared === pinned)
|
|
3280
|
+
return null;
|
|
3281
|
+
return declared < pinned ? `${where} is on config schema ${declared}, which home-hosted ${pinned} refuses to boot; run \`home-hosted migrate\` before this plugin writes it` : `${where} is on config schema ${declared}, written by a newer home-hosted than this plugin's ${pinned}; update home-hosted, or pin this plugin to the release that wrote it`;
|
|
3282
|
+
}
|
|
3211
3283
|
function stampedMeta(existing, writtenBy, where, file) {
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3284
|
+
const onDisk = fs12.existsSync(file) ? readJson(file) : null;
|
|
3285
|
+
const diskMeta = isRecord(onDisk) && isRecord(onDisk.meta) ? onDisk.meta : {};
|
|
3286
|
+
const meta = { ...diskMeta, ...isRecord(existing) ? existing : {} };
|
|
3287
|
+
if (onDisk === null) {
|
|
3288
|
+
const pinned = panelConfigSchema();
|
|
3289
|
+
if (pinned !== null)
|
|
3290
|
+
meta.schema = pinned;
|
|
3291
|
+
} else {
|
|
3292
|
+
const refusal = schemaRefusal(meta, where, false);
|
|
3293
|
+
if (refusal !== null)
|
|
3294
|
+
throw new ConfigLayoutError(refusal);
|
|
3222
3295
|
}
|
|
3223
3296
|
meta.writtenBy = writtenBy;
|
|
3224
3297
|
return meta;
|
|
@@ -6461,6 +6534,8 @@ var HomeHostedService = class extends Service {
|
|
|
6461
6534
|
servers = read.servers;
|
|
6462
6535
|
if (read.error !== null)
|
|
6463
6536
|
lastError ??= read.error;
|
|
6537
|
+
const file = readConfig(this.options.home, this.managedWorkspace());
|
|
6538
|
+
lastError ??= schemaRefusal(file.raw?.meta, `servers.config.json in ${this.managedWorkspace()}`, !file.exists);
|
|
6464
6539
|
}
|
|
6465
6540
|
return {
|
|
6466
6541
|
defaultEntryId: this.options.defaultEntryId,
|
|
@@ -6694,63 +6769,6 @@ var HomeHostedService = class extends Service {
|
|
|
6694
6769
|
}
|
|
6695
6770
|
};
|
|
6696
6771
|
|
|
6697
|
-
// src/rpc.ts
|
|
6698
|
-
function answer(endpoint, result) {
|
|
6699
|
-
return Response.json({ v: RPC_VERSION, endpoint, result });
|
|
6700
|
-
}
|
|
6701
|
-
function registerRpc(ctx, service) {
|
|
6702
|
-
ctx.inject(["connection"], (scoped) => {
|
|
6703
|
-
const connection = scoped.connection;
|
|
6704
|
-
if (connection?.fetch?.register === void 0)
|
|
6705
|
-
return;
|
|
6706
|
-
scoped.effect(() => {
|
|
6707
|
-
let dispose;
|
|
6708
|
-
try {
|
|
6709
|
-
dispose = connection.fetch.register({
|
|
6710
|
-
// The exact-route registry is keyed by the full request path, so the
|
|
6711
|
-
// `/api` prefix belongs here; the page composes the same URL from
|
|
6712
|
-
// API_BASE + RPC_PATH.
|
|
6713
|
-
path: `/api${RPC_PATH}`,
|
|
6714
|
-
methods: ["POST"],
|
|
6715
|
-
requestBody: "buffered",
|
|
6716
|
-
fetch: async (request) => {
|
|
6717
|
-
let body;
|
|
6718
|
-
try {
|
|
6719
|
-
body = await request.json();
|
|
6720
|
-
} catch {
|
|
6721
|
-
return Response.json({ error: "the request body is not JSON" }, { status: 400 });
|
|
6722
|
-
}
|
|
6723
|
-
if (body === null || typeof body !== "object" || body.v !== RPC_VERSION) {
|
|
6724
|
-
return Response.json(
|
|
6725
|
-
{ error: `this page and the host disagree on the protocol version (expected ${String(RPC_VERSION)})` },
|
|
6726
|
-
{ status: 409 }
|
|
6727
|
-
);
|
|
6728
|
-
}
|
|
6729
|
-
try {
|
|
6730
|
-
const value = await service.call(body.endpoint, body.payload);
|
|
6731
|
-
return answer(body.endpoint, { ok: true, value });
|
|
6732
|
-
} catch (error) {
|
|
6733
|
-
const code = error instanceof HomeHostedError ? error.code : "INTERNAL";
|
|
6734
|
-
const message2 = error instanceof Error ? error.message : String(error);
|
|
6735
|
-
const detail = error?.detail;
|
|
6736
|
-
return answer(body.endpoint, { ok: false, error: { code, message: message2, ...detail === void 0 ? {} : { detail } } });
|
|
6737
|
-
}
|
|
6738
|
-
}
|
|
6739
|
-
});
|
|
6740
|
-
} catch (error) {
|
|
6741
|
-
const logger = scoped.logger;
|
|
6742
|
-
logger?.warn?.(`[dsh-home-hosted] could not register ${RPC_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
6743
|
-
return () => {
|
|
6744
|
-
};
|
|
6745
|
-
}
|
|
6746
|
-
return () => {
|
|
6747
|
-
void Promise.resolve(dispose()).catch(() => {
|
|
6748
|
-
});
|
|
6749
|
-
};
|
|
6750
|
-
}, "dsh-home-hosted: rpc route");
|
|
6751
|
-
});
|
|
6752
|
-
}
|
|
6753
|
-
|
|
6754
6772
|
// src/home-hosted/instances-notice.ts
|
|
6755
6773
|
var INSTANCES_CONTEXT_ORDER = 130;
|
|
6756
6774
|
function instancesContextText(instances, enabled) {
|