@toloka-platform/sdk 0.1.0-preview.6 → 0.1.0-preview.8

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 (120) hide show
  1. package/README.md +10 -15
  2. package/dist/cli/toloka.js +209 -121
  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 +20 -9
  7. package/docs/cli-v2.md +3 -3
  8. package/docs/cli.md +44 -16
  9. package/docs/getting-started.md +7 -12
  10. package/docs/limitations.md +6 -6
  11. package/docs/testing-examples.md +248 -0
  12. package/docs/testing.md +115 -0
  13. package/examples/face-image-collection/README.md +7 -1
  14. package/examples/face-image-collection/fields.ts +20 -8
  15. package/examples/face-image-collection/nodes/annotate-faces/node.ts +1 -9
  16. package/examples/face-image-collection/nodes/order-faces/node.ts +3 -5
  17. package/examples/face-image-collection/pipeline.browser.integration.spec.ts +78 -0
  18. package/examples/video-pair-rating/README.md +14 -6
  19. package/examples/video-pair-rating/nodes/adjudicate/node.ts +1 -3
  20. package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +1 -3
  21. package/examples/video-pair-rating/nodes/adjudication_match/node.ts +15 -17
  22. package/examples/video-pair-rating/nodes/assemble/node.ts +25 -27
  23. package/examples/video-pair-rating/nodes/cascade/node.ts +24 -26
  24. package/examples/video-pair-rating/nodes/finalize_overall/node.ts +17 -19
  25. package/examples/video-pair-rating/nodes/merge_adjudication/node.ts +13 -15
  26. package/examples/video-pair-rating/nodes/merge_op_adjudication/node.ts +12 -14
  27. package/examples/video-pair-rating/nodes/op_quorum_resolve/node.ts +13 -15
  28. package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +1 -1
  29. package/examples/video-pair-rating/nodes/rate_pair/node.ts +1 -1
  30. package/examples/video-pair-rating/nodes/route_adjudication/node.ts +1 -3
  31. package/examples/video-pair-rating/nodes/route_op_adjudication/node.ts +5 -7
  32. package/examples/video-pair-rating/nodes/route_overall/node.ts +1 -3
  33. package/examples/video-pair-rating/nodes/shuffle_ab/node.ts +11 -13
  34. package/examples/video-pair-rating/pipeline.browser.integration.spec.ts +267 -0
  35. package/openapi/gen/api.ts +48 -11
  36. package/openapi/gen/models/createDatasetExportBody.ts +3 -3
  37. package/openapi/gen/models/createDatasetExportBodyOneOf.ts +6 -0
  38. package/openapi/gen/models/createDatasetExportBodyOneOfFormat.ts +2 -0
  39. package/openapi/gen/models/createDatasetExportBodyOneOfThree.ts +10 -0
  40. package/openapi/gen/models/createDatasetExportBodyOneOfThreeContract.ts +2 -0
  41. package/openapi/gen/models/createDatasetExportBodyOneOfThreeFormat.ts +2 -0
  42. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilter.ts +9 -0
  43. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItem.ts +6 -0
  44. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  45. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  46. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  47. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItem.ts +12 -0
  48. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemOperator.ts +2 -0
  49. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
  50. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  51. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  52. package/openapi/gen/models/createDatasetExportBodyOneOfThreeItemFilterFieldValuesItemValue.ts +2 -0
  53. package/openapi/gen/models/datasetExportReport.ts +9 -0
  54. package/openapi/gen/models/datasetExportReportErrorsItem.ts +5 -0
  55. package/openapi/gen/models/datasetExportReportTokens.ts +7 -0
  56. package/openapi/gen/models/datasetItem.ts +0 -2
  57. package/openapi/gen/models/exportJob.ts +2 -0
  58. package/openapi/gen/models/exportJobReport.ts +3 -0
  59. package/openapi/gen/models/index.ts +48 -1
  60. package/openapi/gen/models/sampleDatasetItemsBody.ts +4 -0
  61. package/openapi/gen/models/sampleDatasetItemsBodyAnyOf.ts +8 -0
  62. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilter.ts +9 -0
  63. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItem.ts +6 -0
  64. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  65. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  66. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  67. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItem.ts +12 -0
  68. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemOperator.ts +2 -0
  69. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItem.ts +4 -0
  70. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  71. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  72. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfItemFilterFieldValuesItemValue.ts +2 -0
  73. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethree.ts +8 -0
  74. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilter.ts +9 -0
  75. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItem.ts +6 -0
  76. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItem.ts +4 -0
  77. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOf.ts +5 -0
  78. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldSourcesItemSourcesItemOneOfTwo.ts +5 -0
  79. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItem.ts +12 -0
  80. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemOperator.ts +2 -0
  81. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItem.ts +4 -0
  82. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOf.ts +5 -0
  83. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemSourcesItemOneOfTwo.ts +5 -0
  84. package/openapi/gen/models/sampleDatasetItemsBodyAnyOfOnethreeItemFilterFieldValuesItemValue.ts +2 -0
  85. package/openapi/gen/models/sampleJob.ts +13 -0
  86. package/openapi/gen/models/sampleJobStatus.ts +2 -0
  87. package/openapi/gen/models/sampleResult.ts +5 -0
  88. package/package.json +23 -2
  89. package/skills/catalog.json +14 -6
  90. package/skills/toloka-build/SKILL.md +35 -12
  91. package/skills/toloka-build/references/node-semantics.md +32 -29
  92. package/skills/toloka-build/references/workflow-and-cli.md +61 -27
  93. package/skills/toloka-report-issue/references/report-template.md +1 -1
  94. package/src/authoring/index.ts +27 -26
  95. package/src/checks/ast.ts +32 -0
  96. package/src/checks/index.ts +14 -0
  97. package/src/checks/postprocess-collisions.ts +145 -0
  98. package/src/checks/task-ui-declarations.generated.ts +19 -0
  99. package/src/checks/task-ui-typecheck.ts +345 -0
  100. package/src/checks/typescript-vfs.d.ts +26 -0
  101. package/src/checks/typescript-vfs.js +5 -0
  102. package/src/cli/analytics.ts +9 -19
  103. package/src/cli/auto-update.ts +367 -0
  104. package/src/cli/boolean-flag.ts +13 -0
  105. package/src/cli/check.ts +977 -0
  106. package/src/cli/credentials.ts +3 -0
  107. package/src/cli/program.ts +184 -20
  108. package/src/cli/project-context.ts +14 -4
  109. package/src/cli/runs.ts +21 -11
  110. package/src/cli/settings.ts +93 -0
  111. package/src/cli/skill.ts +17 -6
  112. package/src/importer.ts +12 -16
  113. package/src/testing/code-node.ts +160 -0
  114. package/src/testing/index.ts +15 -0
  115. package/src/testing/task-ui-browser.ts +120 -0
  116. package/src/testing/task-ui-validation.ts +135 -0
  117. package/src/testing/task-ui.ts +557 -0
  118. package/src/testing/test-evidence.ts +170 -0
  119. package/src/testing/types.ts +141 -0
  120. package/openapi/gen/models/datasetItemDuplicateOfSubmissionId.ts +0 -2
@@ -52,6 +52,9 @@ export const credentialsPath = (environment: CliEnvironment = process.env): stri
52
52
  export const legacyCredentialsPath = (environment: CliEnvironment = process.env): string =>
53
53
  join(home(environment), ".toloka", "credentials.json");
54
54
 
55
+ export const configDirectory = (environment: CliEnvironment = process.env): string =>
56
+ dirname(credentialsPath(environment));
57
+
55
58
  const removeTrailingSlashes = (value: string): string => {
56
59
  let end = value.length;
57
60
  while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
@@ -1,15 +1,26 @@
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";
12
21
  import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
22
+ import type { AutoUpdateHooks } from "./auto-update";
23
+ import { maybeAutoUpdate } from "./auto-update";
13
24
  import { PRODUCTION_BASE_URL, resolveInternalBaseUrl, resolveTolokaConfig } from "./config";
14
25
  import type { TolokaEnvironment } from "./config";
15
26
  import {
@@ -18,6 +29,7 @@ import {
18
29
  requireStoredCredentials,
19
30
  } from "./credentials";
20
31
  import type { StoredCredentials } from "./credentials";
32
+ import { getCliSetting, readCliSettings, setCliSetting } from "./settings";
21
33
  import {
22
34
  generateDatasetFieldDeclarations,
23
35
  listProjectDatasets,
@@ -83,6 +95,7 @@ export type ProgramContext = {
83
95
  readonly environment: TolokaEnvironment;
84
96
  readonly io: CliIo;
85
97
  readonly injectedClient?: PotatoClient;
98
+ readonly runChecks?: typeof checkProject;
86
99
  readonly analytics?: CliAnalytics;
87
100
  };
88
101
 
@@ -202,6 +215,29 @@ export type CreatedProgram = {
202
215
  export function createProgram(context: ProgramContext): CreatedProgram {
203
216
  const program = new Command("toloka");
204
217
  const exitCode = { value: 0 };
218
+ const runChecks = context.runChecks ?? checkProject;
219
+ const runChecksInterruptibly = async (
220
+ options: CheckProjectOptions,
221
+ runner: (options: CheckProjectOptions) => Promise<CheckProjectResult> = runChecks,
222
+ ): Promise<{ result: CheckProjectResult; interruptedExitCode?: number }> => {
223
+ const abortController = new AbortController();
224
+ let interruptedExitCode: number | undefined;
225
+ const interrupt = (exitCode: number) => {
226
+ interruptedExitCode = exitCode;
227
+ abortController.abort();
228
+ };
229
+ const interruptSigint = () => interrupt(130);
230
+ const interruptSigterm = () => interrupt(143);
231
+ process.on("SIGINT", interruptSigint);
232
+ process.on("SIGTERM", interruptSigterm);
233
+ try {
234
+ const result = await runner({ ...options, signal: abortController.signal });
235
+ return { result, ...(interruptedExitCode === undefined ? {} : { interruptedExitCode }) };
236
+ } finally {
237
+ process.off("SIGINT", interruptSigint);
238
+ process.off("SIGTERM", interruptSigterm);
239
+ }
240
+ };
205
241
  let warnedAboutStaleSkills = false;
206
242
 
207
243
  program
@@ -236,7 +272,8 @@ export function createProgram(context: ProgramContext): CreatedProgram {
236
272
  commandNames.includes("help") ||
237
273
  commandNames.includes("report") ||
238
274
  commandNames.includes("url") ||
239
- commandNames.includes("version")
275
+ commandNames.includes("version") ||
276
+ commandNames.includes("config")
240
277
  )
241
278
  return;
242
279
  const sourceRoot = await resolveSourceRoot(globals(), context.io);
@@ -250,6 +287,29 @@ export function createProgram(context: ProgramContext): CreatedProgram {
250
287
  .description("print the current CLI version")
251
288
  .action(() => context.io.stdout(SDK_VERSION));
252
289
 
290
+ const config = program.command("config").description("read and write machine CLI settings");
291
+ config
292
+ .command("get")
293
+ .argument("[name]", "setting name; omit to print every setting")
294
+ .description("print a CLI setting, or every setting")
295
+ .action(async (name?: string) => {
296
+ const settings = await readCliSettings(context.environment);
297
+ context.io.stdout(getCliSetting(settings, name).output);
298
+ });
299
+ config
300
+ .command("set")
301
+ .argument("<name>", "setting name")
302
+ .argument("<value>", "setting value")
303
+ .description("write a CLI setting for this machine")
304
+ .action(async (name: string, value: string) => {
305
+ const result = await setCliSetting(context.environment, name, value);
306
+ context.io.stdout(result.output);
307
+ });
308
+ config.action(async () => {
309
+ const settings = await readCliSettings(context.environment);
310
+ context.io.stdout(getCliSetting(settings, undefined).output);
311
+ });
312
+
253
313
  program
254
314
  .command("login")
255
315
  .description("store an API key for this machine after verifying it")
@@ -324,8 +384,9 @@ export function createProgram(context: ProgramContext): CreatedProgram {
324
384
  .description("select and manage the current platform project");
325
385
  project
326
386
  .command("list")
387
+ .option("--limit <n>", "maximum projects to list", positiveInteger)
327
388
  .description("list remote projects this key can see")
328
- .action(async () => {
389
+ .action(async (options: { limit?: number }) => {
329
390
  const flags = globals();
330
391
  const { client, stored, state } = await resolveContext(flags, context);
331
392
  const selected = resolveSelectedProject(flags, context, stored, state);
@@ -333,6 +394,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
333
394
  client,
334
395
  io: context.io,
335
396
  ...(selected === undefined ? {} : { context: selected }),
397
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
336
398
  });
337
399
  });
338
400
  project
@@ -440,9 +502,12 @@ export function createProgram(context: ProgramContext): CreatedProgram {
440
502
  .action(async () => {
441
503
  const flags = globals();
442
504
  const sourceRoot = resolveInitRoot(flags);
443
- const stored = await readCommandCredentials(flags, context);
444
505
  const state = await readTolokaState(sourceRoot);
445
- const selected = resolveSelectedProject(flags, context, stored, state);
506
+ const selected = resolveProjectContext({
507
+ flags,
508
+ environment: context.environment,
509
+ ...(state === undefined ? {} : { state }),
510
+ });
446
511
  const result = await initProject(
447
512
  sourceRoot,
448
513
  selected === undefined ? {} : { projectId: selected.id },
@@ -500,13 +565,89 @@ export function createProgram(context: ProgramContext): CreatedProgram {
500
565
  if (!result.valid) exitCode.value = 1;
501
566
  });
502
567
 
568
+ program
569
+ .command("check")
570
+ .argument("[pipeline]", "pipeline source directory or name")
571
+ .option("--node <node>", "check one code or human-work node by name or id")
572
+ .option("--json", "print the structured check report", false)
573
+ .option("--watch", "rerun checks when project files change", false)
574
+ .description("run the complete local source, type, and test gate")
575
+ .action(
576
+ async (
577
+ pipeline: string | undefined,
578
+ options: { node?: string; json: boolean; watch: boolean },
579
+ ) => {
580
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
581
+ const checkOptions = {
582
+ environment: context.environment,
583
+ sourceRoot,
584
+ ...(pipeline === undefined ? {} : { pipeline }),
585
+ ...(options.node === undefined ? {} : { node: options.node }),
586
+ ...(options.json ? { json: true } : {}),
587
+ };
588
+ const writeCheckOutput = async (output: string): Promise<void> => {
589
+ const resultPath = context.environment[CHECK_RESULT_FILE_ENV];
590
+ if (resultPath === undefined) context.io.stdout(output);
591
+ else await writeFile(resultPath, output, "utf8");
592
+ };
593
+ const checkRunner =
594
+ context.runChecks === undefined &&
595
+ context.environment[CHECK_RESULT_FILE_ENV] === undefined
596
+ ? runFreshCheck
597
+ : runChecks;
598
+ if (options.watch) {
599
+ const abortController = new AbortController();
600
+ const stop = () => abortController.abort();
601
+ process.on("SIGINT", stop);
602
+ process.on("SIGTERM", stop);
603
+ try {
604
+ await watchProjectChecks(
605
+ checkOptions,
606
+ async (result) => {
607
+ await writeCheckOutput(result.output);
608
+ exitCode.value = result.exitCode;
609
+ },
610
+ undefined,
611
+ abortController.signal,
612
+ );
613
+ } finally {
614
+ process.off("SIGINT", stop);
615
+ process.off("SIGTERM", stop);
616
+ }
617
+ return;
618
+ }
619
+ const { result, interruptedExitCode } = await runChecksInterruptibly(
620
+ checkOptions,
621
+ checkRunner,
622
+ );
623
+ await writeCheckOutput(result.output);
624
+ exitCode.value = interruptedExitCode ?? result.exitCode;
625
+ },
626
+ );
627
+
503
628
  program
504
629
  .command("apply")
505
- .description("make the platform match source")
630
+ .description("check source, then make the platform match it")
506
631
  .action(async () => {
507
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
632
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
633
+ const { result: checked, interruptedExitCode } = await runChecksInterruptibly({
634
+ sourceRoot,
635
+ environment: context.environment,
636
+ });
637
+ context.io.stdout(checked.output);
638
+ if (interruptedExitCode !== undefined) {
639
+ exitCode.value = interruptedExitCode;
640
+ return;
641
+ }
642
+ if (checked.exitCode !== 0) {
643
+ exitCode.value = checked.exitCode;
644
+ return;
645
+ }
646
+ assert(checked.source, "Passing checks must return project source");
647
+ const { client, projectId } = await resolveContext(globals(), context);
508
648
  const result = await applyProject(client, {
509
649
  sourceRoot,
650
+ source: checked.source,
510
651
  ...(projectId === undefined ? {} : { projectId }),
511
652
  });
512
653
  context.io.stdout(result.output);
@@ -699,11 +840,17 @@ export function createProgram(context: ProgramContext): CreatedProgram {
699
840
  .description("operate on a live pipeline in the selected project");
700
841
  pipeline
701
842
  .command("list")
843
+ .option("--limit <n>", "maximum pipelines to list", positiveInteger)
702
844
  .description("list live pipelines in the selected project")
703
- .action(async () => {
845
+ .action(async (options: { limit?: number }) => {
704
846
  const { client, projectId, state } = await resolveContext(globals(), context);
705
847
  assert(projectId, MISSING_PROJECT_ID_MESSAGE);
706
- const result = await listLivePipelines({ client, projectId, state });
848
+ const result = await listLivePipelines({
849
+ client,
850
+ projectId,
851
+ state,
852
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
853
+ });
707
854
  context.io.stdout(result.output);
708
855
  });
709
856
  pipeline
@@ -1005,27 +1152,44 @@ export async function runTolokaCli(
1005
1152
  environment: TolokaEnvironment = process.env,
1006
1153
  io: CliIo = terminalIo(),
1007
1154
  injectedClient?: PotatoClient,
1008
- analytics: CliAnalytics = createCliAnalytics({ environment }),
1155
+ analytics?: CliAnalytics,
1156
+ runChecks?: typeof checkProject,
1157
+ autoUpdate?: AutoUpdateHooks,
1009
1158
  ): Promise<number> {
1159
+ const autoUpdateResult = await maybeAutoUpdate({
1160
+ argv,
1161
+ environment,
1162
+ io,
1163
+ ...(autoUpdate === undefined ? {} : { hooks: autoUpdate }),
1164
+ });
1165
+ if (autoUpdateResult.status === "reexec") return autoUpdateResult.exitCode;
1166
+
1167
+ const activeAnalytics =
1168
+ environment[CHECK_RESULT_FILE_ENV] === undefined
1169
+ ? (analytics ?? createCliAnalytics({ environment }))
1170
+ : undefined;
1010
1171
  const { program, exitCode } = createProgram({
1011
1172
  environment,
1012
1173
  io,
1013
- analytics,
1174
+ ...(activeAnalytics === undefined ? {} : { analytics: activeAnalytics }),
1014
1175
  ...(injectedClient === undefined ? {} : { injectedClient }),
1176
+ ...(runChecks === undefined ? {} : { runChecks }),
1015
1177
  });
1016
1178
 
1017
1179
  const startedAt = Date.now();
1018
1180
  const result = await runProgram(program, exitCode, argv, io);
1019
- try {
1020
- analytics.captureCommand({
1021
- command: commandNameFromArgv(argv),
1022
- durationMs: Date.now() - startedAt,
1023
- exitCode: result.code,
1024
- outcome: result.outcome,
1025
- ...(result.error === undefined ? {} : { error: result.error }),
1026
- });
1027
- await analytics.shutdown();
1028
- } catch {}
1181
+ if (activeAnalytics !== undefined) {
1182
+ try {
1183
+ activeAnalytics.captureCommand({
1184
+ command: commandNameFromArgv(argv),
1185
+ durationMs: Date.now() - startedAt,
1186
+ exitCode: result.code,
1187
+ outcome: result.outcome,
1188
+ ...(result.error === undefined ? {} : { error: result.error }),
1189
+ });
1190
+ await activeAnalytics.shutdown();
1191
+ } catch {}
1192
+ }
1029
1193
 
1030
1194
  return result.code;
1031
1195
  }
@@ -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 (
@@ -0,0 +1,93 @@
1
+ import { chmod, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
4
+ import type { CliEnvironment } from "./credentials";
5
+ import { configDirectory, preparePrivateFilePath } from "./credentials";
6
+
7
+ export const AUTO_UPDATE_SETTING = "auto-update";
8
+
9
+ export type CliSettings = {
10
+ readonly autoUpdate: boolean;
11
+ };
12
+
13
+ export const DEFAULT_CLI_SETTINGS: CliSettings = {
14
+ autoUpdate: true,
15
+ };
16
+
17
+ export class SettingsError extends Error {
18
+ constructor(message: string, options?: { readonly cause?: unknown }) {
19
+ super(message, options);
20
+ this.name = "SettingsError";
21
+ }
22
+ }
23
+
24
+ export const settingsPath = (environment: CliEnvironment = process.env): string =>
25
+ join(configDirectory(environment), "settings.json");
26
+
27
+ const asSettings = (payload: unknown): CliSettings => {
28
+ const autoUpdate =
29
+ typeof payload === "object" && payload !== null
30
+ ? Reflect.get(payload, "autoUpdate")
31
+ : undefined;
32
+ if (typeof autoUpdate !== "boolean") return DEFAULT_CLI_SETTINGS;
33
+ return { autoUpdate };
34
+ };
35
+
36
+ export async function readCliSettings(
37
+ environment: CliEnvironment = process.env,
38
+ ): Promise<CliSettings> {
39
+ try {
40
+ return asSettings(JSON.parse(await readFile(settingsPath(environment), "utf8")));
41
+ } catch {
42
+ return DEFAULT_CLI_SETTINGS;
43
+ }
44
+ }
45
+
46
+ export async function writeCliSettings(
47
+ settings: CliSettings,
48
+ environment: CliEnvironment = process.env,
49
+ ): Promise<string> {
50
+ const path = settingsPath(environment);
51
+ await preparePrivateFilePath(path);
52
+ await writeFile(path, `${JSON.stringify({ autoUpdate: settings.autoUpdate }, null, 2)}\n`, {
53
+ mode: 0o600,
54
+ });
55
+ await chmod(path, 0o600);
56
+ return path;
57
+ }
58
+
59
+ export const parseBooleanSetting = (value: string): boolean => {
60
+ if (isTruthyFlag(value)) return true;
61
+ if (isFalseyFlag(value)) return false;
62
+ throw new SettingsError(`Expected a boolean (true or false), got ${JSON.stringify(value)}.`);
63
+ };
64
+
65
+ export const formatCliSettings = (settings: CliSettings): string =>
66
+ `${AUTO_UPDATE_SETTING}=${settings.autoUpdate}`;
67
+
68
+ export const getCliSetting = (
69
+ settings: CliSettings,
70
+ name: string | undefined,
71
+ ): { readonly output: string } => {
72
+ if (name === undefined || name.length === 0) return { output: formatCliSettings(settings) };
73
+ if (name !== AUTO_UPDATE_SETTING) {
74
+ throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`);
75
+ }
76
+ return { output: String(settings.autoUpdate) };
77
+ };
78
+
79
+ export async function setCliSetting(
80
+ environment: CliEnvironment,
81
+ name: string,
82
+ value: string,
83
+ ): Promise<{ readonly output: string; readonly settings: CliSettings }> {
84
+ if (name !== AUTO_UPDATE_SETTING) {
85
+ throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`);
86
+ }
87
+ const settings = { autoUpdate: parseBooleanSetting(value) };
88
+ await writeCliSettings(settings, environment);
89
+ return {
90
+ settings,
91
+ output: `Set ${AUTO_UPDATE_SETTING} to ${settings.autoUpdate}.`,
92
+ };
93
+ }
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(
package/src/importer.ts CHANGED
@@ -220,7 +220,7 @@ function formatSource(value: unknown, depth = 0): string {
220
220
  return `{\n${properties}\n${indentation(depth)}}`;
221
221
  }
222
222
 
223
- function transformFieldProxy(
223
+ function transformFieldReference(
224
224
  reference: { id: string; optional: boolean },
225
225
  fieldReference: FieldReference,
226
226
  ): unknown {
@@ -276,10 +276,10 @@ function authoringSettings(node: GraphNode, fieldReference: FieldReference): unk
276
276
  return {
277
277
  ...settings,
278
278
  started: startedFieldProxies.output.map((reference) =>
279
- transformFieldProxy(reference, fieldReference),
279
+ transformFieldReference(reference, fieldReference),
280
280
  ),
281
281
  completed: completedFieldProxies.input.map((reference) =>
282
- transformFieldProxy(reference, fieldReference),
282
+ transformFieldReference(reference, fieldReference),
283
283
  ),
284
284
  };
285
285
  }
@@ -287,14 +287,12 @@ function authoringSettings(node: GraphNode, fieldReference: FieldReference): unk
287
287
  const { fieldProxies, ...settings } = node.settings;
288
288
  return {
289
289
  ...settings,
290
- fieldProxies: {
291
- input: fieldProxies.input.map((reference) =>
292
- transformFieldProxy(reference, fieldReference),
293
- ),
294
- output: fieldProxies.output.map((reference) =>
295
- transformFieldProxy(reference, fieldReference),
296
- ),
297
- },
290
+ input: fieldProxies.input.map((reference) =>
291
+ transformFieldReference(reference, fieldReference),
292
+ ),
293
+ output: fieldProxies.output.map((reference) =>
294
+ transformFieldReference(reference, fieldReference),
295
+ ),
298
296
  };
299
297
  }
300
298
  case "generation": {
@@ -322,11 +320,9 @@ function authoringSettings(node: GraphNode, fieldReference: FieldReference): unk
322
320
  field: fieldReference(fieldProxyId),
323
321
  })),
324
322
  })),
325
- fieldProxies: {
326
- input: fieldProxies.input.map((reference) =>
327
- transformFieldProxy(reference, fieldReference),
328
- ),
329
- },
323
+ input: fieldProxies.input.map((reference) =>
324
+ transformFieldReference(reference, fieldReference),
325
+ ),
330
326
  };
331
327
  }
332
328
  case "quorum": {