@higherdev/cli 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/host.js +119 -0
- package/dist/index.js +27 -9
- package/dist/out.js +2 -2
- package/dist/tui/Dashboard.js +4 -3
- package/dist/tui/Panels.js +4 -2
- package/dist/tui/agent-rows.js +14 -2
- package/dist/tui/data.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
49
49
|
| `hd env ls` | List workspace environment variable names |
|
|
50
50
|
| `hd env set NAME=VALUE [NAME=VALUE...]` | Set workspace environment variables |
|
|
51
51
|
| `hd env rm NAME` | Remove a workspace environment variable |
|
|
52
|
+
| `hd host roll [--host HOST] [--address ADDR] [--checkout DIR]` | Pull, install, and restart a runner over SSH, then watch drain |
|
|
52
53
|
| `hd host env ls` | List environment variable names on the workspace's default host |
|
|
53
54
|
| `hd host env set NAME=VALUE [NAME=VALUE...]` | Set host-only environment variables |
|
|
54
55
|
| `hd host env rm NAME` | Remove a host-only environment variable |
|
|
@@ -64,9 +65,15 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
64
65
|
| `hd upgrade --host HOST [--checkout DIR] [options]` | Refresh this host configuration |
|
|
65
66
|
|
|
66
67
|
`hd init` uses `~/hdx` as its checkout by default. Pass `--checkout DIR` when the runner source is
|
|
67
|
-
elsewhere.
|
|
68
|
+
elsewhere. Pass `--address HOST` to store `RUNNER_SSH_ADDRESS` so `hd host roll` can SSH to that
|
|
69
|
+
host. If the runner environment or service unit already exists, inspect the paths it prints and
|
|
68
70
|
pass `--force` only when replacing that host configuration is intentional.
|
|
69
71
|
|
|
72
|
+
`hd host roll` SSHes to the host address (`--address`, or `RUNNER_SSH_ADDRESS` from the host
|
|
73
|
+
heartbeat), pulls the checkout, installs, and restarts the systemd unit. The runner drains live
|
|
74
|
+
runs for up to 15 minutes (`RUNNER_DRAIN_TIMEOUT_MS`) before force-closing leftovers. The systemd
|
|
75
|
+
unit `TimeoutStopSec` is one minute above that drain timeout.
|
|
76
|
+
|
|
70
77
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
71
78
|
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` with push
|
|
72
79
|
access for user-owned repositories or admin for organization-owned repositories unless `--runner-user USER`
|
package/dist/host.js
CHANGED
|
@@ -53,6 +53,35 @@ export function launchdPlist(opts) {
|
|
|
53
53
|
</plist>
|
|
54
54
|
`;
|
|
55
55
|
}
|
|
56
|
+
export const DEFAULT_DRAIN_TIMEOUT_MS = 15 * 60_000;
|
|
57
|
+
export const HOST_ROLL_USAGE = "usage: hd host roll [--host HOST] [--address ADDR] [--checkout DIR]";
|
|
58
|
+
export function systemdStopTimeoutSec(drainTimeoutMs = DEFAULT_DRAIN_TIMEOUT_MS) {
|
|
59
|
+
return Math.ceil(drainTimeoutMs / 1_000) + 60;
|
|
60
|
+
}
|
|
61
|
+
export function formatDrainStatus(live, until, now = Date.now()) {
|
|
62
|
+
const remaining = Date.parse(until) - now;
|
|
63
|
+
const minutes = Number.isFinite(remaining) ? Math.max(0, Math.ceil(remaining / 60_000)) : 0;
|
|
64
|
+
const runs = live === 1 ? "1 run" : `${live} runs`;
|
|
65
|
+
return `draining: ${runs}, up to ${minutes}m`;
|
|
66
|
+
}
|
|
67
|
+
export function hostDrainFromStats(stats) {
|
|
68
|
+
if (!stats || typeof stats !== "object" || Array.isArray(stats))
|
|
69
|
+
return null;
|
|
70
|
+
const draining = stats.draining;
|
|
71
|
+
if (!draining || typeof draining !== "object" || Array.isArray(draining))
|
|
72
|
+
return null;
|
|
73
|
+
const live = Number(draining.live);
|
|
74
|
+
const until = draining.until;
|
|
75
|
+
if (!Number.isFinite(live) || live < 0 || typeof until !== "string")
|
|
76
|
+
return null;
|
|
77
|
+
return { live, until };
|
|
78
|
+
}
|
|
79
|
+
export function sshAddressFromStats(stats) {
|
|
80
|
+
if (!stats || typeof stats !== "object" || Array.isArray(stats))
|
|
81
|
+
return null;
|
|
82
|
+
const value = stats.ssh_address;
|
|
83
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
84
|
+
}
|
|
56
85
|
export function systemdUnit(opts) {
|
|
57
86
|
return `[Unit]
|
|
58
87
|
Description=HDX runner
|
|
@@ -65,6 +94,7 @@ WorkingDirectory=${opts.workDir}
|
|
|
65
94
|
ExecStart=${opts.nodePath} --experimental-strip-types ${opts.runnerPath}
|
|
66
95
|
Restart=always
|
|
67
96
|
RestartSec=5
|
|
97
|
+
TimeoutStopSec=${systemdStopTimeoutSec(opts.drainTimeoutMs)}
|
|
68
98
|
|
|
69
99
|
[Install]
|
|
70
100
|
WantedBy=default.target
|
|
@@ -85,6 +115,8 @@ export function parseHostFlags(argv) {
|
|
|
85
115
|
flags.host = next();
|
|
86
116
|
else if (arg === "--checkout")
|
|
87
117
|
flags.checkout = next();
|
|
118
|
+
else if (arg === "--address")
|
|
119
|
+
flags.address = next();
|
|
88
120
|
else if (arg === "--force")
|
|
89
121
|
flags.force = true;
|
|
90
122
|
else if (arg === "--env-out")
|
|
@@ -104,6 +136,45 @@ export function parseHostFlags(argv) {
|
|
|
104
136
|
}
|
|
105
137
|
return flags;
|
|
106
138
|
}
|
|
139
|
+
export function parseHostRollFlags(argv) {
|
|
140
|
+
const flags = {};
|
|
141
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
142
|
+
const arg = argv[i];
|
|
143
|
+
const next = () => {
|
|
144
|
+
const value = argv[i + 1];
|
|
145
|
+
if (!value || value.startsWith("--"))
|
|
146
|
+
throw new Error(`Missing value for ${arg}`);
|
|
147
|
+
i += 1;
|
|
148
|
+
return value;
|
|
149
|
+
};
|
|
150
|
+
if (arg === "--host")
|
|
151
|
+
flags.host = next();
|
|
152
|
+
else if (arg === "--address")
|
|
153
|
+
flags.address = next();
|
|
154
|
+
else if (arg === "--checkout")
|
|
155
|
+
flags.checkout = next();
|
|
156
|
+
else if (!arg.startsWith("-") && !flags.host)
|
|
157
|
+
flags.host = arg;
|
|
158
|
+
else
|
|
159
|
+
throw new Error(arg.startsWith("-") ? `Unknown option: ${arg}` : HOST_ROLL_USAGE);
|
|
160
|
+
}
|
|
161
|
+
return flags;
|
|
162
|
+
}
|
|
163
|
+
function shellQuote(value) {
|
|
164
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
165
|
+
}
|
|
166
|
+
export function hostRollCommand(checkout) {
|
|
167
|
+
return [
|
|
168
|
+
"set -e",
|
|
169
|
+
`cd ${shellQuote(checkout)}`,
|
|
170
|
+
"git pull --ff-only",
|
|
171
|
+
"pnpm install",
|
|
172
|
+
"systemctl --user restart hdx-runner.service",
|
|
173
|
+
].join(" && ");
|
|
174
|
+
}
|
|
175
|
+
async function defaultSsh(address, command) {
|
|
176
|
+
return execFileAsync("ssh", ["-o", "BatchMode=yes", address, "bash", "-lc", command], { encoding: "utf8" });
|
|
177
|
+
}
|
|
107
178
|
async function exists(file) {
|
|
108
179
|
try {
|
|
109
180
|
await access(file);
|
|
@@ -159,6 +230,9 @@ export async function initHost(flags, env = process.env) {
|
|
|
159
230
|
RUNNER_HOST: host,
|
|
160
231
|
WORKTREE_ROOT: worktreeRoot,
|
|
161
232
|
GH_TOKEN: env.GH_TOKEN || existing.GH_TOKEN || "",
|
|
233
|
+
...(flags.address || env.RUNNER_SSH_ADDRESS || existing.RUNNER_SSH_ADDRESS
|
|
234
|
+
? { RUNNER_SSH_ADDRESS: flags.address || env.RUNNER_SSH_ADDRESS || existing.RUNNER_SSH_ADDRESS }
|
|
235
|
+
: {}),
|
|
162
236
|
};
|
|
163
237
|
await mkdir(path.dirname(envFile), { recursive: true });
|
|
164
238
|
await writeFile(envFile, formatEnvFile(merged), { encoding: "utf8", mode: 0o600 });
|
|
@@ -198,3 +272,48 @@ export async function initHost(flags, env = process.env) {
|
|
|
198
272
|
lines.push("Start the runner service to register this host.");
|
|
199
273
|
return lines.join("\n");
|
|
200
274
|
}
|
|
275
|
+
const ROLL_WAIT_MS = DEFAULT_DRAIN_TIMEOUT_MS + 2 * 60_000;
|
|
276
|
+
export async function hostRoll(flags, deps = {}) {
|
|
277
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
278
|
+
const now = deps.now ?? Date.now;
|
|
279
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
280
|
+
const ssh = deps.ssh ?? defaultSsh;
|
|
281
|
+
const loadStatus = deps.getStatus ?? (await import("./api.js")).getStatus;
|
|
282
|
+
const snapshot = await loadStatus();
|
|
283
|
+
const host = flags.host ?? snapshot.workspace.default_host;
|
|
284
|
+
const address = flags.address ?? snapshot.host?.ssh_address ?? null;
|
|
285
|
+
if (!address) {
|
|
286
|
+
throw new Error(`hd host roll needs an SSH address for ${host}. Pass --address or set RUNNER_SSH_ADDRESS on the host.`);
|
|
287
|
+
}
|
|
288
|
+
const checkout = expandHome(flags.checkout ?? "~/hdx", homedir());
|
|
289
|
+
const command = hostRollCommand(checkout);
|
|
290
|
+
const lines = [`rolling ${host} at ${address}`];
|
|
291
|
+
log(lines[0]);
|
|
292
|
+
await ssh(address, command);
|
|
293
|
+
const restarted = `restarted ${host}; waiting for drain`;
|
|
294
|
+
lines.push(restarted);
|
|
295
|
+
log(restarted);
|
|
296
|
+
const before = snapshot.host?.last_seen_at ?? null;
|
|
297
|
+
const deadline = now() + (deps.waitMs ?? ROLL_WAIT_MS);
|
|
298
|
+
let lastDrain = "";
|
|
299
|
+
while (now() < deadline) {
|
|
300
|
+
const status = await loadStatus();
|
|
301
|
+
const drain = status.host?.draining ?? null;
|
|
302
|
+
if (drain) {
|
|
303
|
+
const line = formatDrainStatus(drain.live, drain.until, now());
|
|
304
|
+
if (line !== lastDrain) {
|
|
305
|
+
lastDrain = line;
|
|
306
|
+
lines.push(line);
|
|
307
|
+
log(line);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (status.host?.last_seen_at && status.host.last_seen_at !== before && status.host.runner_version) {
|
|
311
|
+
const line = `${host} runner ${status.host.runner_version}`;
|
|
312
|
+
lines.push(line);
|
|
313
|
+
log(line);
|
|
314
|
+
return lines;
|
|
315
|
+
}
|
|
316
|
+
await sleep(2_000);
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`timed out waiting for ${host} to finish draining`);
|
|
319
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { approveEpic, answerDecision, cancelTicket, createEpic, deleteAgent, deleteEpic, getStatus, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
5
|
-
import { initHost, parseHostFlags } from "./host.js";
|
|
5
|
+
import { formatDrainStatus, hostRoll, initHost, parseHostFlags, parseHostRollFlags } from "./host.js";
|
|
6
6
|
import { login, parseLoginFlags } from "./login.js";
|
|
7
7
|
import { loadConfig } from "./config.js";
|
|
8
8
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
@@ -63,6 +63,9 @@ async function cmdStatus() {
|
|
|
63
63
|
const data = await getStatus();
|
|
64
64
|
const state = data.workspace.paused ? c.yellow("off") : c.green("on");
|
|
65
65
|
console.log(`${c.bold(data.workspace.name)} ${c.dim(data.workspace.repo)} ${state}\n`);
|
|
66
|
+
if (data.host?.draining) {
|
|
67
|
+
console.log(`${c.yellow(formatDrainStatus(data.host.draining.live, data.host.draining.until))}\n`);
|
|
68
|
+
}
|
|
66
69
|
const waiting = [...new Set(data.tickets.map((ticket) => ticket.stuck_reason
|
|
67
70
|
?.match(/^Waiting on (\w+) until (\d{1,2}:\d{2})\.$/)).filter(Boolean)
|
|
68
71
|
.map((match) => `${match?.[1]} waiting until ${match?.[2]}`))];
|
|
@@ -423,12 +426,23 @@ function githubSecretMissing(error) {
|
|
|
423
426
|
return /404|not found|does not exist/i.test(message);
|
|
424
427
|
}
|
|
425
428
|
export async function cmdEnv(argv, deps = {}) {
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
+
const parsed = flags(argv);
|
|
430
|
+
const [rawAction, ...args] = parsed.rest;
|
|
431
|
+
const action = !rawAction && parsed.bools.has("vercel") ? "ls" : rawAction;
|
|
432
|
+
if (action === "ls" && !args.length && !Object.keys(parsed.opts).length
|
|
433
|
+
&& [...parsed.bools].every((name) => name === "vercel")) {
|
|
434
|
+
const showVercel = parsed.bools.has("vercel");
|
|
435
|
+
const [{ env }, detail] = await Promise.all([listWorkspaceEnv(), showVercel ? getWorkspace() : null]);
|
|
429
436
|
if (!env.length)
|
|
430
437
|
return console.log(c.dim("No workspace environment variables."));
|
|
431
|
-
|
|
438
|
+
if (!showVercel)
|
|
439
|
+
console.log(table(["NAME", "UPDATED"], env.map((row) => [row.name, row.updated_at])));
|
|
440
|
+
else {
|
|
441
|
+
const production = new Set(detail?.workspace.settings?.vercel?.production_env ?? []);
|
|
442
|
+
console.log(table(["NAME", "UPDATED", "VERCEL"], env.map((row) => [
|
|
443
|
+
row.name, row.updated_at, production.has(row.name) ? "yes" : "no",
|
|
444
|
+
])));
|
|
445
|
+
}
|
|
432
446
|
return;
|
|
433
447
|
}
|
|
434
448
|
if (action === "set" && args.length) {
|
|
@@ -471,11 +485,15 @@ export async function cmdEnv(argv, deps = {}) {
|
|
|
471
485
|
console.log(`synced removal of ${name} to ${repo}`);
|
|
472
486
|
return;
|
|
473
487
|
}
|
|
474
|
-
fail("usage: hd env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME");
|
|
488
|
+
fail("usage: hd env ls [--vercel] | set NAME=VALUE [NAME=VALUE...] | rm NAME");
|
|
475
489
|
}
|
|
476
|
-
async function cmdHost(argv) {
|
|
490
|
+
async function cmdHost(argv, deps = {}) {
|
|
477
491
|
const [scope, action, ...args] = argv;
|
|
478
|
-
|
|
492
|
+
if (scope === "roll") {
|
|
493
|
+
await hostRoll(parseHostRollFlags([action, ...args].filter((value) => Boolean(value))), deps);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const usage = "usage: hd host roll [--host HOST] [--address ADDR] [--checkout DIR] | env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME";
|
|
479
497
|
if (scope !== "env")
|
|
480
498
|
fail(usage);
|
|
481
499
|
const host = (await getStatus()).workspace.default_host;
|
|
@@ -573,7 +591,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
573
591
|
return;
|
|
574
592
|
}
|
|
575
593
|
if (cmd === "host") {
|
|
576
|
-
await cmdHost(rest);
|
|
594
|
+
await cmdHost(rest, deps);
|
|
577
595
|
return;
|
|
578
596
|
}
|
|
579
597
|
if (cmd === "pause" || cmd === "off") {
|
package/dist/out.js
CHANGED
|
@@ -70,8 +70,8 @@ export function usage() {
|
|
|
70
70
|
` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
|
|
71
71
|
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
72
|
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
73
|
-
` ${c.blue("hd env ls | set | rm")}
|
|
74
|
-
` ${c.blue("hd host env ls | set | rm")}
|
|
73
|
+
` ${c.blue("hd env ls [--vercel] | set | rm")} workspace environment`,
|
|
74
|
+
` ${c.blue("hd host roll | env ls | set | rm")} roll a host or edit host env`,
|
|
75
75
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
76
76
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
77
77
|
` ${c.blue("hd inbox [--all] [--limit N] [--json]")} inbox messages`,
|
package/dist/tui/Dashboard.js
CHANGED
|
@@ -117,10 +117,11 @@ export function AgentsColumn({ board, width, rows, }) {
|
|
|
117
117
|
]
|
|
118
118
|
: []),
|
|
119
119
|
...shown.map((row) => {
|
|
120
|
-
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited"
|
|
121
|
-
|
|
120
|
+
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited" || row.state === "draining"
|
|
121
|
+
? "warning" : "muted";
|
|
122
|
+
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, row.state === "draining" ? null : (_jsx(Text, { color: UI.dim, children: row.run
|
|
122
123
|
? ` · ${row.ticket?.key ?? row.run.kind} ${elapsed(row.run.started_at ?? row.run.created_at)}`
|
|
123
|
-
: ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` })] })] }, row.key));
|
|
124
|
+
: ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` }))] })] }, row.key));
|
|
124
125
|
}),
|
|
125
126
|
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
|
126
127
|
] }));
|
package/dist/tui/Panels.js
CHANGED
|
@@ -28,8 +28,10 @@ export function AgentsPanel({ board, width = 80, rows = 12, }) {
|
|
|
28
28
|
]
|
|
29
29
|
: []),
|
|
30
30
|
...shown.map((row) => {
|
|
31
|
-
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited"
|
|
32
|
-
|
|
31
|
+
const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited" || row.state === "draining"
|
|
32
|
+
? "warning" : "muted";
|
|
33
|
+
const state = row.state === "draining" ? ""
|
|
34
|
+
: row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state;
|
|
33
35
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Text, { color: UI.text, children: pad(truncate(row.name, 13), 14) }), _jsx(Text, { color: UI.dim, children: pad(row.agent?.role ?? row.run?.kind ?? "", 13) }), _jsx(Text, { color: UI.dim, children: pad(truncate(row.agent?.model ?? "", 23), 24) }), _jsx(Text, { color: UI.text, children: pad(state, row.limitedUntil ? 22 : 9) }), _jsxs(Text, { color: UI.dim, children: [row.ticket ? `${row.ticket.key} ` : "", row.run ? elapsed(row.run.started_at ?? row.run.created_at) : ""] })] }, row.key));
|
|
34
36
|
}),
|
|
35
37
|
_jsx(More, { count: displayRows.length - shown.length }, "more"),
|
package/dist/tui/agent-rows.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatDrainStatus } from "../host.js";
|
|
1
2
|
const LIMITED_UNTIL = /^(?:Waiting on )?(\w+) (?:limited )?until (\d{1,2}:\d{2})\.?$/;
|
|
2
3
|
export function limitedUntilByProvider(tickets) {
|
|
3
4
|
const limited = new Map();
|
|
@@ -22,7 +23,18 @@ function agentForRun(board, run) {
|
|
|
22
23
|
return board.agents.find((agent) => agent.enabled && agent.role === role && agent.provider === run.provider) ?? null;
|
|
23
24
|
}
|
|
24
25
|
/** Expand live invocations into rows, then append enabled agents with no live run. */
|
|
25
|
-
export function agentDisplayRows(board) {
|
|
26
|
+
export function agentDisplayRows(board, now = Date.now()) {
|
|
27
|
+
const drain = board.hostDrain
|
|
28
|
+
? [{
|
|
29
|
+
key: "drain",
|
|
30
|
+
name: formatDrainStatus(board.hostDrain.live, board.hostDrain.until, now),
|
|
31
|
+
agent: null,
|
|
32
|
+
run: null,
|
|
33
|
+
ticket: null,
|
|
34
|
+
state: "draining",
|
|
35
|
+
limitedUntil: null,
|
|
36
|
+
}]
|
|
37
|
+
: [];
|
|
26
38
|
const activeAgents = new Set();
|
|
27
39
|
const live = board.runs
|
|
28
40
|
.filter((run) => run.status === "running" || run.status === "queued")
|
|
@@ -71,5 +83,5 @@ export function agentDisplayRows(board) {
|
|
|
71
83
|
limitedUntil: until ?? null,
|
|
72
84
|
};
|
|
73
85
|
});
|
|
74
|
-
return [...live, ...idle];
|
|
86
|
+
return [...drain, ...live, ...idle];
|
|
75
87
|
}
|
package/dist/tui/data.js
CHANGED
|
@@ -80,6 +80,7 @@ export async function loadSnapshot(config = loadConfig(), options = {}) {
|
|
|
80
80
|
earlierHasMore: earlier.length > earlierLimit,
|
|
81
81
|
availability,
|
|
82
82
|
runs: status.recent_runs ?? status.live_runs,
|
|
83
|
+
hostDrain: status.host?.draining ?? null,
|
|
83
84
|
},
|
|
84
85
|
feed: feedData.entries,
|
|
85
86
|
};
|