@vostride/agent-qa-plugin-kit 0.0.1 → 0.3.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/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/README.md +78 -0
- package/dist/index.cjs +814 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +241 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +800 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +323 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +259 -0
- package/dist/protocol.d.ts +259 -0
- package/dist/protocol.js +291 -0
- package/dist/protocol.js.map +1 -0
- package/dist/scripted-plugin.js +985 -0
- package/dist/scripted-plugin.js.map +1 -0
- package/dist/testing.cjs +973 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +236 -0
- package/dist/testing.d.ts +236 -0
- package/dist/testing.js +969 -0
- package/dist/testing.js.map +1 -0
- package/package.json +80 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { z, z as z$1 } from "zod";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { format, inspect, isDeepStrictEqual } from "node:util";
|
|
5
|
+
// src/credential.ts
|
|
6
|
+
function credential(description) {
|
|
7
|
+
return z$1.string().meta({
|
|
8
|
+
credential: true,
|
|
9
|
+
...description ? { description } : {}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
// src/protocol/messages.ts
|
|
13
|
+
const PLUGIN_METHODS = [
|
|
14
|
+
"initialize",
|
|
15
|
+
"check",
|
|
16
|
+
"acquire",
|
|
17
|
+
"uploadApp",
|
|
18
|
+
"started",
|
|
19
|
+
"end",
|
|
20
|
+
"release",
|
|
21
|
+
"shutdown"
|
|
22
|
+
];
|
|
23
|
+
const OPTIONAL_PLUGIN_METHODS = [
|
|
24
|
+
"check",
|
|
25
|
+
"uploadApp",
|
|
26
|
+
"started",
|
|
27
|
+
"end"
|
|
28
|
+
];
|
|
29
|
+
const PLUGIN_SERVES = [
|
|
30
|
+
"playwright",
|
|
31
|
+
"cdp",
|
|
32
|
+
"appium"
|
|
33
|
+
];
|
|
34
|
+
const PLUGIN_RUN_FACTS = [
|
|
35
|
+
"testName",
|
|
36
|
+
"runId",
|
|
37
|
+
"failure"
|
|
38
|
+
];
|
|
39
|
+
const PLUGIN_FAILURE_KINDS = [
|
|
40
|
+
"unreachable",
|
|
41
|
+
"auth",
|
|
42
|
+
"capacity",
|
|
43
|
+
"timeout",
|
|
44
|
+
"version",
|
|
45
|
+
"refused",
|
|
46
|
+
"ended",
|
|
47
|
+
"lost"
|
|
48
|
+
];
|
|
49
|
+
const PLUGIN_ERROR_CODES = {
|
|
50
|
+
parse: -32700,
|
|
51
|
+
invalidRequest: -32600,
|
|
52
|
+
methodNotFound: -32601,
|
|
53
|
+
invalidParams: -32602,
|
|
54
|
+
plugin: -32e3,
|
|
55
|
+
cancelled: -32800
|
|
56
|
+
};
|
|
57
|
+
const PROGRESS_METHOD = "$/progress";
|
|
58
|
+
const MAX_MESSAGE_BYTES = 1048576;
|
|
59
|
+
const JsonObjectSchema = z$1.record(z$1.string(), z$1.unknown());
|
|
60
|
+
const HandleSchema = z$1.json();
|
|
61
|
+
const RequestIdSchema = z$1.union([z$1.string(), z$1.number()]);
|
|
62
|
+
const InitializeParamsSchema = z$1.object({
|
|
63
|
+
protocolVersions: z$1.array(z$1.number().int()),
|
|
64
|
+
host: z$1.object({
|
|
65
|
+
pid: z$1.number().int(),
|
|
66
|
+
agentQaVersion: z$1.string(),
|
|
67
|
+
playwrightVersion: z$1.string(),
|
|
68
|
+
webdriverioVersion: z$1.string()
|
|
69
|
+
}),
|
|
70
|
+
entry: z$1.string(),
|
|
71
|
+
options: JsonObjectSchema,
|
|
72
|
+
runFacts: z$1.object({ runId: z$1.string().optional() }).optional()
|
|
73
|
+
});
|
|
74
|
+
z$1.object({
|
|
75
|
+
protocolVersion: z$1.number().int(),
|
|
76
|
+
plugin: z$1.object({
|
|
77
|
+
name: z$1.string(),
|
|
78
|
+
version: z$1.string()
|
|
79
|
+
}),
|
|
80
|
+
serves: z$1.array(z$1.enum(PLUGIN_SERVES)),
|
|
81
|
+
methods: z$1.array(z$1.enum(OPTIONAL_PLUGIN_METHODS))
|
|
82
|
+
});
|
|
83
|
+
const CheckParamsSchema = z$1.object({ remotes: z$1.array(z$1.object({
|
|
84
|
+
name: z$1.string(),
|
|
85
|
+
settings: JsonObjectSchema
|
|
86
|
+
})) });
|
|
87
|
+
const CheckResultSchema = z$1.object({
|
|
88
|
+
state: z$1.enum(["ok", "warn"]),
|
|
89
|
+
lines: z$1.array(z$1.string())
|
|
90
|
+
});
|
|
91
|
+
const WebSessionSchema = z$1.object({
|
|
92
|
+
platform: z$1.literal("web"),
|
|
93
|
+
engine: z$1.enum([
|
|
94
|
+
"chromium",
|
|
95
|
+
"firefox",
|
|
96
|
+
"webkit"
|
|
97
|
+
]),
|
|
98
|
+
context: z$1.enum(["new", "reuse"])
|
|
99
|
+
});
|
|
100
|
+
const DeviceSessionSchema = z$1.object({
|
|
101
|
+
platform: z$1.enum(["android", "ios"]),
|
|
102
|
+
capabilities: JsonObjectSchema
|
|
103
|
+
});
|
|
104
|
+
const AcquireParamsSchema = z$1.object({
|
|
105
|
+
remote: z$1.string(),
|
|
106
|
+
settings: JsonObjectSchema,
|
|
107
|
+
session: z$1.discriminatedUnion("platform", [WebSessionSchema, DeviceSessionSchema]),
|
|
108
|
+
run: z$1.object({ testName: z$1.string().optional() }).optional()
|
|
109
|
+
});
|
|
110
|
+
const AcquireResultSchema = z$1.object({
|
|
111
|
+
handle: HandleSchema,
|
|
112
|
+
protocol: z$1.enum(PLUGIN_SERVES),
|
|
113
|
+
url: z$1.string(),
|
|
114
|
+
headers: z$1.record(z$1.string(), z$1.string()).optional(),
|
|
115
|
+
capabilities: JsonObjectSchema.optional(),
|
|
116
|
+
mask: z$1.array(z$1.string()).optional(),
|
|
117
|
+
link: z$1.string().optional()
|
|
118
|
+
});
|
|
119
|
+
const UploadAppParamsSchema = z$1.object({
|
|
120
|
+
remote: z$1.string(),
|
|
121
|
+
settings: JsonObjectSchema,
|
|
122
|
+
platform: z$1.enum(["android", "ios"]),
|
|
123
|
+
path: z$1.string(),
|
|
124
|
+
sha256: z$1.string(),
|
|
125
|
+
size: z$1.number().int().nonnegative()
|
|
126
|
+
});
|
|
127
|
+
const UploadAppResultSchema = z$1.object({ ref: z$1.string() });
|
|
128
|
+
const StartedParamsSchema = z$1.object({
|
|
129
|
+
handle: HandleSchema,
|
|
130
|
+
sessionId: z$1.string().optional()
|
|
131
|
+
});
|
|
132
|
+
const StartedResultSchema = z$1.object({
|
|
133
|
+
link: z$1.string().optional(),
|
|
134
|
+
appiumVersion: z$1.string().optional()
|
|
135
|
+
});
|
|
136
|
+
const EndParamsSchema = z$1.object({
|
|
137
|
+
handle: HandleSchema,
|
|
138
|
+
outcome: z$1.enum([
|
|
139
|
+
"passed",
|
|
140
|
+
"failed",
|
|
141
|
+
"error",
|
|
142
|
+
"cancelled"
|
|
143
|
+
]),
|
|
144
|
+
reason: z$1.string().optional()
|
|
145
|
+
});
|
|
146
|
+
const ReleaseParamsSchema = z$1.object({ handle: HandleSchema });
|
|
147
|
+
const CancelParamsSchema = z$1.object({ id: RequestIdSchema });
|
|
148
|
+
z$1.object({
|
|
149
|
+
id: RequestIdSchema,
|
|
150
|
+
message: z$1.string(),
|
|
151
|
+
queued: z$1.number().int().nonnegative().optional(),
|
|
152
|
+
percent: z$1.number().min(0).max(100).optional()
|
|
153
|
+
});
|
|
154
|
+
z$1.object({
|
|
155
|
+
kind: z$1.enum(PLUGIN_FAILURE_KINDS),
|
|
156
|
+
path: z$1.string().optional(),
|
|
157
|
+
handle: HandleSchema.optional(),
|
|
158
|
+
supported: z$1.array(z$1.number().int()).optional(),
|
|
159
|
+
requested: z$1.array(z$1.number().int()).optional()
|
|
160
|
+
});
|
|
161
|
+
// src/define-plugin.ts
|
|
162
|
+
var PluginError = class extends Error {
|
|
163
|
+
kind;
|
|
164
|
+
path;
|
|
165
|
+
handle;
|
|
166
|
+
constructor(message, options) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "PluginError";
|
|
169
|
+
this.kind = options.kind;
|
|
170
|
+
this.path = options.path;
|
|
171
|
+
this.handle = options.handle;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
function definePlugin(spec) {
|
|
175
|
+
const methods = OPTIONAL_PLUGIN_METHODS.filter((method) => typeof spec[method] === "function");
|
|
176
|
+
return {
|
|
177
|
+
...spec,
|
|
178
|
+
methods
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// src/package-root.ts
|
|
182
|
+
function findPackageRoot(fromDir) {
|
|
183
|
+
let dir = fromDir;
|
|
184
|
+
for (;;) {
|
|
185
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
186
|
+
const parent = dirname(dir);
|
|
187
|
+
if (parent === dir) return void 0;
|
|
188
|
+
dir = parent;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readPackageJson(packageRoot) {
|
|
192
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
193
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
194
|
+
}
|
|
195
|
+
// src/protocol/framing.ts
|
|
196
|
+
function encodeMessage(message) {
|
|
197
|
+
return `${JSON.stringify(message)}\n`;
|
|
198
|
+
}
|
|
199
|
+
const NEWLINE = 10;
|
|
200
|
+
function createLineDecoder({ maxLineBytes, onLine, onOverflow }) {
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
const decoder = new TextDecoder();
|
|
203
|
+
let parts = [];
|
|
204
|
+
let size = 0;
|
|
205
|
+
let discarding = false;
|
|
206
|
+
const append = (segment) => {
|
|
207
|
+
if (discarding || segment.length === 0) return;
|
|
208
|
+
if (size + segment.length > maxLineBytes) {
|
|
209
|
+
discarding = true;
|
|
210
|
+
parts = [];
|
|
211
|
+
size = 0;
|
|
212
|
+
onOverflow();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
parts.push(new Uint8Array(segment));
|
|
216
|
+
size += segment.length;
|
|
217
|
+
};
|
|
218
|
+
const finishLine = () => {
|
|
219
|
+
const wasDiscarding = discarding;
|
|
220
|
+
const lineParts = parts;
|
|
221
|
+
const lineSize = size;
|
|
222
|
+
parts = [];
|
|
223
|
+
size = 0;
|
|
224
|
+
discarding = false;
|
|
225
|
+
if (wasDiscarding || lineSize === 0) return;
|
|
226
|
+
const bytes = new Uint8Array(lineSize);
|
|
227
|
+
let offset = 0;
|
|
228
|
+
for (const part of lineParts) {
|
|
229
|
+
bytes.set(part, offset);
|
|
230
|
+
offset += part.length;
|
|
231
|
+
}
|
|
232
|
+
const line = decoder.decode(bytes).replace(/\r$/, "");
|
|
233
|
+
if (line.trim() !== "") onLine(line);
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
push(chunk) {
|
|
237
|
+
const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
|
|
238
|
+
let start = 0;
|
|
239
|
+
while (start <= bytes.length) {
|
|
240
|
+
const newline = bytes.indexOf(NEWLINE, start);
|
|
241
|
+
if (newline === -1) {
|
|
242
|
+
append(bytes.subarray(start));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
append(bytes.subarray(start, newline));
|
|
246
|
+
finishLine();
|
|
247
|
+
start = newline + 1;
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
end() {
|
|
251
|
+
finishLine();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// src/run-plugin.ts
|
|
256
|
+
const CANCEL_ANSWER_GRACE_MS = 4e3;
|
|
257
|
+
const HOST_PID_POLL_MS = 1e3;
|
|
258
|
+
const RELEASE_ON_CLOSE_BOUND_MS = 3e4;
|
|
259
|
+
const RELEASE_ON_SIGTERM_BOUND_MS = 4e3;
|
|
260
|
+
const SUPPORTED_PROTOCOL_VERSIONS = [1];
|
|
261
|
+
var ProtocolFailure = class extends Error {
|
|
262
|
+
code;
|
|
263
|
+
data;
|
|
264
|
+
constructor(code, message, data) {
|
|
265
|
+
super(message);
|
|
266
|
+
this.code = code;
|
|
267
|
+
this.data = data;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function resolveIo(io) {
|
|
271
|
+
return {
|
|
272
|
+
stdin: io.stdin ?? process.stdin,
|
|
273
|
+
stdout: io.stdout ?? process.stdout,
|
|
274
|
+
exit: io.exit ?? ((code) => process.exit(code)),
|
|
275
|
+
entryPath: io.entryPath ?? process.argv[1],
|
|
276
|
+
cancelAnswerGraceMs: io.cancelAnswerGraceMs ?? 4e3,
|
|
277
|
+
hostPidPollMs: io.hostPidPollMs ?? 1e3
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function routeStrayOutputToStderr() {
|
|
281
|
+
const toLog = (...args) => {
|
|
282
|
+
process.stderr.write(`${format(...args)}\n`);
|
|
283
|
+
};
|
|
284
|
+
console.log = toLog;
|
|
285
|
+
console.info = toLog;
|
|
286
|
+
console.debug = toLog;
|
|
287
|
+
console.warn = toLog;
|
|
288
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
289
|
+
}
|
|
290
|
+
function hostGone(hostPid, startPpid) {
|
|
291
|
+
if (process.ppid !== startPpid) return true;
|
|
292
|
+
try {
|
|
293
|
+
process.kill(hostPid, 0);
|
|
294
|
+
return false;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return error.code === "ESRCH";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function handleKey(handle) {
|
|
300
|
+
const canonical = (value) => {
|
|
301
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
302
|
+
if (value === null || typeof value !== "object") return value;
|
|
303
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
304
|
+
};
|
|
305
|
+
return JSON.stringify(canonical(handle));
|
|
306
|
+
}
|
|
307
|
+
function within(work, boundMs) {
|
|
308
|
+
let timer;
|
|
309
|
+
const bound = new Promise((resolve) => {
|
|
310
|
+
timer = setTimeout(() => resolve(void 0), boundMs);
|
|
311
|
+
});
|
|
312
|
+
return Promise.race([work, bound]).finally(() => clearTimeout(timer));
|
|
313
|
+
}
|
|
314
|
+
function firstLine(message) {
|
|
315
|
+
return message.split(/\r?\n/, 1)[0] ?? "";
|
|
316
|
+
}
|
|
317
|
+
function isRecord$1(value) {
|
|
318
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
319
|
+
}
|
|
320
|
+
function isRequestId(value) {
|
|
321
|
+
return typeof value === "string" || typeof value === "number";
|
|
322
|
+
}
|
|
323
|
+
function isPluginMethod(method) {
|
|
324
|
+
return PLUGIN_METHODS.includes(method);
|
|
325
|
+
}
|
|
326
|
+
function isOptionalMethod(method) {
|
|
327
|
+
return OPTIONAL_PLUGIN_METHODS.includes(method);
|
|
328
|
+
}
|
|
329
|
+
function issuePath(issue) {
|
|
330
|
+
return issue.path.map(String).join(".");
|
|
331
|
+
}
|
|
332
|
+
function parseParams(schema, params, method) {
|
|
333
|
+
const parsed = schema.safeParse(params ?? {});
|
|
334
|
+
if (parsed.success) return parsed.data;
|
|
335
|
+
const issue = parsed.error.issues[0];
|
|
336
|
+
const where = issue === void 0 ? "" : issuePath(issue);
|
|
337
|
+
throw new ProtocolFailure(PLUGIN_ERROR_CODES.invalidParams, `${method} params${where === "" ? "" : ` at ${where}`}: ${issue?.message ?? "invalid"}`);
|
|
338
|
+
}
|
|
339
|
+
function refuse(schema, value, prefix) {
|
|
340
|
+
const parsed = schema.safeParse(value);
|
|
341
|
+
if (parsed.success) return parsed.data;
|
|
342
|
+
const issue = parsed.error.issues[0];
|
|
343
|
+
const inner = issue === void 0 ? "" : issuePath(issue);
|
|
344
|
+
const path = inner === "" ? void 0 : `${prefix}.${inner}`;
|
|
345
|
+
throw new PluginError(`${path ?? prefix}: ${issue?.message ?? "invalid"}`, {
|
|
346
|
+
kind: "refused",
|
|
347
|
+
...path === void 0 ? {} : { path }
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function checkedResult(schema, value, method) {
|
|
351
|
+
const parsed = schema.safeParse(value);
|
|
352
|
+
if (parsed.success) return value;
|
|
353
|
+
const issue = parsed.error.issues[0];
|
|
354
|
+
const where = issue === void 0 ? "" : issuePath(issue);
|
|
355
|
+
const handle = method === "acquire" && isRecord$1(value) && value.handle !== void 0 ? value.handle : void 0;
|
|
356
|
+
throw new PluginError(`${method} answered a malformed result${where === "" ? "" : ` at ${where}`}: ${issue?.message ?? "invalid"}`, {
|
|
357
|
+
kind: "refused",
|
|
358
|
+
...handle === void 0 ? {} : { handle }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function pluginIdentity(entryPath) {
|
|
362
|
+
const unknown = {
|
|
363
|
+
name: "unknown",
|
|
364
|
+
version: "unknown"
|
|
365
|
+
};
|
|
366
|
+
if (entryPath === void 0) return unknown;
|
|
367
|
+
try {
|
|
368
|
+
const root = findPackageRoot(dirname(realpathSync(entryPath)));
|
|
369
|
+
if (root === void 0) return unknown;
|
|
370
|
+
const pkg = readPackageJson(root);
|
|
371
|
+
return {
|
|
372
|
+
name: typeof pkg.name === "string" ? pkg.name : unknown.name,
|
|
373
|
+
version: typeof pkg.version === "string" ? pkg.version : unknown.version
|
|
374
|
+
};
|
|
375
|
+
} catch {
|
|
376
|
+
return unknown;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function runPlugin(definition, ioInput = {}) {
|
|
380
|
+
const io = resolveIo(ioInput);
|
|
381
|
+
const write = io.stdout.write.bind(io.stdout);
|
|
382
|
+
const ownsProcess = ioInput.stdout === void 0;
|
|
383
|
+
if (ownsProcess) {
|
|
384
|
+
routeStrayOutputToStderr();
|
|
385
|
+
process.stdout.on("error", () => {});
|
|
386
|
+
process.stderr.on("error", () => {});
|
|
387
|
+
process.on("SIGINT", () => {});
|
|
388
|
+
process.on("uncaughtException", (error) => {
|
|
389
|
+
try {
|
|
390
|
+
writeSync(2, `${inspect(error)}\n`);
|
|
391
|
+
} catch {}
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const send = (message) => {
|
|
396
|
+
if (leaving) return;
|
|
397
|
+
write(encodeMessage(message));
|
|
398
|
+
};
|
|
399
|
+
const answer = (id, result) => send({
|
|
400
|
+
jsonrpc: "2.0",
|
|
401
|
+
id,
|
|
402
|
+
result
|
|
403
|
+
});
|
|
404
|
+
const fail = (id, code, message, data) => send({
|
|
405
|
+
jsonrpc: "2.0",
|
|
406
|
+
id,
|
|
407
|
+
error: {
|
|
408
|
+
code,
|
|
409
|
+
message: firstLine(message),
|
|
410
|
+
...data === void 0 ? {} : { data }
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
let options;
|
|
414
|
+
let runFacts;
|
|
415
|
+
let initialized = false;
|
|
416
|
+
let initializing = false;
|
|
417
|
+
let shutdownRan = false;
|
|
418
|
+
let leaving = false;
|
|
419
|
+
let hostPoll;
|
|
420
|
+
const startPpid = process.ppid;
|
|
421
|
+
const pending = /* @__PURE__ */ new Map();
|
|
422
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
423
|
+
const held = /* @__PURE__ */ new Map();
|
|
424
|
+
const releasing = /* @__PURE__ */ new Map();
|
|
425
|
+
const detachedContext = () => ({
|
|
426
|
+
signal: new AbortController().signal,
|
|
427
|
+
progress: () => {},
|
|
428
|
+
runFacts
|
|
429
|
+
});
|
|
430
|
+
const hold = (handle) => held.set(handleKey(handle), handle);
|
|
431
|
+
const releaseOnce = (handle, ctx) => {
|
|
432
|
+
const key = handleKey(handle);
|
|
433
|
+
const running = releasing.get(key);
|
|
434
|
+
if (running !== void 0) return running;
|
|
435
|
+
const work = (async () => {
|
|
436
|
+
try {
|
|
437
|
+
await definition.release({
|
|
438
|
+
handle,
|
|
439
|
+
options
|
|
440
|
+
}, ctx);
|
|
441
|
+
held.delete(key);
|
|
442
|
+
} finally {
|
|
443
|
+
releasing.delete(key);
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
releasing.set(key, work);
|
|
447
|
+
return work;
|
|
448
|
+
};
|
|
449
|
+
const releaseHeld = (attempted) => Promise.allSettled([...held].filter(([key]) => !attempted.has(key)).map(([key, handle]) => {
|
|
450
|
+
attempted.add(key);
|
|
451
|
+
return releaseOnce(handle, detachedContext());
|
|
452
|
+
}));
|
|
453
|
+
const settings = (value) => refuse(definition.settings, value, "settings");
|
|
454
|
+
const leave = async (boundMs) => {
|
|
455
|
+
if (leaving) return;
|
|
456
|
+
leaving = true;
|
|
457
|
+
clearInterval(hostPoll);
|
|
458
|
+
const sequence = async () => {
|
|
459
|
+
if (!initialized) return;
|
|
460
|
+
for (const entry of pending.values()) if (entry.method !== "release" && entry.method !== "shutdown") entry.controller.abort();
|
|
461
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
462
|
+
await Promise.allSettled([releaseHeld(attempted), ...inFlight]);
|
|
463
|
+
await releaseHeld(attempted);
|
|
464
|
+
if (shutdownRan) return;
|
|
465
|
+
shutdownRan = true;
|
|
466
|
+
await definition.shutdown?.({ options }, detachedContext());
|
|
467
|
+
};
|
|
468
|
+
await within(sequence().catch(() => {}), boundMs);
|
|
469
|
+
write("", () => io.exit(0));
|
|
470
|
+
};
|
|
471
|
+
const watchHost = (hostPid) => {
|
|
472
|
+
hostPoll = setInterval(() => {
|
|
473
|
+
if (hostGone(hostPid, startPpid)) leave(RELEASE_ON_CLOSE_BOUND_MS);
|
|
474
|
+
}, io.hostPidPollMs);
|
|
475
|
+
hostPoll.unref();
|
|
476
|
+
};
|
|
477
|
+
const initialize = async (params, ctx) => {
|
|
478
|
+
if (initialized || initializing) throw new ProtocolFailure(PLUGIN_ERROR_CODES.invalidRequest, "initialize was already called");
|
|
479
|
+
const input = parseParams(InitializeParamsSchema, params, "initialize");
|
|
480
|
+
const offered = input.protocolVersions.filter((version) => SUPPORTED_PROTOCOL_VERSIONS.includes(version));
|
|
481
|
+
if (offered.length === 0) throw new ProtocolFailure(PLUGIN_ERROR_CODES.plugin, `this plugin speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}, and the host offered ${input.protocolVersions.join(", ") || "none"}`, {
|
|
482
|
+
kind: "version",
|
|
483
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
484
|
+
requested: input.protocolVersions
|
|
485
|
+
});
|
|
486
|
+
const parsedOptions = refuse(definition.options, input.options, "options");
|
|
487
|
+
initializing = true;
|
|
488
|
+
try {
|
|
489
|
+
await definition.initialize?.({
|
|
490
|
+
options: parsedOptions,
|
|
491
|
+
host: input.host,
|
|
492
|
+
entry: input.entry,
|
|
493
|
+
runFacts: input.runFacts
|
|
494
|
+
}, {
|
|
495
|
+
...ctx,
|
|
496
|
+
runFacts: input.runFacts
|
|
497
|
+
});
|
|
498
|
+
} finally {
|
|
499
|
+
initializing = false;
|
|
500
|
+
}
|
|
501
|
+
options = parsedOptions;
|
|
502
|
+
runFacts = input.runFacts;
|
|
503
|
+
initialized = true;
|
|
504
|
+
watchHost(input.host.pid);
|
|
505
|
+
return {
|
|
506
|
+
protocolVersion: Math.max(...offered),
|
|
507
|
+
plugin: pluginIdentity(io.entryPath),
|
|
508
|
+
serves: [...definition.serves],
|
|
509
|
+
methods: [...definition.methods]
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
const call = async (method, params, ctx) => {
|
|
513
|
+
if (method === "initialize") return initialize(params, ctx);
|
|
514
|
+
if (!isPluginMethod(method) || isOptionalMethod(method) && (!definition.methods.includes(method) || typeof definition[method] !== "function")) throw new ProtocolFailure(PLUGIN_ERROR_CODES.methodNotFound, `${method} is not a method of this plugin`);
|
|
515
|
+
if (!initialized) throw new ProtocolFailure(PLUGIN_ERROR_CODES.invalidRequest, `${method} came before initialize`);
|
|
516
|
+
const current = options;
|
|
517
|
+
switch (method) {
|
|
518
|
+
case "acquire": {
|
|
519
|
+
const input = parseParams(AcquireParamsSchema, params, method);
|
|
520
|
+
const result = await definition.acquire({
|
|
521
|
+
...input,
|
|
522
|
+
settings: settings(input.settings),
|
|
523
|
+
options: current
|
|
524
|
+
}, ctx);
|
|
525
|
+
return checkedResult(AcquireResultSchema, result, method);
|
|
526
|
+
}
|
|
527
|
+
case "release": {
|
|
528
|
+
const input = parseParams(ReleaseParamsSchema, params, method);
|
|
529
|
+
await releaseOnce(input.handle, ctx);
|
|
530
|
+
return {};
|
|
531
|
+
}
|
|
532
|
+
case "shutdown":
|
|
533
|
+
shutdownRan = true;
|
|
534
|
+
await releaseHeld(/* @__PURE__ */ new Set());
|
|
535
|
+
await definition.shutdown?.({ options: current }, ctx);
|
|
536
|
+
return {};
|
|
537
|
+
case "check": {
|
|
538
|
+
const remotes = parseParams(CheckParamsSchema, params, method).remotes.map((remote) => ({
|
|
539
|
+
name: remote.name,
|
|
540
|
+
settings: settings(remote.settings)
|
|
541
|
+
}));
|
|
542
|
+
const result = await definition.check?.({
|
|
543
|
+
remotes,
|
|
544
|
+
options: current
|
|
545
|
+
}, ctx);
|
|
546
|
+
return checkedResult(CheckResultSchema, result, method);
|
|
547
|
+
}
|
|
548
|
+
case "uploadApp": {
|
|
549
|
+
const input = parseParams(UploadAppParamsSchema, params, method);
|
|
550
|
+
const result = await definition.uploadApp?.({
|
|
551
|
+
...input,
|
|
552
|
+
settings: settings(input.settings),
|
|
553
|
+
options: current
|
|
554
|
+
}, ctx);
|
|
555
|
+
return checkedResult(UploadAppResultSchema, result, method);
|
|
556
|
+
}
|
|
557
|
+
case "started": {
|
|
558
|
+
const input = parseParams(StartedParamsSchema, params, method);
|
|
559
|
+
const result = await definition.started?.({
|
|
560
|
+
...input,
|
|
561
|
+
options: current
|
|
562
|
+
}, ctx);
|
|
563
|
+
return checkedResult(StartedResultSchema, result, method);
|
|
564
|
+
}
|
|
565
|
+
case "end": {
|
|
566
|
+
const input = parseParams(EndParamsSchema, params, method);
|
|
567
|
+
await definition.end?.({
|
|
568
|
+
...input,
|
|
569
|
+
options: current
|
|
570
|
+
}, ctx);
|
|
571
|
+
return {};
|
|
572
|
+
}
|
|
573
|
+
default: throw new ProtocolFailure(PLUGIN_ERROR_CODES.methodNotFound, `${method} is not a method of this plugin`);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const createdHandle = (method, outcome) => {
|
|
577
|
+
if (outcome.ok) {
|
|
578
|
+
if (method !== "acquire" || !isRecord$1(outcome.value)) return void 0;
|
|
579
|
+
return outcome.value.handle;
|
|
580
|
+
}
|
|
581
|
+
return outcome.error instanceof PluginError ? outcome.error.handle : void 0;
|
|
582
|
+
};
|
|
583
|
+
const answerFailure = (id, error) => {
|
|
584
|
+
if (error instanceof ProtocolFailure) fail(id, error.code, error.message, error.data);
|
|
585
|
+
else if (error instanceof PluginError) fail(id, PLUGIN_ERROR_CODES.plugin, error.message, {
|
|
586
|
+
kind: error.kind,
|
|
587
|
+
...error.path === void 0 ? {} : { path: error.path },
|
|
588
|
+
...error.handle === void 0 ? {} : { handle: error.handle }
|
|
589
|
+
});
|
|
590
|
+
else {
|
|
591
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
592
|
+
fail(id, PLUGIN_ERROR_CODES.plugin, message, { kind: "unreachable" });
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const dispatch = async (id, method, params) => {
|
|
596
|
+
const entry = {
|
|
597
|
+
method,
|
|
598
|
+
controller: new AbortController(),
|
|
599
|
+
answered: false
|
|
600
|
+
};
|
|
601
|
+
pending.set(id, entry);
|
|
602
|
+
const ctx = {
|
|
603
|
+
signal: entry.controller.signal,
|
|
604
|
+
progress: (update) => {
|
|
605
|
+
if (entry.answered) return;
|
|
606
|
+
send({
|
|
607
|
+
jsonrpc: "2.0",
|
|
608
|
+
method: PROGRESS_METHOD,
|
|
609
|
+
params: {
|
|
610
|
+
id,
|
|
611
|
+
...update
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
},
|
|
615
|
+
runFacts
|
|
616
|
+
};
|
|
617
|
+
let outcome;
|
|
618
|
+
try {
|
|
619
|
+
outcome = {
|
|
620
|
+
ok: true,
|
|
621
|
+
value: await call(method, params, ctx)
|
|
622
|
+
};
|
|
623
|
+
} catch (error) {
|
|
624
|
+
outcome = {
|
|
625
|
+
ok: false,
|
|
626
|
+
error
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
clearTimeout(entry.graceTimer);
|
|
630
|
+
if (pending.get(id) === entry) pending.delete(id);
|
|
631
|
+
const handle = createdHandle(method, outcome);
|
|
632
|
+
const tracked = initialized ? handle : void 0;
|
|
633
|
+
if (tracked !== void 0) hold(tracked);
|
|
634
|
+
if (entry.answered) {
|
|
635
|
+
if (tracked !== void 0) await releaseOnce(tracked, detachedContext()).catch(() => {});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (leaving) return;
|
|
639
|
+
entry.answered = true;
|
|
640
|
+
if (outcome.ok) answer(id, outcome.value);
|
|
641
|
+
else if (entry.controller.signal.aborted) {
|
|
642
|
+
const message = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
643
|
+
fail(id, PLUGIN_ERROR_CODES.cancelled, message, {
|
|
644
|
+
kind: "timeout",
|
|
645
|
+
...handle === void 0 ? {} : { handle }
|
|
646
|
+
});
|
|
647
|
+
} else answerFailure(id, outcome.error);
|
|
648
|
+
};
|
|
649
|
+
const cancel = (params) => {
|
|
650
|
+
const parsed = CancelParamsSchema.safeParse(params);
|
|
651
|
+
if (!parsed.success) return;
|
|
652
|
+
const { id } = parsed.data;
|
|
653
|
+
const entry = pending.get(id);
|
|
654
|
+
if (entry === void 0 || entry.controller.signal.aborted) return;
|
|
655
|
+
entry.controller.abort();
|
|
656
|
+
entry.graceTimer = setTimeout(() => {
|
|
657
|
+
if (entry.answered) return;
|
|
658
|
+
entry.answered = true;
|
|
659
|
+
fail(id, PLUGIN_ERROR_CODES.cancelled, `${entry.method} did not stop within ${io.cancelAnswerGraceMs} ms of its cancel`, { kind: "timeout" });
|
|
660
|
+
}, io.cancelAnswerGraceMs);
|
|
661
|
+
};
|
|
662
|
+
const handleLine = (line) => {
|
|
663
|
+
let message;
|
|
664
|
+
try {
|
|
665
|
+
message = JSON.parse(line);
|
|
666
|
+
} catch {
|
|
667
|
+
fail(null, PLUGIN_ERROR_CODES.parse, "the line is not JSON");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!isRecord$1(message)) {
|
|
671
|
+
fail(null, PLUGIN_ERROR_CODES.invalidRequest, "a message must be a JSON-RPC 2.0 object");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const id = isRequestId(message.id) ? message.id : null;
|
|
675
|
+
if (message.method === void 0 && ("result" in message || "error" in message)) return;
|
|
676
|
+
if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
677
|
+
fail(id, PLUGIN_ERROR_CODES.invalidRequest, "a request needs jsonrpc \"2.0\" and a method");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (!("id" in message)) {
|
|
681
|
+
if (message.method === "$/cancel") cancel(message.params);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (id === null) {
|
|
685
|
+
fail(null, PLUGIN_ERROR_CODES.invalidRequest, "a request id is a string or a number");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (leaving) return;
|
|
689
|
+
const run = dispatch(id, message.method, message.params);
|
|
690
|
+
inFlight.add(run);
|
|
691
|
+
run.catch(() => {}).finally(() => inFlight.delete(run));
|
|
692
|
+
};
|
|
693
|
+
const decoder = createLineDecoder({
|
|
694
|
+
maxLineBytes: MAX_MESSAGE_BYTES,
|
|
695
|
+
onLine: handleLine,
|
|
696
|
+
onOverflow: () => fail(null, PLUGIN_ERROR_CODES.invalidRequest, `a message is longer than ${MAX_MESSAGE_BYTES} bytes`)
|
|
697
|
+
});
|
|
698
|
+
const stdinClosed = () => {
|
|
699
|
+
if (leaving) return;
|
|
700
|
+
decoder.end();
|
|
701
|
+
leave(RELEASE_ON_CLOSE_BOUND_MS);
|
|
702
|
+
};
|
|
703
|
+
if (ownsProcess) process.on("SIGTERM", () => void leave(RELEASE_ON_SIGTERM_BOUND_MS));
|
|
704
|
+
io.stdin.on("data", (chunk) => decoder.push(chunk));
|
|
705
|
+
io.stdin.on("end", stdinClosed);
|
|
706
|
+
io.stdin.on("error", stdinClosed);
|
|
707
|
+
}
|
|
708
|
+
// src/protocol/manifest.ts
|
|
709
|
+
const PLUGIN_MANIFEST_FIELD = "agent-qa";
|
|
710
|
+
const PackagePathSchema = z$1.string().min(1);
|
|
711
|
+
const RemotePluginManifestSchema = z$1.strictObject({
|
|
712
|
+
kind: z$1.literal("remote"),
|
|
713
|
+
runs: z$1.literal("process"),
|
|
714
|
+
protocol: z$1.number().int().positive(),
|
|
715
|
+
entry: PackagePathSchema,
|
|
716
|
+
serves: z$1.array(z$1.enum(PLUGIN_SERVES)).nonempty(),
|
|
717
|
+
methods: z$1.array(z$1.enum(OPTIONAL_PLUGIN_METHODS)),
|
|
718
|
+
options: PackagePathSchema,
|
|
719
|
+
settings: PackagePathSchema,
|
|
720
|
+
runFacts: z$1.array(z$1.enum(PLUGIN_RUN_FACTS)).optional(),
|
|
721
|
+
skill: PackagePathSchema.optional()
|
|
722
|
+
});
|
|
723
|
+
const ModelAuthManifestSchema = z$1.strictObject({
|
|
724
|
+
kind: z$1.literal("model-auth"),
|
|
725
|
+
runs: z$1.literal("in-process"),
|
|
726
|
+
entry: PackagePathSchema,
|
|
727
|
+
options: PackagePathSchema.optional()
|
|
728
|
+
});
|
|
729
|
+
const ModelProviderManifestSchema = z$1.strictObject({
|
|
730
|
+
kind: z$1.literal("model-provider"),
|
|
731
|
+
runs: z$1.literal("in-process"),
|
|
732
|
+
entry: PackagePathSchema,
|
|
733
|
+
options: PackagePathSchema.optional()
|
|
734
|
+
});
|
|
735
|
+
const PluginManifestSchema = z$1.discriminatedUnion("kind", [
|
|
736
|
+
RemotePluginManifestSchema,
|
|
737
|
+
ModelAuthManifestSchema,
|
|
738
|
+
ModelProviderManifestSchema
|
|
739
|
+
]);
|
|
740
|
+
// src/emit.ts
|
|
741
|
+
const OPTIONS_SCHEMA_PATH = "./dist/options.schema.json";
|
|
742
|
+
const SETTINGS_SCHEMA_PATH = "./dist/settings.schema.json";
|
|
743
|
+
function pluginManifest(definition) {
|
|
744
|
+
const manifest = PluginManifestSchema.parse({
|
|
745
|
+
protocol: 1,
|
|
746
|
+
kind: "remote",
|
|
747
|
+
runs: "process",
|
|
748
|
+
entry: "./dist/main.js",
|
|
749
|
+
serves: [...definition.serves],
|
|
750
|
+
methods: [...definition.methods],
|
|
751
|
+
options: OPTIONS_SCHEMA_PATH,
|
|
752
|
+
settings: SETTINGS_SCHEMA_PATH,
|
|
753
|
+
...definition.runFacts === void 0 ? {} : { runFacts: [...definition.runFacts] },
|
|
754
|
+
...definition.skill === void 0 ? {} : { skill: definition.skill }
|
|
755
|
+
});
|
|
756
|
+
if (manifest.kind !== "remote") throw new Error("a plugin built with the kit is remote");
|
|
757
|
+
return manifest;
|
|
758
|
+
}
|
|
759
|
+
function schemaText(schema) {
|
|
760
|
+
return `${JSON.stringify(z$1.toJSONSchema(schema, {
|
|
761
|
+
io: "input",
|
|
762
|
+
target: "draft-2020-12"
|
|
763
|
+
}), null, 2)}\n`;
|
|
764
|
+
}
|
|
765
|
+
function isRecord(value) {
|
|
766
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
767
|
+
}
|
|
768
|
+
function emitPluginFiles(input) {
|
|
769
|
+
const { definition, packageDir } = input;
|
|
770
|
+
const manifest = pluginManifest(definition);
|
|
771
|
+
const optionsSchemaFile = join(packageDir, OPTIONS_SCHEMA_PATH);
|
|
772
|
+
const settingsSchemaFile = join(packageDir, SETTINGS_SCHEMA_PATH);
|
|
773
|
+
mkdirSync(join(packageDir, "dist"), { recursive: true });
|
|
774
|
+
writeFileSync(optionsSchemaFile, schemaText(definition.options));
|
|
775
|
+
writeFileSync(settingsSchemaFile, schemaText(definition.settings));
|
|
776
|
+
const packageJsonFile = join(packageDir, "package.json");
|
|
777
|
+
const parsed = JSON.parse(readFileSync(packageJsonFile, "utf8"));
|
|
778
|
+
if (!isRecord(parsed)) throw new Error(`${packageJsonFile} is not a JSON object`);
|
|
779
|
+
const field = isRecord(parsed["agent-qa"]) ? parsed[PLUGIN_MANIFEST_FIELD] : {};
|
|
780
|
+
const packageJsonChanged = !isDeepStrictEqual(field.plugin, manifest);
|
|
781
|
+
if (packageJsonChanged) {
|
|
782
|
+
const next = {
|
|
783
|
+
...parsed,
|
|
784
|
+
[PLUGIN_MANIFEST_FIELD]: {
|
|
785
|
+
...field,
|
|
786
|
+
plugin: manifest
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
writeFileSync(packageJsonFile, `${JSON.stringify(next, null, 2)}\n`);
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
manifest,
|
|
793
|
+
optionsSchemaFile,
|
|
794
|
+
settingsSchemaFile,
|
|
795
|
+
packageJsonChanged
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
export { CANCEL_ANSWER_GRACE_MS, HOST_PID_POLL_MS, PluginError, credential, definePlugin, emitPluginFiles, pluginManifest, runPlugin, z };
|
|
799
|
+
|
|
800
|
+
//# sourceMappingURL=index.js.map
|