@zswarm/core 0.2.2 → 0.2.3
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/assets/host-install.sh +36 -2
- package/assets/host-report.sh +15 -1
- package/dist/harness.d.ts +10 -0
- package/dist/harness.js +33 -1
- package/dist/host/cli.js +50 -4
- package/dist/host/crew.d.ts +2 -0
- package/dist/host/crew.js +13 -0
- package/dist/host/host.js +13 -0
- package/dist/host/relay.d.ts +63 -1
- package/dist/host/relay.js +239 -2
- package/dist/host/services.d.ts +9 -2
- package/dist/host/services.js +105 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ops/delivery.d.ts +51 -2
- package/dist/ops/delivery.js +162 -31
- package/dist/ops/dispatch.js +83 -10
- package/dist/schema.js +44 -3
- package/dist/zellij/client.d.ts +1 -0
- package/dist/zellij/client.js +16 -0
- package/dist/zellij/session.d.ts +11 -0
- package/dist/zellij/session.js +35 -3
- package/package.json +1 -1
package/assets/host-install.sh
CHANGED
|
@@ -24,6 +24,8 @@ while [ $# -gt 0 ]; do
|
|
|
24
24
|
*) echo "host-install: unknown argument $1" >&2; exit 2 ;;
|
|
25
25
|
esac
|
|
26
26
|
done
|
|
27
|
+
case "$PREFIX" in /*) ;; *) PREFIX="$(pwd)/$PREFIX" ;; esac
|
|
28
|
+
case "$BIN" in /*) ;; *) BIN="$(pwd)/$BIN" ;; esac
|
|
27
29
|
|
|
28
30
|
step() { printf '%-10s %s\n' "$1" "$2"; }
|
|
29
31
|
fetch() { curl -fsSL --retry 3 -o "$2" "$1"; }
|
|
@@ -95,8 +97,40 @@ if [ "$ZSWARM_VERSION" = latest ] || [ "$have" != "$ZSWARM_VERSION" ]; then
|
|
|
95
97
|
else
|
|
96
98
|
step zswarm "present $have"
|
|
97
99
|
fi
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
# The npm bin is `#!/usr/bin/env node`, which sshd, sudo, launchd and systemd
|
|
101
|
+
# cannot run when no node is on PATH. Write a small POSIX sh launcher for each
|
|
102
|
+
# command that execs the Node.js this script installed by absolute path.
|
|
103
|
+
resolve() { # $1 path (possibly through symlinks) -> absolute target path
|
|
104
|
+
target="$1"
|
|
105
|
+
while [ -L "$target" ]; do
|
|
106
|
+
link="$(readlink "$target")"
|
|
107
|
+
case "$link" in
|
|
108
|
+
/*) target="$link" ;;
|
|
109
|
+
*) target="$(dirname "$target")/$link" ;;
|
|
110
|
+
esac
|
|
111
|
+
done
|
|
112
|
+
# npm links with a relative target, so fold away any `..` segments for a
|
|
113
|
+
# clean absolute path in the launcher. Logical pwd keeps the prefix as given
|
|
114
|
+
# (on macOS /var is a symlink to /private/var; -P would mix the two).
|
|
115
|
+
dir="$(cd "$(dirname "$target")" 2>/dev/null && pwd)" || dir="$(dirname "$target")"
|
|
116
|
+
printf '%s/%s\n' "$dir" "$(basename "$target")"
|
|
117
|
+
}
|
|
118
|
+
write_launcher() { # $1 command name, $2 npm bin symlink to the package script
|
|
119
|
+
pkg="$(resolve "$2")"
|
|
120
|
+
if [ ! -f "$pkg" ]; then
|
|
121
|
+
echo "host-install: cannot find the $1 script at $pkg" >&2
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
rm -f "$BIN/$1"
|
|
125
|
+
cat > "$BIN/$1" <<EOF
|
|
126
|
+
#!/bin/sh
|
|
127
|
+
# zswarm launcher: exec the Node.js host install pinned, whatever PATH says.
|
|
128
|
+
exec "$node_dir/bin/node" "$pkg" "\$@"
|
|
129
|
+
EOF
|
|
130
|
+
chmod 0755 "$BIN/$1"
|
|
131
|
+
}
|
|
132
|
+
write_launcher zswarm "$npm_prefix/bin/zswarm"
|
|
133
|
+
write_launcher zswarm-mcp "$npm_prefix/bin/zswarm-mcp"
|
|
100
134
|
|
|
101
135
|
# --- Zellij ------------------------------------------------------------------
|
|
102
136
|
have_zellij="$("$BIN/zellij" --version 2>/dev/null | awk '{print $2}' || true)"
|
package/assets/host-report.sh
CHANGED
|
@@ -7,7 +7,18 @@ v() { "$@" 2>/dev/null | head -1; }
|
|
|
7
7
|
echo "os=$(uname -s)"
|
|
8
8
|
echo "arch=$(uname -m)"
|
|
9
9
|
echo "user=$(id -un)"
|
|
10
|
-
|
|
10
|
+
# node: the runtime zswarm actually uses. When zswarm is a host-install launcher,
|
|
11
|
+
# read the Node.js it execs from the launcher, not the first node on PATH.
|
|
12
|
+
node_bin=""
|
|
13
|
+
zw="$(command -v zswarm 2>/dev/null || true)"
|
|
14
|
+
if [ -n "$zw" ] && [ -f "$zw" ]; then
|
|
15
|
+
node_bin="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$zw" | head -1)"
|
|
16
|
+
fi
|
|
17
|
+
if [ -n "$node_bin" ] && [ -x "$node_bin" ]; then
|
|
18
|
+
echo "node=$(v "$node_bin" --version | sed 's/^v//')"
|
|
19
|
+
else
|
|
20
|
+
echo "node=$(v node --version | sed 's/^v//')"
|
|
21
|
+
fi
|
|
11
22
|
zv="$(v zswarm --version)"
|
|
12
23
|
case "$zv" in
|
|
13
24
|
[0-9]*) ;;
|
|
@@ -19,6 +30,9 @@ echo "python3=$(v python3 -c 'import platform; print(platform.python_version())'
|
|
|
19
30
|
echo "disk_home_gb=$(df -Pk "$HOME" 2>/dev/null | awk 'NR==2 {printf "%d", $4/1048576}')"
|
|
20
31
|
echo "disk_tmp_gb=$(df -Pk "${TMPDIR:-/tmp}" 2>/dev/null | awk 'NR==2 {printf "%d", $4/1048576}')"
|
|
21
32
|
echo "sessions=$(zellij list-sessions --no-formatting 2>/dev/null | grep -v EXITED | awk '{print $1}' | tr '\n' ' ')"
|
|
33
|
+
exited_names="$(zellij list-sessions --no-formatting 2>/dev/null | awk '/EXITED/ {print $1}' | tr '\n' ' ')"
|
|
34
|
+
echo "exited_sessions=$(printf '%s' "$exited_names" | wc -w | tr -d ' ')"
|
|
35
|
+
echo "exited_session_names=$exited_names"
|
|
22
36
|
if [ "$(uname -s)" = Linux ]; then
|
|
23
37
|
echo "linger=$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null)"
|
|
24
38
|
systemctl --user list-units --all --plain --no-legend 'zswarm-*' 2>/dev/null | awk '{print "service=" $1 " " $3 " " $4}'
|
package/dist/harness.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export type HarnessProfile = {
|
|
|
9
9
|
* against the trailing lines; a hit means the pane wants input, not time.
|
|
10
10
|
*/
|
|
11
11
|
waiting: readonly RegExp[];
|
|
12
|
+
/**
|
|
13
|
+
* A hint that the harness accepted the paste but queued it behind a running
|
|
14
|
+
* turn. `send` matches these against the trailing lines after the paste.
|
|
15
|
+
*/
|
|
16
|
+
queued: readonly RegExp[];
|
|
12
17
|
};
|
|
13
18
|
/**
|
|
14
19
|
* Identify the harness from a pane's command and title, case-insensitively.
|
|
@@ -19,3 +24,8 @@ export declare function resolveHarness(pane: {
|
|
|
19
24
|
command?: string | null;
|
|
20
25
|
title?: string | null;
|
|
21
26
|
}): HarnessProfile;
|
|
27
|
+
/**
|
|
28
|
+
* Does this screen's trailing lines show the harness's queued-message hint?
|
|
29
|
+
* Only the last few lines count: a quoted marker in scrollback is not a queue.
|
|
30
|
+
*/
|
|
31
|
+
export declare function queuedPrompt(screen: string, profile: HarnessProfile): boolean;
|
package/dist/harness.js
CHANGED
|
@@ -51,10 +51,29 @@ const WAITING = {
|
|
|
51
51
|
],
|
|
52
52
|
pi: GENERIC_WAITING,
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Queued-message hints seen live. Claude Code prints
|
|
56
|
+
* "Press up to edit queued messages" while a turn runs and the pasted body is
|
|
57
|
+
* held above the prompt. No other harness shows an equally clear marker yet,
|
|
58
|
+
* so every profile carries only this base set.
|
|
59
|
+
*/
|
|
60
|
+
const GENERIC_QUEUED = [/press up to edit queued messages/i];
|
|
61
|
+
/**
|
|
62
|
+
* Queued lines per harness. Every observed harness currently carries only the
|
|
63
|
+
* generic Claude Code hint; add harness-specific patterns here when seen.
|
|
64
|
+
*/
|
|
65
|
+
const QUEUED = {
|
|
66
|
+
codex: GENERIC_QUEUED,
|
|
67
|
+
cursor: GENERIC_QUEUED,
|
|
68
|
+
opencode: GENERIC_QUEUED,
|
|
69
|
+
gemini: GENERIC_QUEUED,
|
|
70
|
+
pi: GENERIC_QUEUED,
|
|
71
|
+
};
|
|
54
72
|
const UNKNOWN = {
|
|
55
73
|
name: "unknown",
|
|
56
74
|
submit: "auto",
|
|
57
75
|
waiting: GENERIC_WAITING,
|
|
76
|
+
queued: GENERIC_QUEUED,
|
|
58
77
|
};
|
|
59
78
|
function escapeRegExp(text) {
|
|
60
79
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -99,5 +118,18 @@ export function resolveHarness(pane) {
|
|
|
99
118
|
const name = matchCommand(pane.command) ?? matchTitle(pane.title) ?? "unknown";
|
|
100
119
|
if (name === "unknown")
|
|
101
120
|
return UNKNOWN;
|
|
102
|
-
return { name, submit: SUBMIT[name], waiting: WAITING[name] };
|
|
121
|
+
return { name, submit: SUBMIT[name], waiting: WAITING[name], queued: QUEUED[name] };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Does this screen's trailing lines show the harness's queued-message hint?
|
|
125
|
+
* Only the last few lines count: a quoted marker in scrollback is not a queue.
|
|
126
|
+
*/
|
|
127
|
+
export function queuedPrompt(screen, profile) {
|
|
128
|
+
const lines = screen
|
|
129
|
+
.replace(/\r/g, "")
|
|
130
|
+
.split("\n")
|
|
131
|
+
.map((line) => line.trimEnd())
|
|
132
|
+
.filter((line) => line.length > 0)
|
|
133
|
+
.slice(-6);
|
|
134
|
+
return profile.queued.some((re) => lines.some((line) => re.test(line)));
|
|
103
135
|
}
|
package/dist/host/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { ZellijError } from "../errors.js";
|
|
3
3
|
import { crewCheck, crewDown, crewStart, crewStatus, crewUp } from "./crew.js";
|
|
4
4
|
import { hostDoctor, hostInstall, hostInstallArgs, hostScriptPath } from "./host.js";
|
|
5
|
-
import { pruneRelays, runRelayDir, startRelay, waitForRelay } from "./relay.js";
|
|
5
|
+
import { cancelAllTo, cancelRelay, listRelays, pruneRelays, runRelayDir, startRelay, waitForRelay } from "./relay.js";
|
|
6
6
|
import { runSlot } from "./slot.js";
|
|
7
7
|
/**
|
|
8
8
|
* Host commands: CLI-only (not MCP ops), because they run long, pass argv
|
|
@@ -78,8 +78,23 @@ async function crew(argv) {
|
|
|
78
78
|
return 2;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** A plain-text table for `relay --list`, one row per relay. */
|
|
82
|
+
function relayTable(rows) {
|
|
83
|
+
const header = ["ID", "STATE", "TO", "SESSION", "AGE", "LAST CHECK", "JOB"];
|
|
84
|
+
const body = rows.map((r) => [
|
|
85
|
+
r.id,
|
|
86
|
+
r.state,
|
|
87
|
+
r.to,
|
|
88
|
+
r.session ?? "-",
|
|
89
|
+
`${r.age}s`,
|
|
90
|
+
r.lastCheck === undefined ? "-" : String(r.lastCheck),
|
|
91
|
+
r.job ?? "-",
|
|
92
|
+
]);
|
|
93
|
+
const widths = header.map((h, i) => Math.max(h.length, ...body.map((row) => row[i].length)));
|
|
94
|
+
return [header, ...body].map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd()).join("\n");
|
|
95
|
+
}
|
|
81
96
|
async function relay(argv) {
|
|
82
|
-
const { flags } = parseHostArgs(argv, ["--foreground", "--prune"]);
|
|
97
|
+
const { flags } = parseHostArgs(argv, ["--foreground", "--prune", "--list", "--cancel-all", "--json"]);
|
|
83
98
|
const run = str(flags["--run"]);
|
|
84
99
|
if (run)
|
|
85
100
|
return runRelayDir(run);
|
|
@@ -93,6 +108,33 @@ async function relay(argv) {
|
|
|
93
108
|
process.stdout.write(message);
|
|
94
109
|
return 0;
|
|
95
110
|
}
|
|
111
|
+
if (flags["--list"] === true) {
|
|
112
|
+
const rows = listRelays();
|
|
113
|
+
if (flags["--json"] === true)
|
|
114
|
+
return json(() => ({ relays: rows }));
|
|
115
|
+
process.stdout.write(`${relayTable(rows)}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const cancelId = str(flags["--cancel"]);
|
|
119
|
+
if (cancelId) {
|
|
120
|
+
const res = cancelRelay(cancelId);
|
|
121
|
+
if (flags["--json"] === true)
|
|
122
|
+
return json(() => res);
|
|
123
|
+
process.stdout.write(`${res.reason}\n`);
|
|
124
|
+
for (const line of res.actions)
|
|
125
|
+
process.stdout.write(`${line}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (flags["--cancel-all"] === true) {
|
|
129
|
+
const to = str(flags["--to"]);
|
|
130
|
+
if (!to)
|
|
131
|
+
throw new ZellijError("usage", "relay --cancel-all needs --to PANE");
|
|
132
|
+
const results = cancelAllTo(to);
|
|
133
|
+
if (flags["--json"] === true)
|
|
134
|
+
return json(() => ({ cancelled: results }));
|
|
135
|
+
process.stdout.write(results.length === 0 ? `no running relays to ${to}\n` : `${results.map((r) => r.reason).join("\n")}\n`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
96
138
|
if (flags["--prune"]) {
|
|
97
139
|
return json(() => ({ removed: pruneRelays(Number(str(flags["--days"]) ?? 14)) }));
|
|
98
140
|
}
|
|
@@ -109,6 +151,7 @@ async function relay(argv) {
|
|
|
109
151
|
check: str(flags["--check"]),
|
|
110
152
|
message: str(flags["--message"]),
|
|
111
153
|
timeoutMin: flags["--timeout-min"] ? Number(str(flags["--timeout-min"])) : undefined,
|
|
154
|
+
maxAgeMin: flags["--max-age"] ? Number(str(flags["--max-age"])) : undefined,
|
|
112
155
|
foreground: flags["--foreground"] === true,
|
|
113
156
|
}));
|
|
114
157
|
}
|
|
@@ -170,9 +213,12 @@ const COMMANDS = {
|
|
|
170
213
|
usage: [
|
|
171
214
|
"zswarm relay --to PANE --done REGEX --reply-to PANE [--serve TARGET] [--session S] [--body-file F]",
|
|
172
215
|
" [--reply-session S] [--from NAME] [--check SCRIPT] [--message TEMPLATE] [--id ID]",
|
|
173
|
-
" [--timeout-min 240] [--foreground]",
|
|
216
|
+
" [--timeout-min 240] [--max-age MIN] [--foreground]",
|
|
174
217
|
" send the body to PANE over serve (ZSWARM_SERVE / ZSWARM_SERVE_TOKEN), then a detached relay",
|
|
175
|
-
" waits for REGEX (and SCRIPT exit 0) and pastes a message into the local --reply-to pane",
|
|
218
|
+
" waits for REGEX (and SCRIPT exit 0) and pastes a message into the local --reply-to pane;",
|
|
219
|
+
" --max-age (default --timeout-min + 60) is a hard end that always yields a final status",
|
|
220
|
+
"zswarm relay --list [--json] id, state, target, session, age, last check and job per relay dir",
|
|
221
|
+
"zswarm relay --cancel ID | --cancel-all --to PANE stop the job, mark it CANCELLED, paste nothing",
|
|
176
222
|
"zswarm relay --wait ID [--timeout-min 240] block until relay ID has its message, print it (pull path)",
|
|
177
223
|
"zswarm relay --prune [--days 14] remove old relay dirs and undelivered messages",
|
|
178
224
|
].join("\n"),
|
package/dist/host/crew.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type CrewDeps = {
|
|
|
12
12
|
};
|
|
13
13
|
/** Live session names; injected in tests. */
|
|
14
14
|
sessions?: (env: NodeJS.ProcessEnv) => Promise<string[]>;
|
|
15
|
+
/** Exited (resurrectable) session names; injected in tests. */
|
|
16
|
+
exitedSessions?: (env: NodeJS.ProcessEnv) => Promise<string[]>;
|
|
15
17
|
/** Pane titles (with exited flags) of a session; injected in tests. */
|
|
16
18
|
panes?: (session: string, env: NodeJS.ProcessEnv) => Promise<Array<{
|
|
17
19
|
title: string;
|
package/dist/host/crew.js
CHANGED
|
@@ -83,6 +83,14 @@ async function defaultSessions(env) {
|
|
|
83
83
|
return [];
|
|
84
84
|
return (res.data.sessions ?? []).map((s) => s.name ?? "");
|
|
85
85
|
}
|
|
86
|
+
async function defaultExitedSessions(env) {
|
|
87
|
+
const res = await dispatchZswarm({ op: "sessions", all: true }, undefined, { env });
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
return [];
|
|
90
|
+
return (res.data.sessions ?? [])
|
|
91
|
+
.filter((s) => s.exited === true)
|
|
92
|
+
.map((s) => s.name ?? "");
|
|
93
|
+
}
|
|
86
94
|
async function defaultPanes(session, env) {
|
|
87
95
|
const res = await dispatchZswarm({ op: "list", session }, undefined, { env });
|
|
88
96
|
if (!res.ok)
|
|
@@ -252,12 +260,17 @@ export async function crewStatus(input) {
|
|
|
252
260
|
? readCrewFiles(crewDir(account, session, env))
|
|
253
261
|
: undefined;
|
|
254
262
|
const live = (await (input.deps?.sessions ?? defaultSessions)(env)).includes(session);
|
|
263
|
+
const exited = (await (input.deps?.exitedSessions ?? defaultExitedSessions)(env)).includes(session);
|
|
255
264
|
const panes = live ? await (input.deps?.panes ?? defaultPanes)(session, { ...env, ZSWARM_BUS: "0" }) : [];
|
|
256
265
|
out.live = live;
|
|
266
|
+
out.exited = exited;
|
|
257
267
|
out.firstPane = files?.first ?? null;
|
|
258
268
|
out.layoutTook = files ? panes.some((p) => p.title === files.first) : null;
|
|
259
269
|
out.panes = panes;
|
|
260
270
|
out.healthy = services.every((s) => s.active) && live && out.layoutTook !== false && panes.every((p) => !p.exited);
|
|
271
|
+
if (live && exited) {
|
|
272
|
+
out.note = `an exited session named ${session} is also listed (resurrectable); remove it with zswarm sessions --prune-exited`;
|
|
273
|
+
}
|
|
261
274
|
return out;
|
|
262
275
|
}
|
|
263
276
|
export function crewDown(input) {
|
package/dist/host/host.js
CHANGED
|
@@ -90,6 +90,19 @@ export function hostChecks(facts, services, session) {
|
|
|
90
90
|
tool("zswarm", true, "zswarm host install");
|
|
91
91
|
tool("zellij", true, "zswarm host install");
|
|
92
92
|
tool("python3", false, "zswarm slot needs Python 3.9+");
|
|
93
|
+
// Exited sessions are resurrectable leftovers; a pile of them is a warning.
|
|
94
|
+
if (facts.exited_sessions !== undefined && facts.exited_sessions !== "") {
|
|
95
|
+
const exited = Number(facts.exited_sessions);
|
|
96
|
+
if (Number.isFinite(exited)) {
|
|
97
|
+
checks.push({
|
|
98
|
+
id: "exited_sessions",
|
|
99
|
+
state: exited > 10 ? "warn" : "pass",
|
|
100
|
+
detail: exited > 10
|
|
101
|
+
? `${exited} exited (resurrectable) sessions; prune with zswarm sessions --prune-exited`
|
|
102
|
+
: `${exited} exited`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
93
106
|
for (const id of ["disk_home_gb", "disk_tmp_gb"]) {
|
|
94
107
|
const gb = Number(facts[id]);
|
|
95
108
|
if (!Number.isFinite(gb) || facts[id] === "")
|
package/dist/host/relay.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ import type { OpsResult } from "../ops/types.js";
|
|
|
16
16
|
* message.txt. It reads nothing else, so moving or updating the caller's files
|
|
17
17
|
* while it waits cannot break it.
|
|
18
18
|
*/
|
|
19
|
-
export type RelayStatus = "DONE" | "TIMEOUT" | "PANE_DIED" | "SERVE_DOWN" | "UNCONFIRMED";
|
|
19
|
+
export type RelayStatus = "DONE" | "TIMEOUT" | "MAX_AGE" | "CHECK_BROKEN" | "CANCELLED" | "PANE_DIED" | "SERVE_DOWN" | "UNCONFIRMED";
|
|
20
|
+
/** How a detached relay can be stopped: the launchd label, systemd unit, or pid. */
|
|
21
|
+
export type RelayJob = {
|
|
22
|
+
kind: "launchd" | "systemd" | "pid";
|
|
23
|
+
label?: string;
|
|
24
|
+
unit?: string;
|
|
25
|
+
pid?: number;
|
|
26
|
+
};
|
|
20
27
|
export type RelayConfig = {
|
|
21
28
|
id: string;
|
|
22
29
|
serve: string;
|
|
@@ -28,6 +35,8 @@ export type RelayConfig = {
|
|
|
28
35
|
replySession?: string;
|
|
29
36
|
from: string;
|
|
30
37
|
timeoutMin: number;
|
|
38
|
+
/** Hard cap on the relay's age; defaults to `timeoutMin` plus 60 minutes. */
|
|
39
|
+
maxAgeMin?: number;
|
|
31
40
|
check?: string;
|
|
32
41
|
message?: string;
|
|
33
42
|
/**
|
|
@@ -35,6 +44,13 @@ export type RelayConfig = {
|
|
|
35
44
|
* pasted body, an earlier task's done line). Only a line beyond these counts.
|
|
36
45
|
*/
|
|
37
46
|
seen?: string[];
|
|
47
|
+
/** Recorded by launchRelay so `--cancel` can stop the detached job. */
|
|
48
|
+
job?: RelayJob;
|
|
49
|
+
/** Final status once the loop ends; absent while the relay runs. */
|
|
50
|
+
status?: RelayStatus;
|
|
51
|
+
/** Set by `--cancel`; a late loop must not overwrite it. */
|
|
52
|
+
cancelled?: boolean;
|
|
53
|
+
cancelledAt?: number;
|
|
38
54
|
};
|
|
39
55
|
export type RelayDeps = {
|
|
40
56
|
/** dispatchZswarm with an env; injected in tests. */
|
|
@@ -82,6 +98,8 @@ export type RelayStartInput = {
|
|
|
82
98
|
check?: string;
|
|
83
99
|
message?: string;
|
|
84
100
|
timeoutMin?: number;
|
|
101
|
+
/** Hard cap on the relay's age; defaults to `timeoutMin` plus 60 minutes. */
|
|
102
|
+
maxAgeMin?: number;
|
|
85
103
|
foreground?: boolean;
|
|
86
104
|
env?: NodeJS.ProcessEnv;
|
|
87
105
|
deps?: RelayDeps;
|
|
@@ -112,5 +130,49 @@ export declare function waitForRelay(id: string, opts?: {
|
|
|
112
130
|
sleep?: (ms: number) => Promise<void>;
|
|
113
131
|
now?: () => number;
|
|
114
132
|
}): Promise<string | null>;
|
|
133
|
+
export type RelayJobRunner = (cmd: string, args: string[]) => {
|
|
134
|
+
status: number;
|
|
135
|
+
stdout: string;
|
|
136
|
+
stderr?: string;
|
|
137
|
+
};
|
|
138
|
+
export type RelayAdminDeps = {
|
|
139
|
+
env?: NodeJS.ProcessEnv;
|
|
140
|
+
now?: () => number;
|
|
141
|
+
/** Runs launchctl/systemctl; injected in tests. */
|
|
142
|
+
run?: RelayJobRunner;
|
|
143
|
+
/** Liveness probe for a plain detached pid; injected in tests. */
|
|
144
|
+
alive?: (pid: number) => boolean;
|
|
145
|
+
/** Sends SIGTERM to a plain detached pid; injected in tests. */
|
|
146
|
+
kill?: (pid: number) => void;
|
|
147
|
+
/** A pid's command line (undefined when unknown); injected in tests. */
|
|
148
|
+
commandLine?: (pid: number) => string | undefined;
|
|
149
|
+
};
|
|
150
|
+
export type RelayRow = {
|
|
151
|
+
id: string;
|
|
152
|
+
dir: string;
|
|
153
|
+
to: string;
|
|
154
|
+
session?: string;
|
|
155
|
+
/** Epoch milliseconds the run started (the `<ts>` in its directory name). */
|
|
156
|
+
started: number;
|
|
157
|
+
/** Seconds since it started. */
|
|
158
|
+
age: number;
|
|
159
|
+
state: string;
|
|
160
|
+
lastCheck?: number;
|
|
161
|
+
/** The launchd label / systemd unit / pid, when the job is still loaded. */
|
|
162
|
+
job?: string;
|
|
163
|
+
};
|
|
164
|
+
export type RelayCancelResult = {
|
|
165
|
+
id: string;
|
|
166
|
+
cancelled: boolean;
|
|
167
|
+
state: string;
|
|
168
|
+
reason: string;
|
|
169
|
+
actions: string[];
|
|
170
|
+
};
|
|
171
|
+
/** One row per relay run directory; newest first. */
|
|
172
|
+
export declare function listRelays(deps?: RelayAdminDeps): RelayRow[];
|
|
173
|
+
/** Stop a running relay's job, mark its directory CANCELLED and scrub its token. */
|
|
174
|
+
export declare function cancelRelay(id: string, deps?: RelayAdminDeps): RelayCancelResult;
|
|
175
|
+
/** Cancel every running relay that targets `to`. */
|
|
176
|
+
export declare function cancelAllTo(to: string, deps?: RelayAdminDeps): RelayCancelResult[];
|
|
115
177
|
/** Remove relay directories and undelivered messages older than `days`. */
|
|
116
178
|
export declare function pruneRelays(days?: number, env?: NodeJS.ProcessEnv): string[];
|