@toloka-platform/sdk 0.2.0 → 0.2.1

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 (40) hide show
  1. package/dist/cli/toloka.js +156 -137
  2. package/dist/cli/toloka.js.jsc +0 -0
  3. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  4. package/dist/cli/toloka.js.sha256 +1 -1
  5. package/docs/authoring.md +2 -0
  6. package/docs/cli.md +13 -3
  7. package/openapi/gen/api.ts +30 -0
  8. package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +1 -1
  9. package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +1 -1
  10. package/openapi/gen/models/index.ts +3 -0
  11. package/openapi/gen/models/listAudienceSpecializations200.ts +7 -0
  12. package/openapi/gen/models/listAudienceSpecializationsParams.ts +5 -0
  13. package/openapi/gen/models/specialization.ts +6 -0
  14. package/openapi/gen/models/trainingCostEstimateOneOfTwoseven.ts +0 -2
  15. package/openapi/gen/models/trainingRunQuote.ts +0 -2
  16. package/package.json +1 -1
  17. package/skills/catalog.json +8 -8
  18. package/skills/toloka-build/references/node-semantics.md +2 -2
  19. package/skills/toloka-build/references/workflow-and-cli.md +6 -3
  20. package/skills/toloka-fine-tuning/SKILL.md +2 -0
  21. package/skills/toloka-gisting/SKILL.md +3 -1
  22. package/src/checks/task-ui-typecheck.ts +4 -1
  23. package/src/cli/analytics.ts +1 -0
  24. package/src/cli/audience.ts +31 -0
  25. package/src/cli/auto-update.ts +363 -24
  26. package/src/cli/check.ts +51 -52
  27. package/src/cli/cli-entry.ts +31 -0
  28. package/src/cli/commands/audience.ts +22 -0
  29. package/src/cli/commands/training.ts +10 -2
  30. package/src/cli/context.ts +7 -2
  31. package/src/cli/init.ts +7 -2
  32. package/src/cli/models.ts +1 -21
  33. package/src/cli/native-runtime.ts +7 -0
  34. package/src/cli/program.ts +2 -0
  35. package/src/cli/skill.ts +34 -26
  36. package/src/cli/table.ts +21 -0
  37. package/src/cli/trainings.ts +1 -22
  38. package/src/cli/version.ts +12 -1
  39. package/src/client.ts +17 -0
  40. package/src/index.ts +1 -0
@@ -1,9 +1,23 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
1
2
  import { spawn } from "node:child_process";
2
3
  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 { findCliEntry } from "./cli-entry";
4
+ import { constants, createReadStream, realpathSync } from "node:fs";
5
+ import {
6
+ chmod,
7
+ lstat,
8
+ mkdir,
9
+ mkdtemp,
10
+ open,
11
+ readFile,
12
+ realpath,
13
+ rename,
14
+ rm,
15
+ writeFile,
16
+ } from "node:fs/promises";
17
+ import { dirname, join, sep } from "node:path";
18
+ import { Readable, Transform } from "node:stream";
19
+ import { pipeline } from "node:stream/promises";
20
+ import { cliRelaunchCommand, findCliEntry } from "./cli-entry";
7
21
  import type { CliEnvironment } from "./credentials";
8
22
  import { configDirectory, preparePrivateFilePath } from "./credentials";
9
23
  import {
@@ -19,6 +33,7 @@ import { CHECK_RESULT_FILE_ENV } from "./check";
19
33
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
20
34
  import { TolokaCliError } from "./cli-error";
21
35
  import { formatCliError } from "./format-error";
36
+ import { bunSubprocessEnvironment, isNativeCli } from "./native-runtime";
22
37
 
23
38
  export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
24
39
  export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
@@ -34,9 +49,15 @@ export type AutoUpdateState = {
34
49
  readonly lastCheckedAt: string;
35
50
  };
36
51
 
52
+ export type NpmPackageDistribution = {
53
+ readonly tarball: string;
54
+ readonly integrity: string;
55
+ };
56
+
37
57
  export type NpmPackageView = {
38
58
  readonly "dist-tags"?: Readonly<Record<string, string>>;
39
59
  readonly time?: Readonly<Record<string, string>>;
60
+ readonly versions?: Readonly<Record<string, { readonly dist?: NpmPackageDistribution }>>;
40
61
  };
41
62
 
42
63
  export type AutoUpdateInstall = (options: {
@@ -44,6 +65,7 @@ export type AutoUpdateInstall = (options: {
44
65
  readonly version: string;
45
66
  readonly registry: string;
46
67
  readonly environment: CliEnvironment;
68
+ readonly distribution?: NpmPackageDistribution;
47
69
  }) => Promise<void>;
48
70
 
49
71
  export type AutoUpdateReexec = (options: {
@@ -54,9 +76,13 @@ export type AutoUpdateReexec = (options: {
54
76
  export type AutoUpdateHooks = {
55
77
  readonly now?: () => Date;
56
78
  readonly fetchRegistry?: (url: string) => Promise<NpmPackageView>;
79
+ readonly fetchTarball?: (url: string, init: RequestInit) => Promise<Response>;
57
80
  readonly install?: AutoUpdateInstall;
58
81
  readonly reexec?: AutoUpdateReexec;
59
82
  readonly isGlobalInstall?: () => boolean | Promise<boolean>;
83
+ readonly isNative?: () => boolean;
84
+ readonly executablePath?: () => string;
85
+ readonly runtimeExecutable?: string;
60
86
  };
61
87
 
62
88
  export type AutoUpdateResult =
@@ -71,7 +97,12 @@ type UpdateCheck = { readonly status: "skip" } | { readonly status: "check"; rea
71
97
 
72
98
  type UpdateCandidate =
73
99
  | { readonly status: "skip" }
74
- | { readonly status: "update"; readonly currentVersion: string; readonly version: string };
100
+ | {
101
+ readonly status: "update";
102
+ readonly currentVersion: string;
103
+ readonly version: string;
104
+ readonly distribution?: NpmPackageDistribution;
105
+ };
75
106
 
76
107
  export const autoUpdateStatePath = (environment: CliEnvironment = process.env): string =>
77
108
  join(configDirectory(environment), "auto-update.json");
@@ -80,6 +111,31 @@ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
80
111
 
81
112
  export const npmPackageMetadataUrl = (packageName: string): string =>
82
113
  new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
114
+ const NATIVE_CLI_TARGETS: Readonly<Record<string, true>> = {
115
+ "darwin-arm64": true,
116
+ "darwin-x64": true,
117
+ "linux-arm64": true,
118
+ "linux-x64": true,
119
+ };
120
+
121
+ export const nativeCliPackageName = (
122
+ platform = process.platform,
123
+ architecture = process.arch,
124
+ libc?: "glibc" | "musl",
125
+ ): string => {
126
+ if (platform === "linux" && libc === "musl") {
127
+ throw new TolokaCliError("The standalone Toloka CLI supports glibc Linux, not musl Linux.", {
128
+ hint: { disabled: true },
129
+ });
130
+ }
131
+ if (!Object.hasOwn(NATIVE_CLI_TARGETS, `${platform}-${architecture}`)) {
132
+ throw new TolokaCliError(
133
+ `The standalone Toloka CLI does not support ${platform}-${architecture}.`,
134
+ { hint: { disabled: true } },
135
+ );
136
+ }
137
+ return `@toloka-platform/cli-${platform}-${architecture}`;
138
+ };
83
139
 
84
140
  const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
85
141
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
@@ -90,15 +146,41 @@ const asStringRecord = (value: unknown): Readonly<Record<string, string>> | unde
90
146
  }
91
147
  return record;
92
148
  };
149
+ const asNpmPackageDistribution = (value: unknown): NpmPackageDistribution | undefined => {
150
+ if (typeof value !== "object" || value === null) return undefined;
151
+ const tarball = Reflect.get(value, "tarball");
152
+ const integrity = Reflect.get(value, "integrity");
153
+ return typeof tarball === "string" &&
154
+ tarball.length > 0 &&
155
+ typeof integrity === "string" &&
156
+ integrity.length > 0
157
+ ? { tarball, integrity }
158
+ : undefined;
159
+ };
160
+
161
+ const asNpmPackageVersions = (
162
+ value: unknown,
163
+ ): Readonly<Record<string, { readonly dist?: NpmPackageDistribution }>> | undefined => {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
165
+ const versions: Record<string, { readonly dist?: NpmPackageDistribution }> = {};
166
+ for (const [version, manifest] of Object.entries(value)) {
167
+ if (typeof manifest !== "object" || manifest === null) continue;
168
+ const distribution = asNpmPackageDistribution(Reflect.get(manifest, "dist"));
169
+ versions[version] = distribution === undefined ? {} : { dist: distribution };
170
+ }
171
+ return versions;
172
+ };
93
173
 
94
174
  export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined => {
95
175
  if (typeof payload !== "object" || payload === null) return undefined;
96
176
  const tags = asStringRecord(Reflect.get(payload, "dist-tags"));
97
177
  const time = asStringRecord(Reflect.get(payload, "time"));
98
- if (tags === undefined && time === undefined) return {};
178
+ const versions = asNpmPackageVersions(Reflect.get(payload, "versions"));
179
+ if (tags === undefined && time === undefined && versions === undefined) return {};
99
180
  return {
100
181
  ...(tags === undefined ? {} : { "dist-tags": tags }),
101
182
  ...(time === undefined ? {} : { time }),
183
+ ...(versions === undefined ? {} : { versions }),
102
184
  };
103
185
  };
104
186
 
@@ -219,7 +301,228 @@ const defaultFetchRegistry = async (url: string): Promise<NpmPackageView> => {
219
301
  return view;
220
302
  };
221
303
 
222
- const defaultInstall: AutoUpdateInstall = async ({
304
+ type IntegrityAlgorithm = "sha512" | "sha384" | "sha256";
305
+
306
+ const INTEGRITY_ALGORITHMS: readonly IntegrityAlgorithm[] = ["sha512", "sha384", "sha256"];
307
+
308
+ const parseIntegrity = (
309
+ integrity: string,
310
+ ): { readonly algorithm: IntegrityAlgorithm; readonly digest: Buffer } => {
311
+ const tokens = integrity.split(/\s+/u);
312
+ for (const algorithm of INTEGRITY_ALGORITHMS) {
313
+ const prefix = `${algorithm}-`;
314
+ const token = tokens.find((candidate) => candidate.startsWith(prefix));
315
+ if (token === undefined) continue;
316
+ const encoded = token.slice(prefix.length);
317
+ if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) continue;
318
+ const digest = Buffer.from(encoded, "base64");
319
+ if (
320
+ digest.length > 0 &&
321
+ digest.toString("base64").replace(/=+$/u, "") === encoded.replace(/=+$/u, "")
322
+ ) {
323
+ return { algorithm, digest };
324
+ }
325
+ }
326
+ throw new RegistryError("npm registry returned unsupported package integrity metadata");
327
+ };
328
+
329
+ const downloadVerifiedTarball = async (
330
+ distribution: NpmPackageDistribution,
331
+ target: string,
332
+ fetchTarball: (url: string, init: RequestInit) => Promise<Response>,
333
+ ): Promise<void> => {
334
+ const tarballUrl = new URL(distribution.tarball, DEFAULT_NPM_REGISTRY);
335
+ const registryUrl = new URL(DEFAULT_NPM_REGISTRY);
336
+ if (tarballUrl.protocol !== "https:" || tarballUrl.origin !== registryUrl.origin) {
337
+ throw new RegistryError("npm registry returned a tarball outside registry.npmjs.org");
338
+ }
339
+ const response = await fetchTarball(tarballUrl.toString(), {
340
+ headers: { accept: "application/octet-stream" },
341
+ redirect: "error",
342
+ signal: AbortSignal.timeout(30_000),
343
+ });
344
+ if (!response.ok || response.body === null) {
345
+ throw new RegistryError(`npm registry tarball download responded ${response.status}`);
346
+ }
347
+ const expected = parseIntegrity(distribution.integrity);
348
+ const hash = createHash(expected.algorithm);
349
+ const verify = new Transform({
350
+ transform(chunk: Buffer, _encoding, callback) {
351
+ hash.update(chunk);
352
+ callback(null, chunk);
353
+ },
354
+ });
355
+ const handle = await open(
356
+ target,
357
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
358
+ 0o600,
359
+ );
360
+ await pipeline(Readable.from(response.body), verify, handle.createWriteStream());
361
+ const actual = hash.digest();
362
+ if (actual.length !== expected.digest.length || !timingSafeEqual(actual, expected.digest)) {
363
+ throw new RegistryError("npm package tarball failed its registry integrity check");
364
+ }
365
+ };
366
+
367
+ const runEmbeddedBunInstall = async (options: {
368
+ readonly stagingDirectory: string;
369
+ readonly tarballPath: string;
370
+ readonly registry: string;
371
+ readonly environment: CliEnvironment;
372
+ readonly runtimeExecutable: string;
373
+ }): Promise<void> => {
374
+ const { stagingDirectory, tarballPath, registry, environment } = options;
375
+ const home = join(stagingDirectory, "home");
376
+ const config = join(stagingDirectory, "bunfig.toml");
377
+ const npmConfig = join(stagingDirectory, "npmrc");
378
+ await mkdir(home);
379
+ await Promise.all([
380
+ writeFile(join(stagingDirectory, "package.json"), `${JSON.stringify({ private: true })}\n`),
381
+ writeFile(
382
+ config,
383
+ `env = false\ntelemetry = false\n\n[install]\nregistry = "${registry}"\nignoreScripts = true\noffline = true\noptional = false\npeer = false\ndev = false\n\n[install.cache]\ndir = "./cache"\ndisable = true\ndisableManifest = true\n\n[install.lockfile]\nsave = false\n`,
384
+ ),
385
+ writeFile(npmConfig, ""),
386
+ ]);
387
+ const child = spawn(
388
+ options.runtimeExecutable,
389
+ [
390
+ "add",
391
+ tarballPath,
392
+ `--cwd=${stagingDirectory}`,
393
+ `--config=${config}`,
394
+ `--registry=${registry}`,
395
+ "--ignore-scripts",
396
+ "--offline",
397
+ "--no-save",
398
+ "--linker=hoisted",
399
+ ],
400
+ {
401
+ cwd: stagingDirectory,
402
+ env: bunSubprocessEnvironment(
403
+ {
404
+ ...process.env,
405
+ ...environment,
406
+ HOME: home,
407
+ XDG_CONFIG_HOME: home,
408
+ NPM_CONFIG_USERCONFIG: npmConfig,
409
+ BUN_INSTALL_CACHE_DIR: join(stagingDirectory, "cache"),
410
+ },
411
+ true,
412
+ ),
413
+ stdio: ["ignore", "pipe", "pipe"],
414
+ },
415
+ );
416
+ const [close, stdout, stderr] = await Promise.all([
417
+ once(child, "close"),
418
+ collectOutput(child.stdout),
419
+ collectOutput(child.stderr),
420
+ ]);
421
+ const exitCode = close[0];
422
+ if (exitCode === 0) return;
423
+ const detail = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
424
+ throw new TolokaCliError(
425
+ detail.length > 0
426
+ ? `Embedded Bun package staging failed: ${detail}`
427
+ : `Embedded Bun package staging exited ${String(exitCode)}`,
428
+ {
429
+ hint: {
430
+ description: "Reinstall the Toloka CLI, or disable automatic updates in toloka config.",
431
+ },
432
+ },
433
+ );
434
+ };
435
+
436
+ const defaultNativeInstall = async (options: {
437
+ readonly packageName: string;
438
+ readonly version: string;
439
+ readonly registry: string;
440
+ readonly environment: CliEnvironment;
441
+ readonly distribution: NpmPackageDistribution;
442
+ readonly executablePath: string;
443
+ readonly fetchTarball: (url: string, init: RequestInit) => Promise<Response>;
444
+ readonly runtimeExecutable: string;
445
+ }): Promise<void> => {
446
+ const executablePath = await realpath(options.executablePath);
447
+ const currentExecutable = await lstat(executablePath);
448
+ if (!currentExecutable.isFile()) {
449
+ throw new TolokaCliError("Refusing to replace a non-file Toloka executable.", {
450
+ hint: { disabled: true },
451
+ });
452
+ }
453
+ const stagingDirectory = await mkdtemp(join(dirname(executablePath), ".toloka-update-"));
454
+ try {
455
+ const tarballPath = join(stagingDirectory, "package.tgz");
456
+ await downloadVerifiedTarball(options.distribution, tarballPath, options.fetchTarball);
457
+ await runEmbeddedBunInstall({
458
+ stagingDirectory,
459
+ tarballPath,
460
+ registry: options.registry,
461
+ environment: options.environment,
462
+ runtimeExecutable: options.runtimeExecutable,
463
+ });
464
+ const packageDirectory = join(
465
+ stagingDirectory,
466
+ "node_modules",
467
+ ...options.packageName.split("/"),
468
+ );
469
+ const manifest: unknown = JSON.parse(
470
+ await readFile(join(packageDirectory, "package.json"), "utf8"),
471
+ );
472
+ if (
473
+ typeof manifest !== "object" ||
474
+ manifest === null ||
475
+ Reflect.get(manifest, "name") !== options.packageName ||
476
+ Reflect.get(manifest, "version") !== options.version
477
+ ) {
478
+ throw new TolokaCliError("Staged native CLI package has unexpected metadata.", {
479
+ hint: { disabled: true },
480
+ });
481
+ }
482
+ const candidate = join(packageDirectory, "bin", "toloka");
483
+ const checksumPath = join(packageDirectory, "bin", "toloka.sha256");
484
+ const [candidateStats, checksumStats, checksumSource] = await Promise.all([
485
+ lstat(candidate),
486
+ lstat(checksumPath),
487
+ readFile(checksumPath, "utf8"),
488
+ ]);
489
+ if (!candidateStats.isFile() || !checksumStats.isFile()) {
490
+ throw new TolokaCliError("Staged native CLI package has an invalid artifact layout.", {
491
+ hint: { disabled: true },
492
+ });
493
+ }
494
+ const expectedChecksum = /^([a-f0-9]{64}) toloka\n$/u.exec(checksumSource)?.[1];
495
+ if (expectedChecksum === undefined) {
496
+ throw new TolokaCliError("Staged native CLI package has an invalid checksum file.", {
497
+ hint: { disabled: true },
498
+ });
499
+ }
500
+ const hash = createHash("sha256");
501
+ for await (const chunk of createReadStream(candidate)) hash.update(chunk);
502
+ const actualChecksum = hash.digest();
503
+ const expectedChecksumBytes = Buffer.from(expectedChecksum, "hex");
504
+ if (
505
+ actualChecksum.length !== expectedChecksumBytes.length ||
506
+ !timingSafeEqual(actualChecksum, expectedChecksumBytes)
507
+ ) {
508
+ throw new TolokaCliError("Staged native CLI executable failed its checksum.", {
509
+ hint: { disabled: true },
510
+ });
511
+ }
512
+ await chmod(candidate, currentExecutable.mode & 0o777);
513
+ const candidateHandle = await open(candidate, "r");
514
+ try {
515
+ await candidateHandle.sync();
516
+ } finally {
517
+ await candidateHandle.close();
518
+ }
519
+ await rename(candidate, executablePath);
520
+ } finally {
521
+ await rm(stagingDirectory, { recursive: true, force: true });
522
+ }
523
+ };
524
+
525
+ const defaultJsInstall: AutoUpdateInstall = async ({
223
526
  packageName,
224
527
  version,
225
528
  registry,
@@ -255,9 +558,13 @@ const defaultInstall: AutoUpdateInstall = async ({
255
558
  };
256
559
 
257
560
  const defaultReexec: AutoUpdateReexec = async ({ argv, environment }) => {
258
- const entry = process.argv[1];
259
- const child = spawn(process.execPath, entry === undefined ? [...argv] : [entry, ...argv], {
260
- env: { ...process.env, ...environment, TOLOKA_SKIP_AUTO_UPDATE: "1" },
561
+ const command = cliRelaunchCommand(argv, {
562
+ ...process.env,
563
+ ...environment,
564
+ TOLOKA_SKIP_AUTO_UPDATE: "1",
565
+ });
566
+ const child = spawn(command.executable, command.args, {
567
+ env: command.environment,
261
568
  stdio: "inherit",
262
569
  });
263
570
  const [code] = await once(child, "close");
@@ -270,11 +577,14 @@ const shouldCheckForUpdate = async (options: {
270
577
  readonly argv: readonly string[];
271
578
  readonly environment: CliEnvironment;
272
579
  readonly hooks: AutoUpdateHooks;
580
+ readonly native: boolean;
273
581
  }): Promise<UpdateCheck> => {
274
- const { argv, environment, hooks } = options;
582
+ const { argv, environment, hooks, native } = options;
275
583
  if (skippedCommand(argv)) return { status: "skip" };
276
- const isGlobal = hooks.isGlobalInstall ?? (() => isGlobalCliInstall());
277
- if (!(await isGlobal())) return { status: "skip" };
584
+ if (!native) {
585
+ const isGlobal = hooks.isGlobalInstall ?? (() => isGlobalCliInstall());
586
+ if (!(await isGlobal())) return { status: "skip" };
587
+ }
278
588
  const settings = await readCliSettings(environment);
279
589
  if (!autoUpdateEnabled(environment, settings)) return { status: "skip" };
280
590
 
@@ -293,15 +603,16 @@ const selectUpdateCandidate = async (options: {
293
603
  readonly now: Date;
294
604
  readonly currentVersion: string;
295
605
  readonly fetchRegistry: (url: string) => Promise<NpmPackageView>;
606
+ readonly packageName: string;
296
607
  }): Promise<UpdateCandidate> => {
297
- const { environment, io, now, currentVersion, fetchRegistry } = options;
608
+ const { environment, io, now, currentVersion, fetchRegistry, packageName } = options;
298
609
  try {
299
610
  await writeAutoUpdateState({ lastCheckedAt: now.toISOString() }, environment);
300
611
  } catch {}
301
612
 
302
613
  let view: NpmPackageView;
303
614
  try {
304
- view = await fetchRegistry(npmPackageMetadataUrl(SDK_PACKAGE_NAME));
615
+ view = await fetchRegistry(npmPackageMetadataUrl(packageName));
305
616
  } catch (error) {
306
617
  io.stderr(
307
618
  `Auto-update check failed: ${error instanceof Error ? error.message : String(error)}.`,
@@ -316,7 +627,12 @@ const selectUpdateCandidate = async (options: {
316
627
  const published = publishedAt(view, version);
317
628
  if (published === undefined) return { status: "skip" };
318
629
  if (now.getTime() - published.getTime() < AUTO_UPDATE_QUARANTINE_MS) return { status: "skip" };
319
- return { status: "update", currentVersion, version };
630
+ return {
631
+ status: "update",
632
+ currentVersion,
633
+ version,
634
+ distribution: view.versions?.[version]?.dist,
635
+ };
320
636
  };
321
637
 
322
638
  export async function maybeAutoUpdate(options: {
@@ -327,8 +643,16 @@ export async function maybeAutoUpdate(options: {
327
643
  readonly hooks?: AutoUpdateHooks;
328
644
  }): Promise<AutoUpdateResult> {
329
645
  const { argv, environment, io, hooks = {} } = options;
330
- const check = await shouldCheckForUpdate({ argv, environment, hooks });
646
+ const native = hooks.isNative?.() ?? isNativeCli();
647
+ const check = await shouldCheckForUpdate({ argv, environment, hooks, native });
331
648
  if (check.status === "skip") return continueResult();
649
+ let packageName: string;
650
+ try {
651
+ packageName = native ? nativeCliPackageName() : SDK_PACKAGE_NAME;
652
+ } catch (error) {
653
+ io.stderr(`Auto-update check failed: ${formatCliError(error)}`);
654
+ return continueResult();
655
+ }
332
656
 
333
657
  const candidate = await selectUpdateCandidate({
334
658
  environment,
@@ -336,25 +660,40 @@ export async function maybeAutoUpdate(options: {
336
660
  now: check.now,
337
661
  currentVersion: options.currentVersion ?? SDK_VERSION,
338
662
  fetchRegistry: hooks.fetchRegistry ?? defaultFetchRegistry,
663
+ packageName,
339
664
  });
340
665
  if (candidate.status === "skip") return continueResult();
341
666
 
342
- const install = hooks.install ?? defaultInstall;
343
667
  try {
344
- await install({
345
- packageName: SDK_PACKAGE_NAME,
668
+ const installOptions = {
669
+ packageName,
346
670
  version: candidate.version,
347
671
  registry: npmRegistryUrl(),
348
672
  environment,
349
- });
673
+ ...(candidate.distribution === undefined ? {} : { distribution: candidate.distribution }),
674
+ };
675
+ if (hooks.install !== undefined) {
676
+ await hooks.install(installOptions);
677
+ } else if (native) {
678
+ if (candidate.distribution === undefined) {
679
+ throw new RegistryError("npm registry metadata has no native CLI distribution");
680
+ }
681
+ await defaultNativeInstall({
682
+ ...installOptions,
683
+ distribution: candidate.distribution,
684
+ executablePath: hooks.executablePath?.() ?? process.execPath,
685
+ fetchTarball: hooks.fetchTarball ?? fetch,
686
+ runtimeExecutable: hooks.runtimeExecutable ?? process.execPath,
687
+ });
688
+ } else {
689
+ await defaultJsInstall(installOptions);
690
+ }
350
691
  } catch (error) {
351
692
  io.stderr(`Auto-update to ${candidate.version} failed: ${formatCliError(error)}`);
352
693
  return continueResult();
353
694
  }
354
695
 
355
- io.stderr(
356
- `Updated ${SDK_PACKAGE_NAME} from ${candidate.currentVersion} to ${candidate.version}.`,
357
- );
696
+ io.stderr(`Updated ${packageName} from ${candidate.currentVersion} to ${candidate.version}.`);
358
697
  const reexec = hooks.reexec ?? defaultReexec;
359
698
  return { status: "reexec", exitCode: await reexec({ argv, environment }) };
360
699
  }