home-hosted 0.7.11 → 0.7.13
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 +15 -0
- package/README.md +26 -3
- package/dist/cli.js +411 -103
- package/dist/cli.js.map +1 -1
- package/package.json +4 -2
- package/uis/stock/dist/ui.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,8 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { defineCommand, runCommand } from "citty";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import readline from "node:readline";
|
|
7
|
-
import { exec, execFile, spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { exec, execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import { describeRoute, openAPIRouteHandler, resolver, validator } from "hono-openapi";
|
|
10
11
|
import { createFactory } from "hono/factory";
|
|
@@ -13,7 +14,6 @@ import { isDevelopment } from "std-env";
|
|
|
13
14
|
import { DetailedError } from "@namesmt/utils";
|
|
14
15
|
import { type } from "arktype";
|
|
15
16
|
import crypto, { X509Certificate, createPrivateKey, createPublicKey, randomBytes } from "node:crypto";
|
|
16
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
17
17
|
import { streamSSE } from "hono/streaming";
|
|
18
18
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
19
19
|
import { Bot } from "grammy";
|
|
@@ -306,6 +306,112 @@ var init_runtime = __esmMin((() => {
|
|
|
306
306
|
NANNY_COMMAND = "__nanny";
|
|
307
307
|
}));
|
|
308
308
|
//#endregion
|
|
309
|
+
//#region src/helpers/daemon-log.ts
|
|
310
|
+
var daemon_log_exports = /* @__PURE__ */ __exportAll({
|
|
311
|
+
followLog: () => followLog,
|
|
312
|
+
readLog: () => readLog,
|
|
313
|
+
rotateLog: () => rotateLog,
|
|
314
|
+
tailLog: () => tailLog
|
|
315
|
+
});
|
|
316
|
+
function rotateLog(file) {
|
|
317
|
+
try {
|
|
318
|
+
if (fs.statSync(file).size < LOG_ROTATE_BYTES) return;
|
|
319
|
+
fs.rmSync(`${file}.1`, { force: true });
|
|
320
|
+
fs.renameSync(file, `${file}.1`);
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The last `lines` lines, or `''` when there is no log to read.
|
|
325
|
+
*
|
|
326
|
+
* Split on the trailing newline rather than after it: a log file ends with one, so a plain
|
|
327
|
+
* `split('\n').slice(-lines)` counts that empty last element as a line and quietly returns
|
|
328
|
+
* one fewer than asked for.
|
|
329
|
+
*/
|
|
330
|
+
function tailLog(file, lines = 15) {
|
|
331
|
+
return withoutTrailingBlank(readLogLines(file)).slice(-lines).join("\n");
|
|
332
|
+
}
|
|
333
|
+
function readLogLines(file) {
|
|
334
|
+
try {
|
|
335
|
+
return fs.readFileSync(file, "utf8").split("\n");
|
|
336
|
+
} catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** A file ending in a newline splits into a final empty element; it is not a line. */
|
|
341
|
+
function withoutTrailingBlank(lines) {
|
|
342
|
+
return lines.at(-1) === "" ? lines.slice(0, -1) : lines;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* One rotation boundary's worth of lines, oldest first.
|
|
346
|
+
*
|
|
347
|
+
* The current file plus its `.1` ancestor, so `logs` shows what happened just before a
|
|
348
|
+
* rotation rather than starting blank. `lines <= 0` means the whole thing.
|
|
349
|
+
*/
|
|
350
|
+
function readLog(file, lines) {
|
|
351
|
+
const parts = [];
|
|
352
|
+
for (const candidate of [`${file}.1`, file]) try {
|
|
353
|
+
parts.push(fs.readFileSync(candidate, "utf8"));
|
|
354
|
+
} catch {}
|
|
355
|
+
const all = withoutTrailingBlank(parts.join("").split("\n"));
|
|
356
|
+
return lines > 0 ? all.slice(-lines) : all;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Prints the last `lines` of the log, then keeps printing whatever is appended.
|
|
360
|
+
*
|
|
361
|
+
* Polled rather than watched on purpose. `config-watch` documents why: an editor's save
|
|
362
|
+
* replaces the inode, and `fs.watch` is undependable on some filesystems. A foreground
|
|
363
|
+
* command also has to notice the rotation `up` performs, which shows up simply as the file
|
|
364
|
+
* getting *smaller* — and a poll sees that without any special case.
|
|
365
|
+
*
|
|
366
|
+
* Resolves when the caller's signal arrives, so the command exits the way a pager would.
|
|
367
|
+
*/
|
|
368
|
+
async function followLog(file, lines, write, pollMs = 250) {
|
|
369
|
+
const initial = readLog(file, lines);
|
|
370
|
+
if (initial.length > 0) write(`${initial.join("\n")}\n`);
|
|
371
|
+
let offset = sizeOf(file);
|
|
372
|
+
let stopped = false;
|
|
373
|
+
const stop = () => {
|
|
374
|
+
stopped = true;
|
|
375
|
+
};
|
|
376
|
+
process.once("SIGINT", stop);
|
|
377
|
+
process.once("SIGTERM", stop);
|
|
378
|
+
try {
|
|
379
|
+
for (;;) {
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
381
|
+
if (stopped) break;
|
|
382
|
+
const size = sizeOf(file);
|
|
383
|
+
if (size < offset) offset = 0;
|
|
384
|
+
if (size === offset) continue;
|
|
385
|
+
try {
|
|
386
|
+
const handle = fs.openSync(file, "r");
|
|
387
|
+
try {
|
|
388
|
+
const length = size - offset;
|
|
389
|
+
const buffer = Buffer$1.alloc(length);
|
|
390
|
+
fs.readSync(handle, buffer, 0, length, offset);
|
|
391
|
+
write(buffer.toString("utf8").replace(/\n+$/, "\n"));
|
|
392
|
+
} finally {
|
|
393
|
+
fs.closeSync(handle);
|
|
394
|
+
}
|
|
395
|
+
offset = size;
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
} finally {
|
|
399
|
+
process.removeListener("SIGINT", stop);
|
|
400
|
+
process.removeListener("SIGTERM", stop);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function sizeOf(file) {
|
|
404
|
+
try {
|
|
405
|
+
return fs.statSync(file).size;
|
|
406
|
+
} catch {
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var LOG_ROTATE_BYTES;
|
|
411
|
+
var init_daemon_log = __esmMin((() => {
|
|
412
|
+
LOG_ROTATE_BYTES = 5242880;
|
|
413
|
+
}));
|
|
414
|
+
//#endregion
|
|
309
415
|
//#region src/helpers/version.ts
|
|
310
416
|
var version_exports = /* @__PURE__ */ __exportAll({
|
|
311
417
|
appVersion: () => appVersion,
|
|
@@ -3193,6 +3299,18 @@ function createControlRoute(deps) {
|
|
|
3193
3299
|
assertLocalCall(c, "start or stop a server");
|
|
3194
3300
|
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.stop(c.req.param("id"));
|
|
3195
3301
|
return c.json(result, statusForAction(result));
|
|
3302
|
+
}).post("/servers/:id/restart", describeRoute({
|
|
3303
|
+
tags: ["panel"],
|
|
3304
|
+
summary: "Restart one server, without a session (local token required)",
|
|
3305
|
+
responses: {
|
|
3306
|
+
200: { description: "Restarted" },
|
|
3307
|
+
403: { description: "Bad token, or not a local caller" },
|
|
3308
|
+
404: { description: "No such server" }
|
|
3309
|
+
}
|
|
3310
|
+
}), validate("query", workspaceQuerySchema), async (c) => {
|
|
3311
|
+
assertLocalCall(c, "restart a server");
|
|
3312
|
+
const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.restart(c.req.param("id"));
|
|
3313
|
+
return c.json(result, statusForAction(result));
|
|
3196
3314
|
});
|
|
3197
3315
|
}
|
|
3198
3316
|
var init_control = __esmMin((() => {
|
|
@@ -6018,7 +6136,7 @@ function createLogsRoute(deps) {
|
|
|
6018
6136
|
});
|
|
6019
6137
|
}
|
|
6020
6138
|
var MIN_TAIL, MAX_TAIL, DEFAULT_TAIL, idParam$2, downloadQuery, historyQuery;
|
|
6021
|
-
var init_logs = __esmMin((() => {
|
|
6139
|
+
var init_logs$1 = __esmMin((() => {
|
|
6022
6140
|
init_factory();
|
|
6023
6141
|
init_openapi$1();
|
|
6024
6142
|
init_validator();
|
|
@@ -8372,20 +8490,7 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
8372
8490
|
return;
|
|
8373
8491
|
}
|
|
8374
8492
|
await this.assertPortsFree(this.config.httpPort, this.config.httpsPort);
|
|
8375
|
-
|
|
8376
|
-
this.lastError = `the engine did not accept the configuration: ${error instanceof Error ? error.message : String(error)}`;
|
|
8377
|
-
throw new DetailedError(this.lastError, {
|
|
8378
|
-
statusCode: 502,
|
|
8379
|
-
code: "ENGINE_UNREACHABLE"
|
|
8380
|
-
});
|
|
8381
|
-
});
|
|
8382
|
-
if (result.status >= 400) {
|
|
8383
|
-
this.lastError = engineMessage(result.body) ?? `the engine rejected the configuration (HTTP ${result.status})`;
|
|
8384
|
-
throw new DetailedError(this.lastError, {
|
|
8385
|
-
statusCode: 400,
|
|
8386
|
-
code: "PROXY_CONFIG_REJECTED"
|
|
8387
|
-
});
|
|
8388
|
-
}
|
|
8493
|
+
await this.postConfiguration(rendered);
|
|
8389
8494
|
this.rememberConfig(text);
|
|
8390
8495
|
this.rememberApplied(this.config.httpPort, this.config.httpsPort);
|
|
8391
8496
|
const swept = this.sweepUnroutedCertificates();
|
|
@@ -8431,22 +8536,34 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
8431
8536
|
}
|
|
8432
8537
|
return removed;
|
|
8433
8538
|
}
|
|
8434
|
-
/**
|
|
8435
|
-
|
|
8539
|
+
/**
|
|
8540
|
+
* Posts one rendered configuration to the engine's admin endpoint and reports a refusal.
|
|
8541
|
+
*
|
|
8542
|
+
* The three call sites used to spell this out themselves, and one of them forgot to record
|
|
8543
|
+
* `lastError` — which is what `status()` shows as the proxy's state, so a configuration the
|
|
8544
|
+
* engine refused could leave the panel reporting the proxy as healthy. Keeping both the
|
|
8545
|
+
* message and the state in one place is the point.
|
|
8546
|
+
*/
|
|
8547
|
+
async postConfiguration(rendered) {
|
|
8436
8548
|
const result = await this.request("POST", "/load", rendered).catch((error) => {
|
|
8437
|
-
|
|
8549
|
+
this.lastError = `the engine did not accept the configuration: ${error instanceof Error ? error.message : String(error)}`;
|
|
8550
|
+
throw new DetailedError(this.lastError, {
|
|
8438
8551
|
statusCode: 502,
|
|
8439
8552
|
code: "ENGINE_UNREACHABLE"
|
|
8440
8553
|
});
|
|
8441
8554
|
});
|
|
8442
8555
|
if (result.status >= 400) {
|
|
8443
|
-
|
|
8444
|
-
throw new DetailedError(
|
|
8556
|
+
this.lastError = engineMessage(result.body) ?? `the engine rejected the configuration (HTTP ${result.status})`;
|
|
8557
|
+
throw new DetailedError(this.lastError, {
|
|
8445
8558
|
statusCode: 400,
|
|
8446
8559
|
code: "PROXY_CONFIG_REJECTED"
|
|
8447
8560
|
});
|
|
8448
8561
|
}
|
|
8449
8562
|
}
|
|
8563
|
+
/** Hands a configuration to the running engine without touching the file on disk. */
|
|
8564
|
+
async loadIntoEngine(rendered) {
|
|
8565
|
+
await this.postConfiguration(rendered);
|
|
8566
|
+
}
|
|
8450
8567
|
/** Advances `previous.json` only for a configuration the engine accepted. */
|
|
8451
8568
|
rememberConfig(text) {
|
|
8452
8569
|
if (fs.existsSync(this.options.configPath)) writeFileAtomic(this.options.previousConfigPath, fs.readFileSync(this.options.configPath, "utf8"), { mode: 384 });
|
|
@@ -8511,14 +8628,7 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
8511
8628
|
});
|
|
8512
8629
|
const current = fs.existsSync(this.options.configPath) ? fs.readFileSync(this.options.configPath, "utf8") : null;
|
|
8513
8630
|
const previous = fs.readFileSync(this.options.previousConfigPath, "utf8");
|
|
8514
|
-
|
|
8515
|
-
if (result.status >= 400) {
|
|
8516
|
-
this.lastError = engineMessage(result.body) ?? `the engine rejected the configuration (HTTP ${result.status})`;
|
|
8517
|
-
throw new DetailedError(this.lastError, {
|
|
8518
|
-
statusCode: 400,
|
|
8519
|
-
code: "PROXY_CONFIG_REJECTED"
|
|
8520
|
-
});
|
|
8521
|
-
}
|
|
8631
|
+
await this.postConfiguration(JSON.parse(previous));
|
|
8522
8632
|
writeFileAtomic(this.options.configPath, previous, { mode: 384 });
|
|
8523
8633
|
if (current === null) fs.rmSync(this.options.previousConfigPath, { force: true });
|
|
8524
8634
|
else writeFileAtomic(this.options.previousConfigPath, current, { mode: 384 });
|
|
@@ -10314,7 +10424,7 @@ var init_app = __esmMin((() => {
|
|
|
10314
10424
|
init_ddns$1();
|
|
10315
10425
|
init_events();
|
|
10316
10426
|
init_health();
|
|
10317
|
-
init_logs();
|
|
10427
|
+
init_logs$1();
|
|
10318
10428
|
init_metrics();
|
|
10319
10429
|
init_notifications$1();
|
|
10320
10430
|
init_proxy();
|
|
@@ -14021,7 +14131,20 @@ async function swapUsedPercent() {
|
|
|
14021
14131
|
}
|
|
14022
14132
|
return 0;
|
|
14023
14133
|
}
|
|
14024
|
-
/**
|
|
14134
|
+
/**
|
|
14135
|
+
* Memory used, as a percentage of what the machine actually has.
|
|
14136
|
+
*
|
|
14137
|
+
* The definition matters more than the arithmetic, and it differs per platform:
|
|
14138
|
+
*
|
|
14139
|
+
* - Linux: `/proc/meminfo`'s `MemAvailable` already excludes cache, because Linux counts
|
|
14140
|
+
* page cache as reclaimable. `os.freemem()` happens to read the same value here.
|
|
14141
|
+
* - macOS: **`os.freemem()` is wrong for this.** libuv reports `host_statistics64`'s
|
|
14142
|
+
* `free_count` alone, while macOS keeps as much RAM as it can in *inactive* and
|
|
14143
|
+
* speculative* pages — file cache it drops on demand. A perfectly healthy Mac therefore
|
|
14144
|
+
* reads as 90%+ used, which is the report a user brought back from a 0.7.3 panel. Read
|
|
14145
|
+
* `vm_stat` and count what macOS itself counts as available.
|
|
14146
|
+
* - Windows: `os.freemem()` is the free physical page count, which is the ordinary reading.
|
|
14147
|
+
*/
|
|
14025
14148
|
function memoryInfo() {
|
|
14026
14149
|
try {
|
|
14027
14150
|
const info = fs.readFileSync("/proc/meminfo", "utf8");
|
|
@@ -14036,14 +14159,81 @@ function memoryInfo() {
|
|
|
14036
14159
|
};
|
|
14037
14160
|
} catch {
|
|
14038
14161
|
const total = os.totalmem();
|
|
14039
|
-
const
|
|
14162
|
+
const available = availableMemoryBytes(process.platform, os.freemem());
|
|
14040
14163
|
return {
|
|
14041
|
-
memoryUsedPercent: total > 0 ? (total -
|
|
14164
|
+
memoryUsedPercent: total > 0 && available !== null ? Math.max(0, Math.min(100, (total - available) / total * 100)) : 0,
|
|
14042
14165
|
swapUsedPercent: 0
|
|
14043
14166
|
};
|
|
14044
14167
|
}
|
|
14045
14168
|
}
|
|
14046
14169
|
/**
|
|
14170
|
+
* Which "available memory" reading a platform's fallback should use.
|
|
14171
|
+
*
|
|
14172
|
+
* Split out from `memoryInfo` because the branch it decides is otherwise unreachable off
|
|
14173
|
+
* macOS: Linux answers from `/proc/meminfo` first, so the fallback never runs there and no
|
|
14174
|
+
* Linux test can prove which reader was chosen. Taking the platform and the OS reading as
|
|
14175
|
+
* arguments makes the decision testable on any runner, which matters — using the wrong
|
|
14176
|
+
* reader here is the whole macOS bug.
|
|
14177
|
+
*/
|
|
14178
|
+
function availableMemoryBytes(platform, osFree, vmStat) {
|
|
14179
|
+
if (platform !== "darwin") return osFree;
|
|
14180
|
+
return macAvailableBytes(vmStat);
|
|
14181
|
+
}
|
|
14182
|
+
/**
|
|
14183
|
+
* What macOS counts as available: free + inactive + speculative pages.
|
|
14184
|
+
*
|
|
14185
|
+
* `vm_stat` reports pages, so the page size is needed — and it is not always 4096 (Apple
|
|
14186
|
+
* silicon uses 16384 for the physical page size, and `vm_stat` has printed a `page size of`
|
|
14187
|
+
* header since macOS 11). Both are handled: the header when it is there, `getpagesize` when
|
|
14188
|
+
* it is not.
|
|
14189
|
+
*
|
|
14190
|
+
* Returns `null` when the reading cannot be taken, so the caller reports "unknown" rather
|
|
14191
|
+
* than inventing a percentage.
|
|
14192
|
+
*/
|
|
14193
|
+
function macAvailableBytes(vmStat) {
|
|
14194
|
+
try {
|
|
14195
|
+
const text = vmStat ?? execFileSync("vm_stat", {
|
|
14196
|
+
timeout: 3e3,
|
|
14197
|
+
encoding: "utf8"
|
|
14198
|
+
});
|
|
14199
|
+
const pageSize = Number.parseInt(/page size of (\d+) bytes/.exec(text)?.[1] ?? "", 10);
|
|
14200
|
+
const pages = (label) => {
|
|
14201
|
+
const found = new RegExp(`^${label}:\\s+(\\d+)`, "m").exec(text)?.[1];
|
|
14202
|
+
return Number.parseInt(found ?? "", 10);
|
|
14203
|
+
};
|
|
14204
|
+
const size = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : getPageSize();
|
|
14205
|
+
if (!Number.isFinite(size) || size <= 0) return null;
|
|
14206
|
+
let totalPages = 0;
|
|
14207
|
+
let sawAny = false;
|
|
14208
|
+
for (const label of [
|
|
14209
|
+
"Pages free",
|
|
14210
|
+
"Pages inactive",
|
|
14211
|
+
"Pages speculative",
|
|
14212
|
+
"Pages purgeable"
|
|
14213
|
+
]) {
|
|
14214
|
+
const value = pages(label);
|
|
14215
|
+
if (Number.isFinite(value)) {
|
|
14216
|
+
totalPages += value;
|
|
14217
|
+
sawAny = true;
|
|
14218
|
+
}
|
|
14219
|
+
}
|
|
14220
|
+
return sawAny ? totalPages * size : null;
|
|
14221
|
+
} catch {
|
|
14222
|
+
return null;
|
|
14223
|
+
}
|
|
14224
|
+
}
|
|
14225
|
+
/** The OS page size, without adding a dependency for one sysconf. */
|
|
14226
|
+
function getPageSize() {
|
|
14227
|
+
try {
|
|
14228
|
+
return Number.parseInt(execFileSync("getconf", ["PAGESIZE"], {
|
|
14229
|
+
timeout: 3e3,
|
|
14230
|
+
encoding: "utf8"
|
|
14231
|
+
}).trim(), 10);
|
|
14232
|
+
} catch {
|
|
14233
|
+
return NaN;
|
|
14234
|
+
}
|
|
14235
|
+
}
|
|
14236
|
+
/**
|
|
14047
14237
|
* Best-effort CPU temperature from Linux thermal zones / hwmon. macOS and
|
|
14048
14238
|
* Windows expose no unprivileged sensor, so those platforms report null and the
|
|
14049
14239
|
* UI simply hides the reading.
|
|
@@ -14991,6 +15181,7 @@ function stateSignature(state) {
|
|
|
14991
15181
|
state.host.alerts.join(","),
|
|
14992
15182
|
state.backups.files.length,
|
|
14993
15183
|
state.backups.entries.length,
|
|
15184
|
+
`${state.ui.custom}:${state.ui.meta?.name ?? ""}:${state.ui.meta?.version ?? ""}`,
|
|
14994
15185
|
state.proxy === void 0 ? "" : [
|
|
14995
15186
|
state.proxy.config.enabled,
|
|
14996
15187
|
state.proxy.status.state,
|
|
@@ -16446,21 +16637,6 @@ function toUpFlags(args) {
|
|
|
16446
16637
|
printConfig: args.printConfig === true
|
|
16447
16638
|
};
|
|
16448
16639
|
}
|
|
16449
|
-
/** One rotation is enough for a console log. */
|
|
16450
|
-
function rotateLog(file) {
|
|
16451
|
-
try {
|
|
16452
|
-
if (fs.statSync(file).size < LOG_ROTATE_BYTES) return;
|
|
16453
|
-
fs.rmSync(`${file}.1`, { force: true });
|
|
16454
|
-
fs.renameSync(file, `${file}.1`);
|
|
16455
|
-
} catch {}
|
|
16456
|
-
}
|
|
16457
|
-
function tailLog(file, lines = 15) {
|
|
16458
|
-
try {
|
|
16459
|
-
return fs.readFileSync(file, "utf8").split("\n").slice(-lines).join("\n").trimEnd();
|
|
16460
|
-
} catch {
|
|
16461
|
-
return "";
|
|
16462
|
-
}
|
|
16463
|
-
}
|
|
16464
16640
|
async function runUp(flags, entry) {
|
|
16465
16641
|
const { runControlPlane } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
16466
16642
|
if (flags.foreground || flags.printConfig) {
|
|
@@ -16545,14 +16721,14 @@ function upCommand(entry) {
|
|
|
16545
16721
|
}
|
|
16546
16722
|
});
|
|
16547
16723
|
}
|
|
16548
|
-
var DEFAULT_PORT$1,
|
|
16724
|
+
var DEFAULT_PORT$1, upArgs;
|
|
16549
16725
|
var init_up = __esmMin((() => {
|
|
16550
16726
|
init_args();
|
|
16551
16727
|
init_io();
|
|
16728
|
+
init_daemon_log();
|
|
16552
16729
|
init_runtime();
|
|
16553
16730
|
init_version();
|
|
16554
16731
|
DEFAULT_PORT$1 = 3999;
|
|
16555
|
-
LOG_ROTATE_BYTES = 5242880;
|
|
16556
16732
|
upArgs = {
|
|
16557
16733
|
config: {
|
|
16558
16734
|
type: "string",
|
|
@@ -16698,17 +16874,107 @@ var init_down = __esmMin((() => {
|
|
|
16698
16874
|
});
|
|
16699
16875
|
}));
|
|
16700
16876
|
//#endregion
|
|
16877
|
+
//#region src/cli/server.ts
|
|
16878
|
+
var server_exports = /* @__PURE__ */ __exportAll({
|
|
16879
|
+
restartServerCommand: () => restartServerCommand,
|
|
16880
|
+
runServerAction: () => runServerAction,
|
|
16881
|
+
startCommand: () => startCommand,
|
|
16882
|
+
stopCommand: () => stopCommand
|
|
16883
|
+
});
|
|
16884
|
+
async function runServerAction(action, id, workspace) {
|
|
16885
|
+
const { clearRuntime, isProcessAlive, readRuntime, requestControl } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
16886
|
+
const runtime = readRuntime();
|
|
16887
|
+
if (runtime === null || !isProcessAlive(runtime.pid)) {
|
|
16888
|
+
if (runtime !== null) clearRuntime();
|
|
16889
|
+
fail("home-hosted is not running — start it with `home-hosted up`");
|
|
16890
|
+
}
|
|
16891
|
+
const query = workspace === void 0 || workspace.length === 0 ? "" : `?workspace=${encodeURIComponent(workspace)}`;
|
|
16892
|
+
const answer = await requestControl(runtime, `/_hh/servers/${encodeURIComponent(id)}/${action}${query}`);
|
|
16893
|
+
if (answer === null) fail(`the control panel is not answering on ${runtime.probeUrl}`);
|
|
16894
|
+
const body = answer.body ?? {};
|
|
16895
|
+
if (body.ok !== true) {
|
|
16896
|
+
const reason = body.error ?? body.message;
|
|
16897
|
+
if (reason !== void 0) fail(reason);
|
|
16898
|
+
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`);
|
|
16899
|
+
fail(`the panel answered ${answer.status}`);
|
|
16900
|
+
}
|
|
16901
|
+
const verb = action === "start" ? "started" : action === "stop" ? "stopped" : "restarted";
|
|
16902
|
+
process.stdout.write(`${green(verb)} ${workspace === void 0 ? "" : `${workspace}/`}${id}\n`);
|
|
16903
|
+
}
|
|
16904
|
+
function serverCommand(action) {
|
|
16905
|
+
return defineCommand({
|
|
16906
|
+
meta: {
|
|
16907
|
+
name: action,
|
|
16908
|
+
description: DESCRIPTION[action]
|
|
16909
|
+
},
|
|
16910
|
+
args: {
|
|
16911
|
+
id: {
|
|
16912
|
+
type: "positional",
|
|
16913
|
+
required: false,
|
|
16914
|
+
description: "the server id from the workspace's servers.config.json"
|
|
16915
|
+
},
|
|
16916
|
+
workspace: {
|
|
16917
|
+
type: "string",
|
|
16918
|
+
alias: "w",
|
|
16919
|
+
description: "the workspace to act in (default: the panel's default workspace)"
|
|
16920
|
+
}
|
|
16921
|
+
},
|
|
16922
|
+
run: async ({ args }) => {
|
|
16923
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
16924
|
+
if (id.length === 0) fail(`${action} needs a server id — see \`home-hosted status\` or the workspace's servers.config.json`);
|
|
16925
|
+
await runServerAction(action, id, typeof args.workspace === "string" ? args.workspace.trim() : void 0);
|
|
16926
|
+
}
|
|
16927
|
+
});
|
|
16928
|
+
}
|
|
16929
|
+
var DESCRIPTION, startCommand, stopCommand, restartServerCommand;
|
|
16930
|
+
var init_server = __esmMin((() => {
|
|
16931
|
+
init_io();
|
|
16932
|
+
DESCRIPTION = {
|
|
16933
|
+
start: "start one server (the panel keeps running)",
|
|
16934
|
+
stop: "stop one server (the panel keeps running)",
|
|
16935
|
+
restart: "restart one server (the panel keeps running)"
|
|
16936
|
+
};
|
|
16937
|
+
startCommand = serverCommand("start");
|
|
16938
|
+
stopCommand = serverCommand("stop");
|
|
16939
|
+
restartServerCommand = serverCommand("restart");
|
|
16940
|
+
}));
|
|
16941
|
+
//#endregion
|
|
16701
16942
|
//#region src/cli/restart.ts
|
|
16702
16943
|
var restart_exports = /* @__PURE__ */ __exportAll({ restartCommand: () => restartCommand });
|
|
16703
|
-
/**
|
|
16944
|
+
/**
|
|
16945
|
+
* `restart` restarts the panel (`down`, then `up`) — or, given a server id, just that entry.
|
|
16946
|
+
*
|
|
16947
|
+
* The id is optional on purpose. `restart` already meant the panel, and it still does when
|
|
16948
|
+
* called the way it always was; adding a positional would have been a hard error before this
|
|
16949
|
+
* ("Unexpected argument 'web'"), so nothing existing changes meaning. The per-server case is
|
|
16950
|
+
* what the UI's Restart button has always done and the shell could not.
|
|
16951
|
+
*/
|
|
16704
16952
|
function restartCommand(entry) {
|
|
16705
16953
|
return defineCommand({
|
|
16706
16954
|
meta: {
|
|
16707
16955
|
name: "restart",
|
|
16708
|
-
description: "down
|
|
16956
|
+
description: "down then up — or one server, given an id"
|
|
16957
|
+
},
|
|
16958
|
+
args: {
|
|
16959
|
+
...upArgs,
|
|
16960
|
+
id: {
|
|
16961
|
+
type: "positional",
|
|
16962
|
+
required: false,
|
|
16963
|
+
description: "restart just this server instead of the panel"
|
|
16964
|
+
},
|
|
16965
|
+
workspace: {
|
|
16966
|
+
type: "string",
|
|
16967
|
+
alias: "w",
|
|
16968
|
+
description: "the workspace to act in (default: the panel's default workspace)"
|
|
16969
|
+
}
|
|
16709
16970
|
},
|
|
16710
|
-
args: upArgs,
|
|
16711
16971
|
run: async ({ args }) => {
|
|
16972
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
16973
|
+
if (id.length > 0) {
|
|
16974
|
+
await runServerAction("restart", id, typeof args.workspace === "string" ? args.workspace.trim() : void 0);
|
|
16975
|
+
return;
|
|
16976
|
+
}
|
|
16977
|
+
if (args.workspace !== void 0) fail("restart --workspace needs a server id — `home-hosted restart <id> --workspace <id>`");
|
|
16712
16978
|
await runDown();
|
|
16713
16979
|
await runUp(toUpFlags(args), entry);
|
|
16714
16980
|
}
|
|
@@ -16716,6 +16982,8 @@ function restartCommand(entry) {
|
|
|
16716
16982
|
}
|
|
16717
16983
|
var init_restart = __esmMin((() => {
|
|
16718
16984
|
init_down();
|
|
16985
|
+
init_io();
|
|
16986
|
+
init_server();
|
|
16719
16987
|
init_up();
|
|
16720
16988
|
}));
|
|
16721
16989
|
//#endregion
|
|
@@ -16798,61 +17066,80 @@ var init_status = __esmMin((() => {
|
|
|
16798
17066
|
});
|
|
16799
17067
|
}));
|
|
16800
17068
|
//#endregion
|
|
16801
|
-
//#region src/cli/
|
|
16802
|
-
var
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
17069
|
+
//#region src/cli/logs.ts
|
|
17070
|
+
var logs_exports = /* @__PURE__ */ __exportAll({
|
|
17071
|
+
logsArgs: () => logsArgs,
|
|
17072
|
+
logsCommand: () => logsCommand,
|
|
17073
|
+
runLogs: () => runLogs
|
|
16806
17074
|
});
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
if (
|
|
16811
|
-
|
|
16812
|
-
|
|
17075
|
+
/** `all` or a negative count means the whole log; a bad number is not worth a hard failure. */
|
|
17076
|
+
function parseLines(raw) {
|
|
17077
|
+
if (raw === void 0) return DEFAULT_LINES;
|
|
17078
|
+
if (raw.trim().toLowerCase() === "all") return 0;
|
|
17079
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17080
|
+
if (!Number.isFinite(parsed)) return DEFAULT_LINES;
|
|
17081
|
+
return parsed < 0 ? 0 : parsed;
|
|
17082
|
+
}
|
|
17083
|
+
async function runLogs(input) {
|
|
17084
|
+
const { daemonLogPath } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
17085
|
+
const { readLog } = await Promise.resolve().then(() => (init_daemon_log(), daemon_log_exports));
|
|
17086
|
+
const lines = parseLines(input.lines);
|
|
17087
|
+
if (input.follow === true) {
|
|
17088
|
+
const { followLog } = await Promise.resolve().then(() => (init_daemon_log(), daemon_log_exports));
|
|
17089
|
+
await followLog(daemonLogPath, lines === 0 ? DEFAULT_LINES : lines, (chunk) => process.stdout.write(chunk));
|
|
17090
|
+
return;
|
|
16813
17091
|
}
|
|
16814
|
-
const
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
17092
|
+
const output = readLog(daemonLogPath, lines);
|
|
17093
|
+
if (input.json === true) {
|
|
17094
|
+
process.stdout.write(`${JSON.stringify({
|
|
17095
|
+
path: daemonLogPath,
|
|
17096
|
+
lines: output
|
|
17097
|
+
}, null, 2)}\n`);
|
|
17098
|
+
return;
|
|
17099
|
+
}
|
|
17100
|
+
if (output.length === 0) {
|
|
17101
|
+
process.stdout.write(`${dim(`no output yet — ${daemonLogPath}`)}\n`);
|
|
17102
|
+
return;
|
|
16823
17103
|
}
|
|
16824
|
-
process.stdout.write(`${
|
|
17104
|
+
process.stdout.write(`${bold(daemonLogPath)}\n`);
|
|
17105
|
+
for (const line of output) process.stdout.write(`${line}\n`);
|
|
16825
17106
|
}
|
|
16826
|
-
|
|
16827
|
-
|
|
17107
|
+
var logsArgs, DEFAULT_LINES, logsCommand;
|
|
17108
|
+
var init_logs = __esmMin((() => {
|
|
17109
|
+
init_io();
|
|
17110
|
+
logsArgs = {
|
|
17111
|
+
lines: {
|
|
17112
|
+
type: "string",
|
|
17113
|
+
description: "how many lines to show (default 50, `all` for everything)"
|
|
17114
|
+
},
|
|
17115
|
+
follow: {
|
|
17116
|
+
type: "boolean",
|
|
17117
|
+
description: "keep printing as the panel writes (like tail -f)"
|
|
17118
|
+
},
|
|
17119
|
+
json: {
|
|
17120
|
+
type: "boolean",
|
|
17121
|
+
description: "print machine-readable JSON"
|
|
17122
|
+
}
|
|
17123
|
+
};
|
|
17124
|
+
DEFAULT_LINES = 50;
|
|
17125
|
+
logsCommand = defineCommand({
|
|
16828
17126
|
meta: {
|
|
16829
|
-
name:
|
|
16830
|
-
description:
|
|
17127
|
+
name: "logs",
|
|
17128
|
+
description: "show the panel's own console output"
|
|
16831
17129
|
},
|
|
16832
|
-
args:
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
17130
|
+
args: logsArgs,
|
|
17131
|
+
async run({ args }) {
|
|
17132
|
+
try {
|
|
17133
|
+
await runLogs({
|
|
17134
|
+
lines: args.lines,
|
|
17135
|
+
follow: args.follow,
|
|
17136
|
+
json: args.json
|
|
17137
|
+
});
|
|
17138
|
+
} catch (error) {
|
|
17139
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
16842
17140
|
}
|
|
16843
|
-
},
|
|
16844
|
-
run: async ({ args }) => {
|
|
16845
|
-
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
16846
|
-
if (id.length === 0) fail(`${action} needs a server id — see \`home-hosted status\` or the workspace's servers.config.json`);
|
|
16847
|
-
await runServerAction(action, id, typeof args.workspace === "string" ? args.workspace.trim() : void 0);
|
|
16848
17141
|
}
|
|
16849
17142
|
});
|
|
16850
|
-
}
|
|
16851
|
-
var startCommand, stopCommand;
|
|
16852
|
-
var init_server = __esmMin((() => {
|
|
16853
|
-
init_io();
|
|
16854
|
-
startCommand = serverCommand("start");
|
|
16855
|
-
stopCommand = serverCommand("stop");
|
|
16856
17143
|
}));
|
|
16857
17144
|
//#endregion
|
|
16858
17145
|
//#region src/cli/set-password.ts
|
|
@@ -17971,8 +18258,9 @@ var HEADER = "home-hosted — a control panel for the processes on your home ser
|
|
|
17971
18258
|
var SYNOPSIS = {
|
|
17972
18259
|
"up": "home-hosted up [options]",
|
|
17973
18260
|
"down": "home-hosted down",
|
|
17974
|
-
"restart": "home-hosted restart [options]",
|
|
18261
|
+
"restart": "home-hosted restart [<id>] [options]",
|
|
17975
18262
|
"status": "home-hosted status [--json]",
|
|
18263
|
+
"logs": "home-hosted logs [--lines <n>] [--follow] [--json]",
|
|
17976
18264
|
"start": "home-hosted start <id> [--workspace <id>]",
|
|
17977
18265
|
"stop": "home-hosted stop <id> [--workspace <id>]",
|
|
17978
18266
|
"set-password": "home-hosted set-password",
|
|
@@ -17986,8 +18274,9 @@ var SYNOPSIS = {
|
|
|
17986
18274
|
var SUMMARIES = {
|
|
17987
18275
|
"up": "start it in the background (detached)",
|
|
17988
18276
|
"down": "stop it, and everything it supervises",
|
|
17989
|
-
"restart": "down
|
|
18277
|
+
"restart": "down then up — or one server, given an id",
|
|
17990
18278
|
"status": "is it running, where, and how to reach it",
|
|
18279
|
+
"logs": "show the panel's own console output",
|
|
17991
18280
|
"start": "start one server, leaving the panel up",
|
|
17992
18281
|
"stop": "stop one server, leaving the panel up",
|
|
17993
18282
|
"set-password": "set the panel password without the API",
|
|
@@ -18016,10 +18305,26 @@ var STATUS_SECTION = {
|
|
|
18016
18305
|
heading: "Options for status",
|
|
18017
18306
|
lines: [["--json", "print machine-readable JSON"]]
|
|
18018
18307
|
};
|
|
18308
|
+
var LOGS_SECTION = {
|
|
18309
|
+
heading: "Options for logs",
|
|
18310
|
+
lines: [
|
|
18311
|
+
["--lines <n>", "how many lines to show (default 50, `all` for everything)"],
|
|
18312
|
+
["--follow", "keep printing as the panel writes (like tail -f)"],
|
|
18313
|
+
["--json", "print machine-readable JSON"]
|
|
18314
|
+
]
|
|
18315
|
+
};
|
|
18019
18316
|
var SERVER_SECTION = {
|
|
18020
18317
|
heading: "Arguments for start/stop",
|
|
18021
18318
|
lines: [["<id>", "the server id from servers.config.json"]]
|
|
18022
18319
|
};
|
|
18320
|
+
var RESTART_SECTION = {
|
|
18321
|
+
heading: "Options for restart",
|
|
18322
|
+
lines: [
|
|
18323
|
+
["<id>", "restart just this server, leaving the panel up (optional)"],
|
|
18324
|
+
["-w, --workspace <id>", "the workspace to act in, with an id"],
|
|
18325
|
+
...UP_SECTION.lines
|
|
18326
|
+
]
|
|
18327
|
+
};
|
|
18023
18328
|
var SET_PASSWORD_SECTION = {
|
|
18024
18329
|
heading: "Options for set-password",
|
|
18025
18330
|
lines: [["--clear", "remove the password, which disables authentication"]]
|
|
@@ -18138,6 +18443,8 @@ var USAGE = [
|
|
|
18138
18443
|
var UP_COMMANDS = /* @__PURE__ */ new Set(["up", "restart"]);
|
|
18139
18444
|
var SECTIONS = {
|
|
18140
18445
|
"status": STATUS_SECTION,
|
|
18446
|
+
"logs": LOGS_SECTION,
|
|
18447
|
+
"restart": RESTART_SECTION,
|
|
18141
18448
|
"start": SERVER_SECTION,
|
|
18142
18449
|
"stop": SERVER_SECTION,
|
|
18143
18450
|
"set-password": SET_PASSWORD_SECTION,
|
|
@@ -18155,7 +18462,7 @@ var SECTIONS = {
|
|
|
18155
18462
|
function commandHelp(command) {
|
|
18156
18463
|
const synopsis = SYNOPSIS[command];
|
|
18157
18464
|
if (synopsis === void 0) return USAGE;
|
|
18158
|
-
const section = UP_COMMANDS.has(command) ? UP_SECTION :
|
|
18465
|
+
const section = SECTIONS[command] ?? (UP_COMMANDS.has(command) ? UP_SECTION : void 0);
|
|
18159
18466
|
const options = section === void 0 ? dim("no options") : renderSection(section);
|
|
18160
18467
|
return [
|
|
18161
18468
|
dim(HEADER),
|
|
@@ -18186,6 +18493,7 @@ var COMMANDS = {
|
|
|
18186
18493
|
"down": () => Promise.resolve().then(() => (init_down(), down_exports)).then((module) => module.downCommand),
|
|
18187
18494
|
"restart": () => Promise.resolve().then(() => (init_restart(), restart_exports)).then((module) => module.restartCommand(CLI_ENTRY)),
|
|
18188
18495
|
"status": () => Promise.resolve().then(() => (init_status(), status_exports)).then((module) => module.statusCommand),
|
|
18496
|
+
"logs": () => Promise.resolve().then(() => (init_logs(), logs_exports)).then((module) => module.logsCommand),
|
|
18189
18497
|
"start": () => Promise.resolve().then(() => (init_server(), server_exports)).then((module) => module.startCommand),
|
|
18190
18498
|
"stop": () => Promise.resolve().then(() => (init_server(), server_exports)).then((module) => module.stopCommand),
|
|
18191
18499
|
"set-password": () => Promise.resolve().then(() => (init_set_password(), set_password_exports)).then((module) => module.setPasswordCommand),
|