@kortyx/cli 0.13.0 → 0.15.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/CHANGELOG.md +30 -0
- package/dist/index.js +342 -42
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.15.0](https://github.com/kortyx-io/kortyx/compare/cli-v0.14.0...cli-v0.15.0) (2026-10-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **telemetry:** preserve complete private error diagnostics ([#290](https://github.com/kortyx-io/kortyx/issues/290)) ([e1c5a88](https://github.com/kortyx-io/kortyx/commit/e1c5a88c494016fd84a1af40579412fcfde31271))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @kortyx/agent bumped to 0.31.1
|
|
16
|
+
* @kortyx/core bumped to 0.12.0
|
|
17
|
+
* @kortyx/telemetry-contracts bumped to 0.15.0
|
|
18
|
+
|
|
19
|
+
## [0.14.0](https://github.com/kortyx-io/kortyx/compare/cli-v0.13.0...cli-v0.14.0) (2026-10-07)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **evals:** group suite runs and expose CLI results ([#284](https://github.com/kortyx-io/kortyx/issues/284)) ([146fd71](https://github.com/kortyx-io/kortyx/commit/146fd71a88587e5591dd0511a4c0cb57969f9d07))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Dependencies
|
|
28
|
+
|
|
29
|
+
* The following workspace dependencies were updated
|
|
30
|
+
* dependencies
|
|
31
|
+
* @kortyx/agent bumped to 0.31.0
|
|
32
|
+
|
|
3
33
|
## [0.13.0](https://github.com/kortyx-io/kortyx/compare/cli-v0.12.0...cli-v0.13.0) (2026-10-07)
|
|
4
34
|
|
|
5
35
|
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/index.ts
|
|
26
|
-
var
|
|
26
|
+
var import_promises6 = require("fs/promises");
|
|
27
27
|
var import_node_module = __toESM(require("module"));
|
|
28
28
|
var import_node_os3 = require("os");
|
|
29
29
|
var import_node_path6 = require("path");
|
|
@@ -148,7 +148,7 @@ var StudioApiTransport = class {
|
|
|
148
148
|
}
|
|
149
149
|
if (!response.ok) {
|
|
150
150
|
await response.body?.cancel();
|
|
151
|
-
const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read or the operation's required permission (eval:run for execution)." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
|
|
151
|
+
const message = response.status === 401 ? "Invalid, expired, or revoked Studio read key." : response.status === 403 ? "The API key lacks studio:read or the operation's required permission (diagnostics:read for diagnostics; eval:run for execution)." : response.status === 404 ? "Entity not found in this connection's project. Check the URL and selected connection." : `Studio API returned HTTP ${response.status}.`;
|
|
152
152
|
throw new StudioReadError(
|
|
153
153
|
"api_error",
|
|
154
154
|
message,
|
|
@@ -208,7 +208,7 @@ var StudioApiTransport = class {
|
|
|
208
208
|
};
|
|
209
209
|
var StudioReadClient = class extends StudioApiTransport {
|
|
210
210
|
async get(path, schema, query = {}) {
|
|
211
|
-
if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)
|
|
211
|
+
if (!/^\/v1\/studio\/(?:context|catalogs|workflows|(?:runs|sessions|interrupts)(?:\/[^/?#]+)?|diagnostics\/[a-f0-9-]{36})$/.test(
|
|
212
212
|
path
|
|
213
213
|
) || /(?:^|\/)(?:\.|\.\.|%2e(?:%2e)?)(?:\/|$)/i.test(path)) {
|
|
214
214
|
throw new StudioReadError(
|
|
@@ -236,6 +236,7 @@ x-api-env: &api-env
|
|
|
236
236
|
x-bootstrap-keys: &bootstrap-keys
|
|
237
237
|
KORTYX_TELEMETRY_API_KEY: \${KORTYX_TELEMETRY_API_KEY}
|
|
238
238
|
KORTYX_STUDIO_API_KEY: \${KORTYX_STUDIO_API_KEY}
|
|
239
|
+
KORTYX_STUDIO_ENABLE_DIAGNOSTICS: \${KORTYX_STUDIO_ENABLE_DIAGNOSTICS:-0}
|
|
239
240
|
KORTYX_STUDIO_ENABLE_EVALS: \${KORTYX_STUDIO_ENABLE_EVALS:-0}
|
|
240
241
|
KORTYX_STUDIO_ENABLE_REVIEWS: \${KORTYX_STUDIO_ENABLE_REVIEWS:-0}
|
|
241
242
|
|
|
@@ -1430,7 +1431,9 @@ var parseEvalRunTarget = (input) => {
|
|
|
1430
1431
|
} catch {
|
|
1431
1432
|
throw new StudioReadError("invalid_target", "Invalid eval run URL.");
|
|
1432
1433
|
}
|
|
1433
|
-
const match = locator.pathname.match(
|
|
1434
|
+
const match = locator.pathname.match(
|
|
1435
|
+
/\/evals\/(?:runs|evaluations)\/([^/]+)\/?$/
|
|
1436
|
+
);
|
|
1434
1437
|
if (!match || !["http:", "https:"].includes(locator.protocol) || locator.username || locator.password)
|
|
1435
1438
|
throw new StudioReadError(
|
|
1436
1439
|
"invalid_target",
|
|
@@ -1446,6 +1449,59 @@ var parseEvalRunTarget = (input) => {
|
|
|
1446
1449
|
return { id, url };
|
|
1447
1450
|
};
|
|
1448
1451
|
var StudioEvalClient = class extends StudioApiTransport {
|
|
1452
|
+
evaluations() {
|
|
1453
|
+
return this.requestJson(
|
|
1454
|
+
"GET",
|
|
1455
|
+
"/v1/studio/evals/evaluations",
|
|
1456
|
+
import_evals.StudioEvaluationHistorySchema
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
evaluation(id) {
|
|
1460
|
+
this.validateId(id);
|
|
1461
|
+
return this.requestJson(
|
|
1462
|
+
"GET",
|
|
1463
|
+
`/v1/studio/evals/evaluations/${id}`,
|
|
1464
|
+
import_evals.StudioEvaluationDetailSchema
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
evaluationResults(id) {
|
|
1468
|
+
this.validateId(id);
|
|
1469
|
+
return this.requestJson(
|
|
1470
|
+
"GET",
|
|
1471
|
+
`/v1/studio/evals/evaluations/${id}/results`,
|
|
1472
|
+
import_evals.StudioEvaluationResultsSchema
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
startEvaluation(input) {
|
|
1476
|
+
const parsed = import_evals.StudioEvaluationStartRequestSchema.safeParse(input);
|
|
1477
|
+
if (!parsed.success)
|
|
1478
|
+
throw new StudioReadError(
|
|
1479
|
+
"invalid_eval_request",
|
|
1480
|
+
"Invalid evaluation selection or execution limits."
|
|
1481
|
+
);
|
|
1482
|
+
return this.requestJson(
|
|
1483
|
+
"POST",
|
|
1484
|
+
"/v1/studio/evals/evaluations",
|
|
1485
|
+
import_zod3.z.object({ id: import_zod3.z.uuid() }),
|
|
1486
|
+
{},
|
|
1487
|
+
parsed.data
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
cancelEvaluation(id) {
|
|
1491
|
+
this.validateId(id);
|
|
1492
|
+
return this.requestJson(
|
|
1493
|
+
"POST",
|
|
1494
|
+
`/v1/studio/evals/evaluations/${id}/cancel`,
|
|
1495
|
+
import_zod3.z.object({ ok: import_zod3.z.literal(true) })
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
validateId(id) {
|
|
1499
|
+
if (!import_zod3.z.uuid().safeParse(id).success)
|
|
1500
|
+
throw new StudioReadError(
|
|
1501
|
+
"invalid_target",
|
|
1502
|
+
"Expected an evaluation UUID."
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1449
1505
|
targets() {
|
|
1450
1506
|
return this.requestJson(
|
|
1451
1507
|
"GET",
|
|
@@ -1961,11 +2017,65 @@ function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
|
1961
2017
|
options
|
|
1962
2018
|
);
|
|
1963
2019
|
});
|
|
2020
|
+
const terminal = (status) => status !== "queued" && status !== "running";
|
|
2021
|
+
const resultExitCode = (status) => status === "passed" ? 0 : status === "failed" ? 1 : status === "cancelled" ? 130 : 2;
|
|
2022
|
+
const waitForEvaluation = async (client, id, timeout) => {
|
|
2023
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
2024
|
+
for (; ; ) {
|
|
2025
|
+
const { run: run2 } = await client.evaluation(id);
|
|
2026
|
+
if (terminal(run2.status)) return run2;
|
|
2027
|
+
if (Date.now() >= deadline)
|
|
2028
|
+
throw new StudioReadError(
|
|
2029
|
+
"eval_wait_timeout",
|
|
2030
|
+
`Evaluation ${id} is still running. Inspect it with evals runs get; waiting did not cancel it.`
|
|
2031
|
+
);
|
|
2032
|
+
await new Promise(
|
|
2033
|
+
(resolve6) => setTimeout(resolve6, Math.min(2e3, deadline - Date.now()))
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
const readEvaluation = async (client, id, includeContent) => {
|
|
2038
|
+
const { run: run2 } = await client.evaluationResults(id);
|
|
2039
|
+
return includeContent ? run2 : { ...run2, suites: run2.suites.map(compactDetail) };
|
|
2040
|
+
};
|
|
2041
|
+
const waitOptions = (command) => command.option(
|
|
2042
|
+
"--timeout <seconds>",
|
|
2043
|
+
"Maximum time to wait (1\u20137200 seconds); does not cancel execution.",
|
|
2044
|
+
integer2(7200),
|
|
2045
|
+
1800
|
|
2046
|
+
);
|
|
1964
2047
|
const runs = evals.command("runs").description("Start and inspect saved eval runs.");
|
|
1965
2048
|
selectionOptions(
|
|
1966
|
-
runs.command("start
|
|
1967
|
-
"
|
|
2049
|
+
runs.command("start [suite-id]").description(
|
|
2050
|
+
"Start an evaluation containing all or selected suites; optionally wait for results."
|
|
1968
2051
|
)
|
|
2052
|
+
).option("--all", "Run every suite registered on the selected application.").option(
|
|
2053
|
+
"--suite <id>",
|
|
2054
|
+
"Select a suite; repeat for several suites.",
|
|
2055
|
+
(value, previous) => [...previous, value],
|
|
2056
|
+
[]
|
|
2057
|
+
).option("--name <name>", "Display name for this evaluation run.").option(
|
|
2058
|
+
"--source <source>",
|
|
2059
|
+
"Trigger: manual, deployment, schedule or ci.",
|
|
2060
|
+
(value) => {
|
|
2061
|
+
if (!["manual", "deployment", "schedule", "ci"].includes(value))
|
|
2062
|
+
throw new import_commander3.InvalidArgumentError(
|
|
2063
|
+
"Expected manual, deployment, schedule or ci."
|
|
2064
|
+
);
|
|
2065
|
+
return value;
|
|
2066
|
+
},
|
|
2067
|
+
"manual"
|
|
2068
|
+
).option("--commit <sha>", "Deployed application commit.").option("--deployment-url <url>", "Deployment or CI job URL.").option(
|
|
2069
|
+
"--idempotency-key <key>",
|
|
2070
|
+
"Reuse a matching saved evaluation after a trigger retry."
|
|
2071
|
+
).option(
|
|
2072
|
+
"--wait",
|
|
2073
|
+
"Wait for final results and return a pass/fail exit code."
|
|
2074
|
+
).option(
|
|
2075
|
+
"--timeout <seconds>",
|
|
2076
|
+
"Maximum wait in seconds (1\u20137200); execution continues after timeout.",
|
|
2077
|
+
integer2(7200),
|
|
2078
|
+
1800
|
|
1969
2079
|
).option(
|
|
1970
2080
|
"--case <id>",
|
|
1971
2081
|
"Select a case; repeat for multiple cases.",
|
|
@@ -1986,39 +2096,102 @@ function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
|
1986
2096
|
},
|
|
1987
2097
|
"studio"
|
|
1988
2098
|
).action(async (suiteId, options) => {
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
2099
|
+
const ids = [...suiteId ? [suiteId] : [], ...options.suite ?? []];
|
|
2100
|
+
if (!options.all && !ids.length || options.all && ids.length || new Set(ids).size !== ids.length)
|
|
2101
|
+
throw new StudioReadError(
|
|
2102
|
+
"invalid_suite_selection",
|
|
2103
|
+
"Choose --all or unique suite IDs (positional or repeated --suite)."
|
|
2104
|
+
);
|
|
2105
|
+
const data = await discover(options);
|
|
2106
|
+
const matches = data.targets.filter(
|
|
2107
|
+
(target2) => target2.manifest && (options.all || ids.every(
|
|
2108
|
+
(id) => target2.manifest.suites.some((suite) => suite.id === id)
|
|
2109
|
+
))
|
|
1992
2110
|
);
|
|
1993
|
-
|
|
2111
|
+
const target = matches[0];
|
|
2112
|
+
if (matches.length !== 1 || !target?.manifest)
|
|
2113
|
+
throw new StudioReadError(
|
|
2114
|
+
"invalid_suite_selection",
|
|
2115
|
+
"Select one available application with --target and --environment; all selected suites must belong to it."
|
|
2116
|
+
);
|
|
2117
|
+
if (!data.canRun)
|
|
1994
2118
|
throw new StudioReadError(
|
|
1995
2119
|
"eval_forbidden",
|
|
1996
2120
|
"This Studio key requires eval:run to execute suites."
|
|
1997
2121
|
);
|
|
2122
|
+
const suites2 = options.all ? target.manifest.suites : ids.map(
|
|
2123
|
+
(id) => target.manifest.suites.find((suite) => suite.id === id)
|
|
2124
|
+
);
|
|
1998
2125
|
const caseIds = options.case ?? [];
|
|
1999
|
-
if (
|
|
2126
|
+
if (caseIds.length && (options.all || suites2.length !== 1))
|
|
2000
2127
|
throw new StudioReadError(
|
|
2001
2128
|
"invalid_case_selection",
|
|
2002
|
-
"
|
|
2129
|
+
"--case requires exactly one selected suite."
|
|
2003
2130
|
);
|
|
2004
|
-
|
|
2131
|
+
if (new Set(caseIds).size !== caseIds.length || caseIds.some(
|
|
2132
|
+
(id) => !suites2[0]?.cases.some((item) => item.id === id)
|
|
2133
|
+
) || suites2.some(
|
|
2134
|
+
(suite) => (caseIds.length || suite.cases.length) * options.repetitions > 100
|
|
2135
|
+
))
|
|
2136
|
+
throw new StudioReadError(
|
|
2137
|
+
"invalid_case_selection",
|
|
2138
|
+
"Select unique existing cases and at most 100 attempts per suite."
|
|
2139
|
+
);
|
|
2140
|
+
const run2 = await data.client.startEvaluation({
|
|
2005
2141
|
targetId: target.id,
|
|
2142
|
+
selection: options.all ? "all" : "selected",
|
|
2143
|
+
suites: suites2.map((suite) => ({
|
|
2144
|
+
suiteId: suite.id,
|
|
2145
|
+
suiteRevision: target.revisions[suite.id] ?? "",
|
|
2146
|
+
...caseIds.length ? { caseIds } : {}
|
|
2147
|
+
})),
|
|
2006
2148
|
judge: options.judge ?? "studio",
|
|
2007
|
-
suiteId,
|
|
2008
|
-
suiteRevision: target.revisions[suiteId] ?? "",
|
|
2009
2149
|
repetitions: options.repetitions,
|
|
2010
2150
|
concurrency: options.concurrency,
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
id: run2.id,
|
|
2017
|
-
status: "queued",
|
|
2018
|
-
studioUrl: connection.studioUrl ? `${connection.studioUrl}/evals/runs/${run2.id}` : null
|
|
2151
|
+
name: options.name,
|
|
2152
|
+
metadata: {
|
|
2153
|
+
source: options.source ?? "manual",
|
|
2154
|
+
commit: options.commit,
|
|
2155
|
+
deploymentUrl: options.deploymentUrl
|
|
2019
2156
|
},
|
|
2020
|
-
options
|
|
2021
|
-
);
|
|
2157
|
+
idempotencyKey: options.idempotencyKey
|
|
2158
|
+
});
|
|
2159
|
+
const studioUrl = data.connection.studioUrl ? `${data.connection.studioUrl}/evals/evaluations/${run2.id}` : null;
|
|
2160
|
+
if (!options.wait) {
|
|
2161
|
+
print(
|
|
2162
|
+
{
|
|
2163
|
+
connection: data.connection.name,
|
|
2164
|
+
id: run2.id,
|
|
2165
|
+
status: "queued",
|
|
2166
|
+
studioUrl
|
|
2167
|
+
},
|
|
2168
|
+
options
|
|
2169
|
+
);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
try {
|
|
2173
|
+
const completed = await waitForEvaluation(
|
|
2174
|
+
data.client,
|
|
2175
|
+
run2.id,
|
|
2176
|
+
options.timeout
|
|
2177
|
+
);
|
|
2178
|
+
print(
|
|
2179
|
+
{
|
|
2180
|
+
connection: data.connection.name,
|
|
2181
|
+
studioUrl,
|
|
2182
|
+
run: await readEvaluation(
|
|
2183
|
+
data.client,
|
|
2184
|
+
run2.id,
|
|
2185
|
+
options.includeContent ?? false
|
|
2186
|
+
)
|
|
2187
|
+
},
|
|
2188
|
+
options
|
|
2189
|
+
);
|
|
2190
|
+
process.exitCode = resultExitCode(completed.status);
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
process.exitCode = 2;
|
|
2193
|
+
throw error;
|
|
2194
|
+
}
|
|
2022
2195
|
});
|
|
2023
2196
|
common(
|
|
2024
2197
|
runs.command("list").description("List the latest 100 saved runs in this project.")
|
|
@@ -2027,12 +2200,12 @@ function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
|
2027
2200
|
"Filter results; defaults to the connection environment."
|
|
2028
2201
|
).action(async (options) => {
|
|
2029
2202
|
const { connection, client } = await clientFor(options);
|
|
2030
|
-
const data = await client.
|
|
2203
|
+
const data = await client.evaluations();
|
|
2031
2204
|
const environment = options.environment ?? connection.environment;
|
|
2032
2205
|
print(
|
|
2033
2206
|
{
|
|
2034
2207
|
connection: connection.name,
|
|
2035
|
-
runs: data.runs.filter((run2) => !environment || run2.environment === environment).map(
|
|
2208
|
+
runs: data.runs.filter((run2) => !environment || run2.environment === environment).map((run2) => run2)
|
|
2036
2209
|
},
|
|
2037
2210
|
options
|
|
2038
2211
|
);
|
|
@@ -2044,15 +2217,59 @@ function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
|
2044
2217
|
).action(async (input, options) => {
|
|
2045
2218
|
const target = parseEvalRunTarget(input);
|
|
2046
2219
|
const { connection, client } = await clientFor(options, target.url);
|
|
2047
|
-
|
|
2220
|
+
let run2;
|
|
2221
|
+
try {
|
|
2222
|
+
run2 = await readEvaluation(
|
|
2223
|
+
client,
|
|
2224
|
+
target.id,
|
|
2225
|
+
options.includeContent ?? false
|
|
2226
|
+
);
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
if (!(error instanceof StudioReadError) || error.status !== 404)
|
|
2229
|
+
throw error;
|
|
2230
|
+
const legacy = await client.run(target.id);
|
|
2231
|
+
run2 = options.includeContent ? legacy.run : compactDetail(legacy.run);
|
|
2232
|
+
}
|
|
2048
2233
|
print(
|
|
2049
2234
|
{
|
|
2050
2235
|
connection: connection.name,
|
|
2051
|
-
run:
|
|
2236
|
+
run: run2
|
|
2052
2237
|
},
|
|
2053
2238
|
options
|
|
2054
2239
|
);
|
|
2055
2240
|
});
|
|
2241
|
+
waitOptions(
|
|
2242
|
+
common(
|
|
2243
|
+
runs.command("wait <id-or-url>").description(
|
|
2244
|
+
"Wait for a grouped evaluation and print its final suite/case results."
|
|
2245
|
+
)
|
|
2246
|
+
)
|
|
2247
|
+
).action(async (input, options) => {
|
|
2248
|
+
const target = parseEvalRunTarget(input);
|
|
2249
|
+
const { connection, client } = await clientFor(options, target.url);
|
|
2250
|
+
try {
|
|
2251
|
+
const completed = await waitForEvaluation(
|
|
2252
|
+
client,
|
|
2253
|
+
target.id,
|
|
2254
|
+
options.timeout
|
|
2255
|
+
);
|
|
2256
|
+
print(
|
|
2257
|
+
{
|
|
2258
|
+
connection: connection.name,
|
|
2259
|
+
run: await readEvaluation(
|
|
2260
|
+
client,
|
|
2261
|
+
target.id,
|
|
2262
|
+
options.includeContent ?? false
|
|
2263
|
+
)
|
|
2264
|
+
},
|
|
2265
|
+
options
|
|
2266
|
+
);
|
|
2267
|
+
process.exitCode = resultExitCode(completed.status);
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
process.exitCode = 2;
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2056
2273
|
common(
|
|
2057
2274
|
runs.command("cancel <id-or-url>").description(
|
|
2058
2275
|
"Request cooperative cancellation of a queued or running eval."
|
|
@@ -2060,7 +2277,14 @@ function registerStudioEvalCommands(studio, log, request = fetch) {
|
|
|
2060
2277
|
).action(async (input, options) => {
|
|
2061
2278
|
const target = parseEvalRunTarget(input);
|
|
2062
2279
|
const { connection, client } = await clientFor(options, target.url);
|
|
2063
|
-
|
|
2280
|
+
let data;
|
|
2281
|
+
try {
|
|
2282
|
+
data = await client.cancelEvaluation(target.id);
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
if (!(error instanceof StudioReadError) || error.status !== 404)
|
|
2285
|
+
throw error;
|
|
2286
|
+
data = await client.cancel(target.id);
|
|
2287
|
+
}
|
|
2064
2288
|
print({ connection: connection.name, id: target.id, ...data }, options);
|
|
2065
2289
|
});
|
|
2066
2290
|
}
|
|
@@ -2349,6 +2573,8 @@ var resetStudio = async (home, confirm, runtime) => {
|
|
|
2349
2573
|
};
|
|
2350
2574
|
|
|
2351
2575
|
// src/studio/read-command.ts
|
|
2576
|
+
var import_node_crypto2 = require("crypto");
|
|
2577
|
+
var import_promises5 = require("fs/promises");
|
|
2352
2578
|
var import_telemetry_contracts5 = require("@kortyx/telemetry-contracts");
|
|
2353
2579
|
var import_commander4 = require("commander");
|
|
2354
2580
|
|
|
@@ -2712,6 +2938,80 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
2712
2938
|
)
|
|
2713
2939
|
};
|
|
2714
2940
|
};
|
|
2941
|
+
const diagnostics = studio.command("diagnostics").description("Inspect or download a retained private error diagnostic.");
|
|
2942
|
+
for (const action of ["get", "download"]) {
|
|
2943
|
+
common(
|
|
2944
|
+
diagnostics.command(action).argument("<diagnostic-id>").option(
|
|
2945
|
+
"--environment <name>",
|
|
2946
|
+
"Diagnostic environment (or connection default)."
|
|
2947
|
+
).option(
|
|
2948
|
+
"--output <path>",
|
|
2949
|
+
"Write a diagnostic JSON file without overwriting an existing file."
|
|
2950
|
+
)
|
|
2951
|
+
).action(async (id, options) => {
|
|
2952
|
+
if (!/^[a-f0-9-]{36}$/i.test(id))
|
|
2953
|
+
throw new StudioReadError(
|
|
2954
|
+
"invalid_diagnostic_id",
|
|
2955
|
+
"Expected a diagnostic UUID."
|
|
2956
|
+
);
|
|
2957
|
+
const { connection, client } = await clientFor(options);
|
|
2958
|
+
const environment = options.environment ?? connection.environment;
|
|
2959
|
+
if (!environment)
|
|
2960
|
+
throw new StudioReadError(
|
|
2961
|
+
"environment_required",
|
|
2962
|
+
"Provide --environment for diagnostic retrieval."
|
|
2963
|
+
);
|
|
2964
|
+
const value = await client.get(
|
|
2965
|
+
`/v1/studio/diagnostics/${id}`,
|
|
2966
|
+
import_telemetry_contracts5.DiagnosticResponseSchema,
|
|
2967
|
+
{
|
|
2968
|
+
env: environment,
|
|
2969
|
+
...action === "download" ? { download: "1" } : {}
|
|
2970
|
+
}
|
|
2971
|
+
);
|
|
2972
|
+
if (action === "get") {
|
|
2973
|
+
const { content, ...summary2 } = value;
|
|
2974
|
+
print(
|
|
2975
|
+
{
|
|
2976
|
+
...summary2,
|
|
2977
|
+
capture: {
|
|
2978
|
+
status: content?.capture.status ?? value.manifest.captureStatus,
|
|
2979
|
+
omissions: content?.capture.omissions.length ?? 0,
|
|
2980
|
+
redactions: content?.capture.redactions.length ?? 0
|
|
2981
|
+
},
|
|
2982
|
+
...options.includeContent ? { content } : {},
|
|
2983
|
+
note: "Private diagnostic access requires diagnostics:read. Use download for the complete redacted artifact."
|
|
2984
|
+
},
|
|
2985
|
+
options
|
|
2986
|
+
);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (value.state !== "available" || !value.content)
|
|
2990
|
+
throw new StudioReadError(
|
|
2991
|
+
"diagnostic_incomplete",
|
|
2992
|
+
`Diagnostic is ${value.state}; received ${value.receivedParts} of ${value.manifest.partCount} parts.`
|
|
2993
|
+
);
|
|
2994
|
+
const bytes = JSON.stringify(value.content);
|
|
2995
|
+
if ((0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex") !== value.contentChecksum || Buffer.byteLength(bytes) !== value.contentByteLength)
|
|
2996
|
+
throw new StudioReadError(
|
|
2997
|
+
"diagnostic_integrity_failed",
|
|
2998
|
+
"Diagnostic download failed integrity verification."
|
|
2999
|
+
);
|
|
3000
|
+
if (options.output) {
|
|
3001
|
+
await (0, import_promises5.writeFile)(options.output, bytes, { flag: "wx", mode: 384 });
|
|
3002
|
+
print(
|
|
3003
|
+
{
|
|
3004
|
+
diagnosticId: id,
|
|
3005
|
+
state: value.state,
|
|
3006
|
+
file: options.output,
|
|
3007
|
+
bytes: value.contentByteLength,
|
|
3008
|
+
checksum: value.contentChecksum
|
|
3009
|
+
},
|
|
3010
|
+
options
|
|
3011
|
+
);
|
|
3012
|
+
} else log(bytes);
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
2715
3015
|
const inspect = async (input, options, entity) => {
|
|
2716
3016
|
const target = parseStudioTarget(input, entity);
|
|
2717
3017
|
const { connection, client } = await clientFor(options, target.url);
|
|
@@ -3100,7 +3400,7 @@ var registerStudioReadCommands = (studio, log, request = fetch) => {
|
|
|
3100
3400
|
};
|
|
3101
3401
|
|
|
3102
3402
|
// src/studio/runtime.ts
|
|
3103
|
-
var
|
|
3403
|
+
var import_node_crypto3 = require("crypto");
|
|
3104
3404
|
var import_node_net = require("net");
|
|
3105
3405
|
var stringOutput = (value) => typeof value === "string" ? value : "";
|
|
3106
3406
|
var isCommandNotFound = (error) => error instanceof Error && ("code" in error && error.code === "ENOENT" || error.message.includes("ENOENT"));
|
|
@@ -3139,7 +3439,7 @@ var defaultStudioRuntime = {
|
|
|
3139
3439
|
run,
|
|
3140
3440
|
portAvailable,
|
|
3141
3441
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
3142
|
-
random: (bytes) => (0,
|
|
3442
|
+
random: (bytes) => (0, import_node_crypto3.randomBytes)(bytes).toString("base64url"),
|
|
3143
3443
|
request: fetch,
|
|
3144
3444
|
log: (message = "") => console.log(message),
|
|
3145
3445
|
error: (message = "") => console.error(message)
|
|
@@ -3675,7 +3975,7 @@ var readPackageName = async (cwd) => {
|
|
|
3675
3975
|
let current = cwd;
|
|
3676
3976
|
for (; ; ) {
|
|
3677
3977
|
try {
|
|
3678
|
-
const raw = await (0,
|
|
3978
|
+
const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(current, "package.json"), "utf8");
|
|
3679
3979
|
const parsed = JSON.parse(raw);
|
|
3680
3980
|
if (isRecord(parsed) && typeof parsed.name === "string") {
|
|
3681
3981
|
return parsed.name;
|
|
@@ -3692,7 +3992,7 @@ var loadEnvFiles = async (cwd) => {
|
|
|
3692
3992
|
const path = (0, import_node_path6.join)(cwd, filename);
|
|
3693
3993
|
let raw;
|
|
3694
3994
|
try {
|
|
3695
|
-
raw = await (0,
|
|
3995
|
+
raw = await (0, import_promises6.readFile)(path, "utf8");
|
|
3696
3996
|
} catch {
|
|
3697
3997
|
continue;
|
|
3698
3998
|
}
|
|
@@ -3732,12 +4032,12 @@ var resolveOptions = async (options) => {
|
|
|
3732
4032
|
};
|
|
3733
4033
|
};
|
|
3734
4034
|
var createFrameworkMarkerStubs = async () => {
|
|
3735
|
-
const root = await (0,
|
|
4035
|
+
const root = await (0, import_promises6.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "kortyx-cli-stubs-"));
|
|
3736
4036
|
for (const moduleName of ["server-only", "client-only"]) {
|
|
3737
4037
|
const directory = (0, import_node_path6.join)(root, moduleName);
|
|
3738
|
-
await (0,
|
|
3739
|
-
await (0,
|
|
3740
|
-
await (0,
|
|
4038
|
+
await (0, import_promises6.mkdir)(directory, { recursive: true });
|
|
4039
|
+
await (0, import_promises6.writeFile)((0, import_node_path6.join)(directory, "index.js"), "", "utf8");
|
|
4040
|
+
await (0, import_promises6.writeFile)(
|
|
3741
4041
|
(0, import_node_path6.join)(directory, "package.json"),
|
|
3742
4042
|
JSON.stringify({ name: moduleName, version: "0.0.0", main: "index.js" }),
|
|
3743
4043
|
"utf8"
|
|
@@ -3752,7 +4052,7 @@ var createFrameworkMarkerStubs = async () => {
|
|
|
3752
4052
|
if (previousNodePath === void 0) delete process.env.NODE_PATH;
|
|
3753
4053
|
else process.env.NODE_PATH = previousNodePath;
|
|
3754
4054
|
moduleWithPrivateInit._initPaths?.();
|
|
3755
|
-
await (0,
|
|
4055
|
+
await (0, import_promises6.rm)(root, { recursive: true, force: true });
|
|
3756
4056
|
}
|
|
3757
4057
|
};
|
|
3758
4058
|
};
|
|
@@ -3794,7 +4094,7 @@ var findNearestFile = async (startDirectory, filename) => {
|
|
|
3794
4094
|
for (; ; ) {
|
|
3795
4095
|
const candidate = (0, import_node_path6.join)(current, filename);
|
|
3796
4096
|
try {
|
|
3797
|
-
await (0,
|
|
4097
|
+
await (0, import_promises6.access)(candidate);
|
|
3798
4098
|
return candidate;
|
|
3799
4099
|
} catch {
|
|
3800
4100
|
const parent = (0, import_node_path6.dirname)(current);
|
|
@@ -3806,7 +4106,7 @@ var findNearestFile = async (startDirectory, filename) => {
|
|
|
3806
4106
|
var registerTsconfigPaths = async (tsconfigPath) => {
|
|
3807
4107
|
const parsed = import_typescript2.default.parseConfigFileTextToJson(
|
|
3808
4108
|
tsconfigPath,
|
|
3809
|
-
await (0,
|
|
4109
|
+
await (0, import_promises6.readFile)(tsconfigPath, "utf8")
|
|
3810
4110
|
);
|
|
3811
4111
|
if (parsed.error)
|
|
3812
4112
|
throw new Error(`Invalid TypeScript configuration: ${tsconfigPath}`);
|
|
@@ -4093,7 +4393,7 @@ var main = async () => {
|
|
|
4093
4393
|
console.error(
|
|
4094
4394
|
process.argv.includes("--json") ? JSON.stringify(error.toJSON()) : `[${error.code}] ${error.message}`
|
|
4095
4395
|
);
|
|
4096
|
-
process.exitCode
|
|
4396
|
+
process.exitCode ??= 1;
|
|
4097
4397
|
return;
|
|
4098
4398
|
}
|
|
4099
4399
|
const failure = (0, import_errors.serializeFailure)(error);
|