@hardfin/cli 0.1.1-dev.39 → 0.1.1-dev.40
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 +6 -1
- package/dist/cli.js +4 -4
- package/dist/{program-d7dXAbE4.js → program-C38xnnim.js} +555 -554
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -373,7 +373,8 @@ asking and ask before anything changes.
|
|
|
373
373
|
|
|
374
374
|
A command is a read when its endpoint's method is GET, or for `hardfin api` when no other
|
|
375
375
|
`--method` is given. Anything else is a write, and `hardfin_read` refuses it by naming
|
|
376
|
-
`hardfin_write`.
|
|
376
|
+
`hardfin_write`. The server asks the CLI's own parser what a call would run, so a flag whose
|
|
377
|
+
value is `--help`, or a `--method` given twice, is placed by what the command would do. Both tools carry a title, the read-only or destructive hint, and a link to
|
|
377
378
|
the API reference, which Claude's connector directory requires.
|
|
378
379
|
|
|
379
380
|
A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
|
|
@@ -392,6 +393,10 @@ The server uses whatever credential this machine holds, so an agent acts as the
|
|
|
392
393
|
ran `hardfin login`. Give an agent an API key through `HARDFIN_API_KEY` when it should act
|
|
393
394
|
as an integration instead.
|
|
394
395
|
|
|
396
|
+
A call cannot set `--api-url`, `--api-key` or `--issuer-url`. Both tools refuse them, because
|
|
397
|
+
a call that chose the host could send the credential to it. Set `HARDFIN_API_URL`,
|
|
398
|
+
`HARDFIN_API_KEY` or `HARDFIN_ISSUER_URL` where the server is started.
|
|
399
|
+
|
|
395
400
|
## Diagnosing a problem
|
|
396
401
|
|
|
397
402
|
`hardfin status` prints everything a support request needs, and is the first thing to send.
|
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]="b40d6c93-b462-5248-a0a0-bbc0f5456ba1")}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-C38xnnim.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
|
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
35
|
try {
|
|
36
|
-
const { toReported } = await import("./program-
|
|
36
|
+
const { toReported } = await import("./program-C38xnnim.js");
|
|
37
37
|
await toReported(error, false, void 0, "fatal");
|
|
38
38
|
} finally {
|
|
39
39
|
process.exit(1);
|
|
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
|
|
|
51
51
|
export {};
|
|
52
52
|
|
|
53
53
|
//# sourceMappingURL=cli.js.map
|
|
54
|
-
//# debugId=
|
|
54
|
+
//# debugId=b40d6c93-b462-5248-a0a0-bbc0f5456ba1
|
|
@@ -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]="23617f68-b6d8-5249-bca7-f5585fdb94f2")}catch(e){}}();
|
|
4
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";
|
|
@@ -15,6 +15,114 @@ import { createInterface } from "node:readline/promises";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
18
|
+
//#region src/output/writer.ts
|
|
19
|
+
/**
|
|
20
|
+
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
21
|
+
* otherwise reach a request as the text "[object Object]".
|
|
22
|
+
*/
|
|
23
|
+
function toText(value) {
|
|
24
|
+
if (typeof value === "string") return value;
|
|
25
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
26
|
+
if (value === void 0 || value === null) return "";
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
/** writeData prints a command's result on stdout. */
|
|
30
|
+
function writeData(value) {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
36
|
+
}
|
|
37
|
+
/** writeFailure prints why a command failed on stderr, with a code only a machine reading JSON sees. */
|
|
38
|
+
function writeFailure(code, message, isJSON, errors, requestId, status) {
|
|
39
|
+
if (!isJSON) {
|
|
40
|
+
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
41
|
+
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
42
|
+
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const payload = {
|
|
46
|
+
code,
|
|
47
|
+
message,
|
|
48
|
+
errors: errors ?? [{
|
|
49
|
+
error: message,
|
|
50
|
+
statusCode: 0
|
|
51
|
+
}],
|
|
52
|
+
requestId: requestId ?? null,
|
|
53
|
+
status: status ?? null
|
|
54
|
+
};
|
|
55
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
56
|
+
}
|
|
57
|
+
/** isJSONOutput decides whether this invocation prints machine-readable output. */
|
|
58
|
+
function isJSONOutput(flags) {
|
|
59
|
+
if (flags["json"] === true) return true;
|
|
60
|
+
return !process.stdout.isTTY;
|
|
61
|
+
}
|
|
62
|
+
/** version is what this build reports, read from the published package manifest. */
|
|
63
|
+
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/cli/parser.ts
|
|
66
|
+
/** toParser builds the parser from the registry, and hands whatever it parses to onRun. */
|
|
67
|
+
function toParser(entries, onRun, isSilent = false) {
|
|
68
|
+
const program = new Command();
|
|
69
|
+
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").enablePositionalOptions();
|
|
70
|
+
toQuietParser(program, isSilent);
|
|
71
|
+
for (const flag of GLOBAL_FLAGS) program.option(`--${flag.name} <${flag.valueName}>`, flag.description);
|
|
72
|
+
for (const command of entries) program.addCommand(toProgram(program, command, onRun, isSilent), { hidden: command.hidden });
|
|
73
|
+
return program;
|
|
74
|
+
}
|
|
75
|
+
/** toProgram wires one registry command into the parser. */
|
|
76
|
+
function toProgram(root, command, onRun, isSilent) {
|
|
77
|
+
const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
|
|
78
|
+
toQuietParser(program, isSilent);
|
|
79
|
+
for (const argument of command.arguments) {
|
|
80
|
+
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
81
|
+
program.argument(argument.required ? `<${name}>` : `[${name}]`, argument.description);
|
|
82
|
+
}
|
|
83
|
+
for (const flag of command.flags) {
|
|
84
|
+
const short = flag.short ? `-${flag.short}, ` : "";
|
|
85
|
+
const value = flag.valueName ? ` <${flag.valueName}>` : "";
|
|
86
|
+
const option = new Option(`${short}--${flag.name}${value}`, flag.description);
|
|
87
|
+
if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
|
|
88
|
+
if (flag.repeatable) option.argParser(collect);
|
|
89
|
+
if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
|
|
90
|
+
program.addOption(option);
|
|
91
|
+
}
|
|
92
|
+
for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
|
|
93
|
+
for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(root, subcommand, onRun, isSilent), { hidden: subcommand.hidden });
|
|
94
|
+
if (!command.run) return program;
|
|
95
|
+
program.action((...parsed) => {
|
|
96
|
+
const flags = parsed[parsed.length - 2] ?? {};
|
|
97
|
+
return onRun({
|
|
98
|
+
command,
|
|
99
|
+
args: parsed.slice(0, parsed.length - 2).flatMap(toArgumentList),
|
|
100
|
+
flags,
|
|
101
|
+
globals: root.opts()
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return program;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* toQuietParser has the parser hand back its mistakes, so they are answered like any other
|
|
108
|
+
* failure. A silent parser also keeps its help and version to itself, for a caller that only
|
|
109
|
+
* wants to know what a command line means.
|
|
110
|
+
*/
|
|
111
|
+
function toQuietParser(program, isSilent) {
|
|
112
|
+
program.exitOverride().configureOutput({ outputError: () => void 0 });
|
|
113
|
+
if (isSilent) program.configureOutput({
|
|
114
|
+
writeOut: () => void 0,
|
|
115
|
+
writeErr: () => void 0
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function toArgumentList(value) {
|
|
119
|
+
if (Array.isArray(value)) return value.map(String);
|
|
120
|
+
return value === void 0 ? [] : [toText(value)];
|
|
121
|
+
}
|
|
122
|
+
function collect(value, previous) {
|
|
123
|
+
return [...previous ?? [], value];
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
18
126
|
//#region src/report/expected.ts
|
|
19
127
|
/**
|
|
20
128
|
* ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
|
|
@@ -474,51 +582,6 @@ function keep(issuer, document) {
|
|
|
474
582
|
} catch {}
|
|
475
583
|
}
|
|
476
584
|
//#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
|
|
522
585
|
//#region src/auth/grant.ts
|
|
523
586
|
const TokenResponse = z.looseObject({
|
|
524
587
|
access_token: z.string(),
|
|
@@ -954,8 +1017,6 @@ function toErrorCode(error) {
|
|
|
954
1017
|
if (error instanceof LockHeld) return "busy";
|
|
955
1018
|
return "internal_error";
|
|
956
1019
|
}
|
|
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
1020
|
//#endregion
|
|
960
1021
|
//#region src/command/agent-guide.ts
|
|
961
1022
|
const agentGuideCommand = defineCommand({
|
|
@@ -2546,68 +2607,339 @@ async function runLogout(input) {
|
|
|
2546
2607
|
});
|
|
2547
2608
|
return ExitCode.OK;
|
|
2548
2609
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
"false",
|
|
2552
|
-
"off",
|
|
2553
|
-
"no"
|
|
2554
|
-
]);
|
|
2610
|
+
//#endregion
|
|
2611
|
+
//#region src/command/operation.ts
|
|
2555
2612
|
/**
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
2613
|
+
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
2614
|
+
* lists, which is what a response always uses.
|
|
2558
2615
|
*/
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
const base = {
|
|
2562
|
-
dsn,
|
|
2563
|
-
environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
|
|
2564
|
-
release: toRelease(version$1)
|
|
2565
|
-
};
|
|
2566
|
-
if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
|
|
2567
|
-
...base,
|
|
2568
|
-
enabled: false,
|
|
2569
|
-
from: "DO_NOT_TRACK"
|
|
2570
|
-
};
|
|
2571
|
-
const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
|
|
2572
|
-
if (asked !== void 0 && OFF.has(asked)) return {
|
|
2573
|
-
...base,
|
|
2574
|
-
enabled: false,
|
|
2575
|
-
from: "HARDFIN_TELEMETRY"
|
|
2576
|
-
};
|
|
2577
|
-
if (!isDsn(dsn)) return {
|
|
2578
|
-
...base,
|
|
2579
|
-
enabled: false,
|
|
2580
|
-
from: "HARDFIN_SENTRY_DSN is not a valid DSN"
|
|
2581
|
-
};
|
|
2582
|
-
if (asked !== void 0) return {
|
|
2583
|
-
...base,
|
|
2584
|
-
enabled: true,
|
|
2585
|
-
from: "HARDFIN_TELEMETRY"
|
|
2586
|
-
};
|
|
2587
|
-
return isCheckout ? {
|
|
2588
|
-
...base,
|
|
2589
|
-
enabled: false,
|
|
2590
|
-
from: "run from a checkout"
|
|
2591
|
-
} : {
|
|
2592
|
-
...base,
|
|
2593
|
-
enabled: true,
|
|
2594
|
-
from: "default"
|
|
2595
|
-
};
|
|
2616
|
+
function toEnum(values) {
|
|
2617
|
+
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
2596
2618
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2619
|
+
const UNSET_FLAG = {
|
|
2620
|
+
name: "unset",
|
|
2621
|
+
description: "A field to clear, named as its flag is, repeatable",
|
|
2622
|
+
valueName: "field",
|
|
2623
|
+
repeatable: true,
|
|
2624
|
+
schema: z.array(z.string())
|
|
2625
|
+
};
|
|
2626
|
+
const INPUT_FLAG = {
|
|
2627
|
+
name: "input",
|
|
2628
|
+
description: "A file holding the JSON request body, or - for standard input. A flag given as well replaces its field",
|
|
2629
|
+
valueName: "path",
|
|
2630
|
+
schema: z.string()
|
|
2631
|
+
};
|
|
2632
|
+
const FILE_FLAG = {
|
|
2633
|
+
name: "file",
|
|
2634
|
+
description: "The file to upload",
|
|
2635
|
+
valueName: "path",
|
|
2636
|
+
schema: z.string()
|
|
2637
|
+
};
|
|
2638
|
+
const OUTPUT_FLAG = {
|
|
2639
|
+
name: "output",
|
|
2640
|
+
description: "Where to write the file, or - for standard output",
|
|
2641
|
+
valueName: "path",
|
|
2642
|
+
schema: z.string()
|
|
2643
|
+
};
|
|
2644
|
+
const JSON_FLAG = {
|
|
2645
|
+
name: "json",
|
|
2646
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
2647
|
+
schema: z.boolean()
|
|
2648
|
+
};
|
|
2649
|
+
/** defineOperation turns one endpoint into the command that calls it. */
|
|
2650
|
+
function defineOperation(operation) {
|
|
2651
|
+
const flags = [
|
|
2652
|
+
...operation.queryFlags,
|
|
2653
|
+
...operation.bodyFlags,
|
|
2654
|
+
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
2655
|
+
...operation.bodyFlags.length > 0 ? [INPUT_FLAG] : [],
|
|
2656
|
+
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
2657
|
+
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
2658
|
+
JSON_FLAG
|
|
2659
|
+
];
|
|
2660
|
+
return {
|
|
2661
|
+
name: operation.name,
|
|
2662
|
+
summary: operation.summary,
|
|
2663
|
+
description: operation.description ?? operation.summary,
|
|
2664
|
+
arguments: operation.pathParameters,
|
|
2665
|
+
flags,
|
|
2666
|
+
examples: operation.example ? [{
|
|
2667
|
+
description: operation.summary,
|
|
2668
|
+
command: operation.example
|
|
2669
|
+
}] : [],
|
|
2670
|
+
endpoint: {
|
|
2671
|
+
method: operation.method,
|
|
2672
|
+
path: operation.path
|
|
2673
|
+
},
|
|
2674
|
+
run: (input) => runOperation(operation, input)
|
|
2675
|
+
};
|
|
2603
2676
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2677
|
+
async function runOperation(operation, input) {
|
|
2678
|
+
const path = toPath(operation, input.args);
|
|
2679
|
+
if (path === void 0) {
|
|
2680
|
+
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
2681
|
+
return ExitCode.USAGE;
|
|
2682
|
+
}
|
|
2683
|
+
const given = toGivenBody(input.flags["input"]);
|
|
2684
|
+
if (given instanceof InputFailure) {
|
|
2685
|
+
writeFailure("invalid_input", given.message, input.isJSON);
|
|
2686
|
+
return ExitCode.USAGE;
|
|
2687
|
+
}
|
|
2688
|
+
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0 && !hasPath(given, flag.jsonPath));
|
|
2689
|
+
if (missing.length > 0) {
|
|
2690
|
+
writeFailure("missing_flag", `this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
2691
|
+
return ExitCode.USAGE;
|
|
2692
|
+
}
|
|
2693
|
+
const body = toBody(operation, input.flags, given);
|
|
2694
|
+
if (body instanceof Error) {
|
|
2695
|
+
writeFailure("invalid_flag", body.message, input.isJSON);
|
|
2696
|
+
return ExitCode.USAGE;
|
|
2697
|
+
}
|
|
2698
|
+
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
2699
|
+
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);
|
|
2700
|
+
return ExitCode.USAGE;
|
|
2701
|
+
}
|
|
2702
|
+
let form;
|
|
2703
|
+
if (operation.upload) {
|
|
2704
|
+
const built = await toForm(operation.upload, input.flags);
|
|
2705
|
+
if (built instanceof Error) {
|
|
2706
|
+
writeFailure("invalid_input", built.message, input.isJSON);
|
|
2707
|
+
return ExitCode.USAGE;
|
|
2708
|
+
}
|
|
2709
|
+
form = built;
|
|
2710
|
+
}
|
|
2711
|
+
try {
|
|
2712
|
+
const envelope = await request$1({
|
|
2713
|
+
apiUrl: input.resolved.settings.apiUrl,
|
|
2714
|
+
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
2715
|
+
method: operation.method,
|
|
2716
|
+
path,
|
|
2717
|
+
query: toQuery(operation, input.flags),
|
|
2718
|
+
body,
|
|
2719
|
+
form,
|
|
2720
|
+
downloads: operation.downloads
|
|
2721
|
+
});
|
|
2722
|
+
if (operation.downloads) return toWritten(envelope.data, input);
|
|
2723
|
+
writeData(envelope.data);
|
|
2724
|
+
return ExitCode.OK;
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (error instanceof NoCredential) {
|
|
2727
|
+
writeFailure("not_signed_in", error.message, input.isJSON);
|
|
2728
|
+
return ExitCode.NOT_AUTHENTICATED;
|
|
2729
|
+
}
|
|
2730
|
+
if (error instanceof RequestFailure) {
|
|
2731
|
+
writeFailure(toStatusCode(error.status), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
2732
|
+
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
2733
|
+
}
|
|
2734
|
+
throw error;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* toWritten puts a downloaded file where it was asked for. A terminal is never written to,
|
|
2739
|
+
* because a person would otherwise have their session filled with a file's bytes.
|
|
2740
|
+
*/
|
|
2741
|
+
function toWritten(download, input) {
|
|
2742
|
+
const asked = input.flags["output"];
|
|
2743
|
+
const path = typeof asked === "string" ? asked : download.fileName ?? "-";
|
|
2744
|
+
if (path === "-") {
|
|
2745
|
+
if (process.stdout.isTTY) {
|
|
2746
|
+
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);
|
|
2747
|
+
return ExitCode.USAGE;
|
|
2748
|
+
}
|
|
2749
|
+
process.stdout.write(download.bytes);
|
|
2750
|
+
return ExitCode.OK;
|
|
2751
|
+
}
|
|
2752
|
+
writeFileSync(path, download.bytes);
|
|
2753
|
+
writeData({
|
|
2754
|
+
written: path,
|
|
2755
|
+
bytes: download.bytes.length,
|
|
2756
|
+
contentType: download.contentType
|
|
2757
|
+
});
|
|
2758
|
+
return ExitCode.OK;
|
|
2759
|
+
}
|
|
2760
|
+
/** toPath fills the path template from the positional arguments, in order. */
|
|
2761
|
+
function toPath(operation, args) {
|
|
2762
|
+
if (args.length !== operation.pathParameters.length) return;
|
|
2763
|
+
let path = operation.path;
|
|
2764
|
+
for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
|
|
2765
|
+
return path;
|
|
2766
|
+
}
|
|
2767
|
+
/** toQuery carries only the flags this invocation actually set. */
|
|
2768
|
+
function toQuery(operation, flags) {
|
|
2769
|
+
const query = new URLSearchParams();
|
|
2770
|
+
for (const flag of operation.queryFlags) {
|
|
2771
|
+
const value = flags[toOptionKey(flag.name)];
|
|
2772
|
+
if (value === void 0) continue;
|
|
2773
|
+
const parsed = flag.schema.safeParse(value);
|
|
2774
|
+
const carried = parsed.success ? parsed.data : value;
|
|
2775
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
2776
|
+
}
|
|
2777
|
+
return query;
|
|
2778
|
+
}
|
|
2779
|
+
/** toOptionKey names the parsed flag, which the parser reports in camel case. */
|
|
2780
|
+
function toOptionKey(name) {
|
|
2781
|
+
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2782
|
+
}
|
|
2783
|
+
/** toForm builds the file part and its fields, which an upload endpoint takes. */
|
|
2784
|
+
async function toForm(upload, flags) {
|
|
2785
|
+
const path = flags["file"];
|
|
2786
|
+
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
2787
|
+
const form = new FormData();
|
|
2788
|
+
try {
|
|
2789
|
+
form.append(upload.filePart, await openAsBlob(path), basename(path));
|
|
2790
|
+
} catch {
|
|
2791
|
+
return /* @__PURE__ */ new Error(`${path} cannot be read`);
|
|
2792
|
+
}
|
|
2793
|
+
for (const field of upload.fields) {
|
|
2794
|
+
const value = flags[toOptionKey(field.name)];
|
|
2795
|
+
if (value !== void 0) {
|
|
2796
|
+
const parsed = field.schema.safeParse(value);
|
|
2797
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return form;
|
|
2801
|
+
}
|
|
2802
|
+
/** toGivenBody reads the --input body, which a generated command takes only as a JSON object. */
|
|
2803
|
+
function toGivenBody(source) {
|
|
2804
|
+
if (typeof source !== "string") return;
|
|
2805
|
+
const read = toJSONInput(source);
|
|
2806
|
+
if (read instanceof InputFailure) return read;
|
|
2807
|
+
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"}`);
|
|
2808
|
+
return read;
|
|
2809
|
+
}
|
|
2810
|
+
/** hasPath reports whether a body already holds a value at a field's path. */
|
|
2811
|
+
function hasPath(body, path) {
|
|
2812
|
+
let holder = body;
|
|
2813
|
+
for (const name of path) {
|
|
2814
|
+
if (typeof holder !== "object" || holder === null || !(name in holder)) return false;
|
|
2815
|
+
holder = holder[name];
|
|
2816
|
+
}
|
|
2817
|
+
return true;
|
|
2818
|
+
}
|
|
2819
|
+
/** toBody builds the request body from the --input body and the flags, a flag winning over its field. */
|
|
2820
|
+
function toBody(operation, flags, given) {
|
|
2821
|
+
const body = given ? structuredClone(given) : {};
|
|
2822
|
+
let hasField = given !== void 0;
|
|
2823
|
+
for (const flag of operation.bodyFlags) {
|
|
2824
|
+
const value = flags[toOptionKey(flag.name)];
|
|
2825
|
+
if (value === void 0) continue;
|
|
2826
|
+
if (flag.element) {
|
|
2827
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
2828
|
+
if (elements instanceof Error) return elements;
|
|
2829
|
+
set(body, flag.jsonPath, elements);
|
|
2830
|
+
hasField = true;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
const parsed = flag.schema.safeParse(value);
|
|
2834
|
+
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
2835
|
+
hasField = true;
|
|
2836
|
+
}
|
|
2837
|
+
const cleared = toCleared(operation, flags, body);
|
|
2838
|
+
if (cleared instanceof Error) return cleared;
|
|
2839
|
+
return hasField || cleared ? body : void 0;
|
|
2840
|
+
}
|
|
2841
|
+
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
2842
|
+
function toCleared(operation, flags, body) {
|
|
2843
|
+
const named = flags["unset"];
|
|
2844
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
2845
|
+
for (const name of names) {
|
|
2846
|
+
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
2847
|
+
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
2848
|
+
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
2849
|
+
set(body, flag.jsonPath, null);
|
|
2850
|
+
}
|
|
2851
|
+
return names.length > 0;
|
|
2852
|
+
}
|
|
2853
|
+
/** toElements reads the key=value pairs a repeated flag carries for one array element. */
|
|
2854
|
+
function toElements(flag, values) {
|
|
2855
|
+
const elements = [];
|
|
2856
|
+
for (const value of values) {
|
|
2857
|
+
const element = {};
|
|
2858
|
+
for (const pair of value.split(",")) {
|
|
2859
|
+
const split = pair.indexOf("=");
|
|
2860
|
+
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`);
|
|
2861
|
+
const key = pair.slice(0, split).trim();
|
|
2862
|
+
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
2863
|
+
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
2864
|
+
const value = pair.slice(split + 1).trim();
|
|
2865
|
+
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`);
|
|
2866
|
+
element[key] = value;
|
|
2867
|
+
}
|
|
2868
|
+
elements.push(element);
|
|
2869
|
+
}
|
|
2870
|
+
return elements;
|
|
2871
|
+
}
|
|
2872
|
+
/** set writes a value at its path, building the objects a nested field needs. */
|
|
2873
|
+
function set(body, path, value) {
|
|
2874
|
+
let holder = body;
|
|
2875
|
+
for (const name of path.slice(0, -1)) {
|
|
2876
|
+
holder[name] = holder[name] ?? {};
|
|
2877
|
+
holder = holder[name];
|
|
2878
|
+
}
|
|
2879
|
+
holder[path[path.length - 1] ?? ""] = value;
|
|
2880
|
+
}
|
|
2881
|
+
const OFF = /* @__PURE__ */ new Set([
|
|
2882
|
+
"0",
|
|
2883
|
+
"false",
|
|
2884
|
+
"off",
|
|
2885
|
+
"no"
|
|
2886
|
+
]);
|
|
2887
|
+
/**
|
|
2888
|
+
* toTelemetry decides whether defects are reported. A person's opt out wins over everything,
|
|
2889
|
+
* and a CLI run from a checkout reports only when its developer asks it to.
|
|
2890
|
+
*/
|
|
2891
|
+
function toTelemetry(env = process.env, isCheckout = isRunFromCheckout()) {
|
|
2892
|
+
const dsn = env["HARDFIN_SENTRY_DSN"] || "https://a4819e652e9fa63c0ab67e9febe44565@o1388102.ingest.us.sentry.io/4512176532684800";
|
|
2893
|
+
const base = {
|
|
2894
|
+
dsn,
|
|
2895
|
+
environment: isCheckout ? "local" : version$1.includes("-dev.") ? "preview" : "production",
|
|
2896
|
+
release: toRelease(version$1)
|
|
2897
|
+
};
|
|
2898
|
+
if (env["DO_NOT_TRACK"] && !OFF.has(env["DO_NOT_TRACK"].toLowerCase())) return {
|
|
2899
|
+
...base,
|
|
2900
|
+
enabled: false,
|
|
2901
|
+
from: "DO_NOT_TRACK"
|
|
2902
|
+
};
|
|
2903
|
+
const asked = env["HARDFIN_TELEMETRY"]?.toLowerCase();
|
|
2904
|
+
if (asked !== void 0 && OFF.has(asked)) return {
|
|
2905
|
+
...base,
|
|
2906
|
+
enabled: false,
|
|
2907
|
+
from: "HARDFIN_TELEMETRY"
|
|
2908
|
+
};
|
|
2909
|
+
if (!isDsn(dsn)) return {
|
|
2910
|
+
...base,
|
|
2911
|
+
enabled: false,
|
|
2912
|
+
from: "HARDFIN_SENTRY_DSN is not a valid DSN"
|
|
2913
|
+
};
|
|
2914
|
+
if (asked !== void 0) return {
|
|
2915
|
+
...base,
|
|
2916
|
+
enabled: true,
|
|
2917
|
+
from: "HARDFIN_TELEMETRY"
|
|
2918
|
+
};
|
|
2919
|
+
return isCheckout ? {
|
|
2920
|
+
...base,
|
|
2921
|
+
enabled: false,
|
|
2922
|
+
from: "run from a checkout"
|
|
2923
|
+
} : {
|
|
2924
|
+
...base,
|
|
2925
|
+
enabled: true,
|
|
2926
|
+
from: "default"
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* isRunFromCheckout answers whether this CLI runs from a clone of its repository, as npm link
|
|
2931
|
+
* and node dist/cli.js do. The published package holds dist and nothing beside it.
|
|
2932
|
+
*/
|
|
2933
|
+
function isRunFromCheckout() {
|
|
2934
|
+
return existsSync(fileURLToPath(new URL("../src", import.meta.url)));
|
|
2935
|
+
}
|
|
2936
|
+
/** isDsn answers whether a DSN can be read as the URL of a Sentry project. */
|
|
2937
|
+
function isDsn(dsn) {
|
|
2938
|
+
try {
|
|
2939
|
+
return new URL(dsn).host !== "";
|
|
2940
|
+
} catch {
|
|
2941
|
+
return false;
|
|
2942
|
+
}
|
|
2611
2943
|
}
|
|
2612
2944
|
/**
|
|
2613
2945
|
* toRelease names a version as Sentry knows it. Sentry refuses a slash in a release name, so
|
|
@@ -3178,26 +3510,36 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
3178
3510
|
"logout",
|
|
3179
3511
|
"mcp"
|
|
3180
3512
|
]);
|
|
3181
|
-
/** The global options, which sit before a command and take a value of their own. */
|
|
3182
|
-
const GLOBAL_OPTIONS_WITH_VALUES = new Set(GLOBAL_FLAGS.filter((flag) => flag.valueName).map((flag) => `--${flag.name}`));
|
|
3183
3513
|
/**
|
|
3184
|
-
*
|
|
3185
|
-
*
|
|
3514
|
+
* toParsed asks the CLI's own parser what a call would run, without running it. The server
|
|
3515
|
+
* decides from this answer and never from the words themselves, because a word such as
|
|
3516
|
+
* --help may be a flag's value, and a flag given twice takes its last value.
|
|
3186
3517
|
*/
|
|
3187
|
-
function
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3518
|
+
function toParsed(args, commands) {
|
|
3519
|
+
let ran;
|
|
3520
|
+
const parser = toParser(commands, (parsed) => {
|
|
3521
|
+
ran = parsed;
|
|
3522
|
+
}, true);
|
|
3523
|
+
try {
|
|
3524
|
+
parser.parse(args, { from: "user" });
|
|
3525
|
+
} catch (error) {
|
|
3526
|
+
if (!(error instanceof CommanderError)) throw error;
|
|
3196
3527
|
}
|
|
3528
|
+
return {
|
|
3529
|
+
ran,
|
|
3530
|
+
globals: parser.opts()
|
|
3531
|
+
};
|
|
3197
3532
|
}
|
|
3198
|
-
/**
|
|
3199
|
-
|
|
3200
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* toFixedOption names a global option a call tries to set. Where the CLI calls and what it
|
|
3535
|
+
* calls with are decided where the server is started, or a call could send the credential
|
|
3536
|
+
* to another host.
|
|
3537
|
+
*/
|
|
3538
|
+
function toFixedOption(parsed) {
|
|
3539
|
+
const flag = GLOBAL_FLAGS.find((entry) => parsed.globals[toOptionKey(entry.name)] !== void 0);
|
|
3540
|
+
if (!flag) return;
|
|
3541
|
+
const variable = `HARDFIN_${flag.name.toUpperCase().replaceAll("-", "_")}`;
|
|
3542
|
+
return `--${flag.name} cannot be set in a call, because it is fixed where this server is started. Leave it out, or set ${variable} in the server's environment`;
|
|
3201
3543
|
}
|
|
3202
3544
|
async function toToolResult(request, commands, run) {
|
|
3203
3545
|
const params = request.params;
|
|
@@ -3210,9 +3552,12 @@ async function toToolResult(request, commands, run) {
|
|
|
3210
3552
|
const refused = toRefusedArgument(args);
|
|
3211
3553
|
if (refused) return toError(refused);
|
|
3212
3554
|
const asked = args;
|
|
3213
|
-
const
|
|
3214
|
-
|
|
3215
|
-
if (
|
|
3555
|
+
const parsed = toParsed(asked, commands);
|
|
3556
|
+
const fixed = toFixedOption(parsed);
|
|
3557
|
+
if (fixed) return toError(fixed);
|
|
3558
|
+
const command = parsed.ran?.command.name ?? "";
|
|
3559
|
+
if (REFUSED_COMMANDS.has(command)) return toError(`${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`);
|
|
3560
|
+
if (name === READ_TOOL.name && toAccessOf(parsed) === "write") return toError(`this command changes data, so it runs through ${WRITE_TOOL.name}`);
|
|
3216
3561
|
const outcome = await run(asked, called?.input);
|
|
3217
3562
|
return {
|
|
3218
3563
|
content: [{
|
|
@@ -3235,46 +3580,14 @@ function toError(text) {
|
|
|
3235
3580
|
const LOCAL_READS = /* @__PURE__ */ new Set([
|
|
3236
3581
|
"status",
|
|
3237
3582
|
"config",
|
|
3238
|
-
"agent-guide"
|
|
3239
|
-
"help"
|
|
3583
|
+
"agent-guide"
|
|
3240
3584
|
]);
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
if (toIsAskingForHelp(args)) return "read";
|
|
3248
|
-
let level = commands;
|
|
3249
|
-
let found;
|
|
3250
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3251
|
-
const word = args[index] ?? "";
|
|
3252
|
-
if (GLOBAL_OPTIONS_WITH_VALUES.has(word)) {
|
|
3253
|
-
index += 1;
|
|
3254
|
-
continue;
|
|
3255
|
-
}
|
|
3256
|
-
if (word.startsWith("-")) continue;
|
|
3257
|
-
const next = level.find((entry) => entry.name === word);
|
|
3258
|
-
if (!next) break;
|
|
3259
|
-
found = next;
|
|
3260
|
-
level = next.subcommands ?? [];
|
|
3261
|
-
if (level.length === 0) break;
|
|
3262
|
-
}
|
|
3263
|
-
if (!found) return "read";
|
|
3264
|
-
if (found.endpoint) return found.endpoint.method === "GET" ? "read" : "write";
|
|
3265
|
-
if (found.name === "api") return toApiMethod(args) === "GET" ? "read" : "write";
|
|
3266
|
-
if (LOCAL_READS.has(found.name) || (found.subcommands?.length ?? 0) > 0) return "read";
|
|
3267
|
-
return "write";
|
|
3268
|
-
}
|
|
3269
|
-
/** toApiMethod reads the method hardfin api was given, which is GET when none was. */
|
|
3270
|
-
function toApiMethod(args) {
|
|
3271
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3272
|
-
const word = args[index] ?? "";
|
|
3273
|
-
if (word === "-X" || word === "--method") return (args[index + 1] ?? "GET").toUpperCase();
|
|
3274
|
-
if (word.startsWith("--method=")) return word.slice(9).toUpperCase();
|
|
3275
|
-
if (word.startsWith("-X") && word.length > 2) return word.slice(2).toUpperCase();
|
|
3276
|
-
}
|
|
3277
|
-
return "GET";
|
|
3585
|
+
function toAccessOf(parsed) {
|
|
3586
|
+
if (!parsed.ran) return "read";
|
|
3587
|
+
const { command, flags } = parsed.ran;
|
|
3588
|
+
if (command.endpoint) return command.endpoint.method === "GET" ? "read" : "write";
|
|
3589
|
+
if (command.name === "api") return toText(flags["method"] ?? "GET").toUpperCase() === "GET" ? "read" : "write";
|
|
3590
|
+
return LOCAL_READS.has(command.name) ? "read" : "write";
|
|
3278
3591
|
}
|
|
3279
3592
|
/** The most one argument can hold, which Linux refuses past 128 KiB. */
|
|
3280
3593
|
const ARGUMENT_BYTES = 131071;
|
|
@@ -3376,363 +3689,92 @@ async function serve(commands, run = toCliRunner()) {
|
|
|
3376
3689
|
}
|
|
3377
3690
|
await Promise.all(answering);
|
|
3378
3691
|
}
|
|
3379
|
-
/** toAnswer replies to one line, and never rejects, because a rejection ends the server. */
|
|
3380
|
-
async function toAnswer(line, commands, run) {
|
|
3381
|
-
let response;
|
|
3382
|
-
let failure;
|
|
3383
|
-
try {
|
|
3384
|
-
response = await toResponse(toRequest(line), commands, run);
|
|
3385
|
-
} catch (error) {
|
|
3386
|
-
response = error instanceof ProtocolFailure ? toErrorResponse(error.id, error.code, error.message) : toErrorResponse(toRequestId(line), -32603, error instanceof Error ? error.message : String(error));
|
|
3387
|
-
failure = error instanceof ProtocolFailure ? void 0 : error;
|
|
3388
|
-
}
|
|
3389
|
-
if (response) write(`${JSON.stringify(response)}\n`);
|
|
3390
|
-
if (failure !== void 0) await report(failure, {
|
|
3391
|
-
level: "error",
|
|
3392
|
-
mechanism: "mcp.request",
|
|
3393
|
-
mode: "mcp",
|
|
3394
|
-
invocation: {
|
|
3395
|
-
command: "mcp",
|
|
3396
|
-
flags: []
|
|
3397
|
-
}
|
|
3398
|
-
});
|
|
3399
|
-
}
|
|
3400
|
-
/** toRequest reads one line, refusing what the protocol does not allow to be a request. */
|
|
3401
|
-
function toRequest(line) {
|
|
3402
|
-
let parsed;
|
|
3403
|
-
try {
|
|
3404
|
-
parsed = JSON.parse(line);
|
|
3405
|
-
} catch {
|
|
3406
|
-
throw new ProtocolFailure(-32700, "this line is not JSON", null);
|
|
3407
|
-
}
|
|
3408
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new ProtocolFailure(-32600, "a request is a JSON object, and this server takes one per line", null);
|
|
3409
|
-
const request = parsed;
|
|
3410
|
-
if (typeof request.method !== "string") throw new ProtocolFailure(-32600, "a request names a method", toAllowedId(request.id));
|
|
3411
|
-
if (request.id !== void 0 && request.id !== null && typeof request.id !== "string" && typeof request.id !== "number") throw new ProtocolFailure(-32600, "an id is a string, a number, or null", null);
|
|
3412
|
-
return request;
|
|
3413
|
-
}
|
|
3414
|
-
function toAllowedId(id) {
|
|
3415
|
-
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
3416
|
-
}
|
|
3417
|
-
function toErrorResponse(id, code, message) {
|
|
3418
|
-
return {
|
|
3419
|
-
jsonrpc: "2.0",
|
|
3420
|
-
id,
|
|
3421
|
-
error: {
|
|
3422
|
-
code,
|
|
3423
|
-
message
|
|
3424
|
-
}
|
|
3425
|
-
};
|
|
3426
|
-
}
|
|
3427
|
-
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
3428
|
-
function toRequestId(line) {
|
|
3429
|
-
try {
|
|
3430
|
-
return toAllowedId(JSON.parse(line).id);
|
|
3431
|
-
} catch {
|
|
3432
|
-
return null;
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
/** write puts a reply on stdout, and stops quietly when the client has gone. */
|
|
3436
|
-
function write(text) {
|
|
3437
|
-
try {
|
|
3438
|
-
process.stdout.write(text);
|
|
3439
|
-
} catch {}
|
|
3440
|
-
}
|
|
3441
|
-
//#endregion
|
|
3442
|
-
//#region src/command/mcp.ts
|
|
3443
|
-
const mcpCommand = defineCommand({
|
|
3444
|
-
name: "mcp",
|
|
3445
|
-
summary: "Serve this CLI to an agent over the Model Context Protocol",
|
|
3446
|
-
description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
|
|
3447
|
-
arguments: [],
|
|
3448
|
-
flags: [{
|
|
3449
|
-
name: "json",
|
|
3450
|
-
description: "Accepted for consistency, and ignored, because the protocol decides the output",
|
|
3451
|
-
schema: z.boolean()
|
|
3452
|
-
}],
|
|
3453
|
-
examples: [{
|
|
3454
|
-
description: "Register with an agent",
|
|
3455
|
-
command: "hardfin mcp"
|
|
3456
|
-
}, {
|
|
3457
|
-
description: "Add it to Claude Code",
|
|
3458
|
-
command: "claude mcp add hardfin -- hardfin mcp"
|
|
3459
|
-
}],
|
|
3460
|
-
run: runMcp
|
|
3461
|
-
});
|
|
3462
|
-
async function runMcp(input) {
|
|
3463
|
-
await serve(input.commands);
|
|
3464
|
-
return ExitCode.OK;
|
|
3465
|
-
}
|
|
3466
|
-
//#endregion
|
|
3467
|
-
//#region src/command/operation.ts
|
|
3468
|
-
/**
|
|
3469
|
-
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
3470
|
-
* lists, which is what a response always uses.
|
|
3471
|
-
*/
|
|
3472
|
-
function toEnum(values) {
|
|
3473
|
-
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
3474
|
-
}
|
|
3475
|
-
const UNSET_FLAG = {
|
|
3476
|
-
name: "unset",
|
|
3477
|
-
description: "A field to clear, named as its flag is, repeatable",
|
|
3478
|
-
valueName: "field",
|
|
3479
|
-
repeatable: true,
|
|
3480
|
-
schema: z.array(z.string())
|
|
3481
|
-
};
|
|
3482
|
-
const INPUT_FLAG = {
|
|
3483
|
-
name: "input",
|
|
3484
|
-
description: "A file holding the JSON request body, or - for standard input. A flag given as well replaces its field",
|
|
3485
|
-
valueName: "path",
|
|
3486
|
-
schema: z.string()
|
|
3487
|
-
};
|
|
3488
|
-
const FILE_FLAG = {
|
|
3489
|
-
name: "file",
|
|
3490
|
-
description: "The file to upload",
|
|
3491
|
-
valueName: "path",
|
|
3492
|
-
schema: z.string()
|
|
3493
|
-
};
|
|
3494
|
-
const OUTPUT_FLAG = {
|
|
3495
|
-
name: "output",
|
|
3496
|
-
description: "Where to write the file, or - for standard output",
|
|
3497
|
-
valueName: "path",
|
|
3498
|
-
schema: z.string()
|
|
3499
|
-
};
|
|
3500
|
-
const JSON_FLAG = {
|
|
3501
|
-
name: "json",
|
|
3502
|
-
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
3503
|
-
schema: z.boolean()
|
|
3504
|
-
};
|
|
3505
|
-
/** defineOperation turns one endpoint into the command that calls it. */
|
|
3506
|
-
function defineOperation(operation) {
|
|
3507
|
-
const flags = [
|
|
3508
|
-
...operation.queryFlags,
|
|
3509
|
-
...operation.bodyFlags,
|
|
3510
|
-
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
3511
|
-
...operation.bodyFlags.length > 0 ? [INPUT_FLAG] : [],
|
|
3512
|
-
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
3513
|
-
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
3514
|
-
JSON_FLAG
|
|
3515
|
-
];
|
|
3516
|
-
return {
|
|
3517
|
-
name: operation.name,
|
|
3518
|
-
summary: operation.summary,
|
|
3519
|
-
description: operation.description ?? operation.summary,
|
|
3520
|
-
arguments: operation.pathParameters,
|
|
3521
|
-
flags,
|
|
3522
|
-
examples: operation.example ? [{
|
|
3523
|
-
description: operation.summary,
|
|
3524
|
-
command: operation.example
|
|
3525
|
-
}] : [],
|
|
3526
|
-
endpoint: {
|
|
3527
|
-
method: operation.method,
|
|
3528
|
-
path: operation.path
|
|
3529
|
-
},
|
|
3530
|
-
run: (input) => runOperation(operation, input)
|
|
3531
|
-
};
|
|
3532
|
-
}
|
|
3533
|
-
async function runOperation(operation, input) {
|
|
3534
|
-
const path = toPath(operation, input.args);
|
|
3535
|
-
if (path === void 0) {
|
|
3536
|
-
writeFailure("invalid_arguments", `this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
3537
|
-
return ExitCode.USAGE;
|
|
3538
|
-
}
|
|
3539
|
-
const given = toGivenBody(input.flags["input"]);
|
|
3540
|
-
if (given instanceof InputFailure) {
|
|
3541
|
-
writeFailure("invalid_input", given.message, input.isJSON);
|
|
3542
|
-
return ExitCode.USAGE;
|
|
3543
|
-
}
|
|
3544
|
-
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0 && !hasPath(given, flag.jsonPath));
|
|
3545
|
-
if (missing.length > 0) {
|
|
3546
|
-
writeFailure("missing_flag", `this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
3547
|
-
return ExitCode.USAGE;
|
|
3548
|
-
}
|
|
3549
|
-
const body = toBody(operation, input.flags, given);
|
|
3550
|
-
if (body instanceof Error) {
|
|
3551
|
-
writeFailure("invalid_flag", body.message, input.isJSON);
|
|
3552
|
-
return ExitCode.USAGE;
|
|
3553
|
-
}
|
|
3554
|
-
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
3555
|
-
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);
|
|
3556
|
-
return ExitCode.USAGE;
|
|
3557
|
-
}
|
|
3558
|
-
let form;
|
|
3559
|
-
if (operation.upload) {
|
|
3560
|
-
const built = await toForm(operation.upload, input.flags);
|
|
3561
|
-
if (built instanceof Error) {
|
|
3562
|
-
writeFailure("invalid_input", built.message, input.isJSON);
|
|
3563
|
-
return ExitCode.USAGE;
|
|
3564
|
-
}
|
|
3565
|
-
form = built;
|
|
3566
|
-
}
|
|
3692
|
+
/** toAnswer replies to one line, and never rejects, because a rejection ends the server. */
|
|
3693
|
+
async function toAnswer(line, commands, run) {
|
|
3694
|
+
let response;
|
|
3695
|
+
let failure;
|
|
3567
3696
|
try {
|
|
3568
|
-
|
|
3569
|
-
apiUrl: input.resolved.settings.apiUrl,
|
|
3570
|
-
credential: await toRequestCredential(input.resolved.settings, input.resolved.sources),
|
|
3571
|
-
method: operation.method,
|
|
3572
|
-
path,
|
|
3573
|
-
query: toQuery(operation, input.flags),
|
|
3574
|
-
body,
|
|
3575
|
-
form,
|
|
3576
|
-
downloads: operation.downloads
|
|
3577
|
-
});
|
|
3578
|
-
if (operation.downloads) return toWritten(envelope.data, input);
|
|
3579
|
-
writeData(envelope.data);
|
|
3580
|
-
return ExitCode.OK;
|
|
3697
|
+
response = await toResponse(toRequest(line), commands, run);
|
|
3581
3698
|
} catch (error) {
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
return ExitCode.NOT_AUTHENTICATED;
|
|
3585
|
-
}
|
|
3586
|
-
if (error instanceof RequestFailure) {
|
|
3587
|
-
writeFailure(toStatusCode(error.status), error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
3588
|
-
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
3589
|
-
}
|
|
3590
|
-
throw error;
|
|
3699
|
+
response = error instanceof ProtocolFailure ? toErrorResponse(error.id, error.code, error.message) : toErrorResponse(toRequestId(line), -32603, error instanceof Error ? error.message : String(error));
|
|
3700
|
+
failure = error instanceof ProtocolFailure ? void 0 : error;
|
|
3591
3701
|
}
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
if (path === "-") {
|
|
3601
|
-
if (process.stdout.isTTY) {
|
|
3602
|
-
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);
|
|
3603
|
-
return ExitCode.USAGE;
|
|
3702
|
+
if (response) write(`${JSON.stringify(response)}\n`);
|
|
3703
|
+
if (failure !== void 0) await report(failure, {
|
|
3704
|
+
level: "error",
|
|
3705
|
+
mechanism: "mcp.request",
|
|
3706
|
+
mode: "mcp",
|
|
3707
|
+
invocation: {
|
|
3708
|
+
command: "mcp",
|
|
3709
|
+
flags: []
|
|
3604
3710
|
}
|
|
3605
|
-
process.stdout.write(download.bytes);
|
|
3606
|
-
return ExitCode.OK;
|
|
3607
|
-
}
|
|
3608
|
-
writeFileSync(path, download.bytes);
|
|
3609
|
-
writeData({
|
|
3610
|
-
written: path,
|
|
3611
|
-
bytes: download.bytes.length,
|
|
3612
|
-
contentType: download.contentType
|
|
3613
3711
|
});
|
|
3614
|
-
return ExitCode.OK;
|
|
3615
|
-
}
|
|
3616
|
-
/** toPath fills the path template from the positional arguments, in order. */
|
|
3617
|
-
function toPath(operation, args) {
|
|
3618
|
-
if (args.length !== operation.pathParameters.length) return;
|
|
3619
|
-
let path = operation.path;
|
|
3620
|
-
for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
|
|
3621
|
-
return path;
|
|
3622
|
-
}
|
|
3623
|
-
/** toQuery carries only the flags this invocation actually set. */
|
|
3624
|
-
function toQuery(operation, flags) {
|
|
3625
|
-
const query = new URLSearchParams();
|
|
3626
|
-
for (const flag of operation.queryFlags) {
|
|
3627
|
-
const value = flags[toOptionKey(flag.name)];
|
|
3628
|
-
if (value === void 0) continue;
|
|
3629
|
-
const parsed = flag.schema.safeParse(value);
|
|
3630
|
-
const carried = parsed.success ? parsed.data : value;
|
|
3631
|
-
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
3632
|
-
}
|
|
3633
|
-
return query;
|
|
3634
|
-
}
|
|
3635
|
-
/** toOptionKey names the parsed flag, which the parser reports in camel case. */
|
|
3636
|
-
function toOptionKey(name) {
|
|
3637
|
-
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
3638
3712
|
}
|
|
3639
|
-
/**
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
3643
|
-
const form = new FormData();
|
|
3713
|
+
/** toRequest reads one line, refusing what the protocol does not allow to be a request. */
|
|
3714
|
+
function toRequest(line) {
|
|
3715
|
+
let parsed;
|
|
3644
3716
|
try {
|
|
3645
|
-
|
|
3717
|
+
parsed = JSON.parse(line);
|
|
3646
3718
|
} catch {
|
|
3647
|
-
|
|
3648
|
-
}
|
|
3649
|
-
for (const field of upload.fields) {
|
|
3650
|
-
const value = flags[toOptionKey(field.name)];
|
|
3651
|
-
if (value !== void 0) {
|
|
3652
|
-
const parsed = field.schema.safeParse(value);
|
|
3653
|
-
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
3654
|
-
}
|
|
3719
|
+
throw new ProtocolFailure(-32700, "this line is not JSON", null);
|
|
3655
3720
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const read = toJSONInput(source);
|
|
3662
|
-
if (read instanceof InputFailure) return read;
|
|
3663
|
-
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"}`);
|
|
3664
|
-
return read;
|
|
3721
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new ProtocolFailure(-32600, "a request is a JSON object, and this server takes one per line", null);
|
|
3722
|
+
const request = parsed;
|
|
3723
|
+
if (typeof request.method !== "string") throw new ProtocolFailure(-32600, "a request names a method", toAllowedId(request.id));
|
|
3724
|
+
if (request.id !== void 0 && request.id !== null && typeof request.id !== "string" && typeof request.id !== "number") throw new ProtocolFailure(-32600, "an id is a string, a number, or null", null);
|
|
3725
|
+
return request;
|
|
3665
3726
|
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
let holder = body;
|
|
3669
|
-
for (const name of path) {
|
|
3670
|
-
if (typeof holder !== "object" || holder === null || !(name in holder)) return false;
|
|
3671
|
-
holder = holder[name];
|
|
3672
|
-
}
|
|
3673
|
-
return true;
|
|
3727
|
+
function toAllowedId(id) {
|
|
3728
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
3674
3729
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
if (flag.element) {
|
|
3683
|
-
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
3684
|
-
if (elements instanceof Error) return elements;
|
|
3685
|
-
set(body, flag.jsonPath, elements);
|
|
3686
|
-
hasField = true;
|
|
3687
|
-
continue;
|
|
3730
|
+
function toErrorResponse(id, code, message) {
|
|
3731
|
+
return {
|
|
3732
|
+
jsonrpc: "2.0",
|
|
3733
|
+
id,
|
|
3734
|
+
error: {
|
|
3735
|
+
code,
|
|
3736
|
+
message
|
|
3688
3737
|
}
|
|
3689
|
-
|
|
3690
|
-
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
3691
|
-
hasField = true;
|
|
3692
|
-
}
|
|
3693
|
-
const cleared = toCleared(operation, flags, body);
|
|
3694
|
-
if (cleared instanceof Error) return cleared;
|
|
3695
|
-
return hasField || cleared ? body : void 0;
|
|
3738
|
+
};
|
|
3696
3739
|
}
|
|
3697
|
-
/**
|
|
3698
|
-
function
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
3704
|
-
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
3705
|
-
set(body, flag.jsonPath, null);
|
|
3740
|
+
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
3741
|
+
function toRequestId(line) {
|
|
3742
|
+
try {
|
|
3743
|
+
return toAllowedId(JSON.parse(line).id);
|
|
3744
|
+
} catch {
|
|
3745
|
+
return null;
|
|
3706
3746
|
}
|
|
3707
|
-
return names.length > 0;
|
|
3708
3747
|
}
|
|
3709
|
-
/**
|
|
3710
|
-
function
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
for (const pair of value.split(",")) {
|
|
3715
|
-
const split = pair.indexOf("=");
|
|
3716
|
-
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`);
|
|
3717
|
-
const key = pair.slice(0, split).trim();
|
|
3718
|
-
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
3719
|
-
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
3720
|
-
const value = pair.slice(split + 1).trim();
|
|
3721
|
-
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`);
|
|
3722
|
-
element[key] = value;
|
|
3723
|
-
}
|
|
3724
|
-
elements.push(element);
|
|
3725
|
-
}
|
|
3726
|
-
return elements;
|
|
3748
|
+
/** write puts a reply on stdout, and stops quietly when the client has gone. */
|
|
3749
|
+
function write(text) {
|
|
3750
|
+
try {
|
|
3751
|
+
process.stdout.write(text);
|
|
3752
|
+
} catch {}
|
|
3727
3753
|
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3754
|
+
//#endregion
|
|
3755
|
+
//#region src/command/mcp.ts
|
|
3756
|
+
const mcpCommand = defineCommand({
|
|
3757
|
+
name: "mcp",
|
|
3758
|
+
summary: "Serve this CLI to an agent over the Model Context Protocol",
|
|
3759
|
+
description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
|
|
3760
|
+
arguments: [],
|
|
3761
|
+
flags: [{
|
|
3762
|
+
name: "json",
|
|
3763
|
+
description: "Accepted for consistency, and ignored, because the protocol decides the output",
|
|
3764
|
+
schema: z.boolean()
|
|
3765
|
+
}],
|
|
3766
|
+
examples: [{
|
|
3767
|
+
description: "Register with an agent",
|
|
3768
|
+
command: "hardfin mcp"
|
|
3769
|
+
}, {
|
|
3770
|
+
description: "Add it to Claude Code",
|
|
3771
|
+
command: "claude mcp add hardfin -- hardfin mcp"
|
|
3772
|
+
}],
|
|
3773
|
+
run: runMcp
|
|
3774
|
+
});
|
|
3775
|
+
async function runMcp(input) {
|
|
3776
|
+
await serve(input.commands);
|
|
3777
|
+
return ExitCode.OK;
|
|
3736
3778
|
}
|
|
3737
3779
|
//#endregion
|
|
3738
3780
|
//#region src/command/commands.ts
|
|
@@ -6428,43 +6470,9 @@ function toRejectedFlag(command, flags) {
|
|
|
6428
6470
|
//#region src/cli/program.ts
|
|
6429
6471
|
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
6430
6472
|
function toCli(entries = commands) {
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
toQuietParser(program);
|
|
6434
|
-
for (const flag of GLOBAL_FLAGS) program.option(`--${flag.name} <${flag.valueName}>`, flag.description);
|
|
6435
|
-
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
6436
|
-
return program;
|
|
6437
|
-
}
|
|
6438
|
-
/** toProgram wires one registry command into the parser. */
|
|
6439
|
-
function toProgram(root, command) {
|
|
6440
|
-
const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
|
|
6441
|
-
toQuietParser(program);
|
|
6442
|
-
for (const argument of command.arguments) {
|
|
6443
|
-
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
6444
|
-
program.argument(argument.required ? `<${name}>` : `[${name}]`, argument.description);
|
|
6445
|
-
}
|
|
6446
|
-
for (const flag of command.flags) {
|
|
6447
|
-
const short = flag.short ? `-${flag.short}, ` : "";
|
|
6448
|
-
const value = flag.valueName ? ` <${flag.valueName}>` : "";
|
|
6449
|
-
const option = new Option(`${short}--${flag.name}${value}`, flag.description);
|
|
6450
|
-
if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
|
|
6451
|
-
if (flag.repeatable) option.argParser(collect);
|
|
6452
|
-
if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
|
|
6453
|
-
program.addOption(option);
|
|
6454
|
-
}
|
|
6455
|
-
for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
|
|
6456
|
-
for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(root, subcommand), { hidden: subcommand.hidden });
|
|
6457
|
-
if (!command.run) return program;
|
|
6458
|
-
program.action(async (...parsed) => {
|
|
6459
|
-
const flags = parsed[parsed.length - 2] ?? {};
|
|
6460
|
-
const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
|
|
6461
|
-
process.exitCode = await toExitCode(root, command, args, flags);
|
|
6473
|
+
return toParser(entries, async ({ command, args, flags, globals }) => {
|
|
6474
|
+
process.exitCode = await toExitCode(globals, command, args, flags);
|
|
6462
6475
|
});
|
|
6463
|
-
return program;
|
|
6464
|
-
}
|
|
6465
|
-
/** toQuietParser has the parser hand back its mistakes, so they are answered like any other failure. */
|
|
6466
|
-
function toQuietParser(program) {
|
|
6467
|
-
program.exitOverride().configureOutput({ outputError: () => void 0 });
|
|
6468
6476
|
}
|
|
6469
6477
|
/** The parser's codes for a run that only showed help or the version, which is not a failure. */
|
|
6470
6478
|
const SHOWN = /* @__PURE__ */ new Set([
|
|
@@ -6492,7 +6500,7 @@ function toParseCode(code) {
|
|
|
6492
6500
|
default: return "invalid_arguments";
|
|
6493
6501
|
}
|
|
6494
6502
|
}
|
|
6495
|
-
async function toExitCode(
|
|
6503
|
+
async function toExitCode(globals, command, args, flags) {
|
|
6496
6504
|
const isJSON = isJSONOutput(flags);
|
|
6497
6505
|
const rejected = toRejectedFlag(command, flags);
|
|
6498
6506
|
if (rejected) {
|
|
@@ -6502,9 +6510,9 @@ async function toExitCode(root, command, args, flags) {
|
|
|
6502
6510
|
let resolved;
|
|
6503
6511
|
try {
|
|
6504
6512
|
resolved = toSettings({
|
|
6505
|
-
apiUrl:
|
|
6506
|
-
apiKey:
|
|
6507
|
-
issuerUrl:
|
|
6513
|
+
apiUrl: globals["apiUrl"],
|
|
6514
|
+
apiKey: globals["apiKey"],
|
|
6515
|
+
issuerUrl: globals["issuerUrl"]
|
|
6508
6516
|
});
|
|
6509
6517
|
return await command.run?.({
|
|
6510
6518
|
args,
|
|
@@ -6523,13 +6531,6 @@ async function toExitCode(root, command, args, flags) {
|
|
|
6523
6531
|
return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
|
|
6524
6532
|
}
|
|
6525
6533
|
}
|
|
6526
|
-
function toArgumentList(value) {
|
|
6527
|
-
if (Array.isArray(value)) return value.map(String);
|
|
6528
|
-
return value === void 0 ? [] : [toText(value)];
|
|
6529
|
-
}
|
|
6530
|
-
function collect(value, previous) {
|
|
6531
|
-
return [...previous ?? [], value];
|
|
6532
|
-
}
|
|
6533
6534
|
/** toReported reports a defect, and tells a person at a terminal that it was reported. */
|
|
6534
6535
|
async function toReported(error, isJSON, resolved, level = "error") {
|
|
6535
6536
|
const invocation = toInvocation(commands, process.argv.slice(2));
|
|
@@ -6545,5 +6546,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6545
6546
|
//#endregion
|
|
6546
6547
|
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6547
6548
|
|
|
6548
|
-
//# sourceMappingURL=program-
|
|
6549
|
-
//# debugId=
|
|
6549
|
+
//# sourceMappingURL=program-C38xnnim.js.map
|
|
6550
|
+
//# debugId=23617f68-b6d8-5249-bca7-f5585fdb94f2
|