@hardfin/cli 0.1.0-dev.29 → 0.1.0-dev.31
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 +30 -3
- package/dist/cli.js +11 -5
- package/dist/completion-tree.json +5 -0
- package/dist/{program-CnXYN9xS.js → program-B0vISolG.js} +315 -197
- package/dist/{scrub-VtQmxap1.js → scrub-BN3gyBpR.js} +23 -14
- package/dist/{sentry-D5lKx0nm.js → sentry-CLaeAWNa.js} +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,23 @@ Three things fail locally with exit code 2, before any request is made: a value
|
|
|
87
87
|
enum, a required field that is missing, and `--unset` on a field the API will not take null
|
|
88
88
|
for.
|
|
89
89
|
|
|
90
|
+
### When a command fails
|
|
91
|
+
|
|
92
|
+
A person at a terminal reads one plain line on stderr. Anything else reads JSON, also on
|
|
93
|
+
stderr, so a pipe still carries only the result.
|
|
94
|
+
|
|
95
|
+
| Field | Holds |
|
|
96
|
+
| ----------- | ------------------------------------------------------------------------ |
|
|
97
|
+
| `code` | A fixed name for the failure, such as `not_found` or `missing_flag` |
|
|
98
|
+
| `message` | The same plain sentence a person reads |
|
|
99
|
+
| `errors` | Every error the API's envelope named, with its own numeric `statusCode` |
|
|
100
|
+
| `requestId` | The API's request ID, or null when the request never reached it |
|
|
101
|
+
| `status` | The HTTP status, or null for a failure caught before any request is made |
|
|
102
|
+
|
|
103
|
+
An agent branches on `code`. A code is never renamed and never changes meaning, while the
|
|
104
|
+
message may be reworded at any time. `hardfin agent-guide` lists every code with what it
|
|
105
|
+
means, and `src/output/failure.ts` defines them.
|
|
106
|
+
|
|
90
107
|
### Overrides
|
|
91
108
|
|
|
92
109
|
Two endpoints sometimes want one name. `DELETE /asset/{assetKey}/ownership` and
|
|
@@ -142,8 +159,18 @@ hardfin status # what the CLI is configured with and signed in as
|
|
|
142
159
|
hardfin logout # forget this machine, and ask Hardfin to revoke it
|
|
143
160
|
```
|
|
144
161
|
|
|
145
|
-
An API key is still the way to run unattended. `HARDFIN_API_KEY
|
|
146
|
-
so CI and an agent sandbox need no browser.
|
|
162
|
+
An API key is still the way to run unattended. `--api-key`, or `HARDFIN_API_KEY`, wins over
|
|
163
|
+
a stored sign in, so CI and an agent sandbox need no browser. The flag beats the variable.
|
|
164
|
+
|
|
165
|
+
An API key is held to two rules before it is sent:
|
|
166
|
+
|
|
167
|
+
| Rule | Why |
|
|
168
|
+
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
169
|
+
| It goes only over https, or to this machine | Anything between here and the host could read it |
|
|
170
|
+
| A key you set, by flag or exported variable, never goes to an API URL that `config.local.json` or `.env` in the working directory chose | A repository you cloned could otherwise send your key to its own server |
|
|
171
|
+
|
|
172
|
+
A key and an API URL that come from the same folder's files still work together, which is
|
|
173
|
+
how a local build is set up. Pass `--api-url` to send a key you set to that folder's address.
|
|
147
174
|
|
|
148
175
|
### What login does
|
|
149
176
|
|
|
@@ -356,7 +383,7 @@ settings, and the one nearest the top wins.
|
|
|
356
383
|
|
|
357
384
|
| Layer | Where | Beats |
|
|
358
385
|
| ----------- | --------------------------------------------------------------------- | ---------------- |
|
|
359
|
-
| Flag | `--api-url`
|
|
386
|
+
| Flag | `--api-url`, `--api-key`, `--issuer-url` | everything below |
|
|
360
387
|
| Environment | an exported `HARDFIN_*` variable | the files below |
|
|
361
388
|
| Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
|
|
362
389
|
| Config file | `config.local.json` in the working directory | the defaults |
|
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]="bb3026cb-ab06-5af0-9743-290b61ec91a3")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,8 +14,14 @@ 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, wasReportAttempted } = await import("./program-
|
|
18
|
-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-B0vISolG.js");
|
|
18
|
+
try {
|
|
19
|
+
await toCli().parseAsync(process.argv);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const code = toParseExitCode(error, process.argv.slice(2));
|
|
22
|
+
if (code === void 0) throw error;
|
|
23
|
+
process.exitCode = code;
|
|
24
|
+
}
|
|
19
25
|
if (wasReportAttempted()) process.exit();
|
|
20
26
|
} else process.stdout.write(`${answer}\n`);
|
|
21
27
|
} catch (error) {
|
|
@@ -27,7 +33,7 @@ async function toCrash(error) {
|
|
|
27
33
|
isCrashing = true;
|
|
28
34
|
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
29
35
|
try {
|
|
30
|
-
const { toReported } = await import("./program-
|
|
36
|
+
const { toReported } = await import("./program-B0vISolG.js");
|
|
31
37
|
await toReported(error, false, void 0, "fatal");
|
|
32
38
|
} finally {
|
|
33
39
|
process.exit(1);
|
|
@@ -45,4 +51,4 @@ async function toQuickAnswer(args) {
|
|
|
45
51
|
export {};
|
|
46
52
|
|
|
47
53
|
//# sourceMappingURL=cli.js.map
|
|
48
|
-
//# debugId=
|
|
54
|
+
//# debugId=bb3026cb-ab06-5af0-9743-290b61ec91a3
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"description": "The API to call, whose host also holds the authorization server",
|
|
6
6
|
"valueName": "url"
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"name": "api-key",
|
|
10
|
+
"description": "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
|
|
11
|
+
"valueName": "key"
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
"name": "issuer-url",
|
|
10
15
|
"description": "The authorization server, when it does not sit at the API's host",
|
|
@@ -1,10 +1,10 @@
|
|
|
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]="
|
|
4
|
-
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-
|
|
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]="ab584cf3-443f-569e-8167-fa0cbab9ab68")}catch(e){}}();
|
|
4
|
+
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { n as toCompletion, t as toAnswer$1 } from "./completion-tree-rR_Rmeij.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { Command, Option } from "commander";
|
|
7
|
+
import { Command, CommanderError, Option } from "commander";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
10
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
@@ -144,152 +144,6 @@ function toOrigin(apiUrl) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
//#endregion
|
|
147
|
-
//#region src/output/writer.ts
|
|
148
|
-
/**
|
|
149
|
-
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
150
|
-
* otherwise reach a request as the text "[object Object]".
|
|
151
|
-
*/
|
|
152
|
-
function toText(value) {
|
|
153
|
-
if (typeof value === "string") return value;
|
|
154
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
155
|
-
if (value === void 0 || value === null) return "";
|
|
156
|
-
return JSON.stringify(value);
|
|
157
|
-
}
|
|
158
|
-
/** writeData prints a command's result on stdout. */
|
|
159
|
-
function writeData(value) {
|
|
160
|
-
if (typeof value === "string") {
|
|
161
|
-
process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
165
|
-
}
|
|
166
|
-
/** writeFailure prints why a command failed on stderr, as text or as JSON. */
|
|
167
|
-
function writeFailure(message, isJSON, errors, requestId, status) {
|
|
168
|
-
if (!isJSON) {
|
|
169
|
-
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
170
|
-
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
171
|
-
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const payload = {
|
|
175
|
-
errors: errors ?? [{
|
|
176
|
-
error: message,
|
|
177
|
-
statusCode: 0
|
|
178
|
-
}],
|
|
179
|
-
requestId: requestId ?? null,
|
|
180
|
-
status: status ?? null
|
|
181
|
-
};
|
|
182
|
-
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
183
|
-
}
|
|
184
|
-
/** isJSONOutput decides whether this invocation prints machine-readable output. */
|
|
185
|
-
function isJSONOutput(flags) {
|
|
186
|
-
if (flags["json"] === true) return true;
|
|
187
|
-
return !process.stdout.isTTY;
|
|
188
|
-
}
|
|
189
|
-
/** version is what this build reports, read from the published package manifest. */
|
|
190
|
-
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
191
|
-
//#endregion
|
|
192
|
-
//#region src/command/agent-guide.ts
|
|
193
|
-
const agentGuideCommand = defineCommand({
|
|
194
|
-
name: "agent-guide",
|
|
195
|
-
summary: "Print instructions an agent can follow to use this CLI",
|
|
196
|
-
description: "Writes Markdown describing every command, its flags, and its exit codes. Save it as a skill file, an AGENTS.md section, or paste it into a system prompt.",
|
|
197
|
-
arguments: [],
|
|
198
|
-
flags: [{
|
|
199
|
-
name: "json",
|
|
200
|
-
description: "Print the command registry as JSON instead of Markdown",
|
|
201
|
-
schema: z.boolean().optional()
|
|
202
|
-
}],
|
|
203
|
-
examples: [{
|
|
204
|
-
description: "Write a skill file",
|
|
205
|
-
command: "hardfin agent-guide > SKILL.md"
|
|
206
|
-
}, {
|
|
207
|
-
description: "Inspect the registry",
|
|
208
|
-
command: "hardfin agent-guide --json"
|
|
209
|
-
}],
|
|
210
|
-
run: runAgentGuide
|
|
211
|
-
});
|
|
212
|
-
/** toGuide renders the registry as the Markdown an agent reads. */
|
|
213
|
-
function toGuide(commands, version) {
|
|
214
|
-
const lines = [
|
|
215
|
-
"# Hardfin CLI",
|
|
216
|
-
"",
|
|
217
|
-
`The \`hardfin\` command calls the Hardfin API. This guide describes version ${version} of the CLI, which speaks API version ${API_VERSION}.`,
|
|
218
|
-
"",
|
|
219
|
-
"## Before calling",
|
|
220
|
-
"",
|
|
221
|
-
"- Authenticate by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
|
|
222
|
-
"- Every command prints JSON on stdout and diagnostics on stderr",
|
|
223
|
-
"- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
|
|
224
|
-
"",
|
|
225
|
-
"## Exit codes",
|
|
226
|
-
"",
|
|
227
|
-
"| Code | Means |",
|
|
228
|
-
"| --- | --- |",
|
|
229
|
-
"| 0 | The command succeeded |",
|
|
230
|
-
"| 1 | The command failed |",
|
|
231
|
-
"| 2 | The command was called wrongly |",
|
|
232
|
-
"| 4 | No usable credential |",
|
|
233
|
-
"| 130 | The person pressed Ctrl-C |",
|
|
234
|
-
"",
|
|
235
|
-
"## Commands",
|
|
236
|
-
""
|
|
237
|
-
];
|
|
238
|
-
for (const command of commands.filter((command) => !command.hidden)) lines.push(...toCommandLines(command, []));
|
|
239
|
-
return lines.join("\n");
|
|
240
|
-
}
|
|
241
|
-
/** toCommandLines describes one command and everything nested under it. */
|
|
242
|
-
function toCommandLines(command, parents) {
|
|
243
|
-
const path = [...parents, command.name];
|
|
244
|
-
const lines = [
|
|
245
|
-
`### \`hardfin ${path.join(" ")}\``,
|
|
246
|
-
"",
|
|
247
|
-
command.description ?? command.summary,
|
|
248
|
-
""
|
|
249
|
-
];
|
|
250
|
-
if (command.arguments.length > 0) {
|
|
251
|
-
lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
|
|
252
|
-
for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
|
|
253
|
-
lines.push("");
|
|
254
|
-
}
|
|
255
|
-
if (command.flags.length > 0) {
|
|
256
|
-
lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
|
|
257
|
-
for (const flag of command.flags) {
|
|
258
|
-
const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
|
|
259
|
-
lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
|
|
260
|
-
}
|
|
261
|
-
lines.push("");
|
|
262
|
-
}
|
|
263
|
-
for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
|
|
264
|
-
for (const subcommand of command.subcommands ?? []) lines.push(...toCommandLines(subcommand, path));
|
|
265
|
-
return lines;
|
|
266
|
-
}
|
|
267
|
-
async function runAgentGuide(input) {
|
|
268
|
-
if (input.flags["json"] === true) {
|
|
269
|
-
writeData(input.commands.map(toSummary));
|
|
270
|
-
return ExitCode.OK;
|
|
271
|
-
}
|
|
272
|
-
writeData(toGuide(input.commands, version$1));
|
|
273
|
-
return ExitCode.OK;
|
|
274
|
-
}
|
|
275
|
-
function toSummary(command) {
|
|
276
|
-
return {
|
|
277
|
-
name: command.name,
|
|
278
|
-
summary: command.summary,
|
|
279
|
-
description: command.description ?? command.summary,
|
|
280
|
-
arguments: command.arguments,
|
|
281
|
-
flags: command.flags.map((flag) => ({
|
|
282
|
-
name: flag.name,
|
|
283
|
-
short: flag.short ?? null,
|
|
284
|
-
description: flag.description,
|
|
285
|
-
valueName: flag.valueName ?? null,
|
|
286
|
-
repeatable: flag.repeatable ?? false
|
|
287
|
-
})),
|
|
288
|
-
examples: command.examples,
|
|
289
|
-
subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
//#endregion
|
|
293
147
|
//#region src/auth/jwt.ts
|
|
294
148
|
/**
|
|
295
149
|
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
@@ -620,6 +474,51 @@ function keep(issuer, document) {
|
|
|
620
474
|
} catch {}
|
|
621
475
|
}
|
|
622
476
|
//#endregion
|
|
477
|
+
//#region src/output/writer.ts
|
|
478
|
+
/**
|
|
479
|
+
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
480
|
+
* otherwise reach a request as the text "[object Object]".
|
|
481
|
+
*/
|
|
482
|
+
function toText(value) {
|
|
483
|
+
if (typeof value === "string") return value;
|
|
484
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
485
|
+
if (value === void 0 || value === null) return "";
|
|
486
|
+
return JSON.stringify(value);
|
|
487
|
+
}
|
|
488
|
+
/** writeData prints a command's result on stdout. */
|
|
489
|
+
function writeData(value) {
|
|
490
|
+
if (typeof value === "string") {
|
|
491
|
+
process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
495
|
+
}
|
|
496
|
+
/** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
|
|
497
|
+
function writeFailure(code, message, isJSON, errors, requestId, status) {
|
|
498
|
+
if (!isJSON) {
|
|
499
|
+
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
500
|
+
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
501
|
+
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const payload = {
|
|
505
|
+
code,
|
|
506
|
+
message,
|
|
507
|
+
errors: errors ?? [{
|
|
508
|
+
error: message,
|
|
509
|
+
statusCode: 0
|
|
510
|
+
}],
|
|
511
|
+
requestId: requestId ?? null,
|
|
512
|
+
status: status ?? null
|
|
513
|
+
};
|
|
514
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
515
|
+
}
|
|
516
|
+
/** isJSONOutput decides whether this invocation prints machine-readable output. */
|
|
517
|
+
function isJSONOutput(flags) {
|
|
518
|
+
if (flags["json"] === true) return true;
|
|
519
|
+
return !process.stdout.isTTY;
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
623
522
|
//#region src/auth/grant.ts
|
|
624
523
|
const TokenResponse = z.looseObject({
|
|
625
524
|
access_token: z.string(),
|
|
@@ -795,12 +694,15 @@ const held = /* @__PURE__ */ new Map();
|
|
|
795
694
|
* unattended caller sets it deliberately, and otherwise the stored refresh token buys an
|
|
796
695
|
* access token that lives only in memory.
|
|
797
696
|
*/
|
|
798
|
-
async function toRequestCredential(settings) {
|
|
799
|
-
if (settings.apiKey)
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
697
|
+
async function toRequestCredential(settings, sources) {
|
|
698
|
+
if (settings.apiKey) {
|
|
699
|
+
refuseForeignKey(settings, sources);
|
|
700
|
+
return {
|
|
701
|
+
header: "X-API-Key",
|
|
702
|
+
value: settings.apiKey,
|
|
703
|
+
kind: "api key"
|
|
704
|
+
};
|
|
705
|
+
}
|
|
804
706
|
refuseForeignHost(settings);
|
|
805
707
|
const stored = toCredential(settings.issuerUrl);
|
|
806
708
|
if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
|
|
@@ -876,6 +778,20 @@ function refuseForeignHost(settings) {
|
|
|
876
778
|
if (api.host !== issuer.host) throw new NoCredential(`this sign in belongs to ${issuer.host}, so it is not sent to ${api.host}. Set HARDFIN_API_KEY to call another host, or point --issuer-url at the server that signs you in`);
|
|
877
779
|
if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`${settings.apiUrl} is not https, so a sign in is not sent to it`);
|
|
878
780
|
}
|
|
781
|
+
/** The layers a file in the working directory sets, which a cloned repository can carry. */
|
|
782
|
+
const DIRECTORY_SOURCES = /* @__PURE__ */ new Set(["config file", "env file"]);
|
|
783
|
+
/** refuseForeignKey keeps an API key off plain connections and off hosts a stray file chose. */
|
|
784
|
+
function refuseForeignKey(settings, sources) {
|
|
785
|
+
const api = toHost$1(settings.apiUrl);
|
|
786
|
+
if (api === void 0) throw new NoCredential(`${settings.apiUrl} is not a URL this CLI can call`);
|
|
787
|
+
if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`Your API key was not sent, because ${api.host} does not use a secure connection.`);
|
|
788
|
+
const isKeyChosen = sources?.apiKey !== void 0 && !DIRECTORY_SOURCES.has(sources.apiKey);
|
|
789
|
+
const isUrlFromFolder = sources?.apiUrl !== void 0 && DIRECTORY_SOURCES.has(sources.apiUrl);
|
|
790
|
+
if (isKeyChosen && isUrlFromFolder) {
|
|
791
|
+
const file = sources.apiUrl === "config file" ? CONFIG_FILE : ".env";
|
|
792
|
+
throw new NoCredential(`Your API key was not sent to ${api.host}, because that address comes from a file in this folder, not from you.\nDetails: the API URL is set in ${file}. Pass --api-url to use it anyway`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
879
795
|
function toHost$1(url) {
|
|
880
796
|
try {
|
|
881
797
|
return new URL(url);
|
|
@@ -891,12 +807,15 @@ function isLoopback(hostname) {
|
|
|
891
807
|
* an access token this process already holds. An error report uses it, because a refresh
|
|
892
808
|
* cut short by the process ending would spend the refresh token and sign the person out.
|
|
893
809
|
*/
|
|
894
|
-
function toHeldCredential(settings) {
|
|
895
|
-
if (settings.apiKey)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
810
|
+
function toHeldCredential(settings, sources) {
|
|
811
|
+
if (settings.apiKey) {
|
|
812
|
+
refuseForeignKey(settings, sources);
|
|
813
|
+
return {
|
|
814
|
+
header: "X-API-Key",
|
|
815
|
+
value: settings.apiKey,
|
|
816
|
+
kind: "api key"
|
|
817
|
+
};
|
|
818
|
+
}
|
|
900
819
|
const current = held.get(settings.issuerUrl);
|
|
901
820
|
if (!current || (current.expiresAt ?? 0) - EXPIRY_MARGIN_MS <= Date.now()) return;
|
|
902
821
|
return {
|
|
@@ -980,6 +899,172 @@ function toStatusMessage(status) {
|
|
|
980
899
|
if (status === 403) return "the organization is deactivated, or its access has ended";
|
|
981
900
|
return `the request failed with status ${status}`;
|
|
982
901
|
}
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/output/failure.ts
|
|
904
|
+
/** FAILURE_CODES is every code a failure can carry, and none is ever renamed or given a new meaning. */
|
|
905
|
+
const FAILURE_CODES = {
|
|
906
|
+
unknown_command: "No command has that name. Run hardfin --help for the list",
|
|
907
|
+
invalid_arguments: "The command was given the wrong positional arguments",
|
|
908
|
+
missing_flag: "A flag the command needs was not given",
|
|
909
|
+
invalid_flag: "A flag was given a value it does not take",
|
|
910
|
+
invalid_input: "A file the command was given cannot be read, or does not hold JSON",
|
|
911
|
+
empty_request: "The command would change nothing, because no field was given",
|
|
912
|
+
output_required: "The answer is a file, and needs --output or a pipe",
|
|
913
|
+
not_confirmed: "The change was not confirmed. Pass --yes to confirm it without a terminal",
|
|
914
|
+
shell_unavailable: "The shell did not say where its startup file is",
|
|
915
|
+
invalid_config: "A config file or setting cannot be read",
|
|
916
|
+
not_signed_in: "No usable credential. Run hardfin login, or set HARDFIN_API_KEY",
|
|
917
|
+
sign_in_failed: "Signing in did not finish",
|
|
918
|
+
unreachable: "The CLI could not connect to the server",
|
|
919
|
+
timed_out: "Something the CLI waited on took too long",
|
|
920
|
+
busy: "Another hardfin command is holding the credential lock",
|
|
921
|
+
bad_request: "The API refused the request as written (HTTP 400)",
|
|
922
|
+
unauthorized: "The API did not accept the credential (HTTP 401)",
|
|
923
|
+
forbidden: "The credential may not do this (HTTP 403)",
|
|
924
|
+
not_found: "The record or path does not exist (HTTP 404)",
|
|
925
|
+
conflict: "The request conflicts with the record's current state (HTTP 409)",
|
|
926
|
+
too_large: "The request is larger than the API accepts (HTTP 413)",
|
|
927
|
+
invalid_request: "The API could not process the request (HTTP 422)",
|
|
928
|
+
rate_limited: "Too many requests. Wait, then try again (HTTP 429)",
|
|
929
|
+
server_error: "The API failed to answer (HTTP 5xx)",
|
|
930
|
+
api_error: "The API refused the request with another status",
|
|
931
|
+
internal_error: "A problem in hardfin itself, which has been reported"
|
|
932
|
+
};
|
|
933
|
+
/** toStatusCode names a refusal by the HTTP status the API answered with. */
|
|
934
|
+
function toStatusCode(status) {
|
|
935
|
+
switch (status) {
|
|
936
|
+
case 400: return "bad_request";
|
|
937
|
+
case 401: return "unauthorized";
|
|
938
|
+
case 403: return "forbidden";
|
|
939
|
+
case 404: return "not_found";
|
|
940
|
+
case 409: return "conflict";
|
|
941
|
+
case 413: return "too_large";
|
|
942
|
+
case 422: return "invalid_request";
|
|
943
|
+
case 429: return "rate_limited";
|
|
944
|
+
default: return status >= 500 ? "server_error" : "api_error";
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** toErrorCode names a failure nothing handled closer to where it happened. */
|
|
948
|
+
function toErrorCode(error) {
|
|
949
|
+
if (error instanceof RequestFailure) return toStatusCode(error.status);
|
|
950
|
+
if (error instanceof NoCredential) return "not_signed_in";
|
|
951
|
+
if (error instanceof ConfigFailure) return "invalid_config";
|
|
952
|
+
if (error instanceof Unreachable) return "unreachable";
|
|
953
|
+
if (error instanceof TimedOut) return "timed_out";
|
|
954
|
+
if (error instanceof LockHeld) return "busy";
|
|
955
|
+
return "internal_error";
|
|
956
|
+
}
|
|
957
|
+
/** version is what this build reports, read from the published package manifest. */
|
|
958
|
+
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/command/agent-guide.ts
|
|
961
|
+
const agentGuideCommand = defineCommand({
|
|
962
|
+
name: "agent-guide",
|
|
963
|
+
summary: "Print instructions an agent can follow to use this CLI",
|
|
964
|
+
description: "Writes Markdown describing every command, its flags, and its exit codes. Save it as a skill file, an AGENTS.md section, or paste it into a system prompt.",
|
|
965
|
+
arguments: [],
|
|
966
|
+
flags: [{
|
|
967
|
+
name: "json",
|
|
968
|
+
description: "Print the command registry as JSON instead of Markdown",
|
|
969
|
+
schema: z.boolean().optional()
|
|
970
|
+
}],
|
|
971
|
+
examples: [{
|
|
972
|
+
description: "Write a skill file",
|
|
973
|
+
command: "hardfin agent-guide > SKILL.md"
|
|
974
|
+
}, {
|
|
975
|
+
description: "Inspect the registry",
|
|
976
|
+
command: "hardfin agent-guide --json"
|
|
977
|
+
}],
|
|
978
|
+
run: runAgentGuide
|
|
979
|
+
});
|
|
980
|
+
/** toGuide renders the registry as the Markdown an agent reads. */
|
|
981
|
+
function toGuide(commands, version) {
|
|
982
|
+
const lines = [
|
|
983
|
+
"# Hardfin CLI",
|
|
984
|
+
"",
|
|
985
|
+
`The \`hardfin\` command calls the Hardfin API. This guide describes version ${version} of the CLI, which speaks API version ${API_VERSION}.`,
|
|
986
|
+
"",
|
|
987
|
+
"## Before calling",
|
|
988
|
+
"",
|
|
989
|
+
"- Authenticate with `--api-key`, by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
|
|
990
|
+
"- Every command prints JSON on stdout and diagnostics on stderr",
|
|
991
|
+
"- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
|
|
992
|
+
"",
|
|
993
|
+
"## Exit codes",
|
|
994
|
+
"",
|
|
995
|
+
"| Code | Means |",
|
|
996
|
+
"| --- | --- |",
|
|
997
|
+
"| 0 | The command succeeded |",
|
|
998
|
+
"| 1 | The command failed |",
|
|
999
|
+
"| 2 | The command was called wrongly |",
|
|
1000
|
+
"| 4 | No usable credential |",
|
|
1001
|
+
"| 130 | The person pressed Ctrl-C |",
|
|
1002
|
+
"",
|
|
1003
|
+
"## Error codes",
|
|
1004
|
+
"",
|
|
1005
|
+
"A failed command prints JSON on stderr with a `code`, a `message`, the API's `errors`, its `requestId` and the HTTP `status`. Branch on `code`, which never changes meaning. The message is for a person.",
|
|
1006
|
+
"",
|
|
1007
|
+
"| Code | Means |",
|
|
1008
|
+
"| --- | --- |",
|
|
1009
|
+
...Object.entries(FAILURE_CODES).map(([code, meaning]) => `| \`${code}\` | ${meaning} |`),
|
|
1010
|
+
"",
|
|
1011
|
+
"## Commands",
|
|
1012
|
+
""
|
|
1013
|
+
];
|
|
1014
|
+
for (const command of commands.filter((command) => !command.hidden)) lines.push(...toCommandLines(command, []));
|
|
1015
|
+
return lines.join("\n");
|
|
1016
|
+
}
|
|
1017
|
+
/** toCommandLines describes one command and everything nested under it. */
|
|
1018
|
+
function toCommandLines(command, parents) {
|
|
1019
|
+
const path = [...parents, command.name];
|
|
1020
|
+
const lines = [
|
|
1021
|
+
`### \`hardfin ${path.join(" ")}\``,
|
|
1022
|
+
"",
|
|
1023
|
+
command.description ?? command.summary,
|
|
1024
|
+
""
|
|
1025
|
+
];
|
|
1026
|
+
if (command.arguments.length > 0) {
|
|
1027
|
+
lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
|
|
1028
|
+
for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
|
|
1029
|
+
lines.push("");
|
|
1030
|
+
}
|
|
1031
|
+
if (command.flags.length > 0) {
|
|
1032
|
+
lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
|
|
1033
|
+
for (const flag of command.flags) {
|
|
1034
|
+
const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
|
|
1035
|
+
lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
|
|
1036
|
+
}
|
|
1037
|
+
lines.push("");
|
|
1038
|
+
}
|
|
1039
|
+
for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
|
|
1040
|
+
for (const subcommand of command.subcommands ?? []) lines.push(...toCommandLines(subcommand, path));
|
|
1041
|
+
return lines;
|
|
1042
|
+
}
|
|
1043
|
+
async function runAgentGuide(input) {
|
|
1044
|
+
if (input.flags["json"] === true) {
|
|
1045
|
+
writeData(input.commands.map(toSummary));
|
|
1046
|
+
return ExitCode.OK;
|
|
1047
|
+
}
|
|
1048
|
+
writeData(toGuide(input.commands, version$1));
|
|
1049
|
+
return ExitCode.OK;
|
|
1050
|
+
}
|
|
1051
|
+
function toSummary(command) {
|
|
1052
|
+
return {
|
|
1053
|
+
name: command.name,
|
|
1054
|
+
summary: command.summary,
|
|
1055
|
+
description: command.description ?? command.summary,
|
|
1056
|
+
arguments: command.arguments,
|
|
1057
|
+
flags: command.flags.map((flag) => ({
|
|
1058
|
+
name: flag.name,
|
|
1059
|
+
short: flag.short ?? null,
|
|
1060
|
+
description: flag.description,
|
|
1061
|
+
valueName: flag.valueName ?? null,
|
|
1062
|
+
repeatable: flag.repeatable ?? false
|
|
1063
|
+
})),
|
|
1064
|
+
examples: command.examples,
|
|
1065
|
+
subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
983
1068
|
const apiCommand = defineCommand({
|
|
984
1069
|
name: "api",
|
|
985
1070
|
summary: "Call a Hardfin API endpoint and print what it answers",
|
|
@@ -1043,30 +1128,30 @@ const apiCommand = defineCommand({
|
|
|
1043
1128
|
async function runApi(input) {
|
|
1044
1129
|
const path = input.args[0];
|
|
1045
1130
|
if (!path) {
|
|
1046
|
-
writeFailure("a path is required, such as /customer", input.isJSON);
|
|
1131
|
+
writeFailure("invalid_arguments", "a path is required, such as /customer", input.isJSON);
|
|
1047
1132
|
return ExitCode.USAGE;
|
|
1048
1133
|
}
|
|
1049
1134
|
if (path.split("/").includes("..")) {
|
|
1050
|
-
writeFailure("a path stays inside the API, so it cannot contain ..", input.isJSON);
|
|
1135
|
+
writeFailure("invalid_arguments", "a path stays inside the API, so it cannot contain ..", input.isJSON);
|
|
1051
1136
|
return ExitCode.USAGE;
|
|
1052
1137
|
}
|
|
1053
1138
|
const query = toQuery$1(input.flags["field"]);
|
|
1054
1139
|
if (query === void 0) {
|
|
1055
|
-
writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
|
|
1140
|
+
writeFailure("invalid_flag", "each --field is key=value, such as -f limit=50", input.isJSON);
|
|
1056
1141
|
return ExitCode.USAGE;
|
|
1057
1142
|
}
|
|
1058
1143
|
let body;
|
|
1059
1144
|
if (typeof input.flags["input"] === "string") {
|
|
1060
1145
|
body = toBody$1(input.flags["input"]);
|
|
1061
1146
|
if (body === void 0) {
|
|
1062
|
-
writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
|
|
1147
|
+
writeFailure("invalid_input", `${input.flags["input"]} does not hold JSON`, input.isJSON);
|
|
1063
1148
|
return ExitCode.USAGE;
|
|
1064
1149
|
}
|
|
1065
1150
|
}
|
|
1066
1151
|
try {
|
|
1067
1152
|
writeData((await request$1({
|
|
1068
1153
|
apiUrl: input.resolved.settings.apiUrl,
|
|
1069
|
-
credential: await toRequestCredential(input.resolved.settings),
|
|
1154
|
+
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
1070
1155
|
method: toText(input.flags["method"] ?? "GET").toUpperCase(),
|
|
1071
1156
|
path,
|
|
1072
1157
|
query,
|
|
@@ -1075,11 +1160,11 @@ async function runApi(input) {
|
|
|
1075
1160
|
return ExitCode.OK;
|
|
1076
1161
|
} catch (error) {
|
|
1077
1162
|
if (error instanceof NoCredential) {
|
|
1078
|
-
writeFailure(error.message, input.isJSON);
|
|
1163
|
+
writeFailure("not_signed_in", error.message, input.isJSON);
|
|
1079
1164
|
return ExitCode.NOT_AUTHENTICATED;
|
|
1080
1165
|
}
|
|
1081
1166
|
if (error instanceof RequestFailure) {
|
|
1082
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1167
|
+
writeFailure(toStatusCode(error.status), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1083
1168
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
1084
1169
|
}
|
|
1085
1170
|
throw error;
|
|
@@ -1485,17 +1570,17 @@ async function runChange(input, change) {
|
|
|
1485
1570
|
const place = toPlace();
|
|
1486
1571
|
const shell = input.args[0] ?? toShell(place);
|
|
1487
1572
|
if (!shell || !SHELLS.includes(shell)) {
|
|
1488
|
-
writeFailure(`name the shell to ${change} for: ${SHELLS.join(", ")}`, input.isJSON);
|
|
1573
|
+
writeFailure("invalid_arguments", `name the shell to ${change} for: ${SHELLS.join(", ")}`, input.isJSON);
|
|
1489
1574
|
return ExitCode.USAGE;
|
|
1490
1575
|
}
|
|
1491
1576
|
const plan = toPlan(shell, place);
|
|
1492
1577
|
if (change === "install" && shell === "powershell" && !plan.startup) {
|
|
1493
|
-
writeFailure("PowerShell did not report its profile, so there is nowhere to load the script from", input.isJSON);
|
|
1578
|
+
writeFailure("shell_unavailable", "PowerShell did not report its profile, so there is nowhere to load the script from", input.isJSON);
|
|
1494
1579
|
return ExitCode.ERROR;
|
|
1495
1580
|
}
|
|
1496
1581
|
process.stderr.write(toDescription(plan, change));
|
|
1497
1582
|
if (!await isConfirmed(input.flags["yes"] === true)) {
|
|
1498
|
-
writeFailure(process.stdin.isTTY ? "nothing was changed" : "nothing was changed. Pass --yes to change it without a terminal", input.isJSON);
|
|
1583
|
+
writeFailure("not_confirmed", process.stdin.isTTY ? "nothing was changed" : "nothing was changed. Pass --yes to change it without a terminal", input.isJSON);
|
|
1499
1584
|
return ExitCode.USAGE;
|
|
1500
1585
|
}
|
|
1501
1586
|
if (change === "install") {
|
|
@@ -1658,7 +1743,7 @@ function toSchemaValues(schema) {
|
|
|
1658
1743
|
async function runCompletion(input) {
|
|
1659
1744
|
const shell = input.args[0] ?? "";
|
|
1660
1745
|
if (!SHELLS.includes(shell)) {
|
|
1661
|
-
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
1746
|
+
writeFailure("invalid_arguments", `${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
1662
1747
|
return ExitCode.USAGE;
|
|
1663
1748
|
}
|
|
1664
1749
|
writeData(toScript(shell));
|
|
@@ -2064,25 +2149,25 @@ async function runLogin(input) {
|
|
|
2064
2149
|
}
|
|
2065
2150
|
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
2066
2151
|
if (callback.error) {
|
|
2067
|
-
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
2152
|
+
writeFailure("sign_in_failed", `sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
2068
2153
|
return ExitCode.ERROR;
|
|
2069
2154
|
}
|
|
2070
2155
|
if (callback.source === "listener" ? callback.state !== state : callback.state !== void 0 && callback.state !== state) {
|
|
2071
|
-
writeFailure("the browser came back with a state this sign in did not send", input.isJSON);
|
|
2156
|
+
writeFailure("sign_in_failed", "the browser came back with a state this sign in did not send", input.isJSON);
|
|
2072
2157
|
return ExitCode.ERROR;
|
|
2073
2158
|
}
|
|
2074
2159
|
if (callback.issuer !== void 0 && callback.issuer.replace(/\/+$/, "") !== metadata.issuer.replace(/\/+$/, "")) {
|
|
2075
|
-
writeFailure(`the browser came back from ${callback.issuer}, which is not ${metadata.issuer}`, input.isJSON);
|
|
2160
|
+
writeFailure("sign_in_failed", `the browser came back from ${callback.issuer}, which is not ${metadata.issuer}`, input.isJSON);
|
|
2076
2161
|
return ExitCode.ERROR;
|
|
2077
2162
|
}
|
|
2078
2163
|
if (!callback.code) {
|
|
2079
|
-
writeFailure("the browser came back without an authorization code", input.isJSON);
|
|
2164
|
+
writeFailure("sign_in_failed", "the browser came back without an authorization code", input.isJSON);
|
|
2080
2165
|
return ExitCode.ERROR;
|
|
2081
2166
|
}
|
|
2082
2167
|
return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
|
|
2083
2168
|
} catch (error) {
|
|
2084
2169
|
if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
|
|
2085
|
-
writeFailure(error.message, input.isJSON);
|
|
2170
|
+
writeFailure("sign_in_failed", error.message, input.isJSON);
|
|
2086
2171
|
return ExitCode.ERROR;
|
|
2087
2172
|
}
|
|
2088
2173
|
throw error;
|
|
@@ -2092,7 +2177,7 @@ async function runLogin(input) {
|
|
|
2092
2177
|
async function runDeviceLogin(input, metadata, scope) {
|
|
2093
2178
|
const settings = input.resolved.settings;
|
|
2094
2179
|
if (!metadata.device_authorization_endpoint) {
|
|
2095
|
-
writeFailure(`${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
|
|
2180
|
+
writeFailure("sign_in_failed", `${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
|
|
2096
2181
|
return ExitCode.ERROR;
|
|
2097
2182
|
}
|
|
2098
2183
|
const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
|
|
@@ -2126,7 +2211,7 @@ async function toDeviceTokens(approved, typed) {
|
|
|
2126
2211
|
async function toSignedIn(input, metadata, tokens) {
|
|
2127
2212
|
const refreshToken = tokens.refreshToken;
|
|
2128
2213
|
if (!refreshToken) {
|
|
2129
|
-
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
2214
|
+
writeFailure("sign_in_failed", "the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
2130
2215
|
return ExitCode.ERROR;
|
|
2131
2216
|
}
|
|
2132
2217
|
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
@@ -2478,7 +2563,7 @@ function toServerReport(metadata) {
|
|
|
2478
2563
|
async function toApiReport(resolved, options) {
|
|
2479
2564
|
const settings = resolved.settings;
|
|
2480
2565
|
try {
|
|
2481
|
-
const credential = options.isHeldOnly ? toHeldCredential(settings) : await toRequestCredential(settings);
|
|
2566
|
+
const credential = options.isHeldOnly ? toHeldCredential(settings, resolved.sources) : await toRequestCredential(settings, resolved.sources);
|
|
2482
2567
|
if (!credential) return {
|
|
2483
2568
|
checked: false,
|
|
2484
2569
|
url: settings.apiUrl,
|
|
@@ -2610,7 +2695,7 @@ async function report(error, occasion, resolved) {
|
|
|
2610
2695
|
const settings = resolved ?? toResolved();
|
|
2611
2696
|
const status = settings ? await toStatus(settings) : void 0;
|
|
2612
2697
|
const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
|
|
2613
|
-
return await (await import("./sentry-
|
|
2698
|
+
return await (await import("./sentry-CLaeAWNa.js")).send({
|
|
2614
2699
|
error,
|
|
2615
2700
|
occasion,
|
|
2616
2701
|
telemetry,
|
|
@@ -2802,7 +2887,7 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
2802
2887
|
"mcp"
|
|
2803
2888
|
]);
|
|
2804
2889
|
/** The global options, which sit before a command and take a value of their own. */
|
|
2805
|
-
const GLOBAL_OPTIONS_WITH_VALUES =
|
|
2890
|
+
const GLOBAL_OPTIONS_WITH_VALUES = new Set(GLOBAL_FLAGS.filter((flag) => flag.valueName).map((flag) => `--${flag.name}`));
|
|
2806
2891
|
/**
|
|
2807
2892
|
* toCommandName reads which command a call names. A global option sits before it, so
|
|
2808
2893
|
* reading the first argument alone would let --issuer-url x logout through.
|
|
@@ -3059,28 +3144,28 @@ function defineOperation(operation) {
|
|
|
3059
3144
|
async function runOperation(operation, input) {
|
|
3060
3145
|
const path = toPath(operation, input.args);
|
|
3061
3146
|
if (path === void 0) {
|
|
3062
|
-
writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
3147
|
+
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
3063
3148
|
return ExitCode.USAGE;
|
|
3064
3149
|
}
|
|
3065
3150
|
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0);
|
|
3066
3151
|
if (missing.length > 0) {
|
|
3067
|
-
writeFailure(`this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
3152
|
+
writeFailure("missing_flag", `this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
3068
3153
|
return ExitCode.USAGE;
|
|
3069
3154
|
}
|
|
3070
3155
|
const body = toBody(operation, input.flags);
|
|
3071
3156
|
if (body instanceof Error) {
|
|
3072
|
-
writeFailure(body.message, input.isJSON);
|
|
3157
|
+
writeFailure("invalid_flag", body.message, input.isJSON);
|
|
3073
3158
|
return ExitCode.USAGE;
|
|
3074
3159
|
}
|
|
3075
3160
|
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
3076
|
-
writeFailure(`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);
|
|
3161
|
+
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);
|
|
3077
3162
|
return ExitCode.USAGE;
|
|
3078
3163
|
}
|
|
3079
3164
|
let form;
|
|
3080
3165
|
if (operation.upload) {
|
|
3081
3166
|
const built = await toForm(operation.upload, input.flags);
|
|
3082
3167
|
if (built instanceof Error) {
|
|
3083
|
-
writeFailure(built.message, input.isJSON);
|
|
3168
|
+
writeFailure("invalid_input", built.message, input.isJSON);
|
|
3084
3169
|
return ExitCode.USAGE;
|
|
3085
3170
|
}
|
|
3086
3171
|
form = built;
|
|
@@ -3088,7 +3173,7 @@ async function runOperation(operation, input) {
|
|
|
3088
3173
|
try {
|
|
3089
3174
|
const envelope = await request$1({
|
|
3090
3175
|
apiUrl: input.resolved.settings.apiUrl,
|
|
3091
|
-
credential: await toRequestCredential(input.resolved.settings),
|
|
3176
|
+
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
3092
3177
|
method: operation.method,
|
|
3093
3178
|
path,
|
|
3094
3179
|
query: toQuery(operation, input.flags),
|
|
@@ -3101,11 +3186,11 @@ async function runOperation(operation, input) {
|
|
|
3101
3186
|
return ExitCode.OK;
|
|
3102
3187
|
} catch (error) {
|
|
3103
3188
|
if (error instanceof NoCredential) {
|
|
3104
|
-
writeFailure(error.message, input.isJSON);
|
|
3189
|
+
writeFailure("not_signed_in", error.message, input.isJSON);
|
|
3105
3190
|
return ExitCode.NOT_AUTHENTICATED;
|
|
3106
3191
|
}
|
|
3107
3192
|
if (error instanceof RequestFailure) {
|
|
3108
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
3193
|
+
writeFailure(toStatusCode(error.status), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
3109
3194
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
3110
3195
|
}
|
|
3111
3196
|
throw error;
|
|
@@ -3120,7 +3205,7 @@ function toWritten(download, input) {
|
|
|
3120
3205
|
const path = typeof asked === "string" ? asked : download.fileName ?? "-";
|
|
3121
3206
|
if (path === "-") {
|
|
3122
3207
|
if (process.stdout.isTTY) {
|
|
3123
|
-
writeFailure("this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
3208
|
+
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);
|
|
3124
3209
|
return ExitCode.USAGE;
|
|
3125
3210
|
}
|
|
3126
3211
|
process.stdout.write(download.bytes);
|
|
@@ -5921,7 +6006,8 @@ function toRejectedFlag(command, flags) {
|
|
|
5921
6006
|
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
5922
6007
|
function toCli(entries = commands) {
|
|
5923
6008
|
const program = new Command();
|
|
5924
|
-
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").
|
|
6009
|
+
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").enablePositionalOptions();
|
|
6010
|
+
toQuietParser(program);
|
|
5925
6011
|
for (const flag of GLOBAL_FLAGS) program.option(`--${flag.name} <${flag.valueName}>`, flag.description);
|
|
5926
6012
|
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
5927
6013
|
return program;
|
|
@@ -5929,6 +6015,7 @@ function toCli(entries = commands) {
|
|
|
5929
6015
|
/** toProgram wires one registry command into the parser. */
|
|
5930
6016
|
function toProgram(root, command) {
|
|
5931
6017
|
const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
|
|
6018
|
+
toQuietParser(program);
|
|
5932
6019
|
for (const argument of command.arguments) {
|
|
5933
6020
|
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
5934
6021
|
program.argument(argument.required ? `<${name}>` : `[${name}]`, argument.description);
|
|
@@ -5952,17 +6039,48 @@ function toProgram(root, command) {
|
|
|
5952
6039
|
});
|
|
5953
6040
|
return program;
|
|
5954
6041
|
}
|
|
6042
|
+
/** toQuietParser has the parser hand back its mistakes, so they are answered like any other failure. */
|
|
6043
|
+
function toQuietParser(program) {
|
|
6044
|
+
program.exitOverride().configureOutput({ outputError: () => void 0 });
|
|
6045
|
+
}
|
|
6046
|
+
/** The parser's codes for a run that only showed help or the version, which is not a failure. */
|
|
6047
|
+
const SHOWN = /* @__PURE__ */ new Set([
|
|
6048
|
+
"commander.helpDisplayed",
|
|
6049
|
+
"commander.help",
|
|
6050
|
+
"commander.version"
|
|
6051
|
+
]);
|
|
6052
|
+
/** toParseExitCode answers a mistake the parser caught before any command ran. */
|
|
6053
|
+
function toParseExitCode(error, args) {
|
|
6054
|
+
if (!(error instanceof CommanderError)) return;
|
|
6055
|
+
if (SHOWN.has(error.code)) return error.exitCode;
|
|
6056
|
+
const isJSON = args.includes("--json") || !process.stdout.isTTY;
|
|
6057
|
+
writeFailure(toParseCode(error.code), error.message.replace(/^error: /, ""), isJSON);
|
|
6058
|
+
if (!isJSON) process.stderr.write(" Run the command with --help to see what it takes\n");
|
|
6059
|
+
return ExitCode.USAGE;
|
|
6060
|
+
}
|
|
6061
|
+
function toParseCode(code) {
|
|
6062
|
+
switch (code) {
|
|
6063
|
+
case "commander.unknownCommand": return "unknown_command";
|
|
6064
|
+
case "commander.unknownOption":
|
|
6065
|
+
case "commander.optionMissingArgument":
|
|
6066
|
+
case "commander.missingMandatoryOptionValue":
|
|
6067
|
+
case "commander.conflictingOption":
|
|
6068
|
+
case "commander.invalidArgument": return "invalid_flag";
|
|
6069
|
+
default: return "invalid_arguments";
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
5955
6072
|
async function toExitCode(root, command, args, flags) {
|
|
5956
6073
|
const isJSON = isJSONOutput(flags);
|
|
5957
6074
|
const rejected = toRejectedFlag(command, flags);
|
|
5958
6075
|
if (rejected) {
|
|
5959
|
-
writeFailure(rejected, isJSON);
|
|
6076
|
+
writeFailure("invalid_flag", rejected, isJSON);
|
|
5960
6077
|
return ExitCode.USAGE;
|
|
5961
6078
|
}
|
|
5962
6079
|
let resolved;
|
|
5963
6080
|
try {
|
|
5964
6081
|
resolved = toSettings({
|
|
5965
6082
|
apiUrl: root.opts()["apiUrl"],
|
|
6083
|
+
apiKey: root.opts()["apiKey"],
|
|
5966
6084
|
issuerUrl: root.opts()["issuerUrl"]
|
|
5967
6085
|
});
|
|
5968
6086
|
return await command.run?.({
|
|
@@ -5977,7 +6095,7 @@ async function toExitCode(root, command, args, flags) {
|
|
|
5977
6095
|
process.stderr.write("\n");
|
|
5978
6096
|
return ExitCode.INTERRUPTED;
|
|
5979
6097
|
}
|
|
5980
|
-
writeFailure(error instanceof Error ? error.message : String(error), isJSON);
|
|
6098
|
+
writeFailure(toErrorCode(error), error instanceof Error ? error.message : String(error), isJSON);
|
|
5981
6099
|
await toReported(error, isJSON, resolved);
|
|
5982
6100
|
return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
|
|
5983
6101
|
}
|
|
@@ -6002,7 +6120,7 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6002
6120
|
if (id && !isJSON) process.stderr.write(`This looks like a problem in hardfin itself, and it has been reported to the Hardfin team. Reference: ${id}\n`);
|
|
6003
6121
|
}
|
|
6004
6122
|
//#endregion
|
|
6005
|
-
export { toCli, toReported, wasReportAttempted };
|
|
6123
|
+
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6006
6124
|
|
|
6007
|
-
//# sourceMappingURL=program-
|
|
6008
|
-
//# debugId=
|
|
6125
|
+
//# sourceMappingURL=program-B0vISolG.js.map
|
|
6126
|
+
//# debugId=ab584cf3-443f-569e-8167-fa0cbab9ab68
|
|
@@ -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]="3af5060a-b6fe-5f2e-9661-01f4460a7b04")}catch(e){}}();
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
//#region src/command/registry.ts
|
|
@@ -17,17 +17,26 @@ function defineCommand(command) {
|
|
|
17
17
|
return command;
|
|
18
18
|
}
|
|
19
19
|
/** GLOBAL_FLAGS are the options the CLI takes before any command. */
|
|
20
|
-
const GLOBAL_FLAGS = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const GLOBAL_FLAGS = [
|
|
21
|
+
{
|
|
22
|
+
name: "api-url",
|
|
23
|
+
description: "The API to call, whose host also holds the authorization server",
|
|
24
|
+
valueName: "url",
|
|
25
|
+
schema: z.string()
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "api-key",
|
|
29
|
+
description: "An API key to call with, in place of HARDFIN_API_KEY or a sign in",
|
|
30
|
+
valueName: "key",
|
|
31
|
+
schema: z.string()
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "issuer-url",
|
|
35
|
+
description: "The authorization server, when it does not sit at the API's host",
|
|
36
|
+
valueName: "url",
|
|
37
|
+
schema: z.string()
|
|
38
|
+
}
|
|
39
|
+
];
|
|
31
40
|
//#endregion
|
|
32
41
|
//#region src/report/scrub.ts
|
|
33
42
|
const REDACTED = "[redacted]";
|
|
@@ -109,5 +118,5 @@ function toFlag(flags, word) {
|
|
|
109
118
|
//#endregion
|
|
110
119
|
export { defineCommand as a, GLOBAL_FLAGS as i, toScrubbed as n, ExitCode as r, toInvocation as t };
|
|
111
120
|
|
|
112
|
-
//# sourceMappingURL=scrub-
|
|
113
|
-
//# debugId=
|
|
121
|
+
//# sourceMappingURL=scrub-BN3gyBpR.js.map
|
|
122
|
+
//# debugId=3af5060a-b6fe-5f2e-9661-01f4460a7b04
|
|
@@ -1,7 +1,7 @@
|
|
|
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]="
|
|
4
|
-
import { n as toScrubbed } from "./scrub-
|
|
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]="853940c3-9e72-566b-ad70-9f043e6aba8d")}catch(e){}}();
|
|
4
|
+
import { n as toScrubbed } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
6
|
import { request } from "node:http";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -4385,5 +4385,5 @@ function toTags(delivery) {
|
|
|
4385
4385
|
//#endregion
|
|
4386
4386
|
export { send };
|
|
4387
4387
|
|
|
4388
|
-
//# sourceMappingURL=sentry-
|
|
4389
|
-
//# debugId=
|
|
4388
|
+
//# sourceMappingURL=sentry-CLaeAWNa.js.map
|
|
4389
|
+
//# debugId=853940c3-9e72-566b-ad70-9f043e6aba8d
|