@hardfin/cli 0.1.1-dev.55 → 0.1.1-dev.57
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 +38 -0
- package/dist/cli.js +4 -4
- package/dist/completion-tree.json +29 -0
- package/dist/{program-BhH7Zc0y.js → program-PaNgBWFL.js} +600 -420
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,19 @@ A value is sent as the type the document names. `--useful-life 36` sends the num
|
|
|
72
72
|
decimal such as `--salvage-value 1500.00` stays a string, which is how the API takes money
|
|
73
73
|
without losing precision.
|
|
74
74
|
|
|
75
|
+
Inside a `key=value` flag, `=` gives a value as you typed it and `:=` gives it as JSON.
|
|
76
|
+
|
|
77
|
+
| You type | Is sent as |
|
|
78
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
79
|
+
| `value=36` | the text "36", because a custom field may hold a serial number |
|
|
80
|
+
| `value:=36` | the number 36 |
|
|
81
|
+
| `value:=true` | the boolean true |
|
|
82
|
+
| `deliverAt:=null` | null |
|
|
83
|
+
| `order=2` or `order:=2` | the number 2, because the document types `order` as a whole number |
|
|
84
|
+
|
|
85
|
+
A value the field does not take fails locally with exit code 2, such as `order=two`, or
|
|
86
|
+
`id:=42` for a field that takes text alone.
|
|
87
|
+
|
|
75
88
|
A download is written where you name it, and a terminal is never filled with a file's bytes.
|
|
76
89
|
With no `--output`, the file takes the name it was uploaded under, in the working directory.
|
|
77
90
|
That name never chooses a folder, a hidden file, or a file that is already there. The command
|
|
@@ -130,6 +143,31 @@ An agent branches on `code`. A code is never renamed and never changes meaning,
|
|
|
130
143
|
message may be reworded at any time. `hardfin agent-guide` lists every code with what it
|
|
131
144
|
means, and `src/output/failure.ts` defines them.
|
|
132
145
|
|
|
146
|
+
### Calling an endpoint no command covers
|
|
147
|
+
|
|
148
|
+
`hardfin api` calls any path under the API. It takes the same parts a generated command does,
|
|
149
|
+
named by hand.
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
hardfin api /customer -f page=2 -f limit=50 # query parameters
|
|
153
|
+
hardfin api -X PATCH /item/item_V1StGXR8Z5jdHi6B --input body.json # a JSON body
|
|
154
|
+
hardfin api /file/file_7hq2mx9pkr4stz8w --output photo.jpg # a download
|
|
155
|
+
hardfin api -X POST /file --file photo.jpg -F fileType=ASSET_FILE # an upload, as multipart
|
|
156
|
+
hardfin api /file/file_7hq2mx9pkr4stz8w -H 'If-None-Match: "abc"' --output photo.jpg
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Flag | Sends |
|
|
160
|
+
| ------------------ | ---------------------------------------------------------------------------------- |
|
|
161
|
+
| `-f key=value` | A query parameter, repeatable |
|
|
162
|
+
| `--input <path>` | A JSON body, from a file or `-` for standard input |
|
|
163
|
+
| `--file <path>` | A file as multipart, under the part name `data` unless `--file-part` names another |
|
|
164
|
+
| `-F key=value` | A field beside the file, repeatable |
|
|
165
|
+
| `-H "Name: value"` | A request header, repeatable |
|
|
166
|
+
| `--output <path>` | Nothing. It writes the file the endpoint answers with, or `-` for standard output |
|
|
167
|
+
|
|
168
|
+
A header cannot set `Authorization`, `X-API-Key` or `X-API-Version`, which come from your sign
|
|
169
|
+
in and settings. `--file` and `--input` cannot be given together.
|
|
170
|
+
|
|
133
171
|
### Reading the commands from a program
|
|
134
172
|
|
|
135
173
|
`hardfin agent-guide` describes the CLI to an agent, as Markdown or as JSON. Name a command to
|
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]="1c7d3a24-925a-543e-a2eb-caf5f5968ada")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,7 +14,7 @@ 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-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-PaNgBWFL.js");
|
|
18
18
|
try {
|
|
19
19
|
await toCli().parseAsync(process.argv);
|
|
20
20
|
} catch (error) {
|
|
@@ -33,7 +33,7 @@ async function toCrash(error) {
|
|
|
33
33
|
isCrashing = true;
|
|
34
34
|
const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
35
35
|
try {
|
|
36
|
-
const { toReported, writeCrash } = await import("./program-
|
|
36
|
+
const { toReported, writeCrash } = await import("./program-PaNgBWFL.js");
|
|
37
37
|
writeCrash(error, isJSON);
|
|
38
38
|
await toReported(error, isJSON, void 0, "fatal");
|
|
39
39
|
} catch {
|
|
@@ -54,4 +54,4 @@ async function toQuickAnswer(args) {
|
|
|
54
54
|
export {};
|
|
55
55
|
|
|
56
56
|
//# sourceMappingURL=cli.js.map
|
|
57
|
-
//# debugId=
|
|
57
|
+
//# debugId=1c7d3a24-925a-543e-a2eb-caf5f5968ada
|
|
@@ -2566,6 +2566,35 @@
|
|
|
2566
2566
|
"description": "A file holding the JSON request body, or - for stdin",
|
|
2567
2567
|
"valueName": "file"
|
|
2568
2568
|
},
|
|
2569
|
+
{
|
|
2570
|
+
"name": "file",
|
|
2571
|
+
"description": "A file to upload as multipart, in place of a JSON body",
|
|
2572
|
+
"valueName": "path"
|
|
2573
|
+
},
|
|
2574
|
+
{
|
|
2575
|
+
"name": "file-part",
|
|
2576
|
+
"description": "The name of the part the file is sent as, which is data when not given",
|
|
2577
|
+
"valueName": "name"
|
|
2578
|
+
},
|
|
2579
|
+
{
|
|
2580
|
+
"name": "form",
|
|
2581
|
+
"short": "F",
|
|
2582
|
+
"description": "A field sent beside the file as key=value, repeatable",
|
|
2583
|
+
"valueName": "key=value",
|
|
2584
|
+
"repeatable": true
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
"name": "header",
|
|
2588
|
+
"short": "H",
|
|
2589
|
+
"description": "A request header as Name: value, repeatable",
|
|
2590
|
+
"valueName": "name: value",
|
|
2591
|
+
"repeatable": true
|
|
2592
|
+
},
|
|
2593
|
+
{
|
|
2594
|
+
"name": "output",
|
|
2595
|
+
"description": "Where to write a file the endpoint answers with, or - for standard output",
|
|
2596
|
+
"valueName": "path"
|
|
2597
|
+
},
|
|
2569
2598
|
{
|
|
2570
2599
|
"name": "json",
|
|
2571
2600
|
"description": "Print machine-readable output, which is the default when stdout is not a terminal"
|
|
@@ -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]="566d9b65-d8fb-507d-8b0e-a21deb646246")}catch(e){}}();
|
|
4
4
|
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BZ0egWxC.js";
|
|
5
5
|
import { n as toAnswer$1, r as toCompletion, t as PATH_VALUES } from "./completion-tree-DLC6ARLR.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -1127,6 +1127,7 @@ async function request$1(options) {
|
|
|
1127
1127
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
1128
1128
|
for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
|
|
1129
1129
|
const headers = {
|
|
1130
|
+
...options.headers,
|
|
1130
1131
|
[options.credential.header]: options.credential.value,
|
|
1131
1132
|
"X-API-Version": API_VERSION,
|
|
1132
1133
|
Accept: options.downloads ? "*/*" : "application/json"
|
|
@@ -1477,7 +1478,8 @@ function toDescribed(flag) {
|
|
|
1477
1478
|
default: flag.defaultValue,
|
|
1478
1479
|
schema: toFlagSchema(flag),
|
|
1479
1480
|
sends: sent.queryName !== void 0 ? { query: sent.queryName } : sent.jsonPath !== void 0 ? { body: sent.jsonPath } : void 0,
|
|
1480
|
-
element: sent.element
|
|
1481
|
+
element: sent.element,
|
|
1482
|
+
elementTypes: sent.elementTypes
|
|
1481
1483
|
};
|
|
1482
1484
|
return Object.fromEntries(Object.entries(described).filter(([, value]) => value !== void 0 && value !== false));
|
|
1483
1485
|
}
|
|
@@ -1557,10 +1559,363 @@ function toJSONInput(source) {
|
|
|
1557
1559
|
return new InputFailure(`${name} does not hold JSON`);
|
|
1558
1560
|
}
|
|
1559
1561
|
}
|
|
1562
|
+
//#endregion
|
|
1563
|
+
//#region src/command/operation.ts
|
|
1564
|
+
/**
|
|
1565
|
+
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
1566
|
+
* lists, which is what a response always uses.
|
|
1567
|
+
*/
|
|
1568
|
+
function toEnum(values) {
|
|
1569
|
+
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
1570
|
+
}
|
|
1571
|
+
const UNSET_FLAG = {
|
|
1572
|
+
name: "unset",
|
|
1573
|
+
description: "A field to clear, named as its flag is, repeatable",
|
|
1574
|
+
valueName: "field",
|
|
1575
|
+
repeatable: true,
|
|
1576
|
+
schema: z.array(z.string())
|
|
1577
|
+
};
|
|
1578
|
+
const INPUT_FLAG = {
|
|
1579
|
+
name: "input",
|
|
1580
|
+
description: "A file holding the JSON request body, or - for standard input. A flag given as well replaces its field",
|
|
1581
|
+
valueName: "path",
|
|
1582
|
+
schema: z.string()
|
|
1583
|
+
};
|
|
1584
|
+
const FILE_FLAG = {
|
|
1585
|
+
name: "file",
|
|
1586
|
+
description: "The file to upload",
|
|
1587
|
+
valueName: "path",
|
|
1588
|
+
schema: z.string()
|
|
1589
|
+
};
|
|
1590
|
+
const OUTPUT_FLAG = {
|
|
1591
|
+
name: "output",
|
|
1592
|
+
description: "Where to write the file, or - for standard output",
|
|
1593
|
+
valueName: "path",
|
|
1594
|
+
schema: z.string()
|
|
1595
|
+
};
|
|
1596
|
+
const JSON_FLAG$1 = {
|
|
1597
|
+
name: "json",
|
|
1598
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1599
|
+
schema: z.boolean()
|
|
1600
|
+
};
|
|
1601
|
+
/** defineOperation turns one endpoint into the command that calls it. */
|
|
1602
|
+
function defineOperation(operation) {
|
|
1603
|
+
const flags = [
|
|
1604
|
+
...operation.queryFlags,
|
|
1605
|
+
...operation.bodyFlags,
|
|
1606
|
+
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
1607
|
+
...operation.bodyFlags.length > 0 ? [INPUT_FLAG] : [],
|
|
1608
|
+
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
1609
|
+
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
1610
|
+
JSON_FLAG$1
|
|
1611
|
+
];
|
|
1612
|
+
return {
|
|
1613
|
+
name: operation.name,
|
|
1614
|
+
summary: operation.summary,
|
|
1615
|
+
description: operation.description ?? operation.summary,
|
|
1616
|
+
arguments: operation.pathParameters,
|
|
1617
|
+
flags,
|
|
1618
|
+
examples: operation.example ? [{
|
|
1619
|
+
description: operation.summary,
|
|
1620
|
+
command: operation.example
|
|
1621
|
+
}] : [],
|
|
1622
|
+
endpoint: {
|
|
1623
|
+
method: operation.method,
|
|
1624
|
+
path: operation.path
|
|
1625
|
+
},
|
|
1626
|
+
run: (input) => runOperation(operation, input)
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
async function runOperation(operation, input) {
|
|
1630
|
+
const path = toPath(operation, input.args);
|
|
1631
|
+
if (path === void 0) {
|
|
1632
|
+
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s), each the key of a record`, input.isJSON);
|
|
1633
|
+
return ExitCode.USAGE;
|
|
1634
|
+
}
|
|
1635
|
+
const given = toGivenBody$1(input.flags["input"]);
|
|
1636
|
+
if (given instanceof InputFailure) {
|
|
1637
|
+
writeFailure("invalid_input", given.message, input.isJSON);
|
|
1638
|
+
return ExitCode.USAGE;
|
|
1639
|
+
}
|
|
1640
|
+
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0 && !hasPath(given, flag.jsonPath));
|
|
1641
|
+
if (missing.length > 0) {
|
|
1642
|
+
writeFailure("missing_flag", `this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
1643
|
+
return ExitCode.USAGE;
|
|
1644
|
+
}
|
|
1645
|
+
const body = toBody(operation, input.flags, given);
|
|
1646
|
+
if (body instanceof Error) {
|
|
1647
|
+
writeFailure("invalid_flag", body.message, input.isJSON);
|
|
1648
|
+
return ExitCode.USAGE;
|
|
1649
|
+
}
|
|
1650
|
+
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
1651
|
+
writeFailure("empty_request", `this command changes nothing unless a flag is given, such as ${operation.bodyFlags.slice(0, 3).map((flag) => `--${flag.name}`).join(", ")}. Run it with --help for the rest`, input.isJSON);
|
|
1652
|
+
return ExitCode.USAGE;
|
|
1653
|
+
}
|
|
1654
|
+
let form;
|
|
1655
|
+
if (operation.upload) {
|
|
1656
|
+
const built = await toForm$1(operation.upload, input.flags);
|
|
1657
|
+
if (built instanceof Error) {
|
|
1658
|
+
writeFailure("invalid_input", built.message, input.isJSON);
|
|
1659
|
+
return ExitCode.USAGE;
|
|
1660
|
+
}
|
|
1661
|
+
form = built;
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
const envelope = await request$1({
|
|
1665
|
+
apiUrl: input.resolved.settings.apiUrl,
|
|
1666
|
+
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
1667
|
+
method: operation.method,
|
|
1668
|
+
path,
|
|
1669
|
+
query: toQuery$1(operation, input.flags),
|
|
1670
|
+
body,
|
|
1671
|
+
form,
|
|
1672
|
+
downloads: operation.downloads
|
|
1673
|
+
});
|
|
1674
|
+
if (operation.downloads) return toWritten(envelope.data, input);
|
|
1675
|
+
writeData(envelope.data);
|
|
1676
|
+
return ExitCode.OK;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
if (error instanceof NoCredential) {
|
|
1679
|
+
writeFailure("not_signed_in", error.message, input.isJSON);
|
|
1680
|
+
return ExitCode.NOT_AUTHENTICATED;
|
|
1681
|
+
}
|
|
1682
|
+
if (error instanceof RequestFailure) {
|
|
1683
|
+
writeFailure(toRequestCode(error), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1684
|
+
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
1685
|
+
}
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* toWritten puts a downloaded file where it was asked for. A terminal is never written to,
|
|
1691
|
+
* because a person would otherwise have their session filled with a file's bytes.
|
|
1692
|
+
*/
|
|
1693
|
+
function toWritten(download, input) {
|
|
1694
|
+
const asked = input.flags["output"];
|
|
1695
|
+
const offered = typeof asked === "string" ? void 0 : download.fileName;
|
|
1696
|
+
const path = typeof asked === "string" ? asked : offered === void 0 ? "-" : toLocalName(offered);
|
|
1697
|
+
if (path === void 0 || offered !== void 0 && existsSync(path)) {
|
|
1698
|
+
writeFailure("output_required", path === void 0 ? `this file is offered as ${JSON.stringify(offered)}, which is not a name to write here, so name where to write it with --output` : `${path} already exists, so name where to write this file with --output`, input.isJSON);
|
|
1699
|
+
return ExitCode.USAGE;
|
|
1700
|
+
}
|
|
1701
|
+
if (path === "-") {
|
|
1702
|
+
if (process.stdout.isTTY) {
|
|
1703
|
+
writeFailure("output_required", "this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
1704
|
+
return ExitCode.USAGE;
|
|
1705
|
+
}
|
|
1706
|
+
process.stdout.write(download.bytes);
|
|
1707
|
+
return ExitCode.OK;
|
|
1708
|
+
}
|
|
1709
|
+
writeFileSync(path, download.bytes);
|
|
1710
|
+
writeData({
|
|
1711
|
+
written: path,
|
|
1712
|
+
bytes: download.bytes.length,
|
|
1713
|
+
contentType: download.contentType
|
|
1714
|
+
});
|
|
1715
|
+
return ExitCode.OK;
|
|
1716
|
+
}
|
|
1717
|
+
/** toLocalName reduces a name the API offered to one file in the working directory, or to nothing. */
|
|
1718
|
+
function toLocalName(offered) {
|
|
1719
|
+
const name = offered.split(/[\\/]/).pop() ?? "";
|
|
1720
|
+
return name === "" || name.startsWith(".") ? void 0 : name;
|
|
1721
|
+
}
|
|
1722
|
+
/** The arguments a URL reads as "this folder" and "the one above", and one that names nothing. */
|
|
1723
|
+
const NOT_A_KEY = /* @__PURE__ */ new Set([
|
|
1724
|
+
"",
|
|
1725
|
+
".",
|
|
1726
|
+
".."
|
|
1727
|
+
]);
|
|
1728
|
+
/** toPath fills the path template from the positional arguments, in order. */
|
|
1729
|
+
function toPath(operation, args) {
|
|
1730
|
+
if (args.length !== operation.pathParameters.length || args.some((argument) => NOT_A_KEY.has(argument))) return;
|
|
1731
|
+
let path = operation.path;
|
|
1732
|
+
for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
|
|
1733
|
+
return path;
|
|
1734
|
+
}
|
|
1735
|
+
/** toQuery carries only the flags this invocation actually set. */
|
|
1736
|
+
function toQuery$1(operation, flags) {
|
|
1737
|
+
const query = new URLSearchParams();
|
|
1738
|
+
for (const flag of operation.queryFlags) {
|
|
1739
|
+
const value = flags[toOptionKey(flag.name)];
|
|
1740
|
+
if (value === void 0) continue;
|
|
1741
|
+
const parsed = flag.schema.safeParse(value);
|
|
1742
|
+
const carried = parsed.success ? parsed.data : value;
|
|
1743
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
1744
|
+
}
|
|
1745
|
+
return query;
|
|
1746
|
+
}
|
|
1747
|
+
/** toOptionKey names the parsed flag, which the parser reports in camel case. */
|
|
1748
|
+
function toOptionKey(name) {
|
|
1749
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1750
|
+
}
|
|
1751
|
+
/** toForm builds the file part and its fields, which an upload endpoint takes. */
|
|
1752
|
+
async function toForm$1(upload, flags) {
|
|
1753
|
+
const path = flags["file"];
|
|
1754
|
+
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
1755
|
+
const form = new FormData();
|
|
1756
|
+
try {
|
|
1757
|
+
form.append(upload.filePart, await openAsBlob(path), basename(path));
|
|
1758
|
+
} catch {
|
|
1759
|
+
return /* @__PURE__ */ new Error(`${path} cannot be read`);
|
|
1760
|
+
}
|
|
1761
|
+
for (const field of upload.fields) {
|
|
1762
|
+
const value = flags[toOptionKey(field.name)];
|
|
1763
|
+
if (value !== void 0) {
|
|
1764
|
+
const parsed = field.schema.safeParse(value);
|
|
1765
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return form;
|
|
1769
|
+
}
|
|
1770
|
+
/** toGivenBody reads the --input body, which a generated command takes only as a JSON object. */
|
|
1771
|
+
function toGivenBody$1(source) {
|
|
1772
|
+
if (typeof source !== "string") return;
|
|
1773
|
+
const read = toJSONInput(source);
|
|
1774
|
+
if (read instanceof InputFailure) return read;
|
|
1775
|
+
if (typeof read !== "object" || read === null || Array.isArray(read)) return new InputFailure(`the body in ${source === "-" ? "standard input" : source} must be a JSON object, such as {"name": "Acme"}`);
|
|
1776
|
+
return read;
|
|
1777
|
+
}
|
|
1778
|
+
/** hasPath reports whether a body already holds a value at a field's path. */
|
|
1779
|
+
function hasPath(body, path) {
|
|
1780
|
+
let holder = body;
|
|
1781
|
+
for (const name of path) {
|
|
1782
|
+
if (typeof holder !== "object" || holder === null || !(name in holder)) return false;
|
|
1783
|
+
holder = holder[name];
|
|
1784
|
+
}
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
/** toBody builds the request body from the --input body and the flags, a flag winning over its field. */
|
|
1788
|
+
function toBody(operation, flags, given) {
|
|
1789
|
+
const body = given ? structuredClone(given) : {};
|
|
1790
|
+
let hasField = given !== void 0;
|
|
1791
|
+
for (const flag of operation.bodyFlags) {
|
|
1792
|
+
const value = flags[toOptionKey(flag.name)];
|
|
1793
|
+
if (value === void 0) continue;
|
|
1794
|
+
if (flag.element) {
|
|
1795
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
1796
|
+
if (elements instanceof Error) return elements;
|
|
1797
|
+
set(body, flag.jsonPath, elements);
|
|
1798
|
+
hasField = true;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
const parsed = flag.schema.safeParse(value);
|
|
1802
|
+
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
1803
|
+
hasField = true;
|
|
1804
|
+
}
|
|
1805
|
+
const cleared = toCleared(operation, flags, body);
|
|
1806
|
+
if (cleared instanceof Error) return cleared;
|
|
1807
|
+
return hasField || cleared ? body : void 0;
|
|
1808
|
+
}
|
|
1809
|
+
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
1810
|
+
function toCleared(operation, flags, body) {
|
|
1811
|
+
const named = flags["unset"];
|
|
1812
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
1813
|
+
for (const name of names) {
|
|
1814
|
+
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
1815
|
+
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
1816
|
+
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
1817
|
+
set(body, flag.jsonPath, null);
|
|
1818
|
+
}
|
|
1819
|
+
return names.length > 0;
|
|
1820
|
+
}
|
|
1821
|
+
/** toElements reads the key=value pairs a repeated flag carries for one array element. */
|
|
1822
|
+
function toElements(flag, values) {
|
|
1823
|
+
const elements = [];
|
|
1824
|
+
for (const value of values) {
|
|
1825
|
+
const element = {};
|
|
1826
|
+
for (const pair of value.split(",")) {
|
|
1827
|
+
const split = pair.indexOf("=");
|
|
1828
|
+
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ") ?? ""}=, such as --${flag.name} ${flag.element?.[0] ?? "field"}=value. A value cannot contain a comma`);
|
|
1829
|
+
const isJSON = pair[split - 1] === ":";
|
|
1830
|
+
const key = pair.slice(0, isJSON ? split - 1 : split).trim();
|
|
1831
|
+
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
1832
|
+
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
1833
|
+
const value = pair.slice(split + 1).trim();
|
|
1834
|
+
if (flag.elementUuids?.includes(key) && !z.uuid().safeParse(value).success) return /* @__PURE__ */ new Error(`--${flag.name} needs a UUID for ${key}, such as 3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90, and ${value || "nothing"} is not one`);
|
|
1835
|
+
const types = flag.elementTypes?.[key];
|
|
1836
|
+
const typed = isJSON ? toJSONValue(value, types ?? ["string"]) : toTyped(value, types);
|
|
1837
|
+
if (typed === NOT_TAKEN) return /* @__PURE__ */ new Error(`--${flag.name} needs ${toNamedTypes(types ?? ["string"])} for ${key}, and ${value || "nothing"} is not one`);
|
|
1838
|
+
element[key] = typed;
|
|
1839
|
+
}
|
|
1840
|
+
elements.push(element);
|
|
1841
|
+
}
|
|
1842
|
+
return elements;
|
|
1843
|
+
}
|
|
1844
|
+
/** Marks text that is none of the types a field takes. */
|
|
1845
|
+
const NOT_TAKEN = Symbol("not taken");
|
|
1846
|
+
const NUMBER = /^-?\d+(\.\d+)?$/;
|
|
1847
|
+
/**
|
|
1848
|
+
* toTyped reads the text after key= as the type the document gives the field. A field that
|
|
1849
|
+
* takes text is sent as typed, whatever it looks like, because 36 may be a serial number. A
|
|
1850
|
+
* field that takes no text has only one reading, so order=2 is sent as the number.
|
|
1851
|
+
*/
|
|
1852
|
+
function toTyped(text, types) {
|
|
1853
|
+
if (types === void 0 || types.includes("string")) return text;
|
|
1854
|
+
if (types.includes("null") && text === "null") return null;
|
|
1855
|
+
if ((types.includes("integer") || types.includes("number")) && NUMBER.test(text)) {
|
|
1856
|
+
const value = Number(text);
|
|
1857
|
+
if (types.includes("number") || Number.isInteger(value)) return value;
|
|
1858
|
+
}
|
|
1859
|
+
if (types.includes("boolean") && (text === "true" || text === "false")) return text === "true";
|
|
1860
|
+
return NOT_TAKEN;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* toJSONValue reads the text after key:= as JSON, and takes it when the field takes its type.
|
|
1864
|
+
* It is how a custom field is given the number 36 or true, where key= would send the text.
|
|
1865
|
+
*/
|
|
1866
|
+
function toJSONValue(text, types) {
|
|
1867
|
+
let value;
|
|
1868
|
+
try {
|
|
1869
|
+
value = JSON.parse(text);
|
|
1870
|
+
} catch {
|
|
1871
|
+
return NOT_TAKEN;
|
|
1872
|
+
}
|
|
1873
|
+
const type = value === null ? "null" : typeof value;
|
|
1874
|
+
return (types.includes(type) || type === "number" && types.includes("integer") && Number.isInteger(value)) && type !== "object" ? value : NOT_TAKEN;
|
|
1875
|
+
}
|
|
1876
|
+
/** toNamedTypes says in words what a field takes, for the message that refuses a value. */
|
|
1877
|
+
function toNamedTypes(types) {
|
|
1878
|
+
const named = {
|
|
1879
|
+
integer: "a whole number",
|
|
1880
|
+
number: "a number",
|
|
1881
|
+
boolean: "true or false",
|
|
1882
|
+
string: "text in quotes",
|
|
1883
|
+
null: "null"
|
|
1884
|
+
};
|
|
1885
|
+
return types.filter((type) => type in named).map((type) => named[type]).join(" or ");
|
|
1886
|
+
}
|
|
1887
|
+
/** set writes a value at its path, building the objects a nested field needs. */
|
|
1888
|
+
function set(body, path, value) {
|
|
1889
|
+
let holder = body;
|
|
1890
|
+
for (const name of path.slice(0, -1)) {
|
|
1891
|
+
holder[name] = holder[name] ?? {};
|
|
1892
|
+
holder = holder[name];
|
|
1893
|
+
}
|
|
1894
|
+
holder[path[path.length - 1] ?? ""] = value;
|
|
1895
|
+
}
|
|
1896
|
+
//#endregion
|
|
1897
|
+
//#region src/command/api.ts
|
|
1898
|
+
const METHODS = [
|
|
1899
|
+
"GET",
|
|
1900
|
+
"POST",
|
|
1901
|
+
"PATCH",
|
|
1902
|
+
"PUT",
|
|
1903
|
+
"DELETE"
|
|
1904
|
+
];
|
|
1905
|
+
/** The part name the API's upload endpoints take a file under. */
|
|
1906
|
+
const DEFAULT_FILE_PART = "data";
|
|
1907
|
+
/** The headers that carry the credential and the version, which the CLI sets and a caller does not. */
|
|
1908
|
+
const KEPT_HEADERS = /* @__PURE__ */ new Set([
|
|
1909
|
+
"authorization",
|
|
1910
|
+
"x-api-key",
|
|
1911
|
+
"x-api-version",
|
|
1912
|
+
"host",
|
|
1913
|
+
"content-length"
|
|
1914
|
+
]);
|
|
1560
1915
|
const apiCommand = defineCommand({
|
|
1561
1916
|
name: "api",
|
|
1562
1917
|
summary: "Call a Hardfin API endpoint and print what it answers",
|
|
1563
|
-
description: "Reaches every endpoint the API publishes. The path is everything after /v2, and the
|
|
1918
|
+
description: "Reaches every endpoint the API publishes, for when no generated command fits. The path is everything after /v2, and what the endpoint answered with is printed. A file is uploaded with --file and downloaded with --output.",
|
|
1564
1919
|
arguments: [{
|
|
1565
1920
|
name: "path",
|
|
1566
1921
|
description: "The endpoint path, such as /customer or /item/item_V1StGXR8Z5jdHi6B",
|
|
@@ -1572,13 +1927,7 @@ const apiCommand = defineCommand({
|
|
|
1572
1927
|
short: "X",
|
|
1573
1928
|
description: "The HTTP method to use",
|
|
1574
1929
|
valueName: "method",
|
|
1575
|
-
schema: z.enum(
|
|
1576
|
-
"GET",
|
|
1577
|
-
"POST",
|
|
1578
|
-
"PATCH",
|
|
1579
|
-
"PUT",
|
|
1580
|
-
"DELETE"
|
|
1581
|
-
]),
|
|
1930
|
+
schema: z.enum(METHODS),
|
|
1582
1931
|
defaultValue: "GET"
|
|
1583
1932
|
},
|
|
1584
1933
|
{
|
|
@@ -1595,6 +1944,40 @@ const apiCommand = defineCommand({
|
|
|
1595
1944
|
valueName: "file",
|
|
1596
1945
|
schema: z.string().optional()
|
|
1597
1946
|
},
|
|
1947
|
+
{
|
|
1948
|
+
name: "file",
|
|
1949
|
+
description: "A file to upload as multipart, in place of a JSON body",
|
|
1950
|
+
valueName: "path",
|
|
1951
|
+
schema: z.string().optional()
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
name: "file-part",
|
|
1955
|
+
description: `The name of the part the file is sent as, which is ${DEFAULT_FILE_PART} when not given`,
|
|
1956
|
+
valueName: "name",
|
|
1957
|
+
schema: z.string().optional()
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
name: "form",
|
|
1961
|
+
short: "F",
|
|
1962
|
+
description: "A field sent beside the file as key=value, repeatable",
|
|
1963
|
+
valueName: "key=value",
|
|
1964
|
+
repeatable: true,
|
|
1965
|
+
schema: z.array(z.string()).default([])
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
name: "header",
|
|
1969
|
+
short: "H",
|
|
1970
|
+
description: "A request header as Name: value, repeatable",
|
|
1971
|
+
valueName: "name: value",
|
|
1972
|
+
repeatable: true,
|
|
1973
|
+
schema: z.array(z.string()).default([])
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
name: "output",
|
|
1977
|
+
description: "Where to write a file the endpoint answers with, or - for standard output",
|
|
1978
|
+
valueName: "path",
|
|
1979
|
+
schema: z.string().optional()
|
|
1980
|
+
},
|
|
1598
1981
|
{
|
|
1599
1982
|
name: "json",
|
|
1600
1983
|
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
@@ -1613,6 +1996,18 @@ const apiCommand = defineCommand({
|
|
|
1613
1996
|
{
|
|
1614
1997
|
description: "Change an item",
|
|
1615
1998
|
command: "hardfin api -X PATCH /item/item_V1StGXR8Z5jdHi6B --input body.json"
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
description: "Download a file",
|
|
2002
|
+
command: "hardfin api /file/file_7hq2mx9pkr4stz8w --output photo.jpg"
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
description: "Upload a file, with the fields the endpoint takes beside it",
|
|
2006
|
+
command: "hardfin api -X POST /file --file photo.jpg -F fileType=ASSET_FILE -F forEntity=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70"
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
description: "Send a header",
|
|
2010
|
+
command: "hardfin api /file/file_7hq2mx9pkr4stz8w -H \"If-None-Match: \\\"abc\\\"\" --output photo.jpg"
|
|
1616
2011
|
}
|
|
1617
2012
|
],
|
|
1618
2013
|
run: runApi
|
|
@@ -1627,11 +2022,22 @@ async function runApi(input) {
|
|
|
1627
2022
|
writeFailure("invalid_arguments", "a path stays inside the API, so it cannot climb out of it with ..", input.isJSON);
|
|
1628
2023
|
return ExitCode.USAGE;
|
|
1629
2024
|
}
|
|
1630
|
-
const query = toQuery
|
|
2025
|
+
const query = toQuery(input.flags["field"]);
|
|
1631
2026
|
if (query === void 0) {
|
|
1632
2027
|
writeFailure("invalid_flag", "each --field is key=value, such as -f limit=50", input.isJSON);
|
|
1633
2028
|
return ExitCode.USAGE;
|
|
1634
2029
|
}
|
|
2030
|
+
const headers = toHeaders(input.flags["header"]);
|
|
2031
|
+
if (headers instanceof Error) {
|
|
2032
|
+
writeFailure("invalid_flag", headers.message, input.isJSON);
|
|
2033
|
+
return ExitCode.USAGE;
|
|
2034
|
+
}
|
|
2035
|
+
const method = toText(input.flags["method"] ?? "GET").toUpperCase();
|
|
2036
|
+
const isUpload = typeof input.flags["file"] === "string";
|
|
2037
|
+
if (isUpload && (typeof input.flags["input"] === "string" || method === "GET")) {
|
|
2038
|
+
writeFailure("invalid_flag", method === "GET" ? "--file uploads a file, so give the method with -X, such as -X POST" : "--file and --input both give the body, so give one of them", input.isJSON);
|
|
2039
|
+
return ExitCode.USAGE;
|
|
2040
|
+
}
|
|
1635
2041
|
let body;
|
|
1636
2042
|
if (typeof input.flags["input"] === "string") {
|
|
1637
2043
|
body = toJSONInput(input.flags["input"]);
|
|
@@ -1640,15 +2046,26 @@ async function runApi(input) {
|
|
|
1640
2046
|
return ExitCode.USAGE;
|
|
1641
2047
|
}
|
|
1642
2048
|
}
|
|
2049
|
+
const form = isUpload ? await toForm(input.flags) : void 0;
|
|
2050
|
+
if (form instanceof Error) {
|
|
2051
|
+
writeFailure("invalid_input", form.message, input.isJSON);
|
|
2052
|
+
return ExitCode.USAGE;
|
|
2053
|
+
}
|
|
2054
|
+
const isDownload = typeof input.flags["output"] === "string";
|
|
1643
2055
|
try {
|
|
1644
|
-
|
|
2056
|
+
const envelope = await request$1({
|
|
1645
2057
|
apiUrl: input.resolved.settings.apiUrl,
|
|
1646
2058
|
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
1647
|
-
method
|
|
2059
|
+
method,
|
|
1648
2060
|
path,
|
|
1649
2061
|
query,
|
|
1650
|
-
body
|
|
1651
|
-
|
|
2062
|
+
body,
|
|
2063
|
+
form,
|
|
2064
|
+
headers,
|
|
2065
|
+
downloads: isDownload
|
|
2066
|
+
});
|
|
2067
|
+
if (isDownload) return toWritten(envelope.data, input);
|
|
2068
|
+
writeData(envelope.data);
|
|
1652
2069
|
return ExitCode.OK;
|
|
1653
2070
|
} catch (error) {
|
|
1654
2071
|
if (error instanceof NoCredential) {
|
|
@@ -1670,11 +2087,39 @@ function isInsideApi(apiUrl, path) {
|
|
|
1670
2087
|
const root = base.pathname.replace(/\/+$/, "");
|
|
1671
2088
|
return target.origin === base.origin && (target.pathname === root || target.pathname.startsWith(`${root}/`));
|
|
1672
2089
|
} catch {
|
|
1673
|
-
return false;
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
/** toHeaders reads the repeated --header flags, and refuses one the CLI sets itself. */
|
|
2094
|
+
function toHeaders(given) {
|
|
2095
|
+
const headers = {};
|
|
2096
|
+
for (const header of Array.isArray(given) ? given : []) {
|
|
2097
|
+
const split = header.indexOf(":");
|
|
2098
|
+
const name = header.slice(0, Math.max(split, 0)).trim();
|
|
2099
|
+
if (split < 1 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) return /* @__PURE__ */ new Error(`--header is Name: value, such as -H "If-None-Match: abc", and ${JSON.stringify(header)} is not`);
|
|
2100
|
+
if (KEPT_HEADERS.has(name.toLowerCase())) return /* @__PURE__ */ new Error(`--header cannot set ${name}, because the CLI sets it from your sign in and settings`);
|
|
2101
|
+
headers[name] = header.slice(split + 1).trim();
|
|
2102
|
+
}
|
|
2103
|
+
return headers;
|
|
2104
|
+
}
|
|
2105
|
+
/** toForm builds the multipart body an upload sends: the file, and the fields given beside it. */
|
|
2106
|
+
async function toForm(flags) {
|
|
2107
|
+
const path = toText(flags["file"]);
|
|
2108
|
+
const form = new FormData();
|
|
2109
|
+
try {
|
|
2110
|
+
form.append(toText(flags["filePart"] ?? DEFAULT_FILE_PART), await openAsBlob(path), basename(path));
|
|
2111
|
+
} catch {
|
|
2112
|
+
return /* @__PURE__ */ new Error(`${path} cannot be read`);
|
|
2113
|
+
}
|
|
2114
|
+
for (const field of Array.isArray(flags["form"]) ? flags["form"] : []) {
|
|
2115
|
+
const split = field.indexOf("=");
|
|
2116
|
+
if (split < 1) return /* @__PURE__ */ new Error(`each --form is key=value, such as -F fileType=ASSET_FILE, and ${JSON.stringify(field)} is not`);
|
|
2117
|
+
form.append(field.slice(0, split), field.slice(split + 1));
|
|
1674
2118
|
}
|
|
2119
|
+
return form;
|
|
1675
2120
|
}
|
|
1676
2121
|
/** toQuery folds the repeated --field flags into query parameters. */
|
|
1677
|
-
function toQuery
|
|
2122
|
+
function toQuery(fields) {
|
|
1678
2123
|
const query = new URLSearchParams();
|
|
1679
2124
|
for (const field of Array.isArray(fields) ? fields : []) {
|
|
1680
2125
|
const split = field.indexOf("=");
|
|
@@ -1932,7 +2377,7 @@ const YES_FLAG = {
|
|
|
1932
2377
|
description: "Make the change without asking",
|
|
1933
2378
|
schema: z.boolean()
|
|
1934
2379
|
};
|
|
1935
|
-
const JSON_FLAG
|
|
2380
|
+
const JSON_FLAG = {
|
|
1936
2381
|
name: "json",
|
|
1937
2382
|
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1938
2383
|
schema: z.boolean()
|
|
@@ -1942,7 +2387,7 @@ const installCommand = defineCommand({
|
|
|
1942
2387
|
summary: "Set up completion in your shell",
|
|
1943
2388
|
description: "Writes the completion script under your data directory and loads it from your shell's startup file, between two marked lines. Fish loads it from its completions directory, so no startup file changes. Run it again after upgrading, so the script matches the CLI.",
|
|
1944
2389
|
arguments: [SHELL_ARGUMENT],
|
|
1945
|
-
flags: [YES_FLAG, JSON_FLAG
|
|
2390
|
+
flags: [YES_FLAG, JSON_FLAG],
|
|
1946
2391
|
examples: [{
|
|
1947
2392
|
description: "Install for the shell you are using",
|
|
1948
2393
|
command: "hardfin completion install"
|
|
@@ -1957,7 +2402,7 @@ const uninstallCommand = defineCommand({
|
|
|
1957
2402
|
summary: "Remove completion from your shell",
|
|
1958
2403
|
description: "Removes the script install wrote, and the marked lines it added to your shell's startup file.",
|
|
1959
2404
|
arguments: [SHELL_ARGUMENT],
|
|
1960
|
-
flags: [YES_FLAG, JSON_FLAG
|
|
2405
|
+
flags: [YES_FLAG, JSON_FLAG],
|
|
1961
2406
|
examples: [{
|
|
1962
2407
|
description: "Remove completion from zsh",
|
|
1963
2408
|
command: "hardfin completion uninstall zsh"
|
|
@@ -2962,419 +3407,132 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
2962
3407
|
if (input.isJSON) writeEvent({
|
|
2963
3408
|
event: "device_code",
|
|
2964
3409
|
userCode: device.userCode,
|
|
2965
|
-
verificationUri: device.verificationUri,
|
|
2966
|
-
url,
|
|
2967
|
-
browserOpened: opened,
|
|
2968
|
-
waitSeconds: Math.max(0, Math.round((device.expiresAt - Date.now()) / 1e3))
|
|
2969
|
-
});
|
|
2970
|
-
else process.stderr.write([
|
|
2971
|
-
"",
|
|
2972
|
-
` Code ${toPrintable(device.userCode)}`,
|
|
2973
|
-
` At ${toPrintable(device.verificationUri)}`,
|
|
2974
|
-
"",
|
|
2975
|
-
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
2976
|
-
].join("\n"));
|
|
2977
|
-
const prompt = toPrompt(url);
|
|
2978
|
-
try {
|
|
2979
|
-
return await toSignedIn(input, metadata, await toDeviceTokens(toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted));
|
|
2980
|
-
} finally {
|
|
2981
|
-
prompt.close();
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
/**
|
|
2985
|
-
* toDeviceTokens waits for the approval, or for the keyboard. Ctrl-C arrives as an Interrupted
|
|
2986
|
-
* and stops quietly. Text followed by Enter means the person typed the code here, where it
|
|
2987
|
-
* does nothing, so they are told where it goes instead.
|
|
2988
|
-
*/
|
|
2989
|
-
async function toDeviceTokens(approved, typed) {
|
|
2990
|
-
return await Promise.race([approved, typed.then(() => {
|
|
2991
|
-
throw new SignInStopped("Sign in stopped. The code goes on the page in your browser, not here. Run hardfin login to try again.");
|
|
2992
|
-
})]);
|
|
2993
|
-
}
|
|
2994
|
-
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
2995
|
-
async function toSignedIn(input, metadata, tokens) {
|
|
2996
|
-
const refreshToken = tokens.refreshToken;
|
|
2997
|
-
if (!refreshToken) {
|
|
2998
|
-
writeFailure("sign_in_failed", "the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
2999
|
-
return ExitCode.ERROR;
|
|
3000
|
-
}
|
|
3001
|
-
const scopes = toGrantedScopes(tokens);
|
|
3002
|
-
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt, scopes));
|
|
3003
|
-
if (input.isJSON) {
|
|
3004
|
-
writeData({
|
|
3005
|
-
signedIn: true,
|
|
3006
|
-
issuer: metadata.issuer,
|
|
3007
|
-
scope: tokens.scope ?? null,
|
|
3008
|
-
scopes: scopes ?? null,
|
|
3009
|
-
canWrite: toCanWrite(scopes),
|
|
3010
|
-
storedIn: backend
|
|
3011
|
-
});
|
|
3012
|
-
return ExitCode.OK;
|
|
3013
|
-
}
|
|
3014
|
-
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
3015
|
-
writeData([
|
|
3016
|
-
`Signed in to ${metadata.issuer}`,
|
|
3017
|
-
`Scope ${scopes?.join(" ") ?? tokens.scope ?? "as granted"}`,
|
|
3018
|
-
...toCanWrite(scopes) === false ? ["Access read-only, so a command that changes a record will be refused"] : [],
|
|
3019
|
-
`Stored in ${stored}`,
|
|
3020
|
-
"",
|
|
3021
|
-
"Run hardfin status to see what this CLI is using",
|
|
3022
|
-
...isInstalled() ? [] : ["Run hardfin completion install to complete commands with Tab"]
|
|
3023
|
-
].join("\n"));
|
|
3024
|
-
return ExitCode.OK;
|
|
3025
|
-
}
|
|
3026
|
-
/** writeEvent tells a program what a sign in is waiting on, as one line of JSON on stderr. */
|
|
3027
|
-
function writeEvent(event) {
|
|
3028
|
-
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
3029
|
-
}
|
|
3030
|
-
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
3031
|
-
function toAuthorizationUrl(endpoint, request) {
|
|
3032
|
-
const url = new URL(endpoint);
|
|
3033
|
-
url.searchParams.set("response_type", "code");
|
|
3034
|
-
url.searchParams.set("client_id", request.clientId);
|
|
3035
|
-
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
3036
|
-
url.searchParams.set("scope", request.scope);
|
|
3037
|
-
url.searchParams.set("state", request.state);
|
|
3038
|
-
url.searchParams.set("code_challenge", request.challenge);
|
|
3039
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
3040
|
-
return url.toString();
|
|
3041
|
-
}
|
|
3042
|
-
//#endregion
|
|
3043
|
-
//#region src/command/logout.ts
|
|
3044
|
-
const logoutCommand = defineCommand({
|
|
3045
|
-
name: "logout",
|
|
3046
|
-
summary: "Sign out, and tell Hardfin to forget this machine",
|
|
3047
|
-
description: "Removes the stored refresh token and asks the authorization server to revoke it, so a copy taken from this machine stops working.",
|
|
3048
|
-
arguments: [],
|
|
3049
|
-
flags: [{
|
|
3050
|
-
name: "json",
|
|
3051
|
-
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
3052
|
-
schema: z.boolean()
|
|
3053
|
-
}],
|
|
3054
|
-
examples: [{
|
|
3055
|
-
description: "Sign out",
|
|
3056
|
-
command: "hardfin logout"
|
|
3057
|
-
}],
|
|
3058
|
-
run: runLogout
|
|
3059
|
-
});
|
|
3060
|
-
async function runLogout(input) {
|
|
3061
|
-
const settings = input.resolved.settings;
|
|
3062
|
-
const stored = toCredential(settings.issuerUrl);
|
|
3063
|
-
if (!stored) {
|
|
3064
|
-
writeData({
|
|
3065
|
-
signedOut: false,
|
|
3066
|
-
issuer: settings.issuerUrl,
|
|
3067
|
-
reason: "no sign in was stored"
|
|
3068
|
-
});
|
|
3069
|
-
return ExitCode.OK;
|
|
3070
|
-
}
|
|
3071
|
-
let revoked = false;
|
|
3072
|
-
try {
|
|
3073
|
-
const metadata = await toMetadata(settings.issuerUrl);
|
|
3074
|
-
if (metadata.revocation_endpoint) {
|
|
3075
|
-
await revoke(metadata.revocation_endpoint, settings.clientId, stored.refreshToken);
|
|
3076
|
-
revoked = true;
|
|
3077
|
-
}
|
|
3078
|
-
} catch {
|
|
3079
|
-
revoked = false;
|
|
3080
|
-
}
|
|
3081
|
-
await withLock(() => {
|
|
3082
|
-
forget(settings.issuerUrl);
|
|
3083
|
-
});
|
|
3084
|
-
forgetHeldTokens();
|
|
3085
|
-
writeData({
|
|
3086
|
-
signedOut: true,
|
|
3087
|
-
issuer: settings.issuerUrl,
|
|
3088
|
-
revoked
|
|
3089
|
-
});
|
|
3090
|
-
return ExitCode.OK;
|
|
3091
|
-
}
|
|
3092
|
-
//#endregion
|
|
3093
|
-
//#region src/command/operation.ts
|
|
3094
|
-
/**
|
|
3095
|
-
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
3096
|
-
* lists, which is what a response always uses.
|
|
3097
|
-
*/
|
|
3098
|
-
function toEnum(values) {
|
|
3099
|
-
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
3100
|
-
}
|
|
3101
|
-
const UNSET_FLAG = {
|
|
3102
|
-
name: "unset",
|
|
3103
|
-
description: "A field to clear, named as its flag is, repeatable",
|
|
3104
|
-
valueName: "field",
|
|
3105
|
-
repeatable: true,
|
|
3106
|
-
schema: z.array(z.string())
|
|
3107
|
-
};
|
|
3108
|
-
const INPUT_FLAG = {
|
|
3109
|
-
name: "input",
|
|
3110
|
-
description: "A file holding the JSON request body, or - for standard input. A flag given as well replaces its field",
|
|
3111
|
-
valueName: "path",
|
|
3112
|
-
schema: z.string()
|
|
3113
|
-
};
|
|
3114
|
-
const FILE_FLAG = {
|
|
3115
|
-
name: "file",
|
|
3116
|
-
description: "The file to upload",
|
|
3117
|
-
valueName: "path",
|
|
3118
|
-
schema: z.string()
|
|
3119
|
-
};
|
|
3120
|
-
const OUTPUT_FLAG = {
|
|
3121
|
-
name: "output",
|
|
3122
|
-
description: "Where to write the file, or - for standard output",
|
|
3123
|
-
valueName: "path",
|
|
3124
|
-
schema: z.string()
|
|
3125
|
-
};
|
|
3126
|
-
const JSON_FLAG = {
|
|
3127
|
-
name: "json",
|
|
3128
|
-
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
3129
|
-
schema: z.boolean()
|
|
3130
|
-
};
|
|
3131
|
-
/** defineOperation turns one endpoint into the command that calls it. */
|
|
3132
|
-
function defineOperation(operation) {
|
|
3133
|
-
const flags = [
|
|
3134
|
-
...operation.queryFlags,
|
|
3135
|
-
...operation.bodyFlags,
|
|
3136
|
-
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
3137
|
-
...operation.bodyFlags.length > 0 ? [INPUT_FLAG] : [],
|
|
3138
|
-
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
3139
|
-
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
3140
|
-
JSON_FLAG
|
|
3141
|
-
];
|
|
3142
|
-
return {
|
|
3143
|
-
name: operation.name,
|
|
3144
|
-
summary: operation.summary,
|
|
3145
|
-
description: operation.description ?? operation.summary,
|
|
3146
|
-
arguments: operation.pathParameters,
|
|
3147
|
-
flags,
|
|
3148
|
-
examples: operation.example ? [{
|
|
3149
|
-
description: operation.summary,
|
|
3150
|
-
command: operation.example
|
|
3151
|
-
}] : [],
|
|
3152
|
-
endpoint: {
|
|
3153
|
-
method: operation.method,
|
|
3154
|
-
path: operation.path
|
|
3155
|
-
},
|
|
3156
|
-
run: (input) => runOperation(operation, input)
|
|
3157
|
-
};
|
|
3158
|
-
}
|
|
3159
|
-
async function runOperation(operation, input) {
|
|
3160
|
-
const path = toPath(operation, input.args);
|
|
3161
|
-
if (path === void 0) {
|
|
3162
|
-
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s), each the key of a record`, input.isJSON);
|
|
3163
|
-
return ExitCode.USAGE;
|
|
3164
|
-
}
|
|
3165
|
-
const given = toGivenBody$1(input.flags["input"]);
|
|
3166
|
-
if (given instanceof InputFailure) {
|
|
3167
|
-
writeFailure("invalid_input", given.message, input.isJSON);
|
|
3168
|
-
return ExitCode.USAGE;
|
|
3169
|
-
}
|
|
3170
|
-
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0 && !hasPath(given, flag.jsonPath));
|
|
3171
|
-
if (missing.length > 0) {
|
|
3172
|
-
writeFailure("missing_flag", `this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
3173
|
-
return ExitCode.USAGE;
|
|
3174
|
-
}
|
|
3175
|
-
const body = toBody(operation, input.flags, given);
|
|
3176
|
-
if (body instanceof Error) {
|
|
3177
|
-
writeFailure("invalid_flag", body.message, input.isJSON);
|
|
3178
|
-
return ExitCode.USAGE;
|
|
3179
|
-
}
|
|
3180
|
-
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
3181
|
-
writeFailure("empty_request", `this command changes nothing unless a flag is given, such as ${operation.bodyFlags.slice(0, 3).map((flag) => `--${flag.name}`).join(", ")}. Run it with --help for the rest`, input.isJSON);
|
|
3182
|
-
return ExitCode.USAGE;
|
|
3183
|
-
}
|
|
3184
|
-
let form;
|
|
3185
|
-
if (operation.upload) {
|
|
3186
|
-
const built = await toForm(operation.upload, input.flags);
|
|
3187
|
-
if (built instanceof Error) {
|
|
3188
|
-
writeFailure("invalid_input", built.message, input.isJSON);
|
|
3189
|
-
return ExitCode.USAGE;
|
|
3190
|
-
}
|
|
3191
|
-
form = built;
|
|
3192
|
-
}
|
|
3193
|
-
try {
|
|
3194
|
-
const envelope = await request$1({
|
|
3195
|
-
apiUrl: input.resolved.settings.apiUrl,
|
|
3196
|
-
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
3197
|
-
method: operation.method,
|
|
3198
|
-
path,
|
|
3199
|
-
query: toQuery(operation, input.flags),
|
|
3200
|
-
body,
|
|
3201
|
-
form,
|
|
3202
|
-
downloads: operation.downloads
|
|
3203
|
-
});
|
|
3204
|
-
if (operation.downloads) return toWritten(envelope.data, input);
|
|
3205
|
-
writeData(envelope.data);
|
|
3206
|
-
return ExitCode.OK;
|
|
3207
|
-
} catch (error) {
|
|
3208
|
-
if (error instanceof NoCredential) {
|
|
3209
|
-
writeFailure("not_signed_in", error.message, input.isJSON);
|
|
3210
|
-
return ExitCode.NOT_AUTHENTICATED;
|
|
3211
|
-
}
|
|
3212
|
-
if (error instanceof RequestFailure) {
|
|
3213
|
-
writeFailure(toRequestCode(error), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
3214
|
-
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
3215
|
-
}
|
|
3216
|
-
throw error;
|
|
3410
|
+
verificationUri: device.verificationUri,
|
|
3411
|
+
url,
|
|
3412
|
+
browserOpened: opened,
|
|
3413
|
+
waitSeconds: Math.max(0, Math.round((device.expiresAt - Date.now()) / 1e3))
|
|
3414
|
+
});
|
|
3415
|
+
else process.stderr.write([
|
|
3416
|
+
"",
|
|
3417
|
+
` Code ${toPrintable(device.userCode)}`,
|
|
3418
|
+
` At ${toPrintable(device.verificationUri)}`,
|
|
3419
|
+
"",
|
|
3420
|
+
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
3421
|
+
].join("\n"));
|
|
3422
|
+
const prompt = toPrompt(url);
|
|
3423
|
+
try {
|
|
3424
|
+
return await toSignedIn(input, metadata, await toDeviceTokens(toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted));
|
|
3425
|
+
} finally {
|
|
3426
|
+
prompt.close();
|
|
3217
3427
|
}
|
|
3218
3428
|
}
|
|
3219
3429
|
/**
|
|
3220
|
-
*
|
|
3221
|
-
*
|
|
3430
|
+
* toDeviceTokens waits for the approval, or for the keyboard. Ctrl-C arrives as an Interrupted
|
|
3431
|
+
* and stops quietly. Text followed by Enter means the person typed the code here, where it
|
|
3432
|
+
* does nothing, so they are told where it goes instead.
|
|
3222
3433
|
*/
|
|
3223
|
-
function
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3434
|
+
async function toDeviceTokens(approved, typed) {
|
|
3435
|
+
return await Promise.race([approved, typed.then(() => {
|
|
3436
|
+
throw new SignInStopped("Sign in stopped. The code goes on the page in your browser, not here. Run hardfin login to try again.");
|
|
3437
|
+
})]);
|
|
3438
|
+
}
|
|
3439
|
+
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
3440
|
+
async function toSignedIn(input, metadata, tokens) {
|
|
3441
|
+
const refreshToken = tokens.refreshToken;
|
|
3442
|
+
if (!refreshToken) {
|
|
3443
|
+
writeFailure("sign_in_failed", "the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
3444
|
+
return ExitCode.ERROR;
|
|
3230
3445
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3446
|
+
const scopes = toGrantedScopes(tokens);
|
|
3447
|
+
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt, scopes));
|
|
3448
|
+
if (input.isJSON) {
|
|
3449
|
+
writeData({
|
|
3450
|
+
signedIn: true,
|
|
3451
|
+
issuer: metadata.issuer,
|
|
3452
|
+
scope: tokens.scope ?? null,
|
|
3453
|
+
scopes: scopes ?? null,
|
|
3454
|
+
canWrite: toCanWrite(scopes),
|
|
3455
|
+
storedIn: backend
|
|
3456
|
+
});
|
|
3237
3457
|
return ExitCode.OK;
|
|
3238
3458
|
}
|
|
3239
|
-
|
|
3240
|
-
writeData(
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3459
|
+
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
3460
|
+
writeData([
|
|
3461
|
+
`Signed in to ${metadata.issuer}`,
|
|
3462
|
+
`Scope ${scopes?.join(" ") ?? tokens.scope ?? "as granted"}`,
|
|
3463
|
+
...toCanWrite(scopes) === false ? ["Access read-only, so a command that changes a record will be refused"] : [],
|
|
3464
|
+
`Stored in ${stored}`,
|
|
3465
|
+
"",
|
|
3466
|
+
"Run hardfin status to see what this CLI is using",
|
|
3467
|
+
...isInstalled() ? [] : ["Run hardfin completion install to complete commands with Tab"]
|
|
3468
|
+
].join("\n"));
|
|
3245
3469
|
return ExitCode.OK;
|
|
3246
3470
|
}
|
|
3247
|
-
/**
|
|
3248
|
-
function
|
|
3249
|
-
|
|
3250
|
-
return name === "" || name.startsWith(".") ? void 0 : name;
|
|
3471
|
+
/** writeEvent tells a program what a sign in is waiting on, as one line of JSON on stderr. */
|
|
3472
|
+
function writeEvent(event) {
|
|
3473
|
+
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
3251
3474
|
}
|
|
3252
|
-
/**
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
"
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
return path;
|
|
3475
|
+
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
3476
|
+
function toAuthorizationUrl(endpoint, request) {
|
|
3477
|
+
const url = new URL(endpoint);
|
|
3478
|
+
url.searchParams.set("response_type", "code");
|
|
3479
|
+
url.searchParams.set("client_id", request.clientId);
|
|
3480
|
+
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
3481
|
+
url.searchParams.set("scope", request.scope);
|
|
3482
|
+
url.searchParams.set("state", request.state);
|
|
3483
|
+
url.searchParams.set("code_challenge", request.challenge);
|
|
3484
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
3485
|
+
return url.toString();
|
|
3264
3486
|
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3487
|
+
//#endregion
|
|
3488
|
+
//#region src/command/logout.ts
|
|
3489
|
+
const logoutCommand = defineCommand({
|
|
3490
|
+
name: "logout",
|
|
3491
|
+
summary: "Sign out, and tell Hardfin to forget this machine",
|
|
3492
|
+
description: "Removes the stored refresh token and asks the authorization server to revoke it, so a copy taken from this machine stops working.",
|
|
3493
|
+
arguments: [],
|
|
3494
|
+
flags: [{
|
|
3495
|
+
name: "json",
|
|
3496
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
3497
|
+
schema: z.boolean()
|
|
3498
|
+
}],
|
|
3499
|
+
examples: [{
|
|
3500
|
+
description: "Sign out",
|
|
3501
|
+
command: "hardfin logout"
|
|
3502
|
+
}],
|
|
3503
|
+
run: runLogout
|
|
3504
|
+
});
|
|
3505
|
+
async function runLogout(input) {
|
|
3506
|
+
const settings = input.resolved.settings;
|
|
3507
|
+
const stored = toCredential(settings.issuerUrl);
|
|
3508
|
+
if (!stored) {
|
|
3509
|
+
writeData({
|
|
3510
|
+
signedOut: false,
|
|
3511
|
+
issuer: settings.issuerUrl,
|
|
3512
|
+
reason: "no sign in was stored"
|
|
3513
|
+
});
|
|
3514
|
+
return ExitCode.OK;
|
|
3274
3515
|
}
|
|
3275
|
-
|
|
3276
|
-
}
|
|
3277
|
-
/** toOptionKey names the parsed flag, which the parser reports in camel case. */
|
|
3278
|
-
function toOptionKey(name) {
|
|
3279
|
-
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
3280
|
-
}
|
|
3281
|
-
/** toForm builds the file part and its fields, which an upload endpoint takes. */
|
|
3282
|
-
async function toForm(upload, flags) {
|
|
3283
|
-
const path = flags["file"];
|
|
3284
|
-
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
3285
|
-
const form = new FormData();
|
|
3516
|
+
let revoked = false;
|
|
3286
3517
|
try {
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
for (const field of upload.fields) {
|
|
3292
|
-
const value = flags[toOptionKey(field.name)];
|
|
3293
|
-
if (value !== void 0) {
|
|
3294
|
-
const parsed = field.schema.safeParse(value);
|
|
3295
|
-
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
return form;
|
|
3299
|
-
}
|
|
3300
|
-
/** toGivenBody reads the --input body, which a generated command takes only as a JSON object. */
|
|
3301
|
-
function toGivenBody$1(source) {
|
|
3302
|
-
if (typeof source !== "string") return;
|
|
3303
|
-
const read = toJSONInput(source);
|
|
3304
|
-
if (read instanceof InputFailure) return read;
|
|
3305
|
-
if (typeof read !== "object" || read === null || Array.isArray(read)) return new InputFailure(`the body in ${source === "-" ? "standard input" : source} must be a JSON object, such as {"name": "Acme"}`);
|
|
3306
|
-
return read;
|
|
3307
|
-
}
|
|
3308
|
-
/** hasPath reports whether a body already holds a value at a field's path. */
|
|
3309
|
-
function hasPath(body, path) {
|
|
3310
|
-
let holder = body;
|
|
3311
|
-
for (const name of path) {
|
|
3312
|
-
if (typeof holder !== "object" || holder === null || !(name in holder)) return false;
|
|
3313
|
-
holder = holder[name];
|
|
3314
|
-
}
|
|
3315
|
-
return true;
|
|
3316
|
-
}
|
|
3317
|
-
/** toBody builds the request body from the --input body and the flags, a flag winning over its field. */
|
|
3318
|
-
function toBody(operation, flags, given) {
|
|
3319
|
-
const body = given ? structuredClone(given) : {};
|
|
3320
|
-
let hasField = given !== void 0;
|
|
3321
|
-
for (const flag of operation.bodyFlags) {
|
|
3322
|
-
const value = flags[toOptionKey(flag.name)];
|
|
3323
|
-
if (value === void 0) continue;
|
|
3324
|
-
if (flag.element) {
|
|
3325
|
-
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
3326
|
-
if (elements instanceof Error) return elements;
|
|
3327
|
-
set(body, flag.jsonPath, elements);
|
|
3328
|
-
hasField = true;
|
|
3329
|
-
continue;
|
|
3330
|
-
}
|
|
3331
|
-
const parsed = flag.schema.safeParse(value);
|
|
3332
|
-
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
3333
|
-
hasField = true;
|
|
3334
|
-
}
|
|
3335
|
-
const cleared = toCleared(operation, flags, body);
|
|
3336
|
-
if (cleared instanceof Error) return cleared;
|
|
3337
|
-
return hasField || cleared ? body : void 0;
|
|
3338
|
-
}
|
|
3339
|
-
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
3340
|
-
function toCleared(operation, flags, body) {
|
|
3341
|
-
const named = flags["unset"];
|
|
3342
|
-
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
3343
|
-
for (const name of names) {
|
|
3344
|
-
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
3345
|
-
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
3346
|
-
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
3347
|
-
set(body, flag.jsonPath, null);
|
|
3348
|
-
}
|
|
3349
|
-
return names.length > 0;
|
|
3350
|
-
}
|
|
3351
|
-
/** toElements reads the key=value pairs a repeated flag carries for one array element. */
|
|
3352
|
-
function toElements(flag, values) {
|
|
3353
|
-
const elements = [];
|
|
3354
|
-
for (const value of values) {
|
|
3355
|
-
const element = {};
|
|
3356
|
-
for (const pair of value.split(",")) {
|
|
3357
|
-
const split = pair.indexOf("=");
|
|
3358
|
-
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ") ?? ""}=, such as --${flag.name} ${flag.element?.[0] ?? "field"}=value. A value cannot contain a comma`);
|
|
3359
|
-
const key = pair.slice(0, split).trim();
|
|
3360
|
-
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
3361
|
-
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
3362
|
-
const value = pair.slice(split + 1).trim();
|
|
3363
|
-
if (flag.elementUuids?.includes(key) && !z.uuid().safeParse(value).success) return /* @__PURE__ */ new Error(`--${flag.name} needs a UUID for ${key}, such as 3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90, and ${value || "nothing"} is not one`);
|
|
3364
|
-
element[key] = value;
|
|
3518
|
+
const metadata = await toMetadata(settings.issuerUrl);
|
|
3519
|
+
if (metadata.revocation_endpoint) {
|
|
3520
|
+
await revoke(metadata.revocation_endpoint, settings.clientId, stored.refreshToken);
|
|
3521
|
+
revoked = true;
|
|
3365
3522
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
return elements;
|
|
3369
|
-
}
|
|
3370
|
-
/** set writes a value at its path, building the objects a nested field needs. */
|
|
3371
|
-
function set(body, path, value) {
|
|
3372
|
-
let holder = body;
|
|
3373
|
-
for (const name of path.slice(0, -1)) {
|
|
3374
|
-
holder[name] = holder[name] ?? {};
|
|
3375
|
-
holder = holder[name];
|
|
3523
|
+
} catch {
|
|
3524
|
+
revoked = false;
|
|
3376
3525
|
}
|
|
3377
|
-
|
|
3526
|
+
await withLock(() => {
|
|
3527
|
+
forget(settings.issuerUrl);
|
|
3528
|
+
});
|
|
3529
|
+
forgetHeldTokens();
|
|
3530
|
+
writeData({
|
|
3531
|
+
signedOut: true,
|
|
3532
|
+
issuer: settings.issuerUrl,
|
|
3533
|
+
revoked
|
|
3534
|
+
});
|
|
3535
|
+
return ExitCode.OK;
|
|
3378
3536
|
}
|
|
3379
3537
|
//#endregion
|
|
3380
3538
|
//#region src/mcp/confine.ts
|
|
@@ -4826,6 +4984,12 @@ const commands = [
|
|
|
4826
4984
|
"destinationId",
|
|
4827
4985
|
"originId"
|
|
4828
4986
|
],
|
|
4987
|
+
elementTypes: {
|
|
4988
|
+
"deliverAt": ["string", "null"],
|
|
4989
|
+
"deliverAtTimezone": ["string", "null"],
|
|
4990
|
+
"shipAt": ["string", "null"],
|
|
4991
|
+
"shipAtTimezone": ["string", "null"]
|
|
4992
|
+
},
|
|
4829
4993
|
schema: z.array(z.string())
|
|
4830
4994
|
}]
|
|
4831
4995
|
})]
|
|
@@ -4856,6 +5020,10 @@ const commands = [
|
|
|
4856
5020
|
"shipAt"
|
|
4857
5021
|
],
|
|
4858
5022
|
elementUuids: ["assetId"],
|
|
5023
|
+
elementTypes: {
|
|
5024
|
+
"deliverAt": ["string", "null"],
|
|
5025
|
+
"shipAt": ["string", "null"]
|
|
5026
|
+
},
|
|
4859
5027
|
schema: z.array(z.string())
|
|
4860
5028
|
}]
|
|
4861
5029
|
})]
|
|
@@ -4933,6 +5101,14 @@ const commands = [
|
|
|
4933
5101
|
repeatable: true,
|
|
4934
5102
|
element: ["fieldId", "value"],
|
|
4935
5103
|
elementUuids: ["fieldId"],
|
|
5104
|
+
elementTypes: { "value": [
|
|
5105
|
+
"array",
|
|
5106
|
+
"boolean",
|
|
5107
|
+
"number",
|
|
5108
|
+
"object",
|
|
5109
|
+
"string",
|
|
5110
|
+
"null"
|
|
5111
|
+
] },
|
|
4936
5112
|
schema: z.array(z.string())
|
|
4937
5113
|
},
|
|
4938
5114
|
{
|
|
@@ -6277,6 +6453,10 @@ const commands = [
|
|
|
6277
6453
|
"section"
|
|
6278
6454
|
],
|
|
6279
6455
|
elementUuids: ["fieldId"],
|
|
6456
|
+
elementTypes: {
|
|
6457
|
+
"order": ["integer"],
|
|
6458
|
+
"section": ["integer"]
|
|
6459
|
+
},
|
|
6280
6460
|
schema: z.array(z.string())
|
|
6281
6461
|
},
|
|
6282
6462
|
{
|
|
@@ -7177,5 +7357,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
7177
7357
|
//#endregion
|
|
7178
7358
|
export { toCli, toParseExitCode, toReported, wasReportAttempted, writeCrash };
|
|
7179
7359
|
|
|
7180
|
-
//# sourceMappingURL=program-
|
|
7181
|
-
//# debugId=
|
|
7360
|
+
//# sourceMappingURL=program-PaNgBWFL.js.map
|
|
7361
|
+
//# debugId=566d9b65-d8fb-507d-8b0e-a21deb646246
|