home-hosted 0.6.5 → 0.6.6
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 +3 -0
- package/README.md +5 -2
- package/dist/cli.js +234 -77
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/uis/stock/dist/assets/{index-Dqs9Vd8M.js → index-B8A85L38.js} +2 -2
- package/uis/stock/dist/index.html +1 -1
package/AGENTS.md
CHANGED
|
@@ -247,6 +247,9 @@ either is a last resort, and never an accidental one.
|
|
|
247
247
|
every entry's child) instead of killing it — a port held by a sibling is a config mistake.
|
|
248
248
|
- **Never expose beyond loopback without auth and a non-default password.** `checkExposure()` is the
|
|
249
249
|
single rule, enforced at startup, on every settings write, and in the UI.
|
|
250
|
+
- **The CLI drives a running panel through `/_hh`, never `/api`.** `down` and `start`/`stop` read
|
|
251
|
+
run.json's token and speak over loopback through one guard (`assertLocalCall`), so they need no
|
|
252
|
+
session, password or API token — and a new local command must not open an unauthenticated `/api` route.
|
|
250
253
|
- **UIs are external clients.** Nothing in `src/**` may know a UI's markup or files;
|
|
251
254
|
`$HHOSTED_HOME/.ui` overrides the packaged UI at runtime.
|
|
252
255
|
- **A restore writes only where a config says** — archive allowlist, `origin` matching first,
|
package/README.md
CHANGED
|
@@ -79,8 +79,8 @@ some arguments, and the environment you give it:
|
|
|
79
79
|
{ "id": "gateway", "command": "node", "args": ["server.js"], "port": 4000, "autostart": true }
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
<sub>Manage them from the panel, `home-hosted
|
|
83
|
-
the same status line for your own monitor, no session needed.</sub>
|
|
82
|
+
<sub>Manage them from the panel, `home-hosted start|stop <id>` from a shell, `home-hosted status --json`,
|
|
83
|
+
or `GET /api/state`; `GET /healthz` is the same status line for your own monitor, no session needed.</sub>
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
@@ -293,6 +293,8 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
|
|
|
293
293
|
| `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
|
|
294
294
|
| `home-hosted restart` | `down`, then `up` |
|
|
295
295
|
| `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
|
|
296
|
+
| `home-hosted start <id>` | start one server, leaving the panel and its other servers alone |
|
|
297
|
+
| `home-hosted stop <id>` | stop one server — the panel stays up |
|
|
296
298
|
| `home-hosted set-password` | set the panel password without opening a browser |
|
|
297
299
|
| `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
|
|
298
300
|
| `home-hosted migrate` | bring `servers.config.json` up to this release's schema (`--dry-run`, `--yes`) |
|
|
@@ -311,6 +313,7 @@ up, restart -c/--config -p/--port --host --open --no-autostart --foregroun
|
|
|
311
313
|
|
|
312
314
|
down (no flags)
|
|
313
315
|
status --json
|
|
316
|
+
start, stop <id> (the server's id in servers.config.json; both need the panel up)
|
|
314
317
|
init --dir --name --pm --no-install -y/--yes
|
|
315
318
|
set-password --clear
|
|
316
319
|
set-token --generate --clear
|
package/dist/cli.js
CHANGED
|
@@ -133,6 +133,7 @@ function aliasesOf(def) {
|
|
|
133
133
|
function findArg(argsDef, name) {
|
|
134
134
|
for (const [key, def] of Object.entries(argsDef)) {
|
|
135
135
|
if (def === void 0) continue;
|
|
136
|
+
if (def.type === "positional") continue;
|
|
136
137
|
if ((/* @__PURE__ */ new Set([
|
|
137
138
|
key,
|
|
138
139
|
kebab(key),
|
|
@@ -152,10 +153,15 @@ function findArg(argsDef, name) {
|
|
|
152
153
|
*/
|
|
153
154
|
function rejectUnknownFlags(argv, argsDef) {
|
|
154
155
|
if (argsDef === void 0 || Object.keys(argsDef).length === 0) return null;
|
|
156
|
+
let positionals = Object.values(argsDef).filter((def) => def?.type === "positional").length;
|
|
155
157
|
for (let index = 0; index < argv.length; index++) {
|
|
156
158
|
const token = argv[index];
|
|
157
159
|
if (token === "--") return null;
|
|
158
|
-
if (!token.startsWith("-") || token.length === 1)
|
|
160
|
+
if (!token.startsWith("-") || token.length === 1) {
|
|
161
|
+
if (positionals === 0) return `Unexpected argument '${token}'`;
|
|
162
|
+
positionals -= 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
159
165
|
const body = token.startsWith("--") ? token.slice(2) : token.slice(1);
|
|
160
166
|
const equals = body.indexOf("=");
|
|
161
167
|
const name = equals === -1 ? body : body.slice(0, equals);
|
|
@@ -300,6 +306,50 @@ var init_runtime = __esmMin((() => {
|
|
|
300
306
|
NANNY_COMMAND = "__nanny";
|
|
301
307
|
}));
|
|
302
308
|
//#endregion
|
|
309
|
+
//#region src/helpers/version.ts
|
|
310
|
+
var version_exports = /* @__PURE__ */ __exportAll({
|
|
311
|
+
appVersion: () => appVersion,
|
|
312
|
+
versionMismatchNote: () => versionMismatchNote
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* The running release, read from the package manifest.
|
|
316
|
+
*
|
|
317
|
+
* The caller may be `src/**` under tsx or the built `dist/cli.js`, so the manifest
|
|
318
|
+
* is found by walking up rather than by a fixed relative path. `src/cli.ts` keeps
|
|
319
|
+
* its own read because it may only import node builtins statically.
|
|
320
|
+
*/
|
|
321
|
+
function appVersion() {
|
|
322
|
+
if (cached !== null) return cached;
|
|
323
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
325
|
+
try {
|
|
326
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
327
|
+
if (manifest.name === "home-hosted" && typeof manifest.version === "string") {
|
|
328
|
+
cached = manifest.version;
|
|
329
|
+
return cached;
|
|
330
|
+
}
|
|
331
|
+
} catch {}
|
|
332
|
+
const parent = path.dirname(dir);
|
|
333
|
+
if (parent === dir) break;
|
|
334
|
+
dir = parent;
|
|
335
|
+
}
|
|
336
|
+
cached = "0.0.0";
|
|
337
|
+
return cached;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The one line to add when this CLI and the panel it is talking to are not the same
|
|
341
|
+
* release — which is what a linked checkout, a global install and a project pin
|
|
342
|
+
* produce. `null` when they agree.
|
|
343
|
+
*/
|
|
344
|
+
function versionMismatchNote(panelVersion) {
|
|
345
|
+
const cli = appVersion();
|
|
346
|
+
return cli === panelVersion ? null : `note: this CLI is ${cli}, the panel is ${panelVersion}`;
|
|
347
|
+
}
|
|
348
|
+
var cached;
|
|
349
|
+
var init_version = __esmMin((() => {
|
|
350
|
+
cached = null;
|
|
351
|
+
}));
|
|
352
|
+
//#endregion
|
|
303
353
|
//#region src/helpers/cookies.ts
|
|
304
354
|
function parseCookies(header) {
|
|
305
355
|
const cookies = {};
|
|
@@ -893,20 +943,22 @@ password: passwordValueSchema.optional() }).onUndeclaredKey("reject");
|
|
|
893
943
|
tls: tlsStatusSchema
|
|
894
944
|
});
|
|
895
945
|
appStateSchema = type({
|
|
896
|
-
control: controlViewSchema,
|
|
897
|
-
defaults: defaultsSchema,
|
|
898
|
-
logs: logsSchema,
|
|
899
|
-
notifications: notificationViewSchema,
|
|
900
|
-
host: hostViewSchema,
|
|
901
|
-
backups: backupsViewSchema,
|
|
902
|
-
configPath: "string",
|
|
903
|
-
configError: "string | null",
|
|
946
|
+
"control": controlViewSchema,
|
|
947
|
+
"defaults": defaultsSchema,
|
|
948
|
+
"logs": logsSchema,
|
|
949
|
+
"notifications": notificationViewSchema,
|
|
950
|
+
"host": hostViewSchema,
|
|
951
|
+
"backups": backupsViewSchema,
|
|
952
|
+
"configPath": "string",
|
|
953
|
+
"configError": "string | null",
|
|
904
954
|
/** The directory the panel was started from; relative entry paths use it. */
|
|
905
|
-
projectDir: "string",
|
|
955
|
+
"projectDir": "string",
|
|
906
956
|
/** `HHOSTED_HOME`: every file home-hosted owns lives under here. */
|
|
907
|
-
dataRoot: "string",
|
|
908
|
-
logsDir: "string",
|
|
909
|
-
|
|
957
|
+
"dataRoot": "string",
|
|
958
|
+
"logsDir": "string",
|
|
959
|
+
/** The release this panel is running. Optional: an older panel does not send one. */
|
|
960
|
+
"version?": "string",
|
|
961
|
+
"servers": serverViewSchema.array()
|
|
910
962
|
});
|
|
911
963
|
type({
|
|
912
964
|
type: "\"hello\" | \"state\" | \"log\" | \"server\"",
|
|
@@ -1934,11 +1986,13 @@ function buildAppState(deps) {
|
|
|
1934
1986
|
projectDir,
|
|
1935
1987
|
dataRoot,
|
|
1936
1988
|
logsDir: deps.logsDir,
|
|
1989
|
+
version: appVersion(),
|
|
1937
1990
|
servers: deps.views
|
|
1938
1991
|
};
|
|
1939
1992
|
}
|
|
1940
1993
|
var init_state$1 = __esmMin((() => {
|
|
1941
1994
|
init_paths();
|
|
1995
|
+
init_version();
|
|
1942
1996
|
init_exposure();
|
|
1943
1997
|
}));
|
|
1944
1998
|
//#endregion
|
|
@@ -2108,6 +2162,14 @@ var init_backups$1 = __esmMin((() => {
|
|
|
2108
2162
|
nameParam = type({ name: "string >= 1" });
|
|
2109
2163
|
}));
|
|
2110
2164
|
//#endregion
|
|
2165
|
+
//#region src/helpers/action-result.ts
|
|
2166
|
+
/** Unknown ids are 404; a server that exists but cannot act is a 409. */
|
|
2167
|
+
function statusForAction(result) {
|
|
2168
|
+
if (result.ok) return 200;
|
|
2169
|
+
return result.error?.startsWith("unknown server") ? 404 : 409;
|
|
2170
|
+
}
|
|
2171
|
+
var init_action_result = __esmMin((() => {}));
|
|
2172
|
+
//#endregion
|
|
2111
2173
|
//#region src/helpers/deferred.ts
|
|
2112
2174
|
/**
|
|
2113
2175
|
* Runs a task after the current response has been written.
|
|
@@ -2126,35 +2188,64 @@ var init_deferred = __esmMin((() => {}));
|
|
|
2126
2188
|
//#endregion
|
|
2127
2189
|
//#region src/api/control.ts
|
|
2128
2190
|
/**
|
|
2129
|
-
* The local control channel used by `home-hosted down`.
|
|
2191
|
+
* The local control channel used by `home-hosted down` and `home-hosted start|stop`.
|
|
2130
2192
|
*
|
|
2131
2193
|
* It is deliberately outside `/api` (so no session is needed) and guarded by a
|
|
2132
2194
|
* token that only the owner of `run.json` can read, plus a loopback check: a
|
|
2133
2195
|
* different local user, or anyone on the network, gets nothing.
|
|
2134
2196
|
*/
|
|
2135
2197
|
function createControlRoute(deps) {
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
summary: "Stop the panel and everything it supervises (local token required)",
|
|
2139
|
-
responses: {
|
|
2140
|
-
200: { description: "Stopping" },
|
|
2141
|
-
403: { description: "Bad token, or not a local caller" }
|
|
2142
|
-
}
|
|
2143
|
-
}), (c) => {
|
|
2198
|
+
/** The one place the local channel's credentials are read. */
|
|
2199
|
+
function assertLocalCall(c, action) {
|
|
2144
2200
|
const token = c.req.header("x-home-hosted-token");
|
|
2145
2201
|
if (token === void 0 || token !== deps.runtimeToken) throw new DetailedError("invalid token", {
|
|
2146
2202
|
statusCode: 403,
|
|
2147
2203
|
code: "INVALID_TOKEN"
|
|
2148
2204
|
});
|
|
2149
|
-
if (!isLoopbackRequest(c)) throw new DetailedError(
|
|
2205
|
+
if (!isLoopbackRequest(c)) throw new DetailedError(`only this machine may ${action}`, {
|
|
2150
2206
|
statusCode: 403,
|
|
2151
2207
|
code: "NOT_LOOPBACK"
|
|
2152
2208
|
});
|
|
2209
|
+
}
|
|
2210
|
+
return appFactory.createApp().post("/shutdown", describeRoute({
|
|
2211
|
+
tags: ["panel"],
|
|
2212
|
+
summary: "Stop the panel and everything it supervises (local token required)",
|
|
2213
|
+
responses: {
|
|
2214
|
+
200: { description: "Stopping" },
|
|
2215
|
+
403: { description: "Bad token, or not a local caller" }
|
|
2216
|
+
}
|
|
2217
|
+
}), (c) => {
|
|
2218
|
+
assertLocalCall(c, "stop the control panel");
|
|
2153
2219
|
afterResponse(deps.onShutdown, (error) => logger.error("shutdown failed", error));
|
|
2154
2220
|
return c.json({ ok: true });
|
|
2221
|
+
}).post("/servers/:id/start", describeRoute({
|
|
2222
|
+
tags: ["panel"],
|
|
2223
|
+
summary: "Start one server, without a session (local token required)",
|
|
2224
|
+
responses: {
|
|
2225
|
+
200: { description: "Started" },
|
|
2226
|
+
403: { description: "Bad token, or not a local caller" },
|
|
2227
|
+
404: { description: "No such server" }
|
|
2228
|
+
}
|
|
2229
|
+
}), async (c) => {
|
|
2230
|
+
assertLocalCall(c, "start or stop a server");
|
|
2231
|
+
const result = await deps.supervisor.start(c.req.param("id"));
|
|
2232
|
+
return c.json(result, statusForAction(result));
|
|
2233
|
+
}).post("/servers/:id/stop", describeRoute({
|
|
2234
|
+
tags: ["panel"],
|
|
2235
|
+
summary: "Stop one server, without a session (local token required)",
|
|
2236
|
+
responses: {
|
|
2237
|
+
200: { description: "Stopped" },
|
|
2238
|
+
403: { description: "Bad token, or not a local caller" },
|
|
2239
|
+
404: { description: "No such server" }
|
|
2240
|
+
}
|
|
2241
|
+
}), async (c) => {
|
|
2242
|
+
assertLocalCall(c, "start or stop a server");
|
|
2243
|
+
const result = await deps.supervisor.stop(c.req.param("id"));
|
|
2244
|
+
return c.json(result, statusForAction(result));
|
|
2155
2245
|
});
|
|
2156
2246
|
}
|
|
2157
2247
|
var init_control = __esmMin((() => {
|
|
2248
|
+
init_action_result();
|
|
2158
2249
|
init_deferred();
|
|
2159
2250
|
init_factory();
|
|
2160
2251
|
init_logger();
|
|
@@ -2676,38 +2767,6 @@ var init_schema = __esmMin((() => {
|
|
|
2676
2767
|
];
|
|
2677
2768
|
}));
|
|
2678
2769
|
//#endregion
|
|
2679
|
-
//#region src/helpers/version.ts
|
|
2680
|
-
var version_exports = /* @__PURE__ */ __exportAll({ appVersion: () => appVersion });
|
|
2681
|
-
/**
|
|
2682
|
-
* The running release, read from the package manifest.
|
|
2683
|
-
*
|
|
2684
|
-
* The caller may be `src/**` under tsx or the built `dist/cli.js`, so the manifest
|
|
2685
|
-
* is found by walking up rather than by a fixed relative path. `src/cli.ts` keeps
|
|
2686
|
-
* its own read because it may only import node builtins statically.
|
|
2687
|
-
*/
|
|
2688
|
-
function appVersion() {
|
|
2689
|
-
if (cached !== null) return cached;
|
|
2690
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
2691
|
-
for (let depth = 0; depth < 4; depth++) {
|
|
2692
|
-
try {
|
|
2693
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
2694
|
-
if (manifest.name === "home-hosted" && typeof manifest.version === "string") {
|
|
2695
|
-
cached = manifest.version;
|
|
2696
|
-
return cached;
|
|
2697
|
-
}
|
|
2698
|
-
} catch {}
|
|
2699
|
-
const parent = path.dirname(dir);
|
|
2700
|
-
if (parent === dir) break;
|
|
2701
|
-
dir = parent;
|
|
2702
|
-
}
|
|
2703
|
-
cached = "0.0.0";
|
|
2704
|
-
return cached;
|
|
2705
|
-
}
|
|
2706
|
-
var cached;
|
|
2707
|
-
var init_version = __esmMin((() => {
|
|
2708
|
-
cached = null;
|
|
2709
|
-
}));
|
|
2710
|
-
//#endregion
|
|
2711
2770
|
//#region src/config/parse.ts
|
|
2712
2771
|
function isRecord$1(value) {
|
|
2713
2772
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3233,11 +3292,6 @@ var init_store = __esmMin((() => {
|
|
|
3233
3292
|
}));
|
|
3234
3293
|
//#endregion
|
|
3235
3294
|
//#region src/api/servers/$.routes.ts
|
|
3236
|
-
/** Unknown ids are 404; a server that exists but cannot start is a 409. */
|
|
3237
|
-
function statusFor(result) {
|
|
3238
|
-
if (result.ok) return 200;
|
|
3239
|
-
return result.error?.startsWith("unknown server") ? 404 : 409;
|
|
3240
|
-
}
|
|
3241
3295
|
function unknownServer(id) {
|
|
3242
3296
|
return new DetailedError(`unknown server "${id}"`, {
|
|
3243
3297
|
statusCode: 404,
|
|
@@ -3387,7 +3441,7 @@ function createServersRoute(deps) {
|
|
|
3387
3441
|
}
|
|
3388
3442
|
}), validate("param", idParam), async (c) => {
|
|
3389
3443
|
const result = await deps.supervisor.start(c.req.valid("param").id);
|
|
3390
|
-
return c.json(result,
|
|
3444
|
+
return c.json(result, statusForAction(result));
|
|
3391
3445
|
}).post("/:id/stop", describeRoute({
|
|
3392
3446
|
tags: ["servers"],
|
|
3393
3447
|
summary: "Stop a server",
|
|
@@ -3407,7 +3461,7 @@ function createServersRoute(deps) {
|
|
|
3407
3461
|
}
|
|
3408
3462
|
}), validate("param", idParam), async (c) => {
|
|
3409
3463
|
const result = await deps.supervisor.restart(c.req.valid("param").id);
|
|
3410
|
-
return c.json(result,
|
|
3464
|
+
return c.json(result, statusForAction(result));
|
|
3411
3465
|
}).post("/:id/clear-logs", describeRoute({
|
|
3412
3466
|
tags: ["servers"],
|
|
3413
3467
|
summary: "Forget the buffered log lines",
|
|
@@ -3433,7 +3487,7 @@ function createServersRoute(deps) {
|
|
|
3433
3487
|
const { id } = c.req.valid("param");
|
|
3434
3488
|
const result = await deps.supervisor.freePort(id);
|
|
3435
3489
|
if (!result.ok) throw new DetailedError(result.error ?? `could not free the port for "${id}"`, {
|
|
3436
|
-
statusCode:
|
|
3490
|
+
statusCode: statusForAction(result),
|
|
3437
3491
|
code: "FREE_PORT_FAILED"
|
|
3438
3492
|
});
|
|
3439
3493
|
return c.json(result);
|
|
@@ -3486,6 +3540,7 @@ function createServersRoute(deps) {
|
|
|
3486
3540
|
var idParam, MAX_PENDING_WRITES, serverResponse, serversResponse, okResponse;
|
|
3487
3541
|
var init_$_routes = __esmMin((() => {
|
|
3488
3542
|
init_store();
|
|
3543
|
+
init_action_result();
|
|
3489
3544
|
init_factory();
|
|
3490
3545
|
init_openapi$1();
|
|
3491
3546
|
init_validator();
|
|
@@ -3968,6 +4023,7 @@ var daemon_exports = /* @__PURE__ */ __exportAll({
|
|
|
3968
4023
|
newToken: () => newToken,
|
|
3969
4024
|
probeRuntime: () => probeRuntime,
|
|
3970
4025
|
readRuntime: () => readRuntime,
|
|
4026
|
+
requestControl: () => requestControl,
|
|
3971
4027
|
requestShutdown: () => requestShutdown,
|
|
3972
4028
|
runtimeSchema: () => runtimeSchema,
|
|
3973
4029
|
writeRuntime: () => writeRuntime
|
|
@@ -4000,10 +4056,10 @@ function isProcessAlive$1(pid) {
|
|
|
4000
4056
|
}
|
|
4001
4057
|
/** A degraded panel is still answering: a 503 must not read as "not running". */
|
|
4002
4058
|
async function probeRuntime(runtime, timeoutMs = 2500) {
|
|
4003
|
-
const
|
|
4059
|
+
const answer = await localCall(runtime, "/healthz", "GET", void 0, timeoutMs);
|
|
4004
4060
|
return {
|
|
4005
|
-
reachable:
|
|
4006
|
-
degraded: status === 503
|
|
4061
|
+
reachable: answer !== null,
|
|
4062
|
+
degraded: answer?.status === 503
|
|
4007
4063
|
};
|
|
4008
4064
|
}
|
|
4009
4065
|
/**
|
|
@@ -4011,15 +4067,36 @@ async function probeRuntime(runtime, timeoutMs = 2500) {
|
|
|
4011
4067
|
* shut down cleanly on every platform (a bare signal is not graceful on Windows).
|
|
4012
4068
|
*/
|
|
4013
4069
|
async function requestShutdown(runtime, timeoutMs = 4e3) {
|
|
4014
|
-
const
|
|
4015
|
-
return
|
|
4070
|
+
const answer = await localCall(runtime, "/_hh/shutdown", "POST", runtime.token, timeoutMs);
|
|
4071
|
+
return answer !== null && answer.status >= 200 && answer.status < 300;
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Drives a supervised server from this machine (`home-hosted start`/`stop`). The
|
|
4075
|
+
* token comes from run.json, so it needs no session, no password and no API token —
|
|
4076
|
+
* only the owner of that 0600 file, talking over loopback.
|
|
4077
|
+
*
|
|
4078
|
+
* The wait is generous on purpose: a start may have to run a bootstrap, which has a
|
|
4079
|
+
* 120s default timeout of its own. Past this it is a wedged panel, not a slow one.
|
|
4080
|
+
*/
|
|
4081
|
+
async function requestControl(runtime, path, timeoutMs = 15e4) {
|
|
4082
|
+
const answer = await localCall(runtime, path, "POST", runtime.token, timeoutMs);
|
|
4083
|
+
if (answer === null) return null;
|
|
4084
|
+
let body = null;
|
|
4085
|
+
try {
|
|
4086
|
+
body = JSON.parse(answer.body);
|
|
4087
|
+
} catch {}
|
|
4088
|
+
return {
|
|
4089
|
+
status: answer.status,
|
|
4090
|
+
body
|
|
4091
|
+
};
|
|
4016
4092
|
}
|
|
4017
4093
|
/**
|
|
4018
4094
|
* Talks to the panel over loopback. Node's `fetch` cannot be told to accept the
|
|
4019
4095
|
* self-signed certificate an uploaded TLS pair usually is, which would break
|
|
4020
|
-
* `status
|
|
4096
|
+
* `status`, the graceful `down` and the local start/stop — so this speaks
|
|
4097
|
+
* http/https directly.
|
|
4021
4098
|
*/
|
|
4022
|
-
function
|
|
4099
|
+
function localCall(runtime, path, method, token, timeoutMs) {
|
|
4023
4100
|
return new Promise((resolve) => {
|
|
4024
4101
|
const url = new URL(`${runtime.probeUrl}${path}`);
|
|
4025
4102
|
const secure = url.protocol === "https:";
|
|
@@ -4032,8 +4109,15 @@ function localRequest(runtime, path, method, token, timeoutMs) {
|
|
|
4032
4109
|
headers: token === void 0 ? {} : { "x-home-hosted-token": token },
|
|
4033
4110
|
timeout: timeoutMs
|
|
4034
4111
|
}, (response) => {
|
|
4035
|
-
|
|
4036
|
-
response.
|
|
4112
|
+
let body = "";
|
|
4113
|
+
response.setEncoding("utf8");
|
|
4114
|
+
response.on("data", (chunk) => {
|
|
4115
|
+
body += chunk;
|
|
4116
|
+
});
|
|
4117
|
+
response.once("end", () => resolve({
|
|
4118
|
+
status: response.statusCode ?? 0,
|
|
4119
|
+
body
|
|
4120
|
+
}));
|
|
4037
4121
|
});
|
|
4038
4122
|
request.once("error", () => resolve(null));
|
|
4039
4123
|
request.once("timeout", () => {
|
|
@@ -9530,7 +9614,9 @@ async function runUp(flags, entry) {
|
|
|
9530
9614
|
const { daemonLogPath, dataRoot, projectDir } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
9531
9615
|
const existing = readRuntime();
|
|
9532
9616
|
if (existing !== null && isProcessAlive(existing.pid)) {
|
|
9533
|
-
process.stdout.write(`${green("already running")} (pid ${existing.pid}) at ${existing.url}\n`);
|
|
9617
|
+
process.stdout.write(`${green("already running")} home-hosted ${existing.version} (pid ${existing.pid}) at ${existing.url}\n`);
|
|
9618
|
+
const note = versionMismatchNote(existing.version);
|
|
9619
|
+
if (note !== null) process.stdout.write(`${dim(note)}\n`);
|
|
9534
9620
|
process.stdout.write(`${dim("stop it with `home-hosted down`")}\n`);
|
|
9535
9621
|
return;
|
|
9536
9622
|
}
|
|
@@ -9566,7 +9652,7 @@ async function runUp(flags, entry) {
|
|
|
9566
9652
|
if (output.length > 0) process.stderr.write(`${dim(`${daemonLogPath}:`)}\n${output}\n`);
|
|
9567
9653
|
process.exit(1);
|
|
9568
9654
|
}
|
|
9569
|
-
process.stdout.write(`${green(
|
|
9655
|
+
process.stdout.write(`${green(`home-hosted ${runtime.version} is up`)} (pid ${runtime.pid})\n`);
|
|
9570
9656
|
process.stdout.write(` ${bold(runtime.url)}\n`);
|
|
9571
9657
|
process.stdout.write(` ${dim(`project ${runtime.projectDir}`)}\n`);
|
|
9572
9658
|
process.stdout.write(` ${dim(`state ${runtime.dataRoot}`)}\n`);
|
|
@@ -9600,6 +9686,7 @@ var init_up = __esmMin((() => {
|
|
|
9600
9686
|
init_args();
|
|
9601
9687
|
init_io();
|
|
9602
9688
|
init_runtime();
|
|
9689
|
+
init_version();
|
|
9603
9690
|
DEFAULT_PORT$1 = 3999;
|
|
9604
9691
|
LOG_ROTATE_BYTES = 5242880;
|
|
9605
9692
|
upArgs = {
|
|
@@ -9652,14 +9739,20 @@ async function runDown() {
|
|
|
9652
9739
|
}
|
|
9653
9740
|
if (!isProcessAlive(runtime.pid)) {
|
|
9654
9741
|
clearRuntime();
|
|
9655
|
-
process.stdout.write(
|
|
9742
|
+
process.stdout.write(`home-hosted is not running (removed a stale run.json from home-hosted ${runtime.version})\n`);
|
|
9656
9743
|
return;
|
|
9657
9744
|
}
|
|
9745
|
+
/** What was stopped, and whether this CLI is a different release than it was. */
|
|
9746
|
+
const reportStopped = (forced) => {
|
|
9747
|
+
process.stdout.write(`${green("stopped")}${forced ? " (forced)" : ""} ${dim(`— was home-hosted ${runtime.version}`)}\n`);
|
|
9748
|
+
const note = versionMismatchNote(runtime.version);
|
|
9749
|
+
if (note !== null) process.stdout.write(`${dim(note)}\n`);
|
|
9750
|
+
};
|
|
9658
9751
|
process.stdout.write(`stopping pid ${runtime.pid}…\n`);
|
|
9659
9752
|
if (!await requestShutdown(runtime)) signal(runtime.pid, "SIGTERM");
|
|
9660
9753
|
if (await waitForExit(runtime.pid, 2e4)) {
|
|
9661
9754
|
clearRuntime();
|
|
9662
|
-
|
|
9755
|
+
reportStopped(false);
|
|
9663
9756
|
await reportPersistent();
|
|
9664
9757
|
return;
|
|
9665
9758
|
}
|
|
@@ -9667,7 +9760,7 @@ async function runDown() {
|
|
|
9667
9760
|
forceStop(runtime.pid);
|
|
9668
9761
|
await waitForExit(runtime.pid, 5e3);
|
|
9669
9762
|
clearRuntime();
|
|
9670
|
-
|
|
9763
|
+
reportStopped(true);
|
|
9671
9764
|
await reportPersistent();
|
|
9672
9765
|
}
|
|
9673
9766
|
/**
|
|
@@ -9725,6 +9818,7 @@ function forceStop(pid) {
|
|
|
9725
9818
|
var downCommand;
|
|
9726
9819
|
var init_down = __esmMin((() => {
|
|
9727
9820
|
init_io();
|
|
9821
|
+
init_version();
|
|
9728
9822
|
downCommand = defineCommand({
|
|
9729
9823
|
meta: {
|
|
9730
9824
|
name: "down",
|
|
@@ -9836,6 +9930,55 @@ var init_status = __esmMin((() => {
|
|
|
9836
9930
|
});
|
|
9837
9931
|
}));
|
|
9838
9932
|
//#endregion
|
|
9933
|
+
//#region src/cli/server.ts
|
|
9934
|
+
var server_exports = /* @__PURE__ */ __exportAll({
|
|
9935
|
+
runServerAction: () => runServerAction,
|
|
9936
|
+
startCommand: () => startCommand,
|
|
9937
|
+
stopCommand: () => stopCommand
|
|
9938
|
+
});
|
|
9939
|
+
async function runServerAction(action, id) {
|
|
9940
|
+
const { clearRuntime, isProcessAlive, readRuntime, requestControl } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
9941
|
+
const runtime = readRuntime();
|
|
9942
|
+
if (runtime === null || !isProcessAlive(runtime.pid)) {
|
|
9943
|
+
if (runtime !== null) clearRuntime();
|
|
9944
|
+
fail("home-hosted is not running — start it with `home-hosted up`");
|
|
9945
|
+
}
|
|
9946
|
+
const answer = await requestControl(runtime, `/_hh/servers/${encodeURIComponent(id)}/${action}`);
|
|
9947
|
+
if (answer === null) fail(`the control panel is not answering on ${runtime.probeUrl}`);
|
|
9948
|
+
const body = answer.body ?? {};
|
|
9949
|
+
if (body.ok !== true) {
|
|
9950
|
+
const reason = body.error ?? body.message;
|
|
9951
|
+
if (reason !== void 0) fail(reason);
|
|
9952
|
+
if (answer.status === 404) fail(`the panel on ${runtime.url} is home-hosted ${runtime.version}, which does not know this command — restart it on this release`);
|
|
9953
|
+
fail(`the panel answered ${answer.status}`);
|
|
9954
|
+
}
|
|
9955
|
+
process.stdout.write(`${green(action === "start" ? "started" : "stopped")} ${id}\n`);
|
|
9956
|
+
}
|
|
9957
|
+
function serverCommand(action) {
|
|
9958
|
+
return defineCommand({
|
|
9959
|
+
meta: {
|
|
9960
|
+
name: action,
|
|
9961
|
+
description: action === "start" ? "start one server (the panel keeps running)" : "stop one server (the panel keeps running)"
|
|
9962
|
+
},
|
|
9963
|
+
args: { id: {
|
|
9964
|
+
type: "positional",
|
|
9965
|
+
required: false,
|
|
9966
|
+
description: "the server id from servers.config.json"
|
|
9967
|
+
} },
|
|
9968
|
+
run: async ({ args }) => {
|
|
9969
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
9970
|
+
if (id.length === 0) fail(`${action} needs a server id — see \`home-hosted status\` or servers.config.json`);
|
|
9971
|
+
await runServerAction(action, id);
|
|
9972
|
+
}
|
|
9973
|
+
});
|
|
9974
|
+
}
|
|
9975
|
+
var startCommand, stopCommand;
|
|
9976
|
+
var init_server = __esmMin((() => {
|
|
9977
|
+
init_io();
|
|
9978
|
+
startCommand = serverCommand("start");
|
|
9979
|
+
stopCommand = serverCommand("stop");
|
|
9980
|
+
}));
|
|
9981
|
+
//#endregion
|
|
9839
9982
|
//#region src/cli/set-password.ts
|
|
9840
9983
|
var set_password_exports = /* @__PURE__ */ __exportAll({
|
|
9841
9984
|
runSetPassword: () => runSetPassword,
|
|
@@ -10894,6 +11037,8 @@ var SYNOPSIS = {
|
|
|
10894
11037
|
"down": "home-hosted down",
|
|
10895
11038
|
"restart": "home-hosted restart [options]",
|
|
10896
11039
|
"status": "home-hosted status [--json]",
|
|
11040
|
+
"start": "home-hosted start <id>",
|
|
11041
|
+
"stop": "home-hosted stop <id>",
|
|
10897
11042
|
"set-password": "home-hosted set-password",
|
|
10898
11043
|
"set-token": "home-hosted set-token",
|
|
10899
11044
|
"migrate": "home-hosted migrate",
|
|
@@ -10907,6 +11052,8 @@ var SUMMARIES = {
|
|
|
10907
11052
|
"down": "stop it, and everything it supervises",
|
|
10908
11053
|
"restart": "down, then up",
|
|
10909
11054
|
"status": "is it running, where, and how to reach it",
|
|
11055
|
+
"start": "start one server, leaving the panel up",
|
|
11056
|
+
"stop": "stop one server, leaving the panel up",
|
|
10910
11057
|
"set-password": "set the panel password without the API",
|
|
10911
11058
|
"set-token": "set the API token that scripts and agents use",
|
|
10912
11059
|
"migrate": "bring the config up to this release's schema",
|
|
@@ -10933,6 +11080,10 @@ var STATUS_SECTION = {
|
|
|
10933
11080
|
heading: "Options for status",
|
|
10934
11081
|
lines: [["--json", "print machine-readable JSON"]]
|
|
10935
11082
|
};
|
|
11083
|
+
var SERVER_SECTION = {
|
|
11084
|
+
heading: "Arguments for start/stop",
|
|
11085
|
+
lines: [["<id>", "the server id from servers.config.json"]]
|
|
11086
|
+
};
|
|
10936
11087
|
var SET_PASSWORD_SECTION = {
|
|
10937
11088
|
heading: "Options for set-password",
|
|
10938
11089
|
lines: [["--clear", "remove the password, which disables authentication"]]
|
|
@@ -11044,11 +11195,15 @@ var USAGE = [
|
|
|
11044
11195
|
"",
|
|
11045
11196
|
renderSection(STATUS_SECTION),
|
|
11046
11197
|
"",
|
|
11198
|
+
renderSection(SERVER_SECTION),
|
|
11199
|
+
"",
|
|
11047
11200
|
SHARED_TRAILER
|
|
11048
11201
|
].join("\n");
|
|
11049
11202
|
var UP_COMMANDS = /* @__PURE__ */ new Set(["up", "restart"]);
|
|
11050
11203
|
var SECTIONS = {
|
|
11051
11204
|
"status": STATUS_SECTION,
|
|
11205
|
+
"start": SERVER_SECTION,
|
|
11206
|
+
"stop": SERVER_SECTION,
|
|
11052
11207
|
"set-password": SET_PASSWORD_SECTION,
|
|
11053
11208
|
"set-token": SET_TOKEN_SECTION,
|
|
11054
11209
|
"migrate": MIGRATE_SECTION,
|
|
@@ -11095,6 +11250,8 @@ var COMMANDS = {
|
|
|
11095
11250
|
"down": () => Promise.resolve().then(() => (init_down(), down_exports)).then((module) => module.downCommand),
|
|
11096
11251
|
"restart": () => Promise.resolve().then(() => (init_restart(), restart_exports)).then((module) => module.restartCommand(CLI_ENTRY)),
|
|
11097
11252
|
"status": () => Promise.resolve().then(() => (init_status(), status_exports)).then((module) => module.statusCommand),
|
|
11253
|
+
"start": () => Promise.resolve().then(() => (init_server(), server_exports)).then((module) => module.startCommand),
|
|
11254
|
+
"stop": () => Promise.resolve().then(() => (init_server(), server_exports)).then((module) => module.stopCommand),
|
|
11098
11255
|
"set-password": () => Promise.resolve().then(() => (init_set_password(), set_password_exports)).then((module) => module.setPasswordCommand),
|
|
11099
11256
|
"set-token": () => Promise.resolve().then(() => (init_set_token(), set_token_exports)).then((module) => module.setTokenCommand),
|
|
11100
11257
|
"migrate": () => Promise.resolve().then(() => (init_migrate(), migrate_exports)).then((module) => module.migrateCommand),
|