@toloka-platform/sdk 0.2.0-preview.2 → 0.2.0-preview.4
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 +10 -4
- package/dist/cli/toloka.js +162 -154
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/authoring.md +18 -1
- package/docs/cli-v2.md +6 -4
- package/docs/cli.md +58 -11
- package/docs/getting-started.md +3 -3
- package/docs/limitations.md +1 -1
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/examples/face-image-collection/README.md +2 -2
- package/openapi/gen/api.ts +78 -0
- package/openapi/gen/models/baseModel.ts +1 -0
- package/openapi/gen/models/checkReport.ts +2 -1
- package/openapi/gen/models/checkReportAnyOfTwoseven.ts +5 -0
- package/openapi/gen/models/checkReportAnyOfTwosevenResultsItem.ts +6 -0
- package/openapi/gen/models/checkReportAnyOfTwosevenResultsItemVerdict.ts +2 -0
- package/openapi/gen/models/currentUser.ts +3 -0
- package/openapi/gen/models/currentUserTeamsItem.ts +6 -0
- package/openapi/gen/models/deliveryOrder.ts +9 -0
- package/openapi/gen/models/deliveryOrderClientName.ts +2 -0
- package/openapi/gen/models/getTrainingRunMetricHistoryParams.ts +4 -0
- package/openapi/gen/models/gistingTrainingConfiguration.ts +6 -2
- package/openapi/gen/models/gistingTrainingConfigurationBatchSize.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationInput.ts +6 -2
- package/openapi/gen/models/gistingTrainingConfigurationInputBatchSize.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +2 -0
- package/openapi/gen/models/index.ts +28 -4
- package/openapi/gen/models/listDeliveryOrders200.ts +5 -0
- package/openapi/gen/models/sftTrainingConfiguration.ts +6 -3
- package/openapi/gen/models/sftTrainingConfigurationBatchSize.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationInput.ts +6 -3
- package/openapi/gen/models/sftTrainingConfigurationInputBatchSize.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationInputLoraRank.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationLoraRank.ts +2 -0
- package/openapi/gen/models/trainingRunMetricHistory.ts +9 -0
- package/openapi/gen/models/trainingRunMetricHistoryMetric.ts +2 -0
- package/openapi/gen/models/trainingRunMetricHistoryPointsItem.ts +9 -0
- package/openapi/gen/models/trainingRunMetricHistoryPointsItemNonFinite.ts +2 -0
- package/openapi/gen/models/trainingRunMetricHistoryPointsItemValue.ts +2 -0
- package/openapi/gen/models/trainingRunMetrics.ts +9 -0
- package/openapi/gen/models/trainingRunMetricsMetricsItem.ts +8 -0
- package/openapi/gen/models/trainingRunMetricsMetricsItemLatest.ts +9 -0
- package/openapi/gen/models/trainingRunMetricsMetricsItemLatestNonFinite.ts +2 -0
- package/openapi/gen/models/trainingRunMetricsMetricsItemLatestValue.ts +2 -0
- package/openapi/gen/models/trainingRunMetricsMetricsItemMetric.ts +2 -0
- package/openapi/gen/models/trainingRunMetricsUnavailableReason.ts +2 -0
- package/package.json +2 -2
- package/skills/catalog.json +55 -7
- package/skills/toloka-build/SKILL.md +35 -21
- package/skills/toloka-build/references/node-semantics.md +13 -11
- package/skills/toloka-build/references/workflow-and-cli.md +31 -20
- package/skills/toloka-fine-tuning/SKILL.md +155 -0
- package/skills/toloka-get-started/SKILL.md +70 -0
- package/skills/toloka-gisting/SKILL.md +166 -0
- package/skills/toloka-report-issue/references/report-template.md +1 -1
- package/src/authoring/index.ts +14 -0
- package/src/checks/task-ui-typecheck.ts +12 -10
- package/src/cli/analytics.ts +88 -15
- package/src/cli/auth.ts +93 -21
- package/src/cli/auto-update.ts +36 -39
- package/src/cli/check.ts +88 -39
- package/src/cli/cli-entry.ts +3 -2
- package/src/cli/cli-error.ts +43 -0
- package/src/cli/commands/auth.ts +89 -0
- package/src/cli/commands/config.ts +28 -0
- package/src/cli/commands/dataset.ts +85 -0
- package/src/cli/commands/delivery-order.ts +18 -0
- package/src/cli/commands/models.ts +121 -0
- package/src/cli/commands/pipeline.ts +131 -0
- package/src/cli/commands/project.ts +168 -0
- package/src/cli/commands/report.ts +43 -0
- package/src/cli/commands/secret.ts +40 -0
- package/src/cli/commands/skill.ts +36 -0
- package/src/cli/commands/source.ts +204 -0
- package/src/cli/commands/status.ts +17 -0
- package/src/cli/commands/training.ts +261 -0
- package/src/cli/commands/version.ts +14 -0
- package/src/cli/config.ts +7 -8
- package/src/cli/context.ts +251 -0
- package/src/cli/credentials.ts +12 -4
- package/src/cli/data.ts +96 -34
- package/src/cli/delivery-orders.ts +47 -0
- package/src/cli/environment.ts +26 -0
- package/src/cli/file-system.ts +6 -2
- package/src/cli/format-error.ts +45 -2
- package/src/cli/init.ts +14 -11
- package/src/cli/models.ts +43 -12
- package/src/cli/program.ts +120 -1347
- package/src/cli/project-context.ts +126 -43
- package/src/cli/project.ts +80 -28
- package/src/cli/report.ts +24 -9
- package/src/cli/runs.ts +33 -8
- package/src/cli/sdk-version.ts +38 -0
- package/src/cli/settings.ts +13 -5
- package/src/cli/skill.ts +67 -24
- package/src/cli/source.ts +3 -2
- package/src/cli/terminal-io.ts +110 -0
- package/src/cli/trainings.ts +242 -57
- package/src/cli/version.ts +3 -3
- package/src/client.ts +21 -9
- package/src/importer.ts +32 -2
- package/src/transport.ts +13 -1
- package/openapi/gen/models/gistingTrainingConfigurationInputRegime.ts +0 -2
- package/openapi/gen/models/gistingTrainingConfigurationRegime.ts +0 -2
- package/openapi/gen/models/sftTrainingConfigurationInputRegime.ts +0 -2
- package/openapi/gen/models/sftTrainingConfigurationRegime.ts +0 -2
|
@@ -265,6 +265,16 @@ const mapParseErrorsToBlocks = (
|
|
|
265
265
|
return report;
|
|
266
266
|
};
|
|
267
267
|
|
|
268
|
+
const runtimeSyntaxReport = (source: string): TaskUiDiagnosticsReport => {
|
|
269
|
+
const jsxReport = mapParseErrorsToBlocks(source, parseSync("runtime.jsx", source).errors);
|
|
270
|
+
const tsxReport = mapParseErrorsToBlocks(source, parseSync("runtime.tsx", source).errors);
|
|
271
|
+
return {
|
|
272
|
+
...tsxReport,
|
|
273
|
+
preprocess: jsxReport.preprocess,
|
|
274
|
+
postprocess: jsxReport.postprocess,
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
268
278
|
export const createTaskUiTypechecker = ({
|
|
269
279
|
reactDeclaration,
|
|
270
280
|
elementsDeclaration,
|
|
@@ -311,11 +321,7 @@ export const createTaskUiTypechecker = ({
|
|
|
311
321
|
...syntacticDiagnostics,
|
|
312
322
|
...otherDiagnostics,
|
|
313
323
|
]);
|
|
314
|
-
const
|
|
315
|
-
const runtimeReport = mapParseErrorsToBlocks(
|
|
316
|
-
runtimeSource,
|
|
317
|
-
parseSync("runtime.jsx", runtimeSource).errors,
|
|
318
|
-
);
|
|
324
|
+
const runtimeReport = runtimeSyntaxReport(buildRuntimeSource(input));
|
|
319
325
|
return mergeRuntimeDiagnostics(typeScriptReport, syntacticReport, runtimeReport);
|
|
320
326
|
},
|
|
321
327
|
|
|
@@ -325,11 +331,7 @@ export const createTaskUiTypechecker = ({
|
|
|
325
331
|
typeScriptSource,
|
|
326
332
|
parseSync(FILE_NAME, typeScriptSource).errors,
|
|
327
333
|
);
|
|
328
|
-
const
|
|
329
|
-
const runtimeReport = mapParseErrorsToBlocks(
|
|
330
|
-
runtimeSource,
|
|
331
|
-
parseSync("runtime.jsx", runtimeSource).errors,
|
|
332
|
-
);
|
|
334
|
+
const runtimeReport = runtimeSyntaxReport(buildRuntimeSource(input));
|
|
333
335
|
return mergeRuntimeDiagnostics(typeScriptReport, typeScriptReport, runtimeReport);
|
|
334
336
|
},
|
|
335
337
|
});
|
package/src/cli/analytics.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { PostHog } from "posthog-node";
|
|
6
|
-
import { PipelineGraphValidationError
|
|
5
|
+
import { PipelineGraphValidationError } from "../errors";
|
|
7
6
|
import type { TolokaEnvironment } from "./config";
|
|
8
7
|
import type { CliEnvironment } from "./credentials";
|
|
9
8
|
import { credentialsPath, preparePrivateFilePath } from "./credentials";
|
|
10
9
|
import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
|
|
11
|
-
import { SDK_VERSION } from "./
|
|
10
|
+
import { SDK_VERSION } from "./sdk-version";
|
|
11
|
+
import { assertCli, TolokaCliError } from "./cli-error";
|
|
12
|
+
import { findPotatoApiError } from "./format-error";
|
|
12
13
|
|
|
13
14
|
export const POSTHOG_PROJECT_KEY = "phc_kqHt0f53B5MVqURyzWpW1aNlbOmYt1YbUYza0JPcg19";
|
|
14
15
|
export const POSTHOG_HOST = "https://eu.posthog.com";
|
|
@@ -56,6 +57,11 @@ export type PosthogLike = {
|
|
|
56
57
|
properties?: Record<string, unknown>;
|
|
57
58
|
disableGeoip: boolean;
|
|
58
59
|
}) => void;
|
|
60
|
+
captureException: (
|
|
61
|
+
error: unknown,
|
|
62
|
+
distinctId: string,
|
|
63
|
+
properties?: Record<string, unknown>,
|
|
64
|
+
) => void;
|
|
59
65
|
alias: (event: { distinctId: string; alias: string }) => void;
|
|
60
66
|
flush: () => Promise<void>;
|
|
61
67
|
on?: (event: "error", listener: (error: Error) => void) => void;
|
|
@@ -72,7 +78,9 @@ export function telemetryEnabled(environment: CliEnvironment): boolean {
|
|
|
72
78
|
return true;
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
type CommandGroup = Readonly<Record<string, true | Readonly<Record<string, true>>>>;
|
|
82
|
+
|
|
83
|
+
const COMMAND_GROUPS: Readonly<Record<string, CommandGroup>> = {
|
|
76
84
|
project: { list: true, add: true, switch: true, current: true, remove: true, create: true },
|
|
77
85
|
pipeline: {
|
|
78
86
|
list: true,
|
|
@@ -83,7 +91,31 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
|
|
|
83
91
|
cancel: true,
|
|
84
92
|
results: true,
|
|
85
93
|
},
|
|
86
|
-
dataset: { list: true, fields: true, load: true, upload: true },
|
|
94
|
+
dataset: { list: true, create: true, fields: true, load: true, upload: true },
|
|
95
|
+
"delivery-order": { list: true },
|
|
96
|
+
training: {
|
|
97
|
+
list: true,
|
|
98
|
+
get: true,
|
|
99
|
+
create: true,
|
|
100
|
+
update: true,
|
|
101
|
+
cost: true,
|
|
102
|
+
run: true,
|
|
103
|
+
runs: true,
|
|
104
|
+
status: true,
|
|
105
|
+
resume: true,
|
|
106
|
+
cancel: true,
|
|
107
|
+
metrics: true,
|
|
108
|
+
logs: true,
|
|
109
|
+
},
|
|
110
|
+
"base-model": { list: true, get: true },
|
|
111
|
+
model: {
|
|
112
|
+
list: true,
|
|
113
|
+
get: true,
|
|
114
|
+
versions: true,
|
|
115
|
+
create: true,
|
|
116
|
+
archive: true,
|
|
117
|
+
version: { download: true },
|
|
118
|
+
},
|
|
87
119
|
secret: { set: true },
|
|
88
120
|
skill: { list: true, search: true, install: true },
|
|
89
121
|
config: { get: true, set: true },
|
|
@@ -115,9 +147,13 @@ const nameFromTokens = (tokens: readonly string[]): string => {
|
|
|
115
147
|
if (subcommands === undefined) return ROOT_COMMANDS[command] === true ? command : "unknown";
|
|
116
148
|
|
|
117
149
|
const subcommand = tokens[1];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
const nested = subcommand === undefined ? undefined : subcommands[subcommand];
|
|
151
|
+
if (nested === undefined) return command;
|
|
152
|
+
if (nested === true) return `${command} ${subcommand}`;
|
|
153
|
+
const leaf = tokens[2];
|
|
154
|
+
return leaf !== undefined && nested[leaf] === true
|
|
155
|
+
? `${command} ${subcommand} ${leaf}`
|
|
156
|
+
: `${command} ${subcommand}`;
|
|
121
157
|
};
|
|
122
158
|
|
|
123
159
|
export function commandNameFromArgv(argv: readonly string[]): string {
|
|
@@ -138,12 +174,19 @@ export function commandNameFromArgv(argv: readonly string[]): string {
|
|
|
138
174
|
|
|
139
175
|
export function classifyCliError(error: unknown): { readonly error_code: string } {
|
|
140
176
|
if (error === undefined) return { error_code: "command_failed" };
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
177
|
+
const apiError = findPotatoApiError(error);
|
|
178
|
+
if (apiError !== undefined) return { error_code: apiError.code };
|
|
179
|
+
if (error instanceof PipelineGraphValidationError) return { error_code: error.code };
|
|
144
180
|
return { error_code: error instanceof Error ? "error" : "unknown" };
|
|
145
181
|
}
|
|
146
182
|
|
|
183
|
+
const cliErrorProperties = (error: unknown): Record<string, unknown> => {
|
|
184
|
+
const apiError = findPotatoApiError(error);
|
|
185
|
+
return apiError === undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { status_code: apiError.status, request_id: apiError.requestId };
|
|
188
|
+
};
|
|
189
|
+
|
|
147
190
|
const anonymousIdPath = (environment: CliEnvironment): string =>
|
|
148
191
|
join(dirname(credentialsPath(environment)), "anonymous-id");
|
|
149
192
|
|
|
@@ -171,7 +214,11 @@ const noopAnalytics = (): CliAnalytics => ({
|
|
|
171
214
|
identifyCurrentUser() {},
|
|
172
215
|
captureCommand() {},
|
|
173
216
|
async submitIssueReport() {
|
|
174
|
-
|
|
217
|
+
assertCli(
|
|
218
|
+
false,
|
|
219
|
+
"Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.",
|
|
220
|
+
{ hint: { disabled: true } },
|
|
221
|
+
);
|
|
175
222
|
},
|
|
176
223
|
async shutdown() {},
|
|
177
224
|
});
|
|
@@ -237,18 +284,40 @@ export function createCliAnalytics(options: {
|
|
|
237
284
|
.then(() => {
|
|
238
285
|
if (client === undefined || distinctId === undefined) return;
|
|
239
286
|
const shared = sharedProperties();
|
|
287
|
+
const command = nameFromTokens(event.command.split(" "));
|
|
288
|
+
|
|
240
289
|
if (event.outcome === "failed") {
|
|
241
290
|
const classified = classifyCliError(event.error);
|
|
291
|
+
const errorProperties = cliErrorProperties(event.error);
|
|
292
|
+
|
|
293
|
+
if (event.error !== undefined) {
|
|
294
|
+
const exception = new TolokaCliError(`CLI command failed: ${classified.error_code}`, {
|
|
295
|
+
hint: { disabled: true },
|
|
296
|
+
});
|
|
297
|
+
exception.name = "PotatoCliError";
|
|
298
|
+
client.captureException(exception, distinctId, {
|
|
299
|
+
...shared,
|
|
300
|
+
command,
|
|
301
|
+
duration_ms: event.durationMs,
|
|
302
|
+
exit_code: event.exitCode,
|
|
303
|
+
$process_person_profile: false,
|
|
304
|
+
$geoip_disable: true,
|
|
305
|
+
...classified,
|
|
306
|
+
...errorProperties,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
242
310
|
client.capture({
|
|
243
311
|
distinctId,
|
|
244
312
|
event: "sdk_cli_command_failed",
|
|
245
313
|
disableGeoip: true,
|
|
246
314
|
properties: {
|
|
247
315
|
...shared,
|
|
248
|
-
command
|
|
316
|
+
command,
|
|
249
317
|
duration_ms: event.durationMs,
|
|
250
318
|
exit_code: event.exitCode,
|
|
251
319
|
...classified,
|
|
320
|
+
...errorProperties,
|
|
252
321
|
},
|
|
253
322
|
});
|
|
254
323
|
return;
|
|
@@ -259,7 +328,7 @@ export function createCliAnalytics(options: {
|
|
|
259
328
|
disableGeoip: true,
|
|
260
329
|
properties: {
|
|
261
330
|
...shared,
|
|
262
|
-
command
|
|
331
|
+
command,
|
|
263
332
|
duration_ms: event.durationMs,
|
|
264
333
|
exit_code: event.exitCode,
|
|
265
334
|
outcome: event.outcome,
|
|
@@ -270,7 +339,11 @@ export function createCliAnalytics(options: {
|
|
|
270
339
|
},
|
|
271
340
|
submitIssueReport(report) {
|
|
272
341
|
const submission = queue.then(async () => {
|
|
273
|
-
|
|
342
|
+
assertCli(
|
|
343
|
+
client && distinctId,
|
|
344
|
+
"PostHog issue reporting could not be initialized.",
|
|
345
|
+
{ hint: { description: "Set TOLOKA_TELEMETRY=1 and check that this machine can reach PostHog, then retry." } },
|
|
346
|
+
);
|
|
274
347
|
deliveryErrors.length = 0;
|
|
275
348
|
client.capture({
|
|
276
349
|
distinctId,
|
package/src/cli/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { createClient } from "../client";
|
|
2
|
+
import { PotatoApiError } from "../errors";
|
|
3
3
|
import { PRODUCTION_BASE_URL, consoleUrlFor, resolveInternalBaseUrl } from "./config";
|
|
4
4
|
import {
|
|
5
5
|
clearStoredCredentials,
|
|
@@ -18,6 +18,41 @@ import type {
|
|
|
18
18
|
} from "./credentials";
|
|
19
19
|
import type { ProjectContext } from "./project-context";
|
|
20
20
|
import { formatProjectContextLine } from "./project-context";
|
|
21
|
+
import { assertCli, TolokaCliError } from "./cli-error";
|
|
22
|
+
|
|
23
|
+
type ProbeFailureKind = "rejected" | "unreachable" | "platform";
|
|
24
|
+
|
|
25
|
+
type ProbeFailure = { readonly kind: ProbeFailureKind; readonly reason: string };
|
|
26
|
+
|
|
27
|
+
const classifyProbeFailure = (error: unknown): ProbeFailure => {
|
|
28
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
29
|
+
if (!(error instanceof PotatoApiError) || error.status === undefined)
|
|
30
|
+
return { kind: "unreachable", reason };
|
|
31
|
+
return {
|
|
32
|
+
kind: error.status === 401 || error.status === 403 ? "rejected" : "platform",
|
|
33
|
+
reason,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const PROBE_FAILURE_HEADLINE: Record<ProbeFailureKind, string> = {
|
|
38
|
+
rejected: "Rejected",
|
|
39
|
+
unreachable: "Not reachable",
|
|
40
|
+
platform: "Platform error",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const LOGIN_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
|
|
44
|
+
rejected: () => "Check that the key was copied whole and has not been revoked.",
|
|
45
|
+
unreachable: (baseUrl) =>
|
|
46
|
+
`${baseUrl} did not answer, so the key was not evaluated. Check network access, VPN, or proxy settings and retry.`,
|
|
47
|
+
platform: () => "The platform failed before confirming the key. Retry later.",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const STATUS_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
|
|
51
|
+
rejected: () => "Run toloka login to store a working key.",
|
|
52
|
+
unreachable: (baseUrl) =>
|
|
53
|
+
`Check network access to ${baseUrl}; the stored key was not evaluated.`,
|
|
54
|
+
platform: () => "The platform failed before confirming the key. Retry later.",
|
|
55
|
+
};
|
|
21
56
|
|
|
22
57
|
export const apiKeyInstructions = (baseUrl: string): string => {
|
|
23
58
|
const console = consoleUrlFor(baseUrl);
|
|
@@ -54,6 +89,14 @@ export type AuthProbe = {
|
|
|
54
89
|
readonly pagination: { readonly total: number };
|
|
55
90
|
}>;
|
|
56
91
|
};
|
|
92
|
+
readonly me?: () => Promise<{
|
|
93
|
+
readonly internal: boolean;
|
|
94
|
+
readonly teams?: ReadonlyArray<{
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly writable: boolean;
|
|
97
|
+
readonly internal?: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
}>;
|
|
57
100
|
};
|
|
58
101
|
|
|
59
102
|
export type LoginOptions = {
|
|
@@ -76,21 +119,26 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
76
119
|
const write = options.write ?? (() => undefined);
|
|
77
120
|
const baseUrl = normalizeBaseUrl(
|
|
78
121
|
options.baseUrl ??
|
|
79
|
-
environment.
|
|
122
|
+
environment.TOLOKA_BASE_URL ??
|
|
80
123
|
resolveInternalBaseUrl(environment) ??
|
|
81
124
|
PRODUCTION_BASE_URL,
|
|
82
125
|
);
|
|
83
126
|
|
|
84
|
-
let apiKey = options.apiKey ?? environment.
|
|
127
|
+
let apiKey = options.apiKey ?? environment.TOLOKA_API_KEY;
|
|
85
128
|
if (apiKey === undefined) {
|
|
86
|
-
|
|
129
|
+
assertCli(
|
|
87
130
|
options.prompt,
|
|
88
|
-
"Missing API key.
|
|
131
|
+
"Missing API key. Without a terminal, pipe the key to toloka login --api-key-stdin, pass --api-key, or set TOLOKA_API_KEY.",
|
|
132
|
+
{ hint: { disabled: true } },
|
|
89
133
|
);
|
|
90
134
|
write(`\n${apiKeyInstructions(baseUrl)}\n`);
|
|
91
135
|
apiKey = (await options.prompt("API key: ", { mask: true })).trim();
|
|
92
136
|
}
|
|
93
|
-
|
|
137
|
+
assertCli(
|
|
138
|
+
apiKey.length > 0,
|
|
139
|
+
"No API key entered.",
|
|
140
|
+
{ hint: { description: "Re-run toloka login and paste the key at the prompt, or pass --api-key." } },
|
|
141
|
+
);
|
|
94
142
|
const credentials: StoredCredentials = { baseUrl, apiKey };
|
|
95
143
|
const probe: AuthProbe = options.probe?.(credentials) ?? createClient(credentials);
|
|
96
144
|
|
|
@@ -99,14 +147,14 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
99
147
|
const page = await probe.projects.listPage({ limit: 1 });
|
|
100
148
|
reachable = `Authenticated against ${baseUrl}: ${page.pagination.total} project(s) visible.`;
|
|
101
149
|
} catch (error) {
|
|
102
|
-
const
|
|
103
|
-
throw new
|
|
150
|
+
const failure = classifyProbeFailure(error);
|
|
151
|
+
throw new TolokaCliError(
|
|
104
152
|
[
|
|
105
|
-
`Could not authenticate against ${baseUrl}: ${reason}`,
|
|
153
|
+
`Could not authenticate against ${baseUrl}: ${failure.reason}`,
|
|
106
154
|
"",
|
|
107
|
-
|
|
155
|
+
`Nothing was saved. ${LOGIN_FAILURE_ADVICE[failure.kind](baseUrl)}`,
|
|
108
156
|
].join("\n"),
|
|
109
|
-
{ cause: error },
|
|
157
|
+
{ cause: error, hint: { disabled: true } },
|
|
110
158
|
);
|
|
111
159
|
}
|
|
112
160
|
|
|
@@ -136,7 +184,7 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
136
184
|
reachable,
|
|
137
185
|
`Saved credentials to ${credentialsFile}.`,
|
|
138
186
|
"",
|
|
139
|
-
"toloka reads them automatically. --api-key and
|
|
187
|
+
"toloka reads them automatically. --api-key and TOLOKA_API_KEY still win when set.",
|
|
140
188
|
].join("\n"),
|
|
141
189
|
};
|
|
142
190
|
}
|
|
@@ -183,9 +231,9 @@ const resolveApiKey = (
|
|
|
183
231
|
options.flagApiKey === undefined
|
|
184
232
|
? undefined
|
|
185
233
|
: { value: options.flagApiKey, source: "from --api-key" },
|
|
186
|
-
environment.
|
|
234
|
+
environment.TOLOKA_API_KEY === undefined
|
|
187
235
|
? undefined
|
|
188
|
-
: { value: environment.
|
|
236
|
+
: { value: environment.TOLOKA_API_KEY, source: "from TOLOKA_API_KEY" },
|
|
189
237
|
found === undefined
|
|
190
238
|
? undefined
|
|
191
239
|
: { value: found.credentials.apiKey, source: `stored in ${storedPath}` },
|
|
@@ -202,9 +250,9 @@ const resolvePlatform = (
|
|
|
202
250
|
options.flagBaseUrl === undefined
|
|
203
251
|
? undefined
|
|
204
252
|
: { value: options.flagBaseUrl.replace(/\/+$/u, ""), source: "from --base-url" },
|
|
205
|
-
environment.
|
|
253
|
+
environment.TOLOKA_BASE_URL === undefined
|
|
206
254
|
? undefined
|
|
207
|
-
: { value: environment.
|
|
255
|
+
: { value: environment.TOLOKA_BASE_URL, source: "from TOLOKA_BASE_URL" },
|
|
208
256
|
internalBaseUrl === undefined
|
|
209
257
|
? undefined
|
|
210
258
|
: { value: internalBaseUrl, source: `from ENV=${environment.ENV}` },
|
|
@@ -213,11 +261,29 @@ const resolvePlatform = (
|
|
|
213
261
|
);
|
|
214
262
|
};
|
|
215
263
|
|
|
264
|
+
const accountLines = async (probe: AuthProbe): Promise<readonly string[]> => {
|
|
265
|
+
if (probe.me === undefined) return [];
|
|
266
|
+
try {
|
|
267
|
+
const me = await probe.me();
|
|
268
|
+
if (me.teams === undefined) return [`Internal: ${me.internal}`];
|
|
269
|
+
const teams = me.teams.map((team) => {
|
|
270
|
+
const qualifiers = [...(team.internal ? ["internal"] : []), ...(team.writable ? [] : ["read-only"])];
|
|
271
|
+
return qualifiers.length === 0 ? team.id : `${team.id} (${qualifiers.join(", ")})`;
|
|
272
|
+
});
|
|
273
|
+
return [
|
|
274
|
+
`Internal: ${me.internal}`,
|
|
275
|
+
`Teams: ${teams.length === 0 ? "(none)" : teams.join(", ")}`,
|
|
276
|
+
];
|
|
277
|
+
} catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
216
282
|
export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthStatusResult> {
|
|
217
283
|
const environment = options.environment ?? process.env;
|
|
218
284
|
const credentialLookup =
|
|
219
285
|
options.credentialLookup ??
|
|
220
|
-
(await findCommandCredentials(environment, options.flagApiKey ?? environment.
|
|
286
|
+
(await findCommandCredentials(environment, options.flagApiKey ?? environment.TOLOKA_API_KEY));
|
|
221
287
|
const { found, warning } = credentialLookup;
|
|
222
288
|
const storedPath = found?.path ?? credentialsPath(environment);
|
|
223
289
|
const apiKey = resolveApiKey(options, environment, found, storedPath);
|
|
@@ -244,8 +310,8 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
244
310
|
projectLine,
|
|
245
311
|
);
|
|
246
312
|
|
|
247
|
-
if (environment.
|
|
248
|
-
lines.push("", `
|
|
313
|
+
if (environment.TOLOKA_API_KEY !== undefined && found !== undefined) {
|
|
314
|
+
lines.push("", `TOLOKA_API_KEY is set, so the key stored in ${found.path} is ignored.`);
|
|
249
315
|
}
|
|
250
316
|
if (found?.legacy === true) {
|
|
251
317
|
lines.push(
|
|
@@ -260,10 +326,16 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
260
326
|
try {
|
|
261
327
|
const page = await probe.projects.listPage({ limit: 1 });
|
|
262
328
|
lines.push("", `Reachable: ${page.pagination.total} project(s) visible.`);
|
|
329
|
+
lines.push(...(await accountLines(probe)));
|
|
263
330
|
return { authenticated: true, output: lines.join("\n") };
|
|
264
331
|
} catch (error) {
|
|
265
|
-
const
|
|
266
|
-
lines.push(
|
|
332
|
+
const failure = classifyProbeFailure(error);
|
|
333
|
+
lines.push(
|
|
334
|
+
"",
|
|
335
|
+
`${PROBE_FAILURE_HEADLINE[failure.kind]}: ${failure.reason}`,
|
|
336
|
+
"",
|
|
337
|
+
STATUS_FAILURE_ADVICE[failure.kind](baseUrl),
|
|
338
|
+
);
|
|
267
339
|
return { authenticated: false, output: lines.join("\n") };
|
|
268
340
|
}
|
|
269
341
|
}
|
package/src/cli/auto-update.ts
CHANGED
|
@@ -6,16 +6,22 @@ import { join, sep } from "node:path";
|
|
|
6
6
|
import { findCliEntry } from "./cli-entry";
|
|
7
7
|
import type { CliEnvironment } from "./credentials";
|
|
8
8
|
import { configDirectory, preparePrivateFilePath } from "./credentials";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
compareSdkVersions,
|
|
11
|
+
releaseChannelFor,
|
|
12
|
+
SDK_PACKAGE_NAME,
|
|
13
|
+
SDK_VERSION,
|
|
14
|
+
type ReleaseChannel,
|
|
15
|
+
} from "./sdk-version";
|
|
10
16
|
import { commandNameFromArgv } from "./analytics";
|
|
11
17
|
import { readCliSettings } from "./settings";
|
|
12
18
|
import { CHECK_RESULT_FILE_ENV } from "./check";
|
|
13
19
|
import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
|
|
14
|
-
import
|
|
20
|
+
import { TolokaCliError } from "./cli-error";
|
|
21
|
+
import { formatCliError } from "./format-error";
|
|
15
22
|
|
|
16
23
|
export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
17
24
|
export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
|
|
18
|
-
export const SDK_PACKAGE_NAME = packageMetadata.name;
|
|
19
25
|
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20
26
|
|
|
21
27
|
const SKIP_COMMANDS: Readonly<Record<string, true>> = {
|
|
@@ -75,32 +81,6 @@ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
|
|
|
75
81
|
export const npmPackageMetadataUrl = (packageName: string): string =>
|
|
76
82
|
new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
|
|
77
83
|
|
|
78
|
-
export const parseSdkVersion = (version: string): readonly number[] | undefined => {
|
|
79
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:-preview\.(\d+))?$/u.exec(version);
|
|
80
|
-
if (match === null) return undefined;
|
|
81
|
-
const [, major, minor, patch, preview] = match;
|
|
82
|
-
return [
|
|
83
|
-
Number(major),
|
|
84
|
-
Number(minor),
|
|
85
|
-
Number(patch),
|
|
86
|
-
preview === undefined ? Number.POSITIVE_INFINITY : Number(preview),
|
|
87
|
-
];
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const compareSdkVersions = (left: string, right: string): number | undefined => {
|
|
91
|
-
const parsedLeft = parseSdkVersion(left);
|
|
92
|
-
const parsedRight = parseSdkVersion(right);
|
|
93
|
-
if (parsedLeft === undefined || parsedRight === undefined) return undefined;
|
|
94
|
-
const length = Math.max(parsedLeft.length, parsedRight.length);
|
|
95
|
-
for (let index = 0; index < length; index += 1) {
|
|
96
|
-
const leftPart = parsedLeft[index] ?? 0;
|
|
97
|
-
const rightPart = parsedRight[index] ?? 0;
|
|
98
|
-
if (leftPart < rightPart) return -1;
|
|
99
|
-
if (leftPart > rightPart) return 1;
|
|
100
|
-
}
|
|
101
|
-
return 0;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
84
|
const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
|
|
105
85
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
106
86
|
const record: Record<string, string> = {};
|
|
@@ -122,9 +102,22 @@ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined =
|
|
|
122
102
|
};
|
|
123
103
|
};
|
|
124
104
|
|
|
125
|
-
export const latestPublishedVersion = (
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
export const latestPublishedVersion = (
|
|
106
|
+
view: NpmPackageView,
|
|
107
|
+
currentVersion: string,
|
|
108
|
+
): string | undefined => {
|
|
109
|
+
const tags = view["dist-tags"];
|
|
110
|
+
if (tags === undefined) return undefined;
|
|
111
|
+
const channels: readonly ReleaseChannel[] =
|
|
112
|
+
releaseChannelFor(currentVersion) === "preview" ? ["latest", "preview"] : ["latest"];
|
|
113
|
+
let newest: string | undefined;
|
|
114
|
+
for (const channel of channels) {
|
|
115
|
+
const candidate = tags[channel];
|
|
116
|
+
if (candidate === undefined || candidate.length === 0) continue;
|
|
117
|
+
if (newest === undefined || (compareSdkVersions(candidate, newest) ?? 0) > 0)
|
|
118
|
+
newest = candidate;
|
|
119
|
+
}
|
|
120
|
+
return newest;
|
|
128
121
|
};
|
|
129
122
|
|
|
130
123
|
const publishedAt = (view: NpmPackageView, version: string): Date | undefined => {
|
|
@@ -208,9 +201,9 @@ const collectOutput = async (stream: NodeJS.ReadableStream | null): Promise<stri
|
|
|
208
201
|
return Buffer.concat(chunks).toString("utf8");
|
|
209
202
|
};
|
|
210
203
|
|
|
211
|
-
class RegistryError extends
|
|
204
|
+
class RegistryError extends TolokaCliError {
|
|
212
205
|
constructor(message: string) {
|
|
213
|
-
super(message);
|
|
206
|
+
super(message, { hint: { disabled: true } });
|
|
214
207
|
this.name = "RegistryError";
|
|
215
208
|
}
|
|
216
209
|
}
|
|
@@ -248,10 +241,16 @@ const defaultInstall: AutoUpdateInstall = async ({
|
|
|
248
241
|
const exitCode = close[0];
|
|
249
242
|
if (exitCode === 0) return;
|
|
250
243
|
const detail = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
|
|
251
|
-
throw new
|
|
244
|
+
throw new TolokaCliError(
|
|
252
245
|
detail.length > 0
|
|
253
246
|
? `bun add --global failed: ${detail}`
|
|
254
247
|
: `bun add --global exited ${String(exitCode)}`,
|
|
248
|
+
{
|
|
249
|
+
hint: {
|
|
250
|
+
description:
|
|
251
|
+
"Run the bun add --global command yourself, or turn the check off with toloka config set auto-update false.",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
255
254
|
);
|
|
256
255
|
};
|
|
257
256
|
|
|
@@ -310,7 +309,7 @@ const selectUpdateCandidate = async (options: {
|
|
|
310
309
|
return { status: "skip" };
|
|
311
310
|
}
|
|
312
311
|
|
|
313
|
-
const version = latestPublishedVersion(view);
|
|
312
|
+
const version = latestPublishedVersion(view, currentVersion);
|
|
314
313
|
if (version === undefined) return { status: "skip" };
|
|
315
314
|
const order = compareSdkVersions(currentVersion, version);
|
|
316
315
|
if (order === undefined || order >= 0) return { status: "skip" };
|
|
@@ -349,9 +348,7 @@ export async function maybeAutoUpdate(options: {
|
|
|
349
348
|
environment,
|
|
350
349
|
});
|
|
351
350
|
} catch (error) {
|
|
352
|
-
io.stderr(
|
|
353
|
-
`Auto-update to ${candidate.version} failed: ${error instanceof Error ? error.message : String(error)}.`,
|
|
354
|
-
);
|
|
351
|
+
io.stderr(`Auto-update to ${candidate.version} failed: ${formatCliError(error)}`);
|
|
355
352
|
return continueResult();
|
|
356
353
|
}
|
|
357
354
|
|