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

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.
@@ -1,5 +1,5 @@
1
1
  import { assert } from "es-toolkit";
2
- import { readTolokaState, writeTolokaState } from "../state";
2
+ import { readTolokaState, TOLOKA_STATE_FILENAME, writeTolokaState } from "../state";
3
3
  import type { TolokaState } from "../state";
4
4
  import type { AuthPrompt } from "./auth";
5
5
  import type { TolokaCliFlags, TolokaEnvironment } from "./config";
@@ -238,7 +238,7 @@ export async function listRemoteProjects(options: {
238
238
  options.context?.id === project.id && options.context.alias !== undefined
239
239
  ? `\t${options.context.alias}`
240
240
  : "";
241
- return `${mark}\t${project.id}\t${project.name}${alias}`;
241
+ return `${mark}\t${project.id}\t${project.name}\t${project.teamId}${alias}`;
242
242
  });
243
243
  if (page.pagination.hasMore) {
244
244
  lines.push(
@@ -250,6 +250,51 @@ export async function listRemoteProjects(options: {
250
250
  return { output };
251
251
  }
252
252
 
253
+ export type TrackedResourceKind = "pipeline" | "dataset";
254
+
255
+ export async function untrackProjectResource(options: {
256
+ readonly sourceRoot: string;
257
+ readonly type: TrackedResourceKind;
258
+ readonly name: string;
259
+ readonly confirm: boolean;
260
+ readonly io: ProjectIo;
261
+ }): Promise<{ readonly output: string }> {
262
+ const state = await readTolokaState(options.sourceRoot);
263
+ assert(state, `No ${TOLOKA_STATE_FILENAME} found in ${options.sourceRoot}.`);
264
+ const bindings = options.type === "pipeline" ? state.pipelines : state.datasets;
265
+ const binding = bindings[options.name];
266
+ assert(
267
+ binding,
268
+ `${options.type === "pipeline" ? "Pipeline" : "Dataset"} ${options.name} is not tracked.`,
269
+ );
270
+ const id = "pipelineId" in binding ? binding.pipelineId : binding.datasetId;
271
+ const label = `${options.type} ${options.name} (${id})`;
272
+
273
+ if (!options.confirm) {
274
+ assert(
275
+ options.io.prompt,
276
+ `Refusing to untrack ${label} without confirmation. Pass --confirm after reviewing ${TOLOKA_STATE_FILENAME}.`,
277
+ );
278
+ const answer = (
279
+ await options.io.prompt(`Untrack ${label}? The live resource will not be deleted. [y/N] `)
280
+ )
281
+ .trim()
282
+ .toLowerCase();
283
+ assert(answer === "y" || answer === "yes", "Untrack canceled.");
284
+ }
285
+
286
+ const remaining = Object.fromEntries(
287
+ Object.entries(bindings).filter(([candidate]) => candidate !== options.name),
288
+ );
289
+ await writeTolokaState(options.sourceRoot, {
290
+ ...state,
291
+ ...(options.type === "pipeline" ? { pipelines: remaining } : { datasets: remaining }),
292
+ });
293
+ const output = `Untracked ${label}. The live ${options.type} was not deleted.`;
294
+ options.io.stdout(output);
295
+ return { output };
296
+ }
297
+
253
298
  export async function addProject(options: {
254
299
  readonly environment: CliEnvironment;
255
300
  readonly flags?: TolokaCliFlags;
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
4
4
  import type {
5
5
  PipelineGraphSnapshot,
6
6
  PipelineGraphV1,
7
+ PipelineValidationIssue,
7
8
  PipelineValidationReport,
8
9
  } from "../../openapi/gen/models";
9
10
  import { resolvePipelineDefinition, validatePipelineDefinition } from "../authoring";
@@ -83,10 +84,13 @@ export type ApplyProjectResult = {
83
84
 
84
85
  export type ValidateProjectOptions = PlanProjectOptions;
85
86
 
87
+ export type PipelineValidationStatus = "valid" | "invalid" | "checks_pending";
88
+
86
89
  export type PipelineValidationResult = {
87
90
  readonly sourceDirectory: string;
88
91
  readonly pipelineId?: string;
89
92
  readonly report?: PipelineValidationReport;
93
+ readonly status?: PipelineValidationStatus;
90
94
  readonly skipped?: string;
91
95
  };
92
96
 
@@ -860,10 +864,47 @@ export async function applyProject(
860
864
  const issueLocation = (issue: { nodeId?: string; checkId?: string }) =>
861
865
  [issue.nodeId, issue.checkId].filter((value): value is string => value !== undefined).join("/");
862
866
 
867
+ const PENDING_CHECK_CODE = "not_ran_node_check";
868
+
869
+ const normalizeValidationIssue = (issue: PipelineValidationIssue): PipelineValidationIssue =>
870
+ issue.code === "empty_node_checks"
871
+ ? { ...issue, message: "No stored check cases are configured for this node" }
872
+ : issue;
873
+
874
+ const normalizeValidationReport = (
875
+ report: PipelineValidationReport,
876
+ ): { readonly report: PipelineValidationReport; readonly status: PipelineValidationStatus } => {
877
+ const seen = new Set<string>();
878
+ const issues = report.issues.flatMap((rawIssue) => {
879
+ const issue = normalizeValidationIssue(rawIssue);
880
+ const key = [
881
+ issue.code,
882
+ issue.severity,
883
+ issue.nodeId ?? "",
884
+ issue.checkId ?? "",
885
+ issue.message,
886
+ ].join("\n");
887
+ if (seen.has(key)) return [];
888
+ seen.add(key);
889
+ return [issue];
890
+ });
891
+ const pending = issues.some((issue) => issue.code === PENDING_CHECK_CODE);
892
+ const blocking = issues.some(
893
+ (issue) => issue.severity === "error" && issue.code !== PENDING_CHECK_CODE,
894
+ );
895
+ const status: PipelineValidationStatus =
896
+ blocking || (!report.valid && !pending) ? "invalid" : pending ? "checks_pending" : "valid";
897
+ return {
898
+ status,
899
+ report: { ...report, valid: status !== "invalid", issues },
900
+ };
901
+ };
902
+
863
903
  const formatValidationIssues = (report: PipelineValidationReport) =>
864
904
  report.issues.map((issue) => {
865
905
  const location = issueLocation(issue);
866
- return ` ${issue.severity} ${issue.code}${location.length === 0 ? "" : ` ${location}`}: ${issue.message}`;
906
+ const severity = issue.code === PENDING_CHECK_CODE ? "pending" : issue.severity;
907
+ return ` ${severity} ${issue.code}${location.length === 0 ? "" : ` ${location}`}: ${issue.message}`;
867
908
  });
868
909
 
869
910
  export async function validateProject(
@@ -898,24 +939,26 @@ export async function validateProject(
898
939
  resolvePipelineDefinition(pipeline.source.definition, { datasetId: dataset.id }),
899
940
  pipeline.snapshot.graph,
900
941
  );
901
- const report = await client.pipelines.validateGraph({
902
- pipelineId: pipeline.pipeline.id,
903
- graph,
904
- });
942
+ const normalized = normalizeValidationReport(
943
+ await client.pipelines.validateGraph({
944
+ pipelineId: pipeline.pipeline.id,
945
+ graph,
946
+ }),
947
+ );
905
948
  results.push({
906
949
  sourceDirectory: pipeline.source.sourceDirectory,
907
950
  pipelineId: pipeline.pipeline.id,
908
- report,
951
+ report: normalized.report,
952
+ status: normalized.status,
909
953
  });
910
954
  }
911
- const valid = results.every((result) => result.report?.valid !== false);
955
+ const valid = results.every((result) => result.status !== "invalid");
912
956
  const lines = results.flatMap((result) => {
913
957
  if (result.skipped !== undefined)
914
958
  return [`- ${result.sourceDirectory}: skipped (${result.skipped})`];
915
959
  assert(result.report, `Pipeline ${result.sourceDirectory} is missing a validation report`);
916
- const header = result.report.valid
917
- ? `- ${result.sourceDirectory}: valid`
918
- : `- ${result.sourceDirectory}: invalid`;
960
+ assert(result.status, `Pipeline ${result.sourceDirectory} is missing a validation status`);
961
+ const header = `- ${result.sourceDirectory}: ${result.status.replace("_", "-")}`;
919
962
  return [header, ...formatValidationIssues(result.report)];
920
963
  });
921
964
  return {
package/src/cli/skill.ts CHANGED
@@ -2,11 +2,9 @@ import { assert } from "es-toolkit";
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
5
  import { assertSafeDestination, entryStats, hasErrorCode } from "./file-system";
7
6
  import { formatCliError } from "./format-error";
8
7
 
9
- export const BUNDLED_SKILL_CATALOG_ROOT = fileURLToPath(new URL("../../skills", import.meta.url));
10
8
  export const SKILL_CATALOG_MANIFEST = "catalog.json";
11
9
  export const SKILL_INSTALL_ROOT = ".agents/skills";
12
10
  export const SKILL_ENTRY_POINT = "SKILL.md";
@@ -133,15 +131,16 @@ export async function resolveBundledSkillCatalogRoot(
133
131
  const runtimeRoot =
134
132
  entry === undefined ? undefined : await findSkillCatalogRootAbove(dirname(entry));
135
133
  if (runtimeRoot !== undefined) return runtimeRoot;
134
+ const workingDirectoryRoot = await findSkillCatalogRootAbove(process.cwd());
136
135
  const searched =
137
136
  entry === undefined
138
137
  ? "process.argv[1] names no runtime entry"
139
138
  : `none exists between ${entry} and the filesystem root`;
140
139
  assert(
141
- await isFileAt(join(BUNDLED_SKILL_CATALOG_ROOT, SKILL_CATALOG_MANIFEST)),
142
- `Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and the source checkout catalog ${BUNDLED_SKILL_CATALOG_ROOT} is absent. Reinstall @toloka-platform/sdk.`,
140
+ workingDirectoryRoot !== undefined,
141
+ `Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and no ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} exists above ${process.cwd()} either. Reinstall @toloka-platform/sdk.`,
143
142
  );
144
- return BUNDLED_SKILL_CATALOG_ROOT;
143
+ return workingDirectoryRoot;
145
144
  }
146
145
 
147
146
  const loadBundledSkillCatalog = async (): Promise<SkillCatalog> =>
@@ -0,0 +1,68 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+
5
+ import { assert } from "es-toolkit";
6
+ import { SDK_VERSION } from "./init";
7
+
8
+ export type CliInstallChannel = "global-registry" | "linked" | "project-local";
9
+
10
+ type VersionInfoOptions = {
11
+ readonly globalBinDirectory?: string;
12
+ readonly globalPackageDirectory?: string;
13
+ };
14
+
15
+ const GLOBAL_PACKAGE_MARKER = `${sep}install${sep}global${sep}`;
16
+
17
+ const realpathOrSelf = (path: string): string => {
18
+ try {
19
+ return realpathSync(path);
20
+ } catch {
21
+ return path;
22
+ }
23
+ };
24
+
25
+ const classifyInstallChannel = (
26
+ entryPath: string,
27
+ packageRoot: string,
28
+ globalBinDirectory: string,
29
+ globalPackageDirectory: string,
30
+ ): CliInstallChannel => {
31
+ const resolvedEntryPath = realpathOrSelf(entryPath);
32
+ if (resolvedEntryPath.includes(GLOBAL_PACKAGE_MARKER)) return "global-registry";
33
+ if (realpathOrSelf(globalPackageDirectory) === packageRoot) return "linked";
34
+ if (dirname(resolve(entryPath)) === resolve(globalBinDirectory)) return "linked";
35
+ return "project-local";
36
+ };
37
+
38
+ export const cliVersionInfo = (entryPath = process.argv[1], options: VersionInfoOptions = {}) => {
39
+ assert(entryPath, "CLI entry path is unavailable");
40
+ const globalBinDirectory =
41
+ options.globalBinDirectory ??
42
+ resolve(process.env.BUN_INSTALL ?? join(homedir(), ".bun"), "bin");
43
+ const globalPackageDirectory =
44
+ options.globalPackageDirectory ??
45
+ resolve(
46
+ globalBinDirectory,
47
+ "..",
48
+ "install",
49
+ "global",
50
+ "node_modules",
51
+ "@toloka-platform",
52
+ "sdk",
53
+ );
54
+ const packageRoot = resolve(dirname(realpathOrSelf(entryPath)), "..");
55
+ const channel = classifyInstallChannel(
56
+ entryPath,
57
+ packageRoot,
58
+ globalBinDirectory,
59
+ globalPackageDirectory,
60
+ );
61
+
62
+ return {
63
+ version: SDK_VERSION,
64
+ commit: process.env.TOLOKA_BUILD_COMMIT ?? null,
65
+ channel,
66
+ ...(channel === "linked" ? { path: packageRoot } : {}),
67
+ };
68
+ };
package/src/index.ts CHANGED
@@ -69,6 +69,7 @@ export {
69
69
  connect,
70
70
  definePipeline,
71
71
  field,
72
+ optional,
72
73
  generation,
73
74
  isPipelineDefinition,
74
75
  match,
@@ -91,6 +92,7 @@ export type {
91
92
  InputPort,
92
93
  MatchAuthoringSettings,
93
94
  OutputPort,
95
+ OptionalFieldBinding,
94
96
  PipelineDefinition,
95
97
  PipelineDefinitionBindings,
96
98
  PipelineDefinitionInput,