@toloka-platform/sdk 0.1.0-preview.7 → 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.
- package/README.md +3 -4
- package/dist/cli/toloka.js +133 -130
- 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 +3 -5
- package/docs/cli.md +15 -0
- package/docs/limitations.md +1 -1
- package/examples/face-image-collection/nodes/order-faces/node.ts +2 -4
- package/examples/video-pair-rating/nodes/adjudication_match/node.ts +15 -17
- package/examples/video-pair-rating/nodes/assemble/node.ts +25 -27
- package/examples/video-pair-rating/nodes/cascade/node.ts +24 -26
- package/examples/video-pair-rating/nodes/finalize_overall/node.ts +17 -19
- package/examples/video-pair-rating/nodes/merge_adjudication/node.ts +13 -15
- package/examples/video-pair-rating/nodes/merge_op_adjudication/node.ts +12 -14
- package/examples/video-pair-rating/nodes/op_quorum_resolve/node.ts +13 -15
- package/examples/video-pair-rating/nodes/route_adjudication/node.ts +1 -3
- package/examples/video-pair-rating/nodes/route_op_adjudication/node.ts +5 -7
- package/examples/video-pair-rating/nodes/route_overall/node.ts +1 -3
- package/examples/video-pair-rating/nodes/shuffle_ab/node.ts +11 -13
- package/openapi/gen/models/datasetItem.ts +0 -2
- package/openapi/gen/models/index.ts +0 -1
- package/package.json +1 -1
- package/skills/catalog.json +4 -4
- package/skills/toloka-build/SKILL.md +1 -0
- package/skills/toloka-build/references/node-semantics.md +5 -5
- package/skills/toloka-build/references/workflow-and-cli.md +6 -1
- package/skills/toloka-report-issue/references/report-template.md +1 -1
- package/src/authoring/index.ts +27 -26
- package/src/cli/analytics.ts +8 -19
- package/src/cli/auto-update.ts +367 -0
- package/src/cli/boolean-flag.ts +13 -0
- package/src/cli/credentials.ts +3 -0
- package/src/cli/program.ts +37 -1
- package/src/cli/settings.ts +93 -0
- package/src/importer.ts +12 -16
- package/openapi/gen/models/datasetItemDuplicateOfSubmissionId.ts +0 -2
package/src/authoring/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface Connection {
|
|
|
51
51
|
readonly settings: PipelineGraphV1["connections"][number]["settings"];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
type
|
|
54
|
+
type FieldReference = {
|
|
55
55
|
readonly field: Field;
|
|
56
56
|
readonly optional: boolean;
|
|
57
57
|
};
|
|
@@ -124,26 +124,24 @@ type QuorumOutputEntity = FieldEntity<QuorumNodeSettings["entities"]["output"][n
|
|
|
124
124
|
type QuorumQualityCriterion = FieldTarget<QuorumNodeSettings["qualityCriteria"][number]>;
|
|
125
125
|
type QuorumExam = FieldExam<NonNullable<QuorumNodeSettings["exam"]>>;
|
|
126
126
|
|
|
127
|
-
type MatchPredicate =
|
|
127
|
+
type MatchPredicate = Pick<
|
|
128
128
|
MatchNodeSettings["conditions"][number]["predicates"][number],
|
|
129
|
-
"
|
|
129
|
+
"jsonPath" | "operator" | "value"
|
|
130
130
|
> & {
|
|
131
131
|
readonly field: Field;
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
export type RootAuthoringSettings =
|
|
135
|
-
RootNodeSettings,
|
|
136
|
-
"datasetId" | "itemFilter" | "startedFieldProxies" | "completedFieldProxies"
|
|
137
|
-
> & {
|
|
134
|
+
export type RootAuthoringSettings = Pick<RootNodeSettings, "itemLimit"> & {
|
|
138
135
|
readonly started: ReadonlyArray<RootFieldReference>;
|
|
139
136
|
readonly completed: ReadonlyArray<RootFieldReference>;
|
|
140
137
|
};
|
|
141
138
|
|
|
142
|
-
export type CodeAuthoringSettings =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
export type CodeAuthoringSettings = Pick<
|
|
140
|
+
CodeNodeSettings,
|
|
141
|
+
"about" | "code" | "envVars" | "checks"
|
|
142
|
+
> & {
|
|
143
|
+
readonly input: ReadonlyArray<FieldReference>;
|
|
144
|
+
readonly output: ReadonlyArray<FieldReference>;
|
|
147
145
|
};
|
|
148
146
|
|
|
149
147
|
export type GenerationAuthoringSettings = Omit<
|
|
@@ -162,15 +160,13 @@ export type GenerationAuthoringSettings = Omit<
|
|
|
162
160
|
readonly exam?: GenerationExam;
|
|
163
161
|
};
|
|
164
162
|
|
|
165
|
-
export type MatchAuthoringSettings =
|
|
163
|
+
export type MatchAuthoringSettings = Pick<MatchNodeSettings, "about"> & {
|
|
166
164
|
readonly conditions: ReadonlyArray<
|
|
167
165
|
Omit<MatchNodeSettings["conditions"][number], "predicates"> & {
|
|
168
166
|
readonly predicates: ReadonlyArray<MatchPredicate>;
|
|
169
167
|
}
|
|
170
168
|
>;
|
|
171
|
-
readonly
|
|
172
|
-
readonly input: ReadonlyArray<FieldProxyReference>;
|
|
173
|
-
};
|
|
169
|
+
readonly input: ReadonlyArray<FieldReference>;
|
|
174
170
|
};
|
|
175
171
|
|
|
176
172
|
export type QuorumAuthoringSettings = Omit<
|
|
@@ -316,11 +312,14 @@ function referencedFields(value: unknown): ReadonlyArray<Field> {
|
|
|
316
312
|
return references;
|
|
317
313
|
}
|
|
318
314
|
|
|
319
|
-
function
|
|
315
|
+
function compileFieldReference(reference: FieldReference): { id: string; optional: boolean } {
|
|
320
316
|
return { id: reference.field.id, optional: reference.optional };
|
|
321
317
|
}
|
|
322
318
|
|
|
323
|
-
function
|
|
319
|
+
function compileRootFieldReference(reference: RootFieldReference): {
|
|
320
|
+
id: string;
|
|
321
|
+
optional: boolean;
|
|
322
|
+
} {
|
|
324
323
|
return isField(reference)
|
|
325
324
|
? { id: reference.id, optional: false }
|
|
326
325
|
: { id: reference.field.id, optional: reference.optional ?? false };
|
|
@@ -457,8 +456,8 @@ function createRoot(
|
|
|
457
456
|
type: "root",
|
|
458
457
|
settings: {
|
|
459
458
|
...settingsWithoutFieldProxies,
|
|
460
|
-
startedFieldProxies: { output: started.map(
|
|
461
|
-
completedFieldProxies: { input: completed.map(
|
|
459
|
+
startedFieldProxies: { output: started.map(compileRootFieldReference) },
|
|
460
|
+
completedFieldProxies: { input: completed.map(compileRootFieldReference) },
|
|
462
461
|
},
|
|
463
462
|
started: outputPort(id, "started"),
|
|
464
463
|
completed: inputPort(id, "completed"),
|
|
@@ -477,14 +476,15 @@ export function code(
|
|
|
477
476
|
options: AuthoringIdOptions = {},
|
|
478
477
|
): AuthoredCodeNode {
|
|
479
478
|
const id = options.id ?? mintedId("code", name);
|
|
479
|
+
const { input, output, ...wireSettings } = settings;
|
|
480
480
|
return {
|
|
481
481
|
id,
|
|
482
482
|
type: "code",
|
|
483
483
|
settings: {
|
|
484
|
-
...
|
|
484
|
+
...wireSettings,
|
|
485
485
|
fieldProxies: {
|
|
486
|
-
input:
|
|
487
|
-
output:
|
|
486
|
+
input: input.map(compileFieldReference),
|
|
487
|
+
output: output.map(compileFieldReference),
|
|
488
488
|
},
|
|
489
489
|
},
|
|
490
490
|
input: inputPort(id, "input"),
|
|
@@ -515,19 +515,20 @@ export function match(
|
|
|
515
515
|
options: AuthoringIdOptions = {},
|
|
516
516
|
): AuthoredMatchNode {
|
|
517
517
|
const id = options.id ?? mintedId("match", name);
|
|
518
|
+
const { conditions, input, ...wireSettings } = settings;
|
|
518
519
|
return {
|
|
519
520
|
id,
|
|
520
521
|
type: "match",
|
|
521
522
|
settings: {
|
|
522
|
-
...
|
|
523
|
-
conditions:
|
|
523
|
+
...wireSettings,
|
|
524
|
+
conditions: conditions.map((condition) => ({
|
|
524
525
|
...condition,
|
|
525
526
|
predicates: condition.predicates.map(({ field: referencedField, ...predicate }) => ({
|
|
526
527
|
...predicate,
|
|
527
528
|
fieldProxyId: referencedField.id,
|
|
528
529
|
})),
|
|
529
530
|
})),
|
|
530
|
-
fieldProxies: { input:
|
|
531
|
+
fieldProxies: { input: input.map(compileFieldReference) },
|
|
531
532
|
},
|
|
532
533
|
input: inputPort(id, "input"),
|
|
533
534
|
default: outputPort(id, "default"),
|
package/src/cli/analytics.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { PipelineGraphValidationError, PotatoApiError } from "../errors";
|
|
|
7
7
|
import type { TolokaEnvironment } from "./config";
|
|
8
8
|
import type { CliEnvironment } from "./credentials";
|
|
9
9
|
import { credentialsPath, preparePrivateFilePath } from "./credentials";
|
|
10
|
+
import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
|
|
10
11
|
import { SDK_VERSION } from "./init";
|
|
11
12
|
|
|
12
13
|
export const POSTHOG_PROJECT_KEY = "phc_kqHt0f53B5MVqURyzWpW1aNlbOmYt1YbUYza0JPcg19";
|
|
@@ -61,27 +62,13 @@ export type PosthogLike = {
|
|
|
61
62
|
shutdown: (shutdownTimeoutMs?: number) => Promise<void>;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
const isTruthy = (value: string | undefined): boolean => {
|
|
65
|
-
if (value === undefined) return false;
|
|
66
|
-
const normalized = value.trim().toLowerCase();
|
|
67
|
-
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const isFalsey = (value: string | undefined): boolean => {
|
|
71
|
-
if (value === undefined) return false;
|
|
72
|
-
const normalized = value.trim().toLowerCase();
|
|
73
|
-
return (
|
|
74
|
-
normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off"
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
65
|
export function telemetryEnabled(environment: CliEnvironment): boolean {
|
|
79
66
|
const explicit = environment.TOLOKA_TELEMETRY ?? process.env.TOLOKA_TELEMETRY;
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
84
|
-
if (
|
|
67
|
+
if (isFalseyFlag(explicit)) return false;
|
|
68
|
+
if (isTruthyFlag(explicit)) return true;
|
|
69
|
+
if (isTruthyFlag(environment.DO_NOT_TRACK ?? process.env.DO_NOT_TRACK)) return false;
|
|
70
|
+
if (isTruthyFlag(environment.CI ?? process.env.CI)) return false;
|
|
71
|
+
if (isTruthyFlag(process.env.VITEST)) return false;
|
|
85
72
|
return true;
|
|
86
73
|
}
|
|
87
74
|
|
|
@@ -99,12 +86,14 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
|
|
|
99
86
|
dataset: { list: true, fields: true, load: true, upload: true },
|
|
100
87
|
secret: { set: true },
|
|
101
88
|
skill: { list: true, search: true, install: true },
|
|
89
|
+
config: { get: true, set: true },
|
|
102
90
|
};
|
|
103
91
|
|
|
104
92
|
const ROOT_COMMANDS: Readonly<Record<string, true>> = {
|
|
105
93
|
apply: true,
|
|
106
94
|
check: true,
|
|
107
95
|
"auth-status": true,
|
|
96
|
+
config: true,
|
|
108
97
|
help: true,
|
|
109
98
|
import: true,
|
|
110
99
|
init: true,
|
|
@@ -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
|
+
};
|
package/src/cli/credentials.ts
CHANGED
|
@@ -52,6 +52,9 @@ export const credentialsPath = (environment: CliEnvironment = process.env): stri
|
|
|
52
52
|
export const legacyCredentialsPath = (environment: CliEnvironment = process.env): string =>
|
|
53
53
|
join(home(environment), ".toloka", "credentials.json");
|
|
54
54
|
|
|
55
|
+
export const configDirectory = (environment: CliEnvironment = process.env): string =>
|
|
56
|
+
dirname(credentialsPath(environment));
|
|
57
|
+
|
|
55
58
|
const removeTrailingSlashes = (value: string): string => {
|
|
56
59
|
let end = value.length;
|
|
57
60
|
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
package/src/cli/program.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { authStatus, login, logout } from "./auth";
|
|
|
19
19
|
import type { AuthPrompt } from "./auth";
|
|
20
20
|
import { commandNameFromArgv, createCliAnalytics } from "./analytics";
|
|
21
21
|
import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
|
|
22
|
+
import type { AutoUpdateHooks } from "./auto-update";
|
|
23
|
+
import { maybeAutoUpdate } from "./auto-update";
|
|
22
24
|
import { PRODUCTION_BASE_URL, resolveInternalBaseUrl, resolveTolokaConfig } from "./config";
|
|
23
25
|
import type { TolokaEnvironment } from "./config";
|
|
24
26
|
import {
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
requireStoredCredentials,
|
|
28
30
|
} from "./credentials";
|
|
29
31
|
import type { StoredCredentials } from "./credentials";
|
|
32
|
+
import { getCliSetting, readCliSettings, setCliSetting } from "./settings";
|
|
30
33
|
import {
|
|
31
34
|
generateDatasetFieldDeclarations,
|
|
32
35
|
listProjectDatasets,
|
|
@@ -269,7 +272,8 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
269
272
|
commandNames.includes("help") ||
|
|
270
273
|
commandNames.includes("report") ||
|
|
271
274
|
commandNames.includes("url") ||
|
|
272
|
-
commandNames.includes("version")
|
|
275
|
+
commandNames.includes("version") ||
|
|
276
|
+
commandNames.includes("config")
|
|
273
277
|
)
|
|
274
278
|
return;
|
|
275
279
|
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
@@ -283,6 +287,29 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
283
287
|
.description("print the current CLI version")
|
|
284
288
|
.action(() => context.io.stdout(SDK_VERSION));
|
|
285
289
|
|
|
290
|
+
const config = program.command("config").description("read and write machine CLI settings");
|
|
291
|
+
config
|
|
292
|
+
.command("get")
|
|
293
|
+
.argument("[name]", "setting name; omit to print every setting")
|
|
294
|
+
.description("print a CLI setting, or every setting")
|
|
295
|
+
.action(async (name?: string) => {
|
|
296
|
+
const settings = await readCliSettings(context.environment);
|
|
297
|
+
context.io.stdout(getCliSetting(settings, name).output);
|
|
298
|
+
});
|
|
299
|
+
config
|
|
300
|
+
.command("set")
|
|
301
|
+
.argument("<name>", "setting name")
|
|
302
|
+
.argument("<value>", "setting value")
|
|
303
|
+
.description("write a CLI setting for this machine")
|
|
304
|
+
.action(async (name: string, value: string) => {
|
|
305
|
+
const result = await setCliSetting(context.environment, name, value);
|
|
306
|
+
context.io.stdout(result.output);
|
|
307
|
+
});
|
|
308
|
+
config.action(async () => {
|
|
309
|
+
const settings = await readCliSettings(context.environment);
|
|
310
|
+
context.io.stdout(getCliSetting(settings, undefined).output);
|
|
311
|
+
});
|
|
312
|
+
|
|
286
313
|
program
|
|
287
314
|
.command("login")
|
|
288
315
|
.description("store an API key for this machine after verifying it")
|
|
@@ -1127,7 +1154,16 @@ export async function runTolokaCli(
|
|
|
1127
1154
|
injectedClient?: PotatoClient,
|
|
1128
1155
|
analytics?: CliAnalytics,
|
|
1129
1156
|
runChecks?: typeof checkProject,
|
|
1157
|
+
autoUpdate?: AutoUpdateHooks,
|
|
1130
1158
|
): Promise<number> {
|
|
1159
|
+
const autoUpdateResult = await maybeAutoUpdate({
|
|
1160
|
+
argv,
|
|
1161
|
+
environment,
|
|
1162
|
+
io,
|
|
1163
|
+
...(autoUpdate === undefined ? {} : { hooks: autoUpdate }),
|
|
1164
|
+
});
|
|
1165
|
+
if (autoUpdateResult.status === "reexec") return autoUpdateResult.exitCode;
|
|
1166
|
+
|
|
1131
1167
|
const activeAnalytics =
|
|
1132
1168
|
environment[CHECK_RESULT_FILE_ENV] === undefined
|
|
1133
1169
|
? (analytics ?? createCliAnalytics({ environment }))
|