@toloka-platform/sdk 0.2.0-preview.3 → 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/dist/cli/toloka.js +136 -136
- 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/cli-v2.md +3 -2
- package/docs/cli.md +32 -4
- package/openapi/gen/models/baseModel.ts +1 -0
- package/openapi/gen/models/currentUser.ts +2 -0
- package/openapi/gen/models/currentUserTeamsItem.ts +6 -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 +9 -4
- 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/package.json +1 -1
- package/skills/catalog.json +10 -10
- package/skills/toloka-build/SKILL.md +28 -17
- package/skills/toloka-build/references/node-semantics.md +7 -7
- package/skills/toloka-build/references/workflow-and-cli.md +13 -10
- package/skills/toloka-fine-tuning/SKILL.md +145 -62
- package/skills/toloka-get-started/SKILL.md +35 -20
- package/skills/toloka-gisting/SKILL.md +156 -66
- package/src/cli/analytics.ts +50 -23
- package/src/cli/auth.ts +31 -12
- package/src/cli/auto-update.ts +12 -6
- package/src/cli/check.ts +76 -29
- package/src/cli/cli-entry.ts +3 -2
- package/src/cli/cli-error.ts +43 -0
- package/src/cli/commands/dataset.ts +13 -3
- package/src/cli/commands/models.ts +6 -7
- package/src/cli/commands/pipeline.ts +6 -7
- package/src/cli/commands/project.ts +9 -16
- package/src/cli/commands/report.ts +6 -2
- package/src/cli/commands/secret.ts +5 -4
- package/src/cli/commands/source.ts +4 -4
- package/src/cli/commands/status.ts +2 -3
- package/src/cli/commands/training.ts +50 -17
- package/src/cli/config.ts +2 -2
- package/src/cli/context.ts +55 -17
- package/src/cli/credentials.ts +12 -4
- package/src/cli/data.ts +96 -34
- package/src/cli/delivery-orders.ts +18 -1
- package/src/cli/file-system.ts +6 -2
- package/src/cli/format-error.ts +45 -2
- package/src/cli/init.ts +13 -7
- package/src/cli/models.ts +43 -12
- package/src/cli/program.ts +4 -55
- package/src/cli/project-context.ts +117 -32
- package/src/cli/project.ts +80 -28
- package/src/cli/report.ts +24 -9
- package/src/cli/runs.ts +33 -8
- package/src/cli/settings.ts +13 -5
- package/src/cli/skill.ts +61 -22
- package/src/cli/source.ts +3 -2
- package/src/cli/terminal-io.ts +13 -6
- package/src/cli/trainings.ts +129 -35
- package/src/cli/version.ts +2 -2
- package/src/client.ts +3 -9
- 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
package/src/cli/format-error.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PotatoApiError } from "../errors";
|
|
2
|
+
import { formatHint, TolokaCliError } from "./cli-error";
|
|
2
3
|
|
|
3
4
|
export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
|
|
4
5
|
const details =
|
|
@@ -10,10 +11,52 @@ export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
|
|
|
10
11
|
return details === undefined || details.length === 0 ? undefined : details;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const apiErrorHint = (error: PotatoApiError): string | undefined => {
|
|
15
|
+
if (error.code === "wait_timeout") {
|
|
16
|
+
return "Hint: The job is still running. Check it later with its status command; commands that accept --timeout can wait longer.";
|
|
17
|
+
}
|
|
18
|
+
if (error.status === 401 || error.status === 403) {
|
|
19
|
+
return "Hint: Run toloka whoami to see which key and platform are in use; toloka login stores a new key.";
|
|
20
|
+
}
|
|
21
|
+
if (error.status === 404) {
|
|
22
|
+
return "Hint: Check the id against toloka project current and the matching list command; ids belong to one project on one platform.";
|
|
23
|
+
}
|
|
24
|
+
if (error.status === 429) {
|
|
25
|
+
return "Hint: The platform is rate limiting this key; wait a moment and retry.";
|
|
26
|
+
}
|
|
27
|
+
if (error.status !== undefined && error.status >= 500) {
|
|
28
|
+
return "Hint: The platform failed to handle the request. Retry; if it keeps failing, write a report as the toloka-report-issue skill describes and send it with toloka report <report.md>.";
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const joinLines = (lines: ReadonlyArray<string | undefined>): string =>
|
|
34
|
+
lines.filter((line) => line !== undefined && line.length > 0).join("\n");
|
|
35
|
+
|
|
36
|
+
export const findPotatoApiError = (error: unknown): PotatoApiError | undefined => {
|
|
37
|
+
if (error instanceof PotatoApiError) return error;
|
|
38
|
+
if (error instanceof AggregateError) {
|
|
39
|
+
for (const nested of error.errors) {
|
|
40
|
+
const apiError = findPotatoApiError(nested);
|
|
41
|
+
if (apiError !== undefined) return apiError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return error instanceof Error ? findPotatoApiError(error.cause) : undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
13
47
|
export const formatCliError = (error: unknown): string => {
|
|
48
|
+
if (error instanceof TolokaCliError) {
|
|
49
|
+
const cause = findPotatoApiError(error.cause);
|
|
50
|
+
const causeLines =
|
|
51
|
+
cause === undefined
|
|
52
|
+
? []
|
|
53
|
+
: cause.message === error.message
|
|
54
|
+
? [apiErrorDetails(cause)]
|
|
55
|
+
: [cause.message, apiErrorDetails(cause)];
|
|
56
|
+
return joinLines([error.message, ...causeLines, formatHint(error.hint)]);
|
|
57
|
+
}
|
|
14
58
|
if (error instanceof PotatoApiError) {
|
|
15
|
-
|
|
16
|
-
return details === undefined ? error.message : `${error.message}\n${details}`;
|
|
59
|
+
return joinLines([error.message, apiErrorDetails(error), apiErrorHint(error)]);
|
|
17
60
|
}
|
|
18
61
|
return error instanceof Error ? error.message : String(error);
|
|
19
62
|
};
|
package/src/cli/init.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { lstat, mkdir, readdir, readFile, realpath, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
3
|
import { readTolokaState, writeTolokaState } from "../state";
|
|
5
4
|
import { assertSafeDestination, entryStats, isMissingFileError } from "./file-system";
|
|
6
5
|
import { SDK_PACKAGE_NAME, SDK_VERSION } from "./sdk-version";
|
|
7
6
|
import { SKILL_ENTRY_POINT, SKILL_INSTALL_ROOT } from "./skill";
|
|
7
|
+
import { assertCli } from "./cli-error";
|
|
8
8
|
|
|
9
9
|
const BOOTSTRAP_NAME = "toloka-bootstrap";
|
|
10
10
|
const CLAUDE_POINTER_PATH = `.claude/skills/${BOOTSTRAP_NAME}/SKILL.md`;
|
|
@@ -106,14 +106,16 @@ const locateBlock = (source: string, markers: BlockMarkers, path: string) => {
|
|
|
106
106
|
const beginIndex = source.indexOf(markers.begin);
|
|
107
107
|
const endIndex = source.indexOf(markers.end);
|
|
108
108
|
if (beginIndex === -1 && endIndex === -1) return undefined;
|
|
109
|
-
|
|
109
|
+
assertCli(
|
|
110
110
|
beginIndex !== -1 && endIndex !== -1 && beginIndex < endIndex,
|
|
111
111
|
`Malformed Toloka block in ${path}: expected one ${markers.begin} followed by one ${markers.end}. Repair the file by hand, then re-run \`toloka init\`.`,
|
|
112
|
+
{ hint: { disabled: true } },
|
|
112
113
|
);
|
|
113
|
-
|
|
114
|
+
assertCli(
|
|
114
115
|
!source.includes(markers.begin, beginIndex + markers.begin.length) &&
|
|
115
116
|
!source.includes(markers.end, endIndex + markers.end.length),
|
|
116
117
|
`Multiple Toloka blocks in ${path}: keep exactly one ${markers.begin} ... ${markers.end} block, then re-run \`toloka init\`.`,
|
|
118
|
+
{ hint: { disabled: true } },
|
|
117
119
|
);
|
|
118
120
|
return { beginIndex, endIndex: endIndex + markers.end.length };
|
|
119
121
|
};
|
|
@@ -121,9 +123,10 @@ const locateBlock = (source: string, markers: BlockMarkers, path: string) => {
|
|
|
121
123
|
const mergeBootstrap = (source: string, path: string) => {
|
|
122
124
|
const current = locateBlock(source, CURRENT_BLOCK, path);
|
|
123
125
|
const legacy = locateBlock(source, LEGACY_BLOCK, path);
|
|
124
|
-
|
|
126
|
+
assertCli(
|
|
125
127
|
current === undefined || legacy === undefined,
|
|
126
128
|
`Ambiguous Toloka blocks in ${path}: both ${CURRENT_BLOCK.begin} and ${LEGACY_BLOCK.begin} are present. Keep one, then re-run \`toloka init\`.`,
|
|
129
|
+
{ hint: { disabled: true } },
|
|
127
130
|
);
|
|
128
131
|
const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
|
|
129
132
|
const block = BOOTSTRAP_BLOCK.replaceAll("\n", lineEnding);
|
|
@@ -146,9 +149,10 @@ const hasSdkDependency = async (targetDirectory: string) => {
|
|
|
146
149
|
const source = await readOptionalFile(join(targetDirectory, "package.json"));
|
|
147
150
|
if (source === undefined) return false;
|
|
148
151
|
const manifest: unknown = JSON.parse(source);
|
|
149
|
-
|
|
152
|
+
assertCli(
|
|
150
153
|
typeof manifest === "object" && manifest !== null,
|
|
151
154
|
"package.json must contain a JSON object.",
|
|
155
|
+
{ hint: { description: "Fix package.json in the source root, then re-run toloka init." } },
|
|
152
156
|
);
|
|
153
157
|
return ["dependencies", "devDependencies"].some((key) => {
|
|
154
158
|
const dependencies = Reflect.get(manifest, key);
|
|
@@ -170,9 +174,10 @@ const writeIfChanged = async (path: string, source: string) => {
|
|
|
170
174
|
const assertRefreshablePointer = async (targetRoot: string, relativePath: string) => {
|
|
171
175
|
const path = join(targetRoot, relativePath);
|
|
172
176
|
const existing = await readOptionalFile(path);
|
|
173
|
-
|
|
177
|
+
assertCli(
|
|
174
178
|
existing === undefined || isGenerated(existing),
|
|
175
179
|
`Refusing to overwrite ${path}: it carries no \`<!-- toloka:sdk-version ... -->\` marker, so it is not a pointer generated by ${SDK_PACKAGE_NAME}. Move it aside, then re-run \`toloka init\`.`,
|
|
180
|
+
{ hint: { disabled: true } },
|
|
176
181
|
);
|
|
177
182
|
};
|
|
178
183
|
|
|
@@ -215,9 +220,10 @@ const bindCheckout = async (
|
|
|
215
220
|
): Promise<string | undefined> => {
|
|
216
221
|
if (projectId === undefined) return undefined;
|
|
217
222
|
const existing = await readTolokaState(targetRoot);
|
|
218
|
-
|
|
223
|
+
assertCli(
|
|
219
224
|
existing === undefined || existing.projectId === projectId,
|
|
220
225
|
`This directory is already bound to ${existing?.projectId}.`,
|
|
226
|
+
{ hint: { description: "Run toloka init without --project-id to keep the current binding, or remove toloka.json to bind another project." } },
|
|
221
227
|
);
|
|
222
228
|
if (existing !== undefined) return undefined;
|
|
223
229
|
await writeTolokaState(targetRoot, { projectId, pipelines: {}, datasets: {} });
|
package/src/cli/models.ts
CHANGED
|
@@ -5,9 +5,9 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { Readable, Transform } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
|
|
8
|
-
import { assert } from "es-toolkit";
|
|
9
8
|
|
|
10
9
|
import type { BaseModel, BaseModelId, Model, ModelVersion, PotatoClient } from "../client";
|
|
10
|
+
import { assertCli, REPORT_HINT, RETRY_OR_REPORT_HINT } from "./cli-error";
|
|
11
11
|
|
|
12
12
|
type ModelApi = Pick<PotatoClient, "baseModels" | "models">;
|
|
13
13
|
type ModelIo = { readonly stdout: (text: string) => void };
|
|
@@ -46,6 +46,7 @@ const formatBaseModel = (model: BaseModel): string =>
|
|
|
46
46
|
["Provider", model.provider],
|
|
47
47
|
["Upstream model", model.upstreamModel],
|
|
48
48
|
["Upstream revision", model.upstreamRevision],
|
|
49
|
+
["Upstream snapshot", model.upstreamUrl],
|
|
49
50
|
]);
|
|
50
51
|
|
|
51
52
|
const formatModel = (model: Model): string =>
|
|
@@ -62,7 +63,11 @@ const formatModel = (model: Model): string =>
|
|
|
62
63
|
|
|
63
64
|
export const parseBaseModelId = (value: string): BaseModelId => {
|
|
64
65
|
const modelId = BASE_MODEL_IDS.find((candidate) => candidate === value);
|
|
65
|
-
|
|
66
|
+
assertCli(
|
|
67
|
+
modelId,
|
|
68
|
+
`Unknown base model ${value}.`,
|
|
69
|
+
{ hint: { description: "Run toloka base-model list to see the catalog." } },
|
|
70
|
+
);
|
|
66
71
|
return modelId;
|
|
67
72
|
};
|
|
68
73
|
|
|
@@ -76,7 +81,11 @@ const resolveModel = async (
|
|
|
76
81
|
for await (const model of client.models.listAll({ projectId, limit: 100 })) {
|
|
77
82
|
if (model.name === nameOrId) matches.push(model);
|
|
78
83
|
}
|
|
79
|
-
|
|
84
|
+
assertCli(
|
|
85
|
+
matches.length === 1,
|
|
86
|
+
`Model ${nameOrId} was not found or is ambiguous.`,
|
|
87
|
+
{ hint: { description: "Run toloka model list and pass the model id (mdl.…)." } },
|
|
88
|
+
);
|
|
80
89
|
return matches[0]!;
|
|
81
90
|
};
|
|
82
91
|
|
|
@@ -87,8 +96,16 @@ const resolveModelVersion = async (
|
|
|
87
96
|
): Promise<ModelVersion> => {
|
|
88
97
|
if (nameOrId.startsWith("mdv.")) return client.models.getVersion({ modelVersionId: nameOrId });
|
|
89
98
|
const separator = nameOrId.lastIndexOf("@");
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
assertCli(
|
|
100
|
+
separator > 0,
|
|
101
|
+
`Model Version ${nameOrId} was not found. Use mdv.<id> or Model@name.`,
|
|
102
|
+
{ hint: { disabled: true } },
|
|
103
|
+
);
|
|
104
|
+
assertCli(
|
|
105
|
+
projectId,
|
|
106
|
+
"Select a project to resolve Model Version names.",
|
|
107
|
+
{ hint: { description: "Select a project with toloka project add or toloka project switch, or pass the version id (mdv.…) directly." } },
|
|
108
|
+
);
|
|
92
109
|
const model = await resolveModel(client, projectId, nameOrId.slice(0, separator));
|
|
93
110
|
const versionName = nameOrId.slice(separator + 1);
|
|
94
111
|
const matches: ModelVersion[] = [];
|
|
@@ -99,7 +116,11 @@ const resolveModelVersion = async (
|
|
|
99
116
|
})) {
|
|
100
117
|
if (version.name === versionName) matches.push(version);
|
|
101
118
|
}
|
|
102
|
-
|
|
119
|
+
assertCli(
|
|
120
|
+
matches.length === 1,
|
|
121
|
+
`Model Version ${nameOrId} was not found or is ambiguous.`,
|
|
122
|
+
{ hint: { description: "Run toloka model versions <model> and pass the version id (mdv.…)." } },
|
|
123
|
+
);
|
|
103
124
|
return matches[0]!;
|
|
104
125
|
};
|
|
105
126
|
|
|
@@ -116,12 +137,14 @@ export const listBaseModels = async (
|
|
|
116
137
|
models.length === 0
|
|
117
138
|
? "No base models found."
|
|
118
139
|
: formatTable([
|
|
119
|
-
["ID", "NAME", "PROVIDER", "UPSTREAM MODEL"],
|
|
140
|
+
["ID", "NAME", "PROVIDER", "UPSTREAM MODEL", "REVISION", "UPSTREAM SNAPSHOT"],
|
|
120
141
|
...models.map((model) => [
|
|
121
142
|
model.id,
|
|
122
143
|
model.displayName,
|
|
123
144
|
model.provider,
|
|
124
145
|
model.upstreamModel,
|
|
146
|
+
model.upstreamRevision.slice(0, 12),
|
|
147
|
+
model.upstreamUrl,
|
|
125
148
|
]),
|
|
126
149
|
]),
|
|
127
150
|
);
|
|
@@ -238,25 +261,31 @@ export const downloadModelVersion = async (
|
|
|
238
261
|
for (const file of descriptor.files) {
|
|
239
262
|
const target = resolve(root, file.path);
|
|
240
263
|
const relativeTarget = relative(root, target);
|
|
241
|
-
|
|
264
|
+
assertCli(
|
|
242
265
|
relativeTarget !== "" &&
|
|
243
266
|
relativeTarget !== ".." &&
|
|
244
267
|
!relativeTarget.startsWith(`..${sep}`) &&
|
|
245
268
|
!isAbsolute(relativeTarget),
|
|
246
269
|
`Invalid Model Version file path: ${file.path}`,
|
|
270
|
+
{ hint: REPORT_HINT },
|
|
247
271
|
);
|
|
248
272
|
let parent = root;
|
|
249
273
|
for (const segment of file.path.split("/").slice(0, -1)) {
|
|
250
274
|
parent = resolve(parent, segment);
|
|
251
275
|
await mkdir(parent, { recursive: true });
|
|
252
276
|
const realParent = await realpath(parent);
|
|
253
|
-
|
|
277
|
+
assertCli(
|
|
254
278
|
realParent === realRoot || realParent.startsWith(`${realRoot}${sep}`),
|
|
255
279
|
`Invalid Model Version file path: ${file.path}`,
|
|
280
|
+
{ hint: REPORT_HINT },
|
|
256
281
|
);
|
|
257
282
|
}
|
|
258
283
|
const response = await fetch(file.url, { redirect: "error" });
|
|
259
|
-
|
|
284
|
+
assertCli(
|
|
285
|
+
response.ok && response.body,
|
|
286
|
+
`Failed to download Model Version file ${file.path}.`,
|
|
287
|
+
{ hint: RETRY_OR_REPORT_HINT },
|
|
288
|
+
);
|
|
260
289
|
const hash = createHash("sha256");
|
|
261
290
|
let byteSize = 0;
|
|
262
291
|
const verify = new Transform({
|
|
@@ -274,13 +303,15 @@ export const downloadModelVersion = async (
|
|
|
274
303
|
0o600,
|
|
275
304
|
);
|
|
276
305
|
await pipeline(Readable.from(response.body), verify, handle.createWriteStream());
|
|
277
|
-
|
|
306
|
+
assertCli(
|
|
278
307
|
byteSize === file.byteSize,
|
|
279
308
|
`Downloaded Model Version file ${file.path} has an unexpected size.`,
|
|
309
|
+
{ hint: { description: "Delete the partial download and retry; if it keeps failing, run toloka report." } },
|
|
280
310
|
);
|
|
281
|
-
|
|
311
|
+
assertCli(
|
|
282
312
|
hash.digest("hex") === file.sha256,
|
|
283
313
|
`Downloaded Model Version file ${file.path} has an invalid hash.`,
|
|
314
|
+
{ hint: { description: "Delete the partial download and retry; if it keeps failing, run toloka report." } },
|
|
284
315
|
);
|
|
285
316
|
await rename(temporary, target);
|
|
286
317
|
} catch (error) {
|
package/src/cli/program.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, CommanderError } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import type { PotatoClient } from "../client";
|
|
3
3
|
import { commandNameFromArgv, createCliAnalytics } from "./analytics";
|
|
4
4
|
import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
|
|
5
5
|
import type { AutoUpdateHooks } from "./auto-update";
|
|
@@ -19,11 +19,9 @@ import { registerSourceCommands } from "./commands/source";
|
|
|
19
19
|
import { registerStatusCommand } from "./commands/status";
|
|
20
20
|
import { registerTrainingCommands } from "./commands/training";
|
|
21
21
|
import { registerVersionCommand } from "./commands/version";
|
|
22
|
-
import {
|
|
23
|
-
import { findCommandCredentials } from "./credentials";
|
|
22
|
+
import type { TolokaEnvironment } from "./config";
|
|
24
23
|
import {
|
|
25
24
|
CREDENTIAL_PRECEDENCE,
|
|
26
|
-
resolveInternalUser,
|
|
27
25
|
resolveSourceRoot,
|
|
28
26
|
type CliIo,
|
|
29
27
|
type CreatedProgram,
|
|
@@ -39,51 +37,6 @@ import { terminalIo } from "./terminal-io";
|
|
|
39
37
|
export type { CliIo, CreatedProgram, ProgramContext } from "./context";
|
|
40
38
|
export { terminalIo } from "./terminal-io";
|
|
41
39
|
|
|
42
|
-
const argvFlag = (argv: readonly string[], flag: string): string | undefined => {
|
|
43
|
-
const index = argv.indexOf(flag);
|
|
44
|
-
if (index < 0) return undefined;
|
|
45
|
-
const value = argv[index + 1];
|
|
46
|
-
return value === undefined || value.startsWith("-") ? undefined : value;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const resolveInternalCommandAccess = async (
|
|
50
|
-
argv: readonly string[],
|
|
51
|
-
environment: TolokaEnvironment,
|
|
52
|
-
injectedClient: PotatoClient | undefined,
|
|
53
|
-
): Promise<boolean | undefined> => {
|
|
54
|
-
const commandName = commandNameFromArgv(argv);
|
|
55
|
-
if (
|
|
56
|
-
commandName !== "help" &&
|
|
57
|
-
commandName !== "project create" &&
|
|
58
|
-
!commandName.startsWith("delivery-order")
|
|
59
|
-
) {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
if (injectedClient !== undefined) return resolveInternalUser(injectedClient);
|
|
63
|
-
try {
|
|
64
|
-
const apiKey = argvFlag(argv, "--api-key");
|
|
65
|
-
const baseUrl = argvFlag(argv, "--base-url");
|
|
66
|
-
const { found } = await findCommandCredentials(
|
|
67
|
-
environment,
|
|
68
|
-
apiKey ?? environment.TOLOKA_API_KEY,
|
|
69
|
-
);
|
|
70
|
-
const config = resolveTolokaConfig(
|
|
71
|
-
{
|
|
72
|
-
...(apiKey === undefined ? {} : { apiKey }),
|
|
73
|
-
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
74
|
-
},
|
|
75
|
-
undefined,
|
|
76
|
-
environment,
|
|
77
|
-
found?.credentials,
|
|
78
|
-
);
|
|
79
|
-
return await resolveInternalUser(
|
|
80
|
-
createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl }),
|
|
81
|
-
);
|
|
82
|
-
} catch {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
40
|
export function createProgram(context: ProgramContext): CreatedProgram {
|
|
88
41
|
const program = new Command("toloka");
|
|
89
42
|
const exitCode = { value: 0 };
|
|
@@ -137,7 +90,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
137
90
|
registerConfigCommands(program, host);
|
|
138
91
|
registerAuthCommands(program, host);
|
|
139
92
|
registerProjectCommands(program, host);
|
|
140
|
-
|
|
93
|
+
registerDeliveryOrderCommands(program, host);
|
|
141
94
|
registerSourceCommands(program, host);
|
|
142
95
|
registerSecretCommands(program, host);
|
|
143
96
|
registerModelCommands(program, host);
|
|
@@ -263,11 +216,7 @@ export async function runTolokaCli(
|
|
|
263
216
|
...(injectedClient === undefined ? {} : { injectedClient }),
|
|
264
217
|
...(runChecks === undefined ? {} : { runChecks }),
|
|
265
218
|
};
|
|
266
|
-
const
|
|
267
|
-
const { program, exitCode } = createProgram({
|
|
268
|
-
...baseContext,
|
|
269
|
-
...(internalUser === undefined ? {} : { internalUser }),
|
|
270
|
-
});
|
|
219
|
+
const { program, exitCode } = createProgram(baseContext);
|
|
271
220
|
const startedAt = Date.now();
|
|
272
221
|
|
|
273
222
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PotatoApiError } from "../errors";
|
|
2
2
|
import { readTolokaState, TOLOKA_STATE_FILENAME, writeTolokaState } from "../state";
|
|
3
3
|
import type { TolokaState } from "../state";
|
|
4
4
|
import type { AuthPrompt } from "./auth";
|
|
@@ -6,6 +6,8 @@ import type { TolokaCliFlags, TolokaEnvironment } from "./config";
|
|
|
6
6
|
import { resolveInternalBaseUrl } from "./config";
|
|
7
7
|
import { normalizeBaseUrl, requireStoredCredentials, writeStoredCredentials } from "./credentials";
|
|
8
8
|
import type { CliEnvironment, StoredCredentials, StoredProjectAlias } from "./credentials";
|
|
9
|
+
import { apiErrorDetails } from "./format-error";
|
|
10
|
+
import { assertCli, TolokaCliError } from "./cli-error";
|
|
9
11
|
|
|
10
12
|
type ProjectRecord = { readonly id: string; readonly name: string; readonly teamId: string };
|
|
11
13
|
|
|
@@ -24,20 +26,26 @@ export type ProjectGetApi = {
|
|
|
24
26
|
};
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
export type AccountTeam = { readonly id: string; readonly writable: boolean; readonly internal?: boolean };
|
|
30
|
+
|
|
27
31
|
export type ProjectCreateApi = {
|
|
32
|
+
readonly me: () => Promise<{ readonly teams?: readonly AccountTeam[] }>;
|
|
28
33
|
readonly projects: {
|
|
29
34
|
readonly create: (input: {
|
|
30
35
|
readonly teamId: string;
|
|
31
36
|
readonly name?: string;
|
|
32
37
|
readonly deliveryOrderId?: string;
|
|
33
38
|
}) => Promise<ProjectRecord>;
|
|
34
|
-
readonly listAll: (params?: object) => AsyncIterable<ProjectRecord>;
|
|
35
39
|
};
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
export const MISSING_PROJECT_ID_MESSAGE =
|
|
39
43
|
"Missing project ID. Pass --project-id, set TOLOKA_PROJECT_ID, use a bound toloka.json, run toloka project create for a new project, or run toloka project add for an existing project.";
|
|
40
44
|
|
|
45
|
+
export function assertProjectSelected(projectId: string | undefined): asserts projectId is string {
|
|
46
|
+
assertCli(projectId, MISSING_PROJECT_ID_MESSAGE, { hint: { disabled: true } });
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export const NO_PROJECT_SELECTED_MESSAGE =
|
|
42
50
|
"No project selected. Run toloka project create for a new project, toloka project add for an existing project, or cd into a checkout.";
|
|
43
51
|
|
|
@@ -70,9 +78,10 @@ export type ProjectIo = {
|
|
|
70
78
|
|
|
71
79
|
export const validateProjectAlias = (alias: string): string => {
|
|
72
80
|
const trimmed = alias.trim();
|
|
73
|
-
|
|
81
|
+
assertCli(
|
|
74
82
|
trimmed.length > 0 && ALIAS_PATTERN.test(trimmed),
|
|
75
83
|
"Project alias must be non-empty and contain no / or whitespace.",
|
|
84
|
+
{ hint: { description: "Use letters, digits, dashes, or underscores, for example my-project." } },
|
|
76
85
|
);
|
|
77
86
|
return trimmed;
|
|
78
87
|
};
|
|
@@ -183,11 +192,16 @@ const assertStoredNetworkMatches = (
|
|
|
183
192
|
firstNonEmpty(flags.baseUrl) ??
|
|
184
193
|
firstNonEmpty(environment.TOLOKA_BASE_URL) ??
|
|
185
194
|
resolveInternalBaseUrl(environment);
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
assertCli(
|
|
196
|
+
flagOrEnvKey === undefined || flagOrEnvKey === stored.apiKey,
|
|
197
|
+
NETWORK_MISMATCH_MESSAGE,
|
|
198
|
+
{ hint: { disabled: true } },
|
|
199
|
+
);
|
|
200
|
+
assertCli(
|
|
188
201
|
flagOrEnvBase === undefined ||
|
|
189
202
|
normalizeBaseUrl(flagOrEnvBase) === normalizeBaseUrl(stored.baseUrl),
|
|
190
203
|
NETWORK_MISMATCH_MESSAGE,
|
|
204
|
+
{ hint: { disabled: true } },
|
|
191
205
|
);
|
|
192
206
|
};
|
|
193
207
|
|
|
@@ -214,9 +228,9 @@ const promptOrFail = async (
|
|
|
214
228
|
question: string,
|
|
215
229
|
missing: string,
|
|
216
230
|
): Promise<string> => {
|
|
217
|
-
|
|
231
|
+
assertCli(io?.prompt, missing, { hint: { disabled: true } });
|
|
218
232
|
const value = (await io.prompt(question)).trim();
|
|
219
|
-
|
|
233
|
+
assertCli(value.length > 0, missing, { hint: { disabled: true } });
|
|
220
234
|
return value;
|
|
221
235
|
};
|
|
222
236
|
|
|
@@ -254,27 +268,37 @@ export async function untrackProjectResource(options: {
|
|
|
254
268
|
readonly io: ProjectIo;
|
|
255
269
|
}): Promise<{ readonly output: string }> {
|
|
256
270
|
const state = await readTolokaState(options.sourceRoot);
|
|
257
|
-
|
|
271
|
+
assertCli(
|
|
272
|
+
state,
|
|
273
|
+
`No ${TOLOKA_STATE_FILENAME} found in ${options.sourceRoot}.`,
|
|
274
|
+
{ hint: { description: "Run toloka init in the source root, or cd into the checkout that holds toloka.json." } },
|
|
275
|
+
);
|
|
258
276
|
const bindings = options.type === "pipeline" ? state.pipelines : state.datasets;
|
|
259
277
|
const binding = bindings[options.name];
|
|
260
|
-
|
|
278
|
+
assertCli(
|
|
261
279
|
binding,
|
|
262
280
|
`${options.type === "pipeline" ? "Pipeline" : "Dataset"} ${options.name} is not tracked.`,
|
|
281
|
+
{ hint: { description: "Open toloka.json to see the tracked pipelines and datasets." } },
|
|
263
282
|
);
|
|
264
283
|
const id = "pipelineId" in binding ? binding.pipelineId : binding.datasetId;
|
|
265
284
|
const label = `${options.type} ${options.name} (${id})`;
|
|
266
285
|
|
|
267
286
|
if (!options.confirm) {
|
|
268
|
-
|
|
287
|
+
assertCli(
|
|
269
288
|
options.io.prompt,
|
|
270
289
|
`Refusing to untrack ${label} without confirmation. Pass --confirm after reviewing ${TOLOKA_STATE_FILENAME}.`,
|
|
290
|
+
{ hint: { disabled: true } },
|
|
271
291
|
);
|
|
272
292
|
const answer = (
|
|
273
293
|
await options.io.prompt(`Untrack ${label}? The live resource will not be deleted. [y/N] `)
|
|
274
294
|
)
|
|
275
295
|
.trim()
|
|
276
296
|
.toLowerCase();
|
|
277
|
-
|
|
297
|
+
assertCli(
|
|
298
|
+
answer === "y" || answer === "yes",
|
|
299
|
+
"Untrack canceled.",
|
|
300
|
+
{ hint: { disabled: true } },
|
|
301
|
+
);
|
|
278
302
|
}
|
|
279
303
|
|
|
280
304
|
const remaining = Object.fromEntries(
|
|
@@ -319,9 +343,10 @@ export async function addProject(options: {
|
|
|
319
343
|
|
|
320
344
|
const existing = ownProject(found.credentials.projects, alias);
|
|
321
345
|
|
|
322
|
-
|
|
346
|
+
assertCli(
|
|
323
347
|
existing === undefined || existing.id === projectId,
|
|
324
348
|
`Alias ${alias} already points at ${existing?.id}. Run toloka project remove ${alias} first.`,
|
|
349
|
+
{ hint: { disabled: true } },
|
|
325
350
|
);
|
|
326
351
|
|
|
327
352
|
await options.afterPreflight?.();
|
|
@@ -346,7 +371,11 @@ export async function switchProject(options: {
|
|
|
346
371
|
assertStoredNetworkMatches(options.flags ?? {}, options.environment, found.credentials);
|
|
347
372
|
const alias = validateProjectAlias(options.alias);
|
|
348
373
|
const project = ownProject(found.credentials.projects, alias);
|
|
349
|
-
|
|
374
|
+
assertCli(
|
|
375
|
+
project,
|
|
376
|
+
`Unknown project alias ${alias}.`,
|
|
377
|
+
{ hint: { description: "Run toloka project add <alias> <project-id> to register it, or check the alias spelling." } },
|
|
378
|
+
);
|
|
350
379
|
|
|
351
380
|
await writeRegistry({ ...found.credentials, current: alias }, options.environment);
|
|
352
381
|
const output = `Using project ${project.id} as ${alias}.`;
|
|
@@ -367,7 +396,11 @@ export async function removeProject(options: {
|
|
|
367
396
|
string,
|
|
368
397
|
StoredProjectAlias
|
|
369
398
|
>;
|
|
370
|
-
|
|
399
|
+
assertCli(
|
|
400
|
+
ownProject(projects, alias),
|
|
401
|
+
`Unknown project alias ${alias}.`,
|
|
402
|
+
{ hint: { description: "Run toloka project add <alias> <project-id> to register it, or check the alias spelling." } },
|
|
403
|
+
);
|
|
371
404
|
delete projects[alias];
|
|
372
405
|
|
|
373
406
|
const current = found.credentials.current === alias ? undefined : found.credentials.current;
|
|
@@ -401,25 +434,71 @@ export function currentProject(options: {
|
|
|
401
434
|
return project === undefined ? { output } : { output, project };
|
|
402
435
|
}
|
|
403
436
|
|
|
404
|
-
const uniqueTeamId = (projects: ReadonlyArray<{ readonly teamId: string }>): string | undefined => {
|
|
405
|
-
const teams = [...new Set(projects.map((project) => project.teamId))];
|
|
406
|
-
return teams.length === 1 ? teams[0] : undefined;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
437
|
const resolveTeamId = async (
|
|
410
438
|
client: ProjectCreateApi,
|
|
411
439
|
teamId: string | undefined,
|
|
412
440
|
): Promise<string> => {
|
|
413
441
|
const explicit = firstNonEmpty(teamId);
|
|
414
442
|
if (explicit !== undefined) return explicit;
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
443
|
+
const { teams } = await client.me();
|
|
444
|
+
assertCli(
|
|
445
|
+
teams !== undefined,
|
|
446
|
+
"Missing team id. Pass --team-id. This platform does not report team memberships, so the team cannot be inferred.",
|
|
447
|
+
{
|
|
448
|
+
hint: {
|
|
449
|
+
description: "Copy the team id from the platform URL after /teams/, or update the platform.",
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
const writable = teams.filter((team) => team.writable).map((team) => team.id);
|
|
454
|
+
assertCli(
|
|
455
|
+
writable.length > 0,
|
|
456
|
+
"This account has no writable team, so it cannot create a project. Ask a team admin for write access or use toloka project add for an existing project.",
|
|
457
|
+
{ hint: { disabled: true } },
|
|
421
458
|
);
|
|
422
|
-
|
|
459
|
+
const [single] = writable;
|
|
460
|
+
assertCli(
|
|
461
|
+
single !== undefined && writable.length === 1,
|
|
462
|
+
`Missing team id. Pass --team-id. This account can create projects in several teams: ${writable.join(", ")}.`,
|
|
463
|
+
{ hint: { disabled: true } },
|
|
464
|
+
);
|
|
465
|
+
return single;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const DELIVERY_ORDER_HINT =
|
|
469
|
+
"Internal teams attach a Sunday delivery order to every project: run toloka delivery-order list, then re-run toloka project create with --delivery-order-id <id>.";
|
|
470
|
+
|
|
471
|
+
const DELIVERY_ORDER_NOT_ALLOWED_HINT =
|
|
472
|
+
"The owning team is not internal, so re-run toloka project create without --delivery-order-id. toloka whoami marks internal teams (internal) on its Teams: line.";
|
|
473
|
+
|
|
474
|
+
const deliveryOrderHint = (
|
|
475
|
+
input: Parameters<ProjectCreateApi["projects"]["create"]>[0],
|
|
476
|
+
details: string,
|
|
477
|
+
): string | undefined => {
|
|
478
|
+
if (!details.includes("deliveryOrderId")) return undefined;
|
|
479
|
+
if (input.deliveryOrderId === undefined && details.includes("required")) return DELIVERY_ORDER_HINT;
|
|
480
|
+
if (input.deliveryOrderId !== undefined && details.includes("only be set")) {
|
|
481
|
+
return DELIVERY_ORDER_NOT_ALLOWED_HINT;
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const createLiveProject = async (
|
|
487
|
+
client: ProjectCreateApi,
|
|
488
|
+
input: Parameters<ProjectCreateApi["projects"]["create"]>[0],
|
|
489
|
+
): Promise<ProjectRecord> => {
|
|
490
|
+
try {
|
|
491
|
+
return await client.projects.create(input);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error instanceof PotatoApiError && error.status === 400) {
|
|
494
|
+
const details = apiErrorDetails(error);
|
|
495
|
+
const hint = details === undefined ? undefined : deliveryOrderHint(input, details);
|
|
496
|
+
if (hint !== undefined) {
|
|
497
|
+
throw new TolokaCliError(error.message, { cause: error, hint: { description: hint } });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
423
502
|
};
|
|
424
503
|
|
|
425
504
|
const bindCheckout = async (
|
|
@@ -437,12 +516,17 @@ const bindCheckout = async (
|
|
|
437
516
|
});
|
|
438
517
|
} catch (error) {
|
|
439
518
|
const reason = error instanceof Error ? error.message : String(error);
|
|
440
|
-
throw new
|
|
519
|
+
throw new TolokaCliError(
|
|
441
520
|
[
|
|
442
521
|
`Created project ${created.id} as ${alias}.`,
|
|
443
522
|
`Failed to write toloka.json: ${reason}`,
|
|
444
523
|
].join("\n"),
|
|
445
|
-
{
|
|
524
|
+
{
|
|
525
|
+
cause: error,
|
|
526
|
+
hint: {
|
|
527
|
+
description: `The project exists. Make ${sourceRoot} writable, then run toloka --project-id ${created.id} init to bind it.`,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
446
530
|
);
|
|
447
531
|
}
|
|
448
532
|
return undefined;
|
|
@@ -474,9 +558,10 @@ export async function createProject(options: {
|
|
|
474
558
|
"Missing project alias. Pass an alias or run this in a terminal.",
|
|
475
559
|
)),
|
|
476
560
|
);
|
|
477
|
-
|
|
561
|
+
assertCli(
|
|
478
562
|
found.credentials.projects?.[alias] === undefined,
|
|
479
563
|
`Alias ${alias} already exists. Run toloka project remove ${alias} first.`,
|
|
564
|
+
{ hint: { disabled: true } },
|
|
480
565
|
);
|
|
481
566
|
|
|
482
567
|
const existingState =
|
|
@@ -486,7 +571,7 @@ export async function createProject(options: {
|
|
|
486
571
|
|
|
487
572
|
const teamId = await resolveTeamId(options.client, options.teamId);
|
|
488
573
|
const deliveryOrderId = firstNonEmpty(options.deliveryOrderId);
|
|
489
|
-
const created = await options.client
|
|
574
|
+
const created = await createLiveProject(options.client, {
|
|
490
575
|
teamId,
|
|
491
576
|
name: firstNonEmpty(options.name) ?? alias,
|
|
492
577
|
...(deliveryOrderId === undefined ? {} : { deliveryOrderId }),
|
|
@@ -499,12 +584,12 @@ export async function createProject(options: {
|
|
|
499
584
|
);
|
|
500
585
|
} catch (error) {
|
|
501
586
|
const reason = error instanceof Error ? error.message : String(error);
|
|
502
|
-
throw new
|
|
587
|
+
throw new TolokaCliError(
|
|
503
588
|
[
|
|
504
589
|
`Created ${created.id} but failed to update the local registry: ${reason}`,
|
|
505
590
|
`Run: toloka project add ${alias} ${created.id}`,
|
|
506
591
|
].join("\n"),
|
|
507
|
-
{ cause: error },
|
|
592
|
+
{ cause: error, hint: { disabled: true } },
|
|
508
593
|
);
|
|
509
594
|
}
|
|
510
595
|
|