@hardfin/cli 0.0.2-dev.15 → 0.0.2-dev.17
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 +91 -66
- package/dist/cli.js +152 -45
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -26,37 +26,37 @@ npm install -g @hardfin/cli@dev
|
|
|
26
26
|
|
|
27
27
|
Most commands are generated from the published API document rather than written by hand.
|
|
28
28
|
|
|
29
|
-
| Piece
|
|
30
|
-
|
|
|
31
|
-
| `hardfin api`
|
|
32
|
-
| `src/command/surface.generated.ts` | Every endpoint as a command, rewritten by the generator
|
|
33
|
-
| `surface-overrides.json`
|
|
34
|
-
| `spec/core.openapi.yaml`
|
|
35
|
-
| `scripts/generate-surface.mjs`
|
|
29
|
+
| Piece | Holds |
|
|
30
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `hardfin api` | A call to any endpoint, written by hand, and the escape hatch when no generated command fits |
|
|
32
|
+
| `src/command/surface.generated.ts` | Every endpoint as a command, rewritten by the generator |
|
|
33
|
+
| `surface-overrides.json` | The operations whose generated name is wrong |
|
|
34
|
+
| `spec/core.openapi.yaml` | The API document the generator reads, vendored here |
|
|
35
|
+
| `scripts/generate-surface.mjs` | The generator |
|
|
36
36
|
|
|
37
37
|
### How an endpoint becomes a command
|
|
38
38
|
|
|
39
39
|
The first path segment is the noun. Every later literal segment nests under it. The method
|
|
40
40
|
and the shape of the last segment decide the verb.
|
|
41
41
|
|
|
42
|
-
| Endpoint
|
|
43
|
-
|
|
|
44
|
-
| `GET /asset`
|
|
45
|
-
| `POST /asset`
|
|
46
|
-
| `GET /asset/{assetKey}`
|
|
47
|
-
| `PATCH /asset/{assetKey}`
|
|
42
|
+
| Endpoint | Command |
|
|
43
|
+
| ------------------------------------ | -------------------------------------------- |
|
|
44
|
+
| `GET /asset` | `hardfin asset list` |
|
|
45
|
+
| `POST /asset` | `hardfin asset create` |
|
|
46
|
+
| `GET /asset/{assetKey}` | `hardfin asset get <assetKey>` |
|
|
47
|
+
| `PATCH /asset/{assetKey}` | `hardfin asset update <assetKey>` |
|
|
48
48
|
| `PATCH /asset/{assetKey}/accounting` | `hardfin asset accounting update <assetKey>` |
|
|
49
49
|
|
|
50
50
|
Everything an endpoint takes becomes a flag or an argument. No command asks for JSON.
|
|
51
51
|
|
|
52
|
-
| The document says
|
|
53
|
-
|
|
|
54
|
-
| A path parameter
|
|
55
|
-
| A query or body field
|
|
56
|
-
| A field inside a nested object | `--address-city`, the path joined
|
|
57
|
-
| An array of values
|
|
58
|
-
| An array of objects
|
|
59
|
-
| A file upload
|
|
52
|
+
| The document says | You type |
|
|
53
|
+
| ------------------------------ | ---------------------------------------------------------------------- |
|
|
54
|
+
| A path parameter | a positional argument |
|
|
55
|
+
| A query or body field | `--field-name`, in kebab case |
|
|
56
|
+
| A field inside a nested object | `--address-city`, the path joined |
|
|
57
|
+
| An array of values | the flag, repeated |
|
|
58
|
+
| An array of objects | the flag, repeated, each carrying that element's fields as `key=value` |
|
|
59
|
+
| A file upload | `--file <path>`, sent as multipart |
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
62
|
hardfin asset ownership create ast_4f9xk2mq7plr8stz \
|
|
@@ -71,6 +71,13 @@ A value is sent as the type the document names. `--useful-life 36` sends the num
|
|
|
71
71
|
decimal such as `--salvage-value 1500.00` stays a string, which is how the API takes money
|
|
72
72
|
without losing precision.
|
|
73
73
|
|
|
74
|
+
A download is written where you name it, and a terminal is never filled with a file's bytes.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
hardfin file get file_7hq2mx9pkr4stz8w --output photo.jpg
|
|
78
|
+
hardfin file get file_7hq2mx9pkr4stz8w --output - | wc -c
|
|
79
|
+
```
|
|
80
|
+
|
|
74
81
|
Clear a field with `--unset`, naming the flag:
|
|
75
82
|
|
|
76
83
|
```sh
|
|
@@ -118,10 +125,10 @@ what the api-spec bridge publishes. Commit the document and the generated comman
|
|
|
118
125
|
|
|
119
126
|
Two checks keep the pair honest.
|
|
120
127
|
|
|
121
|
-
| Check
|
|
122
|
-
|
|
|
128
|
+
| Check | Refuses |
|
|
129
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
123
130
|
| The generator | a document whose `info.version` is not a date, which means an earlier release |
|
|
124
|
-
| CI
|
|
131
|
+
| CI | a vendored document that was updated without regenerating the commands |
|
|
125
132
|
|
|
126
133
|
The first one matters because `hardfinhq/api-spec` can sit a release behind the monorepo
|
|
127
134
|
while its bridge pull request is open. Generating from that document would replace the
|
|
@@ -151,9 +158,9 @@ so CI and an agent sandbox need no browser.
|
|
|
151
158
|
|
|
152
159
|
While it waits, the terminal takes two things.
|
|
153
160
|
|
|
154
|
-
| Key
|
|
155
|
-
|
|
|
156
|
-
| `c`
|
|
161
|
+
| Key | Does |
|
|
162
|
+
| -------------------------- | ----------------------------------------- |
|
|
163
|
+
| `c` | Copies the URL to the clipboard |
|
|
157
164
|
| A pasted value, then enter | Finishes the sign in without the listener |
|
|
158
165
|
|
|
159
166
|
Paste whichever of these you have: the whole redirect URL from the browser's address bar,
|
|
@@ -190,9 +197,9 @@ nothing about whether the credential is good.
|
|
|
190
197
|
|
|
191
198
|
### Where the refresh token is kept
|
|
192
199
|
|
|
193
|
-
| Host
|
|
194
|
-
|
|
|
195
|
-
| macOS, Windows, and Linux with a secret service
|
|
200
|
+
| Host | Kept in |
|
|
201
|
+
| --------------------------------------------------------------- | ----------------------------------------------------- |
|
|
202
|
+
| macOS, Windows, and Linux with a secret service | the OS keyring |
|
|
196
203
|
| Everything else, including WSL, containers, and agent sandboxes | `$XDG_STATE_HOME/hardfin/credentials.json`, mode 0600 |
|
|
197
204
|
|
|
198
205
|
`HARDFIN_NO_BROWSER=1` keeps `hardfin login` from opening a tab, which is what a script or a
|
|
@@ -210,10 +217,10 @@ production never share a sign in.
|
|
|
210
217
|
Nothing. Hardfin registers this CLI as a first-party client, and its key is the same in every
|
|
211
218
|
environment, so `hardfin login` works out of the box.
|
|
212
219
|
|
|
213
|
-
| Setting
|
|
214
|
-
|
|
|
215
|
-
| `clientId`, or `HARDFIN_CLIENT_ID`
|
|
216
|
-
| `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server
|
|
220
|
+
| Setting | Holds | Default |
|
|
221
|
+
| ------------------------------------ | ----------------------------------- | ----------------------------- |
|
|
222
|
+
| `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
|
|
223
|
+
| `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
|
|
217
224
|
|
|
218
225
|
## Completing commands in your shell
|
|
219
226
|
|
|
@@ -244,11 +251,11 @@ claude mcp add hardfin -- hardfin mcp
|
|
|
244
251
|
Every tool a server lists sits in the agent's context for the entire session, so this server
|
|
245
252
|
offers one.
|
|
246
253
|
|
|
247
|
-
| Offered as
|
|
248
|
-
|
|
|
249
|
-
| The `hardfin` tool
|
|
250
|
-
| `hardfin://guide`
|
|
251
|
-
| `hardfin://commands` | The command tree as JSON
|
|
254
|
+
| Offered as | Holds | Costs context |
|
|
255
|
+
| -------------------- | ---------------------------------------------------------------- | --------------------------------- |
|
|
256
|
+
| The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
|
|
257
|
+
| `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
|
|
258
|
+
| `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
|
|
252
259
|
|
|
253
260
|
Fifty tools would describe the same surface and crowd out the work. An agent that needs the
|
|
254
261
|
list reads a resource, or runs `["--help"]`.
|
|
@@ -272,14 +279,14 @@ hardfin status --json # for a support request
|
|
|
272
279
|
hardfin status --offline # no network calls at all
|
|
273
280
|
```
|
|
274
281
|
|
|
275
|
-
| Section
|
|
276
|
-
|
|
|
277
|
-
| `cli`
|
|
278
|
-
| `host`
|
|
279
|
-
| `configuration`
|
|
280
|
-
| `credential`
|
|
281
|
-
| `authorizationServer` | Whether discovery worked, and every endpoint it named
|
|
282
|
-
| `api`
|
|
282
|
+
| Section | Holds |
|
|
283
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
284
|
+
| `cli` | The version, the API version it speaks, Node, and whether a terminal is attached |
|
|
285
|
+
| `host` | The operating system, kernel, build, processor, memory, timezone, shell, and the distribution and WSL details on Linux |
|
|
286
|
+
| `configuration` | Every setting, its value, and the layer it came from |
|
|
287
|
+
| `credential` | Whether you are signed in, where the refresh token is kept, and when it was issued and last renewed |
|
|
288
|
+
| `authorizationServer` | Whether discovery worked, and every endpoint it named |
|
|
289
|
+
| `api` | Whether the API accepted the credential, when the access token expires, and its scopes |
|
|
283
290
|
|
|
284
291
|
Nothing secret is printed. An API key and a refresh token are each reported as a
|
|
285
292
|
`sha256:` fingerprint, which identifies a credential across two machines without disclosing
|
|
@@ -294,13 +301,13 @@ says which of the two you are looking at.
|
|
|
294
301
|
A local build reaches a local server without editing code. Four layers supply the same
|
|
295
302
|
settings, and the one nearest the top wins.
|
|
296
303
|
|
|
297
|
-
| Layer
|
|
298
|
-
|
|
|
299
|
-
| Flag
|
|
300
|
-
| Environment | an exported `HARDFIN_*` variable
|
|
301
|
-
| Env file
|
|
302
|
-
| Config file | `config.local.json` in the working directory
|
|
303
|
-
| Default
|
|
304
|
+
| Layer | Where | Beats |
|
|
305
|
+
| ----------- | --------------------------------------------------------------------- | ---------------- |
|
|
306
|
+
| Flag | `--api-url` | everything below |
|
|
307
|
+
| Environment | an exported `HARDFIN_*` variable | the files below |
|
|
308
|
+
| Env file | `.env` in the working directory, or the file `HARDFIN_ENV_FILE` names | the config file |
|
|
309
|
+
| Config file | `config.local.json` in the working directory | the defaults |
|
|
310
|
+
| Default | the published API | nothing |
|
|
304
311
|
|
|
305
312
|
An exported variable beats `.env` because Node leaves a variable that is already set alone.
|
|
306
313
|
|
|
@@ -336,6 +343,24 @@ The CLI reads whatever `config.local.json` and `.env` sit in the directory you r
|
|
|
336
343
|
A directory you do not control can therefore point the CLI at a server you do not expect, so
|
|
337
344
|
run `hardfin config` when a command reaches somewhere surprising.
|
|
338
345
|
|
|
346
|
+
## Working on the CLI
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
npm run check-types # tsc
|
|
350
|
+
npm run lint # eslint, with type-aware rules
|
|
351
|
+
npm run prettier:write # formatting
|
|
352
|
+
npm test # vitest
|
|
353
|
+
npm run build # tsdown, into dist/cli.js
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
CI runs all five on every pull request, plus a check that the generated commands still match
|
|
357
|
+
the vendored API document. A pull request that fails any of them cannot merge.
|
|
358
|
+
|
|
359
|
+
Two lint rules exist because of defects that reached the repository. `no-floating-promises`
|
|
360
|
+
covers a promise nobody waits for, which once killed the process when a sign in was
|
|
361
|
+
cancelled. `no-base-to-string` covers a value stringified as `[object Object]`, which a
|
|
362
|
+
request would have carried to the API.
|
|
363
|
+
|
|
339
364
|
## Releasing
|
|
340
365
|
|
|
341
366
|
This section is for anyone who merges a pull request in this repository. It tells you where
|
|
@@ -358,11 +383,11 @@ package published from a public repository, and this repository is private.
|
|
|
358
383
|
|
|
359
384
|
### What each merge publishes
|
|
360
385
|
|
|
361
|
-
| Merge
|
|
362
|
-
|
|
|
363
|
-
| Pull request into `dev` | always
|
|
364
|
-
| Promotion into `main`
|
|
365
|
-
| Hotfix into `main`
|
|
386
|
+
| Merge | Publishes | Version | Dist-tag |
|
|
387
|
+
| ----------------------- | ---------------------------------------------------------- | ---------------------------------------------- | -------- |
|
|
388
|
+
| Pull request into `dev` | always | the next version, suffixed `-dev.<run number>` | `dev` |
|
|
389
|
+
| Promotion into `main` | when `package.json` names a version that is not yet on npm | that version | `latest` |
|
|
390
|
+
| Hotfix into `main` | same rule as a promotion | that version | `latest` |
|
|
366
391
|
|
|
367
392
|
A preview never moves `latest`, so `npm install @hardfin/cli` keeps returning the released
|
|
368
393
|
version.
|
|
@@ -395,13 +420,13 @@ on `main` until someone brings it back.
|
|
|
395
420
|
A required check reads `version` from `package.json` on every pull request and judges it
|
|
396
421
|
against the branch the pull request targets.
|
|
397
422
|
|
|
398
|
-
| Version
|
|
399
|
-
|
|
|
400
|
-
| Plain, above what `main` holds
|
|
401
|
-
| Plain, equal to what `main` holds
|
|
402
|
-
| Plain, below what `main` holds
|
|
403
|
-
| Carrying a `-dev` or any other prerelease suffix | refused
|
|
404
|
-
| Not a semver version
|
|
423
|
+
| Version | Into `dev` | Into `main` |
|
|
424
|
+
| ------------------------------------------------ | ---------- | -------------------------------------- |
|
|
425
|
+
| Plain, above what `main` holds | passes | passes |
|
|
426
|
+
| Plain, equal to what `main` holds | passes | refused, because npm already serves it |
|
|
427
|
+
| Plain, below what `main` holds | refused | refused |
|
|
428
|
+
| Carrying a `-dev` or any other prerelease suffix | refused | refused |
|
|
429
|
+
| Not a semver version | refused | refused |
|
|
405
430
|
|
|
406
431
|
A prerelease suffix is refused everywhere because the release workflow appends it at publish
|
|
407
432
|
time. A version equal to `main`'s is fine on `dev`, since the workflow increments the patch
|
package/dist/cli.js
CHANGED
|
@@ -120,6 +120,16 @@ function toOrigin(apiUrl) {
|
|
|
120
120
|
}
|
|
121
121
|
//#endregion
|
|
122
122
|
//#region src/output/writer.ts
|
|
123
|
+
/**
|
|
124
|
+
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
125
|
+
* otherwise reach a request as the text "[object Object]".
|
|
126
|
+
*/
|
|
127
|
+
function toText(value) {
|
|
128
|
+
if (typeof value === "string") return value;
|
|
129
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
130
|
+
if (value === void 0 || value === null) return "";
|
|
131
|
+
return JSON.stringify(value);
|
|
132
|
+
}
|
|
123
133
|
/** writeData prints a command's result on stdout. */
|
|
124
134
|
function writeData(value) {
|
|
125
135
|
if (typeof value === "string") {
|
|
@@ -248,7 +258,8 @@ function toSummary(command) {
|
|
|
248
258
|
valueName: flag.valueName ?? null,
|
|
249
259
|
repeatable: flag.repeatable ?? false
|
|
250
260
|
})),
|
|
251
|
-
examples: command.examples
|
|
261
|
+
examples: command.examples,
|
|
262
|
+
subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
//#endregion
|
|
@@ -372,7 +383,7 @@ async function toTokens(url, form) {
|
|
|
372
383
|
body: new URLSearchParams(form)
|
|
373
384
|
});
|
|
374
385
|
const body = await response.json().catch(() => void 0);
|
|
375
|
-
if (!response.ok) throw new GrantFailure(
|
|
386
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
376
387
|
const parsed = TokenResponse.safeParse(body);
|
|
377
388
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
378
389
|
return {
|
|
@@ -477,12 +488,12 @@ function forgetKeyring(issuer) {
|
|
|
477
488
|
function forgetFile(issuer) {
|
|
478
489
|
const held = readFile();
|
|
479
490
|
if (held[issuer] === void 0) return;
|
|
480
|
-
|
|
481
|
-
if (Object.keys(
|
|
491
|
+
const remaining = Object.fromEntries(Object.entries(held).filter(([name]) => name !== issuer));
|
|
492
|
+
if (Object.keys(remaining).length === 0) {
|
|
482
493
|
rmSync(toCredentialPath(), { force: true });
|
|
483
494
|
return;
|
|
484
495
|
}
|
|
485
|
-
writeFile(
|
|
496
|
+
writeFile(remaining);
|
|
486
497
|
}
|
|
487
498
|
/** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
|
|
488
499
|
function toKeyring(issuer) {
|
|
@@ -709,11 +720,11 @@ var RequestFailure = class extends Error {
|
|
|
709
720
|
/** request calls one /v2 endpoint and returns the envelope it answered with. */
|
|
710
721
|
async function request(options) {
|
|
711
722
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
712
|
-
|
|
723
|
+
for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
|
|
713
724
|
const headers = {
|
|
714
725
|
[options.credential.header]: options.credential.value,
|
|
715
726
|
"X-API-Version": API_VERSION,
|
|
716
|
-
Accept: "application/json"
|
|
727
|
+
Accept: options.downloads ? "*/*" : "application/json"
|
|
717
728
|
};
|
|
718
729
|
if (options.body !== void 0 && options.form === void 0) headers["Content-Type"] = "application/json";
|
|
719
730
|
const response = await fetch(url, {
|
|
@@ -721,6 +732,11 @@ async function request(options) {
|
|
|
721
732
|
headers,
|
|
722
733
|
body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
|
|
723
734
|
});
|
|
735
|
+
if (options.downloads && response.ok) return { data: {
|
|
736
|
+
bytes: Buffer.from(await response.arrayBuffer()),
|
|
737
|
+
contentType: response.headers.get("content-type"),
|
|
738
|
+
fileName: toFileName(response.headers.get("content-disposition"))
|
|
739
|
+
} };
|
|
724
740
|
const envelope = toEnvelope(await response.text());
|
|
725
741
|
if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
|
|
726
742
|
error: toStatusMessage(response.status),
|
|
@@ -735,6 +751,11 @@ async function request(options) {
|
|
|
735
751
|
}
|
|
736
752
|
return envelope ?? { data: null };
|
|
737
753
|
}
|
|
754
|
+
/** toFileName reads the name a download was offered under, when the server names one. */
|
|
755
|
+
function toFileName(disposition) {
|
|
756
|
+
const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition ?? "");
|
|
757
|
+
return matched?.[1] ? decodeURIComponent(matched[1]) : void 0;
|
|
758
|
+
}
|
|
738
759
|
function toLeadingSlash(path) {
|
|
739
760
|
return path.startsWith("/") ? path : `/${path}`;
|
|
740
761
|
}
|
|
@@ -834,7 +855,7 @@ async function runApi(input) {
|
|
|
834
855
|
writeData((await request({
|
|
835
856
|
apiUrl: input.resolved.settings.apiUrl,
|
|
836
857
|
credential: await toRequestCredential(input.resolved.settings),
|
|
837
|
-
method:
|
|
858
|
+
method: toText(input.flags["method"] ?? "GET").toUpperCase(),
|
|
838
859
|
path,
|
|
839
860
|
query,
|
|
840
861
|
body
|
|
@@ -962,7 +983,7 @@ function toFlagNames(command) {
|
|
|
962
983
|
return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
|
|
963
984
|
}
|
|
964
985
|
async function runCompletion(input) {
|
|
965
|
-
const shell =
|
|
986
|
+
const shell = input.args[0] ?? "";
|
|
966
987
|
if (!SHELLS.includes(shell)) {
|
|
967
988
|
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
968
989
|
return ExitCode.USAGE;
|
|
@@ -1116,7 +1137,7 @@ async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
|
1116
1137
|
})
|
|
1117
1138
|
});
|
|
1118
1139
|
const body = await response.json().catch(() => void 0);
|
|
1119
|
-
if (!response.ok) throw new GrantFailure(
|
|
1140
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
1120
1141
|
const parsed = DeviceResponse.safeParse(body);
|
|
1121
1142
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
|
|
1122
1143
|
return {
|
|
@@ -1192,8 +1213,12 @@ async function toListener(timeoutMs) {
|
|
|
1192
1213
|
}, timeoutMs);
|
|
1193
1214
|
return {
|
|
1194
1215
|
redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
|
|
1195
|
-
callback: callback.finally(() =>
|
|
1196
|
-
|
|
1216
|
+
callback: callback.finally(() => {
|
|
1217
|
+
close(server, timer);
|
|
1218
|
+
}),
|
|
1219
|
+
close: () => {
|
|
1220
|
+
close(server, timer);
|
|
1221
|
+
}
|
|
1197
1222
|
};
|
|
1198
1223
|
}
|
|
1199
1224
|
function toCallback(request) {
|
|
@@ -1395,7 +1420,7 @@ async function runLogin(input) {
|
|
|
1395
1420
|
try {
|
|
1396
1421
|
const clientId = settings.clientId;
|
|
1397
1422
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
1398
|
-
const scope =
|
|
1423
|
+
const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
|
|
1399
1424
|
if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
|
|
1400
1425
|
const listener = await toListener(WAIT_MS);
|
|
1401
1426
|
const pkce = toPkce();
|
|
@@ -1551,7 +1576,9 @@ async function runLogout(input) {
|
|
|
1551
1576
|
} catch {
|
|
1552
1577
|
revoked = false;
|
|
1553
1578
|
}
|
|
1554
|
-
await withLock(() =>
|
|
1579
|
+
await withLock(() => {
|
|
1580
|
+
forget(settings.issuerUrl);
|
|
1581
|
+
});
|
|
1555
1582
|
forgetHeldTokens();
|
|
1556
1583
|
writeData({
|
|
1557
1584
|
signedOut: true,
|
|
@@ -1613,7 +1640,7 @@ async function toResponse(request, commands, run) {
|
|
|
1613
1640
|
description: "The command tree as JSON",
|
|
1614
1641
|
mimeType: "application/json"
|
|
1615
1642
|
}] });
|
|
1616
|
-
case "resources/read": return answer(toResource(
|
|
1643
|
+
case "resources/read": return answer(toResource(toText(request.params?.["uri"] ?? ""), commands));
|
|
1617
1644
|
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1618
1645
|
case "ping": return answer({});
|
|
1619
1646
|
default:
|
|
@@ -1658,7 +1685,21 @@ function toTree(commands) {
|
|
|
1658
1685
|
subcommands: command.subcommands ? toTree(command.subcommands) : void 0
|
|
1659
1686
|
}));
|
|
1660
1687
|
}
|
|
1688
|
+
/** Signing in needs a browser and a person, neither of which an agent's session has. */
|
|
1689
|
+
const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
1690
|
+
"login",
|
|
1691
|
+
"logout",
|
|
1692
|
+
"mcp"
|
|
1693
|
+
]);
|
|
1661
1694
|
async function toToolResult(params, run) {
|
|
1695
|
+
const name = params?.["name"];
|
|
1696
|
+
if (name !== void 0 && name !== TOOL.name) return {
|
|
1697
|
+
content: [{
|
|
1698
|
+
type: "text",
|
|
1699
|
+
text: `this server offers one tool, ${TOOL.name}`
|
|
1700
|
+
}],
|
|
1701
|
+
isError: true
|
|
1702
|
+
};
|
|
1662
1703
|
const args = (params?.["arguments"])?.args;
|
|
1663
1704
|
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
|
|
1664
1705
|
content: [{
|
|
@@ -1667,7 +1708,15 @@ async function toToolResult(params, run) {
|
|
|
1667
1708
|
}],
|
|
1668
1709
|
isError: true
|
|
1669
1710
|
};
|
|
1670
|
-
const
|
|
1711
|
+
const asked = args;
|
|
1712
|
+
if (REFUSED_COMMANDS.has(asked[0] ?? "")) return {
|
|
1713
|
+
content: [{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: `${asked[0] ?? ""} is run by a person at a terminal, not through this server. Run hardfin ${asked[0] ?? ""} yourself, then call this tool again`
|
|
1716
|
+
}],
|
|
1717
|
+
isError: true
|
|
1718
|
+
};
|
|
1719
|
+
const outcome = await run(asked);
|
|
1671
1720
|
return {
|
|
1672
1721
|
content: [{
|
|
1673
1722
|
type: "text",
|
|
@@ -1676,6 +1725,14 @@ async function toToolResult(params, run) {
|
|
|
1676
1725
|
isError: outcome.code !== 0
|
|
1677
1726
|
};
|
|
1678
1727
|
}
|
|
1728
|
+
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
1729
|
+
function toRequestId(line) {
|
|
1730
|
+
try {
|
|
1731
|
+
return JSON.parse(line).id ?? null;
|
|
1732
|
+
} catch {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1679
1736
|
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1680
1737
|
function toCliRunner() {
|
|
1681
1738
|
return (args) => new Promise((resolve) => {
|
|
@@ -1691,11 +1748,13 @@ function toCliRunner() {
|
|
|
1691
1748
|
child.stderr.on("data", (chunk) => {
|
|
1692
1749
|
stderr += chunk.toString();
|
|
1693
1750
|
});
|
|
1694
|
-
child.on("close", (code) =>
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1751
|
+
child.on("close", (code) => {
|
|
1752
|
+
resolve({
|
|
1753
|
+
stdout,
|
|
1754
|
+
stderr,
|
|
1755
|
+
code: code ?? 1
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1699
1758
|
});
|
|
1700
1759
|
}
|
|
1701
1760
|
/** serve answers requests on stdin until the client closes it. */
|
|
@@ -1709,9 +1768,9 @@ async function serve(commands, run = toCliRunner()) {
|
|
|
1709
1768
|
} catch (error) {
|
|
1710
1769
|
response = {
|
|
1711
1770
|
jsonrpc: "2.0",
|
|
1712
|
-
id:
|
|
1771
|
+
id: toRequestId(line),
|
|
1713
1772
|
error: {
|
|
1714
|
-
code: -
|
|
1773
|
+
code: -32603,
|
|
1715
1774
|
message: error instanceof Error ? error.message : String(error)
|
|
1716
1775
|
}
|
|
1717
1776
|
};
|
|
@@ -1766,6 +1825,12 @@ const FILE_FLAG = {
|
|
|
1766
1825
|
valueName: "path",
|
|
1767
1826
|
schema: z.string()
|
|
1768
1827
|
};
|
|
1828
|
+
const OUTPUT_FLAG = {
|
|
1829
|
+
name: "output",
|
|
1830
|
+
description: "Where to write the file, or - for standard output",
|
|
1831
|
+
valueName: "path",
|
|
1832
|
+
schema: z.string()
|
|
1833
|
+
};
|
|
1769
1834
|
const JSON_FLAG = {
|
|
1770
1835
|
name: "json",
|
|
1771
1836
|
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
@@ -1778,6 +1843,7 @@ function defineOperation(operation) {
|
|
|
1778
1843
|
...operation.bodyFlags,
|
|
1779
1844
|
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
1780
1845
|
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
1846
|
+
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
1781
1847
|
JSON_FLAG
|
|
1782
1848
|
];
|
|
1783
1849
|
return {
|
|
@@ -1823,15 +1889,18 @@ async function runOperation(operation, input) {
|
|
|
1823
1889
|
form = built;
|
|
1824
1890
|
}
|
|
1825
1891
|
try {
|
|
1826
|
-
|
|
1892
|
+
const envelope = await request({
|
|
1827
1893
|
apiUrl: input.resolved.settings.apiUrl,
|
|
1828
1894
|
credential: await toRequestCredential(input.resolved.settings),
|
|
1829
1895
|
method: operation.method,
|
|
1830
1896
|
path,
|
|
1831
1897
|
query: toQuery(operation, input.flags),
|
|
1832
1898
|
body,
|
|
1833
|
-
form
|
|
1834
|
-
|
|
1899
|
+
form,
|
|
1900
|
+
downloads: operation.downloads
|
|
1901
|
+
});
|
|
1902
|
+
if (operation.downloads) return toWritten(envelope.data, input);
|
|
1903
|
+
writeData(envelope.data);
|
|
1835
1904
|
return ExitCode.OK;
|
|
1836
1905
|
} catch (error) {
|
|
1837
1906
|
if (error instanceof NoCredential) {
|
|
@@ -1846,6 +1915,29 @@ async function runOperation(operation, input) {
|
|
|
1846
1915
|
return ExitCode.ERROR;
|
|
1847
1916
|
}
|
|
1848
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* toWritten puts a downloaded file where it was asked for. A terminal is never written to,
|
|
1920
|
+
* because a person would otherwise have their session filled with a file's bytes.
|
|
1921
|
+
*/
|
|
1922
|
+
function toWritten(download, input) {
|
|
1923
|
+
const asked = input.flags["output"];
|
|
1924
|
+
const path = typeof asked === "string" ? asked : download.fileName ?? "-";
|
|
1925
|
+
if (path === "-") {
|
|
1926
|
+
if (process.stdout.isTTY) {
|
|
1927
|
+
writeFailure("this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
1928
|
+
return ExitCode.USAGE;
|
|
1929
|
+
}
|
|
1930
|
+
process.stdout.write(download.bytes);
|
|
1931
|
+
return ExitCode.OK;
|
|
1932
|
+
}
|
|
1933
|
+
writeFileSync(path, download.bytes);
|
|
1934
|
+
writeData({
|
|
1935
|
+
written: path,
|
|
1936
|
+
bytes: download.bytes.length,
|
|
1937
|
+
contentType: download.contentType
|
|
1938
|
+
});
|
|
1939
|
+
return ExitCode.OK;
|
|
1940
|
+
}
|
|
1849
1941
|
/** toPath fills the path template from the positional arguments, in order. */
|
|
1850
1942
|
function toPath(operation, args) {
|
|
1851
1943
|
if (args.length !== operation.pathParameters.length) return;
|
|
@@ -1861,7 +1953,7 @@ function toQuery(operation, flags) {
|
|
|
1861
1953
|
if (value === void 0) continue;
|
|
1862
1954
|
const parsed = flag.schema.safeParse(value);
|
|
1863
1955
|
const carried = parsed.success ? parsed.data : value;
|
|
1864
|
-
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName,
|
|
1956
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
1865
1957
|
}
|
|
1866
1958
|
return query;
|
|
1867
1959
|
}
|
|
@@ -1883,7 +1975,7 @@ async function toForm(upload, flags) {
|
|
|
1883
1975
|
const value = flags[toOptionKey(field.name)];
|
|
1884
1976
|
if (value !== void 0) {
|
|
1885
1977
|
const parsed = field.schema.safeParse(value);
|
|
1886
|
-
form.append(field.jsonPath[0] ?? field.name,
|
|
1978
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
1887
1979
|
}
|
|
1888
1980
|
}
|
|
1889
1981
|
return form;
|
|
@@ -1896,7 +1988,7 @@ function toBody(operation, flags) {
|
|
|
1896
1988
|
const value = flags[toOptionKey(flag.name)];
|
|
1897
1989
|
if (value === void 0) continue;
|
|
1898
1990
|
if (flag.element) {
|
|
1899
|
-
const elements = toElements(flag, Array.isArray(value) ? value.map(
|
|
1991
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
1900
1992
|
if (elements instanceof Error) return elements;
|
|
1901
1993
|
set(body, flag.jsonPath, elements);
|
|
1902
1994
|
hasField = true;
|
|
@@ -1913,7 +2005,7 @@ function toBody(operation, flags) {
|
|
|
1913
2005
|
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
1914
2006
|
function toCleared(operation, flags, body) {
|
|
1915
2007
|
const named = flags["unset"];
|
|
1916
|
-
const names = Array.isArray(named) ? named.map(
|
|
2008
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
1917
2009
|
for (const name of names) {
|
|
1918
2010
|
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
1919
2011
|
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
@@ -2575,6 +2667,7 @@ const surfaceCommands = [
|
|
|
2575
2667
|
name: "is-in-service-date-managed-automatically",
|
|
2576
2668
|
jsonPath: ["isInServiceDateManagedAutomatically"],
|
|
2577
2669
|
description: "Whether Hardfin sets the in-service date itself, which sending an in-service date turns off",
|
|
2670
|
+
negatable: true,
|
|
2578
2671
|
nullable: true,
|
|
2579
2672
|
schema: z.boolean()
|
|
2580
2673
|
},
|
|
@@ -2641,6 +2734,7 @@ const surfaceCommands = [
|
|
|
2641
2734
|
name: "automatic",
|
|
2642
2735
|
jsonPath: ["automatic"],
|
|
2643
2736
|
description: "Whether Hardfin sets the asset's in-service date itself",
|
|
2737
|
+
negatable: true,
|
|
2644
2738
|
required: true,
|
|
2645
2739
|
schema: z.boolean()
|
|
2646
2740
|
}]
|
|
@@ -3343,12 +3437,14 @@ const surfaceCommands = [
|
|
|
3343
3437
|
name: "is-customer",
|
|
3344
3438
|
jsonPath: ["isCustomer"],
|
|
3345
3439
|
description: "Whether the company is a customer",
|
|
3440
|
+
negatable: true,
|
|
3346
3441
|
schema: z.boolean()
|
|
3347
3442
|
},
|
|
3348
3443
|
{
|
|
3349
3444
|
name: "is-supplier",
|
|
3350
3445
|
jsonPath: ["isSupplier"],
|
|
3351
3446
|
description: "Whether the company is a supplier",
|
|
3447
|
+
negatable: true,
|
|
3352
3448
|
schema: z.boolean()
|
|
3353
3449
|
},
|
|
3354
3450
|
{
|
|
@@ -3448,6 +3544,7 @@ const surfaceCommands = [
|
|
|
3448
3544
|
name: "is-archived",
|
|
3449
3545
|
jsonPath: ["isArchived"],
|
|
3450
3546
|
description: "Whether the customer is archived",
|
|
3547
|
+
negatable: true,
|
|
3451
3548
|
nullable: true,
|
|
3452
3549
|
schema: z.boolean()
|
|
3453
3550
|
},
|
|
@@ -3455,6 +3552,7 @@ const surfaceCommands = [
|
|
|
3455
3552
|
name: "is-customer",
|
|
3456
3553
|
jsonPath: ["isCustomer"],
|
|
3457
3554
|
description: "Whether the company is a customer",
|
|
3555
|
+
negatable: true,
|
|
3458
3556
|
nullable: true,
|
|
3459
3557
|
schema: z.boolean()
|
|
3460
3558
|
},
|
|
@@ -3462,6 +3560,7 @@ const surfaceCommands = [
|
|
|
3462
3560
|
name: "is-supplier",
|
|
3463
3561
|
jsonPath: ["isSupplier"],
|
|
3464
3562
|
description: "Whether the company is a supplier",
|
|
3563
|
+
negatable: true,
|
|
3465
3564
|
nullable: true,
|
|
3466
3565
|
schema: z.boolean()
|
|
3467
3566
|
},
|
|
@@ -3536,7 +3635,8 @@ const surfaceCommands = [
|
|
|
3536
3635
|
description: "True when the file downloads as an attachment rather than opening inline",
|
|
3537
3636
|
schema: z.boolean()
|
|
3538
3637
|
}],
|
|
3539
|
-
bodyFlags: []
|
|
3638
|
+
bodyFlags: [],
|
|
3639
|
+
downloads: true
|
|
3540
3640
|
})]
|
|
3541
3641
|
},
|
|
3542
3642
|
{
|
|
@@ -3639,6 +3739,7 @@ const surfaceCommands = [
|
|
|
3639
3739
|
name: "accepts-bulk-serials",
|
|
3640
3740
|
jsonPath: ["acceptsBulkSerials"],
|
|
3641
3741
|
description: "Whether a BULK item records serial numbers on its units, which SERVICE and DEVICE items ignore",
|
|
3742
|
+
negatable: true,
|
|
3642
3743
|
schema: z.boolean()
|
|
3643
3744
|
},
|
|
3644
3745
|
{
|
|
@@ -3775,6 +3876,7 @@ const surfaceCommands = [
|
|
|
3775
3876
|
name: "accepts-bulk-serials",
|
|
3776
3877
|
jsonPath: ["acceptsBulkSerials"],
|
|
3777
3878
|
description: "Whether a BULK item records serial numbers on its units, read only beside type",
|
|
3879
|
+
negatable: true,
|
|
3778
3880
|
nullable: true,
|
|
3779
3881
|
schema: z.boolean()
|
|
3780
3882
|
},
|
|
@@ -3803,6 +3905,7 @@ const surfaceCommands = [
|
|
|
3803
3905
|
name: "is-archived",
|
|
3804
3906
|
jsonPath: ["isArchived"],
|
|
3805
3907
|
description: "Whether the item is archived, which cannot be null",
|
|
3908
|
+
negatable: true,
|
|
3806
3909
|
nullable: true,
|
|
3807
3910
|
schema: z.boolean()
|
|
3808
3911
|
},
|
|
@@ -4272,18 +4375,21 @@ const surfaceCommands = [
|
|
|
4272
4375
|
name: "is-inventory",
|
|
4273
4376
|
jsonPath: ["isInventory"],
|
|
4274
4377
|
description: "Whether assets at the location count as inventory for reporting",
|
|
4378
|
+
negatable: true,
|
|
4275
4379
|
schema: z.boolean()
|
|
4276
4380
|
},
|
|
4277
4381
|
{
|
|
4278
4382
|
name: "is-inventory-override",
|
|
4279
4383
|
jsonPath: ["isInventoryOverride"],
|
|
4280
4384
|
description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
|
|
4385
|
+
negatable: true,
|
|
4281
4386
|
schema: z.boolean()
|
|
4282
4387
|
},
|
|
4283
4388
|
{
|
|
4284
4389
|
name: "is-transient",
|
|
4285
4390
|
jsonPath: ["isTransient"],
|
|
4286
4391
|
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4392
|
+
negatable: true,
|
|
4287
4393
|
schema: z.boolean()
|
|
4288
4394
|
},
|
|
4289
4395
|
{
|
|
@@ -4425,6 +4531,7 @@ const surfaceCommands = [
|
|
|
4425
4531
|
name: "is-archived",
|
|
4426
4532
|
jsonPath: ["isArchived"],
|
|
4427
4533
|
description: "Whether the location is archived, and archiving a site archives its zones",
|
|
4534
|
+
negatable: true,
|
|
4428
4535
|
nullable: true,
|
|
4429
4536
|
schema: z.boolean()
|
|
4430
4537
|
},
|
|
@@ -4432,6 +4539,7 @@ const surfaceCommands = [
|
|
|
4432
4539
|
name: "is-inventory",
|
|
4433
4540
|
jsonPath: ["isInventory"],
|
|
4434
4541
|
description: "Whether assets at the location count as inventory for reporting",
|
|
4542
|
+
negatable: true,
|
|
4435
4543
|
nullable: true,
|
|
4436
4544
|
schema: z.boolean()
|
|
4437
4545
|
},
|
|
@@ -4439,6 +4547,7 @@ const surfaceCommands = [
|
|
|
4439
4547
|
name: "is-inventory-override",
|
|
4440
4548
|
jsonPath: ["isInventoryOverride"],
|
|
4441
4549
|
description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
|
|
4550
|
+
negatable: true,
|
|
4442
4551
|
nullable: true,
|
|
4443
4552
|
schema: z.boolean()
|
|
4444
4553
|
},
|
|
@@ -4446,6 +4555,7 @@ const surfaceCommands = [
|
|
|
4446
4555
|
name: "is-transient",
|
|
4447
4556
|
jsonPath: ["isTransient"],
|
|
4448
4557
|
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4558
|
+
negatable: true,
|
|
4449
4559
|
nullable: true,
|
|
4450
4560
|
schema: z.boolean()
|
|
4451
4561
|
},
|
|
@@ -4694,27 +4804,24 @@ function toEnvironmentReport() {
|
|
|
4694
4804
|
apiVersion: API_VERSION,
|
|
4695
4805
|
node: process.version,
|
|
4696
4806
|
platform: process.platform,
|
|
4697
|
-
interactive:
|
|
4807
|
+
interactive: process.stdin.isTTY
|
|
4698
4808
|
};
|
|
4699
4809
|
}
|
|
4700
4810
|
/** toConfigurationReport names every setting, its source, and never a secret's value. */
|
|
4701
4811
|
function toConfigurationReport(resolved) {
|
|
4702
|
-
const
|
|
4812
|
+
const report = { configFile: CONFIG_FILE };
|
|
4813
|
+
for (const [key, value] of Object.entries(resolved.settings)) {
|
|
4703
4814
|
const from = resolved.sources[key];
|
|
4704
|
-
|
|
4815
|
+
report[key] = key === "apiKey" ? {
|
|
4705
4816
|
set: value !== void 0,
|
|
4706
4817
|
fingerprint: toFingerprint(value),
|
|
4707
4818
|
from
|
|
4708
|
-
}
|
|
4709
|
-
return [key, {
|
|
4819
|
+
} : {
|
|
4710
4820
|
value: value ?? null,
|
|
4711
4821
|
from
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
return
|
|
4715
|
-
...Object.fromEntries(entries),
|
|
4716
|
-
configFile: CONFIG_FILE
|
|
4717
|
-
};
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
return report;
|
|
4718
4825
|
}
|
|
4719
4826
|
function toCredentialReport(apiKey, stored) {
|
|
4720
4827
|
if (apiKey) return {
|
|
@@ -4810,8 +4917,7 @@ function toFlattened(value) {
|
|
|
4810
4917
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
4811
4918
|
const holder = value;
|
|
4812
4919
|
if (holder.from === void 0) return;
|
|
4813
|
-
|
|
4814
|
-
return `${String(shown)} (${holder.from})`;
|
|
4920
|
+
return `${toText(holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set"))} (${holder.from})`;
|
|
4815
4921
|
}
|
|
4816
4922
|
/** toLines lays the report out for a person, one indented line per value. */
|
|
4817
4923
|
function toLines(report, depth = 0) {
|
|
@@ -4873,6 +4979,7 @@ function toProgram(command) {
|
|
|
4873
4979
|
const short = flag.short ? `-${flag.short}, ` : "";
|
|
4874
4980
|
const value = flag.valueName ? ` <${flag.valueName}>` : "";
|
|
4875
4981
|
const option = new Option(`${short}--${flag.name}${value}`, flag.description);
|
|
4982
|
+
if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
|
|
4876
4983
|
if (flag.repeatable) option.argParser(collect);
|
|
4877
4984
|
if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
|
|
4878
4985
|
program.addOption(option);
|
|
@@ -4913,7 +5020,7 @@ async function toExitCode(command, args, flags) {
|
|
|
4913
5020
|
}
|
|
4914
5021
|
function toArgumentList(value) {
|
|
4915
5022
|
if (Array.isArray(value)) return value.map(String);
|
|
4916
|
-
return value === void 0 ? [] : [
|
|
5023
|
+
return value === void 0 ? [] : [toText(value)];
|
|
4917
5024
|
}
|
|
4918
5025
|
function collect(value, previous) {
|
|
4919
5026
|
return [...previous ?? [], value];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardfin/cli",
|
|
3
|
-
"version": "0.0.2-dev.
|
|
3
|
+
"version": "0.0.2-dev.17",
|
|
4
4
|
"description": "Command line interface for the Hardfin API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hardfin, Inc.",
|
|
@@ -37,7 +37,12 @@
|
|
|
37
37
|
"check-types": "tsc --noEmit",
|
|
38
38
|
"test": "vitest run",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
|
-
"prepublishOnly": "npm run build"
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
|
42
|
+
"lint:fix": "eslint . --fix",
|
|
43
|
+
"prettier:check": "prettier --check .",
|
|
44
|
+
"prettier:write": "prettier --write .",
|
|
45
|
+
"test:coverage": "vitest run --coverage"
|
|
41
46
|
},
|
|
42
47
|
"dependencies": {
|
|
43
48
|
"@napi-rs/keyring": "^2.1.0",
|
|
@@ -45,10 +50,16 @@
|
|
|
45
50
|
"zod": "^4.6.5"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^10.0.1",
|
|
48
54
|
"@types/node": "^22.15.0",
|
|
49
55
|
"@vitest/coverage-v8": "^5.0.1",
|
|
56
|
+
"eslint": "^10.11.0",
|
|
57
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
58
|
+
"globals": "^17.12.0",
|
|
59
|
+
"prettier": "^3.9.9",
|
|
50
60
|
"tsdown": "^0.23.0",
|
|
51
61
|
"typescript": "^5.9.0",
|
|
62
|
+
"typescript-eslint": "^8.70.1",
|
|
52
63
|
"unrun": "^0.3.1",
|
|
53
64
|
"vitest": "^5.0.1",
|
|
54
65
|
"yaml": "^2.9.1"
|