@hardfin/cli 0.1.1-dev.69 → 0.1.1-dev.71
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 +61 -21
- package/dist/{check-CDkHR_Qt.js → check-CvjTUx-K.js} +81 -79
- package/dist/cli.js +5 -5
- package/dist/completion-tree.json +1 -1
- package/dist/{notice-Bq9ifPV6.js → notice-D6mbHF9O.js} +4 -4
- package/dist/{program-eT-hgKX0.js → program-CC8Iqb14.js} +297 -240
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ Most commands are generated from the published API document rather than written
|
|
|
85
85
|
| `src/command/surface.generated.ts` | Every endpoint as a command, rewritten by the generator |
|
|
86
86
|
| `surface-overrides.json` | The operations whose generated name is wrong |
|
|
87
87
|
| `spec/core.openapi.yaml` | The API document the generator reads, vendored here |
|
|
88
|
+
| `spec/source.json` | Where the vendored document came from, and whether it is a preview |
|
|
88
89
|
| `scripts/generate-surface.mjs` | The generator |
|
|
89
90
|
|
|
90
91
|
### How an endpoint becomes a command
|
|
@@ -199,9 +200,10 @@ A field the API does not take null for is sent the word as text. The same holds
|
|
|
199
200
|
hardfin asset accounting update ast_4f9xk2mq7plr8stz --unset useful-life
|
|
200
201
|
```
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
enum, a required field that is missing,
|
|
204
|
-
for.
|
|
203
|
+
Four things fail locally with exit code 2, before any request is made: a value outside an
|
|
204
|
+
enum, a required field that is missing, a list element missing a field every element needs,
|
|
205
|
+
and `--unset` on a field the API will not take null for. `--help` starts a required flag's
|
|
206
|
+
description with `Required.`, and names the fields every element of a repeated flag needs.
|
|
205
207
|
|
|
206
208
|
### When a command fails
|
|
207
209
|
|
|
@@ -277,16 +279,17 @@ hardfin agent-guide --json customer create | jq -r '.commands[0].flags[] | selec
|
|
|
277
279
|
The JSON holds `cli`, `apiVersion`, `exitCodes`, `errorCodes`, `globalFlags` and `commands`.
|
|
278
280
|
Each command names its `path`, its `endpoint`, whether it `runs`, and its flags.
|
|
279
281
|
|
|
280
|
-
| A flag's
|
|
281
|
-
|
|
|
282
|
-
| `required`
|
|
283
|
-
| `schema`
|
|
284
|
-
| `sends`
|
|
285
|
-
| `clearable`
|
|
286
|
-
| `repeatable`
|
|
287
|
-
| `negatable`
|
|
288
|
-
| `element`
|
|
289
|
-
| `
|
|
282
|
+
| A flag's | Holds |
|
|
283
|
+
| ----------------- | --------------------------------------------------------------------------- |
|
|
284
|
+
| `required` | Present when the command fails without it |
|
|
285
|
+
| `schema` | The `type`, and the `enum`, `format`, `minimum` or `maximum` that narrow it |
|
|
286
|
+
| `sends` | The query parameter, or the path of the body field, the value goes to |
|
|
287
|
+
| `clearable` | Present when `--unset` can clear the field |
|
|
288
|
+
| `repeatable` | Present when the flag is given once per value |
|
|
289
|
+
| `negatable` | Present when a boolean also takes a `--no-` form |
|
|
290
|
+
| `element` | The fields one value carries as `key=value`, for an array of objects |
|
|
291
|
+
| `elementRequired` | The fields of `element` every value must carry |
|
|
292
|
+
| `default` | The value used when the flag is not given |
|
|
290
293
|
|
|
291
294
|
A part that is absent means no. The description is read from the same declarations the parser
|
|
292
295
|
enforces, so it cannot say one thing while the command does another.
|
|
@@ -322,24 +325,61 @@ reads it. Nothing fetches a document during a build, a release, or CI.
|
|
|
322
325
|
Refresh it with the script, which then rewrites the generated commands:
|
|
323
326
|
|
|
324
327
|
```sh
|
|
325
|
-
scripts/update-spec.sh #
|
|
328
|
+
scripts/update-spec.sh # the newer of api-spec's main and active-preview
|
|
329
|
+
scripts/update-spec.sh --main # api-spec's main, the published contract
|
|
330
|
+
scripts/update-spec.sh --preview # api-spec's active-preview, and fails when there is none
|
|
326
331
|
scripts/update-spec.sh ../hardfin # bundles the fragmented source in a monorepo checkout
|
|
327
332
|
```
|
|
328
333
|
|
|
329
|
-
The
|
|
330
|
-
|
|
334
|
+
The first three read `hardfinhq/api-spec` over your own git access. The monorepo form runs the
|
|
335
|
+
same bundler at the same version CI uses, so the result matches what the api-spec bridge
|
|
336
|
+
publishes. Commit the document, `spec/source.json` and the generated files together.
|
|
331
337
|
|
|
332
|
-
|
|
338
|
+
#### Previews
|
|
333
339
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
|
337
|
-
|
|
|
340
|
+
`hardfinhq/api-spec` has two branches the script reads.
|
|
341
|
+
|
|
342
|
+
| Branch | Holds | Exists |
|
|
343
|
+
| ---------------- | ------------------------------------------------------- | --------------------------------------------- |
|
|
344
|
+
| `main` | The published contract, which the production API serves | always |
|
|
345
|
+
| `active-preview` | The next contract, before it publishes | while the monorepo's `api-preview` tag exists |
|
|
346
|
+
|
|
347
|
+
Pushing or moving the monorepo's `api-preview` tag rebuilds `active-preview` from `main` with
|
|
348
|
+
the tagged commit's document. Deleting the tag deletes the branch. With no option, the script
|
|
349
|
+
reads `active-preview` while it exists and its last commit is newer than `main`'s. Once `main`
|
|
350
|
+
publishes past it, `main` holds everything the preview did, and the script reads `main`.
|
|
351
|
+
|
|
352
|
+
A preview lets a dev build carry the next contract before it publishes. Merging a preview
|
|
353
|
+
document to `dev` publishes it as a `-dev` build like any other change.
|
|
354
|
+
|
|
355
|
+
The script records where the document came from in `spec/source.json`, including whether it is
|
|
356
|
+
a preview. A preview names an API version the production API does not serve yet, so it never
|
|
357
|
+
reaches a release.
|
|
358
|
+
|
|
359
|
+
| Check | Refuses |
|
|
360
|
+
| ------------------------------------------------ | ------------------------------------------------------- |
|
|
361
|
+
| The version check, on a pull request into `main` | a `spec/source.json` that names a preview |
|
|
362
|
+
| The release workflow, on a push to `main` | the same, for a preview that reached `main` another way |
|
|
363
|
+
|
|
364
|
+
Run `scripts/update-spec.sh --main` and commit the result before releasing a `dev` that carries a preview.
|
|
365
|
+
|
|
366
|
+
#### Checks on the document
|
|
367
|
+
|
|
368
|
+
Three checks keep the document and the generated files honest.
|
|
369
|
+
|
|
370
|
+
| Check | Refuses |
|
|
371
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
372
|
+
| The generator | a document whose `info.version` is not a date, which means an earlier release |
|
|
373
|
+
| CI | a vendored document that was updated without regenerating the commands |
|
|
374
|
+
| CI | an API version that differs from the document's, because the generator writes it to `src/command/api-version.generated.ts` |
|
|
338
375
|
|
|
339
376
|
The first one matters because `hardfinhq/api-spec` can sit a release behind the monorepo
|
|
340
377
|
while its bridge pull request is open. Generating from that document would replace the
|
|
341
378
|
current commands with an earlier API's.
|
|
342
379
|
|
|
380
|
+
The API version every request sends is the document's `info.version`. Nothing sets it by hand,
|
|
381
|
+
so a new document cannot leave requests naming the version before it.
|
|
382
|
+
|
|
343
383
|
## Signing in
|
|
344
384
|
|
|
345
385
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4a31fc43-cc3a-59be-a501-eb86f4115b3f")}catch(e){}}();
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -8,79 +8,6 @@ import { dirname, join, resolve, sep } from "node:path";
|
|
|
8
8
|
import { parseEnv } from "node:util";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
//#region src/output/writer.ts
|
|
12
|
-
/**
|
|
13
|
-
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
14
|
-
* otherwise reach a request as the text "[object Object]".
|
|
15
|
-
*/
|
|
16
|
-
function toText(value) {
|
|
17
|
-
if (typeof value === "string") return value;
|
|
18
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
19
|
-
if (value === void 0 || value === null) return "";
|
|
20
|
-
return JSON.stringify(value);
|
|
21
|
-
}
|
|
22
|
-
/** Every control character but a newline and a tab, which are the two a line of text needs. */
|
|
23
|
-
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
24
|
-
/**
|
|
25
|
-
* toPrintable makes text safe to show at a terminal. A name or a message from the API can
|
|
26
|
-
* hold an escape sequence, which a terminal would act on. Each such character is written
|
|
27
|
-
* as its code, so the text reads as what it holds.
|
|
28
|
-
*/
|
|
29
|
-
function toPrintable(text) {
|
|
30
|
-
return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* toJSON writes a value for whoever is reading the stream. A person at a terminal gets it
|
|
34
|
-
* indented. A program gets it on one line, because the indentation of a page of records is
|
|
35
|
-
* close to half of what an agent would otherwise have to read.
|
|
36
|
-
*/
|
|
37
|
-
function toJSON(value, stream) {
|
|
38
|
-
return stream.isTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
39
|
-
}
|
|
40
|
-
/** writeData prints a command's result on stdout. */
|
|
41
|
-
function writeData(value) {
|
|
42
|
-
if (typeof value === "string") {
|
|
43
|
-
const text = toPrintable(value);
|
|
44
|
-
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
process.stdout.write(`${toJSON(value ?? null, process.stdout)}\n`);
|
|
48
|
-
}
|
|
49
|
-
/** The code of the last failure written, which decides whether the CLI checks for an update. */
|
|
50
|
-
let lastFailureCode;
|
|
51
|
-
/** toLastFailureCode answers the code of the last failure this run wrote. */
|
|
52
|
-
function toLastFailureCode() {
|
|
53
|
-
return lastFailureCode;
|
|
54
|
-
}
|
|
55
|
-
/** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
|
|
56
|
-
function writeFailure(code, message, isJSON, errors, requestId, status) {
|
|
57
|
-
lastFailureCode = code;
|
|
58
|
-
if (!isJSON) {
|
|
59
|
-
process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
60
|
-
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
|
|
61
|
-
if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const payload = {
|
|
65
|
-
code,
|
|
66
|
-
message,
|
|
67
|
-
errors: errors ?? [{
|
|
68
|
-
error: message,
|
|
69
|
-
statusCode: 0
|
|
70
|
-
}],
|
|
71
|
-
requestId: requestId ?? null,
|
|
72
|
-
status: status ?? null
|
|
73
|
-
};
|
|
74
|
-
process.stderr.write(`${toJSON(payload, process.stderr)}\n`);
|
|
75
|
-
}
|
|
76
|
-
/** isJSONOutput decides whether this invocation prints machine-readable output. */
|
|
77
|
-
function isJSONOutput(flags) {
|
|
78
|
-
if (flags["json"] === true) return true;
|
|
79
|
-
return !process.stdout.isTTY;
|
|
80
|
-
}
|
|
81
|
-
/** version is what this build reports, read from the published package manifest. */
|
|
82
|
-
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
83
|
-
//#endregion
|
|
84
11
|
//#region src/report/expected.ts
|
|
85
12
|
/**
|
|
86
13
|
* ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
|
|
@@ -112,6 +39,8 @@ var TimedOut = class extends ExpectedFailure {
|
|
|
112
39
|
this.name = "TimedOut";
|
|
113
40
|
}
|
|
114
41
|
};
|
|
42
|
+
/** version is what this build reports, read from the published package manifest. */
|
|
43
|
+
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
115
44
|
/** The values that mean no, for a variable that turns something on or off. */
|
|
116
45
|
const OFF = /* @__PURE__ */ new Set([
|
|
117
46
|
"0",
|
|
@@ -184,9 +113,11 @@ function toRelease(of) {
|
|
|
184
113
|
return `hardfin-cli@${of}`;
|
|
185
114
|
}
|
|
186
115
|
//#endregion
|
|
116
|
+
//#region src/command/api-version.generated.ts
|
|
117
|
+
/** API_VERSION is the version of the API document the commands were generated from. */
|
|
118
|
+
const API_VERSION = "2026-10-22";
|
|
119
|
+
//#endregion
|
|
187
120
|
//#region src/config/settings.ts
|
|
188
|
-
/** The API version this build was written against, sent on every request. */
|
|
189
|
-
const API_VERSION = "2026-10-21";
|
|
190
121
|
/** The file a local build reads its overrides from, in the working directory. */
|
|
191
122
|
const CONFIG_FILE = "config.local.json";
|
|
192
123
|
const DEFAULT_API_URL = "https://api.hardfin.com/v2";
|
|
@@ -304,6 +235,77 @@ function toOrigin(apiUrl) {
|
|
|
304
235
|
}
|
|
305
236
|
}
|
|
306
237
|
//#endregion
|
|
238
|
+
//#region src/output/writer.ts
|
|
239
|
+
/**
|
|
240
|
+
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
241
|
+
* otherwise reach a request as the text "[object Object]".
|
|
242
|
+
*/
|
|
243
|
+
function toText(value) {
|
|
244
|
+
if (typeof value === "string") return value;
|
|
245
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
246
|
+
if (value === void 0 || value === null) return "";
|
|
247
|
+
return JSON.stringify(value);
|
|
248
|
+
}
|
|
249
|
+
/** Every control character but a newline and a tab, which are the two a line of text needs. */
|
|
250
|
+
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
251
|
+
/**
|
|
252
|
+
* toPrintable makes text safe to show at a terminal. A name or a message from the API can
|
|
253
|
+
* hold an escape sequence, which a terminal would act on. Each such character is written
|
|
254
|
+
* as its code, so the text reads as what it holds.
|
|
255
|
+
*/
|
|
256
|
+
function toPrintable(text) {
|
|
257
|
+
return text.replace(CONTROL, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* toJSON writes a value for whoever is reading the stream. A person at a terminal gets it
|
|
261
|
+
* indented. A program gets it on one line, because the indentation of a page of records is
|
|
262
|
+
* close to half of what an agent would otherwise have to read.
|
|
263
|
+
*/
|
|
264
|
+
function toJSON(value, stream) {
|
|
265
|
+
return stream.isTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
266
|
+
}
|
|
267
|
+
/** writeData prints a command's result on stdout. */
|
|
268
|
+
function writeData(value) {
|
|
269
|
+
if (typeof value === "string") {
|
|
270
|
+
const text = toPrintable(value);
|
|
271
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(`${toJSON(value ?? null, process.stdout)}\n`);
|
|
275
|
+
}
|
|
276
|
+
/** The code of the last failure written, which decides whether the CLI checks for an update. */
|
|
277
|
+
let lastFailureCode;
|
|
278
|
+
/** toLastFailureCode answers the code of the last failure this run wrote. */
|
|
279
|
+
function toLastFailureCode() {
|
|
280
|
+
return lastFailureCode;
|
|
281
|
+
}
|
|
282
|
+
/** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
|
|
283
|
+
function writeFailure(code, message, isJSON, errors, requestId, status) {
|
|
284
|
+
lastFailureCode = code;
|
|
285
|
+
if (!isJSON) {
|
|
286
|
+
process.stderr.write(`error: ${toPrintable(message)}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
287
|
+
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${toPrintable(entry.error)} (${entry.statusCode})\n`);
|
|
288
|
+
if (requestId) process.stderr.write(` request ${toPrintable(requestId)}\n`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const payload = {
|
|
292
|
+
code,
|
|
293
|
+
message,
|
|
294
|
+
errors: errors ?? [{
|
|
295
|
+
error: message,
|
|
296
|
+
statusCode: 0
|
|
297
|
+
}],
|
|
298
|
+
requestId: requestId ?? null,
|
|
299
|
+
status: status ?? null
|
|
300
|
+
};
|
|
301
|
+
process.stderr.write(`${toJSON(payload, process.stderr)}\n`);
|
|
302
|
+
}
|
|
303
|
+
/** isJSONOutput decides whether this invocation prints machine-readable output. */
|
|
304
|
+
function isJSONOutput(flags) {
|
|
305
|
+
if (flags["json"] === true) return true;
|
|
306
|
+
return !process.stdout.isTTY;
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
307
309
|
//#region src/system/state.ts
|
|
308
310
|
/** toStateDirectory names the folder the CLI keeps what it remembers between runs in. */
|
|
309
311
|
function toStateDirectory() {
|
|
@@ -494,7 +496,7 @@ function writeCache(cache) {
|
|
|
494
496
|
} catch {}
|
|
495
497
|
}
|
|
496
498
|
//#endregion
|
|
497
|
-
export {
|
|
499
|
+
export { ExpectedFailure as C, TimedOut as E, version$1 as S, LockHeld as T, CONFIG_FILE as _, isShipped as a, API_VERSION as b, toOutdatedTo as c, toJSON as d, toLastFailureCode as f, writeFailure as g, writeData as h, toUpdateCheckRefusal as i, toStateDirectory as l, toText as m, isReminderDue as n, toPackageDirectory as o, toPrintable as p, toCheck as r, noteNotices as s, PACKAGE as t, isJSONOutput as u, ConfigFailure as v, Interrupted as w, toTelemetry as x, toSettings as y };
|
|
498
500
|
|
|
499
|
-
//# sourceMappingURL=check-
|
|
500
|
-
//# debugId=
|
|
501
|
+
//# sourceMappingURL=check-CvjTUx-K.js.map
|
|
502
|
+
//# debugId=4a31fc43-cc3a-59be-a501-eb86f4115b3f
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3bf6cdf0-d851-549b-9a43-bb3c8520e8ff")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,8 +14,8 @@ process.on("unhandledRejection", (error) => void toCrash(error));
|
|
|
14
14
|
try {
|
|
15
15
|
const answer = await toQuickAnswer(process.argv.slice(2));
|
|
16
16
|
if (answer === void 0) {
|
|
17
|
-
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-
|
|
18
|
-
const { startUpdateCheck, writeUpdateNotice } = await import("./notice-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-CC8Iqb14.js");
|
|
18
|
+
const { startUpdateCheck, writeUpdateNotice } = await import("./notice-D6mbHF9O.js");
|
|
19
19
|
const pending = startUpdateCheck(process.argv.slice(2));
|
|
20
20
|
try {
|
|
21
21
|
await toCli().parseAsync(process.argv);
|
|
@@ -36,7 +36,7 @@ async function toCrash(error) {
|
|
|
36
36
|
isCrashing = true;
|
|
37
37
|
const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
38
38
|
try {
|
|
39
|
-
const { toReported, writeCrash } = await import("./program-
|
|
39
|
+
const { toReported, writeCrash } = await import("./program-CC8Iqb14.js");
|
|
40
40
|
writeCrash(error, isJSON);
|
|
41
41
|
await toReported(error, isJSON, void 0, "fatal");
|
|
42
42
|
} catch {
|
|
@@ -57,4 +57,4 @@ async function toQuickAnswer(args) {
|
|
|
57
57
|
export {};
|
|
58
58
|
|
|
59
59
|
//# sourceMappingURL=cli.js.map
|
|
60
|
-
//# debugId=
|
|
60
|
+
//# debugId=3bf6cdf0-d851-549b-9a43-bb3c8520e8ff
|