@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.
- package/dist/cli/toloka.js +156 -137
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/authoring.md +2 -0
- package/docs/cli.md +13 -3
- package/openapi/gen/api.ts +30 -0
- package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +1 -1
- package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +1 -1
- package/openapi/gen/models/index.ts +3 -0
- package/openapi/gen/models/listAudienceSpecializations200.ts +7 -0
- package/openapi/gen/models/listAudienceSpecializationsParams.ts +5 -0
- package/openapi/gen/models/specialization.ts +6 -0
- package/openapi/gen/models/trainingCostEstimateOneOfTwoseven.ts +0 -2
- package/openapi/gen/models/trainingRunQuote.ts +0 -2
- package/package.json +1 -1
- package/skills/catalog.json +8 -8
- package/skills/toloka-build/references/node-semantics.md +2 -2
- package/skills/toloka-build/references/workflow-and-cli.md +6 -3
- package/skills/toloka-fine-tuning/SKILL.md +2 -0
- package/skills/toloka-gisting/SKILL.md +3 -1
- package/src/checks/task-ui-typecheck.ts +4 -1
- package/src/cli/analytics.ts +1 -0
- package/src/cli/audience.ts +31 -0
- package/src/cli/auto-update.ts +363 -24
- package/src/cli/check.ts +51 -52
- package/src/cli/cli-entry.ts +31 -0
- package/src/cli/commands/audience.ts +22 -0
- package/src/cli/commands/training.ts +10 -2
- package/src/cli/context.ts +7 -2
- package/src/cli/init.ts +7 -2
- package/src/cli/models.ts +1 -21
- package/src/cli/native-runtime.ts +7 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/skill.ts +34 -26
- package/src/cli/table.ts +21 -0
- package/src/cli/trainings.ts +1 -22
- package/src/cli/version.ts +12 -1
- package/src/client.ts +17 -0
- package/src/index.ts +1 -0
package/src/cli/auto-update.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
| {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
345
|
-
packageName
|
|
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
|
}
|