@toloka-platform/sdk 0.1.0-preview.5 → 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.
Files changed (127) hide show
  1. package/README.md +7 -11
  2. package/dist/cli/toloka.js +211 -125
  3. package/dist/cli/toloka.js.jsc +0 -0
  4. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  5. package/dist/cli/toloka.js.sha256 +1 -1
  6. package/docs/authoring.md +17 -4
  7. package/docs/cli-v2.md +3 -3
  8. package/docs/cli.md +55 -17
  9. package/docs/datasets.md +8 -9
  10. package/docs/getting-started.md +7 -12
  11. package/docs/limitations.md +10 -13
  12. package/docs/testing-examples.md +248 -0
  13. package/docs/testing.md +115 -0
  14. package/docs/troubleshooting.md +0 -11
  15. package/examples/face-image-collection/README.md +7 -1
  16. package/examples/face-image-collection/fields.ts +20 -8
  17. package/examples/face-image-collection/nodes/annotate-faces/node.ts +1 -9
  18. package/examples/face-image-collection/nodes/order-faces/node.ts +1 -1
  19. package/examples/face-image-collection/pipeline.browser.integration.spec.ts +78 -0
  20. package/examples/video-pair-rating/README.md +14 -6
  21. package/examples/video-pair-rating/nodes/adjudicate/node.ts +1 -3
  22. package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +1 -3
  23. package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +1 -1
  24. package/examples/video-pair-rating/nodes/rate_pair/node.ts +1 -1
  25. package/examples/video-pair-rating/pipeline.browser.integration.spec.ts +267 -0
  26. package/openapi/gen/api.ts +819 -898
  27. package/openapi/gen/models/baseModel.ts +9 -0
  28. package/openapi/gen/models/baseModelId.ts +2 -0
  29. package/openapi/gen/models/baseModelIdProperty.ts +2 -0
  30. package/openapi/gen/models/createDatasetExportBody.ts +3 -3
  31. package/openapi/gen/models/createDatasetExportBodyOneOf.ts +6 -0
  32. package/openapi/gen/models/createDatasetExportBodyOneOfFormat.ts +2 -0
  33. package/openapi/gen/models/createDatasetExportBodyOneOfThree.ts +10 -0
  34. package/openapi/gen/models/createDatasetExportBodyOneOfThreeContract.ts +2 -0
  35. package/openapi/gen/models/createDatasetExportBodyOneOfThreeFormat.ts +2 -0
  36. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilter.ts +9 -0
  37. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItem.ts +6 -0
  38. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  39. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  40. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  41. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItem.ts +12 -0
  42. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemOperator.ts +2 -0
  43. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
  44. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  45. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  46. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemValue.ts +2 -0
  47. package/openapi/gen/models/createProjectModelBody.ts +8 -0
  48. package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
  49. package/openapi/gen/models/datasetExportReport.ts +9 -0
  50. package/openapi/gen/models/datasetExportReportErrorsItem.ts +5 -0
  51. package/openapi/gen/models/datasetExportReportTokens.ts +7 -0
  52. package/openapi/gen/models/exportJob.ts +2 -0
  53. package/openapi/gen/models/exportJobReport.ts +3 -0
  54. package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  55. package/openapi/gen/models/index.ts +64 -0
  56. package/openapi/gen/models/listBaseModels200.ts +5 -0
  57. package/openapi/gen/models/listModelVersions200.ts +7 -0
  58. package/openapi/gen/models/listModelVersionsParams.ts +5 -0
  59. package/openapi/gen/models/listProjectModels200.ts +7 -0
  60. package/openapi/gen/models/listProjectModelsParams.ts +5 -0
  61. package/openapi/gen/models/model.ts +16 -0
  62. package/openapi/gen/models/modelArchivedAt.ts +2 -0
  63. package/openapi/gen/models/modelDescription.ts +2 -0
  64. package/openapi/gen/models/modelId.ts +2 -0
  65. package/openapi/gen/models/modelVersion.ts +9 -0
  66. package/openapi/gen/models/modelVersionId.ts +2 -0
  67. package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  68. package/openapi/gen/models/sampleDatasetItemsBody.ts +4 -0
  69. package/openapi/gen/models/sampleDatasetItemsBodyAnyOf.ts +8 -0
  70. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilter.ts +9 -0
  71. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItem.ts +6 -0
  72. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  73. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  74. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  75. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItem.ts +12 -0
  76. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemOperator.ts +2 -0
  77. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItem.ts +4 -0
  78. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  79. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  80. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemValue.ts +2 -0
  81. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethree.ts +8 -0
  82. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilter.ts +9 -0
  83. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItem.ts +6 -0
  84. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  85. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  86. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  87. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItem.ts +12 -0
  88. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemOperator.ts +2 -0
  89. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
  90. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  91. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  92. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemValue.ts +2 -0
  93. package/openapi/gen/models/sampleJob.ts +13 -0
  94. package/openapi/gen/models/sampleJobStatus.ts +2 -0
  95. package/openapi/gen/models/sampleResult.ts +5 -0
  96. package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  97. package/package.json +23 -2
  98. package/skills/catalog.json +14 -6
  99. package/skills/toloka-build/SKILL.md +34 -12
  100. package/skills/toloka-build/references/node-semantics.md +27 -24
  101. package/skills/toloka-build/references/workflow-and-cli.md +67 -27
  102. package/skills/toloka-report-issue/references/report-template.md +1 -1
  103. package/src/checks/ast.ts +32 -0
  104. package/src/checks/index.ts +14 -0
  105. package/src/checks/postprocess-collisions.ts +145 -0
  106. package/src/checks/task-ui-declarations.generated.ts +19 -0
  107. package/src/checks/task-ui-typecheck.ts +345 -0
  108. package/src/checks/typescript-vfs.d.ts +26 -0
  109. package/src/checks/typescript-vfs.js +5 -0
  110. package/src/cli/analytics.ts +1 -0
  111. package/src/cli/check.ts +977 -0
  112. package/src/cli/data.ts +148 -25
  113. package/src/cli/models.ts +188 -0
  114. package/src/cli/program.ts +246 -20
  115. package/src/cli/project-context.ts +14 -4
  116. package/src/cli/runs.ts +21 -11
  117. package/src/cli/skill.ts +17 -6
  118. package/src/client.ts +109 -35
  119. package/src/errors.ts +25 -10
  120. package/src/testing/code-node.ts +160 -0
  121. package/src/testing/index.ts +15 -0
  122. package/src/testing/task-ui-browser.ts +120 -0
  123. package/src/testing/task-ui-validation.ts +135 -0
  124. package/src/testing/task-ui.ts +557 -0
  125. package/src/testing/test-evidence.ts +170 -0
  126. package/src/testing/types.ts +141 -0
  127. package/src/transport.ts +16 -13
@@ -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";
@@ -33,6 +42,16 @@ import {
33
42
  projectStatus,
34
43
  validateProject,
35
44
  } from "./project";
45
+ import {
46
+ archiveModel,
47
+ createModel,
48
+ getBaseModel,
49
+ getModel,
50
+ listBaseModels,
51
+ listModels,
52
+ listModelVersions,
53
+ parseBaseModelId,
54
+ } from "./models";
36
55
  import {
37
56
  MISSING_PROJECT_ID_MESSAGE,
38
57
  addProject,
@@ -73,6 +92,7 @@ export type ProgramContext = {
73
92
  readonly environment: TolokaEnvironment;
74
93
  readonly io: CliIo;
75
94
  readonly injectedClient?: PotatoClient;
95
+ readonly runChecks?: typeof checkProject;
76
96
  readonly analytics?: CliAnalytics;
77
97
  };
78
98
 
@@ -192,6 +212,29 @@ export type CreatedProgram = {
192
212
  export function createProgram(context: ProgramContext): CreatedProgram {
193
213
  const program = new Command("toloka");
194
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
+ };
195
238
  let warnedAboutStaleSkills = false;
196
239
 
197
240
  program
@@ -314,8 +357,9 @@ export function createProgram(context: ProgramContext): CreatedProgram {
314
357
  .description("select and manage the current platform project");
315
358
  project
316
359
  .command("list")
360
+ .option("--limit <n>", "maximum projects to list", positiveInteger)
317
361
  .description("list remote projects this key can see")
318
- .action(async () => {
362
+ .action(async (options: { limit?: number }) => {
319
363
  const flags = globals();
320
364
  const { client, stored, state } = await resolveContext(flags, context);
321
365
  const selected = resolveSelectedProject(flags, context, stored, state);
@@ -323,6 +367,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
323
367
  client,
324
368
  io: context.io,
325
369
  ...(selected === undefined ? {} : { context: selected }),
370
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
326
371
  });
327
372
  });
328
373
  project
@@ -430,9 +475,12 @@ export function createProgram(context: ProgramContext): CreatedProgram {
430
475
  .action(async () => {
431
476
  const flags = globals();
432
477
  const sourceRoot = resolveInitRoot(flags);
433
- const stored = await readCommandCredentials(flags, context);
434
478
  const state = await readTolokaState(sourceRoot);
435
- const selected = resolveSelectedProject(flags, context, stored, state);
479
+ const selected = resolveProjectContext({
480
+ flags,
481
+ environment: context.environment,
482
+ ...(state === undefined ? {} : { state }),
483
+ });
436
484
  const result = await initProject(
437
485
  sourceRoot,
438
486
  selected === undefined ? {} : { projectId: selected.id },
@@ -490,13 +538,89 @@ export function createProgram(context: ProgramContext): CreatedProgram {
490
538
  if (!result.valid) exitCode.value = 1;
491
539
  });
492
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
+
493
601
  program
494
602
  .command("apply")
495
- .description("make the platform match source")
603
+ .description("check source, then make the platform match it")
496
604
  .action(async () => {
497
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
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);
498
621
  const result = await applyProject(client, {
499
622
  sourceRoot,
623
+ source: checked.source,
500
624
  ...(projectId === undefined ? {} : { projectId }),
501
625
  });
502
626
  context.io.stdout(result.output);
@@ -533,6 +657,94 @@ export function createProgram(context: ProgramContext): CreatedProgram {
533
657
  }
534
658
  });
535
659
 
660
+ const baseModel = program
661
+ .command("base-model")
662
+ .description("inspect the global base model catalog");
663
+ baseModel
664
+ .command("list")
665
+ .option("--json", "print JSON", false)
666
+ .description("list base models")
667
+ .action(async (options: { json: boolean }) => {
668
+ const { client } = await resolveContext(globals(), context);
669
+ await listBaseModels(client, { json: options.json }, context.io);
670
+ });
671
+ baseModel
672
+ .command("get")
673
+ .argument("<id>", "base model ID")
674
+ .option("--json", "print JSON", false)
675
+ .description("show one base model")
676
+ .action(async (id: string, options: { json: boolean }) => {
677
+ const { client } = await resolveContext(globals(), context);
678
+ await getBaseModel(client, { id: parseBaseModelId(id), json: options.json }, context.io);
679
+ });
680
+
681
+ const model = program
682
+ .command("model")
683
+ .description("manage project Models and immutable versions");
684
+ model
685
+ .command("list")
686
+ .option("--json", "print JSON", false)
687
+ .description("list Models owned by the selected project")
688
+ .action(async (options: { json: boolean }) => {
689
+ const { client, projectId } = await resolveContext(globals(), context);
690
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
691
+ await listModels(client, { projectId, json: options.json }, context.io);
692
+ });
693
+ model
694
+ .command("get")
695
+ .argument("<name-or-id>", "project Model name or ID")
696
+ .option("--json", "print JSON", false)
697
+ .description("show one project Model")
698
+ .action(async (nameOrId: string, options: { json: boolean }) => {
699
+ const { client, projectId } = await resolveContext(globals(), context);
700
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
701
+ await getModel(client, { projectId, nameOrId, json: options.json }, context.io);
702
+ });
703
+ model
704
+ .command("versions")
705
+ .argument("<name-or-id>", "project Model name or ID")
706
+ .option("--json", "print JSON", false)
707
+ .description("list immutable versions of a model")
708
+ .action(async (nameOrId: string, options: { json: boolean }) => {
709
+ const { client, projectId } = await resolveContext(globals(), context);
710
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
711
+ await listModelVersions(client, { projectId, nameOrId, json: options.json }, context.io);
712
+ });
713
+ model
714
+ .command("create")
715
+ .argument("<name>", "new Model name")
716
+ .requiredOption("--base <id>", "base model ID")
717
+ .option("--description <text>", "Model description")
718
+ .option("--json", "print JSON", false)
719
+ .description("create project Model metadata without a Model Version")
720
+ .action(
721
+ async (name: string, options: { base: string; description?: string; json: boolean }) => {
722
+ const { client, projectId } = await resolveContext(globals(), context);
723
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
724
+ await createModel(
725
+ client,
726
+ {
727
+ projectId,
728
+ name,
729
+ description: options.description,
730
+ baseModelId: parseBaseModelId(options.base),
731
+ json: options.json,
732
+ },
733
+ context.io,
734
+ );
735
+ },
736
+ );
737
+ model
738
+ .command("archive")
739
+ .option("--json", "print JSON", false)
740
+ .argument("<name-or-id>", "project Model name or ID")
741
+ .description("archive a project Model")
742
+ .action(async (nameOrId: string, options: { json: boolean }) => {
743
+ const { client, projectId } = await resolveContext(globals(), context);
744
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
745
+ await archiveModel(client, { projectId, nameOrId, json: options.json }, context.io);
746
+ });
747
+
536
748
  const dataset = program
537
749
  .command("dataset")
538
750
  .description("inspect and move dataset catalog and rows");
@@ -580,7 +792,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
580
792
  dataset
581
793
  .command("upload")
582
794
  .argument("<name>", "dataset name or id")
583
- .argument("<file>", "JSONL file, one JSON object per line")
795
+ .argument("<file>", "JSON array, JSONL, or CSV file")
584
796
  .description("append rows to a dataset")
585
797
  .action(async (name: string, file: string) => {
586
798
  const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
@@ -601,11 +813,17 @@ export function createProgram(context: ProgramContext): CreatedProgram {
601
813
  .description("operate on a live pipeline in the selected project");
602
814
  pipeline
603
815
  .command("list")
816
+ .option("--limit <n>", "maximum pipelines to list", positiveInteger)
604
817
  .description("list live pipelines in the selected project")
605
- .action(async () => {
818
+ .action(async (options: { limit?: number }) => {
606
819
  const { client, projectId, state } = await resolveContext(globals(), context);
607
820
  assert(projectId, MISSING_PROJECT_ID_MESSAGE);
608
- const result = await listLivePipelines({ client, projectId, state });
821
+ const result = await listLivePipelines({
822
+ client,
823
+ projectId,
824
+ state,
825
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
826
+ });
609
827
  context.io.stdout(result.output);
610
828
  });
611
829
  pipeline
@@ -907,27 +1125,35 @@ export async function runTolokaCli(
907
1125
  environment: TolokaEnvironment = process.env,
908
1126
  io: CliIo = terminalIo(),
909
1127
  injectedClient?: PotatoClient,
910
- analytics: CliAnalytics = createCliAnalytics({ environment }),
1128
+ analytics?: CliAnalytics,
1129
+ runChecks?: typeof checkProject,
911
1130
  ): Promise<number> {
1131
+ const activeAnalytics =
1132
+ environment[CHECK_RESULT_FILE_ENV] === undefined
1133
+ ? (analytics ?? createCliAnalytics({ environment }))
1134
+ : undefined;
912
1135
  const { program, exitCode } = createProgram({
913
1136
  environment,
914
1137
  io,
915
- analytics,
1138
+ ...(activeAnalytics === undefined ? {} : { analytics: activeAnalytics }),
916
1139
  ...(injectedClient === undefined ? {} : { injectedClient }),
1140
+ ...(runChecks === undefined ? {} : { runChecks }),
917
1141
  });
918
1142
 
919
1143
  const startedAt = Date.now();
920
1144
  const result = await runProgram(program, exitCode, argv, io);
921
- try {
922
- analytics.captureCommand({
923
- command: commandNameFromArgv(argv),
924
- durationMs: Date.now() - startedAt,
925
- exitCode: result.code,
926
- outcome: result.outcome,
927
- ...(result.error === undefined ? {} : { error: result.error }),
928
- });
929
- await analytics.shutdown();
930
- } catch {}
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
+ }
931
1157
 
932
1158
  return result.code;
933
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 listAll: (params?: object) => AsyncIterable<ProjectRecord>;
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 lines: string[] = [];
230
- for await (const project of options.client.projects.listAll()) {
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
- lines.push(`${mark}\t${project.id}\t${project.name}${alias}`);
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 live = await collectLivePipelineRefs(options.client, options.projectId, options.state);
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
- return {
93
- outcome: "reported",
94
- output: live
95
- .map((ref) =>
96
- ref.sourceDirectory === undefined
97
- ? `${ref.name}\t${ref.pipelineId}`
98
- : `${ref.name}\t${ref.pipelineId}\t${ref.sourceDirectory}`,
99
- )
100
- .join("\n"),
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 needle = query.trim().toLowerCase();
195
- return skills.filter((skill) =>
196
- [skill.id, skill.name, skill.description, ...skill.tags].some((field) =>
197
- field.toLowerCase().includes(needle),
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(