@toloka-platform/sdk 0.1.0-preview.6 → 0.1.0-preview.7
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 +7 -11
- package/dist/cli/toloka.js +208 -123
- 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 +17 -4
- package/docs/cli-v2.md +3 -3
- package/docs/cli.md +29 -16
- package/docs/getting-started.md +7 -12
- package/docs/limitations.md +6 -6
- package/docs/testing-examples.md +248 -0
- package/docs/testing.md +115 -0
- package/examples/face-image-collection/README.md +7 -1
- package/examples/face-image-collection/fields.ts +20 -8
- package/examples/face-image-collection/nodes/annotate-faces/node.ts +1 -9
- package/examples/face-image-collection/nodes/order-faces/node.ts +1 -1
- package/examples/face-image-collection/pipeline.browser.integration.spec.ts +78 -0
- package/examples/video-pair-rating/README.md +14 -6
- package/examples/video-pair-rating/nodes/adjudicate/node.ts +1 -3
- package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +1 -3
- package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +1 -1
- package/examples/video-pair-rating/nodes/rate_pair/node.ts +1 -1
- package/examples/video-pair-rating/pipeline.browser.integration.spec.ts +267 -0
- package/openapi/gen/api.ts +48 -11
- package/openapi/gen/models/createDatasetExportBody.ts +3 -3
- package/openapi/gen/models/createDatasetExportBodyOneOf.ts +6 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfFormat.ts +2 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThree.ts +10 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeContract.ts +2 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeFormat.ts +2 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilter.ts +9 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItem.ts +6 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItem.ts +12 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemOperator.ts +2 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemValue.ts +2 -0
- package/openapi/gen/models/datasetExportReport.ts +9 -0
- package/openapi/gen/models/datasetExportReportErrorsItem.ts +5 -0
- package/openapi/gen/models/datasetExportReportTokens.ts +7 -0
- package/openapi/gen/models/exportJob.ts +2 -0
- package/openapi/gen/models/exportJobReport.ts +3 -0
- package/openapi/gen/models/index.ts +48 -0
- package/openapi/gen/models/sampleDatasetItemsBody.ts +4 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOf.ts +8 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilter.ts +9 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItem.ts +6 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItem.ts +12 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemOperator.ts +2 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemValue.ts +2 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethree.ts +8 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilter.ts +9 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItem.ts +6 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItem.ts +12 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemOperator.ts +2 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
- package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemValue.ts +2 -0
- package/openapi/gen/models/sampleJob.ts +13 -0
- package/openapi/gen/models/sampleJobStatus.ts +2 -0
- package/openapi/gen/models/sampleResult.ts +5 -0
- package/package.json +23 -2
- package/skills/catalog.json +14 -6
- package/skills/toloka-build/SKILL.md +34 -12
- package/skills/toloka-build/references/node-semantics.md +27 -24
- package/skills/toloka-build/references/workflow-and-cli.md +55 -26
- package/skills/toloka-report-issue/references/report-template.md +1 -1
- package/src/checks/ast.ts +32 -0
- package/src/checks/index.ts +14 -0
- package/src/checks/postprocess-collisions.ts +145 -0
- package/src/checks/task-ui-declarations.generated.ts +19 -0
- package/src/checks/task-ui-typecheck.ts +345 -0
- package/src/checks/typescript-vfs.d.ts +26 -0
- package/src/checks/typescript-vfs.js +5 -0
- package/src/cli/analytics.ts +1 -0
- package/src/cli/check.ts +977 -0
- package/src/cli/program.ts +147 -19
- package/src/cli/project-context.ts +14 -4
- package/src/cli/runs.ts +21 -11
- package/src/cli/skill.ts +17 -6
- package/src/testing/code-node.ts +160 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/task-ui-browser.ts +120 -0
- package/src/testing/task-ui-validation.ts +135 -0
- package/src/testing/task-ui.ts +557 -0
- package/src/testing/test-evidence.ts +170 -0
- package/src/testing/types.ts +141 -0
package/src/cli/program.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
2
2
|
import { assert } from "es-toolkit";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { createClient } from "../client";
|
|
6
7
|
import type { PotatoClient } from "../client";
|
|
7
8
|
import { readTolokaState } from "../state";
|
|
8
9
|
import type { TolokaState } from "../state";
|
|
10
|
+
import {
|
|
11
|
+
CHECK_RESULT_FILE_ENV,
|
|
12
|
+
checkProject,
|
|
13
|
+
watchProjectChecks,
|
|
14
|
+
runFreshCheck,
|
|
15
|
+
type CheckProjectOptions,
|
|
16
|
+
type CheckProjectResult,
|
|
17
|
+
} from "./check";
|
|
9
18
|
import { authStatus, login, logout } from "./auth";
|
|
10
19
|
import type { AuthPrompt } from "./auth";
|
|
11
20
|
import { commandNameFromArgv, createCliAnalytics } from "./analytics";
|
|
@@ -83,6 +92,7 @@ export type ProgramContext = {
|
|
|
83
92
|
readonly environment: TolokaEnvironment;
|
|
84
93
|
readonly io: CliIo;
|
|
85
94
|
readonly injectedClient?: PotatoClient;
|
|
95
|
+
readonly runChecks?: typeof checkProject;
|
|
86
96
|
readonly analytics?: CliAnalytics;
|
|
87
97
|
};
|
|
88
98
|
|
|
@@ -202,6 +212,29 @@ export type CreatedProgram = {
|
|
|
202
212
|
export function createProgram(context: ProgramContext): CreatedProgram {
|
|
203
213
|
const program = new Command("toloka");
|
|
204
214
|
const exitCode = { value: 0 };
|
|
215
|
+
const runChecks = context.runChecks ?? checkProject;
|
|
216
|
+
const runChecksInterruptibly = async (
|
|
217
|
+
options: CheckProjectOptions,
|
|
218
|
+
runner: (options: CheckProjectOptions) => Promise<CheckProjectResult> = runChecks,
|
|
219
|
+
): Promise<{ result: CheckProjectResult; interruptedExitCode?: number }> => {
|
|
220
|
+
const abortController = new AbortController();
|
|
221
|
+
let interruptedExitCode: number | undefined;
|
|
222
|
+
const interrupt = (exitCode: number) => {
|
|
223
|
+
interruptedExitCode = exitCode;
|
|
224
|
+
abortController.abort();
|
|
225
|
+
};
|
|
226
|
+
const interruptSigint = () => interrupt(130);
|
|
227
|
+
const interruptSigterm = () => interrupt(143);
|
|
228
|
+
process.on("SIGINT", interruptSigint);
|
|
229
|
+
process.on("SIGTERM", interruptSigterm);
|
|
230
|
+
try {
|
|
231
|
+
const result = await runner({ ...options, signal: abortController.signal });
|
|
232
|
+
return { result, ...(interruptedExitCode === undefined ? {} : { interruptedExitCode }) };
|
|
233
|
+
} finally {
|
|
234
|
+
process.off("SIGINT", interruptSigint);
|
|
235
|
+
process.off("SIGTERM", interruptSigterm);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
205
238
|
let warnedAboutStaleSkills = false;
|
|
206
239
|
|
|
207
240
|
program
|
|
@@ -324,8 +357,9 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
324
357
|
.description("select and manage the current platform project");
|
|
325
358
|
project
|
|
326
359
|
.command("list")
|
|
360
|
+
.option("--limit <n>", "maximum projects to list", positiveInteger)
|
|
327
361
|
.description("list remote projects this key can see")
|
|
328
|
-
.action(async () => {
|
|
362
|
+
.action(async (options: { limit?: number }) => {
|
|
329
363
|
const flags = globals();
|
|
330
364
|
const { client, stored, state } = await resolveContext(flags, context);
|
|
331
365
|
const selected = resolveSelectedProject(flags, context, stored, state);
|
|
@@ -333,6 +367,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
333
367
|
client,
|
|
334
368
|
io: context.io,
|
|
335
369
|
...(selected === undefined ? {} : { context: selected }),
|
|
370
|
+
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
336
371
|
});
|
|
337
372
|
});
|
|
338
373
|
project
|
|
@@ -440,9 +475,12 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
440
475
|
.action(async () => {
|
|
441
476
|
const flags = globals();
|
|
442
477
|
const sourceRoot = resolveInitRoot(flags);
|
|
443
|
-
const stored = await readCommandCredentials(flags, context);
|
|
444
478
|
const state = await readTolokaState(sourceRoot);
|
|
445
|
-
const selected =
|
|
479
|
+
const selected = resolveProjectContext({
|
|
480
|
+
flags,
|
|
481
|
+
environment: context.environment,
|
|
482
|
+
...(state === undefined ? {} : { state }),
|
|
483
|
+
});
|
|
446
484
|
const result = await initProject(
|
|
447
485
|
sourceRoot,
|
|
448
486
|
selected === undefined ? {} : { projectId: selected.id },
|
|
@@ -500,13 +538,89 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
500
538
|
if (!result.valid) exitCode.value = 1;
|
|
501
539
|
});
|
|
502
540
|
|
|
541
|
+
program
|
|
542
|
+
.command("check")
|
|
543
|
+
.argument("[pipeline]", "pipeline source directory or name")
|
|
544
|
+
.option("--node <node>", "check one code or human-work node by name or id")
|
|
545
|
+
.option("--json", "print the structured check report", false)
|
|
546
|
+
.option("--watch", "rerun checks when project files change", false)
|
|
547
|
+
.description("run the complete local source, type, and test gate")
|
|
548
|
+
.action(
|
|
549
|
+
async (
|
|
550
|
+
pipeline: string | undefined,
|
|
551
|
+
options: { node?: string; json: boolean; watch: boolean },
|
|
552
|
+
) => {
|
|
553
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
554
|
+
const checkOptions = {
|
|
555
|
+
environment: context.environment,
|
|
556
|
+
sourceRoot,
|
|
557
|
+
...(pipeline === undefined ? {} : { pipeline }),
|
|
558
|
+
...(options.node === undefined ? {} : { node: options.node }),
|
|
559
|
+
...(options.json ? { json: true } : {}),
|
|
560
|
+
};
|
|
561
|
+
const writeCheckOutput = async (output: string): Promise<void> => {
|
|
562
|
+
const resultPath = context.environment[CHECK_RESULT_FILE_ENV];
|
|
563
|
+
if (resultPath === undefined) context.io.stdout(output);
|
|
564
|
+
else await writeFile(resultPath, output, "utf8");
|
|
565
|
+
};
|
|
566
|
+
const checkRunner =
|
|
567
|
+
context.runChecks === undefined &&
|
|
568
|
+
context.environment[CHECK_RESULT_FILE_ENV] === undefined
|
|
569
|
+
? runFreshCheck
|
|
570
|
+
: runChecks;
|
|
571
|
+
if (options.watch) {
|
|
572
|
+
const abortController = new AbortController();
|
|
573
|
+
const stop = () => abortController.abort();
|
|
574
|
+
process.on("SIGINT", stop);
|
|
575
|
+
process.on("SIGTERM", stop);
|
|
576
|
+
try {
|
|
577
|
+
await watchProjectChecks(
|
|
578
|
+
checkOptions,
|
|
579
|
+
async (result) => {
|
|
580
|
+
await writeCheckOutput(result.output);
|
|
581
|
+
exitCode.value = result.exitCode;
|
|
582
|
+
},
|
|
583
|
+
undefined,
|
|
584
|
+
abortController.signal,
|
|
585
|
+
);
|
|
586
|
+
} finally {
|
|
587
|
+
process.off("SIGINT", stop);
|
|
588
|
+
process.off("SIGTERM", stop);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const { result, interruptedExitCode } = await runChecksInterruptibly(
|
|
593
|
+
checkOptions,
|
|
594
|
+
checkRunner,
|
|
595
|
+
);
|
|
596
|
+
await writeCheckOutput(result.output);
|
|
597
|
+
exitCode.value = interruptedExitCode ?? result.exitCode;
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
|
|
503
601
|
program
|
|
504
602
|
.command("apply")
|
|
505
|
-
.description("make the platform match
|
|
603
|
+
.description("check source, then make the platform match it")
|
|
506
604
|
.action(async () => {
|
|
507
|
-
const
|
|
605
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
606
|
+
const { result: checked, interruptedExitCode } = await runChecksInterruptibly({
|
|
607
|
+
sourceRoot,
|
|
608
|
+
environment: context.environment,
|
|
609
|
+
});
|
|
610
|
+
context.io.stdout(checked.output);
|
|
611
|
+
if (interruptedExitCode !== undefined) {
|
|
612
|
+
exitCode.value = interruptedExitCode;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (checked.exitCode !== 0) {
|
|
616
|
+
exitCode.value = checked.exitCode;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
assert(checked.source, "Passing checks must return project source");
|
|
620
|
+
const { client, projectId } = await resolveContext(globals(), context);
|
|
508
621
|
const result = await applyProject(client, {
|
|
509
622
|
sourceRoot,
|
|
623
|
+
source: checked.source,
|
|
510
624
|
...(projectId === undefined ? {} : { projectId }),
|
|
511
625
|
});
|
|
512
626
|
context.io.stdout(result.output);
|
|
@@ -699,11 +813,17 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
699
813
|
.description("operate on a live pipeline in the selected project");
|
|
700
814
|
pipeline
|
|
701
815
|
.command("list")
|
|
816
|
+
.option("--limit <n>", "maximum pipelines to list", positiveInteger)
|
|
702
817
|
.description("list live pipelines in the selected project")
|
|
703
|
-
.action(async () => {
|
|
818
|
+
.action(async (options: { limit?: number }) => {
|
|
704
819
|
const { client, projectId, state } = await resolveContext(globals(), context);
|
|
705
820
|
assert(projectId, MISSING_PROJECT_ID_MESSAGE);
|
|
706
|
-
const result = await listLivePipelines({
|
|
821
|
+
const result = await listLivePipelines({
|
|
822
|
+
client,
|
|
823
|
+
projectId,
|
|
824
|
+
state,
|
|
825
|
+
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
826
|
+
});
|
|
707
827
|
context.io.stdout(result.output);
|
|
708
828
|
});
|
|
709
829
|
pipeline
|
|
@@ -1005,27 +1125,35 @@ export async function runTolokaCli(
|
|
|
1005
1125
|
environment: TolokaEnvironment = process.env,
|
|
1006
1126
|
io: CliIo = terminalIo(),
|
|
1007
1127
|
injectedClient?: PotatoClient,
|
|
1008
|
-
analytics
|
|
1128
|
+
analytics?: CliAnalytics,
|
|
1129
|
+
runChecks?: typeof checkProject,
|
|
1009
1130
|
): Promise<number> {
|
|
1131
|
+
const activeAnalytics =
|
|
1132
|
+
environment[CHECK_RESULT_FILE_ENV] === undefined
|
|
1133
|
+
? (analytics ?? createCliAnalytics({ environment }))
|
|
1134
|
+
: undefined;
|
|
1010
1135
|
const { program, exitCode } = createProgram({
|
|
1011
1136
|
environment,
|
|
1012
1137
|
io,
|
|
1013
|
-
analytics,
|
|
1138
|
+
...(activeAnalytics === undefined ? {} : { analytics: activeAnalytics }),
|
|
1014
1139
|
...(injectedClient === undefined ? {} : { injectedClient }),
|
|
1140
|
+
...(runChecks === undefined ? {} : { runChecks }),
|
|
1015
1141
|
});
|
|
1016
1142
|
|
|
1017
1143
|
const startedAt = Date.now();
|
|
1018
1144
|
const result = await runProgram(program, exitCode, argv, io);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1145
|
+
if (activeAnalytics !== undefined) {
|
|
1146
|
+
try {
|
|
1147
|
+
activeAnalytics.captureCommand({
|
|
1148
|
+
command: commandNameFromArgv(argv),
|
|
1149
|
+
durationMs: Date.now() - startedAt,
|
|
1150
|
+
exitCode: result.code,
|
|
1151
|
+
outcome: result.outcome,
|
|
1152
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
1153
|
+
});
|
|
1154
|
+
await activeAnalytics.shutdown();
|
|
1155
|
+
} catch {}
|
|
1156
|
+
}
|
|
1029
1157
|
|
|
1030
1158
|
return result.code;
|
|
1031
1159
|
}
|
|
@@ -11,7 +11,10 @@ type ProjectRecord = { readonly id: string; readonly name: string; readonly team
|
|
|
11
11
|
|
|
12
12
|
export type ProjectListApi = {
|
|
13
13
|
readonly projects: {
|
|
14
|
-
readonly
|
|
14
|
+
readonly listPage: (params: { readonly limit: number }) => Promise<{
|
|
15
|
+
readonly projects: readonly ProjectRecord[];
|
|
16
|
+
readonly pagination: { readonly total: number; readonly hasMore: boolean };
|
|
17
|
+
}>;
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
|
|
@@ -225,15 +228,22 @@ export async function listRemoteProjects(options: {
|
|
|
225
228
|
readonly client: ProjectListApi;
|
|
226
229
|
readonly context?: ProjectContext;
|
|
227
230
|
readonly io: ProjectIo;
|
|
231
|
+
readonly limit?: number;
|
|
228
232
|
}): Promise<{ readonly output: string }> {
|
|
229
|
-
const
|
|
230
|
-
|
|
233
|
+
const limit = options.limit ?? 20;
|
|
234
|
+
const page = await options.client.projects.listPage({ limit });
|
|
235
|
+
const lines = page.projects.map((project) => {
|
|
231
236
|
const mark = options.context?.id === project.id ? "*" : " ";
|
|
232
237
|
const alias =
|
|
233
238
|
options.context?.id === project.id && options.context.alias !== undefined
|
|
234
239
|
? `\t${options.context.alias}`
|
|
235
240
|
: "";
|
|
236
|
-
|
|
241
|
+
return `${mark}\t${project.id}\t${project.name}${alias}`;
|
|
242
|
+
});
|
|
243
|
+
if (page.pagination.hasMore) {
|
|
244
|
+
lines.push(
|
|
245
|
+
`Showing ${page.projects.length} of ${page.pagination.total} projects. Raise --limit to see more.`,
|
|
246
|
+
);
|
|
237
247
|
}
|
|
238
248
|
const output = lines.length === 0 ? "No projects visible." : lines.join("\n");
|
|
239
249
|
options.io.stdout(output);
|
package/src/cli/runs.ts
CHANGED
|
@@ -84,21 +84,31 @@ export async function listLivePipelines(options: {
|
|
|
84
84
|
readonly client: PotatoClient;
|
|
85
85
|
readonly projectId: string;
|
|
86
86
|
readonly state?: TolokaState;
|
|
87
|
+
readonly limit?: number;
|
|
87
88
|
}): Promise<RunCommandResult> {
|
|
88
|
-
const
|
|
89
|
+
const limit = options.limit ?? 20;
|
|
90
|
+
const page = await options.client.pipelines.listPage({ projectId: options.projectId, limit });
|
|
91
|
+
const sourceDirectoryById = bindingByLiveId(options.projectId, options.state);
|
|
92
|
+
const live = page.pipelines
|
|
93
|
+
.map((pipeline) => toRef(pipeline, sourceDirectoryById))
|
|
94
|
+
.toSorted((left, right) => {
|
|
95
|
+
const byName = left.name.localeCompare(right.name);
|
|
96
|
+
return byName === 0 ? left.pipelineId.localeCompare(right.pipelineId) : byName;
|
|
97
|
+
});
|
|
89
98
|
if (live.length === 0) {
|
|
90
99
|
return { outcome: "reported", output: "This project has no pipelines." };
|
|
91
100
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
101
|
+
const lines = live.map((ref) =>
|
|
102
|
+
ref.sourceDirectory === undefined
|
|
103
|
+
? `${ref.name}\t${ref.pipelineId}`
|
|
104
|
+
: `${ref.name}\t${ref.pipelineId}\t${ref.sourceDirectory}`,
|
|
105
|
+
);
|
|
106
|
+
if (page.pagination.hasMore) {
|
|
107
|
+
lines.push(
|
|
108
|
+
`Showing ${live.length} of ${page.pagination.total} pipelines. Raise --limit to see more.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { outcome: "reported", output: lines.join("\n") };
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
const collectLivePipelineRefs = async (
|
package/src/cli/skill.ts
CHANGED
|
@@ -191,12 +191,23 @@ export const searchSkills = (
|
|
|
191
191
|
skills: readonly SkillDescriptor[],
|
|
192
192
|
query: string,
|
|
193
193
|
): SkillDescriptor[] => {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
194
|
+
const needles = query.trim().toLowerCase().split(/\s+/u).filter(Boolean);
|
|
195
|
+
if (needles.length === 0) return [];
|
|
196
|
+
|
|
197
|
+
return skills
|
|
198
|
+
.map((skill, index) => {
|
|
199
|
+
const searchable = [skill.id, skill.name, skill.description, ...skill.tags]
|
|
200
|
+
.join("\n")
|
|
201
|
+
.toLowerCase();
|
|
202
|
+
return {
|
|
203
|
+
skill,
|
|
204
|
+
index,
|
|
205
|
+
score: needles.filter((needle) => searchable.includes(needle)).length,
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.filter(({ score }) => score > 0)
|
|
209
|
+
.toSorted((left, right) => right.score - left.score || left.index - right.index)
|
|
210
|
+
.map(({ skill }) => skill);
|
|
200
211
|
};
|
|
201
212
|
|
|
202
213
|
export async function skillStatus(
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { AuthoredCodeNode } from "../authoring";
|
|
6
|
+
|
|
7
|
+
export type RunCodeNodeOptions = {
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
pythonExecutable?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class CodeNodeExecutionError extends Error {
|
|
14
|
+
readonly exitCode: number | null;
|
|
15
|
+
readonly stderr: string;
|
|
16
|
+
|
|
17
|
+
constructor(message: string, exitCode: number | null, stderr: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "CodeNodeExecutionError";
|
|
20
|
+
this.exitCode = exitCode;
|
|
21
|
+
this.stderr = stderr;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CodeNodeInfrastructureError extends Error {
|
|
26
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
27
|
+
super(message, options);
|
|
28
|
+
this.name = "CodeNodeInfrastructureError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const collectOutput = async (stream: NodeJS.ReadableStream): Promise<string> => {
|
|
33
|
+
const chunks: Buffer[] = [];
|
|
34
|
+
for await (const chunk of stream) {
|
|
35
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
36
|
+
}
|
|
37
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const staticEnvironment = (node: AuthoredCodeNode): Record<string, string> =>
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
(node.settings.envVars ?? [])
|
|
43
|
+
.filter((entry) => entry.type === "static")
|
|
44
|
+
.map((entry) => [entry.name, entry.value]),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const decodeCodeNodeOutput = (
|
|
48
|
+
nodeId: string,
|
|
49
|
+
serialized: string,
|
|
50
|
+
exitCode: number | null,
|
|
51
|
+
stderr: string,
|
|
52
|
+
): Record<string, unknown> => {
|
|
53
|
+
let output: unknown;
|
|
54
|
+
try {
|
|
55
|
+
output = JSON.parse(serialized);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new CodeNodeExecutionError(
|
|
58
|
+
`Code node ${nodeId} returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
59
|
+
exitCode,
|
|
60
|
+
stderr,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (output === null || typeof output !== "object" || Array.isArray(output)) {
|
|
64
|
+
throw new CodeNodeExecutionError(
|
|
65
|
+
`Code node ${nodeId} returned a non-object value`,
|
|
66
|
+
exitCode,
|
|
67
|
+
stderr,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return output as Record<string, unknown>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const runCodeNode = async (
|
|
74
|
+
node: AuthoredCodeNode,
|
|
75
|
+
inputs: Record<string, unknown>,
|
|
76
|
+
{ env = {}, pythonExecutable = "python3", timeoutMs = 10_000 }: RunCodeNodeOptions = {},
|
|
77
|
+
): Promise<Record<string, unknown>> => {
|
|
78
|
+
const directory = await mkdtemp(join(tmpdir(), "toloka-code-node-"));
|
|
79
|
+
const sourcePath = join(directory, "node.py");
|
|
80
|
+
const mainTypesPath = join(directory, "main_types.py");
|
|
81
|
+
const runnerPath = join(directory, "runner.py");
|
|
82
|
+
const outputPath = join(directory, "output.json");
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
writeFile(sourcePath, node.settings.code.code, "utf8"),
|
|
87
|
+
writeFile(
|
|
88
|
+
mainTypesPath,
|
|
89
|
+
"from typing import Any\n\nInputs = dict[str, Any]\nOutputs = dict[str, Any]\n",
|
|
90
|
+
"utf8",
|
|
91
|
+
),
|
|
92
|
+
writeFile(
|
|
93
|
+
runnerPath,
|
|
94
|
+
`import asyncio\nimport json\nimport sys\n\nfrom node import main\n\nasync def run():\n result = await main(json.load(sys.stdin))\n with open(sys.argv[1], "w", encoding="utf-8") as output:\n json.dump(result, output)\n\nasyncio.run(run())\n`,
|
|
95
|
+
"utf8",
|
|
96
|
+
),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const child = spawn(pythonExecutable, [runnerPath, outputPath], {
|
|
100
|
+
cwd: directory,
|
|
101
|
+
env: { ...process.env, ...staticEnvironment(node), ...env },
|
|
102
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
103
|
+
});
|
|
104
|
+
const {
|
|
105
|
+
promise: closed,
|
|
106
|
+
resolve: resolveClose,
|
|
107
|
+
reject: rejectClose,
|
|
108
|
+
} = Promise.withResolvers<number | null>();
|
|
109
|
+
child.once("close", resolveClose);
|
|
110
|
+
child.once("error", (error) =>
|
|
111
|
+
rejectClose(
|
|
112
|
+
new CodeNodeInfrastructureError(
|
|
113
|
+
`Unable to start Python executable ${JSON.stringify(pythonExecutable)}`,
|
|
114
|
+
{ cause: error },
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
let timedOut = false;
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
timedOut = true;
|
|
121
|
+
child.kill("SIGKILL");
|
|
122
|
+
}, timeoutMs);
|
|
123
|
+
child.stdin.end(JSON.stringify(inputs));
|
|
124
|
+
|
|
125
|
+
const [exitCode, , stderr] = await Promise.all([
|
|
126
|
+
closed,
|
|
127
|
+
collectOutput(child.stdout),
|
|
128
|
+
collectOutput(child.stderr),
|
|
129
|
+
]).finally(() => clearTimeout(timeout));
|
|
130
|
+
|
|
131
|
+
if (timedOut) {
|
|
132
|
+
throw new CodeNodeExecutionError(
|
|
133
|
+
`Code node ${node.id} exceeded the ${timeoutMs}ms timeout`,
|
|
134
|
+
exitCode,
|
|
135
|
+
stderr,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
throw new CodeNodeExecutionError(
|
|
140
|
+
`Code node ${node.id} exited with code ${exitCode ?? "unknown"}`,
|
|
141
|
+
exitCode,
|
|
142
|
+
stderr,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let serialized: string;
|
|
147
|
+
try {
|
|
148
|
+
serialized = await readFile(outputPath, "utf8");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new CodeNodeExecutionError(
|
|
151
|
+
`Code node ${node.id} did not produce an output file: ${error instanceof Error ? error.message : String(error)}`,
|
|
152
|
+
exitCode,
|
|
153
|
+
stderr,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return decodeCodeNodeOutput(node.id, serialized, exitCode, stderr);
|
|
157
|
+
} finally {
|
|
158
|
+
await rm(directory, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { CodeNodeExecutionError, CodeNodeInfrastructureError, runCodeNode } from "./code-node";
|
|
2
|
+
export type { RunCodeNodeOptions } from "./code-node";
|
|
3
|
+
export { expect, test } from "./task-ui";
|
|
4
|
+
export { TaskUiInfrastructureError, TaskUiValidationError } from "./types";
|
|
5
|
+
export type {
|
|
6
|
+
RenderTaskUiOptions,
|
|
7
|
+
TaskUiComponentDescription,
|
|
8
|
+
TaskUiCoverage,
|
|
9
|
+
TaskUiEntityError,
|
|
10
|
+
TaskUiFixture,
|
|
11
|
+
TaskUiFormError,
|
|
12
|
+
TaskUiNode,
|
|
13
|
+
TaskUiSession,
|
|
14
|
+
TypedTestOptions,
|
|
15
|
+
} from "./types";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
import { chromium, type Browser, type Page } from "playwright";
|
|
3
|
+
import {
|
|
4
|
+
TaskUiInfrastructureError,
|
|
5
|
+
type TaskUiComponentDescription,
|
|
6
|
+
type TbxBrowserRequest,
|
|
7
|
+
type TbxTemplate,
|
|
8
|
+
type TbxWindowApi,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_TBX_LAUNCHER_URL =
|
|
12
|
+
"https://tlkfrontprodpublic.blob.core.windows.net/template-builder-production/tb-jsx.agents/index.html?postprocess=true";
|
|
13
|
+
|
|
14
|
+
export function evaluateWithApi(
|
|
15
|
+
page: Page,
|
|
16
|
+
request: Extract<TbxBrowserRequest, { operation: "setConfig" }>,
|
|
17
|
+
): Promise<string>;
|
|
18
|
+
export function evaluateWithApi(
|
|
19
|
+
page: Page,
|
|
20
|
+
request: Extract<TbxBrowserRequest, { operation: "getComponentTree" }>,
|
|
21
|
+
): Promise<string>;
|
|
22
|
+
export function evaluateWithApi(
|
|
23
|
+
page: Page,
|
|
24
|
+
request: Extract<TbxBrowserRequest, { operation: "getComponentState" }>,
|
|
25
|
+
): Promise<Record<string, unknown>>;
|
|
26
|
+
export function evaluateWithApi(
|
|
27
|
+
page: Page,
|
|
28
|
+
request: Extract<TbxBrowserRequest, { operation: "getComponentValue" }>,
|
|
29
|
+
): Promise<unknown>;
|
|
30
|
+
export function evaluateWithApi(
|
|
31
|
+
page: Page,
|
|
32
|
+
request: Extract<TbxBrowserRequest, { operation: "setComponentValue" }>,
|
|
33
|
+
): Promise<void>;
|
|
34
|
+
export function evaluateWithApi(
|
|
35
|
+
page: Page,
|
|
36
|
+
request: Extract<TbxBrowserRequest, { operation: "getValidationErrors" }>,
|
|
37
|
+
): Promise<Array<{ componentId: string; error: string }>>;
|
|
38
|
+
export function evaluateWithApi(
|
|
39
|
+
page: Page,
|
|
40
|
+
request: Extract<TbxBrowserRequest, { operation: "getComponentsDescriptions" }>,
|
|
41
|
+
): Promise<TaskUiComponentDescription[]>;
|
|
42
|
+
export function evaluateWithApi(
|
|
43
|
+
page: Page,
|
|
44
|
+
request: Extract<TbxBrowserRequest, { operation: "getData" }>,
|
|
45
|
+
): Promise<Record<string, unknown> | undefined>;
|
|
46
|
+
export async function evaluateWithApi(page: Page, request: TbxBrowserRequest): Promise<unknown> {
|
|
47
|
+
assert(!page.isClosed(), "Task UI browser page is closed");
|
|
48
|
+
|
|
49
|
+
return page.evaluate(async (input) => {
|
|
50
|
+
const api: TbxWindowApi | undefined = Reflect.get(globalThis, "LAUNCHER_EMBED_API");
|
|
51
|
+
if (api === undefined) throw new Error("Task UI launcher API is unavailable");
|
|
52
|
+
|
|
53
|
+
switch (input.operation) {
|
|
54
|
+
case "setConfig":
|
|
55
|
+
return api.agents.setConfig(input.config);
|
|
56
|
+
case "getComponentTree":
|
|
57
|
+
return api.agents.getComponentTree("xml");
|
|
58
|
+
case "getComponentState":
|
|
59
|
+
return api.agents.getComponentState(input.componentId);
|
|
60
|
+
case "getComponentValue":
|
|
61
|
+
return api.agents.getComponentValue(input.componentId);
|
|
62
|
+
case "setComponentValue":
|
|
63
|
+
return api.agents.setComponentValue(input.componentId, input.value);
|
|
64
|
+
case "getValidationErrors":
|
|
65
|
+
return api.agents.getValidationErrors();
|
|
66
|
+
case "getComponentsDescriptions":
|
|
67
|
+
return api.agents.getComponentsDescriptions();
|
|
68
|
+
case "getData":
|
|
69
|
+
return api.getData();
|
|
70
|
+
}
|
|
71
|
+
}, request);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const selectComponentDocumentation = (
|
|
75
|
+
descriptions: TaskUiComponentDescription[],
|
|
76
|
+
names?: string[],
|
|
77
|
+
): Record<string, TaskUiComponentDescription> => {
|
|
78
|
+
const selected =
|
|
79
|
+
names === undefined
|
|
80
|
+
? descriptions
|
|
81
|
+
: descriptions.filter((description) => names.includes(description.name));
|
|
82
|
+
const byName = Object.fromEntries(selected.map((description) => [description.name, description]));
|
|
83
|
+
if (names !== undefined) {
|
|
84
|
+
const missing = names.filter((name) => !(name in byName));
|
|
85
|
+
assert(missing.length === 0, `Unknown Task UI components: ${missing.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
return byName;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const readDataPath = (state: Record<string, unknown>): string | undefined => {
|
|
91
|
+
const path = state.dataPath;
|
|
92
|
+
return typeof path === "string" ? path : undefined;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const componentIds = (tree: string): string[] =>
|
|
96
|
+
[...tree.matchAll(/\bid="([^"]+)"/gu)].map((match) => match[1]!);
|
|
97
|
+
|
|
98
|
+
export const launchDefaultBrowser = async (): Promise<Browser> => {
|
|
99
|
+
try {
|
|
100
|
+
return await chromium.launch({ headless: true });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new TaskUiInfrastructureError(
|
|
103
|
+
"Local Task UI testing requires Playwright and Chromium. Run `bun add --dev playwright vitest` and `bunx playwright install chromium`.",
|
|
104
|
+
{ cause: error },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const waitForTaskUiApi = async (page: Page): Promise<void> => {
|
|
110
|
+
await page.waitForFunction(
|
|
111
|
+
() => {
|
|
112
|
+
const api: TbxWindowApi | undefined = Reflect.get(globalThis, "LAUNCHER_EMBED_API");
|
|
113
|
+
return api?.agents !== undefined;
|
|
114
|
+
},
|
|
115
|
+
undefined,
|
|
116
|
+
{ timeout: 15_000 },
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type { Browser, Page, TbxTemplate };
|