opencode-waitfor 0.1.0 → 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,275 @@
1
+ // src/index.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/format.ts
5
+ function formatResult(input) {
6
+ const { type, target, poll } = input;
7
+ const elapsed_seconds = Math.round(poll.elapsedMs / 1000 * 100) / 100;
8
+ const metadata = {
9
+ success: poll.success,
10
+ type,
11
+ target,
12
+ elapsed_seconds,
13
+ attempts: poll.attempts,
14
+ last: poll.last
15
+ };
16
+ if (!poll.success)
17
+ metadata.reason = "timeout";
18
+ const title = poll.success ? `${type} target ready after ${elapsed_seconds}s` : `${type} target NOT ready (timed out after ${elapsed_seconds}s)`;
19
+ const output = poll.success ? `Target "${target}" became ready after ${poll.attempts} attempt(s), ${elapsed_seconds}s.
20
+ Last observation: ${JSON.stringify(poll.last)}` : `Timed out waiting for "${target}" after ${poll.attempts} attempt(s), ${elapsed_seconds}s.
21
+ Last observation: ${JSON.stringify(poll.last)}`;
22
+ return { title, output, metadata };
23
+ }
24
+
25
+ // src/poll.ts
26
+ function sleep(ms, signal) {
27
+ return new Promise((res) => {
28
+ if (signal.aborted)
29
+ return res();
30
+ const onAbort = () => {
31
+ clearTimeout(t);
32
+ signal.removeEventListener("abort", onAbort);
33
+ res();
34
+ };
35
+ const t = setTimeout(() => {
36
+ signal.removeEventListener("abort", onAbort);
37
+ res();
38
+ }, ms);
39
+ signal.addEventListener("abort", onAbort);
40
+ });
41
+ }
42
+ async function poll(opts) {
43
+ const { probe, timeoutMs, intervalMs, signal: external } = opts;
44
+ const controller = new AbortController;
45
+ const onExternalAbort = () => controller.abort();
46
+ if (external) {
47
+ if (external.aborted)
48
+ controller.abort();
49
+ else
50
+ external.addEventListener("abort", onExternalAbort);
51
+ }
52
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
53
+ const start = Date.now();
54
+ let attempts = 0;
55
+ let last = {};
56
+ try {
57
+ while (true) {
58
+ if (controller.signal.aborted)
59
+ break;
60
+ attempts++;
61
+ const result = await probe(controller.signal);
62
+ last = result.snapshot;
63
+ if (result.ok) {
64
+ return { success: true, attempts, elapsedMs: Date.now() - start, last };
65
+ }
66
+ if (controller.signal.aborted)
67
+ break;
68
+ const elapsed = Date.now() - start;
69
+ if (elapsed + intervalMs > timeoutMs)
70
+ break;
71
+ await sleep(intervalMs, controller.signal);
72
+ }
73
+ return { success: false, attempts, elapsedMs: Date.now() - start, last };
74
+ } finally {
75
+ clearTimeout(timer);
76
+ if (external)
77
+ external.removeEventListener("abort", onExternalAbort);
78
+ }
79
+ }
80
+
81
+ // src/probes/command.ts
82
+ import { exec } from "node:child_process";
83
+ function commandProbe(command, expect, opts) {
84
+ const wanted = expect?.exit_code ?? 0;
85
+ return (signal) => {
86
+ return new Promise((res) => {
87
+ const child = exec(command, { cwd: opts.cwd, timeout: opts.timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
88
+ signal.removeEventListener("abort", onAbort);
89
+ const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
90
+ const snapshot = {
91
+ exitCode: code,
92
+ stdoutPreview: String(stdout).slice(0, opts.maxBytes),
93
+ stderrPreview: String(stderr).slice(0, opts.maxBytes)
94
+ };
95
+ res({ ok: code === wanted, snapshot });
96
+ });
97
+ const onAbort = () => child.kill();
98
+ if (signal.aborted)
99
+ child.kill();
100
+ else
101
+ signal.addEventListener("abort", onAbort);
102
+ });
103
+ };
104
+ }
105
+
106
+ // src/dotpath.ts
107
+ function resolve(obj, path) {
108
+ const segments = path.split(".");
109
+ let current = obj;
110
+ for (const seg of segments) {
111
+ if (current === null || current === undefined)
112
+ return;
113
+ if (Array.isArray(current)) {
114
+ if (!/^\d+$/.test(seg))
115
+ return;
116
+ current = current[Number(seg)];
117
+ } else if (typeof current === "object") {
118
+ current = current[seg];
119
+ } else {
120
+ return;
121
+ }
122
+ }
123
+ return current;
124
+ }
125
+
126
+ // src/probes/http.ts
127
+ function statusOk(status, expect) {
128
+ if (!expect || expect.status === undefined)
129
+ return status >= 200 && status < 300;
130
+ const wanted = Array.isArray(expect.status) ? expect.status : [expect.status];
131
+ return wanted.includes(status);
132
+ }
133
+ function httpProbe(target, expect, opts) {
134
+ return async (signal) => {
135
+ const ac = new AbortController;
136
+ const onAbort = () => ac.abort();
137
+ if (signal.aborted)
138
+ ac.abort();
139
+ else
140
+ signal.addEventListener("abort", onAbort);
141
+ const timer = setTimeout(() => ac.abort(), opts.perAttemptMs);
142
+ try {
143
+ const res = await fetch(target, { signal: ac.signal });
144
+ const text = await res.text();
145
+ const bodyPreview = text.slice(0, opts.maxBytes);
146
+ const snapshot = { status: res.status, bodyPreview };
147
+ if (!statusOk(res.status, expect))
148
+ return { ok: false, snapshot };
149
+ if (expect?.json_match) {
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(text);
153
+ } catch {
154
+ return { ok: false, snapshot: { ...snapshot, error: "response not JSON" } };
155
+ }
156
+ for (const [path, want] of Object.entries(expect.json_match)) {
157
+ if (String(resolve(parsed, path)) !== String(want)) {
158
+ return { ok: false, snapshot };
159
+ }
160
+ }
161
+ }
162
+ return { ok: true, snapshot };
163
+ } catch (e) {
164
+ return { ok: false, snapshot: { error: e.message } };
165
+ } finally {
166
+ clearTimeout(timer);
167
+ signal.removeEventListener("abort", onAbort);
168
+ }
169
+ };
170
+ }
171
+
172
+ // src/probes/tcp.ts
173
+ import { Socket } from "node:net";
174
+ function tcpProbe(host, port, opts) {
175
+ return (signal) => {
176
+ return new Promise((res) => {
177
+ const socket = new Socket;
178
+ let settled = false;
179
+ const done = (result) => {
180
+ if (settled)
181
+ return;
182
+ settled = true;
183
+ clearTimeout(timer);
184
+ signal.removeEventListener("abort", onAbort);
185
+ socket.destroy();
186
+ res(result);
187
+ };
188
+ const onAbort = () => done({ ok: false, snapshot: { connected: false, error: "aborted" } });
189
+ const timer = setTimeout(() => done({ ok: false, snapshot: { connected: false, error: "timeout" } }), opts.perAttemptMs);
190
+ if (signal.aborted)
191
+ return onAbort();
192
+ signal.addEventListener("abort", onAbort);
193
+ socket.once("connect", () => done({ ok: true, snapshot: { connected: true } }));
194
+ socket.once("error", (e) => done({ ok: false, snapshot: { connected: false, error: e.message } }));
195
+ socket.connect(port, host);
196
+ });
197
+ };
198
+ }
199
+
200
+ // src/infer.ts
201
+ function inferType(target) {
202
+ if (target.includes("://"))
203
+ return "http";
204
+ if (/^[\w.-]+:\d+$/.test(target))
205
+ return "tcp";
206
+ return "command";
207
+ }
208
+
209
+ // src/validate.ts
210
+ function validate(args) {
211
+ const timeout = args.timeout ?? 60;
212
+ const interval = args.interval ?? 2;
213
+ if (timeout <= 0)
214
+ throw new Error("wait_for: timeout must be > 0 seconds");
215
+ if (interval <= 0)
216
+ throw new Error("wait_for: interval must be > 0 seconds");
217
+ if (interval > timeout)
218
+ throw new Error("wait_for: interval must be <= timeout");
219
+ const type = inferType(args.target);
220
+ const e = args.expect;
221
+ if (e) {
222
+ if (e.exit_code !== undefined && type !== "command") {
223
+ throw new Error(`wait_for: expect.exit_code is only valid for command targets (inferred: ${type})`);
224
+ }
225
+ if ((e.status !== undefined || e.json_match !== undefined) && type !== "http") {
226
+ throw new Error(`wait_for: expect.status/json_match are only valid for http targets (inferred: ${type})`);
227
+ }
228
+ }
229
+ return { type, timeoutMs: timeout * 1000, intervalMs: interval * 1000 };
230
+ }
231
+
232
+ // src/index.ts
233
+ var MAX_BYTES = 2048;
234
+ var WaitForPlugin = async ({ directory }) => {
235
+ return {
236
+ tool: {
237
+ wait_for: tool({
238
+ description: "Poll a target until it is ready or a timeout elapses. " + "The condition type is inferred from `target`: a URL with scheme (http:// or https://) is polled over HTTP; " + "a bare host:port is polled as a TCP connection; anything else is run as a shell command. " + "Use this instead of writing manual sleep/curl loops. Returns success or a timeout result with the last observed state. " + "Examples: wait_for a dev server (http://localhost:3000), a deployed /health version (target http://host/health, expect.json_match {status: ok, version: <sha>}), " + "a database port (localhost:5432), or docker health (target: docker inspect -f '{{.State.Health.Status}}' pg | grep -q healthy).",
239
+ args: {
240
+ target: tool.schema.string().describe("URL (with scheme), host:port, or a shell command."),
241
+ timeout: tool.schema.number().positive().optional().describe("Total seconds to wait. Default 60."),
242
+ interval: tool.schema.number().positive().optional().describe("Seconds between attempts. Default 2."),
243
+ expect: tool.schema.object({
244
+ status: tool.schema.union([tool.schema.number(), tool.schema.array(tool.schema.number())]).optional().describe("HTTP only: acceptable status code(s). Default any 2xx."),
245
+ json_match: tool.schema.record(tool.schema.string(), tool.schema.string()).optional().describe("HTTP only: dot-path -> expected value (compared as strings)."),
246
+ exit_code: tool.schema.number().optional().describe("Command only: required exit code. Default 0.")
247
+ }).optional()
248
+ },
249
+ async execute(args, context) {
250
+ const { type, timeoutMs, intervalMs } = validate(args);
251
+ const perAttemptMs = Math.min(intervalMs, 1e4);
252
+ let probe;
253
+ if (type === "http") {
254
+ probe = httpProbe(args.target, args.expect, { perAttemptMs, maxBytes: MAX_BYTES });
255
+ } else if (type === "tcp") {
256
+ const m = args.target.match(/^([\w.-]+):(\d+)$/);
257
+ probe = tcpProbe(m[1], Number(m[2]), { perAttemptMs });
258
+ } else {
259
+ probe = commandProbe(args.target, args.expect, {
260
+ cwd: context.directory ?? directory,
261
+ maxBytes: MAX_BYTES,
262
+ timeoutMs
263
+ });
264
+ }
265
+ const result = await poll({ probe, timeoutMs, intervalMs, signal: context.abort });
266
+ return formatResult({ type, target: args.target, poll: result });
267
+ }
268
+ })
269
+ }
270
+ };
271
+ };
272
+ var src_default = WaitForPlugin;
273
+ export {
274
+ src_default as default
275
+ };
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "opencode-waitfor",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "opencode plugin: a wait_for tool that polls HTTP/TCP/command targets until ready or timeout.",
5
5
  "type": "module",
6
- "main": "src/index.ts",
7
- "exports": { ".": "./src/index.ts" },
8
- "files": ["src", "README.md", "LICENSE"],
6
+ "main": "./dist/index.js",
7
+ "exports": { ".": "./dist/index.js" },
8
+ "files": ["dist", "README.md", "LICENSE"],
9
9
  "keywords": ["opencode", "opencode-plugin", "wait", "healthcheck", "poll"],
10
10
  "license": "MIT",
11
- "peerDependencies": { "@opencode-ai/plugin": "*", "zod": "*" },
11
+ "dependencies": { "@opencode-ai/plugin": "^1.17.15" },
12
12
  "devDependencies": {
13
- "@opencode-ai/plugin": "^1.17.18",
14
13
  "@types/node": "^22.0.0",
15
- "typescript": "^5.6.0",
16
- "zod": "^3.23.0"
14
+ "typescript": "^5.6.0"
17
15
  },
18
16
  "scripts": {
17
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external '@opencode-ai/*'",
18
+ "prepublishOnly": "bun run build",
19
19
  "test": "bun test",
20
20
  "typecheck": "tsc --noEmit"
21
21
  }
package/src/dotpath.ts DELETED
@@ -1,16 +0,0 @@
1
- export function resolve(obj: unknown, path: string): unknown {
2
- const segments = path.split(".")
3
- let current: unknown = obj
4
- for (const seg of segments) {
5
- if (current === null || current === undefined) return undefined
6
- if (Array.isArray(current)) {
7
- if (!/^\d+$/.test(seg)) return undefined
8
- current = current[Number(seg)]
9
- } else if (typeof current === "object") {
10
- current = (current as Record<string, unknown>)[seg]
11
- } else {
12
- return undefined
13
- }
14
- }
15
- return current
16
- }
package/src/format.ts DELETED
@@ -1,29 +0,0 @@
1
- import type { WaitType } from "./infer"
2
- import type { PollResult } from "./poll"
3
-
4
- export function formatResult(input: {
5
- type: WaitType
6
- target: string
7
- poll: PollResult
8
- }): { title: string; output: string; metadata: Record<string, unknown> } {
9
- const { type, target, poll } = input
10
- const elapsed_seconds = Math.round((poll.elapsedMs / 1000) * 100) / 100
11
- const metadata: Record<string, unknown> = {
12
- success: poll.success,
13
- type,
14
- target,
15
- elapsed_seconds,
16
- attempts: poll.attempts,
17
- last: poll.last,
18
- }
19
- if (!poll.success) metadata.reason = "timeout"
20
-
21
- const title = poll.success
22
- ? `${type} target ready after ${elapsed_seconds}s`
23
- : `${type} target NOT ready (timed out after ${elapsed_seconds}s)`
24
- const output = poll.success
25
- ? `Target "${target}" became ready after ${poll.attempts} attempt(s), ${elapsed_seconds}s.\nLast observation: ${JSON.stringify(poll.last)}`
26
- : `Timed out waiting for "${target}" after ${poll.attempts} attempt(s), ${elapsed_seconds}s.\nLast observation: ${JSON.stringify(poll.last)}`
27
-
28
- return { title, output, metadata }
29
- }
package/src/index.ts DELETED
@@ -1,79 +0,0 @@
1
- import { type Plugin, tool } from "@opencode-ai/plugin"
2
- import { formatResult } from "./format"
3
- import { poll, type Probe } from "./poll"
4
- import { commandProbe } from "./probes/command"
5
- import { httpProbe } from "./probes/http"
6
- import { tcpProbe } from "./probes/tcp"
7
- import { validate, type WaitArgs } from "./validate"
8
-
9
- const MAX_BYTES = 2048
10
-
11
- const WaitForPlugin: Plugin = async ({ directory }) => {
12
- return {
13
- tool: {
14
- wait_for: tool({
15
- description:
16
- "Poll a target until it is ready or a timeout elapses. " +
17
- "The condition type is inferred from `target`: a URL with scheme (http:// or https://) is polled over HTTP; " +
18
- "a bare host:port is polled as a TCP connection; anything else is run as a shell command. " +
19
- "Use this instead of writing manual sleep/curl loops. Returns success or a timeout result with the last observed state. " +
20
- "Examples: wait_for a dev server (http://localhost:3000), a deployed /health version (target http://host/health, expect.json_match {status: ok, version: <sha>}), " +
21
- "a database port (localhost:5432), or docker health (target: docker inspect -f '{{.State.Health.Status}}' pg | grep -q healthy).",
22
- args: {
23
- target: tool.schema
24
- .string()
25
- .describe("URL (with scheme), host:port, or a shell command."),
26
- timeout: tool.schema
27
- .number()
28
- .positive()
29
- .optional()
30
- .describe("Total seconds to wait. Default 60."),
31
- interval: tool.schema
32
- .number()
33
- .positive()
34
- .optional()
35
- .describe("Seconds between attempts. Default 2."),
36
- expect: tool.schema
37
- .object({
38
- status: tool.schema
39
- .union([tool.schema.number(), tool.schema.array(tool.schema.number())])
40
- .optional()
41
- .describe("HTTP only: acceptable status code(s). Default any 2xx."),
42
- json_match: tool.schema
43
- .record(tool.schema.string(), tool.schema.string())
44
- .optional()
45
- .describe("HTTP only: dot-path -> expected value (compared as strings)."),
46
- exit_code: tool.schema
47
- .number()
48
- .optional()
49
- .describe("Command only: required exit code. Default 0."),
50
- })
51
- .optional(),
52
- },
53
- async execute(args, context) {
54
- const { type, timeoutMs, intervalMs } = validate(args as WaitArgs)
55
- const perAttemptMs = Math.min(intervalMs, 10000)
56
-
57
- let probe: Probe
58
- if (type === "http") {
59
- probe = httpProbe(args.target, args.expect, { perAttemptMs, maxBytes: MAX_BYTES })
60
- } else if (type === "tcp") {
61
- const m = args.target.match(/^([\w.-]+):(\d+)$/)!
62
- probe = tcpProbe(m[1], Number(m[2]), { perAttemptMs })
63
- } else {
64
- probe = commandProbe(args.target, args.expect, {
65
- cwd: context.directory ?? directory,
66
- maxBytes: MAX_BYTES,
67
- timeoutMs,
68
- })
69
- }
70
-
71
- const result = await poll({ probe, timeoutMs, intervalMs, signal: context.abort })
72
- return formatResult({ type, target: args.target, poll: result })
73
- },
74
- }),
75
- },
76
- }
77
- }
78
-
79
- export default WaitForPlugin
package/src/infer.ts DELETED
@@ -1,7 +0,0 @@
1
- export type WaitType = "http" | "tcp" | "command"
2
-
3
- export function inferType(target: string): WaitType {
4
- if (target.includes("://")) return "http"
5
- if (/^[\w.-]+:\d+$/.test(target)) return "tcp"
6
- return "command"
7
- }
package/src/poll.ts DELETED
@@ -1,66 +0,0 @@
1
- export interface ProbeResult {
2
- ok: boolean
3
- snapshot: Record<string, unknown>
4
- }
5
- export type Probe = (signal: AbortSignal) => Promise<ProbeResult>
6
- export interface PollResult {
7
- success: boolean
8
- attempts: number
9
- elapsedMs: number
10
- last: Record<string, unknown>
11
- }
12
-
13
- function sleep(ms: number, signal: AbortSignal): Promise<void> {
14
- return new Promise((res) => {
15
- if (signal.aborted) return res()
16
- const onAbort = () => {
17
- clearTimeout(t)
18
- signal.removeEventListener("abort", onAbort)
19
- res()
20
- }
21
- const t = setTimeout(() => {
22
- signal.removeEventListener("abort", onAbort)
23
- res()
24
- }, ms)
25
- signal.addEventListener("abort", onAbort)
26
- })
27
- }
28
-
29
- export async function poll(opts: {
30
- probe: Probe
31
- timeoutMs: number
32
- intervalMs: number
33
- signal?: AbortSignal
34
- }): Promise<PollResult> {
35
- const { probe, timeoutMs, intervalMs, signal: external } = opts
36
- const controller = new AbortController()
37
- const onExternalAbort = () => controller.abort()
38
- if (external) {
39
- if (external.aborted) controller.abort()
40
- else external.addEventListener("abort", onExternalAbort)
41
- }
42
- const timer = setTimeout(() => controller.abort(), timeoutMs)
43
- const start = Date.now()
44
- let attempts = 0
45
- let last: Record<string, unknown> = {}
46
-
47
- try {
48
- while (true) {
49
- if (controller.signal.aborted) break
50
- attempts++
51
- const result = await probe(controller.signal)
52
- last = result.snapshot
53
- if (result.ok) {
54
- return { success: true, attempts, elapsedMs: Date.now() - start, last }
55
- }
56
- if (controller.signal.aborted) break
57
- const elapsed = Date.now() - start
58
- if (elapsed + intervalMs > timeoutMs) break
59
- await sleep(intervalMs, controller.signal)
60
- }
61
- return { success: false, attempts, elapsedMs: Date.now() - start, last }
62
- } finally {
63
- clearTimeout(timer)
64
- if (external) external.removeEventListener("abort", onExternalAbort)
65
- }
66
- }
@@ -1,40 +0,0 @@
1
- import { exec } from "node:child_process"
2
- import type { Probe, ProbeResult } from "../poll"
3
-
4
- export interface CommandExpect {
5
- exit_code?: number
6
- }
7
-
8
- export function commandProbe(
9
- command: string,
10
- expect: CommandExpect | undefined,
11
- opts: { cwd: string; maxBytes: number; timeoutMs: number },
12
- ): Probe {
13
- const wanted = expect?.exit_code ?? 0
14
- return (signal: AbortSignal): Promise<ProbeResult> => {
15
- return new Promise<ProbeResult>((res) => {
16
- const child = exec(
17
- command,
18
- { cwd: opts.cwd, timeout: opts.timeoutMs, maxBuffer: 1024 * 1024 },
19
- (err, stdout, stderr) => {
20
- signal.removeEventListener("abort", onAbort)
21
- const code =
22
- err && typeof (err as { code?: unknown }).code === "number"
23
- ? (err as { code: number }).code
24
- : err
25
- ? 1
26
- : 0
27
- const snapshot = {
28
- exitCode: code,
29
- stdoutPreview: String(stdout).slice(0, opts.maxBytes),
30
- stderrPreview: String(stderr).slice(0, opts.maxBytes),
31
- }
32
- res({ ok: code === wanted, snapshot })
33
- },
34
- )
35
- const onAbort = () => child.kill()
36
- if (signal.aborted) child.kill()
37
- else signal.addEventListener("abort", onAbort)
38
- })
39
- }
40
- }
@@ -1,53 +0,0 @@
1
- import type { Probe, ProbeResult } from "../poll"
2
- import { resolve } from "../dotpath"
3
-
4
- export interface HttpExpect {
5
- status?: number | number[]
6
- json_match?: Record<string, string>
7
- }
8
-
9
- function statusOk(status: number, expect: HttpExpect | undefined): boolean {
10
- if (!expect || expect.status === undefined) return status >= 200 && status < 300
11
- const wanted = Array.isArray(expect.status) ? expect.status : [expect.status]
12
- return wanted.includes(status)
13
- }
14
-
15
- export function httpProbe(
16
- target: string,
17
- expect: HttpExpect | undefined,
18
- opts: { perAttemptMs: number; maxBytes: number },
19
- ): Probe {
20
- return async (signal: AbortSignal): Promise<ProbeResult> => {
21
- const ac = new AbortController()
22
- const onAbort = () => ac.abort()
23
- if (signal.aborted) ac.abort()
24
- else signal.addEventListener("abort", onAbort)
25
- const timer = setTimeout(() => ac.abort(), opts.perAttemptMs)
26
- try {
27
- const res = await fetch(target, { signal: ac.signal })
28
- const text = await res.text()
29
- const bodyPreview = text.slice(0, opts.maxBytes)
30
- const snapshot: Record<string, unknown> = { status: res.status, bodyPreview }
31
- if (!statusOk(res.status, expect)) return { ok: false, snapshot }
32
- if (expect?.json_match) {
33
- let parsed: unknown
34
- try {
35
- parsed = JSON.parse(text)
36
- } catch {
37
- return { ok: false, snapshot: { ...snapshot, error: "response not JSON" } }
38
- }
39
- for (const [path, want] of Object.entries(expect.json_match)) {
40
- if (String(resolve(parsed, path)) !== String(want)) {
41
- return { ok: false, snapshot }
42
- }
43
- }
44
- }
45
- return { ok: true, snapshot }
46
- } catch (e) {
47
- return { ok: false, snapshot: { error: (e as Error).message } }
48
- } finally {
49
- clearTimeout(timer)
50
- signal.removeEventListener("abort", onAbort)
51
- }
52
- }
53
- }
package/src/probes/tcp.ts DELETED
@@ -1,35 +0,0 @@
1
- import { Socket } from "node:net"
2
- import type { Probe, ProbeResult } from "../poll"
3
-
4
- export function tcpProbe(
5
- host: string,
6
- port: number,
7
- opts: { perAttemptMs: number },
8
- ): Probe {
9
- return (signal: AbortSignal): Promise<ProbeResult> => {
10
- return new Promise<ProbeResult>((res) => {
11
- const socket = new Socket()
12
- let settled = false
13
- const done = (result: ProbeResult) => {
14
- if (settled) return
15
- settled = true
16
- clearTimeout(timer)
17
- signal.removeEventListener("abort", onAbort)
18
- socket.destroy()
19
- res(result)
20
- }
21
- const onAbort = () => done({ ok: false, snapshot: { connected: false, error: "aborted" } })
22
- const timer = setTimeout(
23
- () => done({ ok: false, snapshot: { connected: false, error: "timeout" } }),
24
- opts.perAttemptMs,
25
- )
26
- if (signal.aborted) return onAbort()
27
- signal.addEventListener("abort", onAbort)
28
- socket.once("connect", () => done({ ok: true, snapshot: { connected: true } }))
29
- socket.once("error", (e) =>
30
- done({ ok: false, snapshot: { connected: false, error: (e as Error).message } }),
31
- )
32
- socket.connect(port, host)
33
- })
34
- }
35
- }
package/src/validate.ts DELETED
@@ -1,36 +0,0 @@
1
- import { inferType, type WaitType } from "./infer"
2
-
3
- export interface WaitArgs {
4
- target: string
5
- timeout?: number
6
- interval?: number
7
- expect?: {
8
- status?: number | number[]
9
- json_match?: Record<string, string>
10
- exit_code?: number
11
- }
12
- }
13
-
14
- export function validate(args: WaitArgs): {
15
- type: WaitType
16
- timeoutMs: number
17
- intervalMs: number
18
- } {
19
- const timeout = args.timeout ?? 60
20
- const interval = args.interval ?? 2
21
- if (timeout <= 0) throw new Error("wait_for: timeout must be > 0 seconds")
22
- if (interval <= 0) throw new Error("wait_for: interval must be > 0 seconds")
23
- if (interval > timeout) throw new Error("wait_for: interval must be <= timeout")
24
-
25
- const type = inferType(args.target)
26
- const e = args.expect
27
- if (e) {
28
- if (e.exit_code !== undefined && type !== "command") {
29
- throw new Error(`wait_for: expect.exit_code is only valid for command targets (inferred: ${type})`)
30
- }
31
- if ((e.status !== undefined || e.json_match !== undefined) && type !== "http") {
32
- throw new Error(`wait_for: expect.status/json_match are only valid for http targets (inferred: ${type})`)
33
- }
34
- }
35
- return { type, timeoutMs: timeout * 1000, intervalMs: interval * 1000 }
36
- }