@phnx-labs/agents-cli 1.20.63 → 1.20.65
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/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor evaluate/fire loop.
|
|
3
|
+
*
|
|
4
|
+
* Modeled on the routines daemon: a single MonitorEngine lives inside runDaemon()
|
|
5
|
+
* beside the cron JobScheduler. On each tick it evaluates every enabled monitor
|
|
6
|
+
* that is DUE and owned by this device, applies the condition through the native
|
|
7
|
+
* state-diff store, and on a fire dispatches the action, writes a fire record, and
|
|
8
|
+
* updates state. A per-monitor rate limit auto-pauses a firehose.
|
|
9
|
+
*
|
|
10
|
+
* v1 covers the poll model (command, poll, poll-http, file, device). Push sources
|
|
11
|
+
* (ws, webhook) return null from `evaluate` — they deliver through `subscribe` /
|
|
12
|
+
* the webhook receiver, wired in a follow-up; the engine treats them as inert.
|
|
13
|
+
*/
|
|
14
|
+
import { listMonitors, monitorRunsOnThisDevice, parseInterval, setMonitorEnabled, } from './config.js';
|
|
15
|
+
import { evaluateSource } from './sources/index.js';
|
|
16
|
+
import { hasChanged, readState, writeState, recordFireTime, writeFireRecord, } from './state.js';
|
|
17
|
+
import { dispatchAction } from './dispatch.js';
|
|
18
|
+
/** How often the engine wakes to check which monitors are due. */
|
|
19
|
+
const TICK_MS = 5_000;
|
|
20
|
+
/** Default evaluation cadence for sources that carry no explicit interval. */
|
|
21
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
22
|
+
function truncateSummary(text) {
|
|
23
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
24
|
+
return oneLine.length > 240 ? oneLine.slice(0, 240) + '…' : oneLine;
|
|
25
|
+
}
|
|
26
|
+
function buildEvent(monitor, summary, payload) {
|
|
27
|
+
return { monitorName: monitor.name, firedAt: new Date().toISOString(), summary, payload };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Apply a monitor's condition to an observation. Pure (reads state, never
|
|
31
|
+
* writes), so both the tick loop and the `test` dry-run share it.
|
|
32
|
+
*/
|
|
33
|
+
export function decideFire(monitor, observation) {
|
|
34
|
+
const cond = monitor.condition;
|
|
35
|
+
const raw = observation.raw;
|
|
36
|
+
const payload = observation.meta ?? {};
|
|
37
|
+
const dedupeKey = cond.dedupeKey;
|
|
38
|
+
if (cond.mode === 'every') {
|
|
39
|
+
return {
|
|
40
|
+
fire: true,
|
|
41
|
+
value: raw,
|
|
42
|
+
dedupeKey,
|
|
43
|
+
persist: false,
|
|
44
|
+
event: buildEvent(monitor, truncateSummary(raw), payload),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (cond.mode === 'match') {
|
|
48
|
+
let matched = null;
|
|
49
|
+
try {
|
|
50
|
+
matched = new RegExp(cond.match ?? '').exec(raw);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
matched = null;
|
|
54
|
+
}
|
|
55
|
+
if (!matched) {
|
|
56
|
+
return { fire: false, value: raw, dedupeKey, persist: false, event: null };
|
|
57
|
+
}
|
|
58
|
+
const matchedValue = matched[0];
|
|
59
|
+
const changed = hasChanged(monitor.name, matchedValue, dedupeKey);
|
|
60
|
+
return {
|
|
61
|
+
fire: changed,
|
|
62
|
+
value: matchedValue,
|
|
63
|
+
dedupeKey,
|
|
64
|
+
persist: changed,
|
|
65
|
+
event: changed ? buildEvent(monitor, truncateSummary(matchedValue), payload) : null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// on-change (default): the first observation establishes a silent baseline;
|
|
69
|
+
// thereafter fire when the de-dupe signature differs from last-seen.
|
|
70
|
+
const prior = readState(monitor.name);
|
|
71
|
+
if (!prior) {
|
|
72
|
+
return { fire: false, value: raw, dedupeKey, persist: true, event: null };
|
|
73
|
+
}
|
|
74
|
+
const changed = hasChanged(monitor.name, raw, dedupeKey);
|
|
75
|
+
return {
|
|
76
|
+
fire: changed,
|
|
77
|
+
value: raw,
|
|
78
|
+
dedupeKey,
|
|
79
|
+
persist: changed,
|
|
80
|
+
event: changed ? buildEvent(monitor, truncateSummary(raw), payload) : null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** One evaluation of a monitor's source + condition, without side effects. Used by `test`. */
|
|
84
|
+
export async function evaluateMonitorOnce(monitor) {
|
|
85
|
+
const observation = await evaluateSource(monitor.source);
|
|
86
|
+
if (!observation)
|
|
87
|
+
return { observation: null, decision: null };
|
|
88
|
+
return { observation, decision: decideFire(monitor, observation) };
|
|
89
|
+
}
|
|
90
|
+
/** The durable monitor engine. One instance per daemon. */
|
|
91
|
+
export class MonitorEngine {
|
|
92
|
+
logFn;
|
|
93
|
+
timer = null;
|
|
94
|
+
monitors = [];
|
|
95
|
+
lastEval = new Map();
|
|
96
|
+
ticking = false;
|
|
97
|
+
constructor(logFn = () => { }) {
|
|
98
|
+
this.logFn = logFn;
|
|
99
|
+
}
|
|
100
|
+
/** Load owned+enabled monitors and start the tick loop. */
|
|
101
|
+
start() {
|
|
102
|
+
this.loadAll();
|
|
103
|
+
this.logFn('INFO', `Monitor engine started (${this.monitors.length} monitor(s) on this device)`);
|
|
104
|
+
this.timer = setInterval(() => void this.tick(), TICK_MS);
|
|
105
|
+
}
|
|
106
|
+
/** Reload monitor configs (SIGHUP). */
|
|
107
|
+
reload() {
|
|
108
|
+
this.loadAll();
|
|
109
|
+
this.logFn('INFO', `Monitor engine reloaded (${this.monitors.length} monitor(s) on this device)`);
|
|
110
|
+
}
|
|
111
|
+
/** Stop the tick loop. */
|
|
112
|
+
stop() {
|
|
113
|
+
if (this.timer) {
|
|
114
|
+
clearInterval(this.timer);
|
|
115
|
+
this.timer = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
loadAll() {
|
|
119
|
+
this.monitors = listMonitors().filter((m) => m.enabled && monitorRunsOnThisDevice(m));
|
|
120
|
+
}
|
|
121
|
+
intervalMs(monitor) {
|
|
122
|
+
if (monitor.source.interval)
|
|
123
|
+
return parseInterval(monitor.source.interval) ?? DEFAULT_INTERVAL_MS;
|
|
124
|
+
return DEFAULT_INTERVAL_MS;
|
|
125
|
+
}
|
|
126
|
+
isDue(monitor, now) {
|
|
127
|
+
const last = this.lastEval.get(monitor.name) ?? 0;
|
|
128
|
+
return now - last >= this.intervalMs(monitor);
|
|
129
|
+
}
|
|
130
|
+
/** Evaluate every due monitor once. Overlap-guarded so a slow cycle never stacks. */
|
|
131
|
+
async tick() {
|
|
132
|
+
if (this.ticking)
|
|
133
|
+
return;
|
|
134
|
+
this.ticking = true;
|
|
135
|
+
try {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
for (const monitor of this.monitors) {
|
|
138
|
+
if (!this.isDue(monitor, now))
|
|
139
|
+
continue;
|
|
140
|
+
this.lastEval.set(monitor.name, now);
|
|
141
|
+
await this.runMonitor(monitor);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
this.ticking = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async runMonitor(monitor) {
|
|
149
|
+
try {
|
|
150
|
+
const { observation, decision } = await evaluateMonitorOnce(monitor);
|
|
151
|
+
if (!observation || !decision)
|
|
152
|
+
return;
|
|
153
|
+
if (decision.fire && decision.event) {
|
|
154
|
+
await this.fire(monitor, decision, decision.event);
|
|
155
|
+
}
|
|
156
|
+
else if (decision.persist) {
|
|
157
|
+
// Silent baseline / no-change: record the value so we don't re-fire.
|
|
158
|
+
writeState(monitor.name, decision.value, decision.dedupeKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.logFn('ERROR', `monitor '${monitor.name}' evaluation failed: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async fire(monitor, decision, event) {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
let fireTimes;
|
|
168
|
+
// Firehose guard: auto-pause a monitor that exceeds its rate limit.
|
|
169
|
+
if (monitor.rateLimit) {
|
|
170
|
+
const windowMs = parseInterval(monitor.rateLimit.per) ?? 60_000;
|
|
171
|
+
fireTimes = recordFireTime(monitor.name, now, windowMs);
|
|
172
|
+
if (fireTimes.length > monitor.rateLimit.max) {
|
|
173
|
+
writeState(monitor.name, decision.value, decision.dedupeKey, { lastFiredAt: event.firedAt, fireTimes });
|
|
174
|
+
try {
|
|
175
|
+
setMonitorEnabled(monitor.name, false);
|
|
176
|
+
}
|
|
177
|
+
catch { /* best-effort pause */ }
|
|
178
|
+
this.logFn('WARN', `monitor '${monitor.name}' exceeded rate limit (${monitor.rateLimit.max}/${monitor.rateLimit.per}) — auto-paused`);
|
|
179
|
+
this.loadAll();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
let result;
|
|
184
|
+
try {
|
|
185
|
+
result = await dispatchAction(monitor, event);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
result = { kind: monitor.action.type, ok: false, error: err.message };
|
|
189
|
+
}
|
|
190
|
+
writeFireRecord(event, {
|
|
191
|
+
...(result.runId ? { runId: result.runId } : {}),
|
|
192
|
+
action: result.kind,
|
|
193
|
+
ok: result.ok,
|
|
194
|
+
...(result.error ? { error: result.error } : {}),
|
|
195
|
+
});
|
|
196
|
+
writeState(monitor.name, decision.value, decision.dedupeKey, { lastFiredAt: event.firedAt, fireTimes });
|
|
197
|
+
this.logFn(result.ok ? 'INFO' : 'ERROR', `monitor '${monitor.name}' fired → ${result.kind}` +
|
|
198
|
+
(result.runId ? ` (run: ${result.runId})` : '') +
|
|
199
|
+
(result.ok ? '' : ` FAILED: ${result.error}`));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command / poll source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Runs a shell command; its stdout is the observation. `poll` re-runs the same
|
|
5
|
+
* command on an interval — the evaluation is identical, so poll.ts delegates
|
|
6
|
+
* here. No agent, no sandbox: a plain `/bin/sh -c` (or `cmd /c` on Windows).
|
|
7
|
+
*/
|
|
8
|
+
import type { MonitorSource } from '../config.js';
|
|
9
|
+
import type { Observation } from './types.js';
|
|
10
|
+
/** Run the source command and return its combined stdout as the observation. */
|
|
11
|
+
export declare function evaluate(source: MonitorSource): Promise<Observation | null>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command / poll source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Runs a shell command; its stdout is the observation. `poll` re-runs the same
|
|
5
|
+
* command on an interval — the evaluation is identical, so poll.ts delegates
|
|
6
|
+
* here. No agent, no sandbox: a plain `/bin/sh -c` (or `cmd /c` on Windows).
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
10
|
+
/** Run the source command and return its combined stdout as the observation. */
|
|
11
|
+
export function evaluate(source) {
|
|
12
|
+
const command = source.command;
|
|
13
|
+
if (!command)
|
|
14
|
+
return Promise.resolve(null);
|
|
15
|
+
const [bin, args] = process.platform === 'win32'
|
|
16
|
+
? ['cmd', ['/c', command]]
|
|
17
|
+
: ['/bin/sh', ['-c', command]];
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
execFile(bin, args, { encoding: 'utf-8', timeout: DEFAULT_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
20
|
+
const exitCode = err && typeof err.code === 'number'
|
|
21
|
+
? err.code
|
|
22
|
+
: err
|
|
23
|
+
? 1
|
|
24
|
+
: 0;
|
|
25
|
+
// A non-zero exit is still a real observation (the diff might be exactly
|
|
26
|
+
// "command started failing"); surface stderr when stdout is empty.
|
|
27
|
+
const raw = (stdout && stdout.length > 0 ? stdout : stderr ?? '').replace(/\s+$/, '');
|
|
28
|
+
resolve({ raw, meta: { exitCode } });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* A fleet device becomes the watched source. Reuses the real load/health probe
|
|
5
|
+
* (lib/devices/health.ts) — the first scheduler consumer of it — so a monitor can
|
|
6
|
+
* fire "when a box goes loaded / unreachable". The observation is
|
|
7
|
+
* `<reachable>\t<headroom>` plus the full DeviceStats in meta, so an on-change
|
|
8
|
+
* monitor fires on a headroom-bucket flip and a match monitor can match `loaded`.
|
|
9
|
+
*/
|
|
10
|
+
import type { MonitorSource } from '../config.js';
|
|
11
|
+
import type { Observation } from './types.js';
|
|
12
|
+
/** Probe the device and return reachability + headroom bucket as the observation. */
|
|
13
|
+
export declare function evaluate(source: MonitorSource): Promise<Observation | null>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* A fleet device becomes the watched source. Reuses the real load/health probe
|
|
5
|
+
* (lib/devices/health.ts) — the first scheduler consumer of it — so a monitor can
|
|
6
|
+
* fire "when a box goes loaded / unreachable". The observation is
|
|
7
|
+
* `<reachable>\t<headroom>` plus the full DeviceStats in meta, so an on-change
|
|
8
|
+
* monitor fires on a headroom-bucket flip and a match monitor can match `loaded`.
|
|
9
|
+
*/
|
|
10
|
+
import { loadDevices } from '../../devices/registry.js';
|
|
11
|
+
import { probeDeviceStats, probeLocalStats, headroom } from '../../devices/health.js';
|
|
12
|
+
import { machineId, normalizeHost } from '../../machine-id.js';
|
|
13
|
+
/** Probe the device and return reachability + headroom bucket as the observation. */
|
|
14
|
+
export async function evaluate(source) {
|
|
15
|
+
const name = source.device;
|
|
16
|
+
if (!name)
|
|
17
|
+
return null;
|
|
18
|
+
const registry = await loadDevices();
|
|
19
|
+
const wanted = normalizeHost(name);
|
|
20
|
+
const entry = Object.entries(registry).find(([k]) => normalizeHost(k) === wanted);
|
|
21
|
+
const stats = entry && normalizeHost(entry[0]) !== machineId()
|
|
22
|
+
? await probeDeviceStats(entry[1])
|
|
23
|
+
: await probeLocalStats(name);
|
|
24
|
+
const bucket = headroom(stats);
|
|
25
|
+
return {
|
|
26
|
+
raw: `${stats.reachable ? 'reachable' : 'unreachable'}\t${bucket}`,
|
|
27
|
+
meta: {
|
|
28
|
+
reachable: stats.reachable,
|
|
29
|
+
headroom: bucket,
|
|
30
|
+
loadPercent: stats.loadPercent,
|
|
31
|
+
memPercent: stats.memPercent,
|
|
32
|
+
ncpu: stats.ncpu,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* The observation is the file's content (bounded) plus mtime/size in meta, so an
|
|
5
|
+
* on-change monitor fires on any edit. For a directory, the observation is a
|
|
6
|
+
* sorted listing with per-entry mtime/size. Push-follow reuses followFile
|
|
7
|
+
* (lib/log-follow.ts) — the same cross-platform tail the routines log viewer uses.
|
|
8
|
+
*/
|
|
9
|
+
import type { MonitorSource } from '../config.js';
|
|
10
|
+
import type { Observation } from './types.js';
|
|
11
|
+
/** Snapshot the file (or directory) as the observation. */
|
|
12
|
+
export declare function evaluate(source: MonitorSource): Promise<Observation | null>;
|
|
13
|
+
/** Push-follow the file, emitting each appended chunk as an observation. */
|
|
14
|
+
export declare function subscribe(source: MonitorSource, onObs: (obs: Observation) => void): () => void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* The observation is the file's content (bounded) plus mtime/size in meta, so an
|
|
5
|
+
* on-change monitor fires on any edit. For a directory, the observation is a
|
|
6
|
+
* sorted listing with per-entry mtime/size. Push-follow reuses followFile
|
|
7
|
+
* (lib/log-follow.ts) — the same cross-platform tail the routines log viewer uses.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { followFile } from '../../log-follow.js';
|
|
12
|
+
const MAX_CONTENT = 256 * 1024;
|
|
13
|
+
/** Snapshot the file (or directory) as the observation. */
|
|
14
|
+
export function evaluate(source) {
|
|
15
|
+
const filePath = source.path;
|
|
16
|
+
if (!filePath)
|
|
17
|
+
return Promise.resolve(null);
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs.statSync(filePath);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
const entries = fs
|
|
22
|
+
.readdirSync(filePath, { withFileTypes: true })
|
|
23
|
+
.map((e) => {
|
|
24
|
+
try {
|
|
25
|
+
const s = fs.statSync(path.join(filePath, e.name));
|
|
26
|
+
return `${e.name}\t${s.size}\t${Math.round(s.mtimeMs)}`;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return `${e.name}\t?`;
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.sort();
|
|
33
|
+
return Promise.resolve({
|
|
34
|
+
raw: entries.join('\n'),
|
|
35
|
+
meta: { kind: 'directory', count: entries.length },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
if (content.length > MAX_CONTENT)
|
|
40
|
+
content = content.slice(content.length - MAX_CONTENT);
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
raw: content,
|
|
43
|
+
meta: { kind: 'file', size: stat.size, mtimeMs: Math.round(stat.mtimeMs) },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// Absent file is a real observation (it may have just been deleted/created).
|
|
48
|
+
return Promise.resolve({ raw: `missing: ${err.message}`, meta: { kind: 'missing' } });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Push-follow the file, emitting each appended chunk as an observation. */
|
|
52
|
+
export function subscribe(source, onObs) {
|
|
53
|
+
const filePath = source.path;
|
|
54
|
+
if (!filePath)
|
|
55
|
+
return () => { };
|
|
56
|
+
return followFile(filePath, (text) => onObs({ raw: text, meta: { kind: 'append' } }), { fromEnd: true });
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll-http source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* GETs the URL; the observation is `<status>\n<body>` so a status flip OR a body
|
|
5
|
+
* change both register as a diff. Uses the built-in fetch (Node 22+).
|
|
6
|
+
*/
|
|
7
|
+
import type { MonitorSource } from '../config.js';
|
|
8
|
+
import type { Observation } from './types.js';
|
|
9
|
+
/** GET the source URL and return status + body as the observation. */
|
|
10
|
+
export declare function evaluate(source: MonitorSource): Promise<Observation | null>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll-http source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* GETs the URL; the observation is `<status>\n<body>` so a status flip OR a body
|
|
5
|
+
* change both register as a diff. Uses the built-in fetch (Node 22+).
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const MAX_BODY = 1024 * 1024;
|
|
9
|
+
/** GET the source URL and return status + body as the observation. */
|
|
10
|
+
export async function evaluate(source) {
|
|
11
|
+
const url = source.url;
|
|
12
|
+
if (!url)
|
|
13
|
+
return null;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(url, { signal: controller.signal, redirect: 'follow' });
|
|
18
|
+
let body = await res.text();
|
|
19
|
+
if (body.length > MAX_BODY)
|
|
20
|
+
body = body.slice(0, MAX_BODY);
|
|
21
|
+
return {
|
|
22
|
+
raw: `${res.status}\n${body}`,
|
|
23
|
+
meta: { status: res.status, ok: res.ok },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
// A network failure is itself a real observation (the endpoint went down);
|
|
28
|
+
// surface it so an on-change monitor can fire on reachability flips.
|
|
29
|
+
return { raw: `error: ${err.message}`, meta: { status: 0, ok: false } };
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-evaluator registry: source.type → evaluator.
|
|
3
|
+
*
|
|
4
|
+
* The engine looks up the evaluator by type and calls its poll-model `evaluate`.
|
|
5
|
+
* Push-based sources (ws, webhook) return null from `evaluate` and deliver
|
|
6
|
+
* observations through their `subscribe` instead.
|
|
7
|
+
*/
|
|
8
|
+
import type { MonitorSourceType, MonitorSource } from '../config.js';
|
|
9
|
+
import type { Observation, SourceEvaluator } from './types.js';
|
|
10
|
+
export type { Observation, SourceEvaluator, SourceSubscriber } from './types.js';
|
|
11
|
+
/** Poll-model evaluator per source type. */
|
|
12
|
+
export declare const SOURCE_EVALUATORS: Record<MonitorSourceType, SourceEvaluator>;
|
|
13
|
+
/** Evaluate one source snapshot. Returns null when the source is push-only or has nothing new. */
|
|
14
|
+
export declare function evaluateSource(source: MonitorSource): Promise<Observation | null>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-evaluator registry: source.type → evaluator.
|
|
3
|
+
*
|
|
4
|
+
* The engine looks up the evaluator by type and calls its poll-model `evaluate`.
|
|
5
|
+
* Push-based sources (ws, webhook) return null from `evaluate` and deliver
|
|
6
|
+
* observations through their `subscribe` instead.
|
|
7
|
+
*/
|
|
8
|
+
import * as command from './command.js';
|
|
9
|
+
import * as poll from './poll.js';
|
|
10
|
+
import * as http from './http.js';
|
|
11
|
+
import * as file from './file.js';
|
|
12
|
+
import * as device from './device.js';
|
|
13
|
+
import * as ws from './ws.js';
|
|
14
|
+
import * as webhook from './webhook.js';
|
|
15
|
+
/** Poll-model evaluator per source type. */
|
|
16
|
+
export const SOURCE_EVALUATORS = {
|
|
17
|
+
command: command.evaluate,
|
|
18
|
+
poll: poll.evaluate,
|
|
19
|
+
'poll-http': http.evaluate,
|
|
20
|
+
file: file.evaluate,
|
|
21
|
+
device: device.evaluate,
|
|
22
|
+
ws: ws.evaluate,
|
|
23
|
+
webhook: webhook.evaluate,
|
|
24
|
+
};
|
|
25
|
+
/** Evaluate one source snapshot. Returns null when the source is push-only or has nothing new. */
|
|
26
|
+
export function evaluateSource(source) {
|
|
27
|
+
const evaluator = SOURCE_EVALUATORS[source.type];
|
|
28
|
+
if (!evaluator)
|
|
29
|
+
return Promise.resolve(null);
|
|
30
|
+
return evaluator(source);
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* `poll` re-runs a shell command on an interval and diffs the output. The
|
|
5
|
+
* evaluation is identical to the `command` source — the difference is only the
|
|
6
|
+
* scheduling cadence (source.interval), which the engine owns — so this
|
|
7
|
+
* delegates to command.ts rather than duplicating the shell-run.
|
|
8
|
+
*/
|
|
9
|
+
export { evaluate } from './command.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* `poll` re-runs a shell command on an interval and diffs the output. The
|
|
5
|
+
* evaluation is identical to the `command` source — the difference is only the
|
|
6
|
+
* scheduling cadence (source.interval), which the engine owns — so this
|
|
7
|
+
* delegates to command.ts rather than duplicating the shell-run.
|
|
8
|
+
*/
|
|
9
|
+
export { evaluate } from './command.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared source-evaluator contract.
|
|
3
|
+
*
|
|
4
|
+
* Every source module exports `evaluate(source)` (the poll model — one snapshot,
|
|
5
|
+
* or null when nothing is observable this tick / the source is push-only) and,
|
|
6
|
+
* where push-based (ws, file-follow), a `subscribe(source, onObs)` returning an
|
|
7
|
+
* unsubscribe fn.
|
|
8
|
+
*/
|
|
9
|
+
import type { MonitorSource } from '../config.js';
|
|
10
|
+
/** One observation of a source: the raw text plus optional structured metadata. */
|
|
11
|
+
export interface Observation {
|
|
12
|
+
raw: string;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** Poll-model evaluator: return one observation, or null when none is available. */
|
|
16
|
+
export type SourceEvaluator = (source: MonitorSource) => Promise<Observation | null>;
|
|
17
|
+
/** Push-model subscriber: call onObs on each frame; return an unsubscribe fn. */
|
|
18
|
+
export type SourceSubscriber = (source: MonitorSource, onObs: (obs: Observation) => void) => () => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared source-evaluator contract.
|
|
3
|
+
*
|
|
4
|
+
* Every source module exports `evaluate(source)` (the poll model — one snapshot,
|
|
5
|
+
* or null when nothing is observable this tick / the source is push-only) and,
|
|
6
|
+
* where push-based (ws, file-follow), a `subscribe(source, onObs)` returning an
|
|
7
|
+
* unsubscribe fn.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Webhooks are push-based (an inbound signed HTTP delivery), so the poll-model
|
|
5
|
+
* `evaluate` returns null. When a delivery arrives, the receiver matches it
|
|
6
|
+
* against monitors with `matchWebhook`, which reuses the github/linear matchers
|
|
7
|
+
* from lib/triggers/webhook.ts by projecting the monitor's webhook filters onto a
|
|
8
|
+
* synthesized JobConfig-shaped trigger.
|
|
9
|
+
*/
|
|
10
|
+
import { type IncomingWebhook } from '../../triggers/webhook.js';
|
|
11
|
+
import type { JobTrigger } from '../../routines.js';
|
|
12
|
+
import type { MonitorSource, MonitorConfig } from '../config.js';
|
|
13
|
+
import type { Observation } from './types.js';
|
|
14
|
+
/** Push-only: nothing to snapshot on a poll tick. */
|
|
15
|
+
export declare function evaluate(_source: MonitorSource): Promise<Observation | null>;
|
|
16
|
+
/** Project a monitor's webhook source onto a JobTrigger for the shared matcher. */
|
|
17
|
+
export declare function monitorTrigger(source: MonitorSource): JobTrigger | null;
|
|
18
|
+
/**
|
|
19
|
+
* True when an incoming webhook matches this monitor's webhook source — reuses
|
|
20
|
+
* the routines matcher (lib/triggers/webhook.ts) so github/linear filter
|
|
21
|
+
* semantics stay in one place.
|
|
22
|
+
*/
|
|
23
|
+
export declare function matchWebhook(monitor: MonitorConfig, webhook: IncomingWebhook): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Webhooks are push-based (an inbound signed HTTP delivery), so the poll-model
|
|
5
|
+
* `evaluate` returns null. When a delivery arrives, the receiver matches it
|
|
6
|
+
* against monitors with `matchWebhook`, which reuses the github/linear matchers
|
|
7
|
+
* from lib/triggers/webhook.ts by projecting the monitor's webhook filters onto a
|
|
8
|
+
* synthesized JobConfig-shaped trigger.
|
|
9
|
+
*/
|
|
10
|
+
import { jobMatchesWebhook } from '../../triggers/webhook.js';
|
|
11
|
+
/** Push-only: nothing to snapshot on a poll tick. */
|
|
12
|
+
export function evaluate(_source) {
|
|
13
|
+
return Promise.resolve(null);
|
|
14
|
+
}
|
|
15
|
+
/** Project a monitor's webhook source onto a JobTrigger for the shared matcher. */
|
|
16
|
+
export function monitorTrigger(source) {
|
|
17
|
+
const w = source.webhook;
|
|
18
|
+
if (!w)
|
|
19
|
+
return null;
|
|
20
|
+
if (w.source === 'github') {
|
|
21
|
+
return {
|
|
22
|
+
type: 'github_event',
|
|
23
|
+
event: w.event,
|
|
24
|
+
...(w.repo ? { repo: w.repo } : {}),
|
|
25
|
+
...(w.branch ? { branch: w.branch } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
type: 'linear_event',
|
|
30
|
+
event: w.event,
|
|
31
|
+
...(w.action ? { action: w.action } : {}),
|
|
32
|
+
...(w.teamKey ? { teamKey: w.teamKey } : {}),
|
|
33
|
+
...(w.label ? { label: w.label } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True when an incoming webhook matches this monitor's webhook source — reuses
|
|
38
|
+
* the routines matcher (lib/triggers/webhook.ts) so github/linear filter
|
|
39
|
+
* semantics stay in one place.
|
|
40
|
+
*/
|
|
41
|
+
export function matchWebhook(monitor, webhook) {
|
|
42
|
+
const trigger = monitorTrigger(monitor.source);
|
|
43
|
+
if (!trigger)
|
|
44
|
+
return false;
|
|
45
|
+
const shim = { name: monitor.name, trigger };
|
|
46
|
+
return jobMatchesWebhook(shim, webhook);
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* ws is push-based: each frame is an observation. The poll-model `evaluate`
|
|
5
|
+
* returns null (there is nothing to snapshot on a tick); real observations arrive
|
|
6
|
+
* through `subscribe`, which opens a persistent client (the same `ws` client
|
|
7
|
+
* lib/browser/cdp.ts uses — not the platform undici WebSocket).
|
|
8
|
+
*/
|
|
9
|
+
import type { MonitorSource } from '../config.js';
|
|
10
|
+
import type { Observation } from './types.js';
|
|
11
|
+
/** Push-only: nothing to snapshot on a poll tick. */
|
|
12
|
+
export declare function evaluate(_source: MonitorSource): Promise<Observation | null>;
|
|
13
|
+
/** Open a persistent WebSocket; emit each received frame as an observation. */
|
|
14
|
+
export declare function subscribe(source: MonitorSource, onObs: (obs: Observation) => void): () => void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket source evaluator.
|
|
3
|
+
*
|
|
4
|
+
* ws is push-based: each frame is an observation. The poll-model `evaluate`
|
|
5
|
+
* returns null (there is nothing to snapshot on a tick); real observations arrive
|
|
6
|
+
* through `subscribe`, which opens a persistent client (the same `ws` client
|
|
7
|
+
* lib/browser/cdp.ts uses — not the platform undici WebSocket).
|
|
8
|
+
*/
|
|
9
|
+
import WSWebSocket from 'ws';
|
|
10
|
+
/** Push-only: nothing to snapshot on a poll tick. */
|
|
11
|
+
export function evaluate(_source) {
|
|
12
|
+
return Promise.resolve(null);
|
|
13
|
+
}
|
|
14
|
+
/** Open a persistent WebSocket; emit each received frame as an observation. */
|
|
15
|
+
export function subscribe(source, onObs) {
|
|
16
|
+
const url = source.wsUrl;
|
|
17
|
+
if (!url)
|
|
18
|
+
return () => { };
|
|
19
|
+
let closed = false;
|
|
20
|
+
let socket = null;
|
|
21
|
+
const connect = () => {
|
|
22
|
+
if (closed)
|
|
23
|
+
return;
|
|
24
|
+
socket = new WSWebSocket(url, { maxPayload: 8 * 1024 * 1024 });
|
|
25
|
+
socket.onmessage = (ev) => onObs({ raw: String(ev.data), meta: { kind: 'frame' } });
|
|
26
|
+
socket.onclose = () => {
|
|
27
|
+
if (!closed)
|
|
28
|
+
setTimeout(connect, 5_000); // reconnect on drop
|
|
29
|
+
};
|
|
30
|
+
socket.onerror = () => {
|
|
31
|
+
try {
|
|
32
|
+
socket?.close();
|
|
33
|
+
}
|
|
34
|
+
catch { /* already closing */ }
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
connect();
|
|
38
|
+
return () => {
|
|
39
|
+
closed = true;
|
|
40
|
+
try {
|
|
41
|
+
socket?.close();
|
|
42
|
+
}
|
|
43
|
+
catch { /* already closed */ }
|
|
44
|
+
};
|
|
45
|
+
}
|