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