@plune-ai/cli 0.5.0 → 0.7.0
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/README.md +2 -1
- package/dist/cli.cjs +1093 -141
- package/dist/index.cjs +264 -12
- package/dist/index.d.cts +104 -423
- package/dist/index.d.ts +104 -423
- package/dist/index.js +260 -12
- package/dist/reporter-core.cjs +407 -0
- package/dist/reporter-core.d.cts +315 -0
- package/dist/reporter-core.d.ts +315 -0
- package/dist/reporter-core.js +363 -0
- package/package.json +22 -5
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/reporter-core/index.ts
|
|
31
|
+
var reporter_core_exports = {};
|
|
32
|
+
__export(reporter_core_exports, {
|
|
33
|
+
DEFAULT_FALLBACK_PATH: () => DEFAULT_FALLBACK_PATH,
|
|
34
|
+
RESULT_KEY_MAX: () => RESULT_KEY_MAX,
|
|
35
|
+
UNSUPPORTED_VARS: () => UNSUPPORTED_VARS,
|
|
36
|
+
appendBatch: () => appendBatch,
|
|
37
|
+
createClient: () => createClient,
|
|
38
|
+
readEnv: () => readEnv,
|
|
39
|
+
resultKey: () => resultKey,
|
|
40
|
+
startRun: () => startRun
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(reporter_core_exports);
|
|
43
|
+
|
|
44
|
+
// src/cli/api-url.ts
|
|
45
|
+
var DEFAULT_API_URL = "https://beta-api.plune.ai";
|
|
46
|
+
function resolveApiUrl(explicit) {
|
|
47
|
+
const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
|
|
48
|
+
return raw.replace(/\/+$/, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/cli/credentials.ts
|
|
52
|
+
var import_node_fs = require("fs");
|
|
53
|
+
var import_node_os = require("os");
|
|
54
|
+
var import_node_path = require("path");
|
|
55
|
+
function credentialsFile() {
|
|
56
|
+
const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
|
|
57
|
+
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
58
|
+
}
|
|
59
|
+
function loadToken() {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
62
|
+
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/reporter-core/client.ts
|
|
69
|
+
var RETRY_DELAYS_MS = [300, 900];
|
|
70
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
71
|
+
async function detailOf(res) {
|
|
72
|
+
try {
|
|
73
|
+
const body = await res.json();
|
|
74
|
+
return typeof body.error === "string" ? body.error : "";
|
|
75
|
+
} catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function retryAfterMs(res) {
|
|
80
|
+
const raw = res.headers.get("retry-after");
|
|
81
|
+
if (raw === null) return null;
|
|
82
|
+
const seconds = Number(raw.trim());
|
|
83
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
|
|
84
|
+
}
|
|
85
|
+
function classify(status) {
|
|
86
|
+
if (status === 401 || status === 403) return "auth";
|
|
87
|
+
if (status === 409) return "conflict";
|
|
88
|
+
if (status === 400) return "invalid";
|
|
89
|
+
if (status === 429 || status >= 500) return "unavailable";
|
|
90
|
+
return "other";
|
|
91
|
+
}
|
|
92
|
+
function createClient(opts) {
|
|
93
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
94
|
+
const wait = opts.wait ?? sleep;
|
|
95
|
+
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
96
|
+
const scrub = (text) => opts.token === "" ? text : text.split(opts.token).join("***");
|
|
97
|
+
async function post(path2, body) {
|
|
98
|
+
let last = {
|
|
99
|
+
kind: "unavailable",
|
|
100
|
+
status: null,
|
|
101
|
+
detail: "no attempt was made"
|
|
102
|
+
};
|
|
103
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
104
|
+
let res;
|
|
105
|
+
try {
|
|
106
|
+
res = await doFetch(`${base}${path2}`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
|
|
113
|
+
if (attempt < RETRY_DELAYS_MS.length) await wait(RETRY_DELAYS_MS[attempt]);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (res.ok) return { ok: true, status: res.status, body: await res.json() };
|
|
117
|
+
const detail = scrub(await detailOf(res));
|
|
118
|
+
const kind = classify(res.status);
|
|
119
|
+
last = { kind, status: res.status, detail };
|
|
120
|
+
if (kind !== "unavailable") break;
|
|
121
|
+
if (attempt < RETRY_DELAYS_MS.length) {
|
|
122
|
+
await wait(retryAfterMs(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { ok: false, ...last };
|
|
126
|
+
}
|
|
127
|
+
return { post };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/reporter-core/env.ts
|
|
131
|
+
var UNSUPPORTED_VARS = [
|
|
132
|
+
"PLUNE_RUN_TITLE",
|
|
133
|
+
"PLUNE_ENV",
|
|
134
|
+
"PLUNE_LABELS",
|
|
135
|
+
"PLUNE_GROUP"
|
|
136
|
+
];
|
|
137
|
+
function value(env, name) {
|
|
138
|
+
const raw = env[name]?.trim();
|
|
139
|
+
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
140
|
+
}
|
|
141
|
+
function flag(env, name) {
|
|
142
|
+
const raw = value(env, name)?.toLowerCase();
|
|
143
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
144
|
+
}
|
|
145
|
+
function count(env, name) {
|
|
146
|
+
const raw = value(env, name);
|
|
147
|
+
if (raw === void 0) return void 0;
|
|
148
|
+
const n = Number(raw);
|
|
149
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
150
|
+
}
|
|
151
|
+
function readEnv(env = process.env) {
|
|
152
|
+
const externalKey = value(env, "PLUNE_RUN");
|
|
153
|
+
const token = value(env, "PLUNE_TOKEN");
|
|
154
|
+
const apiUrl = value(env, "PLUNE_API_URL");
|
|
155
|
+
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
156
|
+
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
157
|
+
return {
|
|
158
|
+
config: {
|
|
159
|
+
...externalKey !== void 0 ? { externalKey } : {},
|
|
160
|
+
...token !== void 0 ? { token } : {},
|
|
161
|
+
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
162
|
+
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
163
|
+
...batchSize !== void 0 ? { batchSize } : {}
|
|
164
|
+
},
|
|
165
|
+
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
166
|
+
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
167
|
+
// way this process must not be the one to call it finished.
|
|
168
|
+
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
|
|
169
|
+
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/reporter-core/fallback.ts
|
|
174
|
+
var fs = __toESM(require("fs"), 1);
|
|
175
|
+
var path = __toESM(require("path"), 1);
|
|
176
|
+
var DEFAULT_FALLBACK_PATH = path.join(".plune", "pending-results.jsonl");
|
|
177
|
+
function appendBatch(file, batch) {
|
|
178
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
179
|
+
const line = {
|
|
180
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
181
|
+
runId: batch.runId,
|
|
182
|
+
externalKey: batch.externalKey,
|
|
183
|
+
results: batch.results
|
|
184
|
+
};
|
|
185
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
186
|
+
`, "utf8");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/reporter-core/session.ts
|
|
190
|
+
var RESOLVE_CHUNK = 500;
|
|
191
|
+
var BATCH_MAX = 500;
|
|
192
|
+
var BATCH_DEFAULT = 100;
|
|
193
|
+
var EXPECTED_MAX = 1e4;
|
|
194
|
+
function chunk(items, size) {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
async function startRun(passed, expected) {
|
|
200
|
+
const env = readEnv();
|
|
201
|
+
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
202
|
+
const cfg = { ...env.config, ...stated };
|
|
203
|
+
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
204
|
+
`));
|
|
205
|
+
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
206
|
+
const externalKey = cfg.externalKey ?? null;
|
|
207
|
+
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
208
|
+
const token = cfg.token ?? loadToken() ?? "";
|
|
209
|
+
const client = createClient({
|
|
210
|
+
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
211
|
+
token,
|
|
212
|
+
...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
|
|
213
|
+
});
|
|
214
|
+
const stats = {
|
|
215
|
+
accepted: 0,
|
|
216
|
+
duplicate: 0,
|
|
217
|
+
conflict: 0,
|
|
218
|
+
rejected: 0,
|
|
219
|
+
unresolved: 0,
|
|
220
|
+
deferred: 0
|
|
221
|
+
};
|
|
222
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
223
|
+
let buffer = [];
|
|
224
|
+
let runId = null;
|
|
225
|
+
let joined = false;
|
|
226
|
+
let offline = token === "";
|
|
227
|
+
let done = false;
|
|
228
|
+
if (offline) {
|
|
229
|
+
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
230
|
+
}
|
|
231
|
+
if (env.ignored.length > 0) {
|
|
232
|
+
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
233
|
+
}
|
|
234
|
+
function defer(results) {
|
|
235
|
+
appendBatch(fallbackPath, { runId, externalKey, results });
|
|
236
|
+
stats.deferred += results.length;
|
|
237
|
+
}
|
|
238
|
+
async function resolveKeys(values) {
|
|
239
|
+
const unknown = values.filter((k) => !resolved.has(k.value));
|
|
240
|
+
if (unknown.length === 0 || offline) return;
|
|
241
|
+
for (const part of chunk(unknown, RESOLVE_CHUNK)) {
|
|
242
|
+
const out = await client.post(
|
|
243
|
+
"/v1/test-cases/resolve",
|
|
244
|
+
{ keys: part }
|
|
245
|
+
);
|
|
246
|
+
if (!out.ok) {
|
|
247
|
+
log(`plune: could not look up test cases (${out.detail || out.kind}).`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!offline) {
|
|
254
|
+
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
255
|
+
const declared = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
256
|
+
const configuration = declared.length === 0 ? void 0 : {
|
|
257
|
+
expected: declared.flatMap((keys) => {
|
|
258
|
+
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
259
|
+
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
260
|
+
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
261
|
+
})
|
|
262
|
+
};
|
|
263
|
+
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
264
|
+
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
265
|
+
}
|
|
266
|
+
const start = await client.post("/v1/runs", {
|
|
267
|
+
schemaVersion: 2,
|
|
268
|
+
kind: cfg.kind ?? "automated",
|
|
269
|
+
...externalKey !== null ? { externalKey } : {},
|
|
270
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
271
|
+
...configuration !== void 0 ? { configuration } : {}
|
|
272
|
+
});
|
|
273
|
+
if (start.ok) {
|
|
274
|
+
runId = start.body.run.id;
|
|
275
|
+
joined = start.status === 200;
|
|
276
|
+
} else {
|
|
277
|
+
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function matchFor(result) {
|
|
281
|
+
if (result.testCaseId !== void 0) return { kind: "found", testCaseId: result.testCaseId };
|
|
282
|
+
let unanswered = false;
|
|
283
|
+
for (const key of result.keys) {
|
|
284
|
+
const hit = resolved.get(key.value);
|
|
285
|
+
if (typeof hit === "string") return { kind: "found", testCaseId: hit };
|
|
286
|
+
if (hit === void 0) unanswered = true;
|
|
287
|
+
}
|
|
288
|
+
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
289
|
+
}
|
|
290
|
+
async function flush() {
|
|
291
|
+
if (buffer.length === 0) return;
|
|
292
|
+
const pending = buffer;
|
|
293
|
+
buffer = [];
|
|
294
|
+
if (offline || runId === null) {
|
|
295
|
+
defer(pending);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
299
|
+
const submissions = [];
|
|
300
|
+
const unanswered = [];
|
|
301
|
+
for (const result of pending) {
|
|
302
|
+
const match = matchFor(result);
|
|
303
|
+
if (match.kind === "found") {
|
|
304
|
+
const { keys: _keys, ...rest } = result;
|
|
305
|
+
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
306
|
+
} else if (match.kind === "unmatched") {
|
|
307
|
+
stats.unresolved += 1;
|
|
308
|
+
} else {
|
|
309
|
+
unanswered.push(result);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (unanswered.length > 0) defer(unanswered);
|
|
313
|
+
if (submissions.length === 0) return;
|
|
314
|
+
const out = await client.post(
|
|
315
|
+
`/v1/runs/${runId}/results`,
|
|
316
|
+
{ results: submissions }
|
|
317
|
+
);
|
|
318
|
+
if (out.ok) {
|
|
319
|
+
stats.accepted += out.body.counts.accepted ?? 0;
|
|
320
|
+
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
321
|
+
stats.conflict += out.body.counts.conflict ?? 0;
|
|
322
|
+
stats.rejected += out.body.counts.rejected ?? 0;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (out.kind === "auth") {
|
|
326
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
327
|
+
offline = true;
|
|
328
|
+
} else if (out.kind === "conflict") {
|
|
329
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
330
|
+
} else {
|
|
331
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
332
|
+
}
|
|
333
|
+
defer(submissions);
|
|
334
|
+
}
|
|
335
|
+
function summarise() {
|
|
336
|
+
const parts = [`${stats.accepted} accepted`];
|
|
337
|
+
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
|
338
|
+
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
339
|
+
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
340
|
+
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
341
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
342
|
+
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
get runId() {
|
|
346
|
+
return runId;
|
|
347
|
+
},
|
|
348
|
+
get joined() {
|
|
349
|
+
return joined;
|
|
350
|
+
},
|
|
351
|
+
get stats() {
|
|
352
|
+
return stats;
|
|
353
|
+
},
|
|
354
|
+
async add(result) {
|
|
355
|
+
buffer.push(result);
|
|
356
|
+
if (buffer.length >= batchSize) await flush();
|
|
357
|
+
},
|
|
358
|
+
flush,
|
|
359
|
+
async finish(reason) {
|
|
360
|
+
if (done) return;
|
|
361
|
+
done = true;
|
|
362
|
+
await flush();
|
|
363
|
+
if (runId !== null && !offline) {
|
|
364
|
+
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
365
|
+
event: "finish",
|
|
366
|
+
...reason !== void 0 ? { reason } : {}
|
|
367
|
+
});
|
|
368
|
+
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
369
|
+
}
|
|
370
|
+
summarise();
|
|
371
|
+
},
|
|
372
|
+
async leaveOpen() {
|
|
373
|
+
if (done) return;
|
|
374
|
+
done = true;
|
|
375
|
+
await flush();
|
|
376
|
+
if (runId !== null) {
|
|
377
|
+
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
378
|
+
}
|
|
379
|
+
summarise();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/util/hash.ts
|
|
385
|
+
var import_node_crypto = require("crypto");
|
|
386
|
+
function sha256(input) {
|
|
387
|
+
return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/reporter-core/result-key.ts
|
|
391
|
+
var RESULT_KEY_MAX = 200;
|
|
392
|
+
var HASH_CHARS = 32;
|
|
393
|
+
function resultKey(testId, retry) {
|
|
394
|
+
const plain = `${testId}#${retry}`;
|
|
395
|
+
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
396
|
+
}
|
|
397
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
398
|
+
0 && (module.exports = {
|
|
399
|
+
DEFAULT_FALLBACK_PATH,
|
|
400
|
+
RESULT_KEY_MAX,
|
|
401
|
+
UNSUPPORTED_VARS,
|
|
402
|
+
appendBatch,
|
|
403
|
+
createClient,
|
|
404
|
+
readEnv,
|
|
405
|
+
resultKey,
|
|
406
|
+
startRun
|
|
407
|
+
});
|