@toloka-platform/sdk 0.2.0-preview.2 → 0.2.0-preview.3
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 +159 -151
- 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 +4 -3
- package/docs/cli.md +29 -10
- 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/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 +1 -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/index.ts +19 -0
- package/openapi/gen/models/listDeliveryOrders200.ts +5 -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 +10 -7
- package/skills/toloka-build/references/node-semantics.md +6 -4
- package/skills/toloka-build/references/workflow-and-cli.md +21 -13
- package/skills/toloka-fine-tuning/SKILL.md +72 -0
- package/skills/toloka-get-started/SKILL.md +55 -0
- package/skills/toloka-gisting/SKILL.md +76 -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 +52 -6
- package/src/cli/auth.ts +69 -16
- package/src/cli/auto-update.ts +24 -33
- package/src/cli/check.ts +12 -10
- package/src/cli/commands/auth.ts +89 -0
- package/src/cli/commands/config.ts +28 -0
- package/src/cli/commands/dataset.ts +75 -0
- package/src/cli/commands/delivery-order.ts +18 -0
- package/src/cli/commands/models.ts +122 -0
- package/src/cli/commands/pipeline.ts +132 -0
- package/src/cli/commands/project.ts +175 -0
- package/src/cli/commands/report.ts +39 -0
- package/src/cli/commands/secret.ts +39 -0
- package/src/cli/commands/skill.ts +36 -0
- package/src/cli/commands/source.ts +204 -0
- package/src/cli/commands/status.ts +18 -0
- package/src/cli/commands/training.ts +228 -0
- package/src/cli/commands/version.ts +14 -0
- package/src/cli/config.ts +5 -6
- package/src/cli/context.ts +213 -0
- package/src/cli/delivery-orders.ts +30 -0
- package/src/cli/environment.ts +26 -0
- package/src/cli/init.ts +1 -4
- package/src/cli/program.ts +168 -1344
- package/src/cli/project-context.ts +9 -11
- package/src/cli/sdk-version.ts +38 -0
- package/src/cli/skill.ts +6 -2
- package/src/cli/terminal-io.ts +103 -0
- package/src/cli/trainings.ts +118 -27
- package/src/cli/version.ts +1 -1
- package/src/client.ts +18 -0
- package/src/importer.ts +32 -2
package/src/cli/analytics.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { TolokaEnvironment } from "./config";
|
|
|
8
8
|
import type { CliEnvironment } from "./credentials";
|
|
9
9
|
import { credentialsPath, preparePrivateFilePath } from "./credentials";
|
|
10
10
|
import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
|
|
11
|
-
import { SDK_VERSION } from "./
|
|
11
|
+
import { SDK_VERSION } from "./sdk-version";
|
|
12
12
|
|
|
13
13
|
export const POSTHOG_PROJECT_KEY = "phc_kqHt0f53B5MVqURyzWpW1aNlbOmYt1YbUYza0JPcg19";
|
|
14
14
|
export const POSTHOG_HOST = "https://eu.posthog.com";
|
|
@@ -56,6 +56,11 @@ export type PosthogLike = {
|
|
|
56
56
|
properties?: Record<string, unknown>;
|
|
57
57
|
disableGeoip: boolean;
|
|
58
58
|
}) => void;
|
|
59
|
+
captureException: (
|
|
60
|
+
error: unknown,
|
|
61
|
+
distinctId: string,
|
|
62
|
+
properties?: Record<string, unknown>,
|
|
63
|
+
) => void;
|
|
59
64
|
alias: (event: { distinctId: string; alias: string }) => void;
|
|
60
65
|
flush: () => Promise<void>;
|
|
61
66
|
on?: (event: "error", listener: (error: Error) => void) => void;
|
|
@@ -84,6 +89,7 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
|
|
|
84
89
|
results: true,
|
|
85
90
|
},
|
|
86
91
|
dataset: { list: true, fields: true, load: true, upload: true },
|
|
92
|
+
"delivery-order": { list: true },
|
|
87
93
|
secret: { set: true },
|
|
88
94
|
skill: { list: true, search: true, install: true },
|
|
89
95
|
config: { get: true, set: true },
|
|
@@ -136,14 +142,34 @@ export function commandNameFromArgv(argv: readonly string[]): string {
|
|
|
136
142
|
return nameFromTokens(tokens);
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
const findPotatoApiError = (error: unknown): PotatoApiError | undefined => {
|
|
146
|
+
if (error instanceof PotatoApiError) return error;
|
|
147
|
+
|
|
148
|
+
if (error instanceof AggregateError) {
|
|
149
|
+
for (const nested of error.errors) {
|
|
150
|
+
const apiError = findPotatoApiError(nested);
|
|
151
|
+
if (apiError !== undefined) return apiError;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return error instanceof Error ? findPotatoApiError(error.cause) : undefined;
|
|
156
|
+
};
|
|
157
|
+
|
|
139
158
|
export function classifyCliError(error: unknown): { readonly error_code: string } {
|
|
140
159
|
if (error === undefined) return { error_code: "command_failed" };
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
160
|
+
const apiError = findPotatoApiError(error);
|
|
161
|
+
if (apiError !== undefined) return { error_code: apiError.code };
|
|
162
|
+
if (error instanceof PipelineGraphValidationError) return { error_code: error.code };
|
|
144
163
|
return { error_code: error instanceof Error ? "error" : "unknown" };
|
|
145
164
|
}
|
|
146
165
|
|
|
166
|
+
const cliErrorProperties = (error: unknown): Record<string, unknown> => {
|
|
167
|
+
const apiError = findPotatoApiError(error);
|
|
168
|
+
return apiError === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { status_code: apiError.status, request_id: apiError.requestId };
|
|
171
|
+
};
|
|
172
|
+
|
|
147
173
|
const anonymousIdPath = (environment: CliEnvironment): string =>
|
|
148
174
|
join(dirname(credentialsPath(environment)), "anonymous-id");
|
|
149
175
|
|
|
@@ -237,18 +263,38 @@ export function createCliAnalytics(options: {
|
|
|
237
263
|
.then(() => {
|
|
238
264
|
if (client === undefined || distinctId === undefined) return;
|
|
239
265
|
const shared = sharedProperties();
|
|
266
|
+
const command = nameFromTokens(event.command.split(" "));
|
|
267
|
+
|
|
240
268
|
if (event.outcome === "failed") {
|
|
241
269
|
const classified = classifyCliError(event.error);
|
|
270
|
+
const errorProperties = cliErrorProperties(event.error);
|
|
271
|
+
|
|
272
|
+
if (event.error !== undefined) {
|
|
273
|
+
const exception = new Error(`CLI command failed: ${classified.error_code}`);
|
|
274
|
+
exception.name = "PotatoCliError";
|
|
275
|
+
client.captureException(exception, distinctId, {
|
|
276
|
+
...shared,
|
|
277
|
+
command,
|
|
278
|
+
duration_ms: event.durationMs,
|
|
279
|
+
exit_code: event.exitCode,
|
|
280
|
+
$process_person_profile: false,
|
|
281
|
+
$geoip_disable: true,
|
|
282
|
+
...classified,
|
|
283
|
+
...errorProperties,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
242
287
|
client.capture({
|
|
243
288
|
distinctId,
|
|
244
289
|
event: "sdk_cli_command_failed",
|
|
245
290
|
disableGeoip: true,
|
|
246
291
|
properties: {
|
|
247
292
|
...shared,
|
|
248
|
-
command
|
|
293
|
+
command,
|
|
249
294
|
duration_ms: event.durationMs,
|
|
250
295
|
exit_code: event.exitCode,
|
|
251
296
|
...classified,
|
|
297
|
+
...errorProperties,
|
|
252
298
|
},
|
|
253
299
|
});
|
|
254
300
|
return;
|
|
@@ -259,7 +305,7 @@ export function createCliAnalytics(options: {
|
|
|
259
305
|
disableGeoip: true,
|
|
260
306
|
properties: {
|
|
261
307
|
...shared,
|
|
262
|
-
command
|
|
308
|
+
command,
|
|
263
309
|
duration_ms: event.durationMs,
|
|
264
310
|
exit_code: event.exitCode,
|
|
265
311
|
outcome: event.outcome,
|
package/src/cli/auth.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assert } from "es-toolkit";
|
|
2
2
|
import { createClient } from "../client";
|
|
3
|
+
import { PotatoApiError } from "../errors";
|
|
3
4
|
import { PRODUCTION_BASE_URL, consoleUrlFor, resolveInternalBaseUrl } from "./config";
|
|
4
5
|
import {
|
|
5
6
|
clearStoredCredentials,
|
|
@@ -19,6 +20,40 @@ import type {
|
|
|
19
20
|
import type { ProjectContext } from "./project-context";
|
|
20
21
|
import { formatProjectContextLine } from "./project-context";
|
|
21
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
|
+
};
|
|
56
|
+
|
|
22
57
|
export const apiKeyInstructions = (baseUrl: string): string => {
|
|
23
58
|
const console = consoleUrlFor(baseUrl);
|
|
24
59
|
const where =
|
|
@@ -54,6 +89,7 @@ export type AuthProbe = {
|
|
|
54
89
|
readonly pagination: { readonly total: number };
|
|
55
90
|
}>;
|
|
56
91
|
};
|
|
92
|
+
readonly me?: () => Promise<{ readonly internal: boolean }>;
|
|
57
93
|
};
|
|
58
94
|
|
|
59
95
|
export type LoginOptions = {
|
|
@@ -76,16 +112,16 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
76
112
|
const write = options.write ?? (() => undefined);
|
|
77
113
|
const baseUrl = normalizeBaseUrl(
|
|
78
114
|
options.baseUrl ??
|
|
79
|
-
environment.
|
|
115
|
+
environment.TOLOKA_BASE_URL ??
|
|
80
116
|
resolveInternalBaseUrl(environment) ??
|
|
81
117
|
PRODUCTION_BASE_URL,
|
|
82
118
|
);
|
|
83
119
|
|
|
84
|
-
let apiKey = options.apiKey ?? environment.
|
|
120
|
+
let apiKey = options.apiKey ?? environment.TOLOKA_API_KEY;
|
|
85
121
|
if (apiKey === undefined) {
|
|
86
122
|
assert(
|
|
87
123
|
options.prompt,
|
|
88
|
-
"Missing API key.
|
|
124
|
+
"Missing API key. Without a terminal, pipe the key to toloka login --api-key-stdin, pass --api-key, or set TOLOKA_API_KEY.",
|
|
89
125
|
);
|
|
90
126
|
write(`\n${apiKeyInstructions(baseUrl)}\n`);
|
|
91
127
|
apiKey = (await options.prompt("API key: ", { mask: true })).trim();
|
|
@@ -99,12 +135,12 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
99
135
|
const page = await probe.projects.listPage({ limit: 1 });
|
|
100
136
|
reachable = `Authenticated against ${baseUrl}: ${page.pagination.total} project(s) visible.`;
|
|
101
137
|
} catch (error) {
|
|
102
|
-
const
|
|
138
|
+
const failure = classifyProbeFailure(error);
|
|
103
139
|
throw new Error(
|
|
104
140
|
[
|
|
105
|
-
`Could not authenticate against ${baseUrl}: ${reason}`,
|
|
141
|
+
`Could not authenticate against ${baseUrl}: ${failure.reason}`,
|
|
106
142
|
"",
|
|
107
|
-
|
|
143
|
+
`Nothing was saved. ${LOGIN_FAILURE_ADVICE[failure.kind](baseUrl)}`,
|
|
108
144
|
].join("\n"),
|
|
109
145
|
{ cause: error },
|
|
110
146
|
);
|
|
@@ -136,7 +172,7 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
|
|
|
136
172
|
reachable,
|
|
137
173
|
`Saved credentials to ${credentialsFile}.`,
|
|
138
174
|
"",
|
|
139
|
-
"toloka reads them automatically. --api-key and
|
|
175
|
+
"toloka reads them automatically. --api-key and TOLOKA_API_KEY still win when set.",
|
|
140
176
|
].join("\n"),
|
|
141
177
|
};
|
|
142
178
|
}
|
|
@@ -183,9 +219,9 @@ const resolveApiKey = (
|
|
|
183
219
|
options.flagApiKey === undefined
|
|
184
220
|
? undefined
|
|
185
221
|
: { value: options.flagApiKey, source: "from --api-key" },
|
|
186
|
-
environment.
|
|
222
|
+
environment.TOLOKA_API_KEY === undefined
|
|
187
223
|
? undefined
|
|
188
|
-
: { value: environment.
|
|
224
|
+
: { value: environment.TOLOKA_API_KEY, source: "from TOLOKA_API_KEY" },
|
|
189
225
|
found === undefined
|
|
190
226
|
? undefined
|
|
191
227
|
: { value: found.credentials.apiKey, source: `stored in ${storedPath}` },
|
|
@@ -202,9 +238,9 @@ const resolvePlatform = (
|
|
|
202
238
|
options.flagBaseUrl === undefined
|
|
203
239
|
? undefined
|
|
204
240
|
: { value: options.flagBaseUrl.replace(/\/+$/u, ""), source: "from --base-url" },
|
|
205
|
-
environment.
|
|
241
|
+
environment.TOLOKA_BASE_URL === undefined
|
|
206
242
|
? undefined
|
|
207
|
-
: { value: environment.
|
|
243
|
+
: { value: environment.TOLOKA_BASE_URL, source: "from TOLOKA_BASE_URL" },
|
|
208
244
|
internalBaseUrl === undefined
|
|
209
245
|
? undefined
|
|
210
246
|
: { value: internalBaseUrl, source: `from ENV=${environment.ENV}` },
|
|
@@ -213,11 +249,21 @@ const resolvePlatform = (
|
|
|
213
249
|
);
|
|
214
250
|
};
|
|
215
251
|
|
|
252
|
+
const internalStatusLine = async (probe: AuthProbe): Promise<string | undefined> => {
|
|
253
|
+
if (probe.me === undefined) return undefined;
|
|
254
|
+
try {
|
|
255
|
+
const me = await probe.me();
|
|
256
|
+
return `Internal: ${me.internal}`;
|
|
257
|
+
} catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
216
262
|
export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthStatusResult> {
|
|
217
263
|
const environment = options.environment ?? process.env;
|
|
218
264
|
const credentialLookup =
|
|
219
265
|
options.credentialLookup ??
|
|
220
|
-
(await findCommandCredentials(environment, options.flagApiKey ?? environment.
|
|
266
|
+
(await findCommandCredentials(environment, options.flagApiKey ?? environment.TOLOKA_API_KEY));
|
|
221
267
|
const { found, warning } = credentialLookup;
|
|
222
268
|
const storedPath = found?.path ?? credentialsPath(environment);
|
|
223
269
|
const apiKey = resolveApiKey(options, environment, found, storedPath);
|
|
@@ -244,8 +290,8 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
244
290
|
projectLine,
|
|
245
291
|
);
|
|
246
292
|
|
|
247
|
-
if (environment.
|
|
248
|
-
lines.push("", `
|
|
293
|
+
if (environment.TOLOKA_API_KEY !== undefined && found !== undefined) {
|
|
294
|
+
lines.push("", `TOLOKA_API_KEY is set, so the key stored in ${found.path} is ignored.`);
|
|
249
295
|
}
|
|
250
296
|
if (found?.legacy === true) {
|
|
251
297
|
lines.push(
|
|
@@ -260,10 +306,17 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
|
|
|
260
306
|
try {
|
|
261
307
|
const page = await probe.projects.listPage({ limit: 1 });
|
|
262
308
|
lines.push("", `Reachable: ${page.pagination.total} project(s) visible.`);
|
|
309
|
+
const internalLine = await internalStatusLine(probe);
|
|
310
|
+
if (internalLine !== undefined) lines.push(internalLine);
|
|
263
311
|
return { authenticated: true, output: lines.join("\n") };
|
|
264
312
|
} catch (error) {
|
|
265
|
-
const
|
|
266
|
-
lines.push(
|
|
313
|
+
const failure = classifyProbeFailure(error);
|
|
314
|
+
lines.push(
|
|
315
|
+
"",
|
|
316
|
+
`${PROBE_FAILURE_HEADLINE[failure.kind]}: ${failure.reason}`,
|
|
317
|
+
"",
|
|
318
|
+
STATUS_FAILURE_ADVICE[failure.kind](baseUrl),
|
|
319
|
+
);
|
|
267
320
|
return { authenticated: false, output: lines.join("\n") };
|
|
268
321
|
}
|
|
269
322
|
}
|
package/src/cli/auto-update.ts
CHANGED
|
@@ -6,16 +6,20 @@ 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 packageMetadata from "../../package.json" with { type: "json" };
|
|
15
20
|
|
|
16
21
|
export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
17
22
|
export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
|
|
18
|
-
export const SDK_PACKAGE_NAME = packageMetadata.name;
|
|
19
23
|
export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20
24
|
|
|
21
25
|
const SKIP_COMMANDS: Readonly<Record<string, true>> = {
|
|
@@ -75,32 +79,6 @@ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
|
|
|
75
79
|
export const npmPackageMetadataUrl = (packageName: string): string =>
|
|
76
80
|
new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
|
|
77
81
|
|
|
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
82
|
const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
|
|
105
83
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
106
84
|
const record: Record<string, string> = {};
|
|
@@ -122,9 +100,22 @@ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined =
|
|
|
122
100
|
};
|
|
123
101
|
};
|
|
124
102
|
|
|
125
|
-
export const latestPublishedVersion = (
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
export const latestPublishedVersion = (
|
|
104
|
+
view: NpmPackageView,
|
|
105
|
+
currentVersion: string,
|
|
106
|
+
): string | undefined => {
|
|
107
|
+
const tags = view["dist-tags"];
|
|
108
|
+
if (tags === undefined) return undefined;
|
|
109
|
+
const channels: readonly ReleaseChannel[] =
|
|
110
|
+
releaseChannelFor(currentVersion) === "preview" ? ["latest", "preview"] : ["latest"];
|
|
111
|
+
let newest: string | undefined;
|
|
112
|
+
for (const channel of channels) {
|
|
113
|
+
const candidate = tags[channel];
|
|
114
|
+
if (candidate === undefined || candidate.length === 0) continue;
|
|
115
|
+
if (newest === undefined || (compareSdkVersions(candidate, newest) ?? 0) > 0)
|
|
116
|
+
newest = candidate;
|
|
117
|
+
}
|
|
118
|
+
return newest;
|
|
128
119
|
};
|
|
129
120
|
|
|
130
121
|
const publishedAt = (view: NpmPackageView, version: string): Date | undefined => {
|
|
@@ -310,7 +301,7 @@ const selectUpdateCandidate = async (options: {
|
|
|
310
301
|
return { status: "skip" };
|
|
311
302
|
}
|
|
312
303
|
|
|
313
|
-
const version = latestPublishedVersion(view);
|
|
304
|
+
const version = latestPublishedVersion(view, currentVersion);
|
|
314
305
|
if (version === undefined) return { status: "skip" };
|
|
315
306
|
const order = compareSdkVersions(currentVersion, version);
|
|
316
307
|
if (order === undefined || order >= 0) return { status: "skip" };
|
package/src/cli/check.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type TestEvidenceKind,
|
|
26
26
|
} from "../testing/test-evidence";
|
|
27
27
|
import { resolveCliEntry } from "./cli-entry";
|
|
28
|
+
import { API_KEY_ENVIRONMENT_NAMES } from "./environment";
|
|
28
29
|
import { loadProjectSource, type ProjectSource } from "./source";
|
|
29
30
|
|
|
30
31
|
export type CheckStatus = "passed" | "failed" | "missing" | "partial" | "infrastructure_error";
|
|
@@ -269,18 +270,19 @@ const loadSourceWithoutPlatformCredentials = async (
|
|
|
269
270
|
sourceRoot: string,
|
|
270
271
|
environment: NodeJS.ProcessEnv,
|
|
271
272
|
): Promise<ProjectSource> => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
]);
|
|
276
|
-
for (const current of environments.keys()) delete current.POTATO_API_KEY;
|
|
273
|
+
const targets = [process.env, environment];
|
|
274
|
+
const saved = targets.map((current) => API_KEY_ENVIRONMENT_NAMES.map((name) => current[name]));
|
|
275
|
+
for (const current of targets) for (const name of API_KEY_ENVIRONMENT_NAMES) delete current[name];
|
|
277
276
|
try {
|
|
278
277
|
return await loadProjectSource(sourceRoot);
|
|
279
278
|
} finally {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
targets.forEach((current, target) => {
|
|
280
|
+
API_KEY_ENVIRONMENT_NAMES.forEach((name, index) => {
|
|
281
|
+
const value = saved[target]?.[index];
|
|
282
|
+
if (value === undefined) delete current[name];
|
|
283
|
+
else current[name] = value;
|
|
284
|
+
});
|
|
285
|
+
});
|
|
284
286
|
}
|
|
285
287
|
};
|
|
286
288
|
|
|
@@ -683,7 +685,7 @@ export async function checkProject(
|
|
|
683
685
|
try {
|
|
684
686
|
const selectedPipeline = selected.pipelines.length === 1 ? selected.pipelines[0] : undefined;
|
|
685
687
|
const checkEnvironment = { ...(options.environment ?? process.env) };
|
|
686
|
-
delete checkEnvironment
|
|
688
|
+
for (const name of API_KEY_ENVIRONMENT_NAMES) delete checkEnvironment[name];
|
|
687
689
|
const environment = {
|
|
688
690
|
...checkEnvironment,
|
|
689
691
|
[TEST_EVIDENCE_DIR_ENV]: evidenceDirectory,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createClient } from "../../client";
|
|
3
|
+
import { authStatus, login, logout } from "../auth";
|
|
4
|
+
import {
|
|
5
|
+
identifyClientUser,
|
|
6
|
+
resolveSelectedProject,
|
|
7
|
+
resolveSourceRoot,
|
|
8
|
+
type CommandHost,
|
|
9
|
+
} from "../context";
|
|
10
|
+
import { findCommandCredentials, readStoredCredentials } from "../credentials";
|
|
11
|
+
import { resolveTolokaConfig } from "../config";
|
|
12
|
+
import { readTolokaState } from "../../state";
|
|
13
|
+
|
|
14
|
+
export const registerAuthCommands = (program: Command, host: CommandHost): void => {
|
|
15
|
+
const { context, exitCode, globals } = host;
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("login")
|
|
19
|
+
.description("store an API key for this machine after verifying it")
|
|
20
|
+
.option("--api-key-stdin", "read the API key from standard input instead of prompting", false)
|
|
21
|
+
.action(async (options: { apiKeyStdin: boolean }) => {
|
|
22
|
+
const apiKey = options.apiKeyStdin ? (await context.io.readStdin()).trim() : globals().apiKey;
|
|
23
|
+
const injectedClient = context.injectedClient;
|
|
24
|
+
const result = await login({
|
|
25
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
26
|
+
...(globals().baseUrl === undefined ? {} : { baseUrl: globals().baseUrl }),
|
|
27
|
+
...(context.io.prompt === undefined ? {} : { prompt: context.io.prompt }),
|
|
28
|
+
...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
|
|
29
|
+
write: context.io.stdout,
|
|
30
|
+
environment: context.environment,
|
|
31
|
+
});
|
|
32
|
+
context.io.stdout(result.output);
|
|
33
|
+
try {
|
|
34
|
+
const stored = await readStoredCredentials(context.environment);
|
|
35
|
+
if (stored === undefined) return;
|
|
36
|
+
const client = context.injectedClient ?? createClient(stored);
|
|
37
|
+
await identifyClientUser(client, context.analytics);
|
|
38
|
+
} catch {}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("logout")
|
|
43
|
+
.description("remove stored credentials from this machine")
|
|
44
|
+
.action(async () => {
|
|
45
|
+
const result = await logout(context.environment);
|
|
46
|
+
context.io.stdout(result.output);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command("auth-status")
|
|
51
|
+
.alias("whoami")
|
|
52
|
+
.description("show which platform and key the CLI would use")
|
|
53
|
+
.option("--show-token", "print the API key instead of masking it", false)
|
|
54
|
+
.action(async (options: { showToken: boolean }) => {
|
|
55
|
+
const flags = globals();
|
|
56
|
+
const credentialLookup = await findCommandCredentials(
|
|
57
|
+
context.environment,
|
|
58
|
+
flags.apiKey ?? context.environment.TOLOKA_API_KEY,
|
|
59
|
+
);
|
|
60
|
+
const stored = credentialLookup.found?.credentials;
|
|
61
|
+
const sourceRoot = await resolveSourceRoot(flags, context.io);
|
|
62
|
+
const state = await readTolokaState(sourceRoot);
|
|
63
|
+
const project = resolveSelectedProject(flags, context, stored, state);
|
|
64
|
+
const injectedClient = context.injectedClient;
|
|
65
|
+
|
|
66
|
+
const result = await authStatus({
|
|
67
|
+
environment: context.environment,
|
|
68
|
+
showToken: options.showToken,
|
|
69
|
+
credentialLookup,
|
|
70
|
+
...(flags.apiKey === undefined ? {} : { flagApiKey: flags.apiKey }),
|
|
71
|
+
...(flags.baseUrl === undefined ? {} : { flagBaseUrl: flags.baseUrl }),
|
|
72
|
+
...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
|
|
73
|
+
...(project === undefined ? {} : { project }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
context.io.stdout(result.output);
|
|
77
|
+
if (!result.authenticated) {
|
|
78
|
+
exitCode.value = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const config = resolveTolokaConfig(flags, state, context.environment, stored);
|
|
83
|
+
const client =
|
|
84
|
+
context.injectedClient ??
|
|
85
|
+
createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
86
|
+
await identifyClientUser(client, context.analytics);
|
|
87
|
+
} catch {}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { CommandHost } from "../context";
|
|
3
|
+
import { getCliSetting, readCliSettings, setCliSetting } from "../settings";
|
|
4
|
+
|
|
5
|
+
export const registerConfigCommands = (program: Command, host: CommandHost): void => {
|
|
6
|
+
const config = program.command("config").description("read and write machine CLI settings");
|
|
7
|
+
config
|
|
8
|
+
.command("get")
|
|
9
|
+
.argument("[name]", "setting name; omit to print every setting")
|
|
10
|
+
.description("print a CLI setting, or every setting")
|
|
11
|
+
.action(async (name?: string) => {
|
|
12
|
+
const settings = await readCliSettings(host.context.environment);
|
|
13
|
+
host.context.io.stdout(getCliSetting(settings, name).output);
|
|
14
|
+
});
|
|
15
|
+
config
|
|
16
|
+
.command("set")
|
|
17
|
+
.argument("<name>", "setting name")
|
|
18
|
+
.argument("<value>", "setting value")
|
|
19
|
+
.description("write a CLI setting for this machine")
|
|
20
|
+
.action(async (name: string, value: string) => {
|
|
21
|
+
const result = await setCliSetting(host.context.environment, name, value);
|
|
22
|
+
host.context.io.stdout(result.output);
|
|
23
|
+
});
|
|
24
|
+
config.action(async () => {
|
|
25
|
+
const settings = await readCliSettings(host.context.environment);
|
|
26
|
+
host.context.io.stdout(getCliSetting(settings, undefined).output);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import { positiveInteger, resolveContext, type CommandHost } from "../context";
|
|
5
|
+
import {
|
|
6
|
+
generateDatasetFieldDeclarations,
|
|
7
|
+
listProjectDatasets,
|
|
8
|
+
loadDataset,
|
|
9
|
+
uploadDataset,
|
|
10
|
+
} from "../data";
|
|
11
|
+
import { MISSING_PROJECT_ID_MESSAGE } from "../project-context";
|
|
12
|
+
|
|
13
|
+
export const registerDatasetCommands = (program: Command, host: CommandHost): void => {
|
|
14
|
+
const { context, globals } = host;
|
|
15
|
+
const dataset = program
|
|
16
|
+
.command("dataset")
|
|
17
|
+
.description("inspect and move dataset catalog and rows");
|
|
18
|
+
dataset
|
|
19
|
+
.command("list")
|
|
20
|
+
.description("list live datasets in the selected project")
|
|
21
|
+
.action(async () => {
|
|
22
|
+
const { client, projectId } = await resolveContext(globals(), context);
|
|
23
|
+
assert(projectId, MISSING_PROJECT_ID_MESSAGE);
|
|
24
|
+
const result = await listProjectDatasets(client, { projectId });
|
|
25
|
+
context.io.stdout(result.output);
|
|
26
|
+
});
|
|
27
|
+
dataset
|
|
28
|
+
.command("fields")
|
|
29
|
+
.argument("<name>", "dataset name or id")
|
|
30
|
+
.description("print field declarations inferred from uploaded rows")
|
|
31
|
+
.action(async (name: string) => {
|
|
32
|
+
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
33
|
+
const result = await generateDatasetFieldDeclarations(client, {
|
|
34
|
+
sourceRoot,
|
|
35
|
+
datasetName: name,
|
|
36
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
37
|
+
});
|
|
38
|
+
context.io.stdout(result.output);
|
|
39
|
+
});
|
|
40
|
+
dataset
|
|
41
|
+
.command("load")
|
|
42
|
+
.argument("<name>", "dataset name or id")
|
|
43
|
+
.requiredOption("--out <file>", "JSONL file to write")
|
|
44
|
+
.option("--limit <n>", "how many rows to keep after download", positiveInteger)
|
|
45
|
+
.option("--timeout <ms>", "how long to wait for the export", positiveInteger)
|
|
46
|
+
.description("download live dataset rows into a JSONL file")
|
|
47
|
+
.action(async (name: string, options: { out: string; limit?: number; timeout?: number }) => {
|
|
48
|
+
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
49
|
+
const result = await loadDataset(client, {
|
|
50
|
+
sourceRoot,
|
|
51
|
+
datasetName: name,
|
|
52
|
+
outPath: resolve(options.out),
|
|
53
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
54
|
+
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
55
|
+
...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
|
|
56
|
+
});
|
|
57
|
+
context.io.stdout(result.output);
|
|
58
|
+
});
|
|
59
|
+
dataset
|
|
60
|
+
.command("upload")
|
|
61
|
+
.argument("<name>", "dataset name or id")
|
|
62
|
+
.argument("<file>", "JSON array, JSONL, or CSV file")
|
|
63
|
+
.description("append rows to a dataset")
|
|
64
|
+
.action(async (name: string, file: string) => {
|
|
65
|
+
const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
|
|
66
|
+
const result = await uploadDataset(client, {
|
|
67
|
+
sourceRoot,
|
|
68
|
+
datasetName: name,
|
|
69
|
+
filePath: resolve(file),
|
|
70
|
+
...(projectId === undefined ? {} : { projectId }),
|
|
71
|
+
});
|
|
72
|
+
context.io.stdout(result.output);
|
|
73
|
+
if (result.warning !== undefined) context.io.stderr(result.warning);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { resolveContext, type CommandHost } from "../context";
|
|
3
|
+
import { listDeliveryOrders } from "../delivery-orders";
|
|
4
|
+
|
|
5
|
+
export const registerDeliveryOrderCommands = (program: Command, host: CommandHost): void => {
|
|
6
|
+
const { context, globals } = host;
|
|
7
|
+
const deliveryOrder = program
|
|
8
|
+
.command("delivery-order")
|
|
9
|
+
.description("list Sunday delivery orders for project create");
|
|
10
|
+
deliveryOrder
|
|
11
|
+
.command("list")
|
|
12
|
+
.description("list active Sunday delivery orders this key can attach")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const { client } = await resolveContext(globals(), context);
|
|
15
|
+
const result = await listDeliveryOrders({ client });
|
|
16
|
+
context.io.stdout(result.output);
|
|
17
|
+
});
|
|
18
|
+
};
|