@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.
Binary file
@@ -1 +1 @@
1
- ce1d79afa2e57b085b55b98cf5c4cb90ab84b8a6ba50e586586f208a70f7f088 toloka.js.jsc
1
+ b3ff8750d4254e81d4dccbba6db162057e702b7b823d76d6adc2e6b122212251 toloka.js.jsc
@@ -1 +1 @@
1
- 44e401115084c2e912fe03234c4104090d1698c95e170f16cac9c4505952709a toloka.js
1
+ 122b2c1f1afe960176a8f8f5557f847da14ed454809f44d8ed4a5212be6ace2a toloka.js
package/docs/authoring.md CHANGED
@@ -37,19 +37,24 @@ The name is a bare identifier. When a field is a pipeline **input**, its name mu
37
37
 
38
38
  ## Node constructors
39
39
 
40
- Five constructors cover the whole palette. Each takes a name, a settings object, and optional `{ id }`.
40
+ Four work-node constructors take a name, settings, and optional `{ id }`: `code`, `generation`, `quorum`, and `match`. `root` is the single pipeline boundary and takes only settings plus optional `{ id }`.
41
41
 
42
42
  ### root
43
43
 
44
44
  Defines the pipeline's input and output contract. `started` is what each item carries in from the dataset; `completed` is what gets written back.
45
45
 
46
46
  ```ts
47
- import { root } from "@toloka-platform/sdk";
47
+ import { optional, root } from "@toloka-platform/sdk";
48
48
 
49
- export const collection = root({ started: [imageUrl], completed: [annotations] });
49
+ export const collection = root({
50
+ started: [imageUrl],
51
+ completed: [optional(annotations)],
52
+ });
50
53
  ```
51
54
 
52
- Pass `{ field, optional: true }` instead of a bare field to make a binding optional.
55
+ Pass a bare field for a required binding. Use `optional(field)` for new optional bindings. The explicit `{ field, optional }` form remains supported and may appear in imported source.
56
+
57
+ `root()` deliberately has no name argument: a pipeline has exactly one boundary, and its default wire id is `root`.
53
58
 
54
59
  ### code
55
60
 
package/docs/cli-v2.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CLI v2: project as the root
2
2
 
3
- How `toloka` should work in the next version. Preview is unreleased, so this is a clean cutover, not a compatibility layer.
3
+ How `toloka` should work in the next version. Beta remains a clean cutover rather than a compatibility layer.
4
4
 
5
5
  The platform object is a **project** (`pp.…`, today's pipeline project). Windmill calls the same role a workspace. PostHog calls it a project. We keep **project** so the CLI matches the API and the console.
6
6
 
@@ -212,7 +212,7 @@ Do not add those groups until the resource exists. The rule is already fixed: th
212
212
  | `toloka runs` / `cost` / `cancel` / `results` | `toloka pipeline …` |
213
213
  | root = `toloka.json` or `pipelines/` | root = `toloka.json` only |
214
214
 
215
- No shims. Preview callers update.
215
+ No shims. Beta callers update.
216
216
 
217
217
  ## Out of scope
218
218
 
package/docs/cli.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `toloka` CLI reads TypeScript source as the desired state, compares it with the live platform state, and reconciles the two.
4
4
 
5
- This CLI is Preview software. Commands, flags, and output can change without a major version bump.
5
+ This CLI is Beta software. Commands, flags, and output can change before general availability.
6
6
 
7
7
  `toloka` requires Bun 1.4.0. The published package ships matching CLI bytecode; the launcher falls back to TypeScript source in a development checkout when the bytecode is absent.
8
8
 
@@ -61,6 +61,14 @@ The CLI is resource-first: `toloka <resource> <action>`. A Base Action has one m
61
61
 
62
62
  Resources may also expose specialized actions beyond the Base Actions, such as `pipeline cost`, `skill search`, and `skill install`.
63
63
 
64
+ ## version
65
+
66
+ Print the SDK/CLI version. `toloka version --json` also prints build provenance: the build commit when the bytecode bundle recorded one, the install channel (`global-registry`, `linked`, or `project-local`), and the local package path for linked installs. `toloka -v` and `toloka --version` remain semver-only for scripts.
67
+
68
+ ```sh
69
+ toloka version [--json]
70
+ ```
71
+
64
72
  ## project
65
73
 
66
74
  Register and select the platform project later commands use.
@@ -73,9 +81,12 @@ toloka project current
73
81
  toloka project url
74
82
  toloka project remove <alias>
75
83
  toloka project create [alias] [--name <name>] [--team-id <id>]
84
+ toloka project untrack <pipeline|dataset> <name> [--confirm]
76
85
  ```
77
86
 
78
- `add`, `switch`, `remove`, and `create` write the machine registry in the credentials file. They refuse `--api-key` / `--base-url` / `POTATO_*` when those would select a different key or platform than the stored file. `create` reuses the only visible team when every listed project shares one; otherwise pass `--team-id`.
87
+ `add`, `switch`, `remove`, and `create` write the machine registry in the credentials file. They refuse `--api-key` / `--base-url` / `POTATO_*` when those would select a different key or platform than the stored file. `project list` prints project id, name, and team id. `create` reuses the only visible team when every listed project shares one; otherwise pass `--team-id`.
88
+
89
+ `project untrack` removes one pipeline or dataset binding from `toloka.json` without deleting the live resource. It previews the name and id in a terminal; outside a terminal, pass `--confirm` after reviewing the state file.
79
90
 
80
91
  `url` fetches the selected project to resolve its team and prints only its platform UI URL to stdout, making the output safe to capture in scripts and agent workflows.
81
92
 
@@ -4,11 +4,11 @@ Install the SDK, sign in, and get a pipeline project reconciling from source in
4
4
 
5
5
  ## Install
6
6
 
7
- Install the bytecode-cached CLI globally and declare the Preview SDK in the project:
7
+ Install the bytecode-cached CLI globally and declare the Beta SDK in the project:
8
8
 
9
9
  ```sh
10
10
  curl -fsSL https://platform.toloka.ai/cli/install.sh | sh
11
- bun add @toloka-platform/sdk@preview
11
+ bun add @toloka-platform/sdk@latest
12
12
  ```
13
13
 
14
14
  The installer requires Bun 1.4.0, installs `@toloka-platform/sdk` globally, and exposes `toloka` through Bun's global binary directory. The published CLI includes matching precompiled bytecode. The project install makes SDK imports resolvable.
@@ -140,4 +140,4 @@ Exit code `0` means the run reached a successful terminal state, `1` a failed on
140
140
  - [Nodes](./nodes.md) — which node to reach for, and the pricing floors you must clear
141
141
  - [CLI](./cli.md) — every command and flag
142
142
  - [Troubleshooting](./troubleshooting.md) — the failures you will hit
143
- - [Limitations](./limitations.md) — Preview status and what this SDK does not do yet
143
+ - [Limitations](./limitations.md) — Beta status and what this SDK does not do yet
@@ -1,8 +1,8 @@
1
1
  # Limitations
2
2
 
3
- What this Preview SDK does not do yet, stated plainly so you can decide what to depend on.
3
+ What this Beta SDK does not do yet, stated plainly so you can decide what to depend on.
4
4
 
5
- `@toloka-platform/sdk` is `0.1.0-preview.8`. This is a Preview: the authoring API, the CLI surface, and the generated source layout can change without a major version bump. Pin an exact version. Do not depend on this in production yet.
5
+ `@toloka-platform/sdk` is `0.1.0`. This is a Beta: the authoring API, the CLI surface, and the generated source layout can change before general availability. Pin an exact version. Do not depend on this in production yet.
6
6
 
7
7
  ## Local checks do not certify deployability
8
8
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@toloka-platform/sdk",
3
- "version": "0.1.0-preview.8",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
- "description": "Preview. Build, inspect, and operate Toloka pipeline projects as TypeScript source. The public API may change.",
5
+ "description": "Beta. Build, inspect, and operate Toloka pipeline projects as TypeScript source. The public API may change.",
6
6
  "license": "Apache-2.0",
7
7
  "bin": {
8
8
  "toloka": "bin/toloka.js"
@@ -28,7 +28,7 @@
28
28
  "publishConfig": {
29
29
  "access": "public",
30
30
  "registry": "https://registry.npmjs.org/",
31
- "tag": "preview"
31
+ "tag": "latest"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "bun ./scripts/build-cli.ts ./dist/cli",
@@ -13,8 +13,8 @@
13
13
  "testing",
14
14
  "checks"
15
15
  ],
16
- "version": "0.1.0-preview.8",
17
- "digest": "87dca7b1cc0cf3d61f54ab27703ea4b60622355ec07e289ce1242bb3c2233482",
16
+ "version": "0.1.0",
17
+ "digest": "bcdb60e89bc4b8e345c1793c7a7d5275e8b196e171bd09adba242ed89f117a50",
18
18
  "files": ["SKILL.md", "references/node-semantics.md", "references/workflow-and-cli.md"]
19
19
  },
20
20
  {
@@ -22,8 +22,8 @@
22
22
  "name": "Report a Toloka problem",
23
23
  "description": "Report a problem with the Toloka platform, CLI, or SDK as a redacted Markdown issue report sent through the toloka CLI only after explicit approval. Use when the user hits unexpected behavior, an error, or a bug and wants Toloka engineers to investigate.",
24
24
  "tags": ["support", "issue", "bug", "report"],
25
- "version": "0.1.0-preview.8",
26
- "digest": "044f8e63faf2a236931a78d3555e6808563199b5f5a06741ca3f2ee9063a71fa",
25
+ "version": "0.1.0",
26
+ "digest": "969aedef68e56243395e0a9115d90041c7c1e27f759762b07e62a7d521a8a251",
27
27
  "files": ["SKILL.md", "references/report-template.md"]
28
28
  }
29
29
  ]
@@ -5,7 +5,7 @@ description: Onboard users and build, test, inspect, or change Toloka platform p
5
5
 
6
6
  # Build Toloka pipelines as code
7
7
 
8
- This SDK is Preview software. The authoring API, the CLI, and generated source can change. Do not invent helpers or flags that are not in the installed package.
8
+ This SDK is Beta software. The authoring API, the CLI, and generated source can change before general availability. Do not invent helpers or flags that are not in the installed package.
9
9
 
10
10
  Use this skill when the user wants to get started or to create, inspect, test, or change a Toloka pipeline project as code. Work in the project source tree; do not translate the request into direct platform API calls.
11
11
 
@@ -43,13 +43,13 @@ connect(annotate.output, collection.completed);
43
43
 
44
44
  ### Pick and configure it
45
45
 
46
- Create exactly one `root` node. Use `started` for dataset fields the pipeline reads and `completed` for fields it writes back to the same dataset. A completed field is not a separate output destination: the platform creates or uses that dataset column per item.
46
+ Create exactly one `root` node with `root(settings)`. It has no name argument because it represents the pipeline boundary, not a named unit of work. Use `started` for dataset fields the pipeline reads and `completed` for fields it writes back to the same dataset. A completed field is not a separate output destination: the platform creates or uses that dataset column per item.
47
47
 
48
48
  The public authoring shape is `RootAuthoringSettings`:
49
49
 
50
50
  - `started`: fields exposed through the `started` output port.
51
51
  - `completed`: fields accepted through the `completed` input port and written to the dataset.
52
- - Each entry can be a `Field` or `{ field, optional }`; a bare `Field` is required.
52
+ - Each entry can be a required `Field`, `optional(field)`, or an explicit `{ field, optional }` binding. Prefer `optional(field)` for new optional bindings and preserve the explicit object form in imported source.
53
53
  - `itemLimit` exists in both `RootNodeSettings` and `RootAuthoringSettings`. Do not invent a limit; author one only when the user explicitly requests a bounded test or batch. Apply preserves a live `itemLimit` unless source declares its own.
54
54
 
55
55
  The dataset name belongs in `definePipeline({ dataset: 'name', ... })`, not in the root settings. `toloka apply` resolves or creates the named dataset and supplies its ID.
@@ -83,7 +83,7 @@ All commands accept `--source-root <directory>`. Prefer running from the source
83
83
  The real command surface is:
84
84
 
85
85
  ```text
86
- toloka version
86
+ toloka version [--json]
87
87
  toloka -v
88
88
  toloka --version
89
89
  toloka login
@@ -99,6 +99,7 @@ toloka project current
99
99
  toloka project url
100
100
  toloka project remove <alias>
101
101
  toloka project create [alias] [--name <name>] [--team-id <id>]
102
+ toloka project untrack <pipeline|dataset> <name> [--confirm]
102
103
  toloka init
103
104
  toloka import [projectId]
104
105
  toloka check [pipeline] [--node <name-or-id>] [--json] [--watch]
@@ -26,7 +26,7 @@ The CLI exits 1 with `Potato API request failed with status 500`. The same comma
26
26
 
27
27
  ## Environment
28
28
 
29
- - toloka CLI 0.1.0-preview.8 (`toloka --version`), Bun 1.4.0, macOS 15
29
+ - toloka CLI 0.1.0 (`toloka --version`), Bun 1.4.0, macOS 15
30
30
  - Platform: https://platform.toloka.ai
31
31
 
32
32
  ## Redactions
@@ -56,6 +56,8 @@ type FieldReference = {
56
56
  readonly optional: boolean;
57
57
  };
58
58
 
59
+ export type OptionalFieldBinding = { readonly field: Field; readonly optional: true };
60
+
59
61
  type RootFieldReference = Field | { readonly field: Field; readonly optional?: boolean };
60
62
 
61
63
  type FieldRelation<Relation> = Relation extends { id: string }
@@ -445,10 +447,18 @@ export function field<const Name extends string>(
445
447
  };
446
448
  }
447
449
 
450
+ export function optional(referencedField: Field): OptionalFieldBinding {
451
+ return { field: referencedField, optional: true };
452
+ }
453
+
448
454
  function createRoot(
449
455
  settings: RootAuthoringSettings,
450
456
  options: AuthoringIdOptions = {},
451
457
  ): AuthoredRootNode {
458
+ assert(
459
+ typeof settings === "object" && settings !== null && !Array.isArray(settings),
460
+ "root() defines the single pipeline boundary; call root({ started, completed }) without a name",
461
+ );
452
462
  const id = options.id ?? "root";
453
463
  const { started, completed, ...settingsWithoutFieldProxies } = settings;
454
464
  return {
@@ -1,4 +1,3 @@
1
- import { createRequire } from "node:module";
2
1
  import path from "node:path";
3
2
 
4
3
  import { assert } from "es-toolkit";
@@ -11,7 +10,7 @@ import {
11
10
  createVirtualTypeScriptEnvironment,
12
11
  } from "./typescript-vfs.js";
13
12
 
14
- const tsLibDirectory = path.dirname(createRequire(import.meta.url).resolve("typescript-api"));
13
+ const tsLibDirectory = path.dirname(ts.getDefaultLibFilePath({ target: ts.ScriptTarget.Latest }));
15
14
 
16
15
  const BLOCK_MARKERS = {
17
16
  data: "DATA_INJECTION",
@@ -3,7 +3,7 @@ import { once } from "node:events";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { chmod, readFile, writeFile } from "node:fs/promises";
5
5
  import { join, sep } from "node:path";
6
- import { fileURLToPath } from "node:url";
6
+ import { findCliEntry } from "./cli-entry";
7
7
  import type { CliEnvironment } from "./credentials";
8
8
  import { configDirectory, preparePrivateFilePath } from "./credentials";
9
9
  import { SDK_VERSION } from "./init";
@@ -123,11 +123,7 @@ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined =
123
123
  };
124
124
 
125
125
  export const latestPublishedVersion = (view: NpmPackageView): string | undefined => {
126
- const tags = view["dist-tags"];
127
- if (tags === undefined) return undefined;
128
- const preview = tags.preview;
129
- if (typeof preview === "string" && preview.length > 0) return preview;
130
- const latest = tags.latest;
126
+ const latest = view["dist-tags"]?.latest;
131
127
  return typeof latest === "string" && latest.length > 0 ? latest : undefined;
132
128
  };
133
129
 
@@ -172,7 +168,7 @@ const BUN_GLOBAL_MARKER = `${sep}install${sep}global${sep}`;
172
168
 
173
169
  export const isGlobalCliInstall = (
174
170
  entry = process.argv[1],
175
- runtimePath = fileURLToPath(import.meta.url),
171
+ runtimePath = findCliEntry(),
176
172
  ): boolean =>
177
173
  [entry, runtimePath].some((path) => {
178
174
  if (typeof path !== "string") return false;
package/src/cli/check.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { existsSync, watch } from "node:fs";
1
+ import { watch } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { once } from "node:events";
5
5
  import { mkdtemp, readFile, rm } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
8
 
10
9
  import { assert } from "es-toolkit";
11
10
  import {
@@ -25,6 +24,7 @@ import {
25
24
  type TestEvidence,
26
25
  type TestEvidenceKind,
27
26
  } from "../testing/test-evidence";
27
+ import { resolveCliEntry } from "./cli-entry";
28
28
  import { loadProjectSource, type ProjectSource } from "./source";
29
29
 
30
30
  export type CheckStatus = "passed" | "failed" | "missing" | "partial" | "infrastructure_error";
@@ -114,6 +114,18 @@ export type CheckProjectResult = {
114
114
  readonly source?: ProjectSource;
115
115
  readonly output: string;
116
116
  };
117
+
118
+ export type FreshCheckFailurePhase = "spawn" | "execute" | "read_result" | "parse_result";
119
+
120
+ export class FreshCheckProcessError extends Error {
121
+ readonly phase: FreshCheckFailurePhase;
122
+
123
+ constructor(phase: FreshCheckFailurePhase, message: string, options?: ErrorOptions) {
124
+ super(message, options);
125
+ this.name = "FreshCheckProcessError";
126
+ this.phase = phase;
127
+ }
128
+ }
117
129
  const MAX_DIAGNOSTIC_CHARS = 5_000;
118
130
  const MAX_TOOL_OUTPUT_BYTES = 64 * 1024;
119
131
  export const CHECK_RESULT_FILE_ENV = "TOLOKA_INTERNAL_CHECK_RESULT_FILE";
@@ -865,45 +877,93 @@ const parseFreshCheckResult = (value: unknown, json: boolean): CheckProjectResul
865
877
  output: json ? JSON.stringify({ exitCode, results }) : formatCheckResults(results),
866
878
  };
867
879
  };
880
+
881
+ const checkProcessOutput = (run: ToolRunResult): string =>
882
+ [run.stderr, run.stdout].filter(Boolean).join("\n");
883
+
884
+ export const readFreshCheckResult = async (
885
+ run: ToolRunResult,
886
+ readResult: () => Promise<string>,
887
+ json = false,
888
+ ): Promise<CheckProjectResult> => {
889
+ let raw: string;
890
+ try {
891
+ raw = await readResult();
892
+ } catch (error) {
893
+ if (run.exitCode !== 0) {
894
+ const output = checkProcessOutput(run);
895
+ throw new FreshCheckProcessError(
896
+ "execute",
897
+ output.length === 0
898
+ ? `Local check process exited ${run.exitCode} before producing results`
899
+ : `Local check process failed before producing results:\n${output}`,
900
+ { cause: error },
901
+ );
902
+ }
903
+ throw new FreshCheckProcessError(
904
+ "read_result",
905
+ "Local check process completed without producing a result file",
906
+ { cause: error },
907
+ );
908
+ }
909
+
910
+ let parsed: unknown;
911
+ try {
912
+ parsed = JSON.parse(raw);
913
+ } catch (error) {
914
+ throw new FreshCheckProcessError("parse_result", "Local check process returned invalid JSON", {
915
+ cause: error,
916
+ });
917
+ }
918
+
919
+ try {
920
+ return parseFreshCheckResult(parsed, json);
921
+ } catch (error) {
922
+ throw new FreshCheckProcessError(
923
+ "parse_result",
924
+ `Local check process returned an invalid result: ${error instanceof Error ? error.message : String(error)}`,
925
+ { cause: error },
926
+ );
927
+ }
928
+ };
868
929
  export const runFreshCheck = async (options: CheckProjectOptions): Promise<CheckProjectResult> => {
869
- const sourceEntry = fileURLToPath(new URL("./bin.ts", import.meta.url));
870
- const entry = existsSync(sourceEntry) ? sourceEntry : fileURLToPath(import.meta.url);
930
+ const entry = resolveCliEntry();
871
931
  const resultDirectory = await mkdtemp(join(tmpdir(), "toloka-check-watch-result-"));
872
932
  const resultPath = join(resultDirectory, "result.json");
873
- let run: ToolRunResult | undefined;
874
933
  try {
875
- run = await runModule(
876
- options.sourceRoot,
877
- entry,
878
- [
879
- "--source-root",
934
+ let run: ToolRunResult;
935
+ try {
936
+ run = await runModule(
880
937
  options.sourceRoot,
881
- "check",
882
- ...(options.pipeline === undefined ? [] : [options.pipeline]),
883
- ...(options.node === undefined ? [] : ["--node", options.node]),
884
- "--json",
885
- ],
886
- {
887
- ...(process.env.PATH === undefined ? {} : { PATH: process.env.PATH }),
888
- ...(options.environment ?? process.env),
889
- [CHECK_RESULT_FILE_ENV]: resultPath,
890
- },
891
- options.signal,
892
- "bun",
893
- );
894
- return parseFreshCheckResult(
895
- JSON.parse(await readFile(resultPath, "utf8")),
938
+ entry,
939
+ [
940
+ "--source-root",
941
+ options.sourceRoot,
942
+ "check",
943
+ ...(options.pipeline === undefined ? [] : [options.pipeline]),
944
+ ...(options.node === undefined ? [] : ["--node", options.node]),
945
+ "--json",
946
+ ],
947
+ {
948
+ ...(process.env.PATH === undefined ? {} : { PATH: process.env.PATH }),
949
+ ...(options.environment ?? process.env),
950
+ [CHECK_RESULT_FILE_ENV]: resultPath,
951
+ },
952
+ options.signal,
953
+ "bun",
954
+ );
955
+ } catch (error) {
956
+ throw new FreshCheckProcessError(
957
+ "spawn",
958
+ `Unable to start the local check process: ${error instanceof Error ? error.message : String(error)}`,
959
+ { cause: error },
960
+ );
961
+ }
962
+ return await readFreshCheckResult(
963
+ run,
964
+ async () => readFile(resultPath, "utf8"),
896
965
  options.json === true,
897
966
  );
898
- } catch (error) {
899
- const detail = [
900
- run?.stderr,
901
- run?.stdout,
902
- error instanceof Error ? error.message : String(error),
903
- ]
904
- .filter(Boolean)
905
- .join("\n");
906
- throw new Error(`Fresh check process returned unreadable output: ${detail}`, { cause: error });
907
967
  } finally {
908
968
  await rm(resultDirectory, { recursive: true, force: true });
909
969
  }
@@ -0,0 +1,41 @@
1
+ import { assert } from "es-toolkit";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ const CLI_ENTRY = join("bin", "toloka.js");
6
+
7
+ const runtimePath = (entry: string): string => {
8
+ try {
9
+ return realpathSync(entry);
10
+ } catch {
11
+ return entry;
12
+ }
13
+ };
14
+
15
+ const findCliEntryAbove = (directory: string): string | undefined => {
16
+ const candidate = join(directory, CLI_ENTRY);
17
+ if (existsSync(candidate)) return candidate;
18
+ const parent = dirname(directory);
19
+ return parent === directory ? undefined : findCliEntryAbove(parent);
20
+ };
21
+
22
+ export const findCliEntry = (
23
+ runtimeEntry: string | undefined = process.argv[1],
24
+ ): string | undefined => {
25
+ const fromRuntimeEntry =
26
+ runtimeEntry === undefined ? undefined : findCliEntryAbove(dirname(runtimePath(runtimeEntry)));
27
+ return fromRuntimeEntry ?? findCliEntryAbove(process.cwd());
28
+ };
29
+
30
+ export const resolveCliEntry = (runtimeEntry: string | undefined = process.argv[1]): string => {
31
+ const entry = runtimeEntry === undefined ? undefined : runtimePath(runtimeEntry);
32
+ if (entry !== undefined && existsSync(entry) && findCliEntryAbove(dirname(entry)) !== undefined) {
33
+ return entry;
34
+ }
35
+ const workingDirectoryEntry = findCliEntryAbove(process.cwd());
36
+ assert(
37
+ workingDirectoryEntry !== undefined,
38
+ `Cannot locate the toloka CLI entry point. An installed @toloka-platform/sdk ships ${CLI_ENTRY} in a directory above its runtime entry, but none exists above ${runtimeEntry ?? "the unset process.argv[1]"} or above ${process.cwd()}. Reinstall @toloka-platform/sdk.`,
39
+ );
40
+ return workingDirectoryEntry;
41
+ };
package/src/cli/cost.ts CHANGED
@@ -80,7 +80,13 @@ const collectNodeRows = (
80
80
  };
81
81
 
82
82
  const collectTotalRows = (total: PipelineCostEstimate["total"]): TableRow[] => {
83
- const totals: TableRow[] = [["Total", total.expected, total.worstCase]];
83
+ const totals: TableRow[] =
84
+ total.expected === total.worstCase
85
+ ? [["Run cost", total.expected, ""]]
86
+ : [
87
+ ["Expected run cost", total.expected, ""],
88
+ ["Maximum run cost", "", total.worstCase],
89
+ ];
84
90
  if (total.bestCase != null) {
85
91
  totals.push(["Best case", total.bestCase, ""]);
86
92
  }
@@ -64,6 +64,7 @@ import {
64
64
  removeProject,
65
65
  resolveProjectContext,
66
66
  switchProject,
67
+ untrackProjectResource,
67
68
  } from "./project-context";
68
69
  import { estimatePipelineCost } from "./cost";
69
70
  import { submitIssueReport } from "./report";
@@ -81,6 +82,7 @@ import {
81
82
 
82
83
  import type { RunOutcome } from "./runs";
83
84
  import { skillInstallCommand, skillListCommand, skillSearchCommand } from "./skill";
85
+ import { cliVersionInfo } from "./version";
84
86
  import { findSourceRoot } from "./source";
85
87
 
86
88
  export type CliIo = {
@@ -242,7 +244,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
242
244
 
243
245
  program
244
246
  .description(
245
- "Preview. Build and operate Toloka pipeline projects from TypeScript source. The public API may change.",
247
+ "Beta. Build and operate Toloka pipeline projects from TypeScript source. The public API may change before general availability.",
246
248
  )
247
249
  .version(SDK_VERSION, "-v, --version", "output the current version")
248
250
  .option(
@@ -282,10 +284,14 @@ export function createProgram(context: ProgramContext): CreatedProgram {
282
284
  warnedAboutStaleSkills = true;
283
285
  context.io.stderr(warning);
284
286
  });
287
+
285
288
  program
286
289
  .command("version")
290
+ .option("--json", "print version and build provenance as JSON", false)
287
291
  .description("print the current CLI version")
288
- .action(() => context.io.stdout(SDK_VERSION));
292
+ .action((options: { json: boolean }) =>
293
+ context.io.stdout(options.json ? JSON.stringify(cliVersionInfo()) : SDK_VERSION),
294
+ );
289
295
 
290
296
  const config = program.command("config").description("read and write machine CLI settings");
291
297
  config
@@ -472,6 +478,26 @@ export function createProgram(context: ProgramContext): CreatedProgram {
472
478
  io: context.io,
473
479
  });
474
480
  });
481
+ project
482
+ .command("untrack")
483
+ .argument("<type>", "pipeline or dataset")
484
+ .argument("<name>", "tracked resource name")
485
+ .option("--confirm", "confirm untracking in a non-interactive environment", false)
486
+ .description("remove a pipeline or dataset from local tracking without deleting it")
487
+ .action(async (type: string, name: string, options: { confirm: boolean }) => {
488
+ assert(
489
+ type === "pipeline" || type === "dataset",
490
+ "Resource type must be pipeline or dataset.",
491
+ );
492
+ const sourceRoot = await resolveSourceRoot(globals(), context.io);
493
+ await untrackProjectResource({
494
+ sourceRoot,
495
+ type,
496
+ name,
497
+ confirm: options.confirm,
498
+ io: context.io,
499
+ });
500
+ });
475
501
  project
476
502
  .command("create")
477
503
  .argument("[alias]", "local name for the new project")