@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
package/src/cli/report.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { constants } from "node:fs";
|
|
4
3
|
import { lstat, open, type FileHandle } from "node:fs/promises";
|
|
5
4
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import type { CliAnalytics } from "./analytics";
|
|
6
|
+
import { assertCli, TolokaCliError } from "./cli-error";
|
|
7
7
|
|
|
8
8
|
export type IssueReportAuthor = "human" | "agent";
|
|
9
9
|
|
|
@@ -31,9 +31,10 @@ const assertNoSymlinkParents = async (absolutePath: string, sourceRoot: string):
|
|
|
31
31
|
for (let index = 1; index < segments.length; index += 1) {
|
|
32
32
|
const directory = join(sourceRoot, ...segments.slice(0, index));
|
|
33
33
|
const stats = await lstat(directory);
|
|
34
|
-
|
|
34
|
+
assertCli(
|
|
35
35
|
!stats.isSymbolicLink(),
|
|
36
36
|
`Report file ${absolutePath} is inside the symbolic link ${directory}; select a path that does not traverse symbolic links.`,
|
|
37
|
+
{ hint: { disabled: true } },
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
};
|
|
@@ -49,20 +50,30 @@ const openReportFile = async (
|
|
|
49
50
|
try {
|
|
50
51
|
handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
51
52
|
} catch (error) {
|
|
52
|
-
|
|
53
|
+
assertCli(
|
|
53
54
|
!(error instanceof Error && "code" in error && error.code === "ELOOP"),
|
|
54
55
|
`Report file ${absolutePath} is a symbolic link; select the file it points to directly.`,
|
|
56
|
+
{ hint: { disabled: true } },
|
|
55
57
|
);
|
|
56
58
|
throw error;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
try {
|
|
60
62
|
const stats = await handle.stat();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
assertCli(
|
|
64
|
+
stats.isFile(),
|
|
65
|
+
`Report file ${absolutePath} is not a regular file.`,
|
|
66
|
+
{ hint: { description: "Pass the path of a Markdown or text file." } },
|
|
67
|
+
);
|
|
68
|
+
assertCli(
|
|
69
|
+
stats.size > 0,
|
|
70
|
+
`Report file ${absolutePath} is empty.`,
|
|
71
|
+
{ hint: { description: "Write the report first; the toloka-report-issue skill describes the format." } },
|
|
72
|
+
);
|
|
73
|
+
assertCli(
|
|
64
74
|
stats.size <= ISSUE_REPORT_MAX_BYTES,
|
|
65
75
|
`Report file ${absolutePath} is ${stats.size} bytes; the limit is 256 KiB.`,
|
|
76
|
+
{ hint: { description: "Trim the report to under 256 KiB, for example by shortening logs." } },
|
|
66
77
|
);
|
|
67
78
|
return { handle, absolutePath, size: stats.size };
|
|
68
79
|
} catch (error) {
|
|
@@ -75,14 +86,17 @@ const readReportText = async (path: string, sourceRoot: string, cwd: string): Pr
|
|
|
75
86
|
const opened = await openReportFile(path, sourceRoot, cwd);
|
|
76
87
|
try {
|
|
77
88
|
const bytes = await opened.handle.readFile();
|
|
78
|
-
|
|
89
|
+
assertCli(
|
|
79
90
|
bytes.byteLength === opened.size,
|
|
80
91
|
`${opened.absolutePath} changed while it was being read.`,
|
|
92
|
+
{ hint: { description: "Re-run toloka report once the file is no longer being written." } },
|
|
81
93
|
);
|
|
82
94
|
try {
|
|
83
95
|
return REPORT_DECODER.decode(bytes);
|
|
84
96
|
} catch {
|
|
85
|
-
throw new
|
|
97
|
+
throw new TolokaCliError(`Report file ${opened.absolutePath} is not valid UTF-8.`, {
|
|
98
|
+
hint: { description: "Save the report as UTF-8 text." },
|
|
99
|
+
});
|
|
86
100
|
}
|
|
87
101
|
} finally {
|
|
88
102
|
await opened.handle.close();
|
|
@@ -107,9 +121,10 @@ export async function submitIssueReport(options: {
|
|
|
107
121
|
readonly io: IssueReportIo;
|
|
108
122
|
}): Promise<number> {
|
|
109
123
|
const { analytics, io } = options;
|
|
110
|
-
|
|
124
|
+
assertCli(
|
|
111
125
|
analytics.issueReportingEnabled,
|
|
112
126
|
"Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.",
|
|
127
|
+
{ hint: { disabled: true } },
|
|
113
128
|
);
|
|
114
129
|
const reportText = await readReportText(options.path, options.sourceRoot, options.cwd);
|
|
115
130
|
io.stdout("Issue report preview:");
|
package/src/cli/runs.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import select from "@inquirer/select";
|
|
2
|
-
import { assert } from "es-toolkit";
|
|
3
2
|
import type { Pipeline, PotatoClient } from "../client";
|
|
4
3
|
import { PotatoApiError } from "../errors";
|
|
5
4
|
import type { TolokaState } from "../state";
|
|
6
5
|
import type { RunItem, RunJob } from "../index";
|
|
6
|
+
import { assertCli } from "./cli-error";
|
|
7
7
|
|
|
8
8
|
export type PipelineRef = {
|
|
9
9
|
readonly pipelineId: string;
|
|
@@ -131,10 +131,18 @@ const uniqueMatch = (
|
|
|
131
131
|
ambiguous: string,
|
|
132
132
|
unknown: string,
|
|
133
133
|
): PipelineRef | undefined => {
|
|
134
|
-
|
|
134
|
+
assertCli(
|
|
135
|
+
matches.length <= 1,
|
|
136
|
+
ambiguous,
|
|
137
|
+
{ hint: { description: "Pass the pipeline id shown by toloka pipeline list instead of the name." } },
|
|
138
|
+
);
|
|
135
139
|
if (matches.length !== 1) return undefined;
|
|
136
140
|
const [ref] = matches;
|
|
137
|
-
|
|
141
|
+
assertCli(
|
|
142
|
+
ref,
|
|
143
|
+
unknown,
|
|
144
|
+
{ hint: { description: "Run toloka pipeline list to see the pipelines of this project." } },
|
|
145
|
+
);
|
|
138
146
|
return ref;
|
|
139
147
|
};
|
|
140
148
|
|
|
@@ -174,7 +182,11 @@ const resolveToken = (token: string, live: readonly PipelineRef[]): PipelineRef
|
|
|
174
182
|
);
|
|
175
183
|
if (basename !== undefined) return basename;
|
|
176
184
|
|
|
177
|
-
|
|
185
|
+
assertCli(
|
|
186
|
+
false,
|
|
187
|
+
unknown,
|
|
188
|
+
{ hint: { description: "Run toloka pipeline list to see the pipelines of this project." } },
|
|
189
|
+
);
|
|
178
190
|
};
|
|
179
191
|
|
|
180
192
|
export async function resolvePipelineRef(options: {
|
|
@@ -188,7 +200,11 @@ export async function resolvePipelineRef(options: {
|
|
|
188
200
|
|
|
189
201
|
if (options.pipeline !== undefined) return resolveToken(options.pipeline, live);
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
assertCli(
|
|
204
|
+
live.length > 0,
|
|
205
|
+
"This project has no pipelines.",
|
|
206
|
+
{ hint: { description: "Run toloka apply to create the pipelines declared in source." } },
|
|
207
|
+
);
|
|
192
208
|
|
|
193
209
|
if (live.length > 1 && options.select !== undefined) {
|
|
194
210
|
const chosen = await options.select(
|
|
@@ -199,13 +215,22 @@ export async function resolvePipelineRef(options: {
|
|
|
199
215
|
|
|
200
216
|
if (live.length === 1) {
|
|
201
217
|
const [only] = live;
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
assertCli(
|
|
219
|
+
only,
|
|
220
|
+
"This project has no pipelines.",
|
|
221
|
+
{ hint: { description: "Run toloka apply to create the pipelines declared in source." } },
|
|
222
|
+
);
|
|
223
|
+
assertCli(
|
|
224
|
+
false,
|
|
225
|
+
`Pass a pipeline name. This project has: ${formatPipelineChoice(only)}.`,
|
|
226
|
+
{ hint: { disabled: true } },
|
|
227
|
+
);
|
|
204
228
|
}
|
|
205
229
|
|
|
206
|
-
|
|
230
|
+
assertCli(
|
|
207
231
|
false,
|
|
208
232
|
`This project tracks several pipelines, so name one: ${live.map(formatPipelineChoice).join(", ")}.`,
|
|
233
|
+
{ hint: { disabled: true } },
|
|
209
234
|
);
|
|
210
235
|
}
|
|
211
236
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import packageMetadata from "../../package.json" with { type: "json" };
|
|
2
|
+
|
|
3
|
+
export const SDK_PACKAGE_NAME = packageMetadata.name;
|
|
4
|
+
export const SDK_VERSION = packageMetadata.version;
|
|
5
|
+
|
|
6
|
+
export type ReleaseChannel = "latest" | "preview";
|
|
7
|
+
|
|
8
|
+
export const parseSdkVersion = (version: string): readonly number[] | undefined => {
|
|
9
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-preview\.(\d+))?$/u.exec(version);
|
|
10
|
+
if (match === null) return undefined;
|
|
11
|
+
const [, major, minor, patch, preview] = match;
|
|
12
|
+
return [
|
|
13
|
+
Number(major),
|
|
14
|
+
Number(minor),
|
|
15
|
+
Number(patch),
|
|
16
|
+
preview === undefined ? Number.POSITIVE_INFINITY : Number(preview),
|
|
17
|
+
];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const releaseChannelFor = (version: string): ReleaseChannel | undefined => {
|
|
21
|
+
const parsed = parseSdkVersion(version);
|
|
22
|
+
if (parsed === undefined) return undefined;
|
|
23
|
+
return parsed[3] === Number.POSITIVE_INFINITY ? "latest" : "preview";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const compareSdkVersions = (left: string, right: string): number | undefined => {
|
|
27
|
+
const parsedLeft = parseSdkVersion(left);
|
|
28
|
+
const parsedRight = parseSdkVersion(right);
|
|
29
|
+
if (parsedLeft === undefined || parsedRight === undefined) return undefined;
|
|
30
|
+
const length = Math.max(parsedLeft.length, parsedRight.length);
|
|
31
|
+
for (let index = 0; index < length; index += 1) {
|
|
32
|
+
const leftPart = parsedLeft[index] ?? 0;
|
|
33
|
+
const rightPart = parsedRight[index] ?? 0;
|
|
34
|
+
if (leftPart < rightPart) return -1;
|
|
35
|
+
if (leftPart > rightPart) return 1;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
};
|
package/src/cli/settings.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
|
|
4
4
|
import type { CliEnvironment } from "./credentials";
|
|
5
5
|
import { configDirectory, preparePrivateFilePath } from "./credentials";
|
|
6
|
+
import { TolokaCliError } from "./cli-error";
|
|
7
|
+
import type { TolokaCliErrorOptions } from "./cli-error";
|
|
6
8
|
|
|
7
9
|
export const AUTO_UPDATE_SETTING = "auto-update";
|
|
8
10
|
|
|
@@ -14,8 +16,8 @@ export const DEFAULT_CLI_SETTINGS: CliSettings = {
|
|
|
14
16
|
autoUpdate: true,
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
export class SettingsError extends
|
|
18
|
-
constructor(message: string, options
|
|
19
|
+
export class SettingsError extends TolokaCliError {
|
|
20
|
+
constructor(message: string, options: TolokaCliErrorOptions) {
|
|
19
21
|
super(message, options);
|
|
20
22
|
this.name = "SettingsError";
|
|
21
23
|
}
|
|
@@ -59,7 +61,9 @@ export async function writeCliSettings(
|
|
|
59
61
|
export const parseBooleanSetting = (value: string): boolean => {
|
|
60
62
|
if (isTruthyFlag(value)) return true;
|
|
61
63
|
if (isFalseyFlag(value)) return false;
|
|
62
|
-
throw new SettingsError(`Expected a boolean (true or false), got ${JSON.stringify(value)}
|
|
64
|
+
throw new SettingsError(`Expected a boolean (true or false), got ${JSON.stringify(value)}.`, {
|
|
65
|
+
hint: { description: "Run toloka config set auto-update true or false." },
|
|
66
|
+
});
|
|
63
67
|
};
|
|
64
68
|
|
|
65
69
|
export const formatCliSettings = (settings: CliSettings): string =>
|
|
@@ -71,7 +75,9 @@ export const getCliSetting = (
|
|
|
71
75
|
): { readonly output: string } => {
|
|
72
76
|
if (name === undefined || name.length === 0) return { output: formatCliSettings(settings) };
|
|
73
77
|
if (name !== AUTO_UPDATE_SETTING) {
|
|
74
|
-
throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}
|
|
78
|
+
throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`, {
|
|
79
|
+
hint: { disabled: true },
|
|
80
|
+
});
|
|
75
81
|
}
|
|
76
82
|
return { output: String(settings.autoUpdate) };
|
|
77
83
|
};
|
|
@@ -82,7 +88,9 @@ export async function setCliSetting(
|
|
|
82
88
|
value: string,
|
|
83
89
|
): Promise<{ readonly output: string; readonly settings: CliSettings }> {
|
|
84
90
|
if (name !== AUTO_UPDATE_SETTING) {
|
|
85
|
-
throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}
|
|
91
|
+
throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`, {
|
|
92
|
+
hint: { disabled: true },
|
|
93
|
+
});
|
|
86
94
|
}
|
|
87
95
|
const settings = { autoUpdate: parseBooleanSetting(value) };
|
|
88
96
|
await writeCliSettings(settings, environment);
|
package/src/cli/skill.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { assertSafeDestination, entryStats, hasErrorCode } from "./file-system";
|
|
6
5
|
import { formatCliError } from "./format-error";
|
|
6
|
+
import { releaseChannelFor, SDK_PACKAGE_NAME, SDK_VERSION } from "./sdk-version";
|
|
7
|
+
import { assertCli, REINSTALL_HINT, TolokaCliError } from "./cli-error";
|
|
7
8
|
|
|
8
9
|
export const SKILL_CATALOG_MANIFEST = "catalog.json";
|
|
9
10
|
export const SKILL_INSTALL_ROOT = ".agents/skills";
|
|
@@ -15,6 +16,9 @@ const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
|
15
16
|
const MAX_SKILL_ID_LENGTH = 64;
|
|
16
17
|
const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
17
18
|
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
19
|
+
const UPGRADE_HINT = `bun add --global ${SDK_PACKAGE_NAME}@${
|
|
20
|
+
releaseChannelFor(SDK_VERSION) ?? "latest"
|
|
21
|
+
}`;
|
|
18
22
|
|
|
19
23
|
export type SkillDescriptor = {
|
|
20
24
|
readonly id: string;
|
|
@@ -72,31 +76,51 @@ const isSafeRelativePath = (path: string) =>
|
|
|
72
76
|
path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
73
77
|
|
|
74
78
|
const parseDescriptor = (value: unknown, invalid: (detail: string) => string): SkillDescriptor => {
|
|
75
|
-
|
|
79
|
+
assertCli(isRecord(value), invalid("every skill must be an object."), { hint: REINSTALL_HINT });
|
|
76
80
|
const { id, name, description, tags, version, digest, files } = value;
|
|
77
|
-
|
|
81
|
+
assertCli(
|
|
78
82
|
isSafeSkillId(id),
|
|
79
83
|
invalid(
|
|
80
84
|
`skill id ${JSON.stringify(id)} must be at most ${MAX_SKILL_ID_LENGTH} lowercase letters, digits, and single hyphens.`,
|
|
81
85
|
),
|
|
86
|
+
{ hint: REINSTALL_HINT },
|
|
82
87
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
assertCli(isNonEmptyString(name), invalid(`skill ${id} needs a name.`), { hint: REINSTALL_HINT });
|
|
89
|
+
assertCli(
|
|
90
|
+
isNonEmptyString(description),
|
|
91
|
+
invalid(`skill ${id} needs a description.`),
|
|
92
|
+
{ hint: REINSTALL_HINT },
|
|
93
|
+
);
|
|
94
|
+
assertCli(
|
|
95
|
+
isStringArray(tags),
|
|
96
|
+
invalid(`skill ${id} tags must be strings.`),
|
|
97
|
+
{ hint: REINSTALL_HINT },
|
|
98
|
+
);
|
|
99
|
+
assertCli(
|
|
100
|
+
isNonEmptyString(version),
|
|
101
|
+
invalid(`skill ${id} needs a version.`),
|
|
102
|
+
{ hint: REINSTALL_HINT },
|
|
103
|
+
);
|
|
104
|
+
assertCli(
|
|
88
105
|
typeof digest === "string" && DIGEST_PATTERN.test(digest),
|
|
89
106
|
invalid(`skill ${id} digest must be the sha256 hex digest of its files.`),
|
|
107
|
+
{ hint: REINSTALL_HINT },
|
|
90
108
|
);
|
|
91
|
-
|
|
109
|
+
assertCli(
|
|
92
110
|
isStringArray(files) && files.includes(SKILL_ENTRY_POINT),
|
|
93
111
|
invalid(`skill ${id} files must include ${SKILL_ENTRY_POINT}.`),
|
|
112
|
+
{ hint: REINSTALL_HINT },
|
|
94
113
|
);
|
|
95
|
-
|
|
114
|
+
assertCli(
|
|
96
115
|
files.every(isSafeRelativePath),
|
|
97
116
|
invalid(`skill ${id} files must be relative paths inside its directory.`),
|
|
117
|
+
{ hint: REINSTALL_HINT },
|
|
118
|
+
);
|
|
119
|
+
assertCli(
|
|
120
|
+
new Set(files).size === files.length,
|
|
121
|
+
invalid(`skill ${id} lists a file more than once.`),
|
|
122
|
+
{ hint: REINSTALL_HINT },
|
|
98
123
|
);
|
|
99
|
-
assert(new Set(files).size === files.length, invalid(`skill ${id} lists a file more than once.`));
|
|
100
124
|
return { id, name, description, tags, version, digest, files };
|
|
101
125
|
};
|
|
102
126
|
|
|
@@ -104,7 +128,10 @@ const parseManifest = (source: string, manifestPath: string): unknown => {
|
|
|
104
128
|
try {
|
|
105
129
|
return JSON.parse(source);
|
|
106
130
|
} catch (error) {
|
|
107
|
-
throw new
|
|
131
|
+
throw new TolokaCliError(`Invalid skill catalog ${manifestPath}: ${formatCliError(error)}`, {
|
|
132
|
+
cause: error,
|
|
133
|
+
hint: REINSTALL_HINT,
|
|
134
|
+
});
|
|
108
135
|
}
|
|
109
136
|
};
|
|
110
137
|
|
|
@@ -136,9 +163,10 @@ export async function resolveBundledSkillCatalogRoot(
|
|
|
136
163
|
entry === undefined
|
|
137
164
|
? "process.argv[1] names no runtime entry"
|
|
138
165
|
: `none exists between ${entry} and the filesystem root`;
|
|
139
|
-
|
|
166
|
+
assertCli(
|
|
140
167
|
workingDirectoryRoot !== undefined,
|
|
141
168
|
`Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and no ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} exists above ${process.cwd()} either. Reinstall @toloka-platform/sdk.`,
|
|
169
|
+
{ hint: { disabled: true } },
|
|
142
170
|
);
|
|
143
171
|
return workingDirectoryRoot;
|
|
144
172
|
}
|
|
@@ -150,23 +178,30 @@ export async function loadSkillCatalog(catalogRoot: string): Promise<SkillCatalo
|
|
|
150
178
|
const manifestPath = join(catalogRoot, SKILL_CATALOG_MANIFEST);
|
|
151
179
|
const invalid = (detail: string) => `Invalid skill catalog ${manifestPath}: ${detail}`;
|
|
152
180
|
const { size } = await stat(manifestPath);
|
|
153
|
-
|
|
181
|
+
assertCli(
|
|
154
182
|
size <= MAX_MANIFEST_BYTES,
|
|
155
183
|
invalid(`the manifest is ${size} bytes; the limit is ${MAX_MANIFEST_BYTES} bytes.`),
|
|
184
|
+
{ hint: REINSTALL_HINT },
|
|
156
185
|
);
|
|
157
186
|
const manifest = parseManifest(await readFile(manifestPath, "utf8"), manifestPath);
|
|
158
|
-
|
|
187
|
+
assertCli(
|
|
159
188
|
isRecord(manifest) && Array.isArray(manifest.skills),
|
|
160
189
|
invalid("expected an object with a skills array."),
|
|
190
|
+
{ hint: REINSTALL_HINT },
|
|
161
191
|
);
|
|
162
|
-
|
|
192
|
+
assertCli(
|
|
163
193
|
manifest.skills.length <= MAX_SKILL_COUNT,
|
|
164
194
|
invalid(`${manifest.skills.length} skills exceed the limit of ${MAX_SKILL_COUNT}.`),
|
|
195
|
+
{ hint: REINSTALL_HINT },
|
|
165
196
|
);
|
|
166
197
|
const skills = manifest.skills.map((skill) => parseDescriptor(skill, invalid));
|
|
167
198
|
const ids = new Set<string>();
|
|
168
199
|
for (const skill of skills) {
|
|
169
|
-
|
|
200
|
+
assertCli(
|
|
201
|
+
!ids.has(skill.id),
|
|
202
|
+
invalid(`skill id ${skill.id} appears more than once.`),
|
|
203
|
+
{ hint: REINSTALL_HINT },
|
|
204
|
+
);
|
|
170
205
|
ids.add(skill.id);
|
|
171
206
|
}
|
|
172
207
|
return { root: catalogRoot, manifestPath, skills };
|
|
@@ -249,9 +284,14 @@ const swapDirectories = async (
|
|
|
249
284
|
try {
|
|
250
285
|
await renameEntry(backup, destination);
|
|
251
286
|
} catch (restoreError) {
|
|
252
|
-
throw new
|
|
287
|
+
throw new TolokaCliError(
|
|
253
288
|
`Failed to replace ${destination}; the previous version is preserved at ${backup}. ${formatCliError(restoreError)}`,
|
|
254
|
-
{
|
|
289
|
+
{
|
|
290
|
+
cause: error,
|
|
291
|
+
hint: {
|
|
292
|
+
description: `Move ${backup} back to ${destination} by hand, then re-run toloka skill install <skill-id> --force.`,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
255
295
|
);
|
|
256
296
|
}
|
|
257
297
|
throw error;
|
|
@@ -267,17 +307,19 @@ export async function installSkill({
|
|
|
267
307
|
rename: renameEntry = rename,
|
|
268
308
|
}: InstallSkillOptions): Promise<InstalledSkill> {
|
|
269
309
|
const skill = catalog.skills.find((candidate) => candidate.id === skillId);
|
|
270
|
-
|
|
310
|
+
assertCli(
|
|
271
311
|
skill,
|
|
272
|
-
`Skill ${JSON.stringify(skillId)} is not in the catalog. Run \`toloka skill list\` to see the available skills.`,
|
|
312
|
+
`Skill ${JSON.stringify(skillId)} is not in the catalog bundled with ${SDK_PACKAGE_NAME} ${SDK_VERSION}. Run \`toloka skill list\` to see the available skills; a newer release may ship it (${UPGRADE_HINT}).`,
|
|
313
|
+
{ hint: { disabled: true } },
|
|
273
314
|
);
|
|
274
315
|
const relativeDestination = `${SKILL_INSTALL_ROOT}/${skill.id}`;
|
|
275
316
|
await assertSafeDestination(sourceRoot, relativeDestination);
|
|
276
317
|
const destination = join(sourceRoot, relativeDestination);
|
|
277
318
|
const existing = await entryStats(destination);
|
|
278
|
-
|
|
319
|
+
assertCli(
|
|
279
320
|
existing === undefined || force,
|
|
280
321
|
`${relativeDestination} already exists. Re-run with --force to replace it with the catalog version of ${skill.id}.`,
|
|
322
|
+
{ hint: { disabled: true } },
|
|
281
323
|
);
|
|
282
324
|
|
|
283
325
|
const skillsRoot = dirname(destination);
|
|
@@ -289,9 +331,10 @@ export async function installSkill({
|
|
|
289
331
|
await mkdir(dirname(target), { recursive: true });
|
|
290
332
|
await copyFile(join(catalog.root, skill.id, path), target);
|
|
291
333
|
}
|
|
292
|
-
|
|
334
|
+
assertCli(
|
|
293
335
|
(await computeSkillDigest(temporary, skill.files)) === skill.digest,
|
|
294
336
|
`Skill ${skill.id} failed its digest check: the packaged files do not match the catalog digest ${skill.digest}. Nothing was installed.`,
|
|
337
|
+
{ hint: REINSTALL_HINT },
|
|
295
338
|
);
|
|
296
339
|
if (existing === undefined) {
|
|
297
340
|
await renameEntry(temporary, destination);
|
|
@@ -348,7 +391,7 @@ export async function skillSearchCommand({
|
|
|
348
391
|
return;
|
|
349
392
|
}
|
|
350
393
|
io.stdout(
|
|
351
|
-
`No skills match ${JSON.stringify(query)}. Run \`toloka skill list\` to see
|
|
394
|
+
`No skills match ${JSON.stringify(query)} in the catalog bundled with ${SDK_PACKAGE_NAME} ${SDK_VERSION}. Run \`toloka skill list\` to see it; a newer release may add more (${UPGRADE_HINT}).`,
|
|
352
395
|
);
|
|
353
396
|
}
|
|
354
397
|
|
package/src/cli/source.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { readdir, stat } from "node:fs/promises";
|
|
3
2
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
3
|
import { pathToFileURL } from "node:url";
|
|
5
4
|
import { validatePipelineDefinition } from "../authoring";
|
|
6
5
|
import type { PipelineDefinition } from "../authoring";
|
|
6
|
+
import { assertCli } from "./cli-error";
|
|
7
7
|
|
|
8
8
|
export type ProjectPipelineSource = {
|
|
9
9
|
readonly sourceDirectory: string;
|
|
@@ -64,9 +64,10 @@ export async function loadProjectSource(sourceRoot: string): Promise<ProjectSour
|
|
|
64
64
|
const moduleUrl = pathToFileURL(modulePath);
|
|
65
65
|
moduleUrl.searchParams.set("version", `${moduleStat.mtimeMs}-${moduleStat.size}`);
|
|
66
66
|
const module: unknown = await import(moduleUrl.href);
|
|
67
|
-
|
|
67
|
+
assertCli(
|
|
68
68
|
typeof module === "object" && module !== null,
|
|
69
69
|
`Invalid ${modulePath}: expected a module`,
|
|
70
|
+
{ hint: { description: "Make the file a TypeScript module that exports the pipeline definition." } },
|
|
70
71
|
);
|
|
71
72
|
const definition: unknown = Reflect.get(module, "default");
|
|
72
73
|
validatePipelineDefinition(definition);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import type { AuthPrompt } from "./auth";
|
|
3
|
+
import type { CliIo } from "./context";
|
|
4
|
+
import { pickPipeline } from "./runs";
|
|
5
|
+
import { TolokaCliError } from "./cli-error";
|
|
6
|
+
|
|
7
|
+
const stdinClosedError = () =>
|
|
8
|
+
new TolokaCliError("Standard input closed before an API key was entered.", {
|
|
9
|
+
hint: {
|
|
10
|
+
description:
|
|
11
|
+
"Run toloka login in an interactive terminal, or pipe the key with toloka login --api-key-stdin.",
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const readStandardInput = async (): Promise<string> => {
|
|
16
|
+
process.stdin.setEncoding("utf8");
|
|
17
|
+
const chunks: string[] = [];
|
|
18
|
+
for await (const chunk of process.stdin) chunks.push(String(chunk));
|
|
19
|
+
return chunks.join("");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const readMaskedLine = async (question: string): Promise<string> => {
|
|
23
|
+
process.stdout.write(question);
|
|
24
|
+
const wasRaw = process.stdin.isRaw;
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
process.stdin.resume();
|
|
27
|
+
process.stdin.setEncoding("utf8");
|
|
28
|
+
|
|
29
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
30
|
+
let value = "";
|
|
31
|
+
|
|
32
|
+
const restore = () => {
|
|
33
|
+
process.stdin.off("data", onData);
|
|
34
|
+
process.stdin.off("end", onEnd);
|
|
35
|
+
process.stdin.off("error", onError);
|
|
36
|
+
process.stdin.setRawMode(wasRaw);
|
|
37
|
+
process.stdin.pause();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const finish = () => {
|
|
41
|
+
restore();
|
|
42
|
+
process.stdout.write("\n");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const onEnd = () => {
|
|
46
|
+
finish();
|
|
47
|
+
reject(stdinClosedError());
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onError = (error: Error) => {
|
|
51
|
+
finish();
|
|
52
|
+
reject(error);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onData = (chunk: string) => {
|
|
56
|
+
for (const character of chunk) {
|
|
57
|
+
if (character === "\r" || character === "\n") {
|
|
58
|
+
finish();
|
|
59
|
+
resolve(value);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (character === "\u0003" || character === "\u0004") {
|
|
63
|
+
finish();
|
|
64
|
+
reject(
|
|
65
|
+
character === "\u0003"
|
|
66
|
+
? new TolokaCliError("Canceled.", { hint: { disabled: true } })
|
|
67
|
+
: stdinClosedError(),
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (character === "\u007f" || character === "\b") {
|
|
72
|
+
if (value.length > 0) {
|
|
73
|
+
value = value.slice(0, -1);
|
|
74
|
+
process.stdout.write("\b \b");
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
value += character;
|
|
79
|
+
process.stdout.write("*");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
process.stdin.on("data", onData);
|
|
84
|
+
process.stdin.once("end", onEnd);
|
|
85
|
+
process.stdin.once("error", onError);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return await promise;
|
|
89
|
+
} finally {
|
|
90
|
+
restore();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const promptFromTerminal: AuthPrompt = async (question, options = {}) => {
|
|
95
|
+
if (options.mask === true) return readMaskedLine(question);
|
|
96
|
+
|
|
97
|
+
const input = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
98
|
+
try {
|
|
99
|
+
return await input.question(question);
|
|
100
|
+
} finally {
|
|
101
|
+
input.close();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const terminalIo = (): CliIo => ({
|
|
106
|
+
stdout: (text) => console.log(text),
|
|
107
|
+
stderr: (text) => console.error(text),
|
|
108
|
+
readStdin: readStandardInput,
|
|
109
|
+
...(process.stdin.isTTY ? { prompt: promptFromTerminal, select: pickPipeline } : {}),
|
|
110
|
+
});
|