@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
@@ -0,0 +1,367 @@
1
+ import { spawn } from "node:child_process";
2
+ import { once } from "node:events";
3
+ import { realpathSync } from "node:fs";
4
+ import { chmod, readFile, writeFile } from "node:fs/promises";
5
+ import { join, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import type { CliEnvironment } from "./credentials";
8
+ import { configDirectory, preparePrivateFilePath } from "./credentials";
9
+ import { SDK_VERSION } from "./init";
10
+ import { commandNameFromArgv } from "./analytics";
11
+ import { readCliSettings } from "./settings";
12
+ import { CHECK_RESULT_FILE_ENV } from "./check";
13
+ import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
14
+ import packageMetadata from "../../package.json" with { type: "json" };
15
+
16
+ export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
+ export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
18
+ export const SDK_PACKAGE_NAME = packageMetadata.name;
19
+ export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
20
+
21
+ const SKIP_COMMANDS: Readonly<Record<string, true>> = {
22
+ config: true,
23
+ help: true,
24
+ version: true,
25
+ };
26
+
27
+ export type AutoUpdateState = {
28
+ readonly lastCheckedAt: string;
29
+ };
30
+
31
+ export type NpmPackageView = {
32
+ readonly "dist-tags"?: Readonly<Record<string, string>>;
33
+ readonly time?: Readonly<Record<string, string>>;
34
+ };
35
+
36
+ export type AutoUpdateInstall = (options: {
37
+ readonly packageName: string;
38
+ readonly version: string;
39
+ readonly registry: string;
40
+ readonly environment: CliEnvironment;
41
+ }) => Promise<void>;
42
+
43
+ export type AutoUpdateReexec = (options: {
44
+ readonly argv: readonly string[];
45
+ readonly environment: CliEnvironment;
46
+ }) => Promise<number>;
47
+
48
+ export type AutoUpdateHooks = {
49
+ readonly now?: () => Date;
50
+ readonly fetchRegistry?: (url: string) => Promise<NpmPackageView>;
51
+ readonly install?: AutoUpdateInstall;
52
+ readonly reexec?: AutoUpdateReexec;
53
+ readonly isGlobalInstall?: () => boolean | Promise<boolean>;
54
+ };
55
+
56
+ export type AutoUpdateResult =
57
+ | { readonly status: "continue" }
58
+ | { readonly status: "reexec"; readonly exitCode: number };
59
+
60
+ type CliIoWriter = {
61
+ readonly stderr: (text: string) => void;
62
+ };
63
+
64
+ type UpdateCheck = { readonly status: "skip" } | { readonly status: "check"; readonly now: Date };
65
+
66
+ type UpdateCandidate =
67
+ | { readonly status: "skip" }
68
+ | { readonly status: "update"; readonly currentVersion: string; readonly version: string };
69
+
70
+ export const autoUpdateStatePath = (environment: CliEnvironment = process.env): string =>
71
+ join(configDirectory(environment), "auto-update.json");
72
+
73
+ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
74
+
75
+ export const npmPackageMetadataUrl = (packageName: string): string =>
76
+ new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
77
+
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
+ const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
105
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
106
+ const record: Record<string, string> = {};
107
+ for (const [key, entry] of Object.entries(value)) {
108
+ if (typeof entry !== "string") return undefined;
109
+ record[key] = entry;
110
+ }
111
+ return record;
112
+ };
113
+
114
+ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined => {
115
+ if (typeof payload !== "object" || payload === null) return undefined;
116
+ const tags = asStringRecord(Reflect.get(payload, "dist-tags"));
117
+ const time = asStringRecord(Reflect.get(payload, "time"));
118
+ if (tags === undefined && time === undefined) return {};
119
+ return {
120
+ ...(tags === undefined ? {} : { "dist-tags": tags }),
121
+ ...(time === undefined ? {} : { time }),
122
+ };
123
+ };
124
+
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;
131
+ return typeof latest === "string" && latest.length > 0 ? latest : undefined;
132
+ };
133
+
134
+ const publishedAt = (view: NpmPackageView, version: string): Date | undefined => {
135
+ const raw = view.time?.[version];
136
+ if (typeof raw !== "string" || raw.length === 0) return undefined;
137
+ const date = new Date(raw);
138
+ return Number.isNaN(date.getTime()) ? undefined : date;
139
+ };
140
+
141
+ const asState = (payload: unknown): AutoUpdateState | undefined => {
142
+ if (typeof payload !== "object" || payload === null) return undefined;
143
+ const lastCheckedAt = Reflect.get(payload, "lastCheckedAt");
144
+ if (typeof lastCheckedAt !== "string" || lastCheckedAt.length === 0) return undefined;
145
+ const date = new Date(lastCheckedAt);
146
+ if (Number.isNaN(date.getTime())) return undefined;
147
+ return { lastCheckedAt };
148
+ };
149
+
150
+ export async function readAutoUpdateState(
151
+ environment: CliEnvironment = process.env,
152
+ ): Promise<AutoUpdateState | undefined> {
153
+ try {
154
+ return asState(JSON.parse(await readFile(autoUpdateStatePath(environment), "utf8")));
155
+ } catch {
156
+ return undefined;
157
+ }
158
+ }
159
+
160
+ export async function writeAutoUpdateState(
161
+ state: AutoUpdateState,
162
+ environment: CliEnvironment = process.env,
163
+ ): Promise<string> {
164
+ const path = autoUpdateStatePath(environment);
165
+ await preparePrivateFilePath(path);
166
+ await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
167
+ await chmod(path, 0o600);
168
+ return path;
169
+ }
170
+
171
+ const BUN_GLOBAL_MARKER = `${sep}install${sep}global${sep}`;
172
+
173
+ export const isGlobalCliInstall = (
174
+ entry = process.argv[1],
175
+ runtimePath = fileURLToPath(import.meta.url),
176
+ ): boolean =>
177
+ [entry, runtimePath].some((path) => {
178
+ if (typeof path !== "string") return false;
179
+ try {
180
+ return realpathSync(path).includes(BUN_GLOBAL_MARKER);
181
+ } catch {
182
+ return path.includes(BUN_GLOBAL_MARKER);
183
+ }
184
+ });
185
+
186
+ export function autoUpdateEnabled(
187
+ environment: CliEnvironment,
188
+ settings: { readonly autoUpdate: boolean },
189
+ ): boolean {
190
+ if (isTruthyFlag(environment.TOLOKA_SKIP_AUTO_UPDATE)) return false;
191
+ const explicit = environment.TOLOKA_AUTO_UPDATE;
192
+ if (isFalseyFlag(explicit)) return false;
193
+ if (isTruthyFlag(explicit)) return true;
194
+ if (environment[CHECK_RESULT_FILE_ENV] !== undefined) return false;
195
+ if (isTruthyFlag(environment.CI)) return false;
196
+ if (isTruthyFlag(environment.VITEST)) return false;
197
+ return settings.autoUpdate;
198
+ }
199
+
200
+ const skippedCommand = (argv: readonly string[]): boolean => {
201
+ const command = commandNameFromArgv(argv);
202
+ return SKIP_COMMANDS[command] === true || command.startsWith("config ");
203
+ };
204
+
205
+ const collectOutput = async (stream: NodeJS.ReadableStream | null): Promise<string> => {
206
+ if (stream === null) return "";
207
+ const chunks: Buffer[] = [];
208
+ stream.on("data", (chunk: Buffer | string) => {
209
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
210
+ });
211
+ await once(stream, "end").catch(() => undefined);
212
+ return Buffer.concat(chunks).toString("utf8");
213
+ };
214
+
215
+ class RegistryError extends Error {
216
+ constructor(message: string) {
217
+ super(message);
218
+ this.name = "RegistryError";
219
+ }
220
+ }
221
+
222
+ const defaultFetchRegistry = async (url: string): Promise<NpmPackageView> => {
223
+ const response = await fetch(url, {
224
+ headers: { accept: "application/json" },
225
+ signal: AbortSignal.timeout(3000),
226
+ });
227
+ if (!response.ok) throw new RegistryError(`npm registry responded ${response.status}`);
228
+ const view = asNpmPackageView(await response.json());
229
+ if (view === undefined) throw new RegistryError("npm registry returned invalid package metadata");
230
+ return view;
231
+ };
232
+
233
+ const defaultInstall: AutoUpdateInstall = async ({
234
+ packageName,
235
+ version,
236
+ registry,
237
+ environment,
238
+ }) => {
239
+ const child = spawn(
240
+ process.execPath,
241
+ ["add", "--global", `${packageName}@${version}`, `--registry=${registry}`],
242
+ {
243
+ env: { ...process.env, ...environment },
244
+ stdio: ["ignore", "pipe", "pipe"],
245
+ },
246
+ );
247
+ const [close, stdout, stderr] = await Promise.all([
248
+ once(child, "close"),
249
+ collectOutput(child.stdout),
250
+ collectOutput(child.stderr),
251
+ ]);
252
+ const exitCode = close[0];
253
+ if (exitCode === 0) return;
254
+ const detail = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
255
+ throw new Error(
256
+ detail.length > 0
257
+ ? `bun add --global failed: ${detail}`
258
+ : `bun add --global exited ${String(exitCode)}`,
259
+ );
260
+ };
261
+
262
+ const defaultReexec: AutoUpdateReexec = async ({ argv, environment }) => {
263
+ const entry = process.argv[1];
264
+ const child = spawn(process.execPath, entry === undefined ? [...argv] : [entry, ...argv], {
265
+ env: { ...process.env, ...environment, TOLOKA_SKIP_AUTO_UPDATE: "1" },
266
+ stdio: "inherit",
267
+ });
268
+ const [code] = await once(child, "close");
269
+ return typeof code === "number" ? code : 1;
270
+ };
271
+
272
+ const continueResult = (): AutoUpdateResult => ({ status: "continue" });
273
+
274
+ const shouldCheckForUpdate = async (options: {
275
+ readonly argv: readonly string[];
276
+ readonly environment: CliEnvironment;
277
+ readonly hooks: AutoUpdateHooks;
278
+ }): Promise<UpdateCheck> => {
279
+ const { argv, environment, hooks } = options;
280
+ if (skippedCommand(argv)) return { status: "skip" };
281
+ const isGlobal = hooks.isGlobalInstall ?? (() => isGlobalCliInstall());
282
+ if (!(await isGlobal())) return { status: "skip" };
283
+ const settings = await readCliSettings(environment);
284
+ if (!autoUpdateEnabled(environment, settings)) return { status: "skip" };
285
+
286
+ const now = hooks.now?.() ?? new Date();
287
+ const state = await readAutoUpdateState(environment);
288
+ if (state !== undefined) {
289
+ const lastCheckedAt = new Date(state.lastCheckedAt).getTime();
290
+ if (now.getTime() - lastCheckedAt < AUTO_UPDATE_INTERVAL_MS) return { status: "skip" };
291
+ }
292
+ return { status: "check", now };
293
+ };
294
+
295
+ const selectUpdateCandidate = async (options: {
296
+ readonly environment: CliEnvironment;
297
+ readonly io: CliIoWriter;
298
+ readonly now: Date;
299
+ readonly currentVersion: string;
300
+ readonly fetchRegistry: (url: string) => Promise<NpmPackageView>;
301
+ }): Promise<UpdateCandidate> => {
302
+ const { environment, io, now, currentVersion, fetchRegistry } = options;
303
+ try {
304
+ await writeAutoUpdateState({ lastCheckedAt: now.toISOString() }, environment);
305
+ } catch {}
306
+
307
+ let view: NpmPackageView;
308
+ try {
309
+ view = await fetchRegistry(npmPackageMetadataUrl(SDK_PACKAGE_NAME));
310
+ } catch (error) {
311
+ io.stderr(
312
+ `Auto-update check failed: ${error instanceof Error ? error.message : String(error)}.`,
313
+ );
314
+ return { status: "skip" };
315
+ }
316
+
317
+ const version = latestPublishedVersion(view);
318
+ if (version === undefined) return { status: "skip" };
319
+ const order = compareSdkVersions(currentVersion, version);
320
+ if (order === undefined || order >= 0) return { status: "skip" };
321
+ const published = publishedAt(view, version);
322
+ if (published === undefined) return { status: "skip" };
323
+ if (now.getTime() - published.getTime() < AUTO_UPDATE_QUARANTINE_MS) return { status: "skip" };
324
+ return { status: "update", currentVersion, version };
325
+ };
326
+
327
+ export async function maybeAutoUpdate(options: {
328
+ readonly argv: readonly string[];
329
+ readonly environment: CliEnvironment;
330
+ readonly io: CliIoWriter;
331
+ readonly currentVersion?: string;
332
+ readonly hooks?: AutoUpdateHooks;
333
+ }): Promise<AutoUpdateResult> {
334
+ const { argv, environment, io, hooks = {} } = options;
335
+ const check = await shouldCheckForUpdate({ argv, environment, hooks });
336
+ if (check.status === "skip") return continueResult();
337
+
338
+ const candidate = await selectUpdateCandidate({
339
+ environment,
340
+ io,
341
+ now: check.now,
342
+ currentVersion: options.currentVersion ?? SDK_VERSION,
343
+ fetchRegistry: hooks.fetchRegistry ?? defaultFetchRegistry,
344
+ });
345
+ if (candidate.status === "skip") return continueResult();
346
+
347
+ const install = hooks.install ?? defaultInstall;
348
+ try {
349
+ await install({
350
+ packageName: SDK_PACKAGE_NAME,
351
+ version: candidate.version,
352
+ registry: npmRegistryUrl(),
353
+ environment,
354
+ });
355
+ } catch (error) {
356
+ io.stderr(
357
+ `Auto-update to ${candidate.version} failed: ${error instanceof Error ? error.message : String(error)}.`,
358
+ );
359
+ return continueResult();
360
+ }
361
+
362
+ io.stderr(
363
+ `Updated ${SDK_PACKAGE_NAME} from ${candidate.currentVersion} to ${candidate.version}.`,
364
+ );
365
+ const reexec = hooks.reexec ?? defaultReexec;
366
+ return { status: "reexec", exitCode: await reexec({ argv, environment }) };
367
+ }
@@ -0,0 +1,13 @@
1
+ export const isTruthyFlag = (value: string | undefined): boolean => {
2
+ if (value === undefined) return false;
3
+ const normalized = value.trim().toLowerCase();
4
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
5
+ };
6
+
7
+ export const isFalseyFlag = (value: string | undefined): boolean => {
8
+ if (value === undefined) return false;
9
+ const normalized = value.trim().toLowerCase();
10
+ return (
11
+ normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off"
12
+ );
13
+ };