@toloka-platform/sdk 0.2.0-preview.2 → 0.2.0-preview.3

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 (79) hide show
  1. package/README.md +10 -4
  2. package/dist/cli/toloka.js +159 -151
  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 +18 -1
  7. package/docs/cli-v2.md +4 -3
  8. package/docs/cli.md +29 -10
  9. package/docs/getting-started.md +3 -3
  10. package/docs/limitations.md +1 -1
  11. package/docs/testing.md +1 -1
  12. package/docs/troubleshooting.md +1 -1
  13. package/examples/face-image-collection/README.md +2 -2
  14. package/openapi/gen/api.ts +78 -0
  15. package/openapi/gen/models/checkReport.ts +2 -1
  16. package/openapi/gen/models/checkReportAnyOfTwoseven.ts +5 -0
  17. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItem.ts +6 -0
  18. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItemVerdict.ts +2 -0
  19. package/openapi/gen/models/currentUser.ts +1 -0
  20. package/openapi/gen/models/deliveryOrder.ts +9 -0
  21. package/openapi/gen/models/deliveryOrderClientName.ts +2 -0
  22. package/openapi/gen/models/getTrainingRunMetricHistoryParams.ts +4 -0
  23. package/openapi/gen/models/index.ts +19 -0
  24. package/openapi/gen/models/listDeliveryOrders200.ts +5 -0
  25. package/openapi/gen/models/trainingRunMetricHistory.ts +9 -0
  26. package/openapi/gen/models/trainingRunMetricHistoryMetric.ts +2 -0
  27. package/openapi/gen/models/trainingRunMetricHistoryPointsItem.ts +9 -0
  28. package/openapi/gen/models/trainingRunMetricHistoryPointsItemNonFinite.ts +2 -0
  29. package/openapi/gen/models/trainingRunMetricHistoryPointsItemValue.ts +2 -0
  30. package/openapi/gen/models/trainingRunMetrics.ts +9 -0
  31. package/openapi/gen/models/trainingRunMetricsMetricsItem.ts +8 -0
  32. package/openapi/gen/models/trainingRunMetricsMetricsItemLatest.ts +9 -0
  33. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestNonFinite.ts +2 -0
  34. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestValue.ts +2 -0
  35. package/openapi/gen/models/trainingRunMetricsMetricsItemMetric.ts +2 -0
  36. package/openapi/gen/models/trainingRunMetricsUnavailableReason.ts +2 -0
  37. package/package.json +2 -2
  38. package/skills/catalog.json +55 -7
  39. package/skills/toloka-build/SKILL.md +10 -7
  40. package/skills/toloka-build/references/node-semantics.md +6 -4
  41. package/skills/toloka-build/references/workflow-and-cli.md +21 -13
  42. package/skills/toloka-fine-tuning/SKILL.md +72 -0
  43. package/skills/toloka-get-started/SKILL.md +55 -0
  44. package/skills/toloka-gisting/SKILL.md +76 -0
  45. package/skills/toloka-report-issue/references/report-template.md +1 -1
  46. package/src/authoring/index.ts +14 -0
  47. package/src/checks/task-ui-typecheck.ts +12 -10
  48. package/src/cli/analytics.ts +52 -6
  49. package/src/cli/auth.ts +69 -16
  50. package/src/cli/auto-update.ts +24 -33
  51. package/src/cli/check.ts +12 -10
  52. package/src/cli/commands/auth.ts +89 -0
  53. package/src/cli/commands/config.ts +28 -0
  54. package/src/cli/commands/dataset.ts +75 -0
  55. package/src/cli/commands/delivery-order.ts +18 -0
  56. package/src/cli/commands/models.ts +122 -0
  57. package/src/cli/commands/pipeline.ts +132 -0
  58. package/src/cli/commands/project.ts +175 -0
  59. package/src/cli/commands/report.ts +39 -0
  60. package/src/cli/commands/secret.ts +39 -0
  61. package/src/cli/commands/skill.ts +36 -0
  62. package/src/cli/commands/source.ts +204 -0
  63. package/src/cli/commands/status.ts +18 -0
  64. package/src/cli/commands/training.ts +228 -0
  65. package/src/cli/commands/version.ts +14 -0
  66. package/src/cli/config.ts +5 -6
  67. package/src/cli/context.ts +213 -0
  68. package/src/cli/delivery-orders.ts +30 -0
  69. package/src/cli/environment.ts +26 -0
  70. package/src/cli/init.ts +1 -4
  71. package/src/cli/program.ts +168 -1344
  72. package/src/cli/project-context.ts +9 -11
  73. package/src/cli/sdk-version.ts +38 -0
  74. package/src/cli/skill.ts +6 -2
  75. package/src/cli/terminal-io.ts +103 -0
  76. package/src/cli/trainings.ts +118 -27
  77. package/src/cli/version.ts +1 -1
  78. package/src/client.ts +18 -0
  79. package/src/importer.ts +32 -2
@@ -8,7 +8,7 @@ import type { TolokaEnvironment } from "./config";
8
8
  import type { CliEnvironment } from "./credentials";
9
9
  import { credentialsPath, preparePrivateFilePath } from "./credentials";
10
10
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
11
- import { SDK_VERSION } from "./init";
11
+ import { SDK_VERSION } from "./sdk-version";
12
12
 
13
13
  export const POSTHOG_PROJECT_KEY = "phc_kqHt0f53B5MVqURyzWpW1aNlbOmYt1YbUYza0JPcg19";
14
14
  export const POSTHOG_HOST = "https://eu.posthog.com";
@@ -56,6 +56,11 @@ export type PosthogLike = {
56
56
  properties?: Record<string, unknown>;
57
57
  disableGeoip: boolean;
58
58
  }) => void;
59
+ captureException: (
60
+ error: unknown,
61
+ distinctId: string,
62
+ properties?: Record<string, unknown>,
63
+ ) => void;
59
64
  alias: (event: { distinctId: string; alias: string }) => void;
60
65
  flush: () => Promise<void>;
61
66
  on?: (event: "error", listener: (error: Error) => void) => void;
@@ -84,6 +89,7 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
84
89
  results: true,
85
90
  },
86
91
  dataset: { list: true, fields: true, load: true, upload: true },
92
+ "delivery-order": { list: true },
87
93
  secret: { set: true },
88
94
  skill: { list: true, search: true, install: true },
89
95
  config: { get: true, set: true },
@@ -136,14 +142,34 @@ export function commandNameFromArgv(argv: readonly string[]): string {
136
142
  return nameFromTokens(tokens);
137
143
  }
138
144
 
145
+ const findPotatoApiError = (error: unknown): PotatoApiError | undefined => {
146
+ if (error instanceof PotatoApiError) return error;
147
+
148
+ if (error instanceof AggregateError) {
149
+ for (const nested of error.errors) {
150
+ const apiError = findPotatoApiError(nested);
151
+ if (apiError !== undefined) return apiError;
152
+ }
153
+ }
154
+
155
+ return error instanceof Error ? findPotatoApiError(error.cause) : undefined;
156
+ };
157
+
139
158
  export function classifyCliError(error: unknown): { readonly error_code: string } {
140
159
  if (error === undefined) return { error_code: "command_failed" };
141
- if (error instanceof PotatoApiError || error instanceof PipelineGraphValidationError) {
142
- return { error_code: error.code };
143
- }
160
+ const apiError = findPotatoApiError(error);
161
+ if (apiError !== undefined) return { error_code: apiError.code };
162
+ if (error instanceof PipelineGraphValidationError) return { error_code: error.code };
144
163
  return { error_code: error instanceof Error ? "error" : "unknown" };
145
164
  }
146
165
 
166
+ const cliErrorProperties = (error: unknown): Record<string, unknown> => {
167
+ const apiError = findPotatoApiError(error);
168
+ return apiError === undefined
169
+ ? {}
170
+ : { status_code: apiError.status, request_id: apiError.requestId };
171
+ };
172
+
147
173
  const anonymousIdPath = (environment: CliEnvironment): string =>
148
174
  join(dirname(credentialsPath(environment)), "anonymous-id");
149
175
 
@@ -237,18 +263,38 @@ export function createCliAnalytics(options: {
237
263
  .then(() => {
238
264
  if (client === undefined || distinctId === undefined) return;
239
265
  const shared = sharedProperties();
266
+ const command = nameFromTokens(event.command.split(" "));
267
+
240
268
  if (event.outcome === "failed") {
241
269
  const classified = classifyCliError(event.error);
270
+ const errorProperties = cliErrorProperties(event.error);
271
+
272
+ if (event.error !== undefined) {
273
+ const exception = new Error(`CLI command failed: ${classified.error_code}`);
274
+ exception.name = "PotatoCliError";
275
+ client.captureException(exception, distinctId, {
276
+ ...shared,
277
+ command,
278
+ duration_ms: event.durationMs,
279
+ exit_code: event.exitCode,
280
+ $process_person_profile: false,
281
+ $geoip_disable: true,
282
+ ...classified,
283
+ ...errorProperties,
284
+ });
285
+ }
286
+
242
287
  client.capture({
243
288
  distinctId,
244
289
  event: "sdk_cli_command_failed",
245
290
  disableGeoip: true,
246
291
  properties: {
247
292
  ...shared,
248
- command: nameFromTokens(event.command.split(" ")),
293
+ command,
249
294
  duration_ms: event.durationMs,
250
295
  exit_code: event.exitCode,
251
296
  ...classified,
297
+ ...errorProperties,
252
298
  },
253
299
  });
254
300
  return;
@@ -259,7 +305,7 @@ export function createCliAnalytics(options: {
259
305
  disableGeoip: true,
260
306
  properties: {
261
307
  ...shared,
262
- command: nameFromTokens(event.command.split(" ")),
308
+ command,
263
309
  duration_ms: event.durationMs,
264
310
  exit_code: event.exitCode,
265
311
  outcome: event.outcome,
package/src/cli/auth.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { assert } from "es-toolkit";
2
2
  import { createClient } from "../client";
3
+ import { PotatoApiError } from "../errors";
3
4
  import { PRODUCTION_BASE_URL, consoleUrlFor, resolveInternalBaseUrl } from "./config";
4
5
  import {
5
6
  clearStoredCredentials,
@@ -19,6 +20,40 @@ import type {
19
20
  import type { ProjectContext } from "./project-context";
20
21
  import { formatProjectContextLine } from "./project-context";
21
22
 
23
+ type ProbeFailureKind = "rejected" | "unreachable" | "platform";
24
+
25
+ type ProbeFailure = { readonly kind: ProbeFailureKind; readonly reason: string };
26
+
27
+ const classifyProbeFailure = (error: unknown): ProbeFailure => {
28
+ const reason = error instanceof Error ? error.message : String(error);
29
+ if (!(error instanceof PotatoApiError) || error.status === undefined)
30
+ return { kind: "unreachable", reason };
31
+ return {
32
+ kind: error.status === 401 || error.status === 403 ? "rejected" : "platform",
33
+ reason,
34
+ };
35
+ };
36
+
37
+ const PROBE_FAILURE_HEADLINE: Record<ProbeFailureKind, string> = {
38
+ rejected: "Rejected",
39
+ unreachable: "Not reachable",
40
+ platform: "Platform error",
41
+ };
42
+
43
+ const LOGIN_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
44
+ rejected: () => "Check that the key was copied whole and has not been revoked.",
45
+ unreachable: (baseUrl) =>
46
+ `${baseUrl} did not answer, so the key was not evaluated. Check network access, VPN, or proxy settings and retry.`,
47
+ platform: () => "The platform failed before confirming the key. Retry later.",
48
+ };
49
+
50
+ const STATUS_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
51
+ rejected: () => "Run toloka login to store a working key.",
52
+ unreachable: (baseUrl) =>
53
+ `Check network access to ${baseUrl}; the stored key was not evaluated.`,
54
+ platform: () => "The platform failed before confirming the key. Retry later.",
55
+ };
56
+
22
57
  export const apiKeyInstructions = (baseUrl: string): string => {
23
58
  const console = consoleUrlFor(baseUrl);
24
59
  const where =
@@ -54,6 +89,7 @@ export type AuthProbe = {
54
89
  readonly pagination: { readonly total: number };
55
90
  }>;
56
91
  };
92
+ readonly me?: () => Promise<{ readonly internal: boolean }>;
57
93
  };
58
94
 
59
95
  export type LoginOptions = {
@@ -76,16 +112,16 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
76
112
  const write = options.write ?? (() => undefined);
77
113
  const baseUrl = normalizeBaseUrl(
78
114
  options.baseUrl ??
79
- environment.POTATO_BASE_URL ??
115
+ environment.TOLOKA_BASE_URL ??
80
116
  resolveInternalBaseUrl(environment) ??
81
117
  PRODUCTION_BASE_URL,
82
118
  );
83
119
 
84
- let apiKey = options.apiKey ?? environment.POTATO_API_KEY;
120
+ let apiKey = options.apiKey ?? environment.TOLOKA_API_KEY;
85
121
  if (apiKey === undefined) {
86
122
  assert(
87
123
  options.prompt,
88
- "Missing API key. Pass --api-key, or set POTATO_API_KEY, when stdin is not a terminal.",
124
+ "Missing API key. Without a terminal, pipe the key to toloka login --api-key-stdin, pass --api-key, or set TOLOKA_API_KEY.",
89
125
  );
90
126
  write(`\n${apiKeyInstructions(baseUrl)}\n`);
91
127
  apiKey = (await options.prompt("API key: ", { mask: true })).trim();
@@ -99,12 +135,12 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
99
135
  const page = await probe.projects.listPage({ limit: 1 });
100
136
  reachable = `Authenticated against ${baseUrl}: ${page.pagination.total} project(s) visible.`;
101
137
  } catch (error) {
102
- const reason = error instanceof Error ? error.message : String(error);
138
+ const failure = classifyProbeFailure(error);
103
139
  throw new Error(
104
140
  [
105
- `Could not authenticate against ${baseUrl}: ${reason}`,
141
+ `Could not authenticate against ${baseUrl}: ${failure.reason}`,
106
142
  "",
107
- "Nothing was saved. Check that the key was copied whole and has not been revoked.",
143
+ `Nothing was saved. ${LOGIN_FAILURE_ADVICE[failure.kind](baseUrl)}`,
108
144
  ].join("\n"),
109
145
  { cause: error },
110
146
  );
@@ -136,7 +172,7 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
136
172
  reachable,
137
173
  `Saved credentials to ${credentialsFile}.`,
138
174
  "",
139
- "toloka reads them automatically. --api-key and POTATO_API_KEY still win when set.",
175
+ "toloka reads them automatically. --api-key and TOLOKA_API_KEY still win when set.",
140
176
  ].join("\n"),
141
177
  };
142
178
  }
@@ -183,9 +219,9 @@ const resolveApiKey = (
183
219
  options.flagApiKey === undefined
184
220
  ? undefined
185
221
  : { value: options.flagApiKey, source: "from --api-key" },
186
- environment.POTATO_API_KEY === undefined
222
+ environment.TOLOKA_API_KEY === undefined
187
223
  ? undefined
188
- : { value: environment.POTATO_API_KEY, source: "from POTATO_API_KEY" },
224
+ : { value: environment.TOLOKA_API_KEY, source: "from TOLOKA_API_KEY" },
189
225
  found === undefined
190
226
  ? undefined
191
227
  : { value: found.credentials.apiKey, source: `stored in ${storedPath}` },
@@ -202,9 +238,9 @@ const resolvePlatform = (
202
238
  options.flagBaseUrl === undefined
203
239
  ? undefined
204
240
  : { value: options.flagBaseUrl.replace(/\/+$/u, ""), source: "from --base-url" },
205
- environment.POTATO_BASE_URL === undefined
241
+ environment.TOLOKA_BASE_URL === undefined
206
242
  ? undefined
207
- : { value: environment.POTATO_BASE_URL, source: "from POTATO_BASE_URL" },
243
+ : { value: environment.TOLOKA_BASE_URL, source: "from TOLOKA_BASE_URL" },
208
244
  internalBaseUrl === undefined
209
245
  ? undefined
210
246
  : { value: internalBaseUrl, source: `from ENV=${environment.ENV}` },
@@ -213,11 +249,21 @@ const resolvePlatform = (
213
249
  );
214
250
  };
215
251
 
252
+ const internalStatusLine = async (probe: AuthProbe): Promise<string | undefined> => {
253
+ if (probe.me === undefined) return undefined;
254
+ try {
255
+ const me = await probe.me();
256
+ return `Internal: ${me.internal}`;
257
+ } catch {
258
+ return undefined;
259
+ }
260
+ };
261
+
216
262
  export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthStatusResult> {
217
263
  const environment = options.environment ?? process.env;
218
264
  const credentialLookup =
219
265
  options.credentialLookup ??
220
- (await findCommandCredentials(environment, options.flagApiKey ?? environment.POTATO_API_KEY));
266
+ (await findCommandCredentials(environment, options.flagApiKey ?? environment.TOLOKA_API_KEY));
221
267
  const { found, warning } = credentialLookup;
222
268
  const storedPath = found?.path ?? credentialsPath(environment);
223
269
  const apiKey = resolveApiKey(options, environment, found, storedPath);
@@ -244,8 +290,8 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
244
290
  projectLine,
245
291
  );
246
292
 
247
- if (environment.POTATO_API_KEY !== undefined && found !== undefined) {
248
- lines.push("", `POTATO_API_KEY is set, so the key stored in ${found.path} is ignored.`);
293
+ if (environment.TOLOKA_API_KEY !== undefined && found !== undefined) {
294
+ lines.push("", `TOLOKA_API_KEY is set, so the key stored in ${found.path} is ignored.`);
249
295
  }
250
296
  if (found?.legacy === true) {
251
297
  lines.push(
@@ -260,10 +306,17 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
260
306
  try {
261
307
  const page = await probe.projects.listPage({ limit: 1 });
262
308
  lines.push("", `Reachable: ${page.pagination.total} project(s) visible.`);
309
+ const internalLine = await internalStatusLine(probe);
310
+ if (internalLine !== undefined) lines.push(internalLine);
263
311
  return { authenticated: true, output: lines.join("\n") };
264
312
  } catch (error) {
265
- const reason = error instanceof Error ? error.message : String(error);
266
- lines.push("", `Not reachable: ${reason}`, "", "Run toloka login to store a working key.");
313
+ const failure = classifyProbeFailure(error);
314
+ lines.push(
315
+ "",
316
+ `${PROBE_FAILURE_HEADLINE[failure.kind]}: ${failure.reason}`,
317
+ "",
318
+ STATUS_FAILURE_ADVICE[failure.kind](baseUrl),
319
+ );
267
320
  return { authenticated: false, output: lines.join("\n") };
268
321
  }
269
322
  }
@@ -6,16 +6,20 @@ import { join, sep } from "node:path";
6
6
  import { findCliEntry } from "./cli-entry";
7
7
  import type { CliEnvironment } from "./credentials";
8
8
  import { configDirectory, preparePrivateFilePath } from "./credentials";
9
- import { SDK_VERSION } from "./init";
9
+ import {
10
+ compareSdkVersions,
11
+ releaseChannelFor,
12
+ SDK_PACKAGE_NAME,
13
+ SDK_VERSION,
14
+ type ReleaseChannel,
15
+ } from "./sdk-version";
10
16
  import { commandNameFromArgv } from "./analytics";
11
17
  import { readCliSettings } from "./settings";
12
18
  import { CHECK_RESULT_FILE_ENV } from "./check";
13
19
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
14
- import packageMetadata from "../../package.json" with { type: "json" };
15
20
 
16
21
  export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
22
  export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
18
- export const SDK_PACKAGE_NAME = packageMetadata.name;
19
23
  export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
20
24
 
21
25
  const SKIP_COMMANDS: Readonly<Record<string, true>> = {
@@ -75,32 +79,6 @@ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
75
79
  export const npmPackageMetadataUrl = (packageName: string): string =>
76
80
  new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
77
81
 
78
- export const parseSdkVersion = (version: string): readonly number[] | undefined => {
79
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-preview\.(\d+))?$/u.exec(version);
80
- if (match === null) return undefined;
81
- const [, major, minor, patch, preview] = match;
82
- return [
83
- Number(major),
84
- Number(minor),
85
- Number(patch),
86
- preview === undefined ? Number.POSITIVE_INFINITY : Number(preview),
87
- ];
88
- };
89
-
90
- export const compareSdkVersions = (left: string, right: string): number | undefined => {
91
- const parsedLeft = parseSdkVersion(left);
92
- const parsedRight = parseSdkVersion(right);
93
- if (parsedLeft === undefined || parsedRight === undefined) return undefined;
94
- const length = Math.max(parsedLeft.length, parsedRight.length);
95
- for (let index = 0; index < length; index += 1) {
96
- const leftPart = parsedLeft[index] ?? 0;
97
- const rightPart = parsedRight[index] ?? 0;
98
- if (leftPart < rightPart) return -1;
99
- if (leftPart > rightPart) return 1;
100
- }
101
- return 0;
102
- };
103
-
104
82
  const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
105
83
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
106
84
  const record: Record<string, string> = {};
@@ -122,9 +100,22 @@ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined =
122
100
  };
123
101
  };
124
102
 
125
- export const latestPublishedVersion = (view: NpmPackageView): string | undefined => {
126
- const latest = view["dist-tags"]?.latest;
127
- return typeof latest === "string" && latest.length > 0 ? latest : undefined;
103
+ export const latestPublishedVersion = (
104
+ view: NpmPackageView,
105
+ currentVersion: string,
106
+ ): string | undefined => {
107
+ const tags = view["dist-tags"];
108
+ if (tags === undefined) return undefined;
109
+ const channels: readonly ReleaseChannel[] =
110
+ releaseChannelFor(currentVersion) === "preview" ? ["latest", "preview"] : ["latest"];
111
+ let newest: string | undefined;
112
+ for (const channel of channels) {
113
+ const candidate = tags[channel];
114
+ if (candidate === undefined || candidate.length === 0) continue;
115
+ if (newest === undefined || (compareSdkVersions(candidate, newest) ?? 0) > 0)
116
+ newest = candidate;
117
+ }
118
+ return newest;
128
119
  };
129
120
 
130
121
  const publishedAt = (view: NpmPackageView, version: string): Date | undefined => {
@@ -310,7 +301,7 @@ const selectUpdateCandidate = async (options: {
310
301
  return { status: "skip" };
311
302
  }
312
303
 
313
- const version = latestPublishedVersion(view);
304
+ const version = latestPublishedVersion(view, currentVersion);
314
305
  if (version === undefined) return { status: "skip" };
315
306
  const order = compareSdkVersions(currentVersion, version);
316
307
  if (order === undefined || order >= 0) return { status: "skip" };
package/src/cli/check.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  type TestEvidenceKind,
26
26
  } from "../testing/test-evidence";
27
27
  import { resolveCliEntry } from "./cli-entry";
28
+ import { API_KEY_ENVIRONMENT_NAMES } from "./environment";
28
29
  import { loadProjectSource, type ProjectSource } from "./source";
29
30
 
30
31
  export type CheckStatus = "passed" | "failed" | "missing" | "partial" | "infrastructure_error";
@@ -269,18 +270,19 @@ const loadSourceWithoutPlatformCredentials = async (
269
270
  sourceRoot: string,
270
271
  environment: NodeJS.ProcessEnv,
271
272
  ): Promise<ProjectSource> => {
272
- const environments = new Map<NodeJS.ProcessEnv, string | undefined>([
273
- [process.env, process.env.POTATO_API_KEY],
274
- [environment, environment.POTATO_API_KEY],
275
- ]);
276
- for (const current of environments.keys()) delete current.POTATO_API_KEY;
273
+ const targets = [process.env, environment];
274
+ const saved = targets.map((current) => API_KEY_ENVIRONMENT_NAMES.map((name) => current[name]));
275
+ for (const current of targets) for (const name of API_KEY_ENVIRONMENT_NAMES) delete current[name];
277
276
  try {
278
277
  return await loadProjectSource(sourceRoot);
279
278
  } finally {
280
- for (const [current, apiKey] of environments) {
281
- if (apiKey === undefined) delete current.POTATO_API_KEY;
282
- else current.POTATO_API_KEY = apiKey;
283
- }
279
+ targets.forEach((current, target) => {
280
+ API_KEY_ENVIRONMENT_NAMES.forEach((name, index) => {
281
+ const value = saved[target]?.[index];
282
+ if (value === undefined) delete current[name];
283
+ else current[name] = value;
284
+ });
285
+ });
284
286
  }
285
287
  };
286
288
 
@@ -683,7 +685,7 @@ export async function checkProject(
683
685
  try {
684
686
  const selectedPipeline = selected.pipelines.length === 1 ? selected.pipelines[0] : undefined;
685
687
  const checkEnvironment = { ...(options.environment ?? process.env) };
686
- delete checkEnvironment.POTATO_API_KEY;
688
+ for (const name of API_KEY_ENVIRONMENT_NAMES) delete checkEnvironment[name];
687
689
  const environment = {
688
690
  ...checkEnvironment,
689
691
  [TEST_EVIDENCE_DIR_ENV]: evidenceDirectory,
@@ -0,0 +1,89 @@
1
+ import type { Command } from "commander";
2
+ import { createClient } from "../../client";
3
+ import { authStatus, login, logout } from "../auth";
4
+ import {
5
+ identifyClientUser,
6
+ resolveSelectedProject,
7
+ resolveSourceRoot,
8
+ type CommandHost,
9
+ } from "../context";
10
+ import { findCommandCredentials, readStoredCredentials } from "../credentials";
11
+ import { resolveTolokaConfig } from "../config";
12
+ import { readTolokaState } from "../../state";
13
+
14
+ export const registerAuthCommands = (program: Command, host: CommandHost): void => {
15
+ const { context, exitCode, globals } = host;
16
+
17
+ program
18
+ .command("login")
19
+ .description("store an API key for this machine after verifying it")
20
+ .option("--api-key-stdin", "read the API key from standard input instead of prompting", false)
21
+ .action(async (options: { apiKeyStdin: boolean }) => {
22
+ const apiKey = options.apiKeyStdin ? (await context.io.readStdin()).trim() : globals().apiKey;
23
+ const injectedClient = context.injectedClient;
24
+ const result = await login({
25
+ ...(apiKey === undefined ? {} : { apiKey }),
26
+ ...(globals().baseUrl === undefined ? {} : { baseUrl: globals().baseUrl }),
27
+ ...(context.io.prompt === undefined ? {} : { prompt: context.io.prompt }),
28
+ ...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
29
+ write: context.io.stdout,
30
+ environment: context.environment,
31
+ });
32
+ context.io.stdout(result.output);
33
+ try {
34
+ const stored = await readStoredCredentials(context.environment);
35
+ if (stored === undefined) return;
36
+ const client = context.injectedClient ?? createClient(stored);
37
+ await identifyClientUser(client, context.analytics);
38
+ } catch {}
39
+ });
40
+
41
+ program
42
+ .command("logout")
43
+ .description("remove stored credentials from this machine")
44
+ .action(async () => {
45
+ const result = await logout(context.environment);
46
+ context.io.stdout(result.output);
47
+ });
48
+
49
+ program
50
+ .command("auth-status")
51
+ .alias("whoami")
52
+ .description("show which platform and key the CLI would use")
53
+ .option("--show-token", "print the API key instead of masking it", false)
54
+ .action(async (options: { showToken: boolean }) => {
55
+ const flags = globals();
56
+ const credentialLookup = await findCommandCredentials(
57
+ context.environment,
58
+ flags.apiKey ?? context.environment.TOLOKA_API_KEY,
59
+ );
60
+ const stored = credentialLookup.found?.credentials;
61
+ const sourceRoot = await resolveSourceRoot(flags, context.io);
62
+ const state = await readTolokaState(sourceRoot);
63
+ const project = resolveSelectedProject(flags, context, stored, state);
64
+ const injectedClient = context.injectedClient;
65
+
66
+ const result = await authStatus({
67
+ environment: context.environment,
68
+ showToken: options.showToken,
69
+ credentialLookup,
70
+ ...(flags.apiKey === undefined ? {} : { flagApiKey: flags.apiKey }),
71
+ ...(flags.baseUrl === undefined ? {} : { flagBaseUrl: flags.baseUrl }),
72
+ ...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
73
+ ...(project === undefined ? {} : { project }),
74
+ });
75
+
76
+ context.io.stdout(result.output);
77
+ if (!result.authenticated) {
78
+ exitCode.value = 1;
79
+ return;
80
+ }
81
+ try {
82
+ const config = resolveTolokaConfig(flags, state, context.environment, stored);
83
+ const client =
84
+ context.injectedClient ??
85
+ createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
86
+ await identifyClientUser(client, context.analytics);
87
+ } catch {}
88
+ });
89
+ };
@@ -0,0 +1,28 @@
1
+ import type { Command } from "commander";
2
+ import type { CommandHost } from "../context";
3
+ import { getCliSetting, readCliSettings, setCliSetting } from "../settings";
4
+
5
+ export const registerConfigCommands = (program: Command, host: CommandHost): void => {
6
+ const config = program.command("config").description("read and write machine CLI settings");
7
+ config
8
+ .command("get")
9
+ .argument("[name]", "setting name; omit to print every setting")
10
+ .description("print a CLI setting, or every setting")
11
+ .action(async (name?: string) => {
12
+ const settings = await readCliSettings(host.context.environment);
13
+ host.context.io.stdout(getCliSetting(settings, name).output);
14
+ });
15
+ config
16
+ .command("set")
17
+ .argument("<name>", "setting name")
18
+ .argument("<value>", "setting value")
19
+ .description("write a CLI setting for this machine")
20
+ .action(async (name: string, value: string) => {
21
+ const result = await setCliSetting(host.context.environment, name, value);
22
+ host.context.io.stdout(result.output);
23
+ });
24
+ config.action(async () => {
25
+ const settings = await readCliSettings(host.context.environment);
26
+ host.context.io.stdout(getCliSetting(settings, undefined).output);
27
+ });
28
+ };
@@ -0,0 +1,75 @@
1
+ import { assert } from "es-toolkit";
2
+ import { resolve } from "node:path";
3
+ import type { Command } from "commander";
4
+ import { positiveInteger, resolveContext, type CommandHost } from "../context";
5
+ import {
6
+ generateDatasetFieldDeclarations,
7
+ listProjectDatasets,
8
+ loadDataset,
9
+ uploadDataset,
10
+ } from "../data";
11
+ import { MISSING_PROJECT_ID_MESSAGE } from "../project-context";
12
+
13
+ export const registerDatasetCommands = (program: Command, host: CommandHost): void => {
14
+ const { context, globals } = host;
15
+ const dataset = program
16
+ .command("dataset")
17
+ .description("inspect and move dataset catalog and rows");
18
+ dataset
19
+ .command("list")
20
+ .description("list live datasets in the selected project")
21
+ .action(async () => {
22
+ const { client, projectId } = await resolveContext(globals(), context);
23
+ assert(projectId, MISSING_PROJECT_ID_MESSAGE);
24
+ const result = await listProjectDatasets(client, { projectId });
25
+ context.io.stdout(result.output);
26
+ });
27
+ dataset
28
+ .command("fields")
29
+ .argument("<name>", "dataset name or id")
30
+ .description("print field declarations inferred from uploaded rows")
31
+ .action(async (name: string) => {
32
+ const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
33
+ const result = await generateDatasetFieldDeclarations(client, {
34
+ sourceRoot,
35
+ datasetName: name,
36
+ ...(projectId === undefined ? {} : { projectId }),
37
+ });
38
+ context.io.stdout(result.output);
39
+ });
40
+ dataset
41
+ .command("load")
42
+ .argument("<name>", "dataset name or id")
43
+ .requiredOption("--out <file>", "JSONL file to write")
44
+ .option("--limit <n>", "how many rows to keep after download", positiveInteger)
45
+ .option("--timeout <ms>", "how long to wait for the export", positiveInteger)
46
+ .description("download live dataset rows into a JSONL file")
47
+ .action(async (name: string, options: { out: string; limit?: number; timeout?: number }) => {
48
+ const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
49
+ const result = await loadDataset(client, {
50
+ sourceRoot,
51
+ datasetName: name,
52
+ outPath: resolve(options.out),
53
+ ...(projectId === undefined ? {} : { projectId }),
54
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
55
+ ...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
56
+ });
57
+ context.io.stdout(result.output);
58
+ });
59
+ dataset
60
+ .command("upload")
61
+ .argument("<name>", "dataset name or id")
62
+ .argument("<file>", "JSON array, JSONL, or CSV file")
63
+ .description("append rows to a dataset")
64
+ .action(async (name: string, file: string) => {
65
+ const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
66
+ const result = await uploadDataset(client, {
67
+ sourceRoot,
68
+ datasetName: name,
69
+ filePath: resolve(file),
70
+ ...(projectId === undefined ? {} : { projectId }),
71
+ });
72
+ context.io.stdout(result.output);
73
+ if (result.warning !== undefined) context.io.stderr(result.warning);
74
+ });
75
+ };
@@ -0,0 +1,18 @@
1
+ import type { Command } from "commander";
2
+ import { resolveContext, type CommandHost } from "../context";
3
+ import { listDeliveryOrders } from "../delivery-orders";
4
+
5
+ export const registerDeliveryOrderCommands = (program: Command, host: CommandHost): void => {
6
+ const { context, globals } = host;
7
+ const deliveryOrder = program
8
+ .command("delivery-order")
9
+ .description("list Sunday delivery orders for project create");
10
+ deliveryOrder
11
+ .command("list")
12
+ .description("list active Sunday delivery orders this key can attach")
13
+ .action(async () => {
14
+ const { client } = await resolveContext(globals(), context);
15
+ const result = await listDeliveryOrders({ client });
16
+ context.io.stdout(result.output);
17
+ });
18
+ };