@fnndsc/salsa 4.4.3 → 4.5.0

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.
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ /**
3
+ * @file Plugin indexes: where descriptors come from, and reading them.
4
+ *
5
+ * A plugin index is a set of plugin descriptors, one JSON file per plugin at
6
+ * its latest version, in the form CUBE's administrative upload takes, with
7
+ * the JSON Schema they follow beside them. FNNDSC/chris-plugins is the one
8
+ * that matters; a directory of `.json` files or a `.tar.gz` holding one is
9
+ * an index too, on CUBE (how a surface with no host access syncs) or on the
10
+ * host.
11
+ *
12
+ * Every source reads to the same {@link PluginIndex}: its descriptors as
13
+ * text (so a later check can quote the file), the schema when one came with
14
+ * them, and the commit when the source can name one. GitHub's archive of a
15
+ * ref is fetched once, whole; its pax global header names the exact commit.
16
+ *
17
+ * Layout read from any source: descriptors are the `.json` files at the root
18
+ * or under `plugins/`; the schema is `schema/plugin.schema.json` at the root
19
+ * or one level up (a `plugins/` directory handed over on its own).
20
+ *
21
+ * Key components:
22
+ * - PluginIndexSource: a parsed source (GitHub repository at a ref, CUBE path, host path).
23
+ * - PluginIndex: the descriptors, schema and commit read from a source.
24
+ * - pluginIndexSource_parse: read a source word (`FNNDSC/chris-plugins@main`, `host:./plugins`).
25
+ * - pluginIndexArchive_read: read an index from archive bytes.
26
+ * - pluginIndex_load: read an index from any source.
27
+ *
28
+ * Typical usage:
29
+ * ```ts
30
+ * const source: PluginIndexSource | null = pluginIndexSource_parse('FNNDSC/chris-plugins@main', 'cube');
31
+ * const loaded: Result<PluginIndex> = source ? await pluginIndex_load(source) : Err();
32
+ * ```
33
+ *
34
+ * Dependencies: Node.js built-ins (`node:fs`, `node:path`), `fetch` for the
35
+ * GitHub archive, salsa's CUBE file reads for CUBE paths.
36
+ *
37
+ * @module
38
+ */
39
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
40
+ if (k2 === undefined) k2 = k;
41
+ var desc = Object.getOwnPropertyDescriptor(m, k);
42
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
43
+ desc = { enumerable: true, get: function() { return m[k]; } };
44
+ }
45
+ Object.defineProperty(o, k2, desc);
46
+ }) : (function(o, m, k, k2) {
47
+ if (k2 === undefined) k2 = k;
48
+ o[k2] = m[k];
49
+ }));
50
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
51
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
52
+ }) : function(o, v) {
53
+ o["default"] = v;
54
+ });
55
+ var __importStar = (this && this.__importStar) || (function () {
56
+ var ownKeys = function(o) {
57
+ ownKeys = Object.getOwnPropertyNames || function (o) {
58
+ var ar = [];
59
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
60
+ return ar;
61
+ };
62
+ return ownKeys(o);
63
+ };
64
+ return function (mod) {
65
+ if (mod && mod.__esModule) return mod;
66
+ var result = {};
67
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
68
+ __setModuleDefault(result, mod);
69
+ return result;
70
+ };
71
+ })();
72
+ Object.defineProperty(exports, "__esModule", { value: true });
73
+ exports.pluginIndexSource_parse = pluginIndexSource_parse;
74
+ exports.pluginIndexSource_format = pluginIndexSource_format;
75
+ exports.pluginIndexArchive_read = pluginIndexArchive_read;
76
+ exports.pluginIndex_load = pluginIndex_load;
77
+ const node_fs_1 = require("node:fs");
78
+ const nodePath = __importStar(require("node:path"));
79
+ const cumin_1 = require("@fnndsc/cumin");
80
+ const index_js_1 = require("../files/index.js");
81
+ const tar_read_js_1 = require("./tar_read.js");
82
+ /** Where GitHub serves a repository's archive of a ref. */
83
+ const GITHUB_TARBALL_URL = 'https://api.github.com/repos';
84
+ /** The ref read when a GitHub source names none. */
85
+ const DEFAULT_REF = 'main';
86
+ /** File name of the index's JSON Schema. */
87
+ const SCHEMA_FILE = 'plugin.schema.json';
88
+ /** Archive extensions read as a single archive rather than a directory. */
89
+ const ARCHIVE_PATTERN = /\.(tar\.gz|tgz|tar)$/i;
90
+ /** A GitHub repository word: `owner/repo`, optionally `@ref`. */
91
+ const GITHUB_PATTERN = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:@(\S+))?$/;
92
+ /**
93
+ * Parses an index source word.
94
+ *
95
+ * `owner/repo[@ref]` is a GitHub repository; `host:<path>` a host path;
96
+ * `cube:<path>` a CUBE path. Any other word is a path, read where `bare`
97
+ * says: `plugin sync` reads a bare path on CUBE, `plugin validate` (which
98
+ * needs no CUBE) on the host.
99
+ *
100
+ * @param word - The source as typed.
101
+ * @param bare - Where a path with no prefix is read.
102
+ * @param ref - A ref overriding the word's own (`--ref`), for a GitHub source.
103
+ * @returns The parsed source, or null for an empty word.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * pluginIndexSource_parse('FNNDSC/chris-plugins@v1', 'cube'); // { kind: 'github', …, ref: 'v1' }
108
+ * pluginIndexSource_parse('host:./plugins', 'cube'); // { kind: 'host', path: './plugins' }
109
+ * ```
110
+ */
111
+ function pluginIndexSource_parse(word, bare, ref) {
112
+ const trimmed = word.trim();
113
+ if (trimmed === '')
114
+ return null;
115
+ if (trimmed.startsWith('host:'))
116
+ return { kind: 'host', path: trimmed.slice('host:'.length) };
117
+ if (trimmed.startsWith('cube:'))
118
+ return { kind: 'cube', path: trimmed.slice('cube:'.length) };
119
+ const github = trimmed.startsWith('.') ? null : GITHUB_PATTERN.exec(trimmed);
120
+ if (github !== null && github[1] !== undefined && github[2] !== undefined) {
121
+ return { kind: 'github', owner: github[1], repo: github[2], ref: ref ?? github[3] ?? DEFAULT_REF };
122
+ }
123
+ return { kind: bare, path: trimmed };
124
+ }
125
+ /**
126
+ * Renders a source as the operator would write it.
127
+ *
128
+ * @param source - The parsed source.
129
+ * @returns The source word (`FNNDSC/chris-plugins@main`, `host:./plugins`, `cube:/home/u/plugins`).
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * pluginIndexSource_format({ kind: 'host', path: './plugins' }); // 'host:./plugins'
134
+ * ```
135
+ */
136
+ function pluginIndexSource_format(source) {
137
+ if (source.kind === 'github')
138
+ return `${source.owner}/${source.repo}@${source.ref}`;
139
+ return `${source.kind}:${source.path}`;
140
+ }
141
+ /**
142
+ * Reads an index from archive bytes.
143
+ *
144
+ * A single top folder (GitHub's `FNNDSC-chris-plugins-<sha>/`) is the root;
145
+ * the pax global header's `comment` is the commit.
146
+ *
147
+ * @param bytes - The archive, gzipped or plain.
148
+ * @param source - The source word, for the index's `source` and `label`.
149
+ * @returns The index.
150
+ * @throws {Error} When the archive is unreadable, an entry's path is unsafe,
151
+ * or the schema it carries is not JSON.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * const index: PluginIndex = pluginIndexArchive_read(readFileSync('index.tar.gz'), 'FNNDSC/chris-plugins@main');
156
+ * ```
157
+ */
158
+ function pluginIndexArchive_read(bytes, source) {
159
+ const archive = (0, tar_read_js_1.tarArchive_read)(bytes);
160
+ const root = archiveRoot_find(archive.files);
161
+ const entries = archive.files
162
+ .filter((file) => file.path.startsWith(root))
163
+ .map((file) => ({ path: file.path.slice(root.length), text: file.content.toString('utf8') }));
164
+ const commit = archive.globals['comment'] ?? null;
165
+ return pluginIndex_assemble(source, commit, entries);
166
+ }
167
+ /**
168
+ * Reads an index from its source.
169
+ *
170
+ * @param source - The parsed source.
171
+ * @param fetcher - The fetch used for GitHub; the global one by default.
172
+ * @returns The index, or an error on the error stack (the fetch failed, the
173
+ * path is not there, the archive is unreadable).
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * const loaded: Result<PluginIndex> = await pluginIndex_load({ kind: 'host', path: './plugins' });
178
+ * ```
179
+ */
180
+ async function pluginIndex_load(source, fetcher = fetch) {
181
+ const word = pluginIndexSource_format(source);
182
+ try {
183
+ if (source.kind === 'github')
184
+ return (0, cumin_1.Ok)(await pluginIndexGitHub_fetch(source, fetcher));
185
+ if (source.kind === 'host')
186
+ return (0, cumin_1.Ok)(await pluginIndexHost_read(source.path, word));
187
+ return await pluginIndexCube_read(source.path, word);
188
+ }
189
+ catch (error) {
190
+ cumin_1.errorStack.stack_push('error', `plugin index ${word}: ${error instanceof Error ? error.message : String(error)}`);
191
+ return (0, cumin_1.Err)();
192
+ }
193
+ }
194
+ /**
195
+ * Fetches a GitHub repository's archive of a ref and reads it.
196
+ *
197
+ * @param source - The GitHub source.
198
+ * @param fetcher - The fetch to use.
199
+ * @returns The index.
200
+ * @throws {Error} When GitHub does not answer with the archive.
201
+ */
202
+ async function pluginIndexGitHub_fetch(source, fetcher) {
203
+ const url = `${GITHUB_TARBALL_URL}/${source.owner}/${source.repo}/tarball/${encodeURIComponent(source.ref)}`;
204
+ const response = await fetcher(url, { headers: { Accept: 'application/vnd.github+json' } });
205
+ if (!response.ok)
206
+ throw new Error(`GitHub answered HTTP ${response.status} for ${url}`);
207
+ const bytes = Buffer.from(await response.arrayBuffer());
208
+ return pluginIndexArchive_read(bytes, `${source.owner}/${source.repo}@${source.ref}`);
209
+ }
210
+ /**
211
+ * Reads an index from a host path: a directory or an archive.
212
+ *
213
+ * @param path - The host path.
214
+ * @param word - The source word.
215
+ * @returns The index.
216
+ * @throws {Error} When the path cannot be read.
217
+ */
218
+ async function pluginIndexHost_read(path, word) {
219
+ if (ARCHIVE_PATTERN.test(path))
220
+ return pluginIndexArchive_read(await node_fs_1.promises.readFile(path), word);
221
+ const entries = [];
222
+ for (const [relative, directory] of [['', path], ['plugins/', nodePath.join(path, 'plugins')]]) {
223
+ const names = await directoryNames_read(directory);
224
+ for (const name of names.filter((candidate) => candidate.endsWith('.json'))) {
225
+ entries.push({ path: `${relative}${name}`, text: await node_fs_1.promises.readFile(nodePath.join(directory, name), 'utf8') });
226
+ }
227
+ }
228
+ for (const relative of [`schema/${SCHEMA_FILE}`, `../schema/${SCHEMA_FILE}`]) {
229
+ const text = await node_fs_1.promises.readFile(nodePath.join(path, relative), 'utf8').catch(() => null);
230
+ if (text !== null)
231
+ entries.push({ path: relative, text });
232
+ }
233
+ return pluginIndex_assemble(word, null, entries);
234
+ }
235
+ /**
236
+ * Lists a host directory's file names, or none when it is not there.
237
+ *
238
+ * @param directory - The host directory.
239
+ * @returns Its entries' names; empty when the directory does not exist.
240
+ * @throws {Error} On a read failure other than absence.
241
+ */
242
+ async function directoryNames_read(directory) {
243
+ try {
244
+ return (await node_fs_1.promises.readdir(directory)).sort();
245
+ }
246
+ catch (error) {
247
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR')
248
+ return [];
249
+ throw error;
250
+ }
251
+ }
252
+ /**
253
+ * Reads an index from a CUBE path: a directory or an uploaded archive.
254
+ *
255
+ * @param path - The CUBE path, absolute.
256
+ * @param word - The source word.
257
+ * @returns The index, or an error on the error stack.
258
+ */
259
+ async function pluginIndexCube_read(path, word) {
260
+ if (ARCHIVE_PATTERN.test(path)) {
261
+ const bytes = await (0, index_js_1.fileContent_getBinary)(path);
262
+ return bytes.ok ? (0, cumin_1.Ok)(pluginIndexArchive_read(bytes.value, word)) : (0, cumin_1.Err)();
263
+ }
264
+ const root = path.replace(/\/+$/, '');
265
+ const items = await (0, index_js_1.files_listRecursive)(root);
266
+ const entries = [];
267
+ for (const item of items.filter((candidate) => candidate.type === 'file')) {
268
+ const relative = item.path.replace(/^\/+/, '').slice(root.replace(/^\/+/, '').length + 1);
269
+ if (!/^(plugins\/)?[^/]+\.json$/.test(relative) && relative !== `schema/${SCHEMA_FILE}`)
270
+ continue;
271
+ const text = await (0, index_js_1.fileContent_get)(`${root}/${relative}`);
272
+ if (!text.ok)
273
+ return (0, cumin_1.Err)();
274
+ entries.push({ path: relative, text: text.value });
275
+ }
276
+ if (!entries.some((entry) => entry.path.endsWith(SCHEMA_FILE))) {
277
+ const above = await (0, index_js_1.fileContent_get)(`${nodePath.posix.dirname(root)}/schema/${SCHEMA_FILE}`);
278
+ // An index handed over without its schema is still an index; rule 1
279
+ // then says it was not checked rather than this read failing.
280
+ if (above.ok)
281
+ entries.push({ path: `../schema/${SCHEMA_FILE}`, text: above.value });
282
+ else
283
+ cumin_1.errorStack.stack_pop();
284
+ }
285
+ return (0, cumin_1.Ok)(pluginIndex_assemble(word, null, entries));
286
+ }
287
+ /**
288
+ * Finds an archive's single top folder, the index's root.
289
+ *
290
+ * @param files - The archive's files.
291
+ * @returns The root with its trailing slash, or '' when the files share none.
292
+ */
293
+ function archiveRoot_find(files) {
294
+ const tops = new Set(files.map((file) => file.path.split('/')[0] ?? ''));
295
+ const top = [...tops][0];
296
+ return tops.size === 1 && top !== undefined && files.every((file) => file.path.includes('/')) ? `${top}/` : '';
297
+ }
298
+ /**
299
+ * Builds an index from its files, by the layout every source shares.
300
+ *
301
+ * @param source - The source word.
302
+ * @param commit - The commit, when the source names one.
303
+ * @param entries - The files, relative to the root.
304
+ * @returns The index.
305
+ * @throws {Error} When the schema file is not JSON.
306
+ */
307
+ function pluginIndex_assemble(source, commit, entries) {
308
+ const descriptors = entries
309
+ .filter((entry) => /^(plugins\/)?[^/]+\.json$/.test(entry.path) && !entry.path.endsWith(SCHEMA_FILE))
310
+ .map((entry) => ({ file: entry.path.replace(/^plugins\//, ''), text: entry.text }))
311
+ .sort((a, b) => a.file.localeCompare(b.file));
312
+ const schemaEntry = entries.find((entry) => entry.path === `schema/${SCHEMA_FILE}`)
313
+ ?? entries.find((entry) => entry.path === `../schema/${SCHEMA_FILE}`);
314
+ let schema = null;
315
+ if (schemaEntry !== undefined) {
316
+ try {
317
+ schema = JSON.parse(schemaEntry.text);
318
+ }
319
+ catch {
320
+ throw new Error(`${schemaEntry.path} is not JSON`);
321
+ }
322
+ }
323
+ return { source, label: indexLabel_build(source, commit), commit, descriptors, schema };
324
+ }
325
+ /**
326
+ * Builds an index's short name for headers.
327
+ *
328
+ * @param source - The source word.
329
+ * @param commit - The commit, when known.
330
+ * @returns `chris-plugins @ 8c9a1f6` for a repository, else the source word.
331
+ */
332
+ function indexLabel_build(source, commit) {
333
+ const github = GITHUB_PATTERN.exec(source);
334
+ if (github === null || source.includes(':'))
335
+ return commit === null ? source : `${source} @ ${commit.slice(0, 7)}`;
336
+ return `${github[2] ?? source} @ ${commit === null ? (github[3] ?? DEFAULT_REF) : commit.slice(0, 7)}`;
337
+ }
338
+ //# sourceMappingURL=plugin_index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin_index.js","sourceRoot":"","sources":["../../src/plugins/plugin_index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @file Filling a CUBE from a plugin index: `plugin sync`.
3
+ *
4
+ * A sync is a plan, then the plan's adds. The plan compares every descriptor
5
+ * in the index with what the connected CUBE holds and puts each plugin in
6
+ * one group:
7
+ *
8
+ * | group | what it means | action |
9
+ * |---|---|---|
10
+ * | add | not in the CUBE | add |
11
+ * | newer | the index has a newer version | add beside the older one |
12
+ * | skip | same name and version, same descriptor | nothing |
13
+ * | cubeNewer | the CUBE has a newer version | report |
14
+ * | drift | same name and version, different descriptor | report, never touched |
15
+ * | imageTaken | the image is registered under another version | report: CUBE would refuse it |
16
+ * | cubeOnly | in the CUBE, not in the index | report |
17
+ * | invalid | fails rules 1–4 of `plugin validate` | report, never uploaded |
18
+ *
19
+ * A sync never deletes and never changes a plugin already there: deleting
20
+ * cascades to its runs and to its nodes in every pipeline, and CUBE's admin
21
+ * `PUT` changes only compute resources, so a descriptor cannot be corrected
22
+ * in place anyway. Drift ignores what only reflects CUBE's defaults (a limit
23
+ * the index leaves out because it equals the default, `1000m` against a
24
+ * stored `1000`, a field CUBE fills in when it is not given).
25
+ *
26
+ * Applying uploads the plan's adds four at a time, each failure contained to
27
+ * its plugin; what landed stays, so a rerun continues where a stopped one
28
+ * left off. Every CUBE read and the upload go through {@link SyncSeams}, so
29
+ * the plan and the apply are testable without a CUBE.
30
+ *
31
+ * Key components:
32
+ * - SyncGroup / SyncPlanEntry / SyncPlan: the plan.
33
+ * - SyncResult / SyncReceipt: what applying it did.
34
+ * - SyncSeams / syncSeams_cube: the CUBE reads and the upload.
35
+ * - syncPlan_build: compare an index with the CUBE.
36
+ * - syncPlanAdds_list: the entries a plan would upload.
37
+ * - syncPlan_apply: upload them, four at a time.
38
+ * - descriptorDrift_find: how two descriptors of one version differ, defaults aside.
39
+ * - version_compare: dotted-number ordering.
40
+ *
41
+ * Typical usage:
42
+ * ```ts
43
+ * const plan: SyncPlan = await syncPlan_build(index, ['host'], syncSeams_cube);
44
+ * const receipt: SyncReceipt = await syncPlan_apply(plan, syncSeams_cube, (r: SyncResult): void => console.log(r));
45
+ * ```
46
+ *
47
+ * Dependencies: cumin's CUBE reads (plugin pages, parameters) and the
48
+ * administrative upload; the index and its local checks.
49
+ *
50
+ * @module
51
+ */
52
+ import type { PluginIndex } from './plugin_index.js';
53
+ /** The groups a plan's entries fall in, in the order a plan lists them. */
54
+ export declare const SYNC_GROUPS: readonly ["add", "newer", "skip", "cubeNewer", "drift", "imageTaken", "cubeOnly", "invalid"];
55
+ /** One group of the plan. */
56
+ export type SyncGroup = typeof SYNC_GROUPS[number];
57
+ /** The groups that upload. */
58
+ export declare const SYNC_ADD_GROUPS: readonly SyncGroup[];
59
+ /**
60
+ * One plugin's place in the plan.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const entry: SyncPlanEntry = { group: 'newer', name: 'pl-x', version: '1.2.0', cubeVersions: ['1.1.0'] };
65
+ * ```
66
+ */
67
+ export interface SyncPlanEntry {
68
+ /** Its group. */
69
+ readonly group: SyncGroup;
70
+ /** The plugin's name (the file's, for an invalid descriptor that names none). */
71
+ readonly name: string;
72
+ /** The index's version, when the entry comes from the index. */
73
+ readonly version?: string;
74
+ /** The versions the CUBE holds of this name. */
75
+ readonly cubeVersions?: string[];
76
+ /** The index file it comes from. */
77
+ readonly file?: string;
78
+ /** Why it is in a report group, in a line. */
79
+ readonly detail?: string;
80
+ /** The descriptor's text, for the entries that upload. */
81
+ readonly descriptor?: string;
82
+ }
83
+ /**
84
+ * The plan.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const plan: SyncPlan = await syncPlan_build(index, ['host'], seams);
89
+ * plan.entries.filter((e: SyncPlanEntry): boolean => e.group === 'add').length;
90
+ * ```
91
+ */
92
+ export interface SyncPlan {
93
+ /** The index's short name (`chris-plugins @ 8c9a1f6`). */
94
+ readonly label: string;
95
+ /** The index's source word. */
96
+ readonly source: string;
97
+ /** The commit read, when known. */
98
+ readonly commit: string | null;
99
+ /** How many descriptors the index holds. */
100
+ readonly indexed: number;
101
+ /** The compute resources adds are registered on. */
102
+ readonly computes: string[];
103
+ /** Every entry, by group then name. */
104
+ readonly entries: SyncPlanEntry[];
105
+ }
106
+ /**
107
+ * One upload's outcome.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const result: SyncResult = { name: 'pl-x', version: '1.2.0', ok: false, reason: 'Plugin with name pl-x and version 1.2.0 already exists.' };
112
+ * ```
113
+ */
114
+ export interface SyncResult {
115
+ /** The plugin. */
116
+ readonly name: string;
117
+ /** Its version. */
118
+ readonly version: string;
119
+ /** True when CUBE registered it. */
120
+ readonly ok: boolean;
121
+ /** CUBE's reason, when it refused. */
122
+ readonly reason?: string;
123
+ }
124
+ /**
125
+ * What applying a plan did.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const receipt: SyncReceipt = await syncPlan_apply(plan, seams, () => undefined);
130
+ * receipt.failed === 0;
131
+ * ```
132
+ */
133
+ export interface SyncReceipt {
134
+ /** The plan applied. */
135
+ readonly plan: SyncPlan;
136
+ /** Each upload's outcome, in plan order. */
137
+ readonly results: SyncResult[];
138
+ /** Plugins registered. */
139
+ readonly added: number;
140
+ /** Plugins already there, the same. */
141
+ readonly skipped: number;
142
+ /** Plugins in a report group. */
143
+ readonly reported: number;
144
+ /** Uploads that should have worked and did not. */
145
+ readonly failed: number;
146
+ /** Whether a stop (`kill %N`, Ctrl-C) ended it before every add was sent. */
147
+ readonly stopped: boolean;
148
+ /** Adds the plan had that were never sent, because it was stopped. */
149
+ readonly unsent: number;
150
+ }
151
+ /**
152
+ * One plugin row the CUBE holds.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const row: CubePluginRow = { name: 'pl-x', version: '1.0.0', dock_image: 'fnndsc/pl-x:1.0.0' };
157
+ * ```
158
+ */
159
+ export interface CubePluginRow {
160
+ /** Its name. */
161
+ readonly name: string;
162
+ /** Its version. */
163
+ readonly version: string;
164
+ /** Its image. */
165
+ readonly dock_image: string;
166
+ }
167
+ /** What an upload answered. */
168
+ export type UploadAnswer = {
169
+ readonly ok: true;
170
+ } | {
171
+ readonly ok: false;
172
+ readonly reason: string;
173
+ };
174
+ /**
175
+ * The CUBE reads and the upload a sync needs.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const seams: SyncSeams = { ...syncSeams_cube, descriptor_upload: async () => ({ ok: true }) };
180
+ * ```
181
+ */
182
+ export interface SyncSeams {
183
+ /**
184
+ * Reads every plugin the CUBE holds.
185
+ *
186
+ * @returns The rows, every page.
187
+ */
188
+ cubePlugins_read(): Promise<CubePluginRow[]>;
189
+ /**
190
+ * Reads one registered version whole: its fields and every parameter.
191
+ *
192
+ * @param name - The plugin's name.
193
+ * @param version - The version.
194
+ * @returns The plugin's fields with `parameters`, or null when CUBE has no such version.
195
+ */
196
+ cubeDescriptor_read(name: string, version: string): Promise<Record<string, unknown> | null>;
197
+ /**
198
+ * Uploads one descriptor.
199
+ *
200
+ * @param descriptor - The descriptor's text.
201
+ * @param computes - Compute resources to register it on.
202
+ * @returns Whether CUBE registered it, and why not.
203
+ */
204
+ descriptor_upload(descriptor: string, computes: string[]): Promise<UploadAnswer>;
205
+ }
206
+ /** The connected CUBE's reads and upload. */
207
+ export declare const syncSeams_cube: SyncSeams;
208
+ /**
209
+ * Builds the plan: compares an index with the connected CUBE.
210
+ *
211
+ * @param index - The index.
212
+ * @param computes - The compute resources adds would be registered on.
213
+ * @param seams - The CUBE reads.
214
+ * @returns The plan.
215
+ * @throws {Error} When the CUBE cannot be read.
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * const plan: SyncPlan = await syncPlan_build(index, ['host'], syncSeams_cube);
220
+ * ```
221
+ */
222
+ export declare function syncPlan_build(index: PluginIndex, computes: string[], seams: SyncSeams): Promise<SyncPlan>;
223
+ /**
224
+ * Lists the entries a plan would upload.
225
+ *
226
+ * @param plan - The plan.
227
+ * @returns Its add and newer entries, in plan order.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * syncPlanAdds_list(plan).length; // how many plugins a yes registers
232
+ * ```
233
+ */
234
+ export declare function syncPlanAdds_list(plan: SyncPlan): SyncPlanEntry[];
235
+ /**
236
+ * Applies a plan: uploads its adds, four at a time.
237
+ *
238
+ * @param plan - The plan.
239
+ * @param seams - The upload.
240
+ * @param note - Called once per upload as it lands, in the order they land.
241
+ * @param signal - A stop: the uploads in flight finish, no more start, and
242
+ * what landed stays (a rerun continues from it).
243
+ * @returns What it did, `stopped` when the signal ended it early.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * const receipt: SyncReceipt = await syncPlan_apply(plan, syncSeams_cube, (r: SyncResult): void => console.log(r.name, r.ok));
248
+ * ```
249
+ */
250
+ export declare function syncPlan_apply(plan: SyncPlan, seams: SyncSeams, note: (result: SyncResult) => void, signal?: AbortSignal): Promise<SyncReceipt>;
251
+ /**
252
+ * Finds how two descriptors of one version differ, CUBE's defaults aside.
253
+ *
254
+ * @param indexed - The index's descriptor.
255
+ * @param held - The CUBE's plugin fields with its `parameters`.
256
+ * @returns The differing fields (`dock_image`, `parameters.--dir.help`), empty when none.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * descriptorDrift_find({ min_cpu_limit: '1000m' }, { min_cpu_limit: 1000 }); // []
261
+ * ```
262
+ */
263
+ export declare function descriptorDrift_find(indexed: Record<string, unknown>, held: Record<string, unknown>): string[];
264
+ /**
265
+ * Orders two dotted-number versions.
266
+ *
267
+ * @param a - One version.
268
+ * @param b - The other.
269
+ * @returns Negative when `a` is older, positive when newer, zero when equal.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * ['1.10.0', '1.2.0'].sort(version_compare); // ['1.2.0', '1.10.0']
274
+ * ```
275
+ */
276
+ export declare function version_compare(a: string, b: string): number;