home-hosted 0.7.12 → 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 CHANGED
@@ -23,6 +23,7 @@ seed entry, or a path that names one.
23
23
 
24
24
  ```sh
25
25
  pnpm run up|down|restart|status # detached; `down` asks /_hh/shutdown, signals are the fallback
26
+ # `restart <id>` restarts one server via /_hh, panel stays up
26
27
  pnpm run start # up --foreground (systemd, docker, a foreground shell)
27
28
  pnpm exec tsx src/cli.ts logs # the panel's own console output (--lines, --follow, --json)
28
29
  pnpm dev # tsx-watch panel :6000 + the stock UI's Vite :6001 (proxies /api), state in .dev-state/
package/README.md CHANGED
@@ -340,10 +340,11 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
340
340
  | --- | --- |
341
341
  | `home-hosted up` | start the panel detached, and keep it alive in the background |
342
342
  | `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
343
- | `home-hosted restart` | `down`, then `up` |
343
+ | `home-hosted restart` | `down`, then `up` — or `restart <id>` to restart one server, leaving the panel up |
344
344
  | `home-hosted status` | pid, URL, health, uptime, state and log paths (`--json` for scripts) |
345
345
  | `home-hosted start <id>` | start one server in the default workspace — and anything it `dependsOn` (`--workspace <id>`) |
346
346
  | `home-hosted stop <id>` | stop one server, nothing else (`--workspace <id>`) |
347
+ | `home-hosted restart <id>` | restart one server, nothing else (`--workspace <id>`) — the same as the panel's per-server Restart |
347
348
  | `home-hosted logs` | read the panel's own console output — the file `up` redirects it into (`--lines`, `--follow`, `--json`) |
348
349
  | `home-hosted set-password` | set the panel password without opening a browser |
349
350
  | `home-hosted set-token` | set the API token scripts and agents use (`--generate`, `--clear`) |
@@ -359,12 +360,13 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
359
360
  `home-hosted <command> --help` prints what that command takes.
360
361
 
361
362
  ```text
362
- up, restart -c/--config -p/--port --host --open --no-autostart --foreground --print-config
363
+ up -c/--config -p/--port --host --open --no-autostart --foreground --print-config
363
364
 
364
365
  down (no flags)
365
366
  status --json
366
367
  logs --lines <n> --follow --json
367
368
  start, stop <id> [-w/--workspace <id>] (both need the panel up)
369
+ restart [<id>] [-w/--workspace <id>] (no id: the panel; with one: that server)
368
370
  init --dir --name --pm --no-install -y/--yes
369
371
  set-password --clear
370
372
  set-token --generate --clear
@@ -377,7 +379,8 @@ every command --home <dir> --project <dir> (or $HHOSTED_HOME, $HHOSTED
377
379
  env vars HHOSTED_PASSWORD, HHOSTED_MIGRATE=allow, HHOSTED_TOKEN, GITHUB_TOKEN or GH_TOKEN
378
380
  ```
379
381
 
380
- `up` and `restart` share the same flags: `restart` is `down`, then `up` with exactly what it was given.
382
+ `up` and `restart` share the same flags: bare `restart` is `down`, then `up` with exactly what it was
383
+ given. Given a server id — `restart web` — it restarts only that entry, and the panel keeps running.
381
384
  `-c/--config` is the **default workspace's** servers file (`<state>/.hh/default/servers.config.json`),
382
385
  for a launcher that pins one; a workspace picked in the UI keeps its own. `start`/`stop` omit
383
386
  `--workspace` to act in the panel's default workspace.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ 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
8
  import { Buffer as Buffer$1 } from "node:buffer";
9
9
  import os from "node:os";
10
10
  import { describeRoute, openAPIRouteHandler, resolver, validator } from "hono-openapi";
@@ -3299,6 +3299,18 @@ function createControlRoute(deps) {
3299
3299
  assertLocalCall(c, "start or stop a server");
3300
3300
  const result = await requireWorkspace(deps.panel.requireWorkspace.bind(deps.panel), c.req.valid("query").workspace).supervisor.stop(c.req.param("id"));
3301
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));
3302
3314
  });
3303
3315
  }
3304
3316
  var init_control = __esmMin((() => {
@@ -14119,7 +14131,20 @@ async function swapUsedPercent() {
14119
14131
  }
14120
14132
  return 0;
14121
14133
  }
14122
- /** `/proc/meminfo` counts cache as available, which `os.freemem()` does not. */
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
+ */
14123
14148
  function memoryInfo() {
14124
14149
  try {
14125
14150
  const info = fs.readFileSync("/proc/meminfo", "utf8");
@@ -14134,14 +14159,81 @@ function memoryInfo() {
14134
14159
  };
14135
14160
  } catch {
14136
14161
  const total = os.totalmem();
14137
- const free = os.freemem();
14162
+ const available = availableMemoryBytes(process.platform, os.freemem());
14138
14163
  return {
14139
- memoryUsedPercent: total > 0 ? (total - free) / total * 100 : 0,
14164
+ memoryUsedPercent: total > 0 && available !== null ? Math.max(0, Math.min(100, (total - available) / total * 100)) : 0,
14140
14165
  swapUsedPercent: 0
14141
14166
  };
14142
14167
  }
14143
14168
  }
14144
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
+ /**
14145
14237
  * Best-effort CPU temperature from Linux thermal zones / hwmon. macOS and
14146
14238
  * Windows expose no unprivileged sensor, so those platforms report null and the
14147
14239
  * UI simply hides the reading.
@@ -16782,17 +16874,107 @@ var init_down = __esmMin((() => {
16782
16874
  });
16783
16875
  }));
16784
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
16785
16942
  //#region src/cli/restart.ts
16786
16943
  var restart_exports = /* @__PURE__ */ __exportAll({ restartCommand: () => restartCommand });
16787
- /** `restart` is `down`, then `up` with exactly the flags it was given. */
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
+ */
16788
16952
  function restartCommand(entry) {
16789
16953
  return defineCommand({
16790
16954
  meta: {
16791
16955
  name: "restart",
16792
- description: "down, then up"
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
+ }
16793
16970
  },
16794
- args: upArgs,
16795
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>`");
16796
16978
  await runDown();
16797
16979
  await runUp(toUpFlags(args), entry);
16798
16980
  }
@@ -16800,6 +16982,8 @@ function restartCommand(entry) {
16800
16982
  }
16801
16983
  var init_restart = __esmMin((() => {
16802
16984
  init_down();
16985
+ init_io();
16986
+ init_server();
16803
16987
  init_up();
16804
16988
  }));
16805
16989
  //#endregion
@@ -16958,63 +17142,6 @@ var init_logs = __esmMin((() => {
16958
17142
  });
16959
17143
  }));
16960
17144
  //#endregion
16961
- //#region src/cli/server.ts
16962
- var server_exports = /* @__PURE__ */ __exportAll({
16963
- runServerAction: () => runServerAction,
16964
- startCommand: () => startCommand,
16965
- stopCommand: () => stopCommand
16966
- });
16967
- async function runServerAction(action, id, workspace) {
16968
- const { clearRuntime, isProcessAlive, readRuntime, requestControl } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
16969
- const runtime = readRuntime();
16970
- if (runtime === null || !isProcessAlive(runtime.pid)) {
16971
- if (runtime !== null) clearRuntime();
16972
- fail("home-hosted is not running — start it with `home-hosted up`");
16973
- }
16974
- const query = workspace === void 0 || workspace.length === 0 ? "" : `?workspace=${encodeURIComponent(workspace)}`;
16975
- const answer = await requestControl(runtime, `/_hh/servers/${encodeURIComponent(id)}/${action}${query}`);
16976
- if (answer === null) fail(`the control panel is not answering on ${runtime.probeUrl}`);
16977
- const body = answer.body ?? {};
16978
- if (body.ok !== true) {
16979
- const reason = body.error ?? body.message;
16980
- if (reason !== void 0) fail(reason);
16981
- 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`);
16982
- fail(`the panel answered ${answer.status}`);
16983
- }
16984
- process.stdout.write(`${green(action === "start" ? "started" : "stopped")} ${workspace === void 0 ? "" : `${workspace}/`}${id}\n`);
16985
- }
16986
- function serverCommand(action) {
16987
- return defineCommand({
16988
- meta: {
16989
- name: action,
16990
- description: action === "start" ? "start one server (the panel keeps running)" : "stop one server (the panel keeps running)"
16991
- },
16992
- args: {
16993
- id: {
16994
- type: "positional",
16995
- required: false,
16996
- description: "the server id from the workspace's servers.config.json"
16997
- },
16998
- workspace: {
16999
- type: "string",
17000
- alias: "w",
17001
- description: "the workspace to act in (default: the panel's default workspace)"
17002
- }
17003
- },
17004
- run: async ({ args }) => {
17005
- const id = typeof args.id === "string" ? args.id.trim() : "";
17006
- if (id.length === 0) fail(`${action} needs a server id — see \`home-hosted status\` or the workspace's servers.config.json`);
17007
- await runServerAction(action, id, typeof args.workspace === "string" ? args.workspace.trim() : void 0);
17008
- }
17009
- });
17010
- }
17011
- var startCommand, stopCommand;
17012
- var init_server = __esmMin((() => {
17013
- init_io();
17014
- startCommand = serverCommand("start");
17015
- stopCommand = serverCommand("stop");
17016
- }));
17017
- //#endregion
17018
17145
  //#region src/cli/set-password.ts
17019
17146
  var set_password_exports = /* @__PURE__ */ __exportAll({
17020
17147
  runSetPassword: () => runSetPassword,
@@ -18131,7 +18258,7 @@ var HEADER = "home-hosted — a control panel for the processes on your home ser
18131
18258
  var SYNOPSIS = {
18132
18259
  "up": "home-hosted up [options]",
18133
18260
  "down": "home-hosted down",
18134
- "restart": "home-hosted restart [options]",
18261
+ "restart": "home-hosted restart [<id>] [options]",
18135
18262
  "status": "home-hosted status [--json]",
18136
18263
  "logs": "home-hosted logs [--lines <n>] [--follow] [--json]",
18137
18264
  "start": "home-hosted start <id> [--workspace <id>]",
@@ -18147,7 +18274,7 @@ var SYNOPSIS = {
18147
18274
  var SUMMARIES = {
18148
18275
  "up": "start it in the background (detached)",
18149
18276
  "down": "stop it, and everything it supervises",
18150
- "restart": "down, then up",
18277
+ "restart": "down then up — or one server, given an id",
18151
18278
  "status": "is it running, where, and how to reach it",
18152
18279
  "logs": "show the panel's own console output",
18153
18280
  "start": "start one server, leaving the panel up",
@@ -18190,6 +18317,14 @@ var SERVER_SECTION = {
18190
18317
  heading: "Arguments for start/stop",
18191
18318
  lines: [["<id>", "the server id from servers.config.json"]]
18192
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
+ };
18193
18328
  var SET_PASSWORD_SECTION = {
18194
18329
  heading: "Options for set-password",
18195
18330
  lines: [["--clear", "remove the password, which disables authentication"]]
@@ -18309,6 +18444,7 @@ var UP_COMMANDS = /* @__PURE__ */ new Set(["up", "restart"]);
18309
18444
  var SECTIONS = {
18310
18445
  "status": STATUS_SECTION,
18311
18446
  "logs": LOGS_SECTION,
18447
+ "restart": RESTART_SECTION,
18312
18448
  "start": SERVER_SECTION,
18313
18449
  "stop": SERVER_SECTION,
18314
18450
  "set-password": SET_PASSWORD_SECTION,
@@ -18326,7 +18462,7 @@ var SECTIONS = {
18326
18462
  function commandHelp(command) {
18327
18463
  const synopsis = SYNOPSIS[command];
18328
18464
  if (synopsis === void 0) return USAGE;
18329
- const section = UP_COMMANDS.has(command) ? UP_SECTION : SECTIONS[command];
18465
+ const section = SECTIONS[command] ?? (UP_COMMANDS.has(command) ? UP_SECTION : void 0);
18330
18466
  const options = section === void 0 ? dim("no options") : renderSection(section);
18331
18467
  return [
18332
18468
  dim(HEADER),