@agentrix/agentrix-run 0.2.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/bin/agentrix-run.mjs +2 -0
- package/dist/index.cjs +656 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +654 -0
- package/package.json +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_fs = require('node:fs');
|
|
4
|
+
var shared = require('@agentrix/shared');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RESPONSE_MODE = "stream";
|
|
7
|
+
const DEFAULT_WAIT_TIMEOUT_SECONDS = 1800;
|
|
8
|
+
function usage() {
|
|
9
|
+
return [
|
|
10
|
+
"Usage: agentrix-run --agent <agent> --prompt <prompt> [options]",
|
|
11
|
+
"",
|
|
12
|
+
"Options:",
|
|
13
|
+
" --title <text>",
|
|
14
|
+
" --repo <json>",
|
|
15
|
+
" --output-schema <json>",
|
|
16
|
+
" --ref <ref>",
|
|
17
|
+
" --sha <sha>",
|
|
18
|
+
" --base-ref <ref>",
|
|
19
|
+
" --head-ref <ref>",
|
|
20
|
+
" --pr-number <number>",
|
|
21
|
+
" --issue-number <number>",
|
|
22
|
+
" --response-mode <stream|async>",
|
|
23
|
+
" --timeout <seconds>",
|
|
24
|
+
" --capability-profile <name>",
|
|
25
|
+
" --runner-id <cloud-id|machine-id>",
|
|
26
|
+
" --result-file <path>",
|
|
27
|
+
" --base-url <url>",
|
|
28
|
+
" --api-key <api-key>",
|
|
29
|
+
" --metadata <key=value>",
|
|
30
|
+
" --help"
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
function requireValue(args, index, flag) {
|
|
34
|
+
const value = args[index + 1];
|
|
35
|
+
if (!value || value.startsWith("--")) {
|
|
36
|
+
throw new Error(`Missing value for ${flag}`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function parseRepo(value) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(value);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`Invalid --repo JSON: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
46
|
+
}
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
48
|
+
throw new Error("--repo must be a JSON object");
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function isPlainObject(value) {
|
|
53
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
function formatSchemaPath(path) {
|
|
56
|
+
return path.length > 0 ? path.join(".") : "$";
|
|
57
|
+
}
|
|
58
|
+
function normalizeStrictStructuredOutputSchemaNode(value, path, warnings) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.map(
|
|
61
|
+
(item, index) => normalizeStrictStructuredOutputSchemaNode(item, [...path, String(index)], warnings)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(value)) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const node = {};
|
|
68
|
+
for (const [key, child] of Object.entries(value)) {
|
|
69
|
+
node[key] = normalizeStrictStructuredOutputSchemaNode(child, [...path, key], warnings);
|
|
70
|
+
}
|
|
71
|
+
const hasProperties = isPlainObject(node.properties);
|
|
72
|
+
const typeValue = node.type;
|
|
73
|
+
const isObjectSchema = typeValue === "object" || Array.isArray(typeValue) && typeValue.includes("object") || hasProperties;
|
|
74
|
+
if (isObjectSchema) {
|
|
75
|
+
if (node.additionalProperties !== false) {
|
|
76
|
+
node.additionalProperties = false;
|
|
77
|
+
warnings.push(`${formatSchemaPath(path)}: set additionalProperties=false`);
|
|
78
|
+
}
|
|
79
|
+
if (hasProperties) {
|
|
80
|
+
const properties = node.properties;
|
|
81
|
+
const propertyNames = Object.keys(properties);
|
|
82
|
+
const requiredValue = node.required;
|
|
83
|
+
if (requiredValue !== void 0 && !Array.isArray(requiredValue)) {
|
|
84
|
+
throw new Error(`${formatSchemaPath(path)}.required must be an array of property names`);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(requiredValue)) {
|
|
87
|
+
const unknownNames = requiredValue.filter(
|
|
88
|
+
(item) => typeof item !== "string" || !propertyNames.includes(item)
|
|
89
|
+
);
|
|
90
|
+
if (unknownNames.length > 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${formatSchemaPath(path)}.required contains unknown properties: ${unknownNames.map(String).join(", ")}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const normalizedRequired = [...propertyNames];
|
|
97
|
+
const currentRequired = Array.isArray(requiredValue) ? requiredValue : [];
|
|
98
|
+
const isSameRequired = currentRequired.length === normalizedRequired.length && currentRequired.every((item, index) => item === normalizedRequired[index]);
|
|
99
|
+
if (!isSameRequired) {
|
|
100
|
+
node.required = normalizedRequired;
|
|
101
|
+
warnings.push(`${formatSchemaPath(path)}: normalized required to all properties`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
function normalizeStrictStructuredOutputSchema(schema) {
|
|
108
|
+
const warnings = [];
|
|
109
|
+
const normalized = normalizeStrictStructuredOutputSchemaNode(schema, [], warnings);
|
|
110
|
+
if (!isPlainObject(normalized)) {
|
|
111
|
+
throw new Error("--output-schema must normalize to a JSON object");
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
schema: normalized,
|
|
115
|
+
warnings
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function printOutputSchemaWarnings(warnings) {
|
|
119
|
+
if (warnings.length === 0) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const warning of warnings) {
|
|
123
|
+
process.stderr.write(`[agentrix-run] normalized output schema: ${warning}
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function parseOutputSchema(value) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(value);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Error(`Invalid --output-schema JSON: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
133
|
+
}
|
|
134
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
135
|
+
throw new Error("--output-schema must be a JSON object");
|
|
136
|
+
}
|
|
137
|
+
const normalized = normalizeStrictStructuredOutputSchema(parsed);
|
|
138
|
+
printOutputSchemaWarnings(normalized.warnings);
|
|
139
|
+
return normalized.schema;
|
|
140
|
+
}
|
|
141
|
+
function parseMetadataEntry(value) {
|
|
142
|
+
const separatorIndex = value.indexOf("=");
|
|
143
|
+
if (separatorIndex <= 0) {
|
|
144
|
+
throw new Error(`Invalid --metadata value: ${value}`);
|
|
145
|
+
}
|
|
146
|
+
const key = value.slice(0, separatorIndex).trim();
|
|
147
|
+
const metadataValue = value.slice(separatorIndex + 1).trim();
|
|
148
|
+
if (!key) {
|
|
149
|
+
throw new Error(`Invalid --metadata key: ${value}`);
|
|
150
|
+
}
|
|
151
|
+
return [key, metadataValue];
|
|
152
|
+
}
|
|
153
|
+
function parseInteger(value, flag) {
|
|
154
|
+
const parsed = Number.parseInt(value, 10);
|
|
155
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
156
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
}
|
|
160
|
+
function parseArgs(argv) {
|
|
161
|
+
const options = {
|
|
162
|
+
agent: "",
|
|
163
|
+
prompt: "",
|
|
164
|
+
responseMode: DEFAULT_RESPONSE_MODE,
|
|
165
|
+
metadata: {}
|
|
166
|
+
};
|
|
167
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
168
|
+
const arg = argv[index];
|
|
169
|
+
switch (arg) {
|
|
170
|
+
case "--help":
|
|
171
|
+
console.log(usage());
|
|
172
|
+
process.exit(0);
|
|
173
|
+
case "--agent":
|
|
174
|
+
options.agent = requireValue(argv, index, arg);
|
|
175
|
+
index += 1;
|
|
176
|
+
break;
|
|
177
|
+
case "--title":
|
|
178
|
+
options.title = requireValue(argv, index, arg);
|
|
179
|
+
index += 1;
|
|
180
|
+
break;
|
|
181
|
+
case "--prompt":
|
|
182
|
+
options.prompt = requireValue(argv, index, arg);
|
|
183
|
+
index += 1;
|
|
184
|
+
break;
|
|
185
|
+
case "--repo":
|
|
186
|
+
options.repo = parseRepo(requireValue(argv, index, arg));
|
|
187
|
+
index += 1;
|
|
188
|
+
break;
|
|
189
|
+
case "--output-schema":
|
|
190
|
+
options.outputSchema = parseOutputSchema(requireValue(argv, index, arg));
|
|
191
|
+
index += 1;
|
|
192
|
+
break;
|
|
193
|
+
case "--ref":
|
|
194
|
+
options.ref = requireValue(argv, index, arg);
|
|
195
|
+
index += 1;
|
|
196
|
+
break;
|
|
197
|
+
case "--sha":
|
|
198
|
+
options.sha = requireValue(argv, index, arg);
|
|
199
|
+
index += 1;
|
|
200
|
+
break;
|
|
201
|
+
case "--base-ref":
|
|
202
|
+
options.baseRef = requireValue(argv, index, arg);
|
|
203
|
+
index += 1;
|
|
204
|
+
break;
|
|
205
|
+
case "--head-ref":
|
|
206
|
+
options.headRef = requireValue(argv, index, arg);
|
|
207
|
+
index += 1;
|
|
208
|
+
break;
|
|
209
|
+
case "--pr-number":
|
|
210
|
+
options.prNumber = parseInteger(requireValue(argv, index, arg), arg);
|
|
211
|
+
index += 1;
|
|
212
|
+
break;
|
|
213
|
+
case "--issue-number":
|
|
214
|
+
options.issueNumber = parseInteger(requireValue(argv, index, arg), arg);
|
|
215
|
+
index += 1;
|
|
216
|
+
break;
|
|
217
|
+
case "--response-mode": {
|
|
218
|
+
const value = requireValue(argv, index, arg);
|
|
219
|
+
if (value !== "stream" && value !== "async") {
|
|
220
|
+
throw new Error(`Unsupported --response-mode: ${value}`);
|
|
221
|
+
}
|
|
222
|
+
options.responseMode = value;
|
|
223
|
+
index += 1;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "--timeout":
|
|
227
|
+
options.timeoutSeconds = parseInteger(requireValue(argv, index, arg), arg);
|
|
228
|
+
index += 1;
|
|
229
|
+
break;
|
|
230
|
+
case "--capability-profile":
|
|
231
|
+
options.capabilityProfile = requireValue(argv, index, arg);
|
|
232
|
+
index += 1;
|
|
233
|
+
break;
|
|
234
|
+
case "--runner-id":
|
|
235
|
+
options.runnerId = requireValue(argv, index, arg);
|
|
236
|
+
index += 1;
|
|
237
|
+
break;
|
|
238
|
+
case "--result-file":
|
|
239
|
+
options.resultFile = requireValue(argv, index, arg);
|
|
240
|
+
index += 1;
|
|
241
|
+
break;
|
|
242
|
+
case "--base-url":
|
|
243
|
+
options.baseUrl = requireValue(argv, index, arg);
|
|
244
|
+
index += 1;
|
|
245
|
+
break;
|
|
246
|
+
case "--api-key":
|
|
247
|
+
options.apiKey = requireValue(argv, index, arg);
|
|
248
|
+
index += 1;
|
|
249
|
+
break;
|
|
250
|
+
case "--metadata": {
|
|
251
|
+
const [key, value] = parseMetadataEntry(requireValue(argv, index, arg));
|
|
252
|
+
options.metadata[key] = value;
|
|
253
|
+
index += 1;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
default:
|
|
257
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!options.agent.trim()) {
|
|
261
|
+
throw new Error("--agent is required");
|
|
262
|
+
}
|
|
263
|
+
if (!options.prompt.trim()) {
|
|
264
|
+
throw new Error("--prompt is required");
|
|
265
|
+
}
|
|
266
|
+
return options;
|
|
267
|
+
}
|
|
268
|
+
function normalizeRef(value) {
|
|
269
|
+
if (!value) {
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
return value.replace(/^refs\/heads\//, "");
|
|
273
|
+
}
|
|
274
|
+
function safeReadJsonFile(path) {
|
|
275
|
+
if (!path) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const content = node_fs.readFileSync(path, "utf8");
|
|
280
|
+
const parsed = JSON.parse(content);
|
|
281
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
function splitRepositoryPath(value) {
|
|
290
|
+
if (!value) {
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
const parts = value.split("/").filter(Boolean);
|
|
294
|
+
if (parts.length < 2) {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
owner: parts.slice(0, -1).join("/"),
|
|
299
|
+
name: parts[parts.length - 1]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function optionalPositiveInt(value) {
|
|
303
|
+
if (!value) {
|
|
304
|
+
return void 0;
|
|
305
|
+
}
|
|
306
|
+
const parsed = Number.parseInt(value, 10);
|
|
307
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
308
|
+
}
|
|
309
|
+
function getNestedNumber(value, ...keys) {
|
|
310
|
+
let current = value;
|
|
311
|
+
for (const key of keys) {
|
|
312
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
current = current[key];
|
|
316
|
+
}
|
|
317
|
+
return typeof current === "number" ? current : void 0;
|
|
318
|
+
}
|
|
319
|
+
function getNestedString(value, ...keys) {
|
|
320
|
+
let current = value;
|
|
321
|
+
for (const key of keys) {
|
|
322
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
current = current[key];
|
|
326
|
+
}
|
|
327
|
+
return typeof current === "string" ? current : void 0;
|
|
328
|
+
}
|
|
329
|
+
function detectGithub() {
|
|
330
|
+
if (process.env.GITHUB_ACTIONS !== "true") {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const payload = safeReadJsonFile(process.env.GITHUB_EVENT_PATH);
|
|
334
|
+
const repository = splitRepositoryPath(process.env.GITHUB_REPOSITORY);
|
|
335
|
+
const runUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : void 0;
|
|
336
|
+
return {
|
|
337
|
+
repo: {
|
|
338
|
+
gitServerId: process.env.AGENTRIX_GIT_SERVER_ID || "github",
|
|
339
|
+
owner: repository.owner,
|
|
340
|
+
name: repository.name
|
|
341
|
+
},
|
|
342
|
+
git: {
|
|
343
|
+
ref: normalizeRef(process.env.GITHUB_REF),
|
|
344
|
+
sha: process.env.GITHUB_SHA,
|
|
345
|
+
baseRef: process.env.GITHUB_BASE_REF || getNestedString(payload, "pull_request", "base", "ref"),
|
|
346
|
+
headRef: process.env.GITHUB_HEAD_REF || getNestedString(payload, "pull_request", "head", "ref"),
|
|
347
|
+
prNumber: getNestedNumber(payload, "pull_request", "number"),
|
|
348
|
+
issueNumber: getNestedNumber(payload, "issue", "number")
|
|
349
|
+
},
|
|
350
|
+
context: {
|
|
351
|
+
ciProvider: "github_actions",
|
|
352
|
+
eventName: process.env.GITHUB_EVENT_NAME,
|
|
353
|
+
eventAction: getNestedString(payload, "action"),
|
|
354
|
+
runUrl,
|
|
355
|
+
payload
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function detectGitlab() {
|
|
360
|
+
if (process.env.GITLAB_CI !== "true") {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const repository = splitRepositoryPath(process.env.CI_PROJECT_PATH);
|
|
364
|
+
return {
|
|
365
|
+
repo: {
|
|
366
|
+
gitServerId: process.env.AGENTRIX_GIT_SERVER_ID || "gitlab",
|
|
367
|
+
owner: repository.owner,
|
|
368
|
+
name: repository.name
|
|
369
|
+
},
|
|
370
|
+
git: {
|
|
371
|
+
ref: process.env.CI_COMMIT_REF_NAME,
|
|
372
|
+
sha: process.env.CI_COMMIT_SHA,
|
|
373
|
+
baseRef: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
374
|
+
headRef: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
375
|
+
prNumber: optionalPositiveInt(process.env.CI_MERGE_REQUEST_IID),
|
|
376
|
+
issueNumber: optionalPositiveInt(process.env.AGENTRIX_ISSUE_NUMBER)
|
|
377
|
+
},
|
|
378
|
+
context: {
|
|
379
|
+
ciProvider: "gitlab_ci",
|
|
380
|
+
eventName: process.env.AGENTRIX_EVENT_NAME || process.env.CI_PIPELINE_SOURCE,
|
|
381
|
+
eventAction: process.env.AGENTRIX_EVENT_ACTION,
|
|
382
|
+
jobUrl: process.env.CI_JOB_URL,
|
|
383
|
+
runUrl: process.env.CI_PIPELINE_URL
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function detectCiContext() {
|
|
388
|
+
return detectGithub() ?? detectGitlab() ?? {};
|
|
389
|
+
}
|
|
390
|
+
function mergeRepo(explicitRepo, detectedRepo) {
|
|
391
|
+
if (!explicitRepo && !detectedRepo) {
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
...detectedRepo ?? {},
|
|
396
|
+
...explicitRepo ?? {}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function buildRequest(options, detected) {
|
|
400
|
+
const repo = mergeRepo(options.repo, detected.repo);
|
|
401
|
+
const request = {
|
|
402
|
+
agent: options.agent,
|
|
403
|
+
title: options.title,
|
|
404
|
+
prompt: options.prompt,
|
|
405
|
+
outputSchema: options.outputSchema,
|
|
406
|
+
repo: {
|
|
407
|
+
gitServerId: repo?.gitServerId,
|
|
408
|
+
serverRepoId: repo?.serverRepoId,
|
|
409
|
+
owner: repo?.owner,
|
|
410
|
+
name: repo?.name
|
|
411
|
+
},
|
|
412
|
+
git: {
|
|
413
|
+
ref: options.ref ?? detected.git?.ref,
|
|
414
|
+
sha: options.sha ?? detected.git?.sha,
|
|
415
|
+
baseRef: options.baseRef ?? detected.git?.baseRef,
|
|
416
|
+
headRef: options.headRef ?? detected.git?.headRef,
|
|
417
|
+
prNumber: options.prNumber ?? detected.git?.prNumber,
|
|
418
|
+
issueNumber: options.issueNumber ?? detected.git?.issueNumber
|
|
419
|
+
},
|
|
420
|
+
execution: {
|
|
421
|
+
capabilityProfile: options.capabilityProfile,
|
|
422
|
+
runnerId: options.runnerId
|
|
423
|
+
},
|
|
424
|
+
context: detected.context?.ciProvider ? {
|
|
425
|
+
ciProvider: detected.context.ciProvider,
|
|
426
|
+
eventName: process.env.AGENTRIX_EVENT_NAME || detected.context.eventName,
|
|
427
|
+
eventAction: process.env.AGENTRIX_EVENT_ACTION || detected.context.eventAction,
|
|
428
|
+
jobUrl: process.env.AGENTRIX_JOB_URL || detected.context.jobUrl,
|
|
429
|
+
runUrl: process.env.AGENTRIX_RUN_URL || detected.context.runUrl,
|
|
430
|
+
payload: detected.context.payload
|
|
431
|
+
} : void 0,
|
|
432
|
+
metadata: Object.keys(options.metadata).length > 0 ? options.metadata : void 0
|
|
433
|
+
};
|
|
434
|
+
return shared.CreateCiRunRequestSchema.parse(request);
|
|
435
|
+
}
|
|
436
|
+
function resolveBaseUrl(options) {
|
|
437
|
+
const value = options.baseUrl || process.env.AGENTRIX_BASE_URL;
|
|
438
|
+
if (!value) {
|
|
439
|
+
throw new Error("Missing --base-url and AGENTRIX_BASE_URL");
|
|
440
|
+
}
|
|
441
|
+
return value.replace(/\/+$/, "");
|
|
442
|
+
}
|
|
443
|
+
function resolveApiKey(options) {
|
|
444
|
+
const value = options.apiKey || process.env.AGENTRIX_API_KEY;
|
|
445
|
+
if (!value) {
|
|
446
|
+
throw new Error("Missing --api-key and AGENTRIX_API_KEY");
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
async function requestJson(method, url, apiKey, body) {
|
|
451
|
+
const response = await fetch(url, {
|
|
452
|
+
method,
|
|
453
|
+
headers: {
|
|
454
|
+
Authorization: `Bearer ${apiKey}`,
|
|
455
|
+
"Content-Type": "application/json"
|
|
456
|
+
},
|
|
457
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
458
|
+
});
|
|
459
|
+
const text = await response.text();
|
|
460
|
+
const parsed = text ? JSON.parse(text) : void 0;
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
const message = parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
463
|
+
throw new Error(message);
|
|
464
|
+
}
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
async function createRun(baseUrl, apiKey, request) {
|
|
468
|
+
const response = await requestJson("POST", `${baseUrl}/v1/ci/runs`, apiKey, request);
|
|
469
|
+
return shared.CreateCiRunResponseSchema.parse(response);
|
|
470
|
+
}
|
|
471
|
+
function exitCodeForStatus(status) {
|
|
472
|
+
return status === "completed" ? 0 : 1;
|
|
473
|
+
}
|
|
474
|
+
function printStandardFields(payload) {
|
|
475
|
+
console.log(`run_id=${payload.runId}`);
|
|
476
|
+
console.log(`status=${payload.status}`);
|
|
477
|
+
console.log(`detail_url=${payload.detailUrl}`);
|
|
478
|
+
if (payload.result) {
|
|
479
|
+
console.log(`result=${payload.result}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function ensureWaitDeadline(options) {
|
|
483
|
+
const timeoutSeconds = options.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
|
|
484
|
+
return Date.now() + timeoutSeconds * 1e3;
|
|
485
|
+
}
|
|
486
|
+
function writeJsonLine(value) {
|
|
487
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
function writeStreamEvent(type, data) {
|
|
491
|
+
const record = {
|
|
492
|
+
type,
|
|
493
|
+
data
|
|
494
|
+
};
|
|
495
|
+
writeJsonLine(record);
|
|
496
|
+
}
|
|
497
|
+
function buildFinalResultRecord(status) {
|
|
498
|
+
return {
|
|
499
|
+
runId: status.runId,
|
|
500
|
+
status: status.status,
|
|
501
|
+
result: status.result,
|
|
502
|
+
structuredOutput: status.structuredOutput,
|
|
503
|
+
detailUrl: status.detailUrl
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function writeResultFile(path, status) {
|
|
507
|
+
if (!path) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
node_fs.writeFileSync(path, `${JSON.stringify(buildFinalResultRecord(status), null, 2)}
|
|
511
|
+
`, "utf8");
|
|
512
|
+
}
|
|
513
|
+
function writeAsyncResultFile(path, run) {
|
|
514
|
+
if (!path) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const record = {
|
|
518
|
+
runId: run.runId,
|
|
519
|
+
status: run.status,
|
|
520
|
+
result: "",
|
|
521
|
+
detailUrl: run.detailUrl
|
|
522
|
+
};
|
|
523
|
+
node_fs.writeFileSync(path, `${JSON.stringify(record, null, 2)}
|
|
524
|
+
`, "utf8");
|
|
525
|
+
}
|
|
526
|
+
function parseSseBlock(block) {
|
|
527
|
+
const normalized = block.replace(/\r/g, "");
|
|
528
|
+
const lines = normalized.split("\n");
|
|
529
|
+
let event = "message";
|
|
530
|
+
const dataLines = [];
|
|
531
|
+
for (const line of lines) {
|
|
532
|
+
if (!line) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (line.startsWith(":")) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (line.startsWith("event:")) {
|
|
539
|
+
event = line.slice("event:".length).trim();
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (line.startsWith("data:")) {
|
|
543
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (dataLines.length === 0) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const dataText = dataLines.join("\n");
|
|
550
|
+
return {
|
|
551
|
+
event,
|
|
552
|
+
data: JSON.parse(dataText)
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async function createRunStream(baseUrl, apiKey, request, deadline) {
|
|
556
|
+
const timeoutMs = Math.max(deadline - Date.now(), 1);
|
|
557
|
+
const controller = new AbortController();
|
|
558
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
559
|
+
try {
|
|
560
|
+
const response = await fetch(`${baseUrl}/v1/ci/runs`, {
|
|
561
|
+
method: "POST",
|
|
562
|
+
headers: {
|
|
563
|
+
Authorization: `Bearer ${apiKey}`,
|
|
564
|
+
"Content-Type": "application/json",
|
|
565
|
+
Accept: "text/event-stream"
|
|
566
|
+
},
|
|
567
|
+
body: JSON.stringify(request),
|
|
568
|
+
signal: controller.signal
|
|
569
|
+
});
|
|
570
|
+
if (!response.ok) {
|
|
571
|
+
const text = await response.text();
|
|
572
|
+
const parsed = text ? JSON.parse(text) : void 0;
|
|
573
|
+
const message = parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
574
|
+
throw new Error(message);
|
|
575
|
+
}
|
|
576
|
+
if (!response.body) {
|
|
577
|
+
throw new Error("Missing SSE response body");
|
|
578
|
+
}
|
|
579
|
+
const reader = response.body.getReader();
|
|
580
|
+
const decoder = new TextDecoder();
|
|
581
|
+
let buffer = "";
|
|
582
|
+
for (; ; ) {
|
|
583
|
+
const { value, done } = await reader.read();
|
|
584
|
+
if (done) {
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
buffer += decoder.decode(value, { stream: true });
|
|
588
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
589
|
+
while (true) {
|
|
590
|
+
const delimiterIndex = buffer.indexOf("\n\n");
|
|
591
|
+
if (delimiterIndex === -1) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
const block = buffer.slice(0, delimiterIndex);
|
|
595
|
+
buffer = buffer.slice(delimiterIndex + 2);
|
|
596
|
+
const parsed = parseSseBlock(block);
|
|
597
|
+
if (!parsed) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (parsed.event === "run.created" || parsed.event === "task.event") {
|
|
601
|
+
writeStreamEvent(parsed.event, parsed.data);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (parsed.event === "run.completed" || parsed.event === "run.failed" || parsed.event === "run.canceled" || parsed.event === "run.timeout") {
|
|
605
|
+
const finalStatus = shared.CiRunStatusResponseSchema.parse(parsed.data);
|
|
606
|
+
writeStreamEvent(parsed.event, finalStatus);
|
|
607
|
+
return finalStatus;
|
|
608
|
+
}
|
|
609
|
+
if (parsed.event === "run.error") {
|
|
610
|
+
writeStreamEvent(parsed.event, parsed.data);
|
|
611
|
+
const error = parsed.data;
|
|
612
|
+
throw new Error(typeof error.message === "string" ? error.message : "CI run stream failed");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
buffer += decoder.decode();
|
|
617
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
618
|
+
throw new Error("CI run stream ended before terminal event");
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
621
|
+
throw new Error("Timed out waiting for CI run terminal status");
|
|
622
|
+
}
|
|
623
|
+
throw error;
|
|
624
|
+
} finally {
|
|
625
|
+
clearTimeout(timeoutHandle);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function main(argv = process.argv.slice(2)) {
|
|
629
|
+
const options = parseArgs(argv);
|
|
630
|
+
const detected = detectCiContext();
|
|
631
|
+
const baseUrl = resolveBaseUrl(options);
|
|
632
|
+
const apiKey = resolveApiKey(options);
|
|
633
|
+
const request = buildRequest(options, detected);
|
|
634
|
+
if (options.responseMode === "async") {
|
|
635
|
+
const run = await createRun(baseUrl, apiKey, request);
|
|
636
|
+
writeAsyncResultFile(options.resultFile, run);
|
|
637
|
+
printStandardFields({
|
|
638
|
+
runId: run.runId,
|
|
639
|
+
status: run.status,
|
|
640
|
+
detailUrl: run.detailUrl
|
|
641
|
+
});
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
const deadline = ensureWaitDeadline(options);
|
|
645
|
+
const finalStatus = await createRunStream(baseUrl, apiKey, request, deadline);
|
|
646
|
+
writeResultFile(options.resultFile, finalStatus);
|
|
647
|
+
return exitCodeForStatus(finalStatus.status);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
main().then((code) => {
|
|
651
|
+
process.exitCode = code;
|
|
652
|
+
}).catch((error) => {
|
|
653
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
654
|
+
console.error(message);
|
|
655
|
+
process.exitCode = 1;
|
|
656
|
+
});
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { CreateCiRunRequestSchema, CiRunStatusResponseSchema, CreateCiRunResponseSchema } from '@agentrix/shared';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_RESPONSE_MODE = "stream";
|
|
5
|
+
const DEFAULT_WAIT_TIMEOUT_SECONDS = 1800;
|
|
6
|
+
function usage() {
|
|
7
|
+
return [
|
|
8
|
+
"Usage: agentrix-run --agent <agent> --prompt <prompt> [options]",
|
|
9
|
+
"",
|
|
10
|
+
"Options:",
|
|
11
|
+
" --title <text>",
|
|
12
|
+
" --repo <json>",
|
|
13
|
+
" --output-schema <json>",
|
|
14
|
+
" --ref <ref>",
|
|
15
|
+
" --sha <sha>",
|
|
16
|
+
" --base-ref <ref>",
|
|
17
|
+
" --head-ref <ref>",
|
|
18
|
+
" --pr-number <number>",
|
|
19
|
+
" --issue-number <number>",
|
|
20
|
+
" --response-mode <stream|async>",
|
|
21
|
+
" --timeout <seconds>",
|
|
22
|
+
" --capability-profile <name>",
|
|
23
|
+
" --runner-id <cloud-id|machine-id>",
|
|
24
|
+
" --result-file <path>",
|
|
25
|
+
" --base-url <url>",
|
|
26
|
+
" --api-key <api-key>",
|
|
27
|
+
" --metadata <key=value>",
|
|
28
|
+
" --help"
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function requireValue(args, index, flag) {
|
|
32
|
+
const value = args[index + 1];
|
|
33
|
+
if (!value || value.startsWith("--")) {
|
|
34
|
+
throw new Error(`Missing value for ${flag}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function parseRepo(value) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(value);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Invalid --repo JSON: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
44
|
+
}
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new Error("--repo must be a JSON object");
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function formatSchemaPath(path) {
|
|
54
|
+
return path.length > 0 ? path.join(".") : "$";
|
|
55
|
+
}
|
|
56
|
+
function normalizeStrictStructuredOutputSchemaNode(value, path, warnings) {
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return value.map(
|
|
59
|
+
(item, index) => normalizeStrictStructuredOutputSchemaNode(item, [...path, String(index)], warnings)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!isPlainObject(value)) {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
const node = {};
|
|
66
|
+
for (const [key, child] of Object.entries(value)) {
|
|
67
|
+
node[key] = normalizeStrictStructuredOutputSchemaNode(child, [...path, key], warnings);
|
|
68
|
+
}
|
|
69
|
+
const hasProperties = isPlainObject(node.properties);
|
|
70
|
+
const typeValue = node.type;
|
|
71
|
+
const isObjectSchema = typeValue === "object" || Array.isArray(typeValue) && typeValue.includes("object") || hasProperties;
|
|
72
|
+
if (isObjectSchema) {
|
|
73
|
+
if (node.additionalProperties !== false) {
|
|
74
|
+
node.additionalProperties = false;
|
|
75
|
+
warnings.push(`${formatSchemaPath(path)}: set additionalProperties=false`);
|
|
76
|
+
}
|
|
77
|
+
if (hasProperties) {
|
|
78
|
+
const properties = node.properties;
|
|
79
|
+
const propertyNames = Object.keys(properties);
|
|
80
|
+
const requiredValue = node.required;
|
|
81
|
+
if (requiredValue !== void 0 && !Array.isArray(requiredValue)) {
|
|
82
|
+
throw new Error(`${formatSchemaPath(path)}.required must be an array of property names`);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(requiredValue)) {
|
|
85
|
+
const unknownNames = requiredValue.filter(
|
|
86
|
+
(item) => typeof item !== "string" || !propertyNames.includes(item)
|
|
87
|
+
);
|
|
88
|
+
if (unknownNames.length > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${formatSchemaPath(path)}.required contains unknown properties: ${unknownNames.map(String).join(", ")}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const normalizedRequired = [...propertyNames];
|
|
95
|
+
const currentRequired = Array.isArray(requiredValue) ? requiredValue : [];
|
|
96
|
+
const isSameRequired = currentRequired.length === normalizedRequired.length && currentRequired.every((item, index) => item === normalizedRequired[index]);
|
|
97
|
+
if (!isSameRequired) {
|
|
98
|
+
node.required = normalizedRequired;
|
|
99
|
+
warnings.push(`${formatSchemaPath(path)}: normalized required to all properties`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
105
|
+
function normalizeStrictStructuredOutputSchema(schema) {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const normalized = normalizeStrictStructuredOutputSchemaNode(schema, [], warnings);
|
|
108
|
+
if (!isPlainObject(normalized)) {
|
|
109
|
+
throw new Error("--output-schema must normalize to a JSON object");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
schema: normalized,
|
|
113
|
+
warnings
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function printOutputSchemaWarnings(warnings) {
|
|
117
|
+
if (warnings.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const warning of warnings) {
|
|
121
|
+
process.stderr.write(`[agentrix-run] normalized output schema: ${warning}
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseOutputSchema(value) {
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = JSON.parse(value);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`Invalid --output-schema JSON: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
131
|
+
}
|
|
132
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
133
|
+
throw new Error("--output-schema must be a JSON object");
|
|
134
|
+
}
|
|
135
|
+
const normalized = normalizeStrictStructuredOutputSchema(parsed);
|
|
136
|
+
printOutputSchemaWarnings(normalized.warnings);
|
|
137
|
+
return normalized.schema;
|
|
138
|
+
}
|
|
139
|
+
function parseMetadataEntry(value) {
|
|
140
|
+
const separatorIndex = value.indexOf("=");
|
|
141
|
+
if (separatorIndex <= 0) {
|
|
142
|
+
throw new Error(`Invalid --metadata value: ${value}`);
|
|
143
|
+
}
|
|
144
|
+
const key = value.slice(0, separatorIndex).trim();
|
|
145
|
+
const metadataValue = value.slice(separatorIndex + 1).trim();
|
|
146
|
+
if (!key) {
|
|
147
|
+
throw new Error(`Invalid --metadata key: ${value}`);
|
|
148
|
+
}
|
|
149
|
+
return [key, metadataValue];
|
|
150
|
+
}
|
|
151
|
+
function parseInteger(value, flag) {
|
|
152
|
+
const parsed = Number.parseInt(value, 10);
|
|
153
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
154
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
function parseArgs(argv) {
|
|
159
|
+
const options = {
|
|
160
|
+
agent: "",
|
|
161
|
+
prompt: "",
|
|
162
|
+
responseMode: DEFAULT_RESPONSE_MODE,
|
|
163
|
+
metadata: {}
|
|
164
|
+
};
|
|
165
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
166
|
+
const arg = argv[index];
|
|
167
|
+
switch (arg) {
|
|
168
|
+
case "--help":
|
|
169
|
+
console.log(usage());
|
|
170
|
+
process.exit(0);
|
|
171
|
+
case "--agent":
|
|
172
|
+
options.agent = requireValue(argv, index, arg);
|
|
173
|
+
index += 1;
|
|
174
|
+
break;
|
|
175
|
+
case "--title":
|
|
176
|
+
options.title = requireValue(argv, index, arg);
|
|
177
|
+
index += 1;
|
|
178
|
+
break;
|
|
179
|
+
case "--prompt":
|
|
180
|
+
options.prompt = requireValue(argv, index, arg);
|
|
181
|
+
index += 1;
|
|
182
|
+
break;
|
|
183
|
+
case "--repo":
|
|
184
|
+
options.repo = parseRepo(requireValue(argv, index, arg));
|
|
185
|
+
index += 1;
|
|
186
|
+
break;
|
|
187
|
+
case "--output-schema":
|
|
188
|
+
options.outputSchema = parseOutputSchema(requireValue(argv, index, arg));
|
|
189
|
+
index += 1;
|
|
190
|
+
break;
|
|
191
|
+
case "--ref":
|
|
192
|
+
options.ref = requireValue(argv, index, arg);
|
|
193
|
+
index += 1;
|
|
194
|
+
break;
|
|
195
|
+
case "--sha":
|
|
196
|
+
options.sha = requireValue(argv, index, arg);
|
|
197
|
+
index += 1;
|
|
198
|
+
break;
|
|
199
|
+
case "--base-ref":
|
|
200
|
+
options.baseRef = requireValue(argv, index, arg);
|
|
201
|
+
index += 1;
|
|
202
|
+
break;
|
|
203
|
+
case "--head-ref":
|
|
204
|
+
options.headRef = requireValue(argv, index, arg);
|
|
205
|
+
index += 1;
|
|
206
|
+
break;
|
|
207
|
+
case "--pr-number":
|
|
208
|
+
options.prNumber = parseInteger(requireValue(argv, index, arg), arg);
|
|
209
|
+
index += 1;
|
|
210
|
+
break;
|
|
211
|
+
case "--issue-number":
|
|
212
|
+
options.issueNumber = parseInteger(requireValue(argv, index, arg), arg);
|
|
213
|
+
index += 1;
|
|
214
|
+
break;
|
|
215
|
+
case "--response-mode": {
|
|
216
|
+
const value = requireValue(argv, index, arg);
|
|
217
|
+
if (value !== "stream" && value !== "async") {
|
|
218
|
+
throw new Error(`Unsupported --response-mode: ${value}`);
|
|
219
|
+
}
|
|
220
|
+
options.responseMode = value;
|
|
221
|
+
index += 1;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "--timeout":
|
|
225
|
+
options.timeoutSeconds = parseInteger(requireValue(argv, index, arg), arg);
|
|
226
|
+
index += 1;
|
|
227
|
+
break;
|
|
228
|
+
case "--capability-profile":
|
|
229
|
+
options.capabilityProfile = requireValue(argv, index, arg);
|
|
230
|
+
index += 1;
|
|
231
|
+
break;
|
|
232
|
+
case "--runner-id":
|
|
233
|
+
options.runnerId = requireValue(argv, index, arg);
|
|
234
|
+
index += 1;
|
|
235
|
+
break;
|
|
236
|
+
case "--result-file":
|
|
237
|
+
options.resultFile = requireValue(argv, index, arg);
|
|
238
|
+
index += 1;
|
|
239
|
+
break;
|
|
240
|
+
case "--base-url":
|
|
241
|
+
options.baseUrl = requireValue(argv, index, arg);
|
|
242
|
+
index += 1;
|
|
243
|
+
break;
|
|
244
|
+
case "--api-key":
|
|
245
|
+
options.apiKey = requireValue(argv, index, arg);
|
|
246
|
+
index += 1;
|
|
247
|
+
break;
|
|
248
|
+
case "--metadata": {
|
|
249
|
+
const [key, value] = parseMetadataEntry(requireValue(argv, index, arg));
|
|
250
|
+
options.metadata[key] = value;
|
|
251
|
+
index += 1;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!options.agent.trim()) {
|
|
259
|
+
throw new Error("--agent is required");
|
|
260
|
+
}
|
|
261
|
+
if (!options.prompt.trim()) {
|
|
262
|
+
throw new Error("--prompt is required");
|
|
263
|
+
}
|
|
264
|
+
return options;
|
|
265
|
+
}
|
|
266
|
+
function normalizeRef(value) {
|
|
267
|
+
if (!value) {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
return value.replace(/^refs\/heads\//, "");
|
|
271
|
+
}
|
|
272
|
+
function safeReadJsonFile(path) {
|
|
273
|
+
if (!path) {
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const content = readFileSync(path, "utf8");
|
|
278
|
+
const parsed = JSON.parse(content);
|
|
279
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
280
|
+
return parsed;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
function splitRepositoryPath(value) {
|
|
288
|
+
if (!value) {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
const parts = value.split("/").filter(Boolean);
|
|
292
|
+
if (parts.length < 2) {
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
owner: parts.slice(0, -1).join("/"),
|
|
297
|
+
name: parts[parts.length - 1]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function optionalPositiveInt(value) {
|
|
301
|
+
if (!value) {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
const parsed = Number.parseInt(value, 10);
|
|
305
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
306
|
+
}
|
|
307
|
+
function getNestedNumber(value, ...keys) {
|
|
308
|
+
let current = value;
|
|
309
|
+
for (const key of keys) {
|
|
310
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
current = current[key];
|
|
314
|
+
}
|
|
315
|
+
return typeof current === "number" ? current : void 0;
|
|
316
|
+
}
|
|
317
|
+
function getNestedString(value, ...keys) {
|
|
318
|
+
let current = value;
|
|
319
|
+
for (const key of keys) {
|
|
320
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
321
|
+
return void 0;
|
|
322
|
+
}
|
|
323
|
+
current = current[key];
|
|
324
|
+
}
|
|
325
|
+
return typeof current === "string" ? current : void 0;
|
|
326
|
+
}
|
|
327
|
+
function detectGithub() {
|
|
328
|
+
if (process.env.GITHUB_ACTIONS !== "true") {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const payload = safeReadJsonFile(process.env.GITHUB_EVENT_PATH);
|
|
332
|
+
const repository = splitRepositoryPath(process.env.GITHUB_REPOSITORY);
|
|
333
|
+
const runUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : void 0;
|
|
334
|
+
return {
|
|
335
|
+
repo: {
|
|
336
|
+
gitServerId: process.env.AGENTRIX_GIT_SERVER_ID || "github",
|
|
337
|
+
owner: repository.owner,
|
|
338
|
+
name: repository.name
|
|
339
|
+
},
|
|
340
|
+
git: {
|
|
341
|
+
ref: normalizeRef(process.env.GITHUB_REF),
|
|
342
|
+
sha: process.env.GITHUB_SHA,
|
|
343
|
+
baseRef: process.env.GITHUB_BASE_REF || getNestedString(payload, "pull_request", "base", "ref"),
|
|
344
|
+
headRef: process.env.GITHUB_HEAD_REF || getNestedString(payload, "pull_request", "head", "ref"),
|
|
345
|
+
prNumber: getNestedNumber(payload, "pull_request", "number"),
|
|
346
|
+
issueNumber: getNestedNumber(payload, "issue", "number")
|
|
347
|
+
},
|
|
348
|
+
context: {
|
|
349
|
+
ciProvider: "github_actions",
|
|
350
|
+
eventName: process.env.GITHUB_EVENT_NAME,
|
|
351
|
+
eventAction: getNestedString(payload, "action"),
|
|
352
|
+
runUrl,
|
|
353
|
+
payload
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function detectGitlab() {
|
|
358
|
+
if (process.env.GITLAB_CI !== "true") {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const repository = splitRepositoryPath(process.env.CI_PROJECT_PATH);
|
|
362
|
+
return {
|
|
363
|
+
repo: {
|
|
364
|
+
gitServerId: process.env.AGENTRIX_GIT_SERVER_ID || "gitlab",
|
|
365
|
+
owner: repository.owner,
|
|
366
|
+
name: repository.name
|
|
367
|
+
},
|
|
368
|
+
git: {
|
|
369
|
+
ref: process.env.CI_COMMIT_REF_NAME,
|
|
370
|
+
sha: process.env.CI_COMMIT_SHA,
|
|
371
|
+
baseRef: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
372
|
+
headRef: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
373
|
+
prNumber: optionalPositiveInt(process.env.CI_MERGE_REQUEST_IID),
|
|
374
|
+
issueNumber: optionalPositiveInt(process.env.AGENTRIX_ISSUE_NUMBER)
|
|
375
|
+
},
|
|
376
|
+
context: {
|
|
377
|
+
ciProvider: "gitlab_ci",
|
|
378
|
+
eventName: process.env.AGENTRIX_EVENT_NAME || process.env.CI_PIPELINE_SOURCE,
|
|
379
|
+
eventAction: process.env.AGENTRIX_EVENT_ACTION,
|
|
380
|
+
jobUrl: process.env.CI_JOB_URL,
|
|
381
|
+
runUrl: process.env.CI_PIPELINE_URL
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function detectCiContext() {
|
|
386
|
+
return detectGithub() ?? detectGitlab() ?? {};
|
|
387
|
+
}
|
|
388
|
+
function mergeRepo(explicitRepo, detectedRepo) {
|
|
389
|
+
if (!explicitRepo && !detectedRepo) {
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
...detectedRepo ?? {},
|
|
394
|
+
...explicitRepo ?? {}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function buildRequest(options, detected) {
|
|
398
|
+
const repo = mergeRepo(options.repo, detected.repo);
|
|
399
|
+
const request = {
|
|
400
|
+
agent: options.agent,
|
|
401
|
+
title: options.title,
|
|
402
|
+
prompt: options.prompt,
|
|
403
|
+
outputSchema: options.outputSchema,
|
|
404
|
+
repo: {
|
|
405
|
+
gitServerId: repo?.gitServerId,
|
|
406
|
+
serverRepoId: repo?.serverRepoId,
|
|
407
|
+
owner: repo?.owner,
|
|
408
|
+
name: repo?.name
|
|
409
|
+
},
|
|
410
|
+
git: {
|
|
411
|
+
ref: options.ref ?? detected.git?.ref,
|
|
412
|
+
sha: options.sha ?? detected.git?.sha,
|
|
413
|
+
baseRef: options.baseRef ?? detected.git?.baseRef,
|
|
414
|
+
headRef: options.headRef ?? detected.git?.headRef,
|
|
415
|
+
prNumber: options.prNumber ?? detected.git?.prNumber,
|
|
416
|
+
issueNumber: options.issueNumber ?? detected.git?.issueNumber
|
|
417
|
+
},
|
|
418
|
+
execution: {
|
|
419
|
+
capabilityProfile: options.capabilityProfile,
|
|
420
|
+
runnerId: options.runnerId
|
|
421
|
+
},
|
|
422
|
+
context: detected.context?.ciProvider ? {
|
|
423
|
+
ciProvider: detected.context.ciProvider,
|
|
424
|
+
eventName: process.env.AGENTRIX_EVENT_NAME || detected.context.eventName,
|
|
425
|
+
eventAction: process.env.AGENTRIX_EVENT_ACTION || detected.context.eventAction,
|
|
426
|
+
jobUrl: process.env.AGENTRIX_JOB_URL || detected.context.jobUrl,
|
|
427
|
+
runUrl: process.env.AGENTRIX_RUN_URL || detected.context.runUrl,
|
|
428
|
+
payload: detected.context.payload
|
|
429
|
+
} : void 0,
|
|
430
|
+
metadata: Object.keys(options.metadata).length > 0 ? options.metadata : void 0
|
|
431
|
+
};
|
|
432
|
+
return CreateCiRunRequestSchema.parse(request);
|
|
433
|
+
}
|
|
434
|
+
function resolveBaseUrl(options) {
|
|
435
|
+
const value = options.baseUrl || process.env.AGENTRIX_BASE_URL;
|
|
436
|
+
if (!value) {
|
|
437
|
+
throw new Error("Missing --base-url and AGENTRIX_BASE_URL");
|
|
438
|
+
}
|
|
439
|
+
return value.replace(/\/+$/, "");
|
|
440
|
+
}
|
|
441
|
+
function resolveApiKey(options) {
|
|
442
|
+
const value = options.apiKey || process.env.AGENTRIX_API_KEY;
|
|
443
|
+
if (!value) {
|
|
444
|
+
throw new Error("Missing --api-key and AGENTRIX_API_KEY");
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
}
|
|
448
|
+
async function requestJson(method, url, apiKey, body) {
|
|
449
|
+
const response = await fetch(url, {
|
|
450
|
+
method,
|
|
451
|
+
headers: {
|
|
452
|
+
Authorization: `Bearer ${apiKey}`,
|
|
453
|
+
"Content-Type": "application/json"
|
|
454
|
+
},
|
|
455
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
456
|
+
});
|
|
457
|
+
const text = await response.text();
|
|
458
|
+
const parsed = text ? JSON.parse(text) : void 0;
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
const message = parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
461
|
+
throw new Error(message);
|
|
462
|
+
}
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
async function createRun(baseUrl, apiKey, request) {
|
|
466
|
+
const response = await requestJson("POST", `${baseUrl}/v1/ci/runs`, apiKey, request);
|
|
467
|
+
return CreateCiRunResponseSchema.parse(response);
|
|
468
|
+
}
|
|
469
|
+
function exitCodeForStatus(status) {
|
|
470
|
+
return status === "completed" ? 0 : 1;
|
|
471
|
+
}
|
|
472
|
+
function printStandardFields(payload) {
|
|
473
|
+
console.log(`run_id=${payload.runId}`);
|
|
474
|
+
console.log(`status=${payload.status}`);
|
|
475
|
+
console.log(`detail_url=${payload.detailUrl}`);
|
|
476
|
+
if (payload.result) {
|
|
477
|
+
console.log(`result=${payload.result}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function ensureWaitDeadline(options) {
|
|
481
|
+
const timeoutSeconds = options.timeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
|
|
482
|
+
return Date.now() + timeoutSeconds * 1e3;
|
|
483
|
+
}
|
|
484
|
+
function writeJsonLine(value) {
|
|
485
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
486
|
+
`);
|
|
487
|
+
}
|
|
488
|
+
function writeStreamEvent(type, data) {
|
|
489
|
+
const record = {
|
|
490
|
+
type,
|
|
491
|
+
data
|
|
492
|
+
};
|
|
493
|
+
writeJsonLine(record);
|
|
494
|
+
}
|
|
495
|
+
function buildFinalResultRecord(status) {
|
|
496
|
+
return {
|
|
497
|
+
runId: status.runId,
|
|
498
|
+
status: status.status,
|
|
499
|
+
result: status.result,
|
|
500
|
+
structuredOutput: status.structuredOutput,
|
|
501
|
+
detailUrl: status.detailUrl
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function writeResultFile(path, status) {
|
|
505
|
+
if (!path) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
writeFileSync(path, `${JSON.stringify(buildFinalResultRecord(status), null, 2)}
|
|
509
|
+
`, "utf8");
|
|
510
|
+
}
|
|
511
|
+
function writeAsyncResultFile(path, run) {
|
|
512
|
+
if (!path) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const record = {
|
|
516
|
+
runId: run.runId,
|
|
517
|
+
status: run.status,
|
|
518
|
+
result: "",
|
|
519
|
+
detailUrl: run.detailUrl
|
|
520
|
+
};
|
|
521
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}
|
|
522
|
+
`, "utf8");
|
|
523
|
+
}
|
|
524
|
+
function parseSseBlock(block) {
|
|
525
|
+
const normalized = block.replace(/\r/g, "");
|
|
526
|
+
const lines = normalized.split("\n");
|
|
527
|
+
let event = "message";
|
|
528
|
+
const dataLines = [];
|
|
529
|
+
for (const line of lines) {
|
|
530
|
+
if (!line) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (line.startsWith(":")) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (line.startsWith("event:")) {
|
|
537
|
+
event = line.slice("event:".length).trim();
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (line.startsWith("data:")) {
|
|
541
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (dataLines.length === 0) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const dataText = dataLines.join("\n");
|
|
548
|
+
return {
|
|
549
|
+
event,
|
|
550
|
+
data: JSON.parse(dataText)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function createRunStream(baseUrl, apiKey, request, deadline) {
|
|
554
|
+
const timeoutMs = Math.max(deadline - Date.now(), 1);
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
557
|
+
try {
|
|
558
|
+
const response = await fetch(`${baseUrl}/v1/ci/runs`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: {
|
|
561
|
+
Authorization: `Bearer ${apiKey}`,
|
|
562
|
+
"Content-Type": "application/json",
|
|
563
|
+
Accept: "text/event-stream"
|
|
564
|
+
},
|
|
565
|
+
body: JSON.stringify(request),
|
|
566
|
+
signal: controller.signal
|
|
567
|
+
});
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
const text = await response.text();
|
|
570
|
+
const parsed = text ? JSON.parse(text) : void 0;
|
|
571
|
+
const message = parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
|
|
572
|
+
throw new Error(message);
|
|
573
|
+
}
|
|
574
|
+
if (!response.body) {
|
|
575
|
+
throw new Error("Missing SSE response body");
|
|
576
|
+
}
|
|
577
|
+
const reader = response.body.getReader();
|
|
578
|
+
const decoder = new TextDecoder();
|
|
579
|
+
let buffer = "";
|
|
580
|
+
for (; ; ) {
|
|
581
|
+
const { value, done } = await reader.read();
|
|
582
|
+
if (done) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
buffer += decoder.decode(value, { stream: true });
|
|
586
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
587
|
+
while (true) {
|
|
588
|
+
const delimiterIndex = buffer.indexOf("\n\n");
|
|
589
|
+
if (delimiterIndex === -1) {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
const block = buffer.slice(0, delimiterIndex);
|
|
593
|
+
buffer = buffer.slice(delimiterIndex + 2);
|
|
594
|
+
const parsed = parseSseBlock(block);
|
|
595
|
+
if (!parsed) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (parsed.event === "run.created" || parsed.event === "task.event") {
|
|
599
|
+
writeStreamEvent(parsed.event, parsed.data);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (parsed.event === "run.completed" || parsed.event === "run.failed" || parsed.event === "run.canceled" || parsed.event === "run.timeout") {
|
|
603
|
+
const finalStatus = CiRunStatusResponseSchema.parse(parsed.data);
|
|
604
|
+
writeStreamEvent(parsed.event, finalStatus);
|
|
605
|
+
return finalStatus;
|
|
606
|
+
}
|
|
607
|
+
if (parsed.event === "run.error") {
|
|
608
|
+
writeStreamEvent(parsed.event, parsed.data);
|
|
609
|
+
const error = parsed.data;
|
|
610
|
+
throw new Error(typeof error.message === "string" ? error.message : "CI run stream failed");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
buffer += decoder.decode();
|
|
615
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
616
|
+
throw new Error("CI run stream ended before terminal event");
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
619
|
+
throw new Error("Timed out waiting for CI run terminal status");
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
} finally {
|
|
623
|
+
clearTimeout(timeoutHandle);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async function main(argv = process.argv.slice(2)) {
|
|
627
|
+
const options = parseArgs(argv);
|
|
628
|
+
const detected = detectCiContext();
|
|
629
|
+
const baseUrl = resolveBaseUrl(options);
|
|
630
|
+
const apiKey = resolveApiKey(options);
|
|
631
|
+
const request = buildRequest(options, detected);
|
|
632
|
+
if (options.responseMode === "async") {
|
|
633
|
+
const run = await createRun(baseUrl, apiKey, request);
|
|
634
|
+
writeAsyncResultFile(options.resultFile, run);
|
|
635
|
+
printStandardFields({
|
|
636
|
+
runId: run.runId,
|
|
637
|
+
status: run.status,
|
|
638
|
+
detailUrl: run.detailUrl
|
|
639
|
+
});
|
|
640
|
+
return 0;
|
|
641
|
+
}
|
|
642
|
+
const deadline = ensureWaitDeadline(options);
|
|
643
|
+
const finalStatus = await createRunStream(baseUrl, apiKey, request, deadline);
|
|
644
|
+
writeResultFile(options.resultFile, finalStatus);
|
|
645
|
+
return exitCodeForStatus(finalStatus.status);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
main().then((code) => {
|
|
649
|
+
process.exitCode = code;
|
|
650
|
+
}).catch((error) => {
|
|
651
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
652
|
+
console.error(message);
|
|
653
|
+
process.exitCode = 1;
|
|
654
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentrix/agentrix-run",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Native CI submit-and-observe wrapper for Agentrix",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentrix-run": "./bin/agentrix-run.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.cts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"bin",
|
|
27
|
+
"package.json"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"build": "shx rm -rf dist && tsc --noEmit && pkgroll",
|
|
32
|
+
"prepublishOnly": "yarn build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@agentrix/shared": "^2.12.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": ">=20",
|
|
39
|
+
"pkgroll": "^2.14.2",
|
|
40
|
+
"shx": "^0.3.3",
|
|
41
|
+
"typescript": "^5"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"registry": "https://registry.npmjs.org"
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "yarn@1.22.22"
|
|
48
|
+
}
|